拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

基于CNN的人脸表情识别系统源码实战:从训练到部署

基于CNN的人脸表情识别系统源码实战:从训练到部署 简介这份资源是面向深度学习入门者与计算机视觉方向学生的人脸面部表情识别系统项目源码基于卷积神经网络实现可用于课程设计、毕业设计或算法练手。项目采用fer2013人脸数据集覆盖图像获取、预处理、特征提取、运动特征提取与分类判别等完整流程支持从本地图片导入或调用摄像头拍摄对图片和视频进行分析并可在不同模型间切换对比效果。压缩包共47个文件约12.18MB包含5个Python脚本、1个hdf5模型权重、1个ui界面文件、1个qrc资源文件以及png、jpg、gif等界面素材和mp4演示视频另有docx问题记录、ppt项目展示与md说明文档目录结构清晰便于按模块阅读与二次开发。目前已有936人学习下载读者可借此理解表情识别从数据到推理的工程链路参考界面搭建、模型加载与测试排错思路快速搭建自己的识别demo。1. 从一张皱眉自拍说起这套 CNN 表情识别源码到底能跑出什么前阵子帮一个做在线教育的朋友看需求他想在直播课里实时判断学生是不是听懵了——皱眉、撇嘴、瞪眼这些信号如果能自动标出来老师就能及时调整节奏。他一开始想调商用 API算下来一节课的成本比课时费还贵后来我让他直接拿这套基于卷积神经网络的人脸面部表情识别系统源码本地跑。这套东西的核心链路很清晰摄像头或图片进来先做人脸检测裁出 ROI再送进 CNN 做七类表情分类生气、厌恶、恐惧、开心、中性、难过、惊讶最后把结果叠加回画面。它适合两类人一是想拿一个能跑通的深度学习实战项目练手的学生或转行者二是需要在私有环境里做表情分析、又不想把数据传出去的小团队。源码包里通常包含训练脚本、推理脚本、预训练权重和一份依赖清单拿到手不用从零搭网络改改数据路径就能出结果。2. 拆开压缩包先看什么目录结构、依赖与模型选型2.1 拿到源码先别急着 pip install很多人解压完第一件事就是pip install -r requirements.txt然后被一堆版本冲突劝退。我的习惯是先花五分钟把目录扫一遍搞清楚哪些是训练代码、哪些是推理代码、权重文件放在哪、数据从哪读。典型的表情识别项目目录长这样emotion_recognition/ ├── data/ # 数据集目录通常按类别分子文件夹 │ ├── train/ │ │ ├── angry/ │ │ ├── happy/ │ │ └── ... │ └── val/ ├── models/ # 网络定义与权重 │ ├── cnn.py │ └── emotion_cnn.pth ├── train.py # 训练入口 ├── predict.py # 单张图片/摄像头推理 ├── utils/ │ ├── face_detect.py # 人脸检测封装 │ └── dataset.py # Dataset 与 DataLoader └── requirements.txt先确认三件事权重文件在不在没有权重就得自己训时间成本差一个量级、人脸检测用的是 OpenCV 的 Haar 还是 DNN 模块影响精度和速度、分类类别数和顺序顺序错了标签全乱。这三点确认完再动环境。2.2 环境依赖与版本对齐深度学习项目翻车十有八九是版本问题。这套源码常见的技术栈是 PyTorch OpenCV NumPy偶尔带 torchvision 做数据增强。我一般会先建独立虚拟环境避免污染全局# 建一个 Python 3.8 的虚拟环境3.8 对老版本 PyTorch 兼容性最好 conda create -n emotion python3.8 -y conda activate emotion # 先装 PyTorch注意 CUDA 版本要和显卡驱动匹配 # 没有 GPU 就用 CPU 版命令里的 cu118 换成 cpu pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 再装其余依赖 pip install opencv-python numpy pillow matplotlib这里的关键参数是 CUDA 版本。cu118对应 CUDA 11.8如果你的驱动只支持到 11.6就得换成cu116否则torch.cuda.is_available()会返回 False训练直接掉到 CPU 上一个 epoch 能跑半小时。装完务必验证import torch print(torch.__version__) # 确认版本 print(torch.cuda.is_available()) # 必须是 True 才用得上 GPU print(torch.cuda.get_device_name(0)) # 看显卡型号对不对如果第三行报错说明 CUDA 和驱动没对齐别硬跑先解决这个。2.3 为什么是 CNN 而不是别的表情识别本质是图像分类输入是一张对齐后的人脸小图常见 48x48 或 64x64 灰度图输出是七类概率。CNN 的卷积核天然适合提取局部纹理特征——眼角皱纹、嘴角弧度、眉毛角度这些正是表情的判别依据。相比全连接网络CNN 参数量小、平移不变性好人脸稍微偏一点也能认。相比 TransformerCNN 在小数据集上更不容易过拟合训练也快。这套源码用的多半是类似 VGG 或 ResNet 的简化结构几层卷积加池化再堆全连接对 48x48 的输入足够了。选它的理由很实在数据量不大、算力有限、要的是能快速跑通的基线而不是刷榜。3. 把数据喂进去数据集组织、训练脚本与参数调优3.1 数据集怎么摆才不会被 DataLoader 报错CNN 训练最容易被卡住的地方不是网络是数据。PyTorch 的ImageFolder要求按类别分文件夹每个文件夹里放对应类别的图片文件夹名就是标签名。常见做法是data/train/ ├── angry/ (约 4000 张) ├── disgust/ (约 500 张) ├── fear/ (约 4000 张) ├── happy/ (约 7000 张) ├── neutral/ (约 5000 张) ├── sad/ (约 4000 张) └── surprise/ (约 3000 张)注意类别不平衡——disgust 往往只有其他类的十分之一直接训会让模型偏向多数类遇到 disgust 全预测成 neutral。解决办法是在损失函数里加权重或者对少数类做增强。我一般先跑一遍统计import os from collections import Counter root data/train counts {cls: len(os.listdir(os.path.join(root, cls))) for cls in os.listdir(root)} print(counts) total sum(counts.values()) for cls, n in counts.items(): print(f{cls}: {n} ({n/total*100:.1f}%))如果某个类占比低于 5%就得处理否则训出来的模型在那个类上基本是瞎猜。3.2 训练脚本的关键参数逐个说清训练入口一般长这样我把每个参数为什么这么设讲透import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets, transforms from models.cnn import EmotionCNN # 图像预处理训练集做增强验证集只做归一化 train_tf transforms.Compose([ transforms.Grayscale(num_output_channels1), # 表情识别常用灰度减少计算 transforms.Resize((48, 48)), # 统一到网络输入尺寸 transforms.RandomHorizontalFlip(), # 水平翻转增强表情左右对称 transforms.RandomRotation(10), # 小角度旋转容忍头部倾斜 transforms.ToTensor(), transforms.Normalize([0.5], [0.5]) # 单通道归一化到 [-1,1] ]) val_tf transforms.Compose([ transforms.Grayscale(num_output_channels1), transforms.Resize((48, 48)), transforms.ToTensor(), transforms.Normalize([0.5], [0.5]) ]) train_ds datasets.ImageFolder(data/train, transformtrain_tf) val_ds datasets.ImageFolder(data/val, transformval_tf) # batch_size 受显存限制8G 显存跑 48x48 可以到 128 train_loader DataLoader(train_ds, batch_size64, shuffleTrue, num_workers4) val_loader DataLoader(val_ds, batch_size64, shuffleFalse, num_workers4) device torch.device(cuda if torch.cuda.is_available() else cpu) model EmotionCNN(num_classes7).to(device) # 类别不平衡时给少数类更高权重 class_weights torch.tensor([1.0, 3.0, 1.0, 0.8, 0.9, 1.0, 1.2]).to(device) criterion nn.CrossEntropyLoss(weightclass_weights) # 学习率 1e-3 是 Adam 的常用起点太大震荡太小收敛慢 optimizer torch.optim.Adam(model.parameters(), lr1e-3) for epoch in range(30): model.train() for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() out model(imgs) loss criterion(out, labels) loss.backward() optimizer.step() # 每个 epoch 后在验证集上评估别只看训练 loss model.eval() correct total 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(device), labels.to(device) pred model(imgs).argmax(1) correct (pred labels).sum().item() total labels.size(0) print(fepoch {epoch}, val_acc {correct/total:.4f})几个参数的血泪经验num_workers在 Windows 上设 0设大了会卡死batch_size别贪大显存爆了报的是 CUDA out of memory但根因往往是没清缓存学习率如果前几个 epoch loss 不降先降到 1e-4 试试。验证集准确率才是真指标训练 loss 降到 0.01 而验证集不动那是过拟合了。3.3 训练多久算够、怎么判断收敛30 个 epoch 是常见起点但别死守。判断收敛看验证集准确率曲线连续 5 个 epoch 不涨就可以停再训只会过拟合。表情识别在 FER2013 这类数据集上简单 CNN 能到 60% 出头加残差结构或迁移学习能到 65% 以上。如果验证集准确率卡在 40% 以下先查标签顺序对不对、图片有没有读错通道别急着调网络。4. 推理与部署从单张图片到摄像头实时识别4.1 单张图片推理的完整链路训练完拿到权重推理脚本要做的事是读图 → 人脸检测 → 裁剪对齐 → 预处理 → 送网络 → 取最大概率类别。人脸检测这步不能省直接把整张图缩到 48x48 送进去背景会严重干扰。import cv2 import torch import numpy as np from torchvision import transforms from models.cnn import EmotionCNN EMOTIONS [angry, disgust, fear, happy, neutral, sad, surprise] device torch.device(cuda if torch.cuda.is_available() else cpu) model EmotionCNN(num_classes7).to(device) model.load_state_dict(torch.load(models/emotion_cnn.pth, map_locationdevice)) model.eval() # 人脸检测器OpenCV 自带无需额外下载 face_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) tf transforms.Compose([ transforms.ToPILImage(), transforms.Grayscale(num_output_channels1), transforms.Resize((48, 48)), transforms.ToTensor(), transforms.Normalize([0.5], [0.5]) ]) img cv2.imread(test.jpg) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale(gray, scaleFactor1.1, minNeighbors5) for (x, y, w, h) in faces: roi gray[y:yh, x:xw] # 裁出人脸区域 tensor tf(roi).unsqueeze(0).to(device) # 加 batch 维度 with torch.no_grad(): prob torch.softmax(model(tensor), dim1) idx prob.argmax(1).item() label f{EMOTIONS[idx]} {prob[0][idx]:.2f} cv2.rectangle(img, (x, y), (xw, yh), (0, 255, 0), 2) cv2.putText(img, label, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imwrite(result.jpg, img)detectMultiScale的两个参数值得说scaleFactor1.1是每次缩放比例越小检测越细但越慢minNeighbors5是邻域阈值调大能减少误检但可能漏掉侧脸。侧脸检测不到是 Haar 的固有短板要提升就换 DNN 人脸检测器。4.2 摄像头实时识别的帧率优化实时场景下瓶颈往往不在 CNN而在人脸检测和图像拷贝。常见做法是隔帧检测人脸、缓存 ROI 位置中间帧直接复用坐标cap cv2.VideoCapture(0) frame_count 0 faces [] while True: ret, frame cap.read() if not ret: break frame_count 1 gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 每 5 帧检测一次人脸其余帧复用上次结果 if frame_count % 5 0: faces face_cascade.detectMultiScale(gray, 1.1, 5) for (x, y, w, h) in faces: roi gray[y:yh, x:xw] tensor tf(roi).unsqueeze(0).to(device) with torch.no_grad(): idx model(tensor).argmax(1).item() cv2.rectangle(frame, (x, y), (xw, yh), (0, 255, 0), 2) cv2.putText(frame, EMOTIONS[idx], (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imshow(emotion, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()隔帧检测能把帧率从 10 帧提到 25 帧以上。如果还卡把输入分辨率降到 320x240 再检测检测完把坐标按比例映射回原图。4.3 部署时的模型导出要在没有 PyTorch 的环境跑可以导出 ONNXdummy torch.randn(1, 1, 48, 48).to(device) torch.onnx.export( model, dummy, emotion.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch}, output: {0: batch}} )导出后可以用 onnxruntime 推理速度通常比 PyTorch 快一点依赖也轻。注意导出时输入尺寸要和推理时一致动态 batch 用dynamic_axes声明。5. 避坑与排查那些让我重训三次的问题5.1 验证集准确率死活上不去现象训练 loss 一直降验证集准确率卡在 35% 左右不动。原因多半是标签顺序错乱——ImageFolder按文件夹名字母序排标签angry, disgust, fear, happy, neutral, sad, surprise但推理脚本里手写的EMOTIONS列表顺序如果不一样评估时全对不上。解决打印train_ds.classes确认顺序推理脚本直接引用这个列表别手写。5.2 摄像头画面卡成幻灯片现象实时识别只有 5 帧画面一顿一顿。原因是每帧都跑人脸检测Haar 检测本身很吃 CPU。解决按 4.2 节隔帧检测或者换cv2.dnn的轻量人脸检测模型。另外cv2.waitKey(1)别写成waitKey(0)后者会阻塞等按键。5.3 换自己的数据集后全部预测成同一类现象用自己拍的表情图测试不管什么表情都输出 neutral。原因是训练集和测试集分布差异大——训练用的是标准数据集的正脸灰度图自己拍的是侧光、有背景、彩色图。解决推理前的人脸裁剪和对齐要和训练时一致必要时在目标场景的数据上做微调冻结卷积层只训全连接。5.4 CUDA out of memory 但显存明明够现象报显存不足但nvidia-smi看占用不高。原因是 PyTorch 缓存没释放或者num_workers开太多每个进程都占显存。解决训练循环里用torch.cuda.empty_cache()num_workers降到 2 或 0batch_size减半先跑通再往上加。5.5 权重加载报 key 不匹配现象load_state_dict报一堆 missing keys 和 unexpected keys。原因是网络结构改了但权重还是旧的或者保存时用了DataParallel导致 key 多了module.前缀。解决保存时用model.state_dict()而非model.module.state_dict()加载时如果有多余前缀用字典推导去掉state torch.load(emotion_cnn.pth, map_locationdevice) state {k.replace(module., ): v for k, v in state.items()} model.load_state_dict(state)6. 把准确率再抬一截迁移学习与数据增强的实操技巧基线跑通之后想从 60% 往 68% 走最划算的一步是迁移学习。做法是拿在 ImageNet 上预训练过的 ResNet18 或 MobileNetV2把第一层改成单通道或把灰度图复制成三通道最后一层全连接改成 7 类然后分两阶段训先冻结卷积层只训分类头 5 个 epoch再解冻全部用 1e-4 的小学习率微调 15 个 epoch。这样比从零训收敛快得多小数据集上也不容易过拟合。import torchvision.models as models import torch.nn as nn # 用预训练 ResNet18灰度图复制成三通道适配 model models.resnet18(weightsmodels.ResNet18_Weights.DEFAULT) model.conv1 nn.Conv2d(1, 64, kernel_size7, stride2, padding3, biasFalse) model.fc nn.Linear(model.fc.in_features, 7) model model.to(device) # 第一阶段冻结主干只训分类头 for name, param in model.named_parameters(): if fc not in name: param.requires_grad False optimizer torch.optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr1e-3) # 第二阶段解冻全部小学习率微调 for param in model.parameters(): param.requires_grad True optimizer torch.optim.Adam(model.parameters(), lr1e-4)数据增强这块除了翻转和旋转CutMix 和 MixUp 在小数据集上效果明显但实现稍复杂先用基础的够用。另一个容易被忽略的点是标签平滑label smoothing把硬标签 0/1 换成 0.1/0.9能缓解模型过度自信验证集准确率通常能涨 1 到 2 个点criterion nn.CrossEntropyLoss(label_smoothing0.1)验证方法上别只看整体准确率画混淆矩阵看哪两类容易混。表情识别里 sad 和 neutral、fear 和 surprise 经常互相误判这是数据本身的模糊性靠调网络解决不了得从数据标注一致性入手。我一般会固定一个随机种子保证每次训练结果可复现import random, numpy as np seed 42 random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed)从那以后我每次拿到新的表情识别源码都强制先跑一遍混淆矩阵再谈优化——不看清楚模型到底错在哪调参就是瞎猜。这套源码的价值不在于它开箱即用多完美而在于它把从数据到推理的完整链路摊开给你每个环节都能动手改。希望帮到你。本文还有配套的精品资源点击获取
返回列表