一、PCA简介与数学原理:从方差最大化到特征值分解
各位,说到降维,PCA是个绕不开的话题。我刚开始做数据分析那会儿,遇到高维数据就头大——几十个特征堆在一起,别说分析了,光看都眼花。直到我真正理解了PCA,才明白什么叫“四两拨千斤”。
简单来说,PCA干的事情就是:在损失尽可能少信息的前提下,把高维数据压缩到低维。怎么做到的呢?核心就一句话——找到数据方差最大的方向。
1.1 感性理解:为什么是方差?
你想想看,数据在某个方向上越分散,说明这个方向包含的信息越多。如果数据在某个方向上几乎挤成一团,那这个方向扔掉也无所谓。
举个例子:假设你有一堆二维数据点。如果它们在x轴方向很分散,在y轴方向几乎没变化,那我只保留x轴坐标就够了。PCA就是想找到这些“重要”的方向。
我曾在项目中遇到过一个情况:客户给了200多个传感器特征,但其实真正驱动变化的只有3到5个潜在因子。用PCA砍下去,模型效果不仅没降,反而更稳定了。这就是抓住了主要矛盾。
1.2 数学推导:从哪里入手?
好,感性认识有了。下面我带你过一遍数学,别怕,我尽量说人话。
假设我们有数据集 X,m个样本,每个样本n维特征,即 X ∈ R^(m×n)。我们要找第一个主成分方向,记为单位向量 w1(|w1| = 1)。
每个样本在这个方向上的投影是 X w1,这是一个m维的向量。投影后的方差是:
Var = (1/m) * (X w1)^T (X w1) = w1^T ( (1/m) X^T X ) w1
中间那块 (1/m) X^T X 是什么?它就是协方差矩阵,记作 C。所以问题变成:
最大化: w1^T C w1
约束: w1^T w1 = 1
这是一个条件极值问题,用拉格朗日乘子法一解,你猜会得到什么?
核心结论:目标函数对w1求导等于零,最后得到 C w1 = λ w1。
这就是特征值方程!w1是协方差矩阵C的特征向量,λ是对应的特征值。
而且你可以证明:投影后的方差,恰恰就等于这个特征值λ。所以:
- 特征值越大 → 该方向方差越大 → 信息越多 → 越重要
- 特征值越小 → 该方向方差越小 → 信息越少 → 可以丢弃
1.3 从第一主成分到所有主成分
找到第一个方向w1后,第二个方向w2怎么找?
很简单:在与w1正交的所有方向里,再找方差最大的那个。以此类推,我们会得到一组相互正交的方向——这就是主成分。
数学上,这等价于对协方差矩阵C做特征值分解:
C = V Λ V^T
其中:
V = [v1, v2, ..., vn] (特征向量矩阵,各列正交)
Λ = diag(λ1, λ2, ..., λn) (特征值对角阵,通常从大到小排列)
取前k个特征向量,组成投影矩阵 W = [v1, v2, ..., vk] ∈ R^(n×k)。降维后的数据为:
Z = X W (Z ∈ R^(m×k))
我就曾经踩过一个坑:做特征值分解之前忘了数据中心化,结果第一个主成分被均值方向带偏了,降维后完全看不出规律。嗯,这里要提醒各位——标准化和中心化是PCA的前置条件,千万不能忘。
1.4 协方差矩阵的其他路径:SVD
实际工程中,直接对协方差矩阵做特征值分解,计算量有点大(尤其是特征维度高的时候)。更常见的做法是用奇异值分解(SVD)。
对数据矩阵X(已中心化)做SVD:
X = U Σ V^T
可以证明:X^T X = V Σ^2 V^T。所以V的列就是主成分方向,奇异值σi与特征值λi的关系是 λi = σi² / m。
SVD的好处是数值稳定性更好,直接对X操作,不用显式计算协方差矩阵。我个人习惯直接用SVD来实现PCA,Python里就是一行 np.linalg.svd 的事。
下表总结了两种方法的差异:
| 方法 | 对象 | 计算内容 | 适用场景 |
|---|---|---|---|
| 特征值分解 | 协方差矩阵 C | 特征值 + 特征向量 | 维度适中,需要显式协方差 |
| 奇异值分解 (SVD) | 原始数据矩阵 X | 奇异值 + 左右奇异向量 | 高维数据,数值稳定性优先 |
1.5 一个简单的代码示例
光说不练假把式,来看一段Python代码。这里我用iris数据集做个演示:
import numpy as np
from sklearn.datasets import load_iris
# 加载数据
iris = load_iris()
X = iris.data # 150个样本,4个特征
# 1. 中心化
X_centered = X - np.mean(X, axis=0)
# 2. 计算协方差矩阵
C = (X_centered.T @ X_centered) / (X_centered.shape[0] - 1)
# 3. 特征值分解
eigenvalues, eigenvectors = np.linalg.eigh(C)
# 特征值通常升序排列,我们反转一下
idx = np.argsort(eigenvalues)[::-1]
eigenvalues = eigenvalues[idx]
eigenvectors = eigenvectors[:, idx]
# 4. 取前2个主成分
k = 2
W = eigenvectors[:, :k]
Z = X_centered @ W
print("降维后形状:", Z.shape)
print("特征值:", eigenvalues)
print("累计方差解释率:", np.cumsum(eigenvalues) / np.sum(eigenvalues))
你看,核心逻辑就这十几行。累计方差解释率告诉我们:前两个主成分已经抓住了数据90%以上的变异。说白了,丢掉后面两个特征,损失的信息很小。
我的小技巧:选k值的时候,我一般看累计方差解释率的拐点——也就是“陡降”之后的第一个平缓段。如果到80%就开始平了,那k取到那一步就够了。别贪多,多了反而引入噪声。
1.6 方法选择:什么时候用什么?
最后聊聊实战中的选择:
- 维度高(万级以上):用SVD,别算协方差矩阵,内存扛不住。
- 维度低(百级以内),样本多:特征值分解直接算,直观。
- 需要解释主成分含义:用特征值分解,看特征向量每个分量的载荷大小。
- 只需要降维后的数据:用随机化SVD(sklearn里默认就是),速度快。
我曾经在给一个基因表达数据分析时,特征维度两万多,样本才几十。用直接特征值分解,协方差矩阵2万×2万,算到一半内存爆了。换成 sklearn.decomposition.PCA(svd_solver='randomized'),几秒钟出结果。所以选对方法真的能救命。
重要的警告:PCA前一定要做数据标准化(均值为0,方差为1)。如果特征的量纲不同(比如一个特征单位是米,另一个是厘米),PCA会被量纲大的特征带偏。我刚开始学的时候就吃过这个亏,切记。