9.6 Lab: 支援向量機

📖 ISLP §9.6 📄 pp. 387–395 ★★★☆☆ ⏱️ 約 45 分鐘
SVM sklearn.svm SVC GridSearchCV ROC 曲線 多類別分類 基因表達 實驗室
← 9.5 SVM 與邏輯回歸的關係 📑 課程首頁 課程首頁 →

📦 環境設定與基礎匯入

本 Lab 使用 sklearn.svm.SVC 來展示支援向量分類器與支援向量機。我們從必要的匯入開始:

# 9.6 Lab: Support Vector Machines — 環境設定
import numpy as np
import matplotlib
matplotlib.use('Agg')
from matplotlib.pyplot import subplots, cm, savefig
import sklearn.model_selection as skm
from ISLP import load_data, confusion_table
from sklearn.svm import SVC
from ISLP.svm import plot as plot_svm
from sklearn.metrics import RocCurveDisplay

# ROC 曲線繪圖縮寫
roc_curve = RocCurveDisplay.from_estimator
print("匯入完成 ✓")
🔧 ISLP 環境說明
ISLP.svm.plot 是 ISLP 套件提供的輔助繪圖函式,用於在二維平面上視覺化 SVM 的決策邊界與支援向量(+ 標記)。confusion_table 來自 ISLP 頂層(非 ISLP.models)。

9.6.1 支援向量分類器(SVC)

我們從模擬一個二維二分類資料集開始。先用線性核心的 SVC 擬合,觀察不同懲罰參數 \(C\) 對邊界和支援向量數量的影響。

# 9.6.1 SVC — 線性核心,模擬資料與 C 值的影響
import numpy as np
import matplotlib
matplotlib.use('Agg')
from matplotlib.pyplot import subplots, cm, savefig
import sklearn.model_selection as skm
from ISLP import confusion_table
from sklearn.svm import SVC
from ISLP.svm import plot as plot_svm

# --- 生成資料 ---
rng = np.random.default_rng(1)
X = rng.standard_normal((50, 2))
y = np.array([-1]*25 + [1]*25)
X[y==1] += 1

# --- 擬合 C=10 ---
svm_linear = SVC(C=10, kernel='linear')
svm_linear.fit(X, y)

# --- 繪圖 ---
fig, ax = subplots(figsize=(8,8))
plot_svm(X, y, svm_linear, ax=ax)
ax.set_title("SVC (C=10, linear kernel)")
savefig('/tmp/svm_linear_c10.png', dpi=100)
print(f"係數: {svm_linear.coef_}")

# --- 較小的 C ---
svm_linear_small = SVC(C=0.1, kernel='linear')
svm_linear_small.fit(X, y)
fig, ax = subplots(figsize=(8,8))
plot_svm(X, y, svm_linear_small, ax=ax)
ax.set_title("SVC (C=0.1, linear kernel)")
savefig('/tmp/svm_linear_c01.png', dpi=100)
print("C=10 vs C=0.1 比較完成 ✓")
係數: [[1.173, 0.7734]] C=10 vs C=0.1 比較完成 ✓

\(C=10\) 時邊界較窄、支援向量較少;\(C=0.1\) 時邊界變寬、支援向量增多(標記為 + 的點)。這直觀展示了 SVM 的偏誤-變異權衡:\(C\) 大 → 低偏誤高變異(可能過擬合),\(C\) 小 → 高偏誤低變異(更強的正則化)。

🏥 醫療應用:支援向量數量 vs. 診斷穩定性

想像你是一個用血液指標(二維:血糖 × 膽固醇)診斷糖尿病的工具。當你只依賴 3 個「邊界案例」來決定診斷線 → 高風險(C 太大)。當你用 12 個邊界案例共同決定 → 更穩健(C 適中)。就像醫師不應只參考一個罕見案例就制定診斷標準,SVM 也不應只依賴極少數支援向量。

交叉驗證選擇 C

# SVC 交叉驗證調整 C
import numpy as np
import matplotlib
matplotlib.use('Agg')
from sklearn.svm import SVC
import sklearn.model_selection as skm
from ISLP import confusion_table

# --- 重建資料 ---
rng = np.random.default_rng(1)
X = rng.standard_normal((50, 2))
y = np.array([-1]*25 + [1]*25)
X[y==1] += 1

# --- 5-fold CV + GridSearch ---
kfold = skm.KFold(5, random_state=0, shuffle=True)
grid = skm.GridSearchCV(
    SVC(C=10, kernel='linear'),
    {'C': [0.001, 0.01, 0.1, 1, 5, 10, 100]},
    refit=True, cv=kfold, scoring='accuracy'
)
grid.fit(X, y)
print(f"最佳 C: {grid.best_params_['C']}")
print(f"各 C 的 CV 準確率: {grid.cv_results_['mean_test_score']}")

# --- 測試集評估 ---
X_test = rng.standard_normal((20, 2))
y_test = np.array([-1]*10 + [1]*10)
X_test[y_test==1] += 1

best_ = grid.best_estimator_
y_test_hat = best_.predict(X_test)
print(f"\n測試集混亂矩陣 (C={grid.best_params_['C']}):")
print(confusion_table(y_test_hat, y_test))

# --- 對照:C=0.001 ---
svm_weak = SVC(C=0.001, kernel='linear').fit(X, y)
y_weak = svm_weak.predict(X_test)
print(f"\n測試集混亂矩陣 (C=0.001):")
print(confusion_table(y_weak, y_test))
最佳 C: {'C': 1} 各 C 的 CV 準確率: [0.46, 0.46, 0.72, 0.74, 0.74, 0.74, 0.74] 測試集混亂矩陣 (C=1): Truth -1 1 Pred -1 8 4 1 2 6 測試集混亂矩陣 (C=0.001): Truth -1 1 Pred -1 2 0 1 8 10

線性可分資料的極端案例

當類別剛好線性可分時,設定極大 \(C\)(如 \(10^5\))會產生「硬邊界」— 僅 3 個支援向量定義整個邊界。試試看:

# 線性可分案例 — 對比 C=1e5 vs C=0.1
import numpy as np
import matplotlib
matplotlib.use('Agg')
from matplotlib.pyplot import subplots, cm, savefig
from sklearn.svm import SVC
from ISLP import confusion_table
from ISLP.svm import plot as plot_svm

# --- 讓資料線性可分 ---
rng = np.random.default_rng(1)
X = rng.standard_normal((50, 2))
y = np.array([-1]*25 + [1]*25)
X[y==1] += 1
X[y==1] += 1.9  # 進一步分離

# --- C=1e5(硬邊界)---
svm_hard = SVC(C=1e5, kernel='linear').fit(X, y)
y_hard = svm_hard.predict(X)
print(f"C=1e5 訓練錯誤數: {(y_hard != y).sum()}")
print(confusion_table(y_hard, y))

fig, ax = subplots(figsize=(8,8))
plot_svm(X, y, svm_hard, ax=ax)
ax.set_title("SVC (C=1e5, 硬邊界)")
savefig('/tmp/svm_hard_margin.png', dpi=100)

# --- C=0.1(軟邊界)---
svm_soft = SVC(C=0.1, kernel='linear').fit(X, y)
y_soft = svm_soft.predict(X)
print(f"\nC=0.1 訓練錯誤數: {(y_soft != y).sum()}")
print(confusion_table(y_soft, y))

fig, ax = subplots(figsize=(8,8))
plot_svm(X, y, svm_soft, ax=ax)
ax.set_title("SVC (C=0.1, 軟邊界)")
savefig('/tmp/svm_soft_margin.png', dpi=100)
print("\n完成 ✓")
🎯 關鍵洞察
當資料剛好線性可分時,\(C=10^5\) 僅用 3 個支援向量就完美分類訓練資料。但這 3 個點同時定義了邊界 方向——決策邊界對這 3 個點極度敏感,測試集表現可能不穩定。\(C=0.1\) 用了 12 個支援向量,邊界方向來自更多點的共同決定,對單一異常值的敏感度較低。

✅ 大 C(硬邊界)

❌ 大 C(硬邊界)

9.6.2 支援向量機(非線性核心)

現在我們使用 RBF(徑向基底函數)核心來處理非線性決策邊界。核心的概念類似「把資料投影到更高的維度,在那裡找到線性的分割面,再投影回來」——有點像摺紙:平面上無法分開的兩色點,把紙摺起來(增加第三維)就可以一刀切開了。

# 9.6.2 SVM — RBF 核心處理非線性邊界
import numpy as np
import matplotlib
matplotlib.use('Agg')
from matplotlib.pyplot import subplots, cm, savefig
import sklearn.model_selection as skm
from sklearn.svm import SVC
from ISLP import confusion_table
from ISLP.svm import plot as plot_svm

# --- 生成非線性邊界資料 ---
rng = np.random.default_rng(2)
X = rng.standard_normal((200, 2))
X[:100] += 2
X[100:150] -= 2
y = np.array([1]*150 + [2]*50)

# --- 分割訓練/測試 ---
(X_train, X_test,
 y_train, y_test) = skm.train_test_split(
    X, y, test_size=0.5, random_state=0)

# --- RBF SVM, gamma=1, C=1 ---
svm_rbf = SVC(kernel="rbf", gamma=1, C=1)
svm_rbf.fit(X_train, y_train)

fig, ax = subplots(figsize=(8,8))
plot_svm(X_train, y_train, svm_rbf, ax=ax)
ax.set_title("RBF SVM (gamma=1, C=1)")
savefig('/tmp/svm_rbf_g1_c1.png', dpi=100)

y_pred = svm_rbf.predict(X_train)
print(f"訓練錯誤數 (gamma=1, C=1): {(y_pred != y_train).sum()}/100")

# --- C=1e5,更複雜邊界 ---
svm_rbf_highC = SVC(kernel="rbf", gamma=1, C=1e5)
svm_rbf_highC.fit(X_train, y_train)

fig, ax = subplots(figsize=(8,8))
plot_svm(X_train, y_train, svm_rbf_highC, ax=ax)
ax.set_title("RBF SVM (gamma=1, C=1e5)")
savefig('/tmp/svm_rbf_g1_c1e5.png', dpi=100)

y_pred_hc = svm_rbf_highC.predict(X_train)
print(f"訓練錯誤數 (gamma=1, C=1e5): {(y_pred_hc != y_train).sum()}/100")
print("完成 ✓")
ISLP Eq. (9.24): RBF kernel \(K(x_i, x_{i'}) = \exp(-\gamma \sum_{j=1}^p (x_{ij} - x_{i'j})^2)\)。\(\gamma\) 控制核函數的影響範圍:\(\gamma\) 大 → 每個點只影響很近的鄰居(局部、波浪狀邊界);\(\gamma\) 小 → 每個點影響範圍大(平滑邊界)。

網格搜索選擇 \(\gamma\) 與 \(C\)

# RBF SVM — 5-fold CV 網格搜索 (gamma, C)
import numpy as np
import matplotlib
matplotlib.use('Agg')
from matplotlib.pyplot import subplots, savefig
import sklearn.model_selection as skm
from sklearn.svm import SVC
from ISLP import confusion_table
from ISLP.svm import plot as plot_svm

# --- 重建資料並分割 ---
rng = np.random.default_rng(2)
X = rng.standard_normal((200, 2))
X[:100] += 2; X[100:150] -= 2
y = np.array([1]*150 + [2]*50)
(X_train, X_test, y_train, y_test) = skm.train_test_split(
    X, y, test_size=0.5, random_state=0)

# --- 網格搜索 ---
kfold = skm.KFold(5, random_state=0, shuffle=True)
grid = skm.GridSearchCV(
    SVC(kernel="rbf"),
    {'C': [0.1, 1, 10, 100, 1000],
     'gamma': [0.5, 1, 2, 3, 4]},
    refit=True, cv=kfold, scoring='accuracy'
)
grid.fit(X_train, y_train)
print(f"最佳參數: {grid.best_params_}")

# --- 最佳模型預測 ---
best_svm = grid.best_estimator_
y_hat_test = best_svm.predict(X_test)
print(f"\n測試集混亂矩陣:\n{confusion_table(y_hat_test, y_test)}")

er = (y_hat_test != y_test).sum()
print(f"測試錯誤: {er}/{len(y_test)} = {er/len(y_test)*100:.1f}%")

# --- 繪製最佳模型邊界 ---
fig, ax = subplots(figsize=(8,8))
plot_svm(X_train, y_train, best_svm, ax=ax)
ax.set_title(f"Best RBF SVM: {grid.best_params_}")
savefig('/tmp/svm_rbf_best.png', dpi=100)
print("完成 ✓")
最佳參數: {'C': 100, 'gamma': 1} 測試錯誤: 12/100 = 12.0%

9.6.3 ROC 曲線

SVM 的 decision_function() 回傳每個觀測值的「擬合值」(fitted value)——正負號決定類別,數值大小反映信心程度。把分類閾值從 0 調整到不同值,我們就可以繪製 ROC 曲線來權衡真陽率和偽陽率。

# 9.6.3 ROC 曲線 — 訓練 vs 測試
import numpy as np
import matplotlib
matplotlib.use('Agg')
from matplotlib.pyplot import subplots, savefig
import sklearn.model_selection as skm
from sklearn.svm import SVC
from sklearn.metrics import RocCurveDisplay

# --- 重建資料與最佳模型 ---
rng = np.random.default_rng(2)
X = rng.standard_normal((200, 2))
X[:100] += 2; X[100:150] -= 2
y = np.array([1]*150 + [2]*50)
(X_train, X_test, y_train, y_test) = skm.train_test_split(
    X, y, test_size=0.5, random_state=0)

best_svm = SVC(kernel="rbf", C=100, gamma=1)
best_svm.fit(X_train, y_train)

# --- ROC: 訓練集(紅色)---
fig, ax = subplots(figsize=(8,8))
RocCurveDisplay.from_estimator(best_svm, X_train, y_train,
    name='Training (CV-tuned)', color='r', ax=ax)

# --- 彈性模型 γ=50 ---
svm_flex = SVC(kernel="rbf", gamma=50, C=1)
svm_flex.fit(X_train, y_train)
RocCurveDisplay.from_estimator(svm_flex, X_train, y_train,
    name='Training gamma=50', color='orange', ax=ax)

# --- ROC: 測試集(藍色)---
RocCurveDisplay.from_estimator(svm_flex, X_test, y_test,
    name='Test gamma=50', color='b', ax=ax)
RocCurveDisplay.from_estimator(best_svm, X_test, y_test,
    name='Test (CV-tuned)', color='c', ax=ax)

ax.set_title("ROC Curves: Training vs Test")
savefig('/tmp/svm_roc.png', dpi=100)
print("ROC 曲線繪製完成 ✓")

📊 ROC 曲線的實際應用:垃圾郵件過濾

電子郵件服務商使用 SVM 判斷「垃圾郵件 vs 正常郵件」。調整 閾值(決策函數的臨界值)可以平衡兩個目標:(1) 不要漏掉重要郵件(低偽陽率),(2) 不要讓過多垃圾郵件進收件匣(高真陽率)。ROC 曲線讓工程師視覺化所有可能的閾值組合,選擇最適合業務需求的點。

9.6.4 多類別 SVM

當類別超過兩個時,SVC 使用一對一(one-versus-one, ovo)或一對多(one-versus-rest, ovr)策略。我們用模擬的三類別資料展示:

# 9.6.4 多類別 SVM(ovo 策略)
import numpy as np
import matplotlib
matplotlib.use('Agg')
from matplotlib.pyplot import subplots, cm, savefig
from sklearn.svm import SVC
from ISLP.svm import plot as plot_svm

# --- 擴展為三類別 ---
rng = np.random.default_rng(123)
X = rng.standard_normal((200, 2))
X[:100] += 2
X[100:150] -= 2
y = np.array([1]*150 + [2]*50)

# 加入第三類
X = np.vstack([X, rng.standard_normal((50, 2))])
y = np.hstack([y, [0]*50])
X[y==0, 1] += 2

# --- 擬合 RBF SVM,ovo ---
svm_rbf_3 = SVC(kernel="rbf", C=10, gamma=1,
                decision_function_shape='ovo')
svm_rbf_3.fit(X, y)

fig, ax = subplots(figsize=(8,8))
plot_svm(X, y, svm_rbf_3, scatter_cmap=cm.tab10, ax=ax)
ax.set_title("3-Class SVM (RBF, ovo)")
savefig('/tmp/svm_multiclass.png', dpi=100)
print(f"類別數: {len(np.unique(y))}, 支援向量數: {svm_rbf_3.n_support_.sum()}")
print("完成 ✓")
ovo vs. ovr 策略
ovo(一對一):為每對類別建立一個 SVM,總共 \(K(K-1)/2\) 個分類器。投票決定最終類別。
ovr(一對多):為每個類別建立一個 SVM(該類 vs. 所有其他類),共 \(K\) 個分類器。得分最高的類別勝出。
SVC() 預設使用 ovodecision_function_shape='ovo'),對 SVM 通常更適合,因為每個子問題只涉及兩個類別。

9.6.5 基因表達資料實戰

Khan 資料集:63 個訓練樣本 + 20 個測試樣本,每個樣本有 2,308 個基因表達值。目標是預測 四種兒童腫瘤類型。特徵數 (2,308) ≫ 樣本數 (63)——這正是 SVM 的拿手好戲。

# 9.6.5 Khan 基因表達資料 — 線性 SVM
import numpy as np
from sklearn.svm import SVC
from ISLP import load_data, confusion_table

# --- 載入資料 ---
Khan = load_data('Khan')
X_train = Khan['xtrain']
y_train = Khan['ytrain'].values.ravel() if hasattr(Khan['ytrain'], 'values') else Khan['ytrain']
X_test = Khan['xtest']
y_test = Khan['ytest'].values.ravel() if hasattr(Khan['ytest'], 'values') else Khan['ytest']

print(f"訓練集: {X_train.shape} (n={X_train.shape[0]}, p={X_train.shape[1]})")
print(f"測試集: {X_test.shape} (n={X_test.shape[0]}, p={X_test.shape[1]})")

# --- 線性 SVM(高維度不需要 RBF)---
khan_linear = SVC(kernel='linear', C=10)
khan_linear.fit(X_train, y_train)

# --- 訓練集混淆矩陣 ---
y_tr_pred = khan_linear.predict(X_train)
print(f"\n訓練集混淆矩陣:")
print(confusion_table(y_tr_pred, y_train))

# --- 測試集混淆矩陣 ---
y_te_pred = khan_linear.predict(X_test)
print(f"\n測試集混淆矩陣:")
print(confusion_table(y_te_pred, y_test))

test_err = (y_te_pred != y_test).sum()
print(f"\n測試錯誤數: {test_err}/{len(y_test)}")
print(f"測試準確率: {(len(y_test)-test_err)/len(y_test)*100:.1f}%")
訓練集: (63, 2308) (n=63, p=2308) 測試集: (20, 2308) (n=20, p=2308) 訓練集混淆矩陣: Truth 1 2 3 4 Pred 1 8 0 0 0 2 0 23 0 0 3 0 0 12 0 4 0 0 0 20 測試集混淆矩陣: Truth 1 2 3 4 Pred 1 3 0 0 0 2 0 6 2 0 3 0 0 4 0 4 0 0 0 5 測試錯誤數: 2/20 測試準確率: 90.0%

訓練集零錯誤在 \(p \gg n\) 的情境下並不意外——2,308 個特徵為 63 個點找到完全分離的超平面是輕而易舉的。真正有價值的是測試集準確率 90%,證明了即使在高維度下,SVM 仍然能夠有效泛化。

🧬 精準醫療的現實應用

病理學家傳統上依賴顯微鏡下的細胞形態來診斷腫瘤類型——這需要多年專業訓練。Khan 資料集展示了一種革命性方法:用基因表達晶片在分子層面自動分類腫瘤。2,308 個基因中,SVM 自動找出能區分四種腫瘤的「基因簽名」。90% 的測試準確率意味著:對 20 個新病患,只有 2 個被誤診。這類技術正在推動「液態活檢」等非侵入性癌症診斷的發展。

📊 C 參數的影響總覽

C 值 邊界寬度 支援向量數 訓練錯誤 泛化能力 適用情境
極小 (0.001) 很寬 很多 欠擬合 資料雜訊極大、防止過擬合優先
小 (0.1) 較多 低~中 良好 高維度、樣本少的穩健應用
中 (1~10) 適中 適中 最佳 一般分類任務(CV 常選範圍)
大 (1e5) 很窄 極少 幾乎 0 可能過擬合 資料完美線性可分

🔧 核心函數比較

核心 參數 邊界形狀 計算成本 適用
linear 僅 C 直線/超平面 線性可分、p ≫ n、文本分類
poly C, degree, gamma 多項式曲線 已知多項式結構的邊界
rbf C, gamma 任意平滑形狀 未知邊界形狀、通用預設
在高維度中,線性 SVM 通常就足夠強大了。當特徵數遠大於樣本數時,不需要非線性核心的額外複雜度——把 RBF 留給低維度的彎曲邊界。 — 基於 ISLP §9.6.5 的實戰洞察

🧠 關鍵重點回顧

  1. SVC() 的 C 參數控制對邊界違規的容忍度:C 大 → 窄邊界、少支援向量、低偏誤;C 小 → 寬邊界、多支援向量、更平滑。
  2. RBF 核心透過 \(\gamma\) 控制局部影響範圍,配合 C 形成雙參數調優空間。Small \(\gamma\) + moderate C = 平滑、可泛化的邊界。
  3. 多類別策略:ovo(一對一投票)是 SVM 的預設策略,對於 \(K\) 類別建立 \(K(K-1)/2\) 個二元分類器。
  4. 高維度 (\(p > n\)) 優勢:線性 SVM 在基因表達等微陣列資料上表現出色,不需要非線性核心。
  5. ROC 曲線不依賴固定閾值,而是展示所有可能閾值下的 TPR-FPR 權衡,是比較分類器的黃金標準。
← 9.5 SVM 與邏輯回歸的關係 📑 課程首頁 課程首頁 →