ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ResNet人脸表情识别实战:从FER2013数据到模型调参与避坑

ResNet人脸表情识别实战:从FER2013数据到模型调参与避坑 简介基于ResNet的人脸表情识别Python期末大作业完整项目包面向深度学习初学者、K12及高校学生解决人脸图像预处理、残差网络搭建与表情分类的全流程落地问题。压缩包内共17个文件大小5.19MB包含3个Python源码文件、7张表情样例图、2份README说明、JSON类别索引、XML人脸检测器以及mp4演示视频结构上覆盖数据加载划分、ResNet模型定义、训练测试、混淆矩阵绘制等模块。项目基于公开人脸表情数据集涵盖快乐、悲伤、惊讶、恐惧、愤怒、厌恶、中性等类别有助于理解残差块、批量归一化、dropout和数据增强的实际应用。资源内还附有模型文件、依赖清单和演示视频可直接运行并复现实验流程也可作为期末答辩、课程设计或竞赛准备的参考方案。目前已有211人学习适合希望以完整项目快速提升深度学习实践能力的学习者。1. 期末大作业里的 ResNet 人脸表情识别一个压缩包要解决什么期末周最常见的画面就是老师丢下一句“用 Python 做一个基于 ResNet 的人脸表情识别”然后你拿到一个压缩包源码、数据集、模型文件全在里面。题目不大但卡住你的往往不是 ResNet 本身而是数据读不出来、环境装不上、模型加载报错。说白了这是一个七分类任务输入一张人脸图输出 angry、disgust、fear、happy、neutral、sad、surprise 中的一类。源码负责训练和预测数据集负责喂给模型模型文件负责让你不重新训练也能出结果。下面按“为什么选 ResNet → 怎么跑起来 → 参数怎么调 → 常见坑 → 答辩提升”的顺序展开。新手能跟着复现熟练工可以重点看参数边界和避坑部分。2. 为什么是 ResNet表情识别对网络结构的三个硬要求2.1 先认识任务七类表情与 FER2013 数据集大多数期末作业压缩包里放的公开数据集是 FER2013一共 35887 张 48×48 的灰度人脸图。它被分成三块Training、PublicTest 和 PrivateTest。PublicTest 对应你调试时的验证集PrivateTest 对应最终测试集。数据本身是一个 CSV每一行有 emotion、pixels、Usage 三列pixels 是一串用空格分隔的灰度值长度正好 2500。我拿到数据后第一件事不是训练而是先看每一类的数量分布import pandas as pd df pd.read_csv(data/fer2013.csv) print(df[Usage].value_counts()) print(df[emotion].value_counts())输出里你一定会看到类别不均衡。happy 可能占七千多张disgust 只有六百张左右。如果后面训练时不做任何处理模型会偏向样本多的类别disgust 几乎学不出来。这一点期末答辩时老师多半会问你得有对策。也有部分项目把数据整理成图片目录比如train/happy/00001.jpg。这样 dataset.py 会更简单直接按文件夹读标签即可。但无论哪种形式第一步都是确认标签顺序emotion 列是 0 到 6 的整数还是 1 到 7。很多翻车都从这里开始。2.2 ResNet 残差结构解决的是什么问题ResNet 的核心是残差模块让网络去学习一个残差 F(x)最终输出是 F(x) x。这个加号看起来简单却解决了深层网络在反向传播时梯度消失、训练误差反而上升的“退化”问题。即使把层数加到 101 层短接路径还能让梯度直接回传模型不会因为太深而训不动。表情识别任务里模型既要捕捉眉毛、嘴角这种局部纹理又要组合出整体的情绪语义。ResNet18 或 ResNet34 的深度对 48×48 的小图完全够用。更关键的是torchvision 里有现成的 ImageNet 预训练权重加载后微调能比随机初始化快不少。这个优势在期末这种两周交付的场景里非常实际。残差块里的 BatchNorm 也别忽略。训练和推理时 BN 的统计量不同如果后面忘记切model.eval()验证准确率会忽高忽低。这属于最常见的低级坑后面避坑部分会专门说。2.3 为什么不直接选 VGG 或 MobileNet期末答辩最怕被问“你用的网络放到别的主流网络上还成不成立”所以选型理由要提前想好。VGG 结构简单答辩好讲但参数巨大。VGG16 仅全连接层就超过一亿参数在 CPU 上训一个 epoch 能熬到半夜。ResNet18 参数只有 VGG16 的六分之一左右效果却不差。MobileNet 推理快但深度可分离卷积在 48×48 的小图上不一定有明显优势而且预训练权重在 torchvision 里的命名和加载方式对新手不够友好。我一般这样回答这个任务既要控制训练成本又要让模型有能力学深层次的纹理组合ResNet 的残差结构正好平衡了两点。别把“为什么选 ResNet”讲成“网上都说它好”要说清楚结构、成本、可复现性三方面。2.4 加载 resnet 预训练模型的边界写法不同版本的 torchvision 对预训练权重 API 有变化。老代码常写pretrainedTrue新版本推荐weightsResNet18_Weights.IMAGENET1K_V1。直接运行老代码可能报 TypeError。兼容写法如下import torchvision def build_model(num_classes7): try: model torchvision.models.resnet18(pretrainedTrue) except TypeError: model torchvision.models.resnet18( weightstorchvision.models.ResNet18_Weights.IMAGENET1K_V1 ) in_features model.fc.in_features model.fc torch.nn.Linear(in_features, num_classes) return model这段代码把最后一层全连接从原来的 1000 类换成 7 类。in_features在 ResNet18 里是 512不需要硬编码读属性最稳妥。如果你想把前面的层冻结、只训最后几层可以加一段for name, param in model.named_parameters(): if name.startswith(layer4.) or name.startswith(fc.): param.requires_grad True else: param.requires_grad False这个方法在你只有少量样本时很管用。但严格说“表情”这种语义并不像 ImageNet 类别差异那么大只训最后两层容易过拟合。我更建议完整微调只把学习率调低一点留给后面讲参数时讨论。3. 把压缩包跑起来环境、目录与最小复现命令3.1 固定 Python 与依赖先用 conda 建隔离环境期末项目的代码通常都不是最新 PyTorch 写的。你如果直接装 torch 2.5再跑一个三年前写的 train.py可能碰到pretrained参数报警、旧 API 被删、某个依赖版本冲突。我建议按下面固定一套环境conda create -n fer python3.8 -y conda activate fer pip install torch1.13.1 torchvision0.14.1 --index-url https://download.pytorch.org/whl/cu117 pip install opencv-python4.8.1.78 numpy pandas matplotlib tqdmpython3.8的原因很简单老代码里很多字符串处理、DataLoader 参数在 3.10 下也不算大问题但如果项目里用了pytorch-lightning或旧版albumentations3.8 的兼容性最稳。GPU 版本先用 cu117如果你机器没有显卡把--index-url换成https://download.pytorch.org/whl/cpu即可同样能把模型跑起来只是训练慢一些。装完后立刻检查一下 torch 能不能调用 GPUimport torch print(torch.__version__) print(torch.cuda.is_available())如果输出False后面训练不要加--device cuda否则直接崩溃。与其报错到处找原因不如一开始就确认算力环境。3.2 目录结构辨识源码、数据集、模型文件各放哪压缩包解压后我一般先不看代码先看目录结构fer_resnet/ ├─ data/ │ └─ fer2013.csv ├─ src/ │ ├─ dataset.py │ ├─ model.py │ ├─ train.py │ └─ predict.py ├─ checkpoints/ │ └─ resnet18_fer2013.pth └─ requirements.txtdata放数据集src放源码checkpoints放模型文件。注意有些项目把模型权重直接放在根目录下文件名类似best_model.pth。这不算错但你训练脚本里写死的路径和 README 对不上时第一件事就是看相对路径。我遇到的很多“模型加载失败”不是代码问题而是把data目录放在了错误的层级。训练时工作目录在项目根目录脚本里的./data/fer2013.csv就指根目录下的 data如果你在src目录里执行python train.py相对路径会跟着变。最简单的做法是统一在项目根目录执行命令别 cd 到 src 里面。3.3 最小复现步骤先用模型文件出结果再训练拿到一个项目不要一上来就训练。正确顺序是先拿现成模型文件做一次预测确认输入输出链路没问题再谈训练。最小命令长这样python predict.py --image face.jpg --checkpoint checkpoints/resnet18_fer2013.pth如果 predict.py 能输出类似happy: 0.92的结果说明 Python 环境、模型文件、图像读取全部通了。之后你才有信心去动训练参数python train.py --data data/fer2013.csv \ --checkpoint_dir checkpoints \ --arch resnet18 \ --epochs 30 \ --batch_size 64 \ --lr 1e-3--arch指定网络结构--epochs控制训练轮数--batch_size和--lr是最直接影响收敛的参数。如果项目不认这些参数名直接看train.py里的argparse部分它会有--helppython train.py --help这一步比你想的更有用。很多项目连参数名都不统一有的叫--model_name有的叫--backbone。与其猜不如让它自己打印出来。3.4 训练第一轮时重点看什么训练不是放着不管。第一个 epoch 结束时损失应该明显下降比如从 1.9 降到 1.4 左右如果损失完全不降或者直接 NaN就别等 30 个 epoch 了赶紧停。我会在 train.py 里加一行打印让每轮输出训练损失、验证准确率和当前学习率Epoch [1/30] Train Loss: 1.9234 Val Acc: 0.3421 Epoch [2/30] Train Loss: 1.5842 Val Acc: 0.4512 Epoch [3/30] Train Loss: 1.3781 Val Acc: 0.5178如果前三轮验证准确率一直在 40% 以下且不涨大概率不是参数问题而是数据预处理或标签映射出了问题直接看避坑章的第五条比继续等训练更有效。4. 训练参数这样调学习率、batch size 与 ResNet 微调策略4.1 学习率从 1e-3 还是 1e-4 开始ResNet 人脸表情识别的训练策略取决于你从什么状态开始。随机初始化、从头训练时用 1e-3 配合 SGD通常能在二十几个 epoch 内收敛加载 ImageNet 预训练模型后做全量微调我一般降到 1e-4避免大幅破坏已经学好的底层特征。FER2013 只有三万张左右训练图不是大数据集。学习率太高会让 loss 震荡太低则收敛太慢。我常用的优化器配置optimizer torch.optim.SGD( model.parameters(), lr0.001, momentum0.9, weight_decay1e-4, ) scheduler torch.optim.lr_scheduler.CosineAnnealingLR( optimizer, T_max30, eta_min1e-5, )T_max要跟你的总 epoch 数一致也就是 30。余弦退火会把学习率从 0.001 平滑降到 1e-5比固定学习率更容易稳定到最后的瓶颈。如果你没有预训练模型也可以先用 0.01 跑前面几个 epoch但新手我不建议这么激进。一个很容易被忽略的细节pretrained 模型里的 BatchNorm 参数在torch.optim.SGD下会正常更新不需要额外处理。如果你使用lr1e-4且冻结了大部分层那未冻结层的学习率建议保持 1e-3 左右否则最后几层学得太慢。更精细的做法是给不同层分组设置学习率期末作业里没必要答辩讲清楚你已经试过 1e-3 和 1e-4 就够。4.2 batch size 与图像尺寸显存不够时的降级方案很多项目里虽然数据是 48×48但代码会把图 resize 成 224×224因为 torchvision 的预训练权重是在 224×224 上训练的。这个操作正确但代价是显存占用变大。我通常按下面这张参数表起步输入尺寸ResNet18batch size显存要求48×48128约 2GB48×4864约 1.5GB224×22464约 4GB224×22432约 3GB如果你的显卡只有 4GB就用 224×224 batch 32如果只有 CPU建议用 48×48 并把 batch 降到 32。显存不够最常见的报错是CUDA out of memory解决办法不是硬调小 batch 到 4而是先用梯度累积。accumulation_steps 4 for i, (images, labels) in enumerate(train_loader): outputs model(images) loss criterion(outputs, labels) loss loss / accumulation_steps loss.backward() if (i 1) % accumulation_steps 0: optimizer.step() optimizer.zero_grad()这段代码模拟了“batch size 扩大四倍”的效果。原来 16 如果显存不够先降到 16再通过累计四步实现等效 64而每次反传只占 16 张图的显存。代价是训练时间略长但踩坑少。4.3 数据增强与类别不均衡处理FER2013 是灰度图很多代码为了适配 ImageNet 预训练模型会把灰度图转成三通道。此时 Normalize 要使用 ImageNet 的均值和方差from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(10), transforms.ColorJitter(brightness0.2, contrast0.2), transforms.ToTensor(), transforms.Normalize( mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225], ), ])这里ToTensor会把灰度图也转成三维张量通道数变成 1。如果你的 dataset 在读取时先用Image.open(...).convert(RGB)那么一张灰度图会复制成三通道正好对齐 ImageNet 统计量。省略convert(RGB)是很多人验证准确率上不去的隐形原因。类别不均衡的处理我推荐两种方式一起用。一是给损失函数加权重二是用WeightedRandomSampler做采样。最简单的是前一种class_counts df[emotion].value_counts().sort_index().values class_weights 1.0 / class_counts class_weights class_weights / class_weights.sum() criterion torch.nn.CrossEntropyLoss( weighttorch.tensor(class_weights, dtypetorch.float32) )这样 disgust 这种少数类在计算损失时权重更高模型不会因为样本少而完全无视它。注意class_counts要按标签 0 到 6 的顺序排别把value_counts默认的频率输出直接塞进去不然标签顺序对不上反而把模型带偏。5. 避坑指南五个让我熬夜的翻车现场5.1 训练 loss 下降验证准确率却一直卡在 40% 上下现象训练损失从 1.9 降到 1.1看起来正常但验证准确率始终只有 40% 左右甚至不如随机猜测太多。原因读取 FER2013 时pixels 是灰度值被直接ToTensor后变成[1, 48, 48]。而 ResNet 预训练模型期望输入是三通道。虽然张量能传输进模型并计算但第一层卷积学到的通道统计完全错位。另一个可能原因是Normalize用了全零的 mean 和 std或根本没做归一化。解决在数据增强前强制转成 RGB。用 PIL 读图时img.convert(RGB)会把灰度图复制成三层如果你用的是 numpy要写pixels np.array(img, dtypenp.float32).reshape(48, 48) rgb_img np.stack([pixels] * 3, axis-1)然后记得除以 255 再交给ToTensor。做完这一步验证准确率通常会一下子跳到接近 50%之后才谈得上提升。5.2 标签错位训练完 happy 被识别成 sad现象训练阶段准确率 70% 以上但拿自己的照片去预测结果总是错得离谱而且错误模式稳定。原因多半是标签映射不一致。FER2013 的 emotion 是从 0 到 6但有的代码在读取目录时文件夹名 angly、disgust 这种排序方式和数字标签对不上。比如sorted(os.listdir(train))按字母排顺序和你认为的 0-6 不一致标签就整体错位。解决先打印映射关系不要凭直觉label_map { 0: angry, 1: disgust, 2: fear, 3: happy, 4: neutral, 5: sad, 6: surprise, }如果是目录结构直接读每个文件夹的名字用字典生成标签而不是依赖目录顺序。训练前做一个最小验证取训练集第一张图打印它的标签和对应文件夹名确认一致后再开训。这一步只花两分钟能救回一整晚。5.3 验证时忘加 model.eval()准确率忽高忽低现象同一份训练好的权重每次跑验证集准确率都不同波动能有五六个百分点。原因模型里有 BatchNorm 和 Dropout。训练模式下 BN 使用当前 batch 的统计量Dropout 会随机丢弃神经元验证时如果不切换模式模型的输出就带随机性。更隐蔽的是用torch.no_grad()只关掉了梯度并不能关掉 Dropout。解决任何评估和预测都要这样写model.eval() with torch.no_grad(): outputs model(images)注意model.eval()之后如果又要进入训练记得切换回model.train()。很多项目把evaluate()写在训练循环里每轮验证结束直接进入下一轮训练忘记切回 train于是下一轮 loss 异常、准确率抽风。这个问题看似基础却是期末项目里出现频率最高的“玄学”问题。5.4 GPU 显存 OOM但降低 batch 到 8 还是爆现象显存 6GBResNet18 按说跑 batch 64 都没问题但一开训练就CUDA out of memory调成 8 也无济于事。原因数据加载或前向计算时张量被悄悄放到了 GPU 上并且没有及时释放。最常见的是损失函数返回后没有loss.item()拿标量而是保留了一个还会追踪梯度的张量或者验证集也反向传播了一次。另一个原因是 dataset 里把图片读成了 float64显存占用直接翻倍。解决先把 dtype 锁死。img np.asarray(pixels, dtypenp.float32) / 255.0再把验证部分的loss.backward()全部去掉。排查手法是在每个 batch 前打印torch.cuda.memory_allocated()看哪一步显存暴涨。如果确认是梯度累积导致的中间变量就按 4.2 的累积方案写别把 batch 压到极值。5.5 加载模型时报 size mismatch for fc.weight现象从压缩包给出的模型文件加载权重时torch.load报错提示全连接层权重大小不匹配。原因项目里的模型文件是训练好的七分类模型而你当前构建模型时先加载了 ImageNet 预训练权重最后一层 fc 还在 1000 类状态。严格来说源码里的 model.py 已经替换过 fc但你加载顺序错了先 load state_dict再替换 fc自然不匹配。解决先构建模型再替换 fc最后加载权重或者加载时忽略不匹配层state torch.load(checkpoints/resnet18_fer2013.pth, map_locationcpu) state.pop(fc.weight, None) state.pop(fc.bias, None) model.load_state_dict(state, strictFalse)把这两个 key 删掉后新初始化的 fc 层保留随机值前面卷积层的预训练权重照样加载。如果你手里只有最终模型文件、没有源码这个做法是最实用的后悔药。6. 从“能跑”到“能答辩”最后一天还能做的三个提升6.1 打印混淆矩阵和每类准确率期末答辩不能只给一个“总准确率 72%”就完事。老师更想看模型到底哪些类分清、哪些类混淆。用 sklearn 一行就能出矩阵from sklearn.metrics import confusion_matrix, classification_report print(classification_report(y_true, y_pred, target_nameslist(label_map.values()))) cm confusion_matrix(y_true, y_pred)打印出来的 per-class F1 会比总准确率有用得多。比如 disgust 的 F1 很低你可以顺着解释因为数据量最少且与 fear 在面部肌肉形态上相似。这个解释比空谈“数据不均衡”要有力。6.2 用 Grad-CAM 或简单遮挡实验说明模型在看什么如果时间不够做 Grad-CAM最简单的替代是遮挡实验把测试图的人脸眼部、嘴部区域分别涂黑再看预测变化。如果遮挡嘴部后 happy 变成 neutral说明模型确实在利用嘴部信息。下面是一个基础版 Grad-CAM 的 hook 思路conv_output None def hook(module, input, output): global conv_output conv_output output model.layer4[-1].register_forward_hook(hook)前向传播后拿到最后一层卷积特征图对类别得分求梯度再把特征图按梯度的全局平均做加权求和归一化后叠到原图上。答辩时放两张热力图老师会直观看到模型不是随机瞎猜。这一步的成本不高但技术完成度提升非常明显。6.3 给模型文件一个“会说话”的名字并做一次干净复现最后把最佳权重保存为resnet18_fer2013_acc72.3.pth命名里带上模型结构、数据集和准确率。这能帮你在答辩前五分钟快速找到正确权重避免现场加载错文件。最后一次验证一定要固定随机种子保证同一份代码每次跑出来的结果一致random.seed(0) np.random.seed(0) torch.manual_seed(0) torch.cuda.manual_seed_all(0) torch.backends.cudnn.deterministic True我踩过最痛的一次坑就是答辩前夜发现自己在验证时没切model.eval()导致一个 70% 的高分其实是随机波动。后来我学聪明了每次测完先记录torch.cuda.is_available()、模型文件名、batch size、学习率和准确率五个信息换机器复现时能直接对上。这套习惯比任何调参技巧都保命。希望帮到你。本文还有配套的精品资源点击获取
返回列表