目录
6.1 基于最大间隔分隔数据
6.2 寻找最大间隔
6.2.1 分类器求解的优化问题
6.2.2 SVM应用的一般框架
6.3 SMO高效优化算法
6.3.1 Platt的SMO算法
6.3.2 应用简化版SMO算法处理小规模数据集
6.4利用完整Platt SMO算法加速优化
6.5 在复杂数据上应用核函数
6.5.1 利用核函数将数据映射到高维空间
6.5.2 径向基核函数
6.5.3 在测试中使用核函数
本章小结
实现svm有一个很好用的工具包:libsvm,下载地址:https://www.csie.ntu.edu.tw/~cjlin/libsvm/index.html
优点:泛化错误率低,计算开销不大,结果易解释。
缺点:对参数调节和核函数的选择敏感,原始分类器不加修改仅适用于处理二类问题。
适用数据类型:数值型和标称型数据。
如上图所示:当两组数据分隔的足够开,很容易就可以在图上画出一条直线将两组数据点分开时,这组数据被称为线性可分数据,将数据集分隔开来的直线称为分隔超平面。数据点都在二维平面上时,分隔超平面就只是一条直线;数据集是三维时,用来分隔数据的就是一个平面;更高维的就叫超平面,也就是分类的决策边界。分布在超平面一侧的所有数据都属于某个类别,而分布在另一侧的所有数据则属于另一个类别。
我们希望采用这种方式来构建分类器,即如果数据点离决策边界越远,那么其最后的预测结果也就越可信。考虑上图B、C、D中的三条直线,它们都能将数据分隔开,但是其中哪一条最好呢?我们采用的是找到离分隔超平面最近的点,确保它们离分隔面的距离尽可能远。这里点到分隔面的距离被称为间隔。我们希望间隔尽可能大,这是因为如果我们犯错或者在有限数据上训练分类器的话,我们希望分类器尽可能健壮。
支持向量就是离分隔超平面最近的那些点,接下来要试着最大化支持向量到分隔面的距离,需要找到此问题的优化求解方法。
如何求解数据集的最佳分隔直线?先来看下图,分隔超平面的形式可以写成。要计算点A到分隔超平面的距离,就必须给出点到分隔面的法线或垂线的长度,该值为。这里的常数b类似于Logistic回归中的截距。这里的向量w和常数b一起描述了所给数据的分隔线或超平面。接下来讨论分类器。
输入数据给分类器会输出一个类别标签,这相当于一个类似于Sigmoid的函数在作用。下面将使用海维赛德阶跃函数(即单位阶跃)的函数对作用得到,其中当u<0时,f(u)输出-1,反之则输出+1.这和Logistic回归有所不同,那里的类别标签是0或1。
这里的类别标签为什么采用-1和+1,而不是0和1呢?这是由于-1和+1仅仅相差一个符号,方便数学上的处理。我们可以通过一个统一公式来表示间隔或者数据点到分隔超平面的距离,同时不必担心数据到底是属于-1还是+1类。
当计算数据点到分隔面的距离并确定分隔面的放置位置时,间隔通过来计算,这时就能体现出-1和+1类的好处了。如果数据点处于正方向(即+1类)并且离分隔超平面很远的位置时,会是一个很大的正数,同时也会是一个很大的正数。而如果数据点处于负方向(-1类)并且离分隔超平面很远的位置时,此时由于类别标签为-1,则仍然是一个很大的正数。
现在的目标就是找出分类器定义中的w和b。为此,我们必须找到具有最小间隔的数据点,而这些数据点也就是前面提到的支持向量。一旦找到具有最小间隔的数据点,我们就需要对该间隔最大化。这就可以写作:
直接求解上述问题相当困难,所以我们将它转换成为另一种更容易求解的形式。首先考察一下上式中大括号内的部分。由于对乘积进行优化是一件很讨厌的事情,因此我们要做的是固定其中一个因子而最大化其它因子。如果令所有支持向量的都为1,那么就可以通过求的最大值来得到最终解。但是并非所有数据点的都等于1,只有那些离分隔超平面最近的点得到的值采为1.而离超平面越远的数据点,其的值也就越大。
在上述优化问题中,给定了一些约束条件然后求最优值,因此该问题是一个带约束条件的优化问题。这里的约束条件就是。对于这类优化问题,有一个非常著名的求解方法,即拉格朗日乘子法。通过引入拉格朗日乘子,我们就可以基于约束条件来表述原来的问题。由于这里的约束条件都是基于数据点的,因此我们就可以将超平面写成数据点的形式,于是优化目标函数最后可以写成:
其约束条件为:
以上的描述都是基于一个假设:数据必须100%线性可分。但是几乎所有数据都不是完全线性可分的,这时就可以通过引入所谓松弛变量,来允许有些数据点可以处于分隔面的错误一侧。这样我们的优化目标就能保持仍然不变,但是此时新的约束条件则变为:
这里的常数C用于控制“最大化间隔”和“保证大部分点的函数间隔小于1.0”这两个目标的权重。在优化算法的实现代码中,常数C是一个参数,因此可以通过调节该参数得到不同的结果。一旦求出了所有的alpha,那么分隔超平面就可以通过这些alpha来表达。这一结论十分直接,SVM的主要工作是求解这些alpha。
SVM的一般流程:
(1)收集数据:可以使用任意方法。
(2)准备数据:需要数值型数据。
(3)分析数据:有助于可视化分隔超平面。
(4)训练算法:SVM的大部分时间都源自训练,该过程主要实现两个参数的调优。
(5)测试算法:十分简单的计算过程就可以实现。
(6)使用算法:几乎所有分类问题都可以使用SVM,值得一提的是,SVM本身是一个二分类器,对多分类问题应用SVM需要对代码做一些修改。
以前人们使用二次规划求解工具求解最优化问题,这种工具是一种用于在线性约束下优化具有多个变量的二次目标函数的软件。而这些二次规划求解工具则需要强大的计算能力支撑,另外在实现上也十分复杂。所有需要做的围绕优化的事情就是训练分类器,一旦得到alpha的最优值,我们就得到了分隔超平面并能够将之用于数据分类。
1996年,JohnPlatt发布了一个称为SMO的强大算法,用于训练SVM。SMO表示序列最小优化。Platt的SMO算法是将大优化问题分解为多个小优化问题来求解的。这些小优化问题往往很容易求解,并且对他们进行序列求解的结果将它们作为整体来求解的结果是完全一致的。在结果完全相同的同时,SMO算法的求解时间短很多。
SMO算法的目标是求出一系列alpha和b,一旦求出了这些alpha,就很容易计算出权重向量w并得到分隔超平面。
SMO算法的工作原理是:每次循环中选择两个alpha进行优化处理。一旦找到一对合适的alpha,那么就增大其中一个同时较小另一个。这里所谓的“合适”就是指两个alpha必须要符合一定的条件,条件之一就是这两个alpha必须要在间隔边界之外,而其第二个条件则是这两个alpha还没有进行过区间化处理或者不在边界上。
Platt SMO算法的完整实现需要大量代码。接下来的第一个例子对算法进行了简化处理,以便了解算法的基本工作思路。简化版代码虽然量少但执行速度慢。Platt SMO算法中的外循环确定要优化的最佳alpha对。而简化版却会跳过这一部分,首先在数据集上遍历每一个alpha,然后在剩下的alpha集合中随机选择另一个alpha。之所以这样做是因为有一个约束条件:
由于改变一个alpha可能会导致约束条件失效,因此总是同时改变两个alpha。
为此,将构建一个辅助函数,用于在某个区间范围内随机选择一个整数。同时,也需要另一个辅助函数,用于在数值太大时对其进行调整。下面的程序给出了这两个函数的实现,新建“svmMLiA.py”,写入以下代码:
from numpy import *
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
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 L>aj:
ai=L
return aj
制作分类数据集保存到testSet.txt文件中:
import random
for i in range(51):
#x=random.uniform(-1,3)
x=random.uniform(5, 10)
y=random.uniform(-4,4)
#print(x,'\t',y,'\t',-1)
print(x,'\t',y,'\t',1)
在数据集上应用‘svmMLiA.py’中的各个函数,其中‘selctJrand()’中的i是第一个alpha的下标,m是所有alpha的数目。只要函数值不等于输入i,函数就会进行随机选择。‘clipAlpha()’用于调整大于H或小于L的alpha值。
测试上述函数:
if __name__ == '__main__':
dataArr,labelArr=loadDataSet('testSet.txt')
print(labelArr)
输出结果:
可以看得出来,这里采用的类别标签是-1和1,而不是0和1.
上述工作完成之后,就可以使用SMO算法的第一个版本了。
该SMO函数的伪代码大致如下:
创建一个alpha向量并将其初始化为0向量
当迭代次数小于最大迭代次数时(外循环):
对数据集中的每个数据向量(内循环):
如果该数据向量可以被优化:
随机选择另外一个数据向量
同时优化这两个向量
如果两个向量都不能被优化,退出内循环
如果所有向量都没被优化,增加迭代数目,继续下一次循环
打开svmMLiA.py后输入以下程序代码:
from numpy import *
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
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 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=shape(dataMatrix)
alphas=mat(zeros((m,1)))
iter=0
while(itertoler) and (alphas[i]>0)):
j=selectJrand(i,m)
fxj=(multiply(alphas,labelMat)).astype(float).T*\
(dataMatrix*dataMatrix[j,:].T)+b
Ej=fxj-float(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+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*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
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(0alphas[i]):
b=b1
elif(0alphas[j]):
b=b2
else:
b=(b1+b2)/2
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
if __name__ == '__main__':
dataArr,labelArr=loadDataSet('testSet.txt')
#print(labelArr)
b,alphas=smoSimple(dataArr,labelArr,0.6,0.001,40)
print(b)
该函数有5个输入参数,分别是:数据集、类别标签、常数c、容错率和取消前最大的循环次数。每次循环中,将alphaPairsChanged先设为0,然后再对整个集合顺序遍历。变量alphaPairsChanged用于记录alpha是否已经进行优化。首先,fix能够计算出来,这是预测的类别。然后,基于这个实例的预测结果和真实结果的比对,就可以计算误差Ei。如果误差很大,那么可以对该数据实例所对应的alpha值进行优化。在if语句中,不管是正间隔还是负间隔都会被测试。并且在if语句中,也要同时检查alpha值,以保证其不能等于0或c。由于后面alpha小于0或大于c时将被调整为0或c,所以一旦在该if语句中它们等于这两个值的话,那么它们就已经在“边界”上了,因而不再能够减小或增大,因此就不再对它们进行优化了。
接下来,利用辅助函数随机选择第二个alpha值,即alpha[j]。同样,可以采用第一个alpha值(alpha[i])的误差计算方法,来计算这个alpha值的误差。这个过程可以通过copy()的方法来实现,因此稍后可以将新的alpha值与老的alpha值进行比较。python则会通过引用的方式传递所有列表,所以必须明确地告知python要为alphaIold和alphaJold分配新的内存;否则的话,在对新值和旧值进行比较时,就看不到新旧值的变化。之后开始计算L和H,它们用于将alpha[j]调整到0到c之间。如果L和H相等,就不做任何改变,直接执行continue语句。这在python中,则意味着本次循环结束直接进行下一次for的循环。
Eta是alpha[j]的最优修改量,如果eta为0,那就是说需要退出for循环的当前迭代过程。该过程对真实SMO算法进行了简化处理。如果eta为0,那么计算新的alpha[j]就比较麻烦了,现实中,这一情况并不常发生。于是,可以计算出一个新的alpha[j],然后利用辅助函数以及L与H值对其进行调整。
然后,就是需要检查alpha[j]是否有轻微改变。如果是的话,就退出for循环。然后,alpha[i]和alpha[j]同样进行改变,虽然改变的大小一样,但是改变的方向正好相反(即如果一个增加,那么另外一个减少)。在对alpha[i]和alpha[j]进行优化之后,给这两个alpha值设置一个常数项b。
最后,在优化过程结束的同时,必须确保在合适的时机结束循环。如果程序执行到for循环的最后一行都不执行continue语句,那么就已经成功地改变了一对alpha,同时可以增加alphaPairsChanged的值。在for循环之外,需要检查alpha值是否做了更新,如果有更新则将iter设为0后继续运行程序。只有在所有数据集上遍历maxIter次,且不再发生任何alpha修改之后,程序才会停止并退出while循环。
运行上述代码,迭代40次后得到b的值:
为了得到支持向量的个数,输入:
print(shape(alphas[alphas>0]))
运行结果:(1,4)
为了解哪些数据点是支持向量,输入:
for i in range(100):
if alphas[i]>0:
print(dataArr[i],labelArr[i])
运行结果:
[2.7842487287183553, -0.05754641163606955] -1.0
[2.7744955793870116, -1.8904062216981163] -1.0
[2.7849801075744813, -0.3151586065544034] -1.0
[5.123533544031859, -0.29524533476324244] 1.0
在原始数据集上对这些支持向量画圈,代码如下所示:
from numpy import *
import matplotlib.pyplot as plt
import svmMLiA
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
def draw_svm():
dataMat,labelMat=loadDataSet('testSet.txt')
dataArr=array(dataMat)
n=shape(dataArr)[0]
xcord1=[]
ycord1=[]
xcord2=[]
ycord2=[]
for i in range(n):
if int(labelMat[i])==1:
xcord1.append(dataArr[i,0])
ycord1.append(dataArr[i,1])
else:
xcord2.append(dataArr[i,0])
ycord2.append(dataArr[i,1])
return xcord1,xcord2,ycord1,ycord2
if __name__ == '__main__':
dataArr,labelArr=loadDataSet('testSet.txt')
b,alphas=svmMLiA.smoSimple(dataArr,labelArr,0.6,0.001,40)
svmx=[]
svmy=[]
for i in range(102):
if alphas[i]>0:
svmx.append(dataArr[i][0])
svmy.append(dataArr[i][1])
xcord1, xcord2, ycord1, ycord2=draw_svm()
fig=plt.figure()
ax=fig.add_subplot(111)
ax.scatter(xcord1,ycord1,s=30,color='red',marker='s')
ax.scatter(xcord2, ycord2, s=30, color='green')
ax.scatter(svmx, svmy, s=100, facecolor='none', edgecolors='b')
plt.show()
运行后得到带圆圈标记的支持向量:
在几百个点组成的小规模数据集上,简化版SMO算法的运行是没有问题的,但是在更大的数据集上的运行速度就会变慢。完整版的Platt SMO算法和简化版的SMO算法实现alpha的更改和代数运算的优化环节一模一样。在优化过程中,唯一的不同就是选择alpha的方式。完整版的Platt SMO算法应用了一些能够提速的启发方法。
Platt SMO算法是通过一个外循环来选择第一个alpha值的,并且其选择过程会在两种方式之间进行交替:一种方式是在所有数据集上进行单遍扫描,另一种方式则是在非边界alpha中实现单遍扫描。而所谓非边界alpha指的就是那些不等于边界0或c的alpha值。对整个数据集的扫描相当容易,而实现非边界alpha值的扫描时,首先需要建立这些alpha值的列表,然后再对这个表进行遍历。同时,该步骤会跳过那些已知的不会改变的alpha值。
在选择第一个alpha值后,算法会通过一个内循环来选择第二个alpha值。在优化过程中,会通过最大化步长的方式来获得第二个alpha值。在简化版SMO算法中,在选择j之后就计算了错误率Ej。这里会建立一个全局的缓存用于 保存误差值,并从中选择使得步长或者说Ei-Ej最大的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=(multiply(oS.alphas,oS.labelMat).T*(oS.X*oS.X[k,:].T)).astype(float)+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=nonzero(oS.eCache[:,0].A)[0]
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]
首要的事情就是建立一个数据结构来保存所有的重要值,而这个过程可以通过一个对象来完成。这里使用对象的目的并不是为了面向对象的编程,而只是作为一个数据结构来使用对象。在将值传给函数时,可以通过将所有数据移到一个结构中实现,这样就可以省掉手工输入的麻烦了。而此时,数据就可以通过一个对象来进行传递。实际上,当完成其实现时,可以很容易通过python的字典来完成。但是在访问对象成员变量时,这样做会有更多的手工输入操作,对比一下myObject.X和myObject['X']就可以知道这一点。为达到这个目的,需要构建一个仅包含init方法的optStruct类。该方法可以实现其成员变量的填充。除了增加一个mx2的矩阵成员变量eCache之外,这些做法和简化版SMO一模一样。eCache的第一列给出的是eCache是否有效的标志位,而第二列给出的是实际的E值。
对于给定的alpha值,第一个辅助函数calcEk()能够计算E值并返回。以前,该过程是采用内嵌的方式来完成的,但是由于该过程在这个版本的SMO算法中出现频繁,这里必须将其单独拎出来。
下一个函数selectJ()用于选择第二个alpha或者说内循环的alpha值。这里的目标是选择合适的第二个alpha值以保证在每次优化中采用最大步长。该函数的误差值与第一个alpha值Ei和下标i有关。首先将输入值Ei在缓存中设置成为有效的。这里的有效(valid)意味着它已经计算好了。在eCache中,代码nonzero(oS.eCache[:,0].A)[0]构建出了一个非零表。NumPy函数nonzreo()返回了一个列表,而这个列表中包含以输入列表为目录的列表值,这里的值并非0。nonzero()语句返回的是非零E值所对应的alpha值,而不是E值本身。程序会在所有的值上进行循环并选择其中使得改变最大的那个值。如果这是第一次循环的话,那么就随机选择一个alpha值。
最后一个辅助函数updataEk(),它会计算误差值并存入缓存当中。在对alpha值进行优化之后会用到这个值。
上述代码本身的作用并不大,但是当和优化过程及外循环组合在一起时,就能组成强大的SMO算法。
接下来将简单介绍一下用于寻找决策边界的优化例程。打开文本编辑器,添加以下代码:
def innerL(i,oS):
Ei=calcEk(oS,i)
if((oS.labelMat[i]*Ei<-oS.tol) and (oS.alphas[i]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*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)
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(0oS.alphas[i]):
oS.b=b1
elif(0oS.alphas[j]):
oS.b=b2
else:
oS.b=(b1+b2)/2
return 1
else:
return 0
上述代码和smoSimple()函数几乎一模一样,但是这里的代码已经使用了自己的数据结构。该结构在参数oS中传递。第二个重要的修改就是使用SelectJ()而不是selectJrand()来选择第二个alpha值。最后,在alpha值改变时更新Ecache。将上述过程打包在一起(即选择第一个alpha值的外循环):
#完整版Platt SMO的外循环代码
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(iter0) 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:
nonBoundIs=nonzero((oS.alphas.A>0)*(oS.alphas.A
上述完整版的Platt SMO算法,其输入和函数smoSimple()完全一样。函数一开始构建一个数据结构来容纳所有的数据,然后需要对控制函数退出的一些变量进行初始化。整个代码的主体是while循环,这与smoSimple()有些类似,但是这里的循环退出条件更多一些。当迭代次数超过指定的最大值,或者遍历整个集合都未对任意alpha对进行修改时,就退出循环。这里的maxIter变量和函数smoSimple()中的作用有一点不同,后者当没有任何alpha发生变化时会将整个集合的一次遍历过程计成一次迭代,而这里的一次迭代定义为一次循环过程,而不管该循环具体做了什么事。此时,如果在优化过程中存在波动就会停止,因此这里的做法优于smoSimple()函数中的计数方法。
while循环的内部与smoSimple()中有所不同,一开始的for循环在数据集上遍历任意可能的alpha。通过调用innerL()来选择第二个alpha,并在可能时对其进行优化处理。如果有任意一对alpha值发生改变,那么会返回1.第二个for循环遍历所有的非边界alpha值,也就是不在边界0或c上的值。
接下来,对for循环在非边界循环和完整遍历之间进行切换,并打印出迭代次数。最后程序将会返回常数b和alpha值。
运行程序:
if __name__ == '__main__':
dataArr,labelArr=loadDataSet('testSet.txt')
b, alphas = smoP(dataArr, labelArr, 0.6, 0.001, 40)
运行效果:
代码中常数c给出的是不同优化问题的权重。常数c一方面要保障所有样例的间隔不小于1.0,另一方面又要使得分类间隔要尽可能大,并且要在这两方面之间平衡。如果c很大,那么分类器将力图通过分隔超平面对所有的样例都正确分类。
利用alpha值进行分类时,首先必须基于alpha值得到超平面,这也包括了w的计算。下面列出的一个小函数可以用于实现上述任务:
def calcWs(alphas,dataArr,classLabels):
X=mat(dataArr)
labelMat=mat(classLabels).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
上述代码中最重要的部分是for循环,虽然在循环中实现的仅仅是多个数的乘积。看一下前面计算出的任何一个alpha,就不会忘记大部分alpha值为0.而非零alpha所对应的也就是支持向量。虽然上述for循环遍历了数据集中的所有数据,但是最终起作用只有支持向量。由于对w计算毫无作用,所以数据集的其它数据点也就会很容易地舍弃。
if __name__ == '__main__':
dataArr,labelArr=loadDataSet('testSet.txt')
b, alphas = smoP(dataArr, labelArr, 0.6, 0.001, 40)
ws=calcWs(alphas,dataArr,labelArr)
print(ws)
得到参数:[[ 0.44897833]
[-0.12557949]]
现对数据进行分类处理,比如说对第一个数据点分类,可以输入:
if __name__ == '__main__':
dataArr,labelArr=loadDataSet('testSet.txt')
b, alphas = smoP(dataArr, labelArr, 0.6, 0.001, 40)
ws=calcWs(alphas,dataArr,labelArr)
datMat=mat(dataArr)
res=datMat[0]*mat(ws)+b
print(res)
运行结果:[[-0.86866025]]
如果该值大于0,那么其属于1类;如果该值小于0,那么则属于-1类。对于数据点0,应该得到的类别标签是-1,可以通过如下的命令来确认分类结果的正确性:
print(labelArr[0])
运行结果:-1.0
至此,已经可以成功训练出分类器了。
先制作数据集(将生成的数据点保存在“complex_testSet.txt”文件中):
from sklearn.datasets import make_circles
import matplotlib.pyplot as plt
from pandas import DataFrame
# generate 2d classification dataset
X, y = make_circles(n_samples=200, noise=0.05)
# scatter plot, dots colored by class value
df = DataFrame(dict(x=X[:,0], y=X[:,1], label=y))
for i in range(200):
print(X[:,0][i],X[:,1][i],y[i])
colors = {0:'red', 1:'blue'}
fig, ax = plt.subplots()
grouped = df.groupby('label')
for key, group in grouped:
group.plot(ax=ax, kind='scatter', x='x', y='y', label=key, color=colors[key])
plt.show()
对于上图给出的数据,也可以像线性情况一样,利用强大的工具来捕捉数据中的这种模式。接下来,使用一种称为核函数(kernel)的工具将数据转换成易于分类器理解的形式。
上图中,数据点处于一个圆中,人类的大脑能够意识到这一点。然而,对于分类器而言,它只能识别分类器的结果是大于0还是小于0.如果只在x和y轴构成的坐标系中插入直线进行分类的话,并不会得到理想的结果。要对圆中的数据进行某种形式的转换,从而得到某些新的变量来表示数据。在这种表示情况下,更容易得到大于0或者小于0的测试结果。
在这个例子中,将数据从一个特征空间转换到另一个特征空间,在新空间下,可以很容易利用已有的工具对数据进行处理。数学家们喜欢将这个过程称之为从一个特征空间到另一个特征空间的映射。在通常情况下,这种映射会将低维特征空间映射到高维空间。
这种从某个特征空间到另一个特征空间的映射是通过核函数实现的。可以把核函数想象成一个包装器(wrapper)或者是接口(interface),它能把数据从某个很难处理的形式转换成为另一个较容易处理的形式。也可以将它想象成为另外一种距离计算的方法,距离计算的方法有很多种,核函数一样具有多种类型。经过空间转换之后,可以在高维空间中解决线性问题,这就等价于在低维空间中解决非线性问题。
SVM优化中一个特别好的地方就是,所有的运算都可以写成內积(inner product,也称点积)的形式。向量的內积指的是两个向量相乘,之后得到单个标量或者数值。可以把內积运算替换成核函数,而不必做简化处理。将內积替换成核函数的方式被称为核技巧(kernel trick)或者核“变电”(kernel substation)。
核函数并不仅仅应用于支持向量机,很多其他的机器学习算法也都用到核函数。
径向基函数是SVM中常用的一个核函数。径向基函数是一个采用向量作为自变量的函数,能够基于向量距离运算输出一个标量。这个距离可以是从<0,0>向量或者其它向量开始计算的距离。径向基函数的高斯版本公式:
其中,是用户定义的用于确定到达率(reach)或者说函数值跌落到0的速度参数。
上述高斯核函数将数据从其特征空间映射到更高维的空间,具体来说这里是映射到一个无穷维的空间。在上面的例子中,数据点基本上都在一个圆内,对于这个例子,可以直接检查原始数据,并意识到只要度量数据点到圆心的距离即可。然而,如果碰到了一个不是这种形式的新数据集,就会陷入困境。在该数据集上,使用高斯核函数可以得到很好的结果。当然,该函数也可以用于许多其他的数据集,并且也能得到低错误率的结果。
如果在svmMLiA.py文件中添加一个函数并稍做修改,那么就能够在已有代码中使用核函数。首先,打开svmMLiA.py代码文件并输入kernelTrans()。然后,对optStruct()类进行修改:
##转换核函数
def kernelTrans(X,A,kTup):
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):
deltaRow=X[j,:]-A
K[j]=deltaRow*deltaRow.T
K=exp(K/(-1*kTup[1]**2))
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):
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)
在optStruct类的新版本中,除了引入了一个新变量kTup之外,和原来的版本一模一样。kTup是一个包含核函数信息的元组,在初始化方法结束时,矩阵K先被构建,然后再通过调用函数kernelTrans()进行填充。全局的K值只需计算一次。然后,当想要使用核函数时,就可以对它进行调用,这也省去了很多冗余的计算开销。
当计算矩阵K时,该过程多次调用了函数kernelTrans()。该函数有3个输入参数:2个数值型变量和1个元组。元组kTup给出的是核函数的信息。元组的第一个参数是描述所用核函数类型的一个字符串,其它2个参数则都是核函数可能需要的可选参数。该函数首先构建出了一个列向量,然后检查元组以确定核函数的类型。这里只给出了2种选择,但是依然可以很容易地通过添加elif语句来扩展到更多选项。
在线性核函数的情况下,內积计算在“所有数据集”和“数据集中的一行”这两个输入之间展开。在径向基核函数的情况下,在for循环中对于矩阵的每个元素计算高斯函数的值。而在for循环结束之后,将计算过程应用到整个向量上去。值得一提的是,在numpy矩阵中,除法符号意味着对矩阵元素展开计算而不像在MATLAB中一样计算矩阵的逆。
最后,如果遇到一个无法识别的元组,程序就会抛出异常,因为在这种情况下不希望程序再继续运行,这一点相当重要。
为了使用核函数,先期的两个函数innerL()和calcEk()的代码需要做些修改:
def innerL(i,oS):
Ei=calcEk(oS,i)
if((oS.labelMat[i]*Ei<-oS.tol) and (oS.alphas[i]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*oS.K[i,j]- oS.K[i,i]-oS.K[j,j]
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)
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.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(0oS.alphas[i]):
oS.b=b1
elif(0oS.alphas[j]):
oS.b=b2
else:
oS.b=(b1+b2)/2
return 1
else:
return 0
def calcEk(oS,k):
fxk=((multiply(oS.alphas,oS.labelMat).T*oS.K[:,k])+oS.b).astype(float)
Ek=fxk-float(oS.labelMat[k])
return Ek
接下来构建一个对圆形数据点进行有效分类的分类器,该分类器使用了径向基函数。前面提到的径向基函数有一个用户定义的输入,首先,需要确定它的大小,然后利用该核函数构建出一个分类器,代码如下所示:
#利用核函数进行分类的径向基测试函数
def testRbf(k1=1.3):
dataArr,labelArr=loadDataSet('complex_testSet.txt')
b,alphas=smoP(dataArr,labelArr,200,0.0001,10000,('rbf',k1))
datMat=mat(dataArr)
labelMat=mat(labelArr).transpose()
svInd=nonzero(alphas.A>0)[0]
sVs=datMat[svInd]
labelSV=labelMat[svInd]
print('there are %d Support Vecture'%shape(sVs)[0])
m,n=shape(datMat)
errorCount=0
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 training error rate is:%f'%(float(errorCount)/m))
上述代码只有一个可选的输入参数,该输入参数是高斯径向基函数中的一个用户定义变量。整个代码主要是由以前定义的函数集合构成的。首先,程序从文件中读入数据集,然后在该数据集上运行Platt SMO算法,其中核函数的类型为‘rbf’。
优化过程结束后,在后面的矩阵数学运算中建立了数据的矩阵副本,并且找出那些非零的alpha值,从而得到所需要的支持向量;同时,也就得到了这些支持向量和alpha的类别标签值。这些值仅仅是需要分类的值。
整个代码中最重要的是for循环开始的那两行,它们给出了如何利用核函数进行分类。首先利用结构初始化方法中使用过的kernelTrans()函数,得到转换后的数据。然后,再用其与前面的alpha及类别标签值求积。其中需要特别注意的另一件事是,在这几行代码中,是如何做到只需要支持向量数据就可以进行分类的。除此之外,其他数据都可以直接舍弃。
测试上述代码:
if __name__=='__main__':
testRbf()
可以更换不同的k1参数以观测错误率、训练错误率、支持向量个数随k1的变化情况。
支持向量的数目存在一个最优值。SVM的优点在于它能对数据进行高效分类。如果支持向量太少,就可能会得到一个很差的决策边界;如果支持向量太多,就相当于每次都利用这个数据集进行分类,这种分类方法称为k近邻。
支持向量机是一种分类器。之所以成为“机”是因为它会产生一个二值决策结果,即它是一种决策“机”。支持向量机的泛化错误率较低,也就是说它具有良好的学习能力,且学到的结果具有很好的推广性。这些优点使得支持向量机十分流行,有些人认为它是监督学习中最好的定式算法。
支持向量机试图通过求解一个二次优化问题来最大化分类间隔。在过去,训练支持向量机常采用非常复杂并且低效的二次规划求解方法。John Platt引入了SMO算法,此算法可以通过每次只优化2个alpha值来加快SVM的训练速度。本章首先讨论了一个简化版所实现的SMO优化过程,接着给出了完整的Platt SMO算法。相对于简化版而言,完整版算法不仅大大地提高了优化的速度,还使其存在一些进一步提高运行速度的空间。有关这方面的工作,一个经常被引用的参考文献就是“Improvements to Platt's SMO Algorithm for SVM Classifier Design”。
核方法或者说核技巧会将数据(有时是非线性数据)从一个低维空间映射到一个高维空间,可以将一个在低维空间中的非线性问题转换成高维空间下的线性问题来求解。该方法不止在SVM中适用,还可以用于其他算法中。而其中的径向基函数是一个常用的度量两个向量距离的核函数。
支持向量机是一个二分类器。当用其解决多类问题时,则需要额外的方法对其进行扩展。SVM的效果也对优化参数和所用核函数中的参数敏感。