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 的数学环节串起来了。从原始数据到最终降维,每一步都不白走。

PCA 数学原理流程图 原始数据 X 中心化 (X - μ) 协方差矩阵 C 特征分解 选择前 k 个 降维后 Y 关键理解: • 协方差矩阵把原始数据“压缩”成一个矩阵,包含了所有变量间的线性关系。 • 特征值的大小直接告诉你每个方向上的方差(信息量),特征向量告诉你方向。 • 选择前 k 个特征向量组成投影矩阵,就能将原始数据映射到低维空间同时保留最大方差。

特征值排序与主成分选取

特征值排好序后,接下来要回答一个问题:到底保留几维?

我个人习惯用“累积方差贡献率”来拍板。你算一下前 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个就够了。

曾经踩过的坑:有一次我直接保留了所有特征值大于1的主成分(经验法则),结果发现第6个特征值是0.98,也顺手纳入了。最后降维后的分类模型反而变差了。后来一查,那个方向完全是随机噪声,根本不该留。所以别迷信经验法则,算累积方差贡献率才靠谱。

代码实战:从协方差到降维

把上面流程用 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 找到的主成分是不相关的。
• 特征值 = 方差。保留大特征值对应的方向,丢掉小特征值的方向。
• 中心化是必要前提。如果你忘了,协方差矩阵会包含均值信息,主方向就偏了。