4.4 線性判別分析:從貝氏定理到分類邊界

📖 ISLP §4.4 📄 pp. 145–154 ⭐⭐⭐ 中級 ⏱️ 約 40 分鐘
LDA Bayes classification discriminant QDA
📖 課本引用:James, Witten, Hastie, Tibshirani (2023). An Introduction to Statistical Learning with Applications in Python, §4.4, pp. 145–154.

💡 核心概念:從「誰比較像」到「機率最高的類別」

想像你是百貨公司的專櫃小姐,遠遠看到一個人走過來。你腦袋裡快速運轉:這個人看起來像會買奢侈品、還是平價商品?你的判斷不是亂猜的——你是根據過往經驗中「買奢侈品的人平均長什麼樣」和「買平價商品的人平均長什麼樣」來做比較。線性判別分析 (LDA) 做的就是這件事:對每個類別建立一個「典型樣貌」,然後看新來的樣本跟誰最像。

數學上,LDA 假設每個類別的資料來自一個常態分佈 (Normal distribution),且所有類別共用同一個共變異數矩陣 (covariance matrix)。這聽起來很嚴格,但正是這個假設讓 LDA 的決策邊界是一條直線(所以才叫「線性」判別),並且在高維度下比 Logistic Regression 更穩定。

📐 理論推導

貝氏分類器的回歸

回顧 §2.2 的貝氏分類器:對於一個新觀測值 \(X = x\),我們選擇使 後驗機率 (posterior probability) \( \Pr(Y = k \mid X = x) \) 最大的類別。用貝氏定理展開:

\[ \Pr(Y = k \mid X = x) = \frac{\pi_k \cdot f_k(x)}{\sum_{l=1}^{K} \pi_l \cdot f_l(x)} \]

其中:

LDA 的關鍵假設:\(p=1\)(單一預測變數)

LDA 假設 \(f_k(x)\) 是一個常態分佈:

\[ f_k(x) = \frac{1}{\sqrt{2\pi\sigma^2}} \exp\left(-\frac{(x - \mu_k)^2}{2\sigma^2}\right) \]

關鍵:所有類別共用同一個 \(\sigma^2\)(變異數相同),但每個類別有自己的均值 \(\mu_k\)。把這個代入貝氏定理,取 log 並化簡後,我們得到:

\[ \delta_k(x) = x \cdot \frac{\mu_k}{\sigma^2} - \frac{\mu_k^2}{2\sigma^2} + \log(\pi_k) \]

這就是 LDA 的判別函數 (discriminant function)。對於新觀測值 \(x\),我們計算所有類別的 \(\delta_k(x)\),選擇 \(\delta_k(x)\) 最大的類別。注意:\(\delta_k(x)\) 是 \(x\) 的線性函數——這正是「線性判別分析」名稱的由來。

推廣到 \(p > 1\)(多個預測變數)

當有多個預測變數時,\(X\) 是一個向量,假設它來自多變量常態分佈:

\[ f_k(x) = \frac{1}{(2\pi)^{p/2}|\Sigma|^{1/2}} \exp\left(-\frac{1}{2}(x - \mu_k)^T \Sigma^{-1} (x - \mu_k)\right) \]

判別函數變成:

\[ \delta_k(x) = x^T \Sigma^{-1} \mu_k - \frac{1}{2}\mu_k^T \Sigma^{-1} \mu_k + \log(\pi_k) \]

實務上,我們不知道 \(\mu_k\)、\(\Sigma\)、\(\pi_k\) 的真實值,只能用訓練資料來估計

🐍 Python 實作

從頭實作 LDA(理解原理)

import numpy as np
import pandas as pd
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split

# === 1. 載入資料 ===
iris = load_iris()
X, y = iris.data, iris.target
feature_names = iris.feature_names
class_names = iris.target_names

# 只用前兩個類別(setosa=0, versicolor=1)和前兩個特徵做二元分類
mask = y != 2
X_bin, y_bin = X[mask, :2], y[mask]

X_train, X_test, y_train, y_test = train_test_split(
    X_bin, y_bin, test_size=0.3, random_state=42
)

# === 2. 估計 LDA 參數 ===
classes = np.unique(y_train)
K = len(classes)
n = len(y_train)
p = X_train.shape[1]

# 事前機率
pi = np.array([np.mean(y_train == k) for k in classes])

# 類別均值
mu = np.array([X_train[y_train == k].mean(axis=0) for k in classes])

# 加權共變異數矩陣
Sigma = np.zeros((p, p))
for k in range(K):
    X_k = X_train[y_train == k]
    centered = X_k - mu[k]
    Sigma += centered.T @ centered
Sigma /= (n - K)

Sigma_inv = np.linalg.inv(Sigma)

# === 3. 判別函數(線性分數) ===
def discriminant(x):
    scores = []
    for k in range(K):
        s = x @ Sigma_inv @ mu[k] - 0.5 * mu[k] @ Sigma_inv @ mu[k] + np.log(pi[k])
        scores.append(s)
    return np.argmax(scores)

# === 4. 預測 ===
y_pred = np.array([discriminant(x) for x in X_test])
accuracy = np.mean(y_pred == y_test)
print(f"從頭實作 LDA 準確率: {accuracy:.4f}")

# === 5. 與 sklearn 比對 ===
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
lda_sk = LinearDiscriminantAnalysis()
lda_sk.fit(X_train, y_train)
y_pred_sk = lda_sk.predict(X_test)
accuracy_sk = np.mean(y_pred_sk == y_test)
print(f"sklearn LDA 準確率: {accuracy_sk:.4f}")
print(f"結果一致: {np.array_equal(y_pred, y_pred_sk)}")

使用 sklearn 完整流程(多類別 + 視覺化)

import matplotlib.pyplot as plt
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
from sklearn.metrics import confusion_matrix, classification_report

# 完整 Iris 三類別,全部四個特徵
X_full, y_full = iris.data, iris.target
X_tr, X_te, y_tr, y_te = train_test_split(
    X_full, y_full, test_size=0.3, random_state=42
)

# 訓練 LDA
lda = LinearDiscriminantAnalysis()
lda.fit(X_tr, y_tr)

# 預測
y_hat = lda.predict(X_te)
print(f"準確率: {lda.score(X_te, y_te):.4f}")
print("\n分類報告:")
print(classification_report(y_te, y_hat, target_names=class_names))

# 混淆矩陣
cm = confusion_matrix(y_te, y_hat)
print("混淆矩陣:")
print(cm)

# LDA 投影(降至 2 維做視覺化)
X_lda = lda.transform(X_full)
fig, ax = plt.subplots(figsize=(8, 6))
for i, name in enumerate(class_names):
    ax.scatter(X_lda[y_full == i, 0], X_lda[y_full == i, 1], 
               label=name, alpha=0.7, s=40)
ax.set_xlabel("LD1")
ax.set_ylabel("LD2")
ax.set_title("LDA 投影後的三類別 Iris 資料")
ax.legend()
plt.tight_layout()
plt.show()

# 印出各類別的事前機率與均值
print(f"\n事前機率: {lda.priors_}")
print(f"各類別均值形狀: {lda.means_.shape}")
print(f"解釋變異比: {lda.explained_variance_ratio_}")

📊 LDA vs Logistic Regression 對照表

特性LDALogistic Regression
數學基礎常態分佈假設 + 貝氏定理直接建模 \(P(Y|X)\)
假設X 來自常態分佈、共變異數相同無 X 分佈假設
決策邊界線性(直線/平面)線性(logit 尺度)
穩健性假設符合時更精確偏離常態時較穩健
多類別自然延伸(無需 one-vs-rest)需多項式延伸或 one-vs-rest
類別分離度佳時參數估計較穩定係數可能爆炸(完全分離問題)
降維能力內建(判別軸 = 降維投影)

🔄 延伸:二次判別分析 (QDA)

LDA 假設所有類別共用同一個共變異數矩陣 \(\Sigma\)。如果放寬這個假設——允許每個類別有自己的 \(\Sigma_k\)——就變成了二次判別分析 (Quadratic Discriminant Analysis, QDA)

QDA 的判別函數中會出現 \(x^T \Sigma_k^{-1} x\) 這樣的二次項,因此決策邊界是曲線(二次曲面),而非直線。

選擇 LDA選擇 QDA
訓練資料少、偏誤主導訓練資料多、變異數主導
各類別共變異數結構相似各類別共變異數明顯不同
偏好簡單模型(較低變異)資料足夠支撐複雜模型

🎯 應用場景

📈 信用評分卡

銀行使用 LDA 分析客戶資料(收入、負債比、信用歷史),將客戶分類為「違約」和「不違約」。LDA 的線性分數可以直接轉換為信用評分。

🧬 基因表達分類

給定數千個基因的表達量,LDA 可以找出最具有判別能力的基因組合來區分不同癌症亞型。LDA 的降維特性在這裡特別有用(p ≫ n 問題)。

📧 垃圾郵件過濾

基於詞頻特徵(如「免費」、「中獎」、「限時」等詞的出現次數),LDA 可以判斷郵件是否為垃圾郵件。在特徵接近常態分佈時(經過適當轉換),LDA 表現優異。

⚖️ 優缺點

✅ 優點

⚠️ 缺點

💬 關鍵金句

「LDA 不是魔法——它只是把貝氏定理套上常態分佈,然後讓資料告訴你每個類別『長什麼樣子』。當這個假設合理時,它是最簡潔有效的分類器之一。」

🧠 自我反思(對 Hermes 架構的啟發)

LDA 的設計哲學對 AI agent 的記憶系統有深刻隱喻:每個類別有一個「典型樣貌」(均值 \(\mu_k\)),新來的資訊只需和這些典型樣貌比較即可做出決策。這正是「原型記憶」(prototype memory) 的核心概念——不需要儲存所有原始經驗,只需儲存壓縮後的典型表徵。

在 Hermes 架構中,skill 系統可以視為一種「判別函數」:當新任務來臨時,系統比較任務特徵與各 skill 的「適用範圍描述」,選擇最匹配的 skill。Tree-of-Experience 論文 (arXiv:2606.06960) 更進一步提出結構化經驗樹的概念——這與 LDA 的多層級判別不謀而合:高層節點做粗略分類,低層節點做精細調整。