ML 学习站
跳到正文

主成分分析 PCA

高维数据线性降维方法, 几何直觉 + 协方差矩阵特征值分解 + sklearn 上手。

40 分钟3 / 34,546
加载中...

主成分分析 PCA

本章问题: 你有 50 个特征描述一个用户 (年龄、收入、点击次数、收藏数、登录频率...), 画图只能画 2 维。怎么办? PCA 是答案——找到"数据散得最开"的方向, 把高维数据投影过去, 信息损失最小。

1. 为什么需要降维?

实际工作中高维数据到处都是:

场景维度问题
用户画像50-500 维画不出图, 模型训练慢
图像1024×1024 像素太多冗余 (相邻像素相似)
基因表达2 万维维度远大于样本数
文本 TF-IDF词表大小 (万级)稀疏 + 噪声多

降维的3 个核心目的:

  1. 可视化 — 降到 2D/3D 画散点图
  2. 去噪 — 小的特征值方向往往是噪声
  3. 加速后续模型 — 维度降 10 倍, 训练快 10 倍

💡 关键直觉: 数据虽然有 50 维, 但很多维度是相关的 (比如"收入"和"消费金额"高度相关)。真正"独立的信息"可能只有 5-10 个。PCA 就是帮你找到这 5-10 个。

2. PCA 的几何直觉

2.1 找"数据散得最开"的方向

想象 2D 平面上有一堆点, 长成一个椭圆:

把所有点投影到 PC1 这一条线上, 就完成了 2D → 1D 降维。投影后的方差 = PC1 对应的特征值 (越大越好)。

🎯 PCA 的核心目标: 找一个方向, 让数据投影过去之后方差最大。方差大 = 信息多。

2.2 为什么是方差最大 = 信息最多?

方差 = 数据"分散程度" = 信息量。

  • 数据全聚在一起 (方差 0) → 没信息, 全是同一个点
  • 数据散得很开 (方差大) → 区分度高, 信息多

方差大区分度好 (分类时还需要看类别, PCA 是无监督, 不看 y)。

2.3 多个主成分

n 维数据 → 最多 n 个主成分, 按方差从大到小排:

PC1 (方差最大)
PC2 (方差次大, 与 PC1 正交)
PC3 (方差第三, 与 PC1、PC2 正交)
...

通常前 2-3 个主成分就解释了 80%+ 方差, 后面的就是噪声。

import numpy as np
import matplotlib.pyplot as plt

# 生成椭圆分布的 2D 数据
np.random.seed(42)
mean = [0, 0]
cov = [[3, 1.5], [1.5, 1]]  # 协方差矩阵
X = np.random.multivariate_normal(mean, cov, 300)

# 计算主成分方向 (= 协方差矩阵的特征向量)
eig_vals, eig_vecs = np.linalg.eigh(np.cov(X.T))
# eig_vecs[:, -1] 是最大特征值对应的特征向量 (PC1)
pc1 = eig_vecs[:, -1]
pc2 = eig_vecs[:, -2]

fig, ax = plt.subplots(figsize=(8, 6))
ax.scatter(X[:, 0], X[:, 1], alpha=0.5, s=20)

# 画主成分方向 (从原点出发)
for i, (vec, val) in enumerate(zip([pc1, pc2], [eig_vals[-1], eig_vals[-2]])):
    ax.arrow(0, 0, vec[0]*3, vec[1]*3, head_width=0.15,
             color='red' if i == 0 else 'blue', linewidth=2,
             label=f'PC{i+1} (λ={val:.2f})')
ax.axhline(0, color='gray', lw=0.5)
ax.axvline(0, color='gray', lw=0.5)
ax.set_title('2D 数据的两个主成分方向')
ax.legend(); ax.grid(alpha=0.3)
plt.show()

3. 数学: 从几何直觉到公式

3.1 协方差矩阵回顾

对于 d 维数据 X (n 行样本 × d 列特征), 先中心化 (每列减均值):

协方差矩阵是 d × d 矩阵:

  • Σ[i][j] = 特征 i 和特征 j 的协方差
  • Σ[i][i] = 特征 i 的方差
  • Σ 对称、半正定

3.2 特征值分解

PCA 的数学核心: 对 Σ 做特征值分解:

  • v_i = 第 i 个主成分方向 (特征向量, 单位长度)
  • λ_i = 第 i 个主成分的方差 (特征值)

把所有 v_i 按 λ 从大到小排成矩阵 W (d × k, 取前 k 列), 降维就是:

📌 PCA = 找协方差矩阵的特征向量, 投影到方差最大的 k 个方向上。

3.3 为什么特征向量就是方差最大的方向?

数学证明 (拉格朗日乘子):

目标: 找 w, 最大化 w^T Σ w, 约束 ‖w‖ = 1

构造 L = w^T Σ w - λ(w^T w - 1) 对 w 求导: 2Σw - 2λw = 0 → Σw = λw ✓

所以每个特征向量都是一个驻点, λ 越大, 该方向的方差越大。λ 是 w 方向上的方差。

4. PCA 的完整步骤

4.1 算法流程

输入: X (n × d), 目标维度 k
1. 中心化: X̃ = X - mean(X)        ← 每列减均值
2. (可选) 标准化: X̃ / std(X)       ← 各特征量纲不同时必做
3. 算协方差矩阵: Σ = (1/(n-1)) X̃ᵀ X̃
4. 特征值分解: Σ = V Λ Vᵀ
5. 取 V 的前 k 列: W = V[:, :k]
6. 投影: Z = X̃ W                  ← n × k, 即降维结果
7. (可选) 反投影重建: X̂ = Z Wᵀ + mean(X)

4.2 标准化必须吗?

必须做的情况:

  • 特征量纲不同 (比如"年龄 0-100"和"收入 0-100万")
  • 否则方差大的特征 (收入) 会主导主成分

不用做:

  • 所有特征同量纲 (比如都是像素值 0-255)
from sklearn.preprocessing import StandardScaler
X_scaled = StandardScaler().fit_transform(X)

4.3 怎么选 k (降多少维)?

两种常用方法:

方法 1: 累计方差贡献率 ≥ 95%

方法 2: 画 scree plot (碎石图), 找"肘部"

特征值从大到小画图, 找曲线急剧变平的位置 = 肘部, 肘部之前的主成分保留。

from sklearn.decomposition import PCA
import matplotlib.pyplot as plt

pca = PCA().fit(X_scaled)
explained = pca.explained_variance_ratio_

# 累计方差贡献率
cumsum = np.cumsum(explained)
fig, axes = plt.subplots(1, 2, figsize=(12, 4))

# 左: 每个主成分贡献率
axes[0].bar(range(1, len(explained)+1), explained)
axes[0].set_xlabel('主成分'); axes[0].set_ylabel('方差贡献率')
axes[0].set_title('每个主成分的解释方差')

# 右: 累计贡献率
axes[1].plot(range(1, len(cumsum)+1), cumsum, 'o-')
axes[1].axhline(0.95, color='red', ls='--', label='95% 阈值')
axes[1].set_xlabel('主成分'); axes[1].set_ylabel('累计贡献率')
axes[1].set_title('累计方差贡献率'); axes[1].legend()
axes[1].grid(alpha=0.3)
plt.tight_layout()
plt.show()

5. sklearn 上手: IRIS 4 维 → 2 维可视化

IRIS 数据 4 维 (花萼长/宽、花瓣长/宽), 人眼看不出 4 维结构。用 PCA 降到 2D:

from sklearn.datasets import load_iris
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
import matplotlib.pyplot as plt

# 1. 加载数据
iris = load_iris()
X, y = iris.data, iris.target  # X: (150, 4), y: (150,)

# 2. 标准化 (IRIS 各特征量纲差异大, 必须做)
X_scaled = StandardScaler().fit_transform(X)

# 3. PCA 降到 2 维
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X_scaled)  # (150, 2)

print(f"PC1 解释方差: {pca.explained_variance_ratio_[0]:.2%}")
print(f"PC2 解释方差: {pca.explained_variance_ratio_[1]:.2%}")
print(f"累计: {(pca.explained_variance_ratio_.sum()):.2%}")
# PC1 解释方差: 72.96%
# PC2 解释方差: 22.85%
# 累计: 95.81%  ← 2 维就保留了 95.8% 信息!

# 4. 画散点图
fig, ax = plt.subplots(figsize=(8, 6))
for label, name, color in [(0, '山鸢尾', 'red'), (1, '变色鸢尾', 'green'), (2, '维吉尼亚鸢尾', 'blue')]:
    mask = y == label
    ax.scatter(X_pca[mask, 0], X_pca[mask, 1], label=name, s=50, alpha=0.7)
ax.set_xlabel(f'PC1 ({pca.explained_variance_ratio_[0]:.1%})')
ax.set_ylabel(f'PC2 ({pca.explained_variance_ratio_[1]:.1%})')
ax.set_title('IRIS 4 维 → 2 维 (PCA)')
ax.legend(); ax.grid(alpha=0.3)
plt.show()

结果: 三类花在 2D 图上清晰分开! 而且只用了 2 个主成分, 保留了 95.8% 信息。这就是 PCA 的威力。

6. PCA 跟 SVD 的关系

PCA 还有第二种实现方式, 工业界更常用 (数值稳定 + 处理稀疏矩阵):

# 方式 1: sklearn 默认用 SVD 实现 PCA
from sklearn.decomposition import PCA
pca = PCA(n_components=2)
Z = pca.fit_transform(X_scaled)

# 方式 2: 直接用 scipy 的 SVD
import numpy as np
U, S, Vt = np.linalg.svd(X_scaled, full_matrices=False)
Z = X_scaled @ Vt[:2].T  # 等价

数学等价性: 对中心化后的 X 算 SVD, 右奇异向量 V 就是主成分方向。

# 验证 PCA == SVD 方向
from sklearn.decomposition import PCA
import numpy as np

X_centered = X - X.mean(axis=0)
pca = PCA(n_components=2).fit(X_centered)
U, S, Vt = np.linalg.svd(X_centered, full_matrices=False)

# PCA 的 components_ 就是 V 的前 k 列
print("PCA components_:", pca.components_.shape)
print("SVD Vt 前 2 行:", Vt[:2].shape)
# 方向相同 (可能差符号, 但代表同一根轴)

📌 实际工程: sklearn 的 PCA 默认走 SVD (scipy 路径), 不显式算协方差矩阵。好处是数值稳定 (不会因矩阵接近奇异而出错), 还能处理 n \ll d 的情况 (如基因数据)。

7. 应用场景

7.1 数据可视化 (最常见)

高维 → 2D/3D 画散点图, 直观看出聚类 / 异常 / 类别分布。

# MNIST 784 维 (28×28 像素) → 2 维
from sklearn.datasets import load_digits
digits = load_digits()
X = digits.data  # (1797, 64) 实际是 8x8 像素

pca = PCA(n_components=2)
X_2d = pca.fit_transform(X)
plt.scatter(X_2d[:, 0], X_2d[:, 1], c=digits.target, cmap='tab10', s=10)
plt.colorbar(); plt.title('MNIST 64 维 → 2 维')
plt.show()

7.2 去噪

PCA 只保留大方差方向, 小方差方向往往是噪声。把数据投影到 top-k 主成分再重建, 等于滤掉了噪声。

# 图像去噪: 把图像 X 看作样本, 算 PCA, 重建
pca = PCA(n_components=50).fit(X_noisy)
X_denoised = pca.inverse_transform(pca.transform(X_noisy))

7.3 特征压缩 + 加速模型

训练前先 PCA 降维, 模型快几倍到几十倍:

from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression

pipe = Pipeline([
    ('scaler', StandardScaler()),
    ('pca', PCA(n_components=20)),
    ('clf', LogisticRegression(max_iter=1000))
])
pipe.fit(X_train, y_train)

7.4 人脸识别 (Eigenfaces)

经典应用: 把人脸图像展平, 用 PCA 提取"主要人脸特征", 脸库压缩 100 倍。

8. PCA 的局限

8.1 只能抓线性关系

PCA 是线性降维。如果数据分布是弯曲的 (瑞士卷数据集), PCA 会失效。

from sklearn.datasets import make_swiss_roll
X, color = make_swiss_roll(n_samples=1000, noise=0.1, random_state=42)
# PCA 投影会"压扁"瑞士卷, 丢失局部结构
# 这时候用 t-SNE / UMAP / Isomap (非线性降维)

8.2 不看标签, 可能损失分类信息

PCA 是无监督——它只看 X 的方差, 不看 y。最大方差方向不一定是分类最有用的方向。分类任务更推荐 LDA (Linear Discriminant Analysis)。

8.3 主成分难解释

PC1 可能是"收入 × 0.7 + 年龄 × 0.5 + 消费 × 0.3"的线性组合, 没有明确业务含义。要可解释性, 用特征选择 (而非降维)。

9. 跟其他降维方法的对比

方法类型保留什么适合场景
PCA线性全局方差最大通用降维、去噪、压缩
LDA线性类间可分性最大分类前降维 (有标签)
t-SNE非线性局部邻域结构可视化高维 (2D/3D)
UMAP非线性局部 + 全局可视化, 比 t-SNE 快
Isomap非线性流形测地距离瑞士卷这类弯曲数据

💡 降维方法选择: 看全局 + 通用 → PCA; 分类 → LDA; 仅可视化 → t-SNE/UMAP; 流形 → Isomap。

10. 实战检查清单

做 PCA 之前先回答这 5 个问题:

  1. 量纲统一了吗? → 没统一先 StandardScaler
  2. 想降多少维? → 累计方差 ≥ 95% 或肘部法
  3. 数据是线性结构吗? → 否, 考虑 t-SNE/UMAP
  4. 有标签吗? → 有标签且做分类, 考虑 LDA
  5. 要可解释吗? → 要的话用特征选择 (SelectKBest) 而非降维

小结

概念一句话
PCA 目标找方差最大的投影方向
核心数学协方差矩阵的特征值分解
必做步骤中心化 + (量纲不同时) 标准化
选 k 方法累计方差 ≥ 95% 或碎石图肘部
跟 SVD 关系等价 (sklearn 默认走 SVD)
典型应用可视化、去噪、特征压缩
最大局限只能抓线性关系

🎯 一句话总结: PCA 是一种无监督、线性、降维方法, 通过找数据方差最大的方向把高维数据压到低维, 尽可能保留信息。它是机器学习工程师的瑞士军刀——遇到高维问题, 先试 PCA。

练习

  1. 基础: 在 IRIS 数据上, 分别用 PCA 降到 1D/2D/3D, 画图比较三类的可分性。
  2. 思考: MNIST (784 维) 降到多少维能保留 95% 方差? 试 n_components=10, 50, 100, 200 跑 LogisticRegression, 看准确率变化。
  3. 进阶: 给一张带噪声的人脸图像 (用 fetch_olivetti_faces 加载), 用 PCA 重建, 比较 n_components=10, 50, 200 的去噪效果。
  4. 对比: 用 t-SNE 也对 MNIST 降维 2D, 比较跟 PCA 的可视化效果 (哪个类分得更开?)。

章末小测验

检验你对《主成分分析 PCA》的掌握程度。

1

pca 的核心概念是?

学完这章?

🎉 你已经学完这门课!

回到课程目录, 或者生成证书 / 推荐下一门课。

这门课在以下学习路径中

当前课程出现在 1 条系统化路径里, 你可以一键生成完整学习计划, 自动跳过已完成章节。

还有疑问? 问问 AI (v19.5)

基于全站 19 门课 68 章内容检索 + LLM 总结, 会引用具体章节作为出处

讨论区(0)

加载评论中...