支持向量機:西瓜書課后題6.2實戰(zhàn)與核函數(shù)調(diào)參)
剛把西瓜書第六章啃到支持向量機這一章時我一度懷疑自己是不是數(shù)學沒入門。對偶問題、KKT條件、核函數(shù)這些概念書上推導一行接一行但心里始終缺一個“哦原來跑起來是這樣”的畫面。后來強迫自己放下課本先把課后題6.2用LIBSVM跑通一遍再回頭看公式很多含糊的地方就突然通了。這篇筆記就是基于我自己實操后的復盤用Python調(diào)用LIBSVM實現(xiàn)SVM從數(shù)據(jù)準備、訓練、參數(shù)設置到結(jié)果解讀完整走一遍課后題6.2順便把踩過的坑和調(diào)參經(jīng)驗一起寫下來。如果你也正在看西瓜書第六章或者剛接觸SVM、想把第一個SVM實驗跑起來這篇應該能幫你少走不少彎路。1. 課后題6.2到底讓做什么1.1 先別急著寫代碼把題目要求拆開看課后題6.2的原題核心意思是讓你用LIBSVM這個現(xiàn)成工具在西瓜數(shù)據(jù)集3.0上訓練SVM模型然后比較不同核函數(shù)下的表現(xiàn)。這里有個容易被忽略的點題目沒有讓你從零實現(xiàn)SMO算法而是希望你用成熟庫去獲得感性認識。這樣設計是有道理的因為在第六章前面已經(jīng)詳細推導了SVM的數(shù)學原理后面還會有手動實現(xiàn)的題所以這一題的意義在于“先把工具用熟把結(jié)果看懂”。西瓜數(shù)據(jù)集3.0是書里自帶的一個小型二分類數(shù)據(jù)集特征主要是“密度”和“含糖率”標簽是“好瓜”和“壞瓜”。為什么用這個數(shù)據(jù)集因為它只有兩個特征方便可視化也方便觀察支持向量在二維平面上的分布。數(shù)據(jù)規(guī)模不大訓練極快很適合新手反復調(diào)整參數(shù)看效果。1.2 為什么偏偏是LIBSVM而不是自己寫個梯度下降LIBSVM是支持向量機領(lǐng)域最常用的開源庫之一底層用C實現(xiàn)接口簡單輸出信息詳細。它支持線性核、多項式核、RBF核、sigmoid核也支持分類、回歸、交叉驗證等功能。對新手來說最直觀的價值在于訓練完成時它會在終端打印一串診斷信息比如迭代次數(shù)、目標函數(shù)值、支持向量個數(shù)等。這些信息恰好能幫助你對應書上那些抽象概念。熟悉Python的同學可能會問scikit-learn里的SVC不也能用嗎確實能而且sklearn的SVC底層也包含LIBSVM的實現(xiàn)接口更Pythonic。但既然題目點名LIBSVM我建議還是老老實實體驗一下原始庫的輸出格式等你理解了那些指標再用sklearn會更有底氣。這篇筆記里我會兩種方式都提到讓你可以按自己的環(huán)境選擇。1.3 一句話總結(jié)課后題6.2的核心目標題目的訓練過程可以拆成四步準備符合LIBSVM格式的數(shù)據(jù)選擇合適的核函數(shù)和參數(shù)訓練模型讀取并理解訓練日志對比不同核函數(shù)下的支持向量數(shù)量、準確率等指標。做完之后再想想為什么不同核函數(shù)的結(jié)果會不一樣為什么有的模型復雜但準確率并沒那么高這些問題想通了才算真正吃透這一題。2. 環(huán)境準備與LIBSVM的兩種打開方式2.1 小白首選直接用scikit-learn的SVC接口如果你只是想先把實驗跑通最省事的方法是用sklearn。安裝很簡單pip install scikit-learn然后在Python里導入from sklearn import svm clf svm.SVC(kernelrbf, C1.0, gammascale) clf.fit(X_train, y_train)為什么建議這個作為第一種方式因為sklearn的SVC接口把LIBSVM的細節(jié)封裝得很干凈fit之后直接拿模型去預測代碼量最少。但它不會在終端打印LIBSVM那種詳細日志所以很多人跑完sklearn之后對“支持向量個數(shù)”沒有直觀概念。2.2 想看到原始訓練日志就裝原版LIBSVM的Python接口原版LIBSVM在官網(wǎng)下載后里面有一個python目錄里面有svmutil.py和svm.py兩個關(guān)鍵文件。你只需要把python目錄放到你的項目里然后import即可import sys sys.path.append(libsvm/python) # 換成你的實際路徑 from svmutil import *如果你不確定路徑也可以在命令行里進入libsvm/python目錄直接運行Python。這種方式相比sklearn麻煩一點但優(yōu)點是訓練時能看到如下日志optimization finished, #iter 45 nu 0.215038 obj -1.234567, rho -0.345678 nSV 12, nBSV 3 Total nSV 12這一串信息對理解SVM原理非常有幫助后文我會逐行解釋。2.3 準備數(shù)據(jù)把西瓜數(shù)據(jù)集變成LIBSVM格式LIBSVM的數(shù)據(jù)格式是這樣的label index1:value1 index2:value2 ...每行是一個樣本開頭是標簽后面是特征對。比如西瓜數(shù)據(jù)集3.0里的一個樣本可能是1 1:0.697 2:0.460意思就是標簽為1好瓜第1個特征密度為0.697第2個特征含糖率為0.460。如果標簽是壞瓜可以寫成-1。一般來說我們會把原始CSV轉(zhuǎn)成這種格式。假設你手上有一個watermelon.csv列名是density, sugar, labellabel為1或0那么轉(zhuǎn)換代碼可以這樣寫def csv_to_libsvm(input_file, output_file): with open(input_file, r, encodingutf-8) as fin, \ open(output_file, w, encodingutf-8) as fout: for line in fin: parts line.strip().split(,) label 1 if parts[2] 1 else -1 density float(parts[0]) sugar float(parts[1]) fout.write(f{label} 1:{density} 2:{sugar}\n) csv_to_libsvm(watermelon.csv, watermelon.libsvm)實際操作中你還需要切分訓練集和測試集。最簡單的方法是把數(shù)據(jù)按比例隨機打亂后分成兩部分這個后面在代碼里會演示。如果你實在找不到西瓜數(shù)據(jù)集也可以用LIBSVM自帶的heart_scale文件來練手它是一個標準的心跳疾病數(shù)據(jù)集格式已經(jīng)符合要求直接調(diào)用svm_read_problem(heart_scale)即可。3. 手把手訓練SVM線性核、多項式核、RBF核3.1 訓練一個模型的核心代碼用原版LIBSVM的Python接口核心代碼非常短from svmutil import * y, x svm_read_problem(watermelon.libsvm) # 假設前70%做訓練集后30%做測試集 train_count int(len(y) * 0.7) y_train, x_train y[:train_count], x[:train_count] y_test, x_test y[train_count:], x[train_count:] param svm_parameter(-t 2 -c 1.0 -g 0.5) model svm_train(y_train, x_train, param) p_label, p_acc, p_val svm_predict(y_test, x_test, model)這里最關(guān)鍵的是svm_parameter里的參數(shù)。-t 2表示用RBF核-c 1.0是懲罰系數(shù)C-g 0.5是gamma。如果你想用線性核改成-t 0用多項式核改成-t 1同時還要指定-d次數(shù)和-rcoef0。3.2 svm_parameter里的每個參數(shù)到底是什么很多小白直接抄參數(shù)卻完全不懂含義這樣一旦結(jié)果不對就兩眼一抹黑。我把常用參數(shù)整理成一張表參數(shù)命令行寫法含義典型值核函數(shù)類型-t0線性、1多項式、2RBF、3sigmoid2懲罰系數(shù)C-c對誤分類樣本的容忍度越大越嚴格1.0gamma-g核函數(shù)的寬度影響模型復雜度1/特征維度多項式核次數(shù)-d僅在多項式核時有效3coef0-r多項式/sigmoid核的常數(shù)項0是否輸出概率-b1表示輸出概率0不輸出0C是SVM里最核心的懲罰項。你可以把它理解成“班主任查紀律的嚴格程度”C越大模型就越不愿意放過任何一個訓練集里的錯誤點結(jié)果可能是訓練集準確率很高但測試集反而變差也就是過擬合。C太小模型又會太“佛系”連明顯的分類錯誤都容忍導致欠擬合。所以C不是越大越好要和數(shù)據(jù)情況匹配。gamma只對RBF核有決定性影響。它定義了單個樣本的影響半徑gamma越大決策邊界越曲折模型越復雜gamma越小邊界越平滑。很多人上來就調(diào)C卻忽略gamma結(jié)果折騰半天還不如先固定C從不同量級的gamma里找找感覺。3.3 三種核函數(shù)放到循環(huán)里對比既然課后題6.2要求比較不同核函數(shù)最直接的辦法是寫一個循環(huán)把線性核、多項式核、RBF核都訓練一遍然后打印各自的準確率和支持向量數(shù)量kernels [ (Linear, -t 0 -c 1.0), (Poly, -t 1 -d 3 -r 0 -c 1.0), (RBF, -t 2 -c 1.0 -g 0.5), ] for name, opt in kernels: param svm_parameter(opt) model svm_train(y_train, x_train, param) p_label, p_acc, p_val svm_predict(y_test, x_test, model) nsv model.get_nr_sv() print(f{name}: accuracy{p_acc[0]}%, nSV{nsv})這樣跑下來你會發(fā)現(xiàn)線性核在這個二維小數(shù)據(jù)集上可能就已經(jīng)不錯了RBF核則有可能取得更高的測試準確率但支持向量數(shù)量也會變多。這正好呼應了書上“核函數(shù)的選擇會影響模型復雜度”這句話。3.4 為什么訓練前一定要做標準化SVM相關(guān)算法對特征的尺度非常敏感。因為核函數(shù)內(nèi)部要算樣本之間的距離或內(nèi)積如果一個特征值范圍是0到1另一個是100到1000那么大尺度的特征就會主導距離計算另一個特征幾乎白費。西瓜數(shù)據(jù)集的密度和含糖率都在0到1附近量級差異不大所以很多教程里不標準化也能跑出不錯結(jié)果。但真實項目中特征量級差異往往非常大。標準化最常用的是Z-score歸一化讓每個特征均值為0、標準差為1。在sklearn里是這樣寫的from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test)注意fit_transform要用在訓練集上transform用在測試集上千萬不要對整個數(shù)據(jù)集直接fit_transform否則會把測試集的信息泄露到訓練過程中導致結(jié)果虛高。這個錯誤我在第一次做實驗時就犯過后面你會看到后果。4. 結(jié)果怎么看支持向量、準確率、決策邊界4.1 訓練日志里的每一行都是什么意思用原版LIBSVM運行后終端會輸出一段很“唬人”的日志。我拿一次實際訓練的輸出舉例optimization finished, #iter 45 nu 0.215038 obj -1.234567, rho -0.345678 nSV 12, nBSV 3 Total nSV 12逐行拆解optimization finished, #iter 45SMO算法迭代了45次后收斂。迭代次數(shù)多不一定是壞事但有可能是C或者gamma設置得太極端。nu和C相關(guān)的一個參數(shù)用于控制支持向量的比例。數(shù)值越小說明模型越“簡單”。obj目標函數(shù)的最優(yōu)值。對SVM來說這是在約束條件下求解出來的對偶目標函數(shù)值可以粗略理解為模型訓練結(jié)束時的損失。rho決策邊界的偏置項也就是超平面方程里的b。nSV 12支持向量的個數(shù)。這是理解SVM的關(guān)鍵指標。訓練完成后模型只保留這些支持向量用于預測其他樣本都可以丟棄。nBSV 3邊界支持向量的個數(shù)。邊界支持向量通常位于間隔內(nèi)部或者分類錯誤的一側(cè)它們的數(shù)量偏多往往意味著數(shù)據(jù)噪聲較大或者C設置過大。對新手來說最需要盯住的是nSV。如果一個模型訓練準確率很高但nSV占了訓練集的一大半那這個模型基本處于過擬合狀態(tài)泛化能力堪憂。相反如果nSV非常少說明模型只靠很少幾個關(guān)鍵點就能劃分數(shù)據(jù)通常泛化比較好。4.2 用準確率判斷但別只看準確率svm_predict運行結(jié)束后會返回三個結(jié)果預測標簽p_label、準確率p_acc、決策值或概率p_val。其中p_acc本身是一個數(shù)組格式如下Accuracy 92.8571% (13/14) (classification) Mean squared error 0.0714 (regression) Squared correlation coefficient 0.9231 (regression)分類任務里我們最關(guān)心Accuracy 92.8571%但只看這個數(shù)字是有風險的。比如西瓜數(shù)據(jù)集里好瓜壞瓜比例如果接近1:1準確率還有參考價值一旦出現(xiàn)七八成都是好瓜模型就算把所有樣本都預測成好瓜準確率也會高達七八成這顯然不是我們想要的結(jié)果。所以我會習慣性打印混淆矩陣。用sklearn可以一行搞定from sklearn.metrics import confusion_matrix cm confusion_matrix(y_test, p_label) print(cm)混淆矩陣的四格分別代表真正例、假正例、真負例、假負例。看完之后你才能判斷模型是把好瓜全認出來了但誤傷了很多壞瓜還是反過來。這個信息在調(diào)參時特別有用。4.3 把決策邊界畫出來一切變得直觀西瓜數(shù)據(jù)集只有兩個特征這給了我們一個絕佳的可視化機會。在二維平面上畫網(wǎng)格點讓訓練好的模型逐點預測再按預測類別給網(wǎng)格上色就能看到?jīng)Q策邊界長什么樣子。代碼如下import numpy as np import matplotlib.pyplot as plt def plot_decision_boundary(model, X_train, y_train): h 0.01 x_min, x_max X_train[:, 0].min() - 0.1, X_train[:, 0].max() 0.1 y_min, y_max X_train[:, 1].min() - 0.1, X_train[:, 1].max() 0.1 xx, yy np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h)) # 需要把網(wǎng)格坐標轉(zhuǎn)成LIBSVM格式 grid_samples [] for px, py in zip(xx.ravel(), yy.ravel()): grid_samples.append({1: px, 2: py}) p_label, _, _ svm_predict([0]*len(grid_samples), grid_samples, model) p_label np.array(p_label).reshape(xx.shape) plt.contourf(xx, yy, p_label, alpha0.6, cmapplt.cm.coolwarm) plt.scatter(X_train[:, 0], X_train[:, 1], cy_train, edgecolorsk, cmapplt.cm.coolwarm) plt.xlabel(density) plt.ylabel(sugar) plt.show()注意這里svm_predict需要為每個網(wǎng)格點傳一個標簽但因為只是預測標簽值填什么都不會影響結(jié)果。畫出來之后你能清晰看到線性核的邊界是直線RBF核的邊界則是彎曲的支持向量往往就貼在這些邊界附近。看到這個圖再回頭看書上的KKT條件你會對“支持向量是距離超平面最近的點”這句話有完全不同的感受。5. 參數(shù)調(diào)優(yōu)的實戰(zhàn)心得5.1 核函數(shù)選型不要迷信RBF很多教程一上來就說RBF核最萬能直接用它。但對小數(shù)據(jù)集、特征維度不高的時候線性核往往已經(jīng)夠用而且泛化更穩(wěn)。核函數(shù)選型可以簡單按下面的經(jīng)驗判斷特征維度非常高比如文本分類的幾萬維優(yōu)先線性核因為高維空間里線性超平面通常已經(jīng)足夠。特征維度低樣本量適中且數(shù)據(jù)分布有明顯非線性時優(yōu)先RBF。數(shù)據(jù)中有一定的特征交叉關(guān)系多項式核可能更直觀但次數(shù)太高容易過擬合。需要模型可解釋、能直接提取權(quán)重時線性核唯一可選。課后題6.2里三種核都試一遍是必要的但試完以后不要只選測試集準確率最高的那個還要結(jié)合模型復雜度一起考慮。5.2 C和gamma怎么配合在RBF核下C和gamma是兩個需要聯(lián)合調(diào)優(yōu)的參數(shù)。最古典的方法是網(wǎng)格搜索C取2^-5到2^15gamma取2^-15到2^3每一個組合都做交叉驗證。LIBSVM自帶交叉驗證功能參數(shù)里加-v 5表示做5折交叉驗證param svm_parameter(-t 2 -c 2 -g 0.5 -v 5) svm_train(y_train, x_train, param)注意加了-v之后svm_train返回的是交叉驗證準確率而不是一個模型對象。這是新手最容易看代碼跑完卻拿不到模型來預測的原因。之前我自己做小實驗時粗略網(wǎng)格搜索的腳本大概是這樣的best_acc 0 best_param None for c in [0.1, 1, 10, 100]: for g in [0.01, 0.1, 1, 10]: param svm_parameter(f-t 2 -c {c} -g {g} -v 5) acc svm_train(y_train, x_train, param) if acc best_acc: best_acc acc best_param f-t 2 -c {c} -g {g}最后用best_param重新訓練模型并測試。這樣雖然笨但在小數(shù)據(jù)上很快也能讓你直觀看到C和gamma對準確率的影響。5.3 標準化和交叉驗證一起做才靠譜我自己的經(jīng)驗是先標準化再調(diào)參否則網(wǎng)格搜索選出來的參數(shù)會被特征的量級干擾。標準化以后RBF核的gamma才有一個穩(wěn)定的參考范圍。如果數(shù)據(jù)不標準化同樣的C和gamma在不同特征量級下表現(xiàn)會千差萬別你甚至會得到一個看起來“準確率接近100%”的結(jié)果但那只是被大尺度特征帶偏的假象。交叉驗證也千萬別省。尤其在數(shù)據(jù)量少的時候只用一次隨機劃分的測試集來判斷參數(shù)好壞偶然性很大。用5折或10折交叉驗證選出參數(shù)后再用獨立的測試集做最終評估這個流程會讓你的結(jié)果可信得多。6. 踩坑記錄與結(jié)果解讀要點6.1 數(shù)據(jù)標準化前后結(jié)果差很多我第一次直接在原始西瓜數(shù)據(jù)上跑RBF核準確率大概在85%左右。后來順手做了一次標準化同樣的參數(shù)組合直接跳到95%以上。原因就是密度和含糖率雖然量級都小于1但分布范圍不完全一致RBF核本身對特征尺度很敏感。這個坑在課后題里不一定會暴露因為數(shù)據(jù)集太干凈但在任何真實項目里標準化應該成為SVM流程的默認動作。記住對SVM來說標準化不是可選項而是必選項。6.2 類別不平衡會讓準確率失真西瓜數(shù)據(jù)集做成了類別基本均衡的樣子但真實數(shù)據(jù)經(jīng)常不是。一個很典型的現(xiàn)象就是訓練集里好瓜90個、壞瓜10個模型為了降低整體損失會把所有樣本都預測成好瓜最終準確率90%聽起來很漂亮但壞瓜一個都沒找出來。這時候準確率就是騙人的指標。解決辦法有三個一是對少數(shù)類樣本提高懲罰權(quán)重在原版LIBSVM里可以用-w1和-w-1分別設置正類和負類的C權(quán)重二是對少數(shù)類進行過采樣三是改用F1分數(shù)或召回率來評估不要只看準確率。實踐中最立竿見影的是第一種方法。6.3 LIBSVM輸出概率的坑如果你想用SVM輸出每個樣本屬于正類的概率而不是只給分類標簽需要在訓練參數(shù)里加上-b 1param svm_parameter(-t 2 -c 1.0 -g 0.5 -b 1) model svm_train(y_train, x_train, param) p_label, p_acc, p_val svm_predict(y_test, x_test, model)加了-b 1之后p_val才是概率值。如果不加p_val返回的是決策值而且很多情況下是空的。這個坑很隱蔽因為你不看文檔的話很難意識到p_val為什么空了。還有一點要注意加了-b 1之后訓練會變慢因為需要額外做Platt縮放來擬合概率。在樣本量大的時候先評估是否需要概率輸出再決定加不加-b。6.4 一個容易被忽略的點隨機劃分會帶來方差如果你每次跑代碼的結(jié)果都不一樣先檢查一下訓練集和測試集劃分的時候有沒有設置隨機種子。在劃分前加上random_state或seed可以保證結(jié)果可復現(xiàn)。當然即使設置隨機種子網(wǎng)格搜索得到的“最優(yōu)參數(shù)”也只代表在這一份數(shù)據(jù)劃分下的最優(yōu)換一份數(shù)據(jù)劃分結(jié)果可能不同。所以我在做對比實驗時會用cross_val_score多做幾次交叉驗證并看平均分數(shù)而不是只看一次測試集結(jié)果。這組實驗做完我最大的感受是SVM的數(shù)學推導雖然難啃但它落地后的運行邏輯其實非常簡潔。模型訓練完只保留支持向量預測過程就是計算新樣本和支持向量之間的關(guān)系核函數(shù)然后加權(quán)投票。有了LIBSVM輸出日志里那一串數(shù)字再回去翻書上的KKT條件和拉格朗日對偶很多符號就不是“空中樓閣”了。如果你剛做完6.2建議再往前走一步嘗試用Python手寫一個簡單的SMO實現(xiàn)不需要高性能只要能在同樣的西瓜數(shù)據(jù)上跑出類似的支持向量這個過程會對SVM的理解有一個質(zhì)的提升。我已經(jīng)開始動手了等做完再寫一篇筆記分享出來。