简介:这是人工智能导论课程‘基于图像的情绪分析’大作业的完整方案,源码、文档与实验报告齐备,面向课程设计、大作业汇报或答辩场景,适合高校相关专业学生参考,也适合初学者学习表情识别与图像分类流程。压缩包共25个文件、22.54MB,含11个Python脚本,涵盖CNN、VGG、ResNet多个模型的训练、测试与效果对比,以及数据划分、数据视图、人脸检测、情绪映射等功能代码;另有6个Markdown说明、人脸检测XML、演示视频,以及开题报告、进展报告和期末课程项目报告等材料,并附使用说明,目录按功能分层,便于快速上手和定向检索。资源内集成3个参考工程压缩包,可作为二次开发基础。已有251人浏览/学习,遇到运行问题可私信作者,支持远程教学。对需要完成课程设计、准备答辩或梳理完整项目链路的读者,这份资料能从开题、数据处理、模型搭建、效果对比到报告撰写提供全流程参考。
1. 人工智能导论大作业:基于图像的情绪分析,到底在做一个什么问题
人工智能导论大作业里,基于图像的情绪分析是一个特别适合练手的题目:它不要求你发明算法,却要求你把“图像输入到情绪标签输出”这条链路真正跑通,并交出源代码、文档说明和实验报告。很多同学会误以为这只是“下载一个模型跑一遍”,实际上还要处理数据集、选网络、调参数、做评估,最后把过程写成老师看得懂的实验报告。这背后需要你掌握图像分类、迁移学习、模型验证、工程目录组织等一堆知识点,但每一个点又都不会深到让你在导论课阶段就放弃。这篇内容适合正在写人工智能导论大作业、或者想用一个小项目入门图像分类的从业者和学生。我会按一套能交付的工程来拆解,包括技术选型、代码结构、文档组织,以及我踩过的坑。
2. 先把原理立住:情绪分析为什么不是“看图说话”这么简单
2.1 人脸检测与情绪分类的流水线,先拆任务再选模型
基于图像的情绪分析,严格说包含两个层级的任务:第一,在图像里找到人脸;第二,把每个人脸区域划分为预先定义好的情绪类别。传统教材里这套流程叫“级联结构”——先用目标检测把人脸框出来,再用分类网络判断情绪。如果你跳过检测,直接把整张图扔给分类模型,模型会被背景、光照、多个人脸干扰,很难收敛。所以大作业的第一件事,是把这个问题拆成“检测+分类”两个子问题。
检测环节最稳妥的做法是用 OpenCV 的 Haar Cascade 或 MTCNN。Haar Cascade 不需要额外训练,调用一行就出来,缺点是漏检率偏高,尤其是侧脸和暗光。MTCNN 效果好但需要额外安装库。我一般建议大作业先上 Haar Cascade,把流程跑通,再决定要不要换检测器。检测器的输出是一个矩形框 (x, y, w, h),后续情绪分类只处理这个裁剪区域。这一步决定了后面分类器的输入质量,如果框大了会把头发、背景都包进来,框小了会切掉眉毛和嘴,情绪特征不完整。
分类环节要定义情绪标签。公开数据集里最常见的七类表情是生气、厌恶、恐惧、高兴、悲伤、惊讶和中性。注意这七个类别之间的边界很模糊,比如“恐惧”和“惊讶”在眼睛瞪大时特别容易混;“中性”经常被误判成“悲伤”。这不是你的模型有问题,而是情绪本身存在主观性。大作业里最好明确使用数据集官方标签,不要自己重新标注,否则报告里没法说服老师。
2.2 从传统特征到预训练CNN:大作业选型的三条路线
如果只按照导论课讲过的知识选型,你会面对三条路线,各有取舍。
第一条是传统特征加经典分类器:提取 HOG 特征,训练 SVM 分类器。这条路代码量小,几十行就能跑,CPU 也能训。但 HOG 特征对表情这种局部形变很不敏感,在 FER2013 这类真实情绪数据集上通常只有 50% 出头的准确率,和随机猜测的 14% 比有提升,但拿到答辩现场不够好看。除非老师要求“不能只背深度学习”,否则不建议主推。
第二条是小型卷积神经网络:自己写一个 4 到 6 层的 CNN,输入 48x48 灰度图。这能体现你理解卷积、池化、全连接这些基础概念,而且训练参数量不大,普通笔记本 CPU 也能在半小时内跑完一个 epoch。缺点是收敛状态很看运气,同样的网络结构换一个初始化,结果可能差 5 个百分点。“玄学”往往就出现在这条路上。
第三条是迁移学习:加载 ImageNet 预训练模型,例如 ResNet18,把最后一层换成 7 分类,冻结前面几层只微调后三层和分类头。这条路线我最推荐,因为它在导论课作业里不算超纲——你只要说明“预训练权重帮模型在小样本上更快收敛”,老师就能理解。实际效果在 FER2013 上通常比小型 CNN 高出 10 到 15 个点,而且调试时间短。下表对比三条路线的落地感受。
| 路线 | 代码量 | 训练耗时 | 在FER2013参考准确率 | 答辩亮点 | 风险 |
|---|---|---|---|---|---|
| HOG+SVM | 少 | 分钟级 | 约50%-60% | 经典ML过程完整 | 效果说服力弱 |
| 小CNN | 中 | 半小时/epoch | 约60%-70% | 网络结构理解清晰 | 调参看运气 |
| 迁移学习ResNet18 | 中 | 几分钟/epoch | 约70%-75% | 能讲清fine-tune原理 | 需解释为何不用更大网络 |
这里的参考准确率是我用常见配置跑出来的经验区间,不是绝对标准。准确率受数据切分影响很大,同一个模型换个划分就可能浮动 2 到 3 个点。汇报时不要单看准确率,而是把准确率配合混淆矩阵一起讲,这样老师会认为你有评估意识。
选型理由上,迁移学习还有一个隐藏好处:它能帮你少碰梯度消失。导论课作业通常不要求你别出心裁设计网络,直接微调 ResNet18 既安全又有一定创新度——你在报告里多写一句“只替换卷积首层适配灰度图,并微调到最后两层”,就足以展现思考深度。
2.3 数据集与标签:情绪类别怎么定,正负样本别踩坑
公开最常用的情绪分类数据集是 FER2013,它把每张人脸表情保存成 48x48 的灰度像素,总样本量 35887 张,分训练集、公开测试集和私有测试集。另一个常用的是 CK+,图数少但标注干净,多用于验证集。导论课大作业不建议自己爬图,第一费时间,第二标注很难做到一致。用现成数据集,报告里可以写清楚来源、数量、格式,这部分是老师一定会看的。
FER2013 有一个需要提前知道的坑:类别严重不平衡。happy 和 neutral 数量多,disgust 数量特别少,可能只有整个数据集的 2% 到 3%。如果你不做任何处理,模型会对多数类过拟合,最终测试准确率虽然能看,但每个类别的 recall 相差悬殊。解决不平衡最常见的有三种:对训练集做重采样,让每个类别样本数接近;给损失函数加类别权重;或者把过少且容易混淆的类别合并。我一般会用 sklearn 的 compute_class_weight 直接算权重,然后塞进 CrossEntropyLoss 的 weight 参数。这比改代码更省事。
还要注意情绪类别定义。FER2013 的 7 类里,disgust 和 fear 经常样本少且和 anger 混淆。如果你的大作业只有两周,建议保留原始 7 类,用类别权重兜底;但如果最后测试集上 disgust 的 F1 还是不到 0.2,可以在报告的“改进方向”里写“将 disgust 与 fear 合并成 negative 类”,并补做一个 6 类实验,让老师看到你不是只会照跑,而是会分析问题。
数据预处理要固定一个流程:灰度图 → 压缩到统一尺寸 → 归一化。不要用原图尺寸直接训练,因为不同图片宽高比不同,全连接层维度会崩。归一化用 mean=0.5、std=0.5 比较省事,等价于把像素值压到 -1 到 1 之间,配合大多数网络都能稳定训练。如果走迁移学习,第一层输入是三通道,需要把灰度图复制成三通道,并使用 ImageNet 的 mean/std(0.485, 0.456, 0.406 / 0.229, 0.224, 0.225)。这两种归一化都常见,关键是要保证训练和推理用同一个参数,否则模型推理结果会“翻车”。
3. 把代码跑通:从数据集到训练再到推理的最小工程
3.1 项目目录与依赖:一份能直接交的源码结构
动手写代码前先把目录组织好,比写到一半再整理要省力得多。我见过很多同学把训练脚本、数据集、报告全堆在桌面上,最后提交时补一个压缩包,文件名还是“新建文档”。老师打开这种项目,第一印象就是你缺乏工程规范。一套简单的目录结构如下:
emotion_analysis/ ├── data/ │ ├── raw/ # 原始数据集,按标签存子目录 │ └── processed/ # 预处理后的图片,训练时可读取 ├── src/ │ ├── dataset.py # 数据集加载、增强、归一化 │ ├── train.py # 训练主脚本,保存最优权重 │ ├── inference.py # 测试集推理、画混淆矩阵、样本预测图 │ └── utils.py # 随机种子、类别权重等公共函数 ├── docs/ │ ├── README.md # 文档说明,怎么跑起来 │ └── experiment_report.md # 实验报告正文 ├── checkpoints/ # 训练好的模型权重 └── requirements.txt # 依赖清单这个目录的用意是让源码、数据、文档分离:src 只放代码,data 放原始和处理后的数据,docs 放文档说明和实验报告,checkpoints 放权重。大作业只需要交源码加文档时,把 checkpoints 里的大权重文件删掉或另存网盘,压缩包体积就很小。如果老师要求“源代码+文档说明+实验报告”三件套,这个目录直接对应三件事,不要额外拼凑。
依赖清单 requirements.txt 也别随手写。最稳妥的是列出你实际 import 过的核心库,不要用pip freeze > requirements.txt把几十个间接依赖全塞进去。课设环境通常不好复现,越精简越容易装上。下面是一份够用的依赖清单:
opencv-python>=4.6.0 torch>=1.13.0 torchvision>=0.14.0 numpy>=1.21.0 matplotlib>=3.5.0 scikit-learn>=1.0.0 pandas>=1.3.0说明:opencv 负责读取图像和人脸检测;torch/torchvision 负责模型和预训练权重;matplotlib 画混淆矩阵和样本图;scikit-learn 提供 classification_report 和类别权重计算;pandas 用来读 csv 格式的标签文件。版本写下限不写死,是为了在同学之间传递压缩包时不容易因为“缺某个小版本”而崩掉。
3.2 数据加载与预处理:搞定灰度图、归一化和数据增强
先写 Dataset 类,这是 PyTorch 工程里最容易被忽略但最关键的一环。它决定了每条训练样本是怎么从磁盘变成张量的。下面这套代码可以直接适应按标签存子目录的图片结构。
# src/dataset.py import os import torch from torch.utils.data import Dataset from torchvision import transforms from PIL import Image class EmotionDataset(Dataset): def __init__(self, img_dir, label_dict, train=True): self.img_dir = img_dir self.samples = list(label_dict.keys()) self.labels = label_dict self.train = train if train: self.transform = transforms.Compose([ transforms.RandomHorizontalFlip(p=0.5), transforms.RandomAffine(degrees=8, translate=(0.05, 0.05)), transforms.Resize((48, 48)), transforms.ToTensor(), transforms.Normalize(mean=[0.5], std=[0.5]) ]) else: self.transform = transforms.Compose([ transforms.Resize((48, 48)), transforms.ToTensor(), transforms.Normalize(mean=[0.5], std=[0.5]) ]) def __len__(self): return len(self.samples) def __getitem__(self, idx): img_name = self.samples[idx] img = Image.open(os.path.join(self.img_dir, img_name)).convert('L') img = self.transform(img) label = self.labels[img_name] return img, label这段代码的核心是 transform 的差异:训练时随机水平翻转和轻微仿射变换,用于扩大样本多样性,抑制过拟合;测试时不加增强,只做 resize 和归一化。之所以只做 8 度旋转和 5% 平移,是因为情绪分类对姿态变化敏感,角度太大的增强会把人脸五官形状扭曲到失去语义。
这里要特别说明灰度图处理:convert('L')把图片读成单通道,后面 Normalize 用mean=[0.5], std=[0.5],两个参数对应灰度图。如果你改成三通道迁移学习,要么把convert('L')改成convert('RGB'),Normalize 换成 ImageNet 的三通道均值方差;要么像我在训练脚本里那样,把模型第一层改成单通道输入。两种改法都能跑,但别混用——训练时用单通道,推理时如果忘了convert('L'),模型会直接报通道数错误,或者输出概率一团糟,这种“玄学报错”通常是预处理不一致造成的。
3.3 训练脚本关键参数:epoch、batch size、学习率怎么设
训练脚本是整套作业的心脏。这里以 ResNet18 迁移学习为例子,给出了可运行的最小骨架。
# src/train.py import torch import torch.nn as nn from torch.optim import AdamW from torch.utils.data import DataLoader from torchvision import models from dataset import EmotionDataset # 超参数 BATCH_SIZE = 64 EPOCHS = 30 LR = 1e-3 NUM_CLASSES = 7 DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) model.conv1 = nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3, bias=False) model.fc = nn.Linear(model.fc.in_features, NUM_CLASSES) model.to(DEVICE) train_ds = EmotionDataset("data/processed/train", train_label_dict, train=True) val_ds = EmotionDataset("data/processed/val", val_label_dict, train=False) train_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True, num_workers=2) val_loader = DataLoader(val_ds, batch_size=BATCH_SIZE, shuffle=False, num_workers=2) criterion = nn.CrossEntropyLoss() optimizer = AdamW(model.parameters(), lr=LR, weight_decay=1e-4) for epoch in range(EPOCHS): model.train() total_loss = 0.0 for imgs, labels in train_loader: imgs, labels = imgs.to(DEVICE), labels.to(DEVICE) optimizer.zero_grad() preds = model(imgs) loss = criterion(preds, labels) loss.backward() optimizer.step() total_loss += loss.item() avg_loss = total_loss / len(train_loader) print(f"epoch {epoch+1}/{EPOCHS}, loss: {avg_loss:.4f}") # 验证 model.eval() correct = 0 total = 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"val acc: {correct / total:.4f}")关键参数我有几个自己的设置习惯。BATCH_SIZE 取 64,是因为 48x48 的灰度图内存占用小,64 和 128 在训练速度上差异不大,但 64 对 BN 层的稳定性更好。EPOCHS 设 30 而非导论课里常见的 10,是因为迁移学习在微调阶段前几轮还在“适应”新分类头,收敛比从零训练更慢,但也用不着 100 轮。LR 初始 1e-3 配合 AdamW 足够,如果你发现验证损失在涨,就把 LR 降到 1e-4 继续微调。weight_decay=1e-4 算是标准的 L2 正则,能压住一些噪声大的样本对权重的影响。
代码里有一点需要特别注意:验证阶段用torch.no_grad()避免记录梯度,否则验证会占一倍的显存;model.eval()和model.train()切换也很关键,否则 BN 层在验证时还在用训练集统计量,导致准确率偏“虚高”。每次 epoch 结束后记得把验证准确率记到日志里,后面写实验报告需要这个数据。
3.4 推理与可视化:出图、出报告,老师看的不是代码是结果
老师评审这份大作业,不会一行行读你的训练代码,但一定会看你输出的混淆矩阵、分类报告和几张“预测成功/失败”的样本图。所以推理脚本要承担“产出证据”的作用。下面这段代码在测试集上跑一遍,输出每个类别的 precision、recall、F1,并画混淆矩阵保存到 docs 目录。
# src/inference.py import torch import numpy as np import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix, classification_report CLASSES = ["angry", "disgust", "fear", "happy", "sad", "surprise", "neutral"] def plot_confusion_matrix(cm, save_path="docs/confusion_matrix.png"): plt.figure(figsize=(8, 6)) plt.imshow(cm, cmap="Blues") plt.xticks(range(len(CLASSES)), CLASSES, rotation=45) plt.yticks(range(len(CLASSES)), CLASSES) plt.colorbar() plt.xlabel("Predicted") plt.ylabel("True") plt.tight_layout() plt.savefig(save_path, dpi=150) preds_all, labels_all = [], [] model.eval() with torch.no_grad(): for imgs, labels in val_loader: imgs = imgs.to(DEVICE) preds = model(imgs).argmax(dim=1).cpu().numpy() preds_all.extend(preds) labels_all.extend(labels.numpy()) cm = confusion_matrix(labels_all, preds_all) print(classification_report(labels_all, preds_all, target_names=CLASSES)) plot_confusion_matrix(cm)这段代码有两点值得注意。一是把预测移到 CPU 上再转 numpy,避免 GPU 和设备之间频繁拷贝导致显存碎片;二是用extend而不是append,否则会把每个 batch 当成一个列表元素,最后算混淆矩阵时维度对不上。classification_report 会直接给出每个类的指标,你在实验报告里可以直接引用这些数字。
除了混淆矩阵,我还会额外写一个函数,随机抽 4 张测试图像,把真实标签和预测标签并排打印到一张图上。这能直观展示模型的错误形态,答辩时也比只看数字更有说服力。保存图片时注意dpi=150,清晰度足够插入 Word 或 Markdown 报告。
4. 源代码之外,文档说明和实验报告才是拿高分的关键
4.1 文档说明怎么写:从README到环境复现
很多同学把“文档说明”理解成把代码复制一遍再加几句注释,这其实是在浪费机会。老师需要的是一个陌生人(或者两周后的你自己)拿到压缩包后,能在十分钟内重现你的实验结果。README 就是入口。一份合格的 README 至少要回答三件事:这个项目在做什么、怎么安装依赖、怎么跑出报告里的结果。
# 基于图像的情绪分析 ## 项目简介 对输入人脸图像进行七类情绪分类,使用 ResNet18 迁移学习,数据集为 FER2013。 ## 环境要求 - Python 3.8+ - PyTorch 1.13+ - 见 requirements.txt ## 快速开始 1. 将数据集解压到 data/raw 目录 2. python src/train.py 3. python src/inference.py 4. 结果输出到 docs/ ## 结果 最终验证集准确率:70.3%(在私有测试集上)这个模板看起来朴素,但把“复现路径”写清楚了。“结果”一栏的准确率应该来自你最后一次固定种子跑出来的数字,不要为了好看写虚高值。老师如果复现不了,第一反应是怀疑你的实验造假,这比准确率低 2 个点严重得多。所以在 README 里写清楚运行时需要的文件结构,如果你的数据来自 FER2013 官方 csv,还要注明转换脚本放哪里。
4.2 实验报告结构:背景、方法、实验、结论四件套
实验报告需要跟着课程要求的模板走,没有模板时我习惯按以下结构组织。它不是流水账,而是用一个问题串起来:我为什么要做情绪分析,我用了什么方法,我证明了什么。
| 报告章节 | 核心内容 | 建议篇幅 |
|---|---|---|
| 引言 | 情绪分析的应用场景,视频/人机交互/教育 | 约300字 |
| 相关工作 | 传统 HOG+SVM、深度 CNN、迁移学习 | 约500字 |
| 方法 | 检测+分类流水线,网络结构,预处理,训练策略 | 约800字 |
| 实验 | 数据集说明、评价指标、结果表格、消融对比、混淆矩阵 | 约1000字 |
| 结论与改进 | 总结达到的效果,说明类别混淆和未来方案 | 约300字 |
写“方法”部分时,要画一张简单的流程框图:输入图像 → 人脸检测 → 裁剪 → 灰度化/缩放 → 情绪分类网络 → 输出标签。能放进报告里的图,比一百行文字都有效。如果不会画好看的框图,用 PowerPoint 画一个矩形加箭头的图导出 PNG 就可以。
“实验”部分要把训练过程中的真实数据放进去。准确率、loss 曲线、混淆矩阵这三样至少要齐全。有同学觉得自己的准确率只有 70% 太低,其实老师更看重你能否解释为什么。比如你可以写“在混淆矩阵上,sad 和 neutral 最常互混,原因是两种表情的嘴部变化不明显”,这句话比空泛的“模型还需要改进”有价值得多。
4.3 实验记录与对比:把“玄学”写成“可控变量”
从我的经验看,大作业能拉开差距的地方就在这里——能不能把训练过程中那几次“翻车”写成可控的实验变量。比如下面这张表,是我在调试时会记录的内容。
| 组别 | 模型 | 数据增强 | 优化器 | 学习率 | epoch | 验证准确率 | 备注 |
|---|---|---|---|---|---|---|---|
| A | 小型CNN | 无 | Adam | 1e-3 | 20 | 61.4% | baseline |
| B | ResNet18 | 无 | AdamW | 1e-3 | 20 | 68.9% | 迁移学习提升明显 |
| C | ResNet18 | 水平翻转+仿射 | AdamW | 1e-3 | 30 | 71.2% | 增强缓解过拟合 |
| D | ResNet18 | 增强+类别权重 | AdamW | 1e-3 | 30 | 70.6% | 类别权重小幅波动 |
这张表不是虚构,应根据你的实际实验填写。写报告时把每一行“为什么改,改了什么,效果如何”展开成一段,就构成了一节完整的“消融实验”。这对老师的吸引力远大于一次调参尝试的说明,说明你真的在控制变量做实验,而不是碰运气。
要生成这么一张表,你训练脚本里最好把每次运行的配置和时间戳记录成 CSV 或 json。最简单的做法是训练结束时把超参数和验证准确率追加到docs/experiment_log.csv,之后再整理进报告。这不算额外工作,只是把“我记得”改成“数据记得”。
4.4 答辩准备:老师最常问的三个问题
基于图像的情绪分析大作业答辩,老师大概率会围绕“选型”和“结果真实性”提问。我整理了三个高频问题,提前准备好能省掉很多卡壳。
第一,“为什么用 ResNet18,不试试更深的网络?”你回答的核心是预算和收益:设备显存有限,ResNet18 在 FER2013 这种小规模灰度数据集上已经能有 70% 左右准确率,更深的 ResNet34/50 在数据量增加前提下才有优势,在几千张训练图上反而容易过拟合。再补一句“我打算在改进方向里加入 ResNet34 对比”,效果更好。
第二,“灰度图为什么能训练出好模型?”你可以说人脸表情的主要线索来自纹理和轮廓,颜色信息对情绪判断贡献小;同时 FER2013 数据集本身就设计成灰度图,所以沿用这个设定。如果老师追问三通道预训练模型怎么适配,就把替换 conv1 那行代码讲清楚。
第三,“准确率还能怎么提升?”这是一个送分题,不要只说“加数据”。你可以提三个方向:用 MTCNN 替代 Haar 提升人脸框质量,用 Focal Loss 处理类别不平衡,加入 SE 注意力模块让模型更关注眼睛和嘴。每一项都能讲出原理,说明你思考过如何继续深入。
5. 大作业避坑指南:情绪分析最容易翻车的五个地方
5.1 现象:人脸检测框偏移导致情绪全错
我见过不少同学提交的代码,训练时用的数据是“自己从原图中心裁一块”,推理时却用 Haar 检测框去裁,两个框的位置不一致,导致分类器看到的内容完全不同。比如原图人脸居右,中心裁剪裁到了头发和背景,情绪自然全错。
原因:检测框只是定位到人脸,但没有经过和训练数据一致的裁剪逻辑。解决方法是把“检测 → 裁剪 → resize”写成同一个函数,训练和推理都调用。裁剪时还要在检测框基础上外扩 10% 左右,保证额头和下巴不被切掉。检测框的 scaleFactor=1.1、minNeighbors=5 对 640x480 摄像头图像比较合适,框太松就调小 minNeighbors 到 3。
5.2 现象:loss 一直下降,测试集准确率却上不去
这个现象几乎每个训练深度学习的人都会遇到。训练 loss 降到 0.2,看起来模型已经“背下”了训练集,但验证准确率只有 60%。如果再看混淆矩阵,几乎所有类别都偏向 happy 和 neutral,这就是典型的过拟合加上类别不平衡。
原因:模型容量大、训练样本少、情绪类别间相似度高,三者叠加导致泛化差。解决思路分三层:一是增加数据增强,用水平翻转、随机裁剪、亮度和对比度抖动;二是给 CrossEntropyLoss 加类别权重,让少数类错误产生更高损失;三是加早停,验证准确率连续 5 个 epoch 不提升就停止,把最优权重保留下来。记住,不要等到 30 个 epoch 全跑完才去看验证曲线,那就晚了。
5.3 现象:别人拿到你的压缩包,怎么都跑不起来
这是文档说明没做到位,也是大作业最常见的“软性丢分”。你的代码在自己的电脑上能跑,是因为虚拟环境里装满了历史遗留的库,换台机器就容易缺这缺那。最容易翻车的是这几个地方:代码里写了绝对路径;数据集目录名带中文;Python 版本用了 3.11,但对方的是 3.7。
解决:代码里所有路径都用相对路径,从项目根目录出发;README 里写明 Python 版本和安装命令;requirements.txt 只列核心依赖。还要避免使用cv2.imread读中文路径图片,它遇到中文路径会静默失败返回 None,导致训练数据直接少一半。我习惯在 dataset.py 里加一句assert img is not None,没有图片时立刻报错,而不是等训练崩到一半才排查。
5.4 现象:准确率卡在 65% 左右,怎么调都上不去
如果你已经尝试增大 epoch、调学习率,准确率仍停在 65%,大概率不是参数问题,而是数据集本身和任务定义的问题。FER2013 的不少图像是低分辨率、有遮挡、甚至标注错误的,要在这类数据上做到 80%,普通 ResNet18 微调很难。
解决路径是调整任务,不是盲目堆模型。最常见的是把 7 类合并成 6 类,比如把 disgust 并入 sad,因为这两个类别在数据集中数量少且特征重叠。改标签后准确率往往会涨 2 到 4 个点,但注意报告里一定要写“做了标签合并实验”,而不是把数据偷偷丢掉。另一个思路是修改模型结构,比如在全连接前加一个 SE block,让网络更关注眼睛和嘴巴区域。
5.5 现象:报告里的图表和代码结果对不上
最后这个坑很隐蔽,但一旦被发现就是诚信问题。有同学训练时输出了 70% 的准确率,写报告时又心血来潮改了模型,没有重新跑推理,直接把旧准确率填进新实验的表格里。或者两次运行没有固定随机种子,模型权重不同,准确率有自然波动,报告里 A 组和 B 组的差异无法复现。
解决:在 train.py 开头固定随机种子,保证每次运行结果可复现。我常用的函数是:
import random import numpy as np import torch def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed)调用后再开始训练,所有随机增强、初始化、数据打乱都变成确定性的。报告里的每一张图表都对应一个训练日志文件,评审抽查时你能快速指出这个数字来自哪一行 log。这个习惯让我在多次课设答辩里省掉了“这个结果怎么来的”这种尴尬追问。
6. 进阶玩法:把大作业做成能演示的实时情绪分析工具
如果你的时间允许,在交付“源代码+文档说明+实验报告”之外,再加一个摄像头实时演示,答辩时的加分效果非常直接。老师看到你从模型训练到工程部署都有了解,更容易相信你不只是照着网上代码跑了一遍。
实时演示的核心是把训练好的模型加载进来,配合 OpenCV 的摄像头读取。一个最小实现长这样:
import cv2 import torch cap = cv2.VideoCapture(0) face_cascade = cv2.CascadeClassifier( cv2.data.haarcascades + "haarcascade_frontalface_default.xml" ) model.eval() while True: ret, frame = cap.read() gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5) for (x, y, w, h) in faces: face = gray[y:y+h, x:x+w] face = cv2.resize(face, (48, 48)) tensor = torch.from_numpy(face).float() / 255.0 tensor = (tensor - 0.5) / 0.5 tensor = tensor.view(1, 1, 48, 48) 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, CLASSES[pred], (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imshow("emotion demo", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()注意这里我手动做了一次float() / 255.0再(tensor - 0.5) / 0.5,是为了和训练时的Normalize(mean=[0.5], std=[0.5])保持一致。如果你在训练时用了 ImageNet 的均值方差,这里的归一化也要改成对应数值。摄像头画面的光线会一直变,我自己的习惯是先对灰度图做一次直方图均衡化cv2.equalizeHist(gray),让模型在面对高噪点和暗光时更稳定。这个技巧不算在作业要求里,但写进答辩 PPT 的“优化探索”会显得你很在意真实场景的性能。
实时演示还有一个隐含限制:Haar Cascade 检测器对模糊和侧脸非常敏感,演示时离摄像头 1 米左右、正对镜头效果最好。如果现场翻车也不用慌,直接说“这是检测器局限,不是分类器问题”,然后用提前准备好的测试集图片演示分类效果,老师通常能理解。希望这个方向能帮你在人工智能导论大作业里交出一份能讲得清楚、也经得起复现的作品。
本文还有配套的精品资源,点击获取