ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

CNN/VGG/ResNet人脸表情识别实战:从数据到部署全流程

CNN/VGG/ResNet人脸表情识别实战:从数据到部署全流程 简介面向计算机专业毕业设计与深度学习初学者的完整人脸表情识别项目以卷积神经网络为核心覆盖数据预处理、模型搭建、训练评估与实时识别演示的完整流程可直接用于课程作业、论文写作或实战练手。压缩包共36个文件以Python脚本和Jupyter Notebook为主辅以数据集、预训练模型、论文文档及答辩PPT另含演示视频与使用手册整体约446MB目录中针对CNN、VGG、ResNet分别提供模板和测试代码并包含数据划分、情感映射等工具模块。项目经导师指导并获评审98分源码经过编译调试可稳定运行自带的参考工程与模型文件也能帮助读者理解人脸检测、特征提取与表情分类等关键环节。目前已有75人学习适合希望快速搭建人脸表情识别系统并输出论文、答辩等完整成果的学生参考。1. 人脸表情识别不是玩具项目从数据到部署一次走完的 CNN 实战说到卷积神经网络课设和毕设里最能落地的一个题目就是人脸表情识别输入一张人脸输出七类表情中的一类。这个题材好就好在它把数据清洗、模型设计、训练调参、推理部署全串在一起做完一个项目深度学习的完整链路基本都过了一遍。但这个项目有个特点数据环节占的精力比模型环节还大很多同学拿到手就在model_CNN.py里死磕结构最后卡在数据切分、类别映射和摄像头演示上。这套源码把这些环节一次补齐CNN、VGG、ResNet 三套可对照的模型、已训练好的model_resnet.pkl权重、Haar 级联人脸检测、web 演示页面外加论文和答辩 PPT难度正好适合课程设计和本科毕设直接复现。2. 数据管线先立住把 fer2013 原始表变成能送进网络的表情张量打开资源包先别急着看模型。你会发现data_process.py、data_separation.py、data_view.py、image_emotion_mapping.py一字排开数据脚本数量比模型脚本还多。这不是凑目录是原作者把一半以上的时间花在了数据上。做表情识别数据管线的优先级确实高于模型同一个 CNN喂干净的数据和喂错位的数据准确率可能差出十个百分点。2.1 data_process.py 与 data_separation.py原始像素和标签是怎么被拆成三份的Fer2013 格式的原始数据是一张 CSV 表每一行由三部分组成emotion标签、pixels灰度像素串、Usage字段。pixels是 48×48 的灰度值排成一行 2304 个数字。data_process.py干的第一件事就是把这一串数字还原成二维矩阵再除以 255 归一化。这一步不做的话卷积层输出的特征值在一个 epoch 之后就很容易膨胀到溢出表现就是 loss 跳到nan这是新手最容易踩的第一个坑。data_separation.py负责切分。常见做法是直接用 CSV 里自带的 Usage 字段区分 Training、PublicTest、PrivateTest 三份如果手头数据没有这个字段就按 8:1:1 的比例随机切。切分有一点要特别注意尽量按人切不要按样本切。同一个人的不同表情图片如果同时混进训练集和测试集测试准确率会虚高答辩时被问“模型是不是记住了这个人的长相”会很尴尬。这个资源默认走的是标准切分逻辑跑完之后目录结构大致是这样的dataset/ ├── train/ │ ├── angry/ # 索引 0 │ ├── disgust/ # 索引 1 │ ├── fear/ # 索引 2 │ ├── happy/ # 索引 3 │ ├── sad/ # 索引 4 │ ├── surprise/ # 索引 5 │ └── neutral/ # 索引 6 ├── val/ └── test/切完以后顺手跑一遍data_view.py把每个类别各抽一批样本拼成网格图看一眼。这一步看起来是浪费时间的实际能发现两类问题一类是标签错位比如angry目录里混进了sad表情另一类是图片过暗或人脸占比太小这种样本在训练时几乎学不出有效特征不如直接删掉。我一般把可视化脚本放在数据处理完、训练开始前强制跑一次跑完再动手写模型。2.2 用 haar 级联定位人脸为什么它能决定模型上限预训练模型是在裁剪后的人脸上训练的所以推理阶段必须用同一个裁剪策略。资源包里带了haarcascade_frontalface_default.xml这是 OpenCV 自带的经典人脸检测器模型不大但速度很快适合在视频流里逐帧跑。典型用法如下import cv2 face_cascade cv2.CascadeClassifier(haarcascade_frontalface_default.xml) img cv2.imread(demo.jpg) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale( gray, scaleFactor1.1, minNeighbors5, minSize(48, 48) ) for x, y, w, h in faces: face gray[y:y h, x:x w]这里有两个参数直接决定后续分类的上限。scaleFactor表示每轮缩放比例设成 1.1 意味着窗口每次缩小 10%值越小检测越精细但速度越慢minNeighbors控制一个候选框至少要被多少个相邻框确认才算人脸设得越大漏检越多但误检越少。真做实时 demo 时我会把minNeighbors调到 6 到 8宁可偶尔漏检也要避免把背景纹理当成人脸送进表情模型因为表情模型对非人脸输入会输出一个完全没意义的概率分布。另外要强调一句训练数据如果是灰度图推理阶段就必须也用灰度图。上面代码里先转gray再做检测和裁剪这个顺序不能反。如果有人直接把彩色人脸塞给表情模型会先遇到形状不匹配报错更隐蔽的问题是即使强行把三通道图喂进去模型学的是灰度纹理特征输入分布对不上表现会明显变差。2.3 image_emotion_mapping.py类别映射和可视化校验把模型输出的索引映射回文字标签这一步同样容易出事。模型输出的不是字符串而是 0 到 6 的索引image_emotion_mapping.py里维护的就是这张对应表。索引标签训练时容易遇到的问题0angry和 sad 视觉上容易混淆1disgust样本量少容易学不动2fear和 surprise 混淆度高3happy最容易学的一类4sad和 neutral 边界模糊5surprise样本少需要重点看曲线6neutral容易压倒其他类别我的血泪经验是训练脚本、pkl 权重、web 页面三处的映射表必须用同一份拷贝。之前见过有人训练代码里定义的是[angry, fear, happy, ...]网页里又按[happy, sad, angry, ...]读结果全部错位而且显示出来的结果还“看着像那么回事”排查了很久才发现是表的问题。复现项目时直接用资源里的映射文件就好别自己重新排序。3. 三个模型放在一起比着跑CNN、VGG、ResNet 各自的优势与坑模型定义集中在model_CNN.py、model_VGG.py、model_ResNet.py三个文件里配套model_CNN_test.py、model_ResNet_test.py等测试脚本以及一批*_model_template.py模板文件。这种结构明显是为毕业设计准备的论文里要写“比较了三种网络结构”代码里就得有对应的对比实验支撑。3.1 model_CNN.py先用最浅的基线把全流程跑通model_CNN.py是最基础的两层卷积结构输入单通道 48×48 灰度图第一层卷积输出 32 个通道经过池化变成 24×24第二层卷积输出 64 个通道经过池化变成 12×12全连接层从64×12×12拉平接一个 128 维的隐藏层最后输出 7 类。这类实现和下面的结构等价class EmotionCNN(nn.Module): def __init__(self, num_classes7): super().__init__() self.features nn.Sequential( nn.Conv2d(1, 32, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), ) self.classifier nn.Sequential( nn.Linear(64 * 12 * 12, 128), nn.ReLU(inplaceTrue), nn.Dropout(p0.5), nn.Linear(128, num_classes), ) def forward(self, x): x self.features(x) x x.view(x.size(0), -1) return self.classifier(x)注意最后输出层用的是线性层没有内置 Softmax。这不是漏写了是因为 PyTorch 的CrossEntropyLoss会直接在 logits 上算损失推理时再用softmax取概率就行。如果训练时既做 Softmax 又配CrossEntropyLoss等价于在两个概率分布之间算交叉熵梯度会被压得很平。用这个浅网络跑完整条训练流程最大的价值是验证数据管线通不通。我一般拿它先跑两三个 epoch如果 loss 能正常降下来再换 VGG、ResNet 做大实验。深模型一次训练成本高拿浅网络探路能省很多时间。3.2 model_VGG.py 与 model_ResNet.py堆卷积和加残差是两种升级路径VGG 的思路是全部用 3×3 小卷积核靠层数换感受野。两个 3×3 卷积堆叠等效于一个 5×5 卷积三个堆叠等效于一个 7×7 卷积但参数量更少非线性更强。model_VGG.py里通常就是conv 3x3 - BN - ReLU - conv 3x3 - BN - ReLU - pooling这样一组一组堆上去到了后半段接全连接层。ResNet 则是在每一组卷积旁边加一条 shortcut 跳连把输入直接加到输出上。这个小改动解决的是深层网络里梯度消失的问题即使层数很深梯度也可以沿着 shortcut 直接回传。model_ResNet.py和ResNet_model_template.py里可以看到两层卷积和三层卷积两种基础残差块模板文件比裸代码更好改要调整通道数时只需要改nn.Sequential里的数字。这里有两个实际选择问题。第一fer2013 本身是静态人脸表情图数据规模不大ResNet 这类深模型很容易过拟合所以资源里训练脚本一般会配合 BatchNorm 和 Dropout用它们压制验证集上的震荡。第二如果你改了num_classes比如想只分 5 类表情模型定义和 pkl 权重的输出层都要一起改否则就会遇到第 5 章要讲的size mismatch报错。3.3 model_All_Compare.py一次跑通三个模型的对比model_All_Compare.py的作用是把三个模型拉到同一套数据划分、同一套优化器和同样 epoch 数下跑一组对比最后输出准确率和 loss 曲线。对比要以公平为前提比如全部用同样的 batch size、同样的学习率策略只改网络结构本身。资源里给了对比入口你自己复现时可以把表格整理成下面这样对比维度CNNVGGResNet训练速度最快中等较慢特征抽象能力边缘、纹理组合特征深层次语义过拟合风险低较高中残差有抑制作用适合场景快速验证流程中等规模数据数据充足时追求上限写论文时把这组对比放到实验章节说明“浅网络用于验证流程深网络用于提升上限”逻辑上就完整了。答辩时如果被问“为什么选 ResNet”可以从梯度流动和准确率两个角度回答比单纯说“效果好”更有说服力。4. 训练到推理把 model_resnet.pkl 真正用起来拿到这套资源大部分人的目标是绕开漫长训练直接用现成权重跑出识别效果。所以这一章把从加载数据到单张图片预测的全流程拆开说。4.1 DataLoader 与 48×48 输入的设定训练时dataset目录下每个子文件夹是一个类别PyTorch 的ImageFolder可以直接读。入口处一般会接一个 transform 组合把图片统一成 48×48 而且归一化from torch.utils.data import DataLoader from torchvision import datasets, transforms transform transforms.Compose([ transforms.Resize((48, 48)), transforms.ToTensor(), transforms.Normalize([0.5], [0.5]), ]) train_dataset datasets.ImageFolder(dataset/train, transformtransform) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue, num_workers2) for images, labels in train_loader: # images shape: (64, 1, 48, 48) break这里有两个参数值得留意。batch_size设为 64 是显存和训练速度的折中如果你的显卡只有 4GB降到 32 或 16Normalize([0.5], [0.5])把 0 到 1 的像素值映射到 -1 到 1 区间这个均值和方差必须和训练预处理一致。项目里data_process.py归一化过一遍训练脚本再归一化一遍不要中途改数值范围。4.2 优化器、学习率与模型保存fer2013 上我一般会先试 Adam因为它起步快、不太挑学习率适合跑通线如果要刷高准确率再切SGD Momentum(0.9)配合余弦退火或者 StepLR收敛更稳。训练脚本里常见的保存方式是每个 epoch 结束对比验证集准确率只有比历史最好还高才覆盖保存best_acc 0.0 for epoch in range(epochs): train_one_epoch(model, train_loader, optimizer, criterion) val_acc evaluate(model, val_loader) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), model_resnet.pkl) print(fepoch {epoch}: save best model, acc{val_acc:.4f})按验证集准确率保存而不是按训练 loss 保存可以避免把过拟合的中间结果存下来。很多情况下训练 loss 还在降验证 loss 已经反弹了如果保存的是最后一个 epoch 的权重推理效果会明显变差。4.3 加载预训练权重并推理单张图片资源里已经带了model_resnet.pkl你可以跳过训练直接加载。推理代码大概是下面这个模式import torch import torch.nn.functional as F model build_resnet(num_classes7) model.load_state_dict(torch.load(model_resnet.pkl, map_locationcpu)) model.eval() with torch.no_grad(): logits model(face_tensor) # face_tensor shape: (1, 1, 48, 48) prob F.softmax(logits, dim1) idx int(torch.argmax(prob, dim1)) print(prob) # 七类概率例如 [0.02, 0.01, 0.03, 0.88, 0.03, 0.02, 0.01]torch.load的map_locationcpu很有用服务器上训的权重通常带 CUDA 键本地没显卡也能直接加载。model.eval()这一行不能省否则 Dropout 和 BN 的推理行为不对结果会有随机性。torch.no_grad()会关掉梯度计算既省内存又让推理速度更快。如果加载pkl时出现size mismatch说明权重的结构和build_resnet定义不一致最常见原因是训练时的num_classes不是 7。这一点在第 5 章专门讲。4.4 用 templates 页面做交互演示资源里带了templates目录配合 Flask 之类的小服务可以做一个网页版 demo。流程是用户上传一张图片到后端后端先用 Haar 级联裁剪人脸再缩放成 48×48 张量喂给模型把七类概率渲染成柱状图返回前端。答辩现场用摄像头截一张图传到这个页面比在终端里打印一个数字要直观得多。实际做的时候注意路径问题haarcascade_frontalface_default.xml放在项目根目录网页上传的临时图片放在独立的上传目录模型权重用绝对路径或相对路径都行但 Flask 的当前工作目录要确认对否则会报找不到文件。5. 表情识别项目避坑记录最常翻车的五个环节这套资源本身是能跑的但跑的过程中还是有几个高频翻车点值得单独拿出来讲。每条按现象、原因、解决的顺序写都是复现时容易遇到的真问题。5.1 Haar 级联检测不到侧面和暗光场景现象正脸图片一切正常侧脸或光线暗的图片直接返回空列表后续裁剪代码报“找不到人脸”演示中断。原因Haar 级联是基于正脸特征训练的对侧脸、低头、逆光场景召回率很低。表情识别模型本身并没有问题是前面的人脸检测环节把人脸漏掉了。解决先对图片做直方图均衡化提升对比度再把minNeighbors从 5 降到 3scaleFactor从 1.1 改到 1.05让检测更敏感。如果确实需要处理侧脸可以额外准备一张镜像图把两次检测结果合并。批量跑前先抽样看检测率不要直接拿整批数据跑训练。5.2 灰度图和三通道图不一致导致的形状报错现象训练时图片能正常输入到推理阶段报expected 4D input (got 3D)或者mat1 and mat2 shapes cannot be multiplied。原因训练数据走的是ImageFolder transforms.ToTensor()灰度图被自动处理成(1, 48, 48)推理时用 OpenCV 读图cv2.imread默认返回三通道BGR没转灰度就跟模型定义的第一层Conv2d(1, ...)对不上。解决推理入口强制加一行gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)并且在 transform 里用transforms.Grayscale(num_output_channels1)统一格式。每次喂给模型前打印一下face_tensor.shape确认是(1, 1, 48, 48)再继续。5.3 类别映射错位识别结果张冠李戴现象训练 loss 正常下降模型在训练集和测试集上的准确率也不难看但是摄像头预测结果明显不对把开心识别成愤怒把平静识别成悲伤。原因模型输出的索引和页面展示的标签对应不上。ImageFolder是按文件夹名排序的如果训练脚本里手动指定了类别顺序而展示页面用的是另一套顺序两者之间就错位了。解决固定一份映射表训练、测试、展示全部引用同一个文件。资源里的image_emotion_mapping.py就是这个作用。另外在训练前打印train_dataset.class_to_idx和推理代码里的列表一一对照确认再开始。5.4 深模型在小数据集上过拟合验证集准确率飘现象训练 loss 降到很低验证 loss 却持续升高验证集准确率像过山车最后保存出来的模型在真实图片上一塌糊涂。原因fer2013 只有几万张静态图像类别间相似度高ResNet 这类深模型参数量大直接从头训练很容易记住训练样本的细节。解决训练时把数据增强打开随机裁剪、水平翻转、旋转 10 度都加上同时观察验证 loss一旦出现连续几个 epoch 不降反升就把学习率降一个量级或者早停。资源里data_process.py和训练脚本一般带了增强逻辑复现时确认它被调用而不是被注释掉了。5.5 加载 pkl 权重报 size mismatch现象执行model.load_state_dict(torch.load(model_resnet.pkl))报错提示size mismatch for classifier.3.weight之类。原因pkl 里保存的是训练好的state_dict它的形状由训练时的网络结构决定。如果你本地定义的模型输出层是 5 类而权重是 7 类训练出来的最后一层权重自然对不上。解决先确认num_classes7再看模型定义里最后一层全连接输出。如果还不行把 pkl 加载进来打印state_dict的键和形状跟当前模型逐项对比python -c import torch; storch.load(model_resnet.pkl, map_locationcpu); [print(k, v.shape) for k,v in s.items()]这一步能把“模型结构不一致”和“权重文件损坏”快速区分开。权重文件损坏时还会报unexpected key或EOFError那时候就要考虑重新解压一次资源包。6. 进阶把摄像头变成实时表情演示和泛化验证6.1 用 OpenCV 循环把检测和预测串起来资源里的example_dsh.mp4是一个现成的视频示例说明原作者就是按实时视频流来设计演示的。把前面的 Haar 级联、模型推理串到视频循环里就是一个完整的实时表情 demo核心代码很短import cv2 import torch emotion_map [angry, disgust, fear, happy, sad, surprise, neutral] model build_resnet(num_classes7) model.load_state_dict(torch.load(model_resnet.pkl, map_locationcpu)) model.eval() face_cascade cv2.CascadeClassifier(haarcascade_frontalface_default.xml) cap cv2.VideoCapture(0) while True: ok, frame cap.read() if not ok: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale(gray, 1.1, 5, minSize(96, 96)) for x, y, w, h in faces: roi cv2.resize(gray[y:y h, x:x w], (48, 48)) tensor torch.from_numpy(roi).float().div(255) tensor tensor.unsqueeze(0).unsqueeze(0) # (1, 1, 48, 48) with torch.no_grad(): idx model(tensor).argmax(dim1).item() cv2.rectangle(frame, (x, y), (x w, y h), (0, 255, 0), 2) cv2.putText(frame, emotion_map[idx], (x, y - 8), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 0), 2) cv2.imshow(fer, frame) if cv2.waitKey(1) 27: break cap.release() cv2.destroyAllWindows()minSize(96, 96)是为了过滤掉距离太远的小脸也避免模型对模糊小图做无意义预测。实时场景里缩放、归一化、unsqueeze 三步必须和训练时完全一致半路改动数值范围会让准确率肉眼可见地下降。按 ESC 退出这习惯保留着演示时临时要结束循环比较灵活。6.2 验证模型是真学到了泛化规律还是背了样本答辩前有一步我建议一定做拿摄像头分别拍正常表情、夸张表情、戴眼镜、偏头、暗光五组画面把每组画面跑一遍上面的循环记录输出的概率分布。如果正常表情稳定但稍微偏头就被判成另一类说明模型对姿态敏感大概率是人脸检测框不准而不是表情识别的问题。这种“直接喂真实场景”的验证方式比盯着测试集准确率更有说服力。测试集里的图片本质上和训练集同源都比真实摄像头画面干净。只要在你的笔记本摄像头前连续识别 30 秒不飘演示就翻不了车。从那以后我每拿到一个表情识别项目都会在复现时先跑一遍完整推理链路再用摄像头自拍十几秒确认稳定性最后才去看训练曲线的具体数值。模型权重能不能泛化摄像头一照就知道。希望这套检查和调试思路也能帮到你。本文还有配套的精品资源点击获取
返回列表