1.完整SMO算法实现
现在给出完整版的SMO算法,主要体现在参数alpha的选择上。
具体选择过程如下:
第一个参数:
①遍历一遍整个数据集,对每个不满足KKT条件的参数,选作第一个待修改参数
②在上面对整个数据集遍历一遍后,选择那些参数满足的子集,开始遍历,如果发现一个不满足KKT条件的,作为第一个待修改参数,然后找到第二个待修改的参数并修改,修改完后,重新开始遍历这个子集
③遍历完子集后,重新开始①②,直到在执行①和②时没有任何修改就结束。
第二个参数:
①启发式找,找能让最大的
②寻找一个随机位置的满足的可以优化的参数进行修改
③在整个数据集上寻找一个随机位置的可以优化的参数进行修改
④都不行那就找下一个第一个参数。
代码如下所示:
1.定义类来保存数据矩阵 类别向量 容错率 常量C 参数alpha 参数b 每个样本数据误差的缓存
#定义类来保存变量数据以及缓存结果
class optStructK:
def __init__(self,dataMatIn, classLabels, C, toler): # 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
#m*2 第一列是否有效标志 第二列误差值
self.eCache = mat(zeros((self.m,2))) #first column is valid flag
2.计算第k个样本与真实值的误差值
#计算第k个样本与真实值的误差值
def calcEkK(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
3.更新第k个样本误差值的缓存数据
#更新第k个样本的误差值
def updateEkK(oS, k):#after any alpha has changed update the new value in the cache
Ek = calcEk(oS, k)
oS.eCache[k] = [1,Ek]
4.选择第二个参数的方式:如果没有误差缓存则随机选取 若有则遍历所有的误差,从中选择|Ei-Ej|最大的参数作为alphaj 启发式算法,两个样本差距越大,对于结果的优化程度越高
def selectJK(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
validEcacheList = nonzero(oS.eCache[:,0].A)[0]
#如果有之前缓存结果,就选取|Ei-Ej|绝对值最大的j 否则随机返回一个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
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
5.根据选择的第一个参数 启发式选择第二个参数 然后进行二次规划迭代,更新alpha b 以及误差缓存的值,返回值表示是否有一对alpha发生了迭代变化。
def innerLK(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) #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.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) #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
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.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
6.总体SMO算法:第一个参数选择两种方式 第一种全局扫描选取 第二种在非边界上的集合进行扫描选取,如果alphaPairsChanged=0,这说明进行一次扫描后没有参数alpha进行优化处理,则继续进行全局扫描。
def smoPK(dataMatIn, classLabels, C, toler, maxIter): #full Platt SMO
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: #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
# 在非边界上实现单边扫描
else:#go over non-bound (railed) alphas
#返回参数在0-c之间的列表
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
比较运行时间,迭代次数 容错率参数修改 常量C修改对于结果的影响。
d, l = svm.loadDataSet('testSet.txt')
b, a = svm.smoP(d, l, 0.6, 0.001, 40)
w = svm.calcWs(a, d, l)
svm.plotfig_SVM(d, l, w, b, a)
从左到右依次为C=0.6 t=0.001 C=6 t=0.001 C=0.6 t=0.1
容错率决定了对于样本分类预测值与真实值的允许误差
常数C决定了样本间隔与分类间隔的比重 C越大则分类器试图将越多的样本点正确分类
2.核函数应用
如果样本数据在给定的特征向量空间并不是直接线性可分的,可以进行空间映射到更高维空间,一直到实现线性可分。
假设映射为:
SVM最优化问题变化为:
其中以上涉及到无限维的向量运算 因此引入核函数,假定:
用原先维度的向量运算来代替无限维的向量运算。
优化问题变为:
最终解变为:
这里采用高斯核:
径向基函数 用来度量两个向量的距离
1. 计算某个向量A与样本集合X中每一个样本的核函数
def kernelTrans(X, A, kTup): #calc the kernel or transform data to a higher dimensional space
#样本总数 特征总数
m,n = shape(X)
# 用来存储m个核函数A与X的每一行 避免重复计算
K = mat(zeros((m,1)))
#kTup 第一维是核函数类型 线性核
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
#同时进行m个核函数计算
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
2.初始化参数类,计算核函数矩阵
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))) #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)
3.将原先向量的运算转为核函数运算:
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) #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)
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
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类
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
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
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
加载数据集进行核函数的测试: 首先训练样本集 然后测试集测试 给出错误率等。
def testRbf(k1=1.3):
#加载训练集
dataArr,labelArr = loadDataSet('testSetRBF.txt')
b,alphas = smoP(dataArr, labelArr, 200, 0.0001, 10000, ('rbf', k1)) #C=200 important
datMat=mat(dataArr); labelMat = mat(labelArr).transpose()
#大于0的alpha参数
svInd=nonzero(alphas.A>0)[0]
#特征向量组成的矩阵
sVs=datMat[svInd] #get matrix of only support vectors
#特征向量对应的分类
labelSV = labelMat[svInd];
print ("there are %d Support Vectors" % shape(sVs)[0])
m,n = shape(datMat)
errorCount = 0
for i in range(m):
#计算第i个样本与特征矩阵的核函数
kernelEval = kernelTrans(sVs,datMat[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 = loadDataSet('testSetRBF2.txt')
errorCount = 0
datMat=mat(dataArr); labelMat = mat(labelArr).transpose()
m,n = shape(datMat)
for i in range(m):
kernelEval = kernelTrans(sVs,datMat[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))
应用核函数,计算一个新样本的预测值:最终结果只与支持向量有关
#计算第i个样本与特征矩阵的核函数
kernelEval = kernelTrans(sVs,datMat[i,:],('rbf', k1))
#预测值
predict=kernelEval.T * multiply(labelSV,alphas[svInd]) + b
参数k1是如何影响结果的:参数k1代表了每个支持向量对于结果的影响程度,当k1减少时,每个支持向量对于结果影响减少,则支持向量数量需要增加。当k1减少时,支持向量增加 训练错误率减小 测试错误率上升 即k1存在一个最优值。
k1=1.3时,
k=0.8
k1=0.5
k=0.3
k1=0.1
3.SVM手写识别问题
数据样本如下所示:
数据集中只有数字1和9 每个文件都是32X32数字组成。
将每一个二维数组32X32转换为一维向量1X24
#文档内容转为一维向量1x1024
def img2vector(filename):
returnVect = zeros((1,1024))
fr = open(filename)
for i in range(32):
lineStr = fr.readline()
for j in range(32):
returnVect[0,32*i+j] = int(lineStr[j])
return returnVect
读取所有的训练集,处理返回数据矩阵以及类别列向量
#读取所有训练数据集合 转化为数据矩阵以及类别向量
def loadImages(dirName):
from os import listdir
hwLabels = []
trainingFileList = listdir(dirName) #load the training set
m = len(trainingFileList)
trainingMat = zeros((m,1024))
for i in range(m):
fileNameStr = trainingFileList[i]
fileStr = fileNameStr.split('.')[0] #take off .txt
classNumStr = int(fileStr.split('_')[0])
if classNumStr == 9: hwLabels.append(-1)
else: hwLabels.append(1)
trainingMat[i,:] = img2vector('%s/%s' % (dirName, fileNameStr))
return trainingMat, hwLabels
使用训练集进行训练,然后测试集测试,观察核函数的选择对于结果的影响:
def testDigits(kTup=('rbf', 10)):
dataArr,labelArr = loadImages('trainingDigits')
#训练SVM
b,alphas = smoP(dataArr, labelArr, 200, 0.0001, 10000, kTup)
datMat=mat(dataArr); labelMat = mat(labelArr).transpose()
svInd=nonzero(alphas.A>0)[0]
sVs=datMat[svInd]
labelSV = labelMat[svInd];
print ("there are %d Support Vectors" % shape(sVs)[0])
m,n = shape(datMat)
errorCount = 0
#训练
for i in range(m):
kernelEval = kernelTrans(sVs,datMat[i,:],kTup)
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 = loadImages('testDigits')
errorCount = 0
datMat=mat(dataArr); labelMat = mat(labelArr).transpose()
m,n = shape(datMat)
#开始测试
for i in range(m):
kernelEval = kernelTrans(sVs,datMat[i,:],kTup)
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))
使用线性核: svm.testDigits(kTup=('lin',20))
svm.testDigits(kTup=('rbf',0.1))
svm.testDigits(kTup=('rbf',5))
svm.testDigits(kTup=('rbf',10))
svm.testDigits(kTup=('rbf',50))
svm.testDigits(kTup=('rbf',100))
由以上可知,随着k1的逐渐增大,支持向量数量减少 训练错误率增加 测试错误率先减少后增加 大约等于10处取最优值。
4.高斯核函数
高斯核函数原理以及参数选择问题:
1.高斯核函数进行向量无限维运算的推导
取分母等于1,进行计算,泰勒展开式:高斯核函数可以表示为两个无限维向量的乘积
2.高斯核函数对于样本在新特征空间的分布情况
高斯核函数对应的映射函数将样本投射到一个无限维的空间中去,映射到的新空间后,所有的样本点分布在以原点为圆心半径为1的1/4球面上。
证明如下:
如果选得很大,高次特征上的权重衰减得非常快,实际上(数值上近似一下)相当于一个低维的子空间;
如果选得很小,可以将任意的数据映射为线性可分,随之而来的可能是非常严重的过拟合问题。