本文已参与「新人创作礼」活动,一起开启掘金创作之路
在上一篇文章SVM支持向量机与SMO学习笔记(一)中,已经将待求解的未知量由w变成了α,这一节主要介绍求解α的常用算法-SMO算法,序列最小化(Sequence Minimal Optimization)算法,由John Platt于1996年发布。
1. 任务提取
在上一篇中,我们需要求解的目标函数与约束条件为: 通过构造拉格朗日函数,利用KKT条件进行求解出的极值条件为:
1.∂w∂L(w,b,α)=w−∑i=1mαiyixi=0
2.∂b∂L(w,b,α)=∑i=1myiαi=0
Tip:这里是累加求和为零,并不一定每个式子为0
约束条件为:
1.αi∗(1−yi(wTxi+b))=0
2.αi≥0
3.yi(wTxi+b)−1≥0
推导得出其他条件:
L(w,b,α)=21∣∣w∣∣2+∑i=1mαi∗(1−yi(wTxi+b))
1.αi∗(1−yi(wTxi+b))=0
2.minwmaxαL(w,b,α)=maxαminwL(w,b,α)=minwf(w)=f(w∗)
原待求解式为
minwmaxαL(w,b,α),经对偶转换可以先进行求解
minwL(w,b,α),将极值条件代入到
maxαminwL(w,b,α)中,可以得到:
minwmaxαL(w,b,α)=maxαminwL(w,b,α)=maxαminw21∑i=1m∑j=1mαiαjyiyjxixjT+∑i=1m(αi−αiyi∑j=1mαjyjxjTxi−b∗αiyi)=maxα∑i=1mαi−21∑i=1m∑j=1mαiαjyiyjxixjT
SMO算法的目标是求解一系列
α和b来求得上述式子中的最优值。
SMO算法工作原理为,每次选择一对α进行优化处理,一旦找到一对合适的α,就增大一个α同时减小另一个α(这是由限制条件
∑i=1myiαi=0的原因)。这里的合适的α需要满足一定条件,条件之一是两个α必须在间隔边界外(限制条件
0≤αi≤C),而第二个条件则是两个α没有进行过区间化处理或者不在边界上。
选一对α进行优化:
maxα(α1+α2)−21(y12x1x1T∗α12+y22x2x2T∗α22+y1y2x1x2T∗α1α2+y1y2x1Tx2∗α1α2+y1x1∑i=1m−2yixiTαi∗α1+y1x1T∑i=1m−2yixiαi∗α1+y2x2∑i=1m−2yixiTαi∗α2+y2x2T∑i=1m−2yiαixi∗α2)
由
yi2=1;简化方程表示令
Kij=xi∗xjT=xiT∗xj;便于最小化求解,对整个式子取相反数可得:
原式
=minα1,α221K11α12+21K22α22+y1y2K12α1α2+y1∑i=1m−2Ki1yiαi∗α1+y2∑i=1m−2Ki2yiαi∗α2−(α1+α2)
约束条件
1.α1y1+α2y2=−∑i=1m−2αiyi=ζ
2.0≤αi≤C
现在上述问题转变成为了一个类似于二次函数求极值的问题。
2.问题求解
首先,根据
α1y1+α2y2=−∑i=1m−2αiyi=ζ得到
α1=(ζ−α2y2)/y1,将该式代入到求解式中进行消元。 消元后,原式
=21K11(ζ−α2y2)2/y12+21K22α22+K12y2α2(ζ−α2y2)+(ζ−α2y2)∑i=1m−2Ki1yiαi+y2α2∑i=1m−2Ki2yiαi−((ζ−α2y2)/y1+α2)
2.1.极值点求解
首先对
α2进行求导,求出其极值点:
f′(α2)=K11(ζ−α2y2)+K22α2+K12y2ζ−2K12−y2∑i=1m−2Ki2yiαi−(1−y2/y1)
令
f′(α2)=0,可以推出
(K11+K22−2K12)α2=y2(y2−y1+(K11−K12)ζ+∑i=3mαiyi(Ki1−Ki2))
由上一篇提到的核函数可以得出
g(x)=∑i=1mαiyiK(xi,x)+b
则
vi=∑j=3mαjyjK(xi,xj)=g(xi)−∑i=12αjyjK(xi,xj)−b
则上式可变为
(K11+K22−2K12)α2=y2(y2−y1+(K11−K12)ζ+v1−v2)
=y2(y2−y1+(K11−K12)ζ+g(x1)−g(x2)+α1y1(K12−K11)+α2y2(K22−K21))
再将
α2oldy2+α1oldy1=ζ代入
(K11+K22−2K12)α2=α2old(K11+K22−2K12)+y2(E1−E2)
其中
Ei=g(xi)−yi,g(xi)为估计值,yi为实际值,Ei为误差
综上可得:α2new=α2old+y2(E1−E2)/η;η=(K11+K22−2K12)
2.2.定义域考察
在高中我们就知道,求二次函数极值时,还需要考察极值点是否在定义域内,高中数学老师经常说的就是千万不要忘记定义域!
首先我们的约束条件为:
1.α1y1+α2y2=ζ
2.0≤αi≤C
其中约束范围可通过画图看出来,图为SMO论文内的图。 图中方形区域就是指
0≤α1≤C和
0≤α2≤C共同围起来的区域,几条直线就是对应
α1y1+α2y2=ζ的几种不同的情况.
在
α1y1+α2y2=ζ式子中,
y1与
y2分别可以去-1,1,所以一共可分为4种情况:
(1)
y1=1,y2=1,式子变为
α1+α2=ζ
图像变为图像中右图所示,其截距为
ζ,我们针对
ζ不同取值范围来讨论.
①
ζ<0orζ>2C
直线未落到矩形可行范围内,即可行域无解,为空集。
②
0≤ζ≤C
图形对应右图中左下角的直线
α2可行范围为
[0,ζ]
③
C<ζ≤2C
图形对应右图中右上角的直线
α2可行范围为
[ζ−C,C]
综上考虑
ζ有可行域时,
ζ范围可表示为
[max(0,ζ−C),min(ζ,C)]即[max(0,α1+α2−C),min(α1+α2,C)]
(2)
y1=−1,y2=−1,式子变为
α1+α2=−ζ 图像仍为图像中右图所示,其截距为
−ζ,我们针对
−ζ不同取值范围来讨论. 其情况同(1)类似,所以可以看出图中截距k不一定为
ζ ①
−ζ<0or−ζ>2C
直线未落到矩形可行范围内,即可行域无解,为空集。
②
0≤−ζ≤C
图形对应右图中左下角的直线
α2可行范围为
[0,−ζ]
③
C<−ζ≤2C
图形对应右图中右上角的直线
α2可行范围为
[−ζ−C,C]
综上考虑
−ζ有可行域时,
−ζ范围可表示为
[max(0,−ζ−C),min(−ζ,C)]即[max(0,α1+α2−C),min(α1+α2,C)]
(3)
y1=1,y2=−1,式子变为
α1−α2=ζ
图形变为左图中的情况,截距为
ζ,我们针对
ζ不同取值范围来讨论.
①
ζ<−Corζ>C
直线未落到矩形可行范围内,即可行域无解,为空集。
②
0<ζ≤C
图形对应左图中右下角的直线
α2可行范围为
[0,C−ζ]
③
−C≤ζ≤0
图形对应左图中左上角的直线
α2可行范围为
[−ζ,C]
综上考虑
ζ有可行域时,
ζ范围可表示为
[max(0,−ζ),min(C−ζ,C)]即[max(0,α2−α1),min(C+α2−α1,C)]
(4)
y1=−1,y2=1,式子变为
α1−α2=−ζ
情况同前面类似
综合以上四种情况,我们可以得到, (1)
y1=y2时
α2可行范围为[max(0,α1+α2−C),min(α1+α2,C)]
区间端点L=max(0,α1+α2−C),H=min(α1+α2,C)
(2)
y1!=y2时
α2可行范围为[max(0,α2−α1),min(C+α2−α1,C)]
区间端点L=max(0,α2−α1),H=min(C+α2−α1,C)
2.3.开口方向
我们的目标是上文的
minα1,α221K11α12+21K22α22+y1y2K12α1α2+y1∑i=1m−2Ki1yiαi∗α1+y2∑i=1m−2Ki2yiαi∗α2−(α1+α2),要求其最小值,还需要判断极值点是否为极小值点。
(1)开口向上:
K11+K22−2K12>0
判断极值点是否在定义域内,进行参数更新,可以想象一个二次函数
①如果这个极值点在可行域左边,那么我们可以得到这个可行域内二次函数一定在单增,所以此时L应该是那个取最小值的地方。就如大括号的第三种情况
②如果这个极值点在可行域右边,那么此时可行域内一定单减,所以此时H就是那个取最小值的地方,就是大括号里的第一种情况。 (2)开口向下或者退化成为一次函数:
K11+K22−2K12≤0
这种情况下极小值在端点处取得,可以通过计算两端端点值取最小的。
3.参数更新总结
①求出的可行域
②根据这一项
K11+K22−2K12和0的关系:
如果
K11+K22−2K12>0,那么就直接利用之前求好的公式求出新的极值点处的
α2,然后看这个
α2和可行域的关系,得到
α2的新值
如果
K11+K22−2K12≤0,那么就直接求出H和L对应的边界值,哪个小,就让
α2取哪个值
③根据新的
α2求出新的
α1
④更新ω很简单,利用
w−∑i=1mαiyixi=0求解,现在还需要说一下b的更新:
利用
1−yi(wTxi+b)=0进行求解
上式进一步推导,
b=1/yi−∑j=1myiαiK(xi,xj)
b1new=y1−y1α1newK11−y2α2newK12−∑i=3myiαiKi1
E1=g(x1)−y1=α1oldy1K11+α2oldy2K12+∑i=3myiαiKi1+b1old−y1
消元后可得:
b1new=b1old+(α1old−α1new)y1K11+(α2old−α2new)y2K12−E1
4.更新参数
α的选取
4.1.寻找第一个待更新
α
①遍历一遍整个数据集,对每个不满足KKT条件的参数,选作第一个待修改参数
②在上面对整个数据集遍历一遍后,选择那些参数满足
0<α<C的子集,开始遍历,如果发现一个不满足KKT条件的,作为第一个待修改参数,然后找到第二个待修改的参数并修改,修改完后,重新开始遍历这个子集
③遍历完子集后,重新开始①②,直到在执行①和②时没有任何修改就结束
4.2.寻找第二个待更新
α
①启发式找,找能让
∣E1−E2∣最大的
②寻找一个随机位置的满足
0<α<C的可以优化的参数进行修改
③在整个数据集上寻找一个随机位置的可以优化的参数进行修改
④都不行那就找下一个第一个参数
ri = Eiyi 关于寻找不满足KKT条件第一个α认识(有疑问),伪码中 if ((r2<-tol) and (α<C))or((r2>tol)and(α>0)) tol为容错率
KKT条件为:
1.w−∑i=1mαiyixi=0
2.∑i=1myiαi=0
3.yi(wTxi+b)−1≥0
4.0≤α≤C
5.αi∗(1−yi(wTxi+b))=0
ri=Eiyi=(g(xi)−yi)yi=yig(xi)−1
ri<−tol=>yig(xi)<1−tol<1又KKT条件中
yi(wTxi+b)≥1
ri>tol=>y2g(xi)>1+tol>1
以下为部分在机器学习实战中的代码,加深算法理解
import numpy as np
import random
def selectJrand(i,m):
j = i
while j == i:
j = int(random.uniform(0,m))
return j
def clipAlpha(aj,H,L):
if aj>H:
aj = H
if aj<L:
aj = L
return aj
def svmSimple(dataMatIn,classLabels,C,toler,maxIter):
dataMatrix = np.mat(dataMatIn)
labelMat = np.mat(classLabels).transpose()
b = 0
m,n = np.shape(dataMatIn)
alphas = np.zeros((m,1))
iter = 0
while iter < maxIter:
alphaPairsChanged = 0
for i in range(m):
fXi = float(np.multiply(alphas,labelMat).T * dataMatrix * dataMatrix[i,:].T) + b
Ei = fXi - float(labelMat[i])
if ((labelMat[i]*Ei > toler) and (alphas[i] > 0)) or ((labelMat[i] * Ei < -toler) and (alphas[i] < C)): #误差超过容错率
j = selectJrand(i,m)
fXj = float(np.multiply(alphas,labelMat).T * dataMatrix * dataMatrix[j,:].T) + b
Ej = fXj - labelMat[j]
alphaIold = alphas[i].copy()
alphaJold = alphas[j].copy()
if labelMat[i] != labelMat[j]: # 定义域
L = max(0,alphas[j] - alphas[i])
H = min(C,C + alpha[j] - alpha[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[j] -= labelMat[j]*(Ei - Ej)/eta #减号因为eta符号与计算是相反的
alphas[j] = clipAlpha(alphas[j],H,L)
if abs(alphas[j] - alphaJold) < 0.00001:
print("j not moving enough")
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 (alphas[i] > 0) and (alphas[i] < C):
b = b1
elif (alphas[j] > 0) and (alphas[j] < C):
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)
return b,alphas
class optStruct:
def __init__(self,dataMatIn, classLabels, C, toler):
self.X = dataMatIn
self.labelMat = classLabels
self.C = C
self.tol = toler
self.m = np.shape(dataMatIn)[0]
self.alphas = np.mat(np.zeros((self.m,1)))
self.b = 0
self.eCache = np.mat(np.zeros((self.m,2)))
def calcEk(os, k):
fXk = float(np.multiply(os.alphas,os.labelMat[k,:]).T * os.X * os.X[k,:].T) + os.b
Ek = fXk - float(os.labelMat[k,:])
return Ek
def selectJ(i,os,Ei):
maxK = -1
maxDeltaE = 0
Ej = 0
os.eCache[i] = [1,Ei]
validEcacheList = np.nonzero(os.eCache[:,0].A)[0] #.A将mat转成array,返回第一维为0的可用eCache
if len(validEcacheList) > 1:
for k in validEcacheList:
if k == i:
continue
Ek = calcEk(os,k)
deltaE = abs(Ei - Ek)
if deltaE > maxDeltaE:
maxK = k
maxDeltaE = deltaE
Ej = Ek
return maxK,Ej
else: #第一次
j = selectJrand(i,os.m)
Ej = calcEk(os,j)
return j,Ej
def updateEk(os,k):
Ek = calcEk(os,k)
os.eCache[k] = [1,Ek]
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] = clipAlpha(os.alphas[j],H,L)
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])
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
def smoP(dataMatIn, classLabels, C, toler, maxIter, kTup=('lin',0)):
os = optStruct(np.mat(dataMatIn),np.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))
else:
nonBounfIs = np.nonzero((os.alphas.A > 0)*(os.alphas.A < C))[0]
for i in nonBounfIs:
alphaPairsChanged += innerL(i,os)
print("non-bound,iter:%d i:%d, pairs changed %d" % (iter, i, alphaPairsChanged))
iter += 1
if entireSet:
entireSet = False
elif (alphaPairsChanged == 0):
entireSet = True
return os.b,os.alphas
复制代码
参考链接:SMO算法的个人理解