§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)相比,多了兩個關鍵元素:
\(\epsilon_i\) 描述第 \(i\) 個觀測值「犯規」的程度:
| \(\epsilon_i\) 的值 | 意義 |
|---|---|
| \(\epsilon_i = 0\) | 在邊界的正確側,完全沒犯規 ✅ |
| \(0 < \epsilon_i \le 1\) | 在邊界的錯誤側(穿越邊界),但仍在超平面的正確側 |
| \(\epsilon_i > 1\) | 在超平面的錯誤側——直接被誤分類 ❌ |
\(C\) 是所有 \(\epsilon_i\) 總和的上限。可以理解為「犯規總預算」:
| \(C\) 值 | 邊界 | 支援向量數 | 偏差-變異 |
|---|---|---|---|
| \(C = 0\) | 回到硬邊界(零容忍) | 極少 | 低偏差、高變異 |
| \(C\) 小 | 窄邊界、少量違規 | 少 | 低偏差、高變異 ← overfit |
| \(C\) 大 | 寬邊界、容忍多個違規 | 多 | 高偏差、低變異 ← smoother |
「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
我們用 sklearn.svm.SVC(核心參數 C 和 kernel='linear')展示軟邊界分類器的行為。
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 值比較圖')
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()
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}')
數千封郵件,少數邊界模糊(行銷郵件長得像正常郵件)。用軟邊界 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 + γ(雙參數) | 非線性決策邊界 |
SVC 的軟邊界哲學可以直接映射到 multi-agent 系統的可靠性設計:
kanban skill 中的 kanban_block 機制。這也印證了 sleep-consolidation skill 的設計:不是所有資訊都重要,只有「支援向量級」的記憶值得固化。