历经一个月,终于搞定了SVM(支持向量机)-附源代码解析

前言

其实整体算下来,断断续续的也得有快两个月了(原谅博主比较笨)。中间也有好几次放弃,不想写这篇总结了,但是之前立下的誓言,要将学习到的每一个机器学习算法写成博客总结,一方面呢,检验自己是否真的明白了,另一方面,也希望自己的理解能够帮助到一些人。
源码已上传到GitHub上,有需要的小伙伴自取:源码地址,如果有帮到你,不要吝啬你的小⭐⭐

随便唠唠

1.支持向量机名字由来

因为这个算法的关键点就是支持向量,那么,什么叫支持向量呢? 就是离分割超平面(在二维中叫做分割线)最近的那些点。

看上面这个图,中间的实线就是分割超平面,两个虚线所经过的点就是支持向量。这时候大家可能又有疑问了,图b 和图c有什么区别呢? 我们可以看到,图b分割虚线和实线之间的距离比较大,而图c分割虚线和实线之间的距离比较小。SVM要找的就是使间隔最大。

2. 为什么很多书籍和老师都说支持向量机比较难呢?

原理我们从面上很好理解,就是找到使支持向量与分割超平面距离最大的实线即可。但是具体的数学推导,就需要大量的数学方面的知识,比如拉格朗日函数、凸优化、KKT条件、对偶函数。这些知识本身不算太难,但是难点就在于你能不能完整的从头到尾将这个思路理下来,这里我指的是用笔把公式推导下来。我认为还是挺难的,最起码对我来说。之前跟着博客大佬推了一遍公式,两天没看,第三天就立刻忘记了推到思路了,以及里面的一些步骤为什么那么做。

网络上有很多真大佬,写的非常详细,推导过程,以及使用的理论讲的非常清楚,但是,我看完之后,我觉得,这种方式对我这种水平比较低的人来说,不太友好,来来回回看了不下五遍,愣是没有形成一个完整的思路。

所以,在这篇文章中,我想以我的角度,去给有同样困惑的人带来一些帮助。这篇文章不会给你解释数学原理,但是我会在文末给你提供相应知识的链接。

3. 读懂源代码对我们的理解帮助非常巨大

我之前在学习K-mean算法和逻辑回归的时候,认为只要懂算法思想就可以了,并且这些算法思想本身也不复杂。

但是在我学习SVM的时候,我发现,源代码对我的帮助太大了。我当时遇到的问题是,我能够将公式推导完毕,但是不知道如何将数学公式理论和实际代码工程相结合,我相信这不仅是我薄弱的地方,也是许多新人薄弱的地方。也许文字对于算法思路的描述能够让我们感受到原来如此,那么,源代码对于算法思路的描述,能够让我们恍然大悟。这篇文章,我就主要带大家关联源代码和数学公式。

4. 学习机器学习算法是不是只明白思想就行,需不需要动手去写代码实现一下?

很多人跟我的答案肯定是一样的,那就是需要去实现一下。

原因大概有两点:其一,我们需要更加深入的了解算法原理。我们学习算法的目的我们得搞清楚,也许我们是做工程,需要了解大量得算法,在具体项目中选择合适的算法;也许我们是做研究,需要了解算法思想以及其优缺点,对算法进行二次开发改进;再或许我们只是为了了解思想,利用这个思想去思考这个世界,无论你属于哪一个,都需要对算法有一个更加深入的了解。现在有很多现成的机器学习工具包,使用起来非常方便,如果你对实现细节不清楚的话,对于特殊问题,就无法根据现有的算法接口进行定制化修改。

其二,提高我们的工程能力。所谓的工程能力,就是从原来一个想法,到最终实现的一个过程。有人说,我是搞研究的,只需要明白原理,不用写太多。那你告诉我,为什么公司招的都是算法工程师呢?即便是你去研究所工作,没有一定的工程能力,也是会有很大劣势的。

支持向量机公式推导

一、推导

我们单独拿出来图b

我们前面也说过了,我们需要找的就是使d值最大的的分割超平面。

首先超平面公式[见参1]:

w 为法向量,b为截距,法向量指向的一侧为正类,另一侧为负类。

分类决策函数公式:


函数间隔公式[见参2]:


函数间隔体现的是分类的正确性,如果h>0,表示分类正确,如果h<0,表示分类错误。同时也体现着数据点到超平面的远近。

几何间隔公式[见参2]:


几何间距体现的点到超平面的距离,这里和图中支持向量到超平面距离d的一样的。

我们同时将w和b等比例变化,超平面并没有发生改变。比如3w和3b。此时函数间隔也会相应的扩大3倍,但是对于几何间隔来讲,并没有发生改变。

我们要获得最大间隔,就需要几何间距达到最大:

前面我们提到,等比例变换w和b,超平面并不会发生改变,但是函数间隔会发生改变。此时,我们令函数间隔h=1,此时公式变为:


等价于:


等价的原因是求解这两个式子的目标都是一样的。

2. 推导

在推导1中,我们推导出了需要求解的公式,除了公式,它还有一些限定条件:


这里限定条件为函数间隔要≥1.原因是当函数间隔=1时,当前的数据点是距离超平面最近的点,这些点就是支持向量。所以,我们的数据点要满足,所有点的函数距离要≥1,也就是我们前面给定的1.。

如何把条件和公式结合在一块呢?这里用到了拉格朗日函数。我们使用拉格朗日乘子将条件和公式写到一起:

λ为拉格朗日乘子,λ≥0.

这里拉格朗日函数有一个性质,就是对偶性,根据对偶性,原始问题L的对偶问题为其极大极小问题

即 :


接下来我们对

进行计算:

将计算结果带入到式子中:


接下来计算

我们上面的计算先放在这里,我们先说一下KKT条件

前面我们使用了对偶函数来求解问题的解,但是原问题和对偶问题需要满足一定的条件才能等价,这个条件,就叫做KKT条件

上述公式用在我们这里应该是这样的:

我们将上面的式子整理一下:


接着我们重写之前的分类决策公式**【源代码公式1】**:

3.推导-引入松弛变量

为什么要引入松弛变量?

答:很多数据集都是线性不可分的数据集,这意味着我们无法通过一个线性分类器(超平面)去实现数据的分类。同时,也就不能满足函数间隔大于等于1的约束条件(这个条件的前提数据是线性可分的),于是我们引入一个变量,这个变量就叫所松弛变量,去解决这个问题,具体解决办法看下面的公式。

我们令松弛变量为ε。

我们的公式约束函数和公式可修改为:



这里C称之为惩罚参数,当C的值很大时,对误分类的惩罚就越大。

然后改进后的拉格朗日函数是:


紧接着我们进行极大极小对偶问题处理:


最终化简形式为:

4. 推导3-SMO算法

SMO算法是现在运行速度非常快的强大算法,用于训练SVM。

它的主要思路:

  1. 每次循环中选择两个λ进行优化处理,一旦找到一对满足条件的λ,那么就增大其中一个同时减小另外一个。
  2. 这里的条件指的是 两个λ在间隔边界之外,而第二个条件是两个λ没有进行过处理。

根据条件,我们可写出下面的式子:


当y1≠y2,λ1+λ2=d

当y1=y2,λ1-λ2=d

由此我们可以先画出一个图:

由于我们不知道d的取值,所以,有我们画出了两条线,分别对应着d>0和d<0的情况。

我们设初始可行解为
,,最优解为


我们需要对

进行条件约束,根据不等式条件和图中两种不同的等式条件。

我们设

,由此可得出以下两种范围**【源代码公式2】**:

可能大家不理解为什么能写成这样,下面我用一张图来解释一下:

图中我们能够看到,由于我们不知道d的值的正负,所以会出现图中的情况。

对于y1≠y2:

三个绿圈是我们可以取的值,但是为了满足这两种情况和不等式条件的要求,我们取这两种情况的交集,所以,针对下限L,我们取绿点中最大的即

对于上限H,我们取灰色点中的最小值,即


对于y1=y2,同理,这里我就不拿图来再次说明了,直接给出结论了:

对于下限L,我们的取值范围是

对于上限H,我们的取值范围是


接下来,又到了公式推导的时候了,大家挺住!

我们前面选出了两个λ,进行修改,那么,对于相对于这两个λ来讲,其他的λ就是常数了,我们可以对下面公式进行二次推导:

接下来,我们根据λ1和λ2的等式关系,消除掉λ1:

为了方便,我们令y1d=a,y1y2=b

此时,

(这个b和前面决策函数式子中得b不是一回事,我的一个失误,所以,将前面得b在后面以b*的形式出现,大家见谅。)

将我们上面得到的关系带入到w公式中:


还记得我们前面的任务吗?

所以,此时我们对


进行求导:

紧接着,我们根据这个等式可以提出来lλ2:

接着,我们先设几个数(你要是问我为什么设,我只能告诉你是半猜的,根据式子的形式)【源代码公式3】

然后,我们对λ2的等式进行带入化简**【源代码公式4】**:


之后呢,我们对它进行修剪,使用我们之前设定了L和H**【源代码公式5】**:


接着,我们根据来求解λ1的新值**【源代码公式6】**:

求出来了λ之后,我们接下来要求解b*。

根据KKT条件,我们能够得出以下结论:


在我们完成两个变量的更新之后,当λ满足第二种条件时,则有**【源代码公式7】**:

由于我们对于λ满足条件也是假设,所以对于b也会进行一些处理**【源代码公式8】**:

好了,到这里,整个公式全部推导完毕,终于完事了。如果大家有什么问题,欢迎博客底下留言。

支持向量机源代码分析

我们先来看一张源代码输出得图片:

中间蓝线就是分割超平面,图中带有红圈得是支持向量,也就是距离超平面最近得数据点。

注:代码中使用得alphas来替代λ

1. 随机选择另一个alphas

def selectJrand(i, m):
    j = i                                 #选择一个不等于i的j
    while (j == i):
        j = int(random.uniform(0, m))
    return j

这里我们通过随机数得方式,选取一个与i不相等得数。

2. 修剪λ

def clipAlpha(aj,H,L):
    if aj > H:
        aj = H
    if L > aj:
        aj = L
    return aj

3. 主函数

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)# m=100,n=2
    #初始化alpha参数,设为0
    alphas = np.mat(np.zeros((m,1)))# 100个alpha
    #初始化迭代次数
    iter_num = 0
    #最多迭代matIter次
    while (iter_num < maxIter):
        alphaPairsChanged = 0 #用于记录alpha是否已经进行优化
        for i in range(m):
            #步骤1:计算误差Ei
            #此公式为分类决策公式
            fXi = float(np.multiply(alphas,labelMat).T*(dataMatrix*dataMatrix[i,:].T)) + b
            #print("fXi:",fXi)
            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 = dataMatrix[i,:]*dataMatrix[i,:].T +dataMatrix[j,:]*dataMatrix[j,:].T-2.0 * dataMatrix[i,:]*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)
                #abs 函数  返回数字的绝对值
                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

看着特别长,没关系,咱们慢慢说:

  1. 方法参数分别为数据集、类别标签、常数C、容错率、最大循环数
  2. 首先我们初始化alphas和b。
  3. 接下来一个while循环,循环最大次数就是参数maxIter
  4. 然后我们使用一个变量alphaPairsChanged来控制alphas是否进行优化
  5. fXi为分类决策公式,对应得公式为【源代码公式1】
  6. 接着我们求解误差E,对应公式为【源代码公式2】
  7. 通过我们设定得条件,误差值得绝对值是否大于我们参数中得容错率和alpha的取值
  8. 随机选择一个不等于i的数j
  9. 接着往下,通过if语句,来对下限L和上限H进行赋值
  10. 计算eta,也就是我们公式中的η,对应的是公式为【源代码公式3】
  11. 更新alpha[ j ],对应的公式为【源代码公式4】
  12. 紧接着对alpha[ j ]进行修剪,对应【源代码公式5】
  13. 更新alpha[ i ]的值,对应公式为【源代码公式6】
  14. 更新b1、b2的值,对应公式为【源代码公式7】
  15. 根据b1、b2更新b,对应公式为【源代码公式8】

基本上到这里就全部结束了,非常感谢大家能够耐心的看完,如果有需要完整源代码,请在博客底下留言。第一次写这么长的文章,如哪里有不合适的地方,欢迎大家批评指正。

参考资料:

  1. 超平面公式推导
  2. 《统计学习方法 第二版》 p133
  3. 支持向量机原理篇之手撕线性SVM
  4. 如何理解超平面?
  5. 《机器学习实战》

更多推荐

历经一个月,终于搞定了SVM(支持向量机)-附源代码解析