機器學習算法與Python實踐這個系列主要是參考《機器學習實戰》這本書。因爲自己想學習Python,然後也想對一些機器學習算法加深下了解,所以就想通過Python來實現幾個比較常用的機器學習算法。恰好遇見這本同樣定位的書籍,所以就參考這本書的過程來學習了。
在這一節我們主要是對支持向量機進行系統的回顧,以及通過Python來實現。由於內容很多,所以這裏分成三篇博文。第一篇講SVM初級,第二篇講進階,主要是把SVM整條知識鏈理直,第三篇介紹Python的實現。SVM有很多介紹的非常好的博文,具體可以參考本文列出的參考文獻和推薦閱讀資料。在本文中,定位在於把集大成於一身的SVM的整體知識鏈理直,所以不會涉及細節的推導。網上的解說的很好的推導和書籍很多,大家可以進一步參考。
目錄
一、引入
二、線性可分SVM與硬間隔最大化
三、Dual優化問題
3.1、對偶問題
3.2、SVM優化的對偶問題
四、鬆弛向量與軟間隔最大化
五、核函數
六、多類分類之SVM
6.1、“一對多”的方法
6.2、“一對一”的方法
七、KKT條件分析
八、SVM的實現之SMO算法
8.1、座標下降算法
8.2、SMO算法原理
8.3、SMO算法的Python實現
九、參考文獻與推薦閱讀
一、引入
支持向量機(SupportVector Machines),這個名字可是響噹噹的,在機器學習或者模式識別領域可是無人不知,無人不曉啊。八九十年代的時候,和神經網絡一決雌雄,獨領風騷,並吸引了大批爲之狂熱和追隨的粉絲。雖然幾十年過去了,但風采不減當年,在模式識別領域依然佔據着大遍江山。王位穩固了幾十年。當然了,它也繁衍了很多子子孫孫,出現了很多基因改良的版本,也發展了不少裙帶關係。但其中的睿智依然被世人稱道,並將千秋萬代!
好了,買了那麼久廣告,不知道是不是高估了。我們還是腳踏實地,來看看傳說的SVM是個什麼東西吧。我們知道,分類的目的是學會一個分類函數或分類模型(或者叫做分類器),該模型能把數據庫中的數據項映射到給定類別中的某一個,從而可以用於預測未知類別。對於用於分類的支持向量機,它是個二分類的分類模型。也就是說,給定一個包含正例和反例(正樣本點和負樣本點)的樣本集合,支持向量機的目的是尋找一個超平面來對樣本進行分割,把樣本中的正例和反例用超平面分開,但是不是簡單地分看,其原則是使正例和反例之間的間隔最大。學習的目標是在特徵空間中找到一個分類超平面wx+b=0,分類面由法向量w和截距b決定。分類超平面將特徵空間劃分兩部分,一部分是正類,一部分是負類。法向量指向的一側是正類,另一側爲負類。
用一個二維空間裏僅有兩類樣本的分類問題來舉個小例子。假設我們給定了下圖左圖所示的兩類點Class1和Class2(也就是正樣本集和負樣本集)。我們的任務是要找到一個線,把他們劃分開。你會告訴我,那簡單,揮筆一畫,洋洋灑灑五顏六色的線就出來了,然後很得意的和我說,看看吧,下面右圖,都是你要的答案,如果你還想要,我還可以給你畫出無數條。對,沒錯,的確可以畫出無數條。那哪條最好呢?你會問我,怎麼樣衡量“好”?假設Class1和Class2分別是兩條村子的人,他們因爲兩條村子之間的地盤分割的事鬧僵了,叫你去說個理,到底怎麼劃分纔是最公平的。這裏的“好”,可以理解爲對Class1和Class2都是公平的。然後你二話不說,指着黑色那條線,說“就它了!正常人都知道!在兩條村子最中間畫條線很明顯對他們就是公平的,誰也別想多,誰也沒拿少”。這個例子可能不太恰當,但道理還是一樣的。對於分類來說,我們需要確定一個分類的線,如果新的一個樣本到來,如果落在線的左邊,那麼這個樣本就歸爲class1類,如果落在線的右邊,就歸爲class2這一類。那哪條線纔是最好的呢?我們仍然認爲是中間的那條,因爲這樣,對新的樣本的劃分結果我們才認爲最可信,那這裏的“好”就是可信了。另外,在二維空間,分類的就是線,如果是三維的,分類的就是面了,更高維,也有個霸氣的名字叫超平面。因爲它霸氣,所以一般將任何維的分類邊界都統稱爲超平面。
好了。對於人來說,我們可以輕易的找到這條線或者超平面(當然了,那是因爲你可以看到樣本具體的分佈是怎樣的,如果樣本的維度大於三維的話,我們就沒辦法把這些樣本像上面的圖一樣畫出來了,這時候就看不到了,這時候靠人的雙眼也無能爲力了。“如果我能看得見,生命也許完全不同,可能我想要的,我喜歡的我愛的,都不一樣……”),但計算機怎麼知道怎麼找到這條線呢?我們怎麼把我們的找這條線的方法告訴他,讓他按照我們的方法來找到這條線呢?呃,我們要建模!!!把我們的意識“強加”給計算機的某個數學模型,讓他去求解這個模型,得到某個解,這個解就是我們的這條線,那這樣目的就達到了。那下面就得開始建模之旅了。
二、線性可分SVM與硬間隔最大化
其實上面這種分類思想就是SVM的思想。可以表達爲:SVM試圖尋找一個超平面來對樣本進行分割,把樣本中的正例和反例用超平面分開,但是不是很敷衍地簡單的分開,而是盡最大的努力使正例和反例之間的間隔margin最大。這樣它的分類結果才更加可信,而且對於未知的新樣本纔有很好的分類預測能力(機器學習美其名曰泛化能力)。
我們的目標是尋找一個超平面,使得離超平面比較近的點能有更大的間距。也就是我們不考慮所有的點都必須遠離超平面,我們關心求得的超平面能夠讓所有點中離它最近的點具有最大間距。
我們先用數學公式來描述下。假設我們有N個訓練樣本{(x1, y1),(x2, y2), …, (xN, yN)},x是d維向量,而yi∊{+1, -1}是樣本的標籤,分別代表兩個不同的類。這裏我們需要用這些樣本去訓練學習一個線性分類器(超平面):f(x)=sgn(wTx + b),也就是wTx + b大於0的時候,輸出+1,小於0的時候,輸出-1。sgn()表示取符號。而g(x) =wTx + b=0就是我們要尋找的分類超平面,如上圖所示。剛纔說我們要怎麼做了?我們需要這個超平面最大的分隔這兩類。也就是這個分類面到這兩個類的最近的那個樣本的距離相同,而且最大。爲了更好的說明,我們在上圖中找到兩個和這個超平面平行和距離相等的超平面:H1: y = wTx + b=+1 和 H2: y = wTx + b=-1。
好了,這時候我們就需要兩個條件:(1)沒有任何樣本在這兩個平面之間;(2)這兩個平面的距離需要最大。(對任何的H1和H2,我們都可以歸一化係數向量w,這樣就可以得到H1和H2表達式的右邊分別是+1和-1了)。先來看條件(2)。我們需要最大化這個距離,所以就存在一些樣本處於這兩條線上,他們叫支持向量(後面會說到他們的重要性)。那麼它的距離是什麼呢?我們初中就學過,兩條平行線的距離的求法,例如ax+by=c1和ax+by=c2,那他們的距離是|c2-c1|/sqrt(x2+y2)(sqrt()表示開根號)。注意的是,這裏的x和y都表示二維座標。而用w來表示就是H1:w1x1+w2x2=+1和H2:w1x1+w2x2=-1,那H1和H2的距離就是|1+1|/ sqrt(w12+w12)=2/||w||。也就是w的模的倒數的兩倍。也就是說,我們需要最大化margin=2/||w||,爲了最大化這個距離,我們應該最小化||w||,看起來好簡單哦。同時我們還需要滿足條件(2),也就是同時要滿足沒有數據點分佈在H1和H2之間:
也就是,對於任何一個正樣本yi=+1,它都要處於H1的右邊,也就是要保證:y= wTx+ b>=+1。對於任何一個負樣本yi=-1,它都要處於H2的左邊,也就是要保證:y = wTx + b<=-1。這兩個約束,其實可以合併成同一個式子:yi (wTxi + b)>=1。
所以我們的問題就變成:
這是個凸二次規劃問題。什麼叫凸?凸集是指有這麼一個點的集合,其中任取兩個點連一條直線,這條線上的點仍然在這個集合內部,因此說“凸”是很形象的。例如下圖,對於凸函數(在數學表示上,滿足約束條件是仿射函數,也就是線性的Ax+b的形式)來說,局部最優就是全局最優,但對非凸函數來說就不是了。二次表示目標函數是自變量的二次函數。
好了,既然是凸二次規劃問題,就可以通過一些現成的 QP (Quadratic Programming) 的優化工具來得到最優解。所以,我們的問題到此爲止就算全部解決了。雖然這個問題確實是一個標準的 QP 問題,但是它也有它的特殊結構,通過 Lagrange Duality 變換到對偶變量 (dual variable) 的優化問題之後,可以找到一種更加有效的方法來進行求解,而且通常情況下這種方法比直接使用通用的 QP 優化包進行優化要高效得多。也就說,除了用解決QP問題的常規方法之外,還可以應用拉格朗日對偶性,通過求解對偶問題得到最優解,這就是線性可分條件下支持向量機的對偶算法,這樣做的優點在於:一是對偶問題往往更容易求解;二者可以自然的引入核函數,進而推廣到非線性分類問題。那什麼是對偶問題?
三、Dual優化問題
3.1、對偶問題
在約束最優化問題中,常常利用拉格朗日對偶性將原始問題轉換爲對偶問題,通過求解對偶問題而得到原始問題的解。至於這其中的原理和推導參考文獻[3]講得非常好。大家可以參考下。這裏只將對偶問題是怎麼操作的。假設我們的優化問題是:
min f(x)
s.t. hi(x) = 0, i=1, 2, …,n
這是個帶等式約束的優化問題。我們引入拉格朗日乘子,得到拉格朗日函數爲:
L(x, α)=f(x)+α1h1(x)+ α2h2(x)+…+αnhn(x)
然後我們將拉格朗日函數對x求極值,也就是對x求導,導數爲0,就可以得到α關於x的函數,然後再代入拉格朗日函數就變成:
max W(α) = L(x(α), α)
這時候,帶等式約束的優化問題就變成只有一個變量α(多個約束條件就是向量)的優化問題,這時候的求解就很簡單了。同樣是求導另其等於0,解出α即可。需要注意的是,我們把原始的問題叫做primal problem,轉換後的形式叫做dual problem。需要注意的是,原始問題是最小化,轉化爲對偶問題後就變成了求最大值了。對於不等式約束,其實是同樣的操作。簡單地來說,通過給每一個約束條件加上一個 Lagrange multiplier(拉格朗日乘子),我們可以將約束條件融和到目標函數裏去,這樣求解優化問題就會更加容易。(這裏其實涉及到很多蠻有趣的東西的,大家可以參考更多的博文)
3.2、SVM優化的對偶問題
對於SVM,前面提到,其primal problem是以下形式:
同樣的方法引入拉格朗日乘子,我們就可以得到以下拉格朗日函數:
然後對L(w, b, α)分別求w和b的極值。也就是L(w, b,α)對w和b的梯度爲0:∂L/∂w=0和∂L/∂b=0,還需要滿足α>=0。求解這裏導數爲0的式子可以得到:
然後再代入拉格朗日函數後,就變成:
這個就是dual problem(如果我們知道α,我們就知道了w。反過來,如果我們知道w,也可以知道α)。這時候我們就變成了求對α的極大,即是關於對偶變量α的優化問題(沒有了變量w,b,只有α)。當求解得到最優的α*後,就可以同樣代入到上面的公式,導出w*和b*了,最終得出分離超平面和分類決策函數。也就是訓練好了SVM。那來一個新的樣本x後,就可以這樣分類了:
在這裏,其實很多的αi都是0,也就是說w只是一些少量樣本的線性加權值。這種“稀疏”的表示實際上看成是KNN的數據壓縮的版本。也就是說,以後新來的要分類的樣本首先根據w和b做一次線性運算,然後看求的結果是大於0還是小於0來判斷正例還是負例。現在有了αi,我們不需要求出w,只需將新來的樣本和訓練數據中的所有樣本做內積和即可。那有人會說,與前面所有的樣本都做運算是不是太耗時了?其實不然,我們從KKT條件中得到,只有支持向量的αi不爲0,其他情況αi都是0。因此,我們只需求新來的樣本和支持向量的內積,然後運算即可。這種寫法爲下面要提到的核函數(kernel)做了很好的鋪墊。如下圖所示:
四、鬆弛向量與軟間隔最大化
我們之前討論的情況都是建立在樣本的分佈比較優雅和線性可分的假設上,在這種情況下可以找到近乎完美的超平面對兩類樣本進行分離。但如果遇到下面這兩種情況呢?左圖,負類的一個樣本點A不太合羣,跑到正類這邊了,這時候如果按上面的確定分類面的方法,那麼就會得到左圖中紅色這條分類邊界,嗯,看起來不太爽,好像全世界都在將就A一樣。還有就是遇到右圖的這種情況。正類的一個點和負類的一個點都跑到了別人家門口,這時候就找不到一條直線來將他們分開了,那這時候怎麼辦呢?我們真的要對這些零丁的不太聽話的離羣點屈服和將就嗎?就因爲他們的不完美改變我們原來完美的分界面會不會得不償失呢?但又不得不考慮他們,那怎樣才能折中呢?
對於上面說的這種偏離正常位置很遠的數據點,我們稱之爲 outlier,它有可能是採集訓練樣本的時候的噪聲,也有可能是某個標數據的大叔打瞌睡標錯了,把正樣本標成負樣本了。那一般來說,如果我們直接忽略它,原來的分隔超平面還是挺好的,但是由於這個 outlier 的出現,導致分隔超平面不得不被擠歪了,同時 margin 也相應變小了。當然,更嚴重的情況是,如果出現右圖的這種outlier,我們將無法構造出能將數據線性分開的超平面來。
爲了處理這種情況,我們允許數據點在一定程度上偏離超平面。也就是允許一些點跑到H1和H2之間,也就是他們到分類面的間隔會小於1。如下圖:
具體來說,原來的約束條件就變爲:
這時候,我們在目標函數裏面增加一個懲罰項,新的模型就變成(也稱軟間隔):
引入非負參數ξi後(稱爲鬆弛變量),就允許某些樣本點的函數間隔小於1,即在最大間隔區間裏面,或者函數間隔是負數,即樣本點在對方的區域中。而放鬆限制條件後,我們需要重新調整目標函數,以對離羣點進行處罰,目標函數後面加上的第二項就表示離羣點越多,目標函數值越大,而我們要求的是儘可能小的目標函數值。這裏的C是離羣點的權重,C越大表明離羣點對目標函數影響越大,也就是越不希望看到離羣點。這時候,間隔也會很小。我們看到,目標函數控制了離羣點的數目和程度,使大部分樣本點仍然遵守限制條件。
這時候,經過同樣的推導過程,我們的對偶優化問題變成:
此時,我們發現沒有了參數ξi,與之前模型唯一不同在於αi又多了αi<=C的限制條件。需要提醒的是,b的求值公式也發生了改變,改變結果在SMO算法裏面介紹。
五、核函數
如果我們的正常的樣本分佈如下圖左邊所示,之所以說是正常的指的是,不是上面說的那樣由於某些頑固的離羣點導致的線性不可分。它是真的線性不可分。樣本本身的分佈就是這樣的,如果也像樣本那樣,通過鬆弛變量硬拉一條線性分類邊界出來,很明顯這條分類面會非常糟糕。那怎麼辦呢?SVM對線性可分數據有效,對不可分的有何應對良策呢?是核方法(kernel trick)大展身手的時候了。
如上圖右,如果我們可以把我們的原始樣本點通過一個變換,變換到另一個特徵空間,在這個特徵空間上是線性可分的,那麼上面的SVM就可以輕易工作了。也就是說,對於不可分的數據,現在我們要做兩個工作:
1)首先使用一個非線性映射Φ(x)將全部原始數據x變換到另一個特徵空間,在這個空間中,樣本變得線性可分了;
2)然後在特徵空間中使用SVM進行學習分類。
好了,第二個工作沒什麼好說的,和前面的一樣。那第一個粗重活由誰來做呢?我們怎麼知道哪個變換纔可以將我們的數據映射爲線性可分呢?數據維度那麼大,我們又看不到。另外,這個變換會不會使第二步的優化變得複雜,計算量更大呢?對於第一個問題,有個著名的cover定理:將複雜的模式分類問題非線性地投射到高維空間將比投射到低維空間更可能是線性可分的。OK,那容易了,我們就要找到一個所有樣本映射到更高維的空間的映射。對不起,其實要找到這個映射函數很難。但是,支持向量機並沒有直接尋找和計算這種複雜的非線性變換,而是很智慧的通過了一種巧妙的迂迴方法來間接實現這種變換。它就是核函數,不僅具備這種超能力,同時又不會增加太多計算量的兩全其美的方法。我們可以回頭看看上面SVM的優化問題:
可以看到,對樣本x的利用,只是計算第i和第j兩個樣本的內積就可以了。
對於分類決策函數,也是計算兩個樣本的內積。也就是說,訓練SVM和使用SVM都用到了樣本間的內積,而且只用到內積。那如果我們可以找到一種方法來計算兩個樣本映射到高維空間後的內積的值就可以了。核函數就是完成這偉大的使命的:
K(xi, xj)=Φ(xi)T Φ(xj)
也就是兩個樣本xi和xj對應的高維空間的內積Φ(xi)T Φ(xj)通過一個核函數K(xi, xj)計算得到。而不用知道這個變換Φ(x)是何許人也。而且這個核函數計算很簡單,常用的一般是徑向基RBF函數:
這時候,我們的優化的對偶問題就變成了:
和之前的優化問題唯一的不同只是樣本的內積需要用核函數替代而已。優化過程沒有任何差別。而決策函數變成了:
也就是新來的樣本x和我們的所有訓練樣本計算核函數即可。需要注意的是,因爲大部分樣本的拉格朗日因子αi都是0,所以其實我們只需要計算少量的訓練樣本和新來的樣本的核函數,然後求和取符號即可完成對新來樣本x的分類了。支持向量機的決策過程也可以看做一種相似性比較的過程。首先,輸入樣本與一系列模板樣本進行相似性比較,模板樣本就是訓練過程決定的支持向量,而採用的相似性度量就是核函數。樣本與各支持向量比較後的得分進行加權後求和,權值就是訓練時得到的各支持向量的係數αi和類別標號的成績。最後根據加權求和值大小來進行決策。而採用不同的核函數,就相當於採用不同的相似度的衡量方法。
從計算的角度,不管Φ(x)變換的空間維度有多高,甚至是無限維(函數就是無限維的),這個空間的線性支持向量機的求解都可以在原空間通過核函數進行,這樣就可以避免了高維空間裏的計算,而計算核函數的複雜度和計算原始樣本內積的複雜度沒有實質性的增加。
到這裏,忍不住要感嘆幾聲。爲什麼“碰巧”SVM裏需要計算的地方數據向量總是以內積的形式出現?爲什麼“碰巧”存在能簡化映射空間中的內積運算的核函數?爲什麼“碰巧”大部分的樣本對決策邊界的貢獻爲0?…該感謝上帝,還是感謝廣大和偉大的科研工作者啊!讓我等凡夫俗子可以瞥見如此精妙和無與倫比的數學之美!
到這裏,和支持向量機相關的東西就介紹完了。總結一下:支持向量機的基本思想可以概括爲,首先通過非線性變換將輸入空間變換到一個高維的空間,然後在這個新的空間求最優分類面即最大間隔分類面,而這種非線性變換是通過定義適當的內積核函數來實現的。SVM實際上是根據統計學習理論依照結構風險最小化的原則提出的,要求實現兩個目的:1)兩類問題能夠分開(經驗風險最小)2)margin最大化(風險上界最小)既是在保證風險最小的子集中選擇經驗風險最小的函數。
六、多類分類之SVM
SVM是一種典型的兩類分類器,即它只回答屬於正類還是負類的問題。而現實中要解決的問題,往往是多類的問題。那如何由兩類分類器得到多類分類器呢?
6.1、“一對多”的方法
One-Against-All這個方法還是比較容易想到的。就是每次仍然解一個兩類分類的問題。比如我們5個類別,第一次就把類別1的樣本定爲正樣本,其餘2,3,4,5的樣本合起來定爲負樣本,這樣得到一個兩類分類器,它能夠指出一個樣本是還是不是第1類的;第二次我們把類別2 的樣本定爲正樣本,把1,3,4,5的樣本合起來定爲負樣本,得到一個分類器,如此下去,我們可以得到5個這樣的兩類分類器(總是和類別的數目一致)。到了有樣本需要分類的時候,我們就拿着這個樣本挨個分類器的問:是屬於你的麼?是屬於你的麼?哪個分類器點頭說是了,文章的類別就確定了。這種方法的好處是每個優化問題的規模比較小,而且分類的時候速度很快(只需要調用5個分類器就知道了結果)。但有時也會出現兩種很尷尬的情況,例如拿這個樣本問了一圈,每一個分類器都說它是屬於它那一類的,或者每一個分類器都說它不是它那一類的,前者叫分類重疊現象,後者叫不可分類現象。分類重疊倒還好辦,隨便選一個結果都不至於太離譜,或者看看這篇文章到各個超平面的距離,哪個遠就判給哪個。不可分類現象就着實難辦了,只能把它分給第6個類別了……更要命的是,本來各個類別的樣本數目是差不多的,但“其餘”的那一類樣本數總是要數倍於正類(因爲它是除正類以外其他類別的樣本之和嘛),這就人爲的造成了上一節所說的“數據集偏斜”問題。
如下圖左。紅色分類面將紅色與其他兩種顏色分開,綠色分類面將綠色與其他兩種顏色分開,藍色分類面將藍色與其他兩種顏色分開。
在這裏的對某個點的分類實際上是通過衡量這個點到三個決策邊界的距離,因爲到分類面的距離越大,分類越可信嘛。當然了,這個距離是有符號的,如下所示:
例如下圖左,將星星這個點劃分給綠色這一類。右圖將星星這個點劃分給褐色這一類。
6.2、“一對一”的方法
One-Against-One方法是每次選一個類的樣本作正類樣本,而負類樣本則變成只選一個類(稱爲“一對一單挑”的方法,哦,不對,沒有單挑,就是“一對一”的方法,呵呵),這就避免了偏斜。因此過程就是算出這樣一些分類器,第一個只回答“是第1類還是第2類”,第二個只回答“是第1類還是第3類”,第三個只回答“是第1類還是第4類”,如此下去,你也可以馬上得出,這樣的分類器應該有5 X 4/2=10個(通式是,如果有k個類別,則總的兩類分類器數目爲k(k-1)/2)。雖然分類器的數目多了,但是在訓練階段(也就是算出這些分類器的分類平面時)所用的總時間卻比“一類對其餘”方法少很多,在真正用來分類的時候,把一個樣本扔給所有分類器,第一個分類器會投票說它是“1”或者“2”,第二個會說它是“1”或者“3”,讓每一個都投上自己的一票,最後統計票數,如果類別“1”得票最多,就判這篇文章屬於第1類。這種方法顯然也會有分類重疊的現象,但不會有不可分類現象,因爲總不可能所有類別的票數都是0。如下圖右,中間紫色的塊,每類的得票數都是1,那就不知道歸類給那個類好了,只能隨便扔給某個類了(或者衡量這個點到三個決策邊界的距離,因爲到分類面的距離越大,分類越可信嘛),扔掉了就是你命好,扔錯了就不lucky了。
七、KKT條件分析
對KKT條件,請大家參考文獻[13][14]。假設我們優化得到的最優解是:αi*,βi*, ξi*, w*和b*。我們的最優解需要滿足KKT條件:
同時βi*和ξi*都需要大於等於0,而αi*需要在0和C之間。那可以分三種情況討論:
總的來說就是, KKT條件就變成了:
第一個式子表明如果αi=0,那麼該樣本落在兩條間隔線外。第二個式子表明如果αi=C,那麼該樣本有可能落在兩條間隔線內部,也有可能落在兩條間隔線上面,主要看對應的鬆弛變量的取值是等於0還是大於0,第三個式子表明如果0<αi<C,那麼該樣本一定落在分隔線上(這點很重要,b就是拿這些落在分隔線上的點來求的,因爲在分割線上wTx+b=1或者-1嘛,纔是等式,在其他地方,都是不等式,求解不了b)。具體形象化的表示如下:
通過KKT條件可知,αi不等於0的都是支持向量,它有可能落在分隔線上,也有可能落在兩條分隔線內部。KKT條件是非常重要的,在SMO也就是SVM的其中一個實現算法中,我們可以看到它的重要應用。
八、SVM的實現之SMO算法
終於到SVM的實現部分了。那麼神奇和有效的東西還得迴歸到實現纔可以展示其強大的功力。SVM有效而且存在很高效的訓練算法,這也是工業界非常青睞SVM的原因。
前面講到,SVM的學習問題可以轉化爲下面的對偶問題:
需要滿足的KKT條件:
也就是說找到一組αi可以滿足上面的這些條件的就是該目標的一個最優解。所以我們的優化目標是找到一組最優的αi*。一旦求出這些αi*,就很容易計算出權重向量w*和b,並得到分隔超平面了。
這是個凸二次規劃問題,它具有全局最優解,一般可以通過現有的工具來優化。但當訓練樣本非常多的時候,這些優化算法往往非常耗時低效,以致無法使用。從SVM提出到現在,也出現了很多優化訓練的方法。其中,非常出名的一個是1982年由Microsoft Research的John C. Platt在論文《Sequential Minimal Optimization: A Fast Algorithm for TrainingSupport Vector Machines》中提出的Sequential Minimal Optimization序列最小化優化算法,簡稱SMO算法。SMO算法的思想很簡單,它將大優化的問題分解成多個小優化的問題。這些小問題往往比較容易求解,並且對他們進行順序求解的結果與將他們作爲整體來求解的結果完全一致。在結果完全一致的同時,SMO的求解時間短很多。在深入SMO算法之前,我們先來了解下座標下降這個算法,SMO其實基於這種簡單的思想的。
8.1、座標下降(上升)法
假設要求解下面的優化問題:
在這裏,我們需要求解m個變量αi,一般來說是通過梯度下降(這裏是求最大值,所以應該叫上升)等算法每一次迭代對所有m個變量αi也就是α向量進行一次性優化。通過誤差每次迭代調整α向量中每個元素的值。而座標上升法(座標上升與座標下降可以看做是一對,座標上升是用來求解max最優化問題,座標下降用於求min最優化問題)的思想是每次迭代只調整一個變量αi的值,其他變量的值在這次迭代中固定不變。
最裏面語句的意思是固定除αi之外的所有αj(i不等於j),這時W可看作只是關於αi的函數,那麼直接對αi求導優化即可。這裏我們進行最大化求導的順序i是從1到m,可以通過更改優化順序來使W能夠更快地增加並收斂。如果W在內循環中能夠很快地達到最優,那麼座標上升法會是一個很高效的求極值方法。
用個二維的例子來說明下座標下降法:我們需要尋找f(x,y)=x2+xy+y2的最小值處的(x*, y*),也就是下圖的F*點的地方。
假設我們初始的點是A(圖是函數投影到xoy平面的等高線圖,顏色越深值越小),我們需要達到F*的地方。那最快的方法就是圖中黃色線的路徑,一次性就到達了,其實這個是牛頓優化法,但如果是高維的話,這個方法就不太高效了(因爲需要求解矩陣的逆,這個不在這裏討論)。我們也可以按照紅色所指示的路徑來走。從A開始,先固定x,沿着y軸往讓f(x, y)值減小的方向走到B點,然後固定y,沿着x軸往讓f(x, y)值減小的方向走到C點,不斷循環,直到到達F*。反正每次只要我們都往讓f(x, y)值小的地方走就行了,這樣腳踏實地,一步步走,每一步都使f(x, y)慢慢變小,總有一天,皇天不負有心人的。到達F*也是時間問題。到這裏你可能會說,這紅色線比黃色線貧富差距也太嚴重了吧。因爲這裏是二維的簡單的情況嘛。如果是高維的情況,而且目標函數很複雜的話,再加上樣本集很多,那麼在梯度下降中,目標函數對所有αi求梯度或者在牛頓法中對矩陣求逆,都是很耗時的。這時候,如果W只對單個αi優化很快的時候,座標下降法可能會更加高效。
8.2、SMO算法
SMO算法的思想和座標下降法的思想差不多。唯一不同的是,SMO是一次迭代優化兩個α而不是一個。爲什麼要優化兩個呢?
我們回到這個優化問題。我們可以看到這個優化問題存在着一個約束,也就是
假設我們首先固定除α1以外的所有參數,然後在α1上求極值。但需要注意的是,因爲如果固定α1以外的所有參數,由上面這個約束條件可以知道,α1將不再是變量(可以由其他值推出),因爲問題中規定了:
因此,我們需要一次選取兩個參數做優化,比如αi和αj,此時αi可以由αj和其他參數表示出來。這樣回代入W中,W就只是關於αj的函數了,這時候就可以只對αj進行優化了。在這裏就是對αj進行求導,令導數爲0就可以解出這個時候最優的αj了。然後也可以得到αi。這就是一次的迭代過程,一次迭代只調整兩個拉格朗日乘子αi和αj。SMO之所以高效就是因爲在固定其他參數後,對一個參數優化過程很高效(對一個參數的優化可以通過解析求解,而不是迭代。雖然對一個參數的一次最小優化不可能保證其結果就是所優化的拉格朗日乘子的最終結果,但會使目標函數向極小值邁進一步,這樣對所有的乘子做最小優化,直到所有滿足KKT條件時,目標函數達到最小)。
總結下來是:
重複下面過程直到收斂{
(1)選擇兩個拉格朗日乘子αi和αj;
(2)固定其他拉格朗日乘子αk(k不等於i和j),只對αi和αj優化w(α);
(3)根據優化後的αi和αj,更新截距b的值;
}
那訓練裏面這兩三步驟到底是怎麼實現的,需要考慮什麼呢?下面我們來具體分析下:
(1)選擇αi和αj:
我們現在是每次迭代都優化目標函數的兩個拉格朗日乘子αi和αj,然後其他的拉格朗日乘子保持固定。如果有N個訓練樣本,我們就有N個拉格朗日乘子需要優化,但每次我們只挑兩個進行優化,我們就有N(N-1)種選擇。那到底我們要選擇哪對αi和αj呢?選擇哪對纔好呢?想想我們的目標是什麼?我們希望把所有違法KKT條件的樣本都糾正回來,因爲如果所有樣本都滿足KKT條件的話,我們的優化就完成了。那就很直觀了,哪個害羣之馬最嚴重,我們得先對他進行思想教育,讓他儘早迴歸正途。OK,那我們選擇的第一個變量αi就選違法KKT條件最嚴重的那一個。那第二個變量αj怎麼選呢?
我們是希望快點找到最優的N個拉格朗日乘子,使得代價函數最大,換句話說,要最快的找到代價函數最大值的地方對應的N個拉格朗日乘子。這樣我們的訓練時間纔會短。就像你從廣州去北京,有飛機和綠皮車給你選,你選啥?(就算你不考慮速度,也得考慮下空姐的感受嘛,別辜負了她們渴望看到你的期盼,哈哈)。有點離題了,anyway,每次迭代中,哪對αi和αj可以讓我更快的達到代價函數值最大的地方,我們就選他們。或者說,走完這一步,選這對αi和αj代價函數值增加的值最多,比選擇其他所有αi和αj的結合中都多。這樣我們纔可以更快的接近代價函數的最大值,也就是達到優化的目標了。再例如,下圖,我們要從A點走到B點,按藍色的路線走c2方向的時候,一跨一大步,按紅色的路線走c1方向的時候,只能是人類的一小步。所以,藍色路線走兩步就邁進了成功之門,而紅色的路線,人生曲折,好像成功遙遙無期一樣,故曰,選擇比努力更重要!
真囉嗦!說了半天,其實就一句話:爲什麼每次迭代都要選擇最好的αi和αj,就是爲了更快的收斂!那實踐中每次迭代到底要怎樣選αi和αj呢?這有個很好聽的名字叫啓發式選擇,主要思想是先選擇最有可能需要優化(也就是違反KKT條件最嚴重)的αi,再針對這樣的αi選擇最有可能取得較大修正步長的αj。具體是以下兩個過程:
1)第一個變量αi的選擇:
SMO稱選擇第一個變量的過程爲外層循環。外層訓練在訓練樣本中選取違法KKT條件最嚴重的樣本點。並將其對應的變量作爲第一個變量。具體的,檢驗訓練樣本(xi, yi)是否滿足KKT條件,也就是:
該檢驗是在ε範圍內進行的。在檢驗過程中,外層循環首先遍歷所有滿足條件0<αj<C的樣本點,即在間隔邊界上的支持向量點,檢驗他們是否滿足KKT條件,然後選擇違反KKT條件最嚴重的αi。如果這些樣本點都滿足KKT條件,那麼遍歷整個訓練集,檢驗他們是否滿足KKT條件,然後選擇違反KKT條件最嚴重的αi。
優先選擇遍歷非邊界數據樣本,因爲非邊界數據樣本更有可能需要調整,邊界數據樣本常常不能得到進一步調整而留在邊界上。由於大部分數據樣本都很明顯不可能是支持向量,因此對應的α乘子一旦取得零值就無需再調整。遍歷非邊界數據樣本並選出他們當中違反KKT 條件爲止。當某一次遍歷發現沒有非邊界數據樣本得到調整時,遍歷所有數據樣本,以檢驗是否整個集合都滿足KKT條件。如果整個集合的檢驗中又有數據樣本被進一步進化,則有必要再遍歷非邊界數據樣本。這樣,不停地在遍歷所有數據樣本和遍歷非邊界數據樣本之間切換,直到整個樣本集合都滿足KKT條件爲止。以上用KKT條件對數據樣本所做的檢驗都以達到一定精度ε就可以停止爲條件。如果要求十分精確的輸出算法,則往往不能很快收斂。
對整個數據集的遍歷掃描相當容易,而實現對非邊界αi的掃描時,首先需要將所有非邊界樣本的αi值(也就是滿足0<αi<C)保存到新的一個列表中,然後再對其進行遍歷。同時,該步驟跳過那些已知的不會改變的αi值。
2)第二個變量αj的選擇:
在選擇第一個αi後,算法會通過一個內循環來選擇第二個αj值。因爲第二個乘子的迭代步長大致正比於|Ei-Ej|,所以我們需要選擇能夠最大化|Ei-Ej|的第二個乘子(選擇最大化迭代步長的第二個乘子)。在這裏,爲了節省計算時間,我們建立一個全局的緩存用於保存所有樣本的誤差值,而不用每次選擇的時候就重新計算。我們從中選擇使得步長最大或者|Ei-Ej|最大的αj。
(2)優化αi和αj:
選擇這兩個拉格朗日乘子後,我們需要先計算這些參數的約束值。然後再求解這個約束最大化問題。
首先,我們需要給αj找到邊界L<=αj<=H,以保證αj滿足0<=αj<=C的約束。這意味着αj必須落入這個盒子中。由於只有兩個變量(αi, αj),約束可以用二維空間中的圖形來表示,如下圖:
不等式約束使得(αi,αj)在盒子[0, C]x[0, C]內,等式約束使得(αi, αj)在平行於盒子[0, C]x[0, C]的對角線的直線上。因此要求的是目標函數在一條平行於對角線的線段上的最優值。這使得兩個變量的最優化問題成爲實質的單變量的最優化問題。由圖可以得到,αj的上下界可以通過下面的方法得到:
我們優化的時候,αj必須要滿足上面這個約束。也就是說上面是αj的可行域。然後我們開始尋找αj,使得目標函數最大化。通過推導得到αj的更新公式如下:
這裏Ek可以看做對第k個樣本,SVM的輸出與期待輸出,也就是樣本標籤的誤差。
而η實際上是度量兩個樣本i和j的相似性的。在計算η的時候,我們需要使用核函數,那麼就可以用核函數來取代上面的內積。
得到新的αj後,我們需要保證它處於邊界內。換句話說,如果這個優化後的值跑出了邊界L和H,我們就需要簡單的裁剪,將αj收回這個範圍:
最後,得到優化的αj後,我們需要用它來計算αi:
到這裏,αi和αj的優化就完成了。
(3)計算閾值b:
優化αi和αj後,我們就可以更新閾值b,使得對兩個樣本i和j都滿足KKT條件。如果優化後αi不在邊界上(也就是滿足0<αi<C,這時候根據KKT條件,可以得到yigi(xi)=1,這樣我們纔可以計算b),那下面的閾值b1是有效的,因爲當輸入xi時它迫使SVM輸出yi。
同樣,如果0<αj<C,那麼下面的b2也是有效的:
如果0<αi<C和0<αj<C都滿足,那麼b1和b2都有效,而且他們是相等的。如果他們兩個都處於邊界上(也就是αi=0或者αi=C,同時αj=0或者αj=C),那麼在b1和b2之間的閾值都滿足KKT條件,一般我們取他們的平均值b=(b1+b2)/2。所以,總的來說對b的更新如下:
每做完一次最小優化,必須更新每個數據樣本的誤差,以便用修正過的分類面對其他數據樣本再做檢驗,在選擇第二個配對優化數據樣本時用來估計步長。
(4)凸優化問題終止條件:
SMO算法的基本思路是:如果說有變量的解都滿足此最優化問題的KKT條件,那麼這個最優化問題的解就得到了。因爲KKT條件是該最優化問題的充分必要條件(證明請參考文獻)。所以我們可以監視原問題的KKT條件,所以所有的樣本都滿足KKT條件,那麼就表示迭代結束了。但是由於KKT條件本身是比較苛刻的,所以也需要設定一個容忍值,即所有樣本在容忍值範圍內滿足KKT條件則認爲訓練可以結束;當然了,對於對偶問題的凸優化還有其他終止條件,可以參考文獻。
8.3、SMO算法的Python實現
8.3.1、Python的準備工作
我使用的Python是2.7.5版本的。附加的庫有Numpy和Matplotlib。而Matplotlib又依賴dateutil和pyparsing兩個庫,所以我們需要安裝以上三個庫。前面兩個庫還好安裝,直接在官網下對應版本就行。但我找後兩個庫的時候,就沒那麼容易了。後來發現,其實對Python的庫的下載和安裝可以藉助pip工具的。這個是安裝和管理Python包的工具。感覺它有點像ubuntu的apt-get,需要安裝什麼庫,直接下載和安裝一條龍服務。
首先,我們需要到pip的官網:https://pypi.python.org/pypi/pip下載對應我們python版本的pip,例如我的是pip-1.4.1.tar.gz。但安裝pip需要另一個工具,也就是setuptools,我們到https://pypi.python.org/pypi/setuptools/#windows下載ez_setup.py這個文件回來。然後在CMD命令行中執行:(注意他們的路徑)
#python ez_setup.py
這時候,就會自動下載.egg等等文件然後安裝完成。
然後我們解壓pip-1.4.1.tar.gz。進入到該目錄中,執行:
#python setup.py install
這時候就會自動安裝pip到你python目錄下的Scripts文件夾中。我的是C:\Python27\Scripts。
在裏面我們可以看到pip.exe,然後我們進入到該文件夾中:
#cd C:\Python27\Scripts
#pip install dateutil
#pip install pyparsing
這樣就可以把這些額外的庫給下載回來了。非常高端大氣上檔次!
8.3.2、SMO算法的Python實現
在代碼中已經有了比較詳細的註釋了。不知道有沒有錯誤的地方,如果有,還望大家指正(每次的運行結果都有可能不同,另外,感覺有些結果似乎不太正確,但我還沒發現哪裏出錯了,如果大家找到有錯誤的地方,還望大家指點下,衷心感謝)。裏面我寫了個可視化結果的函數,但只能在二維的數據上面使用。直接貼代碼:
SVM.py
- #################################################
- # SVM: support vector machine
- # Author : zouxy
- # Date : 2013-12-12
- # HomePage : http://blog.csdn.net/zouxy09
- # Email : [email protected]
- #################################################
- from numpy import *
- import time
- import matplotlib.pyplot as plt
- # calulate kernel value
- def calcKernelValue(matrix_x, sample_x, kernelOption):
- kernelType = kernelOption[0]
- numSamples = matrix_x.shape[0]
- kernelValue = mat(zeros((numSamples, 1)))
- if kernelType == 'linear':
- kernelValue = matrix_x * sample_x.T
- elif kernelType == 'rbf':
- sigma = kernelOption[1]
- if sigma == 0:
- sigma = 1.0
- for i in xrange(numSamples):
- diff = matrix_x[i, :] - sample_x
- kernelValue[i] = exp(diff * diff.T / (-2.0 * sigma**2))
- else:
- raise NameError('Not support kernel type! You can use linear or rbf!')
- return kernelValue
- # calculate kernel matrix given train set and kernel type
- def calcKernelMatrix(train_x, kernelOption):
- numSamples = train_x.shape[0]
- kernelMatrix = mat(zeros((numSamples, numSamples)))
- for i in xrange(numSamples):
- kernelMatrix[:, i] = calcKernelValue(train_x, train_x[i, :], kernelOption)
- return kernelMatrix
- # define a struct just for storing variables and data
- class SVMStruct:
- def __init__(self, dataSet, labels, C, toler, kernelOption):
- self.train_x = dataSet # each row stands for a sample
- self.train_y = labels # corresponding label
- self.C = C # slack variable
- self.toler = toler # termination condition for iteration
- self.numSamples = dataSet.shape[0] # number of samples
- self.alphas = mat(zeros((self.numSamples, 1))) # Lagrange factors for all samples
- self.b = 0
- self.errorCache = mat(zeros((self.numSamples, 2)))
- self.kernelOpt = kernelOption
- self.kernelMat = calcKernelMatrix(self.train_x, self.kernelOpt)
- # calculate the error for alpha k
- def calcError(svm, alpha_k):
- output_k = float(multiply(svm.alphas, svm.train_y).T * svm.kernelMat[:, alpha_k] + svm.b)
- error_k = output_k - float(svm.train_y[alpha_k])
- return error_k
- # update the error cache for alpha k after optimize alpha k
- def updateError(svm, alpha_k):
- error = calcError(svm, alpha_k)
- svm.errorCache[alpha_k] = [1, error]
- # select alpha j which has the biggest step
- def selectAlpha_j(svm, alpha_i, error_i):
- svm.errorCache[alpha_i] = [1, error_i] # mark as valid(has been optimized)
- candidateAlphaList = nonzero(svm.errorCache[:, 0].A)[0] # mat.A return array
- maxStep = 0; alpha_j = 0; error_j = 0
- # find the alpha with max iterative step
- if len(candidateAlphaList) > 1:
- for alpha_k in candidateAlphaList:
- if alpha_k == alpha_i:
- continue
- error_k = calcError(svm, alpha_k)
- if abs(error_k - error_i) > maxStep:
- maxStep = abs(error_k - error_i)
- alpha_j = alpha_k
- error_j = error_k
- # if came in this loop first time, we select alpha j randomly
- else:
- alpha_j = alpha_i
- while alpha_j == alpha_i:
- alpha_j = int(random.uniform(0, svm.numSamples))
- error_j = calcError(svm, alpha_j)
- return alpha_j, error_j
- # the inner loop for optimizing alpha i and alpha j
- def innerLoop(svm, alpha_i):
- error_i = calcError(svm, alpha_i)
- ### check and pick up the alpha who violates the KKT condition
- ## satisfy KKT condition
- # 1) yi*f(i) >= 1 and alpha == 0 (outside the boundary)
- # 2) yi*f(i) == 1 and 0<alpha< C (on the boundary)
- # 3) yi*f(i) <= 1 and alpha == C (between the boundary)
- ## violate KKT condition
- # because y[i]*E_i = y[i]*f(i) - y[i]^2 = y[i]*f(i) - 1, so
- # 1) if y[i]*E_i < 0, so yi*f(i) < 1, if alpha < C, violate!(alpha = C will be correct)
- # 2) if y[i]*E_i > 0, so yi*f(i) > 1, if alpha > 0, violate!(alpha = 0 will be correct)
- # 3) if y[i]*E_i = 0, so yi*f(i) = 1, it is on the boundary, needless optimized
- if (svm.train_y[alpha_i] * error_i < -svm.toler) and (svm.alphas[alpha_i] < svm.C) or\
- (svm.train_y[alpha_i] * error_i > svm.toler) and (svm.alphas[alpha_i] > 0):
- # step 1: select alpha j
- alpha_j, error_j = selectAlpha_j(svm, alpha_i, error_i)
- alpha_i_old = svm.alphas[alpha_i].copy()
- alpha_j_old = svm.alphas[alpha_j].copy()
- # step 2: calculate the boundary L and H for alpha j
- if svm.train_y[alpha_i] != svm.train_y[alpha_j]:
- L = max(0, svm.alphas[alpha_j] - svm.alphas[alpha_i])
- H = min(svm.C, svm.C + svm.alphas[alpha_j] - svm.alphas[alpha_i])
- else:
- L = max(0, svm.alphas[alpha_j] + svm.alphas[alpha_i] - svm.C)
- H = min(svm.C, svm.alphas[alpha_j] + svm.alphas[alpha_i])
- if L == H:
- return 0
- # step 3: calculate eta (the similarity of sample i and j)
- eta = 2.0 * svm.kernelMat[alpha_i, alpha_j] - svm.kernelMat[alpha_i, alpha_i] \
- - svm.kernelMat[alpha_j, alpha_j]
- if eta >= 0:
- return 0
- # step 4: update alpha j
- svm.alphas[alpha_j] -= svm.train_y[alpha_j] * (error_i - error_j) / eta
- # step 5: clip alpha j
- if svm.alphas[alpha_j] > H:
- svm.alphas[alpha_j] = H
- if svm.alphas[alpha_j] < L:
- svm.alphas[alpha_j] = L
- # step 6: if alpha j not moving enough, just return
- if abs(alpha_j_old - svm.alphas[alpha_j]) < 0.00001:
- updateError(svm, alpha_j)
- return 0
- # step 7: update alpha i after optimizing aipha j
- svm.alphas[alpha_i] += svm.train_y[alpha_i] * svm.train_y[alpha_j] \
- * (alpha_j_old - svm.alphas[alpha_j])
- # step 8: update threshold b
- b1 = svm.b - error_i - svm.train_y[alpha_i] * (svm.alphas[alpha_i] - alpha_i_old) \
- * svm.kernelMat[alpha_i, alpha_i] \
- - svm.train_y[alpha_j] * (svm.alphas[alpha_j] - alpha_j_old) \
- * svm.kernelMat[alpha_i, alpha_j]
- b2 = svm.b - error_j - svm.train_y[alpha_i] * (svm.alphas[alpha_i] - alpha_i_old) \
- * svm.kernelMat[alpha_i, alpha_j] \
- - svm.train_y[alpha_j] * (svm.alphas[alpha_j] - alpha_j_old) \
- * svm.kernelMat[alpha_j, alpha_j]
- if (0 < svm.alphas[alpha_i]) and (svm.alphas[alpha_i] < svm.C):
- svm.b = b1
- elif (0 < svm.alphas[alpha_j]) and (svm.alphas[alpha_j] < svm.C):
- svm.b = b2
- else:
- svm.b = (b1 + b2) / 2.0
- # step 9: update error cache for alpha i, j after optimize alpha i, j and b
- updateError(svm, alpha_j)
- updateError(svm, alpha_i)
- return 1
- else:
- return 0
- # the main training procedure
- def trainSVM(train_x, train_y, C, toler, maxIter, kernelOption = ('rbf', 1.0)):
- # calculate training time
- startTime = time.time()
- # init data struct for svm
- svm = SVMStruct(mat(train_x), mat(train_y), C, toler, kernelOption)
- # start training
- entireSet = True
- alphaPairsChanged = 0
- iterCount = 0
- # Iteration termination condition:
- # Condition 1: reach max iteration
- # Condition 2: no alpha changed after going through all samples,
- # in other words, all alpha (samples) fit KKT condition
- while (iterCount < maxIter) and ((alphaPairsChanged > 0) or entireSet):
- alphaPairsChanged = 0
- # update alphas over all training examples
- if entireSet:
- for i in xrange(svm.numSamples):
- alphaPairsChanged += innerLoop(svm, i)
- print '---iter:%d entire set, alpha pairs changed:%d' % (iterCount, alphaPairsChanged)
- iterCount += 1
- # update alphas over examples where alpha is not 0 & not C (not on boundary)
- else:
- nonBoundAlphasList = nonzero((svm.alphas.A > 0) * (svm.alphas.A < svm.C))[0]
- for i in nonBoundAlphasList:
- alphaPairsChanged += innerLoop(svm, i)
- print '---iter:%d non boundary, alpha pairs changed:%d' % (iterCount, alphaPairsChanged)
- iterCount += 1
- # alternate loop over all examples and non-boundary examples
- if entireSet:
- entireSet = False
- elif alphaPairsChanged == 0:
- entireSet = True
- print 'Congratulations, training complete! Took %fs!' % (time.time() - startTime)
- return svm
- # testing your trained svm model given test set
- def testSVM(svm, test_x, test_y):
- test_x = mat(test_x)
- test_y = mat(test_y)
- numTestSamples = test_x.shape[0]
- supportVectorsIndex = nonzero(svm.alphas.A > 0)[0]
- supportVectors = svm.train_x[supportVectorsIndex]
- supportVectorLabels = svm.train_y[supportVectorsIndex]
- supportVectorAlphas = svm.alphas[supportVectorsIndex]
- matchCount = 0
- for i in xrange(numTestSamples):
- kernelValue = calcKernelValue(supportVectors, test_x[i, :], svm.kernelOpt)
- predict = kernelValue.T * multiply(supportVectorLabels, supportVectorAlphas) + svm.b
- if sign(predict) == sign(test_y[i]):
- matchCount += 1
- accuracy = float(matchCount) / numTestSamples
- return accuracy
- # show your trained svm model only available with 2-D data
- def showSVM(svm):
- if svm.train_x.shape[1] != 2:
- print "Sorry! I can not draw because the dimension of your data is not 2!"
- return 1
- # draw all samples
- for i in xrange(svm.numSamples):
- if svm.train_y[i] == -1:
- plt.plot(svm.train_x[i, 0], svm.train_x[i, 1], 'or')
- elif svm.train_y[i] == 1:
- plt.plot(svm.train_x[i, 0], svm.train_x[i, 1], 'ob')
- # mark support vectors
- supportVectorsIndex = nonzero(svm.alphas.A > 0)[0]
- for i in supportVectorsIndex:
- plt.plot(svm.train_x[i, 0], svm.train_x[i, 1], 'oy')
- # draw the classify line
- w = zeros((2, 1))
- for i in supportVectorsIndex:
- w += multiply(svm.alphas[i] * svm.train_y[i], svm.train_x[i, :].T)
- min_x = min(svm.train_x[:, 0])[0, 0]
- max_x = max(svm.train_x[:, 0])[0, 0]
- y_min_x = float(-svm.b - w[0] * min_x) / w[1]
- y_max_x = float(-svm.b - w[0] * max_x) / w[1]
- plt.plot([min_x, max_x], [y_min_x, y_max_x], '-g')
- plt.show()
測試的數據來自這裏。有100個樣本,每個樣本兩維,最後是對應的標籤,例如:
3.542485 1.977398 -1
3.018896 2.556416 -1
7.551510 -1.580030 1
2.114999 -0.004466 -1
……
測試代碼中首先加載這個數據庫,然後用前面80個樣本來訓練,再用剩下的20個樣本的測試,並顯示訓練後的模型和分類結果。測試代碼如下:
test_SVM.py
- #################################################
- # SVM: support vector machine
- # Author : zouxy
- # Date : 2013-12-12
- # HomePage : http://blog.csdn.net/zouxy09
- # Email : [email protected]
- #################################################
- from numpy import *
- import SVM
- ################## test svm #####################
- ## step 1: load data
- print "step 1: load data..."
- dataSet = []
- labels = []
- fileIn = open('E:/Python/Machine Learning in Action/testSet.txt')
- for line in fileIn.readlines():
- lineArr = line.strip().split('\t')
- dataSet.append([float(lineArr[0]), float(lineArr[1])])
- labels.append(float(lineArr[2]))
- dataSet = mat(dataSet)
- labels = mat(labels).T
- train_x = dataSet[0:81, :]
- train_y = labels[0:81, :]
- test_x = dataSet[80:101, :]
- test_y = labels[80:101, :]
- ## step 2: training...
- print "step 2: training..."
- C = 0.6
- toler = 0.001
- maxIter = 50
- svmClassifier = SVM.trainSVM(train_x, train_y, C, toler, maxIter, kernelOption = ('linear', 0))
- ## step 3: testing
- print "step 3: testing..."
- accuracy = SVM.testSVM(svmClassifier, test_x, test_y)
- ## step 4: show the result
- print "step 4: show the result..."
- print 'The classify accuracy is: %.3f%%' % (accuracy * 100)
- SVM.showSVM(svmClassifier)
運行結果如下:
- step 1: load data...
- step 2: training...
- ---iter:0 entire set, alpha pairs changed:8
- ---iter:1 non boundary, alpha pairs changed:7
- ---iter:2 non boundary, alpha pairs changed:1
- ---iter:3 non boundary, alpha pairs changed:0
- ---iter:4 entire set, alpha pairs changed:0
- Congratulations, training complete! Took 0.058000s!
- step 3: testing...
- step 4: show the result...
- The classify accuracy is: 100.000%
訓練好的模型圖:
九、參考文獻與推薦閱讀
[1] JerryLead的博客,作者根據斯坦福的講義給出了流暢和通俗的推導:SVM系列。
[2]嘉士伯的SVM入門系列,講得很好。
[3] pluskid的支持向量機系列,非常好。其中關於dual問題推導非常贊。
[4] Leo Zhang的SVM學習系列,博客中還包含了很多其他的機器學習算法。
[5] v_july_v的支持向量機通俗導論(理解SVM的三層境界)。結構之法算法之道blog。
[6] 李航的《統計學習方法》,清華大學出版社
[7] SVM學習——Sequential Minimal Optimization
[8] SVM算法實現(一)
[9] Sequential Minimal Optimization: A FastAlgorithm for Training Support Vector Machines
[10] SVM --從“原理”到實現
[11] 支持向量機入門系列