机器学习-分类之支持向量机(SVM)原理及实战
支持向量機(jī)(SVM)
簡介
支持向量機(jī)(Support Vector Machine,SVM),是常見的一種判別方法。在機(jī)器學(xué)習(xí)領(lǐng)域,是一個監(jiān)督學(xué)習(xí)模型,通常用來進(jìn)行模式識別、分類及回歸分析。與其他算法相比,支持向量機(jī)在學(xué)習(xí)復(fù)雜的非線性方程時提供了一種更為清晰、更加強(qiáng)大的方式。
支持向量機(jī)是20世紀(jì)90年代中期發(fā)展起來的基于統(tǒng)計(jì)學(xué)習(xí)理論的一種機(jī)器學(xué)習(xí)方法,通過尋求結(jié)構(gòu)化風(fēng)險最小來提高學(xué)習(xí)機(jī)泛化能力,實(shí)現(xiàn)經(jīng)驗(yàn)風(fēng)險和置信范圍的最小化,從而達(dá)到在統(tǒng)計(jì)樣本量較少的情況下,也能獲得良好統(tǒng)計(jì)規(guī)律的目的。
通俗來講,它是一種二類分類模型,其基本模型定義為特征空間上的間隔最大的線性分類器,即支持向量機(jī)的學(xué)習(xí)策略便是間隔最大化,最終可轉(zhuǎn)化為一個凸二次規(guī)劃問題的求解。
原理
幾個概念
線性可分
如圖,數(shù)據(jù)之間分隔得足夠開,很容易在圖中畫出一條直線將這一組數(shù)據(jù)分開,這組數(shù)據(jù)就被稱為線性可分?jǐn)?shù)據(jù)。
分隔超平面
上述將數(shù)據(jù)集分隔開來的直線稱為分隔超平面,由于上面給出的數(shù)據(jù)點(diǎn)都在二維平面上,所以此時的分隔超平面是一條直線。如果給出的數(shù)據(jù)集點(diǎn)是三維的,那么用來分隔數(shù)據(jù)的就是一個平面。因此,更高維的情況可以以此類推,如果數(shù)據(jù)是100維的,那么就需要一個99維的對象來對數(shù)據(jù)進(jìn)行分隔,這些統(tǒng)稱為超平面。
間隔
如圖片所示,這條分隔線的好壞如何呢?我們希望找到離分隔超平面最近的點(diǎn),確保它們離分割面的距離盡可能遠(yuǎn)。在這里點(diǎn)到分隔面的距離稱為間隔。間隔盡可能大是因?yàn)槿绻稿e或在有限數(shù)據(jù)上訓(xùn)練分類器,我們希望分類器盡可能健壯。
支持向量
離分隔超平面最近的那些點(diǎn)是支持向量。
求解
接下來就是要求解最大支持向量到分隔面的距離,需要找到此類問題的求解方法。如圖,分隔超平面可以寫成wTx+bw^Tx+bwTx+b。要計(jì)算距離,值為∣wTA+b∣∣∣w∣∣\frac{|w^TA+b|}{||w||}∣∣w∣∣∣wTA+b∣?。這里的向量w和常數(shù)b一起描述了所給數(shù)據(jù)的超平面。
對wTx+bw^Tx+bwTx+b使用單位階躍函數(shù)得到f(wTx+b)f(w^Tx+b)f(wTx+b),其中當(dāng)u<0u<0u<0時,f(u)f(u)f(u)輸出-1,反之則輸出+1。這里使用-1和+1是為了教學(xué)上的方便處理,可以通過一個統(tǒng)一公式來表示間隔或數(shù)據(jù)點(diǎn)到分隔超平面的距離。間隔通過label×∣wTx+b∣∣∣w∣∣label\times{|w^Tx+b| \over ||w||}label×∣∣w∣∣∣wTx+b∣?來計(jì)算。如果數(shù)據(jù)處在正方向(+1)類里面且離分隔超平面很遠(yuǎn)的位置時,wTx+bw^Tx+bwTx+b是一個很大的正數(shù)。同時label×∣wTx+b∣∣∣w∣∣label\times{|w^Tx+b| \over ||w||}label×∣∣w∣∣∣wTx+b∣?也會是一個很大的正數(shù)。如果數(shù)據(jù)點(diǎn)處在負(fù)方向(-1)類且離分隔超平面很遠(yuǎn)的位置時,由于此時類別標(biāo)簽為-1,label×∣wTx+b∣∣∣w∣∣label\times{\frac{|w^Tx+b|}{||w||}}label×∣∣w∣∣∣wTx+b∣?仍然是一個很大的正數(shù)。
為了找到具有最小間隔的數(shù)據(jù)點(diǎn),就要找到分類器中定義的www和bbb,最小間隔的數(shù)據(jù)點(diǎn)也就是支持向量。一旦找到支持向量,就需要對該間隔最大化,可以寫作max?w,b(min?n(label×∣wTx+b∣∣∣w∣∣))\max_{w,b}\left(\min_n(label\times{|w^Tx+b| \over ||w||})\right) w,bmax?(nmin?(label×∣∣w∣∣∣wTx+b∣?))
但是直接求解上述問題是非常困難的,所以這里引入拉格朗日乘子法,可以把表達(dá)式寫成下面的式子:
max?_α[∑_i=1mα12∑_i,j=1mlabel(i)×α_i×α_j(x(i),x(j))]{\max\_{\alpha}}\left[\sum\_{i=1}^m\alpha \frac{1}{2}\sum\_{i,j=1}^mlabel^{(i)}\times \alpha\_i\times\alpha\_j{(x^{(i)},x^{(j)})} \right] max_α[∑_i=1mα21?∑_i,j=1mlabel(i)×α_i×α_j(x(i),x(j))]
其中×\times×表示,兩個向量的內(nèi)積。約束條件為C≥α≥0C\geq\alpha\geq0 C≥α≥0 ∑i?1mα_i×label(i)=0\sum_{i-1}^m \alpha\_i\times label^{(i)}=0 i?1∑m?α_i×label(i)=0
這里的常數(shù)C用于控制最大化間隔和保證大部分點(diǎn)的函數(shù)間隔小于1.0這兩個目標(biāo)的權(quán)重。因?yàn)樗袛?shù)據(jù)都可能有干擾數(shù)據(jù),所以通過引入所謂的松弛變量,允許有些數(shù)據(jù)點(diǎn)可以處于分隔面錯誤的一側(cè)。
根據(jù)上式可知,只要求出所有的 α\alphaα,那么分隔面就可以通過α\alphaα來表達(dá),SVM的主要工作就是求α\alphaα。這樣一步步解出分隔面,那么分類問題游刃而解。
算法優(yōu)勢
- 泛化錯誤率低,具有良好的學(xué)習(xí)能力。
- 幾乎所有分類問題都可以使用SVM解決。
- 節(jié)省內(nèi)存開銷。
實(shí)戰(zhàn)
實(shí)現(xiàn)手寫識別系統(tǒng)
為了簡單起見,這里的手寫識別只針對0到9的數(shù)字,為了方便,圖像轉(zhuǎn)為了文本。目錄trainingDigits中含有2000個例子,用于訓(xùn)練,目錄testDigits含有900個例子,用于測試。
雖然手寫識別可以用KNN實(shí)現(xiàn)而且效果不錯,但是KNN畢竟太占內(nèi)存了,而且要保證性能不變的同時使用較少的內(nèi)存。而對于SVM,只需要保留很少的支持向量就可以實(shí)現(xiàn)目標(biāo)效果。
- 流程
- 準(zhǔn)備數(shù)據(jù)
- 分析數(shù)據(jù)
- 使用SMO算法求出α\alphaα和bbb
- 訓(xùn)練算法
- 測試算法
- 使用算法
代碼實(shí)現(xiàn)
# -*- coding=UTF-8 -*- from numpy import *def clipAlpha(aj, H, L):'''輔助函數(shù),調(diào)整a的范圍:param aj::param H::param L::return:'''if aj > H:aj = Hif L > aj:aj = Lreturn ajdef kernelTrans(X, A, kTup):'''修改kernel:param X::param A::param kTup::return:'''m, n = shape(X)K = mat(zeros((m, 1)))if kTup[0] == 'lin':K = X * A.Telif kTup[0] == 'rbf':for j in range(m):deltaRow = X[j, :] - AK[j] = deltaRow*deltaRow.T# numpy中除法意味著對矩陣展開計(jì)算而不是matlab的求矩陣的逆K = exp(K/(-1*kTup[1]**2))# 如果遇到無法識別的元組,程序拋出異常else:raise NameError('Houston We Have a Problem -- That Kernel is not recognized')return Kclass optStruct:'''保存所有重要值,實(shí)現(xiàn)對成員變量的填充'''def __init__(self,dataMatIn, classLabels, C, toler, kTup): # Initialize the structure with the parametersself.X = dataMatInself.labelMat = classLabelsself.C = Cself.tol = tolerself.m = shape(dataMatIn)[0]self.alphas = mat(zeros((self.m,1)))self.b = 0self.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)def calcEk(oS, k):'''計(jì)算E值 計(jì)算誤差:param oS::param k::return:'''fXk = float(multiply(oS.alphas,oS.labelMat).T*oS.K[:,k] + oS.b)Ek = fXk - float(oS.labelMat[k])return Ekdef selectJrand(i, m):''':param i: a的下標(biāo):param m: a的總數(shù):return:'''j = iwhile (j == i):# 簡化版SMO,alpha隨機(jī)選擇j = int(random.uniform(0, m))return jdef selectJ(i, oS, Ei):'''選擇第二個a的值以保證每次優(yōu)化的最大步長(內(nèi)循環(huán)):param i::param oS::param Ei::return:'''maxK = -1maxDeltaE = 0Ej = 0oS.eCache[i] =[1, Ei]validEcacheList = nonzero(oS.eCache[:, 0].A)[0]if(len(validEcacheList)) > 1:for k in validEcacheList:if k == i:continueEk = calcEk(oS, k)deltaE = abs(Ei-Ek)if(deltaE > maxDeltaE):maxK = kmaxDeltaE = deltaEEj = Ekreturn maxK, Ejelse:j = selectJrand(i, oS.m)Ej = calcEk(oS, j)return j, Ejdef updateEk(oS, k):'''計(jì)算誤差值并存入緩存中:param oS::param k::return:'''Ek = calcEk(oS, k)oS.eCache[k] = [1,Ek]def innerL(i, oS):'''選擇第二個a:param i::param oS::return:'''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 0eta = 2.0 * oS.K[i, j] - oS.K[i, i] - oS.K[j, j]if eta >= 0:print("eta>=0")return 0oS.alphas[j] -= oS.labelMat[j]*(Ei - Ej)/etaoS.alphas[j] = clipAlpha(oS.alphas[j], H, L)updateEk(oS, j)if (abs(oS.alphas[j] - alphaJold) < 0.00001):print ("j not moving enough")return 0oS.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 (0 < oS.alphas[i]) and (oS.C > oS.alphas[i]):oS.b = b1elif (0 < oS.alphas[j]) and (oS.C > oS.alphas[j]):oS.b = b2else:oS.b = (b1 + b2)/2.0return 1else:return 0def smoP(dataMatIn, classLabels, C, toler, maxIter,kTup=('lin', 0)):'''實(shí)現(xiàn)platt smo算法:param dataMatIn::param classLabels::param C::param toler::param maxIter::param kTup::return:'''oS = optStruct(mat(dataMatIn), mat(classLabels).transpose(), C, toler, kTup)iter = 0entireSet = True; alphaPairsChanged = 0while (iter < maxIter) and ((alphaPairsChanged > 0) or (entireSet)):alphaPairsChanged = 0if entireSet:for i in range(oS.m):alphaPairsChanged += innerL(i, oS)print("fullSet, iter: %d i:%d, pairs changed %d" % (iter, i, alphaPairsChanged))iter += 1else:nonBoundIs = nonzero((oS.alphas.A > 0) *(oS.alphas.A < C))[0]for i in nonBoundIs:alphaPairsChanged += innerL(i, oS)print("non-bound, iter: %d i:%d, pairs changed %d" % (iter, i, alphaPairsChanged))iter += 1if entireSet:entireSet = Falseelif (alphaPairsChanged == 0):entireSet = Trueprint("迭代次數(shù): %d" % iter)return oS.b, oS.alphasdef img2vector(filename):'''二值化圖像轉(zhuǎn)為向量32*32轉(zhuǎn)為1*1024:param filename: 文件名:return: 向量'''returnVect = zeros((1, 1024))fr = open(filename)for i in range(32):lineStr = fr.readline()for j in range(32):returnVect[0, 32*i+j] = int(lineStr[j])return returnVectdef loadImages(dirName):'''導(dǎo)入數(shù)據(jù)集:param dirName::return:'''from os import listdirhwLabels = []trainingFileList = listdir(dirName)m = len(trainingFileList)trainingMat = zeros((m, 1024))for i in range(m):fileNameStr = trainingFileList[i]fileStr = fileNameStr.split('.')[0]classNumStr = int(fileStr.split('_')[0])# 這里是二分類問題,只分類數(shù)字1和9,數(shù)字分類結(jié)果為9時返回-1if classNumStr == 9:hwLabels.append(-1)else:hwLabels.append(1)trainingMat[i, :] = img2vector('%s/%s' % (dirName, fileNameStr))return trainingMat, hwLabelsdef testDigits(kTup=('rbf', 10)):'''測試算法,使用smop訓(xùn)練:param kTup: 核函數(shù):return:'''dataArr, labelArr = loadImages('data/trainingDigits')b, alphas = smoP(dataArr, labelArr, 200, 0.0001, 10000, kTup)datMat = mat(dataArr)labelMat = mat(labelArr).transpose()svInd = nonzero(alphas.A > 0)[0]sVs = datMat[svInd]labelSV = labelMat[svInd]print("有 %d 支持向量" % shape(sVs)[0])m, n = shape(datMat)errorCount = 0for i in range(m):kernelEval = kernelTrans(sVs, datMat[i, :], kTup)predict = kernelEval.T * multiply(labelSV, alphas[svInd]) + bif sign(predict) != sign(labelArr[i]):errorCount += 1print("訓(xùn)練數(shù)據(jù)錯誤率是: %f" % (float(errorCount)/m))dataArr, labelArr = loadImages('data/testDigits')errorCount = 0datMat = mat(dataArr)labelMat = mat(labelArr).transpose()m,n = shape(datMat)for i in range(m):kernelEval = kernelTrans(sVs, datMat[i, :], kTup)predict = kernelEval.T * multiply(labelSV, alphas[svInd]) + bif sign(predict) != sign(labelArr[i]):errorCount += 1print("測試數(shù)據(jù)錯誤率是: %f" % (float(errorCount)/m))def loadDataSet(filename):'''加載數(shù)據(jù)集:param filename: 文件名:return:'''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, labelMatif __name__ == '__main__':testDigits(('rbf', 20))補(bǔ)充說明
參考書《Python3數(shù)據(jù)分析與機(jī)器學(xué)習(xí)實(shí)戰(zhàn)》,具體數(shù)據(jù)集和代碼可以查看我的GitHub,歡迎star或者fork。
總結(jié)
以上是生活随笔為你收集整理的机器学习-分类之支持向量机(SVM)原理及实战的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux服务-FTP文件服务器部署
- 下一篇: 机器学习-分类之AdaBoost原理及实