资讯动态

基于ResNet的人脸表情识别项目实战:从数据到演示的完整指南

发布时间:2026/10/1 17:48:21 来源:尧图企业网站定制
简介这份资源是面向高校学生与Python初学者的ResNet人脸表情识别完整项目包可直接用于期末大作业、课程设计或深度学习入门实践解决从零搭建表情分类系统时缺少可运行代码与配套数据的问题。压缩包共16个文件约5.2MB包含3个Python脚本分别负责模型定义、训练测试与混淆矩阵绘制7张表情类别示例图覆盖Angry、Happy、Sad、Surprise等七类另有json类别索引、xml人脸检测器、txt依赖清单、md说明文档及mp4演示视频结构清晰、注释完整。目前已有190人学习下载。项目基于ResNet主干网络实现表情分类附带数据集与训练好的模型权重下载后简单部署即可运行既能帮助读者理解卷积网络与迁移学习流程也便于直接提交高质量作业兼具学习参考与实战应用价值。1. 从一份能跑通的 ResNet 表情识别大作业说起期末周前两周实验室学弟发来消息老师要求 Python 大作业必须带界面、带数据集、能演示最好还能讲清楚模型结构。他手里只有一份网上扒下来的 CNN 手写数字识别连摄像头都调不起来。这种场景我见得太多了——课程设计真正卡人的从来不是模型有多深而是「数据从哪来、模型怎么接、界面怎么串、演示怎么不翻车」这四件事能不能一次跑通。这份基于 ResNet 的人脸表情识别项目恰好把这四件事都封在一个压缩包里dataset 目录下按 Angry、Disgust、Fear、Happy、Neutral、Sad、Surprise 七类分好的人脸图model.py 里是带注释的 ResNet 主干class_indices.json 负责类别映射haarcascade_frontalface_default.xml 做人脸检测test.py 和 video 目录里的 jntm.mp4 用来做图片与视频推理演示。它适合谁适合要交期末大作业、课程设计又不想从零搭数据管线的同学也适合已经会写 Python、想拿一个完整小项目练手 ResNet 迁移学习的人。下面我按「资源是什么 → 怎么用 → 坑在哪」的顺序把它拆成能照着复现的步骤。2. 拆开压缩包目录结构与 ResNet 表情分类的选型逻辑2.1 目录里每个文件到底干什么先把压缩包解开根目录大致是这些内容dataset文件夹按七类表情分子目录存放图片model.py定义网络结构class_indices.json保存类别名到索引的映射model目录存放训练好的权重cousion_matrix.py和cousion_matrix_plot负责画混淆矩阵test.py是推理入口video目录放演示视频jntm.mp4haarcascade_frontalface_default.xml是 OpenCV 的人脸检测器requirements.txt列依赖README.md写基本说明。这个结构不是随便摆的它对应一条完整链路数据 → 训练 → 权重 → 推理 → 可视化评估。文件/目录作用是否必须dataset/七类表情训练与验证图片必须model.pyResNet 网络定义必须class_indices.json类别索引映射必须model/训练好的权重文件推理必须test.py图片/视频推理脚本必须haarcascade_frontalface_default.xml人脸检测视频推理必须cousion_matrix.py混淆矩阵绘制评估可选requirements.txt依赖清单建议2.2 为什么选 ResNet 而不是自己堆卷积表情识别属于细粒度分类七类之间的差异集中在嘴角、眉梢、眼睑这些局部区域。自己堆三五层卷积浅层特征抓不住这种细微变化训练时 loss 降得慢验证集准确率容易卡在 50% 上下。ResNet 的残差连接让梯度能跨层回传深层网络也能稳定训练常见做法是拿 ResNet18 或 ResNet34 的预训练权重做迁移学习把最后的全连接层改成七分类输出。这样即使数据集只有几千张图也能在几十个 epoch 内收敛到一个能演示的准确率。选型上还有两个现实理由一是 ResNet 结构规整torchvision.models.resnet18(pretrainedTrue)一行就能加载改fc层即可二是答辩时老师大概率听过 ResNet讲残差块比讲自己设计的网络更有说服力。项目里model.py就是按这个思路写的主干用预训练 ResNet输出层对齐七类。2.3 环境准备与依赖安装先确认 Python 版本建议 3.8 到 3.10太新的版本某些 torch 轮子可能对不上。然后建虚拟环境别直接往系统 Python 里装这是血泪经验——依赖冲突会把你折腾到怀疑人生。# 创建虚拟环境名字随意这里叫 emo python -m venv emo # 激活Windows emo\Scripts\activate # 激活macOS / Linux source emo/bin/activate # 安装依赖requirements.txt 里通常含 torch、torchvision、opencv-python、numpy、matplotlib pip install -r requirements.txt如果requirements.txt里没锁版本torch 装 CPU 版就够跑推理训练想快再换 CUDA 版。装完用下面这段验证环境是否正常import torch import torchvision import cv2 import numpy as np print(torch:, torch.__version__) print(torchvision:, torchvision.__version__) print(opencv:, cv2.__version__) print(cuda available:, torch.cuda.is_available())逻辑说明这段只做导入和版本打印不涉及模型。参数上重点看cuda available返回 False 说明用的是 CPU推理没问题训练会慢可以接受就继续。如果导入 cv2 报错多半是 opencv-python 没装好重装pip install opencv-python即可。3. 数据管线与模型定义把七类表情喂进 ResNet3.1 dataset 目录的读取与预处理dataset下每个表情一个文件夹这种结构可以直接用torchvision.datasets.ImageFolder读它会自动按文件夹名生成类别索引。但要注意ImageFolder 生成的索引是字母序和class_indices.json里的映射必须一致否则推理时标签会错位——这是最常见的翻车点之一。import torch from torchvision import datasets, transforms # 训练时的预处理随机裁剪、翻转增强泛化再归一化 train_transform transforms.Compose([ transforms.Resize((224, 224)), # ResNet 标准输入尺寸 transforms.RandomHorizontalFlip(), # 表情左右翻转仍成立 transforms.ToTensor(), transforms.Normalize( # ImageNet 均值方差 mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225] ) ]) # 验证/推理不做随机增强只做 resize 和归一化 val_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize( mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225] ) ]) dataset datasets.ImageFolder(rootdataset, transformtrain_transform) print(类别映射:, dataset.class_to_idx) print(样本总数:, len(dataset))逻辑说明Resize((224,224))是 ResNet 的固定输入别改成别的尺寸除非你同步改了网络。Normalize用的均值和方差来自 ImageNet因为主干是预训练权重输入分布要匹配。dataset.class_to_idx打印出来的顺序要和你class_indices.json对照不一致就手动改 json别偷懒。3.2 model.py 里的 ResNet 改造点打开model.py核心就两处加载预训练 ResNet替换全连接层。下面是我按项目思路还原的关键代码实际文件里注释更细。import torch.nn as nn from torchvision import models def build_model(num_classes7, pretrainedTrue): # 加载 ResNet18pretrainedTrue 会下载 ImageNet 预训练权重 model models.resnet18(pretrainedpretrained) # 冻结前面所有层只训练最后的全连接数据少时推荐 for param in model.parameters(): param.requires_grad False # 替换 fc 层ResNet18 的 fc 输入是 512 维 in_features model.fc.in_features model.fc nn.Linear(in_features, num_classes) return model if __name__ __main__: net build_model(num_classes7) print(net.fc) # 应输出 Linear(in_features512, out_features7)逻辑说明pretrainedTrue第一次运行会联网下载权重下不动就手动下好放到 torch 缓存目录。冻结参数是为了防止小数据集把预训练特征带偏如果你数据量够大每类上千张可以把requires_grad全放开做微调。num_classes7对应七类表情改类别数时这里和class_indices.json要同步改。3.3 训练循环与关键参数训练脚本通常不在压缩包单独列但按这个项目的结构你需要在model.py基础上补一个训练入口。下面给一个最小可用的训练循环。import torch from torch.utils.data import DataLoader, random_split from torchvision import datasets, transforms from model import build_model device torch.device(cuda if torch.cuda.is_available() else cpu) transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225]) ]) full datasets.ImageFolder(dataset, transformtransform) train_size int(0.8 * len(full)) val_size len(full) - train_size train_set, val_set random_split(full, [train_size, val_size]) train_loader DataLoader(train_set, batch_size32, shuffleTrue) val_loader DataLoader(val_set, batch_size32, shuffleFalse) net build_model(num_classes7).to(device) criterion torch.nn.CrossEntropyLoss() # 只优化 fc 层参数学习率可以稍大 optimizer torch.optim.Adam(net.fc.parameters(), lr1e-3) for epoch in range(20): net.train() for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() out net(imgs) loss criterion(out, labels) loss.backward() optimizer.step() # 验证 net.eval() correct, total 0, 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(device), labels.to(device) out net(imgs) pred out.argmax(dim1) correct (pred labels).sum().item() total labels.size(0) print(fepoch {epoch1}, val acc {correct/total:.4f}) torch.save(net.state_dict(), model/resnet_emotion.pth)逻辑说明batch_size32是显存和收敛速度的折中显存小就降到 16。lr1e-3只针对 fc 层因为前面冻结了这个学习率偏大也稳。random_split按 8:2 切训练验证数据少时可以改成 9:1。保存的权重路径要和test.py里加载路径一致否则推理时报找不到文件。4. 推理与可视化test.py、摄像头与混淆矩阵4.1 test.py 单张图片推理流程test.py是演示入口典型流程是读图 → 人脸检测 → 裁剪人脸 → 预处理 → 送模型 → 输出类别。下面按这个逻辑还原关键片段。import torch import cv2 import json from PIL import Image from torchvision import transforms from model import build_model device torch.device(cuda if torch.cuda.is_available() else cpu) # 加载类别映射 with open(class_indices.json, r, encodingutf-8) as f: class_indices json.load(f) # json 里通常是 {0:angry, ...}反转成索引到名字 idx_to_class {int(k): v for k, v in class_indices.items()} net build_model(num_classes7, pretrainedFalse) net.load_state_dict(torch.load(model/resnet_emotion.pth, map_locationdevice)) net.to(device) net.eval() transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225]) ]) # 人脸检测器 face_cascade cv2.CascadeClassifier(haarcascade_frontalface_default.xml) img cv2.imread(test.jpg) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale(gray, 1.1, 5) for (x, y, w, h) in faces: roi img[y:yh, x:xw] roi cv2.cvtColor(roi, cv2.COLOR_BGR2RGB) roi Image.fromarray(roi) tensor transform(roi).unsqueeze(0).to(device) with torch.no_grad(): out net(tensor) pred out.argmax(dim1).item() label idx_to_class[pred] cv2.rectangle(img, (x, y), (xw, yh), (0, 255, 0), 2) cv2.putText(img, label, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0,255,0), 2) cv2.imwrite(result.jpg, img)逻辑说明detectMultiScale的1.1是缩放步长5是邻域阈值人脸漏检就调小阈值误检多就调大。unsqueeze(0)是加 batch 维度模型要求四维输入。map_locationdevice保证在 CPU 机器上也能加载 GPU 保存的权重这个参数不加换机器就报错。4.2 视频推理与 jntm.mp4 演示视频推理就是把单张图流程套进cv2.VideoCapture循环逐帧检测、逐帧预测。项目里video/jntm.mp4就是给这个用的。cap cv2.VideoCapture(video/jntm.mp4) fourcc cv2.VideoWriter_fourcc(*mp4v) out_writer cv2.VideoWriter(output.mp4, fourcc, 25, (int(cap.get(3)), int(cap.get(4)))) while True: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale(gray, 1.1, 5) for (x, y, w, h) in faces: roi frame[y:yh, x:xw] roi cv2.cvtColor(roi, cv2.COLOR_BGR2RGB) roi Image.fromarray(roi) tensor transform(roi).unsqueeze(0).to(device) with torch.no_grad(): pred net(tensor).argmax(dim1).item() label idx_to_class[pred] cv2.rectangle(frame, (x, y), (xw, yh), (0,255,0), 2) cv2.putText(frame, label, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0,255,0), 2) out_writer.write(frame) cap.release() out_writer.release()逻辑说明VideoWriter的帧率要和源视频一致否则输出会变速。逐帧推理在 CPU 上可能只有几 FPS演示前先跑一遍确认不卡。如果视频里人脸小detectMultiScale的minSize参数可以设个下限减少误检。4.3 混淆矩阵评估模型真实水平cousion_matrix.py用来画混淆矩阵这是答辩时最能体现你「不只是跑通还评估过」的部分。核心是用验证集跑一遍预测统计七类之间的错分情况。from sklearn.metrics import confusion_matrix import matplotlib.pyplot as plt import seaborn as sns import numpy as np # y_true, y_pred 是验证集真实标签和预测标签列表 cm confusion_matrix(y_true, y_pred) plt.figure(figsize(8, 6)) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabelslist(idx_to_class.values()), yticklabelslist(idx_to_class.values())) plt.xlabel(Predicted) plt.ylabel(True) plt.title(Confusion Matrix) plt.tight_layout() plt.savefig(cousion_matrix_plot/cm.png, dpi150)逻辑说明annotTrue显示数值fmtd保证整数格式。看矩阵时重点看对角线以及 Happy 和 Neutral、Sad 和 Angry 之间有没有明显错分——这两组是表情识别里最容易混的。如果某类召回率特别低说明该类样本少或特征不明显可以针对性补数据。5. 避坑与排查跑这个项目最容易翻车的五个地方5.1 类别索引错位导致标签全乱现象推理结果看起来「有输出」但 Happy 的图被标成 Sad整体准确率异常低。原因ImageFolder按文件夹名字母序生成索引而class_indices.json可能是按别的顺序写的两者不一致。解决训练前打印dataset.class_to_idx和 json 逐项对照不一致就改 json 或改文件夹命名保证索引对齐。5.2 预训练权重下载失败现象第一次运行models.resnet18(pretrainedTrue)卡住或报网络错误。原因权重默认从境外地址下载网络不通就失败。解决手动下载 resnet18 权重文件放到~/.cache/torch/hub/checkpoints/目录下文件名保持官方命名再运行就不会重新下载。5.3 人脸检测漏检导致无输出现象图片里明明有人脸但faces为空画面上没有任何框。原因detectMultiScale参数偏严或图片光照差、人脸偏小。解决把scaleFactor从 1.1 调到 1.05minNeighbors从 5 调到 3必要时加minSize(30,30)。如果还不行检查图片是不是灰度图或通道数不对。5.4 权重加载报 missing keys现象load_state_dict报一堆 missing keys 或 unexpected keys。原因保存权重时用了torch.save(net)整个模型加载时却用load_state_dict或者模型结构改过fc 层维度对不上。解决统一用torch.save(net.state_dict(), path)保存加载时先build_model(num_classes7, pretrainedFalse)再load_state_dict。维度对不上就检查num_classes是否一致。5.5 CPU/GPU 切换导致加载失败现象在 GPU 机器上训练的权重拿到 CPU 机器上加载报错。原因torch.load默认按保存时的设备加载。解决加载时加map_locationdevicedevice 根据torch.cuda.is_available()动态选。这个参数加上跨机器演示就不慌了。6. 进阶技巧把演示准确率再往上提一档跑通只是及格线想让大作业拿高分得在演示效果上再抠一抠。我一般会做三件事。第一件是数据层面七类里如果某类样本明显少用transforms加随机旋转、颜色抖动或者干脆复制少数类做过采样让每类数量接近。第二件是推理层面detectMultiScale检测到的人脸往外扩 10% 再裁剪因为表情信息在嘴角和眉梢裁太紧会丢特征。第三件是展示层面把test.py的输出改成实时显示按q退出答辩时直接开摄像头演示比放视频更有说服力。# 摄像头实时演示按 q 退出 cap cv2.VideoCapture(0) while True: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale(gray, 1.1, 5, minSize(60, 60)) for (x, y, w, h) in faces: # 往外扩 10%保留更多表情上下文 pad int(0.1 * w) x1, y1 max(0, x-pad), max(0, y-pad) x2, y2 min(frame.shape[1], xwpad), min(frame.shape[0], yhpad) roi frame[y1:y2, x1:x2] roi cv2.cvtColor(roi, cv2.COLOR_BGR2RGB) roi Image.fromarray(roi) tensor transform(roi).unsqueeze(0).to(device) with torch.no_grad(): pred net(tensor).argmax(dim1).item() label idx_to_class[pred] cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, label, (x1, y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 255, 0), 2) cv2.imshow(Emotion, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()逻辑说明minSize(60,60)过滤掉太小的误检框pad扩边保留表情上下文waitKey(1)控制刷新按 q 退出。这套改完演示时摄像头对着脸框和标签实时跟着走比静态图片直观得多。还有一个小技巧是混淆矩阵的解读。答辩老师如果问「哪类最难分」你直接指着矩阵说 Happy 和 Neutral 之间错分最多因为两者嘴角弧度接近然后补一句「后续可以引入注意力机制强化眼部特征」这就把问题变成了改进方向比干巴巴报准确率强。从那以后我每次交课程设计前都强制走一遍「换机器加载权重 摄像头实时跑 混淆矩阵截图」这三步确认演示环境不翻车再交。希望这份拆解帮到你把这份 ResNet 人脸表情识别项目真正跑成自己的东西。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑