zoukankan      html  css  js  c++  java
  • 统计学习2:线性可分支持向量机(Scipy实现)

    1. 模型

    1.1 超平面

    我们称下面形式的集合为超平面

    [egin{aligned} { m{x} | m{a}^{T} m{x} - b = 0 } end{aligned} ag{1} ]

    其中(m{a} in mathbb{R}^n)(m{a} e m{0} , m{x}in mathbb{R}^n, b in mathbb{R})。解析地看,超平面是关于(m{x})的非平凡线性方程的解空间(因此是一个仿射集,仿射集和凸集的概念参考Stephen Boyd的《凸优化》)从几何上看,它的的法向量为(m{a}),而常数(bin mathbb{R})决定了这个超平面从原点的偏移。这如何得到的呢?这是因为,若我们由法向量(m{a})和超平面上一点(m{x}_{0})确定超平面,则对超平面上任意一点(m{x}),我们可以得到(m{x} - m{x}_0)一定垂直于(m{a}),则超平面的集合便可以表示为

    [egin{aligned} {m{x} | m{a}^{T} (m{x} - m{x}_0) = 0} end{aligned} ag{2} ]

    (mathbb{R}^2)中的几何化的解释如下图所示,其中深色箭头表示(m{x} - m{x}_0)
    超平面几何示意图
    一个超平面将(mathbb{R}^n)划分为两个半空间,(闭的)半空间是具有下列形式的集合:

    [egin{aligned} {m{x} | m{a}^T m{x} -b leqslant 0} end{aligned} ag{3} ]

    即(非平凡)的线性不等式的解空间,其中(a e 0)。半空间是凸的,但不是仿射的。集合({m{x} | m{a}^T m{x} -b < b})是半空间({m{x} | m{a}^T m{x} -b leqslant 0})的内部,称为开半空间。

    1.2 线性可分支持向量机

    我们定义样本空间为(mathcal{X} subseteq mathbb{R}^n),输出空间为(mathcal{Y} = {+1, -1})(m{X})为输入空间上的随机向量,其取值为(m{x}),满足(m{x} in mathcal{X})(Y)为输出空间上的随机变量,设其取值为(y),满足(y in mathcal{Y})。我们将容量为(m)的训练样本表示为:

    [egin{aligned} D = {{m{x}^{(1)}, y^{(1)}}, {m{x}^{(2)}, y^{(2)}},..., {m{x}^{(m)}, y^{(m)}}} end{aligned} ag{4} ]

    (y^{(i)} = +1)时,我们称(m{x}^{(i)})为正例;当(y^{(i)} = -1)时,称(m{x}^{i})为负例。((m{x}^{(i)}, y^{(i)}))称为样本点。
    如果我们假设训练数据集是线性可分的,则我们可以在特征空间中找到一个分离超平面({ m{x} | m{w}^T m{x} + b = 0 }),将特征空间划分为({ m{x} | m{w}^T m{x} + b > 0 })({ m{x} | m{w}^T m{x} + b < 0 })两个开半空间(显然法向量(m{w})
    向的一侧为正,另一侧为负),且为正的一侧对应负类,为负的一侧对应负类。

    如果训练集线性可分,则我们存在无穷多个分离超平面将两类样本分开。如果我们采用感知机的误分类最小的训练策略(也就是仅仅保证分类的正确性),那么我们将求得无穷多个解。我们接下来定义的线性可分支持向量机将利用“间隔最大化”求解最优分离超平面(即能将两组数据正确划分且间隔最大的超平面,我们在“学习策略”板块中将详述这一概念),这时解是唯一的。

    形式化地说,给定线性可分的数据集,通过间隔最大化策略学习得到的分离超平面为

    [egin{aligned} { m{x} | m{w}^{*T} m{x} + b^{*} = 0 } end{aligned} ag{5} ]

    以及相应的分类决策函数

    [egin{aligned} f(m{x}) = ext{sign} (m{w}^{*T} m{x} + b^{*}) end{aligned} ag{6} ]

    称为线性可分支持向量机。

    2. 学习策略

    我们前面提到最好的超平面需要能将两组数据正确划分且间隔最大,那么间隔最大如何形式化地定义呢?我们先来看函数间隔和几何间隔的概念。

    2.1. 函数间隔和几何间隔

    直观地看,一个点距离超平面的远近可以表示则我们对它进行分类的确信程度。一个点距离超平面越远,则我们对它的分类则越有把握。我们给定超平面({ m{x} | m{w}^T m{x} + b = 0})和一个实例点(m{x}^{(i)}),可以发现(|m{w}^T m{x}^{(i)} + b|)可以相对地表示点(m{x}^{(i)})举例超平面的远近,而(m{w}^T m{x}^{(i)} + b)的符号与类标记(y^{(i)})是否一致可以反映分类是否正确。据此,我们用(y^{(i)} (m{w} cdot m{x}^{(i)} + b))来对分类的确信度和正确性进行综合表示。(y^{(i)} (m{w}^T m{x}^{(i)} + b))为正数,表示分类器能够正确完成分类功能,且(y^{(i)} (m{w}^T m{x}^{(i)} + b))越大,则分类器越好;(y^{(i)} (m{w}^T m{x}^{(i)} + b))为负数,就表示分类器连正确分类功能都不能完成了,负得越多,表示错的越离谱(HaHa,应该可以这么理解叭)。 于是,我们给出下列函数间隔的定义。

    函数间隔 对于给定的数据集(D)和超平面({ m{x} | m{w}^T m{x} + b = 0}),定义该超平面关于数据集中样本点((m{x}^{(i)}, y^{(i)}))的函数间隔为

    [egin{aligned} hat{gamma}^{(i)} = y^{(i)}(m{w}^Tm{x}^{(i)} + b) end{aligned} ag{7} ]

    定义该超平面关于训练集(D)的函数间隔为该超平面关于(D)中所有样本点函数间隔的最小值,即

    [egin{aligned} hat{gamma} = underset{i=1,...,m}{min}hat{gamma}^{(i)} end{aligned} ag{8} ]

    如果单单根据函数间隔的大小来选顶最佳超平面,那还不够准确。因为我们知道,令超平面的法向量(m{w})经过缩放变换为(lambda m{w}),超平面的截距项缩放变换为(lambda b),超平面是本身并没有改变的,但函数间隔(hat{gamma}^{(i)})却变为(lambda hat{gamma}^{(i)}),这显然与事实不符。因此,我们需要对超平面的法向量加一些约束,如规范化,令(|| m{w} ||=1),使得间隔是确定的。这时的函数间隔就是我们后面所提到的几何间隔的一种特殊情况。

    我们定义实例((m{x}^{(i)}, y^{(i)}))到超平面({ m{x} | m{w}^T m{x} + b = 0})的带符号距离为(frac{m{w}^T m{x}^{(i)} + b}{||m{w}||})(在法向量那一侧则为正),我们用这个做为来做为分类的确信度。类似地,我们我们用(y^{(i)} frac{m{w}^T m{x}^{(i)} + b}{||m{w}||})来对分类的确信度和正确性进行综合表示。于是,我们给出下列几何间隔的定义。

    几何间隔 对于给定的数据集(D)和超平面({ m{x} | m{w}^T m{x} + b = 0}),定义该超平面关于数据集中样本点((m{x}^{(i)}, y^{(i)}))的几何间隔为

    [egin{aligned} gamma^{(i)} = y^{(i)} frac{m{w}^T m{x}^{(i)} + b}{||m{w}||} end{aligned} ag{9} ]

    定义该超平面关于训练集(D)的函数间隔为该超平面关于(D)中所有样本点函数间隔的最小值,即

    [egin{aligned} gamma = underset{i=1,...,m}{min}gamma^{(i)} end{aligned} ag{10} ]

    对比一下式((7)、(8))和式((9)、(10))我们知道,函数间隔和集合间隔存在下列关系(gamma^{(i)} = frac{hat{gamma}^{(i)}}{||m{w}||})(gamma = frac{hat{gamma}}{||m{w}||})。可以看到,若(||m{w}||=1),则函数间隔等于几何间隔,如果(m{w})(b)都进行大小为(lambda)的缩放变换,函数间隔也会缩放(lambda),但几何间隔不变。

    2.2 间隔最大化

    要想使分离超平面更可靠,我们只需要保证该超平面关于(D)中所有样本点几何间隔的最小值(gamma)尽量大即可(这样自然就能保证所有点的几何间隔尽量大),我们称此为间隔最大化策略(或称为硬间隔最大化,和后面训练集近似线性可分的软间隔最大化相对应)。

    可以知道,满足间隔最大化的超平面是唯一的,它能够对所有训练数据有足够大的确信度分类,也就是说不仅能够对实例点进行分类,而且对于所有实例点能够有足够大的确信度分类,这样的超平面的未知的测试实例有很好的分类预测能力。我们将该问题表述为以下带约束优化问题:

    [egin{aligned} underset{m{w}, b}{max} quad gamma \ ext{s.t.} quad y^{(i)} frac{m{w}^T m{x}^{(i)} + b}{||m{w}||} geq gamma \ quad (i = 1, 2, ..., m) end{aligned} ag{11} ]

    根据(gamma = frac{hat{gamma}}{||m{w}||}),我们可以将优化问题((11))写作:

    [egin{aligned} underset{m{w}, b}{max} quad frac{hat{gamma}}{||m{w}||}\ ext{s.t.} quad y^{(i)} (m{w}^T m{x}^{(i)} + b) geq hat{gamma} \ quad (i = 1, 2, ..., m) end{aligned} ag{12} ]

    我们将(m{w})(b)缩放变换为(lambda m{w})(lambda b),则函数间隔(hat{gamma})变为(lambda hat{gamma})。我们法线函数间隔的这一改变对((11))中最优化问题的约束目标函数和不等式约束都没有影响,也就是说它产生了一个等价的最优化问题。

    这样,就可以取(hat{gamma}=1)。将(hat{gamma}=1)代入上面的最优化问题,相当于最大化(frac{1}{||m{w}||})。不过这样目标函数非凸,一般较难求解,为了将其转换为凸优化问题,我们将原本的优化目标函数等价转换为最小化(frac{1}{2}||m{w}||^2)这样我们就将线性可分支持向量机的学习策略转换为了一个(凸)二次规划问题:

    [egin{aligned} underset{m{w}, b}{min} quad frac{1}{2} || m{w}||^2\ ext{s.t.} quad y^{(i)} (m{w}^T m{x}^{(i)} + b) -1 geq 0 \ quad (i = 1, 2, ..., m) end{aligned} ag{13} ]

    该形式称为线性可分支持向量机的标准型。

    附:


    这里提一下凸优化问题和二次规划问题的概念。凸优化问题定义如下

    [egin{aligned} underset{m{x}}{min} quad f_0(m{x})\ ext{s.t.} quad f_i(m{x}) leq 0 quad (i = 1, 2, ..., m) \ h_i(m{x}) = 0 quad (i = 1, 2, ..., p) end{aligned} ag{14} ]

    其中目标函数(f_0(m{x}))和约束函数(f_i(m{x}))都为凸函数,且等式约束函数(h_i(m{x}))为仿射函数(也就是说,(h_i(m{x}))可以写成(h_i(m{x})= m{a}^Tm{x} + b)的形式。

    特别地,当凸优化问题的目标函数(f_0(m{x}))是(凸)二次型并且约束函数(f_i(m{x}))为仿射时,该问题称为二次规划(QP)。二次规划可以表述为:

    [egin{aligned} underset{m{x}}{min} quad (frac{1}{2})m{x}^T extbf{P}m{x} + extbf{q}^{T} m{x} + r \ ext{s.t.} quad f_i(m{x}) leq 0 quad (i = 1, 2, ..., m) \ h_i(m{x}) = 0 quad (i = 1, 2, ..., p) end{aligned} ag{15} ]

    其中( extbf{P})是对称正定矩阵(在式((13))中,( extbf{P})即是单位阵( extbf{I})),约束函数(f_i(m{x}))和等式约束函数(h_i(m{x}))都是仿射函数。


    接下来我们会介绍该凸二次规划问题的求解算法。

    三. 算法

    接下来我们推导求解线性可分支持向量机的高效算法。(真的是万般皆推导,难的是算法的推导过程,算法实现反而很简单)

    直接求解式((13))计算复杂度过高,而凸优化理论中告诉了我们许多可以高效求解((13))问题的理论。我们将问题((13))的形式做为原始问题(primal problem)。应用拉格朗日对偶性。通过求解对偶问题(dual problem)同样得到原始问题的最优解,而且求解会更加高效。问题求解可分两步走。
    第一步:推导原始问题的对偶形式并求得对偶问题的最优解(alpha^{*})

    我们引入拉格朗日乘子向量(m{alpha} = ( alpha_{1}, alpha_{2},..., alpha_{m} )^T),每个不等式约束对应一个拉格朗日乘子(alpha_i geq 0, i=1,2,...,m),我们以此定义拉格朗日函数:

    [egin{aligned} L(m{w}, b, m{alpha}) = frac{1}{2}||m{w}||^2 + (- sum_{i=1}^{m}alpha_i y^{(i)}(m{w}^Tm{x}^{(i)}+b)) + sum_{i=1}^{m}alpha_i end{aligned} ag{16} ]

    (注意,标准凸优化问题式((14))的约束项是小于等于,我们的式((13))是大于等于,在写出拉格朗日函数前,需要对原来的约束不等式两边同乘(-1)

    根据拉格朗日对偶性,原始问题的对偶问题是极大极小问题:

    [egin{aligned} underset{m{alpha}}{max}underset{m{w}, b}{min}L(m{w}, b, m{alpha}) end{aligned} ag{17} ]

    我们将问题拆解为先对(m{w})(b)求极小,再求对(m{alpha})求极大。

    (1) 首先,我们求(g(m{alpha}) = underset{m{w},b}{min}L(m{w}, b, m{alpha}))。由凸函数(L(m{w}, b, m{alpha}))极值满足的一阶必要条件有

    [egin{aligned} abla_{m{w}}L(m{w}, b, m{alpha}) = m{w} - sum_{i=1}^{m}alpha_i y^{(i)}m{x}^{(i)} = 0 \ abla_{b}L(m{w}, b, m{alpha}) = - sum_{i=1}^{m}alpha_iy^{(i)} = 0 end{aligned} ag{18} ]

    可得:

    [egin{aligned} m{w} = sum_{i=1}^{m}alpha_i y^{(i)}m{x}^{(i)}\ sum_{i=1}^{m}alpha_iy^{(i)} = 0 end{aligned} ag{19} ]

    关于(m{w})的等式是一个对(m{w})的显式替换,而第二个等式是对式(L(m{w}, b, alpha))的一个约束,我们先将式((19))中的等式一代入式((16))(L(m{w}, b, m{alpha}))中有

    [L(m{w}, b, m{alpha}) =frac{1}{2}sum_{i=1}^{m}sum_{j=1}^{m}alpha_ialpha_jy^{(i)}y^{(j)}langle m{x}^{(i)}, m{x}^{(j)} angle - sum_{i=1}^{m}sum_{j=1}^{m}alpha_ialpha_jy^{(i)}y^{(j)}langle m{x}^{(i)}, m{x}^{(j)} angle - sum_{i=1}^{m}alpha_iy^{(i)}b + sum_{i=1}^{m}alpha_i ag{20} ]

    虽然式((19))没有显式的关于(b)的等式可供代入,但我们发现将第二个等式(sum_{i=1}^{m}alpha_iy^{(i)}=0)带入后就可以将(b)的那项消掉,得到原问题的拉格朗日对偶函数(或对偶函数)为

    [g(m{alpha}) = underset{m{w}, b}{min}L(m{w}, b, m{alpha}) = -frac{1}{2}sum_{i=1}^{m}sum_{j=1}^{m}alpha_ialpha_jy^{(i)}y^{(j)}langle m{x}^{(i)}, m{x}^{(j)} angle + sum_{i=1}^{m}alpha_i ]

    (2) 接下来我们再求(underset{m{alpha}}{max}{g(m{alpha})}),这也就是我们需要求的对偶问题。

    [egin{aligned} underset{m{alpha}}{max} -frac{1}{2}sum_{i=1}^{m}sum_{j=1}^{m}alpha_ialpha_jy^{(i)}y^{(j)}langle m{x}^{(i)}, m{x}^{(j)} angle + sum_{i=1}^{m}alpha_i \ s.t. sum_{i=1}^{m}alpha_iy^{(i)} = 0 \ alpha_i geq 0, i, 2,...,m end{aligned} ag{22} ]

    可以看出,式((22))这个对偶问题只用求解(m{alpha})系数而(m{alpha})系数只有支持向量才非0,其他全为0,这样的话求解对偶问题的计算就会更加高效。至于求解式((22))算法的具体实现,一般可采用凸优化求解中的内点法(调用支持专业凸优化的Gurobi库),或者采用我们后面一章将会介绍的SMO算法进行求解,在此不再详述。
    解式((22))后我们得到(m{alpha})的解(m{alpha}^{*} = (alpha_1^{*}, alpha_2^{*}, ..., alpha_m^{*})^{T})

    第二步:由对偶问题的最优解(alpha^{*})求得原始问题式((13))的最优解(m{w}^{*},b^{*})

    如果(alpha^{*} = (alpha_1^{*}, alpha_2^{*}, ..., alpha_{m}^{*})^{T})是对偶最优化问题的解,我们将(alpha_i>0)对应的实例点集合被称为支持向量,我们设(U = {alpha_i | alpha_i >0 }),我们(U)从中随机采一个(alpha_s),对应的样本为((m{x}^{(s)}, y^{(s)})),可按下式求得原始最优化问题((13))的解(m{w}^{*}, b^*)

    [egin{aligned} m{w}^{*} = sum_{i=1}^{m}alpha_i^{*}y^{(i)}m{x}^{(i)} \ b^{*} = y^{(s)} - sum_{i=1}^{m}alpha_i^*y^{(i)}langle m{x}^{(s)}, m{x}^{(i)} angle end{aligned} ag{23} ]

    附:


    ((23))可根据KKT条件推导而得。KKT条件如下:

    [ abla_{m{w}}L(m{w}^{*}, b^{*}, m{alpha}^{*}) = m{w}^{*} - sum_{i=1}^{m}alpha_{i}y^{(i)}m{x}^{(i)} = 0 \ abla_{b}L(m{w}^{*}, b^{*}, m{alpha}^{*}) = - sum_{i=1}^{m}alpha_iy^{(i)} = 0 \ alpha_{i}^{*}[y^{(i)}({m{w^{*}}}^T m{x}^{(i)}+b^{*}) - 1] = 0, quad i=1,2,...,m \ y^{(i)}({m{w^{*}}}^T m{x}^{(i)}+b^{*}) - 1 geq 0, quad i=1, 2,..., m \ a_i^* geq 0 ag{24} ]

    由式((24))中第一个等式,我们可得:

    [m{w}^* = sum_{i=1}^{m}alpha_{i}y^{(i)}m{x}^{(i)} ag{25} ]

    其中至少有一个(alpha_i^{*}>0)(用反证法,假设(alpha_i)全为0,则(m{w}^{*})为0,而易得(m{w})为0不是原始问题((13))的最优解,产生矛盾,故假设不符)。

    ((24))中第三个等式称为KKT互补条件,我们设(U = {alpha_i^* | alpha_i^* >0 }),我们(U)从中随机采一个(alpha_s^*),对应的样本为((m{x}^{(s)}, y^{(s)}))。因为有(alpha_s^*>0),则必有

    [y^{(s)}({m{w^{*}}}^T m{x}^{(s)}+b^{*}) - 1 = 0 ag{26} ]

    我们将式((25))代入式((26)),方程两边同乘(y^{(s)}),并注意到(y^{(s)2}=1),我们有

    [b^{*} = y^{(s)} - sum_{i=1}^{m}alpha_i^{*}y^{(i)}langle m{x}^{(i)}, m{x}^{(s)} angle ag{27} ]


    这样,我们就可以得到分离超平面如下:

    [{m{x} | sum_{i=1}^{m}alpha_i^*y^{(i)}langle m{x}^{(i)}, m{x} angle + b^* = 0} ag{29} ]

    分类决策函数如下:

    [f(m{x}) = ext{sign}(sum_{i=1}^{m}alpha_i^*y^{(i)}langle m{x}^{(i)}, m{x} angle + b^*) ag{29} ]

    (之所以写成(langle m{x}^{(i)}, m{x} angle)的内积形式是为了方便我们后面引入核函数)

    给定测试样本(m{x}^*),我们就能按照式((29))计算其类别(f(m{x}^*))。由式((23))可知,(m{w}^*)(b^*)只依赖于(alpha_i>0)的样本点((m{x}^{(i)}, y^{(i)})),而其他样本点对(m{w}^*)(b)没有影响。我们将训练数据中对应于(a_i^*>0)的实例点(m{x}^{(i)} in mathbb{R}^n)称为支持向量。
    综上,按照式((13))的策略求解线性可分支持向量机的算法如下: 
    线性可分支持向量机学习算法
    一个超平面将(mathbb{R}^n)划分为两个半空间,(闭的)半空间是具有下列形式的集合:
    观察算法的2、3步可知,我们只需要关注(alpha_i>0)对应的相关的实例(也就是支持向量),故实际计算复杂度其实很低。

    代码实现

    为了简便起见,这里我们不使用gurobi库(也不采用下一章才介绍的SMO算法),而采用Scipy内置的minimize函数求解式((22))中的优化问题,该问题是有约束优化问题,我们设置好约束项和优化变量的定义域,采用Scipy内置的'SLSQP'算法对其进行求解。代码实现如下:

    from sklearn.datasets import load_breast_cancer
    from sklearn.model_selection import train_test_split
    from sklearn.metrics import accuracy_score
    from scipy.optimize import minimize
    import numpy as np
    from copy import deepcopy
    from random import choice
    class SVM():
        def __init__(self):
            pass
    
        def objective_func(self, alpha):
            sum_v = 0
            for i in range(self.m):
                for j in range(self.m):
                    sum_v += alpha[i]*alpha[j]*self.y_train[i]*self.y_train[j]*self.X_train[i].dot(self.X_train[j])
            return 1/2*sum_v - alpha.sum()
    
        def fit(self, X_train, y_train):
            self.m = X_train.shape[0] #样本个数
            self.X_train, self.y_train = deepcopy(X_train), deepcopy(y_train)
            cons = (
                {'type': 'eq', 'objective_func': lambda alpha: alpha.dot(self.y_train)},  # sum alpha_i * y_i =0
            )
            bnds = tuple([ (0, None) for i in range(self.m)])
            # 求解得到的alpha最优值
            res = minimize(self.objective_func, np.zeros(self.m), method='SLSQP', bounds=bnds)
            alpha_star = res.x
            print(alpha_star)
            self.w = np.zeros(X_train.shape[1])
            for i in range(self.m):
                self.w += X_train[i]*(alpha_star[i]*y_train[i])
            S_with_idx = [(alpha_star_i, idx) for idx, alpha_star_i in enumerate(alpha_star) if alpha_star_i > 0] 
            (_, s) = choice(S_with_idx) # 从 S={a_i* | a_i* > 0}中随机采一个a_s*,记录s
            self.b = self.y_train[s]
            for i in range(self.m):
                self.b -= X_train[i].dot(X_train[s])*alpha_star[i]*y_train[i]
            del [self.X_train, self.y_train]
    
        def pred(self, X_test):
            # 遍历测试集中的每一个样本
            y_pred = []
            for x in X_test:
                y_hat = np.sign(self.w.dot(x)+self.b)
                y_pred.append(y_hat)
            return np.array(y_pred)
                    
    if __name__ == "__main__":
        X, y = load_breast_cancer(return_X_y=True)
        # 标签统一转化为1和-1
        y = np.where(y==1, y, -1)
        X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.5, random_state=0)
        clf = SVM()
        clf.fit(X_train, y_train)
        y_pred = clf.pred(X_test)
        acc_score = accuracy_score(y_test, y_pred)
        print("The accuracy is: %.1f" % acc_score)
    

    在运行该代码的过程中,我们发现主要时间都耗费在调用minimize函数求解式((22))中的优化问题上。在我的电脑(Macbook pro13 + 8核M1芯片)上经过2分钟算法都无法收敛。所以我们建议大家采用更高效专业的gurobi库,或者采用我们之后将会介绍的SMO算法进行求解。

    参考文献

    • [1] 李航. 统计学习方法(第2版)[M]. 清华大学出版社, 2019.
    • [2] 周志华. 机器学习[M]. 清华大学出版社, 2016.
    • [3] Boyd S, Boyd S P, Vandenberghe L. Convex optimization[M]. Cambridge university press, 2004.
    • [4] https://docs.scipy.org/doc/scipy/
    • [5] https://www.gurobi.com/
    数学是符号的艺术,音乐是上界的语言。
  • 相关阅读:
    Microsoft.Office.Interop.Excel.Application同时存在于
    sql 判断是否为数字字符
    在res/drawable中的文件
    在res/values中的文件
    放在res/anim中的文件(持续更新)
    在res/xml中的文件
    资源种类
    在assets中的文件
    android讲义2之输入界面
    在res/menu中的文件
  • 原文地址:https://www.cnblogs.com/lonelyprince7/p/15200880.html
Copyright © 2011-2022 走看看