想像你是一位人資主管,要從 100 份履歷中挑出 5 位候選人。每份履歷有 50 個欄位:年齡、學歷、工作年資、技能數、語言能力、證照數……。直接看 50 個維度會讓你頭昏眼花。但如果你發現「年齡 + 工作年資 ≒ 經驗指標」,而「技能數 + 證照數 ≒ 專業指標」,把 50 個欄位壓縮成 3-5 個「綜合指標」再評估,效率立刻暴增——這就是維度縮減(Dimension Reduction)的核心精神。
前兩節我們學到兩種控制變異的方法:子集選擇(直接挑幾個變數)和收縮(把係數往零壓)。兩者都用「原始變數」X₁, X₂, …, Xp。現在第三條路出現了:先創造新的「綜合變數」,再用這些綜合變數跑迴歸。
然後用這些 Zₘ 來擬合:
當 M < p 時,我們把問題從「估計 p+1 個 β」降到了「估計 M+1 個 θ」——這就是「縮減」的由來。關鍵眉角在於:φⱼₘ 怎麼選?本節介紹兩種方法:
想像你把 100 個城市的「人口數」和「廣告支出」畫成散佈圖(課本 Figure 6.14)。這兩個變數有明顯的線性關係——人口多的城市通常廣告支出也高。PCA 問:如果只能畫一條線來代表這整坨資料,這條線該怎麼畫?
數學上,PC1 的方向由 負載量(loadings) 決定:
約束條件:φ₁₁² + φ₂₁² = 1(避免無限放大)。得到的 zᵢ₁ 稱為主成分分數(principal component scores)——每個城市從兩個數字變成一個數字。
# PCA 示範 — 用 sklearn 對廣告資料做主成分分析
try:
from google.colab import drive
drive.mount('/content/drive')
DATA_PATH = '/content/drive/MyDrive/ISLP_data/'
except ImportError:
DATA_PATH = '/tmp/'
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
# ponytail: 使用課本 pop vs ad 模擬資料展示 PCA 幾何
np.random.seed(1)
n = 100
pop = np.random.normal(40, 10, n)
ad = 5 + 0.5 * pop + np.random.normal(0, 3, n)
X = np.column_stack([pop, ad])
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
pca = PCA()
pca.fit(X_scaled)
print("主成分解釋變異比例:", pca.explained_variance_ratio_)
print("PC1 負載量 (loadings):", pca.components_[0])
print("PC2 負載量 (loadings):", pca.components_[1])
Z = pca.transform(X_scaled)
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
axes[0].scatter(pop, ad, alpha=0.6, edgecolors='k', linewidth=0.5)
axes[0].set_xlabel('Population'); axes[0].set_ylabel('Ad Spending')
axes[0].set_title('Original Data')
# 畫 PC1 方向
mean_p, mean_a = pop.mean(), ad.mean()
pc1_dir = pca.components_[0] * np.array([pop.std(), ad.std()])
axes[0].arrow(mean_p, mean_a, pc1_dir[0]*15, pc1_dir[1]*15,
color='green', width=0.5, head_width=1.5, label='PC1')
axes[0].arrow(mean_p, mean_a, -pc1_dir[0]*15, -pc1_dir[1]*15,
color='green', width=0.5, head_width=1.5)
axes[0].legend()
axes[1].scatter(Z[:, 0], Z[:, 1], alpha=0.6, edgecolors='k', linewidth=0.5)
axes[1].axhline(0, color='gray', linestyle='--')
axes[1].axvline(0, color='gray', linestyle='--')
axes[1].set_xlabel('PC1'); axes[1].set_ylabel('PC2')
axes[1].set_title('PCA-Transformed Data')
plt.tight_layout()
plt.savefig('/tmp/pca_demo.png', dpi=100)
plt.show()
print("PC1 變異: {:.2f}, PC2 變異: {:.2f}".format(
np.var(Z[:, 0]), np.var(Z[:, 1])))
PCR 的做法非常直覺:
關鍵假設:X 變異最大的方向,通常也是跟 Y 最相關的方向。這個假設不保證成立,但實務上常常「夠用」。
# PCR (Principal Components Regression) 示範
try:
from google.colab import drive
drive.mount('/content/drive')
DATA_PATH = '/content/drive/MyDrive/ISLP_data/'
except ImportError:
DATA_PATH = '/tmp/'
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
from sklearn.decomposition import PCA
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import cross_val_score, KFold
from sklearn.preprocessing import StandardScaler
# ponytail: 用模擬資料展示 PCR 的 bias-variance tradeoff
np.random.seed(42)
n, p = 80, 30
X = np.random.normal(0, 1, (n, p))
# Y 只跟前 5 個主成分相關
U, S, Vt = np.linalg.svd(X, full_matrices=False)
Z_true = U[:, :5] @ np.diag(S[:5])
true_coef = np.array([3, -2, 1.5, -1, 0.5])
Y = Z_true @ true_coef + np.random.normal(0, 0.5, n)
# 標準化
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# 用不同 M 做 PCR
kf = KFold(n_splits=10, shuffle=True, random_state=1)
m_range = np.arange(1, min(p+1, 31))
cv_means, cv_stds = [], []
for m in m_range:
pca = PCA(n_components=m)
Z = pca.fit_transform(X_scaled)
lr = LinearRegression()
scores = cross_val_score(lr, Z, Y, cv=kf, scoring='neg_mean_squared_error')
cv_means.append(-scores.mean())
cv_stds.append(scores.std())
# 畫 CV 曲線
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
ax1.plot(m_range, cv_means, 'b-o', markersize=4)
ax1.fill_between(m_range,
np.array(cv_means) - np.array(cv_stds),
np.array(cv_means) + np.array(cv_stds), alpha=0.2)
best_m = int(m_range[np.argmin(cv_means)])
ax1.axvline(best_m, color='green', linestyle='--', label=f'Best M={best_m}')
ax1.set_xlabel('Number of Components M'); ax1.set_ylabel('Cross-Validation MSE')
ax1.set_title('PCR: CV Error vs M')
ax1.legend()
# 畫係數路徑
coefs = []
for m in m_range:
pca_m = PCA(n_components=m)
Z_m = pca_m.fit_transform(X_scaled)
lr_m = LinearRegression().fit(Z_m, Y)
beta_m = pca_m.components_.T @ lr_m.coef_
coefs.append(beta_m)
coefs = np.array(coefs)
for j in range(min(5, p)):
ax2.plot(m_range, coefs[:, j], alpha=0.7, label=f'Var {j+1}')
ax2.axvline(best_m, color='green', linestyle='--')
ax2.set_xlabel('M'); ax2.set_ylabel('Coefficient (original scale)')
ax2.set_title('PCR Coefficient Paths')
ax2.legend(fontsize=8)
plt.tight_layout()
plt.savefig('/tmp/pcr_demo.png', dpi=100)
plt.show()
print(f"Best M = {best_m}, CV MSE = {cv_means[int(best_m)-1]:.4f}")
print(f"Full LS (M={p}) CV MSE = {cv_means[-1]:.4f}")
課本 Figure 6.18 用兩個模擬資料集展示了 PCR 的典型 U 型曲線:M 太小 → 高偏差(漏掉重要訊號);M 太大 → 高變異(接近最小平方法,過度擬合)。關鍵在於交叉驗證找到「甜蜜點」。
PCR 有一個根本的盲點:PCA 看的是「X 自己的變異」,但變異最大的方向 ≠ 最會預測 Y 的方向。想像你的 Y 是「銷售額」,而 X 變數中有一個是「當天氣溫」——氣溫的變異很大,但可能跟銷售額幾乎無關。PCA 會把大量權重分配給氣溫,但對預測銷售額沒有幫助。
PLS 的解法:讓 Y 參與方向選擇。計算第一 PLS 方向時,每個 φⱼ₁ 設為「Y 對 Xⱼ 的簡單線性迴歸係數」——也就是說,跟 Y 相關性愈高的變數,權重愈大。
算出 Z₁ 後,把每個 Xⱼ 對 Z₁ 做迴歸取殘差(去除已解釋的部分),再用殘差重複上述步驟找 Z₂。重複 M 次,最後用全部 Z₁…Zₘ 對 Y 做最小平方法。
# PLS (Partial Least Squares) 示範 — 比較 PCR vs PLS
try:
from google.colab import drive
drive.mount('/content/drive')
DATA_PATH = '/content/drive/MyDrive/ISLP_data/'
except ImportError:
DATA_PATH = '/tmp/'
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
from sklearn.cross_decomposition import PLSRegression
from sklearn.decomposition import PCA
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import cross_val_score, KFold
from sklearn.preprocessing import StandardScaler
# ponytail: 模擬 Y 只跟某幾個特定變數相關的情境,PLS 應優於 PCR
np.random.seed(42)
n, p = 80, 30
X = np.random.normal(0, 1, (n, p))
# Y 跟第 1, 3, 5 個原始變數強相關(不是主成分!)
Y = 3*X[:, 0] - 2*X[:, 2] + 1.5*X[:, 4] + np.random.normal(0, 0.5, n)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
Y_scaled = (Y - Y.mean()) / Y.std() # 對 PLS 建議標準化 Y
kf = KFold(n_splits=10, shuffle=True, random_state=1)
m_range = np.arange(1, 16)
# PCR
pcr_cv = []
for m in m_range:
Z = PCA(n_components=m).fit_transform(X_scaled)
scores = cross_val_score(LinearRegression(), Z, Y, cv=kf,
scoring='neg_mean_squared_error')
pcr_cv.append(-scores.mean())
# PLS
pls_cv = []
for m in m_range:
pls = PLSRegression(n_components=m, scale=False)
scores = cross_val_score(pls, X_scaled, Y_scaled, cv=kf,
scoring='neg_mean_squared_error')
pls_cv.append(-scores.mean())
fig, ax = plt.subplots(figsize=(8, 5))
ax.plot(m_range, pcr_cv, 'b-o', markersize=5, label='PCR')
ax.plot(m_range, pls_cv, 'r-s', markersize=5, label='PLS')
ax.set_xlabel('Number of Components M')
ax.set_ylabel('Cross-Validation MSE')
ax.set_title('PCR vs PLS: CV Error Comparison')
ax.legend()
plt.tight_layout()
plt.savefig('/tmp/pls_vs_pcr.png', dpi=100)
plt.show()
best_pcr = m_range[np.argmin(pcr_cv)]
best_pls = m_range[np.argmin(pls_cv)]
print(f"PCR best M={best_pcr}, CV MSE={min(pcr_cv):.4f}")
print(f"PLS best M={best_pls}, CV MSE={min(pls_cv):.4f}")
print(f"PLS 相對於 PCR 改善: {(min(pcr_cv)-min(pls_cv))/min(pcr_cv)*100:.1f}%")
| 特性 | 子集選擇 (6.1) | Ridge (6.2) | Lasso (6.2) | PCR (6.3.1) | PLS (6.3.2) |
|---|---|---|---|---|---|
| 降維方式 | 直接挑選變數 | 收縮係數 | 收縮+稀疏 | PCA 轉換,非監督 | PLS 轉換,監督式 |
| 使用原始變數? | ✅ 是 | ✅ 是 | ✅ 是 | ❌ 線性組合 | ❌ 線性組合 |
| 可解釋性 | ⭐⭐⭐ 高 | ⭐⭐ 中 | ⭐⭐⭐ 高 | ⭐ 低 | ⭐ 低 |
| 特徵選取 | ✅ 是 | ❌ 否 | ✅ 是 | ❌ 否 | ❌ 否 |
| p ≫ n 適用 | ❌ 不佳 | ✅ 可 | ✅ 可 | ✅ 可 | ✅ 可 |
| 使用 Y 資訊 | ✅(透過 CV) | ✅(透過 CV) | ✅(透過 CV) | ❌(僅 PCA) | ✅(監督式) |
| 調參數 | 選取變數數 k | λ(懲罰強度) | λ(懲罰強度) | M(主成分數) | M(PLS 方向數) |
用近紅外光譜儀掃描樣本得到 1000+ 個波長的吸收值(p ≫ n),要預測樣本的含水量(Y)。PLS 的監督式降維在這個領域是黃金標準——它自動把跟含水量最相關的波長賦予高權重。
銀行有 50+ 個客戶特徵(收入、負債比、過往還款紀錄、職業類別……),但許多彼此高度相關。先用 PCR 將變數壓縮成 5-8 個主成分,再用這些主成分建立違約預測模型,既減少共線性問題,又維持預測力。
基因晶片同時測量 20,000 個基因的表現量(p=20000),但只有 100 個病患樣本(n=100)。PCR 或 PLS 可將 20,000 維壓縮到 10-20 維,再進行癌症亞型分類——這是 p ≫ n 情境的經典解法。
PCA 教我們一個深刻的設計原則:將高維混亂拆解為互相獨立(正交)的低維組件。每個主成分彼此不相關(Cov(Zᵢ, Zⱼ) = 0),所以你可以獨立地分析每個成分的貢獻,而不必擔心交互作用。
這個原則可以直接應用到 AI agent 系統設計: