Matlab深度学习人脸识别实战:CNN训练全流程与避坑指南
简介一份基于Matlab实现深度学习人脸识别的完整源码包内含44个.m文件压缩后大小仅37KB。代码按照DagNN网络架构组织涉及卷积层、池化层、批量归一化、ReLU、SoftMax、DropOut等核心组件并覆盖数据加载、前向传播与梯度更新等关键步骤可帮助读者理解深度网络在Matlab中的实现方式。资源面向计算机、电子信息工程、数学等专业学生尤其适合课程设计、期末大作业或毕业设计中的人脸识别模块参考。目前已有1224人学习下载使用者需要具备一定编程与深度学习基础能够自行阅读和调试代码并根据自己的数据集调整结构与参数。整套源码的价值在于展示了人脸识别从数据输入、特征提取到分类输出的完整流程以及深度网络各层模块的封装逻辑可作为二次开发的起点但不应直接照搬。1. 为什么要用Matlab做深度学习人脸识别从数据集到训练闭环在工程开发里深度学习人脸识别通常和Python绑定PyTorch、TensorFlow一装就是几个G。但有一类场景很具体算法验证用的就是Matlab设备端原型也建立在Matlab上很多高校毕设和公司预研Demo都拿Matlab做快速验证。这个基于Matlab实现深度学习人脸识别的源码包把数据准备、网络训练、模型测试的完整链路都放在一起直接训练CNN做小规模人脸识别不需要额外配Python环境。对于不想折腾环境、又需要快速复现识别效果的开发者这是一条比自造轮子短很多的路。2. 数据准备与加载管线imageDatastore、图像增强与数据划分2.1 数据集目录结构和imageDatastore加载源码包解压之后一般会看到一个数据集目录里面按人脸身份划分了多个子文件夹。这个目录结构是imageDatastore能直接消费的格式每个子文件夹的名字会成为图像的标签。加载数据用一段很短的代码就能完成% 数据根目录face_dataset 下每个子文件夹代表一个人 imds imageDatastore(face_dataset, ... IncludeSubfolders, true, ... LabelSource, foldernames);imageDatastore的作用是不把整批图片一次性读进内存而是延迟加载训练时按区块读取。这个特性对内存有限的机器尤其重要特别是人脸数据集里有几十个类、几百张图时用imread循环手动读图再拼接成矩阵内存很容易爆掉。IncludeSubfolders这个参数默认是false如果你忘了设成true整个datastore会变成空的训练脚本直接报错说找不到图像。LabelSourcefoldernames表示把子文件夹名映射为标签如果文件夹名是1、2、3那标签对应的就是1、2、3。映射规则是按字母顺序这个细节后面避坑章节还要再讲。% 验证一下加载了多少张图 [imgCount, ~] countEachLabel(imds); disp(imgCount);这一步建议在训练前跑一次确认数据量和类别数符合预期。我拿到一个陌生源码包的习惯是先把所有脚本里的数据加载部分单独拎出来跑一遍确认数据管线是通的再走后面的训练不然等到训练报错时根本分不清是数据问题还是网络问题。2.2 数据增强把小样本扩成可训练的规模人脸识别数据集常常每类只有10张样本直接用这么少的数据训练CNN过拟合几乎是必然的。数据增强是这里最便宜的解决手段Matlab的imageDataAugmenter可以产生平移、旋转、缩放、翻转等变化。% 定义数据增强器轻微旋转、平移、缩放 augmenter imageDataAugmenter(... RandRotation, [-10 10], ... RandXTranslation, [-5 5], ... RandYTranslation, [-5 5], ... RandScale, [0.9 1.1]);这四个参数解释一下RandRotation控制在正负10度内随机旋转真人脸在摄像头画面里本来就不是完全水平的这个范围不会把身份特征扭曲掉RandXTranslation和RandYTranslation控制在正负5个像素内随机平移模拟人脸在画面里的位置波动RandScale控制在0.9到1.1倍之间缩放模拟距离变化。如果数据集里的图片分辨率很低比如只有92乘112那平移范围建议再调小一点改成正负3不然边缘大量补黑会导致特征被破坏。增强器定义好之后还需要套一层augmentedImageDatastore才能喂给trainNetwork训练。这里有个细节augmentedImageDatastore需要你指定输出图像尺寸这个尺寸必须和网络层的imageInputLayer保持一致。% 把原始datastore包一层统一缩放为网络要求的输入尺寸 augimds augmentedImageDatastore([112 92 1], imds, ... DataAugmentation, augmenter); augimds.MiniBatchSize 32;[112 92 1]的意思是高度112、宽度92、单通道灰度图。如果你的训练图片本身是RGB三通道而这个网络输入写的是1通道训练就会报维度不匹配。我一般会先用size(imread(某张图))确认图片到底是几通道再决定这个尺寸参数怎么写。2.3 训练集与验证集划分splitEachLabel的参数细节有了完整数据集下一步是按标签把数据切分成训练集和验证集。直接手写目录分配很容易把某几类完全漏掉所以Matlab提供了splitEachLabel% 按每个类别70%训练、30%验证随机打乱后划分 [imdsTrain, imdsValid] splitEachLabel(imds, 0.7, randomized);splitEachLabel的第一个百分比参数是每个类别里分给训练集的比例剩下的自动给验证集。randomized这个参数很重要如果不加那划分就按文件夹里的先后顺序来某些类的图片可能全部进了训练集验证集里就没有这个类了。人脸识别里这种划分错误在混淆矩阵上看起来会特别怪某一个类别的识别率异常高或者异常低。这里还有一个边界要提醒如果要严格评估模型对“没见过的人”的泛化能力那不能做随机划分而要把指定人的所有图片单独留作测试集。源码包里提供的是按比例随机划分适合作为测评基线真要按open-set协议测试需要自己改划分逻辑。3. 网络搭建与训练把简单CNN调成能识别人脸的结构3.1 网络层结构从卷积层到分类器这个源码包没有用预训练的VGG或者ResNet而是自己搭了一个小CNN。理由很简单人脸数据集小、类别固定40类超大网络在这个数据规模下几乎必然过拟合训练时间还长。小网络在几百张图上几分钟就能跑完效果也够用。下面这段是典型的网络结构定义layers [ imageInputLayer([112 92 1], Name, input) convolution2dLayer(3, 16, Padding, same, Name, conv1) batchNormalizationLayer(Name, bn1) reluLayer(Name, relu1) maxPooling2dLayer(2, Stride, 2, Name, pool1) convolution2dLayer(3, 32, Padding, same, Name, conv2) batchNormalizationLayer(Name, bn2) reluLayer(Name, relu2) maxPooling2dLayer(2, Stride, 2, Name, pool2) fullyConnectedLayer(40, Name, fc) softmaxLayer(Name, softmax) classificationLayer(Name, output)];imageInputLayer定义了输入尺寸为112乘92的单通道图像这个和前面的augmentedImageDatastore尺寸必须对得上。convolution2dLayer用了3乘3卷积核第一层输出16个特征图第二层输出32个特征图。卷积核越大参数越多在这个小型网络里3乘3是性价比最高的选择。batchNormalizationLayer是批归一化让每一层的输入分布更稳定对学习率没那么敏感。reluLayer引入非线性maxPooling2dLayer把特征图尺寸减半减少计算量同时保留主要特征。最后一层fullyConnectedLayer(40)里的40就是数据类别数。这里最容易出错有的人把60个类别的数据加载进来却忘了改这个数字然后训练跑着跑着报错说全连接层输出和真实标签数量不一致。3.2 训练选项学习率、MiniBatchSize与MaxEpochs怎么配网络结构定义好之后训练选项是第二个影响成败的地方。源码包里的训练选项大致是这样的% 定义训练选项 options trainingOptions(sgdm, ... InitialLearnRate, 0.01, ... MaxEpochs, 30, ... MiniBatchSize, 32, ... ValidationData, augmentedImageDatastore([112 92 1], imdsValid), ... ValidationFrequency, 10, ... Shuffle, every-epoch, ... Plots, training-progress, ... Verbose, false);这里选sgdm而不是adam是因为在这类小数据集上sgdm配合合适的初始学习率收敛更稳定调整起来更直观。InitialLearnRate0.01对大多数小CNN是个还不错的起点如果loss下降太慢可以试着调到0.03如果震荡太大就降到0.003。几个关键参数的参考范围整理如下训练参数作用推荐范围InitialLearnRate每次参数更新的步长0.003 ~ 0.03MaxEpochs遍历训练集的轮数20 ~ 50MiniBatchSize每个批次包含的样本数16 ~ 32ValidationFrequency每隔多少迭代做一次验证5 ~ 20ValidationData这里需要特别注意它接收的是datastore对象而不是原始图像文件夹路径。我见过有人直接把imdsValid的路径字符串填在这里然后报参数类型错误。ValidationFrequency10表示每10次迭代验证一次别把这个值和epoch混淆了。Shuffle,every-epoch表示每个epoch重新打乱数据顺序避免模型学到固定的序列位置。如果不打乱有些批次的样本分布不均loss曲线会周期性跳动。3.3 训练流程与断点续训别让训练白跑定义好layers和options之后训练就只剩一行命令。需要用训练集再包一层带增强的datastore% 用训练集再包一层增强datastore统一网络输入尺寸 augimdsTrain augmentedImageDatastore([112 92 1], imdsTrain, ... DataAugmentation, augmenter); % 开始训练 [net, info] trainNetwork(augimdsTrain, layers, options);trainNetwork返回两个东西net是训练好的网络info里记录了每次迭代的loss和accuracy。info这个输出很容易被忽略但它对排查训练问题很重要。训练完之后可以用info.TrainingLoss画一下loss曲线看看有没有剧烈震荡或者迟迟不收敛的情况。训练一次可能几分钟但程序一旦关闭net变量就没了。所以要养成训练完立刻保存的习惯% 保存训练好的模型下次直接load save(face_recognition_model.mat, net);下次要复用的时候一行代码把模型载入内存即可。如果要基于旧模型做微调需要把全连接层替换成新的类别数% 载入已训练好的模型替换全连接层后做微调 load(face_recognition_model.mat, net); lgraph layerGraph(net.Layers); lgraph replaceLayer(lgraph, fc, ... fullyConnectedLayer(newClassNum, Name, fc_new)); lgraph replaceLayer(lgraph, output, ... classificationLayer(Name, output_new)); optionsTransfer trainingOptions(sgdm, ... InitialLearnRate, 0.001, ... MaxEpochs, 10, ... MiniBatchSize, 16); net2 trainNetwork(augimdsTrain, lgraph, optionsTransfer);微调时学习率要放低一个量级比如0.001因为前面的层已经提取了有效特征只需要小幅调整。4. 避坑指南Matlab人脸识别项目里的五个血泪坑4.1 坑一训练时用三通道图预测时给单通道图现象训练过程一切正常acc曲线稳步上升但用classify去预测单张图片时直接报维度不匹配的错误。原因网络输入层定义的是[112 92 1]但有些图像的读取结果其实是RGB三通道训练时被augmentedImageDatastore统一处理了而预测脚本是直接用imread读图读出来是三维数组。这两个管道的输入尺寸不一致模型自然不认识。解决预测之前显式统一通道数和尺寸。读取图片后先检查第三维如果是3就转成灰度图img imread(test_person.jpg); if size(img, 3) 3 img rgb2gray(img); end img imresize(img, [112 92]);注意图片尺寸也要resize到网络输入尺寸忘了resize会报另一个维度错误。4.2 坑二标签自动按字母排序训练好事后才发现错位现象训练完成后打印某一类的分类准确率发现几张同类图片分别被分到不同标签下混淆矩阵东一块西一块。原因Matlab的categorical变量按类别名称的字母序排序。如果人脸类别的文件夹名是person_1、person_2到person_10那排序结果是person_1、person_10、person_2而不是数字递增。分类器内部按这个排序后的索引来组织全连接层输出如果你用文件夹名去对应就会错位。解决用分类器输出的标签和原始datastore里的Labels直接比较别手动映射% 直接用datastore自带的Labels做比较不要自己重建标签 YPred classify(net, imdsTest); acc mean(YPred imdsTest.Labels);如果实在需要自己建标签用categorical时要显式指定类别顺序。这里的关键是永远相信datastore里的Labels而不相信文件夹名的自然排序。4.3 坑三读取图片时方向不对训练loss一直不降现象训练loss一直保持在很高水平accuracy始终在十多个百分点徘徊和随机猜差不多。网络结构、学习率、数据量都检查了都没问题。原因部分采集的人脸图片是横着的或者存放时被旋转过。CNN对旋转角度是敏感的一个90度旋转的人脸和一个正常方向的人脸在模型看来是完全不同的事物。数据增强里虽然加了旋转但增强的旋转范围通常只有正负10度救不了90度误差。解决训练前先随机抽看一批图片确认所有人脸方向一致把旋转过的图片统一转正。这段脚本可以批量完成% 批量旋转方向不对的图片统一转为正向先备份再执行 ds imageDatastore(face_dataset, IncludeSubfolders, true); reset(ds); while hasdata(ds) [img, info] read(ds); if size(img, 1) size(img, 2) img imrotate(img, 90); imwrite(img, info.Filename); end end注意这个操作会覆盖原图跑之前先备份整个数据集目录。我之前就是没备份直接跑转完发现有一批图方向转反了又得重新找原始数据。4.4 坑四loss出现NaN模型直接废掉现象训练到一半loss变成NaN之后全部是NaNacc跌到零再继续训练也不会恢复。原因学习率过大导致梯度爆炸或者输入图片中包含全黑、全白这种极端值经过批归一化和softmax时产生无效数值。MiniBatchSize太小含异常样本的批次刚好撞上也会触发。解决先用小学习率重跑一遍确认数据本身没有异常样本。可以用这段代码找出数据里的坏图% 检查数据集中是否存在全黑或全白图片 ds imageDatastore(face_dataset, IncludeSubfolders, true); reset(ds); while hasdata(ds) [img, info] read(ds); if std(double(img(:))) 1 fprintf(发现异常图片: %s\n, info.Filename); end endstd接近0说明这张图几乎没有任何对比度这种图要么删掉要么用正常图片替换。排查完数据之后再把学习率调回正常值重训。4.5 坑五验证集和训练集混在一起识别率虚高现象验证准确率高达99%但把网络用在真实摄像头拍摄的新图片上准确率立刻掉到六成以下。原因训练脚本用了数据集划分后的验证集做评估但同一批采集数据本身就包含重复或高度相似的图像训练集和验证集里的图片只是稍有差异甚至同一张图片被分到了两边。模型其实在背题而不是在学习泛化特征。解决重新划分数据时保证同一人同一时间拍摄的连续帧要么全部进训练集要么全部进验证集。最简单的做法是按文件名单号、双号切分% 按文件名序号奇偶切分而不是随机切分 allFiles imds.Files; oddIdx find(mod(1:numel(allFiles), 2) 1); evenIdx find(mod(1:numel(allFiles), 2) 0); imdsTrain subset(imds, oddIdx); imdsTest subset(imds, evenIdx);这样划分会让训练难度变大但评估结果才真正反映模型的真实水平。5. 模型验证与结果走读混淆矩阵、单张预测与批量推理5.1 单张图片预测的完整脚本训练完的模型最终要落到单张图片预测上。把预测脚本补完整% 单张图片预测 img imread(face_dataset/person_5/pic14.jpg); if size(img, 3) 3 img rgb2gray(img); end img imresize(img, [112 92]); [YPred, scores] classify(net, img); [sortedScores, idx] sort(scores, descend); fprintf(预测结果: %s, 置信度: %.2f\n, char(YPred), sortedScores(1));classify返回的第二个输出scores是每个类别的概率用sort把概率排序之后能看出预测是否犹豫不决。如果最高分和第二高分接近说明这张图处于两个类的边界上这种情况在真实场景里很常见。5.2 混淆矩阵与批量验证批量预测测试集的代码很简单% 批量预测测试集 YPred classify(net, imdsTest); YReal imdsTest.Labels; acc mean(YPred YReal); fprintf(测试集准确率: %.2f%%\n, acc * 100); figure; plotconfusion(YReal, YPred);plotconfusion会生成一张混淆矩阵图横轴是真实标签纵轴是预测标签。对角线越亮代表分类效果越好。如果某一列非对角线区域有明显亮点说明这一类的图片经常被错分成另一个特定的人这时需要回去检查这两类样本是不是长得太像图片数量是不是严重不均衡。5.3 把模型导出给其他脚本用源码包里的模型最后会存成mat文件在其他脚本里可以这样调用% 载入模型继续复用做实时识别 modelData load(face_recognition_model.mat, net); net modelData.net; label classify(net, preprocessFrame(videoFrame));如果要把模型迁移到Python或其他平台Matlab提供了exportONNXNetwork函数可以把训练好的网络导出成ONNX格式交给其他推理框架使用。不过要注意这个函数在新版本工具里才比较好用老版本不支持导出前先确认你的Matlab版本里有没有这个函数。这套流程走通之后我每次重新训练一个人脸识别模型都会强制走一遍相同清单统一图片方向、确认通道数、核对类别数、检查样本划分、训练完立刻保存net。这些步骤看起来琐碎但每一条都对应一次真实的翻车记录。希望帮到你。本文还有配套的精品资源点击获取