简介:面向需要将主成分分析(PCA)从原理落地到MATLAB编程实践的数据分析初学者,这份代码资源以单个pca.m脚本完整演示了从数据预处理到特征提取的典型降维流程,适用于机器学习、图像处理、信号分析等场景的特征压缩与可视化准备。压缩包内仅包含1个m文件,大小约1KB,结构极简,便于直接阅读、逐段调试和二次修改。代码覆盖数据去均值中心化、协方差矩阵计算、特征值分解、按方差贡献率选择主成分以及向新坐标系投影转换等关键环节,并对中间矩阵和结果均有清晰实现,脚本内步骤几乎与教科书一一对应,易与PCA原理互相印证。已有182人浏览学习,若需快速获得可运行的PCA模板,或想通过MATLAB代码深入理解协方差、特征向量与降维几何意义,这份资料能提供直观参考,也可作为后续项目拓展的起点脚本。
1. 在MATLAB里把PCA代码跑起来之前
看到"pca.zip_PCA Matlab_PCA matlab_PCA 代码_pca"这种命名的压缩包,第一反应通常是解压找pca.m,跑通后塞进自己的项目里。PCA主成分分析本身并不复杂,但对协方差矩阵求特征向量这一步背后,决定了后面所有结果是否可解释。真正容易踩坑的是三件事:数据预处理不一致、pca函数输出理解错误、主成分数量靠拍脑袋决定。这里从数学和pca函数的对应关系出发,落成可直接运行的MATLAB代码,再把缺失值、标准化、图像数据等常见场景讲透,适合第一次在MATLAB里做特征降维的工程师,也适合在matlab图像处理任务里想用PCA压缩特征的人。看完至少能回答三个问题:coeff、score、latent分别怎么用,主成分数量怎么定,以及怎么验证自己的PCA代码没有写错。
2. PCA数学原理如何对应到MATLAB的pca函数
2.1 协方差矩阵的特征值就是投影后的方差
PCA降维的本质,是在原始特征空间里找一组新的正交坐标轴。对n×p的数据矩阵X,每行是一个样本,每列是一个特征,协方差矩阵Σ是p×p。把数据投影到某个单位方向u上,投影方差写成u^T Σ u。要找到方差最大的方向,就是在约束u^T u=1下最大化u^T Σ u,用拉格朗日乘子法求出来的结果正好是Σu=λu。也就是说,主成分分析要解的数学问题就是协方差矩阵的特征值分解,特征向量是主成分方向,特征值是投影后的方差。
很多人把特征值和方差搞混,其实它们描述的是同一件事。特征值越大,说明该方向承载的样本差异越多,丢掉其他方向时损失的信息越少。多个特征向量彼此正交,所以主成分之间互不相关,后续接回归或分类时不容易出现特征共线性的问题。理解这一点后,再看MATLAB的pca输出就非常顺:latent就是特征值,explained就是这个特征值占总方差的比例,两个量的关系一目了然。
2.2 pca函数基于SVD而不是先算协方差矩阵
实际工程里直接对Σ做特征分解并不是最优选择。协方差矩阵是Xc^T Xc/(n-1),当p很大时,先算p×p的矩阵再分解,内存和计算量都会上升,而且Xc^T Xc的条件数是原矩阵的平方,较小的特征值容易被数值误差吃掉。MATLAB的pca函数底层优先走SVD,把中心化后的Xc分解成Xc = U S V^T,协方差矩阵的特征向量就是V的列,特征值等于diag(S).^2/(n-1)。数据矩阵本身的条件数参与计算,数值稳定性更好。历史代码里常见的princomp函数就是用协方差矩阵特征分解实现的,新写的代码我一般直接改用pca。
下面这段代码可以验证两种路径的结果:
X = randn(80, 6); % 路径一:手工求协方差矩阵的特征分解 Xc = X - mean(X); C = (Xc' * Xc) / (size(X, 1) - 1); [V_eig, D_eig] = eig(C); [evals, idx] = sort(diag(D_eig), 'descend'); V_eig = V_eig(:, idx); % 每列一个主成分方向 % 路径二:pca函数默认走SVD [V_pca, ~, latent] = pca(X); % 对比特征值 disp([evals, latent]); % 两列数值应基本一致 % 特征向量可能有符号翻转,先统一符号再对比 flip = sign(sum(V_pca)) .* sign(sum(V_eig)); V_eig = V_eig .* flip; disp(max(abs(V_eig - V_pca))); % 结果接近0说明方向一致代码里的符号统一是必要步骤:特征向量乘以-1后仍然是特征向量,pca每次运行受SVD算法内部状态影响,列符号可能相反。直接用V_eig减V_pca算误差没有意义,需要先把两列方向对齐。如果最后输出的max误差在1e-12数量级,说明pca函数的SVD路径和手工特征分解在数学上等价,两者差异只在数值稳定性上。
2.3 一行命令拿到PCA的所有核心输出
最精简的调用方式是这样:
rng(2024); data = randn(150, 8); [coeff, score, latent, tsquared, explained] = pca(data);data是150个样本、8个特征,pca默认对每列做零均值化,然后返回五个量。coeff是8×8矩阵,每一列是一个主成分的方向向量,模长为1;score是150×8,是原始数据在新坐标系里的坐标;latent是8×1,对应每个主成分解释的方差;tsquared是每个样本的Hotelling T²统计量;explained是每个主成分解释方差的百分比,加起来等于100。
| 输出变量 | 尺寸 | 工程含义 |
|---|---|---|
| coeff | p×p | 主成分方向,各列相互正交 |
| score | n×p | 样本在主成分空间的坐标 |
| latent | p×1 | 各主成分的特征值,即解释方差 |
| tsquared | n×1 | 每个样本到主成分中心的马氏距离 |
| explained | p×1 | 各主成分解释方差的百分比 |
第一次跑通这段代码后,建议先看explained的前几个值。如果前两个值加起来超过85%,代表大部分结构信息集中在两个方向上,绘制score前两列的散点图通常就能看出样本的聚集模式。上面生成的是随机正态数据,不会出现这种陡峭下降,看到latent数值接近均匀,说明数据本身没有明显的低维结构,这时强行降维会损失信息。
3. 用MATLAB实现PCA代码时的预处理和参数调整
3.1 缺失值和量纲差异,先处理再调用pca
pca函数对缺失值的默认行为是删除整行,对应参数Rows的默认值complete。数据中只要有一列是NaN,该行就不会进入计算。这个行为在样本量大的时候不容易被察觉,但一旦数据里缺失较多,latent来自不同的行集合,和全量数据算出来的结果会对不上。另一个选项是pairwise,它逐个特征对计算协方差,只丢弃当前特征对上的缺失行,行数损失小,代价是协方差矩阵可能不是正定的,后续特征值里会出现数值上很小的负值,这是正常现象,但会给一些依赖正定性的下游算法带来困扰。
量纲问题往往更隐蔽。pca只对每列减去均值,默认不除标准差。列单位如果是毫米、千克、秒混在一起,方差大的列会主导前几个主成分,这不是我们想要的特征结构。要统一量纲,设置VariableWeights为variance即可,等效于先把每列做z-score再跑pca,但不用自己额外写标准化代码。
X = randn(120, 5); X(20, 3) = NaN; % 人为制造一个缺失值 [~, ~, latent_complete] = pca(X); % 实际只用了119行 [~, ~, latent_pairwise] = pca(X, 'Rows', 'pairwise'); % 保留全部行 fprintf('complete 特征值: %s\n', mat2str(latent_complete', 4)); fprintf('pairwise 特征值: %s\n', mat2str(latent_pairwise', 4)); % 量纲不一致的模拟数据 X2 = randn(100, 3); X2(:, 1) = X2(:, 1) * 1000; % 第一列量纲远大于另外两列 [~, ~, ~, ~, explained_raw] = pca(X2); [~, ~, ~, ~, explained_std] = pca(X2, 'VariableWeights', 'variance'); disp([explained_raw, explained_std]);pca处理含缺失值的数据时,返回特征值的长度可能小于输入维度,必要时需要检查latent的尺寸再继续下游计算。X2那个例子中,第一列方差被人为放大,explained_raw里第一个主成分占比会非常高;开启VariableWeights后三列被拉回同一尺度,explained_std的分布才体现出真实的变量相关性。两组对比输出可以帮助确认自己的预处理方向有没有偏差。
3.2 一套完整可改写的PCA降维代码
用fisheriris数据把整个流程串起来。这个数据集在MATLAB中自带,不需要额外下载,适合用来验证代码逻辑。
load fisheriris; X = meas; % 150×4,四个花萼花瓣特征 labels = species; % pca默认居中但不缩放,这里四个特征都是厘米,直接跑 [coeff, score, latent, ~, explained] = pca(X); % 累计解释方差图 figure; subplot(1, 2, 1); pareto(explained); grid on; title('各主成分解释方差'); % 按前两个主成分投影并着色 subplot(1, 2, 2); gscatter(score(:, 1), score(:, 2), labels); xlabel('PC1'); ylabel('PC2'); grid on; % 自查:score的方差应该等于latent fprintf('latent(1)=%.4f,var(score(:,1))=%.4f\n', latent(1), var(score(:, 1)));gscatter画出来的效果通常很好:setosa这一类明显分离,virginica和versicolor有重叠,但投影到PC1和PC2之后类间差异被放大。代码最后一行做的事情是自查:pca把数据投影到新坐标后,score每一列的样本方差必须等于latent对应分量。如果不等,说明前面手工做过标准化,或者pca的Centered参数被改动过,需要回头检查数据。工程代码里建议保留这样的断言式检查,宁可多花一行打印,也不要在结果里追查方向反了或数据被重复居中这类问题。
3.3 四个常用参数用对才算真正控制住PCA
pca函数的行为高度依赖参数,项目里改动最频繁的是下面几个:
| 参数 | 默认行为 | 使用建议 | 容易踩的坑 |
|---|---|---|---|
| NumComponents | 保留全部维度 | 内存紧张或明确只要前k个主成分时使用 | 输出尺寸在不同版本间可能有差异,用前先size()确认 |
| Centered | true,每列减均值 | 数据已经手动居中时设为false | 重复居中会让主成分方向解释变绕 |
| VariableWeights | 不缩放 | 特征量纲不一致时设variance | 已经手动zscore过就别再设,等于二次标准化 |
| Rows | complete | 缺失值多且分散时用pairwise | 协方差矩阵可能不正定,小特征值可能失真 |
NumComponents的作用是直接截断coeff和score,比如pca(X, 'NumComponents', 2)返回的主成分系数只有两列,后续投影乘法会快很多。需要留意的是,不同版本里latent和explained返回的长度不一定一致,有的版本在截断后只返回对应主成分的长度,有的仍然返回全量。因此不要盲目在后续代码里写coeff(:, 1:2),建议用完NumComponents后马上用size确认输出尺寸,再继续接下来的逻辑。
Centered是最容易让人迷惑的参数。pca默认会把每列的均值减掉,如果你的数据已经手动执行过X - mean(X),再交给pca就会做二次居中。二次居中的结果是主成分方向不再针对原始数据,而是针对已经被平移过一次的数据,对方差计算影响不大,但对系数解释会变得很绕。我的习惯是:非必要不在外面手动居中,直接交给pca;必须手动居中时,调用里显式写'Centered', false。
4. 把PCA代码扩展到matlab图像处理和批量数据
4.1 图像怎么组装成pca能接受的数据矩阵
matlab图像处理里用PCA,最基本的方式是把一张二维灰度图展开成一维行向量。假设有N张尺寸相同的灰度图,每张有m×n个像素,整理出来的数据矩阵X就是N行、m×n列。每一列是一个像素位置在所有样本里的灰度值,PCA会去寻找哪些像素位置总是一起变亮或一起变暗。
彩色图像需要提前转成灰度,或者把RGB三个通道分开处理,因为pca并不知道通道结构。图片尺寸不统一必须先imresize到同一尺寸,否则reshape出来的矩阵行数都不一致,这一步放在读图循环里做最方便。
imgFiles = dir(fullfile('image_set', '*.png')); num = numel(imgFiles); s = imread(fullfile(imgFiles(1).folder, imgFiles(1).name)); if size(s, 3) == 3 s = rgb2gray(s); end s = imresize(s, [128, 128]); pix = numel(s); X = zeros(num, pix); for k = 1:num img = imread(fullfile(imgFiles(k).folder, imgFiles(k).name)); if size(img, 3) == 3 img = rgb2gray(img); end img = imresize(img, [128, 128]); X(k, :) = double(img(:))'; end [coeff, score, ~, ~, explained] = pca(X);读取图片时,dir返回的只是文件名,读文件时需要拼接folder字段;imread会按扩展名自动识别格式。构建X时先检查尺寸,再看是不是彩色图,最后考虑内存。128×128的灰度图展开后有16384列,1000张图的双精度矩阵约128 MB,这个量级可以直接跑pca。如果是256×256的彩色图,展开后接近20万列,内存会到数百MB,就需要考虑用single转换或分块处理。
| 数据形态 | 矩阵尺寸 | 双精度内存 | 是否适合直接pca |
|---|---|---|---|
| 128×128灰度×500张 | 500×16384 | 64 MB | 适合 |
| 28×28灰度×1000张 | 1000×784 | 6 MB | 很轻松 |
| 256×256彩色×200张 | 200×196608 | 314 MB | 需要优化 |
彩色图的列数会额外增加两个通道,处理前先转成灰度能显著降内存。图像PCA里取前20到50个主成分通常已经能保留大部分结构信息,没必要把所有主成分都求出来。
4.2 把主成分还原成图像来理解代码在做什么
pca求出的coeff每一列都可以还原成一张图。coeff第i列的长度是16384,reshape成128×128之后,图像上亮的位置代表这些像素在该主成分方向上权重更高。score则是每个样本在这个主成分方向上的投影值。
% 显示前9个主成分的可视化 figure; for i = 1:9 subplot(3, 3, i); imshow(reshape(coeff(:, i), 128, 128), []); title(sprintf('PC%d', i)); end % 用前20个主成分重建第1张图 k = 20; Xhat = score(:, 1:k) * coeff(:, 1:k)' + mean(X, 1); rmse = sqrt(mean((X(1, :) - Xhat(1, :)).^2)); fprintf('前%d个主成分重建单张图RMSE=%.4f\n', k, rmse);coeff本身就是矩阵,imshow会直接按元素灰度显示,加[ ]参数是为了让MATLAB动态映射灰度范围。重建公式里最后要加mean(X, 1),因为pca默认居中,score和coeff乘出来是去均值后的重建值,必须把均值加回去才能跟原图对比。如果调用pca时用了'Centered', false,这行里就不用再加mean项,这是常见的分支错误。
4.3 把PCA代码封装成训练函数供新数据使用
实际项目里,PCA的降维逻辑一定要封装起来,训练阶段保存系数和均值,预测阶段用保存的系数对新样本投影。新样本不应该再调用pca重新训练,否则会得到另一套coeff,坐标系变化后所有特征都不可比。
function [coeff, mu, score, explained] = fit_pca(X, k) % 训练专用,算出系数和均值,测试集用这两个量投影 if nargin < 2 error('必须指定保留的主成分数k'); end mu = mean(X, 1); Xc = X - mu; [coeff_all, score_all, ~, ~, explained] = pca(Xc, 'Centered', false); coeff = coeff_all(:, 1:k); score = score_all(:, 1:k); end这里手动减均值后必须设置'Centered', false,否则pca还会再做一次居中。新样本进入时,用同一个mu减均值,再乘系数矩阵就是投影结果:
mu = mean(X_train, 1); [coeff, ~, ~, ~] = fit_pca(X_train, 20); score_test = (X_test - mu) * coeff;把fit_pca放到单独的.m文件里,其他脚本引用时只要传数据矩阵和k两个参数。这个结构同样适用于图像PCA场景,训练集和测试集的划分在图像分类任务里尤为重要,测试集直接复用训练好的coeff,避免信息泄漏。
5. 用解释方差和重建误差校验PCA代码的主成分数量
5.1 特征值和解释方差是选k的第一道依据
主成分数量k的确定,工程上最常用的规则有两个:Kaiser规则是保留特征值大于1的主成分,适合标准化后的数据;累积解释方差法是以explained的累计比例超过90%为阈值,适合一般业务数据。两套规则本身都是经验值,画一条碎石图曲线看拐点更直观。
load fisheriris; [coeff, score, latent, ~, explained] = pca(meas); cumvar = cumsum(explained); k_90 = find(cumvar >= 90, 1, 'first'); fprintf('累计解释方差达到90%%需要%d个主成分\n', k_90); figure; plot(cumvar, 'o-'); hold on; yline(90, '--'); xlabel('主成分数量'); ylabel('累计解释方差(%)'); grid on;fisheriris数据跑出来k_90通常是2,和直接看score前两列散点图的聚类效果吻合,说明保留两个主成分对整个数据集的解释力足够。样本量增大或噪声变强会让explained曲线变平,这时找拐点比死守90%阈值更稳定。
5.2 用留一重建误差避开代码里的过拟合问题
仅靠解释方差选择k容易掉进一个假象:主成分数量越多,重建误差越低,但这不代表泛化能力越好。想确认PCA代码做出来的降维结果能不能用在新样本上,可以用留一法做重建验证:每次拿出一个样本,用剩余样本拟合pca,再用前k个主成分重建被拿出的样本并计算误差。
function err = loo_pca_error(X, k) n = size(X, 1); errs = zeros(n, 1); for i = 1:n tr = X; tr(i, :) = []; mu = mean(tr, 1); [c, ~, ~, ~, ~] = pca(tr - mu, 'Centered', false, 'NumComponents', k); xr = mu + (X(i, :) - mu) * c * c'; errs(i) = sum((X(i, :) - xr).^2); end err = mean(errs); end调用时按k=1到6分别计算,画出误差随k变化的曲线。k太小模型表达力不足,重建误差高;k增大到能表达真实数据结构,误差快速下降并进入平台期;如果数据带噪声,k继续增加误差反而缓慢上升,因为主成分开始拟合噪声。曲线最低点对应的k就是留一法认为最合适的降维维度。
这个函数在样本量上千时会跑得很慢,可以只对随机抽出的200行子集计算,或者用parfor把循环并行化。工程上我不会只盯着留一法的最小值,而是把Kaiser准则、90%累计解释方差和留一误差三个结果放在一起看,三者一致时直接采用;不一致时优先选三者里最小的k作为安全线,后续再用下游任务的验证集精度校准一次。
本文还有配套的精品资源,点击获取