PCA 数学原理:协方差矩阵、特征值与特征向量
很多人学 PCA 上来就调 sklearn,根本不看背后的数学。结果调参调了半天,数据还是降不下去。说实话,我早期也吃过这个亏。
你要是真想用好 PCA,必须先搞明白三个东西:协方差矩阵、特征值、特征向量。它们不是孤立的数学概念,而是 PCA 从数据里提取主方向的“引擎”。
协方差矩阵——数据变量间的“关系网”
想象一下,你有一张表格,每列是一个特征。那协方差矩阵就是这张表格的“缩影”。它告诉我两件事:
- 每个特征自己波动有多大(对角元素——方差)
- 不同特征之间怎么“配合”变化的(非对角元素——协方差)
举个例子。假设你记录了一个人的身高和体重。这两个特征大概率会一起变大变下。协方差矩阵里那个非对角元素就是正的,而且数值不小。要是一个特征增长时另一个反而减少,那协方差就是负的。
我给大家看一段 Python 代码。这是我自己习惯用的写法:
import numpy as np
# 假设数据矩阵 X,每行一个样本,每列一个特征
X = np.array([[2.5, 2.4],
[0.5, 0.7],
[2.2, 2.9],
[1.9, 2.2],
[3.1, 3.0],
[2.3, 2.7],
[2.0, 1.6],
[1.0, 1.1],
[1.5, 1.6],
[1.1, 0.9]])
# 中心化(减去均值)
X_centered = X - X.mean(axis=0)
# 协方差矩阵
cov = np.cov(X_centered, rowvar=False)
print(cov)
输出大概是个 2×2 矩阵,主对角线上是两个特征的方差,非对角线上是它们的协方差。我见过不少新手直接跳进特征分解,却不知道协方差矩阵是否已经“中心化”。你想想看,如果数据均值没归零,协方差算出来就有偏差,后面主方向直接歪掉。
np.cov(…, rowvar=False) 时默认会做均值矫正,但你最好自己先 X - X.mean(axis=0) 显式处理一遍。这样万一你有缺失值要手动填补,逻辑不会乱。
特征值与特征向量——找到“主方向”
协方差矩阵建好了,下一步就是特征分解。为什么要做这个?
说白了,特征向量代表了数据方差最大的方向,而特征值的大小正好等于在这个方向上的方差。特征值越大,说明那个方向的信息越宝贵。
我曾在项目中遇到过一组 100 维的基因表达数据。协方差矩阵是 100×100,特征值从大到小排开,前面 5 个特征值加起来已经占了总方差的 85%。后面的 95 个方向基本没什么变化——全是噪声。你看,这就是特征值在告诉你“哪些维度值得留”。
把数学符号翻译一下:
- 设协方差矩阵 C(方阵且对称)
- 特征方程:C · v = λ · v
- 其中 v 是特征向量,λ 是特征值
用 NumPy 算起来特别简单:
eigenvalues, eigenvectors = np.linalg.eig(cov)
# 按特征值从大到小排序
idx = eigenvalues.argsort()[::-1]
eigenvalues = eigenvalues[idx]
eigenvectors = eigenvectors[:, idx]
print("特征值:", eigenvalues)
print("特征向量:\n", eigenvectors)
核心流程一张图说清
下面这张 SVG 图把 PCA 的数学环节串起来了。从原始数据到最终降维,每一步都不白走。
特征值排序与主成分选取
特征值排好序后,接下来要回答一个问题:到底保留几维?
我个人习惯用“累积方差贡献率”来拍板。你算一下前 k 个特征值之和除以所有特征值之和。如果这个比值超过 90%(或 85%,看你的业务容忍度),我就选 k。
举个例子,还是刚才那 100 维基因数据:
| 主成分序号 | 特征值 | 方差贡献率 | 累积贡献率 |
|---|---|---|---|
| 1 | 42.3 | 42.3% | 42.3% |
| 2 | 25.1 | 25.1% | 67.4% |
| 3 | 12.8 | 12.8% | 80.2% |
| 4 | 6.2 | 6.2% | 86.4% |
| 5 | 4.8 | 4.8% | 91.2% |
看到没?到第5主成分就已经覆盖了91.2%的信息。后面95个成分加在一起才不到9%——基本都是噪声或者测量误差。我一般会再加上一个“肘部法则”看看累积曲线,但像这种数据,前5个就够了。
代码实战:从协方差到降维
把上面流程用 Python 串起来,你就能用原生的 NumPy 实现 PCA:
def pca_numpy(X, n_components):
# 1. 中心化
X_centered = X - X.mean(axis=0)
# 2. 协方差矩阵
cov = np.cov(X_centered, rowvar=False)
# 3. 特征分解
eigenvalues, eigenvectors = np.linalg.eig(cov)
# 4. 排序
idx = eigenvalues.argsort()[::-1]
eigenvalues = eigenvalues[idx]
eigenvectors = eigenvectors[:, idx]
# 5. 取前 n_components 个特征向量
W = eigenvectors[:, :n_components]
# 6. 投影
Y = X_centered @ W
return Y, eigenvalues, eigenvectors
# 测试
Y_pca, evals, evecs = pca_numpy(X, n_components=1)
print("降维后数据形状:", Y_pca.shape)
这段代码不到 15 行。你把它跟上面那张图对比一下,每一步都有对应。我建议你找个真实数据跑一遍,比如 sklearn 的 iris 数据集,用这个手写 PCA 和 sklearn 的 PCA 结果比一比。你会发现特征值和特征向量完全一致——嗯,数学不会骗人。
• 协方差矩阵是对称的,特征向量互相正交,所以 PCA 找到的主成分是不相关的。
• 特征值 = 方差。保留大特征值对应的方向,丢掉小特征值的方向。
• 中心化是必要前提。如果你忘了,协方差矩阵会包含均值信息,主方向就偏了。