一,决策面方程
我们以二维平面为例,假设有一条直线,方程如下:
aX+bY+c = 0
我们可以将此直线向量化:
进一步简化为:
其中,,
我们假设:
然后在二维平面上画出直线和w:
我们可以看到w和直线是垂直的,w即为直线的法向量。
我们推广到多维空间也是一样,只不过:
二,分类间隔
假设,分类点和分类直线如下:
我们知道,点到直线的距离公式如下:
其中,x0,y0为平面上的点。Ax0+By0+c为直线方程。
结合上图,我们将直线方程改为向量形式,则正类直线方程为:,负类直线方程为:
则,我们可以很快求得:
我们要想直线能够更好的进行分类,则只需要间隔W值最大即可,因为上图可以知道,我们的目的为求解:
也相当于求解:
三,求解
求解最优值通常分为以下几类:
(1)无约束问题,如:
这类问题通常采用求导的方式获得,令其导数为0则可获得最优值。
(2)有等式约束问题,如:
这类问题,常常使用的方法就是拉格朗日乘子法(Lagrange Multiplier) ,即把等式约束hi(x)用一个系数与f(x)写为一个式子,称为拉格朗日函数,而系数称为拉格朗日乘子。通过拉格朗日函数对各个变量求导,令其为零,可以求得候选值集合,然后验证求得最优值。
(3)有不等式约束的问题,如:
显然,求解最大间隔就属于这类问题,接下来我们详细介绍求解。
首先我们来了解拉格朗日函数。
我们知道我们要求解的是最小化问题,所以一个直观的想法是如果我能够构造一个函数,使得该函数在可行解区域内与原目标函数完全一致,而在可行解区域外的数值非常大,甚至是无穷大,那么这个没有约束条件的新目标函数的优化问题就与原来有约束条件的原始目标函数的优化问题是等价的问题。这就是使用拉格朗日方程的目的,它将约束条件放到目标函数中,从而将有约束优化问题转换为无约束优化问题。
随后,人们又发现,使用拉格朗日获得的函数,使用求导的方法求解依然困难。进而,需要对问题再进行一次转换,即使用一个数学技巧:拉格朗日对偶。
求解最大间隔函数,第一步:将有约束的原始目标函数转换为无约束的新构造的拉格朗日目标函数
其中,αi是拉格朗日乘子,αi大于等于0。现在我们令:
当样本点不满足约束条件时,即在可行解区域外:
此时,容易求得θ(w)最大值为无穷大。
当样本点满足约束条件时,即在可行解区域内:
此时,显然θ(w)最大值为1/2*||w||^2
我们将上述两种情况结合一下,就得到了新的目标函数:
此时,再看我们的初衷,就是为了建立一个在可行解区域内与原目标函数相同,在可行解区域外函数值趋近于无穷大的新函数,现在我们做到了。
现在,我们的问题变成了求新目标函数的最小值,即:
第二步:将不易求解的优化问题转化为易求解的优化
我们需要使用拉格朗日函数对偶性,将最小和最大的位置交换一下,这样就变成了:
交换以后的新问题是原始问题的对偶问题,这个新问题的最优值用d*来表示。而且d*<=p*。我们关心的是d=p的时候,这才是我们要的解。需要什么条件才能让d=p呢?
- 首先必须满足这个优化问题是凸优化问题。
- 其次,需要满足KKT条件。
凸优化问题的定义是:求取最小值的目标函数为凸函数的一类优化问题。目标函数是凸函数我们已经知道,这个优化问题又是求最小值。所以我们的最优化问题就是凸优化问题。
接下里,就是探讨是否满足KKT条件了。
KKT条件公式如下:
我们求解间隔方程满足KKT条件。
四,对偶问题求解
根据上述推导已知:
首先固定α,要让L(w,b,α)关于w和b最小化,我们分别对w和b偏导数,令其等于0,即:
将上述结果带回L(w,b,α)得到:
从上面的最后一个式子,我们可以看出,此时的L(w,b,α)函数只含有一个变量,即αi。
现在内侧的最小值求解完成,我们求解外侧的最大值,从上面的式子得到:
现在我们的优化问题变成了如上的形式。对于这个问题,我们有更高效的优化算法,即序列最小优化(SMO)算法。我们通过这个优化算法能得到α,再根据α,我们就可以求解出w和b,进而求得我们最初的目的:找到超平面,即"决策平面"。
现在我们要将最初的原始问题,转换到可以使用SMO算法求解的问题,这是一种最流行的求解方法。
五,SMO算法
SMO算法的目标是求出一系列alpha和b,一旦求出了这些alpha,就很容易计算出权重向量w并得到分隔超平面。
SMO算法的工作原理是:每次循环中选择两个alpha进行优化处理。一旦找到了一对合适的alpha,那么就增大其中一个同时减小另一个。这里所谓的"合适"就是指两个alpha必须符合以下两个条件,条件之一就是两个alpha必须要在间隔边界之外,而且第二个条件则是这两个alpha还没有进行过区间化处理或者不在边界上。
(1)计算误差
由上述可知:
(2)计算边界
上面获得的公式,进行变型如下:
实际上,对于上述目标函数,是存在一个假设的,即数据100%线性可分。但是,目前为止,我们知道几乎所有数据都不那么"干净"。这时我们就可以通过引入所谓的松弛变量(slack variable),来允许有些数据点可以处于超平面的错误的一侧。这样我们的优化目标就能保持仍然不变,但是此时我们的约束条件有所改变:
根据KKT条件可以得出其中αi取值的意义为:
- 对于第1种情况,表明αi是正常分类,在边界内部;
- 对于第2种情况,表明αi是支持向量,在边界上;
- 对于第3种情况,表明αi是在两条边界之间,是一个错误的分类。
此外,更新的同时还要受到第二个约束条件的限制:
我们同时更新两个α值,因为只有成对更新,才能保证更新之后的值仍然满足和为0的约束,假设我们选择的两个乘子为α1和α2:
其中,,可以看做常数。我们把α1和α2的更新公式写为:
因为两个因子不好同时求解,所以可以先求第二个乘子α2的解(α2 new),得到α2的解(α2 new)之后,再用α2的解(α2 new)表示α1的解(α1 new )。为了求解α2 new ,得先确定α2 new的取值范围。假设它的上下边界分别为H和L,那么有:
接下来,综合下面两个条件:
我们分两种情况讨论:
(1)当y1不等于y2时,即一个为正1,一个为负1的时候,可以得到:
所以有:
此时,取值范围如下图所示:
(2)当y1等于y2时,即两个都为正1或者都为负1,可以得到:
所以有:
此时,取值范围如下图所示:
如此,根据y1和y2异号或同号,可以得出α2 new的上下界分别为:
(3)计算学习速率η
已经确定了边界,接下来,就是推导迭代式,用于更新 α值。我们已经知道,更新α的边界,接下来就是讨论如何更新α值。我们依然假设选择的两个乘子为α1和α2。固定这两个乘子,进行推导。于是目标函数变成了:
为了描述方便,我们定义如下符号:
最终目标函数变为:
我们不关心constant的部分,因为对于α1和α2来说,它们都是常数项,在求导的时候,直接变为0。对于这个目标函数,如果对其求导,还有个未知数α1,所以要推导出α1和α2的关系,然后用α2代替α1,这样目标函数就剩一个未知数了,我们就可以求导了,推导出迭代公式。所以现在继续推导α1和α2的关系。注意第一个约束条件:
我们在求α1和α2的时候,可以将α3,α4,...,αn和y3,y4,...,yn看作常数项。因此有:
我们不必关心常数B的大小,现在将上述等式两边同时乘以y1,y1=1或者-1,得到(y1y1=1):
接下来,我们将得到的α1带入W(α2)公式得:
这样目标函数中就只剩下α2了,我们对其求偏导:
最终得到:
我们令:
Ei为误差项,η为学习速率。
(4)更新αi
再根据我们已知的公式:
将其代入我们对分母进行化简:
最后得:
(5)裁剪αi
这样,我们就得到了最终需要的迭代公式。这个是没有经过剪辑是的解,需要考虑约束:
根据之前推导的α取值范围,我们得到最终的解析解为:
(6)更新αj
又因为:
消去γ得:
这样,我们就知道了怎样计算α1和α2了,也就是如何对选择的α进行更新。
(7)更新b1,b2
当我们更新了α1和α2之后,需要重新计算阈值b,因为b关系到了我们f(x)的计算,也就关系到了误差Ei的计算。
我们要根据α的取值范围,去更正b的值,使间隔最大化。当α1 new在0和C之间的时候,根据KKT条件可知,这个点是支持向量上的点。因此,满足下列公式:
公式两边同时乘以y1得(y1y1=1):
因为我们是根据α1和α2的值去更新b,所以单独提出i=1和i=2的时候,整理可得:
其中前两项为:
将上述两个公式,整理得:
同理可得b2 new为:
当b1和b2都有效的时候,它们是相等的,即:
(8)更新b
当两个乘子都在边界上,则b阈值和KKT条件一致。当不满足的时候,SMO算法选择他们的中点作为新的阈值:
最后,更新所有的α和b,这样模型就出来了,从而即可求出我们的分类函数。
现在,让我们梳理下SMO算法的步骤:
- 步骤1:计算误差:
- 步骤2:计算上下界L和H:
- 步骤3:计算η:
- 步骤4:更新αj:
- 步骤5:根据取值范围修剪αj:
- 步骤6:更新αi:
- 步骤7:更新b1和b2:
- 步骤8:根据b1和b2更新b:
六,编程实现
数据如下:
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
3.125951 0.293251 -1
2.123252 -0.783563 -1
0.887835 -2.797792 -1
7.139979 -2.329896 1
1.696414 -1.212496 -1
8.117032 0.623493 1
8.497162 -0.266649 1
4.658191 3.507396 -1
8.197181 1.545132 1
1.208047 0.213100 -1
1.928486 -0.321870 -1
2.175808 -0.014527 -1
7.886608 0.461755 1
3.223038 -0.552392 -1
3.628502 2.190585 -1
7.407860 -0.121961 1
7.286357 0.251077 1
2.301095 -0.533988 -1
-0.232542 -0.547690 -1
3.457096 -0.082216 -1
3.023938 -0.057392 -1
8.015003 0.885325 1
8.991748 0.923154 1
7.916831 -1.781735 1
7.616862 -0.217958 1
2.450939 0.744967 -1
7.270337 -2.507834 1
1.749721 -0.961902 -1
1.803111 -0.176349 -1
8.804461 3.044301 1
1.231257 -0.568573 -1
2.074915 1.410550 -1
-0.743036 -1.736103 -1
3.536555 3.964960 -1
8.410143 0.025606 1
7.382988 -0.478764 1
6.960661 -0.245353 1
8.234460 0.701868 1
8.168618 -0.903835 1
1.534187 -0.622492 -1
9.229518 2.066088 1
7.886242 0.191813 1
2.893743 -1.643468 -1
1.870457 -1.040420 -1
5.286862 -2.358286 1
6.080573 0.418886 1
2.544314 1.714165 -1
6.016004 -3.753712 1
0.926310 -0.564359 -1
0.870296 -0.109952 -1
2.369345 1.375695 -1
1.363782 -0.254082 -1
7.279460 -0.189572 1
1.896005 0.515080 -1
8.102154 -0.603875 1
2.529893 0.662657 -1
1.963874 -0.365233 -1
8.132048 0.785914 1
8.245938 0.372366 1
6.543888 0.433164 1
-0.236713 -5.766721 -1
8.112593 0.295839 1
9.803425 1.495167 1
1.497407 -0.552916 -1
1.336267 -1.632889 -1
9.205805 -0.586480 1
1.966279 -1.840439 -1
8.398012 1.584918 1
7.239953 -1.764292 1
7.556201 0.241185 1
9.015509 0.345019 1
8.266085 -0.230977 1
8.545620 2.788799 1
9.295969 1.346332 1
2.404234 0.570278 -1
2.037772 0.021919 -1
1.727631 -0.453143 -1
1.979395 -0.050773 -1
8.092288 -1.372433 1
1.667645 0.239204 -1
9.854303 1.365116 1
7.921057 -1.327587 1
8.500757 1.492372 1
1.339746 -0.291183 -1
3.107511 0.758367 -1
2.609525 0.902979 -1
3.263585 1.367898 -1
2.912122 -0.202359 -1
1.731786 0.589096 -1
2.387003 1.573131 -1
python代码
import matplotlib.pyplot as plt
import numpy as np
# 读取数据
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
# i - alpha
# m - alpha参数个数
def selectJrand(i, m):
j = i #选择一个不等于i的j
while (j == i):
j = int(np.random.uniform(0, m))
return j
# 修剪alpha
# aj - alpha值
# H - alpha上限
# L - alpha下限
def clipAlpha(aj, H, L):
if aj > H:
aj = H
if L > aj:
aj = L
return aj
# 简化的SMO算法
# dataMatIn数据集;classLabels分类标签;C常数;toler容错率;maxIter最大循环次数
def smoSimple(dataMatIn, classLabels, C, toler, maxIter):
dataMatrix = np.mat(dataMatIn); labelMat = np.mat(classLabels).transpose() # 数据转为矩阵
b = 0; m,n = np.shape(dataMatrix)
alphas = np.mat(np.zeros((m,1))) # 初始化为m*1零矩阵
iter = 0
while (iter < maxIter):
alphaPairsChanged = 0 # alpha是否优化标志
for i in range(m):
fXi = float(np.multiply(alphas,labelMat).T*(dataMatrix*dataMatrix[i,:].T)) + b # 计算f(xi)=Σαjyjxixj+b
Ei = fXi - float(labelMat[i]) # 第一步,计算误差Ei
if ((labelMat[i]*Ei < -toler) and (alphas[i] < C)) or ((labelMat[i]*Ei > toler) and (alphas[i] > 0)): #
j = selectJrand(i,m) # 随机选择一个不为i的值,这样就选取出两个参数αi和αj作为变量
fXj = float(np.multiply(alphas,labelMat).T*(dataMatrix*dataMatrix[j,:].T)) + b
Ej = fXj - float(labelMat[j]) #计算误差 Ej
alphaIold = alphas[i].copy(); alphaJold = alphas[j].copy() # 保存更新前的aplpha值,即αold,
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[j] -= labelMat[j]*(Ei - Ej)/eta # 第四步:更新αj
alphas[j] = clipAlpha(alphas[j],H,L) # 第五步:修剪αj
if (abs(alphas[j] - alphaJold) < 0.00001): print ("j not moving enough"); continue
alphas[i] += labelMat[j]*labelMat[i]*(alphaJold - alphas[j]) # 第六步:更新αi
# 第七步:更新b1,b2
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
# 第八步:更新b
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)
return b,alphas
def showClassifer(dataMat, w, b):
#绘制样本点
data_plus = [] #正样本
data_minus = [] #负样本
for i in range(len(dataMat)):
if labelMat[i] > 0:
data_plus.append(dataMat[i])
else:
data_minus.append(dataMat[i])
data_plus_np = np.array(data_plus) #转换为numpy矩阵
data_minus_np = np.array(data_minus) #转换为numpy矩阵
plt.scatter(np.transpose(data_plus_np)[0], np.transpose(data_plus_np)[1], s=30, alpha=0.7) #正样本散点图
plt.scatter(np.transpose(data_minus_np)[0], np.transpose(data_minus_np)[1], s=30, alpha=0.7) #负样本散点图
#绘制直线
x1 = max(dataMat)[0]
x2 = min(dataMat)[0]
a1, a2 = w
b = float(b)
a1 = float(a1[0])
a2 = float(a2[0])
y1, y2 = (-b- a1*x1)/a2, (-b - a1*x2)/a2
plt.plot([x1, x2], [y1, y2])
#找出支持向量点
for i, alpha in enumerate(alphas):
if alpha > 0:
x, y = dataMat[i]
plt.scatter([x], [y], s=150, c='none', alpha=0.7, linewidth=1.5, edgecolor='red')
plt.show()
# 获取w值
def get_w(dataMat, labelMat, alphas):
alphas, dataMat, labelMat = np.array(alphas), np.array(dataMat), np.array(labelMat)
w = np.dot((np.tile(labelMat.reshape(1, -1).T, (1, 2)) * dataMat).T, alphas)
return w.tolist()
if __name__ == '__main__':
dataMat, labelMat = loadDataSet('testData.txt')
b,alphas = smoSimple(dataMat, labelMat,0.6,0.001,40)
w = get_w(dataMat, labelMat, alphas)
showClassifer(dataMat, w, b)