首页 > 其他分享 >支持向量机学习笔记--实现篇(三)

支持向量机学习笔记--实现篇(三)

时间:2023-07-10 20:32:36浏览次数:45  
标签:alphas Ei -- 笔记 算法 labelMat alpha oS 向量


支持向量机学习笔记(三)

前言

两篇文章阐述了支持向量机的原理,在数学的海洋中遨游了快一周,实在撑不下去了,现在准备亲自来实现一把支持向量机的学习算法,序列最小最优化算法,依然需要数学知识和少量的编程基础。参考的书籍为李航的《统计学习方法》和Peter Harrington的《机器学习实战》,参考的学习算法为LIBSVM,以及一篇求解凸二次规划问题的论文-Sequential Minimal Optimization A Fast Algorithm for Training Support Vector Machines.

SMO算法

SMO算法又称序列最小最优化算法,本文讨论支持向量机学习的实现问题。我们知道,支持向量机的学习问题可以形式化为求解凸二次规划问题。这样的凸二次规划问题具有全局最优解,并且有许多最优化算法可以用于这一问题的求解。但是训练样本容量很大时,这些算法往往会变得非常低效,以致于无法使用。所以,如何高效地实现支持向量机学习就成为一个重要的问题。目前人们已提出许多快速实现算法。本节讲述其中的序列最小最优化算法,这种算法1998年由Platt提出。

问题描述

SMO算法要求解如下凸二次规划的对偶问题:


minα 12∑i=1N∑j=1NαiαjyiyjK(xi⋅xj)−∑i=1Nαi



s.t. ∑i=1Nαiyi=0



0≤αi≤C,i=1,2,...,N


在这个问题中,变量是拉格朗日乘子,一个变量

αi对应于一个样本点 (xi,yi);变量的总数等于训练样本容量N。

SMO算法是一种启发式算法,其基本思路是:如果所有变量的解都满足此最优化问题的KKT条件,那么这个最优化问题的解就都得到了。因为KKT条件就是该最优化问题的充分必要条件。(KKT条件是什么?)否则,选择两个变量,固定其他变量,针对两个变量构建一个二次规划问题。(求解子问题?)这个二次规划问题关于这两个变量的解应该更接近原始二次规划问题的解,因为这会使得原始二次规划问题的目标函数值变得更小。(云里雾里。。。大致含义是,每次针对两个变量来求解目标函数的最小值,求解完后,继续寻找新的变量求目标函数,在每次寻找新αi的过程中,目标函数将进一步得到优化,直到所有的αi更新完毕?有待验证!)重要的是,这时,子问题可以通过解析方法求解,这样就可以大大提高整个算法的计算速度。子问题有两个变量,一个是违反KKT条件最严重的那一个,另一个由约束条件自动确定。如此,SMO算法将原问题不断分解为子问题并对子问题求解,进而达到求解原问题的目的。

注意,子问题的两个变量中只有一个是自由变量。假设α1,α2为两个变量,α3,α4,...αN固定,那么由等式约束可知


α1=−y1∑i=2Nαiyi


两边同时乘以

y1,上述式子符合约束条件。如果 α2确定,那么 α1也随之确定。所以子问题中同时更新两个变量。(两变量同时更新?)根据约束式子,可以得:



s.t. α1y1+α2y2+∑i=3Nyiαi=0


变为:



s.t. α1y1+α2y2=−∑i=3Nyiαi=ζ


因此,当

α1确定后, α2也随之确定了。

整个SMO算法包括两个部分:求解两个变量二次规划的解析方法和选择变量的启发式方法。

两个变量二次规划的求解方法

不失一般性,假设选择的两个变量是α1,α2,其他变量αi(i=3,4,...,N)是固定的。于是SMO的最优化问题的子问题可以写成:


minα1,α2 W(α1,α2)=12K11α21+12K22α22+y1y2K12α1α2−(α1+α2)+y1α1∑i=3NyiαiKi1+y2α2∑i=3NyiαiKi2



s.t. α1y1+α2y2=−∑i=3Nyiαi=ζ



0≤αi≤C


其中,

Kij=K(xi,xj),i,j=1,2,...,N,ζ是常数,目标函数式中省略了不含 α1,α2的常数项。

为了求解两个变量的二次规划问题,首先分析约束条件,然后在此约束条件下求极小。

由于只有两个变量(α1,α2),约束可以用二维空间中的图形表示。如下图所示:

y1≠y2⇒α1−α2=ζ(由约束条件求得)


y1=y2⇒α1+α2=ζ(由约束条件求得)

不等式约束使得(α1,α2)在盒子[0,C]×[0,C]内,等式约束使(α1,α2)在平行于盒子[0,C]×[0,C]的对角线的直线上。因此要求的是目标函数在一条平行于对角线的线段上的最优值。这使得两个变量的最优化问题成为实质上的单变量的最优化问题,不妨考虑为变量α2的最优化问题。

假设问题的初始可行解为αold1,αold2,最优解为αnew1,αnew2,并且假设在沿着约束方向未经剪辑时α2的最优解为αnew,unc2.

由于αnew2需要满足不等式约束,所以最优值αnew2的取值范围必须满足条件:


L≤αnew2≤H


其中,L与H是

αnew2所在对角线段端点的界。如果 y1≠y2,则



L=max(0,αold2−αold1),H=min(C,C+αold2−αold1)


如果

y1=y2,则



L=max(0,αold2+αold1−C),H=min(C,αold2+αold1)

规定完α2的取值范围时,便需要根据某种步长,来不断更新α2,从而使得目标函数最小,并达到一个收敛的状态,如下式:


αnew2=αold2+μ


但这里,

μ并非简单的就等于某个实数,在实际算法过程中, μ=y2(E1−E2)η,且



g(x)=∑i=1NαiyiK(xi,x)+b



Ei=g(xi)−yi=(∑j=1NαjyjK(xj,xi)+b)−yi,i=1,2



η=K11+K22−2K12=∥Φ(x1)−Φ(x2)∥2


经过剪辑后

α2的解是:



αnew2=⎧⎩⎨⎪⎪H,αnew,unc2>Hαnew,unc2,L≤αnew,unc2≤HL,αnew,unc2<L


α2并非是随便的更新,每次更新都需要满足它自身的约束条件,未被剪辑的意思是就是根据步长更新完的

α2不能作为更新值,还需要进行一轮剪辑(范围删选)后,才算是最新的 αnew2的值,剪辑条件满足上述函数。

为何α2的更新步长为μ式呢?这需要理论证明一把!
证明:引进记号


vi=∑j=3NαjyjK(xi,xj)=g(xi)−∑j=12αjyjK(xi,xj)−b,i=1,2


目标函数可写成



W(α1,α2)=12K11α21+12K22α22+y1y2K12α1α2−(α1+α2)+y1v1α1+y2v2α2



α1y1=ζ−α2y2及y2i=1,可将 α1表示为



α1=(ζ−y2α2)y1


代入目标函数,得到只是

α2的函数的目标函数:



W(α2)=12K11(ζ−α2y2)2+12K22α22+y1y2K12(ζ−α2y2)α2−(ζ−α2y2)−α2+y1v1(ζ−α2y2)+y2v2α2



α2求导数



∂W∂α2=K11α2+K22α2−2K12α2−K11ζy2+K12ζy2+y1y2−1−v1y2+y2v2


令其为0,得到



(K11+K22−2K12)α2=y2(y2−y1+ζK11−ζK12+v1−v2)



v1,v2,ζ=αold1y1+αold2y2代入,得



(K11+K22−2K12)αnew,unc2=(K11+K22−2K12)αold2+y2(E1−E2)



η=K11+K22−2K12,于是得到



αnew,unc2=αold2+y2(E1−E2)η


目标函数,经过化简后,只得到了关于

α2的函数,因此对目标函数直接求偏导,就能找到该函数的极值。但在目标函数中,还关联了一项 g(x)函数,而 g(x)函数又是关于 α2的函数,

因此从理论上来说,αnew,unc2是否属于慢慢趋近于某个稳定值的过程?

疑问:α2如何从一个初始值,是慢慢逼近正确值,还是一次逼近正确值?

变量的选择方法

SMO算法在每个子问题中选择两个变量优化,其中至少一个变量是违法KKT条件的。

1.第一个变量的选择

SMO称选择第1个变量的过程为外层循环。外层循环在训练样本中选取违反KKT条件最严重的样本点,并将其对应的变量做为第1个变量。具体地,检验训练样本点(xi,yi)是否满足KKT条件,即


αi=0⇔yig(xi)≥1



0≤αi≤C⇔yig(xi)=1



αi=C⇔yig(xi)≤1


其中,

g(xi)=∑Nj=1αjyjK(xi,xj)+b

该检验是在ϵ范围内进行的。在检验过程中,外层循环首先遍历所有满足条件0≤αi≤C的样本点,即在间隔边界上的支持向量点,检验它们是否满足KKT条件。如果这些样本点都满足KKT条件,那么遍历整个训练集,检验它们是否满足KKT条件。

2.第2个变量的选择

SMO称选择第2个变量的过程为内层循环。假设在外层循环中已经找到第1个变量α1,现在要在内层循环中找第2个变量α2。第2个变量选择的标准是希望能使α2有足够大的变化。

αnew2是依赖于|E1−E2|的,为了加快计算速度,一种简单的做法是选择α2,使其对应的|E1−E2|最大。因为α1已定,E1也确定了。为了节省时间,将所有Ei值保存在一个列表中。

在特殊情况下,如果内层循环通过以上方法选择的α2不能使目标函数有足够的下降,那么采用以下启发式规则继续选择α2。遍历在间隔边界上的支持向量点,依次将其对应的变量作为α2试用,直到目标函数有足够的下降。若找不到合适的α2,那么遍历训练数据集;若仍找不到合适的α2,则放弃第1个α1,再通过外层循环寻求另外的α1.

3.计算阈值b和差值Ei

在每次完成两个变量的优化后,都要重新计算阈值b。当0<αnew1<C时,由KKT条件可知:


∑i=1NαiyiKi1+b=y1


于是,



bnew1=y1−∑i=3NαiyiKi1−αnew1y1K11−αnew2y2K21



E1的定义式有



E1=∑i=3NαiyiKi1+αold1y1K11+αold2y2K21+bold−y1


代入

bnew1式可得



bnew1=−E1−y1K11(αnew1−αold1)−y2K21(αnew2−αold2)+bold


如果

αnew1,αnew2同时满足条件 0<αnewi<C,i=1,2那么 bnew1=bnew2.如果 αnew1,αnew2是0或者C,那么 bnew1和bnew2以及它们之间的数都是符合KKT条件的阈值,这时选择它们的中点作为 bnew.

在每次完成两个变量的优化之后,还必须更新对应的Ei值,并将它们保存在列表中。Ei值的更新要用到bnew值,以及所有支持向量对应的αj:


Enewi=∑SyjαjK(xi,xj)+bnew−yi


其中,S是所有支持向量

xj的集合。

算法描述

算法(SMO算法)

输入:线性可分训练数据集

T={(x1,y1),(x2,y2),...,(xN,yN)}

其中, xi∈X=Rn,yi∈Y={−1,+1},i=1,2,...N,精度 ϵ
输出:近似解 α^
(1) 取初值 α(0)=0,令 k=0;
(2) 选取优化变量 α(k)1,α(k)2,解析两个变量的最优化问题,求得最优解 α(k+1)1,α(k+1)2,更新 α为α(k+1);
(3) 若在精度 ϵ范围内满足停机条件

∑i=1Nαiyi=0

0≤αi≤C,i=1,2,...,N

yig(xi)=⎧⎩⎨≥1,{xi|αi=0}=1,{xi|0<αi<C}≤1,{xi|αi=C}

其中,

g(xi)=∑j=1NαjyjK(xi,xj)+b

则转(4);否则令 k=k+1,转(2)
(4) 取 α^=α(k+1).


Code Time

Platt SMO算法的完整版实现需要大量代码。在接下来的第一个例子中,我们将会对算法进行简化处理,以便了解算法的基本工作思路,之后基于简化版给出完整版。

在编写python代码时,需要导入一个第三方依赖库numpy,具体的安装过程及库API请查看官方文档

训练集为:testSet.txt

3.542485    1.977398    -1
3.018896    2.556416    -1
7.551510    -1.580030   1
2.114999    -0.004466   -1
8.127113    1.274372    1
7.108772    -0.986906   1
8.610639    2.046708    1
2.326297    0.265213    -1
3.634009    1.730537    -1
0.341367    -0.894998   -1

简化版platt SMO的支持函数
加载代码和辅助函数

# 统一对数据做预处理操作,生成输入空间,标签向量
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进行随机选择
def selectJrand(i,m):
    j =i;
    while(j==i):
        j = int(random.uniform(0,m))
    return j

# 对alpha[j]进行剪辑操作
def clipAlpha(aj,H,L):
    if aj > H:
        aj = H
    if L > aj:
        aj = L
    return aj

简化版SMO算法

def smoSimple(dataMatIn,classLabels,C,toler,maxIter):
    # 输入空间
    dataMatrix = mat(dataMatIn)
    # 标签向量
    labelMat = mat(classLabels).transpose()
    b =0
    # m为样本数量 n为特征向量维度
    m,n=shape(dataMatrix)
    # alphas  a=(a1,a2,a3,...,am)
    alphas = mat(zeros((m,1)))
    # 迭代次数
    iter =0
    while(iter < maxIter):
        alphaPairsChanged =0
        for i in range(m):
            # 求得 每个样本点的fxi 和 Ei
            fxi = float(multiply(alphas,labelMat).T*\
                (dataMatrix*dataMatrix[i,:].T))+b
            Ei = fxi -float(labelMat[i])
            # 寻找不是支持向量机的点,并且不符合KKT条件,需要进行对alphas的更新
            if ((labelMat[i]*Ei < -toler) and (alphas[i] < C)) or\
                ((labelMat[i]*Ei > toler) and \
                    (alphas[i] >0 )):
                # 选择不是i的其他样本点,这里都是进行随机的选择
                j = selectJrand(i,m)
                fxj = float(multiply(alphas,labelMat).T*\
                (dataMatrix*dataMatrix[j,:].T))+b
                Ej = fxj - float(labelMat[j])
                # 寻找目前的alpha值
                alphaIold = alphas[i].copy()
                alphaJold = alphas[j].copy()
                # alphas 满足的约束条件,在一次更新后,就需要满足
                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
                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
                # 更新alphas值
                alphas[j] -= labelMat[j]*(Ei-Ej)/eta
                alphas[j] = clipAlpha(alphas[j],H,L)

                if (abs(alphas[j]-alphaJold) < 0.00001): print \
                    ("j not moving enough");continue
                alphas[i] +=labelMat[j]*labelMat[i]*\
                            (alphaJold-alphas[j])

                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

                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 ("iter: %d i:%d,pairs changed %d" % \
                        (iter,i,alphaPairsChanged))
        if (alphaPairsChanged == 0): iter += 1
        else: iter =0
        print ("iteration number: %d" % iter)
    print (alphas[alphas > 0])
    return b,alphas

还记得在上篇文章中讲述感知机算法中所推导出的G′′矩阵嘛?本文在分析简单SMO算法时,继续根据G′′进行分析。从而对SMO算法有更加直观的理解。

回顾下,G′′的矩阵式子为:


G′′=αjyjxj⋅xi+b=⎛⎝⎜⎜⎜⎜⎜⎜⎜⎜ α1⋅y1⋅X1⋅XT1α2⋅y2⋅X2⋅XT1⋮αM⋅yM⋅XM⋅XT1bα1⋅y1⋅X1⋅XT2α2⋅y2⋅X2⋅XT2⋮αM⋅yM⋅XM⋅XT2b⋯⋯⋱⋯⋯α1⋅y1⋅X1⋅XTMα2⋅y2⋅X2⋅XTM⋮αM⋅yM⋅XM⋅XTMb⎞⎠⎟⎟⎟⎟⎟⎟⎟⎟


该式能够很直观的表述随机SMO算法的执行流程,SMO算法先进行一系列的初始化后,便开始了第一次迭代。而在这一次迭代的最外层循环,从

G′′矩阵中直观的看出,就是循环遍历 XTj,即对应矩阵的每一列。

在外层循环时,算法计算了fxi和Ei,从矩阵G′′中可以看出,fxi的计算是对第i列的所有元素进行累加,即是我们进行分类预测的确信度。Ei是在第i列中,确信度减去分类标签yi后的“误差”。为何定义Ei,主要有以下两点:第一,在寻找不符合KKT条件的列时,我们需要借助Ei来做判断;第二,当更新alpha对时,对b的更新需要用到Ei作为计算变量。

第一个判断语句的作用,用公式叙述便是,寻找符合:


yiEi>toler,αi>0yiEi<−toler,αi<C


初始化时,所有的

α为0,且 f(xi)=0,因此, yiEi=−1。(思考:为后在此处加入此判断语句,该判断语句的由来是什么?)顺利进入if语句后,算法便开始寻找第二个 αj,这里采用的策略是随机寻找剩下的某一列。构成 (αi,αj)对,同样的,计算fxj和Ej,并且拷贝了 αi和αj的旧值。这些计算变量,都是用来求解目标函数最小值所必须的中间值。

接下来,我们定义了L和H,根据alpha对不同的yi,L和H的定义有所区别。这里L和H的范围限制,是为了限制在后续更新alpha对时,始终让每个alpha对,符合约束条件0≤αi≤C.原因在于,从L和H的定义出发,它们符合αiyi+αjyj=ζ,且均属于[0,C]×[0,C]的盒子内,所以只要alpha对经过剪辑,由αj算出的αi均在这个范围内。因此,在对有范围的约束条件时,当符合两变量相加为某一常数时,需要对两变量进行剪辑操作。否则,更新规则将导致两变量不在盒子内,却符合算法进程,从而计算出与实际有偏差的解。(遗留:L==H时,为何可以跳过后续算法步骤?)

算法顺利进展到这步,我们找到了合适alpha对,因此该目标函数也就简化为对minαi,αjW(αi,αj)求解最小值。对该目标函数求解偏导后,得到了αnewj关于αoldj的更新公式。也就有了后续的算法步骤,求解eta,alphas[j],b1,b2等过程。更新一次alpha对,便能对该目标函数进行一次最小化。第一次循环结束,便开始构造第二个alpha对,每次对不同的alpha对做更新操作,直到遍历结束。那么问题来了,该算法什么时候结束呢?或者说停机的条件是什么?

这里我们先假设两个显而易见的结论是正确的:第一,一次迭代过程,是无法使得G′′中的每一列符合KKT条件约束的,但随着迭代次数的增多,alpha对的随机选择,能够使得矩阵G′′的每一列都符合KKT条件。第二,对n次alpha对进行子目标函数的最小化,最终反映在终极目标函数上。有了上述两条假设,我们便可以解释,在进入第一个for循环遍历时的if语句了,该语句便是我们的停机条件,当且仅当G′′中的所有列符合KKT条件时,不再进行alpha对的挑选以及更新。某列不符合时,对其进行更新,直到符合KKT。从算法的角度来看,KKT条件便是求解目标函数极小值的充分必要条件,也显而易见,为何需要KKT,进行条件的转换,无非是为了能够在算法代码层面,能够找到代码表达式等价于数学求解问题,以程序的方式解决数学问题。类比感知机算法,它的停机条件便是使得G′′中的每一列求和项sum∗yj>0.此处只是变成了KKT不等式条件。(待解决:程序alpha对的更新,如何反映几何函数间隔的变化过程,很难建立联系。)

完整版platt SMO算法

Platt SMO算法是通过一个外循环来选择第一个alpha值的,并且其选择过程会在两种方式之间进行交替:一种方式是在所有数据集上进行单遍扫描,另一种方式则是在非边界alpha中实现单遍扫描。而所谓非边界alpha指的就是那些不等于边界0或C的alpha值。对整个数据集的扫描相当容易,而实现非边界alpha值的扫描时,首先需要建立这些alpha值的列表,然后再对这个表进行遍历。同时,该步骤会跳过那些已知的不会改变的alpha值。

完整版platt SMO的支持函数

class optStruct:
    def __init__(self,dataMatIn,classLabels,C,toler):
        self.X = dataMatIn
        self.labelMat = classLabels
        self.C = C
        self.tol = toler
        self.m = shape(dataMatIn)[0]
        self.alphas = mat(zeros((self.m,1)))
        self.b = 0
        self.eCache = mat(zeros((self.m,2)))

# 封装了成员变量
def calcEk(oS,k):
    fxk = float(multiply(oS.alphas,oS.labelMat).T*\
        (oS.X*oS.X[k,:].T))+oS.b
    Ek = fxk - float(oS.labelMat[k])
    return Ek

# 每次计算一次Ei时,会把Ei放入缓存中
def selectJ(i,oS,Ei):
    maxK =-1; maxDeltaE = 0; Ej = 0
    # 1在这里的作用
    oS.eCache[i] = [1,Ei]
    # 寻找第0列非零元素的下标值
    validEacheList = nonzero(oS.eCache[:,0].A)[0]
    # 有多少非零元素,该list便有多长
    if (len(validEacheList)) >1:
        for k in validEacheList:
            # 自身没有比较的意思
            if k ==i:continue
            Ek = calcEk(oS,k)
            deltaE = abs(Ei-Ek)
            # choose j for maximum step size
            if(deltaE > maxDeltaE):
                maxK = k; maxDeltaE = deltaE; Ej = Ek
        return maxK,Ej
    # 随机选择
    else:
        j = svm.selectJrand(i,oS.m)
        Ej = calcEk(oS,j)
    return j,Ej

def updateEk(oS,k):
    Ek = calcEk(oS,k)
    oS.eCache[k]=[1,Ek]

SMO优化例程
这里的算法与简单SMO内层算法相一致,只增加了对Ej,Ei的缓存。

# 替代了循环遍历G''的每一列
def innerL(i,oS):
    Ei = calcEk(oS,i)
    if ((oS.labelMat[i]*Ei < -oS.tol) and (oS.alphas[i] < oS.C)) or\
        ((oS.labelMat[i]*Ei > oS.tol) and (oS.alphas[i] > 0)):
        j,Ej = selectJ(i,oS,Ei)
        alphaIold = oS.alphas[i].copy();alphaJold = oS.alphas[j].copy()
        if (oS.labelMat[i] != oS.labelMat[j]):
            L = max(0,oS.alphas[j]-oS.alphas[i])
            H = min(oS.C,oS.C + oS.alphas[j]-oS.alphas[i])
        else:
            L = max(0,oS.alphas[j]+oS.alphas[i]-oS.C)
            H = min(oS.C,oS.alphas[j]+oS.alphas[i])
        if L==H: print("L==H"); return 0
        eta = 2.0*oS.X[i,:]*oS.X[j,:].T-oS.X[i,:]*oS.X[i,:].T -\
            oS.X[j,:]*oS.X[j,:].T
        if eta >= 0: print("eta >=0"); return 0
        oS.alphas[j] -= oS.labelMat[j]*(Ei-Ej)/eta
        oS.alphas[j] = svm.clipAlpha(oS.alphas[j],H,L)
        # 更新了alphas[j]带来了Ej的更新
        updateEk(oS,j)

        if(abs(oS.alphas[j] - alphaJold) < 0.00001):
            print("j not moving enough"); return 0
        oS.alphas[i] +=oS.labelMat[j]*oS.labelMat[i]*\
            (alphaJold-oS.alphas[j])
        # 更新了alphas[i]带来了Ei的更新
        updateEk(oS,i)

        b1 = oS.b -Ei - oS.labelMat[i]*(oS.alphas[i]-alphaIold)*\
            oS.X[i,:]*oS.X[i,:].T-oS.labelMat[j]*\
            (oS.alphas[j]-alphaJold)*oS.X[i,:]*oS.X[j,:].T
        b2 = oS.b -Ej - oS.labelMat[i]*(oS.alphas[i]-alphaIold)*\
            oS.X[i,:]*oS.X[j,:].T-oS.labelMat[j]*\
            (oS.alphas[j]-alphaJold)*oS.X[j,:]*oS.X[j,:].T
        if (0 < oS.alphas[i]) and (oS.C >oS.alphas[i]): oS.b = b1
        elif (0 < oS.alphas[j]) and (oS.C >oS.alphas[j]): oS.b = b2
        else: oS.b = (b1+b2)/2.0
        return 1
    else: return 0

完整版platt SMO的外循环代码
该函数完成了两步优化过程,在选择第一个alpha值后,算法会通过一个内循环来选择第二个alpha值。在优化过程中,会通过最大化步长的方式来获得第二个alpha值。第二步优化为,数据集全程扫描策略与在非边界alpha对中进行更新策略交替进行。(为何是对非边界进行alpha对的更新能够使得算法性能提升?)

def smoP(dataMatIn,classLabels,C,toler,maxIter,kTup =('lin',0)):
    oS = optStruct(mat(dataMatIn),mat(classLabels).transpose(),C,toler)
    iter =0
    entireSet  = True; alphaPairsChanged = 0
    while(iter < maxIter) and ((alphaPairsChanged >0) or (entireSet)):
        alphaPairsChanged = 0
        if entireSet:
            for i in range(oS.m):
                alphaPairsChanged += innerL(i,oS)
            print("fullSet,iter: %d i: %d,pairs changed %d" %\
                (iter,i,alphaPairsChanged))
            iter +=1
        else:
            # 两个元素乘积非零,每两个元素做乘法[0,1,1,0,0]*[1,1,0,1,0]=[0,1,0,0,0]
            nonBoundIs = nonzero((oS.alphas.A > 0)*(oS.alphas.A < C))[0]
            for i in nonBoundIs:
                alphaPairsChanged += innerL(i,oS)
                print("nou-bound,iter: %d i:%d,pairs changed %d" %\
                    (iter,i,alphaPairsChanged))
            iter +=1
        # entireSet 控制交替的策略选择
        if entireSet: entireSet = False
        # 必须有alpha对进行更新
        elif(alphaPairsChanged == 0): entireSet = True
        print("iteration number:%d" % iter)
    return oS.b,oS.alphas

在复杂数据上应用核函数

上述算法均是针对线性可分的支持向量机算法,现在需要实现一把核函数,用来支持非线性可分的数据集。


核函数编写:

def kernelTrans(X,A,kTup):
    """
    核函数
    :param X:全部向量
    :param A:某个向量
    :param kTup:核函数名称
    :return: :raise NameError:
    """

    # 5**2 = 25 **表示乘方
    m,n = shape(X)
    K = mat(zeros((m,1)))
    if kTup[0]=='lin': K = X*A.T #线性核函数
    elif kTup[0] =='rbf':
        for j in range(m):
            deltRow = X[j,:]-A
            K[j] = deltRow*deltRow.T
        K = exp(K/(-1*kTup[1]**2))
    else: raise NameError('Houston We Have a Problem -- That Kernel is not recognized')
    return K

有了核函数,在构造中间结构的时候,可以一次性地将支持向量机


f(x)=sign(∑i=1NαiyiK(xi,x)+b)


中的内积

K(xi,x)计算出来存着:

class optStruct:
    def __init__(self,dataMatIn, classLabels, C, toler, kTup):  # Initialize the structure with the parameters 
        self.X = dataMatIn
        self.labelMat = classLabels
        self.C = C
        self.tol = toler
        self.m = shape(dataMatIn)[0]
        self.alphas = mat(zeros((self.m,1)))
        self.b = 0
        self.eCache = mat(zeros((self.m,2)))                        #第一列是有效与否的标记
        self.K = mat(zeros((self.m,self.m)))
        for i in range(self.m):
            self.K[:,i] = kernelTrans(self.X, self.X[i,:], kTup)    #软cache

kernel函数其实就是封装了我们计算G′′矩阵的过程,只是少了αi和yi.

为了使用核函数,先前的两个函数innerL()和calcEk()的代码需要做些修改。修改的结果如下。

innerL():
eta = 2.0*oS.K[i,j]-oS.K[i,i]-oS.K[j,j]
b1 = oS.b -Ei - oS.labelMat[i]*(oS.alphas[i]-alphaIold)*\
            oS.K[i,i]-oS.labelMat[j]*\
            (oS.alphas[j]-alphaJold)*oS.K[i,j]
        b2 = oS.b -Ej - oS.labelMat[i]*(oS.alphas[i]-alphaIold)*\
            oS.K[i,j]-oS.labelMat[j]*\
            (oS.alphas[j]-alphaJold)*oS.K[j,j]

def calcEk(oS,k):
    fxk = float(multiply(oS.alphas,oS.labelMat).T*\
        oS.K[:,k]+oS.b)
    Ek = fxk - float(oS.labelMat[k])
    return Ek

接下来我们将构建对上图的数据点进行有效分类的分类器,该分类器使用了高斯核函数。前面提到的高斯核函数有一个用户定义的输入δ.首先,我们需要确定它的大小,然后利用该核函数构建出一个分类器。整个测试函数如下:

def testRbf(k1 =1.3):
    dataArr,labelArr = svmMLiA.loadDataSet('testSetRBF.txt')
    b,alphas =opt.smoP(dataArr,labelArr,200,0.0001,10000,('rbf',k1))
    dataMat = mat(dataArr);labelMat = mat(labelArr).transpose()
    svInd = nonzero(alphas.A >0)[0]
    svs = dataMat[svInd]
    labelSV = labelMat[svInd]
    print("there are %d support vectors" %shape(svs)[0])
    m,n = shape(dataMat)
    errorCount =0
    for i in range(m):
        # 生成Gramm矩阵
        kernelEval = opt.kernelTrans(svs,dataMat[i,:],('rbf',k1))
        predict = kernelEval.T*multiply(labelSV,alphas[svInd])+b
        if sign(predict) != sign(labelArr[i]): errorCount +=1
    print("the training error rate is: %f" %(float(errorCount)/m))
    dataArr,labelArr =svmMLiA.loadDataSet('testSetRBF2.txt')
    errorCount =0
    dataMat = mat(dataArr);
    labelMat = mat(labelArr).transpose()
    m,n = shape(dataMat)
    for i in range(m):
        # 生成Gramm矩阵
        kernelEval = opt.kernelTrans(svs, dataMat[i, :], ('rbf', k1))
        predict = kernelEval.T * multiply(labelSV, alphas[svInd]) + b
        if sign(predict) != sign(labelArr[i]): errorCount += 1
    print("the test error rate is: %f" % (float(errorCount) / m))

上述代码用到的训练数据集为:testSetRBF.txttestSetRBF2.txt

当k1=1.3时,测试结果如下:

there are 27 support vectors
the training error rate is: 0.030000
the test error rate is: 0.080000

试着减小k1=0.1时,测试结果如下:

there are 26 support vectors
the training error rate is: 0.130000
the test error rate is: 0.270000

发现支持向量数目增多了,同时错误率也下降了,但是训练速度显著减慢。

综上,支持向量机的SMO算法已经全部阐述完毕,从最开始的简单SMO算法,其实是在选择alpha对时,无脑的进行随机选择;到后来完整版SMO算法,有策略性的选择alpha对,可以使得迭代速度加快;最后又增加了核函数,能够让支持向量机区分非线性数据。在学习SMO算法中,令人映象深刻的便是对支持向量机的数学定义如何转化为凸二次规划问题,最终在程序中找到了等价的KKT条件,从而能够让数学形式在代码中有了易于表达的停机条件,从而再转化为求解目标函数的子序列极小值的过程,随机选择alpha对,达到整个目标函数最优值。实在是高!

参考文献及推荐阅读

  • 支持向量机通俗导论-理解SVM三重境界
  • 李航. 统计学习方法[M]. 北京:清华大学出版社,2012
  • Peter Harrington. Machine Learning in Action[M]. 北京:人民邮电出版社,2013


标签:alphas,Ei,--,笔记,算法,labelMat,alpha,oS,向量
From: https://blog.51cto.com/u_16184402/6680336

相关文章

  • 算法细节系列(1):Java swap
    算法细节系列(1):Javaswap问题在C++中,swap算法可以用指针来实现,因此在Java中,如果采用如下代码来对两个数字进行交换时,也不会影响两个对象的值。publicclassTestSwap{publicstaticvoidmain(String[]args){inta=2;intb=3;System.out.prin......
  • 支持向量机学习笔记--原理篇(一)
    支持向量机学习笔记–原理篇(一)前言初步学习机器学习给我最大的感受是它背后需要强大的数学知识,理论推导往往能帮助我们理解其本质。而在我看来,单纯的求解数学问题还不够,我们需要有把这部分理论知识运用到实际应用中去的能力。支持向量机(supportvector)是机器学习中用来解决监督分......
  • Linux 内核0.11 系统调用详解(上)
    备注:本文通过三个问题,引出Linux内核0.11的系统调用。操作系统为什么要引出系统调用?回答这个问题前,请先参看如下图:由图可以看出,从操作系统的角度来看,一台计算机主要分为两级:用户级以及内核级,系统调用主要作用就是连接用户级和内核级的“插座”。上层用户的许多对计算机硬件的操作,......
  • 使用递归函数来实现输入正整数,将正整数分解鸡(质因)数
    介绍一下递归函数:当我们定义一个函数时,如果函数内部调用了自身,那么这个函数就称为递归函数。递归函数是一种解决问题的方法,它将大问题分解为相同或类似的小问题,并通过逐步解决这些小问题来解决整个问题。使用递归函数的核心思想是将一个问题拆解为更简单的子问题,并且解决子问题的方......
  • 字符的格式化
    #请实现一个程序,实现如下需求点:#1.程序开始的时候提示用户输入学生年龄信息格式如下:#MikeMos,9;JackGreen,21#我们假设用户输入上面的信息,必定会遵守下面的规则:#学生信息之间用分号隔开(分号前后可能有不定数量的空格)#每个学生信息里的姓名和年岭之间用......
  • C#开发ESP32E(3)Wifi配置使用
    1.安装Wifi配置库(nanoFramework.System.Device.Wifi)1.1nanoFramework.System.Device.Wifi介绍API预览--地址:https://docs.nanoframework.net/api/System.Device.Wifi.html该库可配置ESP32使用Wifi模块进行通信与Wifi建立连接有如下步骤:创建Wifi适配器扫描Wifi列表......
  • 2023年7月10日 天气:晴
       今天早上起来背了20个英语单词,然后学习了一个小时的Java编程,接下来就看了一会构建之法。最后就是写了一会pta上的作业。  明天打算6点起床然后晨跑半小时,然后编程一小时。再就是出去打会羽毛球。再就是看会英语阅读。......
  • Windows计算机如何在线打开Sketch文件?
    自Sketch诞生以来,只有Mac版本。Windows计算机如何在线打开Sketch文件?即时设计已经解决了你遇到的大部分问题,不占用内存也是免费的。您可以使用此软件直接在线打开Sketch文件,完整预览并导出CSS、SVG、PNG等,还具有编辑功能! 如何导入Sketch文件?如果需要切换设计工具,能够......
  • Linux部分常用零碎命令
    1:开放一具体端口firewall-cmd--zone=public--add-port=8888/tcp--permanent #开放8888端口2:关闭一具体端口firewall-cmd--zone=public--remove-port=8888/tcp--permanent #关闭8888端口3:重启后防火墙生效firewall-cmd--reload #配置立即生效......
  • 《代码中的软件工程》学习总结及心得体会
    本学期我选修了孟宁老师开设的《高级软件工程》课程,作为一名软件工程专业的学生,本课程的内容以及《代码中的软件工程》一书让我受益匪浅。在课程以及书本内容中,我了解到软件工程的概念和重要性。软件工程是一门研究如何以系统化、规范化和可量化的方式开发和维护软件的学科。通过......