ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

主成分分析PCA从零到实操:降维原理、数学推导与Python实现

主成分分析PCA从零到实操:降维原理、数学推导与Python实现 做数据分析这几年我遇到不少朋友一听到PCA就头疼觉得它又是矩阵又是特征值听着像天书。但实际上主成分分析Principal Component AnalysisPCA是机器学习和统计建模里最基础、也最实用的一招它能把几十个乱糟糟的变量压缩成几个核心方向同时尽量保留原始信息。今天这篇就把PCA从零开始捋一遍不绕弯子从它要解决的业务痛点、背后的数学直觉、到一份能直接照着跑通的代码流程希望对准备入门数据科学、或者在特征工程里反复被维度折磨的人有点帮助。1. 先说说PCA到底在解决什么问题1.1 维度灾难为什么数据变量一多就麻烦想象你要对一批用户做画像分析每个用户有年龄、收入、消费金额、注册天数、最近登录时间、点击次数、浏览时长、是否会员、所在城市等级……可能还有几十个营销渠道的响应标记。变量的数量越来越多问题是信息并没有同步增长很多列之间是高度相关的。比如“消费金额”跟“最近登录时间”可能没有强关联但“消费金额”和“点击次数”、和“会员等级”之间往往存在很强的相关关系。变量多带来的结果就是“维度灾难”样本量被稀释、距离度量失真、模型训练变慢、过拟合风险急剧上升。更麻烦的是几百个维度就算你画散点图也没法直接看人脑能理解的三维已经是极限。这时候PCA的价值就出来了。它做的事情说通俗点就是在原始变量的基础上重新构造出一组新的、数量更少的变量主成分这些新变量彼此线性无关而且每一个都尽可能多地抓住原始数据中残存的变异。1.2 主成分分析的核心逻辑没有官方定义前我习惯用一句话概括PCA通过线性变换把数据投影到方差最大的方向上从而用更少的维度表示原来的数据。为什么强调“方差最大”因为方差代表数据的离散程度也代表信息量。如果某个方向上所有样本都挤成一团那么这个方向基本不携带可辨识的信息反过来如果样本在某个方向上的分布跨度很大那这个方向就承载了数据之间最主要的差异。PCA就是沿着这个思路依次找出方差最大的第一个轴、与第一个轴正交且方差第二大的第二个轴、以此类推最后得到一组正交轴。这组正交轴就是“主成分方向”把原始数据往这些轴上一投影得到的新坐标就是“主成分得分”。这听起来不算难真正让人迷糊的是为什么最后落到了要算协方差矩阵的特征值和特征向量以及特征向量跟“方差最大的方向”到底有什么关系。这部分我在下一章专门拆开讲。2. 理解PCA的数学原理其实没你想的那么难2.1 从“方差最大”这个直觉说起设原始数据矩阵 X 有 n 个样本每个样本有 p 个特征记作 X [x_1; x_2; ...; x_n]形状是 (n, p)。我们先做一件非常重要的事对每个特征做中心化处理也就是把每一列的均值减去使每列均值为0。这一步的作用是把坐标系平移到数据的重心后面会解释为什么。现在我们要找一个单位方向向量 w (w_1, w_2, ..., w_p)满足 w 的长度为 1。把所有样本投影到 w 上得到投影后的值 y_i X_i · w点积。这些投影值的方差可以写成Var(y) (1/n) * Σ(X_i · w - mean(X·w))²因为已经中心化了mean(X·w)0所以上式就是 (1/n) * Σ(X_i · w)²。稍微做一下线性代数变换可以写成Var(y) wᵀ * (XᵀX / n) * w其中 XᵀX / n 正好是协方差矩阵因为数据中心化后除以n的XᵀX就是各特征之间的协方差矩阵。于是问题变成了求一个单位向量 w使得 wᵀ C w 最大其中 C 是协方差矩阵。这就是一个带约束的优化问题用拉格朗日乘子法求解对 w 求导令其为零最后会得到C w λ w也就是说方差最大方向 w 恰好是协方差矩阵的特征向量对应的特征值 λ 就是这个方向上的方差。这就是为什么所有PCA教程到最后都要算协方差矩阵的特征值和特征向量它不是凭空冒出来的而是从“最大化方差”这个目标函数一步步推出来的必然结果。2.2 为什么偏偏要用协方差矩阵很多人第一次看到这个结论会反问为什么不是用相关系数矩阵为什么不是直接对原始数据做矩阵分解实际上两者都能推只是角度不同。用协方差矩阵的好处是它自带尺度信息能够反映原始变量本身的波动量级。但有得必有失如果不同特征的单位差很大比如一个特征是“年龄”范围0到100另一个特征是“收入”范围0到100万协方差矩阵会完全被数值量级大的变量主导这时候PCA找到的主成分基本就是那个大数值变量本身降维效果就很尴尬。所以实操里几乎统一的做法是先做标准化z-score让每个特征的均值为0、方差为1然后再计算协方差矩阵。标准化之后再算协方差矩阵其实等价于计算相关系数矩阵。这也是为什么网上很多教程会说“PCA之前一定要标准化”这句话的数学根源就在这。2.3 特征值和特征向量到底在说什么特征向量是一个向量经过协方差矩阵作用后方向不变只伸缩一个倍数这个倍数就是特征值。对应到PCA里特征向量主成分的方向也就是新坐标系中的轴方向特征值数据在对应主成分方向上的方差大小特征值越大说明这个方向上保留的原始信息越多。举个直观的例子假设数据点在二维平面上呈一个倾斜的椭圆分布。长轴方向就是第一个主成分方向特征值大短轴方向是第二个主成分方向特征值小。如果短轴方向的特征值小到可以忽略你就敢把数据投影到长轴上用一维数据近似原来的二维数据误差很小。实际计算时一般不会直接用协方差矩阵做特征值分解而是对中心化后的数据矩阵做奇异值分解SVD。因为SVD在数值上更稳定也不会先计算协方差矩阵导致精度损失尤其当特征数很多时协方差矩阵是 p×p 的计算开销和存储开销都可能失控。sklearn里PCA底层用的就是SVD。2.4 怎么选主成分的数量理论上p 个特征最多能产生 p 个主成分但你不可能全要不然就没意义了。常用的选数量方法有几种累计方差贡献率法计算每个主成分解释的方差占总方差的比例特征值 / 所有特征值之和然后从大到小累加直到累计贡献率达到某个阈值。业务分析里常用80%、90%、95%作为分界线。我自己的习惯是先看85%如果样本量小或后续模型容量有限就提高到95%看看效果差异。碎石图Scree Plot把特征值从大到小画成折线图找“肘部”位置也就是折线从陡峭变平缓的拐点拐点之前的主成分保留之后的基本都是噪声。这个方法比较主观但胜在直观。Kaiser准则只保留特征值大于1的主成分。这个准则在问卷分析和心理学研究里用得比较多因为它针对的是标准化后的数据均值为0、方差为1特征值大于1才说明该主成分解释的信息量超过一个原始变量。但它有个前提是使用相关系数矩阵对原始协方差矩阵的直接套用意义不大。三种方法可以结合着用不要死磕某一个。我经常遇到累计贡献率到95%需要20个主成分但业务方希望压缩到5个以内这时候就得在解释性和信息保留之间做取舍这种取舍通常要靠对业务的理解不是纯数学能替代的。3. 手把手实操从数据到主成分的完整流程3.1 数据标准化成败第一步先说结论做PCA之前几乎必须标准化除非所有特征天然就在同一个量纲、同一个数值范围内而且你明确不想消除量纲差异。标准化的公式就是 z (x - μ) / σ每个特征独立计算。用Python的scikit-learn可以直接调StandardScaler这里提醒几个容易踩的点标准化之前先处理缺失值可以删除、均值填充或用KNN插补绝不能带着NaN去做PCA很多底层实现会直接报错。如果原始数据里有离群值标准差会被拉得很大标准化后正常样本会挤在一起PCA结果可能被离群值主导。稳妥的做法是先做箱线图或z-score绝对值大于3的检查把严重离群值单独处理。分类变量在标准化之前要转成数值编码比如独热编码、标签编码等。但要注意高基数的类别变量做独热编码后会生成大量稀疏列PCA对这种稀疏数据并不友好可以考虑先做目标编码或者用截断SVDTruncatedSVD而不是标准PCA。3.2 完整分析流程五步走整个PCA的实操流程我整理成五步初学者照着走基本不会跑偏第一步加载数据并清洗。先看数据shape、每列缺失比例、数据类型。把明显没有信息量的列如ID列、全为常数的列去掉。第二步特征分离与预处理。将特征列和标签列如果有分开特征列统一转为数值类型处理缺失值和离群值。第三步标准化。用StandardScaler对特征做z-score变换。这步务必只在训练集上fit再用同一组参数transf测试集否则会造成数据泄露。第四步PCA拟合。用PCA或直接SVD做主成分提取记录explained_variance_ratio_解释方差比例和components_主成分系数矩阵。第五步主成分解释与后续建模。画出碎石图、累计贡献率曲线选主成分数量然后可以把主成分得分喂给聚类、回归或分类模型。3.3 代码实现从零手写一遍再用sklearn验证为了让你彻底搞懂原理我先提供一个完全基于numpy的手写版本不做任何封装只看核心逻辑。import numpy as np from sklearn.preprocessing import StandardScaler from sklearn.decomposition import PCA # 模拟一份数据100个样本5个特征 np.random.seed(42) X np.random.randn(100, 5) # 人为制造相关性 X[:, 2] X[:, 0] * 0.8 np.random.randn(100) * 0.2 X[:, 4] X[:, 1] * 0.7 np.random.randn(100) * 0.3 # 1. 标准化 scaler StandardScaler() X_std scaler.fit_transform(X) # 2. 计算协方差矩阵 cov_matrix np.cov(X_std.T) # 3. 计算特征值和特征向量 eig_val, eig_vec np.linalg.eigh(cov_matrix) # 4. 特征值从大到小排序 idx np.argsort(eig_val)[::-1] eig_val eig_val[idx] eig_vec eig_vec[:, idx] # 5. 计算主成分得分 X_pca_manual X_std.dot(eig_vec) # 6. 计算每个主成分的方差解释比例 explained_ratio_manual eig_val / eig_val.sum() print(手动计算结果) print(特征值, eig_val) print(解释方差比例, explained_ratio_manual)注意我用了np.linalg.eigh而不是np.linalg.eig因为协方差矩阵是对称矩阵用eigh更稳定、更快。这也是一个平时容易忽略的小细节。接着用sklearn做验证pca PCA() X_pca pca.fit_transform(X_std) print(sklearn结果) print(解释方差比例, pca.explained_variance_ratio_) print(主成分方向components_) print(pca.components_)你会发现sklearn的explained_variance_ratio_和手动版本的explained_ratio_manual数值几乎一致。components_里的每一行就是对应主成分的方向向量它跟你的eig_vec列向量是对应的只是正负号可能相反这在数学上是允许的因为特征向量乘以-1依然是特征向量不影响投影后的方差。如果你只想提取前两个主成分可以这样pca_2 PCA(n_components2) X_pca_2 pca_2.fit_transform(X_std) # 查看每个原始特征对两个主成分的贡献 loadings pd.DataFrame( pca_2.components_.T, columns[PC1, PC2], indexfeature_names )之后可以把这个X_pca_2画成二维散点图直观看到样本分布。4. 实操中常踩的坑与排查技巧4.1 常见问题速查表我整理了一张PCA实操问题速查表都是这几年实际项目里遇见过的坑不是书上抄来的。问题现象可能原因解决办法第一主成分贡献率异常高99%存在数值量级极大的列或一个变量近似等于另一个变量的线性组合先检查数据是否标准化检查是否存在重复列或高度相关列特征值出现负值协方差矩阵非半正定常见于数据含缺失值填充不当、或样本数小于特征数检查是否有NaN考虑用SVD或增加正则化项主成分方向难以解释原始特征太多、太稀疏或标准化后所有特征权重都接近在PCA前先做特征筛选或用稀疏PCASparsePCA提高可解释性训练集和测试集PCA结果对不上在训练和测试上分别做了fit_transform必须先fit在训练集上再用同一个PCA对象transform测试集降维后模型效果反而变差主成分虽然保留了方差但不一定保留了对标签有用的判别信息试试带监督的线性判别分析LDA或偏最小二乘法PLS或保留更多分量PCA结果每次运行不一致数据随机抽样、或不同库的SVD求解器差异设置random_state用数据全量时检查是否排序了特征值4.2 几个独家避坑心得第一PCA之前一定要去看一眼特征的相关性矩阵。如果原始数据里有多个变量彼此相关系数超过0.95PCA会把它们合并成一个主成分这本是好事但如果你不提前知道解释成分时就会很困惑。我习惯在跑PCA之前先把高相关变量聚个类或者直接删除一部分冗余变量会让后面的主成分载荷更清楚。第二高维小样本场景比如基因表达数据特征几万样本只有几十常规PCA会面临两个问题一是协方差矩阵 p×p 巨大计算慢二是样本量小于特征数时协方差矩阵不可逆特征值分解数值不稳定。标准解法是用SVD直接对样本矩阵分解或者用Kernel PCA、增量PCA。sklearn里PCA(svd_solverrandomized)在高维数据上会快很多我就是从那之后开始习惯性用它。第三离群值对PCA的影响超出你的预期。因为PCA完全基于方差而不成比例地放大离群值的权重。有一次我用一份用户行为数据做PCA第一主成分几乎完全指向一个离群用户那个用户在某项指标上高出常人50倍。后来我加了MADMedian Absolute Deviation和分位数裁剪之后PCA结果才恢复正常。所以别偷懒PCA前最好做一遍离群值筛查。第四注意PCA不能去除非线性冗余。PCA是线性变换它只能捕捉线性相关性。如果数据分布是马蹄形、同心圆这类非线性结构PCA会傻眼这时候得考虑t-SNE、UMAP或Kernel PCA。判断方法很简单降维后可视化如果是一坨重叠的色块可能就是数据本身是非线性结构不是你参数改错了。4.3 一些容易被忽略的细节很多教程只会告诉你PCA怎么做不会告诉你主成分的正负号没有实际意义components_第i行乘以-1之后得到的X_pca列也会全部取反但解释方差、样本点相对距离、后续模型的表现全部不变。所以看见sklearn跑出来的主成分载荷符号跟你预期相反别慌不是模型错了只是符号方向翻转了。还有一点PCA只是在纯数学上找方差最大的方向它完全不关心你跟业务方定义的“重要性”。比如做信用评分时理论上“还款逾期次数”就是业务上最重要的变量但在PCA里它可能因为方差小只落在第三、第四主成分上。所以PCA适合做降维压缩和可视化不适合做特征重要性排序。真要解释业务建议结合随机森林的feature_importance或SHAP值一起来看。5. 进阶PCA的延伸与典型应用场景5.1 数据可视化把高维信息压到人能看懂的范围PCA最广泛的应用就是数据可视化。给你一个高维数据集你画不出多维散点图但可以取前两个主成分作为横纵坐标绘制二维分布图。这在聚类分析里特别常用先跑KMeans再把聚类标签映射到二维PCA图上能很直观地看出簇的分布和重叠程度。有一点要提醒PCA的二维图丢失了部分信息如果前两个主成分累计贡献率只有40%那图上的“距离”是严重失真的。你在图里看到两个簇离得很远不代表原始高维空间也是这样。这时候你要么把贡献率低的事实写在图上要么考虑换UMAP这类更擅长保留局部结构的降维方法展示。做汇报时我会同步放上贡献率数字免得业务同事误读。5.2 特征工程与噪音过滤PCA在业务建模里另一个重要用途是去相关。很多线性模型如线性回归、逻辑回归对特征间的多重共线性很敏感系数会变得极其不稳定。如果你发现VIF值很大可以用PCA把原始特征转换成正交的主成分再拿主成分进模型。这样彻底消除了共线性模型收敛也更稳。代价是主成分的语义变得抽象不太方便给业务解释。PCA还可以做噪音过滤。大概思路是保留前k个主成分然后把主成分得分映射回原始空间得到重构数据X_reconstructed X_pca_k · components_k。这个重构数据相当于从数据中提取了主要结构、丢掉了被判定为噪声的尾成分。在某些信号处理任务里这招能明显提升信噪比。不过具体k得用实验去试没有通解。5.3 PCA在深度学习之外的特殊位置深度学习时代很多传统特征工程方法被弱化了但PCA依然有一席之地。比如在自然语言处理里词向量的后处理会用到PCA做中心化和对称化在推荐系统里矩阵分解的思路和SVD同根同源在生物信息学里PCA是最常见的样本分层和批次效应检测工具之一。很多模型的可解释性分析也会先用PCA做潜在因子提取。它能活这么多年靠的是数学上足够的简单、稳定、可解释。跟LDA做个简单对比PCA是无监督的只看特征本身的方差LDA是有监督的考虑类别间方差和类别内方差的比值。如果你的目标是分类并希望降维LDA往往比PCA保留更多类别信息。PCA是通用工具LDA是分类专用工具两者不是替代关系而是互补关系。顺便提一句PCA的“亲戚”还有不少非负矩阵分解NMF适合处理非负数据如图像和文本独立成分分析ICA适合做信号分离稀疏PCA通过加L1正则化让每个主成分只依赖少数几个原始特征可解释性更强。想进阶的话可以从这几个方向继续挖。最后再分享一个我自己工作里的习惯。我在跑PCA时很少只跑一版参数就收工而是会固定随机种子尝试保留2个、5个、10个、20个主成分配合不同的下游模型去交叉验证。PCA本质上是决策前置很重的一步你选多少个主成分直接影响后续聚类、分类的表现。与其费劲琢磨理论最优解不如把选择交给交叉验证结果用最终的模型指标说话。这个方法虽然朴素但实测下来最稳也最能在项目评审时说服人。希望这篇能把PCA从“听起来很难”变成“随时能用”你接下来去跑任何带一堆特征的数据集不妨先跑一遍PCA看看结构和贡献率说不定会对数据有全新的认识。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进