6.6 Lab: Ridge 與 Lasso(Lab: Ridge & Lasso)

📖 ISLP §6.6 📄 pp. 290–296 ★★☆☆☆ ⏱️ 約 25 分鐘
Ridge Lasso Python 正則化
← 6.5 Lab: 正則化 📑 課程首頁 7.1 多項式迴歸 →

理論背景

課本出處:
James, Witten, Hastie, Tibshirani (2021). An Introduction to Statistical Learning,第六章 Lab:線性模型選擇與正則化,pp. 273-289。
核心概念(生活比喻):
Ridge(脊迴歸):把預算分散給所有部門(大家都少拿一點,但沒有人歸零)。Lasso:直接砍掉表現差的部門(係數直接歸零)。這個 Lab 透過正則化路徑圖視覺化整個收縮過程——觀察每個係數隨著懲罰力度增加而縮小或消失。你不可能什麼都帶。Best Subset 嘗試每種 5 項組合(窮舉,但很慢)。Forward Stepwise 一次加一個(快但貪婪)。Ridge 全部收縮(沒人被拋棄)。Lasso 直接歸零(只有精華存活)。貼到 Google Colab 立刻執行。

公式

\[ \text{Ridge: } \min_\beta \left\{ \sum_{i=1}^n (y_i - X_i\beta)^2 + \lambda \sum_{j=1}^p \beta_j^2 \right\} \]

L2 懲罰將所有係數向零收縮,但永遠不會精確歸零

\[ \text{Lasso: } \min_\beta \left\{ \sum_{i=1}^n (y_i - X_i\beta)^2 + \lambda \sum_{j=1}^p |\beta_j| \right\} \]

L1 懲罰可將係數強制歸零:自動特徵選擇

完整程式碼

正則化路徑視覺化 + 交叉驗證選擇最佳 Alpha

重點:正則化路徑圖(係數收縮 vs lambda)、RidgeCV/LassoCV 最佳 alpha 選擇、係數比較表、預測評估。設計用於 Google Colab。

import numpy as np
import pandas as pd
from sklearn.linear_model import LinearRegression, Ridge, Lasso, RidgeCV, LassoCV
from sklearn.model_selection import train_test_split, cross_val_score, KFold
from sklearn.metrics import mean_squared_error, r2_score
from sklearn.preprocessing import StandardScaler
from itertools import combinations
import warnings
warnings.filterwarnings('ignore')

# 1. Generate synthetic data
np.random.seed(42)
n, p = 200, 30

# Only 5 features matter (sparse true model)
beta_true = np.zeros(p)
beta_true[[0, 1, 4, 9, 15]] = [3.0, -2.0, 1.5, 0.8, -1.2]

X = np.random.randn(n, p)
X[:, 5:] += 0.3 * X[:, :5].sum(axis=1, keepdims=True) / 5
y = X @ beta_true + np.random.randn(n) * 1.5

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=1)

scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

print("Data:", X_train.shape, X_test.shape)
baseline = mean_squared_error(y_test, np.full_like(y_test, y_train.mean()))
print("Baseline MSE:", baseline)

# 2. Best Subset Selection (k=5)
print("\n--- Best Subset (k=5) ---")
best_score = float('inf')
best_combo = None
for combo in combinations(range(p), 5):
    lr = LinearRegression().fit(X_train_scaled[:, combo], y_train)
    mse = mean_squared_error(y_test, lr.predict(X_test_scaled[:, combo]))
    if mse < best_score:
        best_score = mse
        best_combo = combo

true_important = {0, 1, 4, 9, 15}
found = set(best_combo[:5])
print(f"Best 5-feature subset: {sorted(best_combo)}")
print(f"Test MSE: {best_score:.4f}")
print(f"True signal recall: {len(true_important & found)}/5")

# 3. Ridge Regression
print("\n--- Ridge ---")
ridge_cv = RidgeCV(alphas=np.logspace(-3, 3, 50))
ridge_cv.fit(X_train_scaled, y_train)
y_pred_ridge = ridge_cv.predict(X_test_scaled)
print(f"Best alpha: {ridge_cv.alpha_:.4f}")
print(f"Test MSE: {mean_squared_error(y_test, y_pred_ridge):.4f}")

# 4. Lasso Regression
print("\n--- Lasso ---")
lasso_cv = LassoCV(alphas=np.logspace(-3, 1, 50), cv=5,
                   max_iter=10000, random_state=42)
lasso_cv.fit(X_train_scaled, y_train)
y_pred_lasso = lasso_cv.predict(X_test_scaled)
nz = np.sum(lasso_cv.coef_ != 0)
selected = np.where(lasso_cv.coef_ != 0)[0]
print(f"Best alpha: {lasso_cv.alpha_:.4f}, Non-zero coefs: {nz}")
print(f"Selected: {sorted(selected)}")
recall = len(true_important & set(selected)) / len(true_important)
print(f"Recall: {recall:.0%}")
print(f"Test MSE: {mean_squared_error(y_test, y_pred_lasso):.4f}")

# 5. 5-Fold CV Comparison
print("\n--- 5-Fold CV ---")
models = {
    'OLS': LinearRegression(),
    'Ridge': Ridge(alpha=ridge_cv.alpha_),
    'Lasso': Lasso(alpha=lasso_cv.alpha_, max_iter=10000)
}
kf = KFold(n_splits=5, shuffle=True, random_state=42)
for name, model in models.items():
    scores = cross_val_score(model, X_train_scaled, y_train,
                             cv=kf, scoring='neg_mean_squared_error')
    print(f"{name}: CV MSE = {-scores.mean():.4f} (+/- {scores.std():.4f})")

# 6. High-Dimensional: p > n
print("\n--- High-Dim: p=100, n=50 ---")
np.random.seed(0)
beta_big = np.zeros(100)
beta_big[[0, 1, 2]] = [5.0, -3.0, 2.0]
Xb = np.random.randn(50, 100)
yb = Xb @ beta_big + np.random.randn(50) * 0.5
Xb_tr, Xb_te, yb_tr, yb_te = train_test_split(
    Xb, yb, test_size=15, random_state=1)
scb = StandardScaler()
Xb_tr_s = scb.fit_transform(Xb_tr)
Xb_te_s = scb.transform(Xb_te)

ridge_big = RidgeCV(alphas=np.logspace(-2, 2, 30)).fit(Xb_tr_s, yb_tr)
lasso_big = LassoCV(alphas=np.logspace(-2, 1, 30), cv=5,
                    max_iter=10000, random_state=42).fit(Xb_tr_s, yb_tr)

print(f"Ridge (p>n): MSE={mean_squared_error(yb_te, ridge_big.predict(Xb_te_s)):.4f}")
print(f"Lasso (p>n): MSE={mean_squared_error(yb_te, lasso_big.predict(Xb_te_s)):.4f}")
print(f"Lasso selected: {np.sum(lasso_big.coef_ != 0)}/100")
print("\nALL DONE")

加分題:Lasso 係數路徑視覺化

展示真正的訊號特徵(粗線)如何在正則化中存活,而雜訊特徵(細灰線)在低 alpha 時被消滅。

import matplotlib.pyplot as plt

alphas = np.logspace(-3, 1, 100)
coefs = []
for a in alphas:
    lasso = Lasso(alpha=a, max_iter=10000)
    lasso.fit(X_train_scaled, y_train)
    coefs.append(lasso.coef_)
coefs = np.array(coefs)

plt.figure(figsize=(12, 6))
for i in [0, 1, 4, 9, 15]:
    plt.plot(alphas, coefs[:, i], linewidth=2, label=f'Feature {i} (true)')
for i in [2, 3, 5, 6, 7, 8, 10, 11, 12]:
    plt.plot(alphas, coefs[:, i], alpha=0.3, linewidth=0.8, color='gray')
plt.xscale('log')
plt.axvline(lasso_cv.alpha_, color='red', linestyle='--',
            label=f'CV best alpha={lasso_cv.alpha_:.3f}')
plt.xlabel('Alpha (regularization strength)')
plt.ylabel('Coefficient value')
plt.title('Lasso Coefficient Paths: True Signals Survive Longer')
plt.legend(fontsize=8)
plt.tight_layout()
plt.show()

方法比較

方法特徵選擇速度 (p=30)p > n 可用可解釋性
最佳子集明確選 k 個最佳慢:C(p,k)
向前逐步貪婪路徑
Ridge無(全部保留)
Lasso自動歸零是(最多 n 個)
PCR/PLS降維

應用場景

基因組學:20000 個基因,300 位患者

Lasso 是標準工具:自動選出與疾病真正相關的少數基因。Ridge 會保留全部 20000 個基因,每個係數極小(無法解釋)。最佳子集在計算上不可行(C(20000, k) 是天文數字)。

金融:50 個因子,500 個月報酬

交叉驗證的 Ridge 在這裡通常表現最好,因為所有因子可能都有一點影響(總體經濟力量),且 n > p 代表我們撐得住保留全部因子。向前逐步在金融文獻中很受歡迎,因其可解釋性(Fama-MacBeth 傳統)。

優缺點

優點

缺點

關鍵引言

「在高維度中,正則化不是選項——是生存必需品。」
當 p > n 時,OLS 有無限多個完美擬合。你必須押注在簡單性(稀疏性或收縮)上才能泛化。

Self-Reflection

對 Hermes 的啟發:這堂課呼應我們自己的 agent 架構辯論。Lasso 風格「歸零不相關特徵」對應技能修剪(哪些技能真正重要?)。Ridge 風格「全部保留但收縮」對應 RAG 檢索(所有記憶都有貢獻,但以重要性加權)。第六章的關鍵洞察:沒有普遍最佳的方法——正確的選擇完全取決於你的資料結構和你的目標(預測 vs 可解釋性、稠密真相 vs 稀疏真相)。Agent 記憶系統同理:沒有任何單一架構能在所有場景中稱霸(arXiv 2606.24775 本週也得到相同結論)。