10.3 PCA 實戰降維#
本節目標#
- 跑完一個完整的 PCA 專案:標準化 → SVD → 投影 → 視覺化。
- 用變異數解釋率(explained variance ratio)科學地選擇保留維度。
- 把 PCA 當成迴歸的預處理,觀察共線性資料上的效果。
- 與第 7.4 節的手算小例子銜接:這次是真實資料 + 完整管線。
第 7.4 節我們已經知道:PCA 就是「找變異數最大的方向」,而這些方向藏在資料矩陣奇異值分解的 \(V\) 裡。這節把它變成真正能用的工具。
10.3.1 資料:sklearn Wine#
Wine 資料有 178 筆酒、13 個化學特徵、3 個品種——特徵多、尺度不一,正是 PCA 的典型舞台。
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_wine
data = load_wine()
X, y = data.data.astype(float), data.target
print("X 形狀:", X.shape) # (178, 13)
print("各特徵量級差異很大:", X.std(axis=0).round(1))
10.3.2 標準化:不可省的第一步#
Proline 這類特徵的數值是數百上千,而某些特徵不到 1。不標準化的話,PCA 只會被大量級特徵主導。標準化讓每個特徵平均 0、標準差 1:
Xm, Xs = X.mean(axis=0), X.std(axis=0)
Xs_[Xs == 0] = 1
Xc = (X - Xm) / Xs # 中心化 + 縮放
線性代數觀點:中心化後,樣本共變異數矩陣為 \(S = \frac{1}{n-1} X_c^T X_c\) ,PCA 的主軸就是 \(S\) 的特徵向量——也就是 \(X_c\) 的右奇異向量。
10.3.3 SVD 與投影#
U, S, Vt = np.linalg.svd(Xc, full_matrices=False)
# 投影座標:Xc @ V 的前 k 行
scores = Xc @ Vt.T # (178, 13)
print("奇異值:", S.round(2)[:5])
變異數解釋率:每個主成分承載的變異數比例是 \(\frac{s_i^2}{\sum_j s_j^2}\) 。
evr = S**2 / np.sum(S**2)
cum = np.cumsum(evr)
fig, ax = plt.subplots(1, 2, figsize=(11, 4))
ax[0].bar(range(1, 14), evr); ax[0].set_title("各主成分變異數解釋率")
ax[1].plot(range(1, 14), cum, "o-")
ax[1].axhline(0.9, ls="--", c="r"); ax[1].set_title("累積解釋率")
for a in ax: a.grid(alpha=0.3)
plt.tight_layout(); plt.show()
k = np.searchsorted(cum, 0.90) + 1
print(f"保留 90% 變異需要 {k} 個主成分") # 通常 8~10
10.3.4 二維散布圖:三個品種分開了嗎#
plt.figure(figsize=(6, 5))
for c, m in zip([0, 1, 2], ["o", "s", "^"]):
plt.scatter(scores[y == c, 0], scores[y == c, 1],
marker=m, label=data.target_names[c], s=30)
plt.xlabel(f"PC1 ({evr[0]:.0%})"); plt.ylabel(f"PC2 ({evr[1]:.0%})")
plt.legend(); plt.grid(alpha=0.3); plt.tight_layout(); plt.show()
兩個主成分就把三個品種幾乎分開——13 維的資訊被壓進 2 維的圖。再看每個原始特徵對 PC1/PC2 的貢獻(loading):Vt[:2].T 的第 \(i\) 列就是第 \(i\) 個特徵的坐標,可以用來解讀「PC1 大致代表什麼」。
10.3.5 PCA 作為迴歸預處理#
PCA 不只為了畫圖。當特徵高度相關時,正規方程的 \(A^T A\) 病態(回想 10.1 節)。先 PCA 去掉共線性再迴歸,就是 principal component regression:
from sklearn.datasets import make_regression
Xr, yr = make_regression(n_samples=200, n_features=10,
effective_rank=3, noise=10, random_state=0)
Xr_c = (Xr - Xr.mean(axis=0)) / Xr.std(axis=0)
# 比較:條件數 before / after PCA 截斷
cond_before = np.linalg.cond(Xr_c.T @ Xr)
U2, S2, Vt2 = np.linalg.svd(Xr_c, full_matrices=False)
Xr_pca = U2[:, :5] * S2[:5] # 保留前 5 個主成分
A2 = np.column_stack([np.ones(len(Xr_pca)), Xr_pca])
cond_after = np.linalg.cond(A2.T @ A2)
print(f"條件數: 原始 {cond_before:.0f} → PCA 後 {cond_after:.0f}")
w_pca = np.linalg.solve(A2.T @ A2, A2.T @ yr)
pred = A2 @ w_pca
print("PCA 迴歸 RMSE:", np.sqrt(np.mean((pred - yr) ** 2)))
effective_rank=3 代表真實訊息只占 3 維,保留 5 個主成分就幾乎不損失預測力,但矩陣條件數大幅改善、數值更穩定。這也順便示範了壓縮:存 \(U_k, S_k, V_k\) 比存整個 \(X\) 便宜得多(第 7 章 SVD 低秩近似的直接應用)。
10.3.6 與 7.4 節的差異與注意事項#
第 7.4 節用玩具資料手算了主軸;本節補上實戰的四件套:
- 標準化——尺度不齊時必做,否則主成分被量級綁架。
- 解釋率曲線——用 \(\{s_i^2\}\) 決定維度,而不是拍腦袋。
- 投影與反投影——
Xc @ Vt.T是壓縮,(U*S) @ Vt可重建檢視失真。 - 對下游的影響——PCA 改變的是特徵的座標系,下游模型(迴歸、分類)因此更穩定。
常見陷阱:PCA 應該在訓練集上估平均值與主軸,再套到測試集;若把測試集資訊混進標準化,就是資料洩漏。
小結與練習#
- PCA 管線:標準化 → SVD → 取前 \(k\) 個奇異向量 → 投影。
- 變異數解釋率 \(\frac{s_i^2}{\sum s_j^2}\) 是選維度的量化依據。
- PCA 截斷 = 低秩近似,可改善迴歸的條件數。
練習:
- (a) 對
load_digits做 PCA,二維投影後按數字類別上色,看看哪些數字在 2 維仍可分。 - (b) 用 Wine 資料畫 loading 圖(
plt.quiver),找出對 PC1 貢獻最大的三個特徵。 - (c) 挑戰:對你自己有興趣的資料(股票報酬、運動數據、問卷)跑完整管線並寫三句解讀。