《Machine Learning in Action》—— 剖析支持向量機,單手狂撕線性SVM

《Machine Learning in Action》—— 剖析支持向量機,單手狂撕線性SVM

前面在寫NumPy文章的結尾處也有提到,本來是打算按照《機器學習實戰 / Machine Learning in Action》這本書來手撕其中代碼的,但由於實際原因,可能需要先手撕SVM了,這個算法感覺還是挺讓人頭疼,其中內部太複雜了,涉及到的數學公式太多了,也涉及到了許多陌聲的名詞,如:非線性約束條件下的最優化、KKT條件、拉格朗日對偶、最大間隔、最優下界、核函數等等,天書或許、可能、大概就是這樣的吧。

記得與SVM初次邂逅是在17年,那個時候的自己年少輕狂,視圖想着站在巨人的肩膀上,把所有自己感興趣的內容都搞懂,深入骨髓的那種。但後來殘酷的現實讓我明白一個道理:你知道的越多,你不知道的也越多。而且那個時候自己也沒有能力、資源和機會去深入SVM內部,完全無法理解SVM的內部原理。所以,當時自己對SVM的收穫只有一個:SVM主要是用來做分類任務的,僅此而已。

第二次接觸SVM是在準備考研複試吧,當時複試並沒有給出具體內容和範圍,而且自己也還是個初出茅廬的小子,對這種所謂的複試有種莫名的恐懼感。也只有從上屆學長學姐的口中,得知複試的時候老師會考究學生是否有科研的潛力,所以最好把機器學習熟知一下。那個時候也是處於新冠疫情的緊張時期嘛,就瘋狂補習機器學習的內容,其中就包括支持向量機——SVM,主要的學習渠道是吳恩達老師的機器學習課程,感覺講的的確不錯,非常適合我這種菜鳥級選手學習。當時也算是對SVM有了一定的認識吧,也大致瞭解了SVM的工作原理,當然了,也只是對SVM有了個的淺顯的認識,沒有手撕SVM的過程,也沒有完全把它整明白。儘管如此,複試的過程依然被面試導師錘的體無完膚,除了問了機器學習相關內容之外,編譯原理等一些專業知識對於我這個貿易專業的學生來講可太痛苦了,之前也沒有接觸過,全程阿巴阿巴。想到這,眼角又又。。。

第三次面對SVM也就是現在了,想着無論如何也要打通我的任督二脈,一定要搞清楚SVM的來龍去脈,也要像面試老師捶我那樣,把SVM往死裏錘。於是有了下文學習SVM之後的總結,一方面算是重新梳理一遍SVM,另一方面也希望來訪的讀者能夠有所收穫。

對於剛剛接觸SVM的讀者,Taoye主要有以下幾條建議,也是我學習SVM過程中的一個小總結吧:

  • SVM內部的數學公式很多,但請不要未戰先怯,犯下兵家大忌。無論是閱讀該篇文章也好,學習其他相關SVM資源也罷,還請諸君耐心、認真且完整的看完。
  • SVM的原理過程會涉及到很多的符號或記號,一定要梳理清楚他們所代表的含義。另外,推導過程中會存在很多的向量或矩陣,一定要明白其中shape,這一點可能在不同的資料中會有不一樣的處理方式。
  • 在閱讀的同時,一定要拿出稿紙手動推演SVM的過程,儘可能明白整個過程的來龍去脈,有不明白的地方可以留言或查找其他相關資料來輔助自己的理解。
  • 閱讀一遍或許有不少知識不理解,但多閱讀幾遍相信一定會有不少收穫

本文參考了不少書籍資料以及許多大佬的技術文章,行文風格儘可能做到通俗易懂,但其中涉及到的數學公式在所難免,還請諸讀者靜下心來慢慢品嚐。由於個人水平有限,才疏學淺,對於SVM也只是略知皮毛,可能文中有不少表述稍有欠妥、有不少錯誤或不當之處,還請諸君批評指正。

我是Taoye,愛專研,愛分享,熱衷於各種技術,學習之餘喜歡下象棋、聽音樂、聊動漫,希望藉此一畝三分地記錄自己的成長過程以及生活點滴,也希望能結實更多志同道合的圈內朋友,更多內容歡迎來訪微信公主號:玩世不恭的Coder

符號說明

符號 說明
表示單個樣本,其中包含多個屬性,顯示爲一個行向量
表示單個樣本中的某個屬性特徵
表示單個樣本所對應的標籤(具體分類),爲整數,非1即-1
表示的是權值行向量,其中,也是所需要訓練的參數
表示的是決策面中的,也是所需要訓練的參數
表示函數間隔,具體解釋見下文
表示幾何間隔,具體解釋見下文
表示拉格朗日乘子
在這篇文章中表示線性核函數

關於上述的符號說明,僅僅只是本篇文章的一部分,其他符號可通過正文了解。上述符號可能部分暫時不懂,但沒關係,讀者可在閱讀的過程中,隨時返回來查看,即可理解每個符號所代表的意義。

一、SVM是什麼

關於SVM是什麼,之前在Byte Size Biology上看到有篇文章很好的解釋了SVM,在知乎上也有一位名叫“簡之”的用戶通過故事的形式來將其進行轉述,通俗易懂,很好的向首次接觸SVM的讀者介紹了SVM能幹嘛。

油管上也有更爲直觀認識SVM的短視頻(翻牆):https://www.youtube.com/watch?v=3liCbRZPrZA

總結一哈:

對於一個二分類問題,樣本數據是線性可分的,我們則需要通過一根直線(也就是上述例子當中枝條)將兩個不同種類的樣本進行分離。按道理來講,我們已經實現了需求,但是,這根枝條的具體擺放位置可能有無數多個,而我們的最終目的是將枝條擺放到一個最好的位置,從而當我們引入了一些新樣本的時候,依然能最好很好將兩類的數據分離開來,也就是說我們需要將模型的“泛化性能”最大化。之前也看到過一個例子(來源忘了),這裏分享下,大概就是講:我們在通過懸崖吊橋的時候,會不自覺的儘可能往中間走,因爲這樣的話,當左右起風的時候,雖然我們的位置會左右稍微移動,但還不至於跌落懸崖。而越靠近邊緣,風險就越大,就是這麼個道理。而尋找最大“泛化性能”的過程,就是將枝條擺放在距離小球最遠的位置,而小球相對於枝條的位置就是“間隔”,而我們要做的就是將這間隔最大化

上述僅僅是對於線性可分數據分類的一種處理方式,但有的時候,理想是美好的,現實卻是殘酷的。在實際樣本數據中,大多都是線性不可分的,也就是說我們無法找到合適位置的枝條將不同分類的數據分離開來,更別提“間隔最大化”了。這個時候,我們就需要將桌上的小球排起,然後用一個平面(紙)將不同分類小球分離開來。也就是說,我們將低維度映射到了高緯度,這樣對於小球的分類就更加容易了。

再之後,無聊的大人們,把這些球叫做 「data」,把棍子 叫做 「classifier」, 最大間隙trick 叫做「optimization」, 拍桌子叫做「kernelling」, 那張紙叫做「hyperplane」。

二、線性可分SVM與間隔最大化

我們先來看具體看看線性可分的二分類問題。

假如說,我們這裏有一堆樣本,也就是我們常說的訓練集,且表示的是樣本屬性特徵向量,其內部有多個不同屬性,這裏我們不妨指定每個樣本含有兩個屬性特徵,也就是說(之所以用列向量表示,主要是方便後面超平面的構建)。而表示的是每個樣本中所有屬性特徵所對應的標籤,由於這裏的問題屬性二分類,所以的值只存在兩種。爲此,我們可以通過Matplotlib在平面直角座標系中繪製其圖像,初步觀察其分佈規律,具體代碼如下:

import numpy as np
import pylab as pl
from sklearn import svm

%matplotlib inline
print(np.__version__)

"""
    Author: Taoye
    微信公衆號:玩世不恭的Coder
"""

if __name__ == "__main__":
    # np.random.seed(100)        # 可自行設置隨機種子每次隨機產生相同的數據
    X_data = np.concatenate((np.add(np.random.randn(202), [33]),       
                             np.subtract(np.random.randn(202), [33])),
                             axis = 0)      # random隨機生成數據,+ -3達到不同類別數據分隔的目的 

    Y_label = np.concatenate((np.zeros([20]), np.ones([20])), axis = 0)

    svc = svm.SVC(kernel = "linear")
    svc.fit(X_data, Y_label)

    w = svc.coef_[0]      
    ww = -w[0] / w[1]                # 獲取權重w
    xx = np.linspace(-66)
    yy = ww * xx - (svc.intercept_[0]) / w[1]   # intercept_獲取結截距

    b_down = svc.support_vectors_[0]         # 得到對應的支持向量
    yy_down = ww * xx + (b_down[1] - ww * b_down[0])
    b_up = svc.support_vectors_[-1]
    yy_up = ww * xx + (b_up[1] - ww * b_up[0])

    pl.plot(xx, yy, "k-"); pl.plot(xx, yy_down, "k--"); pl.plot(xx, yy_up, "k--")
    pl.scatter(X_data[:, 0], X_data[:, 1], c = Y_label, cmap = pl.cm.Paired)

    pl.show()

執行代碼,可以繪製如下所示圖片,注意:以上代碼每次運行都會隨機產生不同的二分類數據集,如想每次隨機產生相同的數據集,可自行配置np.random.seed隨機種子;另外,還有一點需要需要說明的是,上述代碼使用到了NumPy,關於NumPy的使用,可自行參考之前寫的一篇文章:print( "Hello,NumPy!" )

如上圖所示,我們可以發現,棕色代表一類數據集,此時標籤,藍色代表另一類數據集,標籤,而要想將上圖兩類數據集分離開來,顯然不止一條直線,與上圖兩條虛線平行且居其之間的任意一條直線都能達到此目的。在這無數條直線中,要數上圖中的三條最爲特殊,中間實線居於兩條虛線中間,我們一般稱其爲“決策面”“超平面”,而其所表示的方程,我們一般稱作“決策方程”“超平面方程”,在這裏可以表示爲。(下面會推導)

從上圖我們還可以觀察得到,在所有樣本數據集中,虛線上的樣本距離決策面最近,我們把這種比較特殊的樣本數據一般稱之爲“支持向量”,而支持向量到決策面之間的距離稱爲“間隔”。我們不難發現,決策面的位置主要取決於支持向量,而與支持向量除外的數據樣本沒有關係。(因爲支持向量的確定就已經確定了最大間隔)

關於上述提到的一些關於SVM的名詞概念,在正式推演之前,還是有必要理解清楚的。

前面我們也有提到,關於能將兩類不同數據集相互分隔開來的直線有無數種,而我們要想在這無數種直線找到一條最合適的,也就是達到一個間隔最大化的目的,這就是一個“最優化”問題。而最優化問題,我們需要了解兩個因素,分別是目標函數和優化對象。既然我們是想要達到間隔最大化的目標,那麼目標函數自然就是間隔,而優化對象就是我們的決策面方程。所以,我們首先需要用數學來明確間隔和決策面方程:

我們知道,在平面直角座標系中,一條直線可以用其一般式方程來來表示:

而根據上述圖像,我們可以知道,橫縱座標代表的意義是一個樣本的不同屬性特徵,而標籤則是通過顏色(棕色和藍色)來進行表示。所以上述的直線的一般式方程中的表示的就是一個樣本的兩種屬性特徵,爲了方便理解,我們不妨將其修改爲,並將替換爲,對此,我們可以將上述方程向量化,得到:

不能識別此Latex公式:

 \left(
 \begin{matrix}
   w_1, w_2 \\
  \end{matrix}
  \right
  \times
  \left(
 \begin{matrix}
   x^{(1)} \\
   x^{(2)}
  \end{matrix}
  \right) \
    +b=0 \tag{2-1}

,則上述指向方程最終可以表示爲:

式(1-2)表示的就是我們優化問題的優化對象,也就是決策面方程。我們知道在平面直角座標系中,一條直線可以通過其斜率和截距來確定,而在決策面方程裏,我們不難得到:確定了決策面的方向(斜率) ,而確定了決策面的截距。既然我們已經得到了優化問題的優化對象——決策面方程,那麼接下來就需要得到目標函數——間隔的表達式了。

在此,我們需要引入函數間隔幾何間隔的概念了。

一般來講,我們可以通過樣本點到決策面的距離來表示分類預測的正確程度,距離決策面越遠,一般分類就越正確(可根據圖像自行理解),而在超平面確定的情況下,我們可以通過的值來描述樣本距超平面的遠近(注意:這裏是描述遠近。而不是確切的距離)。我們知道,樣本有不同的分類,所以有的時候的符號具有不確定性,所以我們可以通過的符號來判斷分類結果的正確性。也就是說可以通過值的大小和符號來判斷一個樣本分類的正確性和正確程度,這個就是我們的函數間隔(這個概念務必要理解清楚),我們不妨用來表示:

而我們知道,上述的,表示的是有個樣本,而在個樣本中,固然存在一個樣本使得該值達到最小,該樣本也就是我們前面所說的支持向量,我們把支持向量到超平面的函數間隔定義爲,則:

我們只有函數間隔還不夠,函數間隔描述的僅僅是樣本分類的正確性和正確程度,而不是確切的間隔。當我們成比例的更改的時候,決策面並沒有發生任何改變,而函數間隔卻發生了改變,所以我們需要對進行約束,從而當無論怎麼成比例變動的時候,我們的“間隔”都不會發生改變,也就是進行將規範化,由函數間隔規範後的間隔我們稱之爲幾何間隔,我們不妨用來表示,則某個樣本到超平面的幾何間隔爲

我們可以將式子(1-5)理解成點到直線的距離公式(這個在中學時期就學過的)。對於這個二分類問題,我們不妨將二分類的標籤定爲 ,則可以表示爲(之所以乘以,主要是把絕對值符號去掉,這樣就能描述所有樣本了,而省去了分類討論的麻煩):

而我們知道,上述的,表示的是有個樣本,而在個樣本中,固然存在一個樣本使得該值達到最小,該樣本也就是我們前面所說的支持向量,我們把支持向量到超平面的幾何間隔定義爲,則:

通過上述對函數間隔和幾何間隔的分析,我們可以得到他們之間的關係:

自此,我們已經分析得到了該優化問題的優化對象——決策面方程和目標函數——間隔(幾何間隔)。在之前,我們提到了支持向量的概念,那麼支持向量具有什麼特性呢?細想一下不難發現,支持向量到決策平面的間隔是最近的,即滿足如下式子:

對此,我們就可以通過數學來表達該優化問題:

前面,我們提到了,函數間隔描述的僅僅是樣本分類的正確性和正確程度,而不是確切的間隔。當我們成比例的更改的時候,函數間隔雖然發生了改變,但並不會影響我們的幾何間隔——目標函數,也就是說,此時產生了一個等價的最優化問題。爲了方便描述問題,我們不妨取。另外,我們注意到目標函數可以等價於,(注意,僅僅是等價。而之所以等價爲二分之一、平方的形式,主要是方便後期的求導操作),對此,我們可以將數學表達的優化問題轉化爲如下形式:

關於如上提到的決策平面和間隔等概念,我們可以通過下圖來直觀感受下(理解清楚):

圖片來源:西瓜書
圖片來源:西瓜書

至此,我們已經得到了該優化問題的數學表達,我們不妨通過一個小例子來檢測下:


例子來源:李航-《統計學習方法》第七章內容

例子1:已知一個如圖所示的訓練數據集,其正例點是,負例點爲,試求最大間隔分離的決策面?

根據前面的優化問題表達,我們可以得到如下表示:

求解可以得到目標函數的最小時,,於是最大間隔分離的決策面爲:

其中,爲支持向量。


三、拉格朗日乘數法和對偶問題

首先,我們有必要先了解下什麼是拉格朗日乘數法。

關於對偶問題,《統計學習方法》一書中是如此描述的:

爲了求解線性可分的支持向量機的最優化問題,將它作爲原始最優化問題,應用拉格朗日對偶性,通過求解對偶問題(dual problem)得到原始問題(primary problem)的最優解,這就是線性可分支持向量機的對偶算法(dual algorithm)。這樣做的優點,一是對偶問題往往更容易求解;二是自然引入核函數,進而推廣到非線性分類問題。

按照前面對優化問題的求解思路,構造拉格朗日方程的目的是將約束條件放到目標函數中,從而將有約束優化問題轉換爲無約束優化問題。我們仍然秉承這一思路去解決不等式約束條件下的優化問題,那麼如何針對不等式約束條件下的優化問題構建拉格朗日函數呢?

因爲我們要求解的是最小化問題,所以一個直觀的想法是如果我能夠構造一個函數,使得該函數在可行解區域內與原目標函數完全一致,而在可行解區域外的數值非常大,甚至是無窮大,那麼這個沒有約束條件的新目標函數的優化問題就與原來有約束條件的原始目標函數的優化是等價的問題。

對此,我們重新回顧下原優化問題的數學表達:

關於拉個朗日乘數法的解題思路,其實早在大學《高等數學》這門課程中就已經提到過,我們不妨通過一個小例子來了解下其解題的一般形式:


例子來源:李億民-《微積分方法》第二章內容

例子2:求函數上的最值。

做拉格朗日函數

上面的拉格朗日函數,我們分別對求偏導,得到:

求解得到,所以


上例就是利用拉格朗日求解最優問題的一般思路,先構造拉格朗日函數,然後分別對參數求偏導,最終求解得到最優解。而我們要想得到最大間隔,同樣可以根據該思路進行,只不過式子更加複雜,而我們還需要利用拉格朗日的對偶性來簡化優化問題。

在此之前,我們先來回顧下該優化問題下的數學表達:

按照我們例2的思路,我們首先構造出該優化問題的拉格朗日函數,注意:(2-1)式的限制條件是對於樣本 的,而這裏的,所以想這樣的約束條件有N個,而每個約束條件都對應一個拉格朗日乘子(例2的),我們不妨將該拉格朗日乘子定義爲。據此,我們構建出的該優化問題的拉格朗日函數如下:

其中,爲拉格朗日乘子向量。

,原始目標函數即可轉化爲:

根據上述目標函數,我們可以發現是先求最大值,再求最小值,而內層最小值是關於,而外層最大值是關於,而我們的又是不等式約束,這樣對於求解來講過於麻煩。由拉格朗日的對偶性,原始問題的對偶問題就是極大極小值,其對偶問題如下:

且原問題和對偶問題滿足如關係:,而我們要找的是。讀到這裏,我們應該會存在兩個疑問:

  • 疑問1:爲什麼前面我們令$\theta(w,b)=\mathop{max}\limits_{\alpha>0} \ L(w,b,\alpha)$,而不是其他的表達形式呢?

主要是因爲,這樣替換之後,我們能使得該函數在可行解區域內與原目標函數完全一致,而在可行解區域外的數值非常大,甚至是無窮大,那麼這個沒有約束條件的新目標函數的優化問題就與原來有約束條件的原始目標函數的優化是等價的問題。(這句話要重點理解

令:

之後,在可行解區域之內和可行解區域之外,我們通過分析得到:在可行解區域之內,

此時,我們要求的是 ,而減去一個大於等於0的最大值是多少?不就是等於麼,而同樣也是我們可行解區域之內的目標函數。也就是說在可行解區域之內,就等價於。同理,我們可以分析得到,在的可行解區域之外,此時可區域無窮大。所以沒有約束的新目標函數的優化問題就與原來有約束條件的原始目標函數的優化是等價的問題,即:

  • 疑問2:爲什麼將原問題轉化爲對偶問題之後,在什麼樣的條件下,纔會滿足$d^= p^$?

轉化爲對偶問題之後,要使得等式成立,則需要滿足如下條件,也就是該問題成立時候的KKT條件

前兩個條件我們不難得到,前面我們也有過對其進行分析,那麼第三個條件爲什麼需要滿足呢?

在介紹支持向量和決策平面的時候,我們有提到,最終的決策平面只會與支持向量相關,而與其他大多數樣本數據(非支持向量)無關。我們不妨來對它們分別介紹,當樣本點爲支持向量的時候,此時,即當所取的樣本點爲非支持向量的時候,要使得,則此時的。所以從整體樣本域來講,只有滿足的時候,才能到達目標決策平面與支持向量有關,而與其他大多數非支持向量無關。

綜上,只有滿足KKT條件的時候,才能到達,即在滿足KKT條件的時候,才能將原始目標問題,轉化爲對偶問題,此時兩者是等價的,且此時的目標函數爲內層關於的最小值,而外層是關於的最大值,這樣一來,大大方便了我們對目標函數的求解。所以優化問題,我們轉化如下:

而要解決這個優化問題,我們可以分爲兩步來進行求解:

  1. 先求內層關於的最小值,此時我們應該將視爲常數
  2. 再求外層關於的最大值,由於經過了第一步關於的最小值,這兩個參數已經被消掉了,所以第二步只會存在關於的求解

經過上述對目標函數的問題分析,我們下面根據上述的兩個步驟來手握手式的進行求解。

四、模型的求解

關於拉格朗日的內層求解

首先,我們需要對內層的最小值問題進行求解,即求:

注意,此時僅僅只是關於,而是由外層最大值問題進行求解的,在這裏當做常數處理即可。根據例2,我們需要求出關於的偏導,我們在這假設每個樣本含有個屬性,則,應各位“老婆們”的要求,具體求偏導的詳細過程如下:

爲了讓讀者徹底明白上述過程,所以步驟有點多,這裏就不採用Latex語法來編輯上述公式的推導過程了,當然了,Taoye會盡可能地將過程寫的足夠詳細。上述關於拉格朗日函數求偏導的過程,自認爲已經寫的很詳細了,最主要的是要區分的shape問題,以及各自代表的意義是什麼。對於上述過程有不清楚的,隨時歡迎聯繫Taoye或在下方留言,也歡迎更多讀者來訪微信公衆號:玩世不恭的Coder。

對此,我們已經通過上面過程得到關於偏導所要滿足的式子:

之後,我們將式子(4-2)重新帶回到(4-1)中的最小值問題中,即可消掉參數,也就是說得到的式子僅僅只是關於,具體代回過程如下圖所示:

上圖就是將對求導之後所得到的式子待會的完整過程,務必要好好理解清楚。

關於這個代回的過程,有兩點還是有必要說一下的,這也是前幾天實驗室的同學存在疑問的地方。(參照上述圖片來解疑

  • 疑問1:這裏前一項難道不是和後一項同樣爲0麼?因爲不是說了$\sum_{i=1}^N\alpha_iy_i=0$啊???

其實這個地方的後一項是爲0,而前一項並不一定爲0。因爲後一項中的其實就相當於一個固定的常數,也就是中的每一項所乘的數都是,這樣的話,固定的常數乘以0,結果當然依然等於0咯。

而我們再看看前一項,可以發現前一項中除了之外,還有的存在,而我們的是會隨着樣本的變化而改變的,所以每次乘的數可能並不一定相同。舉個例子理解一下吧:,這個我們都應該知道。當我們在的前面同時乘以一個相同的數,這個等式是依然成立的,然而當我在前面分別乘以一個並不相同的數,那麼這個等式就不成立了,比如 依然成立,而 2+33-45=0就不成立了。

就是這個道理,務必要好好理解清楚。

  • 疑問2:爲什麼這一步可以推導至下面那一步呢???

其實這個問題很好理解,因爲這兩個成績形式的式子是相互獨立的,也就是說雖然都有,但是這兩個並不一定是同時取同一個值的。這就像一種笛卡爾積的形式,所以將前一個式子中的換成
依然成立,所以能推導至下面那一步。。。

通過上述過程,我們已經得到了代回之後的式子,如下:

並且我們觀察可以發現,式子此時僅僅只存在,而已經成功被消掉了。注意:上式中的表示的樣本,也就說這些樣本的各個屬性特徵以及標籤都是已知的,所以上式只有是未知的。

至此,我們已經解決了對偶問題的內層最小值問題,接下來我們就要求解外層的最大值問題了,將最小值的式子代回原對偶問題,我們更新下對偶問題,得到如下:

如上,已經將原對偶轉換爲了上式的樣子,下面我們據此再來看之前的例1


例子來源:李航-《統計學習方法》第七章內容

例子3:已知一個如圖所示的訓練數據集,其正例點是,負例點爲,試求最大間隔分離的決策面?

根據所給數據,其對偶問題是:

我們將代入到目標函數並記爲:


求偏導數並令其爲0,易知在點取極值,但該點不滿足約束條件,所以最小值應在邊界上達到。

時,最小值爲;當時,最小值。所以,當時,此時達到最小。

這樣的話,就說明所對應的點就是支持向量了,根據,我們可以求得此時,再由,可以得到

綜上,我們可以得到決策面最終爲:

其中,爲支持向量。


歷經九九八十一難,終於來到了這最後一步了,只要我們能求得上式的最小值,那麼模型的求解也就該到一段落了。

那麼,問題來了,關於上式,我們應當如何求解呢???

下面就應該是我們的重頭戲閃亮登場了——SMO算法,各位看官掌聲歡迎一下,有請SMO大佬登臺表演。。。

基於SMO算法的外層求解

關於SMO算法,李航老師的《統計學習方法》一書中是這麼描述的,

關於外層問題的求解,我們有許多方法可供使用。當我們的數據樣本比較少的時候,可以將支持向量機的學習問題轉換爲凸二次規劃問題,這樣的凸二次規劃問題具有全局最優解,並且有許多最優化算法可以用於這一問題的求解。但是當訓練樣本容量很大時,這些算法往往變得非常低效,以致無法使用。所以,如何高效地實現支持向量機學習就成爲一個重要的問題。目前人們已提出許多快速實現算法。而在這些算法中,要數Platt的序列最小最優化(sequential minimal optimization SMO)算法最爲人所知。

SMO算法是一種啓發式算法,其基本思想是:如果所有變量的解都滿足此最優化問題的KKT條件,那麼這個最優化問題的解就得到了。因爲KKT條件是該最優化問題的充分必要條件。否則,選擇兩個變量,固定其他變量,針對這兩個變量構建一個二次規劃問題。這個二次規劃問題關於這兩個變量的解就應該更接近原始二次規劃問題的解,因爲這會使得原始二次規劃問題的目標函數值變得更小。更重要的是,這時子問題可以通過解析方法求解,這樣就可以大大提高整個算法的計算速度。子問題有兩個變量,一個是違反KKT條件最嚴重的那一個,另一個由約束條件自動確定。如此,SMO算法將原問題不斷分解爲子問題並對子問題求解,進而達到求解原問題的目的。

上述內容來自李航——《統計學習方法》第二版。

不知道大夥讀完上述關於內容是什麼感受,這裏簡單總結一下李航老師所表達的意思吧。

在Taoye的印象裏,小學時期上語文課的時候學習過一篇文章叫做《走一步,再走一步》。(具體幾年級就記不清楚了)

嘿!您還別說,剛剛去搜索了下這篇課文,還真就叫這個名兒。第一次讀李航老師《統計學習方法》中關於SMO的內容之後,就讓我想起這篇文章。我還專門重新讀了一下這篇文章,主要講的內容是這樣的:

文章中主人公名叫小亨特,他不是天生體弱怯懦嘛,在一次和小夥伴攀登懸崖的時候,由於內心的恐懼、害怕,在攀登途中上不去,也下不來。然後呢,他的小夥伴傑裏就把小亨特的父親找來了,父親對小亨特說:“不要想有多遠,有多困難,你需要想的是邁一小步。這個你能做到。看着手電光指的地方。看到那塊石頭沒有?”,最終通過父親的鼓勵,小亨特成功脫險。文末作者還總結道:在我生命中有很多時刻,每當我遇到一個遙不可及、令人害怕的情境,並感到驚慌失措時,我都能夠應付——因爲我回想起了很久以前自己上過的那一課。我提醒自己不要看下面遙遠的岩石,而是注意相對輕鬆、容易的第一小步,邁出一小步、再一小步,就這樣體會每一步帶來的成就感,直到完成了自己想要完成的,達到了自己的目標,然後再回頭看時,不禁對自己走過的這段漫漫長路感到驚訝和自豪。

把《走一步,再走一步》這篇文章搬出來,真的不是在湊字數從而給大家閱讀帶來壓力,只是覺得SMO算法描述的就是這麼個理兒。算了,不多說了,說多了還真的會有湊字數的嫌疑。(ノへ ̄、)

下面我們開始進入到SMO吧。。。

在這之前,我們把外層的最小值問題再搬出來:

在這裏,我們是假設對於所有的樣本數據都是100%線性可分的。

對於該優化問題的SMO算法,我們可以這樣理解:因爲在我們的數據集中,屬於每個樣本的屬性特徵,爲樣本所對應的標籤,而這些都是已知的,上述優化問題的目標函數只存在爲未知變量,且未知變量有個。而根據SMO算法的思想,我們每次只將其中兩個看做變量,而其他僅僅只是常數。在卻確定其中一個變量的時候,另一個變量就可以通過約束得到。我們不妨將該兩個變量定爲,則SMO不斷執行如下兩個步驟直至收斂:

  • 選取一對需要更新的變量
  • 固定以外的參數,根據求解式(4-5)獲得更新後
  • 更新好之後,重新選取兩個進行不斷更新迭代(重複1、2步驟)

講到這裏,SMO算法是不是和《走一步,再走一步》中主人公類似呢?將一個大的、複雜的問題轉換成多個小問題,然後不斷的迭代更新。

爲什麼我們每次都同時優化兩個參數,而不是一個呢?因爲每次更新兩個參數,才能確保約束條件成立。而當我們僅僅只是修改一個時,那麼就將違背這個約束條件了。

據SMO的思想,我們不妨把目標函數中的單獨拎出來,如下:

注意:因爲我們的僅僅只是對作爲參數,而只是作爲一個參數而存在。還有一點需要注意的是,因爲我們是二分類問題,且樣本數據的標籤爲非1即-1,所以,這個在化簡過程中需要用到。此時我們得到關於的目標函數爲:

我們知道對於這個式子是有一個約束條件的,我們可以根據這個用來表示(注意:),如下:

通過上式,用來表示,我們不妨將帶到中,得到一個只關於的式子:

此時的僅僅只是關於的函數,我們將進行求導,並令導數爲0:



上述就是SMO中限制其中兩個變量的推到過程的推到過程(公式太多,過程有點複雜,確實不方便使用Latex語法,不過過程都已經寫的很詳細了,還是需要靜下心來慢慢手動推導的)下面總結一下上述SMO算法的過程吧:

前面我們不是得到了僅僅關於爲變量的麼,也就是說此時的未知數只有一個,我們要求的最值應該怎麼求呢?當然是對其進行求導咯,然後對導數爲0,即可解出取最值時候的,整理之後得到如下式子:

此時,我們可以發現除了數據樣本相關信息和之外,還有的存在,而我們前面也又說到,SMO算法本身是一個不斷迭代更新的過程,我們需要的是可以通過的更新之前的來修改,從而得到一個新的,我們不妨令新的、舊的。而我們知道舊的之間需要滿足一個限制條件:

所以,我們重新將代回到式,用來代替,(要時刻注意:)得到:

之後我們通過前面拉格朗日得到的關係式,用來代替(4-10)後面的兩個級數,整理最終得到:

PS:關於是什麼,請見上圖中的內容。

通過如上式子,我們就能求得更新之後的,而SMO算法的核心在於兩兩不斷的更新迭代,所以最終我們會得到,每個樣本都會對應一個,而前面我們也有說過,決策面最終只會與支持向量有關,而與非支持向量的樣本無關,所以大多數的都等於0,只有少數爲非0。如此一來,我們就能求解得到向量:,隨後,我們就能通過就能求得參數

還有一點需要注意的是,上述過程都是默認所有樣本數據都是線性可分的,也就是說沒有一個樣本會被誤分類。但這只是理想狀態下,而實際不免會有個別數據不得不被誤分類,這時我們需要定義懲罰參數和容錯率,而容錯率是用來不斷優化的,主要通過實際值與真實值得到。而懲罰參數我們定爲,而要想成功更新得到,則需要確定的範圍。對此,我們不妨定義如下:

而我們知道:

綜合上式,可以確定的範圍:

而這個在不同情況下的的範圍,我們會在下面實際編程的時候需要用到,主要是用來更新值。

接下來,就是更新值了。前面我們已經定義了懲罰參數,且,此時通過更新得到的固然是相等的;但假如我們更新之後的不在這個區間之內,則此時得到的是不相等的,所以我們需要確定在不同情況下更新之後的值:

前面,我們已經得到:

因爲我們是打算通過的值來得到更新之後的,所以把單獨拎出來,得到:

同時,因爲上述中的級數形式可以使用來表示,所以整理之後得:

同理,可以得到

當更新之後的都有效的時候,即在區間之內時,此時,而在不滿足上述條件的時候,我們更新之後的的均值,即:

如此一來,我們就已經完成了SMO算法的流程,該有的參數都已經求解出來了。

說實話,寫到這,Taoye的確有點累了,腦細胞也嚴重不足了,但爲了各位“老婆們”的正常閱讀,還是得繼續寫下去纔行。

下面,我們就通過編程來實現線性SVM算法吧!(本次手撕SVM的數據集依然採用我們前面所隨機創建的)

五、編程手撕線性SVM

在前面,我們其實已經實現了線性SVM的分類,只不過那個時候使用的是sklearn內置的接口。但既然是手撕SVM,當然是需要自己來實現這一功能的。

在這裏需要提前說明的是,上述代碼大量使用到了NumPy操作,關於NumPy的使用,可自行參考之前寫的一篇文章:print( "Hello,NumPy!" )

訓練SVM模型,沒數據集可不行,本次手撕SVM的數據集依然採用我們前面所隨機創建,對此,我們定義一個etablish_data方法來隨機創建一個SVM二分類數據集:

"""
    Author: Taoye
    微信公衆號: 玩世不恭的Coder
    Explain: 用於生成訓練數據集
    Parameters:
        data_number: 樣本數據數目
    Return:
        x_data: 數據樣本的屬性矩陣
        y_label: 樣本屬性所對應的標籤
"""

def etablish_data(data_number):
    x_data = np.concatenate((np.add(np.random.randn(data_number, 2), [33]),       
                             np.subtract(np.random.randn(data_number, 2), [33])),
                             axis = 0)      # random隨機生成數據,+ -3達到不同類別數據分隔的目的 
    temp_data = np.zeros([data_number])
    temp_data.fill(-1)
    y_label = np.concatenate((temp_data, np.ones([data_number])), axis = 0)
    return x_data, y_label

前面,我們在講解SMO算法的時候提到,每次都會選取隨機兩個來進行更新,這裏我們不妨將第一個通過遍歷的形式逐個選取,而另一個則通過np.random模塊來隨機選取,這裏需要主要的是,第二個選取的不能與第一個相同。爲此,我們定義一個方法random_select_alpha_j來隨機選取第二個(第一個已經通過遍歷得到):

"""
    Author: Taoye
    微信公衆號: 玩世不恭的Coder
    Explain: 隨機選取alpha_j
    Parameters:
        alpha_i_index: 第一個alpha的索引
        alpha_number: alpha總數目
    Return:
        alpha_j_index: 第二個alpha的索引
"""

def random_select_alpha_j(alpha_i_index, alpha_number):
    alpha_j_index = alpha_i_index
    while alpha_j_index == alpha_i_index:
        alpha_j_index = np.random.randint(0, alpha_number)
    return alpha_j_index

我們知道,每一個更新之後的都需要滿足。爲此,我們定義一個方法modify_alpha來確定在區間之內:

"""
    Author: Taoye
    微信公衆號: 玩世不恭的Coder
    Explain: 使得alpha_j在[L, R]區間之內
    Parameters:
        alpha_j: 原始alpha_j
        L: 左邊界值
        R: 右邊界值
    Return:
        L,R,alpha_j: 修改之後的alpha_j
"""

def modify_alpha(alpha_j, L, R):
    if alpha_j < L: return L
    if alpha_j > R: return R
    return alpha_j

我們模型訓練是一個迭代更新的過程,而更新的前提是誤差比較大,所以我們需要定義一個方法calc_E_i來計算誤差,但誤差又怎麼計算呢?這一點其實我們在最開始就已經提到過了,誤差是通過模型計算出來的值與其真實值最差得到,也就是前面提到的下面的推導務必要理解清楚,矩陣變換要十分熟悉):

根據上述誤差的推導,我們現在就可以通過代碼來計算誤差了(上面的推導務必要理解清楚,矩陣變換要十分熟悉,才能理解下面代碼所表達的含義):

"""
    Author: Taoye
    微信公衆號: 玩世不恭的Coder
    Explain: 計算誤差並返回
"""

def calc_E(alphas, y_lable, x_data, b, i):
    f_x_i = float(np.multiply(alphas, y_lable).T * (x_data * x_data[i, :].T)) + b
    return f_x_i - float(y_label[i])

同樣的,我們把其他一些用於整體代換的單獨拎出來,並通過方法進行返回,除了上述的誤差之外,還有,相關推導過程讀者可自行根據上述來進行(一定要會),相關公式和代碼如下:

"""
    Author: Taoye
    微信公衆號: 玩世不恭的Coder
    Explain: 計算eta並返回
"""

def calc_eta(x_data, i, j):
    eta = 2.0 * x_data[i, :] * x_data[j, :].T \
            - x_data[i, :] * x_data[i, :].T \
            - x_data[j, :] * x_data[j,:].T
    return eta

"""
    Author: Taoye
    微信公衆號: 玩世不恭的Coder
    Explain: 計算b1, b2並返回
"""

def calc_b(b, x_data, y_label, alphas, alpha_i_old, alpha_j_old, E_i, E_j, i, j):
    b1 = b - E_i \
         - y_label[i] * (alphas[i] - alpha_i_old) * x_data[i, :] * x_data[i, :].T \
         - y_label[j] * (alphas[j] - alpha_j_old) * x_data[i, :] * x_data[j, :].T
    b2 = b - E_j \
         - y_label[i] * (alphas[i] - alpha_i_old) * x_data[i, :] * x_data[j, :].T \
         - y_label[j] * (alphas[j] - alpha_j_old) * x_data[j, :] * x_data[j, :].T
    return b1, b2

OK,準備工作已經完成了,接下來是時候放出我們的核心SMO算法的代碼了,大家可根據前面的SMO思想來理解,下面代碼也會放出詳細的註釋來幫助大家理解:

"""
    Author: Taoye
    微信公衆號: 玩世不恭的Coder
    Explain: SMO核心算法,求得b和laphas向量
    Parameters:
        x_data: 樣本屬性特徵矩陣
        y_label: 屬性特徵對應的標籤
        C:懲罰參數
        toler:容錯率
        max_iter:迭代次數
    Return:
        b: 決策面的參數b
        alphas:獲取決策面參數w所需要的alphas
"""

def linear_smo(x_data, y_label, C, toler, max_iter):
    x_data = np.mat(x_data); y_label = np.mat(y_label).T     # 將數據轉換爲矩陣類型
    m, n = x_data.shape                                      # 得到數據樣本的shape信息
    b, alphas, iter_num = 0, np.mat(np.zeros((m, 1))), 0     # 初始化參數b和alphas和迭代次數
    while (iter_num < max_iter):                            # 最多迭代max_iter次
        alpha_optimization_num = 0   # 定義優化次數
        for i in range(m):          # 遍歷每個樣本,一次選取一個樣本計算誤差
            E_i = calc_E(alphas, y_label, x_data, b, i)      # 樣本i的誤差計算
            if ((y_label[i] * E_i < -toler) and (alphas[i] < C)) or ((y_label[i] * E_i > toler) and (alphas[i] > 0)):
                j = random_select_alpha_j(i, m)              # 隨機選取一個不與i重複j
                E_j = calc_E(alphas, y_label, x_data, b, j)  # 計算樣本j的誤差
                alpha_i_old = alphas[i].copy(); alpha_j_old = alphas[j].copy();
                if (y_label[i] != y_label[j]):               # 重新規範alphas的左右區間
                    L, R = max(0, alphas[j] - alphas[i]), min(C, C + alphas[j] - alphas[i])
                else:
                    L, R = max(0, alphas[j] + alphas[i] - C), min(C, alphas[j] + alphas[i])
                if L == R: print("L==R"); continue          # L==R時選取下一個樣本
                eta = calc_eta(x_data, i, j)                  # 計算eta值
                if eta >= 0: print("eta>=0"); continue
                alphas[j] -= y_label[j] * (E_i - E_j) / eta
                alphas[j] = modify_alpha(alphas[j], L, R)     # 修改alpha[j]
                if (abs(alphas[j] - alpha_j_old) < 0.00001): print("alpha_j更改太小"); continue
                alphas[i] += y_label[j] * y_label[i] * (alpha_j_old - alphas[j])    # 修改alphas[i]
                b1, b2= calc_b(b, x_data, y_label, alphas, alpha_i_old, alpha_j_old, E_i, E_j, i, j)    # 計算b值
                if (0 < alphas[i]) and (C > alphas[i]): b = b1
                elif (0 < alphas[j]) and (C > alphas[j]): b = b2
                else: b = (b1 + b2)/2.0
                alpha_optimization_num += 1
                print("迭代次數:%d,樣本:%d,alphas向量的優化次數:%d" % (iter_num, i+1, alpha_optimization_num))
        if (alpha_optimization_num == 0): iter_num += 1
        else: iter_num = 0
        print("迭代次數:%d" % iter_num)
    return b, alphas

上述SMO核心方法,我們可以通過定義輸入樣本的屬性特徵、標籤以及迭代次數等來得到。隨後,我們可以通過之間的關係來的計算出,關係和相關代碼如下所示:

"""
    Author: Taoye
    微信公衆號: 玩世不恭的Coder
    Explain: 根據公式計算出w權值向量
    Parameters:
        x_data: 樣本屬性特徵矩陣
        y_label: 屬性特徵對應的標籤
        alphas:linear_smo方法所返回的alphas向量
    Return:
        w: 決策面的參數w
"""

def calc_w(x_data, y_label, alphas):
    x_data, y_label, alphas = np.array(x_data), np.array(y_label), np.array(alphas)
    return np.dot((np.tile(y_label.reshape(1-1).T, (12)) * x_data).T, alphas).tolist()

好的,有了,也有了,該有的都有了,接下來就是驗證模型效果了,這裏我們使用Matplotlib來繪製,定義一個plot_result方法來展示模型分類結果:

import pylab as pl

"""
    Author: Taoye
    微信公衆號: 玩世不恭的Coder
    Explain: 繪製出分類結果
    Parameters:
        x_data: 樣本屬性特徵矩陣
        y_label: 屬性特徵對應的標籤
        w:決策面的w參數
        b:決策面的參數b
"""
def plot_result(x_data, y_label, w, b):
    data_number, _ = x_data.shape; middle = int(data_number / 2)
    plt.scatter(x_data[:, 0], x_data[:, 1], c = y_label, cmap = pl.cm.Paired)
    x1, x2 = np.max(x_data), np.min(x_data)
    w1, w2 = w[0][0], w[1][0]
    y1, y2 = (-b - w1 * x1) / w2, (-b - w1 * x2) / w2
    plt.plot([float(x1), float(x2)], [float(y1), float(y2)])    # 繪製決策面
    for index, alpha in enumerate(alphas):
        if alpha > 0:
            b_temp = - w1 * x_data[index][0] - w2 * x_data[index][1]
            y1_temp, y2_temp = (-b_temp - w1 * x1) / w2, (-b_temp - w1 * x2) / w2
            plt.plot([float(x1), float(x2)], [float(y1_temp), float(y2_temp)], "k--")    # 繪製支持向量
            plt.scatter(x_data[index][0], x_data[index][1], s=150, c='none', alpha=0.7, linewidth=2, edgecolor='red')   # 圈出支持向量
    plt.show()

if __name__ == "__main__":
    x_data, y_label = etablish_data(50)
    b, alphas = linear_smo(x_data, y_label, 0.8, 0.0001, 40)
    w = calc_w(x_data, y_label, alphas)
    plot_result(x_data, y_label, w, b)
繪製分類結果
繪製分類結果

完整代碼:

import numpy as np
import pylab as pl
from matplotlib import pyplot as plt

class TearLinearSVM:
    def __init__(self):
        pass

    """
        Author: Taoye
        微信公衆號: 玩世不恭的Coder
        Explain: 用於生成訓練數據集
        Parameters:
            data_number: 樣本數據數目
        Return:
            x_data: 數據樣本的屬性矩陣
            y_label: 樣本屬性所對應的標籤
    """

    def etablish_data(self, data_number):
        x_data = np.concatenate((np.add(np.random.randn(data_number, 2), [33]),       
                                 np.subtract(np.random.randn(data_number, 2), [33])),
                                 axis = 0)      # random隨機生成數據,+ -3達到不同類別數據分隔的目的 
        temp_data = np.zeros([data_number])
        temp_data.fill(-1)
        y_label = np.concatenate((temp_data, np.ones([data_number])), axis = 0)
        return x_data, y_label

    """
        Author: Taoye
        微信公衆號: 玩世不恭的Coder
        Explain: 隨機選取alpha_j
        Parameters:
            alpha_i_index: 第一個alpha的索引
            alpha_number: alpha總數目
        Return:
            alpha_j_index: 第二個alpha的索引
    """

    def random_select_alpha_j(self, alpha_i_index, alpha_number):
        alpha_j_index = alpha_i_index
        while alpha_j_index == alpha_i_index:
            alpha_j_index = np.random.randint(0, alpha_number)
        return alpha_j_index

    """
        Author: Taoye
        微信公衆號: 玩世不恭的Coder
        Explain: 使得alpha_j在[L, R]區間之內
        Parameters:
            alpha_j: 原始alpha_j
            L: 左邊界值
            R: 右邊界值
        Return:
            L,R,alpha_j: 修改之後的alpha_j
    """

    def modify_alpha(self, alpha_j, L, R):
        if alpha_j < L: return L
        if alpha_j > R: return R
        return alpha_j

    """
        Author: Taoye
        微信公衆號: 玩世不恭的Coder
        Explain: 計算誤差並返回
    """

    def calc_E(self, alphas, y_lable, x_data, b, i):
        f_x_i = float(np.dot(np.multiply(alphas, y_lable).T, x_data * x_data[i, :].T)) + b
        return f_x_i - float(y_label[i])

    """
        Author: Taoye
        微信公衆號: 玩世不恭的Coder
        Explain: 計算eta並返回
    """

    def calc_eta(self, x_data, i, j):
        eta = 2.0 * x_data[i, :] * x_data[j, :].T \
                - x_data[i, :] * x_data[i, :].T \
                - x_data[j, :] * x_data[j,:].T
        return eta

    """
        Author: Taoye
        微信公衆號: 玩世不恭的Coder
        Explain: 計算b1, b2並返回
    """

    def calc_b(self, b, x_data, y_label, alphas, alpha_i_old, alpha_j_old, E_i, E_j, i, j):
        b1 = b - E_i \
             - y_label[i] * (alphas[i] - alpha_i_old) * x_data[i, :] * x_data[i, :].T \
             - y_label[j] * (alphas[j] - alpha_j_old) * x_data[i, :] * x_data[j, :].T
        b2 = b - E_j \
             - y_label[i] * (alphas[i] - alpha_i_old) * x_data[i, :] * x_data[j, :].T \
             - y_label[j] * (alphas[j] - alpha_j_old) * x_data[j, :] * x_data[j, :].T
        return b1, b2

    """
        Author: Taoye
        微信公衆號: 玩世不恭的Coder
        Explain: SMO核心算法,求得b和laphas向量
        Parameters:
            x_data: 樣本屬性特徵矩陣
            y_label: 屬性特徵對應的標籤
            C:懲罰參數
            toler:容錯率
            max_iter:迭代次數
        Return:
            b: 決策面的參數b
            alphas:獲取決策面參數w所需要的alphas
    """

    def linear_smo(self, x_data, y_label, C, toler, max_iter):
        x_data = np.mat(x_data); y_label = np.mat(y_label).T     # 將數據轉換爲矩陣類型
        m, n = x_data.shape                                      # 得到數據樣本的shape信息
        b, alphas, iter_num = 0, np.mat(np.zeros((m, 1))), 0     # 初始化參數b和alphas和迭代次數
        while (iter_num < max_iter):                            # 最多迭代max_iter次
            alpha_optimization_num = 0   # 定義優化次數
            for i in range(m):          # 遍歷每個樣本,一次選取一個樣本計算誤差
                E_i = self.calc_E(alphas, y_label, x_data, b, i)      # 樣本i的誤差計算
                if ((y_label[i] * E_i < -toler) and (alphas[i] < C)) or ((y_label[i] * E_i > toler) and (alphas[i] > 0)):
                    j = self.random_select_alpha_j(i, m)              # 隨機選取一個不與i重複j
                    E_j = self.calc_E(alphas, y_label, x_data, b, j)  # 計算樣本j的誤差
                    alpha_i_old = alphas[i].copy(); alpha_j_old = alphas[j].copy();
                    if (y_label[i] != y_label[j]):               # 重新規範alphas的左右區間
                        L, R = max(0, alphas[j] - alphas[i]), min(C, C + alphas[j] - alphas[i])
                    else:
                        L, R = max(0, alphas[j] + alphas[i] - C), min(C, alphas[j] + alphas[i])
                    if L == R: print("L==R"); continue          # L==R時選取下一個樣本
                    eta = self.calc_eta(x_data, i, j)                  # 計算eta值
                    if eta >= 0: print("eta>=0"); continue
                    alphas[j] -= y_label[j] * (E_i - E_j) / eta
                    alphas[j] = self.modify_alpha(alphas[j], L, R)     # 修改alpha[j]
                    if (abs(alphas[j] - alpha_j_old) < 0.00001): print("alpha_j更改太小"); continue
                    alphas[i] += y_label[j] * y_label[i] * (alpha_j_old - alphas[j])    # 修改alphas[i]
                    b1, b2= self.calc_b(b, x_data, y_label, alphas, alpha_i_old, alpha_j_old, E_i, E_j, i, j)    # 計算b值
                    if (0 < alphas[i]) and (C > alphas[i]): b = b1
                    elif (0 < alphas[j]) and (C > alphas[j]): b = b2
                    else: b = (b1 + b2)/2.0
                    alpha_optimization_num += 1
                    print("迭代次數:%d,樣本:%d,alphas向量的優化次數:%d" % (iter_num, i+1, alpha_optimization_num))
            if (alpha_optimization_num == 0): iter_num += 1
            else: iter_num = 0
            print("迭代次數:%d" % iter_num)
        return b, alphas

    """
        Author: Taoye
        微信公衆號: 玩世不恭的Coder
        Explain: 根據公式計算出w權值向量
        Parameters:
            x_data: 樣本屬性特徵矩陣
            y_label: 屬性特徵對應的標籤
            alphas:linear_smo方法所返回的alphas向量
        Return:
            w: 決策面的參數w
    """

    def calc_w(self, x_data, y_label, alphas):
        x_data, y_label, alphas = np.array(x_data), np.array(y_label), np.array(alphas)
        return np.dot((np.tile(y_label.reshape(1-1).T, (12)) * x_data).T, alphas).tolist()

    """
        Author: Taoye
        微信公衆號: 玩世不恭的Coder
        Explain: 繪製出分類結果
        Parameters:
            x_data: 樣本屬性特徵矩陣
            y_label: 屬性特徵對應的標籤
            w:決策面的w參數
            b:決策面的參數b
    """

    def plot_result(self, x_data, y_label, w, b):
        data_number, _ = x_data.shape; middle = int(data_number / 2)
        plt.scatter(x_data[:, 0], x_data[:, 1], c = y_label, cmap = pl.cm.Paired)
        x1, x2 = np.max(x_data), np.min(x_data)
        w1, w2 = w[0][0], w[1][0]
        y1, y2 = (-b - w1 * x1) / w2, (-b - w1 * x2) / w2
        plt.plot([float(x1), float(x2)], [float(y1), float(y2)])    # 繪製決策面

        for index, alpha in enumerate(alphas):
            if alpha > 0:
                b_temp = - w1 * x_data[index][0] - w2 * x_data[index][1]
                y1_temp, y2_temp = (-b_temp - w1 * x1) / w2, (-b_temp - w1 * x2) / w2
                plt.plot([float(x1), float(x2)], [float(y1_temp), float(y2_temp)], "k--")    # 繪製支持向量
                plt.scatter(x_data[index][0], x_data[index][1], s=150, c='none', alpha=0.7, linewidth=2, edgecolor='red')   # 圈出支持向量
        plt.show()

if __name__ == '__main__':
    linear_svm = TearLinearSVM()
    x_data, y_label = linear_svm.etablish_data(50)
    b, alphas = linear_svm.linear_smo(x_data, y_label, 0.80.000140)
    w = linear_svm.calc_w(x_data, y_label, alphas)
    linear_svm.plot_result(x_data, y_label, w, b)

呼呼呼!可算是結束了,做個小總結吧。

SVM是學習機器學習必然接觸到的一個重要算法,所以一定要對其內在原理了解清楚,並不是說一定要手撕SVM的完整代碼,但最起碼使用框架的時候要了解內部都做了什麼“小動作”,不要爲了用而用。

本文介紹了線性SVM的算法原理,主要分爲了五個部分的內容。一、首先通過參考比較權威的書籍以及優秀資料對SVM做了一個比較“良心”的介紹,讓讀者對SVM有一個比較宏觀的概念,這小子(SVM)究竟是誰?竟如此騷氣,讓不少研究者拜倒其石榴裙下。二、其次向讀者介紹了線性SVM以及最大間隔,這部分也是手撕SVM必須要掌握的一些基本概念,並且最終得到了SVM最初的優化問題。三、利用拉格朗日乘數法構建最值問題,將優化問題中的約束問題集成到了目標函數本身,之後利用拉格朗日的對偶性,將最初的優化問題轉化成了內層關於的最小值,外層關於的對偶問題。四、對偶問題的求解,這也是SVM算法的最核心內容,先是對內層關於的函數求導,然後代回原式,從而消掉參數,只留下未知的,隨後利用SMO算法求得迭代更新之後的。五、手撕線性SVM的代碼實現,結果證明,分類的效果還不錯,這是個好傢伙!!!

說實話,這篇文章有點肝,也是挪用了不少其他任務的時間。

這篇文章僅僅只是手撕線性SVM,也就是說大多數據樣本都可以被正確分類,但在實際中,許多的數據集都是線性不可分的,這個時候可能就要引入核函數的概念了。關於非線性SVM,我們留在之後再來肝。。。

本文參考了不少書籍資料以及許多大佬的技術文章,行文風格儘可能做到了通俗易懂,但其中涉及到的數學公式在所難免,還請諸讀者靜下心來慢慢品嚐。由於個人水平有限,才疏學淺,對於SVM也只是略知皮毛,可能文中有不少表述稍有欠妥、有不少錯誤或不當之處,還請諸君批評指正,有任何問題歡迎在下方留言。

我是Taoye,愛專研,愛分享,熱衷於各種技術,學習之餘喜歡下象棋、聽音樂、聊動漫,希望藉此一畝三分地記錄自己的成長過程以及生活點滴,也希望能結實更多志同道合的圈內朋友,更多內容歡迎來訪微信公主號:玩世不恭的Coder

參考資料:

[1] 《機器學習實戰》:Peter Harrington 人民郵電出版社
[2] 《統計學習方法》:李航 第二版 清華大學出版社
[3] 《機器學習》:周志華 清華大學出版社
[4] 《微積分方法》:李億民 中國海洋出版社
[5] Support Vector Machines explained well(翻牆):http://bytesizebio.net/2014/02/05/support-vector-machines-explained-well/
[6] 關於更爲直觀認識SVM的video(翻牆):https://www.youtube.com/watch?v=3liCbRZPrZA
[7] 支持向量機(SVM)是什麼意思?:https://www.zhihu.com/question/21094489/answer/86273196
[8] 看了這篇文章你還不懂SVM你就來打我:https://zhuanlan.zhihu.com/p/49331510
[9] 拉格朗日乘數法:https://www.cnblogs.com/maybe2030/p/4946256.html

推薦閱讀

print( "Hello,NumPy!" )
幹啥啥不行,喫飯第一名
Taoye滲透到一家黑平臺總部,背後的真相細思極恐
《大話數據庫》-SQL語句執行時,底層究竟做了什麼小動作?
那些年,我們玩過的Git,真香
基於Ubuntu+Python+Tensorflow+Jupyter notebook搭建深度學習環境
網絡爬蟲之頁面花式解析
手握手帶你瞭解Docker容器技術
一文詳解Hexo+Github小白建站
​打開ElasticSearch、kibana、logstash的正確方式

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章