一引言:
支持向量機這部分確實很多,想要真正的去理解它,不僅僅知道理論,還要進行相關的代碼編寫和測試,二者想和結合,才能更好的幫助我們理解SVM這一非常優秀的分類算法
支持向量機是一種二類分類算法,假設一個平面可以將所有的樣本分為兩類,位於正側的樣本為一類,值為+1,而位於負一側的樣本為另外一類,值為-1。
我們說分類,不僅僅是將不同的類別樣本分隔開,還要以比較大的置信度來分隔這些樣本,這樣才能使絕大部分樣本被分開。比如,我們想通過一個平面將兩個類別的樣本分開,如果這些樣本是線性可分(或者近視線性可分),那么這樣的平面有很多,但是如果我們加上要以最大的置信度來將這些樣本分開,那么這樣的平面只有一條。那么怎么才能找到這樣的平面呢?這里不得不提到幾個概念
1 幾何間隔
幾何間隔的概念,簡單理解就是樣本點到分隔平面的距離
2 間隔最大化
想要間隔最大化,我們必須找到距離分隔平面最近的點,並且使得距離平面最近的點盡可能的距離平面最遠,這樣,每一個樣本就都能夠以比較大的置信度被分隔開
算法的分類預測能力也就越好
顯然,SVM算法的關鍵所在,就是找到使得間隔最大化的分隔超平面(如果特征是高維度的情況,我們稱這樣的平面為超平面)
這里關於SVM學習,推薦兩本書:統計學習方法(李航)和機器學習實戰,二者結合,可以幫助我們理解svm算法
2 支持向量機
關於支持向量機的推導,無論是書上還是很多很優秀的博客都寫的非常清楚,大家有興趣可以看上面推薦的統計與學習方法書,寫的淺顯易懂,或者看這幾篇博客
http://blog.csdn.net/app_12062011/article/details/50536369
機器學習算法支持向量機系列博客
http://blog.csdn.net/zouxy09/article/details/16955347
http://blog.csdn.net/zouxy09/article/details/17291543
http://blog.csdn.net/zouxy09/article/details/17291805
http://blog.csdn.net/zouxy09/article/details/17292011
這兩位博主都重點講解了SVM的推導過程。這里我就本着站在巨人的肩膀上的思想,不再贅述,我的側重點在於實際的代碼編寫上,比較理論總歸要回到實踐上,這也是每個算法的歸宿所在。
好了,下面我就簡要寫出簡要介紹一下,線性支持向量機,近似線性支持向量機以及非線性支持向量機(核函數)
1 線性支持向量機
求解線性支持向量機的過程是凸二次規划問題,所謂凸二次規划問題,就是目標函數是凸的二次可微函數,約束函數為仿射函數(滿足f(x)=a*x+b,a,x均為n為向量)。而我們說求解凸二次規划問題可以利用對偶算法--即引入拉格朗日算子,利用拉格朗日對偶性將原始問題的最優解問題轉化為拉格朗日對偶問題,這樣就將求w*,b的原始問題的極小問題轉化為求alpha*(alpha>=0)的對偶問題的極大問題,即求出alpha*,在通過KKT條件求出對應的參數w*,b,從而找到這樣的間隔最大化超平面,進而利用該平面完成樣本分類
2 近似線性支持向量機
當數據集並不是嚴格線性可分時,即滿足絕不部分樣本點是線性可分,存在極少部分異常點;這里也就是說存在部分樣本不能滿足約束條件,此時我們可以引入松弛因子,這樣這些樣本點到超平面的函數距離加上松弛因子,就能保證被超平面分隔開來;當然,添加了松弛因子sigma,我們也會添加對應的代價項,使得alpha滿足0=<alpha<=C
3 非線性支持向量機
顯然,當數據集不是線性可分的,即我們不能通過前面的線性模型來對數據集進行分類。此時,我們必須想辦法將這些樣本特征符合線性模型,才能通過線性模型對這些樣本進行分類。這就要用到核函數,核函數的功能就是將低維的特征空間映射到高維的特征空間,而在高維的特征空間中,這些樣本進過轉化后,變成了線性可分的情況,這樣,在高維空間中,我們就能夠利用線性模型來解決數據集分類問題
好了,我們就只講這么寫大致的概念,如果想要透徹理解SVM建議還是要看看上面的書和博客文章,篇幅有限,我這里的中心在於凸二次規划的優化算法--SMO(序列最小最優化算法)
3 SMO算法
SMO是一種用於訓練SVM的強大算法,它將大的優化問題分解為多個小的優化問題來進行求解。而這些小優化問題往往很容易求解,並且對它們進行順序求解和對整體求解結果是一致的。在結果一致的情況下,顯然SMO算法的求解時間要短很多,這樣當數據集容量很大時,SMO就是一致十分高效的算法
SMO算法的目標是找到一系列alpha和b,而求出這些alpha,我們就能求出權重w,這樣就能得到分隔超平面,從而完成分類任務
SMO算法的工作原理是:每次循環中選擇兩個alpha進行優化處理。一旦找到一對合適的alpha,那么就增大其中一個而減少另外一個。這里的"合適",意味着在選擇alpha對時必須滿足一定的條件,條件之一是這兩個alpha不滿足最優化問題的kkt條件,另外一個條件是這兩個alpha還沒有進行區間化處理
對於SMO算法編寫,我們采用由簡單到復雜的方法,層層遞進,完成最終的SMO算法實現,最后通過實際的用例對SVM模型進行訓練,並驗證准確性
1 簡化版SMO算法
簡化版SMO算法,省略了確定要優化的最佳alpha對的步驟,而是首先在數據集上進行遍歷每一個alpha,再在剩余的數據集中找到另外一個alpha,構成要優化的alpha對,同時對其進行優化,這里的同時是要確保公式:Σαi*label(i)=0。所以改變一個alpha顯然會導致等式失效,所以這里需要同時改變兩個alpha。接下來看實際的代碼:
簡易版SMO算法的輔助函數:
#SMO算法相關輔助中的輔助函數 #1 解析文本數據函數,提取每個樣本的特征組成向量,添加到數據矩陣 #添加樣本標簽到標簽向量 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 #2 在樣本集中采取隨機選擇的方法選取第二個不等於第一個alphai的 #優化向量alphaj def selectJrand(i,m): j=i while(j==i): j=int(random.uniform(0,m)) return j #3 約束范圍L<=alphaj<=H內的更新后的alphaj值 def clipAlpha(aj,H,L): if aj>H: aj=H if L>aj: aj=L return aj
上面是簡易版SMO算法需要用到的一些功能,我們將其包裝成函數,需要時調用即可,接下來看算法的偽代碼:
#SMO算法的偽代碼 #創建一個alpha向量並將其初始化為0向量 #當迭代次數小於最大迭代次數時(w外循環) #對數據集中每個數據向量(內循環): #如果該數據向量可以被優化: #隨機選擇另外一個數據向量 #同時優化這兩個向量 #如果兩個向量都不能被優化,退出內循環
#如果所有向量都沒有被優化,增加迭代次數,繼續下一次循環
實際代碼如下:
#@dataMat :數據列表 #@classLabels:標簽列表 #@C :權衡因子(增加松弛因子而在目標優化函數中引入了懲罰項) #@toler :容錯率 #@maxIter :最大迭代次數 def smoSimple(dataMat,classLabels,C,toler,maxIter): #將列表形式轉為矩陣或向量形式 dataMatrix=mat(dataMatIn);labelMat=mat(classLabels).transpose() #初始化b=0,獲取矩陣行列 b=0;m,n=shape(dataMatrix) #新建一個m行1列的向量 alphas=mat(zeros((m,1))) #迭代次數為0 iter=0 while(iter<maxIter): #改變的alpha對數 alphaPairsChanged=0 #遍歷樣本集中樣本 for i in range(m): #計算支持向量機算法的預測值 fXi=float(multiply(alphas,labelMat).T*\ (dataMatrix*dataMatrix[i,:].T))+b #計算預測值與實際值的誤差 Ei=fXi-float(labelMat[i]) #如果不滿足KKT條件,即labelMat[i]*fXi<1(labelMat[i]*fXi-1<-toler) #and alpha<C 或者labelMat[i]*fXi>1(labelMat[i]*fXi-1>toler)and alpha>0 if((labelMat[i]*Ei<-toler)and(alpha<C))or\ ((labelMat[i]*Ei>toler)and(alpha[i]>0))): #隨機選擇第二個變量alphaj j=selectJrand(i,m) #計算第二個變量對應數據的預測值 fXj=float(multiply(alphas,labelMat).T*\ (dataMatrix*dataMatrix[j,:]).T)+b #計算與測試與實際值的差值 Ej=fXj-float(label[j]) #記錄alphai和alphaj的原始值,便於后續的比較 alphaIold=alphas[i].copy() alphaJold=alphas[j].copy() #如何兩個alpha對應樣本的標簽不相同 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 #根據公式計算未經剪輯的alphaj #------------------------------------------ eta=2.0*dataMatrix[i,:]*dataMatrix[j,:].T-\ dataMatrix[i,:]*dataMatrix[i,:].T-\ dataMatrix[j,:]*dataMatrix[j,:].T #如果eta>=0,跳出本次循環 if eta>=0:print("eta>=0"):continue alphas[j]-=labelMat[j]*(Ei-Ej)/eta alphas[j]=clipAlpha(alphas[j],H,L) #------------------------------------------ #如果改變后的alphaj值變化不大,跳出本次循環 if(abs(alphas[j]-alphaJold)<0.00001):print("j not moving\ enough");continue #否則,計算相應的alphai值 alphas[i]+=labelMat[j]*labelMat[i]*(alphaJold-alphas[j]) #再分別計算兩個alpha情況下對於的b值 b1=b-Ei-labelMat[i]*(alphas[i]-alphaIold)*\ dataMatrix[i,:]*dataMat[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 #如果0<alphai<C,那么b=b1 if(0<alphas[i]) and (C>alphas[i]):b=b1 #否則如果0<alphai<C,那么b=b1 elif (0<alphas[j]) and (C>alphas[j]):b=b2 #否則,alphai,alphaj=0或C else:b=(b1+b2)/2.0 #如果走到此步,表面改變了一對alpha值 alphaPairsChanged+=1 print("iter: &d i:%d,paird changed %d",%(iter,i,alphaPairsChanged)) #最后判斷是否有改變的alpha對,沒有就進行下一次迭代 if(alphaPairsChanged==0):iter+=1 #否則,迭代次數置0,繼續循環 else:iter=0 print("iteration number: %d" %iter) #返回最后的b值和alpha向量 return b,alphas
上面的代碼量看起來很多,但事實上只要理解了SVM算法的理論知識,就很容易理解,其只不過是將理論轉化為機器可以運行的語言而已。
上面代碼在一台性能一般的筆記本上對100個樣本的數據集上運行,收斂時間14.5秒,取得了令人滿意的分類效果
當然,上面的代碼通過對整個數據集進行兩次遍歷的方法來尋找alpha對的方法,顯然存在一定的不足,如果數據集規模較小的情況下,或許還可以滿足要求。但是對於大規模的數據集而言,上面的代碼顯然收斂速度非常慢,所以,接下來我們在此基礎上對選取合適的alpha對方法進行改進,采用啟發式的方法來選取合適的alpha對,從而提升運算效率。
2 啟發式選取alpha變量的SMO算法
啟發式的SMO算法一個外循環來選擇第一個alpha值,並且其選擇過程會在下面兩種方法之間進行交替:
(1)在所有數據集上進行單遍掃描
(2)另一種方法是在間隔邊界上樣本點進行單遍掃描,所謂間隔邊界上的點即為支持向量點。
顯然,對於整個數據集遍歷比較容易,而對於那些處於間隔邊界上的點,我們還需要事先將這些點對應的alpha值找出來,存放在一個列表中,然后對列表進行遍歷;此外,在選擇第一個alpha值后,算法會通過一個內循環來選擇第二個值,在優化的過程中依據alpha的更新公式αnew,unc=aold+label*(Ei-Ej)/η,(η=dataMat[i,:]*dataMat[i,:].T+dataMat[j,:]*dataMat[j,:].T-2*dataMat[i,:]*dataMat[j,:].T),可知alpha值的變化程度更Ei-Ej的差值成正比,所以,為了使alpha有足夠大的變化,選擇使Ei-Ej最大的alpha值作為另外一個alpha。所以,我們還可以建立一個全局的緩存用於保存誤差值,便於我們選擇合適的alpha值
下面是創建的一個數據結構類,便於我們存取算法中需要用到的重要數據:
#啟發式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 #修改選擇第二個變量alphaj的方法 def selectJ(i,oS,Ei): maxK=-1;maxDeltaE=-;Ej=0 #將誤差矩陣每一行第一列置1,以此確定出誤差不為0 #的樣本 oS.eCache[i]=[1,Ei] #獲取緩存中Ei不為0的樣本對應的alpha列表 validEcacheList=nonzero(oS.Cache[:,0].A)[0] #在誤差不為0的列表中找出使abs(Ei-Ej)最大的alphaj if(len(validEcacheList)>0): for k in validEcacheList: if k ==i:continue Ek=calcEk(oS,k) deltaE=abs(Ei-Ek) if(deltaE>maxDeltaE): maxK=k;maxDeltaE=deltaE;Ej=Ek return maxK,Ej else: #否則,就從樣本集中隨機選取alphaj j=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外循環代碼 def smoP(dataMatIn,classLabels,C,toler,maxIter,kTup=('lin',0)): #保存關鍵數據 oS=optStruct(mat(dataMatIn),mat(classLabels).transpose(),C,toler) iter=0 enrireSet=True;alphaPairsChanged=0 #選取第一個變量alpha的三種情況,從間隔邊界上選取或者整個數據集 while(iter<maxIter)and((alphaPairsChanged>0)or(entireSet)): alphaPairsChanged=0 #沒有alpha更新對 if entireSet: for i in range(oS.m): alphaPairsChanged+=innerL(i,oS) print("fullSet,iter: %d i:%d,pairs changed %d",%\ (iter,i,alphaPairsChanged)) else: #統計alphas向量中滿足0<alpha<C的alpha列表 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+=1 if entireSet:entireSet=False #如果本次循環沒有改變的alpha對,將entireSet置為true, #下個循環仍遍歷數據集 elif (alphaPairsChanged==0):entireSet=True print("iteration number: %d",%iter) return oS.b,oS.alphas
#內循環尋找alphaj def innerL(i,oS): #計算誤差 Ei=calcEk(oS,i) #違背kkt條件 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=alphas[i].copy();alphaJold=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 #計算兩個alpha值 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]=clipAlpha(oS.alphas[j],H,L) 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]) updateEk(oS,i) #在這兩個alpha值情況下,計算對應的b值 #注,非線性可分情況,將所有內積項替換為核函數K[i,j] 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 #如果有alpha對更新 return 1 #否則返回0 else return 0
顯然,上面的SMO完整代碼是分為內外兩個循環函數來編寫的,采取這樣的結構可以更方便我們去理解選取兩個alpha的過程;既然,我們已經計算出了alpha值和b值,那么顯然我們可以利用公式w*=Σαi*label[i]*dataMat[i,:]計算出相應的權值參數,然后就可以得到間隔超平面的公式w*x+b*來完成樣本的分類了,由於SVM算法是一種二類分類算法,正值為1,負值為-1,即分類的決策函數為跳躍函數sign(w*x+b*)
然后,我們可以編寫一小段測試代碼,來利用SMO算法得到的alpha值和b值,計算分類決策函數,從而實現具體的預測分類了
#求出了alpha值和對應的b值,就可以求出對應的w值,以及分類函數值 def predict(alphas,dataArr,classLabels): X=mat(dataArr);labelMat=mat(classLabels) m,n=shape(X) w=zeros((n,1)) for i in range(m): w+=multiply(alphas[i]*labelMat[i],X[i,:].T) result=dataArr[0]*mat(ws)+b return sign(result)
看一下分類效果:
3 核函數
核函數的目的主要是為了解決非線性分類問題,通過核技巧將低維的非線性特征轉化為高維的線性特征,從而可以通過線性模型來解決非線性的分類問題。
如下圖,當數據集不是線性可分時,即數據集分布是下面的圓形該怎么辦呢?
顯然,此時數據集線性不可分,我們無法用一個超平面來將兩種樣本分隔開;那么我們就希望將這些數據進行轉化,轉化之后的數據就能夠通過一個線性超平面將不同類別的樣本分開,這就需要核函數,核函數的目的主要是為了解決非線性分類問題,通過核技巧將低維的非線性特征轉化為高維的線性特征,從而可以通過線性模型來解決非線性的分類問題。
而徑向基核函數,是SVM中常用的一個核函數。徑向基核函數是一個采用向量作為自變量的函數,能夠基於向量距離運算輸出一個標量。徑向基核函數的高斯版本公式為:
k(x,y)=exp(-||x-y||2/2σ2),其中,σ為到達率,決定了函數值跌落至0的速度
下面通過代碼編寫高斯核函數:
#徑向基核函數是svm常用的核函數 #核轉換函數 def kernelTrans(X,A,kTup): m,n=shape(X) K=mat(zeros((m,1))) #如果核函數類型為'lin' if kTup[0]=='lin':K=X*A.T #如果核函數類型為'rbf':徑向基核函數 #將每個樣本向量利用核函數轉為高維空間 elif kTup[0]=='rbf' for j in range(m): deltaRow=X[j,:]-A K[j]=deltaRow*deltaRow.T K=exp(K/(-1*kTup[1]**2)) else:raise NameError('Houston we Have a Problem -- \ That Kernel is not recognised') return K #對核函數處理的樣本特征,存入到optStruct中 class optStruct: def __init__(self,dataMatIn,classLabels,C,toler,kTup): 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)
需要說明的是,這里引入了一個變量kTup,kTup是一個包含核信息的元組,它提供了選取的核函數的類型,比如線性'lin'或者徑向基核函數'rbf';以及用戶提供的到達率σ
有了高斯核函數之后,我們只要將上面的SMO算法中所有的內積項替換為核函數即可,比如講dataMat[i,:]*dataMat[j,:].T替換為k[i,j]即可,替換效果如下:
def innerL(i,oS): #計算誤差 Ei=calcEk(oS,i) #違背kkt條件 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=alphas[i].copy();alphaJold=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 #計算兩個alpha值 eta=2.0*oS.K[i,j]-oS.K[i,i]-oS.K[j,j] if eta>=0:print("eta>=0");return 0 oS.alphas[j]-=oS.labelMat[j]*(Ei-Ej)/eta oS.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 0 oS.alphas[i]+=oS.labelMat[j]*oS.labelMat[i]*\ (alphaJold-oS.alphas[j]) updateEk(oS,i) #在這兩個alpha值情況下,計算對應的b值 #注,非線性可分情況,將所有內積項替換為核函數K[i,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[i,j] 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 #如果有alpha對更新 return 1 #否則返回0 else return 0
有了核函數,我們就能對非線性的數據集進行分類預測了,接下來就是編寫代碼利用核函數進行測試,需要說明的是,在優化的過程中,我們僅僅需要找到支持向量和其對應的alpha值,而對於其他的樣本值可以不用管,甚至可以舍棄,因為這些樣本將不會對分類預測函數造成任何影響。這也就是SVM相比KNN算法的優秀的地方所在
#測試核函數 #用戶指定到達率 def testRbf(k1=1.3): #第一個測試集 dataArr,labelArr=loadDataSet('testSetRBF.txt') b,alphas=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): kernelEval=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=loadDataSet('testSetRBF2.txt') dataMat=mat(dataArr);labelMat=mat(labelArr).transpose() errorCount=0 m,n=shape(dataMat) for i in range(m): kernelEval=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))
當用戶輸入σ=1.3時的實驗結果為:
當σ=0.1時實驗結果為:
通過輸入不同的σ值(當然,迭代次數也會有一定的影響,我們只討論σ值),我們發現測試錯誤率,訓練誤差率,支持向量個數都會發生變化,在一定的范圍內,支持向量數目的下降,會使得訓練錯誤率和測試錯誤率都下降,但是當抵達某處的最優值時,再次通過增大σ值的方法減少支持向量,此時訓練錯誤率下降,而測試誤差上升
簡言之,對於固定的數據集,支持向量的數目存在一個最優值,如果支持向量太少,會得到一個很差的決策邊界;而支持向量太多,也相當於利用整個數據集進行分類,就類似於KNN算法,顯然運算速度不高。
三,SVM實例:手寫識別問題
相較於第二張的KNN算法,盡管KNN也能取得不錯的效果;但是從節省內存的角度出發,顯然SVM算法更勝一籌,因為其不需要保存真個數據集,而只需要其作用的支持向量點,而取得不錯的分類效果。
#實例:手寫識別問題 #支持向量機由於只需要保存支持向量,所以相對於KNN保存整個數據集占用更少內存 #且取得可比的效果 #基於svm的手寫數字識別 def loadImages(dirName): from os import listdir hwLabels=[] 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]) if classNumStr==9:hwLabels.append(-1) else:hwLabels.append(1) trainingMat[i,:]=img2vector('%s/%s',%(dirName,fileNameStr)) return hwLabels,trainingMat #將圖像轉為向量 def img2vector(fileaddir): featVec=zeros((1,1024)) fr=open(filename) for i in range(32): lineStr=fr.readline() for j in range(32): featVec[0,32*i+j]=int(lineStr[j]) return featVec #利用svm測試數字 def testDigits(kTup=('rbf',10)): #訓練集 dataArr,labelArr=loadDataSet('trainingDigits') b,alphas=smoP(dataArr,labelArr,200,0.0001,10000,kTup) 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): kernelEval=kernelTrans(sVs,dataMat[i,:],kTup) 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=loadDataSet('testDigits.txt') dataMat=mat(dataArr);labelMat=mat(labelArr).transpose() errorCount=0 m,n=shape(dataMat) for i in range(m): kernelEval=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))
下面來看一下,在kTup=('rbf',20)情況下的測試誤差率和支持向量個數情況
並且通過嘗試不同的σ值,以及嘗試了線性核函數,可以得到關於不同σ值的書寫數字識別性能:
內核模式,設置 | 訓練錯誤率(%) | 測試錯誤率(%) | 支持向量數 |
rbf,0.1 | 0 | 52 | 402 |
rbf,5 | 0 | 3.2 | 402 |
rbf,10 | 0 | 0.5 | 99 |
rbf,50 | 0.2 | 2.2 | 41 |
rbf,100 | 4.5 | 4.3 | 26 |
Linear | 2.7 | 2.2 | 38 |
由上圖可以看出,σ值在取10時取得了最好的分類效果,這也印證了我們上面的敘述。即對於固定的數據集,存在最優的支持向量個數,使得分類錯誤率最低。支持向量的個數會隨着σ值的增大而逐漸減少,但是分類錯誤率確實一個先降低后升高的過程。即最小的分類錯誤率並不意味着最少的支持向量個數。
4 總結
支持向量機是一種通過求解凸二次規划問題來解決分類問題的算法,具有較低的泛化錯誤率。而SMO算法可以通過每次只優化兩個alpha值來加快SVM的訓練速度。
核技巧是將數據由低維空間映射到高維空間,可以將一個低維空間中的非線性問題轉換為高維空間下的線性問題來求解。而徑向基核函數是一個常用的度量兩個向量距離的核函數。
最后,支持向量機的優缺點:
優點:泛化錯誤率低,計算開銷不大
缺點:對參數調節和核函數的選擇敏感,且僅適用於二類分類