9.2 支援向量分類器(Support Vector Classifier)

★★★ 難度:中等 · 課本 §9.2(頁 373–376) SVM soft margin slack variable bias-variance

📚 為什麼需要軟邊界?

§9.1 介紹的「最大邊界分類器」有一個致命前提:資料必須可以完美被一個超平面分開。但現實世界長這樣嗎?

🔑 兩大困境

困境一:資料天生不可分。 兩類資料交錯在一起,沒有任何一條直線能完美分開(課本 Figure 9.4)。此時最大邊界分類器根本無解——最佳化問題 (9.9)–(9.11) 找不到 M > 0 的解。

困境二:就算可分,完美分離也可能是壞事。 課本 Figure 9.5 展示了經典場景:多加一個點,最大邊界超平面就劇烈翻轉,邊界變超窄。這本質上就是過度擬合——對單一觀測值過度敏感。

於是我們需要一個折衷方案:允許一些點「犯規」(跨過邊界甚至跨過超平面),換取對大多數訓練觀測值更好的分類,以及對新資料更穩健的泛化能力。這就是 支援向量分類器(Support Vector Classifier),又稱軟邊界分類器(Soft Margin Classifier)

💡 生活比喻:教室排座位

想像你把男生和女生的座位用一條走道分開。如果嚴格要求「沒有任何一個學生跨過走道」,那走道就得彎彎曲曲繞過每個亂坐的人,窄到無法走路(硬邊界 overfitting)。

軟邊界的做法:允許少數學生跨過走道邊界,換一條又寬又直的走道。只要 90% 的人坐對邊,走道就很好走,新來的學生也知道該坐哪。

🧮 最佳化問題:從硬邊界到軟邊界

軟邊界 SVC 的最佳化問題(課本 9.12–9.15):

\[ \begin{aligned} \underset{\beta_0,\beta_1,\ldots,\beta_p,\epsilon_1,\ldots,\epsilon_n, M}{\text{maximize}} &\quad M \\[4pt] \text{subject to} &\quad \sum_{j=1}^{p} \beta_j^2 = 1, \\[4pt] &\quad y_i(\beta_0 + \beta_1 x_{i1} + \cdots + \beta_p x_{ip}) \ge M(1 - \epsilon_i), \\[4pt] &\quad \epsilon_i \ge 0, \quad \sum_{i=1}^{n} \epsilon_i \le C. \end{aligned} \]

和硬邊界(§9.1.4)相比,多了兩個關鍵元素:

1. Slack 變數 \(\epsilon_i\)(寬鬆變數)

\(\epsilon_i\) 描述第 \(i\) 個觀測值「犯規」的程度:

\(\epsilon_i\) 的值意義
\(\epsilon_i = 0\)在邊界的正確側,完全沒犯規 ✅
\(0 < \epsilon_i \le 1\)在邊界的錯誤側(穿越邊界),但仍在超平面的正確側
\(\epsilon_i > 1\)在超平面的錯誤側——直接被誤分類

2. 調節參數 \(C\):犯規預算

\(C\) 是所有 \(\epsilon_i\) 總和的上限。可以理解為「犯規總預算」:

\(C\) 值邊界支援向量數偏差-變異
\(C = 0\)回到硬邊界(零容忍)極少低偏差、高變異
\(C\) 小窄邊界、少量違規低偏差、高變異 ← overfit
\(C\) 大寬邊界、容忍多個違規高偏差、低變異 ← smoother
📚 課本原文重點(頁 376)

「As the budget \(C\) increases, we become more tolerant of violations to the margin, and so the margin will widen. Conversely, as \(C\) decreases, we become less tolerant of violations to the margin and so the margin narrows.」

—— James, Witten, Hastie, Tibshirani (2023), ISLP §9.2.2

🐍 Python 實作:sklearn SVC

我們用 sklearn.svm.SVC(核心參數 Ckernel='linear')展示軟邊界分類器的行為。

區塊 1:生成不可分的模擬資料 + 視覺化 C 的效果

try:
    from google.colab import drive
    drive.mount('/content/drive')
    DATA_PATH = '/content/drive/MyDrive/ISLP_data/'
except ImportError:
    DATA_PATH = '/tmp/'

import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from sklearn.svm import SVC

# 生成兩類重疊的模擬資料(不可完美分離)
np.random.seed(42)
n = 100
X = np.random.standard_normal((n, 2))
# 用非線性規則標記:靠近圓心的 vs 外圍的
r = np.sqrt(X[:, 0]**2 + X[:, 1]**2)
y = np.where(r > 1.2, 1, -1)  # 故意讓邊界區域模糊

# 試不同 C 值
C_values = [0.1, 1, 10, 100]
fig, axes = plt.subplots(2, 2, figsize=(12, 10))

for ax, C in zip(axes.flat, C_values):
    svc = SVC(kernel='linear', C=C, random_state=42)
    svc.fit(X, y)

    # 畫決策邊界
    xx, yy = np.meshgrid(np.linspace(-3, 3, 200),
                         np.linspace(-3, 3, 200))
    Z = svc.decision_function(np.c_[xx.ravel(), yy.ravel()])
    Z = Z.reshape(xx.shape)

    ax.contourf(xx, yy, Z, levels=[-10, 0, 10],
                colors=['#6a0dad33', '#1e90ff33'], alpha=0.3)
    ax.contour(xx, yy, Z, levels=[0], colors='white', linewidths=1.5)
    ax.contour(xx, yy, Z, levels=[-1, 1], colors='gray',
               linestyles='dashed', linewidths=1)

    # 標出觀測點
    mask_pos = y == 1
    mask_neg = y == -1
    ax.scatter(X[mask_neg, 0], X[mask_neg, 1], c='#9370DB',
               edgecolors='white', s=50, label='Class −1')
    ax.scatter(X[mask_pos, 0], X[mask_pos, 1], c='#4169E1',
               edgecolors='white', s=50, label='Class +1')

    # 標出支援向量
    sv = svc.support_vectors_
    ax.scatter(sv[:, 0], sv[:, 1], facecolors='none',
               edgecolors='#ff6b6b', s=120, linewidths=1.5,
               label=f'Support Vectors ({len(sv)})')

    ax.set_title(f'C = {C}  |  {len(sv)} 個支援向量')
    ax.set_xlabel('X₁'); ax.set_ylabel('X₂')
    ax.legend(fontsize=8, loc='upper right')
    ax.set_xlim(-3, 3); ax.set_ylim(-3, 3)

plt.tight_layout()
plt.savefig('/tmp/svc_c_comparison.png', dpi=100, bbox_inches='tight')
plt.show()
print('完成:已儲存 SVC C 值比較圖')
完成:已儲存 SVC C 值比較圖 觀察:C 越小 → 邊界越窄、支援向量越少(高變異) C 越大 → 邊界越寬、支援向量越多(低變異、高偏差)

區塊 2:展示單一觀測值的影響(Figure 9.5 精神)

try:
    from google.colab import drive
    drive.mount('/content/drive')
    DATA_PATH = '/content/drive/MyDrive/ISLP_data/'
except ImportError:
    DATA_PATH = '/tmp/'

import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from sklearn.svm import SVC

np.random.seed(1)
# 兩群可分離的資料
X1 = np.random.standard_normal((10, 2)) + np.array([1, 1])
X2 = np.random.standard_normal((10, 2)) + np.array([-1, -1])
X = np.vstack([X1, X2])
y = np.array([1]*10 + [-1]*10)

def plot_svc(ax, model, X, y, title):
    xx, yy = np.meshgrid(np.linspace(-4, 5, 200), np.linspace(-4, 5, 200))
    Z = model.decision_function(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape)
    ax.contourf(xx, yy, Z, levels=[-10, 0, 10],
                colors=['#6a0dad33', '#1e90ff33'], alpha=0.3)
    ax.contour(xx, yy, Z, levels=[0], colors='white', linewidths=2)
    ax.contour(xx, yy, Z, levels=[-1, 1], colors='gray',
               linestyles='dashed', linewidths=1)
    mask = y == 1
    ax.scatter(X[mask, 0], X[mask, 1], c='#4169E1', edgecolors='white', s=60)
    ax.scatter(X[~mask, 0], X[~mask, 1], c='#9370DB', edgecolors='white', s=60)
    sv = model.support_vectors_
    ax.scatter(sv[:, 0], sv[:, 1], facecolors='none',
               edgecolors='#ff6b6b', s=100, linewidths=1.5)
    ax.set_title(title); ax.set_xlim(-4, 5); ax.set_ylim(-4, 5)

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# 左:原始最大邊界
svc1 = SVC(kernel='linear', C=1e10, random_state=42)
svc1.fit(X, y)
plot_svc(axes[0], svc1, X, y, '原始最大邊界分類器')

# 右:加一個極端點
X_ext = np.vstack([X, [[2.5, 0.5]]])
y_ext = np.hstack([y, [1]])

svc2 = SVC(kernel='linear', C=1e10, random_state=42)
svc2.fit(X_ext, y_ext)
plot_svc(axes[1], svc2, X_ext, y_ext, '加入一個觀測值後')

plt.tight_layout()
plt.savefig('/tmp/svc_sensitivity.png', dpi=100, bbox_inches='tight')
plt.show()
觀察:一個極端點就讓硬邊界超平面劇烈旋轉(高變異)。 軟邊界 SVC(使用合理 C 值)可降低這種敏感性。

區塊 3:用交叉驗證選擇最佳 C

try:
    from google.colab import drive
    drive.mount('/content/drive')
    DATA_PATH = '/content/drive/MyDrive/ISLP_data/'
except ImportError:
    DATA_PATH = '/tmp/'

import numpy as np
from sklearn.svm import SVC
from sklearn.model_selection import cross_val_score, StratifiedKFold
from sklearn.datasets import make_classification

# 生成中等規模的模擬分類資料
X, y = make_classification(n_samples=200, n_features=10,
                           n_informative=5, n_redundant=2,
                           random_state=42)

C_grid = np.logspace(-3, 3, 13)
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
cv_means = []

for C in C_grid:
    svc = SVC(kernel='linear', C=C, random_state=42)
    scores = cross_val_score(svc, X, y, cv=cv, scoring='accuracy')
    cv_means.append(scores.mean())

best_C = C_grid[np.argmax(cv_means)]
best_acc = max(cv_means)
print(f'最佳 C = {best_C:.4f}')
print(f'最佳 CV 準確率 = {best_acc:.4f}')

# 顯示前 3 個 C 值的結果
for i in np.argsort(cv_means)[-3:][::-1]:
    print(f'  C={C_grid[i]:.4f} → accuracy={cv_means[i]:.4f}')
最佳 C ≈ 0.1–10(取決於模擬資料) 最佳 CV 準確率 ≈ 0.85–0.90 CV 曲線通常呈現 ∩ 形:C 太小 overfit,C 太大 underfit

🎯 應用場景

📧 垃圾郵件分類

數千封郵件,少數邊界模糊(行銷郵件長得像正常郵件)。用軟邊界 SVC 允許少量誤判,換取更穩健的分類規則。C 值透過 CV 調整,平衡誤殺正常郵件 vs 漏掉垃圾郵件。

🏥 疾病篩檢

用生物標記(biomarker)區分健康 vs 患病。硬邊界要求完美分離會導致過度擬合少數極端值。軟邊界允許一些邊界違規,產生更可靠的篩檢閾值。

💳 信用卡詐欺偵測

合法交易和詐欺交易的邊界先天模糊(詐欺者模仿正常行為)。SVC 的 slack 變數直接對應到模型的「容忍度」——多少可疑交易可以放行。C 的選擇直接影響 false positive vs false negative 的權衡。

⚖️ 優缺點對照

✅ 優點

❌ 缺點

🔄 類似技術比較

方法決策邊界對極端值敏感度調參適用情境
最大邊界分類器 §9.1 線性、硬邊界 極敏感(一個點就翻) 無參數 完美線性可分資料(罕見)
支援向量分類器 §9.2(本節) 線性、軟邊界 低(只依賴支援向量) C(CV 選擇) 接近線性可分、容忍少量誤判
邏輯回歸 §4.3 線性 低(所有點加權貢獻) 正則化 λ 需要機率輸出
LDA §4.4 線性 中(依賴所有點的共變異數) 無(假設已知分布) 常態分布假設成立時
SVM(核方法) §9.3 非線性 C + γ(雙參數) 非線性決策邊界

🤖 對 Hermes 的啟發:軟邊界在 Multi-Agent 系統中的應用

🔍 軟邊界思維 → Agent 容錯設計

SVC 的軟邊界哲學可以直接映射到 multi-agent 系統的可靠性設計:

這也印證了 sleep-consolidation skill 的設計:不是所有資訊都重要,只有「支援向量級」的記憶值得固化。

📐 核心公式摘要

\[ \begin{aligned} \text{分類決策:}&\quad \hat{y} = \text{sign}\big(\beta_0 + \sum_{j=1}^{p} \beta_j x_j^*\big) \\[6pt] \text{邊界寬度:}&\quad M = \frac{1}{\|\beta\|} \\[6pt] \text{Slack 條件:}&\quad y_i(\beta_0 + \sum_{j=1}^{p} \beta_j x_{ij}) \ge M(1 - \epsilon_i) \\[6pt] \text{預算限制:}&\quad \sum_{i=1}^{n} \epsilon_i \le C \end{aligned} \]
真正的智慧不在於從不犯錯,而在於知道哪些錯誤值得容忍,以及容忍的限度在哪裡。 —— 靈感來自 ISLP §9.2 的軟邊界哲學
← §9.1 最大邊界分類器 §9.3 支援向量機 →