想像你要從 3 個嫌疑犯中找出真兇,你有 500 條線索。聽起來很好對吧?但如果你仔細看——其中 490 條線索根本是路邊撿到的垃圾資訊,跟案件毫無關係。最可怕的是:只要線索夠多,你永遠能「完美解釋」已知的犯罪現場,但這個解釋拿去預測下一個案件時會完全失靈。這,就是高維度的陷阱。
在統計學習中,當特徵數量 \(p\) 接近或超過觀測數 \(n\)(即 \(p \ge n\) 或 \(p \approx n\)),我們就進入了高維度設定(high-dimensional setting)。這在現代資料科學中極其常見:基因表達資料(\(p \approx 20,000\),\(n \approx 100\))、文本分類(詞袋模型的 \(p\) 等於詞彙量)、甚至是簡單的 One-Hot 編碼都可能讓 \(p\) 輕鬆超越 \(n\)。
課本明確定義:高維度就是 \(p > n\) 的情況。但實際上,即使 \(p\) 只是略小於 \(n\)(例如 \(p = 90\),\(n = 100\)),高維度的問題也已經開始浮現。以下是幾個典型的高維度場景:
| 應用場景 | 典型 \(n\) | 典型 \(p\) | \(p/n\) 比例 |
|---|---|---|---|
| 基因微陣列分析 | ~100 位病患 | ~20,000 基因 | 200× |
| 文本情感分類 | ~1,000 篇評論 | ~50,000 詞彙 | 50× |
| 金融高頻交易 | ~500 天 | ~2,000 技術指標 | 4× |
| 醫學影像 (radiomics) | ~200 位病患 | ~1,500 特徵 | 7.5× |
| One-Hot 編碼(多類別) | ~5,000 筆 | ~10,000 虛擬變數 | 2× |
課本的 Figure 6.22 用一個極簡例子說明問題:當只有 \(n = 2\) 個觀測值、要估計截距和斜率(\(p = 2\) 個參數),無論這兩個點在哪,回歸線都會完美穿過它們——殘差為零、\(R^2 = 1\)。但這條線拿去預測新資料時完全無用。
數學上,這是因為當 \(p \ge n\) 時,設計矩陣 \(\mathbf{X}\) 的秩不足,\(\mathbf{X}^T\mathbf{X}\) 不可逆,正規方程 \(\hat{\boldsymbol{\beta}} = (\mathbf{X}^T\mathbf{X})^{-1}\mathbf{X}^T\mathbf{y}\) 有無限多組解——其中任意一組都能讓 training error 歸零。
某 FinTech 公司有 50 筆歷史詐欺案例,卻收集了 200 個特徵(交易時間、IP 地理位置、裝置指紋、字體渲染特徵……)。用 OLS 訓練後,training AUC = 1.0——「完美模型!」上線三個月後發現:false positive rate 高達 40%,大量正常交易被誤攔。這就是高維度完美擬合的經典後果。
課本 Figure 6.23 展示了一個震撼的模擬:\(n = 20\),逐步加入完全與反應變數無關的雜訊特徵。結果:
更糟的是,\(C_p\)、AIC、BIC 這些我們在第 6.1 節學到的調整指標,在高維度下也會崩潰。原因是它們都依賴 \(\hat{\sigma}^2\) 的估計——而當 \(p \ge n\) 時,\(\hat{\sigma}^2 = 0\)(因為殘差平方和為 0)。Adjusted \(R^2\) 同樣可以輕鬆騙到 1.0。
好消息是:第 6 章前面介紹的方法——forward stepwise selection、ridge regression、lasso、PCR——恰好就是高維度回歸的解決方案。它們的共同策略是:用比 OLS 更不靈活(less flexible)的擬合方式來避免過度擬合。
課本 Figure 6.24 用 lasso 在不同 \(p\) 下的表現說明三個關鍵點:
課本強調:增加特徵是一把雙面刃。訊號特徵(真正與 \(Y\) 有關的)能降低 test error;但雜訊特徵只會增加維度、加劇過度擬合風險。更關鍵的是——即使特徵真的有關聯,估計其係數所帶來的變異數膨脹,可能超過它帶來的偏差減少(bias-variance tradeoff 的維度版本)。
研究人員有 150 位乳癌病患的 20,000 個基因表達值。只有約 50 個基因真正與預後相關。若直接用全部基因建模型,test error 極高(雜訊基因淹沒訊號)。使用 lasso 後,自動選出約 30-80 個基因,test error 大幅下降。關鍵是——選出的基因並非「唯一正確答案」,不同訓練集可能選出不同基因組合(這就是 §6.4.4 要討論的詮釋問題)。
課本在最後一節提出了一個深刻警告:在高維度下,即使 lasso 或 ridge 給出了係數估計,我們也無法確知哪些變數「真正」是預測因子。
原因是極端的多重共線性(multicollinearity):當 \(p\) 很大時,模型中任何一個變數都可以用其他所有變數的線性組合來近似表示。這意味著:
某新聞 App 有 500 個推薦特徵(閱讀歷史、搜尋關鍵字、地點、時間、裝置型號……),但只有 200 位活躍用戶參與 A/B 測試。Lasso 選出了 35 個「重要」特徵——但產品經理不能就此宣稱「這 35 個特徵驅動了點擊率」,因為可能有其他高度相關的特徵才是真正的因果因子。正確的態度是:「這些特徵與點擊率相關,但因果關係需要額外的實驗設計來驗證」。
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
try:
from google.colab import drive
drive.mount('/content/drive')
DATA_PATH = '/content/drive/MyDrive/ISLP_data/'
except ImportError:
DATA_PATH = '/tmp/'
np.random.seed(42)
# 模擬:n=20 觀測值,逐步增加完全無關的雜訊特徵
n = 20
X_true = np.random.randn(n, 1) # 只有 1 個真正訊號特徵
beta_true = np.array([3.0])
y = X_true @ beta_true + np.random.randn(n) * 0.5
# 加入雜訊特徵,觀察 R^2 和 MSE 的變化
p_noise_list = [0, 5, 10, 15, 20, 25]
r2_train, mse_train, mse_test = [], [], []
X_test = np.random.randn(200, 1)
y_test = X_test @ beta_true + np.random.randn(200) * 0.5
for p_noise in p_noise_list:
X_noise = np.random.randn(n, p_noise)
X = np.hstack([X_true, X_noise])
X_test_full = np.hstack([X_test, np.random.randn(200, p_noise)])
# OLS via pseudo-inverse (handles p >= n)
beta_hat = np.linalg.lstsq(X, y, rcond=None)[0]
y_pred_train = X @ beta_hat
y_pred_test = X_test_full @ beta_hat
ss_res = np.sum((y - y_pred_train)**2)
ss_tot = np.sum((y - np.mean(y))**2)
r2_train.append(1 - ss_res/ss_tot if ss_tot > 0 else 1.0)
mse_train.append(np.mean((y - y_pred_train)**2))
mse_test.append(np.mean((y_test - y_pred_test)**2))
# 繪圖
fig, axes = plt.subplots(1, 3, figsize=(14, 4))
axes[0].plot(p_noise_list, r2_train, 'o-', color='#58a6ff', markersize=8)
axes[0].axhline(y=1, color='#3fb950', linestyle='--', label='R²=1 (完美)')
axes[0].set_xlabel('雜訊特徵數'); axes[0].set_ylabel('Training R²')
axes[0].set_title('R² 的謊言'); axes[0].legend()
axes[1].plot(p_noise_list, mse_train, 'o-', color='#d2991d', markersize=8)
axes[1].set_xlabel('雜訊特徵數'); axes[1].set_ylabel('Training MSE')
axes[1].set_title('Training MSE → 0')
axes[2].plot(p_noise_list, mse_test, 's-', color='#f85149', markersize=8)
axes[2].set_xlabel('雜訊特徵數'); axes[2].set_ylabel('Test MSE')
axes[2].set_title('Test MSE 暴增!')
plt.tight_layout()
plt.savefig(DATA_PATH + 'highdim_demo.png', dpi=100, bbox_inches='tight')
plt.show()
print(f'p=25 時 Training R²={r2_train[-1]:.3f}, Test MSE={mse_test[-1]:.1f}')
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
from sklearn.linear_model import Lasso, LassoCV
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
try:
from google.colab import drive
drive.mount('/content/drive')
DATA_PATH = '/content/drive/MyDrive/ISLP_data/'
except ImportError:
DATA_PATH = '/tmp/'
np.random.seed(42)
# 高維度模擬:n=80, p=200,只有 10 個訊號特徵
n, p = 80, 200
n_signal = 10
X = np.random.randn(n, p)
beta_true = np.zeros(p)
beta_true[:n_signal] = np.random.choice([2, -2, 1.5, -1.5], n_signal)
y = X @ beta_true + np.random.randn(n) * 0.8
# 標準化
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# 分割
X_train, X_test, y_train, y_test = train_test_split(
X_scaled, y, test_size=0.3, random_state=42
)
# OLS (pseudo-inverse) — 會過度擬合
beta_ols = np.linalg.lstsq(X_train, y_train, rcond=None)[0]
ols_train_mse = np.mean((y_train - X_train @ beta_ols)**2)
ols_test_mse = np.mean((y_test - X_test @ beta_ols)**2)
# Lasso with CV
lasso_cv = LassoCV(cv=5, random_state=42, max_iter=10000,
alphas=np.logspace(-3, 1, 30))
lasso_cv.fit(X_train, y_train)
lasso_train_mse = np.mean((y_train - lasso_cv.predict(X_train))**2)
lasso_test_mse = np.mean((y_test - lasso_cv.predict(X_test))**2)
n_nonzero = np.sum(lasso_cv.coef_ != 0)
print(f'OLS: Train MSE={ols_train_mse:.4f}, Test MSE={ols_test_mse:.2f}')
print(f'Lasso: Train MSE={lasso_train_mse:.4f}, Test MSE={lasso_test_mse:.2f}')
print(f'Lasso best alpha={lasso_cv.alpha_:.3f}, non-zero coefs={n_nonzero}')
print(f'True signal features: {n_signal}')
# 繪圖:比較係數
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
axes[0].stem(range(p), beta_ols, linefmt='#58a6ff', markerfmt='o', basefmt='k-')
axes[0].axhline(y=0, color='#30363d', linestyle='-')
axes[0].set_title('OLS 係數(過度擬合)')
axes[0].set_xlabel('特徵 index'); axes[0].set_ylabel('Coefficient')
axes[1].stem(range(p), lasso_cv.coef_, linefmt='#3fb950', markerfmt='o', basefmt='k-')
axes[1].axhline(y=0, color='#30363d', linestyle='-')
for i in range(n_signal):
axes[1].axvline(x=i, color='#d2991d', linestyle='--', alpha=0.5, linewidth=0.8)
axes[1].set_title(f'Lasso 係數 (α={lasso_cv.alpha_:.3f}, {n_nonzero} non-zero)')
axes[1].set_xlabel('特徵 index'); axes[1].set_ylabel('Coefficient')
plt.tight_layout()
plt.savefig(DATA_PATH + 'highdim_lasso.png', dpi=100, bbox_inches='tight')
plt.show()
| 方法 | \(p > n\) 可用? | 特徵選擇 | 係數收縮 | 計算複雜度 | 適合情境 |
|---|---|---|---|---|---|
| OLS(最小平方法) | ❌ 不可 | 無 | 無 | \(O(p^3)\) | 僅限 \(n \gg p\) |
| Forward Stepwise | ⚠️ 有限 | ✅ 有 | 無 | 中等 | \(p\) 不太大時可嘗試 |
| Ridge Regression | ✅ 可 | ❌ 全部保留 | ✅ 均勻收縮 | \(O(p^3)\) | 所有特徵都有微弱訊號 |
| Lasso | ✅ 可 | ✅ 稀疏選擇 | ✅ 軟閾值 | 中等 | 稀疏訊號(少數特徵重要) |
| PCR(主成分回歸) | ✅ 可 | ❌ 線性組合 | 間接(截斷) | \(O(p^3)\) | 特徵高度相關 |
| PLS(偏最小平方) | ✅ 可 | ❌ 監督組合 | 間接(截斷) | 中等 | \(Y\) 與特徵方向相關 |
第 6 章從子集選擇(§6.1)出發,經過收縮方法(§6.2:Ridge + Lasso),到維度縮減(§6.3:PCR + PLS),最後抵達高維度(§6.4)——每節都在回答同一個問題的子問題:
| 節次 | 核心問題 | 核心方法 | 關鍵取捨 |
|---|---|---|---|
| §6.1 子集選擇 | 選哪些變數? | Best subset, Forward, Backward | 搜尋廣度 vs 計算成本 |
| §6.2 收縮方法 | 如何懲罰複雜度? | Ridge (L2), Lasso (L1) | 偏差 vs 變異數 |
| §6.3 維度縮減 | 如何壓縮變數? | PCR (無監督), PLS (監督) | 資訊保留 vs 預測相關性 |
| §6.4 高維度 | \(p \ge n\) 時怎麼辦? | 上述方法的極限測試 | 模型複雜度 vs 可詮釋性 |