简介:PCA(主成分分析)是高维数据降维的经典算法,被广泛用于机器学习、统计学与图像处理等场景。这份资源是基于Matlab实现的PCA代码包,面向Matlab初学者、算法入门者以及希望在项目中快速调用降维功能的开发者,代码风格精简,便于学习与复用。压缩包内共1个m文件,大小仅329B,属于轻量级代码资源,可直接在Matlab中运行,也可作为模板嵌入更复杂的分析流程。目前已有540人学习下载,作者为qq_61141142。实现覆盖了数据标准化、协方差矩阵计算、eig函数求解特征值与特征向量、按特征值大小排序、选取前k个主成分、投影到新空间以及数据重建步骤,既能帮助理解主成分分析的数学原理,也能直接用于处理自己的数据,是兼顾教学演示与工程落地的实用工具。
1. 从一组PCA Matlab代码开始的降维实战
拿到PCA Matlab代码.zip,里面只有一个PCA Matlab.m,却能串起整个主成分分析流程。很多人在Matlab里用pca()函数一句话做完降维,却说不清协方差矩阵和特征向量到底起了什么作用;而这个脚本的好处是,每一步都摊开,从标准化、协方差、特征值分解到投影重建全部能用debug跟读。适合两类人:一类是刚接触PCA、想搞清楚降维原理的开发者,另一类是在高维特征工程里需要自定义降维逻辑、不想被内置函数黑盒限制的熟手。接下来我直接按这个脚本的顺序,把每一步的原理、参数和坑一次讲透。
2. 数据标准化与协方差矩阵:PCA的数学前提与Matlab实现
2.1 为什么先做零均值化和方差缩放
PCA的核心是找到数据方差最大的方向,但“方差”这个概念对量纲非常敏感。特征A取值范围是0到1,特征B取值范围是0到10000,如果不做处理,协方差矩阵会被特征B的数值尺度主导,主成分方向几乎完全偏向B,A的结构信息直接被淹没。所以通用做法是先对每一列做z-score标准化:减去均值再除以标准差,让每个特征都变成零均值、单位方差。
这里有一个容易忽略的点:标准化之后,协方差矩阵实际上变成了相关系数矩阵,此时PCA找的是“相关结构”而不是“协方差结构”。如果数据本身就是同一物理量纲,比如同一传感器多个通道的读数,可以只做均值中心化而不除以标准差;但如果特征来自温度、压力、流量等不同单位,z-score标准化是必须的。在实际项目中,我会先看特征量纲差异是否超过一个数量级,再决定是否做方差缩放,而不是盲目统一处理。
2.2 标准化与协方差矩阵的Matlab代码
下面这一段对应PCA Matlab.m里的预处理和协方差计算部分,我补了注释和可复现的随机数据:
% 生成测试数据:100个样本,5个特征 rng(42); X = randn(100, 5); X(:, 2) = X(:, 1) * 0.8 + randn(100, 1) * 0.2; % 让第2列与第1列强相关 % 步骤1:零均值化 + 方差缩放 mu = mean(X, 1); % 每个特征的均值,1x5 sigma = std(X, 0, 1); % 每个特征的标准差,1x5,0表示除以n-1 X_std = (X - mu) ./ sigma; % 步骤2:计算协方差矩阵(标准化后的协方差即相关系数矩阵) [n, p] = size(X_std); C = (1 / (n - 1)) * (X_std' * X_std); % 输出矩阵尺寸 fprintf('协方差矩阵维度: %d x %d\n', size(C, 1), size(C, 2));这里mean(X, 1)是沿第一维(行方向)求均值,std(X, 0, 1)中第二个参数0表示使用n-1作为分母,第三个参数1表示按列计算。计算协方差时用X_std' * X_std而不是cov(X_std),是为了明确看到除以n-1的数学过程,避免把自由度修正藏在函数内部。协方差矩阵C是对称半正定矩阵,维度是p x p,这里的p=5对应特征数,不是样本数,这个方向别搞反。
2.3 协方差矩阵的边界:样本量小于特征维度
当n < p,也就是样本数小于特征数时,直接用上面的公式得到的协方差矩阵是奇异的,eig()也会返回接近零的特征值,主成分方向不稳定。这种情况在基因表达谱、用户行为稀疏矩阵里非常常见。常见做法有两种:一是先用SVD直接对数据矩阵做分解,绕开构造协方差矩阵这一步;二是加正则化项,比如在协方差矩阵对角线加上一个很小的lambda * eye(p),等价于岭估计。
下面是一个n=20, p=50的对比示例,展示特征值的分布差异:
X = randn(20, 50); C_raw = (1 / 19) * (X' * X); lambda_ridge = 0.01; C_reg = C_raw + lambda_ridge * eye(50); ev_raw = eig(C_raw); ev_reg = eig(C_reg); fprintf('原始协方差矩阵最小特征值: %.6f\n', min(ev_raw)); fprintf('加正则后最小特征值: %.6f\n', min(ev_reg));lambda_ridge不是超参数里的摆设,它的取值一般参考对角线元素均值的0.01~0.1倍。加正则化会让部分小特征值被抬升,避免后续特征向量求解时出现数值震荡。需要特别注意的是,正则化改变了原始特征值的大小关系,可能导致方差贡献率计算偏差,因此只在必须稳定求解时才使用,常规n >> p场景不要加。
| 场景 | 是否标准化 | 协方差计算方式 | 备注 |
|---|---|---|---|
| 特征量纲不一致 | 是,z-score | X_std' * X_std / (n-1) | 常用 |
| 特征量纲一致 | 仅中心化 | cov(X) | 保留原始尺度 |
| 样本量小于特征数 | 是 | SVD或正则化 | 避免奇异矩阵 |
3. 特征值分解与主成分选取:从eig到投影重建
3.1 特征值排序与方差贡献率
协方差矩阵的特征值和特征向量把“哪个方向的信息最多”变成了可量化的数值。特征值lambda_i表示第i个主成分方向上的方差大小,方差贡献率就是lambda_i / sum(lambda)。Matlab的eig()返回的特征值默认不排序,而且可能是降序也可能是升序,取决于底层LAPACK实现,所以拿到结果后的第一件事永远是排序。
我一般先把特征值和特征向量捆绑到一起,用sort按特征值降序排列,再计算累计贡献率。这一步也是PCA Matlab.m里最容易出错的地方,很多初学者直接把eig()的结果当成主成分顺序,导致选取的前k个方向根本不是方差最大的方向。
% 基于上一章的协方差矩阵C [V, D] = eig(C); lambda = diag(D); % 提取特征值向量 [lambda_sorted, idx] = sort(lambda, 'descend'); V_sorted = V(:, idx); % 特征向量按特征值大小同步排序 % 计算方差贡献率和累计贡献率 total_var = sum(lambda_sorted); explained = lambda_sorted / total_var; cum_explained = cumsum(explained); % 打印前3个主成分的贡献 for i = 1:3 fprintf('PC%d: 方差贡献率=%.4f, 累计=%.4f\n', ... i, explained(i), cum_explained(i)); endsort(lambda, 'descend')的第二个返回值idx是原始位置到降序位置的映射,用它去重排V的列,能保证特征向量和特征值对应关系不错位。cumsum是求累计和的函数,用一个向量就能得到从第一主成分到所有主成分的累计贡献曲线。这段代码建议单独封装成sort_eigen(C)函数,因为在后续换数据集时,这个排序逻辑会被反复使用。
3.2 投影与重建的Matlab实现
选定前k个特征向量后,投影就是原始数据乘以特征向量矩阵。这里有一个维度陷阱:如果X是n x p,特征向量矩阵V(:, 1:k)是p x k,投影结果score = X_std * V(:, 1:k)是n x k。重建则是reconstructed = score * V(:, 1:k)',得到的是n x p,注意投影和重建的特征向量矩阵互为转置关系,不是同一个方向。
% 选择前2个主成分 k = 2; V_k = V_sorted(:, 1:k); score = X_std * V_k; % 降维后的数据,n x k reconstructed = score * V_k'; % 重建数据,n x p % 计算重建误差(均方根误差) recon_error = sqrt(mean((X_std - reconstructed).^2, 'all')); fprintf('k=%d 时重建RMSE: %.6f\n', k, recon_error);score在Matlab的统计工具箱里也叫主成分得分,它的每一列就是样本在对应主方向上的坐标。重建误差可以用'all'参数一次性对所有元素求均值,不需要套两层mean。如果把k从1一直取到p,重建误差会单调递减,到k=p时误差为0;这个单调性可以用来验证降维过程是否有bug,如果出现误差不降反升的情况,多半是特征向量排序错位或者投影时用了转置矩阵。
3.3 k的选择:累计方差阈值与肘部法
k值是PCA里唯一的决策参数,选大了保留噪声,选小了丢信息。工程上最常见的标准是累计方差贡献率超过85%或90%,但这个阈值不是物理定律,如果特征是强噪声的传感器数据,95%以上才够用;如果是用于可视化,往往只要前两维。
拿一组实际运行的示例来说,特征值向量为[3.1, 1.4, 0.7, 0.5, 0.3],累计贡献率如下表:
| 主成分 | 特征值 | 方差贡献率 | 累计贡献率 |
|---|---|---|---|
| PC1 | 3.1 | 51.67% | 51.67% |
| PC2 | 1.4 | 23.33% | 75.00% |
| PC3 | 0.7 | 11.67% | 86.67% |
| PC4 | 0.5 | 8.33% | 95.00% |
用肘部法看,前3个主成分已经到86.67%,曲线从第4个开始明显变平,这时取k=3既保留主要结构又压制噪声。注意累计贡献率不是越高越好,高到90%以后多出来的主成分通常对应单一特征的残余噪声,反而干扰下游聚类或分类模型。
4. 读透PCA Matlab.m:脚本结构与内置pca函数对比
4.1 脚本实现的完整流程
PCA Matlab.m本质上就是把第2章和第3章的内容串成一个线性脚本。整理后的大致结构如下,带注释的版本可以直接替换数据矩阵使用:
function [score, V, explained, reconstructed] = pca_manual(X, k) % 输入: X为n行p列数据矩阵,k为保留主成分个数 % 输出: score是降维结果,V是主方向,explained是贡献率,reconstructed是重建数据 % 标准化 mu = mean(X, 1); sigma = std(X, 0, 1); X_std = (X - mu) ./ sigma; % 协方差矩阵 C = (1 / (size(X, 1) - 1)) * (X_std' * X_std); % 特征值分解与排序 [V, D] = eig(C); [explained, idx] = sort(diag(D), 'descend'); V = V(:, idx); explained = explained / sum(explained); % 投影与重建 V_k = V(:, 1:k); score = X_std * V_k; reconstructed = score * V_k'; end这个函数最值得读的是输入输出设计:k作为显式参数,explained返回全部贡献率而不是只返回前k个,这样外部调用者可以根据贡献率变化重新决定k,不需要重新计算特征值分解。与Matlab内置pca()相比,手动实现的版本暴露了中间量,方便在调试时检查协方差矩阵的数值合理性,但也缺少了内置函数对中心化方式、缺失值处理和SVD收敛算法的自动优化。
4.2 与Matlab内置pca函数的差异
统计工具箱里的pca()是生产环境的首选,它在底层使用SVD而不是显式构造协方差矩阵,数值稳定性更好,运算速度也更快。从使用层面看,两者有以下关键差别:
| 对比项 | 手动脚本pca_manual | 内置pca() |
|---|---|---|
| 输入数据形态 | 必须提前标准化 | 通过'Standardize'参数控制 |
| 主成分方向 | 特征向量,符号不固定 | 默认使最大绝对值方向为正 |
| 缺失值 | 直接报错 | 支持'Algorithm','als'处理 |
| 输出 | 自定义结构 | coeff, score, latent等 |
符号不固定这一点很坑。eig(C)返回的特征向量乘以-1仍然是特征向量,两次运行的V符号可能完全不同,如果拿降维结果去训练模型,模型权重会跟着翻转,但预测结果不受影响。内置pca()会对特征向量做符号调整,保证同一数据多次运行结果一致,可复现性更好。如果你把手动脚本的结果和内置函数对比,不要直接比较V,先看abs(V)的投影距离。
4.3 内置pca的参数设置与输出解析
如果实在需要跨数据集复用,我一般直接调用:
[coeff, score, latent, tsquared, explained] = pca(X, ... 'NumComponents', 3, ... 'Center', true, ... 'Standardize', true); % 重建 X_reconstructed = score * coeff(:, 1:3)' + mean(X);这里'NumComponents'等价于手动版本的k,'Center'控制是否去均值,'Standardize'控制是否按标准差缩放。latent是特征值向量,和手动版本的lambda_sorted对应;explained是百分比贡献率;tsquared是Hotelling T2统计量,用来检测离群样本,手动脚本里没有这个输出。需要注意的是'Standardize', true只在特征量纲不一致时使用,如果已经手动标准化过,再传true会导致重复缩放。
5. 高阶技巧:用交叉验证评估降维效果与批量处理
5.1 最小重构误差法评估k
前面用累计贡献率选k有一个隐含缺陷:它只衡量了“训练集上”的方差保留量,没有考虑过拟合。更严谨的做法是把数据切成训练块和验证块,在训练块上做PCA,把验证块投影到主方向后重建,然后用验证集的重构误差来选择k。这个思路和交叉验证选择模型复杂度是一致的。
% 按行随机切分训练/验证 rng(7); idx = randperm(100); X_train = X_std(idx(1:70), :); X_valid = X_std(idx(71:end), :); % 在训练集上分解 [V, ~] = eig(cov(X_train)); lambda = diag(D); [~, ii] = sort(lambda, 'descend'); V = V(:, ii); % 对多个k计算验证集重构误差 for k = 1:size(X,2) V_k = V(:, 1:k); rst = (X_valid * V_k) * V_k'; err(k) = sqrt(mean((X_valid - rst).^2, 'all')); end [~, best_k] = min(err);randperm打乱样本顺序后按比例切分,避免数据本身存在的批次效应影响评估。这个流程每多试一个k就多一次矩阵乘法和误差计算,在p上千时比较慢,可以用parfor替代for,但要注意V_k在每次迭代中只是列数不同,共享训练集特征向量,不存在数据竞争。
5.2 批量处理多个数据集的脚本范式
实际项目中经常要同时处理多个CSV文件,每个文件的特征维度相同但样本量不同。可以把pca_manual封装到循环里,把所有结果存成结构数组:
files = dir('dataset_*.csv'); for i = 1:length(files) data = readmatrix(files(i).name); [score, V, explained, reconstructed] = pca_manual(data, 2); save(sprintf('pca_result_%d.mat', i), 'score', 'V', 'explained', 'reconstructed'); enddir返回的结构体里包含文件名和修改时间,readmatrix自动识别CSV的数值列,不需要手写csvread。保存时用sprintf生成动态文件名,后续读取时用load加变量名恢复。这个模式适合离线批处理,但如果文件数量上千,建议在循环里加try-catch跳过坏文件,防止单文件格式错误中断整批任务。
5.3 可视化双标图与biplot
降维到二维后,最直观的验证方式是biplot。它把主成分得分和原始特征在主方向上的载荷画在同一张图上,特征向量越长,说明该特征对当前主成分的贡献越大;向量夹角越小,特征相关性越强。
[coeff, score, latent] = pca(X_std, 'NumComponents', 2); biplot(coeff, 'scores', score(:, 1:2), 'varlabels', {'f1','f2','f3','f4','f5'});如果箭头都重叠在一起,说明这些特征高度冗余,即使不做PCA也可以直接删掉部分列;如果某个箭头长度接近零,说明这个特征在前两个主成分里几乎不发挥作用,可以考虑单独检查原始分布是否异常。这个图比累计贡献率更适合向业务方解释降维结果,因为能直接看出哪些变量驱动了数据结构变化。
本文还有配套的精品资源,点击获取