跳轉到

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\) 是這個最佳化問題的解:

\[ \mathbf{v}_1 = \arg\max_{\|\mathbf{v}\| = 1} \operatorname{Var}(X\mathbf{v}) = \arg\max_{\|\mathbf{v}\| = 1} \mathbf{v}^T S \mathbf{v} \]

其中 \(S\) 是資料的協方差矩陣( \(X\) 已中心化):

\[ S = \frac{1}{n-1} X^T 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 = U \Sigma V^T \quad\Longrightarrow\quad X^T X = V \Sigma^T \Sigma V^T \]

比較兩式立刻得到:

\[ \mathbf{v}_i = V \text{ 的第 } i \text{ 欄}, \qquad \lambda_i = \frac{\sigma_i^2}{n-1} \]

主成分方向就是 \(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))         # 看第一主成分主要由哪些特徵構成

三個重點觀察:

  1. 前兩個主成分就解釋約 96% 變異——4 維資料的本質結構確實近乎 2 維
  2. 散布圖中 setosa 完全分離,versicolor 與 virginica 略有重疊—— 這張 2D 圖幾乎重現了分類問題的難度
  3. 讀 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 維資料

練習

  1. 對鳶尾花資料不標準化直接做 PCA,比較前兩個主成分的解釋比例與散布圖,並解釋差異(提示:花萼長的數值範圍最大)。
  2. 寫函數 inverse_pca(Z, Vt, mean) 把 2 維投影重建回原始維度( \(X \approx ZV_k + \bar{x}\) ),對鳶尾花計算重建誤差。
  3. 用 sklearn.decomposition.PCA 重做鳶尾花實例,核對解釋比例與投影結果(記得它預設只中心化、不標準化)。
  4. 挑一個高維資料集(如 load_digits,64 維),畫 scree plot 並回答:幾個主成分能解釋 90% 變異?把前兩主成分的散布圖依數字標籤上色,哪些數字最難分?