PCA从几何直观到NumPy手写实现:主成分分析降维全解析
2026/9/9 16:59:00 网站建设 项目流程

主成分分析(PCA)是机器学习中出场率最高的降维算法,同时也是最容易让人“以为自己懂了”的算法之一。很多初学者背下了“PCA 就是降维”这句话,也能调用几行sklearn代码完成一次数据压缩,但一旦被问到“为什么 PCA 选出来的特征向量就是主成分”“为什么要把数据标准化”“降维之后的数据还能不能还原”,就开始含糊其辞。

这篇文章是 PCA 系列教程的第 1 部分,重点不讲复杂的数学推导,而是先从几何直观出发,讲清楚 PCA 到底对数据做了什么事。我会带大家从一个二维数据的实际例子入手,用手写代码的方式一步步复现 PCA 的核心流程,再用可视化把“投影”“方差最大”“主成分方向”这些抽象概念落到图上。读完这部分,你会真正理解 PCA 为什么是“找到数据变化最大的方向”,也能在不用sklearn的情况下用 NumPy 写出一个可用的 PCA。

1. 这篇文章真正要解决的问题

先说一个很多刚接触机器学习的读者都会遇到的问题:数据维度太高,模型跑不动,可视化也做不了。

比如一个数据集有 100 个特征,你很难直观地看出样本之间的分布规律,也没法直接画出一张二维散点图来观察分类边界。维度继续增加时,还会出现“维度灾难”——样本在高维空间里变得极其稀疏,距离度量几乎失效,模型的训练难度和过拟合风险都会明显上升。

面对这种情况,常见的做法有两种。

第一种是特征选择,在原有特征里挑出一部分“最有用的”,直接丢掉其余特征。这种方法保留了特征的可解释性,但也带来一个问题:你丢掉的特征里可能包含有用的信息,而且特征之间往往存在相关性,单独看某一个特征可能不重要,组合起来却能提供很强的判别能力。

第二种就是特征提取,也是 PCA 所属的类别。它不直接丢弃原始特征,而是通过坐标变换,把原始数据映射到一组新的坐标轴上。这组新坐标轴的特别之处在于:第一个轴方向上的数据方差最大,第二个轴在正交约束下方差次之,以此类推。然后你只需要保留前几个新坐标轴上的投影结果,就能用很少的维度表示原来很高维的数据。

PCA 解决的核心问题,可以概括成一句话:

在尽量减少信息损失的前提下,把高维数据压缩到低维空间,并尽量保留数据的主要变化模式。

它和特征选择的本质区别是:特征选择是“删列”,PCA 是“换坐标系后取主要分量”。理解了这一点,后续所有的代码和公式都是在围绕“坐标变换”和“方差最大”这两个关键词展开。

这篇文章适合以下几类读者:

  • 正在学习机器学习基础课程,遇到了 PCA 但理解还停留在“调用库”层面的初学者;
  • 准备面试,需要把 PCA 的几何意义、数学流程和工程细节讲清楚的同学;
  • 做数据分析或特征工程时,打算用 PCA 降维但不确定参数怎么选、结果怎么看的数据从业者。

读完本文,你可以做到三件事:

  1. 用自己的话说清楚 PCA 的几何直觉和算法步骤;
  2. 用 NumPy 从零实现一个 PCA 类并完成降维;
  3. 通过方差解释率判断该保留多少个主成分,并知道踩坑点在哪里。

2. 基础概念:方差、投影与新坐标轴

要想理解 PCA 的几何直观,必须先过三个基础概念关:方差、投影、新坐标轴。这三个概念在概率论和线性代数课程里各自出现过,但 PCA 把它们组合成了一个完整的优化问题。

2.1 方差度量的是“信息量”

先看一个简单场景。假设一个数据集只有一个特征 (x),样本取值分别是 1、2、3、4、5,另一个数据集的特征 (y) 的取值全部是 3。显然,前者有变化,后者几乎没有变化。

方差衡量的就是数据的离散程度。PCA 里有一个重要观点:方差大的方向承载了更多信息,方差小的方向更像“噪声”。这不是严格的数学定理,而是一种工程上的信息观——如果一个方向上的数据几乎不动,那它对区分样本的贡献就很小,舍弃它不会造成太大损失。

这个观点的几何含义是:一组点在某条直线方向上的投影越分散,说明这条直线越能代表数据的分布形状。

2.2 投影是降维的动作

“投影”这个词在很多机器学习文章里出现,但对初学者来说,它并不直观。用一个生活场景来类比。

想象你站在一个房间里,头顶有一盏灯,你面前的地面上有一张桌子。桌子的四条腿长度不同,但在地面的影子里,桌子呈现出的是一个二维的轮廓。这个过程就是三维物体到二维平面的投影。

在 PCA 中,投影的含义是完全类似的。高维空间中的每个样本点,被映射到一条直线或一个平面上,映射后的坐标就是投影长度。降维的过程,本质上就是选择一条或几条直线,把所有样本点都投射到这些直线张成的低维子空间里。

PCA 要做的事,就是找到这样一条直线:所有样本点在这条直线上的投影方差最大。

2.3 新坐标轴不是随便选的

PCA 最终产出的是一组新坐标轴,它们有非常严格的约束条件:

  1. 第一个主成分方向是原始数据方差最大的方向;
  2. 第二个主成分方向在与第一个方向正交的所有方向中方差最大;
  3. 第三个、第四个主成分以此类推。

这里的“正交”也很关键。为什么要求正交?如果不是正交,新坐标轴之间就会存在冗余相关性,降维后依然保留下特征之间的关联信息,这与 PCA 去除特征相关性的目标背道而驰。正交约束保证新坐标轴两两独立,互不包含对方的信息。

用一个比喻来说明:如果原始数据的两个特征 (x_1) 和 (x_2) 高度相关,它们就像在同一条直线上重复记录了两次信息。PCA 会找到一条沿着数据延伸方向的新轴 (z_1),再找到一条与它垂直(正交)的新轴 (z_2)。原来的两个特征信息几乎都可以用 (z_1) 一个轴表达出来,而 (z_2) 方向上的方差可能非常小,接近于噪声,可以放心丢弃。

2.4 从二维散点图看 PCA 的几何意义

假设有一组二维数据,横轴是“身高”,纵轴是“体重”。这两个特征并不是完全独立的——一般来说,身高高的人体重也可能偏重。如果直接在原始坐标系里看数据,点的分布大概呈现一个倾斜的椭圆。

PCA 做的事情就是旋转这个坐标系:

  • 第一个新坐标轴指向椭圆长轴方向,即数据变化最剧烈的方向;
  • 第二个新坐标轴指向椭圆短轴方向,也就是与长轴垂直的、数据变化最小的方向;
  • 如果短轴方向的方差非常小,说明数据在这个方向上几乎没有差异,可以舍弃;
  • 最终保留下来的长轴方向的一维投影,就是原始数据在二维平面上的“最佳一维表示”。

这句话值得再读一遍:PCA 不是在原坐标系里删掉一个轴,而是先旋转坐标系,让新坐标轴对准数据的自然分布方向,然后再决定哪个轴可以丢弃。

3. 从几何直觉到算法步骤:PCA 的完整流程

当我们在几何层面理解了“找最大方差方向”之后,下一步就是把它变成一个可计算的算法流程。PCA 的算法步骤不复杂,一共五步,每一步都有明确的几何或统计含义。

下面给出 PCA 的完整流程,并用二维数据的例子来说明每一步发生了什么。

3.1 第一步:数据标准化

为什么要做这一步?看一个具体例子。假设一个数据集中有一个特征的单位是“米”,数值范围在 1.5 到 2.0 之间;另一个特征的单位是“厘米”,数值范围在 150 到 200 之间。如果不做标准化,第二个特征的方差可能会远大于第一个特征,PCA 就会把主要注意力放在数值大的特征上,而忽略数值小但可能很重要的特征。

标准化的常用方式是 Z-score 标准化,也就是把每个特征变成均值为 0、标准差为 1 的形式。计算公式:

[ z = \frac{x - \bar{x}}{s} ]

其中 (\bar{x}) 是特征均值,(s) 是特征标准差。

注意一个细节:标准化必须在划分训练集和测试集之前先拟合训练集上的均值和标准差,然后用这套参数分别转换训练集和测试集。如果对两个数据集分别标准化,会造成数据分布不一致,影响后续建模。

3.2 第二步:计算协方差矩阵

数据经过标准化后,特征之间仍然存在相关性,协方差矩阵就是用来度量这种相关性的工具。

对于 (d) 维数据,协方差矩阵是一个 (d \times d) 的对称矩阵。矩阵中的 (C_{ij}) 表示第 (i) 个特征和第 (j) 个特征的协方差:

  • 协方差为正,说明两个特征同向变化;
  • 协方差为负,说明两个特征反向变化;
  • 协方差接近 0,说明两个特征线性相关性很弱。

为什么要算协方差矩阵?因为 PCA 要找的是“方差最大的方向”,而这个方向恰好与协方差矩阵的特征向量有关。从几何上说,协方差矩阵描述的是数据在每个方向上的“散布程度”,所以它自然成为 PCA 的计算核心。

3.3 第三步:计算协方差矩阵的特征值和特征向量

这是 PCA 数学原理中最核心的一步,也是让很多初学者迷惑的地方。

简单理解:协方差矩阵经过特征分解后,会得到一组特征值和对应的特征向量。其中:

  • 特征向量表示方向;
  • 特征值的大小表示该方向上的方差大小,也就是信息量的多少。

特征值越大,对应的特征向量方向上的数据方差越大,这个方向就越重要。把所有特征值从大到小排列,对应的特征向量就构成了新坐标系下的各个坐标轴方向。

在 NumPy 中,这一步可以用numpy.linalg.eig()numpy.linalg.eigh()完成。eigh专门用于对称矩阵,数值稳定性更好,后面代码里会使用这个方法。

3.4 第四步:选择主成分个数

特征值排序后,我们需要决定保留多少个特征向量。

这里有一个常用的统计量叫做“方差解释率”(Explained Variance Ratio),它表示某个主成分占总方差的百分比:

[ \text{解释率} = \frac{\lambda_i}{\sum_{j=1}^{d} \lambda_j} ]

累计解释率就是前 (k) 个主成分的解释率之和。通常情况下,我们希望累计解释率能达到 80% 到 95%。如果前两个主成分已经解释了数据 90% 以上的方差,说明把高维数据降到二维是合理的。

选择主成分个数没有绝对的正确答案,它取决于业务目标:

  • 如果是做可视化,通常选 2 或 3;
  • 如果是为下游模型去噪,可以选择累计解释率 85% 左右的前若干主成分;
  • 如果是压缩存储,需要根据存储限制和重建误差之间做权衡。

3.5 第五步:投影到新坐标空间

假设原始数据矩阵为 (X),形状是 (n \times d),其中 (n) 是样本数,(d) 是特征数。选择前 (k) 个特征向量组成矩阵 (W),形状是 (d \times k)。降维后的数据为:

[ Z = X \cdot W ]

(Z) 的形状是 (n \times k),这就是原始数据在主成分空间中的坐标。从几何上看,这一步就是把所有样本点投影到由前 (k) 个特征向量张成的低维子空间中。

到这里,一个完整的 PCA 流程就结束了。接下来用 Python 代码把上面的流程一步步实现出来。

4. 环境准备与数据集选择

本文所有代码基于 Python 3 和 NumPy 实现,可视化部分使用 Matplotlib。建议在 Jupyter Notebook 或 VS Code 的 Notebook 环境中运行,方便分步观察中间结果。

安装依赖的命令如下:

pip install numpy matplotlib scikit-learn

版本没有硬性要求,只要 NumPy 是 1.20 以上、Matplotlib 是 3.x 版本即可。本文重点演示通用思路,版本差异不影响代码运行。

我们选择两个数据集进行实验。

第一个数据集是手动构造的二维数据。通过一个线性关系生成数据,让两个特征之间存在明显的相关性。这种数据最能直观展示 PCA 的工作原理:二维数据降至一维后,可以看到数据被“压”到一条直线上。

第二个数据集使用sklearn.datasets中的手写数字数据集的一部分,用来展示 PCA 在高维数据压缩和可视化中的实际作用。这一步先埋下伏笔,第 5 章重点用二维数据讲原理,第 6 章展示高维效果。

5. 手写 PCA:完整示例与代码实现

这一节我们用 NumPy 从零实现 PCA,核心流程严格按照第 3 章的五个步骤。建议把你自己的数据替换进去,观察不同数据上 PCA 的表现。

5.1 构造带相关性的二维数据

先从生成实验数据开始。这里人为构造两组特征:x是 0 到 10 之间的随机数,yx近似线性相关,并加入一定噪声。

import numpy as np import matplotlib.pyplot as plt # 固定随机种子,保证结果可复现 np.random.seed(42) n_samples = 200 x = np.random.uniform(0, 10, size=n_samples) y = 2.0 * x + np.random.normal(0, 2.0, size=n_samples) X = np.column_stack((x, y)) print("数据形状:", X.shape) print("前5个样本:\n", X[:5])

这段代码生成了 200 个二维样本。由于yx之间存在明显的线性关系,数据散点图会呈现出一个倾斜的椭圆分布。这正是 PCA 最适合发挥作用的场景:信息主要集中在一个方向(主对角线方向),另一个方向(垂直于对角线方向)方差较小。

5.2 从零实现 PCA 类

下面实现一个简单的 PCA 类,包含标准化、求协方差矩阵、特征分解、降维投影和方差解释率计算这几个核心功能。

class PCAFromScratch: def __init__(self, n_components=None): self.n_components = n_components # 要保留的主成分个数 self.mean_ = None # 训练集各特征均值 self.std_ = None # 训练集各特征标准差 self.components_ = None # 主成分方向(特征向量) self.explained_variance_ = None # 各主成分的特征值 self.explained_variance_ratio_ = None # 方差解释率 def fit(self, X): # 1. 标准化 self.mean_ = np.mean(X, axis=0) self.std_ = np.std(X, axis=0) X_std = (X - self.mean_) / self.std_ # 2. 计算协方差矩阵 cov_matrix = np.cov(X_std, rowvar=False) # 3. 特征分解(eigh 专门处理对称矩阵) eigenvalues, eigenvectors = np.linalg.eigh(cov_matrix) # 4. 特征值从大到小排序 idx = np.argsort(eigenvalues)[::-1] eigenvalues = eigenvalues[idx] eigenvectors = eigenvectors[:, idx] # 5. 保留前 n_components 个主成分 if self.n_components is not None: eigenvalues = eigenvalues[:self.n_components] eigenvectors = eigenvectors[:, :self.n_components] self.components_ = eigenvectors self.explained_variance_ = eigenvalues total_var = np.sum(eigenvalues) self.explained_variance_ratio_ = eigenvalues / total_var return self def transform(self, X): X_std = (X - self.mean_) / self.std_ return np.dot(X_std, self.components_) def fit_transform(self, X): self.fit(X) return self.transform(X)

这段代码有几个细节值得注意。

numpy.linalg.eighnumpy.linalg.eig的选择:协方差矩阵是对称矩阵,使用eigh速度更快、数值稳定性更好。eig在特殊情况下可能返回复数特征值,而eigh会明确按实对称矩阵处理,避免这种意外。

特征值排序:eigh返回的特征值默认从小到大排列,所以需要用argsort()[::-1]做降序处理。如果这一步顺序反了,主成分方向就会选错,这是手写 PCA 时最容易犯的错误。

标准化参数的一致性:fit方法中计算了训练集的均值和标准差,transform方法必须使用同一套参数。如果测试阶段重新计算均值和标准差,数据分布就会被改变,模型效果评估失真。

5.3 在二维数据上运行 PCA

数据已经准备好,PCA 类也已经实现,现在进行拟合与降维:

pca = PCAFromScratch(n_components=1) X_projected = pca.fit_transform(X) print("特征值(方差):", pca.explained_variance_) print("方差解释率:", pca.explained_variance_ratio_) print("第一个主成分方向:", pca.components_[0])

观察输出结果。由于二维数据降到一维,这里只有一个主成分,方差解释率会比较高,通常能达到 95% 以上。第一主成分的方向向量大致指向(1, 2)的归一化方向,这正是原始数据线性关系的斜率方向。

(y = 2x) 意味着数据在一个方向上的变化速度是另一个方向的 2 倍,PCA 自动找到了这个方向。

5.4 可视化降维前后对比

降维的效果需要用可视化来验证。下面的代码把原始数据、第一主成分方向的直线、以及所有样本点在该方向上的投影点画在同一张图上。

# 原始数据散点图 plt.figure(figsize=(8, 6)) plt.scatter(X[:, 0], X[:, 1], alpha=0.6, label="原始数据") # 绘制第一主成分方向直线 origin = np.mean(X_std, axis=0) direction = pca.components_[0] plt.quiver(0, 0, direction[0] * 5, direction[1] * 5, angles='xy', scale_units='xy', scale=1, color='red', width=0.02, label="第一主成分方向") # 投影点(在标准化空间中) X_std = (X - pca.mean_) / pca.std_ t = np.dot(X_std, pca.components_[0]) projected = np.outer(t, pca.components_[0]) plt.scatter(projected[:, 0], projected[:, 1], alpha=0.3, color='green', label="投影点") # 连接原始点和投影点的辅助线 for i in range(20): plt.plot([X_std[i, 0], projected[i, 0]], [X_std[i, 1], projected[i, 1]], color='gray', linewidth=0.5, alpha=0.5) plt.xlabel("特征1(标准化后)") plt.ylabel("特征2(标准化后)") plt.legend() plt.title("PCA 降维可视化:投影到第一主成分方向") plt.axis('equal') plt.show()

从图中可以看到,绿色投影点全部落在红色直线上。原始数据点到投影点的连线长度就是投影误差,也就是降维带来的信息损失。PCA 选择的直线使这些误差的平方和最小,等价于投影方差最大。

这里有一个值得体会的等价关系:

  • 投影方差最大 = 数据在低维表示中保留的信息最多;
  • 投影误差平方和最小 = 原始数据能被低维表示尽量好地重建。

这两个角度用不同的语言描述了同一个优化问题,一个是“保住信息”,一个是“减少损失”。

5.5 使用 sklearn 验证手写结果

手写 PCA 的正确性需要有参照物来验证。sklearn.decomposition.PCA是工业界广泛使用的实现,两者结果应该一致。

from sklearn.decomposition import PCA as SklearnPCA sklearn_pca = SklearnPCA(n_components=1) X_sklearn_projected = sklearn_pca.fit_transform(X) print("手写 PCA 投影结果前5行:\n", X_projected[:5]) print("sklearn PCA 投影结果前5行:\n", X_sklearn_projected[:5]) print("方差解释率对比:") print("手写:", pca.explained_variance_ratio_) print("sklearn:", sklearn_pca.explained_variance_ratio_)

运行后可以看到,两者投影结果的数值基本一致。可能的差别在于正负号方向——PCA 的特征向量符号本身不确定,方向反了说明两个实现选择了同一方向的相反向量,这在数学上完全等价,不影响降维结果。

如果数值有明显差异,优先检查是否对数据做了标准化。sklearn的 PCA 默认不做标准化,而我们手写的实现默认做了标准化,所以这里需要保证两侧处理方式一致。

6. 运行结果与效果验证

上一节的代码运行后,应该能看到三类反馈信息:控制台输出的特征值和方差解释率、降维后的投影点坐标、以及可视化图。

6.1 控制台输出解读

假设输出的特征值约为1.95,方差解释率约为0.97。这意味着第一个主成分保留了原始数据约 97% 的方差,降维到一维造成的损失只有约 3%。对于大多数实际需求来说,这个压缩效率已经非常理想。

方差解释率是验证 PCA 效果的第一个关键指标。如果这个值偏低,比如只有 60%,说明前几个主成分不足以代表原始数据,需要增加保留的主成分个数。

6.2 可视化图如何判断正确

从生成的图里判断 PCA 是否成功,可以看三个信号:

第一个信号:投影点是否全部落在一条直线上。如果投影点仍然散开,说明代码中矩阵乘法或方向选择有误。

第二个信号:直线方向是否看起来与数据主体分布方向一致。如果红色直线指向了其他方向,比如沿着横轴或纵轴,那很可能是特征值排序出了问题。

第三个信号:原始点到直线的连线段长度整体是否比较短。如果很多点的连线都很长,说明投影误差很大,当前主成分方向可能选错了。

6.3 降到一维后的数据怎么观察

可以打印降维后的数据分布:

plt.figure(figsize=(6, 4)) plt.scatter(X_projected[:, 0], np.zeros_like(X_projected[:, 0]), alpha=0.6, color='purple') plt.xlabel("第一主成分得分") plt.title("一维投影分布") plt.show()

当所有点被压到一条线上后,它们在一维空间中的相对位置应该保留原始数据的大致顺序。如果原始数据呈现聚团分布,那么在一维展开图中也应该能看到对应的群组结构。

6.4 如果结果不对,先查这三个地方

如果输出的方差解释率异常,或者投影形状不对劲,按以下顺序排查:

  1. 检查标准化:特征是标准化为均值为 0、方差为 1,还是减均值后忘了除以标准差?
  2. 检查特征向量排序:特征值从大到小排序后,特征向量列是否同时按相同顺序调整?
  3. 检查矩阵乘法维度:X_std是 (n \times d),components_是 (d \times k),乘积才是 (n \times k)。如果维度不对,会立刻报错,但如果是手动拼接矩阵,需要仔细核对列方向。

7. 高维示例:用 PCA 压缩并可视化手写数字数据

二维数据可以帮我们建立几何直觉,但 PCA 在实际项目中更多是处理几十维甚至上千维的数据。这一节用一个经典的高维数据集来检验我们手写 PCA 的效果:手写数字数据集(Digits)。

该数据集中每张图片是 8x8 的灰度图,展开后得到 64 个特征,共 10 个类别,分别代表数字 0 到 9。64 维数据很难直接可视化,但通过 PCA 降到二维后,可以观察不同数字类别的分布形态。

from sklearn.datasets import load_digits digits = load_digits() X_digits = digits.data y_digits = digits.target print("原始数据形状:", X_digits.shape) print("类别:", np.unique(y_digits)) # 降到2维,用于可视化 pca_digits = PCAFromScratch(n_components=2) X_digits_pca = pca_digits.fit_transform(X_digits) print("降维后形状:", X_digits_pca.shape) print("前两个主成分累计方差解释率:", np.sum(pca_digits.explained_variance_ratio_))

可视化结果中,不同数字类别用不同颜色绘制:

plt.figure(figsize=(10, 8)) scatter = plt.scatter(X_digits_pca[:, 0], X_digits_pca[:, 1], c=y_digits, cmap='tab10', alpha=0.7, s=40) plt.colorbar(scatter, label="数字类别") plt.xlabel("第一主成分") plt.ylabel("第二主成分") plt.title("手写数字 64 维数据经 PCA 降至 2 维") plt.show()

从实际经验看,64 维压缩到 2 维后,累计方差解释率通常在 30% 到 40% 之间。这意味着二维投影只保留了不到一半的信息量,散点图中不同类别会呈现部分重叠,但基本上还能看出一些聚类结构。数字 0、1、7 等形状差别较大的类别往往分离度较高,而 3、5、8 这类形状接近的数字则容易混淆。

这个结果引出了一个重要观点:PCA 用于可视化时,要看重的是“类别之间有没有分离趋势”,而不是追求完全可分。如果二维效果不理想,可以尝试降到三维,或者改用 t-SNE、UMAP 等其他非线性降维方法。

8. 常见问题与排查思路

下面把 PCA 使用过程中最常遇到的问题整理成一张排查表。

问题现象可能原因排查方式解决方案
手写 PCA 与 sklearn 结果相差很大数据是否标准化的设置不一致对比两边的均值、标准差处理逻辑统一预处理流程,或设置 sklearn PCA 的whiten=True前的StandardScaler
特征向量方向相反PCA 特征向量符号本身不唯一检查投影结果是否只是符号相反不影响降维效果,可暂时忽略,也可以规定特征向量第一个非零元素为正
方差解释率很低高维数据信息分布太分散打印所有特征值的分布考虑增加主成分个数,或换用非线性降维方法
降到 2 维后可视化效果不理想主成分之间类别混杂观察不同类别的投影分布尝试 3 个主成分,或改用 t-SNE/UMAP
数据没做标准化不同特征量纲差异导致 PCA 偏向大数值特征检查各特征的均值和标准差先使用StandardScaler再调用 PCA
分解后的特征值出现负值(极小值)浮点误差导致对称矩阵特征值略小于 0检查特征值绝对值是否极小如果绝对值接近 0,按 0 处理即可
测试集变换结果异常测试集使用了独立计算的均值和方差检查训练与预测阶段参数是否相同保存训练集的mean_std_,测试时复用

逐个展开说明。

手写与 sklearn 结果不一致:这是跑手写代码时最常遇到的问题。原因多半是 sklearn 默认不标准化,而你在手写实现里做了标准化。解决办法是先给 sklearn PCA 的输入数据套一层StandardScaler的转换,两边再比较。如果数值仍然不一致,检查特征向量的符号方向。

向量方向相反:特征向量描述的是方向,一条直线同时存在两个方向的单位向量。PCA 算法不保证每次运行都返回相同方向,这在理论上不是错误。做数据可视化时影响不大,但如果你需要保存 PCA 模型并在生产环境中把新旧数据的投影保持一致,建议对特征向量做符号规范化。

方差解释率偏低:不要单纯认为“PCA 效果不好”。这往往是数据本身信息分散的体现。比如 64 维手写数字数据降到 2 维时解释率只有 30% 左右,但它仍然展示了明显的类别结构。关键不是解释率绝对值,而是它是否满足你的业务目标。

可视化不理想:如果降到 2 维后各类别重叠严重,不代表数据无法区分。可以看 3 维投影,也可以用非线性方法如 t-SNE、UMAP 做补充分析。PCA 是线性投影,对非线性结构的数据表达能力有限,这是它的边界。

输入特征量纲差异:必须强调,这不是可选项,而是必选项。如果特征是身高和体重这类量纲差异大的数据,不标准化时 PCA 会错误地认为数值大的特征更重要。这里真正容易踩坑的是:你可能在训练集上做了标准化,却忘了对上线后的新样本执行相同变换。生产环境中的推理代码应该保存训练阶段的均值和标准差参数,而不是重新计算。

9. 最佳实践与工程建议

经过前面手写代码和验证,你已经明白了 PCA 的内部机制。但在实际项目中,直接调用库并不够,还需要一组工程层面的规范来避免把 PCA 用错、用偏。

9.1 标准化顺序与 Pipeline

PCA 应该放在特征工程的哪个位置,很多初学者容易搞混。

推荐做法是使用 sklearn 的 Pipeline 将标准化和 PCA 绑定在一起:

from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.decomposition import PCA from sklearn.linear_model import LogisticRegression pipeline = Pipeline([ ('scaler', StandardScaler()), ('pca', PCA(n_components=0.95)), ('clf', LogisticRegression(max_iter=1000)) ]) pipeline.fit(X_train, y_train) test_accuracy = pipeline.score(X_test, y_test)

这里n_components=0.95表示自动选择累计方差解释率达到 95% 的主成分个数。这种方式比手动指定整数更合理,因为它根据数据本身的复杂度自动确定维度。

Pipeline 的优势在于:训练阶段在训练集上拟合标准化参数和 PCA 参数,预测阶段自动复用同一套参数,不会出现数据泄露或参数不一致的问题。

9.2 特征数与样本数的关系

PCA 不是数据越多越好,也不是特征越多越好。当特征数远大于样本数时,协方差矩阵的估计会变得非常不稳定,特征值也可能被严重扭曲。

如果你遇到 (p > n) 的高维小样本问题,比如基因表达数据有 10000 个特征但只有 100 个样本,直接做 PCA 容易过拟合。一个常见替代方案是使用 SVD 方式求解 PCA,sklearn 内部默认使用svd_solver='auto',在数据规模较大时效率更高、数值稳定性更好。更稳妥的做法是先通过方差过滤、相关性分析等手段减少特征数量,再进行 PCA。

9.3 主成分个数的选择

主成分个数的确定是实践中最常见的问题。没有万能答案,但有三个常用参考标准:

  1. 累积解释率法:选择使累计解释率达到 80%~95% 的 (k) 值。
  2. 特征值大于 1 法:只保留特征值大于 1 的主成分。这个标准源于主成分平均解释一个原始特征的方差,低于 1 的主成分信息量还不如一个原始特征。
  3. 肘部法则:画出“主成分序号—特征值”的碎石图,找到曲线从陡峭下降变为平缓的转折点。
# 绘制碎石图 pca_full = PCAFromScratch(n_components=None) pca_full.fit(X) plt.figure(figsize=(8, 5)) plt.plot(range(1, len(pca_full.explained_variance_) + 1), pca_full.explained_variance_, marker='o') plt.xlabel("主成分序号") plt.ylabel("特征值") plt.title("PCA 碎石图") plt.show()

使用碎石图时观察曲线弯曲最明显的位置。如果曲线在第三个点之后变得平缓,说明前两个主成分已经抓住了主要信息,再往后增加主成分带来的收益很低。

9.4 主成分的业务可解释性

PCA 在工程中最大的局限性是“可解释性下降”。原始特征可能叫“年龄”“收入”“点击量”,但第一主成分是原始特征的线性组合,可能同时包含年龄和收入的信息,很难用一个业务名词来命名它。

如果你的项目要求必须解释每个特征为什么被保留,PCA 可能不是最佳选择。相反,如果目标只是压缩数据、加速训练、去除噪声,PCA 非常合适。

建议在实际项目中把业务特征工程和 PCA 结合使用:先用业务经验构造有意义的特征,再用 PCA 压缩数据为模型提供输入。这样既能保留领域知识,又能享受降维带来的效率提升。

9.5 生产环境中的模型持久化

PCA 模型训练完成后,部署时不能只保存降维后的数据,还要保存完整的预处理参数和 PCA 参数。推荐使用joblib保存 Pipeline:

pip install joblib
import joblib joblib.dump(pipeline, 'pipeline_pca.pkl') # 加载模型 loaded_pipeline = joblib.load('pipeline_pca.pkl')

保存 Pipeline 而不是单个 PCA 对象,是因为 Pipeline 把标准化参数和 PCA 参数绑定在了一起。推理时只需调用loaded_pipeline.transform(new_data),新样本就会自然完成标准化和降维。

10. 总结与后续学习方向

这篇教程从几何直观出发,解释了 PCA 的本质:它不是简单删除特征,而是通过旋转坐标系找到数据方差最大的方向,然后把数据投影到新的低维空间。

我们用 NumPy 从零实现了一个 PCA 类,包含标准化、协方差矩阵计算、特征分解、排序和投影五个核心步骤。通过一个线性相关的二维数据集,直观看到了原始数据、主成分方向、投影点三者之间的关系。随后用手写数字数据集展示了 PCA 在高维数据压缩和可视化中的应用,并用 sklearn 验证了手写结果的正确性。

在工程层面,我们讨论了标准化的重要性、Pipeline 的使用方式、主成分个数的选择方法,以及 PCA 在业务解释性上的局限性。

如果你发现 PCA 在手写数字这类非线性数据上的二维可视化效果不够好,这不一定是代码有 bug,而是线性降维本身的能力边界。后续可以学习核 PCA、t-SNE、UMAP 等非线性降维方法,它们在处理复杂数据结构时往往有更好的表现。

PCA 的数学推导部分,包括“为什么协方差矩阵的特征向量就是最大方差方向”“为什么投影方差最大等价于重构误差最小”,会在系列后续文章中逐步展开。建议先把本文的代码完整跑一遍,观察每一行输出和每一张图的变化,这比多背十遍公式都有用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询