简介:一套结合人脸识别与主成分分析(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 跑通 AT&T 人脸库的完整链路
att_faces_31att_www.31attcom_www.31attcom_faces_matlab_这种文件名,一看就是从 31att.com 这类镜像站拖下来的 AT&T 人脸库压缩包。里面装的就是经典的 ORL 人脸数据集:40 个人、每人 10 张、总共 400 张 112×92 像素的 8 位灰度 PGM 图,按s1/1.pgm到s40/10.pgm的组织方式存放。MATLAB 课上要交图像处理大作业、模式识别课设,或者准备人脸识别方向毕业设计开题的人,十有八九会拿到这个包。
这个实验真正花时间的不是后面的 PCA 或深度学习,而是前面那几步脏活:解压后路径怎么组织、PGM 文件怎么读进来、400 张图怎么排成矩阵、训练测试集怎么切。很多同学在这一步用imread一张张读,读一半路径报错,或者把图拉成向量之后发现方向跟别人代码对不上,后面全部白做。这篇文章就按我自己做这类实验的顺序,从文件读取讲到 PCA 特征脸识别,再讲几个最容易翻车的坑,照着敲就能跑通。
2. 先搞懂 AT&T 人脸库的构成:40 人 × 10 张图,读图前必须想清楚四件事
2.1 目录结构、命名规则和图像参数
AT&T 人脸库(也叫 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 的标准写法
% 读取 AT&T 人脸库全部 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 为什么 AT&T 库首选 PCA:数据量小、类内变化单纯,MATLAB 自带函数够用
AT&T 库是 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 是一个比较稳妥的起点。AT&T 库 40 个人,25 个主成分大约占了累计方差贡献率的 95% 左右,后面分类效果一般不会差。K 太小(比如 5)只保留了前几张特征脸,主要描述的是整体亮度变化,区分不出人;K 太大(比如 100)会把噪声也学进去,测试集识别率反而下降。具体 K 怎么调,下一章细说。
3.3 最近邻分类与识别率统计:欧氏距离和余弦距离的差别
投影之后,分类器用最简单的最近邻(1-NN)就能达到很高的识别率。测试集每一张图的投影向量Ytest(:, i),和训练集所有投影向量算距离,取最小距离对应的训练样本标签作为预测结果。距离度量我一般先用欧氏距离,它和 PCA 的均方误差目标是匹配的;余弦距离对向量模长的变化不敏感,如果光照变化大可以换,但对 AT&T 库来说欧氏距离已经够用。
% 最近邻分类并统计识别率 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%”的那个点。AT&T 库上 95% 贡献率对应的 K 通常在 20~35 之间。
explained = cumsum(d) / sum(d) * 100; k95 = find(explained >= 95, 1); k99 = find(explained >= 99, 1); fprintf('95%% 贡献率对应 K=%d,99%% 对应 K=%d\n', k95, k99);需要提醒的是,贡献率曲线只是一个参考。实际分类时,最后几个小特征值对应的方向往往包含区分个人的细节信息,把它们全部砍掉会损失识别率;但保留太多又会带进噪声。我的做法是:先用 k95 跑一版,然后以 5 为步长在 k95±20 范围内扫一遍识别率,选最高点。这个扫描过程在 400 张图上耗时不到一秒钟,属于最划算的调参方式。
4.2 光照归一化与直方图均衡对识别率的影响
AT&T 库整体光照变化不大,但每张图的灰度均值和对比度还是有差异。直接把这些差异带进 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 次重复取平均,效果接近且速度快得多。
留一法和固定切分的选择会影响你对比别人的结果:网上很多论文报告 AT&T 库识别率 97% 以上,用的就是留一法或者每类取 5 张训练。如果你的数字低,先确认评估协议是不是一致,别急着调参数。
4.4 三个容易忽略但影响结果的参数:随机种子、距离度量和图像缩放
rng随机种子经常被忽略,但交叉验证、随机划分、K 均值聚类都要靠它保证可复现。我习惯在脚本开头写rng(42),这样跑出来的数字每次一致,写报告时不会被追问“为什么两次结果不一样”。距离度量方面,欧氏距离和余弦距离在 AT&T 库上差距不大,但如果你换到光照变化大的库(比如 Extended Yale B),余弦距离明显更稳。图像缩放我一般不做:112×92 已经足够小,降到 56×46 虽然训练更快,但会损失眉毛、眼角等细节,识别率下降明显。
4.5 一张完整调参流程表
| 参数/操作 | 推荐值或做法 | 影响 |
|---|---|---|
| 主成分数 K | 累计贡献率 95% 对应值 ±20 内扫描 | 最敏感,直接决定识别率 |
| 灰度均值归一 | 每张图减自身均值 | 去掉亮度差异,识别率 +1~3% |
| 直方图均衡 | histeq(uint8(img)) | 增强对比度,对暗光图有效 |
| 交叉验证 | 留一法或 5 折×10 次重复 | 评估更稳定,推荐 5 折 |
| 距离度量 | 欧氏距离优先,光照大变用余弦 | AT&T 库两者差别不大 |
| 随机种子 | 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)再imshow,MATLAB 会把 >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;,这种按全局奇偶切分的方式,和按人切分有本质区别。AT&T 库按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 特征脸在 AT&T 库上能到 95% 左右,但要再往上走,就得换特征或换模型。我做过三个方向的对比实验,分享给你作为下一步的参考。
第一个方向是 LBP(局部二值模式)特征。LBP 对光照变化不敏感,提取的是纹理信息,和 PCA 的全局线性特征互补。做法是把每张图分成 7×7 的格子,每个格子计算 59 维的均匀模式直方图,拼成一个 2891 维的特征向量,再用 PCA 降维或直接做卡方距离最近邻。我在 AT&T 库上跑过,识别率一般在 97%~98%,比纯 PCA 高一点。代价是特征提取比 PCA 慢不少,但不至于不能接受。
第二个方向是迁移学习,用预训练 CNN 提特征。MATLAB 里可以net = alexnet或resnet50,把全连接层之前的部分当作特征提取器,每张图得到一个 4096 维或 2048 维的特征向量,然后接一个 SVM。AT&T 库上做到 99% 以上不奇怪。注意输入要imresize成网络要求的尺寸(AlexNet 是 227×227),还要把灰度图复制成三通道。代价是需要 Deep Learning Toolbox,另外预训练网络提特征时会把 400 张图过一遍,耗时几十秒,属于可接受的范围。
第三个方向是评估可视化。无论用什么模型,我建议至少输出三样东西:混淆矩阵、按类别分的识别率表、每个测试样本的距离柱状图。混淆矩阵用confusionchart画,可以一眼看出哪些人的脸互相容易混淆;距离柱状图能帮你诊断“识别错误是因为这个测试样本离所有人脸都远,还是离两个人都近”。这些可视化内容放进课设报告里,比只写一句“识别率为 96%”有说服力得多。
做完这三步,你手上就有一套完整的实验流程:数据读取、预处理、特征提取、分类评估、可视化。下次再拿到一个新的人脸库,或者换一个分类任务,只需要替换数据读取部分,后面的流程都能复用。我自己早期做课设时,卡在 PGM 读取上浪费了整整一天,后来才意识到应该先把数据流程写成整齐的函数,再往上叠加算法。希望这篇笔记能帮你跳过这些坑,把时间花在真正有意思的算法对比上。
本文还有配套的精品资源,点击获取