两篇文章阐述了支持向量机的原理,在数学的海洋中遨游了快一周,实在撑不下去了,现在准备亲自来实现一把支持向量机的学习算法,序列最小最优化算法,依然需要数学知识和少量的编程基础。参考的书籍为李航的《统计学习方法》和Peter Harrington的《机器学习实战》,参考的学习算法为LIBSVM,以及一篇求解凸二次规划问题的论文-Sequential Minimal Optimization A Fast Algorithm for Training Support Vector Machines.
SMO算法又称序列最小最优化算法,本文讨论支持向量机学习的实现问题。我们知道,支持向量机的学习问题可以形式化为求解凸二次规划问题。这样的凸二次规划问题具有全局最优解,并且有许多最优化算法可以用于这一问题的求解。但是训练样本容量很大时,这些算法往往会变得非常低效,以致于无法使用。所以,如何高效地实现支持向量机学习就成为一个重要的问题。目前人们已提出许多快速实现算法。本节讲述其中的序列最小最优化算法,这种算法1998年由Platt提出。
SMO算法要求解如下凸二次规划的对偶问题:
SMO算法是一种启发式算法,其基本思路是:如果所有变量的解都满足此最优化问题的KKT条件,那么这个最优化问题的解就都得到了。因为KKT条件就是该最优化问题的充分必要条件。(KKT条件是什么?)否则,选择两个变量,固定其他变量,针对两个变量构建一个二次规划问题。(求解子问题?)这个二次规划问题关于这两个变量的解应该更接近原始二次规划问题的解,因为这会使得原始二次规划问题的目标函数值变得更小。(云里雾里。。。大致含义是,每次针对两个变量来求解目标函数的最小值,求解完后,继续寻找新的变量求目标函数,在每次寻找新 αi 的过程中,目标函数将进一步得到优化,直到所有的 αi 更新完毕?有待验证!)重要的是,这时,子问题可以通过解析方法求解,这样就可以大大提高整个算法的计算速度。子问题有两个变量,一个是违反KKT条件最严重的那一个,另一个由约束条件自动确定。如此,SMO算法将原问题不断分解为子问题并对子问题求解,进而达到求解原问题的目的。
注意,子问题的两个变量中只有一个是自由变量。假设 α1,α2 为两个变量, α3,α4,...αN 固定,那么由等式约束可知
整个SMO算法包括两个部分:求解两个变量二次规划的解析方法和选择变量的启发式方法。
不失一般性,假设选择的两个变量是 α1,α2 ,其他变量 αi(i=3,4,...,N) 是固定的。于是SMO的最优化问题的子问题可以写成:
为了求解两个变量的二次规划问题,首先分析约束条件,然后在此约束条件下求极小。
由于只有两个变量 (α1,α2) ,约束可以用二维空间中的图形表示。如下图所示:
y1≠y2⇒α1−α2=ζ (由约束条件求得)
y1=y2⇒α1+α2=ζ (由约束条件求得)
不等式约束使得 (α1,α2) 在盒子 [0,C]×[0,C] 内,等式约束使 (α1,α2) 在平行于盒子 [0,C]×[0,C] 的对角线的直线上。因此要求的是目标函数在一条平行于对角线的线段上的最优值。这使得两个变量的最优化问题成为实质上的单变量的最优化问题,不妨考虑为变量 α2 的最优化问题。
假设问题的初始可行解为 αold1,αold2 ,最优解为 αnew1,αnew2 ,并且假设在沿着约束方向未经剪辑时 α2 的最优解为 αnew,unc2.
由于 αnew2 需要满足不等式约束,所以最优值 αnew2 的取值范围必须满足条件:
规定完 α2 的取值范围时,便需要根据某种步长,来不断更新 α2 ,从而使得目标函数最小,并达到一个收敛的状态,如下式:
为何 α2 的更新步长为 μ 式呢?这需要理论证明一把!
证明:引进记号
疑问: α2 如何从一个初始值,是慢慢逼近正确值,还是一次逼近正确值?
SMO算法在每个子问题中选择两个变量优化,其中至少一个变量是违法KKT条件的。
1.第一个变量的选择
SMO称选择第1个变量的过程为外层循环。外层循环在训练样本中选取违反KKT条件最严重的样本点,并将其对应的变量做为第1个变量。具体地,检验训练样本点 (xi,yi) 是否满足KKT条件,即
该检验是在 ϵ 范围内进行的。在检验过程中,外层循环首先遍历所有满足条件 0≤αi≤C 的样本点,即在间隔边界上的支持向量点,检验它们是否满足KKT条件。如果这些样本点都满足KKT条件,那么遍历整个训练集,检验它们是否满足KKT条件。
2.第2个变量的选择
SMO称选择第2个变量的过程为内层循环。假设在外层循环中已经找到第1个变量 α1 ,现在要在内层循环中找第2个变量 α2 。第2个变量选择的标准是希望能使 α2 有足够大的变化。
αnew2 是依赖于 |E1−E2| 的,为了加快计算速度,一种简单的做法是选择 α2 ,使其对应的 |E1−E2| 最大。因为 α1 已定, E1 也确定了。为了节省时间,将所有 Ei 值保存在一个列表中。
在特殊情况下,如果内层循环通过以上方法选择的 α2 不能使目标函数有足够的下降,那么采用以下启发式规则继续选择 α2 。遍历在间隔边界上的支持向量点,依次将其对应的变量作为 α2 试用,直到目标函数有足够的下降。若找不到合适的 α2 ,那么遍历训练数据集;若仍找不到合适的 α2 ,则放弃第1个 α1 ,再通过外层循环寻求另外的 α1 .
3.计算阈值b和差值 Ei
在每次完成两个变量的优化后,都要重新计算阈值b。当 0<αnew1<C 时,由KKT条件可知:
在每次完成两个变量的优化之后,还必须更新对应的 Ei 值,并将它们保存在列表中。 Ei 值的更新要用到 bnew 值,以及所有支持向量对应的 αj :
算法(SMO算法)
输入:线性可分训练数据集
T={(x1,y1),(x2,y2),...,(xN,yN)}其中, xi∈X=Rn,yi∈Y={−1,+1},i=1,2,...N ,精度 ϵ
输出:近似解 α^
(1) 取初值 α(0)=0 ,令 k=0 ;
(2) 选取优化变量 α(k)1,α(k)2 ,解析两个变量的最优化问题,求得最优解 α(k+1)1,α(k+1)2 ,更新 α为α(k+1) ;
(3) 若在精度 ϵ 范围内满足停机条件
∑i=1Nαiyi=0
0≤αi≤C,i=1,2,...,N
yig(xi)=⎧⎩⎨≥1,{xi|αi=0}=1,{xi|0<αi<C}≤1,{xi|αi=C}
其中,
g(xi)=∑j=1NαjyjK(xi,xj)+b
则转(4);否则令 k=k+1, 转(2)
(4) 取 α^=α(k+1) .
Platt SMO算法的完整版实现需要大量代码。在接下来的第一个例子中,我们将会对算法进行简化处理,以便了解算法的基本工作思路,之后基于简化版给出完整版。
在编写python代码时,需要导入一个第三方依赖库numpy,具体的安装过程及库API请查看官方文档。
训练集为:testSet.txt
3.542485 1.977398 -1
3.018896 2.556416 -1
7.551510 -1.580030 1
2.114999 -0.004466 -1
8.127113 1.274372 1
7.108772 -0.986906 1
8.610639 2.046708 1
2.326297 0.265213 -1
3.634009 1.730537 -1
0.341367 -0.894998 -1
简化版platt SMO的支持函数
加载代码和辅助函数
# 统一对数据做预处理操作,生成输入空间,标签向量
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
# 对第二次alpha进行随机选择
def selectJrand(i,m):
j =i;
while(j==i):
j = int(random.uniform(0,m))
return j
# 对alpha[j]进行剪辑操作
def clipAlpha(aj,H,L):
if aj > H:
aj = H
if L > aj:
aj = L
return aj
def smoSimple(dataMatIn,classLabels,C,toler,maxIter):
# 输入空间
dataMatrix = mat(dataMatIn)
# 标签向量
labelMat = mat(classLabels).transpose()
b =0
# m为样本数量 n为特征向量维度
m,n=shape(dataMatrix)
# alphas a=(a1,a2,a3,...,am)
alphas = mat(zeros((m,1)))
# 迭代次数
iter =0
while(iter < maxIter):
alphaPairsChanged =0
for i in range(m):
# 求得 每个样本点的fxi 和 Ei
fxi = float(multiply(alphas,labelMat).T*\
(dataMatrix*dataMatrix[i,:].T))+b
Ei = fxi -float(labelMat[i])
# 寻找不是支持向量机的点,并且不符合KKT条件,需要进行对alphas的更新
if ((labelMat[i]*Ei < -toler) and (alphas[i] < C)) or\
((labelMat[i]*Ei > toler) and \
(alphas[i] >0 )):
# 选择不是i的其他样本点,这里都是进行随机的选择
j = selectJrand(i,m)
fxj = float(multiply(alphas,labelMat).T*\
(dataMatrix*dataMatrix[j,:].T))+b
Ej = fxj - float(labelMat[j])
# 寻找目前的alpha值
alphaIold = alphas[i].copy()
alphaJold = alphas[j].copy()
# alphas 满足的约束条件,在一次更新后,就需要满足
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.0*dataMatrix[i,:]*dataMatrix[j,:].T - \
dataMatrix[i,:]*dataMatrix[i,:].T - \
dataMatrix[j,:]*dataMatrix[j,:].T
if eta >=0: print ("eta >=0");continue
# 更新alphas值
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 (0 < alphas[i]) and (C > alphas[i]): b = b1
elif (0 < alphas[j]) and (C > alphas[j]): 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)
print (alphas[alphas > 0])
return b,alphas
还记得在上篇文章中讲述感知机算法中所推导出的 G′′ 矩阵嘛?本文在分析简单SMO算法时,继续根据 G′′ 进行分析。从而对SMO算法有更加直观的理解。
回顾下, G′′ 的矩阵式子为:
在外层循环时,算法计算了fxi和Ei,从矩阵 G′′ 中可以看出,fxi的计算是对第i列的所有元素进行累加,即是我们进行分类预测的确信度。Ei是在第i列中,确信度减去分类标签 yi 后的“误差”。为何定义Ei,主要有以下两点:第一,在寻找不符合KKT条件的列时,我们需要借助Ei来做判断;第二,当更新alpha对时,对b的更新需要用到Ei作为计算变量。
第一个判断语句的作用,用公式叙述便是,寻找符合:
接下来,我们定义了L和H,根据alpha对不同的 yi ,L和H的定义有所区别。这里L和H的范围限制,是为了限制在后续更新alpha对时,始终让每个alpha对,符合约束条件 0≤αi≤C .原因在于,从L和H的定义出发,它们符合 αiyi+αjyj=ζ ,且均属于 [0,C]×[0,C] 的盒子内,所以只要alpha对经过剪辑,由 αj 算出的 αi 均在这个范围内。因此,在对有范围的约束条件时,当符合两变量相加为某一常数时,需要对两变量进行剪辑操作。否则,更新规则将导致两变量不在盒子内,却符合算法进程,从而计算出与实际有偏差的解。(遗留:L==H时,为何可以跳过后续算法步骤?)
算法顺利进展到这步,我们找到了合适alpha对,因此该目标函数也就简化为对 minαi,αjW(αi,αj) 求解最小值。对该目标函数求解偏导后,得到了 αnewj 关于 αoldj 的更新公式。也就有了后续的算法步骤,求解 eta,alphas[j],b1,b2 等过程。更新一次alpha对,便能对该目标函数进行一次最小化。第一次循环结束,便开始构造第二个alpha对,每次对不同的alpha对做更新操作,直到遍历结束。那么问题来了,该算法什么时候结束呢?或者说停机的条件是什么?
这里我们先假设两个显而易见的结论是正确的:第一,一次迭代过程,是无法使得 G′′ 中的每一列符合KKT条件约束的,但随着迭代次数的增多,alpha对的随机选择,能够使得矩阵 G′′ 的每一列都符合KKT条件。第二,对n次alpha对进行子目标函数的最小化,最终反映在终极目标函数上。有了上述两条假设,我们便可以解释,在进入第一个for循环遍历时的if语句了,该语句便是我们的停机条件,当且仅当 G′′ 中的所有列符合KKT条件时,不再进行alpha对的挑选以及更新。某列不符合时,对其进行更新,直到符合KKT。从算法的角度来看,KKT条件便是求解目标函数极小值的充分必要条件,也显而易见,为何需要KKT,进行条件的转换,无非是为了能够在算法代码层面,能够找到代码表达式等价于数学求解问题,以程序的方式解决数学问题。类比感知机算法,它的停机条件便是使得 G′′ 中的每一列求和项 sum∗yj>0 .此处只是变成了KKT不等式条件。(待解决:程序alpha对的更新,如何反映几何函数间隔的变化过程,很难建立联系。)
Platt SMO算法是通过一个外循环来选择第一个alpha值的,并且其选择过程会在两种方式之间进行交替:一种方式是在所有数据集上进行单遍扫描,另一种方式则是在非边界alpha中实现单遍扫描。而所谓非边界alpha指的就是那些不等于边界0或C的alpha值。对整个数据集的扫描相当容易,而实现非边界alpha值的扫描时,首先需要建立这些alpha值的列表,然后再对这个表进行遍历。同时,该步骤会跳过那些已知的不会改变的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 = float(multiply(oS.alphas,oS.labelMat).T*\
(oS.X*oS.X[k,:].T))+oS.b
Ek = fxk - float(oS.labelMat[k])
return Ek
# 每次计算一次Ei时,会把Ei放入缓存中
def selectJ(i,oS,Ei):
maxK =-1; maxDeltaE = 0; Ej = 0
# 1在这里的作用
oS.eCache[i] = [1,Ei]
# 寻找第0列非零元素的下标值
validEacheList = nonzero(oS.eCache[:,0].A)[0]
# 有多少非零元素,该list便有多长
if (len(validEacheList)) >1:
for k in validEacheList:
# 自身没有比较的意思
if k ==i:continue
Ek = calcEk(oS,k)
deltaE = abs(Ei-Ek)
# choose j for maximum step size
if(deltaE > maxDeltaE):
maxK = k; maxDeltaE = deltaE; Ej = Ek
return maxK,Ej
# 随机选择
else:
j = svm.selectJrand(i,oS.m)
Ej = calcEk(oS,j)
return j,Ej
def updateEk(oS,k):
Ek = calcEk(oS,k)
oS.eCache[k]=[1,Ek]
SMO优化例程
这里的算法与简单SMO内层算法相一致,只增加了对Ej,Ei的缓存。
# 替代了循环遍历G''的每一列
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] = svm.clipAlpha(oS.alphas[j],H,L)
# 更新了alphas[j]带来了Ej的更新
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])
# 更新了alphas[i]带来了Ei的更新
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
完整版platt SMO的外循环代码
该函数完成了两步优化过程,在选择第一个alpha值后,算法会通过一个内循环来选择第二个alpha值。在优化过程中,会通过最大化步长的方式来获得第二个alpha值。第二步优化为,数据集全程扫描策略与在非边界alpha对中进行更新策略交替进行。(为何是对非边界进行alpha对的更新能够使得算法性能提升?)
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(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))
iter +=1
else:
# 两个元素乘积非零,每两个元素做乘法[0,1,1,0,0]*[1,1,0,1,0]=[0,1,0,0,0]
nonBoundIs = nonzero((oS.alphas.A > 0)*(oS.alphas.A < C))[0]
for i in nonBoundIs:
alphaPairsChanged += innerL(i,oS)
print("nou-bound,iter: %d i:%d,pairs changed %d" %\
(iter,i,alphaPairsChanged))
iter +=1
# entireSet 控制交替的策略选择
if entireSet: entireSet = False
# 必须有alpha对进行更新
elif(alphaPairsChanged == 0): entireSet = True
print("iteration number:%d" % iter)
return oS.b,oS.alphas
上述算法均是针对线性可分的支持向量机算法,现在需要实现一把核函数,用来支持非线性可分的数据集。
核函数编写:
def kernelTrans(X,A,kTup):
"""
核函数
:param X:全部向量
:param A:某个向量
:param kTup:核函数名称
:return: :raise NameError:
"""
# 5**2 = 25 **表示乘方
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):
deltRow = X[j,:]-A
K[j] = deltRow*deltRow.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): # 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))) #第一列是有效与否的标记
self.K = mat(zeros((self.m,self.m)))
for i in range(self.m):
self.K[:,i] = kernelTrans(self.X, self.X[i,:], kTup) #软cache
kernel函数其实就是封装了我们计算 G′′ 矩阵的过程,只是少了 αi和yi .
为了使用核函数,先前的两个函数innerL()和calcEk()的代码需要做些修改。修改的结果如下。
innerL():
eta = 2.0*oS.K[i,j]-oS.K[i,i]-oS.K[j,j]
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]
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
接下来我们将构建对上图的数据点进行有效分类的分类器,该分类器使用了高斯核函数。前面提到的高斯核函数有一个用户定义的输入 δ .首先,我们需要确定它的大小,然后利用该核函数构建出一个分类器。整个测试函数如下:
def testRbf(k1 =1.3):
dataArr,labelArr = svmMLiA.loadDataSet('testSetRBF.txt')
b,alphas =opt.smoP(dataArr,labelArr,200,0.0001,10000,('rbf',k1))
dataMat = mat(dataArr);labelMat = mat(labelArr).transpose()
svInd = nonzero(alphas.A >0)[0]
svs = dataMat[svInd]
labelSV = labelMat[svInd]
print("there are %d support vectors" %shape(svs)[0])
m,n = shape(dataMat)
errorCount =0
for i in range(m):
# 生成Gramm矩阵
kernelEval = opt.kernelTrans(svs,dataMat[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 =svmMLiA.loadDataSet('testSetRBF2.txt')
errorCount =0
dataMat = mat(dataArr);
labelMat = mat(labelArr).transpose()
m,n = shape(dataMat)
for i in range(m):
# 生成Gramm矩阵
kernelEval = opt.kernelTrans(svs, dataMat[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))
上述代码用到的训练数据集为:testSetRBF.txt和testSetRBF2.txt
当k1=1.3时,测试结果如下:
there are 27 support vectors
the training error rate is: 0.030000
the test error rate is: 0.080000
试着减小k1=0.1时,测试结果如下:
there are 26 support vectors
the training error rate is: 0.130000
the test error rate is: 0.270000
发现支持向量数目增多了,同时错误率也下降了,但是训练速度显著减慢。
综上,支持向量机的SMO算法已经全部阐述完毕,从最开始的简单SMO算法,其实是在选择alpha对时,无脑的进行随机选择;到后来完整版SMO算法,有策略性的选择alpha对,可以使得迭代速度加快;最后又增加了核函数,能够让支持向量机区分非线性数据。在学习SMO算法中,令人映象深刻的便是对支持向量机的数学定义如何转化为凸二次规划问题,最终在程序中找到了等价的KKT条件,从而能够让数学形式在代码中有了易于表达的停机条件,从而再转化为求解目标函数的子序列极小值的过程,随机选择alpha对,达到整个目标函数最优值。实在是高!