第4章 PCA算法三步走:中心化、协方差与特征分解
很多人学PCA,一上来就被“特征降维”四个字唬住了。其实拆开看,核心就三个步骤:数据中心化 → 协方差矩阵 → 特征分解。我个人习惯把这比作“把数据重新摆正、量一量各个方向上的分散程度、再找出最肥的那个方向”。说白了,PCA就是个找主方向的过程。
我先用一张图把整体流程铺开,再一步步细讲。你跟着走一遍,后面写代码就顺了。
步骤一:数据中心化
为什么要中心化?我刚开始学的时候也觉得多此一举。直到有一次,我处理一批身高体重数据,发现PCA出来的第一主成分几乎全是均值偏移——说白了,就是数据没在原点站好队,坐标轴被带偏了。
中心化的目标很简单:让每一维特征的均值变成0。数学上就是:
X_centered = X - mean(X, axis=0)
举个例子,假设你有3个样本,每个样本2个特征:
原始数据 X:
[[2.0, 3.0],
[3.0, 5.0],
[4.0, 4.0]]
每列均值: [3.0, 4.0]
中心化后:
[[-1.0, -1.0],
[ 0.0, 1.0],
[ 1.0, 0.0]]
步骤二:协方差矩阵计算
中心化之后,我们要回答一个问题:各个特征之间是怎么“联动”的? 协方差矩阵就是干这个的。
协方差矩阵 C 的维度是 d×d(d是原始特征数)。C[i][j] 表示第 i 个特征与第 j 个特征的协方差。如果C[i][j] > 0,说明这两个特征同向变化;反之间接变化。
C = (1 / (n - 1)) * X_centered.T @ X_centered
注意我用的是 n-1 而不是 n —— 这是样本协方差的无偏估计。我见过不少新手直接用 n,结果算出来的特征值总是偏小一点。虽然对主方向影响不大,但如果你要做方差解释性分析,那差别就出来了。
步骤三:特征分解
到了最关键的一步。协方差矩阵 C 可以被分解为:
C = Q Λ Qᵀ
其中 Λ 是对角矩阵,对角线上的值就是特征值 λ₁ ≥ λ₂ ≥ … ≥ λd。Q 的列向量是对应的特征向量。
特征值的大小,直接反映了“这个方向上的方差有多大”。我打个比方:你从各个角度去看一团点云,有些角度看起来散得很开,有些角度挤成一团。特征值就是那个“散开程度”的量化指标。
特征向量呢?它就是那个“角度”本身——也就是新的坐标轴方向。
# Python 示例:特征分解
import numpy as np
C = np.array([[2.5, 0.8],
[0.8, 1.2]])
eigenvalues, eigenvectors = np.linalg.eigh(C)
# 默认升序,翻转成降序
idx = np.argsort(eigenvalues)[::-1]
eigenvalues = eigenvalues[idx]
eigenvectors = eigenvectors[:, idx]
print("特征值:", eigenvalues)
print("特征向量:\n", eigenvectors)
# 选择 top-k 特征向量,这里 k=1
k = 1
W = eigenvectors[:, :k]
X_reduced = X_centered @ W
三步汇总与代码实战
把三个步骤串起来,就是一个完整的PCA实现。我个人写教学代码时,喜欢把每一步的中间结果都打印出来,方便调试和理解。
import numpy as np
def pca_manual(X, k):
# 1. 数据中心化
mean = np.mean(X, axis=0)
X_centered = X - mean
# 2. 协方差矩阵
n = X.shape[0]
C = (1 / (n - 1)) * (X_centered.T @ X_centered)
# 3. 特征分解
eigenvalues, eigenvectors = np.linalg.eigh(C)
idx = np.argsort(eigenvalues)[::-1]
eigenvalues = eigenvalues[idx]
eigenvectors = eigenvectors[:, idx]
# 4. 取前 k 个特征向量
W = eigenvectors[:, :k]
X_reduced = X_centered @ W
return X_reduced, eigenvalues[:k], W, mean
# 测试
X = np.array([[2.0, 3.0],
[3.0, 5.0],
[4.0, 4.0],
[5.0, 6.0],
[6.0, 5.0]])
X_red, ev, W, mu = pca_manual(X, k=1)
print("降维结果:", X_red.flatten())
print("Top-1 特征值:", ev[0])
步骤对比速查表
| 步骤 | 输入 | 输出 | 关键公式 | 注意事项 |
|---|---|---|---|---|
| 1. 数据中心化 | 原始矩阵 X (n×d) | 中心化矩阵 X_centered | X_centered = X - μ | μ 是列均值;保存 μ 用于后续变换 |
| 2. 协方差计算 | X_centered | 协方差矩阵 C (d×d) | C = (X_centeredᵀ X_centered)/(n-1) | 用 n-1 做无偏估计;C 是对称矩阵 |
| 3. 特征分解 | 协方差矩阵 C | 特征值 λ, 特征向量 Q | C = Q Λ Qᵀ | 使用 np.linalg.eigh();按 λ 降序排列 |
| 4. 投影降维 | 特征向量 Q, 中心化数据 | 降维矩阵 Y (n×k) | Y = X_centered · Q[:, :k] | k 通常通过方差解释率确定 |
关于特征分解的两种实现路径
除了直接对协方差矩阵做特征分解,还有一条路——奇异值分解 (SVD)。你想想看,X_centered 本身就是 n×d 的矩阵,对它做 SVD 可以直接得到右奇异向量,恰好就是特征向量。
# 用 SVD 实现 PCA(更推荐)
U, S, Vt = np.linalg.svd(X_centered / np.sqrt(n-1), full_matrices=False)
# Vt 的行就是特征向量(按奇异值降序)
eigenvectors_svd = Vt.T
# 奇异值的平方就是特征值
eigenvalues_svd = S ** 2
避坑与经验
说几个我实际踩过的坑:
- 中心化后忘记保存均值向量: 训练集中心化了,预测时也要用同样的均值去减测试集,否则结果全乱套。我一开始就因为这个,上线后模型表现直接崩了。
- 特征值符号翻转: 特征分解得到的特征向量方向不唯一(正负都可以)。这不是错误,但会导致不同库跑出来的主成分方向相反。处理方式:统一用第一个元素的符号做对齐。
- k 值拍脑袋选: 我曾经图省事直接选 k=2,结果丢了40%的方差。后来老老实实画方差解释率曲线,才找到合适的拐点。
好了,PCA的三个核心步骤到这里就拆完了。中心化、协方差、特征分解——每个步骤都不复杂,但串起来就是整个降维领域的基石。你动手把代码跑一遍,中间变量都打印出来,会比看十遍文章都管用。