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

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

前言

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

SMO算法

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

问题描述

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

minα 12i=1Nj=1NαiαjyiyjK(xixj)i=1Nαi

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

0αiC,i=1,2,...,N

在这个问题中,变量是拉格朗日乘子,一个变量 αi 对应于一个样本点 (xi,yi); 变量的总数等于训练样本容量N。

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

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

α1=y1i=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α1i=3NyiαiKi1+y2α2i=3NyiαiKi2

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

0αiC

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

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

由于只有两个变量 (α1,α2) ,约束可以用二维空间中的图形表示。如下图所示:
y1y2α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αnew2H

其中,L与H是 αnew2 所在对角线段端点的界。如果 y1y2 ,则
L=max(0,αold2αold1),H=min(C,C+αold2αold1)

如果 y1=y2 ,则
L=max(0,αold2+αold1C),H=min(C,αold2+αold1)

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

αnew2=αold2+μ

但这里, μ 并非简单的就等于某个实数,在实际算法过程中, μ=y2(E1E2)η ,且
g(x)=i=1NαiyiK(xi,x)+b

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

η=K11+K222K12=Φ(x1)Φ(x2)2

经过剪辑后 α2 的解是:
αnew2=H,αnew,unc2>Hαnew,unc2,Lαnew,unc2HL,α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=ζα2y2y2i=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α22K12α2K11ζy2+K12ζy2+y1y21v1y2+y2v2

令其为0,得到
(K11+K222K12)α2=y2(y2y1+ζK11ζK12+v1v2)

v1,v2,ζ=αold1y1+αold2y2 代入,得
(K11+K222K12)αnew,unc2=(K11+K222K12)αold2+y2(E1E2)

η=K11+K222K12 ,于是得到
αnew,unc2=αold2+y2(E1E2)η

目标函数,经过化简后,只得到了关于 α2 的函数,因此对目标函数直接求偏导,就能找到该函数的极值。但在目标函数中,还关联了一项 g(x) 函数,而 g(x) 函数又是关于 α2 的函数, 因此从理论上来说, αnew,unc2 是否属于慢慢趋近于某个稳定值的过程?

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

变量的选择方法

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

1.第一个变量的选择

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

αi=0yig(xi)1

0αiCyig(xi)=1

αi=Cyig(xi)1

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

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

2.第2个变量的选择

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

αnew2 是依赖于 |E1E2| 的,为了加快计算速度,一种简单的做法是选择 α2 ,使其对应的 |E1E2| 最大。因为 α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=y1i=3NαiyiKi1αnew1y1K11αnew2y2K21

E1 的定义式有
E1=i=3NαiyiKi1+αold1y1K11+αold2y2K21+boldy1

代入 bnew1 式可得
bnew1=E1y1K11(αnew1αold1)y2K21(αnew2αold2)+bold

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

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

Enewi=SyjαjK(xi,xj)+bnewyi

其中,S是所有支持向量 xj 的集合。

算法描述

算法(SMO算法)

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

T={(x1,y1),(x2,y2),...,(xN,yN)}
其中, xiX=Rn,yiY={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αiC,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′′=αjyjxjxi+b= α1y1X1XT1α2y2X2XT1αMyMXMXT1bα1y1X1XT2α2y2X2XT2αMyMXMXT2bα1y1X1XTMα2y2X2XTMαMyMXMXTMb

该式能够很直观的表述随机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αiC .原因在于,从L和H的定义出发,它们符合 αiyi+αjyj=ζ ,且均属于 [0,C]×[0,C] 的盒子内,所以只要alpha对经过剪辑,由 αj 算出的 αi 均在这个范围内。因此,在对有范围的约束条件时,当符合两变量相加为某一常数时,需要对两变量进行剪辑操作。否则,更新规则将导致两变量不在盒子内,却符合算法进程,从而计算出与实际有偏差的解。(遗留:L==H时,为何可以跳过后续算法步骤?)

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

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

为了使用核函数,先前的两个函数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.txt和testSetRBF2.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

你可能感兴趣的:(机器学习,机器学习入门)