☰
基于CNN的人脸表情识别系统:从源码到演示的完整指南
2026/10/12 20:27:07 网站建设 项目流程

简介:这份资源是面向计算机相关专业学生与项目实战学习者的Python期末大作业完整源码,主题为基于CNN的人脸表情识别系统,适合作为课程设计参考或深度学习入门练手项目。压缩包共23个文件,约19.17MB,以10个py脚本为核心,涵盖数据读取、模型定义、训练与识别等模块,另含png、jpeg图像素材、tfrecord数据文件、xml与html页面、md说明文档等,结构完整便于按模块查阅。目前已有144人学习下载,说明该方案在同类作业中具备一定参考价值。项目经导师指导并通过评审,源码均本地编译运行、严格调试,读者可据此掌握卷积神经网络搭建、表情数据预处理、模型训练与摄像头/图片识别推理的完整流程,并借鉴其目录组织与排错思路,快速完成自己的大作业或实战练习。

1. 从一份期末大作业说起:CNN 人脸表情识别到底能跑出什么效果

期末周前两周,实验室里最常见的一幕是:有人拿着「Python期末大作业基于CNN的人脸表情识别系统源码.zip」来问,这东西解压完能不能直接跑、跑出来准不准、答辩时老师会问什么。我当年也踩过这个坑,解压、装环境、改路径、调 batch size,折腾到凌晨三点,最后发现模型在测试集上 60% 出头,演示时摄像头一开,识别结果全是「中性」。这不是源码的问题,是整条链路里几个关键环节没对齐。

人脸表情识别(Facial Expression Recognition,FER)本质是一个图像分类任务:输入一张对齐后的人脸,输出 7 类基本表情——生气、厌恶、恐惧、开心、悲伤、惊讶、中性。CNN 在这里干的事,就是从像素里逐层抽出边缘、纹理、局部器官形状,最后映射到表情类别。它适合谁?适合正在做课程设计、想拿一个完整可演示项目练手的人,也适合已经会写 Python、想补上「数据预处理 → 模型训练 → 推理部署」这条完整链路的人。但如果你指望它直接商用、或者拿它去刷高精度榜单,那得先看清它的边界。

这份源码类项目的价值不在「代码有多长」,而在于它把 FER 的完整流程压缩成了一个可复现的最小闭环。你要做的不是复制粘贴,而是理解每一层为什么这么搭、参数为什么这么设、哪里最容易翻车。下面按「先立住原理,再动手复现,最后排坑」的顺序拆开讲。

2. 拆开这份 CNN 源码:数据、网络、训练三块怎么咬合

2.1 表情数据集长什么样,为什么 FER2013 是绕不开的起点

绝大多数课程设计源码用的都是 FER2013。它包含 35887 张 48×48 灰度人脸图,7 类表情,训练集 28709 张、验证集 3589 张、测试集 3589 张。这个数据集的特点是:分辨率低、标注有噪声、类别不均衡(「开心」和「中性」样本多,「厌恶」和「恐惧」样本少)。源码里如果直接按文件夹读图,不做任何均衡处理,模型会倾向于预测多数类,这就是为什么很多人跑出来「中性」占比特别高。

常见做法是先用torchvision.datasets.ImageFolder或自定义Dataset把图片读成张量,再做归一化。FER2013 的像素均值大约在 0.5 附近,标准差也接近 0.5,所以Normalize((0.5,), (0.5,))是合理起点。如果你用的是 RGB 三通道版本,就把均值标准差都设成(0.5, 0.5, 0.5)。

import torch from torch.utils.data import DataLoader from torchvision import datasets, transforms # FER2013 单通道灰度图,48x48 train_transform = transforms.Compose([ transforms.Grayscale(num_output_channels=1), # 强制单通道 transforms.RandomHorizontalFlip(p=0.5), # 表情左右翻转不改变语义 transforms.RandomRotation(10), # 小角度旋转增强 transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)) # 单通道均值和标准差 ]) train_set = datasets.ImageFolder('data/train', transform=train_transform) train_loader = DataLoader(train_set, batch_size=64, shuffle=True, num_workers=2)

这段代码里三个参数值得盯住:batch_size=64是课程设计里比较稳的值,显存不够就降到 32;num_workers=2在 Windows 上如果报错就改成 0;RandomHorizontalFlip对表情识别是安全的,因为左右翻转不会把「开心」变成「悲伤」。但RandomRotation别开太大,超过 15 度人脸关键点会偏,反而引入噪声。

2.2 一个能跑通的 CNN 结构:三层卷积够不够

源码里常见的网络结构是 3 到 4 个卷积块,每个块是「Conv → ReLU → MaxPool」,最后接全连接层。对于 48×48 的输入,三层卷积已经能覆盖整张图的感受野。下面是一个我常用的最小可用结构:

import torch.nn as nn class SmallFERNet(nn.Module): def __init__(self, num_classes=7): super().__init__() self.features = nn.Sequential( # 输入 1x48x48 -> 32x24x24 nn.Conv2d(1, 32, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(2), # 32x24x24 -> 64x12x12 nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(2), # 64x12x12 -> 128x6x6 nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(2), ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(128 * 6 * 6, 256), nn.ReLU(inplace=True), nn.Dropout(0.5), # 防过拟合 nn.Linear(256, num_classes) ) def forward(self, x): return self.classifier(self.features(x))

padding=1配合kernel_size=3保证卷积后尺寸不变,池化层每次把宽高减半。三层之后特征图是 128×6×6,展平后 4608 维,接 256 维全连接再输出 7 类。Dropout(0.5)在课程设计规模的数据上很关键,不加的话训练集准确率能到 95% 以上,验证集卡在 60% 不动,这就是典型的过拟合。

2.3 训练循环里必须盯住的四个量

训练代码本身不复杂,但四个量决定了模型能不能收敛:学习率、损失函数、优化器、验证频率。课程设计里最稳的组合是Adam + CrossEntropyLoss + lr=1e-3,每训练一个 epoch 就在验证集上跑一次。

import torch.optim as optim device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = SmallFERNet(num_classes=7).to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=1e-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() outputs = model(imgs) loss = criterion(outputs, labels) loss.backward() optimizer.step() # 验证阶段 model.eval() correct, total = 0, 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels = imgs.to(device), labels.to(device) preds = model(imgs).argmax(dim=1) correct += (preds == labels).sum().item() total += labels.size(0) print(f'Epoch {epoch+1}, Val Acc: {correct/total:.4f}')

lr=1e-3是 Adam 的常用起点,如果 loss 震荡就降到 5e-4;CrossEntropyLoss内部已经做了 softmax,不要在模型最后再加一层 softmax;验证时一定要model.eval()和torch.no_grad(),否则 dropout 和 batchnorm 会继续更新,验证结果不可信。30 个 epoch 在单卡上大约 10 到 15 分钟,如果验证准确率在第 10 个 epoch 后不再上升,就可以停了。

3. 从源码到能演示的系统:推理、摄像头、界面怎么接

3.1 单张图片推理:把模型从训练模式切到部署模式

训练完的模型要保存权重,推理时重新加载。这里最容易翻车的地方是:训练时用了Normalize,推理时忘了做同样的归一化,结果模型输出全是乱的。

from PIL import Image def predict_image(model, img_path, device): model.eval() transform = transforms.Compose([ transforms.Grayscale(num_output_channels=1), transforms.Resize((48, 48)), transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)) # 必须和训练一致 ]) img = Image.open(img_path) tensor = transform(img).unsqueeze(0).to(device) # 增加 batch 维度 with torch.no_grad(): logits = model(tensor) prob = torch.softmax(logits, dim=1) pred = prob.argmax(dim=1).item() return pred, prob[0][pred].item()

unsqueeze(0)是把单张图变成1×1×48×48,因为模型 forward 期望有 batch 维度。torch.softmax把 logits 转成概率,方便你看到置信度。如果置信度低于 0.4,基本可以认为模型在「猜」,这时候要么换图,要么检查人脸有没有对齐。

3.2 摄像头实时识别:帧率、人脸检测、表情分类三段式

课程设计演示环节最出彩的是摄像头实时识别。完整链路是:OpenCV 读帧 → 人脸检测器裁出人脸 → 预处理 → CNN 分类 → 在画面上画框和标签。人脸检测用 OpenCV 自带的 Haar 级联就够,不需要额外装模型。

import cv2 face_cascade = cv2.CascadeClassifier( cv2.data.haarcascades + 'haarcascade_frontalface_default.xml' ) 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, scaleFactor=1.1, minNeighbors=5) for (x, y, w, h) in faces: roi = gray[y:y+h, x:x+w] roi = cv2.resize(roi, (48, 48)) roi = roi.astype('float32') / 255.0 roi = (roi - 0.5) / 0.5 # 等价于 Normalize((0.5,), (0.5,)) tensor = torch.from_numpy(roi).unsqueeze(0).unsqueeze(0).to(device) with torch.no_grad(): pred = model(tensor).argmax(dim=1).item() cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2) cv2.putText(frame, EMOTIONS[pred], (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 255, 0), 2) cv2.imshow('FER Demo', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()

scaleFactor=1.1和minNeighbors=5是 Haar 检测的常用参数,前者控制每次缩放比例,后者控制检测框的严格程度。minNeighbors调大误检少但漏检多,调小反过来。摄像头帧率如果掉到 10 帧以下,把检测间隔改成每两帧检测一次,中间帧沿用上一次的框。

3.3 用 Tkinter 或 Gradio 包一层界面:答辩时省一半解释时间

源码里如果带界面,通常是 Tkinter 或 PyQt。Tkinter 更轻,适合课程设计。核心逻辑就是按钮触发文件选择、调用predict_image、把结果显示在 Label 上。如果你不想写桌面界面,用 Gradio 三行代码就能起一个网页版演示:

import gradio as gr def classify(img): pred, conf = predict_image(model, img, device) return f'{EMOTIONS[pred]} ({conf:.2f})' gr.Interface(fn=classify, inputs='image', outputs='text').launch()

gr.Interface会自动生成上传图片和显示文本的页面,launch()默认在本地起服务。答辩时直接浏览器打开,比现场配环境稳得多。

4. 避坑排查:这份源码跑不起来时先看这五条

4.1 报错「No module named torch」或版本冲突

现象:解压后直接python main.py,提示找不到 torch,或者装了 torch 但torchvision版本不匹配。原因:源码作者的环境和你不一样,requirements 里可能没锁版本。解决:先建虚拟环境,再按torch → torchvision → opencv-python → pillow的顺序装,torch 去官网选对应 CUDA 版本的命令,不要直接pip install torch碰运气。装完用python -c "import torch; print(torch.__version__)"确认。

4.2 路径写死导致「FileNotFoundError」

现象:代码里写的是D:/dataset/fer2013/train,你解压到E:/course/下,直接报错。原因:课程设计源码常见问题,作者在自己机器上跑通就打包了。解决:全局搜data/、dataset/、D:、C:这些字符串,改成相对路径或os.path.join(BASE_DIR, 'data')。更稳的做法是在文件开头定义BASE_DIR = os.path.dirname(os.path.abspath(__file__)),所有路径基于它拼。

4.3 验证准确率卡在 25% 不动

现象:训练 loss 缓慢下降,但验证准确率一直在 25% 左右,接近随机猜。原因:最常见的是标签和图片没对齐,ImageFolder按文件夹名排序映射类别,如果你的文件夹名是0,1,2...而标签文件是另一套顺序,就全错位了。解决:打印train_set.class_to_idx确认映射关系,再检查标签文件里的类别顺序是否一致。另一个原因是学习率太大导致梯度爆炸,把lr降到1e-4试一个 epoch。

4.4 摄像头画面卡顿或检测不到人脸

现象:cv2.VideoCapture(0)打开后画面延迟严重,或者人脸框一直不出现。原因:分辨率太高导致每帧处理慢,或者光照太暗 Haar 检测失效。解决:把cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)和HEIGHT, 480设上,降低输入尺寸;检测前做直方图均衡化cv2.equalizeHist(gray);如果还不行,换cv2.CascadeClassifier的frontalface_alt2模型,对侧脸更稳。

4.5 演示时预测结果全是「中性」

现象:训练时验证准确率有 60% 多,但摄像头一开,不管什么表情都输出「中性」。原因:FER2013 里「中性」样本占比最高,模型学到了「预测中性最安全」这个捷径;另外摄像头裁出的人脸和数据集里的人脸分布差异大,模型没泛化过去。解决:训练时给CrossEntropyLoss加weight参数做类别加权,权重按类别频率的倒数算;推理时如果置信度低于 0.5,显示「不确定」而不是硬输出一个类别;演示前用几张自己拍的表情图先测一遍,确认模型在你的光照条件下能工作。

5. 让这份大作业多拿几分:类别加权与置信度阈值的组合技巧

课程设计答辩时,老师最常问的两个问题是「你这个模型为什么准」和「不准的时候怎么办」。前四章把「怎么跑通」讲完了,这一章说一个能让你的系统从「能演示」变成「有说法」的技巧:把类别加权和置信度阈值组合起来用。

FER2013 的类别分布大致是:开心 8989 张、中性 6198 张、悲伤 6077 张、生气 4953 张、惊讶 4002 张、恐惧 4097 张、厌恶 547 张。厌恶类只有 547 张,模型几乎学不到。直接训练的结果是厌恶类召回率接近 0。解决办法是在损失函数里给每个类别一个权重,权重和样本数成反比:

import numpy as np # 按训练集类别计数算权重 class_counts = np.array([4953, 547, 5121, 8989, 6077, 4002, 6198], dtype=np.float32) weights = 1.0 / class_counts weights = weights / weights.sum() * len(class_counts) # 归一化,均值约为 1 class_weights = torch.tensor(weights).to(device) criterion = nn.CrossEntropyLoss(weight=class_weights)

这段代码的关键是weights / weights.sum() * len(class_counts),它让权重均值保持在 1 附近,避免整体 loss 尺度变化太大导致学习率需要重调。加完权重后,厌恶类的召回率通常能从个位数拉到 30% 以上,整体准确率可能掉 1 到 2 个百分点,但模型更「公平」了,答辩时这是一个很好的讨论点。

第二个技巧是推理时的置信度阈值。模型输出的 softmax 概率不是校准过的,高置信度不一定对,但低置信度大概率不可靠。我一般会在演示系统里加一个判断:如果最大概率低于 0.45,就显示「表情不明显」而不是硬报一个类别。这个阈值怎么定?在验证集上跑一遍,画出置信度分布,看正确预测和错误预测的分布交叉点在哪里,取那个位置附近的值。课程设计里不用做得太精细,0.4 到 0.5 之间选一个,演示效果会稳很多。

还有一个容易被忽略的点:输入人脸的对齐。Haar 检测出来的框不一定正,如果人脸有旋转,48×48 的输入里五官位置会偏,模型精度掉得厉害。如果时间允许,加一个简单的眼睛检测,用两眼连线角度做旋转校正,再裁人脸。这一步在课程设计里算加分项,代码量不大,但演示时侧脸也能识别,观感提升明显。

最后说一个我自己的习惯:每次改完参数,先把训练日志里的验证准确率和混淆矩阵存下来,别只记一个最终数字。答辩时老师问「哪个类最容易混」,你直接翻出混淆矩阵,指出「恐惧和惊讶混得多,因为两者眉毛和嘴巴形状接近」,这比背稿子有说服力。模型这东西,跑通只是起点,能说清楚它哪里不行、为什么不行,才是这份大作业真正值钱的地方。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询