
简介基于BP神经网络的人脸识别Matlab实现面向模式识别初学者和Matlab开发者系统展示了从人脸图像读取、灰度转换、直方图均衡化到PCA降维、BP网络构建与训练再到多组测试集精度评估的完整流程。压缩包共67个文件其中21个.m脚本为核心源码涵盖fastPCA、Identification、network_train、GAtotalAcu等关键函数与辅助模块22个.mat数据文件用于保存特征与实验中间结果23张jpg图片作为人脸样本另有1个fig格式GUI界面可直观查看识别效果与数据分布。资源包整体约120MB已有100人学习下载。脚本中提供了testWlRead、testDcwRead、showTestFace等独立测试程序方便分步调试和对照理解适合希望快速上手BP人脸识别项目并基于源码进行算法优化或二次开发的读者具有较好的工程参考价值。1. 基于BP神经网络的人脸识别在Matlab里怎么落地用Matlab跑通一个基于BP神经网络的人脸识别系统大多数人的第一反应是直接newff建一个网络出来训练但真正让新手卡住的往往是图像读取、标签构造、维度压缩和预测结果还原这一类“网络之外”的工作。ORL人脸库每张图是112×92直接铺平输入会有10304维特征不用处理就丢给BP网络训练慢是一回事过拟合几乎不可避免。实践中最稳的路径是把BP网络当作一个现成的分类头前面用PCA或局部特征做降维在几十维数据上跑几十个隐藏节点就够了。这篇文章从Matlab读图开始把整个流程拆成可以照着改的步骤适合课程设计、论文第一版实验也适合想搞明白传统神经网络怎么处理图像的人。2. BP神经网络与人脸识别从特征映射到分类边界2.1 为什么BP神经网络能做人脸识别人脸识别本质上是一个分类任务给一张人脸图像输出它是哪个身份。ORL库里有40个人那就构建一个40类分类器。BP神经网络的三层全连接结构在隐藏层足够宽时可以把低维特征空间里的复杂分类边界拟合出来。Matlab里自带的feedforwardnet就是这个结构输入层节点数等于特征维数输出层节点数等于类别数中间用一层或两层隐藏层做非线性变换。对于每个类别只有十来张图的小型人脸库BP网络比很多深度模型更容易拿到一个稳定的基线结果。实践中不要把人脸识别门禁机里的商用算法和这里的BP网络混为一谈。商用识别系统用的是深度卷积网络加度量学习但BP神经网络人脸识别作为教学和论文对比的经典基线仍然有很强的解释性。它最大的好处是你每一个隐藏节点、每一组权重都能看到适合后续和SVM、KNN做对比实验。2.2 输入不能用原始像素直接铺平直接取112×92灰度图像素作为输入输入层节点数是10304第一层到隐藏层的权重矩阵就有几十万个参数。训练样本只有几百张这种情况下过拟合几乎无法避免。另外tansig/logsig在输入维度过高时梯度更新容易变慢训练窗口里的误差曲线经常是一动不动。所以通常先做PCA降维把10304维压到30~50维。这既是数据压缩也是一种去相关操作去掉的是临近像素之间的冗余信息。不同特征方案对比特征类型维度特点及适用性原始像素直接拉平10304实现最简单但训练极慢极易过拟合PCA降维特征30~60保留全局方差和BP网络配合效果稳定LBP直方图几百到几千对局部纹理更敏感光照变化下更鲁棒PCA维数选择不能完全照抄教程。我一般先看累计贡献率到95%时落在哪个维度然后固定这个维度。如果识别率上不去再尝试增加到60维做对比但不要一次性用到200维。2.3 输出层编码与训练目标矩阵BP神经网络处理多分类输出层最常用的是one-hot编码。40个人就有40个输出神经元第几类就在对应位置置1其余全部置0。训练目标矩阵大小为[类别数 × 样本数]注意Matlab神经网络工具箱里“每列是一个样本”的布局约定。输入矩阵是[特征维数 × 样本数]目标矩阵也必须按列对应样本。生成训练目标矩阵的代码numClass 40; numSamples 240; targets zeros(numClass, numSamples); for i 1:numSamples targets(labels(i), i) 1; % labels(i)是1~40的类标签 end输出层不要直接用labels里的整数1、2、3作为回归目标因为网络会学到“第3类比第1类更接近”这种排序关系。用one-hot后每列只有一个1输出层每个节点独立判断“是不是这一类”。训练完成后网络输出不会恰好是0/1而是一组实数值取这组值中最大值对应的行索引作为预测类别。提示只有当网络输出层是purelin或softmax时比较输出值大小才有意义。如果输出层用了tansig输出范围被压缩到[-1,1]0/1目标会让网络很难逼近两个端点。3. 用Matlab实现BP神经网络人脸识别的完整步骤3.1 批量读取人脸库并统一图像尺寸公开人脸库常用ORL目录结构一般是orl/s1/1.pgm到orl/s40/10.pgm。读取时用循环拼接路径避免手写几十行重复代码。注意不同系统下文件命名可能有差异运行时先dir看一下实际文件名。读取后统一imresize到相同尺寸防止后续拉成向量时长度不一致。imageDir orl; numClass 40; numPerClass 10; allData []; labels []; for c 1:numClass for k 1:numPerClass filename fullfile(imageDir, [s num2str(c)], [num2str(k) .pgm]); img imread(filename); if size(img, 3) 3 img rgb2gray(img); end img imresize(img, [112 92]); allData [allData; img(:)]; labels [labels; c]; end end allData double(allData) / 255;allData每一行是一张拉平后的图img(:)把112×92矩阵变成10304维行向量。除以255是为了把像素范围映射到[0,1]避免过大输入让隐藏层激活函数进入饱和区。用fullfile拼接路径可以自动处理Windows与Linux下的路径分割符差异。如果读取的是jpg目录结构和扩展名要对应修改否则会报路径不存在。3.2 训练集测试集划分与PCA降维划分数据集时我习惯按“每类前6张训练后4张测试”而不是全部样本随机抽。这样每个类别在训练和测试中都存在不至于出现某个身份从未参与过训练的情况。划分完成后PCA只能对训练集做拟合然后再把训练集和测试集都投影到这个PCA空间。如果对两者分别做PCA测试集的信息提前泄漏到了特征子空间里测试识别率会虚高。rng(0); trainX []; testX []; trainY []; testY []; for c 1:numClass idx (c-1)*numPerClass (1:numPerClass); trainX [trainX; allData(idx(1:6), :)]; testX [testX; allData(idx(7:10), :)]; trainY [trainY; repmat(c, 6, 1)]; testY [testY; repmat(c, 4, 1)]; end % 只用训练集拟合PCA coeff pca(trainX); numComponents 45; trainXc trainX - mean(trainX); testXc testX - mean(trainX); trainPca trainXc * coeff(:,1:numComponents); testPca testXc * coeff(:,1:numComponents);pca默认对行样本做分析返回的coeff每一列是一个主成分方向。取前45列就是把样本投影到45维空间。mean(trainX)是对所有训练样本每个像素位置求均值测试集也要减这个均值不能用测试集自己的均值。注意pca函数会在内部对数据做中心化但返回的coeff只代表主成分方向不包含均值信息。实际使用时必须手动减均值否则测试集的投影结果是错的。3.3 创建BP网络并完成训练与测试Matlab里推荐用feedforwardnet创建BP神经网络。隐藏层可以先试[20 15]两层结构训练函数选择trainscg。隐藏层激活函数用tansig输出层用purelin。trainscg是共轭梯度法不需要手动调整学习率收敛速度比traingd快又不像trainlm那样占用大量内存。net feedforwardnet([20 15], trainscg); net.layers{1}.transferFcn tansig; net.layers{2}.transferFcn purelin; net.trainParam.epochs 300; net.trainParam.goal 1e-4; net.trainParam.min_grad 1e-6; targetTrain zeros(numClass, size(trainY, 1)); for i 1:size(trainY, 1) targetTrain(trainY(i), i) 1; end net train(net, trainPca, targetTrain); outputTest sim(net, testPca); [~, pred] max(outputTest, [], 1); pred pred; accuracy sum(pred testY) / length(testY); fprintf(测试集准确率: %.2f%%\n, accuracy * 100);trainPca是[45 × 240]targetTrain是[40 × 240]每列对应一个样本。sim输出[40 × 160]的测试预测结果。max(outputTest, [], 1)沿行方向找最大值得到每列预测的类别索引。如果这里把max误写成max(outputTest)会返回整列最大值而不是每列独立的最大值导致所有预测结果都变成同一个类别。3.4 混淆矩阵看哪些类别被认错准确率只提供一个整体数字看不出误判发生在哪两个人之间。用confusionmat生成混淆矩阵再画成灰度图能很快定位问题样本。ORL人脸库里同一人不同表情、有无眼镜的照片差异很大某些ID之间长相接近误判集中是正常现象。此时不要急着调网络结构先看训练图片本身是否具备足够的辨识度。figure; cm confusionmat(testY, pred); imagesc(cm); colormap(flipud(gray)); colorbar; xlabel(预测标签); ylabel(真实标签); title(sprintf(BP人脸识别混淆矩阵, 准确率%.2f%%, accuracy*100));confusionmat的行是真实标签列是预测标签对角线越亮说明正确识别的样本越多。如果某一行有大片非零值说明这类样本被大量错分可以单独输出该类的训练图片和测试图片检查是否存在头部姿态差异过大或光照突变。4. BP神经网络训练的参数设计、难点与调优4.1 隐藏层神经元个数怎么定隐藏层节点数没有标准答案通常用经验公式初选再根据结果上下调整。40类识别问题、PCA特征45维时常见配置如下表网络结构隐藏节点特点单隐藏层15训练最快拟合能力有限单隐藏层30常用起点多数情况够用双层[25 15]能表达更复杂边界训练稍慢双层[50 30]参数增多小样本下容易过拟合遇到准确率不升反降的情况先降低隐藏层节点数再考虑是否数据增强不够。例如两层[50 30]在只有240个训练样本时参数量过大几乎必然在某个训练轮次后出现验证准确率回落。隐藏层神经元数量我一般从(输入层节点数 输出层节点数) / 2开始然后按5的倍数上下试探。4.2 激活函数、训练函数和学习率的配合经典组合是隐藏层tansig、输出层purelin、训练函数trainscg。tansig的导数在0附近变化更平缓梯度不容易消失。输出层如果也用tansig目标0/1位于函数边界外网络需要不断增大权重才能逼近输出训练误差曲线下降会比较慢。在新版Matlab里可以运行net.trainFcn查看当前训练函数不要以为设置了net.trainParam.lr就一定会改变学习率它只对traingd这类函数有效。net.trainParam.lr 0.02; % 手动学习率仅在traingd下生效 net.trainParam.mc 0.9; % 动量因子 net.trainParam.epochs 500;训练时观察误差曲线常见的三种情况误差震荡不下降学习率偏大或数据归一化没做到[0,1]区间。误差快速下降后长期持平网络已经收敛再看准确率是否达标。误差降到很小但测试准确率低过拟合需要降低隐藏层节点数或增加训练样本。4.3 小样本下用数据增强扩大训练集ORL每类只有10张图训练模式相对少。数据增强可以缓解过拟合常见做法是随机平移、小角度旋转和加高斯噪声。增强只能加在训练集上测试集保持原始图像否则无法评估模型在真实场景中的表现。augImg1 imtranslate(img, [2, 0], FillValues, 0); augImg2 imrotate(img, 3, bilinear, crop); augImg3 imnoise(img, gaussian, 0, 0.01);增强后的图片需要和原图一起参与PCA投影使用已经拟合好的coeff变换而不是重新对增强样本做一次PCA。如果数据增强后识别率没有提升优先检查PCA保留的方差是否不够而不是怀疑增强方式不对。4.4 容易踩的三个坑第一个坑是归一化泄漏在划分训练/测试前就计算全量数据的均值并做标准化训练时模型已经见过测试集统计量测试结果偏高。第二个坑是图像尺寸不一致训练集里混入大小不同的图片拉平后向量长度不一样pca直接报维度错误。第三个坑是预测类别时用了round而不是max网络输出是连续值直接四舍五入会把得分接近0.9和0.1的样本都划成同一类正确做法是取每列得分最高的索引。提示如果你的误差曲线出现锯齿状抖动先检查输入特征是否用的PCA特征。PCA后各维度方差差异大最好除以其标准差把特征缩放到相近尺度再送入BP网络。5. 用留一法交叉验证评估你的BP人脸识别系统固定的一组训练/测试划分只能说明一次运气成分很大的结果。ORL数据集小更客观的评估方式是留一法交叉验证。具体做法是把每人10张图中的每一张轮流留作测试其余9张训练循环10次最终统计10次准确率的平均值。这个策略比单次划分更接近真实泛化性能但训练时间成倍增加。foldAcc zeros(numPerClass, 1); for k 1:numPerClass testMask false(size(labels)); for c 1:numClass testMask((c-1)*numPerClass k) true; end trainMask ~testMask; trainX allData(trainMask, :); testX allData(testMask, :); trainY labels(trainMask); testY labels(testMask); % 复用第3章的PCA与网络创建步骤 coeff pca(trainX); trainXc trainX - mean(trainX); testXc testX - mean(trainX); trainPca trainXc * coeff(:,1:45); testPca testXc * coeff(:,1:45); net feedforwardnet([20 15], trainscg); net.layers{1}.transferFcn tansig; net.layers{2}.transferFcn purelin; net.trainParam.epochs 300; net.trainParam.goal 1e-4; targetTrain zeros(numClass, size(trainY, 1)); for i 1:size(trainY, 1) targetTrain(trainY(i), i) 1; end net train(net, trainPca, targetTrain); outputTest sim(net, testPca); [~, pred] max(outputTest, [], 1); pred pred; foldAcc(k) sum(pred testY) / length(testY); fprintf(fold %d 准确率: %.2f%%\n, k, foldAcc(k)*100); end meanAcc mean(foldAcc); fprintf(留一法平均准确率: %.2f%%\n, meanAcc*100);外层循环的testMask每次选定同一序号的图片作为测试集例如第4次循环是所有人的第4张图保证测试覆盖所有类别。内部重复PCA拟合和网络初始化是必要的因为每次训练集的样本都不同主成分方向和网络权重都要重新估计。如果把PCA结果或网络初始值留在循环外复用留一法就失去了意义。留一法结束后把每个fold的准确率记录成表可以看到哪些图片对识别影响最大。准确率偏低的fold通常对应某张姿态、光照变化明显的图片。最终把错误样本原图、预测标签和真实标签一起输出作为后续优化预处理方式或更换特征提取方法的依据。本文还有配套的精品资源点击获取