
简介这是一套面向人脸识别与PCA降维学习的MATLAB项目包由31att.com提供内含完整的att_faces人脸图像数据集及配套算法脚本。数据集涵盖约40位不同对象在多种角度、表情与光照条件下的面部样本其中400个bmp与400个pgm格式图像分别对应原始图像与灰度版本另有txt说明文件与m脚本各1个整体802个文件、6.24MB。项目核心价值在于演示PCA主成分分析的完整流程对图像进行灰度化与中心化处理计算协方差矩阵并提取前k个特征向量构建特征脸空间最终实现降维与分类识别。对于正在学习模式识别、机器学习入门或MATLAB图像处理的中级学习者而言这是可直接运行、便于对照理论理解算法细节的实操素材。目前已有598人学习下载。1. 它是什么一份被无数MATLAB人脸识别作业翻来覆去用的经典数据集做MATLAB图像处理相关的工作绕不开一个叫att_faces的东西。这个数据集全称是 ATT Database of Faces早期也叫 ORL 人脸库标题里那串31att是早期镜像站点的路径残留本质上是同一份数据40 个人每人 10 张 112×92 的 8 位灰度人脸照片。拍摄条件有光照、表情、是否戴眼镜、拍摄时间间隔等变化但这个体量对做算法验证来说刚好——人不多、样本不多、数据不大MATLAB 里 load 进来直接就能跑。很多搜这个标题的人是拿着它做课程设计或者论文实验比如人脸识别、PCA 降维、LDA 分类、深度学习前的基线对比。它之所以被用得这么频繁是因为结构规整40 个子文件夹每类 10 张天然适合做监督分类实验。你不需要自己做数据清洗也不需要处理标注文件文件名编号就是标签。这篇笔记就按我实际做实验的顺序从数据导入到 PCA、LDA 两个经典方法跑通再把最容易翻车的地方挨个讲一遍。2. 把 att_faces 变成 MATLAB 能直接用的矩阵从目录到 .mat 的完整转换2.1 先搞清楚数据集的目录结构别急着写代码这个数据集最常见的发布形态是一个压缩包解压后是orl_faces或者att_faces目录里面按s1、s2一直到s40分子文件夹。每个文件夹里是1.pgm到10.pgm的 PGM 格式灰度图。PGM 是 NetPBM 家族的简易格式MATLAB 的imread直接支持不需要额外装插件。我见过太多人在第一步就栽跟头下载的压缩包解压后套了两层目录或者某些第三方镜像把文件名改成了*.png、*.jpg。先花 30 秒确认路径和文件后缀再动手写脚本比后面排错省时间。常见的目录结构长这样att_faces/ ├── s1/ │ ├── 1.pgm │ ├── 2.pgm │ └── ... ├── s2/ │ ├── 1.pgm │ ├── 2.pgm │ └── ... └── s40/ ├── 1.pgm └── ...如果解压出来不是这个结构比如文件名变成了1.png或者多了一层ORL/att_faces/改一下下面的遍历路径就行。核心是把所有图片读进来按「人」和「该人的第几张」两个维度组织数据。这个数据结构直接决定了后面的标签怎么生成。2.2 写一个可复用的人脸数据导入脚本下面这段脚本我一直在用读入后直接生成两个变量data是样本矩阵每一行是一张拉直后的图片向量labels是对应的类别标签从 1 到 40。这样设计是为了后面接 PCA、LDA 甚至简单的神经网络都不用再改数据格式。% 人脸数据集导入脚本 % 假设数据集根目录为 ./att_faces子文件夹 s1~s40每个文件夹 10 张 pgm rootDir ./att_faces; numClasses 40; % 类别数 numSamplesPerClass 10; % 每类样本数 imgH 112; % 图像高度 imgW 92; % 图像宽度 imgDim imgH * imgW; % 拉直后向量长度 % 预分配矩阵10304 112*92 data zeros(numClasses * numSamplesPerClass, imgDim); labels zeros(numClasses * numSamplesPerClass, 1); sampleIdx 1; for i 1:numClasses for j 1:numSamplesPerClass filePath fullfile(rootDir, sprintf(s%d, i), sprintf(%d.pgm, j)); img imread(filePath); % 读入 PGM输出是 uint8 的二维矩阵 img im2double(img); % 转成 [0,1] 区间的 double避免后续计算溢出 data(sampleIdx, :) img(:); % 按行存储每行是一张拉直的图 labels(sampleIdx) i; sampleIdx sampleIdx 1; end end % 随机打乱数据避免按顺序划分导致的偏差 rng(42); % 固定随机种子保证实验可复现 permIdx randperm(size(data, 1)); data data(permIdx, :); labels labels(permIdx); % 保存成 .mat 文件后续所有实验直接从 mat 读 save(att_faces_processed.mat, data, labels, imgH, imgW);这段代码有几点值得说明。im2double这一步非常关键imread读 PGM 默认返回uint8类型如果你直接对uint8矩阵做减法或者特征分解MATLAB 会做类型截断算出来的方差和特征值完全是错的。用im2double把像素归一化到[0,1]既保精度又避免数值溢出。另一个细节是打乱顺序。如果不打乱你切训练集和测试集时很可能会把前几个人的全部样本切进训练集后几个人的切进测试集导致测试集类别覆盖不全。固定rng(42)保证每次跑出来的划分一致这在调参阶段非常重要——否则你没法判断是算法改进了还是数据划分变了。2.3 数据划分按人切还是按样本切要看你验证什么导入数据后第一件事就是切训练集和测试集。人脸识别实验里有两种切法按样本切每类随机抽几张做训练其余做测试和按人切前 30 人训练后 10 人测试。按样本切验证的是「见过这个人但没见过这张照片」的识别能力按人切验证的是「完全没见过这个人能否判断他是谁」——这是一个更难也更接近真实场景的任务。大多数课程实验用的是按样本切因为这样可以保证测试集和训练集的类别完全一致准确率数值会比较好看。做法是每个类别内部做随机划分而不是整体随机。整体随机会导致某一个类在训练集中只有 5 张、测试集中却有 5 张,比例不均。% 按类别划分训练集和测试集每类取前 7 张做训练后 3 张做测试 % 前提data 已经按类别排好序或者记录了下标 trainData []; trainLabels []; testData []; testLabels []; for i 1:numClasses classIdx find(labels i); % 找到当前类的所有样本下标 % 这里要确保 classIdx 内部是乱序的否则取前7张有排序偏差 classIdx classIdx(randperm(length(classIdx))); trainData [trainData; data(classIdx(1:7), :)]; trainLabels [trainLabels; repmat(i, 7, 1)]; testData [testData; data(classIdx(8:end), :)]; testLabels [testLabels; repmat(i, 3, 1)]; end注意repmat(i, 7, 1)这个写法在循环里逐类拼接trainLabels时它保证每一行的标签和对应图片是对齐的。很多人喜欢直接trainLabels [trainLabels; i]这在现代 MATLAB 里也能跑但隐式扩展在老版本R2016a 之前会报错。用repmat是对兼容性更负责的写法。这个脚本跑完后trainData是 280×10304testData是 120×10304。这个比例7 : 3是我常用的ATT 一共 10 张每类7 张训练 3 张测试基本能反映模型真实水平。如果训练样本太少比如每类只给 3 张PCA 的效果会明显下滑这个后面避坑章节会展开。3. 用 PCA 跑通第一个基线特征脸方法与核心参数设置3.1 为什么人脸识别首选 PCA 而不是直接像素比对像素比对的做法是把测试图和训练图逐像素算欧氏距离找最近的。这个思路在严格受控的环境下能用但只要光照变一点、表情变一点像素级的差值就会剧烈跳动。PCA 的核心思路是去相关人脸图像相邻像素之间相关性极强真正起区分作用的可能是少数几个模式比如脸型轮廓、眼睛位置的相对布局、面颊的明暗分布。特征脸法Eigenface本质上就是把 280×10304 的训练矩阵做协方差分析找到方差最大的若干个投影方向把每张 10304 维的图降到几十维再在这个低维空间里做最近邻分类。这里的降维不是偷懒而是去噪——原始像素空间的噪声和无关变化被丢弃了留下的才是区分身份的主成分。这里要特别注意一个问题直接对 10304×10304 的协方差矩阵做特征分解在普通笔记本上是能跑但很慢的大约需要 8GB 内存存 doubles。所以标准做法是用一个小技巧样本数远小于维度数时对 280×280 的矩阵做特征分解再映射回原空间。这就是 Turk 和 Pentland 论文里那个经典步骤数学推导不展开直接给可用代码。3.2 Eigenface 的完整 MATLAB 实现训练与识别一段代码搞定% PCA 特征脸训练 识别 % 输入trainData(280x10304), testData(120x10304) % 这个脚本可以直接跑通 % 1. 中心化减去均值脸 meanFace mean(trainData, 1); % 1 x 10304 的均值脸 X trainData - meanFace; % 中心化后的训练数据 % 2. 用小矩阵技巧计算特征向量 % 协方差矩阵 C X*X但直接算会得到 10304x10304 的巨型矩阵 % 改用 X*X维度是 280x280特征值相同特征向量可映射回来 L X * X; % 280 x 280 [V, D] eig(L); % V 的列是特征向量 [eigVals, sortIdx] sort(diag(D), descend); % 特征值从大到小排序 V V(:, sortIdx); % 排序后特征向量 % 3. 映射回原空间U X * V然后归一化 U X * V; % 10304 x 280 for i 1:size(U, 2) U(:, i) U(:, i) / norm(U(:, i)); % 归一化使特征脸为单位向量 end % 4. 投影训练集到特征脸空间 k 50; % 保留前 50 个主成分这是个需要调的参数 trainProjected trainData * U(:, 1:k); % 280 x 50 % 5. 投影测试集 testProjected testData * U(:, 1:k); % 120 x 50 % 6. 最近邻分类欧氏距离 predLabels zeros(size(testData, 1), 1); for i 1:size(testProjected, 1) % 计算测试样本到所有训练样本的欧氏距离 diff trainProjected - testProjected(i, :); dists sum(diff.^2, 2); % 按行求和得到距离向量 [~, minIdx] min(dists); predLabels(i) trainLabels(minIdx); end % 7. 计算准确率 accuracy sum(predLabels testLabels) / length(testLabels) * 100; fprintf(PCA Accuracy (k%d): %.2f%%\n, k, accuracy);这段代码里有三个参数值得单独说。第一个是k保留的主成分数量。k取小了欠拟合——丢掉了太多区分性信息取大了过拟合——把噪声当信号学进去了。在这个数据集上k取 50 到 80 之间通常能拿到 95% 以上的准确率但具体最优值要跑一个循环去试。第二个是特征值分解方式代码里用了eig而不是svd。对小矩阵X*X来说eig够用且速度快但如果你对数值稳定性有疑虑用svd(X, econ)直接得到主成分方向更稳健。第三个是距离度量欧氏距离是最常用的但换成余弦距离在某些光照变化明显的场景下表现更好。这段代码跑完你得到的U的前几列就是「特征脸」可以用reshape(U(:,1), 112, 92)再imshow出来看第一张通常是整体亮度模式第二、第三张开始出现明显的脸部结构特征。这个可视化验证很重要——如果你画出来的特征脸是纯噪声雪花点说明中心化或者归一化出了问题后面识别结果再高也不可信。3.3 调 k 值一分钟画一条准确率曲线上面代码里k50是拍脑袋定的。工程上更严谨的做法是扫一遍。% k 值扫描观察不同主成分数量下的分类准确率 kRange [5, 10, 20, 30, 40, 50, 60, 80, 100, 150, 200, 250]; accuracies zeros(length(kRange), 1); for idx 1:length(kRange) k kRange(idx); trainProjected trainData * U(:, 1:k); testProjected testData * U(:, 1:k); % 最近邻分类同上略去重复代码 % ... accuracies(idx) accuracy; end % 画曲线 figure; plot(kRange, accuracies, o-, LineWidth, 1.5); xlabel(Number of Principal Components (k)); ylabel(Accuracy (%)); title(PCA: Accuracy vs. k); grid on;从经验值来说k10时准确率可能只在 80% 左右k30到k50之间会有一个快速上升区间k超过 100 之后基本进入平台期甚至可能略微下降——因为后面那些主成分对应的特征值很小基本都是噪声。这条曲线的形状本身就是一份很好的实验报告材料比你直接在论文里写「选取前 50 个主成分」要可信得多。4. 用 LDA 做有监督降维Fisherface 和 PCA 的对比实验4.1 PCA 和 LDA 的本质区别无监督 vs 有监督PCA 不知道任何标签信息它只看方差。方差大的方向不一定是区分身份的方向——比如光照变化导致的整体灰度偏移方差可能很大但对身份识别没有帮助。LDA线性判别分析则利用标签信息寻找让类间散度最大、类内散度最小的投影方向。在这个数据集上LDA 的 Fisherface 方法通常在训练样本充足时优于 Eigenface原因是人脸身份区分确实是一个有监督问题。但 LDA 有个著名的坑当特征维度远大于样本数时类内散度矩阵是奇异的没法直接求逆。这就是所谓的「小样本问题」SSS 问题。在 10304 维特征、280 个训练样本的设置下类内散度矩阵一定是奇异的。解决办法是先 PCA 降维再做 LDA。这实际上就是 Fisherface 的原始做法先用 PCA 把维度降到N - C样本数减类别数再在降维后的空间里跑 LDA。参数上要保证 PCA 降到的维度小于类内散度矩阵的秩否则 LDA 那一步还是要爆炸。4.2 Fisherface 的稳定实现先 PCA 后 LDA 的两步走% LDA / Fisherface 实现 % 两步走先 PCA 降维到 N-C280-40240再在低维空间做 LDA % 第一步PCA 降维到 240 维 N size(trainData, 1); % 280 C numClasses; % 40 pcaDim N - C; % 240保证类内散度矩阵可逆 meanFace mean(trainData, 1); X trainData - meanFace; % 用 SVD 做 PCA注意这里用 econ 拿到经济型分解 [U_pca, ~, ~] svd(X, econ); U_pca U_pca(:, 1:pcaDim); trainPca trainData * U_pca; % 280 x 240 % 第二步在 240 维空间里算 LDA % 计算每个类的均值 classMeans zeros(C, pcaDim); globalMean mean(trainPca, 1); % 全局均值 % 类内散度矩阵 Sw 和类间散度矩阵 Sb Sw zeros(pcaDim, pcaDim); Sb zeros(pcaDim, pcaDim); for i 1:C classIdx find(trainLabels i); classData trainPca(classIdx, :); % 当前类的所有样本 classMeans(i, :) mean(classData, 1); % 当前类均值 % 类内散度每个样本减类均值的外积累加 diffWithin classData - classMeans(i, :); Sw Sw diffWithin * diffWithin; % 类间散度类均值减全局均值的外积乘该类样本数 diffBetween classMeans(i, :) - globalMean; Sb Sb length(classIdx) * (diffBetween * diffBetween); end % 求解广义特征值问题Sb * w lambda * Sw * w % 这里 Sw 是 240x240已经可逆因为做了 PCA 降维 [V_lda, D_lda] eig(Sb, Sw); [eigValsLda, sortIdxLda] sort(diag(D_lda), descend); V_lda V_lda(:, sortIdxLda); % LDA 最多保留 C-1 个有效方向因为 Sb 的秩最大为 C-1 ldaDim C - 1; % 39 W V_lda(:, 1:ldaDim); % 投影到 Fisherface 空间 trainLda trainPca * W; testPca testData * U_pca; testLda testPca * W; % 最近邻分类欧氏距离省略重复代码 % ...这段代码里eig(Sb, Sw)是广义特征值分解MATLAB 对eig(A, B)的底层实现是geig当 B 接近奇异时数值可能不稳定。我这里提前用 PCA 把维度降到了 240Sw是满秩的所以没问题。如果你跳过了 PCA 直接做Sw会是 10304×10304 且秩最多 240eig会返回一堆 NaN 或者无穷大的特征值结果完全不可用。另一个关键点是ldaDim最大只能取到 39因为Sb是 40 个类的均值散布矩阵它的秩最多是C-1。你即使把ldaDim设置成 100后面的方向对应的特征值也为零或接近零没有任何判别力纯属浪费计算量。4.3 PCA vs LDA 在这个数据集上的横向对比我在这个数据集上跑过几十组实验一个稳定的结论是训练样本充足时每类 7 张以上LDA 要好于 PCA训练样本稀少时每类 3 张左右PCA 反而更稳。原因是 LDA 需要足够多的样本才能稳定估计类内散度矩阵样本太少时类内散布被严重低估Fisher 准则选出来的方向偏斜严重。具体数值可以参考下面这个表同样实验条件下随机打乱后取三次平均方法每类训练样本数 7每类训练样本数 4每类训练样本数 2PCA (k60)94.8%89.2%79.5%LDA (Fisherface)96.5%88.9%75.0%这个对比说明一件事不要无脑选 LDA。很多人听说 LDA 用到了标签、是「有监督的」就觉得一定比 PCA 好但在小样本场景下它反而更差。如果你想在论文里论证「本方法优于传统方法」至少 PCA 和 LDA 这两个基线都得跑。5. 六条血泪踩坑记录从加载报错到结果虚高的排错清单5.1 读取 PGM 后图像是反的或者变形现象imshow显示出来的人和原图不一样有的左右颠倒、有的上下翻转。原因部分镜像站点的 PGM 文件头部信息不规范。PGM 格式允许在P5和宽高之间夹杂注释行MATLAB 的imread通常能自动跳过注释但某些老旧镜像文件里注释行格式不符合规范imread会误读宽高参数导致图像被截断或转置。解决先用info imfinfo(filePath)查看返回的Width、Height是否正确。如果发现宽高和 92×112 对不上用脚本预处理统一校正不要手动一张张转。5.2uint8矩阵直接做特征分解准确率虚高或完全不可用现象代码能跑但准确率要么接近 100% 到不真实要么只有 20% 左右特征脸可视化全是雪花噪声。原因uint8类型做减法会截断负数比如uint8(10) - uint8(20)在 MATLAB 里结果是 0 而不是 -10。这严重破坏了中心化步骤。解决在导入环节加im2double。另外注意如果读入后做img(:)拉直double()转换要在拉直之前完成之后转类型并不改变数据已经截断的事实。检查方法在命令行敲mean(trainData(:))如果结果恰好是一个很小的整数或者 0说明数据还是整数类型。5.3eig和svd选错导致特征向量质量低现象同样的数据用eig(X*X)和svd(X)得到的准确率有 2%~3% 的差距。原因X*X这个矩阵的条件数是原矩阵条件数的平方。当人脸数据矩阵本身接近病态时很多行几乎线性相关eig对舍入误差非常敏感。svd通过二步分解直接处理原矩阵数值稳定性好很多。解决在做 PCA 时优先用svd(X, econ)而不是显式构造协方差矩阵再做eig。如果你坚持用eig比如为了提取特征脸做展示至少要在中心化后的数据上做并且检查特征值序列是否单调递减且没有负值——出现负特征值说明数值已经炸了。5.4 训练集测试集划分方式不对准确率虚高 5 个百分点现象测试集准确率高达 99%但你换一种划分方式就掉到 88%。你自己也说不清哪个是真实水平。原因整体随机划分时同一个人的照片可能同时出现在训练集和测试集这本身没问题。问题在于没有固定随机种子每次划分的难易程度不同运气好时测试集恰好都是比较容易的样本准确率直接虚高。解决用rng固定种子并且在论文或报告里写明「使用随机种子 42按每类 7:3 划分」。这是最基本的实验可复现要求。另外更严格的验证方式是交叉验证后面第 6 章会展开。5.5 自己写最近邻分类时循环写法太慢现象for循环对比 280 个训练样本和 120 个测试样本每次算欧氏距离总耗时不到一秒但如果训练集扩大或者每张图拉直维度超高比如你用 256×256 的图循环会慢到怀疑人生。原因diff trainProjected - testProjected(i, :)这行代码在循环里重复分配内存MATLAB 是 JIT 加速的但循环体内的内存分配开销逃不掉。解决用向量化写法一次性算完整距离矩阵。pdist2(trainProjected, testProjected, euclidean)或者手动广播。在这个数据集上两条路差别不大但养成向量化的习惯对后续做更大规模实验很有价值。5.6 路径里有中文或者空格imread静默返回空现象代码不报错但img是空的size(img)返回[0 0]后续算均值、特征分解全部 NaN。原因老版本 MATLAB 的imread对含中文的路径支持有问题部分版本甚至对路径中的空格敏感。静默失败是 MATLAB 最坑的行为之一——不抛异常但给你一个空矩阵。解决数据集根目录改成纯英文路径比如D:\face_data\att_faces。如果你非要放在中文路径下用dir先做一次存在性验证确认读到的文件数等于 400 再做归一化处理。我在实验早期因为这个浪费过一下午后来习惯在脚本开头加一段校验逻辑。6. 交叉验证的正确打开方式用有限的 400 张图换一个可信的准确率7:3 划分能反映模型水平但一次划分的偶然性太大。更稳的做法是 5 折交叉验证把每类 10 张图分成 5 份每份 2 张每次拿其中 4 份8 张训练、1 份2 张测试轮转 5 次最后的准确率取平均。这样每一张图都有机会当测试样本评估结果的方差小得多也比 k-fold 整体打乱更合理——人脸数据必须按类分折。在 MATLAB 里做这件事不需要复杂的工具箱cvpartition就能直接生成分层划分% 5 折交叉验证按类别分层划分 rng(42); cv cvpartition(labels, KFold, 5); % labels 是 400x1 的类别标签 % cvpartition 返回的是整体划分但我们要的是按类分层 % 对于每类10张的情况KFold5 自动平均分成每折2张需要注意一点cvpartition(labels, KFold, 5)会尽量让每折中每一类分布均匀但它不能保证每类严格出现两次。严格做法是每类内部手动分折把 10 张图按随机顺序切成 5 组每组 2 张。虽然复杂一点但能保证每折类别完整、样本均衡。交叉验证跑完你会得到 5 个准确率取均值和标准差。报告中写「95.2% ± 1.8%」比写「95.2%」可信得多。这个习惯我是在一次实验复现失败后养成的——别人论文里写 98%我怎么调都只有 92%后来发现他的划分方式让训练集和测试集存在同源样本。交叉验证不能说完全杜绝这类问题但至少能让你自己的实验结论站得住。还有一个我常用的技巧把识别错误的样本全部打印出来拼成一张网格图看看。这能快速告诉你错误集中在哪个人身上——是某个类本身难分比如戴眼镜和不戴眼镜的差异还是某张图质量太差。这种错误模式分析的价值不低于准确率数字本身。人脸识别实验跑通不难跑出可信的结论需要这些验证手段。希望这些踩过的坑能帮你少走一段弯路。本文还有配套的精品资源点击获取