1. 基本知识
二分类:通过分离超平面对数据点进行分类,训练分离超平面。
原理:最大化支持向量到分离超平面的距离。支持向量:离分离超平面最近的点。
2. 完全线性可分(硬间隔)
2.1 SVM基本型
分离超平面:。(训练中更新w和b,或alpha,使得分离超平面分类效果最佳)
某点到分离超平面的函数距离:
某点到分离超平面的几何距离:, ||w||为w的L2范数。
点集到分离超平面的几何距离 => 距离超平面最近的点到其的距离:
SVM的目标:
引入约束条件,假设所有点到超平面的距离都大于等于1,其中里分离超平面距离为1的点称为‘支持向量’(即等号成立时的点)。则问题转化为:
2.2 对偶问题
求解:,以求得分离超平面,为凸二次规划问题。
引入拉格朗日乘子,则拉格朗日函数为:
分别对w,b求偏导,使导数为零:
将w和b由alpha表示,并添加约束条件,最后使得问题转化为:
最后得到:
上述过程满足KKT条件:
上述KKT条件说明,当alpha为0时,对应的数据点不参与w的计算,而当alpha不为0时,为1,说明这些点到分离超平面距离为1,为支持向量。这说明:训练完成后,只需要保留支持向量的样本即可。
3. SMO算法
SMO:sequential minimal optimization 序列最小优化。优化求解alpha。
基本思路:固定之外的所有参数,然后求上的极值。由于约束条件:,则固定之外的其他变量后,可以直接求解。
实验步骤:
step1:选取一对需要更新的,;
step2:固定,以外的参数,求解上述蓝色部分的对偶问题,获得更新后的,
的选取规则:第一个变量选取违背KKT条件程度最大的变量(KKT越违背,则更新alpha后目标函数的增值越大)。
的选取规则:第二个变量选取使目标数值增长最快的变量。
SMO中启发式方法:选取的两个变量,对应的两个样本之间的间隔最大,因为对差别大的两个变量进行更新,会带给目标函数值更大的变化。
当计算得到后,因为对于所有支持向量:, 其中S为所有支持向量的集合。因此可以将一个支持向量带入求得b值,或者求所有支持向量的b值然后取平均。
总结:
1. 初始化为全1向量。
2. 第一轮两个只能随机选择,因为都为1.
3. 以后几轮:更新时,外循环遍历所有数据集或非边界(不等于0或C的),选取第一个。然后根据最大化误差步长来选取第二个, 根据目标函数及其约束条件,更新这两个。
4. 重复上述步骤知道最大迭代步数,或所有都不违背KKT条件。
4. 核函数
当数据线性不可分时,利用核函数将低维度下线性不可分的数据映射到高维度下线性可分的数据。即将数据从旧特征空间映射到新的特征空间。因为SVM中的运算可写成内积的形式(内积后得数为一个标量or数值),所以可以直接用核函数来代替原内积。
若数据完全线性不可分,则引入核函数,即对输入空间进行转换到特征空间:
,将原式中的x的内积转换成核函数。
线性核函数:
多项式核函数:
,d 为多项式次数
高斯核(径向基核函数):
, sigma > 0, 为高斯核的带宽
拉普拉斯核:
,sigma > 0
sigmoid 核:
5. 非完全线性可分(软间隔)
若数据非完全线性可分,则引入松弛变量,,允许少部分数据点处于分割面错误一侧。
优化目标:
其中为“0/1损失函数”:即当某点到分离超平面距离小于1时,考虑常数C,否则还按照线性可分的硬间隔来做。
一般使用一些替代损失来代替上述“非凸,非连续的0/1损失”,一般选择有:
hinge损失:
指数损失:
对数损失:
若引入松弛变量,目标优化函数为:
因此对应的拉格朗日函数:
对求偏导为0,求得:
因此原目标函数的对偶问题为:
软间隔支持向量机的KKT条件为:
6. 合页损失函数形式的目标函数
原目标函数:
新目标函数:
其中:
合页损失函数是指:
7. 代码实现
参考:《机器学习实战》
源码地址以及数据:https://github.com/JieruZhang/MachineLearninginAction_src
简化SMO:
from numpy import *
import random
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
#在简单版SMO中,alpha是随机选择的
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
elif aj < L:
aj = L
return aj
#简单版的SMO算法,随机选择两个alpha,若满足优化条件,则进行优化
def smoSimple(dataMatIn, classLabels, C, tol, maxIter):
'''
dataMatIn: 输入数据集
classLabels: 类别标签
C: 常数
tol: 容错率
maxIter:最大循环数
'''
dataMatrix = mat(dataMatIn)
labelMat = mat(classLabels).transpose()
b = 0
m,n = shape(dataMatrix)
alphas = mat(zeros((m,1)))
iters = 0
#外循环
while iters < maxIter:
alphaPairChanged = 0
#内循环,对于每一个实例,对应的alpha为第一个alpha
for i in range(m):
#预测类别
fxi = float(multiply(alphas,labelMat).T * (dataMatrix*dataMatrix[i,:].T)) + b
#分类误差
Ei = fxi- float(labelMat[i])
#当误差和alpha的值满足优化条件时,进行优化,正负间隔均已考虑
if ((labelMat[i]*Ei < -tol) and (alphas[i] < C) or (labelMat[i]*Ei > tol) and (alphas[i]) > 0):
#因为alphas的和需要为零,所以要再选一个alpha进行优化
#随机选择第二个alpha
j = selectJrand(i,m)
fxj = float(multiply(alphas,labelMat).T * (dataMatrix*dataMatrix[j,:].T)) + b
Ej = fxj- float(labelMat[j])
#保存旧的alpha的值,因为python中传的是引用,所以使用copy函数分配新的内存。
alphaIold = alphas[i].copy()
alphaJold = alphas[j].copy()
#设置L和H的值,用于确保alpha在0到C之间
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])
#如果L和H相等,则不做任何改变,跳出循环
if L == H:
print('L==H')
continue
#eta是alpha[j]的最优修改量
eta = 2.0 * dataMatrix[i,:] * dataMatrix[j,:].T - dataMatrix[i,:] * dataMatrix[i,:].T - dataMatrix[j,:] * dataMatrix[j,:].T
#如果eta不满足条件,则退出当前循环
if eta >= 0:
print('eta>=0')
continue
#更新alpha[j]
alphas[j] -= labelMat[j]*(Ei -Ej)/eta
#限制alpha[j]在0到C之间
alphas[j] = clipAlpha(alphas[j], H, L)
#如果alpha[j]该变量较小,则不做改变,直接退出循环,重新随机选择j
if abs(alphas[j]-alphaJold) < 0.00001:
print('j is not moving enough')
continue
#更新alpha[i], 与j的更新方向相反
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
alphaPairChanged += 1
print('iter: %d i:%d, pairs changed %d'%(iters, i, alphaPairChanged))
#如果alpha未更新过,则进行下一次迭代,知道alpha更新或达到最大迭代次数
if alphaPairChanged == 0:
iters += 1
else:
iters = 0
print('iteration number: %d'%iters)
return b, alphas
#测试上述函数
dataArr, labelArr = loadDataSet('testSet6.txt')
b, alphas = smoSimple(dataArr, labelArr, 0.6, 0.001, 40)
原版SMO:
#完整版SMO,启发式方法选取待更新的两个alpha
#核转换函数
def kernelTrans(X, A, kTup): #calc the kernel or transform data to a higher dimensional space
m,n = shape(X)
K = mat(zeros((m,1)))
if kTup[0]=='lin': K = X * A.T #linear kernel
elif kTup[0]=='rbf':
for j in range(m):
deltaRow = X[j,:] - A
K[j] = deltaRow*deltaRow.T
K = exp(K/(-1*kTup[1]**2)) #divide in NumPy is element-wise not matrix like Matlab
else: raise NameError('Houston We Have a Problem -- \
That Kernel is not recognized')
return K
#建立一个对象,用于存储重要的中间变量
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
#用于缓存误差,帮助寻找最佳的第二个参数alphas[j]
self.eCache = mat(zeros((self.m,2))) #first column is valid flag
self.K = mat(zeros((self.m,self.m)))
for i in range(self.m):
self.K[:,i] = kernelTrans(self.X, self.X[i,:], kTup)
#已知alpha,计算误差E,
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
#启发式选取使得误差最大的第二个变量alpha[j]
def selectJ(i, oS, Ei): #this is the second choice -heurstic, and calcs Ej
maxK = -1; maxDeltaE = 0; Ej = 0
oS.eCache[i] = [1,Ei] #set valid #choose the alpha that gives the maximum delta E
#找出合法的误差E值,即非零值
validEcacheList = nonzero(oS.eCache[:,0].A)[0]
#如果误差缓存中非零值存在,则找到使得误差最大的第二个变量的下标j
if (len(validEcacheList)) > 1:
for k in validEcacheList: #loop through valid Ecache values and find the one that maximizes delta E
if k == i: continue #don't calc for i, waste of time
Ek = calcEk(oS, k)
deltaE = abs(Ei - Ek)
if (deltaE > maxDeltaE):
maxK = k; maxDeltaE = deltaE; Ej = Ek
return maxK, Ej
#若是第一次运行,则误差缓存中为空,只能先随机选取alpha[j]
else: #in this case (first time around) we don't have any valid eCache values
j = selectJrand(i, oS.m)
Ej = calcEk(oS, j)
return j, Ej
#更新误差并存入缓存
def updateEk(oS, k):#after any alpha has changed update the new value in the cache
Ek = calcEk(oS, k)
oS.eCache[k] = [1,Ek]
#内循环,选择第二个alpha[j]并更新所选的两个变量
#大体上和simpleSMO内循环类似,只是选择alpha[j]时使用启发式方法,而不是随机选择。此外,更新alpha后,会将其对应的误差存入缓存,用作以后使用。
#这里使用了核函数
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)):
#启发式选择alpha[j]
j,Ej = selectJ(i, oS, Ei) #this has been changed from selectJrand
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.K[i,j] - oS.K[i,i] - oS.K[j,j] #changed for kernel
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)
#更新alpha后,将对应的误差存入缓存
updateEk(oS, j) #added this for the Ecache
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])#update i by the same amount as j
#更新alpha后,将对应的误差存入缓存
updateEk(oS, i) #added this for the Ecache #the update is in the oppostie direction
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]
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)): #full Platt SMO
oS = optStruct(mat(dataMatIn),mat(classLabels).transpose(),C,toler, kTup)
iter = 0
entireSet = True; alphaPairsChanged = 0
while (iter < maxIter) and ((alphaPairsChanged > 0) or (entireSet)):
alphaPairsChanged = 0
#遍历整个数据集对应的alpha[i]
if entireSet: #go over all
for i in range(oS.m):
alphaPairsChanged += innerL(i,oS)
print ("fullSet, iter: %d i:%d, pairs changed %d" % (iter,i,alphaPairsChanged))
iter += 1
#仅遍历不在边界上的alpha[i],即大于0小于C
else:#go over non-bound (railed) alphas
nonBoundIs = nonzero((oS.alphas.A > 0) * (oS.alphas.A < C))[0]
for i in nonBoundIs:
alphaPairsChanged += innerL(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
#用alpha计算w
def calcWs(alphas,dataArr,classLabels):
X = mat(dataArr); labelMat = mat(classLabels).transpose()
m,n = shape(X)
w = zeros((n,1))
#虽然遍历了整个数据集,但是只有支持向量发挥了作用,只有支持向量的alpha不为0
for i in range(m):
w += multiply(alphas[i]*labelMat[i],X[i,:].T)
return w
#测试一下数据
dataArr, labelArr = loadDataSet('testSet6.txt')
b, alphas = smoP(dataArr,labelArr,0.6,0.001,40)
dataMat = mat(dataArr)
ws = calcWs(alphas,dataArr,labelArr)
print('The predicted label is: ',dataMat[0]*mat(ws)+b)
print('The actual label is: ', labelArr[0])