简介:本资源是一份面向高校人工智能课程学习者的完整大作业实践包,聚焦图像情绪识别这一典型AI应用场景,适用于计算机、人工智能、自动化等专业学生完成课程设计、期末项目或毕设初期开发。压缩包共25个文件,含11个Python源码(覆盖CNN/VGG/ResNet多模型实现、数据预处理、可视化及对比分析)、6个Markdown文档(含详细使用说明、模型测试指南与目录结构解析)、3个辅助工具压缩包(人脸标注、Fer2013数据集PyTorch适配、表情识别开源项目)、2个PDF实验报告(开题与终期报告)、1个Haar级联XML人脸检测模型、1个MP4演示视频及1个DOC进展报告,整体22.54MB。已有251人学习下载,所有代码均经实机测试运行成功,答辩平均分96分;提供清晰的README引导、模块化代码结构与多模型横向对比脚本(model_All_Compare.py),便于理解算法差异、快速复现结果并在此基础上拓展功能。
1. 这不是“调个API就能跑”的情绪识别demo:它是一套可答辩、可复现、可拆解的完整课程级图像情绪分析流水线
你手头那套「人脸识别+表情分类」代码,大概率只在Jupyter里跑通过单张图——而这份2021年广工大《人工智能导论》第二组课程设计,是真正走完「数据采集→人脸裁剪→模型训练→对比实验→可视化分析→报告撰写」全链路的硬核作业。它不依赖云端API,所有模型(CNN/VGG/ResNet)全部用PyTorch从零实现;不靠预训练权重糊弄,Fer2013数据集手动划分train/val/test三份;连开题报告、进展报告、期末报告PDF都齐备,答辩平均分96分不是虚的。适合两类人:一是刚学完《机器学习》但卡在“怎么把课上公式变成能跑的代码”的本科生,二是需要快速搭建教学案例、避免学生抄网上的残缺项目模板的授课老师。它解决的不是“能不能识别”,而是“怎么让一个没跑过GPU训练的新手,在3小时内复现baseline并理解每个模块为什么这么写”。
提示:这不是工业级部署方案,没有Flask接口、没有Docker封装、不支持实时视频流推理。它的价值恰恰在于“裸露”——所有数据路径、超参、模型结构、评估逻辑全部摊开,连haarcascade_frontalface_default.xml这种OpenCV老古董级检测器都保留原样,方便你对照课本讲义逐行debug。
2. 从Fer2013原始数据到可训练数据集:四步数据工程实操
2.1 数据来源与原始结构解析
项目打包了Fer2013-pytorch-CNN-VGG-Resnet--master.zip,解压后核心是fer2013.csv——这个CSV文件包含35887行,每行格式为emotion,pixels,Usage,其中pixels字段是2304个空格分隔的整数(48×48灰度图展平),emotion为0~6共7类(anger/disgust/fear/happy/sad/surprise/neutral)。注意:原始Fer2013官方数据集的Usage列标记为Training/PublicTest/PrivateTest,但本项目未直接使用该划分,而是通过data_separation.py重采样生成自己的train/val/test集,这是关键差异点。
2.2 手动构建图像目录树:为什么不用Pandas直接读CSV?
data_separation.py脚本执行后,会在face_images/下生成标准ImageFolder结构:
face_images/ ├── train/ │ ├── 0/ # anger │ ├── 1/ # disgust │ └── ... ├── val/ └── test/这样做有三个硬性理由:
- PyTorch DataLoader兼容性:避免每次训练都用
pd.read_csv()加载整个CSV再reshape,内存占用直降80%; - 数据增强可插拔:
transforms.Compose([transforms.RandomHorizontalFlip(), transforms.ToTensor()])只能作用于ImageFolder对象; - 跨模型复用:CNN/VGG/ResNet三个模型的
dataset参数完全一致,无需为每个模型单独写数据加载逻辑。
# data_separation.py 关键片段(已加注释) import pandas as pd import numpy as np from PIL import Image import os # 1. 读取原始CSV,过滤掉Usage为'PrivateTest'的样本(因标签不可见,仅用于最终评测) df = pd.read_csv('fer2013.csv') df = df[df['Usage'] != 'PrivateTest'] # 2. 按emotion分组,确保每类至少有500张图才参与训练(防小类过拟合) min_samples_per_class = 500 class_counts = df['emotion'].value_counts() valid_classes = class_counts[class_counts >= min_samples_per_class].index.tolist() df = df[df['emotion'].isin(valid_classes)] # 3. 手动划分:按7:1:2比例切分,且保证每类在各集合中比例均衡 for emotion in range(7): class_df = df[df['emotion'] == emotion] n_total = len(class_df) n_train = int(n_total * 0.7) n_val = int(n_total * 0.1) # 随机打乱后切片(非简单切片!) shuffled = class_df.sample(frac=1, random_state=42).reset_index(drop=True) train_part = shuffled.iloc[:n_train] val_part = shuffled.iloc[n_train:n_train+n_val] test_part = shuffled.iloc[n_train+n_val:] # 4. 写入对应目录(核心:像素转PIL.Image再保存为PNG) for idx, row in train_part.iterrows(): pixels = np.array(row['pixels'].split(), dtype=np.uint8).reshape(48, 48) img = Image.fromarray(pixels) img.save(f"face_images/train/{row['emotion']}/{idx}.png")2.3 人脸区域裁剪:为什么用haarcascade而不直接用MTCNN?
model_CNN.py中FaceDetector类调用cv2.CascadeClassifier('haarcascade_frontalface_default.xml'),而非更准的深度学习检测器,原因很务实:
- 课程约束:《人工智能导论》课时有限,要求“用OpenCV基础模块完成预处理”,MTCNN需额外装
torchvision且推理慢; - 数据适配性:Fer2013全是正脸灰度图,haar检测器召回率>92%,误检主要在
disgust类(嘴角扭曲易被误判为闭眼),但后续data_view.py提供了可视化校验工具; - 可解释性教学:让学生亲手调
scaleFactor=1.1和minNeighbors=5参数,理解检测器敏感度与漏检率的权衡。
2.4 数据可视化验证:data_view.py的三个救命功能
运行python data_view.py --mode show_sample --class_id 3(happy类)会弹出9宫格样本图,确认裁剪质量;--mode show_distribution生成各类别数量柱状图,避免训练集某类样本过少;--mode show_heatmap对test集预测结果画混淆矩阵热力图——这是答辩时评委最爱看的一页图。
注意:
data_view.py依赖matplotlib和seaborn,若报错ModuleNotFoundError: No module named 'seaborn',请先pip install seaborn==0.12.2(高版本seaborn在Python3.7下与旧版PyTorch有兼容问题)。
3. 三大模型源码级对比:CNN/VGG/ResNet在Fer2013上的真实性能边界
3.1 基础CNN模型:model_CNN.py的三层卷积设计哲学
class SimpleCNN(nn.Module): def __init__(self, num_classes=7): super().__init__() self.conv1 = nn.Conv2d(1, 32, kernel_size=3, padding=1) # 输入通道1(灰度图) self.bn1 = nn.BatchNorm2d(32) self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1) self.bn2 = nn.BatchNorm2d(64) self.conv3 = nn.Conv2d(64, 128, kernel_size=3, padding=1) self.bn3 = nn.BatchNorm2d(128) self.pool = nn.MaxPool2d(2) self.dropout = nn.Dropout(0.25) self.fc1 = nn.Linear(128 * 6 * 6, 512) # 48->24->12->6,三次池化后尺寸 self.fc2 = nn.Linear(512, num_classes) def forward(self, x): x = self.pool(F.relu(self.bn1(self.conv1(x)))) x = self.pool(F.relu(self.bn2(self.conv2(x)))) x = self.pool(F.relu(self.bn3(self.conv3(x)))) x = torch.flatten(x, 1) x = F.relu(self.fc1(x)) x = self.dropout(x) x = self.fc2(x) return x关键设计点:
padding=1保证每次卷积后尺寸不变,配合MaxPool2d(2)精准控制下采样节奏;BatchNorm2d放在ReLU前(非后),这是2021年课程要求的“标准写法”,虽非最优但符合教学规范;fc1输入维度128*6*6来自48→24→12→6的三次2倍下采样,必须手算验证,否则torch.flatten报错。
3.2 VGG变体:model_VGG.py如何用11层网络榨干48×48小图
VGG原始结构(如VGG11)输入需224×224,本项目将其压缩为VGG_mini:
- 移除所有
nn.AdaptiveAvgPool2d,改用固定尺寸nn.AvgPool2d(2); - 将
features中最后两个Conv2d(512->512)改为Conv2d(256->256),降低参数量; classifier部分精简为[nn.Linear(256*3*3, 256), nn.ReLU(), nn.Dropout(0.5), nn.Linear(256, 7)]。
这样调整后,VGG_mini参数量约1.2M(原VGG11为132M),在GTX1050Ti上单batch训练时间仅比CNN多0.3秒,但top1准确率提升4.2%(见model_All_Compare.py输出表格)。
3.3 ResNet轻量化:model_ResNet.py的残差块魔改
标准ResNet18需输入≥112×112,本项目采用ResNet18_mini:
- 第一个
conv1将kernel_size从7改为3,stride从2改为1,避免48×48图被过度下采样; layer1(第一个残差块)输出通道从64减至32,layer2从128减至64;- 全局平均池化层
nn.AdaptiveAvgPool2d((1,1))保留,但fc层输入维度从512改为64。
这种改法使ResNet18_mini在Fer2013上达到最高准确率(68.3%),但训练不稳定——model_ResNet_test.py中设置了torch.backends.cudnn.benchmark = True加速,却导致首次epoch loss震荡剧烈,这是典型的小图+残差结构冲突现象。
3.4 模型对比实验:model_All_Compare.py的自动化评测框架
该脚本会自动执行:
- 加载三个模型权重(
model_CNN.pth/model_VGG.pth/model_ResNet.pth); - 在test集上分别计算accuracy/precision/recall/f1-score;
- 生成
results_comparison.csv,含每类详细指标; - 绘制ROC曲线(对7类做one-vs-rest)。
关键参数:--batch_size 64(显存不足时需降至32)、--num_workers 2(Windows系统必须设为0,否则DataLoader卡死)。
4. 训练全流程避坑指南:GPU环境、超参、收敛性三重雷区
4.1 GPU环境配置:CUDA版本与PyTorch的隐性绑定
项目requirements.txt指定torch==1.8.1+cu111,这意味着:
- 必须安装CUDA 11.1(非11.0或11.2),
nvcc --version必须输出Cuda compilation tools, release 11.1; - 若用RTX30系显卡(Ampere架构),
torch==1.8.1无法启用Tensor Cores,但强行升级到torch==1.10.0+cu113会导致model_VGG_test.py中nn.AvgPool2d计算错误(已验证); - 解决方案:保持CUDA 11.1 + PyTorch 1.8.1,用
model_CNN_GPU.py中的torch.cuda.amp.autocast()手动开启混合精度,提速18%且loss曲线更平滑。
4.2 超参数陷阱:学习率衰减与早停机制的致命组合
model_CNN.py中lr_scheduler.StepLR(optimizer, step_size=7, gamma=0.1)与EarlyStopping(patience=5)同时启用时:
- 现象:第12 epoch后val_loss突然飙升,但scheduler仍在第14 epoch才衰减lr,导致模型崩溃;
- 原因:
patience=5指连续5个epoch无改善即触发早停,但step_size=7意味着lr在第7/14/21...epoch衰减,二者周期错位; - 解决:统一为
patience=7,或改用ReduceLROnPlateau(见model_VGG.py第89行),监听val_loss,下降时才衰减lr。
4.3 收敛性玄学:batch_size=32时acc卡在62%不上升
- 现象:CNN模型在train集acc达95%但val集停滞在62%,验证集loss波动剧烈;
- 原因:Fer2013中
disgust类样本仅958张(占2.7%),batch_size=32时单个batch极可能不含该类样本,导致梯度更新偏差; - 解决:在
data_separation.py中启用WeightedRandomSampler,按类别频率反比赋予权重,代码已写在model_CNN_test.py第45行注释里,取消注释即可生效。
4.4 模型保存与加载:.pth文件的序列化隐患
model_ResNet.py中torch.save(model.state_dict(), 'model_ResNet.pth')看似标准,但:
- 现象:加载后
model.eval()时BatchNorm2d.running_mean为全零,导致推理结果全错; - 原因:训练时未调用
model.train(),BN层未更新统计量; - 解决:保存前必须执行
model.train(),或改用torch.save({'model_state_dict': model.state_dict(), 'optimizer_state_dict': optimizer.state_dict()}, 'checkpoint.pth')保存完整状态。
4.5 视频推理翻车:example_dsh.mp4无法实时处理
- 现象:
python video_inference.py --video example_dsh.mp4运行后CPU占用100%,帧率<1fps; - 原因:
cv2.VideoCapture默认用CAP_ANY后端,在Windows上常选错解码器; - 解决:强制指定后端
cv2.VideoCapture('example_dsh.mp4', cv2.CAP_FFMPEG),并添加cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)减少缓存延迟。
5. 实验报告写作实战:从代码输出到答辩PPT的转化技巧
5.1 图表生成:data_view.py与model_All_Compare.py的报告直出能力
运行python model_All_Compare.py --generate_report会自动生成:
confusion_matrix.png:7×7混淆矩阵,字体大小设为12确保答辩投影清晰;roc_curve.png:7条ROC曲线,AUC值标在图例中;training_history.png:三模型loss/acc对比曲线,用不同线型区分(CNN实线/VGG虚线/ResNet点划线)。
这些图直接复制进Word报告,不要截图——plt.savefig()已设置dpi=300和bbox_inches='tight',矢量图缩放不失真。
5.2 报告结构拆解:《人工智能导论》第二组报告的黄金模板
其《人工智能导论》第二组-期末课程项目报告.pdf采用五段式:
| 章节 | 核心内容 | 教学意图 |
|---|---|---|
| 1. 问题定义 | 用FER2013数据集统计表(含各类样本数/占比)说明任务难度,引用2013年原始论文证明数据权威性 | 展示问题意识,非简单复述题目 |
| 2. 方法论 | 对比表格列出CNN/VGG/ResNet的层数/参数量/GPU显存占用/单epoch耗时,强调“为何选这三种模型” | 体现技术选型能力,非堆砌代码 |
| 3. 实验结果 | 混淆矩阵热力图+TOP3错误案例(如把fear误判为sad的原始图+裁剪图+预测概率),附人工归因(“因眉毛下压特征相似”) | 展示分析深度,非只报准确率 |
| 4. 局限性 | 明确写出“对侧脸/遮挡/光照变化鲁棒性差”,并给出改进方向(“可引入GAN生成对抗样本增强”) | 体现批判性思维,非回避缺陷 |
| 5. 总结与致谢 | 致谢指导教师时具体到“感谢张老师在ResNet梯度爆炸问题上提供的L2正则化建议” | 体现过程真实性,非套话 |
5.3 答辩话术设计:如何把“调参失败”说成“主动探索”
当评委问“为什么VGG准确率比CNN高但训练时间长?”时,标准回答是:
“我们刻意将VGG的卷积核数量减半(从64→32),使其参数量与CNN接近(1.3M vs 1.1M),此时VGG仍高出4.2%准确率,证明更深的网络结构对小图特征提取有本质优势——这验证了课程中‘深度增加表达能力’的核心论点。”
血泪经验:答辩时绝不说“我试了很多次”,而要说“我们设计了三组对照实验:第一组固定lr=0.01,第二组用StepLR,第三组用ReduceLROnPlateau,最终选择第三组因其val_loss方差最小”。
5.4 开题报告埋点:如何让中期检查不尴尬
开题报告.pdf中“预期困难”章节写了三条:
- “Fer2013数据集存在标注噪声,计划用交叉验证评估标签可靠性” → 后续
data_view.py中--mode show_label_consistency功能即为此开发; - “ResNet在小图上易过拟合,拟采用DropPath正则化” → 实际用了更简单的
DropBlock(见model_ResNet.py第152行); - “视频实时推理延迟高,考虑用TensorRT优化” → 最终用FFmpeg硬解码解决,但报告里仍保留此条,体现规划前瞻性。
后悔药:从那以后我每次写开题报告,都会在“预期困难”里故意写一条“技术上可行但工作量极大”的点(如“尝试用Transformer替代CNN”),这样中期检查时只要展示出“已调研但暂不实施”的记录,评委就觉得你进度扎实。希望帮到你。
本文还有配套的精品资源,点击获取