支持向量機
前言
支持向量機是一種二分類的模型,它的基本模型是定義在特徵空間上間隔最大的線性分類器,間隔最大使它有別與其他感知機模型。有線性支持向量機,線性可分支持向量機和非線性支持向量機。
線性可分支持向量機(Hard Margin)
函數間隔和幾何間隔
函數間隔
對於給定數據集和超平面,定義超平面關於樣本點的函數間隔爲
定義超平面關於訓練數據集的函數間隔爲超平面關於中所有樣本點的函數間隔之最小值,即
函數間隔可以表示分類預測的正確性及確信度。
幾何間隔
當數據被正確分類時,幾何間隔就是點到超平面的距離爲了求幾何間隔最大,SVM基本問題可以轉化爲求解:(爲幾何間隔,(爲函數間隔)
原理
支持向量機最簡單的情況是線性可分支持向量機,或硬間隔支持向量機。構建它的條件是訓練數據線性可分。其學習策略是最大間隔法。可以表示爲凸二次規劃問題,其原始最優化問題爲
求得最優化問題的解爲,,得到線性可分支持向量機,分離超平面是
分類決策函數是
最大間隔法中,函數間隔與幾何間隔是重要的概念。
線性可分支持向量機的最優解存在且唯一。位於間隔邊界上的實例點爲支持向量。最優分離超平面由支持向量完全決定。
二次規劃問題的對偶問題是
通常,通過求解對偶問題學習線性可分支持向量機,即首先求解對偶問題的最優值,然後求最優值和,得出分離超平面和分類決策函數。
線性近似可分(Soft Margin)
原理
現實中訓練數據是線性可分的情形較少,訓練數據往往是近似線性可分的,這時使用線性支持向量機,或軟間隔支持向量機。線性支持向量機是最基本的支持向量機。
對於噪聲或例外,通過引入鬆弛變量,使其“可分”,得到線性支持向量機學習的凸二次規劃問題,其原始最優化問題是
求解原始最優化問題的解和,得到線性支持向量機,其分離超平面爲
分類決策函數爲
線性可分支持向量機的解唯一但不唯一。對偶問題是
線性支持向量機的對偶學習算法,首先求解對偶問題得到最優解,然後求原始問題最優解和,得出分離超平面和分類決策函數。
對偶問題的解中滿的實例點稱爲支持向量。支持向量可在間隔邊界上,也可在間隔邊界與分離超平面之間,或者在分離超平面誤分一側。最優分離超平面由支持向量完全決定。
合頁損失
線性支持向量機(軟間隔)學習等價於最小化二階範數正則化的合頁函數,也即經驗風險+正則化項(其中C>0):
其中,第一項又稱合頁損失,其函數圖像如下圖,合頁損失要求不僅要分類正確,而且確信度做夠高時損失才爲0.也就是說合頁損失函數對學習有更高的要求.
非線性支持向量機(核技巧)
問題引入
下圖中對一個對二維特徵的數據進行可視化,左圖中可以看出此時的數據呈現非線性,此時,我們對原始數據繪製了右圖線性SVM的決策邊界,可以明顯的觀察到線性SVM對非線性的數據的分類效果極差.但是,其實從數據分佈的情況可以看出,存在一條非線性的環形邊界,可以使的數據明顯分開.
原始數據分佈 | 線性決策邊界 |
然而,當我們把原始數據進行維度映射,如下如圖所示,此時我們可以明顯的發現在此時可以有一個線性超平面將數據分爲兩類,並且可能具有準確度較高.所以我們想是否在SVM存在一種映射使得我們的數據在另一個空間線性可分.而我們此時採用的方法爲核技巧.
核函數
對於輸入空間中的非線性分類問題,可以通過非線性變換將它轉化爲某個高維特徵空間中的線性分類問題,在高維特徵空間中學習線性支持向量機。由於在線性支持向量機學習的對偶問題裏,目標函數和分類決策函數都只涉及實例與實例之間的內積,所以不需要顯式地指定非線性變換,而是用核函數來替換當中的內積。核函數表示,通過一個非線性轉換後的兩個實例間的內積。具體地,是一個核函數,或正定核,意味着存在一個從輸入空間x到特徵空間的映射,對任意,有
對稱函數爲正定核的充要條件如下:對任意,任意正整數,對稱函數對應的Gram矩陣是半正定的。
所以,在線性支持向量機學習的對偶問題中,用核函數替代內積,求解得到的就是非線性支持向量機
使用核函數的優點;
-
不需要每一次都具體計算出原始樣本點映射的新的無窮維度的樣本點,直接使用映射後的新的樣本點的點乘計算公式即可
-
減少計算量
-
較少存儲空間
下圖是使用"rbf"高斯核函數,將數據在高維映射進行線性分類的結果,由圖中可以看出,此時的決策邊界可以輕易的兩類數據分開.
探析高斯核參數
核函數公式
參數對比
過度欠擬合 | 欠擬合 |
just right | 過擬合 |
從上圖中可以觀察到,在高斯核中, 我們參數的數值可以在一定程度影響數據的擬合程度,上圖中當參數值較小時,此時出現欠擬合,而隨着參數值的增大,數據的擬合程度越來好,但是參數值大到一定程度,則會出現過擬合現象.所以,有時候我們在使用RBF核函數時,可以調節值來調節模型的擬合程度,增加模型的泛化能力.
不同核函數的性能
下圖是不同類型的數據集在不同核函數的決策邊界的可視化,由圖中的評測分數可知,高斯核函數無論是在線性還是非線性數據都有較好的性能,而sigmoid核函數總體上性能較差,所以,當數據的分佈不能確定時,可以首先嚐試一下rbf核函數.除此之外,下圖中也繪製了"支持向量"(黑色邊界圓點),數據中"支持向量"的點位於虛線超平面,虛線超平面的中間和分錯的點.
SMO算法
無論是基於Soft Margin還是Hard Margin的支持向量都會通過求解對偶函數來得到模型的參數,只是兩種方法最後的約束條件不一致,不失一般性,下面是線性近似可分的對偶函數,現在主要針對其中的係數進行求解.所以,SMO算法就是要解如下的凸二次規劃的對偶問題:
1.加載數據
def loadDataset(filename):
dataMat = []
yMat = []
fr = open(filename)
for line in fr.readlines():
line = line.strip().split(",")
dataMat.append([float(line[0]),float(line[1]),float(line[2]), float(line[3])])
yMat.append(float(line[4]))
return dataMat, yMat
2. 存儲全局變量
class diyStruct:
def __init__(self, dataMat, yMat, C, toler, kernelParam):
self.dataMat = dataMat
self.yMat = yMat
self.C = C
self.toler = toler #精確誤差度
self.m = shape(dataMat)[0] # 樣本數目
self.E = mat(zeros((self.m, 2))) # 誤差項
self.alphas = mat(zeros((self.m , 1))) # 拉格朗日系數
self.b = 0
self.K = mat(zeros((self.m, self.m))) # 核函數
for i in range(self.m):
self.K[:, i] = transfer2Kernel(self.dataMat, self.dataMat[i, :], kernelParam)
3.核函數
def transfer2Kernel(X, Xi, kernelParam):
m = shape(X)[0]
Ktemp = mat(zeros((m, 1)))
if kernelParam[0] == "rbf":
for i in range(m):
xdelta = X[i, :] - Xi
# 第二範式
Ktemp[i] = xdelta * xdelta.T
Ktemp = exp(-Ktemp/kernelParam[1]**2)
else:raise NameError("underfined kernel name!")
return Ktemp
4.拉格朗日系數裁剪
由於,在進行SMO算法計算, 時,未進行對參數的約束,所以,需要對未裁剪的參數進行約束使得參數值滿足約束條件.由於此時SMO算法兩個變量的最優化問題實質上可以轉變爲單變量的最優化問題,所以,不妨考慮爲單變量的的優化問題.下圖是約束條件:
以下公式是對上圖中參數進行單變量約束的結果:
所以最終的約束條件如下:
def clipAlpha(alphaJ, L, H):
if(alphaJ < L):
alphaJ = L
if(alphaJ > H):
alphaJ = H
return alphaJ
5.裁剪係數b
對於係數的最終選擇考慮,如果此時求解的同時滿足條件, 那麼.如果是0或者C,那麼以及它們之間符合KKT條件的閥值,這時選擇它們的中點作爲.
def calcb(b1new, b2new):
b = b1new
if (b1new != b2new):
b = (b1new + b2new) / 2
return b
6.計算誤差項
def calcE(alphaI, diyObj):
yI = float(diyObj.yMat[alphaI])
gxI = float(multiply(diyObj.alphas, diyObj.yMat).T * diyObj.K[:, alphaI]
+ diyObj.b)
EI = gxI - yI
return EI
7.選擇係數
def selectJ(EI, alphaI, diyObj):
# 第一列索引值表示是否存貯相應的誤差項
nonzeroEIndex = nonzero(diyObj.E[:, 0].A)[0]
alphaJ = 0
EJ = 0
maxDelta = 1
# 第二個變量的選擇
if len(nonzeroEIndex) > 1:
for j in nonzeroEIndex:
# 選擇不同與I節點的值
if alphaI == j :continue
EJtemp = calcE(j, diyObj)
deltaE = abs(EI - EJtemp)
# 選擇最大變化的
if (deltaE > maxDelta):
maxDelta = deltaE
alphaJ = j
EJ = EJtemp
else:
alphaJ = alphaI
while(alphaJ == alphaI):
alphaJ =int(random.uniform(0, diyObj.m))
EJ = calcE(alphaJ, diyObj)
return alphaJ, EJ
8.迭代更新參數
def iterL(alphaI, diyObj):
# 計算係數值
yI = diyObj.yMat[alphaI]
EI = calcE(alphaI, diyObj)
diyObj.E[alphaI] = [1, EI]
#第一個變量的選擇(違反KKT條件)
if((yI * EI > diyObj.toler and diyObj.alphas[alphaI] > 0) or
(yI * EI < - diyObj.toler and diyObj.alphas[alphaI] < diyObj.C)):
# 得到第二個變量
alphaJ, EJ = selectJ(EI, alphaI, diyObj)
yJ = diyObj.yMat[alphaJ]
# old alpha
alpha1old = diyObj.alphas[alphaI].copy()
alpha2old = diyObj.alphas[alphaJ].copy()
# 計算eta
eta = diyObj.K[alphaI, alphaI] + diyObj.K[alphaJ, alphaJ] \
- 2 * diyObj.K[alphaI, alphaJ]
if eta <= 0: return 0
# 裁剪alpha2
alpha2newUnclip = alpha2old + yJ*(EI - EJ)/eta
if (yI == yJ):
L = max(0, alpha1old + alpha2old -diyObj.C)
H = min(diyObj.C, alpha2old+alpha1old)
else:
L = max(0, alpha2old - alpha1old)
H = min(diyObj.C, diyObj.C - alpha1old + alpha2old)
if L==H: return 0
alpha2new = clipAlpha(alpha2newUnclip, L, H)
# 精度滿足條件(停止條件)
if abs(alpha2new - alpha2old) < 0.00001: return 0
# 更新alpha1的值
alpha1new = alpha1old + yI * yJ * (alpha2old - alpha2new)
# 更新b的值(每一次新的參數確定後)
b1new = - EI - yI * diyObj.K[alphaI,alphaI] * (alpha1new - alpha1old) \
- yJ * diyObj.K[alphaJ, alphaI] * (alpha2new - alpha2old) \
+ diyObj.b
b2new = - EJ - yI * diyObj.K[alphaI,alphaJ] * (alpha1new - alpha1old) \
- yJ * diyObj.K[alphaJ, alphaJ] * (alpha2new - alpha2old) \
+ diyObj.b
# 真正的b值
b = calcb(b1new, b2new)
# 存儲值
diyObj.alphas[alphaI] = alpha1new
diyObj.alphas[alphaJ] = alpha2new
diyObj.b = b
#變量優化後需要再次更新E值
diyObj.E[alphaI] = [1, calcE(alphaI, diyObj)]
diyObj.E[alphaJ] = [1, calcE(alphaJ, diyObj)]
return 1
else: return 0
9. SMO
第一個變量的選擇稱爲外循環,首先遍歷整個樣本集,選擇違反KKT條件的αi作爲第一個變量.違反KKT條件公式如下(注意此時加入精確度 )
接着依據相關規則選擇第二個變量(見下面分析),對這兩個變量採用上述方法進行優化。當遍歷完整個樣本集後,遍歷非邊界樣本集(0<αi<C)中違反KKT的αi作爲第一個變量,同樣依據相關規則選擇第二個變量,對此兩個變量進行優化。當遍歷完非邊界樣本集後,再次回到遍歷整個樣本集中尋找,即在整個樣本集與非邊界樣本集上來回切換,尋找違反KKT條件的αi作爲第一個變量。直到遍歷整個樣本集後,沒有違反KKT條件αi,然後退出。邊界上的樣本對應的αi=0或者αi=C,在優化過程中很難變化,然而非邊界樣本0<αi<C會隨着對其他變量的優化會有大的變化.
注意,根據KKT條件:
- 當,此時的點位於虛線超平面的外側或者在虛線超平面上,能正確分類
- 當,此時對應的樣本點位於間隔邊界上,能正確分類
- 當,此時對應的樣本點位於間隔邊界或間隔邊界的內側,根據鬆弛係數確定
def smo(dataMat, yMat, C, toler, iterNum, kernelParam):
diyObj = diyStruct(mat(dataMat), mat(yMat).transpose(), C, toler, kernelParam)
currentToler = 0
changedAlphas = 0 # 記錄此時的alpha對數
allSet = True
# 每次選擇兩個alpha值進行優化
while((currentToler < iterNum and changedAlphas >0)) or (allSet):
changedAlphas = 0
if allSet:
for i in range(diyObj.m):
changedAlphas += iterL(i, diyObj)
# print("iter:%d i:%d,pairs changed %d"
# %(currentToler, i, changedAlphas))
allSet = False
else:
# 遍歷只符合 0<ai<C 的alpha(在虛線超平面上的點)(non_bound)
alphaIs = nonzero((diyObj.alphas.A > 0) * (diyObj.alphas.A < C))[0]
for i in alphaIs:
changedAlphas += iterL(i, diyObj)
# print("iter:%d i:%d,pairs changed %d"
# %(currentToler, i, changedAlphas))
if changedAlphas == 0:
allSet = True
# 記錄迭代次數
currentToler += 1
# print("iteration number: %d" % currentToler)
return diyObj.alphas, diyObj.b
10.測試SVM
源代碼
#測試機的真實結果: 1,1,1,-1,-1,-1,-1,1,1,-1
def testSVM():
result = []
dataMat, yMat=loadDataset("data/bloodTransfusion_noduplicated.txt")
alphas, b = smo(dataMat, yMat, 200, 0.0001, 100, ("rbf", 20))
testData = [[2,50,12500,98],[0,13,3250,28],[1,16,4000,35],[1,24,6000,77],[4,4,1000,4]
,[1,12,3000,35],[4,23,5750,58],[2,7,1750,14],[2,10,2500,28],[1,13,3250,47]]
m, n = shape(testData)
testmat = mat(testData)
for i in range(m):
kernelEval = transfer2Kernel(mat(dataMat), testmat[i, :], ("rbf", 20))
predict = kernelEval.T*multiply(mat(yMat).transpose(), alphas) + b
result.append((sign(predict)))
print("預測結果", result)
testSVM()
預測結果 [matrix([[1.]]), matrix([[1.]]), matrix([[-1.]]), matrix([[-1.]]), matrix([[-1.]]), matrix([[-1.]]), matrix([[-1.]]), matrix([[-1.]]), matrix([[1.]]), matrix([[-1.]])]
使用Sklearn驗證
from sklearn.svm import SVC
dataMat = []
yMat = []
fr = open("data/bloodTransfusion_noduplicated.txt")# 真實數據
for line in fr.readlines():
line = line.strip().split(",")
dataMat.append([float(line[0]),float(line[1]),float(line[2]), float(line[3])])
yMat.append(float(line[4]))
Xtrain = mat(dataMat)
Ytain = mat(yMat)
testData = [[2,50,12500,98],[0,13,3250,28],[1,16,4000,35],[1,24,6000,77],
[4,4,1000,4],[1,12,3000,35],[4,23,5750,58],[2,7,1750,14],
[2,10,2500,28],[1,13,3250,47]]
clf = SVC(gamma=20, tol=0.0001) # 默認使用rbf
clf.fit(dataMat, yMat)
clf.predict(testData)
array([ 1., 1., 1., -1., -1., -1., -1., 1., 1., -1.])
由上可見,兩者預測結果相等,且都爲真實數據.
參考
支持向量機的原理和實現(SMO算法代碼來源,強烈推薦)