📑 本页目录(点开跳转)
05c · SVD 与 PCA 的实践
⏱ 38 分钟 | ⭐⭐⭐ 推导是对的,但没人照着算
🎯 一句话
上一章证明了「主成分 = 协方差矩阵的特征向量」—— 而没有一个正经实现会照着这句话去算。
真实的 PCA 走的是 SVD:更稳、更省,还能直接处理稀疏矩阵。 这一章讲三件事:SVD 和特征分解是什么关系、主成分该留几个、PCA 什么时候会翻车。
📌 上一章的三个结论,本章会一直用到(忘了也不用回头,用到时都会重述一遍): ① 协方差矩阵 Σ = (1/n)·Xcᵀ Xc(Xc = 中心化后的数据),迹 = 总方差 = 特征值之和 ② Σw = λw —— 主成分只能是 Σ 的特征向量,⭐ λ 就是那个方向上的方差 ③ 手算例子:5 个点 → Σ = [[2, 1.8], [1.8, 2]],λ = 3.8 / 0.2,解释方差比 95% / 5%
🧮 一、和 SVD 的关系:为什么实践中没人真去分解协方差矩阵
上一章那两条推导是对的 —— 但没有一个正经实现会照着算。真实的 PCA 走的是 SVD。
两者的关系:一行代数
对中心化后的数据矩阵 $X_c$(n×d)做奇异值分解:
$$X_c = U S V^\top$$
其中 U(n×k)和 V(d×k)的列都是单位正交向量,S 是对角矩阵,对角元 $s_1\ge s_2\ge\dots\ge 0$ 叫奇异值。代进去:
$$\Sigma = \frac1n X_c^\top X_c = \frac1n (USV^\top)^\top(USV^\top) = \frac1n V S^\top U^\top U S V^\top = V\Big(\frac{S^2}{n}\Big)V^\top$$
(中间用了 $U^\top U = I$。)而特征分解写出来是 $\Sigma = W\Lambda W^\top$。两式一对照:
| 特征分解的东西 | 就是 SVD 的什么 |
|---|---|
| 主成分方向 W | ⭐ 右奇异向量 V |
| 特征值 λᵢ(第 i 个主成分的方差) | ⭐ $s_i^2 / n$ |
| 主成分得分 Z = Xc·W | ⭐ $U S$(连乘法都省了) |
用上一章那个手算例子对一下(5 个点,Σ=[[2, 1.8], [1.8, 2]],λ=3.8 / 0.2):$s = [4.359,\ 1.0]$, 则 $s_1^2/n = 19/5 = 3.8$、$s_2^2/n = 1/5 = 0.2$ —— 和特征分解一模一样。
⭐ 四个理由:为什么必须走 SVD
理由 ① 数值稳定(最硬的一条)
算 $X_c^\top X_c$ 这一步会把条件数平方:$\kappa(X^\top X) = \kappa(X)^2$。 条件数平方,意味着你的有效精度直接砍掉一半。
import numpy as np
rng = np.random.RandomState(0)
Q, _ = np.linalg.qr(rng.randn(200, 5))
V, _ = np.linalg.qr(rng.randn(5, 5))
X = Q @ np.diag([1.0, 1e-2, 1e-4, 1e-6, 1e-8]) @ V.T # ⭐ 人造一个病态数据
Xc = X - X.mean(axis=0)
print("cond(X) = %.2e" % np.linalg.cond(Xc)) # 9.96e+07
print("cond(X.T@X) = %.2e" % np.linalg.cond(Xc.T @ Xc)) # 6.79e+15 ⭐ 平方了
lam_eig = np.linalg.eigvalsh(Xc.T @ Xc / len(X))[::-1] # 走协方差特征分解
lam_svd = np.linalg.svd(Xc, compute_uv=False) ** 2 / len(X) # ⭐ 走 SVD
print("最小主成分方差 · 特征分解 =", lam_eig[-1]) # 2.82e-19
print("最小主成分方差 · SVD =", lam_svd[-1]) # 4.99e-19 ← 这个才对
真值是 $(10^{-8})^2/200 = 5\times10^{-19}$。 SVD 给出 4.99e−19(对),协方差特征分解给出 2.82e−19 —— 错了 44%。 病态再严重一点,特征分解甚至会吐出负的方差(数学上不可能,是浮点误差)。
理由 ② 不用把 d×d 那个矩阵造出来
d = 50,000 个特征(文本 TF-IDF 很常见)
→ Σ 是 50000 × 50000 = 25 亿个数,float64 下 20 GB,存都存不下
→ 而 SVD 直接吃 Xc,从来不构造 Σ
理由 ③ n < d 时协方差矩阵一定是奇异的
100 个样本、5000 个特征(组学数据、小样本图像的典型形态):$\Sigma$ 的秩最多是 99, 剩下 4901 个特征值理论上是 0、实际算出来是一堆 ±1e−15 的噪声。 SVD 只会给出 99 个非零奇异值,干干净净。
理由 ④ 只想要前 k 个的时候,有专门的算法
| 方法 | 干什么 / 什么时候用 |
|---|---|
| 截断 SVD | 只求前 k 个奇异值与向量,k 远小于 d 时用 |
| 随机化 SVD | 先随机投影再分解,约 O(n·d·k)。⭐ sklearn 在 d 大时的默认(svd_solver="randomized") |
TruncatedSVD |
不做中心化,所以能直接吃稀疏矩阵。⭐ 文本 TF-IDF 必用(中心化会把稀疏矩阵变稠密,直接爆内存)—— 这就是 LSA |
🔑 所以
sklearn.decomposition.PCA内部就是 SVD,svd_solver参数在选具体走哪种。 你以为你在做"特征分解",实际上一次都没做过。
⚠️ 两个会让你对不上数的小坑
| 坑 | 说明 |
|---|---|
| 主成分的符号是任意的 ⭐ | w 和 −w 都满足 $\Sigma w=\lambda w$,都合法。不同库、不同版本都可能给出相反的符号 —— 别把符号当成稳定语义("PC1 高 = 更富有"换台机器可能就反了) |
| 除以 n 还是 n−1 | 手算除以 n;sklearn 的 explained_variance_ 除以 n−1。绝对值差 n/(n−1) 倍,但 explained_variance_ratio_ 完全一样 |
🔍 二、主成分留几个
主力工具:解释方差比
$$\text{第 }i\text{ 个主成分的解释方差比} = \frac{\lambda_i}{\sum_j \lambda_j} = \frac{\lambda_i}{\mathrm{tr}(\Sigma)}$$
分母就是上一章那条「迹 = 总方差 = 特征值之和」。上一章的手算例子里是 95% / 5%。
五种选法,按靠谱程度排
| 方法 | 怎么做 | 评价 |
|---|---|---|
| 累计阈值 | 取到累计解释方差 ≥ 90%(或 95%、80%)为止 | 最常用,但 90% 这个数字是拍脑袋的,没有理论依据 |
| 肘部法(碎石图) | 画 λ 随 i 下降的曲线,找"拐弯"处 | 直观,但很多真实数据根本没有明显的肘,全靠人眼 |
| Kaiser 准则 | 留 λ > 1 的 | ⚠️ 只在用相关矩阵(即先标准化)时有意义——那时平均特征值恰好是 1。用协方差矩阵时这条毫无道理 |
| 平行分析 | 同形状的随机数据也跑一遍 PCA,只留 λ 超过随机基线的 | 比 Kaiser 严谨,但要多跑几十次 |
| ⭐⭐ 下游任务交叉验证 | 把 k 当成一个普通超参数,在验证集上按最终指标选 | 最靠谱的一条。 你降维不是为了降维,是为了下游模型 —— 那就让下游指标说话 |
🔑 实践建议:先画一次累计解释方差曲线建立量级感(5 个成分就够,还是要 200 个?), 再在那个量级附近拿 3~4 个候选值做交叉验证。别在"90% 还是 95%"上纠结。
⚠️ 三、PCA 的前提与失效场景
六条,前三条是真会翻车的。
① 它只能找线性子空间
PCA 找的是一个平的子空间。数据要是弯的,它就废了。
经典反例:瑞士卷(Swiss roll)
数据本质上是一张【卷起来的二维纸】,内在维度就是 2
但 PCA 只能拿平面去切它 → 两端卷在一起的点被投到同一个位置
🔗 出路是非线性降维:核 PCA(第 6 章 · 核技巧那套把戏原样搬过来, 把内积换成核函数就行)、t-SNE / UMAP(基础教程第 6 章, 只能用来看一眼,不能拿去当特征)、自编码器。
② ⭐ 必须先标准化(除非所有特征本来就同量纲)
⭐ 一句话结论:同一批数据,把收入的单位从"元"换成"万元",第一主成分就从收入变成了年龄。
年收入(元)标准差 ≈ 50000 → 方差 2.5 × 10⁹
年龄(岁) 标准差 ≈ 10 → 方差 100
→ 差 2500 万倍,第一主成分基本就是"年收入"本身
→ 把收入换成【万元】(方差 25),现在年龄反而占主导
为什么:PCA 追的是【方差】,而方差的量纲是"单位的平方"
⭐ 判据不是"要不要标准化",而是"这些特征的方差可不可比"
├─ 收入(元) + 年龄(岁) + 点击数(次) → 必须标准化
└─ 同一张图的 784 个像素(都是 0~255)→ 不标准化反而对
(方差差异此时是【真信息】:边缘像素方差小,本来就该被降权)
③ 💀 方差 ≠ 信息(最容易忽略的一条)
PCA 是无监督的,它压根不看 y。
💀 一个真实会发生的事故形态:
二分类任务,特征里有一个方差极小、但和标签几乎完美相关的变量
(比如某个传感器的微小读数偏移,量级 0.001,却正好区分两类)
PCA 保留 95% 方差 → 这个方向排在最后,被当成噪声丢掉
→ 降维后模型 AUC 从 0.94 掉到 0.71
→ 排查时看不出来:解释方差比 95%,"信息只丢了 5%",报表上一切正常 ⚠️
⭐ 症结:那被丢掉的 5% 方差里,装着 100% 的判别信息
🔑 该补什么:① 降维前后都在验证集上跑一次下游指标,别只看解释方差比; ② 需要"对分类有用的方向"就用 LDA(有监督,最大化类间/类内方差比), 而不是 PCA(基础教程第 6 章有两者的对照)。
④ 对离群点敏感
方差是平方量,一个离得很远的点对协方差矩阵的贡献是平方级的, 足以把 PC1 整个拽过去。这和第 1 章里「一个离群点拽歪最小二乘」是同一个机理 —— 它们背后都是高斯假设那条薄尾巴。缓解:先做稳健的离群点处理,或用 Robust PCA。
⑤ 主成分不可解释
PC1 是「0.31×年龄 − 0.52×收入 + 0.44×点击数 + …」这种所有原始特征的线性组合。 要向业务解释"哪个特征重要"时 PCA 帮不上忙 —— 那种场景该做特征选择而不是特征压缩 (第 5 章出路②:「大多数维度是噪声,删掉它们比压缩更好」)。
⑥ 💀 泄漏:PCA 必须 fit 在训练集上
❌ pca.fit_transform(全部数据) 再切分训练/测试
→ 测试集参与了均值和主成分方向的计算 → 离线分数虚高,上线暴跌
✅ pca.fit(X_train) → 分别 transform;最省事是塞进 Pipeline,
让交叉验证自动只在折内 fit
💡 顺带:白化(whitening)
把每个主成分得分再除以 $\sqrt{\lambda_i}$,让所有方向的方差都变成 1。
PCA → 去掉相关性(各主成分之间协方差为 0)
PCA + 白化 → 去掉相关性【并且】拉平尺度
⚠️ 代价:最小的那几个主成分被放大了 1/√λ 倍
—— 而它们往往正是噪声。白化会把噪声一起放大 ⭐
🔗 这一章连到哪里
| 去哪 | 为什么 |
|---|---|
| 05b · PCA 的推导 | 本章一直在用的 Σ = (1/n)XcᵀXc、Σw = λw、λ = 那个方向上的方差,全都是在那里推出来的 |
| ML基础 06 · 无监督:聚类与降维 | ⭐ 对面是工程视角:TruncatedSVD 什么时候用、PCA 与 LDA 怎么选、泄漏怎么防。本章第三节那六条失效场景,正是那些告诫的来历 |
| 第 6 章 · 核技巧 | 第三节 ① 说 PCA 只能找线性子空间;核 PCA 就是把推导里的内积换成核函数,一步升级成非线性降维 |
| 第 1 章 · 最大似然 | ⭐ 第三节 ④「PCA 对离群点敏感」和那一章「一个离群点拽歪最小二乘」是同一条高斯薄尾巴 |
| ML基础 05 · 评估与过拟合 · 数据这一关 14 | 第三节 ⑥ 那条泄漏事故:前者给五类泄漏的完整清单,后者给实测数字(预处理先于切分,0.984 → 0.849) |
| 推荐算法 05 · 矩阵分解 | ⭐ 同一套 SVD 换个场景:那里分解的是用户 × 物品交互矩阵,拿到的"隐因子"就是这里的主成分 |
| AI基础设施 18c · 剪枝与稀疏化 | 低秩近似的工程版:把权重矩阵做截断 SVD 来压模型,靠的正是「前 k 个奇异值装了大部分能量」这条 |
✅ 检查点
- SVD 的 V、S 分别对应特征分解里的什么?主成分得分怎么直接从 SVD 里拿?
- 为什么实践中不直接对协方差矩阵做特征分解?(说出至少三个理由)
- 主成分留几个,最靠谱的选法是哪个?为什么不是"累计 95%"?
- 有一个方差极小、但和标签几乎完美相关的特征,PCA 会怎么对待它?该怎么避免?
- 什么时候必须标准化,什么时候反而不该?
- 白化在做什么?代价是什么?
👀 答案
- V 的列 = 主成分方向 W;λᵢ = sᵢ²/n;得分 Z = Xc·W = U·S(连那次矩阵乘法都省了)。用上一章的例子验证:s = [4.359, 1.0] → 19/5 = 3.8、1/5 = 0.2 ✅
- ①⭐数值:算 XᵀX 会把条件数平方(κ(XᵀX)=κ(X)²),有效精度砍半——实测最小主成分方差,特征分解算成 2.82e−19、SVD 给 4.99e−19,真值 5e−19,特征分解错 44%;更病态时甚至会吐出负方差。②不用构造 d×d(d=50000 → 25 亿个数、20 GB)。③n < d 时 Σ 必然奇异,多出来的特征值全是浮点噪声。④只要前 k 个时有截断 / 随机化 SVD,且
TruncatedSVD不中心化,能直接吃稀疏矩阵。⭐ sklearn 的 PCA 内部就是 SVD。 - ⭐⭐ 把 k 当成普通超参数,用下游任务在验证集上的指标来选。"95%"那个数字是拍脑袋的,没有理论依据;而且方差多少和下游有没有用是两回事(见下一题)。
- PCA 会把它排在最后、当噪声丢掉——因为 PCA 无监督,压根不看 y。事故形态:保留 95% 方差,AUC 从 0.94 掉到 0.71,而报表上"只丢了 5% 信息"完全看不出问题——⭐那 5% 方差里装着 100% 的判别信息。避免:①降维前后都跑一次下游指标 ②要判别方向就用 LDA(有监督)。
- 方差不可比时必须标准化:收入(元)+年龄(岁) 方差差 2500 万倍,第一主成分基本就是收入;把收入换成万元,第一主成分立刻变成年龄——同一批数据换个单位答案就反了。本来同量纲时不该标准化:同图的 784 个像素都是 0~255,此时方差差异是真信息(边缘像素方差小,本来就该被降权)。
- 把每个主成分得分再除以 √λᵢ,让所有方向的方差都变成 1 —— PCA 去掉相关性,白化在此之上再拉平尺度。⚠️ 代价:最小的那几个主成分被放大了 1/√λ 倍,而它们往往正是噪声。
🛑 可以停在这里
⚡ 走神救援
⭐⭐PCA 与 SVD:对中心化数据做 Xc = U S Vᵀ ⟹ Σ = V(S²/n)Vᵀ,对照 Σ = WΛWᵀ 立刻读出 —— V 就是主成分方向、λᵢ = sᵢ²/n、得分 = U·S(上一章例子:s = [4.359, 1] → 19/5 = 3.8、1/5 = 0.2 ✅)。为什么全走 SVD:①⭐算 XᵀX 把条件数平方(κ(XᵀX)=κ(X)²),有效精度砍半 —— 实测最小主成分方差,特征分解给 2.82e−19、SVD 给 4.99e−19,真值 5e−19,特征分解错 44%;②不用构造 d×d(d=50000 → 25 亿个数、20 GB);③n 小于 d 时 Σ 必然奇异;④截断 / 随机化 SVD 只求前 k 个,
TruncatedSVD不中心化所以能吃稀疏矩阵(文本 LSA)。⭐sklearn 的 PCA 内部就是 SVD。⚠️两个小坑:主成分符号任意(别把符号当语义)、除以 n 还是 n−1 只影响绝对值不影响比值。k 怎么选:累计阈值 / 肘部 / Kaiser(⚠️只在标准化后有意义)/ 平行分析 / ⭐⭐下游任务交叉验证 —— 把 k 当超参数,让最终指标说话("95%"没有理论依据)。六条失效场景:①只能找线性子空间(瑞士卷失效 → 核 PCA / t-SNE / 自编码器)②⭐必须标准化,判据是"方差可不可比":收入(元)+年龄(岁) 方差差 2500 万倍,收入换成万元后第一主成分就从收入变成年龄;而同图的 784 个像素不该标准化 ③💀方差 ≠ 信息:PCA 不看 y,方差极小却和标签近乎完美相关的特征会被当噪声丢掉 —— 保留 95% 方差,AUC 从 0.94 掉到 0.71,报表上"只丢了 5%"完全看不出来,该用 LDA ④对离群点敏感(和最小二乘同一条高斯薄尾巴)⑤主成分不可解释 ⑥💀必须 fit 在训练集上,fit_transform(全部数据)再切分就是泄漏。💡白化 = 再除以 √λ 拉平尺度,⚠️代价是把最小的那几个主成分(往往正是噪声)放大 1/√λ 倍。
下一节 👉 06-核技巧.md
去那里的理由:本章第三节 ① 说 PCA 只能找平的子空间,数据要是弯的它就废了。 第 6 章那套「不去高维空间、却拿到在那里计算的结果」的把戏, 原样套到 PCA 的推导上就是核 PCA —— 之所以能这么套,是因为那两条推导全程只用到内积。