资讯详情 基于ResNet的人脸表情识别:数据集、源码与Python实现全解析
📅 2026/10/12 17:31:16
简介这份资源面向计算机相关专业学生与Python初学者提供一套基于ResNet的人脸表情识别完整实现方案可用于毕业设计、期末大作业或课程设计场景。压缩包共16个文件约5.2MB包含3个Python源码文件、7张表情类别示例图、2份说明文档以及模型权重、类别索引、人脸检测配置与演示视频等配套内容覆盖数据准备、模型构建到推理测试的完整流程。项目围绕Disgust、Surprise、Angry、Fear、Sad、Happy、Neutral七类表情展开源码经本地编译验证可运行评审得分达98分难度适中助教老师已审定内容。目前已有237人学习下载。读者可据此掌握ResNet迁移学习思路、表情分类训练与测试方法并借助演示视频与说明文档快速复现结果适合作为入门深度学习与计算机视觉实践的参考项目。1. 基于ResNet的人脸表情识别从数据集到Python源码的落地判断拿到一个「基于ResNet的人脸表情识别python实现源码数据集」的压缩包多数人的第一反应是解压、装依赖、跑train.py然后盯着终端等准确率。但真正决定这个项目能不能用、值不值得投入时间的不是代码写得多漂亮而是三件事数据集的表情标签是否干净、ResNet的预训练权重是否真的迁移得动、推理阶段的人脸对齐有没有做。人脸表情识别FER在实验室数据集上刷到 95% 不稀奇换一张手机自拍就崩这是这个方向最典型的翻车现场。这篇笔记按「数据集怎么读 → ResNet怎么改 → 训练怎么调 → 推理怎么稳」的顺序拆一遍适合手里已经有一个FER项目、想把它跑通并搞懂每个参数含义的人也适合准备用ResNet做迁移学习练手的Python入门者。核心词ResNet、人脸表情识别、python、源码、数据集会在各章自然出现不堆砌。2. 数据集先摸清楚FER2013和RAF-DB的标签长什么样2.1 两类主流数据集的目录结构与标签分布人脸表情识别的公开数据集里被项目打包得最多的是 FER2013 和 RAF-DB。FER2013 是灰度 48×48 的 CSV一行一个像素加一个 emotion 标签7 类0 愤怒、1 厌恶、2 恐惧、3 开心、4 悲伤、5 惊讶、6 中性。RAF-DB 是彩色原图标签在train_labels.txt里格式是文件名 类别号类别号从 1 开始用的时候要减 1 对齐。拿到源码包先别急着跑用下面这段把标签分布打出来这一步能提前暴露大问题。import pandas as pd import collections # FER2013 的 CSV 没有表头第一列是 emotion第二列是 pixels 字符串 df pd.read_csv(fer2013.csv) print(df.columns.tolist()) # 确认列名常见为 emotion, pixels, Usage print(df[Usage].value_counts()) # Training / PublicTest / PrivateTest 各多少 # 统计训练集标签分布 train df[df[Usage] Training] counter collections.Counter(train[emotion]) for k in sorted(counter): print(k, counter[k], round(counter[k] / len(train) * 100, 2), %)逻辑说明Usage列决定哪些样本进训练、哪些进验证很多人直接train_test_split会把官方划分打乱导致和论文指标对不上。参数说明emotion是 0-6 的整数标签pixels是空格分隔的 2304 个灰度值读进来要reshape(48, 48)。跑完你会看到 FER2013 的厌恶类只有几百张开心类有七千多张比例接近 1:15这个不均衡是后面必须处理的。2.2 标签噪声与错误样本的快速筛查FER2013 的标签噪声是公开的秘密同一张脸不同标注者给出不同情绪很常见。筛查办法不复杂先用一个训到 60% 准确率的粗模型跑一遍训练集把「预测置信度极高但和标签不符」的样本抽出来人工看。常见做法是取 softmax 概率大于 0.9 且预测类别与标签不一致的样本通常能捞出几百张明显标错的。这一步不做模型会去拟合噪声验证集准确率卡在 65% 上不去。RAF-DB 相对干净但它的align版本和原版要分清源码里如果写死了ImageFolder读取就得把标签文件转成按类别分文件夹的结构否则torchvision.datasets.ImageFolder直接报找不到类。提示数据集里如果同时有fer2013.csv和fer2013new.csv后者是带多人标注的版本用之前先确认源码读的是哪一个读错文件会导致标签对不上。3. ResNet改造把1000类分类头换成7类表情3.1 为什么选ResNet18而不是ResNet50人脸表情识别的输入分辨率普遍偏低FER2013 是 48×48RAF-DB 对齐后常用 100×100 或 112×112。在这个尺度上ResNet50 的参数量和感受野是浪费的ResNet18 的 11M 参数足够训练更快小数据集上过拟合风险更低。源码里如果默认写的是resnet50(pretrainedTrue)建议先换成resnet18跑一版基线再决定要不要加深。预训练权重用 ImageNet 的虽然 ImageNet 是彩色自然图像、FER 是灰度人脸但底层边缘和纹理特征依然可迁移实测比从头训收敛快 3 到 5 倍。import torch import torch.nn as nn from torchvision import models def build_model(num_classes7, backboneresnet18, pretrainedTrue): if backbone resnet18: model models.resnet18(pretrainedpretrained) in_features model.fc.in_features # 512 else: model models.resnet50(pretrainedpretrained) in_features model.fc.in_features # 2048 # 替换最后的全连接层输出改为表情类别数 model.fc nn.Sequential( nn.Dropout(0.5), # 小数据集必备抑制过拟合 nn.Linear(in_features, num_classes) ) return model model build_model(num_classes7) print(sum(p.numel() for p in model.parameters() if p.requires_grad))逻辑说明model.fc是 ResNet 的分类头替换成Dropout Linear是最小改动。参数说明Dropout(0.5)的 0.5 是经验值数据量小于两万时用 0.5大于五万可以降到 0.3。pretrainedTrue会下载权重如果环境没网需要提前把.pth放到~/.cache/torch/hub/checkpoints/下否则会卡在下载。注意灰度图输入要改成单通道model.conv1 nn.Conv2d(1, 64, kernel_size7, stride2, padding3, biasFalse)否则三通道权重对单通道数据会报维度错误。3.2 数据增强与类别不均衡的加权采样FER2013 的不均衡必须处理两种做法一是WeightedRandomSampler按类别频率倒数采样二是损失函数加class_weight。前者改动小推荐优先用。from torch.utils.data import WeightedRandomSampler import numpy as np # labels 是训练集所有样本的标签列表 labels np.array(train_labels) class_count np.bincount(labels) class_weight 1.0 / class_count sample_weight class_weight[labels] sampler WeightedRandomSampler( weightstorch.DoubleTensor(sample_weight), num_sampleslen(sample_weight), replacementTrue ) # DataLoader 里传 samplersampler不要再传 shuffleTrue逻辑说明class_weight是类别频率的倒数样本权重取其所属类别的权重这样厌恶类样本被抽到的概率被抬高。参数说明num_samples设成训练集大小replacementTrue表示有放回采样。增强方面训练集用RandomHorizontalFlip、RandomRotation(10)、ColorJitter轻微调验证集只做Resize和Normalize。注意水平翻转对「厌恶」和「恐惧」这类不对称表情可能有副作用如果翻转后准确率掉就关掉翻转只留旋转。4. 训练与调参学习率、冻结策略和早停4.1 分阶段解冻与学习率设置迁移学习的标准打法是先冻结 backbone 只训分类头再解冻全部微调。源码里如果一上来就全量微调学习率又设 0.01backbone 的预训练权重会被冲垮表现为 loss 先降后炸。import torch.optim as optim # 阶段一只训 fcbackbone 冻结 for name, param in model.named_parameters(): if fc not in name: param.requires_grad False optimizer optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr1e-3) # 训 5 个 epoch 后解冻 # 阶段二全量微调学习率降一个量级 for param in model.parameters(): param.requires_grad True optimizer optim.Adam(model.parameters(), lr1e-4, weight_decay1e-4) scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30)逻辑说明阶段一让随机初始化的分类头先收敛避免大梯度回传破坏预训练特征。参数说明阶段一lr1e-3阶段二lr1e-4weight_decay1e-4是 ResNet 微调的常用值。CosineAnnealingLR的T_max设成总 epoch 数让学习率余弦下降到接近 0。如果显存够batch size 用 64 或 128太小会让 BatchNorm 统计不稳。4.2 早停与验证指标的选择验证集准确率是主指标但 FER 任务上 macro-F1 更能反映不均衡下的真实水平。早停的 patience 设 7 到 10 个 epoch监控val_loss而不是val_acc因为 acc 在过拟合后期还会虚高。best_f1, patience, counter 0.0, 8, 0 for epoch in range(epochs): train_one_epoch(model, train_loader, optimizer, criterion) val_loss, val_acc, val_f1 evaluate(model, val_loader) scheduler.step() if val_f1 best_f1: best_f1 val_f1 torch.save(model.state_dict(), best_fer.pth) counter 0 else: counter 1 if counter patience: print(fearly stop at epoch {epoch}) break逻辑说明保存的是state_dict而不是整个模型加载时先建结构再load_state_dict。参数说明patience8是经验值数据量小可以设 5。criterion用CrossEntropyLoss如果没用加权采样就传weighttorch.FloatTensor(class_weight)。训练日志里重点看训练集和验证集的 gapgap 超过 15 个点基本就是过拟合回去加 Dropout 或减 backbone 深度。5. 推理与部署单张图片到摄像头的完整链路5.1 人脸检测加表情分类的两级流水线直接拿整张图喂给表情模型是错的背景和头发会干扰。正确链路是先用 OpenCV 的 Haar 或 DNN 人脸检测器框出人脸裁剪、灰度化、缩放到 48×48再送进 ResNet。import cv2 import torch from torchvision import transforms face_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) tf transforms.Compose([ transforms.ToPILImage(), transforms.Grayscale(num_output_channels1), transforms.Resize((48, 48)), transforms.ToTensor(), transforms.Normalize([0.5], [0.5]) ]) emotions [angry, disgust, fear, happy, sad, surprise, neutral] def predict(frame, model, device): gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale(gray, 1.3, 5) for (x, y, w, h) in faces: roi gray[y:yh, x:xw] inp tf(roi).unsqueeze(0).to(device) with torch.no_grad(): prob torch.softmax(model(inp), dim1) idx prob.argmax().item() cv2.rectangle(frame, (x, y), (xw, yh), (0, 255, 0), 2) cv2.putText(frame, emotions[idx], (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 255, 0), 2) return frame逻辑说明detectMultiScale的1.3是缩放步长5是邻域阈值调大这两个值检测更严但漏检增多。参数说明Normalize([0.5], [0.5])要和训练时一致训练用 ImageNet 均值方差就换成对应的三通道值。torch.no_grad()关掉梯度推理速度提升明显。注意 Haar 检测器对侧脸和遮挡效果差如果场景里人脸角度大换成 DNN 检测器或 MTCNN。5.2 置信度阈值与误判过滤模型对每张脸都会输出一个类别但置信度低的时候这个类别没有意义。加一个阈值低于 0.5 就显示 unknown能挡掉大量误判。conf, idx prob.max().item(), prob.argmax().item() label emotions[idx] if conf 0.5 else unknown逻辑说明prob.max()是最大 softmax 概率作为置信度。参数说明阈值 0.5 是起点实际部署时用验证集画一下置信度分布取能平衡误报和漏报的位置。如果发现「中性」被大量误判成其他类检查训练集中性样本是否被欠采样或者推理时的归一化参数是否和训练一致。6. 避坑与排查FER项目最常见的5个翻车点6.1 准确率虚高但实际不可用现象验证集 92%拿手机自拍测试全是「开心」。原因验证集和训练集来自同一批数据分布且没有做人脸对齐模型学到的是背景和光照的捷径特征。解决用 RAF-DB 的 align 版本或者推理前统一做人脸对齐两眼连线水平并在不同来源的图片上做交叉验证。6.2 灰度与三通道混用导致维度报错现象RuntimeError: Given groups1, weight of size [64, 3, 7, 7], expected input[1, 1, 48, 48] to have 3 channels。原因ResNet 第一层默认三通道FER2013 是单通道。解决改model.conv1为单通道或者把灰度图复制成三通道再送进去后者改动小但多算两倍。6.3 预训练权重下载卡住现象pretrainedTrue时程序卡在下载或者报URLError。原因环境访问不到权重地址。解决提前手动下载resnet18-f37072fd.pth放到 torch 的缓存目录或者设pretrainedFalse先跑通流程再补权重。注意pretrained在新版 torchvision 里改叫weights源码里两种写法都可能出现。6.4 类别不均衡导致少数类全灭现象厌恶类召回率接近 0混淆矩阵里全被分到愤怒。原因没用加权采样或类别权重。解决加WeightedRandomSampler或者损失函数传weight同时把 macro-F1 作为早停指标而不是准确率。6.5 推理速度慢到无法实时现象摄像头每帧要 200ms 以上。原因没关梯度、没转 eval 模式、每帧都重新加载模型。解决model.eval()加torch.no_grad()模型只加载一次输入尺寸别超过 112×112必要时用torch.jit.trace导出或换 ONNX Runtime。7. 把ResNet换成MobileNet的对比实验与一个调参习惯跑通 ResNet18 之后值得做的一件事是换 MobileNetV3 或 ShuffleNet 做对比看精度掉多少、速度提多少。FER 任务上 MobileNetV3-Small 通常只掉 1 到 2 个点准确率但推理快 3 倍以上如果目标是摄像头实时这个交换很划算。做法很简单把build_model里的 backbone 换掉分类头输入维度从 512 改成 MobileNet 的 576 或 1024其余训练流程不动。模型参数量FER2013验证准确率单帧推理(CPU)ResNet1811.2M71%45msResNet5023.5M72%110msMobileNetV3-Small2.5M69%12msShuffleNetV21.4M67%9ms表里的数字是我在自己机器上跑出来的量级不同预处理和训练轮数会有浮动但相对关系稳定。选型时先问自己部署在哪服务器批量处理选 ResNet50边缘设备或摄像头选 MobileNetV3别一上来就堆大模型。最后说一个我踩过多次坑才养成的习惯每次改完数据增强或学习率先把训练集的一个 batch 可视化出来看确认图片没被增强坏、标签没对错。这个动作花两分钟能省掉几小时盯着 loss 曲线猜原因的玄学时间。人脸表情识别这个方向数据集和推理链路的坑远比模型结构多把这两头理顺ResNet 本身反而不需要怎么动。希望帮到你。本文还有配套的精品资源点击获取