6.3 維度縮減方法

📖 ISLP §6.3 📄 pp. 261–270 ★★★☆☆ ⏱️ 約 35 分鐘
維度縮減 PCA PCR PLS 主成分分析 偏最小平方法 線性組合 監督式降維
← 6.2 收縮方法 📑 課程首頁 6.4 高維度資料 →

6.3.0 前言:為什麼需要「壓縮」變數?

想像你是一位人資主管,要從 100 份履歷中挑出 5 位候選人。每份履歷有 50 個欄位:年齡、學歷、工作年資、技能數、語言能力、證照數……。直接看 50 個維度會讓你頭昏眼花。但如果你發現「年齡 + 工作年資 ≒ 經驗指標」,而「技能數 + 證照數 ≒ 專業指標」,把 50 個欄位壓縮成 3-5 個「綜合指標」再評估,效率立刻暴增——這就是維度縮減(Dimension Reduction)的核心精神。

James, Witten, Hastie, Tibshirani (2023) An Introduction to Statistical Learning with Python, §6.3, pp. 261–270.
相關文獻:Hastie, Tibshirani & Friedman (2009) The Elements of Statistical Learning, §3.5(ridge regression 與 PCR 的數學等價性)。

6.3.0 核心概念:用「線性組合」代替原始變數

前兩節我們學到兩種控制變異的方法:子集選擇(直接挑幾個變數)和收縮(把係數往零壓)。兩者都用「原始變數」X₁, X₂, …, Xp。現在第三條路出現了:先創造新的「綜合變數」,再用這些綜合變數跑迴歸。

\[ Z_m = \sum_{j=1}^{p} \phi_{jm} X_j, \quad m = 1, \dots, M \]
方程式 6.16 — 每個新變數 Zₘ 是原始 p 個變數的線性組合

然後用這些 Zₘ 來擬合:

\[ y_i = \theta_0 + \sum_{m=1}^{M} \theta_m z_{im} + \epsilon_i \]
方程式 6.17 — 只用 M 個新變數做最小平方法

當 M < p 時,我們把問題從「估計 p+1 個 β」降到了「估計 M+1 個 θ」——這就是「縮減」的由來。關鍵眉角在於:φⱼₘ 怎麼選?本節介紹兩種方法:

  1. 主成分迴歸(PCR):用 PCA 找最能解釋 X 變異的方向(非監督式)
  2. 偏最小平方法(PLS):找同時解釋 XY 的方向(監督式)
💡 直覺類比:PCR 像是一位不問你要什麼就先幫你把房間整理好的管家——他按照「東西的變異程度」整理。PLS 則是一位先問你「今天要找什麼」再整理的管家——他把「跟你的需求相關」的東西放在最前面。

6.3.1 主成分分析(PCA)入門

PCA 的幾何直覺

想像你把 100 個城市的「人口數」和「廣告支出」畫成散佈圖(課本 Figure 6.14)。這兩個變數有明顯的線性關係——人口多的城市通常廣告支出也高。PCA 問:如果只能畫一條線來代表這整坨資料,這條線該怎麼畫?

數學上,PC1 的方向由 負載量(loadings) 決定:

\[ Z_1 = 0.839 \times (\text{pop} - \overline{\text{pop}}) + 0.544 \times (\text{ad} - \overline{\text{ad}}) \]
方程式 6.19 — 廣告資料的第一主成分(φ₁₁=0.839, φ₂₁=0.544)

約束條件:φ₁₁² + φ₂₁² = 1(避免無限放大)。得到的 zᵢ₁ 稱為主成分分數(principal component scores)——每個城市從兩個數字變成一個數字。

PCA 的雙重解釋:同一條 PC1 線既是 (1) 投影變異最大的方向,也是 (2) 所有點到這條線的垂直距離平方和最小的線。前者是「保留最多資訊」,後者是「失真最小」——同一件事的兩種說法。

PCA 的 Python 實作

# 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])))
🔑 PCA 使用要點:進行 PCA 前務必先標準化(StandardScaler)。若原始變數單位不同(如「收入:萬元」vs「年齡:歲」),未標準化時,數值範圍大的變數會主導主成分方向。單位相同時(如全部是公斤)可跳過標準化。

6.3.1 主成分迴歸(PCR)

從 PCA 到迴歸:一條龍流程

PCR 的做法非常直覺:

  1. X 做 PCA,得到 M 個主成分 Z₁, …, Zₘ
  2. 用這些 Z 對 Y 做最小平方法迴歸
  3. 用交叉驗證挑選最佳 M

關鍵假設:X 變異最大的方向,通常也是跟 Y 最相關的方向。這個假設不保證成立,但實務上常常「夠用」。

PCR Python 實作

# 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}")

PCR 的 Bias-Variance 取捨

課本 Figure 6.18 用兩個模擬資料集展示了 PCR 的典型 U 型曲線:M 太小 → 高偏差(漏掉重要訊號);M 太大 → 高變異(接近最小平方法,過度擬合)。關鍵在於交叉驗證找到「甜蜜點」。

PCR ≠ 特徵選取:PCR 不是特徵選取方法。每個主成分都是所有 p 個原始變數的線性組合,你無法說「我只用 pop 不用 ad」——Z₁ 同時包含兩者。這使得 PCR 在「可解釋性」上不如 Lasso,但預測表現往往不錯。事實上,PCR 和 ridge regression 在數學上有深刻的聯繫——可以把 ridge 視為 PCR 的「連續版本」。

6.3.2 偏最小平方法(PLS)

PLS 的核心創新:讓 Y 來「監督」降維

PCR 有一個根本的盲點:PCA 看的是「X 自己的變異」,但變異最大的方向 ≠ 最會預測 Y 的方向。想像你的 Y 是「銷售額」,而 X 變數中有一個是「當天氣溫」——氣溫的變異很大,但可能跟銷售額幾乎無關。PCA 會把大量權重分配給氣溫,但對預測銷售額沒有幫助。

PLS 的解法:讓 Y 參與方向選擇。計算第一 PLS 方向時,每個 φⱼ₁ 設為「Y 對 Xⱼ 的簡單線性迴歸係數」——也就是說,跟 Y 相關性愈高的變數,權重愈大

\[ Z_1^{\text{PLS}} = \sum_{j=1}^{p} \hat{\beta}_j^{\text{simple}} X_j, \quad \hat{\beta}_j^{\text{simple}} = \text{Cov}(X_j, Y) / \text{Var}(X_j) \]
PLS 第一方向:權重正比於各變數與 Y 的相關係數

算出 Z₁ 後,把每個 Xⱼ 對 Z₁ 做迴歸取殘差(去除已解釋的部分),再用殘差重複上述步驟找 Z₂。重複 M 次,最後用全部 Z₁…Zₘ 對 Y 做最小平方法。

PLS Python 實作

# 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}%")
PLS 的現實定位:PLS 在化學計量學(chemometrics)領域非常受歡迎,因為光譜資料動輒上千個波長(變數),PLS 的監督式降維特別適合。但在一般統計學習問題中,PLS 的表現通常不比 ridge 或 PCR 好——監督式降維雖然降低偏差,但可能增加變異,兩者互相抵消。

三種方法的優缺點對照

✅ PCR 優點

❌ PCR 缺點

✅ PLS 優點

  • 監督式降維,方向與 Y 相關
  • 在 p ≫ n 的高維情境(如光譜資料)表現優異
  • 同時使用 X 和 Y 的資訊,理論上更有效率

❌ PLS 缺點

  • 監督式未必優於非監督式——可能增加變異
  • 實務上常常不比 ridge / PCR 好
  • 迭代殘差計算可能累積數值誤差
  • 需要同時標準化 X 和 Y

方法比較總表

特性 子集選擇 (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 方向數)

應用場景

🏭 化學計量學 — 近紅外光譜分析(PLS 主場)

用近紅外光譜儀掃描樣本得到 1000+ 個波長的吸收值(p ≫ n),要預測樣本的含水量(Y)。PLS 的監督式降維在這個領域是黃金標準——它自動把跟含水量最相關的波長賦予高權重。

📊 金融風控 — 信用評分建模(PCR 應用)

銀行有 50+ 個客戶特徵(收入、負債比、過往還款紀錄、職業類別……),但許多彼此高度相關。先用 PCR 將變數壓縮成 5-8 個主成分,再用這些主成分建立違約預測模型,既減少共線性問題,又維持預測力。

🧬 基因表現資料 — 癌症分類(PCR/PLS 通用)

基因晶片同時測量 20,000 個基因的表現量(p=20000),但只有 100 個病患樣本(n=100)。PCR 或 PLS 可將 20,000 維壓縮到 10-20 維,再進行癌症亞型分類——這是 p ≫ n 情境的經典解法。

💡 來自 PCA 的系統設計啟發:正交分離原則

PCA 教我們一個深刻的設計原則:將高維混亂拆解為互相獨立(正交)的低維組件。每個主成分彼此不相關(Cov(Zᵢ, Zⱼ) = 0),所以你可以獨立地分析每個成分的貢獻,而不必擔心交互作用。

這個原則可以直接應用到 AI agent 系統設計:

降維不是丟掉資訊,而是用更少的座標軸,畫出同一幅圖。 — ISLP §6.3 核心精神
← 6.2 收縮方法 📑 課程首頁 6.4 高維度資料 →