一,決策面方程
我們以二維平面爲例,假設有一條直線,方程如下:
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)