################################################################################################
作者也是一边学习一边领悟一边更新blog,所以有混乱和错误的地方请多多包涵,都会在随后的blog进行改善的~
################################################################################################
SVM下篇,我们主要进行以下几点:
i) 软间隔SVM的说明
ii) 完整版SMO算法的原理与算法代码
iii) 回顾SVM的整体思路(重要!)
核方法不日单独专题说明~
一.软间隔SVM
数据可按以下标准划分为三种:线性可分,近似线性可分和非线性可分。对于完全线性可分的数据,原始优化目标即为:
这个我们在上篇已经讲过,约束条件是限制函数间隔>=1。这样经过Lagrange对偶+SMO算法出来的可叫做硬间隔向量机,因为它非常“是非分明”:支持向量完全在约束边界上,不允许一点“出错”。可对于近似线性可分的数据,存在一些“特异点”并不一定能满足约束条件,那么我们可以加上一个松弛变量L,使原始的函数间隔+L >= 1。这样原始优化目标变成:
C可以看成和SRM的正则因子一样的东西,叫做惩罚参数,是用来平衡间隔与误分类点的:因为做最小化时要使它们同时减小,而它们又是彼此矛盾的变量。
有了原始问题赶快用Lagrange橙子法,记得它有两个约束项故有两个参数alpha, beta:
别忘了对偶时的KKT条件,常规思路先求偏导得到一些关系式(求min),再代入原式求max。注意根据第一次求min时的结果得到两个参数的关系式并代入beta的约束,得到完整的alpha约束(额符号打错):
所以这里就可以看成,上篇里我们写的实际上是软间隔向量机,只不过昨天我看的那本书里没写,今天看李航老师的书才发现(气死我了!)
所以整理完毕之后我们的问题就很明朗,最终学习的算法(优化目标)如下【注意本来是求max但把目标函数符号正负调换所以变成了求min】:
看起来和我们之前的硬间隔向量机没啥区别,只不过改变了alpha的取值范围。根据KKT条件:
可知所有alpha>0对应的点都是支持向量,但是有别于硬间隔向量机的是这里的支持向量既可位于边界上,也可在边界与超平面间,超平面上,甚至误分类那侧,根据以下判据决定:
alpha<C,显然Li = 0,在边界上(g(w)=1)
alpha=C, 0<Li<1,在正确分类这边的边界和超平面之间(0<g(w)<1)
alpha=C, Li=1, 在超平面上(g(w)=0)
alpha=C, Li>1, 在误分类那侧(g(w)<0)
至此我们大概说明了什么是软间隔向量机,它会有更加的容错效果。
二.完整版SMO算法
简单的SMO算法里,我们对alpha的选择是随意的,而为了获取加速效果我们需要改进一下:
class optStruct:
def __init__(self, dataMatrixIn, classMatrixIn, C, toler):
self.X = dataMatrixIn
self.label = classMatrixIn
self. C = C
self.toler = toler
self.m = shape(dataMatrixIn)[0]
self.b = 0
self.alphas = mat(zeros((self.m, 1)))
self.eCache = mat(zeros((self.m, 2)))
定义一个数据结构,用来做参数传递的,基本内容和简化SMO一样,不同的是加入了缓存Ei的矩阵,它的元素是(i,Ei)i是表示Ei是否为0。因为我们在改变alpha值时就会改变Ei值,而Ei是我们用来衡量步长加速算法的,所以必须要全局缓存。
def calcEk(os, k):
res = float(multiply(os.alphas, os.label).T * (os.X*os.X[k, :].T))+os.b
Ek = res - os.label[k]
return Ek
calcEk函数:用来计算Ek的,根据如下公式:
配套更新函数,每次改变alpha值就要更新一次:
def updateEk(os, k):
Ek = calcEk(os, k)
os.eCache[k] = [1, Ek]
接着是选择第二个alpha的函数,即“内循环”,遵循以下规则:选择能使“步长”:|Ei-Ej|最大化的那个alphaJ。如果一开始Ej是空的,就随机选择一个。注意nonzero()函数属于numpy,它返回大于0的Ei的脚标的列表。
def selectJ(i, os, Ei):
maxJ = -1;maxDeltaE = 0;Ej = 0
os.eCache[i] = [1, Ei]
validEcacheList = nonzero(os.eCache[:, 0].A)[0]
if len(validEcacheList)>1:
for k in validEcacheList:
if k == i: continue
Ek = calcEk(os, k)
if(abs(Ek - Ei)>maxDeltaE):
maxDeltaE = abs(Ek - Ei); maxJ = k; Ej = Ek
return maxJ, Ej
else:
maxJ = selectJrand(i, os.m)
Ej = calcEk(os, maxJ)
return maxJ, Ej
接下来是由简化版SMO改进来的优化与参数更新过程,数学推导如下:
def innerProcess(i, os):
Ei = calcEk(os, i)
if ((os.label[i]*Ei < -os.toler) and (os.alphas[i] < os.C)) or ((os.label[i]*Ei > os.toler) and (os.alphas[i] > 0)):
j, Ej = selectJ(i,os, Ei)
alphaIold = os.alphas[i].copy(); alphaJold = os.alphas[j].copy();
if (os.label[i] != os.label[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.label[j]*(Ei - Ej)/eta
os.alphas[j] = adjustAlpha(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.label[j]*os.label[i]*(alphaJold - os.alphas[j])#update i by the same amount as j
#the update is in the oppostie direction
updateEk(os, i)
b1 = os.b - Ei- os.label[i]*(os.alphas[i]-alphaIold)*os.X[i,:]*os.X[i,:].T - os.label[j]*(os.alphas[j]-alphaJold)*os.X[i,:]*os.X[j,:].T
b2 = os.b - Ej- os.label[i]*(os.alphas[i]-alphaIold)*os.X[i,:]*os.X[j,:].T - os.label[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
改进的地方:
1.全部使用os作为统一的数据传递结构
2.每次更新alphaI, alphaJ时更新eCache
3.之前中途continue的语句改成return 0,即没有做任何修改
接下来是外循环,即svm一开始执行的部分,它主要目的是选择第一个参数alpha,同时调用外循环即函数innerProcess进行一对儿alpha,b的优化与参数更新。
外循环的思路是“交替选择”:先线性扫描整个样本集,如果没有做任何改变则转入扫描所有在边界上(0<apha_i<C)对应的样本i,这里没有任何改变的话再回退到整个样本集的扫描......如此交替直到收敛。整个交替过程用一个while()循环来控制,退出条件是达到最大迭代次数(这里定义为一次循环就是一次迭代)并且扫描边界时没有任何alpha值改变:
def SMO(dataMatrixIn, classMatrixIn, C, toler, maxIter):
os = optStruct(mat(dataMatrixIn), mat(classMatrixIn).transpose(), C, toler)
iter = 0; alphaPairsChanged = 0; entireSet = 1
while (iter<maxIter) and ((alphaPairsChanged>0)or(entireSet)):
alphaPairsChanged = 0
if entireSet:
for i in range(os.m):
alphaPairsChanged += innerProcess(i, os)
print 'fullSet, iter %d, i %d, pairs changed %d' % (iter, i, alphaPairsChanged)
iter += 1
else:#go over non-bound (railed) alphas
nonBoundIs = nonzero((os.alphas.A > 0) * (os.alphas.A < C))[0]
for i in nonBoundIs:
alphaPairsChanged += innerProcess(i,os)
print "non-bound, iter: %d i:%d, pairs changed %d" % (iter,i,alphaPairsChanged)
iter += 1
if entireSet: entireSet = False #toggle entire set loop
elif (alphaPairsChanged == 0): entireSet = True
print "iteration number: %d" % iter
return os.b,os.alphas
entireSet变量是控制交替切换的开关。
之后我们写一个用学习到的参数计算W的函数:
def calcW(alphas, dataMatrixIn, classMatrixIn):
X = mat(dataMatrixIn)
labelMat = mat(classMatrixIn).transpose()
m, n = shape(X)
W = zeros((n, 1))
for i in range(m):
W += multiply(alphas[i]*labelMat[i], X[i, :].T)
return W
def execu_2(filepath):
dataArr, labelArr = loadData(filepath)
b, alphas = SMO(dataArr, labelArr, 0.6, 0.001, 40)
W = calcW(alphas, dataArr, labelArr)
dataMat = mat(dataArr)
m = shape(dataMat)[0]
ResList = []
for i in range(m):
Res = dataMat[i]*mat(W) + b
if Res[0] > 0 : ResList.append(1)
else : ResList.append(-1)
if ResList[i] == labelArr[i]:
print 'Right!'
else:
print 'Wrong!'
测试结果也说明了这点~
三.SVM的整体回顾
这里要梳理一遍我们整个提出问题-分析问题-解决问题的思路,请允许我用手绘图而不是latex表示QAQ.....
那么SVM的本篇先到这里,有时间再来写一下它的“外传”:核方法,是用来处理数据非线性可分时的一种方法,适用性要比SVM广。