1. 程式人生 > 其它 >支援向量機(SVM)入門詳解(續)與python實現

支援向量機(SVM)入門詳解(續)與python實現

接前文 支援向量機SVM入門詳解:那些你需要消化的知識

讓我再一次比較完整的重複一下我們要解決的問題:我們有屬於兩個類別的樣本點(並不限定這些點在二維空間中)若干,如圖,

圓形的樣本點定為正樣本(連帶著,我們可以把正樣本所屬的類叫做正類),方形的點定為負例。我們想求得這樣一個線性函式(在n維空間中的線性函式):

g(x)=wx+b

使得所有屬於正類的點x+代入以後有g(x+)≥1,而所有屬於負類的點x-代入後有g(x-)≤-1(之所以總跟1比較,無論正一還是負一,都是因為我們固定了間隔為1,注意間隔和幾何間隔的區別)。代入g(x)後的值如果在1和-1之間,我們就拒絕判斷。

求這樣的g(x)的過程就是求w(一個n維向量)和b(一個實數)兩個引數的過程(但實際上只需要求w,求得以後找某些樣本點代入就可以求得b)。因此在求g(x)的時候,w才是變數。

你肯定能看出來,一旦求出了w(也就求出了b),那麼中間的直線H就知道了(因為它就是wx+b=0嘛,哈哈),那麼H1和H2也就知道了(因為三者是平行的,而且相隔的距離還是||w||決定的)。那麼w是誰決定的?顯然是你給的樣本決定的,一旦你在空間中給出了那些個樣本點,三條直線的位置實際上就唯一確定了(因為我們求的是最優的那三條,當然是唯一的),我們解優化問題的過程也只不過是把這個確定了的東西算出來而已。

樣本確定了w,用數學的語言描述,就是w可以表示為樣本的某種組合:

w=α1x1+α2x2+…+αnxn

式子中的αi是一個一個的數(在嚴格的證明過程中,這些α被稱為拉格朗日乘子),而xi是樣本點,因而是向量,n就是總樣本點的個數。為了方便描述,以下開始嚴格區別數字與向量的乘積和向量間的乘積,我會用α1x1表示數字和向量的乘積,而用<x1,x2>表示向量x1,x2的內積(也叫點積,注意與向量叉積的區別)。因此g(x)的表示式嚴格的形式應該是:

g(x)=<w,x>+b

但是上面的式子還不夠好,你回頭看看圖中正樣本和負樣本的位置,想像一下,我不動所有點的位置,而只是把其中一個正樣本點定為負樣本點(也就是把一個點的形狀從圓形變為方形),結果怎麼樣?三條直線都必須移動(因為對這三條直線的要求是必須把方形和圓形的點正確分開)!這說明w不僅跟樣本點的位置有關,還跟樣本的類別有關(也就是和樣本的“標籤”有關)。因此用下面這個式子表示才算完整:

w=α1y1x1+α2y2x2+…+αnynxn (式1)

其中的yi就是第i個樣本的標籤,它等於1或者-1。其實以上式子的那一堆拉格朗日乘子中,只有很少的一部分不等於0(不等於0才對w起決定作用),這部分不等於0的拉格朗日乘子後面所乘的樣本點,其實都落在H1和H2上,也正是這部分樣本(而不需要全部樣本)唯一的確定了分類函式,當然,更嚴格的說,這些樣本的一部分就可以確定,因為例如確定一條直線,只需要兩個點就可以,即便有三五個都落在上面,我們也不是全都需要。這部分我們真正需要的樣本點,就叫做支援(撐)向量!(名字還挺形象吧,他們“撐”起了分界線)

式子也可以用求和符號簡寫一下:

注意式子中x才是變數,也就是你要分類哪篇文件,就把該文件的向量表示代入到 x的位置,而所有的xi統統都是已知的樣本。還注意到式子中只有xi和x是向量,因此一部分可以從內積符號中拿出來,得到g(x)的式子為:

發現了什麼?w不見啦!從求w變成了求α。

但肯定有人會說,這並沒有把原問題簡化呀。嘿嘿,其實簡化了,只不過在你看不見的地方,以這樣的形式描述問題以後,我們的優化問題少了很大一部分不等式約束(記得這是我們解不了極值問題的萬惡之源)。但是接下來先跳過線性分類器求解的部分,來看看 SVM線上性分類器上所做的重大改進——核函式。

之前一直在討論的線性分類器,器如其名(汗,這是什麼說法啊),只能對線性可分的樣本做處理。如果提供的樣本線性不可分,結果很簡單,線性分類器的求解程式會無限迴圈,永遠也解不出來。這必然使得它的適用範圍大大縮小,而它的很多優點我們實在不原意放棄,怎麼辦呢?是否有某種方法,讓線性不可分的資料變得線性可分呢?

有!其思想說來也簡單,來用一個二維平面中的分類問題作例子,你一看就會明白。事先宣告,下面這個例子是網路早就有的,我一時找不到原作者的正確資訊,在此借用,並加進了我自己的解說而已。

例子是下面這張圖:

我們把橫軸上端點a和b之間紅色部分裡的所有點定為正類,兩邊的黑色部分裡的點定為負類。試問能找到一個線性函式把兩類正確分開麼?不能,因為二維空間裡的線性函式就是指直線,顯然找不到符合條件的直線。

但我們可以找到一條曲線,例如下面這一條:

顯然通過點在這條曲線的上方還是下方就可以判斷點所屬的類別(你在橫軸上隨便找一點,算算這一點的函式值,會發現負類的點函式值一定比0大,而正類的一定比0小)。這條曲線就是我們熟知的二次曲線,它的函式表示式可以寫為:

這樣g(x)就可以轉化為f(y)=<a,y>,你可以把y和a分別迴帶一下,看看等不等於原來的g(x)。用內積的形式寫你可能看不太清楚,實際上f(y)的形式就是:

g(x)=f(y)=ay

在任意維度的空間中,這種形式的函式都是一個線性函式(只不過其中的a和y都是多維向量罷了),因為自變數y的次數不大於1。

看出妙在哪了麼?原來在二維空間中一個線性不可分的問題,對映到四維空間後,變成了線性可分的!因此這也形成了我們最初想解決線性不可分問題的基本思路——向高維空間轉化,使其變得線性可分。

而轉化最關鍵的部分就在於找到x到y的對映方法。遺憾的是,如何找到這個對映,沒有系統性的方法(也就是說,純靠猜和湊)。具體到我們的文字分類問題,文字被表示為上千維的向量,即使維數已經如此之高,也常常是線性不可分的,還要向更高的空間轉化。其中的難度可想而知。

小Tips:為什麼說f(y)=ay是四維空間裡的函式? 大家可能一時沒看明白。回想一下我們二維空間裡的函式定義 g(x)=ax+b 變數x是一維的,為什麼說它是二維空間裡的函式呢?因為還有一個變數我們沒寫出來,它的完整形式其實是 y=g(x)=ax+b 即 y=ax+b 看看,有幾個變數?兩個。那是幾維空間的函式?(作者五歲的弟弟答:五維的。作者:……) 再看看 f(y)=ay 裡面的y是三維的變數,那f(y)是幾維空間裡的函式?(作者五歲的弟弟答:還是五維的。作者:……)

用一個具體文字分類的例子來看看這種向高維空間對映從而分類的方法如何運作,想象一下,我們文字分類問題的原始空間是1000維的(即每個要被分類的文件被表示為一個1000維的向量),在這個維度上問題是線性不可分的。現在我們有一個2000維空間裡的線性函式

f(x’)=<w’,x’>+b

注意向量的右上角有個 ’哦。它能夠將原問題變得可分。式中的 w’和x’都是2000維的向量,只不過w’是定值,而x’是變數(好吧,嚴格說來這個函式是2001維的,哈哈),現在我們的輸入呢,是一個1000維的向量x,分類的過程是先把x變換為2000維的向量x’,然後求這個變換後的向量x’與向量w’的內積,再把這個內積的值和b相加,就得到了結果,看結果大於閾值還是小於閾值就得到了分類結果。

你發現了什麼?我們其實只關心那個高維空間裡內積的值,那個值算出來了,分類結果就算出來了。而從理論上說, x’是經由x變換來的,因此廣義上可以把它叫做x的函式(有一個x,就確定了一個x’,對吧,確定不出第二個),而w’是常量,它是一個低維空間裡的常量w經過變換得到的,所以給了一個w 和x的值,就有一個確定的f(x’)值與其對應。這讓我們幻想,是否能有這樣一種函式K(w,x),他接受低維空間的輸入值,卻能算出高維空間的內積值<w’,x’>?

如果有這樣的函式,那麼當給了一個低維空間的輸入x以後,

g(x)=K(w,x)+b

f(x’)=<w’,x’>+b

這兩個函式的計算結果就完全一樣,我們也就用不著費力找那個對映關係,直接拿低維的輸入往g(x)裡面代就可以了(再次提醒,這回的g(x)就不是線性函式啦,因為你不能保證K(w,x)這個表示式裡的x次數不高於1哦)。

萬幸的是,這樣的K(w,x)確實存在(發現凡是我們人類能解決的問題,大都是巧得不能再巧,特殊得不能再特殊的問題,總是恰好有些能投機取巧的地方才能解決,由此感到人類的渺小),它被稱作核函式(核,kernel),而且還不止一個,事實上,只要是滿足了Mercer條件的函式,都可以作為核函式。核函式的基本作用就是接受兩個低維空間裡的向量,能夠計算出經過某個變換後在高維空間裡的向量內積值。幾個比較常用的核函式,俄,教課書裡都列過,我就不敲了(懶!)。

回想我們上節說的求一個線性分類器,它的形式應該是:

現在這個就是高維空間裡的線性函式(為了區別低維和高維空間裡的函式和向量,我改了函式的名字,並且給w和x都加上了 ’),我們就可以用一個低維空間裡的函式(再一次的,這個低維空間裡的函式就不再是線性的啦)來代替,

又發現什麼了?f(x’) 和g(x)裡的α,y,b全都是一樣一樣的!這就是說,儘管給的問題是線性不可分的,但是我們就硬當它是線性問題來求解,只不過求解過程中,凡是要求內積的時候就用你選定的核函式來算。這樣求出來的α再和你選定的核函式一組合,就得到分類器啦!

明白了以上這些,會自然的問接下來兩個問題:

1. 既然有很多的核函式,針對具體問題該怎麼選擇?

2. 如果使用核函式向高維空間對映後,問題仍然是線性不可分的,那怎麼辦?

第一個問題現在就可以回答你:對核函式的選擇,現在還缺乏指導原則!各種實驗的觀察結果(不光是文字分類)的確表明,某些問題用某些核函式效果很好,用另一些就很差,但是一般來講,徑向基核函式是不會出太大偏差的一種,首選。(我做文字分類系統的時候,使用徑向基核函式,沒有引數調優的情況下,絕大部分類別的準確和召回都在85%以上,可見。雖然libSVM的作者林智仁認為文字分類用線性核函式效果更佳,待考證)

對第二個問題的解決則引出了我們下一節的主題:鬆弛變數。

現在我們已經把一個本來線性不可分的文字分類問題,通過對映到高維空間而變成了線性可分的。就像下圖這樣:

圓形和方形的點各有成千上萬個(畢竟,這就是我們訓練集中文件的數量嘛,當然很大了)。現在想象我們有另一個訓練集,只比原先這個訓練集多了一篇文章,對映到高維空間以後(當然,也使用了相同的核函式),也就多了一個樣本點,但是這個樣本的位置是這樣的:

就是圖中黃色那個點,它是方形的,因而它是負類的一個樣本,這單獨的一個樣本,使得原本線性可分的問題變成了線性不可分的。這樣類似的問題(僅有少數點線性不可分)叫做“近似線性可分”的問題。

以我們人類的常識來判斷,說有一萬個點都符合某種規律(因而線性可分),有一個點不符合,那這一個點是否就代表了分類規則中我們沒有考慮到的方面呢(因而規則應該為它而做出修改)?

其實我們會覺得,更有可能的是,這個樣本點壓根就是錯誤,是噪聲,是提供訓練集的同學人工分類時一打瞌睡錯放進去的。所以我們會簡單的忽略這個樣本點,仍然使用原來的分類器,其效果絲毫不受影響。

但這種對噪聲的容錯性是人的思維帶來的,我們的程式可沒有。由於我們原本的優化問題的表示式中,確實要考慮所有的樣本點(不能忽略某一個,因為程式它怎麼知道該忽略哪一個呢?),在此基礎上尋找正負類之間的最大幾何間隔,而幾何間隔本身代表的是距離,是非負的,像上面這種有噪聲的情況會使得整個問題無解。這種解法其實也叫做“硬間隔”分類法,因為他硬性的要求所有樣本點都滿足和分類平面間的距離必須大於某個值。

因此由上面的例子中也可以看出,硬間隔的分類法其結果容易受少數點的控制,這是很危險的(儘管有句話說真理總是掌握在少數人手中,但那不過是那一小撮人聊以自慰的詞句罷了,咱還是得民主)。

但解決方法也很明顯,就是仿照人的思路,允許一些點到分類平面的距離不滿足原先的要求。由於不同的訓練集各點的間距尺度不太一樣,因此用間隔(而不是幾何間隔)來衡量有利於我們表達形式的簡潔。我們原先對樣本點的要求是:

因為鬆弛變數是非負的,因此最終的結果是要求間隔可以比1小。但是當某些點出現這種間隔比1小的情況時(這些點也叫離群點),意味著我們放棄了對這些點的精確分類,而這對我們的分類器來說是種損失。但是放棄這些點也帶來了好處,那就是使分類面不必向這些點的方向移動,因而可以得到更大的幾何間隔(在低維空間看來,分類邊界也更平滑)。顯然我們必須權衡這種損失和好處。好處很明顯,我們得到的分類間隔越大,好處就越多。回顧我們原始的硬間隔分類對應的優化問題:

||w||2就是我們的目標函式(當然係數可有可無),希望它越小越好,因而損失就必然是一個能使之變大的量(能使它變小就不叫損失了,我們本來就希望目標函式值越小越好)。那如何來衡量損失,有兩種常用的方式,有人喜歡用

其中l都是樣本的數目。兩種方法沒有大的區別。如果選擇了第一種,得到的方法的就叫做二階軟間隔分類器,第二種就叫做一階軟間隔分類器。把損失加入到目標函式裡的時候,就需要一個懲罰因子(cost,也就是libSVM的諸多引數中的C),原來的優化問題就變成了下面這樣:

這個式子有這麼幾點要注意:

一是並非所有的樣本點都有一個鬆弛變數與其對應。實際上只有“離群點”才有,或者也可以這麼看,所有沒離群的點鬆弛變數都等於0(對負類來說,離群點就是在前面圖中,跑到H2右側的那些負樣本點,對正類來說,就是跑到H1左側的那些正樣本點)。

二是鬆弛變數的值實際上標示出了對應的點到底離群有多遠,值越大,點就越遠。

三是懲罰因子C決定了你有多重視離群點帶來的損失,顯然當所有離群點的鬆弛變數的和一定時,你定的C越大,對目標函式的損失也越大,此時就暗示著你非常不願意放棄這些離群點,最極端的情況是你把C定為無限大,這樣只要稍有一個點離群,目標函式的值馬上變成無限大,馬上讓問題變成無解,這就退化成了硬間隔問題。

四是懲罰因子C不是一個變數,整個優化問題在解的時候,C是一個你必須事先指定的值,指定這個值以後,解一下,得到一個分類器,然後用測試資料看看結果怎麼樣,如果不夠好,換一個C的值,再解一次優化問題,得到另一個分類器,再看看效果,如此就是一個引數尋優的過程,但這和優化問題本身決不是一回事,優化問題在解的過程中,C一直是定值,要記住。

五是儘管加了鬆弛變數這麼一說,但這個優化問題仍然是一個優化問題(汗,這不廢話麼),解它的過程比起原始的硬間隔問題來說,沒有任何更加特殊的地方。

從大的方面說優化問題解的過程,就是先試著確定一下w,也就是確定了前面圖中的三條直線,這時看看間隔有多大,又有多少點離群,把目標函式的值算一算,再換一組三條直線(你可以看到,分類的直線位置如果移動了,有些原來離群的點會變得不再離群,而有的本來不離群的點會變成離群點),再把目標函式的值算一算,如此往復(迭代),直到最終找到目標函式最小時的w。

囉嗦了這麼多,讀者一定可以馬上自己總結出來,鬆弛變數也就是個解決線性不可分問題的方法罷了,但是回想一下,核函式的引入不也是為了解決線性不可分的問題麼?為什麼要為了一個問題使用兩種方法呢?

其實兩者還有微妙的不同。一般的過程應該是這樣,還以文字分類為例。在原始的低維空間中,樣本相當的不可分,無論你怎麼找分類平面,總會有大量的離群點,此時用核函式向高維空間對映一下,雖然結果仍然是不可分的,但比原始空間裡的要更加接近線性可分的狀態(就是達到了近似線性可分的狀態),此時再用鬆弛變數處理那些少數“冥頑不化”的離群點,就簡單有效得多啦。

本節中的(式1)也確實是支援向量機最最常用的形式。至此一個比較完整的支援向量機框架就有了,簡單說來,支援向量機就是使用了核函式的軟間隔線性分類法。

接下來要說的東西其實不是鬆弛變數本身,但由於是為了使用鬆弛變數才引入的,因此放在這裡也算合適,那就是懲罰因子C。回頭看一眼引入了鬆弛變數以後的優化問題:

注意其中C的位置,也可以回想一下C所起的作用(表徵你有多麼重視離群點,C越大越重視,越不想丟掉它們)。這個式子是以前做SVM的人寫的,大家也就這麼用,但沒有任何規定說必須對所有的鬆弛變數都使用同一個懲罰因子,我們完全可以給每一個離群點都使用不同的C,這時就意味著你對每個樣本的重視程度都不一樣,有些樣本丟了也就丟了,錯了也就錯了,這些就給一個比較小的C;而有些樣本很重要,決不能分類錯誤(比如中央下達的檔案啥的,笑),就給一個很大的C。

當然實際使用的時候並沒有這麼極端,但一種很常用的變形可以用來解決分類問題中樣本的“偏斜”問題。

先來說說樣本的偏斜問題,也叫資料集偏斜(unbalanced),它指的是參與分類的兩個類別(也可以指多個類別)樣本數量差異很大。比如說正類有10,000個樣本,而負類只給了100個,這會引起的問題顯而易見,可以看看下面的圖:

方形的點是負類。H,H1,H2是根據給的樣本算出來的分類面,由於負類的樣本很少很少,所以有一些本來是負類的樣本點沒有提供,比如圖中兩個灰色的方形點,如果這兩個點有提供的話,那算出來的分類面應該是H’,H2’和H1,他們顯然和之前的結果有出入,實際上負類給的樣本點越多,就越容易出現在灰色點附近的點,我們算出的結果也就越接近於真實的分類面。但現在由於偏斜的現象存在,使得數量多的正類可以把分類面向負類的方向“推”,因而影響了結果的準確性。

對付資料集偏斜問題的方法之一就是在懲罰因子上作文章,想必大家也猜到了,那就是給樣本數量少的負類更大的懲罰因子,表示我們重視這部分樣本(本來數量就少,再拋棄一些,那人家負類還活不活了),因此我們的目標函式中因鬆弛變數而損失的部分就變成了:

其中i=1…p都是正樣本,j=p+1…p+q都是負樣本。libSVM這個演算法包在解決偏斜問題的時候用的就是這種方法。

那C+和C-怎麼確定呢?它們的大小是試出來的(引數調優),但是他們的比例可以有些方法來確定。咱們先假定說C+是5這麼大,那確定C-的一個很直觀的方法就是使用兩類樣本數的比來算,對應到剛才舉的例子,C-就可以定為500這麼大(因為10,000:100=100:1嘛)。

但是這樣並不夠好,回看剛才的圖,你會發現正類之所以可以“欺負”負類,其實並不是因為負類樣本少,真實的原因是負類的樣本分佈的不夠廣(沒擴充到負類本應該有的區域)。說一個具體點的例子,現在想給政治類和體育類的文章做分類,政治類文章很多,而體育類只提供了幾篇關於籃球的文章,這時分類會明顯偏向於政治類,如果要給體育類文章增加樣本,但增加的樣本仍然全都是關於籃球的(也就是說,沒有足球,排球,賽車,游泳等等),那結果會怎樣呢?雖然體育類文章在數量上可以達到與政治類一樣多,但過於集中了,結果仍會偏向於政治類!所以給C+和C-確定比例更好的方法應該是衡量他們分佈的程度。比如可以算算他們在空間中佔據了多大的體積,例如給負類找一個超球——就是高維空間裡的球啦——它可以包含所有負類的樣本,再給正類找一個,比比兩個球的半徑,就可以大致確定分佈的情況。顯然半徑大的分佈就比較廣,就給小一點的懲罰因子。

但是這樣還不夠好,因為有的類別樣本確實很集中,這不是提供的樣本數量多少的問題,這是類別本身的特徵(就是某些話題涉及的面很窄,例如計算機類的文章就明顯不如文化類的文章那麼“天馬行空”),這個時候即便超球的半徑差異很大,也不應該賦予兩個類別不同的懲罰因子。

看到這裡讀者一定瘋了,因為說來說去,這豈不成了一個解決不了的問題?然而事實如此,完全的方法是沒有的,根據需要,選擇實現簡單又合用的就好(例如libSVM就直接使用樣本數量的比)。

從 SVM的那幾張圖可以看出來,SVM是一種典型的兩類分類器,即它只回答屬於正類還是負類的問題。而現實中要解決的問題,往往是多類的問題(少部分例外,例如垃圾郵件過濾,就只需要確定“是”還是“不是”垃圾郵件),比如文字分類,比如數字識別。如何由兩類分類器得到多類分類器,就是一個值得研究的問題。

還以文字分類為例,現成的方法有很多,其中一種一勞永逸的方法,就是真的一次性考慮所有樣本,並求解一個多目標函式的優化問題,一次性得到多個分類面,就像下圖這樣:

多個超平面把空間劃分為多個區域,每個區域對應一個類別,給一篇文章,看它落在哪個區域就知道了它的分類。

看起來很美對不對?只可惜這種演算法還基本停留在紙面上,因為一次性求解的方法計算量實在太大,大到無法實用的地步。

稍稍退一步,我們就會想到所謂“一類對其餘”的方法,就是每次仍然解一個兩類分類的問題。比如我們有5個類別,第一次就把類別1的樣本定為正樣本,其餘2,3,4,5的樣本合起來定為負樣本,這樣得到一個兩類分類器,它能夠指出一篇文章是還是不是第1類的;第二次我們把類別2 的樣本定為正樣本,把1,3,4,5的樣本合起來定為負樣本,得到一個分類器,如此下去,我們可以得到5個這樣的兩類分類器(總是和類別的數目一致)。到了有文章需要分類的時候,我們就拿著這篇文章挨個分類器的問:是屬於你的麼?是屬於你的麼?哪個分類器點頭說是了,文章的類別就確定了。這種方法的好處是每個優化問題的規模比較小,而且分類的時候速度很快(只需要呼叫5個分類器就知道了結果)。但有時也會出現兩種很尷尬的情況,例如拿一篇文章問了一圈,每一個分類器都說它是屬於它那一類的,或者每一個分類器都說它不是它那一類的,前者叫分類重疊現象,後者叫不可分類現象。分類重疊倒還好辦,隨便選一個結果都不至於太離譜,或者看看這篇文章到各個超平面的距離,哪個遠就判給哪個。不可分類現象就著實難辦了,只能把它分給第6個類別了……更要命的是,本來各個類別的樣本數目是差不多的,但“其餘”的那一類樣本數總是要數倍於正類(因為它是除正類以外其他類別的樣本之和嘛),這就人為的造成了上一節所說的“資料集偏斜”問題。

因此我們還得再退一步,還是解兩類分類問題,還是每次選一個類的樣本作正類樣本,而負類樣本則變成只選一個類(稱為“一對一單挑”的方法,哦,不對,沒有單挑,就是“一對一”的方法,呵呵),這就避免了偏斜。因此過程就是算出這樣一些分類器,第一個只回答“是第1類還是第2類”,第二個只回答“是第1類還是第3類”,第三個只回答“是第1類還是第4類”,如此下去,你也可以馬上得出,這樣的分類器應該有5 X 4/2=10個(通式是,如果有k個類別,則總的兩類分類器數目為k(k-1)/2)。雖然分類器的數目多了,但是在訓練階段(也就是算出這些分類器的分類平面時)所用的總時間卻比“一類對其餘”方法少很多,在真正用來分類的時候,把一篇文章扔給所有分類器,第一個分類器會投票說它是“1”或者“2”,第二個會說它是“1”或者“3”,讓每一個都投上自己的一票,最後統計票數,如果類別“1”得票最多,就判這篇文章屬於第1類。這種方法顯然也會有分類重疊的現象,但不會有不可分類現象,因為總不可能所有類別的票數都是0。看起來夠好麼?其實不然,想想分類一篇文章,我們呼叫了多少個分類器?10個,這還是類別數為5的時候,類別數如果是1000,要呼叫的分類器數目會上升至約500,000個(類別數的平方量級)。這如何是好?

看來我們必須再退一步,在分類的時候下功夫,我們還是像一對一方法那樣來訓練,只是在對一篇文章進行分類之前,我們先按照下面圖的樣子來組織分類器(如你所見,這是一個有向無環圖,因此這種方法也叫做DAG SVM)

這樣在分類時,我們就可以先問分類器“1對5”(意思是它能夠回答“是第1類還是第5類”),如果它回答5,我們就往左走,再問“2對5”這個分類器,如果它還說是“5”,我們就繼續往左走,這樣一直問下去,就可以得到分類結果。好處在哪?我們其實只調用了4個分類器(如果類別數是k,則只調用k-1個),分類速度飛快,且沒有分類重疊和不可分類現象!缺點在哪?假如最一開始的分類器回答錯誤(明明是類別1的文章,它說成了5),那麼後面的分類器是無論如何也無法糾正它的錯誤的(因為後面的分類器壓根沒有出現“1”這個類別標籤),其實對下面每一層的分類器都存在這種錯誤向下累積的現象。。

不過不要被DAG方法的錯誤累積嚇倒,錯誤累積在一對其餘和一對一方法中也都存在,DAG方法好於它們的地方就在於,累積的上限,不管是大是小,總是有定論的,有理論證明。而一對其餘和一對一方法中,儘管每一個兩類分類器的泛化誤差限是知道的,但是合起來做多類分類的時候,誤差上界是多少,沒人知道,這意味著準確率低到0也是有可能的,這多讓人鬱悶。

而且現在DAG方法根節點的選取(也就是如何選第一個參與分類的分類器),也有一些方法可以改善整體效果,我們總希望根節點少犯錯誤為好,因此參與第一次分類的兩個類別,最好是差別特別特別大,大到以至於不太可能把他們分錯;或者我們就總取在兩類分類中正確率最高的那個分類器作根節點,或者我們讓兩類分類器在分類的時候,不光輸出類別的標籤,還輸出一個類似“置信度”的東東,當它對自己的結果不太自信的時候,我們就不光按照它的輸出走,把它旁邊的那條路也走一走,等等。

大Tips:SVM的計算複雜度

使用SVM進行分類的時候,實際上是訓練和分類兩個完全不同的過程,因而討論複雜度就不能一概而論,我們這裡所說的主要是訓練階段的複雜度,即解那個二次規劃問題的複雜度。對這個問題的解,基本上要劃分為兩大塊,解析解和數值解。

解析解就是理論上的解,它的形式是表示式,因此它是精確的,一個問題只要有解(無解的問題還跟著摻和什麼呀,哈哈),那它的解析解是一定存在的。當然存在是一回事,能夠解出來,或者可以在可以承受的時間範圍內解出來,就是另一回事了。對SVM來說,求得解析解的時間複雜度最壞可以達到O(Nsv3),其中Nsv是支援向量的個數,而雖然沒有固定的比例,但支援向量的個數多少也和訓練集的大小有關。

數值解就是可以使用的解,是一個一個的數,往往都是近似解。求數值解的過程非常像窮舉法,從一個數開始,試一試它當解效果怎樣,不滿足一定條件(叫做停機條件,就是滿足這個以後就認為解足夠精確了,不需要繼續算下去了)就試下一個,當然下一個數不是亂選的,也有一定章法可循。有的演算法,每次只嘗試一個數,有的就嘗試多個,而且找下一個數字(或下一組數)的方法也各不相同,停機條件也各不相同,最終得到的解精度也各不相同,可見對求數值解的複雜度的討論不能脫開具體的演算法。

一個具體的演算法,Bunch-Kaufman訓練演算法,典型的時間複雜度在O(Nsv3+LNsv2+dLNsv)和O(dL2)之間,其中Nsv是支援向量的個數,L是訓練集樣本的個數,d是每個樣本的維數(原始的維數,沒有經過向高維空間對映之前的維數)。複雜度會有變化,是因為它不光跟輸入問題的規模有關(不光和樣本的數量,維數有關),也和問題最終的解有關(即支援向量有關),如果支援向量比較少,過程會快很多,如果支援向量很多,接近於樣本的數量,就會產生O(dL2)這個十分糟糕的結果(給10,000個樣本,每個樣本1000維,基本就不用算了,算不出來,呵呵,而這種輸入規模對文字分類來說太正常了)。

這樣再回頭看就會明白為什麼一對一方法儘管要訓練的兩類分類器數量多,但總時間實際上比一對其餘方法要少了,因為一對其餘方法每次訓練都考慮了所有樣本(只是每次把不同的部分劃分為正類或者負類而已),自然慢上很多。

SMO演算法的Python實現

在程式碼中已經有了比較詳細的註釋了。不知道有沒有錯誤的地方,如果有,還望大家指正。裡面我寫了個視覺化結果的函式,但只能在二維的資料上面使用。直接貼程式碼:

(注:全部程式碼和資料集:回覆 資料探勘入門與實戰 公眾號號 “SVM”獲取)

SVM.py

測試的資料有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

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%  

訓練好的模型圖: