简介:压缩包内含一套基于反向传播(BP)神经网络实现人脸识别的Matlab完整工程,面向需要完成课程设计、毕业设计或入门神经网络项目实践的开发者。压缩包共67个文件,包含23张人脸样本图片、22个数据文件、21个源码脚本和1个界面文件,整体大小约120MB。工程内提供网络训练、主成分分析、遗传算法优化、图形界面等核心模块,并配有多个针对性测试脚本;数据文件保存完成训练的特征与结果,图片构成训练和测试样本集,界面可直接打开查看操作面板。项目按测试集、样本数据和代码模块组织,便于理清数据读取、特征降维、网络训练到精度评估的完整链路。已有100人学习下载,适合结合反向传播原理对照代码实操,也可作为后续引入卷积神经网络或改进特征表达方法的起点。
1. 为什么这个老方案至今还有人翻出来:BP神经网络人脸识别的实用边界
手机门禁机上的人脸识别早被卷积神经网络包揽,但当你的任务是“在几百张样本上快速交差、在MATLAB里复现一套课程设计、或者搞懂特征怎么从像素变成分类结果”时,BP神经网络反而比深度学习更容易让入门工程师看穿底牌。这个标题指向的正是这样一套方案:用BP神经网络作为分类器,在MATLAB里完成人脸数据的读取、特征提取、模型训练和识别测试。它能解决的是小样本、受限姿态与光照环境下的身份判别问题,适合课程设计、毕业设计以及想对比浅层模型与深度学习差异的从业者。难点不在网络本身,而在数据预处理和训练参数,多数人跑不出理想准确率,是因为把BP当成了黑匣子,却忽略了输入特征的细心打理。
2. 跑通前先想清楚:BP神经网络与人脸识别的匹配点、数据集与特征选型
2.1 人脸识别为什么可以用BP:从模式映射到分类面的原理
BP神经网络是一种多层前馈网络,核心机制是误差反向传播:输入信号经过隐藏层逐层映射,输出与真实标签差异的梯度再回传给每一层权重,完成参数更新。把人脸识别看成分类问题,本质就是让网络学会“这张像素图属于哪个人”——输入是图像特征向量,输出是人员编号或类别概率。
BP网络没有卷积层那种天生的平移和尺度不变性,它只能对“已经对齐好”的输入做分类。所以这个方案里最重要的不是网络结构,而是输入特征怎么整理。你在搜“bp神经网络结构图”时看到的那些三层结构——输入层、隐藏层、输出层——对入门者很够用了:输入层节点数等于特征维数,隐藏层节点数决定网络容量,输出层节点数等于要识别的人数。很多人把BP结构画得非常复杂,但对人脸识别这种小规模分类,两层隐藏层基本顶天,再多反而更难收敛。
从数学角度看,BP网络通过激活函数(通常用tansig或logsig)把原始输入的线性组合映射到非线性空间,理论上可以逼近任意分类边界。人脸图像本质是高维空间中的低维流形,光照、表情、姿态都会扰动像素值。BP网络要做得稳,必须靠特征工程先把这些扰动压下去。所以下面两步——数据规范化和特征降维——比网络参数更值得先花时间。
2.2 数据集与预处理:用MATLAB读入人脸库和灰度归一化
常见的人脸库如Yale、ORL、FERET,都能在公开渠道拿到。以ORL为例,每类人10张灰度照片,尺寸112×92。这类小样本库很适合BP试手。第一步不是直接扔给网络,而是把图片读进来统一尺寸和灰度范围。
% 读取人脸库文件夹,统一尺寸为64x64,并线性归一化到[0,1] imPath = 'faces'; files = dir(fullfile(imPath, '*.pgm')); numSamples = length(files); imageSize = [64, 64]; data = zeros(numSamples, prod(imageSize)); labels = zeros(numSamples, 1); for i = 1:numSamples img = imread(fullfile(files(i).folder, files(i).name)); if size(img, 3) == 3 img = rgb2gray(img); end img = imresize(img, imageSize); img = double(img) / 255; % 把灰度值压到[0,1] data(i, :) = img(:)'; % 展成一行 % 假设文件名形如 person_01.pgm,从数字解析标签 tokens = regexp(files(i).name, '_(\d+)\.', 'tokens'); labels(i) = str2double(tokens{1}{1}); end这段代码做了三件事:用dir定位所有图片;imresize把所有图像缩放到同样大小;将灰度值从[0,255]缩放到[0,1]。这三步在BP人脸识别里缺一不可。imresize缩小到64×64可以显著降低输入维度,从4096维出发,后面的PCA和训练压力都小很多。没有统一尺寸,网络输入层就无法固定,训练也毫无意义。
这里的关键参数是imageSize。64×64是我常用的折中:过大会让输入维度过高,训练时间暴涨且容易过拟合;过小比如32×32,可能丢失眼睛、嘴部等判别信息,识别率下滑。如果你的数据集光照差异较大,可以在归一化前加一句img = histeq(img);做直方图均衡化,能缓解一部分光照干扰。注意归一化必须用double转浮点数,否则除以255后会被截断成整数,等于白做。
2.3 特征怎么取:像素、PCA、LBP该选什么
原始像素是最简单、最不推荐直接塞给BP的特征。一张64×64灰度图展开是4096维,如果你只有几十张训练样本,BP网络要拟合的目标数远大于样本数,必然会过拟合到训练集上,测试集惨不忍睹。所以通常会对原始像素做降维。
下面这个对比表是我实际测试时常用的选择参考:
| 特征类型 | 维度 | 对光照鲁棒性 | 计算成本 | 适合场景 |
|---|---|---|---|---|
| 原始像素 | 4096 | 弱 | 低 | 无要求,纯练手 |
| PCA降维 | 20~100 | 中 | 中 | 样本少、光照稳定 |
| LBP直方图 | 128~2048 | 强 | 中高 | 光照变化大、门禁模拟 |
用PCA降维在MATLAB里只需一行核心调用,但要用好有几个参数值得交代:
% 对原始像素做PCA,保留累计能量95%的维度 meanFace = mean(data, 1); dataCentered = data - meanFace; [coeff, score, latent] = pca(dataCentered); energy = cumsum(latent) / sum(latent); k = find(energy >= 0.95, 1); feature = score(:, 1:k);coeff是主成分方向矩阵,score是投影后的特征,latent是各主成分对应的特征值。find(energy >= 0.95, 1)会找出第一个累计能量超过95%的维度k,通常人脸库用30~80维就能稳住识别率。这里有个容易被忽略的点:PCA的coeff和meanFace必须保存下来。测试阶段读取新图片时,要用同一套meanFace和coeff做投影,而不是重新计算,否则训练和测试特征空间不一致,识别率会崩掉。
如果你不想自己写PCA投影,也可以用MATLAB的pca结合transform的方式,但上面这段已经够用。LBP则对光照更鲁棒,MATLAB图像处理工具箱里有extractLBPFeatures,返回直方图特征,缺点是维度更高,需要配合PCA或卡方距离做筛选。对于课程设计想快速出效果,我建议用PCA降维到50维左右就接BP,效果比直接用原始像素稳定一个台阶。
3. 把BP神经网络在MATLAB里从零搭起来:网络构建、训练与参数设置
3.1 用newff还是feedforwardnet:网络对象选择的差异
老教程和旧教材里会写newff,而新版MATLAB强烈建议用feedforwardnet。两者底层都是前馈网络,但API风格和使用方式不同。newff来自旧版本,到R2020以后仍能用,但警告一堆,很多参数必须用net.trainFcn和net.trainParam去设置,新手容易漏。feedforwardnet是讲得顺一点的封装,可以指定隐藏层神经元数,直接返回一个网络对象,配合train和sim调用。下面这段用的是feedforwardnet。
3.2 训练函数的选择:traingd/traingdm/trainlm对收敛的影响
BP神经网络的“学力”很大程度来自训练函数。MATLAB里常见的三个梯度训练函数分别是traingd(标准梯度下降)、traingdm(加入动量)和trainlm(Levenberg-Marquardt)。人脸识别样本量小、维度不高,我最常用trainlm,它收敛快,精度也不错。但trainlm需要计算雅可比矩阵,内存开销大,如果特征维度几百、样本几千,可能会内存不足。这时换回traingdm反而更稳。
我的经验是:样本数小于500且特征维度低于100时,果断用trainlm;数据再大一些就用traingdm,并把动量和学习率调低。traingd基本不用,收敛太慢,跑几百轮都不见底。具体配置可以通过net.trainParam设置,比如net.trainParam.lr = 0.01;。
3.3 完整训练代码:数据划分、归一化、训练、保存模型
下面这一段把前面讲的东西串起来,是完整的训练流程。假设你已经通过2.3节得到了feature和labels。
% 按7:3划分训练集与测试集,保持类别分布大致均匀 rng(42); % 固定随机数,保证结果可复现 cv = cvpartition(labels, 'HoldOut', 0.3); trainIdx = training(cv); testIdx = test(cv); X_train = feature(trainIdx, :); Y_train = labels(trainIdx, :); X_test = feature(testIdx, :); Y_test = labels(testIdx, :); % 标签转成0/1矩阵,输出层节点数=类别数 classes = unique(labels); numClasses = length(classes); Y_mat = zeros(length(Y_train), numClasses); for i = 1:length(Y_train) Y_mat(i, classes == Y_train(i)) = 1; end % 构建BP网络:一个隐藏层,20个节点 net = feedforwardnet(20); net.layers{1}.transferFcn = 'tansig'; net.layers{2}.transferFcn = 'logsig'; net.trainFcn = 'trainlm'; net.trainParam.epochs = 300; net.trainParam.goal = 1e-4; net.divideFcn = 'dividetrain'; % 手动已经划好测试集,训练时不再次划分 % 训练 [net, tr] = train(net, X_train', Y_mat'); % 预测测试集 predMat = sim(net, X_test'); [~, predIdx] = max(predMat, [], 1); predictions = classes(predIdx)'; % 保存模型 save('bp_face_model.mat', 'net', 'meanFace', 'coeff', 'k', 'classes');这段代码有几个参数值得单独说明。feedforwardnet(20)里的20是隐藏层神经元数,对人脸分类来说20到50都常见。太小欠拟合,太大容易记住训练集中的噪声。我先给20,通常能到90%以上的识别率,再根据验证集调整。transferFcn隐藏层用tansig,输出层用logsig,因为logsig输出范围是0到1,适合做多分类的概率输出。trainFcn用了trainlm,收敛速度很快,但如果你内存吃紧,改成traingdm并设置net.trainParam.lr = 0.01; net.trainParam.mc = 0.9;。
另一个重点是net.divideFcn = 'dividetrain'。默认feedforwardnet会自动把数据分成训练、验证、测试三份,而我们已经手动做了7:3划分,如果再让网络自己划分,测试集就会被网络偷看,识别率虚高。所以必须显式关闭自动划分。后面对sim输出的结果取最大一列作为预测身份,再映射回classes就得到了预测标签。保存时记得把PCA参数meanFace、coeff、k一起存进去,没有它们在测试阶段无法加载新图片。
4. 人脸识别测试与验证:识别率、混淆矩阵和阈值判断
4.1 测试集上跑通识别流程
训练完的网络不是终点,还要在真实测试集上完整跑一遍。很多同学训练误差很小,一上测试集就翻车,问题出在测试流程和训练流程不一致:测试图片没有减训练集的均值,或者没有经过同一套PCA投影。下面是正确顺序的测试代码:
function predLabel = recognizeFace(imgPath, net, meanFace, coeff, k, classes, imageSize) img = imread(imgPath); if size(img, 3) == 3 img = rgb2gray(img); end img = imresize(img, imageSize); img = double(img) / 255; imgVec = img(:)'; imgCentered = imgVec - meanFace; % 用训练集的均值中心化 proj = imgCentered * coeff(:, 1:k); % 投影到主成分空间 out = sim(net, proj'); [~, idx] = max(out); predLabel = classes(idx); end这里最容易漏的是imgCentered = imgVec - meanFace。训练时PCA是在中心化后的数据上做的,测试时也必须先用同一均值中心化。coeff(:, 1:k)取前k个主成分方向,和训练时保持一致。proj是一个1×k向量,sim会输出一个numClasses×1向量,取最大值位置查classes得到预测身份。这一步通过了,才算真正“跑通识别流程”。
4.2 用混淆矩阵和ROC判断模型是否真的可用
光看整体识别率不够,分不清模型是把所有人像认成同一个,还是真正学到判别信息。用MATLAB的confusionmat能看到每一类人的识别情况:
confMat = confusionmat(Y_test, predictions); disp(confMat); % 计算各类别召回率 accuracies = zeros(numClasses, 1); for i = 1:numClasses tp = confMat(i, i); total = sum(confMat(i, :)); accuracies(i) = tp / total; end bar(accuracies); xlabel('人员ID'); ylabel('召回率');混淆矩阵的行是真实身份,列是预测身份。如果某一行大部分落在别的列,说明这一类人被系统性误判。比如第3类总是被认成第5类,多半是这两类人的原始图像长得像,或者训练样本中第3类的图片光照条件更差。这时不要盲目增加网络节点,优先检查样本。ROC曲线能进一步判断模型判别能力,但对多分类需要做“一对多”处理。我更建议直接用混淆矩阵和各类召回率,因为人脸识别业务里最怕的是某个人永远认不出来。
4.3 识别门禁场景下的阈值设置
实际做人脸门禁时,网络输出不是直接判身份,而是要加一个阈值判断“来的人是否在登记库中”。BP网络的输出经过logsig之后每一类都在0到1之间,理想情况下已知人脸会有一个输出接近1,其余接近0。但如果来的人不在库里,网络也会硬输出一个类别。这时候需要在分类结果外再加一层置信度判断。
[outValue, idx] = max(out); if outValue >= 0.7 % 置信度阈值 predLabel = classes(idx); disp(['身份ID:' num2str(predLabel)]); else disp('未匹配到库中人脸,拒绝通行'); end这里outValue取自sim(net, proj')的最大输出值。0.7是我常用的起始阈值,但不是固定值。你需要画出所有测试样本的outValue分布,再决定阈值。假设样本中坏人(不在库里的脸)的max输出普遍在0.3~0.6,那就把阈值设高一些到0.8;如果库中人脸也有不少max输出在0.6左右,阈值设太高会导致误拒率上升。这个阈值调整就是一个“通信博弈”:阈值越低,越容易被陌生人混进来;阈值越高,自己人更容易被拦在外面。门禁机的典型做法是把阈值调高,宁可误拒也不误放。
5. BP人脸识别的5个经典踩坑:从黑匣子到翻车的血泪记录
5.1 现象:网络不收敛,误差曲线一条直线
训练时你盯着MATLAB的nntraintool窗口,误差曲线从一开始就没下降,损失恒定在某个值。网络输出全是同一类,或者全是0.5。
原因:输入特征范围没统一。如果原始像素没有除以255,数值几百上千,梯度可能在反向传播时溢出;或者隐藏层激活函数用了purelin,输出没有非线性能力。也常见于学习率设置太大,导致权重更新在损失面上震荡但不落地。
解决:先把输入特征做线性归一化到[0,1]或标准化到均值0方差1;隐藏层激活函数改成tansig;学习率从0.01往下调,traingd时用0.001起。训练前打印一下min(feature)和max(feature),确认不是极端数值。
5.2 现象:训练集准确率99%,测试集却只有30%
这是最经典的过拟合。BP网络隐藏层节点数设得太高,比如100个隐藏节点,而训练样本只有50张,网络会把每个训练样本的噪声和背景都背下来。测试集遇到新表情或新光照就彻底失效。
解决:隐藏层节点数降到10~30之间;用TrainLM时设net.trainParam.goal = 1e-3而不是1e-6,提前结束;最重要的,加入PCA降维,把特征从4096压到50维,让网络根本没有机会去背太多细节。我做过一次对比,同样网络和样本,直接用像素时测试集只有35%,PCA降维后测试集提高到82%,这就是特征工程的价值。
5.3 现象:不同光照下人脸识别率骤降
训练集里都是均匀光照的照片,测试时换了强左侧光,识别率掉到一半以下。原因是BP网络对光照敏感,原始像素值受光照影响巨大,同一个人的脸亮暗分布完全变了,网络以为换了人。
解决:预处理时加histeq(img)做直方图均衡化;或者特征改用LBP直方图,LBP对局部纹理的编码方式天然不受全局亮度变化影响。如果你的数据源光照复杂,直接上LBP,识别率会稳健很多。再不行就做MNI归一化,把图像均值和方差对齐到一个固定值。
5.4 现象:MATLAB内存不足或训练极慢
特征维度4096加上几百个样本,trainlm计算雅可比矩阵时内存占用会指数级上涨。我遇到过一台8GB内存的电脑,训练到一半MATLAB直接弹出“Out of Memory”。
解决:把图像尺寸从64×64降到32×32,维度变成1024;先用PCA把特征降到30维再训练,训练速度会快几十倍。如果你必须保留高维度,改训练函数为traingdm,它不计算雅可比矩阵,内存友好很多,但需要把epochs加到2000,多等几分钟。另外记得用clear清掉不用的变量,尤其别让整个图像库的原始数据一直挂在workspace里。
5.5 现象:识别结果永远偏向某一个人
测试集里无论输入谁,网络总是预测成同一个人ID。查混淆矩阵发现大部分样本都落在那个人那一列。
原因:样本不均衡。如果你的人脸库里某类人有50张,其他人只有5张,网络学到的是“输出那个人损失最小”。特别是输出层用了logsig,这类别偏差会被进一步放大。
解决:确保每个类别的样本数尽量接近,不够就用数据增强——对图片做小幅平移、旋转、加噪声来补齐;或者改进训练损失函数,对样本少的类别给更高权重。MATLAB里手动给Y_mat乘以权重矩阵也可以。最简单的做法是重新挑选训练样本,让每类至少8张,通常就能解决。
6. 让这个老方案跑得更稳:模型压缩、早停与跨库泛化验证
6.1 用早停和正则化提升泛化能力
feedforwardnet内置了自动验证集早停机制,但前面我用dividetrain关掉了,为的是手动控制测试集。如果你不用手动划分,可以让网络自己把数据分成70/15/15的训练/验证/测试三份,验证集的作用就是早停——当验证误差连续多次不下降时,训练自动终止,防止过拟合。用net.trainParam.max_fail = 10;控制连续失败次数。手动划分时也可以用早停,但不要用同一个测试集做始终停,否则你又偷看测试集了。
正则化方面,MATLAB的trainbr是“贝叶斯正则化”训练函数,它能在训练过程中自动调整权重大小,抑制过拟合。把net.trainFcn = 'trainbr';就能换用。注意trainbr收敛慢,但最终模型的泛化能力通常比trainlm好,特别适合小样本人脸库。
6.2 把MATLAB模型导出为可部署的形式
课程设计交差是一回事,真要把模型在门禁机上跑又是另一回事。MATLAB训练好的net可以用genFunction生成独立的MATLAB函数,这样部署环境不需要完整的神经网络工具箱:
genFunction(net, 'bp_face_predict.m');生成的bp_face_predict函数接收输入特征,返回网络输出。你可以像普通MATLAB函数一样调用它,适合打包给其他同事。如果目标是C++或嵌入式,还可以用coder工具生成C代码,但前提是你不能用feedforwardnet里那些自动划分的复杂操作,必须用训练好的简单网络对象。我通常把PCA投影和网络输出打包成一个函数,再生成C代码,这样门禁机的ARM板子能跑起来。
6.3 验证模型是否真的“会用”的最后一个技巧
我的习惯是准备一份“没见过的人脸集”——从网上找几张不同光照、不同角度的人脸图片,不属于训练库里的任何人。先拿它们测试:如果模型的max输出低于阈值,说明网络还知道“有些脸不是库里的”,这是判断模型是否真正有用的分水岭。很多BP网络实现做到这一步就露馅了。
接着我会故意把同一张人脸裁剪掉下半部分再测试,看看输出有什么变化。如果特征全挤在嘴巴和下颌,面部细节一丢识别率就崩,我就知道这个模型离产品化还很远。这一步不是要追求灵丹妙药,而是帮你建立对模型边界的感觉。靠着这个习惯,我后来在门禁项目里提前发现了很多不敢上线的问题,也少挨了不少骂。希望帮到你。
(正文完)
本文还有配套的精品资源,点击获取