转载:
本文为转载 @ Jack-Cui 的文章,有些地方按照个人理解进行了修改,原文的话可以去博主的专栏查看。
说来惭愧,断更快半个月了,本打算是一周一篇的。感觉SVM瞬间难了不少,推导耗费了很多时间,同时身边的事情也不少,忙了许久。本篇文章参考了诸多大牛的文章写成的,对于什么是SVM做出了生动的阐述,同时也进行了线性SVM的理论推导,以及最后的编程实践,公式较多,还需静下心来一点一点推导。
本文出现的所有代码,均可在作者的github上下载。
SVM的英文全称是Support Vector Machines,我们叫它支持向量机。支持向量机是我们用于分类的一种算法。让我们以一个小故事的形式,开启我们的SVM之旅吧。
在很久以前的情人节,一位大侠要去救他的爱人,但天空中的魔鬼和他玩了一个游戏。
魔鬼在桌子上似乎有规律放了两种颜色的球,说:“你用一根棍分开它们?要求:尽量在放更多球之后,仍然适用。”
于是大侠这样放,干的不错?
然后魔鬼,又在桌上放了更多的球,似乎有一个球站错了阵营。显然,大侠需要对棍做出调整。
SVM就是试图把棍放在最佳位置,好让在棍的两边有尽可能大的间隙。这个间隙就是球到棍的距离。
现在好了,即使魔鬼放了更多的球,棍仍然是一个好的分界线。
魔鬼看到大侠已经学会了一个trick(方法、招式),于是魔鬼给了大侠一个新的挑战。
现在,大侠没有棍可以很好帮他分开两种球了,现在怎么办呢?当然像所有武侠片中一样大侠桌子一拍,球飞到空中。然后,凭借大侠的轻功,大侠抓起一张纸,插到了两种球的中间。
现在,从空中的魔鬼的角度看这些球,这些球看起来像是被一条曲线分开了。
再之后,无聊的大人们,把这些球叫做data,把棍子叫做classifier, 找到最大间隙的trick叫做optimization,拍桌子叫做kernelling, 那张纸叫做hyperplane。
更为直观地感受一下吧(需要):https://www.youtube.com/watch?v=3liCbRZPrZA
当一个分类问题,数据是线性可分的,也就是用一根棍就可以将两种小球分开的时候,我们只要将棍的位置放在让小球距离棍的距离最大化的位置即可,寻找这个最大间隔的过程,就叫做最优化。但是,现实往往是很残酷的,一般的数据是线性不可分的,也就是找不到一个棍将两种小球很好的分类。这个时候,我们就需要像大侠一样,将小球拍起,用一张纸代替小棍将小球进行分类。想要让数据飞起,我们需要的东西就是核函数(kernel),用于切分小球的纸,就是超平面。
也许这个时候,你还是似懂非懂,没关系。根据刚才的描述,可以看出,问题是从线性可分延伸到线性不可分的。那么,我们就按照这个思路,进行原理性的剖析。
先看下线性可分的二分类问题。
上图中的(a)是已有的数据,红色和蓝色分别代表两个不同的类别。数据显然是线性可分的,但是将两类数据点分开的直线显然不止一条。上图的(b)和©分别给出了B、C两种不同的分类方案,其中黑色实线为分界线,术语称为 决策面。每个决策面对应了一个线性分类器。虽然从分类结果上看,分类器A和分类器B的效果是相同的。但是他们的性能是有差距的,看下图:
在"决策面"不变的情况下,我又添加了一个红点。可以看到,分类器A依然能很好的分类结果,而分类器B则出现了分类错误。显然分类器A的 决策面 放置的位置优于分类器B的 决策面 放置的位置,SVM算法也是这么认为的,它的依据就是分类器B的分类间隔比分类器C的分类间隔大。这里涉及到第一个SVM独有的概念 分类间隔 。
这里的分类间隔实际上指的是几何间隔,也就是点到超平面的距离。而几何间隔是由函数间隔得到的。(本文里没有详细描述这两者的概念与关系此处可以参考李航的《统计学习方法》中的SVM章节)
在保证决策面方向不变且不会出现错分样本的情况下移动决策面,会在原来的决策面两侧找到两个极限位置(越过该位置就会产生错分现象),如虚线所示。
虚线的位置由决策面的方向和距离原决策面最近的几个样本的位置决定。而这两条平行虚线正中间的分界线就是在保持当前决策面方向不变的前提下的最优决策面。两条虚线之间的垂直距离就是这个最优决策面对应的分类间隔。
显然每一个可能把数据集正确分开的方向都有一个最优决策面(有些方向无论如何移动决策面的位置也不可能将两类样本完全分开),而不同方向的最优决策面的分类间隔通常是不同的,那个具有“最大间隔”的决策面就是SVM要寻找的最优解。而这个真正的最优解对应的两侧虚线所穿过的样本点,就是SVM中的支持样本点,称为支持向量,实际就是样本中距离超平面最近的一些点,这些点在空间中是从原点出发的向量。
求解这个"决策面"的过程,就是最优化。一个最优化问题通常有两个基本的因素:
在线性SVM算法中,目标函数显然就是那个 分类间隔,而优化对象则是决策面。所以要对SVM问题进行数学建模,首先要对上述两个对象(分类间隔和决策面)进行数学描述。按照一般的思维习惯,我们先描述决策面。
数学建模的时候,先在二维空间建模,然后再推广到多维。
我们都知道二维空间下一条直线的方式如下所示:
现在我们做个小小的改变,让原来的 x x x 轴变成 x 1 x_1 x1, y y y轴变成 x 2 x_2 x2。
移项得:
将公式向量化得:
进一步向量化,用 w w w 列向量和 x x x 列向量和标量 γ \gamma γ 进一步向量化:
其中,向量 w w w 和 x x x 分别为:
ω = [ ω 1 , ω 2 ] T , x = [ x 1 , x 2 ] T \bm{\omega}={[{\omega}_1,\ {\omega}_2]}^{\bm{T}},{\ }\bm{x}={[x_1,x_2]}^{\bm{T}} ω=[ω1, ω2]T, x=[x1,x2]T
这里 w 1 = a , w 2 = − 1 w_1=a,w_2=-1 w1=a,w2=−1。我们都知道,最初的那个直线方程 a a a 和 b b b 的几何意义:
现在假设:
ω = [ ω 1 , ω 2 ] T , x = [ x 1 , x 2 ] T \bm{\omega}={[{\omega}_1,\ {\omega}_2]}^{\bm{T}},{\ }\bm{x}={[x_1,x_2]}^{\bm{T}} ω=[ω1, ω2]T, x=[x1,x2]T
不妨设:
在坐标轴上画出直线和向量 w w w:
蓝色的线代表向量 w w w,红色的先代表直线 y y y。我们可以看到向量 w w w 和直线 y y y 的关系为垂直关系。这说明了向量 w w w 也控制这直线的方向,只不过是与这个直线的方向是垂直的。标量 γ \gamma γ 的作用也没有变,依然决定了直线的截距。此时,我们称 w w w 为直线的法向量。
二维空间的直线方程已经推导完成,将其推广到 n n n 维空间,就变成了超平面方程。(一个超平面,在二维空间的例子就是一个直线)但是它的公式没变,依然是:
不同之处在于:
我们已经顺利推导出了决策面方程,它就是我们的超平面方程,之后,我们统称其为超平面方程。
现在,我们依然对于一个二维平面的简单例子进行推导。
我们已经知道间隔的大小实际上就是支持向量对应的样本点到决策面的距离的二倍。那么图中的距离 d d d 我们怎么求?我们高中都学过,点到直线的距离距离公式如下:
公式中的直线方程为 A x + B y + C = 0 Ax+By+C=0 Ax+By+C=0,点 P P P的坐标为 ( x 0 , y 0 ) (x_0,y_0) (x0,y0)。
现在,将直线方程扩展到多维,求得我们现在的超平面方程,对公式进行如下变形:
这个 d d d 就是分类间隔。其中 ∣ ∣ w ∣ ∣ ||w|| ∣∣w∣∣ 表示 w w w 的2-范数, ∣ ∣ w ∣ ∣ = ∑ i = 1 n w i ||w||=\sqrt{\sum^{n}_{i=1}{w_i}} ∣∣w∣∣=∑i=1nwi。比如对于二维平面:
那么,
我们目的是为了找出一个分类效果好的超平面作为分类器。分类器的好坏的评定依据是分类间隔 d d d 的大小,即分类间隔越大,我们认为这个超平面的分类效果越好。此时,求解超平面的问题就变成了求解分类间隔 d d d 最大化的为题。
看起来,我们已经顺利获得了目标函数的数学形式。但是为了求解w的最大值。我们不得不面对如下问题:
上述我们需要面对的问题就是约束条件,也就是说我们优化的变量 d d d 的取值范围受到了限制和约束。事实上约束条件一直是最优化问题里最让人头疼的东西。但既然我们已经知道了这些约束条件确实存在,就不得不用数学语言对他们进行描述。但SVM算法通过一些巧妙的小技巧,将这些约束条件融合到一个不等式里面。
对每个样本点 x i x_i xi 加上一个类别标签 y i y_i yi:
如果我们的超平面方程能够完全正确地对上图的样本点进行分类,就会满足下面的方程:
如果我们要求再高一点,假设决策面正好处于间隔区域的中轴线上,并且相应的支持向量对应的样本点到决策面的距离为 d d d,那么公式进一步写成:
上述公式的解释就是,对于所有分类标签为1的样本点,它们到直线的距离都大于等于 d d d (支持向量上的样本点到超平面的距离)。对于所有分类标签为-1的样本点,它们到直线的距离都小于等于 d d d。公式两边都除以 d d d,就可以得到:
其中,
因为 ∣ ∣ w ∣ ∣ ||w|| ∣∣w∣∣ 和 d d d 都是标量。所上述公式的两个矢量,依然描述一条直线的法向量和截距。
上述两个公式,都是描述一条直线,数学模型代表的意义是一样的。现在,让我们对 w d w_d wd 和 γ d γ_d γd 重新起个名字,就叫它们 w w w 和 γ γ γ。因此,我们就可以说:“对于存在分类间隔的两类样本点,我们一定可以找到一些超平面面,使其对于所有的样本点均满足下面的条件:
上述方程即给出了SVM最优化问题的约束条件。这时候,可能有人会问了,为什么标记为1和-1呢?因为这样标记方便我们将上述方程变成如下形式:
正是因为标签为1和-1,才方便我们将约束条件变成一个约束方程,从而方便我们的计算。
现在整合一下思路,我们已经得到我们的目标函数:
我们的优化目标是是 d d d 最大化。我们已经说过,我们是用支持向量上的样本点求解 d d d 的最大化的问题的。那么支持向量上的样本点有什么特点呢?
所有支持向量上的样本点,都满足如上公式。
现在我们就可以将我们的目标函数进一步化简:
因为,我们只关心支持向量上的点。随后我们求解d的最大化问题变成了||w||的最小化问题。进而 ∣ ∣ w ∣ ∣ ||w|| ∣∣w∣∣ 的最小化问题等效于
为什么要做这样的等效呢?这是为了在进行最优化的过程中对目标函数求导时比较方便,但这绝对不影响最优化问题最后的求解。我们将最终的目标函数和约束条件放在一起进行描述:
这里 n n n 是样本点的总个数,缩写 s . t . s.t. s.t. 表示Subject to,是服从某某条件的意思。上述公式描述的是一个典型的不等式约束条件下的二次型函数优化问题,同时也是 支持向量机的基本数学模型。
我们已经得到支持向量机的基本数学模型,接下来的问题就是如何根据数学模型,求得我们想要的最优解。在学习求解方法之前,我们得知道一点,想用我下面讲述的求解方法有一个前提,就是我们的目标函数必须是凸函数。理解凸函数,我们还要先明确另一个概念,凸集。在凸几何中,凸集(convex set)是在)凸组合下闭合的放射空间的子集。看一幅图可能更容易理解:
左右量图都是一个集合。如果集合中任意2个元素连线上的点也在集合中,那么这个集合就是凸集 。显然,上图中的左图是一个凸集,上图中的右图是一个非凸集。
凸函数的定义也是如此,其几何意义表示为函数任意两点连线上的值大于对应自变量处的函数值。若这里凸集 C C C 即某个区间 L L L ,那么,设函数 f f f 为定义在区间 L L L 上的函数,若对 L L L 上的任意两点 x 1 , x 2 x_1,x_2 x1,x2 和任意的实数 λ ∈ ( 0 , 1 ) λ{\in}(0, 1) λ∈(0,1),总有:
则函数 f f f 称为 L L L 上的凸函数,当且仅当其上镜图(在函数图像上方的点集)为一个凸集。再看一幅图,也许更容易理解:
像上图这样的函数,它整体就是一个非凸函数,我们无法获得全局最优解的,只能获得局部最优解。比如红框内的部分,如果单独拿出来,它就是一个凸函数。对于我们的目标函数:
很显然,它是一个凸函数。所以,可以使用我接下来讲述的方法求取最优解。
通常我们需要求解的最优化问题有如下几类:
对于第(1)类的优化问题,尝尝使用的方法就是费马引理,即使用求取函数 f ( x ) f(x) f(x) 的导数,然后令其为零,可以求得候选最优值,再在这些候选值中验证;如果是凸函数,可以保证是最优解。这也就是我们高中经常使用的求函数的极值的方法。
对于第(2)类的优化问题,常常使用的方法就是拉格朗日乘子法(Lagrange Multiplier) ,即把等式约束 h i ( x ) h_i(x) hi(x) 用一个系数与 f ( x ) f(x) f(x) 写为一个式子,称为拉格朗日函数,而系数称为拉格朗日乘子。通过拉格朗日函数对各个变量求导,令其为零,可以求得候选值集合,然后验证求得最优值。
对于第(3)类的优化问题,常常使用的方法就是KKT条件。同样地,我们把所有的等式、不等式约束与 f ( x ) f(x) f(x) 写为一个式子,也叫拉格朗日函数,系数也称拉格朗日乘子,通过一些条件,可以求出最优值的 必要条件,这个条件称为KKT条件。(关于KKT条件本文后面会详细说明。)
必要条件和充要条件如果不理解,可以看下面这句话:
- A的必要条件就是A可以推出的结论
- A的充分条件就是可以推出A的前提
了解到这些,现在让我们再看一下我们的最优化问题:
现在,我们的这个对优化问题属于哪一类?很显然,它属于第(3)类问题。因为,在学习求解最优化问题之前,我们还要学习两个东西:拉格朗日函数和KKT条件。(此处可以参考链接)
首先,我们先要从宏观的视野上了解一下拉格朗日对偶问题出现的原因和背景。
我们知道我们要求解的是最小化问题,所以一个直观的想法是如果我能够构造一个函数,使得该函数在可行解区域内与原目标函数完全一致,而在可行解区域外的数值非常大,甚至是无穷大,那么这个没有约束条件的新目标函数的优化问题就与原来有约束条件的原始目标函数的优化问题是等价的问题。这就是使用拉格朗日方程的目的,它将约束条件放到目标函数中,从而将有约束优化问题转换为无约束优化问题。
随后,人们又发现,使用拉格朗日获得的函数,使用求导的方法求解依然困难。进而,需要对问题再进行一次转换,即使用一个数学技巧:拉格朗日对偶。
所以,显而易见的是,我们在拉格朗日优化我们的问题这个道路上,需要进行下面二个步骤:
下面,进行第一步:将有约束的原始目标函数转换为无约束的新构造的拉格朗日目标函数
公式变形如下:
其中 α i \alpha{_i} αi 是拉格朗日乘子,其满足 α i ≥ 0 \alpha{_i}{\geq}0 αi≥0 ,是我们构造新目标函数时引入的系数变量(我们自己设置)。(关于 α i ≥ 0 \alpha{_i}{\geq}0 αi≥0 的原因可以参考陈东岳老师:零基础学SVM)
现在我们令:
当样本点不满足约束条件时,即在可行解区域外:
此时,我们将 α i \alpha{_i} αi 设置为正无穷,此时 θ ( w ) θ(w) θ(w) 显然也是正无穷。
当样本点满足约束条件时,即在可行解区域内:
此时,显然 θ ( w ) θ(w) θ(w) 为原目标函数本身。我们将上述两种情况结合一下,就得到了新的目标函数:
此时,再看我们的初衷,就是为了建立一个在可行解区域内与原目标函数相同,在可行解区域外函数值趋近于无穷大的新函数,现在我们做到了。
现在,我们的问题变成了求新目标函数的最小值,即:
这里用 p ∗ p^* p∗ 表示这个问题的最优值,且和最初的问题是等价的。
接下来,我们进行第二步:将不易求解的优化问题转化为易求解的优化
我们看一下我们的新目标函数,先求最大值,再求最小值。这样的话,我们首先就要面对带有需要求解的参数 w w w 和 b b b 的方程,而 α i \alpha{_i} αi又是不等式约束,这个求解过程不好做。所以,我们需要使用拉格朗日函数对偶性,将最小和最大的位置交换一下,这样就变成了:<
交换以后的新问题是原始问题的 对偶问题,这个新问题的最优值用 d ∗ d^* d∗ 来表示。而且 d ∗ ≤ p ∗ d^*{\leq}p^* d∗≤p∗。(关于 d ≤ p ∗ d{\leq}p^* d≤p∗的证明可以参考陈东岳老师:零基础学SVM)
事实上:
我们关心的是 d ∗ = p ∗ d^*{=}p^* d∗=p∗ 的时候,这才是我们要的解。因此我们需要清楚在什么条件才能让 d ∗ = p ∗ d^*{=}p^* d∗=p∗ ,也就是成立强对偶性。
凸优化问题的定义是:求取最小值的目标函数为凸函数的一类优化问题。在原优化问题中目标函数是凸函数我们已经知道,这个优化问题又是求最小值,所以我们的最优化问题就是凸优化问题。
接下来,就是探讨是否满足KKT条件了。
我们已经使用拉格朗日乘子法对我们的目标函数进行了处理,生成了一个新的目标函数 L ( w , b , α ) L(w,b,\alpha) L(w,b,α)。通过一些条件,可以求出最优值的必要条件,这个条件就是接下来要说的KKT条件。一个最优化模型能够表示成下列标准形式:
那么我们的原始问题对应的标准形式为:
其中的约束条件为不等式约束,记为 g ( x ) ≤ 0 g(x){\leq}0 g(x)≤0,对于这种条件,最优解所在的位置 x ∗ x^* x∗ 有两种可能,或者在边界曲线 g ( x ) = 0 g(x)=0 g(x)=0上或者在可行解区域内部满足不等式 g ( x ) < 0 g(x)<0 g(x)<0 的地方。如下图所示:
第一种情况:最优解在边界上,就相当于约束条件就是 g ( x ) = 0 g(x)=0 g(x)=0 。参考上图,注意此时目标函数的最优解在可行解区域外面,所以目标函数在最优解 x ∗ x^* x∗ 附近的变化趋势是“在可行解区域内侧较大而在区域外侧较小”,与之对应的是函数 g ( x ) g(x) g(x) 在可行解区域内小于0,在区域外大于0,所以在最优解 x ∗ x^* x∗ 附近的变化趋势是内部较小而外部较大。这意味着目标函数的梯度方向与约束条件函数 g ( x ) g(x) g(x) 的梯度方向相反。因此根据推论:目标函数 f ( x ) f(x) f(x) 与函数 g ( x ) g(x) g(x) 的等值线在最优解点 x ∗ x^* x∗ 处相切,即两者在 x ∗ x^* x∗ 点的梯度方向相同或相反,可以得到 ∇ f ( x ∗ ) + α g ( x ∗ ) = 0 \nabla{f(x^*)}+{\alpha}g(x^*)=0 ∇f(x∗)+αg(x∗)=0 ,可以推断出参数 α ≥ 0 \alpha{\geq}0 α≥0.
第二种情况:如果在区域内,则相当于约束条件没有起作用,因此拉格朗日函数 L ( w , b , α ) L(w,b,\alpha) L(w,b,α) 中的参数 α = 0 \alpha{=0} α=0 。
整合这两种情况,可以写出一个约束条件的统一表达,即:
上述三个公式就构成了 KKT条件,全称是Karush-Kuhn-Tucker条件。其中第三条称为 双重补足条件,它保证了
因此,KKT条件的意义在于对最优解的约束,而原始问题中的约束条件是对可行解的约束。
那么KKT条件是如何用于保证 d ∗ = p ∗ d^*=p^* d∗=p∗的呢?这里可以参考陈东岳老师:零基础学SVM中关于3.4 4)对偶问题同解的证明。
现在,凸优化问题和KKT都满足了,问题转换成了对偶问题,而求解这个最优化模型可以分为三个步骤:
根据上述推导已知:
首先固定 α α α,要让 L ( w , b , α ) L(w,b,α) L(w,b,α)关于 w w w和 b b b最小化,我们分别对 w w w和 b b b求偏导数,并令其等于0,即:
将上述结果带回 L ( w , b , α ) L(w,b,α) L(w,b,α) 得到:
从上面的最后一个式子,我们可以看出,此时的 L ( w , b , α ) L(w,b,α) L(w,b,α) 函数只含有一个变量,即 α i α_i αi。
现在内侧的最小值求解完成,我们求解外侧的最大值,从上面的式子得到
现在我们的优化问题变成了如上的形式。对于这个问题,我们有更高效的优化算法,即序列最小优化(SMO)算法。我们通过这个优化算法能得到 α α α,再根据 α α α,我们就可以求解出 w w w 和 b b b,进而求得我们最初的目的:找到超平面,即"决策平面"。
总结一句话:我们为啥使出吃奶的劲儿进行推导?因为我们要将最初的原始问题,转换到可以使用SMO算法求解的问题,这是一种最流行的求解方法。为啥用这种求解方法?因为它牛逼啊!
现在,我们已经得到了可以用SMO算法求解的目标函数,但是对于怎么编程实现SMO算法还是感觉无从下手。那么现在就聊聊如何使用SMO算法进行求解。
1996年,John Platt发布了一个称为SMO的强大算法,用于训练SVM。SMO表示序列最小化(Sequential Minimal Optimizaion)。Platt的SMO算法是将大优化问题分解为多个小优化问题来求解的。这些小优化问题往往很容易求解,并且对它们进行顺序求解的结果与将它们作为整体来求解的结果完全一致的。在结果完全相同的同时,SMO算法的求解时间短很多。
SMO算法的目标是求出一系列 α \alpha α 和 b b b,一旦求出了这些 α \alpha α,就很容易计算出权重向量 w w w 并得到分隔超平面。
SMO算法的工作原理是:每次循环中选择两个 α \alpha α 进行优化处理。一旦找到了一对合适的 α \alpha α ,那么就增大其中一个同时减小另一个。这里所谓的"合适"就是指两个 α \alpha α 必须符合以下两个条件:
先来定义特征到结果的输出函数为:
接着,我们回忆一下原始优化问题,如下:
求导得:
将上述公式带入输出函数中:
与此同时,拉格朗日对偶后得到最终的目标化函数:
将目标函数变形,在前面增加一个负号,将最大值问题转换成最小值问题:
实际上,对于上述目标函数,是存在一个假设的,即数据100%线性可分。但是,目前为止,我们知道几乎所有数据都不那么"干净"。这时我们就可以通过引入所谓的松弛变量(slack variable),来允许有些数据点可以处于超平面的错误的一侧。这样我们的优化目标就能保持仍然不变。
但是此时我们的约束条件有所改变:
关于松弛变量与参数C的来源可以参考支持向量机:Outliers (这个网页打开以后会有些卡,需要等待几分钟才能吧公式加载出来。)
这里先放一下自己的笔记,方便以后查阅,字丑勿喷。
不知道 C C C 怎么来的 自己蠢了好久 才翻到上面那个博客 真的蠢哭了 作者都把参考链接摆出来了
根据KKT条件可以得出其中 α i α_i αi 取值的意义为:
而最优解需要满足KKT条件,即上述3个条件都得满足,以下几种情况出现将会不满足:
也就是说,如果存在不能满足KKT条件的 α i α_i αi ,那么需要更新这些 α i α_i αi ,这是第一个约束条件。
此外,更新的同时还要受到第二个约束条件的限制,即:
因为这个条件,我们同时更新两个 α α α 值,因为只有成对更新,才能保证更新之后的值仍然满足和为0的约束,假设我们选择的两个乘子为 α 1 α_1 α1 和 α 2 α_2 α2:
其中, ζ \zeta ζ 为常数。因为两个因子不好同时求解,所以可以先求第二个乘子 α 2 α_2 α2 的解 α 2 n e w {α_2}^{new} α2new,得到 α 2 α_2 α2 的解 α 2 n e w {α_2}^{new} α2new 之后,再用 α 2 α_2 α2的解 α 2 n e w {α_2}^{new} α2new 表示 α 1 α_1 α1 的解 α 1 n e w {α_1}^{new} α1new 。为了求解 α 2 n e w {α_2}^{new} α2new ,得先确定 α 2 n e w {α_2}^{new} α2new 的取值范围。假设它的上下边界分别为 H H H 和 L L L,那么有:
接下来,综合下面两个条件:
如此,根据 y 1 y_1 y1 和 y 2 y_2 y2 异号或同号,可以得出 α 2 n e w {α_2}^{new} α2new 的上下界分别为:
这个界限就是编程的时候需要用到的。已经确定了边界,接下来,就是推导迭代式,用于更新 α α α 值。
我们已经知道,更新 α α α 的边界,接下来就是讨论如何更新α值。我们依然假设选择的两个乘子为 α 1 α_1 α1 和 α 2 α_2 α2 。固定这两个乘子,进行推导。于是目标函数变成了:
为了描述方便,我们定义如下符号:
最终目标函数变为:
其中, c o n s t a n t constant constant 表示的是红色横线画出的部分,它们均与 α 1 α_1 α1 和 α 2 α_2 α2 无关。
因此对于 α 1 α_1 α1 和 α 2 α_2 α2 来说,它们都是常数项,在求导的时候,直接变为0。对于这个目标函数,如果对其求导,还有个未知数 α 1 α_1 α1 ,所以要推导出 α 1 α_1 α1 和 α 2 α_2 α2 的关系,然后用 α 2 α_2 α2 代替 α 1 α_1 α1 ,这样目标函数就剩一个未知数了,我们就可以求导了,推导出迭代公式。所以现在继续推导 α 1 α_1 α1 和 α 2 α_2 α2 的关系。注意第一个约束条件:
我们在求 α 1 α_1 α1 和 α 2 α_2 α2 的时候,可以移项得到 α 1 y 1 + α 2 y 2 = − ∑ i = 3 n α i y i = B {\alpha}_1y_1+{\alpha}_2y_2=-\sum_{i=3}^{n}{{\alpha}_iy_i}=B α1y1+α2y2=−∑i=3nαiyi=B。因此有:
我们不必关心常数 B B B 的大小,现在将上述等式两边同时乘以 y 1 y_1 y1,由于 y 1 × y 1 = 1 y_1{\times}y_1=1 y1×y1=1得到:
其中 γ = B y 1 γ=By_1 γ=By1 为常数, s = y 1 × y 2 s=y_1{\times}y_2 s=y1×y2。接下来,我们将得到的 α 1 {\alpha}_1 α1带入 W ( α 2 ) W(α_2) W(α2) 公式得:
这样目标函数中就只剩下 α 2 α_2 α2 了,我们对其求偏导(注意: s = y 1 y 2 s=y_1y_2 s=y1y2,所以 s 2 = 1 s^2=1 s2=1,且 y 1 2 = 1 y_1{^2}=1 y12=1, y 2 2 = 1 y_2{^2}=1 y22=1):
继续化简,将 s = y 1 × y 2 s=y_1{\times}y_2 s=y1×y2 带入方程。
即:
我们令:
其中 E i E_i Ei 为误差项, η η η 为学习速率。
再根据我们已知的公式:
将α2 new继续化简得:
这样,我们就得到了最终需要的迭代公式:
这个是没有经过剪辑的解,需要考虑约束:
根据之前推导的 α α α 取值范围,我们得到最终的解析解为:
又因为:
消去 γ γ γ 得:
这样,我们就知道了怎样计算 α 1 α_1 α1 和 α 2 α_2 α2 了,也就是如何对选择的 α α α 进行更新。
当我们更新了 α 1 α_1 α1 和 α 2 α_2 α2 之后,需要重新计算阈值 b b b,因为 b b b 关系到了我们 f ( x ) f(x) f(x) 的计算,也就关系到了误差 E i E_i Ei 的计算。
我们要根据 α α α 的取值范围,去更正 b b b 的值,使间隔最大化。
当 α 1 n e w ∈ ( 0 , C ) {\alpha}_1{^{new}}{\in}(0,C) α1new∈(0,C) 时,根据KKT条件可知,这个样本点是支持向量上的点。因此,满足下列公式:
公式两边同时乘以 y 1 y_1 y1 得(不要忘记 w = ∑ i = 1 n a i y i x i T w=\sum_{i=1}^{n}{a_iy_i{x_i}^T} w=∑i=1naiyixiT ):
因为我们是根据 α 1 α_1 α1 和 α 2 α_2 α2 的值去更新 b b b,所以单独提出 i = 1 i=1 i=1 和 i = 2 i=2 i=2 的时候,整理可得(前方高能~字丑哈哈哈哈 ):
同理可得 b 2 n e w b_2^{new} b2new为:
当 b 1 b_1 b1 和 b 2 b_2 b2 都有效的时候,它们是相等的,即:
当两个乘子都在边界上,则 b b b 阈值和KKT条件一致。当不满足的时候,SMO算法选择他们的中点作为新的阈值:
最后,更新所有的 α α α 和 b b b,这样模型就出来了,从而即可求出我们的分类函数。
步骤3:计算 η η η:
步骤4:更新 α j α_j αj:
步骤5:根据取值范围修剪 α j α_j αj:
步骤6:更新 α i α_i αi:
步骤7:更新 b 1 b_1 b1 和 b 2 b_2 b2:
已经梳理完了SMO算法实现步骤,接下来按照这个思路编写代码,进行实战练习。
我们先使用简单的数据集进行测试:数据集下载地址
编写程序可视化数据集,看下它是长什么样的:
# -*- coding:UTF-8 -*-
import matplotlib.pyplot as plt
import numpy as np
"""
函数说明:读取数据
Parameters:
fileName - 文件名
Returns:
dataMat - 数据矩阵
labelMat - 数据标签
Author:
Jack Cui
Blog:
http://blog.csdn.net/c406495762
Zhihu:
https://www.zhihu.com/people/Jack--Cui/
Modify:
2017-09-21
"""
def loadDataSet(fileName):
dataMat = []; labelMat = []
fr = open(fileName)
for line in fr.readlines(): #逐行读取,滤除空格等
lineArr = line.strip().split('\t')
dataMat.append([float(lineArr[0]), float(lineArr[1])]) #添加数据
labelMat.append(float(lineArr[2])) #添加标签
return dataMat,labelMat
"""
函数说明:数据可视化
Parameters:
dataMat - 数据矩阵
labelMat - 数据标签
Returns:
无
Author:
Jack Cui
Blog:
http://blog.csdn.net/c406495762
Zhihu:
https://www.zhihu.com/people/Jack--Cui/
Modify:
2017-09-21
"""
def showDataSet(dataMat, labelMat):
data_plus = [] #正样本
data_minus = [] #负样本
for i in range(len(dataMat)):
if labelMat[i] > 0:
data_plus.append(dataMat[i])
else:
data_minus.append(dataMat[i])
data_plus_np = np.array(data_plus) #转换为numpy矩阵
data_minus_np = np.array(data_minus) #转换为numpy矩阵
plt.scatter(np.transpose(data_plus_np)[0], np.transpose(data_plus_np)[1]) #正样本散点图
plt.scatter(np.transpose(data_minus_np)[0], np.transpose(data_minus_np)[1]) #负样本散点图
plt.show()
if __name__ == '__main__':
dataMat, labelMat = loadDataSet('testSet.txt')
showDataSet(dataMat, labelMat)
运行程序,查看结果:
这就是我们使用的二维数据集,显然线性可分。现在我们使用简化版的SMO算法进行求解。
按照上述已经推导的步骤编写代码:
# -*- coding:UTF-8 -*-
from time import sleep
import matplotlib.pyplot as plt
import numpy as np
import random
import types
"""
函数说明:读取数据
Parameters:
fileName - 文件名
Returns:
dataMat - 数据矩阵
labelMat - 数据标签
Author:
Jack Cui
Blog:
http://blog.csdn.net/c406495762
Zhihu:
https://www.zhihu.com/people/Jack--Cui/
Modify:
2017-09-21
"""
def loadDataSet(fileName):
dataMat = []; labelMat = []
fr = open(fileName)
for line in fr.readlines(): #逐行读取,滤除空格等
lineArr = line.strip().split('\t')
dataMat.append([float(lineArr[0]), float(lineArr[1])]) #添加数据
labelMat.append(float(lineArr[2])) #添加标签
return dataMat,labelMat
"""
函数说明:随机选择alpha
Parameters:
i - alpha
m - alpha参数个数
Returns:
j -
Author:
Jack Cui
Blog:
http://blog.csdn.net/c406495762
Zhihu:
https://www.zhihu.com/people/Jack--Cui/
Modify:
2017-09-21
"""
def selectJrand(i, m):
j = i #选择一个不等于i的j
while (j == i):
j = int(random.uniform(0, m))
return j
"""
函数说明:修剪alpha
Parameters:
aj - alpha值
H - alpha上限
L - alpha下限
Returns:
aj - alpah值
Author:
Jack Cui
Blog:
http://blog.csdn.net/c406495762
Zhihu:
https://www.zhihu.com/people/Jack--Cui/
Modify:
2017-09-21
"""
def clipAlpha(aj,H,L):
if aj > H:
aj = H
if L > aj:
aj = L
return aj
"""
函数说明:简化版SMO算法
Parameters:
dataMatIn - 数据矩阵
classLabels - 数据标签
C - 松弛变量
toler - 容错率
maxIter - 最大迭代次数
Returns:
无
Author:
Jack Cui
Blog:
http://blog.csdn.net/c406495762
Zhihu:
https://www.zhihu.com/people/Jack--Cui/
Modify:
2017-09-23
"""
def smoSimple(dataMatIn, classLabels, C, toler, maxIter):
#转换为numpy的mat存储
dataMatrix = np.mat(dataMatIn); labelMat = np.mat(classLabels).transpose()
#初始化b参数,统计dataMatrix的维度
b = 0; m,n = np.shape(dataMatrix)
#初始化alpha参数,设为0
alphas = np.mat(np.zeros((m,1)))
#初始化迭代次数
iter_num = 0
#最多迭代matIter次
while (iter_num < maxIter):
alphaPairsChanged = 0
for i in range(m):
#步骤1:计算误差Ei
fXi = float(np.multiply(alphas,labelMat).T*(dataMatrix*dataMatrix[i,:].T)) + b
Ei = fXi - float(labelMat[i])
#优化alpha,更设定一定的容错率。
if ((labelMat[i]*Ei < -toler) and (alphas[i] < C)) or ((labelMat[i]*Ei > toler) and (alphas[i] > 0)):
#随机选择另一个与alpha_i成对优化的alpha_j
j = selectJrand(i,m)
#步骤1:计算误差Ej
fXj = float(np.multiply(alphas,labelMat).T*(dataMatrix*dataMatrix[j,:].T)) + b
Ej = fXj - float(labelMat[j])
#保存更新前的aplpha值,使用深拷贝
alphaIold = alphas[i].copy(); alphaJold = alphas[j].copy();
#步骤2:计算上下界L和H
if (labelMat[i] != labelMat[j]):
L = max(0, alphas[j] - alphas[i])
H = min(C, C + alphas[j] - alphas[i])
else:
L = max(0, alphas[j] + alphas[i] - C)
H = min(C, alphas[j] + alphas[i])
if L==H: print("L==H"); continue
#步骤3:计算eta
eta = 2.0 * dataMatrix[i,:]*dataMatrix[j,:].T - dataMatrix[i,:]*dataMatrix[i,:].T - dataMatrix[j,:]*dataMatrix[j,:].T
if eta >= 0: print("eta>=0"); continue
#步骤4:更新alpha_j
alphas[j] -= labelMat[j]*(Ei - Ej)/eta
#步骤5:修剪alpha_j
alphas[j] = clipAlpha(alphas[j],H,L)
if (abs(alphas[j] - alphaJold) < 0.00001): print("alpha_j变化太小"); continue
#步骤6:更新alpha_i
alphas[i] += labelMat[j]*labelMat[i]*(alphaJold - alphas[j])
#步骤7:更新b_1和b_2
b1 = b - Ei- labelMat[i]*(alphas[i]-alphaIold)*dataMatrix[i,:]*dataMatrix[i,:].T - labelMat[j]*(alphas[j]-alphaJold)*dataMatrix[i,:]*dataMatrix[j,:].T
b2 = b - Ej- labelMat[i]*(alphas[i]-alphaIold)*dataMatrix[i,:]*dataMatrix[j,:].T - labelMat[j]*(alphas[j]-alphaJold)*dataMatrix[j,:]*dataMatrix[j,:].T
#步骤8:根据b_1和b_2更新b
if (0 < alphas[i]) and (C > alphas[i]): b = b1
elif (0 < alphas[j]) and (C > alphas[j]): b = b2
else: b = (b1 + b2)/2.0
#统计优化次数
alphaPairsChanged += 1
#打印统计信息
print("第%d次迭代 样本:%d, alpha优化次数:%d" % (iter_num,i,alphaPairsChanged))
#更新迭代次数
if (alphaPairsChanged == 0): iter_num += 1
else: iter_num = 0
print("迭代次数: %d" % iter_num)
return b,alphas
"""
函数说明:分类结果可视化
Parameters:
dataMat - 数据矩阵
w - 直线法向量
b - 直线解决
Returns:
无
Author:
Jack Cui
Blog:
http://blog.csdn.net/c406495762
Zhihu:
https://www.zhihu.com/people/Jack--Cui/
Modify:
2017-09-23
"""
def showClassifer(dataMat, w, b):
#绘制样本点
data_plus = [] #正样本
data_minus = [] #负样本
for i in range(len(dataMat)):
if labelMat[i] > 0:
data_plus.append(dataMat[i])
else:
data_minus.append(dataMat[i])
data_plus_np = np.array(data_plus) #转换为numpy矩阵
data_minus_np = np.array(data_minus) #转换为numpy矩阵
plt.scatter(np.transpose(data_plus_np)[0], np.transpose(data_plus_np)[1], s=30, alpha=0.7) #正样本散点图
plt.scatter(np.transpose(data_minus_np)[0], np.transpose(data_minus_np)[1], s=30, alpha=0.7) #负样本散点图
#绘制直线
x1 = max(dataMat)[0]
x2 = min(dataMat)[0]
a1, a2 = w
b = float(b)
a1 = float(a1[0])
a2 = float(a2[0])
y1, y2 = (-b- a1*x1)/a2, (-b - a1*x2)/a2
plt.plot([x1, x2], [y1, y2])
#找出支持向量点
for i, alpha in enumerate(alphas):
if abs(alpha) > 0:
x, y = dataMat[i]
plt.scatter([x], [y], s=150, c='none', alpha=0.7, linewidth=1.5, edgecolor='red')
plt.show()
"""
函数说明:计算w
Parameters:
dataMat - 数据矩阵
labelMat - 数据标签
alphas - alphas值
Returns:
无
Author:
Jack Cui
Blog:
http://blog.csdn.net/c406495762
Zhihu:
https://www.zhihu.com/people/Jack--Cui/
Modify:
2017-09-23
"""
def get_w(dataMat, labelMat, alphas):
alphas, dataMat, labelMat = np.array(alphas), np.array(dataMat), np.array(labelMat)
w = np.dot((np.tile(labelMat.reshape(1, -1).T, (1, 2)) * dataMat).T, alphas)
return w.tolist()
if __name__ == '__main__':
dataMat, labelMat = loadDataSet('testSet.txt')
b,alphas = smoSimple(dataMat, labelMat, 0.6, 0.001, 40)
w = get_w(dataMat, labelMat, alphas)
showClassifer(dataMat, w, b)
程序运行结果:
其中,中间的蓝线为求出来的分类器,用红圈圈出的点为支持向量点。
文章内容参考:
写在最后,原作者的文笔真的好好啊,逻辑清晰的同时又简单易懂,爱了爱了,表示对作者的超级感谢!!!虽然路漫漫其修远兮,头发日益少兮,但我还是会努力成为一名合格的程序猿……