
简介一套结合人脸识别与主成分分析PCA降维的MATLAB实践资料由31att.com整理发布面向机器学习初学者和人脸识别研究者帮助解决高维图像数据计算量大、特征冗余的问题既可作为入门练习也能用于算法对比实验。压缩包共802个文件内含400张BMP和400张PGM格式的人脸图像另有1个TXT说明文件与1个M格式的MATLAB脚本总大小6.24MB数据与代码覆盖完整。已有598人浏览学习适合用于复现PCA特征脸算法或作为课程设计、期末项目的参考。包内图像构成可直接读取的数据集M脚本可运行实现图像灰度化、中心化、协方差矩阵分解及前K个主成分提取并输出可视化的特征脸TXT文件则提供必要的说明便于整理实验步骤与结果。学习后可掌握PCA降维在人脸识别中的完整应用链路并可用于后续扩展SVM分类器或改进识别模型。1. 这个标题在讲什么MATLAB 跑通 ATT 人脸库的完整链路att_faces_31att_www.31attcom_www.31attcom_faces_matlab_这种文件名一看就是从 31att.com 这类镜像站拖下来的 ATT 人脸库压缩包。里面装的就是经典的 ORL 人脸数据集40 个人、每人 10 张、总共 400 张 112×92 像素的 8 位灰度 PGM 图按s1/1.pgm到s40/10.pgm的组织方式存放。MATLAB 课上要交图像处理大作业、模式识别课设或者准备人脸识别方向毕业设计开题的人十有八九会拿到这个包。这个实验真正花时间的不是后面的 PCA 或深度学习而是前面那几步脏活解压后路径怎么组织、PGM 文件怎么读进来、400 张图怎么排成矩阵、训练测试集怎么切。很多同学在这一步用imread一张张读读一半路径报错或者把图拉成向量之后发现方向跟别人代码对不上后面全部白做。这篇文章就按我自己做这类实验的顺序从文件读取讲到 PCA 特征脸识别再讲几个最容易翻车的坑照着敲就能跑通。2. 先搞懂 ATT 人脸库的构成40 人 × 10 张图读图前必须想清楚四件事2.1 目录结构、命名规则和图像参数ATT 人脸库也叫 ORL 人脸库压缩包解压后顶层是一个att_faces目录里面排着s1到s40共 40 个子目录每个子目录里是1.pgm到10.pgm十张图。也就是说第s个人的第p张照片完整路径是解压目录/att_faces/s编号/编号.pgm。每张图尺寸是 112×92灰度级 256文件头里能看到P5这个魔数后面跟着宽高和最大灰度值然后就是裸的二进制像素数据。读图之前有四个事必须先确认。第一这个库不同版本可能有小差异早期 ORL 库有人的照片是带边框的但att_faces这个目录里的图基本都裁好了第二虽然格式是 PGM但 MATLAB 的imread直接支持这种格式不需要自己解析文件头第三图像是 8 位灰度读进来是uint8做矩阵运算前必须转double不然减法、除法和特征分解全部会出类型错误第四所有图片尺寸必须一致如果某一张不是 112×92后面拼数据矩阵时会直接报维度不匹配。2.2 用 MATLAB 批量读取 400 张 PGM 的标准写法% 读取 ATT 人脸库全部 400 张图输出一个 10304 x 400 的数据矩阵 clear; clc; close all; dataDir fullfile(pwd, att_faces); % 改成你自己的解压路径 numSubjects 40; % 40 个人 numPerSubject 10; % 每人 10 张 imgH 112; imgW 92; numPixels imgH * imgW; % 10304 维列向量 X zeros(numPixels, numSubjects * numPerSubject); Y cell(numSubjects * numPerSubject, 1); % 顺便存标签 idx 0; for s 1:numSubjects for p 1:numPerSubject idx idx 1; filename fullfile(dataDir, sprintf(s%d, s), sprintf(%d.pgm, p)); img imread(filename); % 读入 uint8 灰度图 X(:, idx) double(img(:)); % 拉成列向量并转 double Y{idx} sprintf(s%d_p%d, s, p); end end disp(size(X)); % 期望看到 10304 x 400这段代码有两处值得说明。fullfile在 Windows 和 Linux 下都能正确处理路径分隔符不要自己用[att_faces/s num2str(s)]这种字符串拼接遇到路径里有空格或反斜杠时会很痛苦。img(:)把 112×92 的矩阵按列优先拉成 10304×1 向量这个顺序和reshape是配套的后面如果要把特征脸显示回图像必须用reshape(eigenface, [112 92])顺序不能乱。2.3 数据矩阵的两种布局样本在列还是样本在行上面代码把每张图作为一列最后得到 10304×400 的矩阵。这是 PCA 的常规布局列是样本行是特征维度。但不少教程里写的是 400×10304也就是每行一个样本两种布局在 MATLAB 里都能做 PCA只是后面代码的转置关系不一样经常有人在这上面栽跟头。我一般统一用“样本在列”的布局。理由有两点一是和eig、svd的矩阵约定直接对应协方差矩阵是Xc * Xc特征向量按列排投影坐标也是按列输出二是后续画特征脸时每一列特征向量reshape成 112×92 就是一张脸不需要额外转置。如果你拿到的别人的代码用的是行布局不要硬改数据矩阵把X换成X就行改了布局就要连带改后面所有公式很容易改错。还有一点要注意数据矩阵用double存400 张图只有 4 亿多字节大概 40MB 内存随便一台电脑都扛得住不需要为了省内存去用single或uint8存。后面做特征分解时MATLAB 会自动把single转成double计算提前转好反而少一层麻烦。3. 用 PCA 特征脸做第一版识别从协方差矩阵到最近邻分类3.1 为什么 ATT 库首选 PCA数据量小、类内变化单纯MATLAB 自带函数够用ATT 库是 1992 年到 1994 年间采集的背景基本是均匀的深色人脸朝向和表情有变化但光照变化不大。这种数据用 PCA主成分分析做特征提取非常合适40 类 × 10 张每类只有 10 个样本用深度学习反而容易过拟合。PCA 把人脸图像投影到低维子空间得到的特征向量叫“特征脸”可视化效果好写进报告里也容易讲清楚。MATLAB 里做 PCA 有三个选择pca函数、eig配合协方差矩阵、svd。pca函数封装得最好还能直接输出累计方差贡献率和投影坐标但它的默认行为是中心化数据、按奇异值分解实现内部对一些细节做了黑匣子处理eig则要求你手动处理均值、排序、归一化虽然代码长一点但每一步都看得见出了错也容易定位。我这里用eig因为它更适合教学和理解而且用在小协方差矩阵上速度极快。3.2 训练测试划分与 PCA 训练的最小代码% 每个人前 8 张做训练后 2 张做测试 trainIdx []; testIdx []; for s 1:40 baseIdx (s - 1) * 10; % 每个人起始偏移 trainIdx [trainIdx, baseIdx (1:8)]; % 1~8 testIdx [testIdx, baseIdx (9:10)]; % 9~10 end Xtrain X(:, trainIdx); Xtest X(:, testIdx); % 中心化减去平均脸 meanFace mean(Xtrain, 2); Xc Xtrain - meanFace; % 用小协方差矩阵做特征分解10304 维太大直接做会卡死 Sigma (Xc) * Xc / (size(Xc, 2) - 1); % 320 x 320 [V, D] eig(Sigma); d diag(D); [d, order] sort(d, descend); % 特征值降序 V V(:, order); % 计算特征脸主成分方向保留前 K 个 K 25; W Xc * V(:, 1:K); % 主成分方向未归一化 W W ./ sqrt(sum(W.^2, 1) 1e-12); % 归一化防止数值溢出 % 把训练集和测试集都投影到特征脸空间 Ytrain W * Xc; Ytest W * (Xtest - meanFace);这段代码的核心逻辑是先中心化、再对 320×320 的小协方差矩阵做特征分解。直接对 10304×10304 的协方差矩阵做eig在普通电脑上要算很久而且数值稳定性差用小协方差矩阵再左乘Xc是标准做法。W的每一列都是一张特征脸归一化加1e-12是防止某列模长恰好为 0 时除出NaN。K 取 25 是一个比较稳妥的起点。ATT 库 40 个人25 个主成分大约占了累计方差贡献率的 95% 左右后面分类效果一般不会差。K 太小比如 5只保留了前几张特征脸主要描述的是整体亮度变化区分不出人K 太大比如 100会把噪声也学进去测试集识别率反而下降。具体 K 怎么调下一章细说。3.3 最近邻分类与识别率统计欧氏距离和余弦距离的差别投影之后分类器用最简单的最近邻1-NN就能达到很高的识别率。测试集每一张图的投影向量Ytest(:, i)和训练集所有投影向量算距离取最小距离对应的训练样本标签作为预测结果。距离度量我一般先用欧氏距离它和 PCA 的均方误差目标是匹配的余弦距离对向量模长的变化不敏感如果光照变化大可以换但对 ATT 库来说欧氏距离已经够用。% 最近邻分类并统计识别率 predLabels zeros(size(testIdx)); for i 1:size(Ytest, 2) diff Ytrain - Ytest(:, i); % 每一列是训练样本 dist sum(diff.^2, 1); % 欧氏距离平方 [~, best] min(dist); predLabels(i) trainIdx(best); end trueLabels testIdx; accuracy mean(predLabels trueLabels); fprintf(PCA 特征脸识别率: %.2f%%\n, accuracy * 100);这里有一个细节predLabels和trueLabels存的是原始样本在 400 张图里的全局序号不是“第几个人”的编号。这样比较准确不会因为1:8和9:10的切分把标签搞串。如果要在报告里打印每类的混淆矩阵再根据序号换算成s1~s40即可。跑完这段识别率正常应该在 93% 到 97% 之间如果低于 90%先去检查trainIdx和testIdx有没有切片错误再看 K 是不是取得太小。4. 把识别率调上去的 5 个实用参数主成分数、光照归一化、留一法与直方图均衡4.1 主成分数 K 怎么定先看累计方差贡献率不要拍脑袋K 是 PCA 里最敏感的参数直接拍一个数容易翻车。我一般先把累计方差贡献率画出来再根据曲线选 K。MATLAB 里可以用cumsum(d) / sum(d)得到累计贡献率向量我习惯打印出贡献率首次超过 90%、95%、99% 时对应的 K 值然后取“超过 95% 且再增加对识别率提升不超过 0.5%”的那个点。ATT 库上 95% 贡献率对应的 K 通常在 20~35 之间。explained cumsum(d) / sum(d) * 100; k95 find(explained 95, 1); k99 find(explained 99, 1); fprintf(95%% 贡献率对应 K%d99%% 对应 K%d\n, k95, k99);需要提醒的是贡献率曲线只是一个参考。实际分类时最后几个小特征值对应的方向往往包含区分个人的细节信息把它们全部砍掉会损失识别率但保留太多又会带进噪声。我的做法是先用 k95 跑一版然后以 5 为步长在 k95±20 范围内扫一遍识别率选最高点。这个扫描过程在 400 张图上耗时不到一秒钟属于最划算的调参方式。4.2 光照归一化与直方图均衡对识别率的影响ATT 库整体光照变化不大但每张图的灰度均值和对比度还是有差异。直接把这些差异带进 PCA第一主成分往往就是“平均亮度”而不是人脸特征这会吃掉一部分判别信息。我一般做两步预处理第一步把每张图减掉自己的灰度均值第二步做直方图均衡化。前者是全局亮度归一后者是局部对比度增强两个操作加在一起通常能让识别率涨 1~3 个百分点。for i 1:size(X, 2) img reshape(X(:, i), [112 92]); img img - mean(img(:)); % 亮度归一 img histeq(uint8(img)); % 直方图均衡输入需 uint8 X(:, i) double(img(:)); end做这一步有个顺序问题直方图均衡后数据就不再是线性灰度了所以必须先减均值再均衡顺序反了的话减均值等于白做。另外histeq的输入必须是uint8或double在 [0,1] 区间直接把 0~255 的double矩阵传进去会得到奇怪的结果我在新手代码里见过太多次这个错。4.3 用留一法做评估把 400 次训练测试变成可重复实验固定切前 8 张训练、后 2 张测试优点是快缺点是对切分顺序敏感。同一批数据换一种切法识别率可能差 2 个百分点。为了在报告里给出一个可信的结论我一般用留一法交叉验证400 张图每次取一张做测试、其余 399 张训练循环 400 次最终识别率取平均。rng(42); % 固定随机种子保证实验可复现 indices crossvalind(LeaveMOut, 400, 1);crossvalind在 MATLAB 里属于 Bioinformatics Toolbox如果没有这个工具箱手写循环也不难先随机打乱 1:400 的索引再按 400 折切分。留一法计算代价是 400 次 PCA 训练每次 399 张图做特征分解在普通笔记本上大约要 5~10 分钟可接受。如果嫌慢可以用 5 折交叉验证代替10 次重复取平均效果接近且速度快得多。留一法和固定切分的选择会影响你对比别人的结果网上很多论文报告 ATT 库识别率 97% 以上用的就是留一法或者每类取 5 张训练。如果你的数字低先确认评估协议是不是一致别急着调参数。4.4 三个容易忽略但影响结果的参数随机种子、距离度量和图像缩放rng随机种子经常被忽略但交叉验证、随机划分、K 均值聚类都要靠它保证可复现。我习惯在脚本开头写rng(42)这样跑出来的数字每次一致写报告时不会被追问“为什么两次结果不一样”。距离度量方面欧氏距离和余弦距离在 ATT 库上差距不大但如果你换到光照变化大的库比如 Extended Yale B余弦距离明显更稳。图像缩放我一般不做112×92 已经足够小降到 56×46 虽然训练更快但会损失眉毛、眼角等细节识别率下降明显。4.5 一张完整调参流程表参数/操作推荐值或做法影响主成分数 K累计贡献率 95% 对应值 ±20 内扫描最敏感直接决定识别率灰度均值归一每张图减自身均值去掉亮度差异识别率 1~3%直方图均衡histeq(uint8(img))增强对比度对暗光图有效交叉验证留一法或 5 折×10 次重复评估更稳定推荐 5 折距离度量欧氏距离优先光照大变用余弦ATT 库两者差别不大随机种子rng(42)保证实验可复现这张表基本覆盖了 PCA 人脸识别的所有关键旋钮。实际调参时一次只改一个变量不要同时动 K 和预处理否则你根本不知道识别率变化是谁引起的。我见过太多同学把 K、距离度量、均衡化一起改结果识别率从 95% 掉到 88%完全定位不到原因。5. MATLAB 读 ATT 人脸库的常见问题与避坑记录5.1imread读 PGM 报错或读出全黑图路径加错、类型没转、目录缺子文件夹现象imread报Error using imread说File att_faces/s1/1.pgm does not exist但资源管理器里明明能看到这个文件或者读出来的图用imshow显示是全黑的。原因分两类。路径问题多半是dataDir设置不对如果 MATLAB 当前工作目录不在解压目录的上一级fullfile(pwd, att_faces)就拼不出正确路径。全黑图的问题是类型imread读 PGM 返回uint8矩阵直接imshow没问题但如果先做了double(img)再imshowMATLAB 会把 1 的值全部按白色处理图像看起来就是白的或花的并不只是黑。解决路径问题用exist(fullfile(dataDir, s1), dir)先检查目录是否存在类型问题记住imshow(uint8(img))显示double矩阵只在计算时用。读取之前先在命令行手动执行dir(att_faces)确认解压目录在当前工作目录下这是最快定位路径问题的方法。5.2eig算出来的特征向量方向不稳定特征脸忽明忽暗识别率抖动现象同一份数据跑两次特征脸图像要么整体反色要么某些特征脸明暗不同识别率在 94%~96% 之间随机抖动。原因特征分解得到的特征向量乘以 -1 仍然是特征向量MATLAB 的eig每次返回的方向可能不同数值库的随机初始化或内存布局差异。这不是代码写错而是数学上本来就有的不确定性问题。如果你把特征脸存成图片再拼成报告两次运行得到的“平均脸”方向很可能是反的。解决在特征向量乘到数据上之后做一个符号统一比如让每个特征向量的第一个元素为正若为负则整列取反。这个操作不影响投影坐标的相对距离所以不会影响识别率只影响可视化。for k 1:size(W, 2) if W(1, k) 0 W(:, k) -W(:, k); end end5.3 训练测试划分时把同一人的图同时分进两边标签泄漏导致识别率虚高现象识别率高达 99% 甚至 100%但换一种评估方式立刻跌到 90% 以下。原因划分索引写错了。比如有人写trainIdx 1:2:400; testIdx 2:2:400;这种按全局奇偶切分的方式和按人切分有本质区别。ATT 库按s1/1.pgm到s40/10.pgm排列全局奇偶切分会把同一个人的照片一半放训练、一半放测试。人脸库中同一人的不同照片高度相似算法学到的是“这张脸”而不是“这个人”测试时自然准确率高。这是在评估协议上最常见的翻车。解决按人划分训练测试索引都要从人的维度生成严格保证同一人的所有照片只出现在训练集或只出现在测试集。用 3.2 节里的baseIdx (1:8)这种写法就不会踩这个坑。报告里也应该写清楚“每人取前 8 张训练、后 2 张测试”否则别人复现不出来。5.4pca函数默认行为与手写eig不一致去均值、奇异值分解、变量名冲突现象用pca(X)和用eig手写算出来的投影坐标对不上识别率也有小差异。原因pca函数默认对输入按列去均值且用 SVD 而不是特征分解求解两者在数值上产生微小差异。此外如果脚本里之前定义过一个叫pca的变量后面调用pca函数时会报Subscript indices must either be real positive integers or logicals这是 MATLAB 初学者最常见的错误之一。解决手写eig流程时确认X已经中心化用pca时确认没有同名变量。我自己的习惯是全程手写不用pca函数因为心里清楚每一步在算什么出了偏差能快速定位。用pca函数没有错但它的默认行为对新手是个黑匣子调参时容易懵。5.5 脚本改来改去运行结果对不上变量污染和脚本顺序依赖现象半天前跑出 96% 识别率今天打开脚本重新运行报错说X未定义或者识别率变了。原因MATLAB 脚本共享工作区。如果上一次运行留下了一个叫X的变量而当前脚本开头没有清空工作区或重新给X赋值就会用到旧数据。更隐蔽的是脚本依赖了运行顺序比如先运行了数据读取段才运行训练段单独运行训练段就会出错。解决整个实验脚本写成“读取 → 预处理 → 划分 → 训练 → 评估”的线性结构开头放clear; clc; close all;不要拆成多个小脚本手动按顺序执行。如果一定要分段用函数封装而不是脚本函数有自己的独立工作区不会互相污染。这一步做得干净能帮你省掉大量无意义的排查时间。6. 把识别率从 96% 推到 99% 的三个进阶方向LBP 特征、迁移学习、评估可视化PCA 特征脸在 ATT 库上能到 95% 左右但要再往上走就得换特征或换模型。我做过三个方向的对比实验分享给你作为下一步的参考。第一个方向是 LBP局部二值模式特征。LBP 对光照变化不敏感提取的是纹理信息和 PCA 的全局线性特征互补。做法是把每张图分成 7×7 的格子每个格子计算 59 维的均匀模式直方图拼成一个 2891 维的特征向量再用 PCA 降维或直接做卡方距离最近邻。我在 ATT 库上跑过识别率一般在 97%~98%比纯 PCA 高一点。代价是特征提取比 PCA 慢不少但不至于不能接受。第二个方向是迁移学习用预训练 CNN 提特征。MATLAB 里可以net alexnet或resnet50把全连接层之前的部分当作特征提取器每张图得到一个 4096 维或 2048 维的特征向量然后接一个 SVM。ATT 库上做到 99% 以上不奇怪。注意输入要imresize成网络要求的尺寸AlexNet 是 227×227还要把灰度图复制成三通道。代价是需要 Deep Learning Toolbox另外预训练网络提特征时会把 400 张图过一遍耗时几十秒属于可接受的范围。第三个方向是评估可视化。无论用什么模型我建议至少输出三样东西混淆矩阵、按类别分的识别率表、每个测试样本的距离柱状图。混淆矩阵用confusionchart画可以一眼看出哪些人的脸互相容易混淆距离柱状图能帮你诊断“识别错误是因为这个测试样本离所有人脸都远还是离两个人都近”。这些可视化内容放进课设报告里比只写一句“识别率为 96%”有说服力得多。做完这三步你手上就有一套完整的实验流程数据读取、预处理、特征提取、分类评估、可视化。下次再拿到一个新的人脸库或者换一个分类任务只需要替换数据读取部分后面的流程都能复用。我自己早期做课设时卡在 PGM 读取上浪费了整整一天后来才意识到应该先把数据流程写成整齐的函数再往上叠加算法。希望这篇笔记能帮你跳过这些坑把时间花在真正有意思的算法对比上。本文还有配套的精品资源点击获取