1. 程式人生 > >《Hands-On Machine Learning with Scikit-Learn & TensorFlow》讀書筆記 第五章 支援向量機

《Hands-On Machine Learning with Scikit-Learn & TensorFlow》讀書筆記 第五章 支援向量機

第5章 支援向量機

支援向量機(SVM)是個非常強大並且有多種功能的機器學習模型,能夠做線性或者非線性的分類,迴歸,甚至異常值檢測。機器學習領域中最為流行的模型之一,是任何學習機器學習的人必備的工具。SVM 特別適合應用於複雜但中小規模資料集的分類問題。

線性支援向量機分類

SVM 的基本思想能夠用一些圖片來解釋得很好。左邊的圖顯示了三種可能的線性分類器的判定邊界。其中用虛線表示的線性模型判定邊界很差,甚至不能正確地劃分類別。另外兩個線性模型在這個資料集表現的很好,但是它們的判定邊界很靠近樣本點,在新的資料上可能不會表現的很好。相比之下,右邊圖中 SVM 分類器的判定邊界實線,不僅分開了兩種類別,而且還儘可能地遠離了最靠近的訓練資料點。可以認為 SVM 分類器在兩種類別之間保持了一條儘可能寬敞的街道(圖中平行的虛線),其被稱為最大間隔分類。

這裡寫圖片描述

我們注意到新增更多的樣本點在“街道”外並不會影響到判定邊界,因為判定邊界是由位於“街道”邊緣的樣本點確定的,這些樣本點被稱為“支援向量”(圖中被圓圈圈起來的點)

這裡寫圖片描述

軟間隔分類

如果我們嚴格地規定所有的資料都不在“街道”上,都在正確地兩邊,稱為硬間隔分類,硬間隔分類有兩個問題,第一,只對線性可分的資料起作用,第二,對異常點敏感。下圖顯示了只有一個異常點的鳶尾花資料集:左邊的圖中很難找到硬間隔,右邊的圖中判定邊界和我們之前在圖 5-1 中沒有異常點的判定邊界非常不一樣,它很難一般化。

這裡寫圖片描述

為了避免上述的問題,我們更傾向於使用更加軟性的模型。目的在保持“街道”儘可能大和避免間隔違規(例如:資料點出現在“街道”中央或者甚至在錯誤的一邊)之間找到一個良好的平衡。這就是軟間隔分類。

在 Scikit-Learn 庫的 SVM 類,可以用C超引數(懲罰係數)來控制這種平衡:較小的C會導致更寬的“街道”,但更多的間隔違規。圖 5-4 顯示了在非線性可分隔的資料集上,兩個軟間隔SVM分類器的判定邊界。左邊圖中,使用了較大的C值,導致更少的間隔違規,但是間隔較小。右邊的圖,使用了較小的C值,間隔變大了,但是許多資料點出現在了“街道”上。然而,第二個分類器似乎泛化地更好:事實上,在這個訓練資料集上減少了預測錯誤,因為實際上大部分的間隔違規點出現在了判定邊界正確的一側。

這裡寫圖片描述

  • 如果 SVM 模型過擬合,可以嘗試通過減小超引數C去調整。

以下的 Scikit-Learn 程式碼載入了內建的鳶尾花(Iris)資料集,縮放特徵,並訓練一個線性 SVM 模型(使用LinearSVC類,超引數C=1,hinge 損失函式)來檢測 Virginica 鳶尾花。

import numpy as np 
from sklearn import datasets 
from sklearn.pipeline import Pipeline 
from sklearn.preprocessing import StandardScaler 
from sklearn.svm import LinearSVC

iris = datasets.load_iris() 
X = iris["data"][:, (2, 3)] # petal length, petal width 
y = (iris["target"] == 2).astype(np.float64) # Iris-Virginica

svm_clf = Pipeline([
    ("scaler", StandardScaler()), 
    ("linear_svc", LinearSVC(C=1, loss="hinge")), 
    ])

svm_clf.fit(X, y)
>>>svm_clf.predict([[5.5, 1.7]])
array([ 1.])

鉸鏈損失(Hinge Loss):主要用於支援向量機(SVM) 中; Hinge loss 的叫法來源於其損失函式的圖形,為一個折線,通用的函式表示式為:

L(mi)=max(0,1mi(w))

表示如果被正確分類,損失是0,否則損失就是 1mi(w)。SVM 的損失函式可以看作是 L2-norm 和 Hinge loss 之和。

這裡寫圖片描述

作為一種選擇,可以在 SVC 類,使用SVC(kernel=”linear”, C=1),但是它比較慢,尤其在較大的訓練集上,所以一般不被推薦。另一個選擇是使用SGDClassifier類,即SGDClassifier(loss=”hinge”, alpha=1/(m*C))。它應用了隨機梯度下降來訓練一個線性 SVM 分類器。儘管它不會和LinearSVC一樣快速收斂,但是對於處理那些不適合放在記憶體的大資料集是非常有用的,或者處理線上分類任務同樣有用。

LinearSVC要使偏置項規範化,首先應該集中訓練集減去它的平均數。如果使用了StandardScaler,那麼它會自動處理。此外,確保設定loss引數為hinge,因為它不是預設值。最後,為了得到更好的效果,需要將dual引數設定為False,除非特徵數比樣本量多.

求解原問題(primal problem)和對偶問題(dual problem)。求解原問題使用的是TRON的優化演算法,對偶問題使用的是Coordinate Descent優化演算法。總的來說,兩個演算法的優化效率都較高,但還是有各自更加擅長的場景。對於樣本量不大,但是維度特別高的場景,如文字分類,更適合對偶問題求解,因為由於樣本量小,計算出來的Kernel Matrix也不大,後面的優化也比較方便。而如果求解原問題,則求導的過程中要頻繁對高維的特徵矩陣進行計算,如果特徵比較稀疏的話,那麼就會多做很多無意義的計算,影響優化的效率。相反,當樣本數非常多,而特徵維度不高時,如果採用求解對偶問題,則由於Kernel Matrix過大,求解並不方便。反倒是求解原問題更加容易。

非線性支援向量機分類

儘管線性 SVM 分類器在許多案例上表現得出乎意料的好,但是很多資料集並不是線性可分的。一種處理非線性資料集方法是增加更多的特徵,例如多項式特徵;在某些情況下可以變成線性可分的資料。在】左圖中,它只有一個特徵x1的簡單的資料集,該資料集不是線性可分的。但是如果增加了第二個特徵 x2=(x1)^2,產生的 2D 資料集就能很好的線性可分。

為了實施這個想法,通過 Scikit-Learn,可以建立一個流水線(Pipeline)去包含多項式特徵(PolynomialFeatures)變換(在 121 頁的“Polynomial Regression”中討論),然後一個StandardScaler和LinearSVC。在衛星資料集(moons datasets)測試一下效果。

from sklearn.datasets import make_moons
X, y = make_moons(n_samples=100, noise=0.15, random_state=42)

def plot_dataset(X, y, axes):
    plt.plot(X[:, 0][y==0], X[:, 1][y==0], "bs")
    plt.plot(X[:, 0][y==1], X[:, 1][y==1], "g^")
    plt.axis(axes)
    plt.grid(True, which='both')
    plt.xlabel(r"$x_1$", fontsize=20)
    plt.ylabel(r"$x_2$", fontsize=20, rotation=0)

plot_dataset(X, y, [-1.5, 2.5, -1, 1.5])
plt.show()

這裡寫圖片描述

from sklearn.datasets import make_moons 
from sklearn.pipeline import Pipeline 
from sklearn.preprocessing import PolynomialFeatures

polynomial_svm_clf = Pipeline([
    ("poly_features", PolynomialFeatures(degree=3)), 
    ("scaler", StandardScaler()), 
    ("svm_clf", LinearSVC(C=10, loss="hinge")) ])

polynomial_svm_clf.fit(X, y)

檢視分類結果

def plot_predictions(clf, axes):
    x0s = np.linspace(axes[0], axes[1], 100)
    x1s = np.linspace(axes[2], axes[3], 100)
    x0, x1 = np.meshgrid(x0s, x1s)
    X = np.c_[x0.ravel(), x1.ravel()]
    y_pred = clf.predict(X).reshape(x0.shape)
    y_decision = clf.decision_function(X).reshape(x0.shape)
    plt.contourf(x0, x1, y_pred, cmap=plt.cm.brg, alpha=0.2)
    plt.contourf(x0, x1, y_decision, cmap=plt.cm.brg, alpha=0.1)

plot_predictions(polynomial_svm_clf, [-1.5, 2.5, -1, 1.5])
plot_dataset(X, y, [-1.5, 2.5, -1, 1.5])
plt.show()

這裡寫圖片描述

多項式核

新增多項式特徵很容易實現,不僅僅在 SVM,在各種機器學習演算法都有不錯的表現,但是低次數的多項式不能處理非常複雜的資料集,而高次數的多項式卻產生了大量的特徵,會使模型變得慢。

幸運的是,當使用 SVM 時,可以運用一個被稱為“核技巧”(kernel trick)的神奇數學技巧。它可以取得就像添加了許多多項式,甚至有高次數的多項式,一樣好的結果。所以不會大量特徵導致的組合爆炸,因為並沒有增加任何特徵。這個技巧可以用 SVC 類來實現。讓我們在衛星資料集測試一下效果。

from sklearn.svm import SVC 

poly_kernel_svm_clf = Pipeline([
        ("scaler", StandardScaler()),
        ("svm_clf", SVC(kernel="poly", degree=3, coef0=1, C=5))
    ])
poly_kernel_svm_clf.fit(X, y)

poly100_kernel_svm_clf = Pipeline([
        ("scaler", StandardScaler()),
        ("svm_clf", SVC(kernel="poly", degree=10, coef0=100, C=5))
    ])
poly100_kernel_svm_clf.fit(X, y)

plt.figure(figsize=(11, 4))

plt.subplot(121)
plot_predictions(poly_kernel_svm_clf, [-1.5, 2.5, -1, 1.5])
plot_dataset(X, y, [-1.5, 2.5, -1, 1.5])
plt.title(r"$d=3, r=1, C=5$", fontsize=18)

plt.subplot(122)
plot_predictions(poly100_kernel_svm_clf, [-1.5, 2.5, -1, 1.5])
plot_dataset(X, y, [-1.5, 2.5, -1, 1.5])
plt.title(r"$d=10, r=100, C=5$", fontsize=18)
plt.show()

這裡寫圖片描述

左圖使用3階的多項式核訓練了一個 SVM 分類器。右圖是使用了10階的多項式核 SVM 分類器。很明顯,如果的模型過擬合,可以減小多項式核的階數。相反的,如果是欠擬合,可以嘗試增大它。超引數coef0控制了高階多項式與低階多項式對模型的影響。

通用的方法是用網格搜尋去找到最優超引數。首先進行非常粗略的網格搜尋一般會很快,然後在找到的最佳值進行更細的網格搜尋。對每個超引數的作用有一個很好的理解可以幫助在正確的超引數空間找到合適的值。

增加相似特徵

另一種解決非線性問題的方法是使用相似函式(similarity funtion)計算每個樣本與特定地標(landmark)的相似度。例如,讓我們來看看前面討論過的一維資料集,並在x1=-2和x1=1之間增加兩個地標。接下來,我們定義一個相似函式,即高斯徑向基函式(Gaussian Radial Basis Function,RBF),設定γ = 0.3。

公式 5-1 RBF

ϕγ(x,)=exp(γ|x|2)

它是個從 0 到 1 的鐘型函式,值為 0 的離地標很遠,值為 1 的在地標上。現在我們準備計算新特徵。例如,我們看一下樣本x1=-1:它距離第一個地標距離是 1,距離第二個地標是 2。因此它的新特徵為x2=exp(-0.3 × (1^2))≈0.74和x3=exp(-0.3 × (2^2))≈0.30。右邊的圖顯示了特徵轉換後的資料集(刪除了原始特徵),現在是線性可分了。

這裡寫圖片描述

高斯 RBF 核

就像多項式特徵法一樣,相似特徵法對各種機器學習演算法同樣也有不錯的表現。但是在所有額外特徵上的計算成本可能很高,特別是在大規模的訓練集上。然而,“核” 技巧再一次顯現了它在 SVM 上的神奇之處:高斯核可以獲得同樣好的結果成為可能,就像在相似特徵法添加了許多相似特徵一樣,但事實上,並不需要在RBF新增它們。我們使用 SVC 類的高斯 RBF 核來檢驗一下。

rbf_kernel_svm_clf = Pipeline((
        ("scaler", StandardScaler()),
        ("svm_clf", SVC(kernel="rbf", gamma=5, C=0.001))
    ))
rbf_kernel_svm_clf.fit(X, y)

這裡寫圖片描述
上圖顯示了用不同的超引數gamma (γ)和C訓練的模型。增大γ使鍾型曲線更窄,導致每個樣本的影響範圍變得更小:即判定邊界最終變得更不規則,在單個樣本週圍環繞。相反的,較小的γ值使鍾型曲線更寬,樣本有更大的影響範圍,判定邊界最終則更加平滑。所以γ是可調整的超引數:如果模型過擬合,應該減小γ值,若欠擬合,則增大γ(與超引數C相似)。

還有其他的核函式,但很少使用。例如,一些核函式是專門用於特定的資料結構。在對文字文件或者 DNA 序列進行分類時,有時會使用字串核(String kernels)(例如,使用 SSK 核(string subsequence kernel)或者基於編輯距離(Levenshtein distance)的核函式)。

這麼多可供選擇的核函式,如何決定使用哪一個?
一般來說,應該先嚐試線性核函式(記住LinearSVC比SVC(kernel=”linear”)要快得多),尤其是當訓練集很大或者有大量的特徵的情況下。如果訓練集不太大,也可以嘗試高斯徑向基核(Gaussian RBF Kernel),它在大多數情況下都很有效。如果有空閒的時間和計算能力,還可以使用交叉驗證和網格搜尋來試驗其他的核函式,特別是有專門用於的訓練集資料結構的核函式。

計算複雜性

LinearSVC類基於liblinear庫,它實現了線性 SVM 的優化演算法。它並不支援核技巧,但是它樣本和特徵的數量幾乎是線性的:訓練時間複雜度大約為O(m × n)。

如果你要非常高的精度,這個演算法需要花費更多時間。這是由容差值超引數ϵ(在 Scikit-learn 稱為tol)控制的。大多數分類任務中,使用預設容差值的效果是已經可以滿足一般要求。

SVC 類基於libsvm庫,它實現了支援核技巧的演算法。訓練時間複雜度通常介於O(m^2 × n)和O(m^3 × n)之間。不幸的是,這意味著當訓練樣本變大時,它將變得極其慢(例如,成千上萬個樣本)。這個演算法對於複雜但小型或中等數量的資料集表現是完美的。然而,它能對特徵數量很好的縮放,尤其對稀疏特徵來說(sparse features)。在這個情況下,演算法對每個樣本的非零特徵的平均數量進行大概的縮放。
這裡寫圖片描述

SVM 迴歸

正如我們之前提到的,SVM 演算法應用廣泛:不僅僅支援線性和非線性的分類任務,還支援線性和非線性的迴歸任務。技巧在於逆轉我們的目標:限制間隔違規的情況下,不是試圖在兩個類別之間找到儘可能大的“街道”(即間隔)。SVM 迴歸任務是限制間隔的情況下,儘量放置更多的樣本在“街道”上。“街道”的寬度由超引數ϵ控制。下圖顯示了在一些隨機生成的線性資料上,兩個線性 SVM 迴歸模型的訓練情況。一個有較大的間隔(ϵ=1.5),另一個間隔較小(ϵ=0.5)。
這裡寫圖片描述

新增更多的資料樣本在間隔之內並不會影響模型的預測,因此,這個模型認為是不敏感的(ϵ-insensitive)。

你可以使用 Scikit-Learn 的LinearSVR類去實現線性 SVM 迴歸。下面的程式碼產生的模型在圖 5-10 左圖(訓練資料需要被中心化和標準化)

from sklearn.svm import LinearSVR
svm_reg = LinearSVR(epsilon=1.5)
svm_reg.fit(X, y)

處理非線性迴歸任務,你可以使用核化的 SVM 模型。