SVM支持向量机理解_KKT条件_拉格朗日对偶_SMO算法

本文详细介绍了支持向量机的基本概念,包括线性可分情况下的优化问题,KKT条件,以及如何将原始问题转化为对偶问题。重点讲解了序列最小最优化(SMO)算法的理论推导和代码实现,阐述了如何通过SMO算法求解对偶问题,并讨论了线性不可分情况的处理。文章还提供了代码示例和选点策略,展示了SMO算法在不同选择下的优化效果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

一、支持向量机基本型(线性可分)

1.1 问题描述

1.2 参考资料

二、KKT条件

2.1 KKT条件的几个部分

2.1.1 原始条件

2.1.2 梯度条件

2.1.3 松弛互补条件

2.1.4 KKT条件小结

2.2 参考资料

三、对偶形式

3.1 由原始问题到对偶问题

3.2 对偶问题的具体形式

3.3 为什么可以将求解极小极打问题转化为求解极大极小问题?(具体证明我也没看)

3.4 参考资料

四、对偶问题的求解

4.1 SMO算法理论推导(仅考虑线性可分时)

4.2 SMO算法代码实现

4.3 编写程序的过程中的一点点收获

4.3.1 关于选点与优化结果

4.3.2 不同的选点方式带来的损失值变化的不同

4.4 参考资料

五、线性不可分情形

六、核函数


一、支持向量机基本型(线性可分)

1.1 问题描述

        在PLA感知机算法中我们求取超平面是保证所有目标分类正确即可,但是我们为了增加我们学习到的算法的鲁棒性,泛化能力更强,我们可以使超平面到样本的间隔距离最大,这里我们定义间隔为“样本到超平面的最近距离”,也就是说我们的超平面就是对应间隔最大的平面。下面我们用数学语言来描述。

        假设样本集eq?%5Cleft%20%5C%7B%20x_%7B1%7D%2Cy_%7B1%7D%20%5Cright%20%5C%7D%2C%5Cleft%20%5C%7B%20x_%7B2%7D%2Cy_%7B2%7D%20%5Cright%20%5C%7D%2C...%2C%5Cleft%20%5C%7B%20x_%7Bn%7D%2Cy_%7Bn%7D%20%5Cright%20%5C%7D%2Cx_%7Bi%7D%5Cin%20R%5E%7Bn%7D%2C%20y_%7Bi%7D%5Cin%20%5Cleft%20%5C%7B%20+1%2C%20-1%20%5Cright%20%5C%7D,设超平面为eq?wx+b%3D0,点到超平面的距离eq?d%3D%5Cfrac%7B%28wx_%7Bi%7D+b%29y_%7Bi%7D%7D%7B%5Cleft%20%5C%7C%20w%20%5Cright%20%5C%7C%7D    eq?i%3D1%2C2%2C3%2C...%2Cn,最小间隔则为eq?d%5E%7B*%7D%3D%5Cunderset%7Bi%7D%7Bmin%7D%5Cfrac%7B%28wx_%7Bi%7D+b%29y_%7Bi%7D%7D%7B%5Cleft%20%5C%7C%20w%20%5Cright%20%5C%7C%7D,当eq?d%5E%7B*%7D达到最大时的eq?w%2Cb就对应我们的超平面。即:

eq?w%2Cb%20%3D%20arg%5Cunderset%7Bw%2C%20b%7D%7Bmax%7D%5Cunderset%7Bi%7D%7Bmin%7D%5Cfrac%7B%28wx_%7Bi%7D+b%29y_%7Bi%7D%7D%7B%5Cleft%20%5C%7C%20w%20%5Cright%20%5C%7C%7D

现在的问题转变成了优化问题。

8c2ccda4db644fedb2d84df791e0c80a.png

对于这样确定的eq?w%2Cb,必然存在eq?%5Cvarepsiloneq?%5Cvarepsilon%20%3D%20%5Cunderset%7Bi%7D%7Bmin%7D%28wx_%7Bi%7D+b%29y_%7Bi%7D,即:

eq?%28wx_%7Bi%7D+b%29y_%7Bi%7D%5Cgeqslant%20%5Cvarepsilon%2C%5Cvarepsilon%20%3E0

eq?w进行缩放是不影响超平面的,所以可以必然存在这样的eq?w%2Cb,使得:

eq?%28wx_%7Bi%7D+b%29y_%7Bi%7D%5Cgeqslant%201     //这个式子隐含了eq?%5Cvarepsilon%20%3E0这个条件了

所以:

eq?d%5E%7B*%7D%3D%5Cunderset%7Bw%2Cb%7D%7Bmax%7D%5Cfrac%7B1%7D%7B%5Cleft%20%5C%7C%20w%20%5Cright%20%5C%7C%7D

要使eq?d%5E%7B*%7D达到最大,则使eq?%5Cleft%20%5C%7C%20w%20%5Cright%20%5C%7C最小即可。所以上述优化问题可以写成下列形式:

eq?%5Cunderset%7Bw%2Cb%7D%7Bmin%7D%5Cfrac%7B1%7D%7B2%7D%5Cleft%20%5C%7C%20w%20%5Cright%20%5C%7C%5E%7B2%7D

eq?s.t.   eq?%28wx_%7Bi%7D%20+%20b%29y_%7Bi%7D%5Cgeqslant%201   eq?i%3D1%2C2%2C...%2Cn

1.2 参考资料

        以上就是支持向量机的基本型。详细内容可参见李航《机器学习方法》第七章支持向量机。网上其他参考资料可参见:

文档资料:

机器学习:支持向量机(SVM)_燕双嘤的博客-CSDN博客_支持向量机

支持向量机通俗导论(理解SVM的三层境界)_v_JULY_v的博客-CSDN博客_支持向量机

视频资料:

推导SVM基本形式_哔哩哔哩_bilibili

【数之道25】机器学习必经之路-SVM支持向量机的数学精华_哔哩哔哩_bilibili

1. 支持向量机(线性模型)问题_哔哩哔哩_bilibili

svm原理推导:1-支持向量机要解决的问题_哔哩哔哩_bilibili

二、KKT条件

        我把KKT条件放到第二节来讲,我觉得更加符合我们的思维方式,在第一节中我们给出了SVM的基本型,那么接下来我们来试图求解这个优化问题,由此引出KKT条件,很多写法将KKT条件直接上来就构建拉格朗日辅助函数,然后就给读者说就是这么回事,这种逻辑其实不合理,应当直接从梯度角度来讲这个事情。

2.1 KKT条件的几个部分

上述优化问题:

eq?%5Cunderset%7Bw%2Cb%7D%7Bmin%7D%5Cfrac%7B1%7D%7B2%7D%5Cleft%20%5C%7C%20w%20%5Cright%20%5C%7C%5E%7B2%7D

eq?s.t.   eq?%28wx_%7Bi%7D%20+%20b%29y_%7Bi%7D%5Cgeqslant%201   eq?i%3D1%2C2%2C...%2Cn

2.1.1 原始条件

            eq?%28wx_%7Bi%7D%20+%20b%29y_%7Bi%7D%5Cgeqslant%201   eq?i%3D1%2C2%2C...%2Cn

2.1.2 梯度条件

        在求解带不等式约束的优化问题时,考虑两种情况一种是不等式约束有效,一种是不等式约束无效。我们把要优化的函数称为目标函数(这个地方只考虑凸函数的情况)。

        (1)当约束条件无效时,目标函数最优值为其梯度为0时的值。

        (2)当约束条件有效时(目标函数不在约束范围内),那么目标函数取得最优值是应当与约束条件“相切”,也就是梯度方向成平行关系,具体可分为以下4种情况,我们假设目标函数为eq?f%28x%29,约束函数为eq?g%28x%29

        1. eq?minf%28x%29   eq?s.t.   eq?g%28x%29%5Cleqslant%200

83af67e0171c4cacaf7a1a4fb7155d53.png

                 此时有:eq?%5Cbigtriangledown%20f%28x%29%20%3D%20%5Calpha%20%5Cbigtriangledown%20g%28x%29  eq?%5Calpha%20%3C0

        2. eq?minf%28x%29  eq?s.t.  eq?g%28x%29%5Cgeqslant%200

c27963064dea4240b0e57ad8624caf9e.png

                  此时有:eq?%5Cbigtriangledown%20f%28x%29%20%3D%20%5Calpha%20%5Cbigtriangledown%20g%28x%29  eq?%5Calpha%20%3E0

        3. eq?maxf%28x%29  eq?s.t.  eq?g%28x%29%5Cleqslant%200

 8d5e21316c7e413eb24a53452270f461.png

                    此时有:eq?%5Cbigtriangledown%20f%28x%29%20%3D%20%5Calpha%20%5Cbigtriangledown%20g%28x%29  eq?%5Calpha%20%3E0

        4. eq?maxf%28x%29  eq?s.t.  eq?g%28x%29%5Cgeqslant%200

d876439c81964533911df9d1bd909e4b.png

                  此时有:eq?%5Cbigtriangledown%20f%28x%29%20%3D%20%5Calpha%20%5Cbigtriangledown%20g%28x%29  eq?%5Calpha%20%3C0

        注意eq?g%28x%29的梯度方向与eq?g%28x%29具体函数相关,这里为了举例方便仅画出示意图默认了梯度方向。

         在SVM基本型的优化问题中,其情形属于第二种情况,即: eq?%5Cbigtriangledown%20f%28x%29%20%3D%20%5Calpha%20%5Cbigtriangledown%20g%28x%29  eq?%5Calpha%20%3E0

综上两种情况可知:

 eq?%5Cbigtriangledown%20f%28x%29%20%3D%200或  eq?%5Cbigtriangledown%20f%28x%29%20%3D%20%5Calpha%20%5Cbigtriangledown%20g%28x%29  eq?%5Calpha%20%3E0

由于原优化问题是多约束条件,我们画出示意图如下:

5acc01877b474d05b9cca7e4e5dadc12.png

此时有:

eq?%5Cbigtriangledown%20f%28x%29%20%3D%20%5Calpha_%7B1%7D%20%5Cbigtriangledown%20g_%7B1%7D%28x%29+%5Calpha_%7B2%7D%20%5Cbigtriangledown%20g_%7B2%7D%28x%29      eq?%5Calpha%20_%7B1%7D%3E0%2C%5Calpha%20_%7B2%7D%3E0

综上所述,代入原式子,即:

eq?w%20%3D%200eq?%5Cleft%5C%7B%5Cbegin%7Bmatrix%7D%20w%20%3D%5Csum_%7Bi%3D1%7D%5E%7Bn%7D%5Calpha%20_%7Bi%7Dy_%7Bi%7Dx_%7Bi%7D%5C%5C%20%5Csum_%7Bi%3D1%7D%5E%7Bn%7D%5Calpha%20_%7Bi%7Dy_%7Bi%7D%3D0%5C%5C%20%5Calpha%20_%7Bi%7D%3E0%5Cend%7Bmatrix%7D%5Cright.

为了将两种情况综合到一起,我们引入了互补松弛条件。

2.1.3 松弛互补条件

        我们把约束条件有效和约束条件无效两种情况综合打一起,实际上对于第二种情况,当约束条件无效时eq?g_%7Bi%7D%28x%29%5Cneq%200,可令对应的乘子eq?%5Calpha%20_%7Bi%7D=0,此时第二种情况也就变成了第一种情况,当约束条件有效时则约束条件eq?g_%7Bi%7D%28x%29=0,所有始终有:

eq?%5Calpha%20_%7Bi%7D*g_%7Bi%7D%28x%29%3D0

我们称之为互补松弛条件。对于原始问题来讲,互补松弛条件就是:

eq?%5Calpha%20_%7Bi%7D*%5B%28wx_%7Bi%7D+b%29y_%7Bi%7D-1%5D%3D0       eq?i%3D1%2C2%2C...%2Cn

2.1.4 KKT条件小结

        综上所述我们引出了KKT条件,对于凸优化问题因为只存在唯一解KKT条件等价于原始优化问题,解KKT条件就是解原始优化问题,原始优化问题的解一定满足KKT条件。

eq?%28wx_%7Bi%7D%20+%20b%29y_%7Bi%7D%5Cgeqslant%201   eq?i%3D1%2C2%2C...%2Cn

eq?%5Cleft%5C%7B%5Cbegin%7Bmatrix%7D%20w%20%3D%5Csum_%7Bi%3D1%7D%5E%7Bn%7D%5Calpha%20_%7Bi%7Dy_%7Bi%7Dx_%7Bi%7D%5C%5C%20%5Csum_%7Bi%3D1%7D%5E%7Bn%7D%5Calpha%20_%7Bi%7Dy_%7Bi%7D%3D0%5C%5C%20%5Calpha%20_%7Bi%7D%5Cgeqslant%200%5Cend%7Bmatrix%7D%5Cright.

eq?%5Calpha%20_%7Bi%7D*%5B%28wx_%7Bi%7D+b%29y_%7Bi%7D-1%5D%3D0   eq?i%3D1%2C2%2C...%2Cn

        上述就是我们所说的KKT条件,KKT条件主要用来求解,但是实际上我们发现KKT条件中的参数还是很多,求解并不容易,为此我们又引入了拉格朗日对偶性,降低要求解的维度。

2.2 参考资料

文档资料:

Qoo-凸二次规划与KTT条件-支持向量机SVM(二) - 知乎

KKT条件,原来如此简单 | 理论+算例实践 - 知乎

Karush-Kuhn-Tucker (KKT)条件 - 知乎

视频资料:

“拉格朗日对偶问题”如何直观理解?“KKT条件” “Slater条件” “凸优化”打包理解_哔哩哔哩_bilibili

拉格朗日对偶性_哔哩哔哩_bilibili

三、对偶形式

3.1 由原始问题到对偶问题

        上面我们已经说到,原问题转化为对偶问题主要原因是对偶问题会好计算一些,对偶问题其实就是拉格朗日极大极小和极小极大问题。我们的原始问题是不等式约束问题,现在我们要将不等式约束问题转化为极大极小或者极小极大问题。我们这里写出原问题:

eq?%5Cunderset%7Bw%2Cb%7D%7Bmin%7D%5Cfrac%7B1%7D%7B2%7D%5Cleft%20%5C%7C%20w%20%5Cright%20%5C%7C%5E%7B2%7D

eq?s.t.   eq?%28wx_%7Bi%7D%20+%20b%29y_%7Bi%7D%5Cgeqslant%201   eq?i%3D1%2C2%2C...%2Cn

        首先构建拉格朗日辅助函数,注意该辅助函数约束条件部分前的符号是减号,不明白的话可以倒回去看梯度条件,就明白这个地方为什么是减号了。

eq?L%28w%2Cb%2C%5Calpha%20%29%20%3D%20%5Cfrac%7B1%7D%7B2%7D%5Cleft%20%5C%7C%20w%20%5Cright%20%5C%7C%5E%7B2%7D-%5Csum_%7Bi%3D1%7D%5E%7Bn%7D%5Calpha%20_%7Bi%7D%5B%28wx_%7Bi%7D%20+%20b%29y_%7Bi%7D%20-%201%5D         eq?%5Calpha%20_%7Bi%7D%5Cgeqslant%200

eq?%5Cunderset%7B%5Calpha_%7Bi%7D%3A%5Calpha_%7Bi%7D%20%5Cgeqslant%200%20%7D%7Bmax%7DL%28w%2Cb%2C%5Calpha%20%29的值,考虑以下两种情况:

        (1)eq?w%2Cb不满足原约束条件,即存在某个eq?x_%7Bi%7D使得:eq?wx_%7Bi%7D+b%20-%201%3C%200,要想值最大则有满足条件的部分eq?%5Calpha%20_%7Bi%7D%3D0,不满足的部分eq?%5Calpha%20_%7Bi%7D%20%3D%20+%5Cinfty,那么有:

eq?%5Cunderset%7B%5Calpha_%7Bi%7D%3A%5Calpha_%7Bi%7D%20%5Cgeqslant%200%20%7D%7Bmax%7DL%28w%2Cb%2C%5Calpha%20%29%20%3D+%5Cinfty

        (2)eq?w%2Cb满足原约束条件,则当eq?%5Calpha%20_%7Bi%7D%3D0时,值取到最大,即:

eq?%5Cunderset%7B%5Calpha_%7Bi%7D%3A%5Calpha_%7Bi%7D%20%5Cgeqslant%200%20%7D%7Bmax%7DL%28w%2Cb%2C%5Calpha%20%29%20%3D%5Cfrac%7B1%7D%7B2%7D%5Cleft%20%5C%7C%20w%20%5Cright%20%5C%7C%5E%7B2%7D

         综上所述:

6d9c876b543b41a1991821a282f8aa88.png

        这里的eq?w%2Cb是无约束的,可以取任意值,实际上就是当取到最优值的时候,对应的eq?w%2Cb必然满足原约束条件,所以可以不管约束条件只管放心大胆的去求极值就行。综上所述,可得极小极大问题eq?%5Cunderset%7Bw%2Cb%7D%7Bmin%7D%5Cunderset%7B%5Calpha_%7Bi%7D%3A%5Calpha_%7Bi%7D%20%5Cgeqslant%200%20%7D%7Bmax%7DL%28w%2Cb%2C%5Calpha%20%29等同于下:

eq?%5Cunderset%7Bw%2Cb%7D%7Bmin%7D%5Cunderset%7B%5Calpha_%7Bi%7D%3A%5Calpha_%7Bi%7D%20%5Cgeqslant%200%20%7D%7Bmax%7DL%28w%2Cb%2C%5Calpha%20%29%20%3D%20%5Cfrac%7B1%7D%7B2%7D%5Cleft%20%5C%7C%20w%20%5Cright%20%5C%7C%5E%7B2%7D

该极小极大问题的解与原始问题的解是一致的。到此为止我们完成了将原始问题转化为拉格朗日极小极大问题。我们顺理成章的可以引出他的对偶问题,极大极小问题:

eq?%5Cunderset%7B%5Calpha_%7Bi%7D%3A%5Calpha_%7Bi%7D%20%5Cgeqslant%200%20%7D%7Bmax%7D%5Cunderset%7Bw%2Cb%7D%7Bmin%7DL%28w%2Cb%2C%5Calpha%20%29

3.2 对偶问题的具体形式

        对于eq?%5Cunderset%7B%5Calpha_%7Bi%7D%3A%5Calpha_%7Bi%7D%20%5Cgeqslant%200%20%7D%7Bmax%7D%5Cunderset%7Bw%2Cb%7D%7Bmin%7DL%28w%2Cb%2C%5Calpha%20%29我们可以先求eq?%5Cunderset%7Bw%2Cb%7D%7Bmin%7DL%28w%2Cb%2C%5Calpha%20%29,该拉格朗日辅助函数可以看出也是一个凸函数,或者说是凹函数(凸函数就是极小值点在最下面,凹函数就是极大值点在最上面,凸的“拱”往下,凹的“拱”往上,这个地方不要太过纠结,毕竟加个负号就相互转换了)。另外,实际上对偶问题一定是一个凹函数,因为对偶函数是一族关于拉格朗日乘子(对应这里就是eq?%5Calpha)的仿射函数的逐点下确界,所以即使原问题不是凸的,对偶问题也是凹的,他的最优值只有一个,即梯度为0的点。因此可以得到条件如下:

eq?%5Cbigtriangledown%20_%7Bw%7DL%28w%2Cb%2C%5Calpha%20%29%20%3D%20w%20-%20%5Csum_%7Bi%3D1%7D%5E%7Bn%7D%5Calpha%20_%7Bi%7Dx_%7Bi%7Dy_%7Bi%7D%20%3D%200

eq?%5Cbigtriangledown%20_%7Bb%7DL%28w%2Cb%2C%5Calpha%20%29%20%3D%20-%5Csum_%7Bi%3D1%7D%5E%7Bn%7D%5Calpha%20_%7Bi%7Dy_%7Bi%7D%3D0

        将该结论代入拉格朗日辅助函数可得:

eq?%5Cunderset%7Bw%2Cb%7D%7Bmin%7DL%28w%2Cb%2C%5Calpha%20%29%3D-%5Cfrac%7B1%7D%7B2%7D%5Csum_%7Bi%3D1%7D%5E%7Bn%7D%5Csum_%7Bj%3D1%7D%5E%7Bn%7D%5Calpha_%7Bi%7D%20%5Calpha%20_%7Bj%7Dy_%7Bi%7Dy_%7Bj%7Dx_%7Bi%7Dx_%7Bj%7D+%5Csum_%7Bi%3D1%7D%5E%7Bn%7D%5Calpha%20_%7Bi%7D

        所以对偶问题可以转变为:

eq?%5Cunderset%7B%5Calpha_%7Bi%7D%3A%5Calpha_%7Bi%7D%20%5Cgeqslant%200%20%7D%7Bmax%7D-%5Cfrac%7B1%7D%7B2%7D%5Csum_%7Bi%3D1%7D%5E%7Bn%7D%5Csum_%7Bj%3D1%7D%5E%7Bn%7D%5Calpha_%7Bi%7D%20%5Calpha%20_%7Bj%7Dy_%7Bi%7Dy_%7Bj%7Dx_%7Bi%7Dx_%7Bj%7D+%5Csum_%7Bi%3D1%7D%5E%7Bn%7D%5Calpha%20_%7Bi%7D

s.t.      eq?-%5Csum_%7Bi%3D1%7D%5E%7Bn%7D%5Calpha%20_%7Bi%7Dy_%7Bi%7D%3D0

         进一步写成如下形式:

eq?%5Cunderset%7B%5Calpha%7D%7Bmin%7D%5Cfrac%7B1%7D%7B2%7D%5Csum_%7Bi%3D1%7D%5E%7Bn%7D%5Csum_%7Bj%3D1%7D%5E%7Bn%7D%5Calpha_%7Bi%7D%20%5Calpha%20_%7Bj%7Dy_%7Bi%7Dy_%7Bj%7Dx_%7Bi%7Dx_%7Bj%7D-%5Csum_%7Bi%3D1%7D%5E%7Bn%7D%5Calpha%20_%7Bi%7D

s.t.    eq?%5Csum_%7Bi%3D1%7D%5E%7Bn%7D%5Calpha%20_%7Bi%7Dy_%7Bi%7D%3D0

                             eq?%5Calpha%20_%7Bi%7D%5Cgeqslant%200    eq?i%3D1%2C2%2C3%2C...%2Cn

这就是对偶问题的具体形式了。

3.3 为什么可以将求解极小极打问题转化为求解极大极小问题?(具体证明我也没看)

        这个地方我只能给出个定理李航《机器学习方法》附录C里面这么说的,对偶问题是强对偶的情况下(目标函数是凸函数,约束集是凸集或者说这里的约束条件全是仿射函数,满足Slater条件),原问题的解eq?w%5E%7B*%7D%2Cb%5E%7B*%7D,与对偶问题的解eq?%5Calpha%20%5E%7B*%7D,这一对解能够满足KKT条件。前面我们已经说了,对于凸优化问题KKT条件解是唯一的,也就是说我们通过eq?%5Calpha%20%5E%7B*%7D和KKT条件求得的eq?w%2Cb,必然就是eq?w%5E%7B*%7D%2Cb%5E%7B*%7D也就是原问题的解。

3.4 参考资料

文档资料:

李航《机器学习方法》第七章、附录C

王书宁《凸优化》第五章

拉格朗日对偶问题_VelvetQuilt的博客-CSDN博客

机器学习中的数学——拉格朗日乘子法(二):不等式约束与KKT条件_von Neumann的博客-CSDN博客_拉格朗日乘子法 不等式约束

视频资料:

【数之道25】机器学习必经之路-SVM支持向量机的数学精华_哔哩哔哩_bilibili

拉格朗日对偶性_哔哩哔哩_bilibili

“拉格朗日对偶问题”如何直观理解?“KKT条件” “Slater条件” “凸优化”打包理解_哔哩哔哩_bilibili

四、对偶问题的求解

4.1 SMO算法理论推导(仅考虑线性可分时)

        (这个地方我先只考虑可分的情形,到不可分情形时我们再逐步深入。)

        经过上文的探讨,我们得到了SVM优化问题的对偶问题,如下:

eq?%5Cunderset%7B%5Calpha%7D%7Bmin%7D%5Cfrac%7B1%7D%7B2%7D%5Csum_%7Bi%3D1%7D%5E%7Bn%7D%5Csum_%7Bj%3D1%7D%5E%7Bn%7D%5Calpha_%7Bi%7D%20%5Calpha%20_%7Bj%7Dy_%7Bi%7Dy_%7Bj%7Dx_%7Bi%7Dx_%7Bj%7D-%5Csum_%7Bi%3D1%7D%5E%7Bn%7D%5Calpha%20_%7Bi%7D

s.t.    eq?%5Csum_%7Bi%3D1%7D%5E%7Bn%7D%5Calpha%20_%7Bi%7Dy_%7Bi%7D%3D0

                             eq?%5Calpha%20_%7Bi%7D%5Cgeqslant%200    eq?i%3D1%2C2%2C3%2C...%2Cn

        该对偶问题也是凸优化问题,针对该类问题我们可以使用梯度下降的方式求解,但是上述优化问题由于有约束条件eq?%5Csum_%7Bi%3D1%7D%5E%7Bn%7D%5Calpha%20_%7Bi%7Dy_%7Bi%7D%3D0的存在,我们在使用梯度下降计算最优解时,不能够单变量下降,应当多个变量组合下降,保证满足约束条件。我们不在这里对使用梯度下降做过多探讨,针对SVM学术界已经提出了更高效的计算方法,即序列最小最优化算法(SMO)该算法于1998年由Platt提出,其论文见参考资料。

        SMO算法思想就是选取两个变量eq?%5Calpha%20_%7Bi%7Deq?%5Calpha%20_%7Bj%7D作为优化对象,其他eq?%5Calpha当做已知量。为啥不是选一个变量来优化?由约束条件决定的没办法单独对一个变量做优化。或者为啥不是选三个变量来优化?选三个就不叫“最小序列”优化算法了,主要是3个变量计算起来也不简单。将eq?%5Calpha%20_%7Bi%7Deq?%5Calpha%20_%7Bj%7D单独拎出来,所以有以下式子(为方便书写,我们假设eq?ieq?j就是1、2,1、2也具有一般性并不会影响结果,eq?X_%7Bij%7D表示eq?x_%7Bi%7D%5Ccdot%20x_%7Bj%7D点积):

        原问题的对偶问题实际上是一个二次规划问题,这个二次规划问题为什么能够分解为二次规划子问题来求解,以及关于为什么每次优化两个值eq?%5Calpha%20_%7Bi%7D%2C%5Calpha%20_%7Bj%7D,能够使原目标函数值减少,并且最终收敛的理论保证作者在原论文中提到过。感兴趣的请自行查看原文,部分原文内容如下:

a8a701789d614e9ca85bdcc346013360.png

        实际对应的理论是在Osuna, E的论文《An improved training algorithm for support vector machines》中提到的。

        我们把eq?%5Calpha%20_%7B1%7D%2C%5Calpha%20_%7B2%7D单独提出来得下式:

eq?%5Cunderset%7B%5Calpha%20_%7B1%7D%2C%5Calpha%20_%7B2%7D%7D%7Bmin%7DL%28%5Calpha%20_%7B1%7D%2C%5Calpha%20_%7B2%7D%29%3D%5Cfrac%7B1%7D%7B2%7D%5Calpha%20_%7B1%7D%5E%7B2%7DX_%7B11%7D+%5Cfrac%7B1%7D%7B2%7D%5Calpha%20_%7B2%7D%5E%7B2%7DX_%7B22%7D+%5Calpha_%7B1%7D%5Calpha_%7B2%7D%20y_%7B1%7Dy_%7B2%7DX_%7B12%7D-%28%5Calpha%20_%7B1%7D+%5Calpha%20_%7B2%7D%29+y_%7B1%7D%5Calpha%20_%7B1%7D%5Csum_%7Bi%3D3%7D%5E%7Bn%7Dy_%7Bi%7D%5Calpha%20_%7Bi%7DX_%7Bi1%7D+y_%7B2%7D%5Calpha%20_%7B2%7D%5Csum_%7Bi%3D3%7D%5E%7Bn%7Dy_%7Bi%7D%5Calpha%20_%7Bi%7DX_%7Bi2%7D

s.t.  eq?%5Calpha%20_%7B1%7Dy_%7B1%7D+%5Calpha%20_%7B2%7Dy_%7B2%7D%20%3D%20-%5Csum_%7Bi%3D3%7D%5E%7Bn%7D%5Calpha%20_%7Bi%7Dy_%7Bi%7D%20%3D%20%5Cvarepsilon        //  eq?%5Cvarepsilon为某一确定值

eq?%5Calpha%20_%7B1%7D%5Cgeqslant%200,eq?%5Calpha%20_%7B2%7D%5Cgeqslant%200

        有上述可知:

eq?%5Calpha_%7B1%7D%20%3D%20y_%7B1%7D%28%5Cvarepsilon%20-%5Calpha%20_%7B2%7Dy_%7B2%7D%29

        代入要优化的目标函数,可得:

eq?L%28%5Calpha%20_%7B2%7D%29%20%3D%5Cfrac%7B1%7D%7B2%7D%28%5Cvarepsilon%20-%5Calpha%20_%7B2%7Dy_%7B2%7D%29%5E2X_%7B11%7D+%5Cfrac%7B1%7D%7B2%7D%5Calpha%20%5E2X_%7B22%7D+%28%5Cvarepsilon%20-%5Calpha%20_%7B2%7Dy_%7B2%7D%29%5Calpha%20_%7B2%7Dy_%7B2%7DX_%7B12%7D-y_%7B1%7D%28%5Cvarepsilon%20-%5Calpha_%7B2%7Dy_%7B2%7D%29-%5Calpha_%7B2%7D+%28%5Cvarepsilon%20-%5Calpha%20_%7B2%7Dy_%7B2%7D%29%5Csum_%7Bi%3D3%7D%5E%7Bn%7Dy_%7Bi%7D%5Calpha%20_%7Bi%7DX_%7Bi1%7D+%5Calpha%20_%7B2%7Dy_%7B2%7D%5Csum_%7Bi%3D3%7D%5E%7Bn%7Dy_%7Bi%7D%5Calpha%20_%7Bi%7DX_%7Bi2%7D

        要想使得eq?L%28%5Calpha_%7B2%7D%29值最小,也就是其梯度等于0时为最小值,我们对eq?L%28%5Calpha_%7B2%7D%29求梯度,如下:

eq?%5Cbigtriangledown_%7B%5Calpha%20_%7B2%7D%7D%20L%28%5Calpha%20_%7B2%7D%29%3D%5Calpha%20_%7B2%7D%28X_%7B11%7D-2X_%7B12%7D+X_%7B22%7D%29-%5Cvarepsilon%20y_%7B2%7DX_%7B11%7D+%5Cvarepsilon%20y_%7B2%7DX_%7B12%7D+y_%7B1%7Dy_%7B2%7D-1-y_%7B2%7D%5Csum_%7Bi%3D3%7D%5E%7Bn%7D%5Calpha%20_%7Bi%7Dy_%7Bi%7DX_%7Bi1%7D+y_%7B2%7D%5Csum_%7Bi%3D3%7D%5E%7Bn%7D%5Calpha%20_%7Bi%7Dy_%7Bi%7DX_%7Bi2%7D%3D0

        得:

eq?%5Calpha%20_%7B2%7D%28X_%7B11%7D-2X_%7B12%7D+X_%7B22%7D%29%3D%5Cvarepsilon%20y_%7B2%7DX_%7B11%7D-%5Cvarepsilon%20y_%7B2%7DX_%7B12%7D-y_%7B1%7Dy_%7B2%7D+1+y_%7B2%7D%5Csum_%7Bi%3D3%7D%5E%7Bn%7D%5Calpha%20_%7Bi%7Dy_%7Bi%7DX_%7Bi1%7D-y_%7B2%7D%5Csum_%7Bi%3D3%7D%5E%7Bn%7D%5Calpha%20_%7Bi%7Dy_%7Bi%7DX_%7Bi2%7D

        又:eq?%5Cvarepsilon%20%3D%20%5Calpha%20_%7B1%7Dy_%7B1%7D+%5Calpha%20_%7B2%7Dy_%7B2%7D,和上式放到一起可以得含两个变量的方程组:

eq?%5Cleft%5C%7B%5Cbegin%7Bmatrix%7D%5Calpha%20_%7B2%7D%28X_%7B11%7D-2X_%7B12%7D+X_%7B22%7D%29%3D%5Cvarepsilon%20y_%7B2%7DX_%7B11%7D-%5Cvarepsilon%20y_%7B2%7DX_%7B12%7D-y_%7B1%7Dy_%7B2%7D+1+y_%7B2%7D%5Csum_%7Bi%3D3%7D%5E%7Bn%7D%5Calpha%20_%7Bi%7Dy_%7Bi%7DX_%7Bi1%7D-y_%7B2%7D%5Csum_%7Bi%3D3%7D%5E%7Bn%7D%5Calpha%20_%7Bi%7Dy_%7Bi%7DX_%7Bi2%7D%5C%5C%20%5Calpha%20_%7B1%7Dy_%7B1%7D+%5Calpha%20_%7B2%7Dy_%7B2%7D%3D%5Cvarepsilon%20%5Cend%7Bmatrix%7D%5Cright.

        这样我们就把求解二次规划子问题的解,变成了求解方程组。这个方程组看起来也不好求,但是我们有方法。求解线性方程组我们可以使用迭代法,只要在当前方程形式下解是逐步收敛的,那么就可以使用这种方法。相关知识请参考李庆扬《数值分析》第五版第6章——解线性方程组的迭代法。我们对eq?%5Calpha%20_%7B1%7Deq?%5Calpha%20_%7B2%7D赋满足条件(也就是前面说的对偶问题里面的两个约束,eq?%5Csum_%7Bi%3D1%7D%5E%7Bn%7D%5Calpha%20_%7Bi%7Dy_%7Bi%7D%3D0eq?%5Calpha%20_%7Bi%7D%5Cgeqslant%200    eq?i%3D1%2C2%2C3%2C...%2Cn)的初值,这里我们可以赋初值eq?%5Calpha%20%3D0。迭代方程如下:

eq?%5Cleft%5C%7B%5Cbegin%7Bmatrix%7D%5Calpha%20_%7B2%7D%5E%7Bn+1%7D%28X_%7B11%7D-2X_%7B12%7D+X_%7B22%7D%29%3D%5Cvarepsilon%20y_%7B2%7DX_%7B11%7D-%5Cvarepsilon%20y_%7B2%7DX_%7B12%7D-y_%7B1%7Dy_%7B2%7D+1+y_%7B2%7D%5Csum_%7Bi%3D3%7D%5E%7Bn%7D%5Calpha%20_%7Bi%7Dy_%7Bi%7DX_%7Bi1%7D-y_%7B2%7D%5Csum_%7Bi%3D3%7D%5E%7Bn%7D%5Calpha%20_%7Bi%7Dy_%7Bi%7DX_%7Bi2%7D%5C%5C%20%5Calpha%20_%7B1%7D%5E%7Bn%7Dy_%7B1%7D+%5Calpha%20_%7B2%7D%5E%7Bn%7Dy_%7B2%7D%3D%5Cvarepsilon%20%5Cend%7Bmatrix%7D%5Cright.

        eq?%5Calpha%20_%7B2%7D%5E%7Bn%7D表示上一次结果,eq?%5Calpha%20_%7B2%7D%5E%7Bn+1%7D表示下一次结果。

        那么新的问题来了,我们该怎么去选取eq?%5Calpha%20_%7B1%7Deq?%5Calpha%20_%7B2%7D来进行迭代,前面说了eq?%5Calpha%20_%7B1%7Deq?%5Calpha%20_%7B2%7D是泛指,可能是eq?%5Calpha%20_%7B3%7Deq?%5Calpha%20_%7B4%7Deq?%5Calpha%20_%7B5%7D等。我们接下来对上述方程进行变形,然后告诉你该怎么去选(这个地方要是去遍历所有组合是不是也能求得解呢?我理解应该也是可以的就是效率低了点,后面实验一下)。

        设:eq?f%28x%29%20%3D%20wx%5E%7BT%7D+b

        根据KKT条件(前面说了对偶问题的解和原问题的解,放到一起满足KKT条件),eq?w%20%3D%5Csum_%7Bi%3D1%7D%5E%7Bn%7D%5Calpha%20_%7Bi%7Dy_%7Bi%7Dx_%7Bi%7D,所以eq?f%28x%29%20%3D%20%5Csum_%7Bi%3D1%7D%5E%7Bn%7D%5Calpha%20_%7Bi%7Dy_%7Bi%7Dx_%7Bi%7Dx%5E%7BT%7D+b

        所以:

eq?%5Cleft%5C%7B%5Cbegin%7Bmatrix%7D%20f%28x_%7B1%7D%29%3D%5Calpha%20_%7B1%7Dy_%7B1%7DX_%7B11%7D+%5Calpha%20_%7B2%7Dy_%7B2%7DX_%7B21%7D+%20%5Csum_%7Bi%3D3%7D%5E%7Bn%7D%5Calpha%20_%7Bi%7Dy_%7Bi%7DX_%7Bi1%7D+b%5C%5C%20f%28x_%7B2%7D%29%3D%5Calpha%20_%7B1%7Dy_%7B1%7DX_%7B12%7D+%5Calpha%20_%7B2%7Dy_%7B2%7DX_%7B22%7D+%20%5Csum_%7Bi%3D3%7D%5E%7Bn%7D%5Calpha%20_%7Bi%7Dy_%7Bi%7DX_%7Bi2%7D+b%20%5Cend%7Bmatrix%7D%5Cright.

eq?%5CRightarrow%20%5Cleft%5C%7B%5Cbegin%7Bmatrix%7D%20%5Csum_%7Bi%3D3%7D%5E%7Bn%7D%5Calpha%20_%7Bi%7Dy_%7Bi%7DX_%7Bi1%7D%3D%20f%28x_%7B1%7D%29-%5Calpha%20_%7B1%7Dy_%7B1%7DX_%7B11%7D-%5Calpha%20_%7B2%7Dy_%7B2%7DX_%7B21%7D-b%5C%5C%20%5Csum_%7Bi%3D3%7D%5E%7Bn%7D%5Calpha%20_%7Bi%7Dy_%7Bi%7DX_%7Bi2%7D%3Df%28x_%7B2%7D%29-%5Calpha%20_%7B1%7Dy_%7B1%7DX_%7B12%7D-%5Calpha%20_%7B2%7Dy_%7B2%7DX_%7B22%7D-b%20%5Cend%7Bmatrix%7D%5Cright.

        将上式代入迭代方程(这里是代入迭代方程的前一步,即eq?%5Calpha%20_%7B1%7D%5E%7Bn%7Deq?%5Calpha%20_%7B2%7D%5E%7Bn%7D)化简后可得:

eq?%5Calpha%20_%7B2%7D%5E%7Bn+1%7D%3D%5Calpha%20_%7B2%7D%5E%7Bn%7D+%5Cfrac%7By_%7B2%7D%5Cleft%20%5B%20f%28x_%7B1%7D%29-y_%7B1%7D-%28f%28x_%7B2%7D%29-y_%7B2%7D%29%20%5Cright%20%5D%7D%7BX_%7B11%7D-2X_%7B12%7D+X_%7B22%7D%7D

        接下来我们再把前面的KKT条件放这里,并进行新的分析:

      

eq?%28wx_%7Bi%7D%20+%20b%29y_%7Bi%7D%5Cgeqslant%201   eq?i%3D1%2C2%2C...%2Cn

eq?%5Cleft%5C%7B%5Cbegin%7Bmatrix%7D%20w%20%3D%5Csum_%7Bi%3D1%7D%5E%7Bn%7D%5Calpha%20_%7Bi%7Dy_%7Bi%7Dx_%7Bi%7D%5C%5C%20%5Csum_%7Bi%3D1%7D%5E%7Bn%7D%5Calpha%20_%7Bi%7Dy_%7Bi%7D%3D0%5C%5C%20%5Calpha%20_%7Bi%7D%5Cgeqslant%200%5Cend%7Bmatrix%7D%5Cright.

eq?%5Calpha%20_%7Bi%7D*%5B%28wx_%7Bi%7D+b%29y_%7Bi%7D-1%5D%3D0   eq?i%3D1%2C2%2C...%2Cn

        我们要注意我们通过迭代去求得的eq?%5Calpha(注意这里的eq?%5Calpha是(eq?%5Calpha%20_%7B1%7D%2C%5Calpha%20_%7B2%7D%2C...%2C%5Calpha%20_%7Bn%7D)向量),什么时候就是我们想要的eq?%5Calpha呢?意思就是什么时候得到的eq?%5Calpha就是是目标函数取得最优值的eq?%5Calpha。回忆前面的内容,满足KKT条件的eq?%5Calpha就是最优的eq?%5Calpha。那么我们就知道该如何去选取eq?%5Calpha%20_%7B1%7D%5E%7Bn%7Deq?%5Calpha%20_%7B2%7D%5E%7Bn%7D,选取违背KKT条件的,又根据Osuna, E的理论对含有违背KKT条件的QP子问题进行优化,一定能够使原问题得到下降,在没有达到最优前那么一定会有对应的eq?%5Calpha%20_%7Bi%7D对应的样本违背KKT条件。

        接下来如何快速的选取对应违背KKT条件的eq?%5Calpha%20_%7Bi%7D呢?再次回到KKT条件。我们以eq?%5Calpha%20_%7Bi%7D为关注点,来分情况判断。

eq?%5Calpha%20_%7Bi%7D%5Cleft%5C%7B%5Cbegin%7Bmatrix%7D%20%3D0%5Cquad%28wx_%7Bi%7D+b%29y_%7Bi%7D%3E1%5C%5C%20%3E0%20%5Cquad%28wx_%7Bi%7D+b%29y_%7Bi%7D%3D1%5Cend%7Bmatrix%7D%5Cright.

        对于无效的约束条件,其对应的拉格朗日乘子eq?%5Calpha%20_%7Bi%7D为0,当约束条件有效时,其对应的拉格朗日乘子eq?%5Calpha%20_%7Bi%7D大于0。eq?%28wx_%7Bi%7D+b%29y_%7Bi%7D代表的就是软间隔距离。利用上式我们可以简化KKT条件的判断。另外由于对偶问题实际上是一个凸优化问题,要想使优化值下降的足够多,那么我们就要想使eq?%5Calpha%20_%7Bi%7D%5E%7Bn%7Deq?%5Calpha%20_%7Bi%7D%5E%7Bn+1%7D之间变化足够大。结合前面的迭代式:

eq?%5Calpha%20_%7B2%7D%5E%7Bn+1%7D%3D%5Calpha%20_%7B2%7D%5E%7Bn%7D+%5Cfrac%7By_%7B2%7D%5Cleft%20%5B%20f%28x_%7B1%7D%29-y_%7B1%7D-%28f%28x_%7B2%7D%29-y_%7B2%7D%29%20%5Cright%20%5D%7D%7BX_%7B11%7D-2X_%7B12%7D+X_%7B22%7D%7D

也就是eq?%5Cfrac%7By_%7B2%7D%5Cleft%20%5B%20f%28x_%7B1%7D%29-y_%7B1%7D-%28f%28x_%7B2%7D%29-y_%7B2%7D%29%20%5Cright%20%5D%7D%7BX_%7B11%7D-2X_%7B12%7D+X_%7B22%7D%7D,变化足够大。在SMO原论文中,这个地方分母是核函数,由于核函数计算比较困难,原论文实际上忽略了分母,只考虑分子的变化足够大,即eq?f%28x_%7B1%7D%29-y_%7B1%7D-%28f%28x_%7B2%7D%29-y_%7B2%7D%29变化足够大,这个就实际上可能会导致优化值降低的不够。那么现在我们就可以在外层循环中先找到一个违背KKT条件的eq?%5Calpha%20_%7B1%7D%5E%7Bn%7D以及其对应的样本(这个地方说是违背越严重的点带来的优化越多,怎么判断严重程度呢?就是用到上面说的简化版的判断是否违背KKT的式子,比如说eq?%5Calpha%20_%7Bi%7D%3D0时,那么eq?%28wx_%7Bi%7D+b%29y_%7Bi%7D-1值越大则说明违背越严重),然后在内层循环中找到一个与eq?%5Calpha%20_%7B1%7D%5E%7Bn%7D能够产生足够大差的eq?%5Calpha%20_%7B2%7D%5E%7Bn%7D以及其对应的样本。接下来我们再考虑一个问题。

eq?%5Calpha%20_%7B2%7D%5E%7Bn+1%7D%3D%5Calpha%20_%7B2%7D%5E%7Bn%7D+%5Cfrac%7By_%7B2%7D%5Cleft%20%5B%20f%28x_%7B1%7D%29-y_%7B1%7D-%28f%28x_%7B2%7D%29-y_%7B2%7D%29%20%5Cright%20%5D%7D%7BX_%7B11%7D-2X_%7B12%7D+X_%7B22%7D%7D

        这个求解实际上对于eq?%5Calpha%20_%7Bi%7D是一个无约束求解,但实际上在KKT条件中,eq?%5Calpha%20_%7Bi%7D%5Cgeqslant%200。因此我们需要对求得的eq?%5Calpha%20_%7Bi%7D进行裁剪。注意要保证eq?%5Calpha%20_%7B1%7D%5E%7Bn+1%7Deq?%5Calpha%20_%7B2%7D%5E%7Bn+1%7D同时满足条件。通过上述迭代式我们求得了eq?%5Calpha%20_%7B2%7D%5E%7Bn+1%7D,我们怎么求eq?%5Calpha%20_%7B1%7D%5E%7Bn+1%7D呢?

又有:eq?%5Calpha%20_%7B1%7D%5E%7Bn%7Dy_%7B1%7D+%5Calpha%20_%7B2%7D%5E%7Bn%7Dy_%7B2%7D%3D%5Calpha%20_%7B1%7D%5E%7Bn+1%7Dy_%7B1%7D+%5Calpha%20_%7B2%7D%5E%7Bn+1%7Dy_%7B2%7D

所以:eq?%5Calpha%20_%7B1%7D%5E%7Bn+1%7D%3D%5Calpha%20_%7B1%7D%5E%7Bn%7D+y_%7B1%7Dy_%7B2%7D%28%5Calpha%20_%7B2%7D%5E%7Bn%7D-%5Calpha%20_%7B2%7D%5E%7Bn+1%7D%29

考虑:eq?%5Calpha%20_%7B1%7D%5Cgeqslant%200eq?%5Calpha%20_%7B2%7D%5Cgeqslant%200

得:eq?%5Cleft%5C%7B%5Cbegin%7Bmatrix%7D%200%5Cleqslant%5Calpha%20_%7B2%7D%5E%7Bn+1%7D%5Cleqslant%20%5Calpha%20_%7B2%7D%5E%7Bn%7D+%5Calpha%20_%7B1%7D%5E%7Bn%7D%5Cquad%20y_%7B1%7D%3Dy_%7B2%7D%5C%5C%20max%5Cleft%20%5C%7B%200%2C%5Calpha%20_%7B2%7D%5E%7Bn%7D-%5Calpha%20_%7B1%7D%5E%7Bn%7D%20%5Cright%20%5C%7D%5Cleqslant%5Calpha%20_%7B2%7D%5E%7Bn+1%7D%20%5Cquad%20y_%7B1%7D%5Cneq%20y_%7B2%7D%5Cend%7Bmatrix%7D%5Cright.

接下来更新阈值b,我们分情况讨论:

        ①当eq?%5Calpha%20_%7B2%7D%5E%7Bn+1%7D%5Cneq%200eq?%5Calpha%20_%7B1%7D%5E%7Bn+1%7D%3D%200时,则eq?%28wx_%7B2%7D+b%29y_%7B2%7D%3D1

        ②当eq?%5Calpha%20_%7B2%7D%5E%7Bn+1%7D%3D%200eq?%5Calpha%20_%7B1%7D%5E%7Bn+1%7D%5Cneq%200时,则eq?%28wx_%7B1%7D+b%29y_%7B1%7D%3D1

        ③当eq?%5Calpha%20_%7B2%7D%5E%7Bn+1%7D%5Cneq%200eq?%5Calpha%20_%7B1%7D%5E%7Bn+1%7D%5Cneq%200时,则eq?%5Cleft%5C%7B%5Cbegin%7Bmatrix%7D%20%28wx_%7B1%7D+b_%7B1%7D%29y_%7B1%7D%3D1%5C%5C%20%28wx_%7B2%7D+b_%7B2%7D%29y_%7B2%7D%3D1%20%5Cend%7Bmatrix%7D%5Cright.,此时eq?b_%7B1%7D%3Db_%7B2%7D

        ③当eq?%5Calpha%20_%7B2%7D%5E%7Bn+1%7D%3D%200eq?%5Calpha%20_%7B1%7D%5E%7Bn+1%7D%3D%200时,则eq?%5Cleft%5C%7B%5Cbegin%7Bmatrix%7D%20%28wx_%7B2%7D+b%29y_%7B2%7D%3E%201%5C%5C%20%28wx_%7B1%7D+b%29y_%7B1%7D%3E%201%20%5Cend%7Bmatrix%7D%5Cright.,即软间隔均大于1。这种情况又要细分讨论。(实际上通过后面的实验我发现,在优化后eq?%5Calpha%20_%7B2%7D%5E%7Bn+1%7Deq?%5Calpha%20_%7B1%7D%5E%7Bn+1%7D其中至少一个会变成支撑向量,所有根本不会出现两者都等于0的情况,所以下面的讨论可以忽略掉,我会在文章结尾部分贴出选点以及每次迭代的优化效果,你就会发现必然会有至少一个优化变量变成了支撑向量)

        (1)当eq?y_%7B1%7D%5Cneq%20y_%7B2%7D时,我们可以得到:eq?-1-wx_%7B1%7D%3Eb%3E1-wx_%7B2%7Deq?-1-wx_%7B2%7D%3Eb%3E1-wx_%7B1%7D,总之获得的是一个区间。这个区间范围内的值都可以取,都可以使KKT条件成立;

        (2)当eq?y_%7B1%7D%3D%20y_%7B2%7D时,我们可以得到:eq?b%3Emax%5Cleft%20%5C%7B1-wx_%7B2%7D%2C1-wx_%7B1%7D%20%5Cright%20%5C%7Deq?b%3Cmin%5Cleft%20%5C%7B-1-wx_%7B2%7D%2C-1-wx_%7B1%7D%20%5Cright%20%5C%7D,这种情况在原论文中实际上被忽略掉了,但是按照前面选取eq?%5Calpha%20_%7B1%7D%5E%7Bn%7Deq?%5Calpha%20_%7B2%7D%5E%7Bn%7D的策略来讲这种情况实际上是不会出现的,因为要使eq?f%28x_%7B1%7D%29-y_%7B1%7D-%28f%28x_%7B2%7D%29-y_%7B2%7D%29变化足够大,那么eq?y_%7B1%7D一定不等于eq?y_%7B2%7D。假设遇到了这种情况,一定要更新b的话,可以让eq?b%3Dmax%5Cleft%20%5C%7B1-wx_%7B2%7D%2C1-wx_%7B1%7D%20%5Cright%20%5C%7Deq?y_%7B1%7D%2Cy_%7B2%7D%3E0eq?b%3Dmin%5Cleft%20%5C%7B-1-wx_%7B2%7D%2C-1-wx_%7B1%7D%20%5Cright%20%5C%7D,eq?y_%7B1%7D%2Cy_%7B2%7D%3C0(这个时候其实就是原文中说的eq?L%3DH时候,这个时候作者直接return 0了放弃本次优化了,实际上也是这么个情况,没有限制到b的范围,和我最初随机给的b实际上没什么本质区别,因为我最初给的b必然能够使至少一个样本满足KKT条件,而现在这个优化后也就使至少两个样本满足KKT条件,没太大意义);

74af7205f3f94db6b7f55e2720a45c68.png

        为了方便理解我画个图来表示上面说的eq?y_%7B1%7D%3D%20y_%7B2%7D的情况优化前和优化后的区别。

9798fed978d94820b3d20c4ec2d4071e.png0d60dad48aec42f897cd2be567d4b833.png

         左侧就是我随便初始化了一个超平面,右侧两个绿色圈出来的样本就是我们选的同号的样本,然后为了让这两个样本满足KKT条件,我们计算出了右侧这个新的超平面。但是我们看结果,新的超平面对于原来有什么大的优化吗?没有。所以这种情况实际上是没什么意义的,上图只是一个示意,想表达的意思就是这种优化和之前的随机初始化没什么区别,运气好你能优化,运气不好越来越差,而且这种情况是不可能出现的,所以作者也直接放弃了这种情况下的优化。所以就得两个异号的样本来做优化才是有意义的,最后优化的超平面会在两个异号的样本之间。

        为了方便计算,我们令eq?E_%7Bi%7D%3D%20f%28x_%7Bi%7D%29-y_%7Bi%7D,并将每个样本的eq?E_%7Bi%7D保存在一个列表中。那么计算eq?%5Calpha%20_%7B2%7D%5E%7Bn+1%7D的迭代式可以写成:

eq?%5Calpha%20_%7B2%7D%5E%7Bn+1%7D%3D%5Calpha%20_%7B2%7D%5E%7Bn%7D+%5Cfrac%7By_%7B2%7D%5Cleft%20%5B%20E_%7B1%7D-E_%7B2%7D%5Cright%20%5D%7D%7BX_%7B11%7D-2X_%7B12%7D+X_%7B22%7D%7D

对b的更新:

eq?%5Cleft%5C%7B%5Cbegin%7Bmatrix%7D%20%5Calpha%20_%7B1%7D%5E%7Bn+1%7Dy_%7B1%7DX_%7B12%7D+%5Calpha%20_%7B2%7D%5E%7Bn+1%7Dy_%7B2%7DX_%7B22%7D+%5Csum_%7Bi%3D3%7D%5E%7Bn%7D%5Calpha%20_%7Bi%7Dy_%7Bi%7DX_%7Bi2%7D+b_%7B2%7D%5E%7Bn+1%7D%3Dy_%7B2%7D%5C%5C%20E_%7B2%7D%5E%7Bn%7D%3D%5Csum_%7Bi%3D3%7D%5E%7Bn%7D%5Calpha%20_%7Bi%7Dy_%7Bi%7DX_%7Bi2%7D+%5Calpha%20_%7B1%7D%5E%7Bn%7Dy_%7B1%7DX_%7B12%7D+%5Calpha%20_%7B2%7D%5E%7Bn%7Dy_%7B2%7DX_%7B22%7D+b_%7B2%7D%5E%7Bn%7D-y_%7B2%7D%20%5Cend%7Bmatrix%7D%5Cright.

两式联立可得:

eq?b_%7B2%7D%5E%7Bn+1%7D%3D-E_%7B2%7D%5E%7Bn%7D-y_%7B1%7DX_%7B12%7D%28%5Calpha%20_%7B1%7D%5E%7Bn+1%7D-%5Calpha%20_%7B1%7D%5E%7Bn%7D%29-y_%7B2%7DX_%7B22%7D%28%5Calpha%20_%7B2%7D%5E%7Bn+1%7D-%5Calpha%20_%7B2%7D%5E%7Bn%7D%29+b%5E%7Bn%7D

同理:

eq?b_%7B1%7D%5E%7Bn+1%7D%3D-E_%7B1%7D%5E%7Bn%7D-y_%7B1%7DX_%7B11%7D%28%5Calpha%20_%7B1%7D%5E%7Bn+1%7D-%5Calpha%20_%7B1%7D%5E%7Bn%7D%29-y_%7B2%7DX_%7B21%7D%28%5Calpha%20_%7B2%7D%5E%7Bn+1%7D-%5Calpha%20_%7B2%7D%5E%7Bn%7D%29+b%5E%7Bn%7D

eq?E_%7Bi%7D的更新:

eq?E_%7Bi%7D%5E%7Bn+1%7D%20%3D%20%5Csum_%7Bi%3D1%7D%5E%7Bn%7Dy_%7Bj%7D%5Calpha%20_%7Bj%7DX_%7Bij%7D+b%5E%7Bn+1%7D-y_%7Bi%7D

在一定精度条件下满足KKT,则停止算法,否则进行循环。算法流程图如下:

90979864e683427796ee5d39417f59af.jpeg

至此我们完成了线性可分情况下的支持向量机的整个流程推导计算流程,有什么问题欢迎交流。

4.2 SMO算法代码实现

        在实际代码实现的过程中,为了方便计算。我们把KKT的条件判断可以改一下,原KKT条件的判断如下:

eq?%5Calpha%20_%7Bi%7D%5Cleft%5C%7B%5Cbegin%7Bmatrix%7D%20%3D0%5Cquad%28wx_%7Bi%7D+b%29y_%7Bi%7D%3E1%5C%5C%20%3E0%20%5Cquad%28wx_%7Bi%7D+b%29y_%7Bi%7D%3D1%5Cend%7Bmatrix%7D%5Cright.

因为eq?f%28x_%7Bi%7D%29%20%3D%20wx_%7Bi%7D%5E%7BT%7D+beq?E_%7Bi%7D%3D%20f%28x_%7Bi%7D%29-y_%7Bi%7D,所以eq?%28wx_%7Bi%7D+b%29y_%7Bi%7D%3Df%28x_%7Bi%7D%29y_%7Bi%7D%3DE_%7Bi%7Dy_%7Bi%7D+1

改后如下:

eq?%5Calpha%20_%7Bi%7D%5Cleft%5C%7B%5Cbegin%7Bmatrix%7D%20%3D0%5Cquad%20E_%7Bi%7Dy_%7Bi%7D%3E0%5C%5C%20%3E0%20%5Cquad%20E_%7Bi%7Dy_%7Bi%7D%3D0%5Cend%7Bmatrix%7D%5Cright.

        详细代码如下(作者时间有限,代码可能比较粗糙还请见谅,下面的代码可直接运行出结果),先贴运行结果如下,这是15个样本的列子:

86cd4e08a4ce4f6687774c6792ab4a8c.png

# 作者: wxj233
# 开发时间: 2023/6/12 8:28


import numpy as np
import matplotlib.pyplot as plt


class SMO:
    def __init__(self, X, t):
        """
        初始化
        :param X: 样本,X每一行为一个样本,列为样本维度。列如:
        [[1,2,3],
        [4,5,6]]
        就是一个两个样本,每个样本有三个维度的数据集。
        :param t: 样本标签,为一维数组,长度应当和样本数量一致。
        :return:
        """
        assert X.shape[0] == t.size, "样本维度与标签维度不一致"
        self.X = X
        self.t = t
        self.a = np.zeros_like(t)  # 初始化α为0,因为没有α这个字母,所以用a代替
        self.b = 0  # 初始化b为0
        self.E = -t
        self.d = 1e-3  # KKT条件的精度范围,因为没有ε,就用字母d代替
        self.w = np.zeros(X.shape[1])

        self.error = np.zeros_like(t)  # 用于记录违背KKT条件的情况

    def KKT_error(self):
        """
        计算精度范围内的KKT的违背情况,没有违背则对应的值为0
        :return:
        """
        for i in range(self.a.size):
            if (self.a[i] == 0 and self.E[i] * self.t[i] > 0) or \
                    (self.a[i] > 0 and np.abs(self.E[i] * self.t[i]) < self.d):  # 精度范围内满足KKT条件
                self.error[i] = 0
            else:
                self.error[i] = self.E[i] * self.t[i]

        return self.error

    def cal_b(self, i_a1, i_a2, a1_new, a2_new, E, a, b):
        """
        计算新的b值
        :param i_a1: 第一个优化值的索引
        :param i_a2: 第二个优化值的索引
        :param a1_new: 第一个拉格朗日乘子的新值
        :param a2_new: 第二个拉格朗日乘子的新值
        :param E: 列表E
        :param a: α列表
        :param b: 阈值
        :return: 新的b
        """
        b1 = -E[i_a1] - self.t[i_a1] * np.dot(self.X[i_a1], self.X[i_a1])*(a1_new - a[i_a1]) \
             - self.t[i_a2] * np.dot(self.X[i_a2], self.X[i_a1])*(a2_new - a[i_a2]) + b
        b2 = -E[i_a2] - self.t[i_a1] * np.dot(self.X[i_a1], self.X[i_a2])*(a1_new - a[i_a1]) \
             - self.t[i_a2] * np.dot(self.X[i_a2], self.X[i_a2])*(a2_new - a[i_a2]) + b
        if a1_new > 0:
            b = b1
            return b
        if a2_new > 0:
            b = b2
            return b
        return (b1 + b2) / 2

    def cal_Ei(self, i_a1, i_a2, a, b):
        """
        计算E1和E2
        :param i_a1: 第一个优化值的索引
        :param i_a2: 第二个优化值的索引
        :param a: α列表
        :param b: 阈值
        :return: E1, E2
        """
        a = a.reshape(-1, 1)
        t = self.t.reshape(-1, 1)
        E1 = np.dot(self.X[i_a1], np.sum(self.X * a * t, axis=0)) + b - t[i_a1]
        E2 = np.dot(self.X[i_a2], np.sum(self.X * a * t, axis=0)) + b - t[i_a2]
        return E1, E2

    def update_w(self):
        """
        更新w
        :return:
        """
        a = self.a.reshape(-1, 1)
        t = self.t.reshape(-1, 1)
        self.w = np.sum(self.X * a * t, axis=0)

    def update_E(self):
        """
        更新列表E
        :return:
        """
        self.update_w()
        self.E = np.dot(self.X, self.w) + self.b - self.t

    def calculate_loss(self, i_a1, i_a2):
        """
        计算拉格朗日乘子对应的损失值,同时会顺带一起返回新的a, b, E, w
        :param i_a1: 第一个优化值的索引
        :param i_a2: 第二个优化值的索引
        :return: loss, a, b, E, w
        """
        b = self.b
        a = np.copy(self.a)
        E = np.copy(self.E)
        t = self.t

        e = np.dot(self.X[i_a1], self.X[i_a1]) + np.dot(self.X[i_a2], self.X[i_a2]) - 2*np.dot(self.X[i_a1], self.X[i_a2])
        while True:
            a2_new = a[i_a2] + t[i_a2] * (E[i_a1] - E[i_a2]) / e

            if t[i_a1] == t[i_a2]:  # 裁剪
                if a2_new < 0 or a2_new > a[i_a1] + a[i_a2]:  # 碰到边界,说明被裁剪了,计算可以停止了。
                    a2_new = np.median(np.array([0, a2_new, a[i_a1] + a[i_a2]]))
                    a1_new = a[i_a1] + t[i_a1] * t[i_a2] * (a[i_a2] - a2_new)
                    b = self.cal_b(i_a1, i_a2, a1_new, a2_new, E, a, b)  # 更新b
                    a[i_a1] = a1_new
                    a[i_a2] = a2_new
                    E[i_a1], E[i_a2] = self.cal_Ei(i_a1, i_a2, a, b)  # 更新E1,E2
                    break
            else:
                if a2_new < 0 or a2_new < a[i_a2] - a[i_a1]:  # 碰到边界,说明被裁剪了,计算可以停止了。
                    a2_new = np.max(np.array([0, a[i_a2] - a[i_a1], a2_new]))
                    a1_new = a[i_a1] + t[i_a1] * t[i_a2] * (a[i_a2] - a2_new)
                    b = self.cal_b(i_a1, i_a2, a1_new, a2_new, E, a, b)  # 更新b
                    a[i_a1] = a1_new
                    a[i_a2] = a2_new
                    E[i_a1], E[i_a2] = self.cal_Ei(i_a1, i_a2, a, b)  # 更新E1,E2
                    break

            if np.abs(a2_new - a[i_a2]) < 1e-5:  # 满足计算精度也可以停止计算了
                a1_new = a[i_a1] + t[i_a1] * t[i_a2] * (a[i_a2] - a2_new)
                b = self.cal_b(i_a1, i_a2, a1_new, a2_new, E, a, b)  # 更新b
                a[i_a1] = a1_new
                a[i_a2] = a2_new
                E[i_a1], E[i_a2] = self.cal_Ei(i_a1, i_a2, a, b)  # 更新E1,E2
                break

            a1_new = a[i_a1] + t[i_a1] * t[i_a2]*(a[i_a2] - a2_new)
            b = self.cal_b(i_a1, i_a2, a1_new, a2_new, E, a, b)  # 更新b
            a[i_a1] = a1_new
            a[i_a2] = a2_new
            E[i_a1], E[i_a2] = self.cal_Ei(i_a1, i_a2, a, b)  # 更新E1,E2

        a1 = a.reshape(-1, 1)
        t1 = t.reshape(-1, 1)
        w = np.sum(self.X * a1 * t1, axis=0)
        loss = 1/2*np.dot(w, w)-np.sum(a1)
        return a, b, E, w, loss


    def plotW(self, *point):
        """
        绘图
        :param point: 选择的点
        :return:
        """
        fig, ax = plt.subplots()  # 绘图
        # 绘制样本点
        samples_x_r = []
        samples_y_r = []
        samples_x_b = []
        samples_y_b = []
        for i in range(self.t.size):
            if self.t[i] == 1:
                samples_x_r.append(self.X[i, 0])
                samples_y_r.append(self.X[i, 1])
            elif self.t[i] == -1:
                samples_x_b.append(self.X[i, 0])
                samples_y_b.append(self.X[i, 1])
        ax.scatter(samples_x_r, samples_y_r, color="r", label="red")
        ax.scatter(samples_x_b, samples_y_b, color="b", label="blue")
        if len(point) == 2:
            i_a1 = point[0]
            i_a2 = point[1]
            ax.scatter(X[i_a1, 0], X[i_a1, 1], color="g", alpha=1, marker="*", label="point1")
            ax.scatter(X[i_a2, 0], X[i_a2, 1], color="y", alpha=1, marker="*", label="point2")

        x = np.arange(-10, 10, 0.01)  # 规定点间隔,依据间隔来画图
        y = (-self.w[0] * x - self.b) / self.w[1]
        ax.plot(x, y, label="wx+b=0")
        y1 = (1-self.w[0] * x - self.b) / self.w[1]
        ax.plot(x, y1, label="wx+b=1")
        y2 = (-1 - self.w[0] * x - self.b) / self.w[1]
        ax.plot(x, y2, label="wx+b=-1")

        ax.set_title("SVM-SMO")
        ax.legend(loc='upper left', bbox_to_anchor=(0.5, 1))  # 图列左上方位置相对于坐标的位置
        ax.set_xlabel('x')
        ax.set_ylabel('y')
        plt.show()

    def getI_a1(self, i_a2):
        """
        按照与E[i_a2]违背程度排序,越违背的排越前面
        :param i_a2: 第一个选点
        :return: 第二个选点的一个列表(这个列表按违背程度排序)
        """
        NKKT_list = []  # 不满足KKT条件的索引列表
        KKT_list = []  # 满足KKT条件的索引列表
        list = []
        if self.a[i_a2] == 0:
            if self.t[i_a2] == -1:
                E = self.E
                min_max_list = np.argsort(E)  # 从小到大排序,选出(E1 - E2)差值最大的点,就是E2尽可能的小
                for i in range(E.size):
                    if min_max_list[i] != i_a2:  # 确保i_a1 != i_a2
                        list.append(min_max_list[i])
                        if self.error[min_max_list[i]] != 0:
                            NKKT_list.append(min_max_list[i])
                        else:
                            KKT_list.append(min_max_list[i])
                NKKT_list.extend(KKT_list)
                return NKKT_list, list
            else:
                E = self.E
                min_max_list = np.argsort(E)  # 从小到大排序,选出(E1 - E2)差值最小的点,就是E2尽可能的大
                for i in range(E.size):
                    if min_max_list[-(i+1)] != i_a2:  # 确保i_a1 != i_a2
                        list.append(min_max_list[-(i+1)])
                        if self.error[min_max_list[-(i+1)]] != 0:
                            NKKT_list.append(min_max_list[-(i+1)])
                        else:
                            KKT_list.append(min_max_list[-(i+1)])
                NKKT_list.extend(KKT_list)
                return NKKT_list, list
        else:
            if self.E[i_a2] > 0:
                E = self.E
                min_max_list = np.argsort(E)  # 从小到大排序,选出(E1 - E2)差值最大的点,就是E2尽可能的小
                for i in range(E.size):
                    if min_max_list[i] != i_a2:  # 确保i_a1 != i_a2
                        list.append(min_max_list[i])
                        if self.error[min_max_list[i]] != 0:
                            NKKT_list.append(min_max_list[i])
                        else:
                            KKT_list.append(min_max_list[i])
                NKKT_list.extend(KKT_list)
                return NKKT_list, list
            else:
                E = self.E
                min_max_list = np.argsort(E)  # 从小到大排序,选出(E1 - E2)差值最小的点,就是E2尽可能的大
                for i in range(E.size):
                    if min_max_list[-(i+1)] != i_a2:  # 确保i_a1 != i_a2
                        list.append(min_max_list[-(i+1)])
                        if self.error[min_max_list[-(i+1)]] != 0:
                            NKKT_list.append(min_max_list[-(i+1)])
                        else:
                            KKT_list.append(min_max_list[-(i+1)])
                NKKT_list.extend(KKT_list)
                return NKKT_list, list

    def loss(self):
        """
        计算当前损失值
        :return:
        """
        return 1/2*np.dot(self.w, self.w)-np.sum(self.a)

    def train(self):
        """
        训练函数
        :return:
        """
        self.update_E()  # 更新列表E,计算一次w
        self.KKT_error()
        last_loss = self.loss()

        loss_y = [last_loss]
        while np.max(np.abs(self.error)) > 0:  # 如果所有点都满足KKT条件了(即误差全部为0),那么说明达到最优训练结束,循环停止。
            issatloss = False  # 是否还能够产生足够的下降
            min_max_sort = np.argsort(np.abs(self.error))
            isValid = False
            for s in range(1, min_max_sort.size+1):
                if isValid:
                    break
                i_a2 = min_max_sort[-s]
                if self.error[i_a2] == 0:  # 说明已经无法满足下降了,因为前面违背KKT条件的点都已经试过了
                    break
                NKKT_list, list = self.getI_a1(i_a2)
                # for i_a1 in NKKT_list:
                for i_a1 in list:
                    a, b, E, w, loss = self.calculate_loss(i_a1, i_a2)
                    if -(loss - last_loss) > 1e-4:
                        # self.plotW(i_a1, i_a2)
                        self.a = a
                        self.b = b
                        self.E = E
                        self.w = w
                        last_loss = loss
                        loss_y.append(last_loss)
                        isValid = True
                        issatloss = True
                        # self.plotW(i_a1, i_a2)
                        break

            if not issatloss:
                print("无法产生足够的下降")
                break
            self.update_E()  # 更新列表E
            self.KKT_error()  # 重新计算KKT的违背情况

        # np.save("NKKT_loss", np.array(loss_y))
        # np.save("list_loss", np.array(loss_y))
        print("α", self.a)
        print("error", np.abs(self.error))
        print(self.loss())
        self.plotW()


if __name__ == '__main__':
    np.random.seed(1)
    X = np.random.rand(15, 2) * 20 - 10  # 产生随机样本
    t = np.sign(X.sum(axis=1))  # 为样本打标记

    smo = SMO(X, t)
    smo.train()

4.3 编写程序的过程中的一点点收获

4.3.1 关于选点与优化结果

        下面我用几幅图来表名选点及其优化效果(左侧为在当前情况下的选点,右侧为优化后的结果):

a7d22b8b0a4f47b28459cb290dc35407.pngaedb28930e9f4db3ad4f65a3d21baf97.png

 06677237184145d3928964abe2e9efd9.pngb2a7fb5e4c3349ddb83e7f12ae3da09a.png

 d4200f57b8b744478121149460978e49.png587820db562e4348899c5e69ef3e62c0.png

 4a0531c23eb642a482a560eefa5422e9.pngbf558db49b294348ad467ec66d9cf31f.png

84a04be74b2f40c79e6eeb0fccd69c2b.pngfe6c9896e8e54b2d96f62d74d197656e.png

 b90d090e9caa4db9982e15e0454d40d8.pngccd317c0f8104bb590249af4cd1e51dd.png

        从结果可以看出,如果选择的两个优化点不是同一类,则在优化后这两个点都会变成支撑向量,如果两个点是同一类,则其中至少一个(这一个是靠近另一个分类的)会变成支撑向量。

4.3.2 不同的选点方式带来的损失值变化的不同

        我在选择第二个点的时候用了两种方式,第一种就是按照eq?E_%7B1%7D-E_%7B2%7D差足够大来选的,我给这种选点方式取名“list_loss”,第二种是优先选在满足违背KKT条件的然后选eq?E_%7B1%7D-E_%7B2%7D差足够大的,我给这种方式取名"NKKT_loss",两种选点方式下的loss值变化如下图(这是20个样本的列子)。

5737f1448bca4370906882809c825dda.png

         从上图可知list_loss的下降速度显然更快,400多次迭代就可得到解,而NKKT_loss需要700次的迭代才能达到同样的效果,这个内容不重要啊,这是我自己随便尝试的,为了加深理解而已。

4.4 参考资料

文档资料:

Sequential Minimal Optimization : A Fast Algorithm for Training Support Vector Machines-学术范

Fast Training of Support Vector Machines Using Sequential Minimal Optimization - 百度学术

[1]Osuna, E.;Freund, R.;Institute of Electric and Electronic Engineer.An improved training algorithm for support vector machines[A].Neural Networks for Signal Processing VII. Proceedings of the 1997 IEEE Signal Processing Society Workshop[C],1997

李庆扬《数值分析》第五版第6章——解线性方程组的迭代法

支持向量机原理详解(六): 序列最小最优化(SMO)算法(Part I) - 知乎

支持向量机原理详解(七): 序列最小最优化(SMO)算法(Part II) - 知乎

 

五、线性不可分情形

        作者最近没时间写,空了再说。

六、核函数

        作者最近没时间写,空了再说。

 

        以上内容花费了我将近一个月时间来理解吸收,作者也是一点点的理解,有不足之处敬请批评指正。

 

 

 

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

楼兰小石头

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值