§6.1 子集選擇法(Subset Selection)

📖 ISLP §6.1 📄 pp. 229–240 ⭐⭐⭐ ⏱️ 約 30 分鐘
Subset Selection AIC BIC Cp Adjusted R² Stepwise

📚 理論核心

📖 課本文獻引用:
James, G., Witten, D., Hastie, T., & Tibshirani, R. (2023). An Introduction to Statistical Learning with Applications in Python. Springer. §6.1 Subset Selection.
💡 核心概念(生活化比喻): 挑選變數就像在自助餐廳選菜——你不是每道菜都夾(全模型 overfitting),也不是只吃白飯(只放截距 underfitting)。子集選擇法給你三種策略:最佳子集(每種組合都試吃一輪再決定,適合菜色少的)、向前逐步(先夾最想吃的,再慢慢補)、向後逐步(先全夾再一道一道放回去)。目標是用最少的變數達成最好的預測,就像用最少的菜吃出最均衡的營養。

🔢 三種子集選擇法

1. 最佳子集選擇(Best Subset Selection)

暴力法。對 \(p\) 個預測變數,嘗試所有 \(2^p\) 種可能組合,對每種組合擬合模型,選出最佳者。

步驟操作
1令 \(\mathcal{M}_0\) 為空模型(僅截距),計算預測誤差
2對 \(k = 1, 2, \ldots, p\):擬合所有 \(\binom{p}{k}\) 個含 \(k\) 個變數的模型,選 RSS 最小者為 \(\mathcal{M}_k\)
3用交叉驗證 / \(C_p\) / AIC / BIC / Adjusted \(R^2\) 從 \(\mathcal{M}_0, \ldots, \mathcal{M}_p\) 中選最佳模型
\[ \text{缺點:}p \approx 40 \text{ 時,}2^p \approx 10^{12}\text{,計算量爆炸 💥} \]

2. 向前逐步選擇(Forward Stepwise Selection)

從空模型開始,每次加入「最有幫助」的一個變數,逐步建構。總共只需擬合約 \(1 + \sum_{k=0}^{p-1} (p-k) \approx 1 + p(p+1)/2\) 個模型。

步驟操作
1從 \(\mathcal{M}_0\)(僅截距)開始
2對 \(k = 0, \ldots, p-1\):在所有 尚未選入 的變數中,選 RSS 最小者加入,得 \(\mathcal{M}_{k+1}\)
3用交叉驗證 / AIC / BIC 從 \(\mathcal{M}_0, \ldots, \mathcal{M}_p\) 中選最佳模型
⚠️ 向前逐步的盲點:它不走回頭路——一旦變數被選入,就不會再被踢出。如果早期選入的變數在後期變得冗餘(與新加入的變數高度相關),它仍會留在模型中。這是向前逐步比最佳子集差的地方,但計算上實惠太多。

3. 向後逐步選擇(Backward Stepwise Selection)

從全模型開始,每次踢出「最沒貢獻」的一個變數。要求 \(n > p\)(樣本數大於變數數)。

步驟操作
1從 \(\mathcal{M}_p\)(全模型)開始
2對 \(k = p, p-1, \ldots, 1\):在所有 目前仍在模型中 的變數中,踢掉 p-value 最大者,得 \(\mathcal{M}_{k-1}\)
3用交叉驗證 / AIC / BIC 從 \(\mathcal{M}_0, \ldots, \mathcal{M}_p\) 中選最佳模型

📏 模型選擇指標

選出 \(\mathcal{M}_0, \mathcal{M}_1, \ldots, \mathcal{M}_p\) 後,用什麼標準挑最終模型?

指標公式越低越好?
\(C_p\)\(C_p = \frac{1}{n}(\text{RSS} + 2d\hat{\sigma}^2)\)✅ 越小越好
AIC\(\text{AIC} = -2\log L + 2d\)✅ 越小越好
BIC\(\text{BIC} = \frac{1}{n}(\text{RSS} + \log(n)\,d\hat{\sigma}^2)\)✅ 越小越好
Adjusted \(R^2\)\(\text{Adj }R^2 = 1 - \frac{\text{RSS}/(n-d-1)}{\text{TSS}/(n-1)}\)✅ 越大越好
💡 BIC vs AIC 的核心差異:BIC 對參數數量的懲罰項是 \(\log(n)\) 而非 AIC 的 \(2\)。當 \(n > 7\) 時 \(\log(n) > 2\),BIC 傾向選擇更簡約的模型。實務上:AIC 適合預測導向,BIC 適合詮釋導向(更傾向選出「真正重要」的少數變數)。

🐍 實作程式碼

# §6.1 子集選擇 — 完整 Python 實作
# 可在 Google Colab 直接執行

import numpy as np
import pandas as pd
import itertools
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import cross_val_score

# ============================================
# 1. 讀取資料(ISLP 課本 Credit 資料集)
# ============================================
url = "https://www.statlearning.com/s/Credit.csv"
credit = pd.read_csv(url)
credit = pd.get_dummies(credit, columns=['Student', 'Gender', 'Married', 'Ethnicity'], drop_first=True)
credit = credit.drop(columns=['Unnamed: 0'])

y = credit['Balance'].values
X_all = credit.drop(columns=['Balance'])
feature_names = X_all.columns.tolist()
X_all = X_all.values
n, p = X_all.shape

print(f"樣本數 n={n}, 預測變數 p={p}")
print(f"若做最佳子集 = 2^{p} = {2**p:,} 個模型 → 不可行,改用向前逐步")

# ============================================
# 2. 向前逐步選擇(Forward Stepwise)
# ============================================
selected = []           # 已選入的變數 index
remaining = list(range(p))  # 尚未選入的變數 index
models_log = []         # 記錄每個 k 的最佳模型

for k in range(p):
    best_rss = float('inf')
    best_var = None
    best_model = None
    
    for j in remaining:
        trial_vars = selected + [j]
        X_trial = X_all[:, trial_vars]
        model = LinearRegression().fit(X_trial, y)
        y_pred = model.predict(X_trial)
        rss = np.sum((y - y_pred) ** 2)
        
        if rss < best_rss:
            best_rss = rss
            best_var = j
            best_model = model
    
    selected.append(best_var)
    remaining.remove(best_var)
    models_log.append({
        'k': k + 1,
        'vars': [feature_names[i] for i in selected],
        'RSS': best_rss,
        'coef_count': len(selected) + 1  # +1 for intercept
    })

# ============================================
# 3. 計算模型選擇指標
# ============================================
sigma2_hat = models_log[-1]['RSS'] / (n - p - 1)  # 用全模型估計 σ²

for m in models_log:
    d = m['coef_count']  # 參數數(含截距)
    m['Cp'] = (m['RSS'] + 2 * d * sigma2_hat) / n
    m['AIC'] = n * np.log(m['RSS'] / n) + 2 * d
    m['BIC'] = n * np.log(m['RSS'] / n) + np.log(n) * d
    m['Adj_R2'] = 1 - (m['RSS'] / (n - d)) / (np.sum((y - np.mean(y))**2) / (n - 1))

# ============================================
# 4. 輸出結果
# ============================================
results_df = pd.DataFrame(models_log)
results_df = results_df.round({'RSS': 0, 'Cp': 1, 'AIC': 0, 'BIC': 0, 'Adj_R2': 4})

print("\n📊 向前逐步選擇結果:")
print(results_df[['k', 'Cp', 'AIC', 'BIC', 'Adj_R2']].to_string(index=False))

# 找出各指標的最優模型
best_cp_k = results_df.loc[results_df['Cp'].idxmin(), 'k']
best_bic_k = results_df.loc[results_df['BIC'].idxmin(), 'k']
best_adjr2_k = results_df.loc[results_df['Adj_R2'].idxmax(), 'k']

print(f"\n✅ C_p 最優:{best_cp_k} 個變數")
print(f"✅ BIC 最優:{best_bic_k} 個變數(最簡約)")
print(f"✅ Adj-R² 最優:{best_adjr2_k} 個變數")

# 顯示 BIC 最優模型的變數
best_bic_vars = results_df.loc[results_df['BIC'].idxmin(), 'vars']
print(f"\n🏆 BIC 選出的關鍵變數:{best_bic_vars}")

# ============================================
# 5. 交叉驗證確認
# ============================================
print("\n🔬 5-fold CV 確認 BIC 最優模型:")
X_best = X_all[:, [feature_names.index(v) for v in best_bic_vars]]
model_best = LinearRegression()
cv_scores = cross_val_score(model_best, X_best, y, cv=5, scoring='neg_mean_squared_error')
rmse_cv = np.sqrt(-cv_scores.mean())
print(f"5-Fold CV RMSE = {rmse_cv:.1f}")

# 對比全模型的 CV
model_full = LinearRegression()
cv_scores_full = cross_val_score(model_full, X_all, y, cv=5, scoring='neg_mean_squared_error')
rmse_cv_full = np.sqrt(-cv_scores_full.mean())
print(f"全模型 (p={p}) 5-Fold CV RMSE = {rmse_cv_full:.1f}")
print(f"➡️ 變數從 {p} 降到 {best_bic_k},CV RMSE 變化: {rmse_cv:.1f} vs {rmse_cv_full:.1f}")

📊 預期輸出(Credit 資料集)

🌍 應用場景

🏥 醫學研究 — 找出關鍵生物標記

你有 500 個基因表現量當預測變數,想找出真正影響疾病風險的那幾個。向前逐步 + BIC 能從 500 個中自動篩出 5-10 個最有預測力的基因,讓後續驗證實驗的成本大幅降低。

📈 信用評分建模

銀行手上有 50+ 個客戶特徵(收入、負債比、職業、學歷...),但法規要求模型必須「可解釋」。向前逐步 + Adjusted R² 能選出 8-12 個核心變數,讓風控部門能清楚說明白「為什麼這個人被拒絕」。

🛒 電商轉換率優化

你有 200 個使用者行為特徵,想找出哪些真正影響購買轉換。向後逐步(從全模型開始踢)適合 n 大 p 中的情境,確保不遺漏任何潛在關鍵因子。

⚖️ 優缺點分析

✅ 優點

❌ 缺點

🆚 與其他方法的比較

方法何時用計算量全域最優?
最佳子集p ≤ 15\(O(2^p)\) 💥✅ 是
向前逐步p > n 也可用,預設首選\(O(p^2)\)❌ 否
向後逐步n > p,且懷疑大部分變數都有用\(O(p^2)\)❌ 否
Lasso (§6.2)p 很大、需要自動變數篩選\(O(np)\)❌ 但 convex
Ridge (§6.2)全部變數都要保留、只需收縮\(O(np)\)N/A
💬 關鍵金句: 「變數選擇的本質不是找到『最好』的模型,而是找到『夠好且夠簡單』的模型。」
— 統計學習領域的普遍共識

🧠 自我內化

🔗 對 Hermes 架構的啟發: 子集選擇的哲學和 AI Agent 的工具選擇(tool selection)有深刻對應——Hermes 有 N 個可用工具/技能,但不是每個任務都要全載入。向前逐步的策略(從最常用的工具開始,按需擴展)比全載入更有效率,BIC 的懲罰項則對應「每個工具都有載入成本(context window 消耗)」。這呼應了我們在 structured-agent-workflow 中對「最小必要工具集」的設計——不是把所有 MCP server 都掛上,而是像向前逐步一樣,從核心工具開始,只在任務真的需要時才擴張。