尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

人脸表情识别消融实验源码解析:VGG16、ResNet50、InceptionV3与CBAM、SE、ECA注意力机制对比

人脸表情识别消融实验源码解析:VGG16、ResNet50、InceptionV3与CBAM、SE、ECA注意力机制对比 简介这份资源面向计算机视觉方向的学生与算法入门者提供一套基于多模型消融实验的人脸表情识别完整源码用于对比主流卷积网络与注意力机制组合在FER2013、RAF数据集上的表现。项目以PyTorch 1.8为框架将VGG16、ResNet50、InceptionV3及自定义CNN分别与CBAM、SE、ECA三种注意力模块结合通过消融实验验证ResNet50CBAM精度最高、拟合效果最好适合作为课程设计、毕业设计或论文复现的参考方案。压缩包共18个文件约244KB包含7个Python脚本、2个Markdown说明文档、3张结果图片及若干占位文件代码按dataloader数据加载、dataset数据集、logs训练日志、result测试结果等模块组织结构清晰便于二次修改。目前已有443人学习读者可借此掌握多模型对比实验的设计思路、注意力模块嵌入方式与训练评估流程。1. 从一份能跑消融实验的人脸表情识别源码说起人脸表情识别这个方向很多人卡在同一个地方模型能跑通但说不清到底哪个模块在起作用。换掉主干网络、加上注意力、调完超参精度涨了两个点是注意力真的有用还是随机种子抽得好这份face-recognize-master源码解决的正是这个问题——它把 VGG16、ResNet50、InceptionV3 和自定义 CNN 放在同一套训练流程里再分别嵌入 CBAM、SE、ECA 三种注意力模块在 FER2013 和 RAF 数据集上做完整的消融对比。作者最后给出的结论是 ResNet50 CBAM 精度最高、拟合效果最好但比结论更有价值的是这套可复现的实验框架。它适合正在做表情识别课程设计、想复现注意力消融实验或者需要一套干净的多模型对比脚手架的人。下面我按「资源结构 → 数据准备 → 模型与注意力实现 → 训练与结果 → 避坑 → 进阶」的顺序拆一遍能直接抄的地方我都标了参数。2. 源码结构与数据加载先搞清楚每个目录在干什么拿到一个压缩包我习惯先看目录树再动手装环境。这份源码的结构不复杂但有几个目录如果理解错了后面训练会直接报错或者读到空数据。2.1 目录职责与文件清单从项目正文给出的结构看核心目录和文件如下路径作用备注dataloader/数据加载逻辑自定义 Dataset 和 DataLoader 封装dataset/存放数据集FER2013、RAF 放这里models/模型定义resnet.py、CNN_face.py、inceptionv3.py、attention.py、vggnet.pylogs/训练日志训练时自动写入result/测试结果保存评估输出train.py训练入口主脚本rewrite_dataset.py数据集重写格式转换用images/示例图片文档配图models/目录是这份源码的核心。resnet.py、inceptionv3.py、vggnet.py分别对应三种主干网络CNN_face.py是自定义卷积网络attention.py里集中放了 CBAM、SE、ECA 三种注意力模块。这种把注意力单独抽出来的写法很实用——你不需要改主干网络代码只要在构建模型时决定插不插、插哪个。dataloader/和dataset/要配合看。dataset/只负责放原始数据dataloader/负责把数据读成张量。很多人第一次跑会把数据直接丢在根目录结果train.py找不到路径这是最常见的翻车点之一。2.2 数据准备FER2013 和 RAF 的目录约定FER2013 原始格式是 CSV每行一个像素序列加标签RAF 通常是按类别分文件夹的图片。这份源码用rewrite_dataset.py做格式统一常见做法是把它转成ImageFolder能识别的结构# 转换后的目录结构应该是这样 dataset/ ├── train/ │ ├── angry/ │ ├── disgust/ │ ├── fear/ │ ├── happy/ │ ├── neutral/ │ ├── sad/ │ └── surprise/ └── test/ ├── angry/ └── ...rewrite_dataset.py的作用就是把 FER2013 的 CSV 按标签拆成上面这种按类分文件夹的图片。运行前先确认两件事一是 CSV 的列顺序通常是 emotion, pixels, usage二是输出路径要和dataloader里写的路径一致。我一般会先跑一遍转换然后ls dataset/train/happy | wc -l数一下每类图片数量确认没有某一类为空。# dataloader 里典型的数据加载写法示意 from torchvision import datasets, transforms from torch.utils.data import DataLoader train_transform transforms.Compose([ transforms.Grayscale(num_output_channels3), # FER2013 是灰度图主干网络要 3 通道 transforms.Resize((224, 224)), # ResNet50/VGG16 默认输入尺寸 transforms.RandomHorizontalFlip(), # 表情左右翻转增强 transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) # ImageNet 统计量 ]) train_set datasets.ImageFolder(dataset/train, transformtrain_transform) train_loader DataLoader(train_set, batch_size64, shuffleTrue, num_workers4)这里有几个参数值得说清楚。Grayscale(num_output_channels3)是因为 FER2013 是单通道灰度图而 ResNet50、VGG16 这些在 ImageNet 上预训练的模型第一层卷积期望 3 通道输入不转就会在 forward 时报通道数不匹配。Resize((224, 224))是这几个主干网络的标准输入尺寸InceptionV3 虽然支持 299但统一到 224 也能跑只是精度略有差异。Normalize用的是 ImageNet 的均值和方差因为主干网络是预训练权重输入分布要尽量对齐。batch_size64在 8G 显存上跑 ResNet50 基本够用显存小就降到 32 或 16。提示num_workers在 Windows 上设成大于 0 有时会卡住遇到卡死先改成 0 试试。3. 模型与注意力机制主干网络怎么选、注意力怎么插这一章是整份源码最值得细看的部分。消融实验的价值全在这里——你得清楚每个模型为什么这么写注意力模块插在哪个位置以及不同组合之间的差异到底来自哪里。3.1 三种主干网络的选型理由models/下放了 ResNet50、InceptionV3、VGG16 和自定义 CNN这不是随便凑数而是覆盖了卷积网络演进的几个典型思路。VGG16 结构最规整全是 3x3 卷积堆叠特征提取能力强但参数量大约 1.38 亿训练慢容易过拟合小数据集。它在消融实验里通常作为「经典基线」出现。ResNet50 引入残差连接解决了深层网络退化问题参数量约 2500 万比 VGG16 轻在 FER2013 这种 3 万多张图的数据集上表现稳定。作者最终结论是 ResNet50 CBAM 最好这和 ResNet 的残差结构能更好承载注意力模块有关——注意力加在残差分支上梯度回传更顺。InceptionV3 用多尺度卷积核并行参数量约 2700 万对多尺度表情特征比如眼睛和嘴巴的尺度差异有天然优势但结构复杂改动注意力模块时插入点不如 ResNet 直观。自定义 CNNCNN_face.py通常是几层卷积加池化的轻量结构用来验证「不加预训练、纯自建网络能到什么水平」是消融实验里的下界参照。# models/resnet.py 里典型的构建方式示意 import torchvision.models as models import torch.nn as nn def get_resnet50(num_classes7, pretrainedTrue): model models.resnet50(pretrainedpretrained) # 替换最后的全连接层适配 7 类表情 model.fc nn.Linear(model.fc.in_features, num_classes) return modelnum_classes7对应 FER2013 的七类表情angry、disgust、fear、happy、neutral、sad、surprise。pretrainedTrue表示加载 ImageNet 预训练权重这是小数据集上能否收敛的关键——从零训练 FER2013 很容易卡在 40% 左右的精度上不去。替换model.fc是因为原模型输出 1000 类要改成 7 类。3.2 CBAM、SE、ECA 三种注意力的插入位置attention.py里集中实现了三种注意力它们的核心区别在于「关注什么维度」。SESqueeze-and-Excitation只做通道注意力先全局平均池化把每个通道压成一个数再过两层全连接算出通道权重最后乘回原特征。它回答的是「哪些通道更重要」。CBAMConvolutional Block Attention Module是通道注意力加空间注意力先做通道注意力类似 SE 但用最大池化和平均池化双路再做空间注意力在通道维度上做池化和卷积算出每个位置的重要性。它同时回答「哪些通道重要」和「哪些位置重要」。ECAEfficient Channel Attention是 SE 的轻量改进用一维卷积代替全连接避免降维带来的信息损失参数量极小。# attention.py 里 CBAM 的典型结构示意 import torch import torch.nn as nn class ChannelAttention(nn.Module): def __init__(self, in_channels, ratio16): super().__init__() self.avg_pool nn.AdaptiveAvgPool2d(1) self.max_pool nn.AdaptiveMaxPool2d(1) self.fc nn.Sequential( nn.Linear(in_channels, in_channels // ratio), nn.ReLU(), nn.Linear(in_channels // ratio, in_channels) ) self.sigmoid nn.Sigmoid() def forward(self, x): avg_out self.fc(self.avg_pool(x).view(x.size(0), -1)) max_out self.fc(self.max_pool(x).view(x.size(0), -1)) out self.sigmoid(avg_out max_out).view(x.size(0), -1, 1, 1) return x * outratio16是 SE 和 CBAM 里通道降维的默认比例通道数除以 16 再升回来。这个值不是随便定的——太小比如 2降维不够、参数量大太大比如 32会丢信息。在 ResNet50 上不同 stage 的通道数不同256、512、1024、2048ratio16基本都能用。AdaptiveAvgPool2d(1)和AdaptiveMaxPool2d(1)把任意尺寸特征图压成 1x1所以注意力模块对输入尺寸不敏感。插入位置通常是在残差块的卷积之后、残差相加之前。以 ResNet50 的 Bottleneck 为例注意力加在第三个卷积输出上再和 shortcut 相加。这个位置能让注意力作用于「即将进入下一层的特征」同时不影响残差主路径的梯度。# 在 ResNet Bottleneck 中插入 CBAM示意 class BottleneckWithCBAM(nn.Module): def __init__(self, ...): super().__init__() # ... 原有卷积层定义 ... self.cbam CBAM(out_channels) # 在残差相加前插入 def forward(self, x): identity x out self.conv1(x) out self.bn1(out) out self.relu(out) out self.conv2(out) out self.bn2(out) out self.relu(out) out self.conv3(out) out self.bn3(out) out self.cbam(out) # 注意力在这里生效 out identity # 再和 shortcut 相加 out self.relu(out) return out3.3 消融实验的组合矩阵消融实验的本质是控制变量。这份源码的组合逻辑是固定数据集和训练超参只变「主干网络」和「注意力类型」两个维度。完整的组合矩阵大致是主干网络无注意力SECBAMECAVGG16✓✓✓✓ResNet50✓✓✓✓InceptionV3✓✓✓✓自定义 CNN✓✓✓✓一共 16 组实验。每组要记录训练精度、验证精度、测试精度和训练时长。作者结论是 ResNet50 CBAM 最优但你在复现时可能会得到略有差异的结果——这取决于随机种子、数据划分方式和训练轮数。我一般会固定torch.manual_seed(42)和numpy.random.seed(42)让结果尽量可复现。# train.py 里固定随机种子的常见写法 import torch import numpy as np import random def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark Falsecudnn.deterministicTrue会让 cuDNN 只用确定性算法代价是速度略慢但换来了可复现性。benchmarkFalse同理关掉自动调优。如果你更在意速度、不在意完全复现可以把这两个都设成相反值。4. 训练、日志与结果把实验跑起来并读懂输出模型和数据都理清之后真正跑训练反而简单。这一章说清楚train.py的关键参数、日志怎么看、结果怎么对比。4.1 训练脚本的关键参数train.py是入口通常包含数据集选择、模型选择、注意力选择、学习率、轮数这些参数。常见做法是用argparse暴露出来方便批量跑消融# 跑一组 ResNet50 CBAM 的典型命令 python train.py \ --dataset fer2013 \ --model resnet50 \ --attention cbam \ --epochs 100 \ --batch_size 64 \ --lr 0.001 \ --seed 42--dataset决定读dataset/下哪份数据--model和--attention决定模型组合--epochs 100是常见设置——FER2013 上 50 轮往往还没收敛100 轮基本能看到平台期。--lr 0.001配合 Adam 优化器是稳妥起点如果 loss 震荡明显就降到 0.0001。--seed 42保证可复现。训练循环里要关注的是 loss 和 accuracy 的走势。常见做法是每个 epoch 结束后在验证集上评估保存验证精度最高的权重# 训练循环核心逻辑示意 best_acc 0.0 for epoch in range(args.epochs): model.train() for imgs, labels in train_loader: imgs, labels imgs.cuda(), labels.cuda() optimizer.zero_grad() outputs model(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() # 验证 model.eval() correct, total 0, 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.cuda(), labels.cuda() outputs model(imgs) _, predicted outputs.max(1) total labels.size(0) correct predicted.eq(labels).sum().item() acc correct / total if acc best_acc: best_acc acc torch.save(model.state_dict(), fresult/best_{args.model}_{args.attention}.pth)optimizer.zero_grad()必须在loss.backward()之前否则梯度会累加。model.eval()和torch.no_grad()在验证时都要加前者关掉 dropout 和 batchnorm 的训练行为后者省显存。保存文件名带上模型和注意力类型方便后面批量对比。4.2 日志与结果对比logs/目录保存训练日志result/保存测试结果。日志里通常每个 epoch 一行记录 train_loss、train_acc、val_loss、val_acc。我一般会写个小脚本把 16 组实验的日志汇总成一张表# 汇总多组实验结果的常见做法 import os import re results [] for fname in os.listdir(logs): if not fname.endswith(.log): continue model, attn re.match(r(.?)_(.?)\.log, fname).groups() with open(flogs/{fname}) as f: lines f.readlines() # 取最后一个 epoch 的验证精度 last lines[-1] val_acc float(re.search(rval_acc[:\s]([\d.]), last).group(1)) results.append((model, attn, val_acc)) results.sort(keylambda x: -x[2]) for model, attn, acc in results: print(f{model:15s} {attn:8s} {acc:.4f})这样跑完 16 组按验证精度排序就能直观看到哪组最好。作者的结论 ResNet50 CBAM 最高你在复现时如果发现某组异常低先检查那组的日志有没有报错或者提前中断。注意不同主干网络的训练时长差异很大。VGG16 一轮可能是 ResNet50 的两三倍跑 16 组实验前先估算总时间别跑到一半发现要几十个小时。5. 避坑与排查这几处最容易翻车消融实验的坑往往不在模型本身而在数据、环境和实验管理上。下面几条是我踩过或者见别人踩过的按「现象 → 原因 → 解决」写。现象训练 loss 一直是 nan。原因通常是学习率太大或者输入没有归一化。FER2013 像素值 0-255如果忘了ToTensor()和Normalize直接送进网络梯度会爆炸。 解决确认 transform 里有ToTensor()把像素缩到 0-1和Normalize学习率从 0.001 起还不行就降到 0.0001。现象验证精度远高于训练精度。原因一般是数据泄漏——训练集和验证集有重叠或者验证集太小。FER2013 官方有 train/val/test 划分如果自己随机切分要确保按样本切而不是按类别切。 解决用官方划分或者用train_test_split时加stratifylabels保证类别比例一致。现象加了注意力之后精度反而下降。原因可能是注意力模块插入位置不对或者在小数据集上过拟合。CBAM 参数量比 SE 大在 FER2013 这种 3 万张的数据集上如果训练轮数不够注意力还没学好就过拟合了。 解决先确认插入位置在残差相加之前再试试减小注意力模块的学习率或者增加数据增强随机裁剪、颜色抖动。现象换到 InceptionV3 时报输入尺寸错误。原因InceptionV3 默认输入是 299x299而 dataloader 里写的是 224x224。 解决要么把 Resize 改成 299要么在构建 InceptionV3 时设aux_logitsFalse并确认输入尺寸。统一用 224 也能跑但精度可能略低。现象多组实验跑完结果无法复现。原因没有固定随机种子或者cudnn.benchmarkTrue导致每次选的卷积算法不同。 解决按 3.3 里的set_seed固定所有随机源并把cudnn.deterministic设为 True。代价是速度慢一点但消融实验要的就是可比性。6. 进阶把消融实验做成可复用的对比框架跑通单组实验只是开始这份源码真正的价值在于它能撑起一套可复用的消融框架。我一般会做三件事把实验配置外置成 YAML、把结果自动汇总成表格、把最优模型导出做推理验证。配置外置是为了批量跑。与其每次改命令行参数不如写一个configs/目录每组实验一个 YAML# configs/resnet50_cbam.yaml dataset: fer2013 model: resnet50 attention: cbam epochs: 100 batch_size: 64 lr: 0.001 seed: 42然后写个脚本遍历所有 YAML依次调train.py。这样 16 组实验可以一条命令跑完中间不用守着。结果汇总我习惯用 pandas 直接读日志生成对比表按「主干网络 × 注意力」的矩阵排列一眼就能看出哪个组合在哪个数据集上最好。如果要做更严格的对比可以每组跑 3 个不同种子取平均减少随机性带来的误判。最后是推理验证。训练完保存的best_*.pth要能加载回来对新图片做预测这一步能暴露很多训练时看不出的问题——比如预处理不一致、类别顺序错位。我一般会拿images/里的示例图跑一遍# 加载最优模型做单张推理 import torch from PIL import Image from torchvision import transforms model.load_state_dict(torch.load(result/best_resnet50_cbam.pth)) model.eval() transform transforms.Compose([ transforms.Grayscale(num_output_channels3), transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) img Image.open(images/test_face.png) img transform(img).unsqueeze(0).cuda() with torch.no_grad(): output model(img) pred output.argmax(1).item() print(f预测类别: {pred})unsqueeze(0)是加 batch 维度因为模型期望输入是[N, C, H, W]。预处理必须和训练时完全一致差一个 Normalize 的均值预测结果就可能全错。这一步我每次都会强制走一遍因为它是训练和部署之间最后一道校验。从那以后我每次做完消融实验都会先拿几张已知标签的图跑推理确认模型不是「训练精度好看但实际乱猜」再去看对比表。希望帮到你。本文还有配套的精品资源点击获取
返回列表