第3章 PCA实现步骤:数据中心化、协方差计算、特征向量选择

要说PCA怎么落地,我先拆解成三步。这三步我做了十几年降维,几乎每次都用。说白了,就是一套线性代数的标准操作。

3.1 数据中心化

第一步,把数据拉回原点。为什么要做这一步?我见过很多新手直接拿原始数据算协方差,结果主成分完全跑偏。原因很简单——如果变量的量纲不同,高均值的变量会主导协方差矩阵。

中心化的公式就一句话:

X_centered = X - np.mean(X, axis=0)

每一个特征减去它自己的均值。这样处理后,每个特征的均值变成0。我记得有一次做用户行为数据,年龄和消费金额差了上百倍,中心化后PCA才能正确抓到结构。

小技巧: 如果数据量很大,可以用X - mean广播计算,numpy非常高效。

还有一点要注意:如果你做训练/测试集,中心化用的均值必须从训练集算,然后应用到测试集上。绝对不能拿测试集的均值去中心化测试集。嗯,这个坑我踩过。

3.2 协方差矩阵计算

中心化之后,算协方差矩阵。它衡量的是特征之间的共变关系。

公式长这样:

C = (1 / (n-1)) * X_centered.T @ X_centered

其中n是样本数。为什么除以n-1?因为样本协方差用无偏估计。你平时用np.cov(X_centered.T)也是一回事。

协方差矩阵是个对称阵,大小是d×d(d是特征数)。对角线上是每个特征的方差,非对角线上是两两特征的协方差。如果某个非对角线绝对值很大,说明这两个特征冗余,可以压缩。

关键理解: PCA要找的方向,就是让投影后的方差最大。而协方差矩阵正好告诉我们数据在哪些方向上“拉得最开”。

我建议你实际操作时打印一下协方差矩阵的热力图。看到一堆高亮格子,就知道有哪些特征需要处理。

3.3 特征值和特征向量

有了协方差矩阵,接下来就是线性代数的核心操作——特征分解。

eigenvalues, eigenvectors = np.linalg.eig(C)
# 或者用更稳定的: np.linalg.eigh(C) 针对对称矩阵

特征值代表对应特征向量方向上的方差大小。特征向量就是我们要找的“主成分方向”。

为什么特征值大的方向就更重要?我换个说法——你把协方差矩阵想象成一个椭球,特征向量的方向是椭球的轴方向,特征值就是轴的长度。最长的轴就是数据变化最剧烈的方向。

3.4 特征向量选择与降维

特征值按从大到小排序,取前k个最大的特征值对应的特征向量。这k个向量构成投影矩阵W。

投影这一步很简单:

X_pca = X_centered @ W  # W的列是选出的特征向量

怎么选k?通常看累计方差贡献率。我习惯画一个“碎石图”,找拐点。

避坑指南: 我曾经在某个项目里直接取特征值>1的个数作为k,结果发现业务指标严重失真。后来才明白,特征值阈值只适用于标准化后的数据,而且对噪声敏感。最好结合业务可解释性来选。

累计方差贡献率计算:

cumsum = np.cumsum(sorted_eigenvalues) / np.sum(eigenvalues)
k = np.argmax(cumsum >= 0.95) + 1  # 取95%方差

实际工作中,k不一定取到95%,有时候80%就够了,看你对信息保留的要求。

3.5 完整流程代码示例

下面是我常用的PCA实现(不用sklearn):

import numpy as np

def pca_manual(X, n_components):
    # 1. 中心化
    mean = np.mean(X, axis=0)
    X_centered = X - mean
    
    # 2. 协方差矩阵
    cov = np.cov(X_centered, rowvar=False)
    
    # 3. 特征分解
    eigenvalues, eigenvectors = np.linalg.eigh(cov)
    
    # 4. 按特征值降序排列
    idx = np.argsort(eigenvalues)[::-1]
    eigenvalues = eigenvalues[idx]
    eigenvectors = eigenvectors[:, idx]
    
    # 5. 选前k个
    W = eigenvectors[:, :n_components]
    
    # 6. 投影
    X_pca = X_centered @ W
    
    return X_pca, eigenvalues, eigenvectors

这段代码可以跑在任意二维以上数据上。你试试就知道了。

3.6 PCA vs SVD的关系

你可能会问,为什么很多实现用SVD而不是特征分解?其实SVD更数值稳定,特别是当样本数小于特征数时。

SVD的公式:

U, S, Vt = np.linalg.svd(X_centered, full_matrices=False)
# 主成分得分 = U @ np.diag(S)
# 加载向量 = Vt.T

SVD直接对数据矩阵分解,不需要先算协方差矩阵。而且SVD的奇异值平方等于特征值乘以(n-1)。所以两者本质等价。

我个人习惯用SVD来做PCA,尤其在数据量大时。

3.7 核心步骤总结

最后把整个流程画成一张图,方便你记:

PCA实现步骤流程图 1. 数据中心化 2. 协方差矩阵 3. 特征分解 4. 排序特征值 5. 选择前k个 6. 投影降维 输入:原始数据矩阵 X (n × d) 输出:降维后矩阵 X_pca (n × k) 关键参数:k (目标维度) 辅助指标:累计方差贡献率 PCA本质:在保持最大方差的前提下,将高维数据映射到低维空间

这张图的核心逻辑就是:数据→中心化→协方差→特征分解→选主成分→投影。每一步都有数学基础,但实际操作也就几行代码。

3.8 常见问题与避坑

问题原因解决办法
主成分方向不稳定特征向量符号不唯一不用纠结,只关心方向,不关心符号
降维后数据失去可解释性主成分是原始特征的线性组合检查加载向量,找贡献大的原始特征
方差解释率低数据本身线性相关性弱考虑t-SNE或UMAP等非线性方法
内存溢出特征数太多导致协方差矩阵过大用SVD分解,或先做特征选择
经验提醒: 我曾经在处理基因表达数据时,特征数有2万,样本只有200个。直接算协方差矩阵要吃掉几十GB内存。后来改用随机SVD,一分钟就搞定。所以数据规模大时,务必用sklearn.decomposition.PCA(svd_solver='randomized')。