暴力法。對 \(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\) 中選最佳模型 |
從空模型開始,每次加入「最有幫助」的一個變數,逐步建構。總共只需擬合約 \(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\) 中選最佳模型 |
從全模型開始,每次踢出「最沒貢獻」的一個變數。要求 \(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)}\) | ✅ 越大越好 |
# §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}")
你有 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 |
structured-agent-workflow 中對「最小必要工具集」的設計——不是把所有 MCP server 都掛上,而是像向前逐步一樣,從核心工具開始,只在任務真的需要時才擴張。