斷斷續續看了好多天,趕緊補上坑。
感謝july的 http://blog.csdn.net/v_july_v/article/details/7624837/
以及CSDN上淘的比較正規的SMO C++ 模板代碼。~LINK~
1995年提出的支持向量機(SVM)模型,是淺層學習中較新代表,當然Adaboost更新一點。
按照Andrew NG的說法: "SVM的效果大概相當於調整最好的神經網絡。"於是,SVM被各種神化,被譽為"未來人類的希望,世界人民的終極武器"。
甚至是IEEE選舉的數據挖掘十大算法中唯一一個神經網絡,對,沒錯,SVM就是個MLP。
SVM忽悠大法第一條“我有核函數!":
傻子都知道RBF徑向基核函數在SVM發明的7年前就已經被用於RBF神經網絡,RBF網絡本質就是個把激活函數從Sigmoid替換成RBF的MLP。
SVM處理線性不可分的能力大致來自於兩個部分,非線性映射(重點)、高維空間(核函數)。
SVM的非線性映射藏的比較隱秘,來自其偽·隱層結構:支持向量神經元層。至於核函數?就是個激活函數罷了。
實際上Logistic-Sigmoid函數也是核函數之一,Logistic回歸不能處理線性不可分問題,並不是因為沒有核函數,其實它有。
缺乏非線性映射變換的隱層結構,才是只能畫線,不能畫圓的關鍵,在這點上,不從神經網絡角度理解SVM,是很容易產生誤解的。
RBF較之於Sigmoid,確實有更快的收斂、更精確的逼近,但是,徑向范圍$\sigma$的選取並非易事,過大過小都會影響映射,至於SVM中默認
取定值的做法?其實並不完美。
SVM忽悠大法第二條“我分類效果好":
SVM的最優間隔理論,從數學上來講,very good,其效果也比直接用Gradient Descent擬合好。
當然,這得益於SVM的分類環境假設:一刀切二類分類。但是,更多情況下是K類分類。
SVM確實有多類改進方法,比如基於投票機制的KNN-SVM,但是效果並不好。
相比於傳統神經網絡天然的多分類輸出層,比如Softmax,遜色不少。
尤其在深度學習里,當樣本和訓練時間不作為第一要素、精度由深度網絡保證,SVM的作用就弱化不少。
比如用戶物體識別的ImageNet網絡里,共有1000類需要分類,訓練多個SVM,來達到多分類的目的,效果不會很好。
原因正如Bengio吐槽決策樹那樣,樣本的輸入空間被切分后,全樣本之間的稀疏特征被分離,不利於整體的協同表達。
當然這在淺層學習中,尤其是在數據挖掘這類的統計數據上,問題並不是很大。
SVM的優勢:
SVM本質就是個MLP,但是這個MLP真的很贊,它智能地解決了MLP中隱層神經元個數問題。
自MLP結構發明以來,隱層數、隱層神經元個數一直是最頭疼的地方。
比如,世界上的許多騙錢模擬大腦計划,大喊着要模擬人腦840億個神經元,瘋狂在隱層上做文章。純粹在浪費時間。
SVM直接出來打臉:隱層神經元無須很多,只要覆蓋支持向量即可。
其次,徑向基函數的徑向基中心選取,在二次規划的求解當中也被解決,而無須依靠訓練。
SVM的劣勢:
來自深度學習大師Yoshua Bengio的吐槽:
學習好的表示(representations)是深度學習的核心目的,而非像 SVM 一樣就是在特征的固定集合做一個線性預測。(吐槽 SVM 用 kernel 轉移重點)。
SVM終究是MLP,精確的MLP,但並不是拯救世界的神器,它只不過是淺層學習這個”數值游戲“的大贏家。
尺有所短,寸有所長。對於SVM和傳統神經網絡,我們的態度應該是不吹不黑。知其然,知其所以然。
①最大化幾何間隔。
如果數據能一道線切開的話,我們希望離這道線最近的點的距離越長越好,否則容易分類錯誤,這就是SVM的核心。
定義函數間隔di=y(wxi+b),其中y=1 or -1,問題在於2*di和di是等效的分隔平面,所以棄之。
所以又定義幾何距離D=d/||w||。||w||=根號(w1^2+w2^2+...wn^2)
目標函數max D,約束條件di=y(wxi+b)>=d, 這里的d是取最小的函數間隔,保證離分割平面最近,且最大化距離。
化簡處理:令w’=w/d, b’=b/d, 這樣約束條件等式相當於兩邊被除了d,變成di=y(w’xi+b)>=1,
D也相當於變成1/||w||,為了方便,以后w’全部用w代替。
di=y(wxi+b)>=1, 這是個有趣的式子,由於y=±1,單獨取出(wxi+b),那么±1的就是傳說中里分隔平面最近的點了,稱之為支持向量。
②探究規划問題
目標函數=max D=max 1/||w||,分式好討厭,等效成min (數學原理不知道是啥=。=)
然后引入拉格朗日乘子α,把目標函數、約束條件捆在一起成一個式子。
這樣有新的目標函數:
這個式子很容易被誤解,它的max針對的是
min針對的是,注意看min/max底部,不要很矛盾地理解成對整個式子。
max部分是重點,引入變量α,成為拉格朗日乘子。max這部分的意義在於等效約束條件,
由於y(wxi+b)>=1,如果出現小於1的情況,那么減去的這部分就是一個負數,負負得正,會使前面的min黯然失色。至於大於1的情況,可以令α=0 踢掉它。
最理想的情況下,=min
然而這個式子還是比較麻煩的,min規划是先決條件,然后還要考慮不定的乘子部分。
利用拉格朗日對偶性大法,逆轉下兩個條件,變成對偶規划問題。,要滿足KKT條件,就有d*<=p*。
KKT條件說白了就是0<α<C. (define C 100),0 or C稱之為邊界。
這樣就可以先定住max求min,最后再求max了。
③大道至簡
上面這個目標函數的min(主元是w、b) 部分化簡很神奇,先分別對主元w和b求偏導,令偏導數為0。
第一個偏導結果巧妙地用alpha、y、x替代了惡心的w
第二個偏導結果則是帶來了一個新的約束條件,好處是化簡目標函數時消掉了b,壞處是給計算alpha乘子帶來麻煩。(由此SMO算法誕生了)
將w的代替式拖到里,經過復雜的化簡(詳見July大神博客),b也被新的約束條件(為0)給消去了。
最后=
,很簡單,很優美。
加上max和兩個約束條件之后
max部分將由SMO算法完成,SMO每次抽取確定兩個乘子,然后更新w、b
更新w:。
,(這是單乘子的更新方式,SMO中不能這么干)
更新b:
這個b比較好玩,w確定之后,實際上分隔平面的方向被確定了。
想想一次函數y=wx+b,如果b沒確定,則這個一次函數就可能是兩個邊界間任意一條直線。
那么b應該是什么值呢,應該取兩條邊界的中間那條比較好。(SMO中b也不能這么干)
④分類函數
訓練數據時,需要一個預測分類函數,以便算出誤差E。(這個E在SMO中至關重要)
分類函數自然就是帶入式子y=wx+b,但是這可不是二元一次方程,w是多維的。
在上面中我們有,那么分類函數就變成
。
<xi,x>是內積。這里先不管這個內積怎么算。
⑤SVM的超級武器——核函數
特征空間的映射的引入使得SVM秒掉了各種回歸模型。它的原理就是將低維度數據,映射到高維度數據。
比如原來數據是2維,如果是非線性數據,很難划分隔面。但是給它加到5維,就很容易一刀切出划分面了。
巧妙的映射了維度,這對分類函數中的內積計算產生了影響,為了計算在高維空間的內積,出現了核函數。
新的分類函數為,核函數取代了原本的內積計算。
目前使用的核函數通常是 "高斯核“函數,又名徑向基核函數。
⑥松弛變量的優化
outlier(異常值)是SVM需要解決的一個問題。
如果就存在一個異常值,離分隔平面超級超級超級近,設這么一個垃圾值為支持向量顯然不是明智選擇。(間隔越小,平面越難被划分,分類效果就越差)
所以引入松弛變量,使得原目標函數變為
切入約束條件后,拉格朗日函數變成:
然后按照⑤中方法化簡,多了一個新主元,並求導令倒數為0。
你不必關系ri是什么,只要知道ri>=0即可,移個項,αi=C+ri。實際意義就是αi多了一個上界C。
同⑤中那樣化簡拉格朗日函數,最后驚奇發現,松弛變量被消了,和⑤最后的式子一樣。
其實松弛變量引入的唯一變化,就是多了個KKT條件多了個上界C。(C的取值很難說,模板代碼里取的是100)
)新的KKT條件如下:
α=0,表示非支持向量點。
0<α<C,表示支持向量點。
α=C,表示的是兩條支持向量間的outlier(這些點通常被無視掉)
⑥SMO算法
1998年4月,位於總部的微軟研究院副主任John Platt發明了這個奇怪的SMO算法,從此SVM超神了。
John Platt那篇論文省去了公式推導,所以大部分情況都是,直接給你個XX情況的公式是XX,以及給出的很殘破的C偽代碼
對初學者很不友好,鏈接 ~Link~,想要完全解釋清楚還是比較難的。
感謝CSDN某網友提供的完整的SMO的C++代碼,里面的變量名幾乎與John Platt論文中的C偽代碼一致。
Part I SMO的公式推導
①新的分類函數與新的目標函數
SMO定義了新的分類函數u,其實也不算新,就是把b的符號變了。
這個式子就是把y=wx-b,w替換了而已,上面已經出現了類似物了。
也定義的新的目標函數
減號兩邊換一下,從max變成了min,意義不明。
②兩個乘子問題
還記得這玩意不,偏導出來的坑爹約束條件。
假設我們每次抽取一個乘子,設為α1,那么改了α1之后,能保證上面那個約束條件成立么?很可惜,不好辦。
所以John Platt提出了SMO(最小序列算法),他認為至少(最小)要同時改變兩個乘子。
分為主動乘子(α2)和被動乘子(α1)。每次主動算α2,然后根據約束條件,推出α1的值,這樣α1光榮地做了嫁衣。
一個有趣的想法,比如這次搞定了α2,到了下一次,上次的α2又變成了α1,被動算出來之后,會不會導致上次的α2過程白算了?然后惡性循環?
其實想多了,實際測試,每次抽兩個乘子,只會使結果變好,而不會卡死在循環里。
於是算α2成為頭等大事。
先確定α2的范圍,已有約束條件:0<=α1<=C, 0<=α2<=C (稱為矩形條件)
y1α1+y2α2=γ,γ=Σ(yi*αi),i=3,4....,m(那個頭疼的約束條件,提取出兩項)
由於yi=±1, 這里分兩種情況。(γ不知道正負)
情況1: y1=y2
y1α1+y2α2=γ => α1+α2=±γ(兩條直線)
情況2:y1≠y2,即異號。
y1α1+y2α2=γ => α1-α2=±γ(兩條直線)
畫個圖
這樣當y1=y2,α2有下界L=max(0,γ-C),上界H=min(γ,C)
y1≠y2,α2有下界L=max(0,-γ),上界H=min(C-γ,C)
注意這里的α2稱之為α2(new),原來的α2稱為α2(old),同時下面所有打(*)號的變量都是old變量。
α2的具體值怎么確定呢?有如下推導。
首先,抽取兩個乘子后,目標函數被剖分成這樣
就是把i、j∈[1,2]的部分提出來了,但是至今不理解vi部分是怎么出來的。
對於條件,令兩個式子各乘y1,有
其中,化簡目標函數:
對α2求導並令導數為0:
化簡:
化簡:
令ui-yi=Ei,繼續化簡,有:
,
裁剪α2(new):
算出α1(new)
特別的,當η<0時,違反了Mercer條件,目標函數為可能∞,這時候α2不能使用上述方式計算,論文中這么寫
看不懂,填坑,先按模板代碼來。
③啟發式抽取乘子。
盡管α1是被動乘子,我們還是得先抽取α1,然后啟發式抽取α2。
啟發式(經驗式)主要是根據已有經驗來安排三種抽取乘子的順序。
最壞情況其實就是O(m)掃一遍全部數據找到一個α2,當然啟發式原則保證我們的RP不會那么差。
一、非邊界(0 or C)最大化|E1-E2|規則:選擇絕對值最大的先計算。
二、非邊界隨機規則:非邊界alpha里隨機選擇α2
三、含邊界隨機規則:實在沒有辦法了,算上邊界再隨機吧。
④收斂條件
由於每次計算α2時,都是由導數決定的,也就是說,每次新的α2產生,都標志着目標函數的優化。
一旦全部α2收斂(即全部乘子收斂,α1若不收斂,下次可能作為α2被調整),則標志着目標函數優化完畢。
SMO主循環遍歷全部數據點選擇α1(mainProcess函數)
次循環隨機/最大化|E1-E2|規則選擇起點開始遍歷選擇α2 (exampleExamine函數)
相當於兩層for循環驗證全部乘子對,保證最后的目標函數是收斂的。
⑤雙乘子下的幾個參數求法
一、α1
α1先通過公式,即 a1 = alph1 - s * (a2 - alph2)
若違反KKT條件,則設為邊界,並按照約束條件對α2進行增/減。
二、b
John Platt不知道從哪搞來的公式。目前不知道推導過程。
雙乘子情況下求b,論文中這么解釋:
①α1非邊界:則b=
②α2非邊界,則b=
③α1、α2均在邊界,則取b1、b2均值
④α1、α2均非邊界,則b1=b2,所以①、②任取一個就行了。
三、w
Part Ⅱ SMO的C++代碼研究 (坑ing)
一、SMO主過程。

void SMO::smo_main_process() { read_in_data(); //initialize if(!is_test_only) { alph.resize(end_support_i,0); b=0; error_cache.resize(N); } self_dot_product.resize(N); precomputed_self_dot_product(); if (!is_test_only) { numChanged = 0; examineAll = 1; while (numChanged > 0 || examineAll) { numChanged = 0; if (examineAll) { for (int k = 0; k < N; k++) numChanged += examineExample (k); } else { for (int k = 0; k < N; k++) if (alph[k] != 0 && alph[k] != C) numChanged += examineExample (k); } if (examineAll == 1) examineAll = 0; else if (numChanged == 0) examineAll = 1; } } }
這個過程由兩個Bool值控制,numChanged 、 examineAll.
首先第一遍檢查全部乘子,之后檢查非邊界乘子(邊界乘子的值通常不會改變了),查不到了再重新檢查全部,如果全部還查不到,則結束,計算完畢。
檢查並修改乘子是一個智能的過程,綜合使用三種手段。

int SMO::examineExample(int i1) { float y1, alph1, E1, r1; y1 = (float)target[i1]; alph1 = alph[i1]; if (alph1 > 0 && alph1 < C) E1 = error_cache[i1]; else E1 = learned_func_nonlinear(i1) - y1; r1 = y1 * E1; if ((r1 < -tolerance && alph1 < C)||(r1 > tolerance && alph1 > 0)) { /////////////使用三種方法選擇第二個乘子 //1:在non-bound乘子中尋找maximum fabs(E1-E2)的樣本 //2:如果上面沒取得進展,那么從隨機位置查找non-boundary 樣本 //3:如果上面也失敗,則從隨機位置查找整個樣本,改為bound樣本 if (examineFirstChoice(i1,E1)) return 1; //1 if (examineNonBound(i1)) return 1; //2 if (examineBound(i1)) return 1; //3 } ///沒有進展 return 0; }
alpha1直接獲取,由於每個乘子對應一條訓練數據,所以該條數據誤差E1=預測值-真實值,預測值就是把該條數據帶入學習方程(分類函數里)去

//徑向基核函數 float SMO::kernel_func(int i,int k) { float sum=dot_product_func(i,k); sum*=-2; sum+=self_dot_product[i]+self_dot_product[k]; return exp(-sum/two_sigma_squared); } float SMO::learned_func_nonlinear(int k) { float sum=0; for(int i=0; i<end_support_i; i++) { if(alph[i]>0) sum+=alph[i]*target[i]*kernel_func(i,k); } sum-=b; return sum; }