☰
从零复现ResNet人脸表情识别:期末大作业值不值得做
2026/10/1 20:18:35 网站建设 项目流程

简介:这份资源是面向高校学生与Python初学者的ResNet人脸表情识别完整项目包,可直接用于期末大作业、课程设计或深度学习入门实践,解决从零搭建图像分类项目时缺少可运行代码与配套数据的问题。压缩包共16个文件,约5.2MB,包含3个Python脚本分别负责模型定义、训练测试与混淆矩阵绘制,7张表情类别示例图覆盖生气、厌恶、恐惧、开心、悲伤、惊讶与中性,另有json类别索引、xml人脸检测器、md说明文档、txt依赖清单及mp4演示视频,结构清晰便于按模块查阅。目前已有190人学习下载。项目代码含注释,新手也能看懂,下载后简单部署即可运行,既能直接作为高分作业提交,也可借助混淆矩阵与演示视频理解ResNet迁移学习流程、数据组织方式与模型评估思路,具备较高的实际参考价值。

1. 从零复现 ResNet 人脸表情识别:这个期末大作业到底值不值得做

如果你正在搜「python大作业-ResNet的人脸表情识别项目源码+数据集」,大概率是期末周临近,手里还差一个能拿得出手的深度学习项目。人脸表情识别(Facial Expression Recognition,FER)恰好卡在一个很舒服的位置:数据集公开、任务定义清晰、ResNet 又是最经典的骨干网络之一,跑通不难,想拿高分也有足够的调优空间。它解决的核心问题是——给一张人脸图,判断它属于愤怒、厌恶、恐惧、开心、悲伤、惊讶还是中性这七类中的哪一类。适合谁做?有 Python 基础、装过 PyTorch、能看懂卷积网络基本结构的学生,以及想补一个完整 CV 落地经验的转行者。这个方向不需要你从零发明算法,但能逼你把数据清洗、迁移学习、类别不平衡、过拟合这些真实工程问题走一遍,这才是它比「手写数字识别」值钱的地方。

2. 数据集选型与 ResNet 迁移学习:为什么不能直接从头训

2.1 FER2013 与 RAF-DB 的取舍

做这个项目,数据集基本绕不开两个:FER2013 和 RAF-DB。FER2013 是 Kaggle 上的老牌数据集,约 3.5 万张 48×48 灰度图,七类标注,优点是获取方便、教程多,缺点是分辨率低、标注噪声大,人类准确率也就 65% 左右。RAF-DB 质量高得多,真实场景彩色图,但需要申请下载,且标注体系是基础表情加复合表情,处理起来更麻烦。

我一般建议期末作业用 FER2013 起步,因为它的坑足够典型,你能在报告里写出「数据清洗」的实打实内容。如果你时间充裕、想要更高上限,可以 FER2013 打底、RAF-DB 做验证。下面这张表是我实际对比后的结论:

维度FER2013RAF-DB
图像尺寸48×48 灰度100×100 彩色
样本量约 35887约 15339
获取难度低,CSV 直接下需邮件申请
标注质量一般,有错标较高
适合场景快速跑通、写报告冲高准确率

选 FER2013 时,第一步不是建模,而是把 CSV 里的像素字符串还原成图像并做可视化抽查。很多「玄学」掉点,根源就是数据里混了错标样本。

2.2 为什么用 ResNet 预训练权重而不是从头训

FER2013 只有三万多张图,从头训一个 ResNet18,验证集准确率大概率卡在 55% 上下,而且震荡严重。原因是参数量相对数据量太大,模型记不住泛化特征,只会背训练集。迁移学习就是解药:用 ImageNet 上预训练的 ResNet,卷积层已经学会了边缘、纹理、局部形状这些通用特征,你只需要替换最后的全连接层,微调高层即可。

这里有个关键改动:ResNet 预训练输入是 224×224 三通道,而 FER2013 是 48×48 单通道。常见做法是把灰度图复制成三通道,并上采样到 224,或者改第一层卷积的输入通道数。前者简单但计算量大,后者省算力但要小心预训练权重对第一层的适配。我一般期末作业用前者,稳。

import torch import torch.nn as nn from torchvision import models, transforms # 加载预训练 ResNet18,替换分类头为 7 类 def build_model(num_classes=7, pretrained=True): model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1 if pretrained else None) # 替换最后的全连接层 in_features = model.fc.in_features model.fc = nn.Linear(in_features, num_classes) return model # 数据预处理:灰度转三通道 + 归一化到 ImageNet 统计量 train_tf = transforms.Compose([ transforms.Grayscale(num_output_channels=3), # 单通道复制成三通道 transforms.Resize((224, 224)), # 对齐预训练输入尺寸 transforms.RandomHorizontalFlip(), # 表情左右翻转仍成立 transforms.RandomRotation(10), # 小角度旋转增强 transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) # ImageNet 统计量 ])

这段代码里,Grayscale(num_output_channels=3)是把单通道灰度复制成三通道,保证和预训练权重兼容;Resize((224,224))是硬性对齐,因为 ResNet 的下采样倍率决定了输入不能太小;Normalize用的均值方差必须和预训练时一致,否则第一层激活分布偏移,微调会变慢。RandomHorizontalFlip对表情识别是安全的,因为「开心」翻转后还是「开心」,但要注意别用垂直翻转,脸倒过来语义就变了。

2.3 分层学习率:别让预训练权重被冲垮

微调时如果所有层用同一个学习率,卷积层学到的通用特征会被快速破坏,这叫灾难性遗忘。稳妥做法是分层设学习率:主干网络用小学习率(比如 1e-4),新加的全连接层用大学习率(比如 1e-3)。

model = build_model(num_classes=7) # 主干参数用小学习率,分类头用大学习率 backbone_params = [p for n, p in model.named_parameters() if not n.startswith('fc')] head_params = [p for n, p in model.named_parameters() if n.startswith('fc')] optimizer = torch.optim.Adam([ {'params': backbone_params, 'lr': 1e-4}, {'params': head_params, 'lr': 1e-3} ], weight_decay=1e-4) # 余弦退火,让学习率平滑下降 scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=30)

weight_decay=1e-4是轻量正则,防止全连接层过拟合;CosineAnnealingLR的T_max设成总 epoch 数,让学习率从初始值平滑降到接近零。这套组合在 FER2013 上,30 个 epoch 能把验证准确率推到 66% 到 68%,比从头训高十几个点。

3. 训练流程与类别不平衡处理:把 66% 再往上抬

3.1 类别不平衡是 FER2013 的隐形天花板

FER2013 七类分布极不均匀,「开心」有一万张左右,「厌恶」只有几百张,差了近二十倍。如果直接用交叉熵,模型会偏向多数类,少数类几乎全预测错。你看到整体准确率还行,但混淆矩阵里「厌恶」那一行惨不忍睹,这就是典型的类别不平衡翻车。

处理手段有三种:重采样、类别加权损失、Focal Loss。期末作业我推荐类别加权,改动最小、效果立竿见影。

import numpy as np from sklearn.utils.class_weight import compute_class_weight # 假设 train_labels 是训练集标签列表 class_weights = compute_class_weight( class_weight='balanced', classes=np.unique(train_labels), y=train_labels ) class_weights = torch.tensor(class_weights, dtype=torch.float32).to(device) # 带权重的交叉熵 criterion = nn.CrossEntropyLoss(weight=class_weights)

compute_class_weight的balanced模式会按「总样本数 / (类别数 × 该类样本数)」自动算权重,样本越少权重越大。这样少数类的损失被放大,梯度更新时不会被多数类淹没。注意权重张量要放到和模型同一个 device 上,否则会报设备不匹配的错。

3.2 完整训练循环与验证指标

光看准确率不够,期末报告里最好给出混淆矩阵和每类的 F1。下面是一个可复用的训练循环骨架:

def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss, correct, total = 0, 0, 0 for imgs, labels in loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(imgs) loss = criterion(outputs, labels) loss.backward() optimizer.step() total_loss += loss.item() * imgs.size(0) preds = outputs.argmax(dim=1) correct += (preds == labels).sum().item() total += imgs.size(0) return total_loss / total, correct / total @torch.no_grad() def evaluate(model, loader, device): model.eval() all_preds, all_labels = [], [] for imgs, labels in loader: imgs = imgs.to(device) outputs = model(imgs) preds = outputs.argmax(dim=1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) return np.array(all_labels), np.array(all_preds)

model.train()和model.eval()的切换不能省,因为 BatchNorm 和 Dropout 在两种模式下行为不同,忘了切会导致验证结果虚高或虚低。torch.no_grad()在验证时关闭梯度,省显存也提速。拿到all_labels和all_preds后,用sklearn.metrics.confusion_matrix和classification_report出报告,这两个图放进论文里很加分。

3.3 数据增强的边界:哪些增强会帮倒忙

增强不是越多越好。表情识别里,水平翻转、小角度旋转(±10°)、随机裁剪是安全的;但颜色抖动要慎用,因为 FER2013 是灰度图,颜色增强没意义;垂直翻转、大角度旋转会破坏面部语义,反而降点。我踩过的坑是加了RandomAffine大范围仿射,验证准确率掉了三个点,因为脸被拉变形后和真实分布差太远。

# 推荐的增强组合,克制但有效 safe_aug = transforms.Compose([ transforms.Grayscale(num_output_channels=3), transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p=0.5), transforms.RandomRotation(degrees=10), transforms.RandomCrop(224, padding=8), # 轻微平移 transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])

RandomCrop(224, padding=8)是先 pad 8 像素再裁回 224,相当于轻微平移,模拟人脸位置偏移,这个增强性价比很高。p=0.5的翻转概率是经验值,太高会让模型见到过多镜像样本,泛化反而受限。

4. 避坑与排查:那些让我熬夜的报错和掉点

4.1 现象:训练 loss 不降,准确率卡在 25%

原因:学习率设太大,或者归一化统计量用错。我见过有人把Normalize的 mean/std 写成 0.5/0.5,结果第一层激活爆炸,loss 直接 NaN。解决:确认 mean/std 是 ImageNet 的[0.485,0.456,0.406]和[0.229,0.224,0.225],学习率先用 1e-4 试,loss 还不降就检查数据标签有没有和图像错位。

4.2 现象:验证准确率远高于训练准确率

原因:验证集太小,或者验证时忘了model.eval(),Dropout 还在随机丢神经元,导致预测不稳定。解决:确认验证集至少占 10%,并在评估函数开头加model.eval()。如果验证集是从训练集里随机切的,注意别让同一张图同时出现在两边。

4.3 现象:显存不够,batch size 只能设 8

原因:ResNet 输入 224×224 比原始 48×48 大二十多倍,显存吃紧。解决:把输入降到 96×96 或 112×112,改第一层卷积的 stride,或者用梯度累积模拟大 batch。期末作业如果显卡只有 4G,建议直接用 112 输入,准确率损失不到一个点。

4.4 现象:少数类永远预测不对

原因:类别不平衡没处理,或者增强对少数类不够。解决:加类别权重,另外可以对少数类做额外过采样。注意过采样别简单复制,容易过拟合,用带增强的过采样更稳。

4.5 现象:测试集准确率比验证集低很多

原因:验证集和测试集分布不一致,或者模型过拟合了验证集(反复调参导致)。解决:固定一个测试集只在最后跑一次,调参只看验证集。如果差距超过五个点,检查测试集有没有混入训练数据,这种数据泄漏是期末作业里最隐蔽的翻车点。

5. 把准确率再抬三个点:TTA 与模型集成的实操技巧

到这一步,你的单模型应该在 66% 到 68%。想冲 70% 以上,最划算的两个技巧是测试时增强(TTA)和轻量集成。TTA 是在推理时对同一张图做多种变换,把预测概率平均,相当于免费涨点。

@torch.no_grad() def predict_with_tta(model, img_tensor, device): model.eval() # 原图 + 水平翻转,两种视角 views = [img_tensor, torch.flip(img_tensor, dims=[3])] probs = [] for v in views: v = v.to(device) out = model(v) probs.append(torch.softmax(out, dim=1)) return torch.stack(probs).mean(dim=0) # 概率平均

torch.flip(img_tensor, dims=[3])是沿宽度维翻转,对应水平镜像。概率平均比投票更平滑,因为保留了置信度信息。实测在 FER2013 上,双视角 TTA 能稳定涨 0.8 到 1.2 个点,几乎零成本。

集成则是训两三个不同骨干(比如 ResNet18 加 ResNet34),推理时把 softmax 概率相加再取 argmax。代价是训练时间翻倍,但期末作业如果时间够,集成能把准确率推到 70% 附近,报告里也更有说服力。我的习惯是:先把单模型和 TTA 做扎实,再决定要不要上集成,别一上来就堆模型,调参都调不过来。

最后说个血泪教训:别在最后一天才跑完整训练。ResNet 微调一轮三十分钟起步,调参要跑好几轮,留足两天缓冲。数据集和源码只是起点,真正拉开分数的是你有没有把混淆矩阵分析清楚、有没有解释清楚每个参数为什么这么设。希望帮到你。

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

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

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

立即咨询