7.4 應用:PCA 降維#
本節目標#
- 理解降維動機:高維資料「看不了、算不動、藏噪音」
- 掌握 PCA 思想:把資料投影到變異數最大的方向
- 建立等價關係:PCA \(=\) 對協方差矩陣做特徵分解 \(=\) 對中心化資料做 SVD
- 完整實作標準化 → 協方差 →
eigh/svd→ 投影的標準流程 - 用鳶尾花資料集畫出主成分散布圖與 scree plot
為什麼要降維#
真實資料動輒幾百、幾千個特徵:一張 \(28 \times 28\) 手寫數字是 784 維, 一段基因表達資料上萬維。高維帶來三個麻煩:
- 看不了:人只能畫 2D、3D 的圖,100 維的資料無法直接目視
- 算不動:許多演算法的計算量隨維度爆炸(維度詛咒)
- 藏噪音:特徵之間常高度相關,真正的訊息只活在少數幾個方向
降維的目標:用少數幾個新座標,盡量保留資料的變異。
import numpy as np
rng = np.random.default_rng(0)
X = rng.multivariate_normal([0, 0], [[3, 2.4], [2.4, 3]], size=300)
print(X.shape) # (300, 2):兩個高度相關的特徵
print(np.corrcoef(X.T)) # 相關係數約 0.8——一個座標幾乎夠用
畫出來看:點雲是一條斜斜的橢圓長條。沿長條方向走,變異很大; 垂直方向走,點都擠在一起。直覺上「長條方向」就是資料最重要的方向—— 這正是 PCA 要找的東西。
PCA 的思想:變異數最大的方向#
主成分分析把座標系旋轉到「資料攤得最開」的方向:
- 第一主成分 \(PC_1\) :投影後變異數最大的方向(單位向量 \(\mathbf{v}_1\) )
- 第二主成分 \(PC_2\) :與 \(PC_1\) 垂直、變異數次大的方向,依此類推
數學上, \(PC_1\) 是這個最佳化問題的解:
其中 \(S\) 是資料的協方差矩陣( \(X\) 已中心化):
這個最佳化的答案是經典結論: \(\mathbf{v}_1\) 是 \(S\) 的最大特徵值對應的特徵向量, 而投影後的變異數恰好就是特徵值 \(\lambda_1\) 。
import numpy as np
import matplotlib.pyplot as plt
rng = np.random.default_rng(0)
X = rng.multivariate_normal([0, 0], [[3, 2.4], [2.4, 3]], size=300)
Xc = X - X.mean(axis=0) # 中心化
w, V = np.linalg.eigh(Xc.T @ Xc / (len(X) - 1)) # 對稱矩陣用 eigh
plt.figure(figsize=(5, 5))
plt.scatter(Xc[:, 0], Xc[:, 1], s=10, alpha=0.5)
for i, lam in enumerate(w[::-1]): # 由大到小畫主成分
v = V[:, ::-1][:, i] * 2.5 * np.sqrt(lam)
plt.arrow(0, 0, v[0], v[1], width=0.03, color=f"C{i}",
label=f"$PC_{i+1}$ ($\\lambda$={lam:.2f})")
plt.axis("equal"); plt.legend(); plt.title("PCA:找到變異數最大的方向")
plt.show()
箭頭互相垂直、沿著點雲的長短軸——協方差矩陣的特徵向量就是橢圓的主軸。
PCA = SVD:同一件事的兩種算法#
協方差矩陣是對稱半正定的,所以有兩條等價的路:
路線一(特徵分解):直接對 \(S = \frac{1}{n-1}X^TX\) 做 eigh。
路線二(SVD):對中心化資料矩陣 \(X\) 做 SVD:
比較兩式立刻得到:
主成分方向就是 \(X\) 的右奇異向量,主成分變異數就是奇異值平方除以 \(n-1\) 。
實務上偏好 SVD 路線:不必顯式形成 \(X^TX\) (當特徵數很大時, \(X^TX\) 是 \(p \times p\) 大矩陣,且平方運算會放大數值誤差), 這也是 sklearn 內部 PCA 的做法。
import numpy as np
rng = np.random.default_rng(1)
X = rng.random((200, 5)) * np.array([10, 5, 1, 0.5, 0.1])
Xc = X - X.mean(axis=0)
# 路線一:特徵分解協方差矩陣
w, V = np.linalg.eigh(Xc.T @ Xc / (len(X) - 1))
w, V = w[::-1], V[:, ::-1] # eigh 回傳由小到大,翻轉
# 路線二:SVD
U, s, Vt = np.linalg.svd(Xc, full_matrices=False)
w2 = s**2 / (len(X) - 1)
print(np.allclose(w, w2)) # True:兩條路殊途同歸
print(np.allclose(np.abs(V), np.abs(Vt.T))) # 方向可能差一個正負號
(特徵向量的整體正負號不唯一,所以比較時取絕對值。)
完整流程實作#
把標準做法整理成一段可複製執行的程式碼:
import numpy as np
import matplotlib.pyplot as plt
def pca(X, n_components=2, standardize=True):
"""標準 PCA 流程:標準化 → 中心化 → SVD → 投影"""
if standardize:
X = (X - X.mean(axis=0)) / X.std(axis=0) # 標準化:每個特徵均值 0、標準差 1
Xc = X - X.mean(axis=0) # 中心化
U, s, Vt = np.linalg.svd(Xc, full_matrices=False)
Z = Xc @ Vt[:n_components].T # 投影到前 n 個主成分
ratio = s**2 / (s**2).sum() # 變異數解釋比例
return Z, Vt[:n_components], ratio
# 延續上一格的 X(5 個特徵)
Z, components, ratio = pca(X)
print(Z.shape) # (200, 2):5 維壓到 2 維
print(ratio[:2]) # 前兩個主成分解釋了多少變異
plt.scatter(Z[:, 0], Z[:, 1], s=15, alpha=0.7)
plt.xlabel("$PC_1$"); plt.ylabel("$PC_2$")
plt.title("投影到前兩個主成分")
plt.show()
兩個實作細節值得記住:
- 要不要標準化:特徵單位不同(公分 vs 公斤)時必須標準化, 否則「數字最大」的特徵會獨占第一主成分
- 投影就是矩陣乘法: \(Z = XV_k\) ,一行
(X - mean) @ components.T
Scree plot:該保留幾個主成分?#
變異數解釋比例 \(\lambda_i / \sum_j \lambda_j\) 告訴你每個主成分的價值; 把它畫成遞減曲線(scree plot),找拐點決定保留幾個:
import numpy as np
import matplotlib.pyplot as plt
def scree_plot(X, standardize=True):
if standardize:
X = (X - X.mean(axis=0)) / X.std(axis=0)
Xc = X - X.mean(axis=0)
s = np.linalg.svd(Xc, compute_uv=False)
ratio = s**2 / (s**2).sum()
fig, ax = plt.subplots(1, 2, figsize=(10, 4))
ax[0].plot(np.arange(1, len(s)+1), ratio, "o-")
ax[0].set_xlabel("主成分 $i$"); ax[0].set_ylabel("解釋比例")
ax[0].set_title("Scree plot")
ax[1].plot(np.arange(1, len(s)+1), np.cumsum(ratio), "o-", color="tomato")
ax[1].axhline(0.95, ls="--", c="gray")
ax[1].set_xlabel("主成分數 $k$"); ax[1].set_ylabel("累積解釋比例")
ax[1].set_title("累積解釋比例(虛線:95%)")
plt.tight_layout(); plt.show()
scree_plot(X)
常用判準:累積解釋比例達到 90%–95% 所需的最小 \(k\) , 或 scree plot 中曲線「彎下腰」的拐點位置。
實例:鳶尾花資料集 4 維 → 2 維#
鳶尾花資料有 150 朵花、4 個特徵(花萼長寬、花瓣長寬)、3 個品種。 4 維看不見,但 PCA 告訴你它其實「幾乎是 2 維的」:
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
iris = load_iris()
X, y = iris.data, iris.target
Z, components, ratio = pca(X) # 沿用前面的 pca 函數
print("前兩個主成分解釋比例:", ratio[:2].round(3)) # 約 [0.73, 0.23]
plt.figure(figsize=(7, 5))
for label, name, color in zip([0, 1, 2], iris.target_names, ["C0", "C1", "C2"]):
plt.scatter(Z[y == label, 0], Z[y == label, 1],
label=name, s=25, alpha=0.8, color=color)
plt.xlabel(f"$PC_1$(解釋 {ratio[0]:.0%})")
plt.ylabel(f"$PC_2$(解釋 {ratio[1]:.0%})")
plt.legend(); plt.title("鳶尾花:4 維資料的 2 維 PCA 視覺化")
plt.show()
print("各主成分的組成(欄 = 原特徵權重):")
print(np.round(components, 2)) # 看第一主成分主要由哪些特徵構成
三個重點觀察:
- 前兩個主成分就解釋約 96% 變異——4 維資料的本質結構確實近乎 2 維
- 散布圖中 setosa 完全分離,versicolor 與 virginica 略有重疊—— 這張 2D 圖幾乎重現了分類問題的難度
- 讀
components第一列可以發現 \(PC_1\) 是四個特徵的「綜合尺寸」—— 大致就是「這朵花有多大」,這就是主成分的可解釋性
再確認 scree plot 與實例一致:
scree_plot(X) # 鳶尾花的累積曲線:前 2 個主成分就超過 95%
小結與練習#
本節重點
- 高維資料看不了、算不動、藏噪音;降維要「少座標、保變異」
- PCA 找變異數最大的投影方向: \(PC_1\) 是協方差矩陣最大特徵值的特徵向量
- 三位一體:PCA \(=\) 協方差的特徵分解 \(=\) 中心化資料的 SVD( \(\lambda_i = \sigma_i^2/(n-1)\) )
- 標準流程:標準化 → 中心化 → SVD → 投影 \(Z = XV_k\)
- Scree plot 與累積解釋比例(90%–95% 判準)決定保留幾個主成分
- 鳶尾花實例:前兩個主成分解釋約 96%,一張 2D 圖看穿 4 維資料
練習
- 對鳶尾花資料不標準化直接做 PCA,比較前兩個主成分的解釋比例與散布圖,並解釋差異(提示:花萼長的數值範圍最大)。
- 寫函數
inverse_pca(Z, Vt, mean)把 2 維投影重建回原始維度( \(X \approx ZV_k + \bar{x}\) ),對鳶尾花計算重建誤差。 - 用
sklearn.decomposition.PCA重做鳶尾花實例,核對解釋比例與投影結果(記得它預設只中心化、不標準化)。 - 挑一個高維資料集(如
load_digits,64 維),畫 scree plot 並回答:幾個主成分能解釋 90% 變異?把前兩主成分的散布圖依數字標籤上色,哪些數字最難分?