☰
图像情绪分析课程设计全流程:CNN/VGG/ResNet人脸表情识别复现指南
2026/9/28 6:36:28 网站建设 项目流程

简介:人工智能导论课程设计——基于图像的情绪分析项目包,面向人工智能、计算机及相关专业的在校学生,适用于课程作业、课程设计、毕业设计或项目初期演示。压缩包共25个文件,包含11个Python源文件(涉及模型定义、训练与测试)、6个Markdown说明文档、2份PDF报告,以及XML配置文件、MP4演示视频和Word文档,整体体积22.54MB,目录结构清晰。源码覆盖CNN、VGG、ResNet等多种主流模型,并附有数据划分、可视化、多模型对比等工具脚本,还包含人脸检测配置,便于理解从人脸定位、特征提取到情绪分类的完整流程。配套文档包括实验报告、开题报告、期末项目报告、进展报告和使用说明,另有视频演示辅助,代码均经过测试且答辩平均分达96分,适合基础薄弱者边看边学,也可在此基础上进行二次开发。目前已有251人学习下载,适合需要完整参考实现或快速搭建情绪识别项目的读者。

1. 图像情绪分析大作业:一个能完整复现的课程设计包

“人工智能导论”的大作业做图像情绪分析,最坑的不是模型选型,而是你花了两周调代码,最后发现交上去的文档和代码对不上。这一份课程设计资源把源代码、实验报告、开题报告、答辩报告全放在一个包里,代码测试过、能跑通,评审平均分 96 分——这本身就是一份可以照着复现的完整范例。它解决的问题很明确:从人脸检测、数据集划分、CNN/VGG/ResNet 训练到单张图片推理,一条链路给你走通。适合计科、人工智能、通信、自动化这些专业的学生拿来复现课程设计,也适合基础一般的人先跑通再改自己的数据集。

我要提醒一句:它并不是一个“调好参数直接出结果”的黑匣子,而是把整套工程骨架摊开给你看。你需要自己读代码、改路径、换数据,但好在每段都有对应的文档说明,踩坑时知道去哪个文件里找答案。接下来我按工程结构、数据准备、模型训练、避坑记录这条线给你拆开讲。

2. 工程结构拆解:五个模块分别管哪一段

2.1 从文件清单看整体设计

拿到压缩包先别急着双击运行,我习惯的做法是先列一遍文件清单,搞清楚每个文件是干什么的。这个项目的文件虽然多,但归类后很清晰:

文件/目录职责
model_CNN.py / model_CNN_test.pyCNN 模型的训练与验证脚本
model_VGG.py / model_VGG_test.pyVGG 模型的训练与验证脚本
model_ResNet.py / model_ResNet_test.pyResNet 模型的训练与验证脚本
model_CNN_GPU.pyCNN 的 GPU 加速训练版本
model_All_Compare.py加载三个模型做横向对比,输出指标
data_separation.py划分训练集、验证集、测试集
data_view.py可视化数据集样本分布
image_emotion_mapping.py单张图片的情绪推理与标签映射
haarcascade_frontalface_default.xmlOpenCV 人脸检测的级联分类器
face_images/ 与 dataset/人脸图片与原始数据集
Face-Annotation-Tool-master.zip自建数据集用的标注工具
Fer2013-pytorch-CNN-VGG-Resnet--master.zipFer2013 数据集的参考实现
Facial-expression-recognition-main.zip另一个表情识别参考项目
video/example_dsh.mp4演示视频,可用于答辩展示
开题报告.pdf / 进展报告.doc / 期末课程项目报告.pdf全过程文档材料
README.md / 使用说明.md项目说明与运行指引

从这套结构能看出来,它不是只给你一个训练脚本就完事,而是把“课程设计”从头到尾的产物都整理好了。文档部分尤其值得注意:开题报告、进展报告、期末报告分别对应课程设计的三个时间节点,这比单纯跑通代码更值钱。

2.2 训练脚本与验证脚本的对应关系

三个模型各有一对训练/验证脚本,这是我在这类项目里比较认可的设计。训练脚本负责加载数据、定义损失函数、训练并保存权重;验证脚本负责加载训练好的权重,在测试集上计算准确率。两者分工明确,避免了把训练和评估逻辑揉在一起导致的“验证时用了训练集”这种低级错误。

model_CNN_GPU.py 是单独拎出来的 GPU 版本。如果你本机只有 CPU,就用 model_CNN.py;如果有 NVIDIA 显卡并且装好了 CUDA 版 PyTorch,就用 GPU 版本,训练速度会快一个量级。model_All_Compare.py 的作用更直接:它把三个模型加载到同一份测试数据上,输出准确率对比,答辩时直接拿这张对比表讲“为什么 ResNet 深度更深但效果不一定最好”,比空口说理论有说服力得多。

2.3 文档材料的使用顺序

我建议你按照“开题报告 → 进展报告 → 期末报告”的顺序读,而不是先读代码。开题报告告诉你当时为什么选情绪分析这个题目、预期目标是什么;进展报告记录的是中途做了什么、遇到什么问题;期末报告才是最终的完整技术方案和实验结果。先把这三份文档过一遍,再回去看代码,你会知道每个模型脚本为什么要这样写,而不是停留在“能跑就行”的层面。

README.md 是入口,使用说明.md 是操作手册,文档里通常会写明数据放在哪个目录、训练命令是什么、权重保存在哪。如果你第一次运行就报错,先回去看这两份文件,大部分路径问题都能解决。

3. 人脸检测与数据准备:把图片变成能训练的表情数据集

3.1 用 haarcascade 做前置人脸检测

表情识别的前置步骤是人脸检测。这个项目用的是 haarcascade_frontalface_default.xml,这是 OpenCV 自带的传统级联分类器。你可能会问:现在深度学习人脸检测一大把,为什么课程设计还用这种传统方案?原因很实际:它不依赖 GPU、不额外装模型文件、单张图片检测只要几十毫秒,对课设场景完全够用。而且它与后面的情绪分类是解耦的——先检测人脸,裁剪出来,再送进分类模型。

import cv2 face_cascade = cv2.CascadeClassifier("haarcascade_frontalface_default.xml") img = cv2.imread("face_images/sample.jpg") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) faces = face_cascade.detectMultiScale( gray, scaleFactor=1.1, minNeighbors=5, minSize=(48, 48) ) for (x, y, w, h) in faces: face = img[y:y+h, x:x+w] face = cv2.resize(face, (48, 48)) # face 就是后续要送给情绪分类模型的输入

scaleFactor=1.1 表示每次缩放步长为 10%,数值越小检测越精细但越慢;minNeighbors=5 表示每个候选框至少要有 5 个相邻确认,调大能减少误检但可能漏检;minSize=(48, 48) 是为了和 Fer2013 数据集的输入尺寸对齐。这里有个常见误区:如果 minSize 设得比实际人脸还大,会直接检测不到人脸,视频里尤其明显。

3.2 data_view.py 与 data_separation.py:先看数据再动手训练

我刚拿到这套代码时,第一件事是先跑 data_separation.py 而不是直接训练。原因很简单——如果不清楚数据集怎么划分的,训练完你根本不知道验证集里有没有混入训练样本。data_separation.py 做的就是把 dataset 目录下的图片按比例拆成训练集、验证集、测试集,常见的划分比例是 8:1:1 或 7:2:1。

# data_separation.py 中的常见划分逻辑 import os import random import shutil source_dir = "dataset" train_dir = "dataset/train" val_dir = "dataset/val" test_dir = "dataset/test" ratio = (0.8, 0.1, 0.1) # 首先按类别遍历目录,比如 happy/ sad/ angry/ for class_name in os.listdir(source_dir): class_path = os.path.join(source_dir, class_name) if not os.path.isdir(class_path): continue images = os.listdir(class_path) random.shuffle(images) train_cnt = int(len(images) * ratio[0]) val_cnt = int(len(images) * ratio[1]) # 前 train_cnt 张进训练集,中间进验证集,其余进测试集

划分的逻辑本身不复杂,但要注意两个细节:一是必须按类别目录分别划分,否则会出现某个类别的测试集为空;二是划分前要用 random.shuffle 打乱顺序,否则按文件名排序会导致同一批人脸都在同一个集合里。data_view.py 则是把划分后的数据可视化出来,让你确认每个类别的样本数量是否均衡。我见过很多人在这一步偷懒,最后发现某个类别只有几十张图,训练出来的模型对该类别几乎不识别。

3.3 Face-Annotation-Tool:自建数据集的标注途径

如果你不想用 Fer2013 这个现成数据集,想用自己的图片训练模型,压缩包里的 Face-Annotation-Tool-master.zip 就派上用场了。这是一个图形化标注工具,你可以用它框出图片中的人脸,并打上对应的情绪标签,导出后作为训练数据。

使用流程一般是:先准备一批包含人脸的图片 → 用工具打开图片 → 框选人脸区域 → 选择情绪类别(happy/sad/angry 等)→ 保存标注结果到 CSV 或图片目录。标注完成后,再通过 data_separation.py 划分数据,就可以进入模型训练阶段。

这里给你一个参考标准:想要训练出能看的模型,每个情绪类别至少准备 500 张以上的人脸图,且最好覆盖不同角度、不同光照。如果样本太少,模型很容易过拟合,训练集准确率 99%,测试集一塌糊涂。

4. 三道模型的训练脚本:CNN/VGG/ResNet 差异与训练参数

4.1 三种网络结构在代码上的本质差异

很多初学者误以为 VGG 和 ResNet 是“另一个算法”,其实它们都是卷积神经网络家族,差别在于网络深度和连接方式。这个项目在同一份数据集上实现了三种结构,正好可以对比它们的效果差异。

model_CNN.py 里的网络结构通常是一个基础卷积网络:卷积层 + 激活函数 + 池化层堆叠,最后接全连接层分类。这种结构参数少、训练快,但对复杂特征的提取能力有限。我把常见的简化版结构列出来供你对照源码理解:

import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, num_classes=7): super().__init__() self.features = nn.Sequential( nn.Conv2d(3, 32, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(2) ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(64 * 12 * 12, 128), nn.ReLU(inplace=True), nn.Dropout(0.5), nn.Linear(128, num_classes) ) def forward(self, x): return self.classifier(self.features(x))

这里的输入是 48×48 的彩色图,经过两次 MaxPool2d 后尺寸变成 12×12,所以全连接层的输入维度是 64×12×12=9216。Dropout(0.5) 是为了防止过拟合,训练时随机丢弃一半神经元,验证时自动关闭。注意:如果你自己改了输入尺寸,这个全连接层的维度必须同步调整,否则会报维度不匹配。

model_VGG.py 的思路是“用更深的卷积层堆叠”,连续用多个 3×3 卷积核代替大卷积核,感受野相同但参数更少、非线性更强。VGG 的参数量比基础 CNN 大很多,在小数据集上训练时更需要数据增强来扛过拟合。

model_ResNet.py 的核心是残差连接。我摘出关键代码块给你看:

class BasicBlock(nn.Module): expansion = 1 def __init__(self, in_channels, out_channels, stride=1): super().__init__() self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False) self.bn1 = nn.BatchNorm2d(out_channels) self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False) self.bn2 = nn.BatchNorm2d(out_channels) self.shortcut = nn.Sequential() if stride != 1 or in_channels != out_channels: self.shortcut = nn.Sequential( nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False), nn.BatchNorm2d(out_channels) ) def forward(self, x): out = F.relu(self.bn1(self.conv1(x)), inplace=True) out = self.bn2(self.conv2(out)) out += self.shortcut(x) return F.relu(out, inplace=True)

这个 BasicBlock 是 ResNet 的最小单元。看 forward 里的 out += self.shortcut(x),就是把输入 x 通过一个跳连接直接加到卷积输出上。这一加,让梯度在反向传播时多了一条直达通道,网络可以堆得很深但不会出现梯度消失。ResNet 在图像情绪这个任务上的表现不一定碾压 VGG,但训练过程通常更稳定。

4.2 训练参数怎么设:从脚本里读出关键信息

打开任何一个训练脚本,重点关注这几个参数——batch_size、学习率、epoch 数和优化器。这类课设项目的常用配置大致如下:

参数常见取值说明
输入尺寸48×48 或 64×64Fer2013 原始尺寸是 48×48
batch_size32 或 64显存不够就降到 16
学习率0.001 或 0.0001Adam 优化器常用 0.001
优化器Adam 或 SGDSGD 需要手动调学习率衰减
epoch 数20~50小数据集 30 轮左右就能收敛
损失函数CrossEntropyLoss多分类标配

batch_size 不是越大越好。在课程设计场景,GPU 显存通常只有几 G,batch_size 设 128 容易显存溢出。这时候你要么调小 batch_size,要么把图片尺寸从 64×64 改成 48×48。学习率的设置我一般看损失曲线:损失震荡不下降,就把学习率除以 10;下降太慢,就适度调大。

4.3 model_All_Compare.py 与 model_CNN_GPU.py 的用法

model_All_Compare.py 的核心逻辑不复杂:加载三个训练好的模型权重,分别对同一批测试图片做预测,计算准确率,最后以表格形式输出对比结果。它的价值在于给你一套可重复的评估流程,而不是靠“感觉哪个模型好”。跑这个脚本之前,先确认三个模型的权重文件都存在,而且类别顺序一致。如果训练脚本里的类别映射和对比脚本里的映射不一致,对比结果就是乱的。

model_CNN_GPU.py 是 CNN 模型的 GPU 加速版,它和 model_CNN.py 的网络结构相同,区别在于把数据和张量显式地放到 CUDA 设备上。用 GPU 训练时,要注意把模型、输入数据、标签都移动到 GPU,漏掉任何一个都会报“device mismatch”错误。如果你的电脑没有 NVIDIA GPU,请不要运行这个脚本,直接跑 CPU 版本。

5. 复现踩坑记录:五条最真实的报错与排查

5.1 dataset 路径找不到

现象:运行 data_separation.py 或训练脚本时,报错 FileNotFoundError,提示找不到 dataset 目录或某个子目录。

原因:压缩包解压后目录层级发生了偏移。常见的情况是代码在 dataset/ 下找 train 文件夹,但实际数据在 dataset/train/train 这种嵌套目录里,或者 data_separation.py 还没运行,train/val/test 这三个子目录根本不存在。

解决:先运行 data_separation.py 生成划分后的目录,再启动训练。如果仍报错,在代码里打印 os.path.exists() 逐一确认路径。我习惯把数据路径在脚本开头统一用绝对路径写死,避免换机器后相对路径失效。

5.2 图片维度 mismatch

现象:训练时在 forward 阶段报错,提示 Expected 4D tensor as input,或者全连接层维度对不上。

原因:模型定义里写死了输入尺寸和全连接层维度,但数据加载时读进来的图片尺寸不一致。比如模型是按 48×48 设计的,数据管道里却直接读了原图没有 resize 到 48×48。也可能是图片本身就是灰度单通道,但模型第一层 Conv2d 输入通道写的 3。

解决:在数据加载代码里统一做 resize 和通道转换。灰度图用 cv2.cvtColor(img, cv2.COLOR_GRAY2RGB) 转成三通道再喂给模型。Check 一下 data_view.py 里显示的图片尺寸,如果显示的形状不是 (3, 48, 48),优先修数据加载而不是改模型结构。

5.3 GPU 显存不足

现象:跑了几个 epoch 后报 CUDA out of memory,程序直接中断。

原因:图片尺寸大、batch_size 大、模型参数量多,三者叠加把显存撑爆了。ResNet 在小显存显卡上尤其容易出现这个问题,它虽然参数不算夸张,但中间特征图多。

解决:降 batch_size 到 16 或 8,这是见效最快的方法。如果还不行,把输入尺寸从 64×64 降到 48×48。再不行,换 model_CNN.py 或者用 CPU 版本训练。注意 PyTorch 的显存不一定是占满才报错,其它程序占用的显存也算在内。

5.4 训练集准确率很高但测试集准确率很低

现象:训练集准确率 95% 以上,测试集只有 60% 出头,两个数字差距很大。

原因:典型过拟合。课堂项目用的数据集通常只有几千张图,模型完全记住了训练样本。另一个可能原因是 data_separation.py 划分时没有按类别分层,导致测试集里某些类别样本极少。

解决:先检查数据划分是否符合预期,再看训练脚本里有没有数据增强。常见的做法是加 RandomHorizontalFlip、RandomRotation 这类轻量增强,并把 Dropout 比例从 0.3 调到 0.5。模型方面可以考虑从 ResNet 换回参数量更小的基础 CNN。

5.5 推理结果永远输出同一个类别

现象:跑 image_emotion_mapping.py 时,不管输入哪张图片,输出的情绪标签都是同一个。

原因:大概率是类别映射表错位。训练时类别顺序是 [angry, disgust, fear, happy, neutral, sad, surprise],但推理脚本里按 [happy, sad, angry, ...] 来映射,或者 softmax 输出取的是概率最大值,但索引对应的标签名写错了。

解决:把训练脚本里的类别顺序和推理脚本里的类别列表逐项对齐。更稳妥的做法是跑 model_All_Compare.py 看整体准确率,如果三个模型对同一批图片都输出同一个标签,那基本可以确定是标签映射文件的问题,而不是模型没训练好。

6. 训练完之后怎么真正用起来:单图推理、指标检验与二次微调

6.1 用 image_emotion_mapping.py 跑通单张图片闭环

训练完成后,你要验证的不是训练准确率,而是模型的“落地能力”。image_emotion_mapping.py 做的就是这件事:输入一张人脸图片,输出情绪类别和置信度。它的核心流程是先做人脸检测,再送进分类模型,最后做 softmax 得到概率分布。有一类常见错误是模型权重文件加载后没有调用 eval(),导致 BatchNorm 和 Dropout 仍然处在训练模式,推理结果抖动。

import cv2 import torch import torch.nn.functional as F model = SimpleCNN(num_classes=7) model.load_state_dict(torch.load("model/cnn.pth", map_location="cpu")) model.eval() # 关键:切换到推理模式 img = cv2.imread("test_face.jpg") img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = cv2.resize(img, (48, 48)) tensor = torch.tensor(img).permute(2, 0, 1).unsqueeze(0).float() / 255.0 with torch.no_grad(): logits = model(tensor) prob = F.softmax(logits, dim=1) top3 = prob.topk(3) # 输出前三个置信度最高的类别和对应概率

注意 permute(2, 0, 1) 是把 HWC 格式转成 CHW,这是 PyTorch 的输入格式要求;除以 255.0 是把像素值归一化到 0~1。如果你用的预训练模型在训练时做过均值方差标准化,推理时也要保持完全一致的前处理。topk(3) 只打印前三个概率,方便你判断模型是“很有把握”还是“几个类别都差不多”。

6.2 从视频演示到二次微调:还能再往前走一步

压缩包里的 video/example_dsh.mp4 可以直接拿来演示。把 6.1 的单图推理流程放进一个循环里,逐帧读取视频、做人脸检测、做分类、画框、写标签,输出成新视频,答辩现场放出来比 PPT 讲效果好很多。但要注意 CPU 逐帧推理很慢,建议每 3~5 帧处理一次,或者先把视频抽帧处理好再合成,而不是实时处理。

如果你手里有自己的表情数据集,最简单的微调做法是:加载项目里训练好的 CNN 权重,冻结前面的卷积层,只训练最后一个全连接分类层。冻结的方式是设置 requires_grad=False,再把分类层的输出类别改成你自己的类别数。这样做不需要从头训练,几十张图片就能把模型调到可用的状态。

我自己的习惯是:每次拿到别人的课程设计代码,一定先把 data_separation.py 跑通,确认数据划分没有混样本,再进入训练环节。检验一个模型能不能用,不是看训练集准确率,而是看它在完全没见过的图片上的表现。从那以后我复现这类项目都强制走一遍这个流程,省掉了大量返工时间。希望帮到你。

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

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

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

立即咨询