支援向量機(support vector machine,SVM)是非常強大、靈活的有監督學習算法,既可用于分類,也可用于回歸。在本節中,我們将介紹支援向量機的原理,并用它解決分類問題。首先還是導入需要用的程式庫:
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
# use seaborn plotting defaults
import seaborn as sns; sns.set()
1、支援向量機的由來
我們首先對每個類進行了随機分布的假設,然後用生成的模型估計新資料點的标簽。那是生成分類方法,這裡将介紹判别分類方法:不再為每類資料模組化,而是用一條分割線(二維空間中的直線或曲線)或者流形體(多元空間中的曲線、曲面等概念的推廣)将各種類型分割開。
from sklearn.datasets.samples_generator import make_blobs
X, y = make_blobs(n_samples=50, centers=2,
random_state=0, cluster_std=0.60)
plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn');

這個線性判别分類器嘗試畫一條将資料分成兩部分的直線,這樣就構成了一個分類模型。對于上圖的二維資料來說,這個任務其實可以手動完成。但是我們馬上發現一個問題:在這兩種類型之間,有不止一條直線可以将它們完美分割。
xfit = np.linspace(-1, 3.5)
plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn')
plt.plot([0.6], [2.1], 'x', color='red', markeredgewidth=2, markersize=10)
for m, b in [(1, 0.65), (0.5, 1.6), (-0.2, 2.9)]:
plt.plot(xfit, m * xfit + b, '-k')
plt.xlim(-1, 3.5);
雖然這三個不同的分割器都能完美地判别這些樣本,但是選擇不同的分割線,可能會讓新的資料點(例如圖中的“X”點)配置設定到不同的标簽。顯然,“畫一條分割不同類型的直線”還不夠,我們需要進一步思考。
2、支援向量機:邊界最大化
支援向量機提供了改進這個問題的方法,它直覺的解釋是:不再畫一條細線來區分類型,而是畫一條到最近點邊界、有寬度的線條。
xfit = np.linspace(-1, 3.5)
plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn')
for m, b, d in [(1, 0.65, 0.33), (0.5, 1.6, 0.55), (-0.2, 2.9, 0.2)]:
yfit = m * xfit + b
plt.plot(xfit, yfit, '-k')
plt.fill_between(xfit, yfit - d, yfit + d, edgecolor='none',
color='#AAAAAA', alpha=0.4)
plt.xlim(-1, 3.5);
在支援向量機中,選擇邊界最大的那條線是模型最優解。支援向量機其實就是一個邊界最大化評估器。
1.拟合支援向量機
來看看這個資料的真實拟合結果:用Scikit-Learn 的支援向量機分類器在資料上訓練一個SVM 模型。這裡用一個線性核函數,并将參數C 設定為一個很大的數(後面會介紹這些設定的意義)。
from sklearn.svm import SVC # "Support vector classifier"
model = SVC(kernel='linear', C=1E10)
model.fit(X, y)
為了實作更好的可視化分類效果,建立一個輔助函數畫出SVM 的決策邊界。
def plot_svc_decision_function(model, ax=None, plot_support=True):
"""畫二維SVC的決策函數"""
if ax is None:
ax = plt.gca()
xlim = ax.get_xlim()
ylim = ax.get_ylim()
# 建立評估模型的網格
x = np.linspace(xlim[0], xlim[1], 30)
y = np.linspace(ylim[0], ylim[1], 30)
Y, X = np.meshgrid(y, x)
xy = np.vstack([X.ravel(), Y.ravel()]).T
P = model.decision_function(xy).reshape(X.shape)
# 畫決策邊界和邊界
ax.contour(X, Y, P, colors='k',
levels=[-1, 0, 1], alpha=0.5,
linestyles=['--', '-', '--'])
# 畫支援向量
if plot_support:
ax.scatter(model.support_vectors_[:, 0],
model.support_vectors_[:, 1],
s=300, linewidth=1, facecolors='none');
ax.set_xlim(xlim)
ax.set_ylim(ylim)
plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn')
plot_svc_decision_function(model);
這就是兩類資料間隔最大的分割線。你會發現有一些點正好就在邊界線上。這些點是拟合的關鍵支援點,被稱為支援向量,支援向量機算法也是以得名。在Scikit-Learn 裡面,支援向量的坐标存放在分類器的support_vectors_ 屬性中:
model.support_vectors_
分類器能夠成功拟合的關鍵因素,就是這些支援向量的位置——任何在正确分類一側遠離邊界線的點都不會影響拟合結果!從技術角度解釋的話,是因為這些點不會對拟合模型的損失函數産生任何影響,是以隻要它們沒有跨越邊界線,它們的位置和數量就都無關緊要。
例如,可以分别畫出資料集前60 個點和前120 個點的拟合結果,并進行對比
def plot_svm(N=10, ax=None):
X, y = make_blobs(n_samples=200, centers=2,
random_state=0, cluster_std=0.60)
X = X[:N]
y = y[:N]
model = SVC(kernel='linear', C=1E10)
model.fit(X, y)
ax = ax or plt.gca()
ax.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn')
ax.set_xlim(-1, 4)
ax.set_ylim(-1, 6)
plot_svc_decision_function(model, ax)
fig, ax = plt.subplots(1, 2, figsize=(16, 6))
fig.subplots_adjust(left=0.0625, right=0.95, wspace=0.1)
for axi, N in zip(ax, [60, 120]):
plot_svm(N, axi)
axi.set_title('N = {0}'.format(N))
我們在左圖中看到的是前60個訓練樣本的模型和支援向量。在右圖中,雖然我們畫了前120 個訓練樣本的支援向量,但是模型并沒有改變:左圖中的3個支援向量仍然适用于右圖。這種對遠離邊界的資料點不敏感的特點正是SVM 模型的優點之一。
from ipywidgets import interact, fixed
interact(plot_svm, N=[10, 200], ax=fixed(None));
2.超越線性邊界:核函數SVM模型
将SVM 模型與核函數組合使用,功能會非常強大。那時,我們将資料投影到多項式和高斯基函數定義的高維空間中,進而實作用線性分類器拟合非線性關系。
在SVM 模型中,我們可以沿用同樣的思路。為了應用核函數,引入一些非線性可分的資料。
from sklearn.datasets.samples_generator import make_circles
X, y = make_circles(100, factor=.1, noise=.1)
clf = SVC(kernel='linear').fit(X, y)
plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn')
plot_svc_decision_function(clf, plot_support=False);
顯然,這裡需要用非線性判别方法來分割資料。回顧一下介紹的基函數回歸方法,想想如何将資料投影到高維空間,進而使線性分割器可以派上用場。例如,一種簡單的投影方法就是計算一個以資料圓圈(middle clump)為中心的徑向基函數:
r = np.exp(-(X ** 2).sum(1))
可以通過三維圖來可視化新增的次元
from mpl_toolkits import mplot3d
def plot_3D(elev=30, azim=30, X=X, y=y):
ax = plt.subplot(projection='3d')
ax.scatter3D(X[:, 0], X[:, 1], r, c=y, s=50, cmap='autumn')
ax.view_init(elev=elev, azim=azim)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('r')
interact(plot_3D, elev=[-90, 90], azip=(-180, 180),
X=fixed(X), y=fixed(y));
增加新次元之後,資料變成了線性可分狀态。如果現在畫一個分割平面,例如r = 0.7,即可将資料分割。
我們還需要仔細選擇和優化投影方式;如果不能将徑向基函數集中到正确的位置,那麼就得不到如此幹淨、可分割的結果。通常,選擇基函數比較困難,我們需要讓模型自動指出最合适的基函數。
一種政策是計算基函數在資料集上每個點的變換結果,讓SVM 算法從所有結果中篩選出最優解。這種基函數變換方式被稱為核變換,是基于每對資料點之間的相似度(或者核函數)計算的。
這種政策的問題是,如果将N個資料點投影到N維空間,當N不斷增大的時候就會出現次元災難,計算量巨大。但由于核函數技巧(
http://bit.ly/2fStZeA) 提供的小程式可以隐式計算核變換資料的拟合,也就是說,不需要建立完全的N 維核函數投影空間!這個核函數技巧内置在SVM 模型中,是使SVM 方法如此強大的充分條件。
在Scikit-Learn 裡面,我們可以應用核函數化的SVM 模型将線性核轉變為RBF(徑向基函數)核,設定kernel模型超參數即可.
clf = SVC(kernel='rbf', C=1E6)
clf.fit(X, y)
plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn')
plot_svc_decision_function(clf)
plt.scatter(clf.support_vectors_[:, 0], clf.support_vectors_[:, 1],
s=300, lw=1, facecolors='none');
通過使用這個核函數化的支援向量機,我們找到了一條合适的非線性決策邊界。在機器學習中,核變換政策經常用于将快速線性方法變換成快速非線性方法,尤其是對于那些可以應用核函數技巧的模型.
3.SVM優化:軟化邊界
到目前為止,我們介紹的模型都是在處理非常幹淨的資料集,裡面都有非常完美的決策邊界。但如果你的資料有一些重疊該怎麼辦呢?例如,有如下所示一些資料
X, y = make_blobs(n_samples=100, centers=2,
random_state=0, cluster_std=1.2)
plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn');
為了解決這個問題,SVM 實作了一些修正因子來“軟化”邊界。為了取得更好的拟合效果,它允許一些點位于邊界線之内。邊界線的硬度可以通過超參數進行控制,通常是C。
如果C 很大,邊界就會很硬,資料點便不能在邊界内“生存”;如果C比較小,邊界線比較軟,有一些資料點就可以穿越邊界線。
X, y = make_blobs(n_samples=100, centers=2,
random_state=0, cluster_std=0.8)
fig, ax = plt.subplots(1, 2, figsize=(16, 6))
fig.subplots_adjust(left=0.0625, right=0.95, wspace=0.1)
for axi, C in zip(ax, [10.0, 0.1]):
model = SVC(kernel='linear', C=C).fit(X, y)
axi.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn')
plot_svc_decision_function(model, axi)
axi.scatter(model.support_vectors_[:, 0],
model.support_vectors_[:, 1],
s=300, lw=1, facecolors='none');
axi.set_title('C = {0:.1f}'.format(C), size=14)
參數C 的最優值視資料集的具體情況而定,通過交叉檢驗或者類似的程式進行計算
3、案例:人臉識别
我們用人臉識别案例來示範支援向量機的實戰過程。這裡用Wild 資料集中帶标記的人臉圖像,裡面包含了數千張公開的人臉照片。Scikit-Learn 内置了擷取照片資料集的功能:
from sklearn.datasets import fetch_lfw_people
faces = fetch_lfw_people(min_faces_per_person=60)
print(faces.target_names)
print(faces.images.shape)
fig, ax = plt.subplots(3, 5)
for i, axi in enumerate(ax.flat):
axi.imshow(faces.images[i], cmap='bone')
axi.set(xticks=[], yticks=[],
xlabel=faces.target_names[faces.target[i]])
每個圖像包含[62×47]、接近3000 像素。雖然可以簡單地将每個像素作為一個特征,但是更高效的方法通常是使用預處理器來提取更有意義的特征。這裡使用主成分分析來提取150 個基本元素,然後将其提供給支援向量機分類器。可以将這個預處理器和分類器打包成管道:
from sklearn.svm import SVC
from sklearn.decomposition import RandomizedPCA
from sklearn.pipeline import make_pipeline
pca = RandomizedPCA(n_components=150, whiten=True, random_state=42)
svc = SVC(kernel='rbf', class_weight='balanced')
model = make_pipeline(pca, svc)
為了測試分類器的訓練效果,将資料集分解成訓練集和測試集進行交叉檢驗:
from sklearn.cross_validation import train_test_split
Xtrain, Xtest, ytrain, ytest = train_test_split(faces.data, faces.target,
random_state=42)
最後,用網格搜尋交叉檢驗來尋找最優參數組合。通過不斷調整參數C(控制邊界線的硬度)和參數gamma(控制徑向基函數核的大小),确定最優模型:
from sklearn.grid_search import GridSearchCV
param_grid = {'svc__C': [1, 5, 10, 50],
'svc__gamma': [0.0001, 0.0005, 0.001, 0.005]}
grid = GridSearchCV(model, param_grid)
%time grid.fit(Xtrain, ytrain)
print(grid.best_params_)
最優參數最終落在了網格的中間位置。如果它們落在了邊緣位置,我們可能就需要擴充網格搜尋範圍,確定最優參數可以被搜尋到。
有了交叉檢驗的模型,現在就可以對測試集的資料進行預測了:
model = grid.best_estimator_
yfit = model.predict(Xtest)
fig, ax = plt.subplots(4, 6)
for i, axi in enumerate(ax.flat):
axi.imshow(Xtest[i].reshape(62, 47), cmap='bone')
axi.set(xticks=[], yticks=[])
axi.set_ylabel(faces.target_names[yfit[i]].split()[-1],
color='black' if yfit[i] == ytest[i] else 'red')
fig.suptitle('Predicted Names; Incorrect Labels in Red', size=14);
在這個小樣本中,我們的最優評估器隻判斷錯了一張照片(最後一行中布什的照片被誤判為布萊爾)。我們可以列印分類效果報告,它會列舉每個标簽的統計結果,進而對評估器的性能有更全面的認識:
from sklearn.metrics import classification_report
print(classification_report(ytest, yfit,
target_names=faces.target_names))
還可以畫出這些标簽的混淆矩陣
from sklearn.metrics import confusion_matrix
mat = confusion_matrix(ytest, yfit)
sns.heatmap(mat.T, square=True, annot=True, fmt='d', cbar=False,
xticklabels=faces.target_names,
yticklabels=faces.target_names)
plt.xlabel('true label')
plt.ylabel('predicted label');
真實環境中的人臉識别問題的照片通常不會被切割得這麼整齊(即使像素相同),兩類人臉分類機制的唯一差别其實是特征選擇:你需要用更複雜的算法找到人臉,然後提取圖檔中與像素化無關的人臉特征。這類問題有一個不錯的解決方案,就是用 OpenCV(
http://opencv.org)配合其他手段,包括最先進的通用圖像和人臉圖像的特征提取工具,來擷取人臉特征資料。
4、支援向量機總結
前面已經簡單介紹了支援向量機的基本原則。支援向量機是一種強大的分類方法,主要有四點理由。
模型依賴的支援向量比較少,說明它們都是非常精緻的模型,消耗記憶體少。
一旦模型訓練完成,預測階段的速度非常快。
由于模型隻受邊界線附近的點的影響,是以它們對于高維資料的學習效果非常好——即使訓練比樣本次元還高的資料也沒有問題,而這是其他算法難以企及的。
與核函數方法的配合極具通用性,能夠适用不同類型的資料。
但是,SVM 模型也有一些缺點。
随着樣本量N 的不斷增加,最差的訓練時間複雜度會達到 [N3];經過高效處理後,也隻能達到 [N2]。是以,大樣本學習的計算成本會非常高。
訓練效果非常依賴于邊界軟化參數C 的選擇是否合理。這需要通過交叉檢驗自行搜尋;當資料集較大時,計算量也非常大。
如果你也有想分享的幹貨,可以登入天池實驗室(notebook),包括賽題的了解、資料分析及可視化、算法模型的分析以及一些核心的思路等内容。
小天會根據你分享内容的數量以及程度,給予豐富的神秘天池大禮以及糧票獎勵。分享成功後你也可以通過下方釘釘群主動聯系我們的社群營運同學(釘釘号:yiwen1991)
天池寶貝們有任何問題,可在戳“留言”評論或加入釘釘群留言,小天會認真傾聽每一個你的建議!