简介EmotionVGGnet情绪识别Python源码包面向深度学习入门者与情感计算方向的研究者提供一套基于VGGNet卷积神经网络的完整情绪识别实践方案可用于语音、图像等多模态数据的分类任务。压缩包共11个文件、约12.38MB包含6个Python脚本、2个hdf5权重文件、1个json配置、1个png结果图与1个md说明覆盖数据预处理、模型构建、训练验证与结果可视化等环节。其中Python脚本承担数据集构建、识别器定义、训练与检测逻辑hdf5保存不同轮次的模型权重json与png则记录训练配置和可视化输出。已有309人学习该资源读者可借此理解VGGNet的卷积、池化与全连接层设计掌握Adam优化、批次与学习率设置、准确率与F1等评估指标的实现方式并参考案例45的代码注释快速复现实验、迁移到相似任务中。1. 情绪识别为什么总在“似笑非笑”上翻车做情绪识别的人大多经历过同一个尴尬模型在标准数据集上准确率能到九十多一换到真实场景遇到“似笑非笑”“皮笑肉不笑”这类表情就集体翻车。EmotionVGGnet 情绪识别这套方案本质是用 VGG 风格的卷积网络做七类或八类人脸表情分类配套一份 Python 源码让从业者能在本地把训练、推理、可视化整条链路跑通。它解决的不是“从零发明算法”而是把一套结构清晰、可改可调的基线交到你手里省掉搭骨架的时间。它适合三类人想入门计算机视觉的开发者、需要快速验证表情分类可行性的人、以及手里有自采人脸数据但缺一套干净训练脚本的团队。前提是你得接受一个现实——情绪识别从来不是“喂数据就出结果”的活标注质量、类别平衡、人脸对齐每一项都能让最终效果差出一大截。下面按“先立住原理、再动手复现、最后讲坑”的顺序拆开讲。2. EmotionVGGnet 的网络结构与选型理由2.1 为什么是 VGG 风格而不是直接上 ResNetVGG 的核心特征是“小卷积核堆叠 通道数翻倍 最大池化降维”结构规整到几乎可以背下来。EmotionVGGnet 沿用这个思路通常由若干组Conv-BN-ReLU加池化构成最后接全连接层输出类别数。选它做情绪识别基线理由很实在一是小卷积核3×3感受野叠加后能捕捉眼角、嘴角这些细粒度纹理而表情的关键信息恰恰藏在这些局部二是结构简单改层、加注意力、换损失函数都方便不像深层残差网络那样牵一发动全身。代价也要说清楚。VGG 风格参数量集中在全连接层显存占用偏高输入分辨率一上去就吃紧。所以常见做法是把输入压到 48×48 或 64×64配合全局平均池化替代大全连接能省下大量参数。如果你手里数据量只有几千张直接堆很深的 VGG 反而容易过拟合这时候减层、加 Dropout 比换更深的网络更管用。2.2 一个可复现的模型定义下面这段是典型的 EmotionVGGnet 结构用 PyTorch 写通道配置和池化位置都标了注释方便你按自己数据调整。import torch import torch.nn as nn class EmotionVGGnet(nn.Module): def __init__(self, num_classes7, in_channels1): super().__init__() # 特征提取4 组卷积通道 64-128-256-512 self.features nn.Sequential( nn.Conv2d(in_channels, 64, 3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.Conv2d(64, 64, 3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 48 - 24 nn.Conv2d(64, 128, 3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.Conv2d(128, 128, 3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 24 - 12 nn.Conv2d(128, 256, 3, padding1), nn.BatchNorm2d(256), nn.ReLU(inplaceTrue), nn.Conv2d(256, 256, 3, padding1), nn.BatchNorm2d(256), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 12 - 6 nn.Conv2d(256, 512, 3, padding1), nn.BatchNorm2d(512), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 6 - 3 ) # 用全局平均池化替代大全连接显著降参数量 self.gap nn.AdaptiveAvgPool2d(1) self.classifier nn.Sequential( nn.Dropout(0.5), nn.Linear(512, 256), nn.ReLU(inplaceTrue), nn.Dropout(0.3), nn.Linear(256, num_classes) ) def forward(self, x): x self.features(x) x self.gap(x).flatten(1) return self.classifier(x)逻辑说明features负责逐层提取从边缘到语义的特征每两次卷积后池化一次空间尺寸按 48→24→12→6→3 递减。AdaptiveAvgPool2d(1)把每个通道压成一个数避免展平后接超大全连接层。参数上num_classes按你的标签体系设七类表情就填 7in_channels灰度图填 1彩色图填 3。Dropout 比例 0.5 和 0.3 是经验值数据少就往上调数据多可以降到 0.2 左右。2.3 输入尺寸与通道数的取舍输入尺寸直接决定显存和速度。48×48 灰度是表情识别里最常见的折中既能保留五官结构又不至于让 batch 小到训练不稳。如果你用彩色图通道变 3第一层卷积参数量翻三倍但表情识别里颜色信息贡献有限很多方案干脆转灰度。这里有个容易忽略的点训练和推理的预处理必须完全一致归一化均值方差、是否转灰度、缩放插值方式任何一处不一致都会让线上效果莫名下降。3. 从零跑通训练与推理的最小闭环3.1 数据组织与 Dataset 写法EmotionVGGnet 这类源码通常约定按类别分文件夹存放图片这是最省心的组织方式。下面这个 Dataset 直接读文件夹名当标签避免手写标签映射出错。import os from PIL import Image from torch.utils.data import Dataset, DataLoader from torchvision import transforms class FaceDataset(Dataset): def __init__(self, root, size48, trainTrue): self.samples [] self.classes sorted(os.listdir(root)) # 文件夹名即类别 self.class_to_idx {c: i for i, c in enumerate(self.classes)} for c in self.classes: cdir os.path.join(root, c) for name in os.listdir(cdir): self.samples.append((os.path.join(cdir, name), self.class_to_idx[c])) # 训练加轻量增强验证只做确定性变换 if train: self.tf transforms.Compose([ transforms.Grayscale(1), transforms.Resize((size, size)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(10), transforms.ToTensor(), transforms.Normalize([0.5], [0.5]), ]) else: self.tf transforms.Compose([ transforms.Grayscale(1), transforms.Resize((size, size)), transforms.ToTensor(), transforms.Normalize([0.5], [0.5]), ]) def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label self.samples[idx] img Image.open(path).convert(RGB) return self.tf(img), label逻辑说明class_to_idx用排序后的文件夹名生成稳定映射保证训练和推理标签顺序一致。训练集用水平翻转和 ±10 度旋转做增强验证集坚决不加随机变换否则评估结果会虚高。归一化用 0.5 均值方差是灰度图的常见选择如果你换成 ImageNet 预训练权重就得改成对应的三通道均值方差。3.2 训练循环与关键超参训练脚本的核心是损失函数、优化器和学习率调度三件套。表情识别里类别不平衡很常见交叉熵配合类别权重能缓解“多数类通吃”。import torch from torch import nn, optim from torch.utils.data import DataLoader device torch.device(cuda if torch.cuda.is_available() else cpu) train_ds FaceDataset(data/train, trainTrue) val_ds FaceDataset(data/val, trainFalse) train_loader DataLoader(train_ds, batch_size64, shuffleTrue, num_workers4) val_loader DataLoader(val_ds, batch_size64, shuffleFalse, num_workers4) model EmotionVGGnet(num_classeslen(train_ds.classes)).to(device) # 类别权重按样本数反比计算缓解不平衡 counts [0] * len(train_ds.classes) for _, y in train_ds.samples: counts[y] 1 weights torch.tensor([sum(counts) / (len(counts) * c) for c in counts], dtypetorch.float).to(device) criterion nn.CrossEntropyLoss(weightweights) optimizer optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max50) for epoch in range(50): model.train() for x, y in train_loader: x, y x.to(device), y.to(device) optimizer.zero_grad() loss criterion(model(x), y) loss.backward() optimizer.step() scheduler.step() # 每个 epoch 后在验证集上评估 model.eval() correct total 0 with torch.no_grad(): for x, y in val_loader: x, y x.to(device), y.to(device) pred model(x).argmax(1) correct (pred y).sum().item() total y.size(0) print(fepoch {epoch}, val_acc {correct / total:.4f})逻辑说明weight_decay1e-4是 Adam 下常用的正则强度配合 Dropout 一起压过拟合。CosineAnnealingLR让学习率从 1e-3 平滑降到接近 0比固定学习率更容易收敛到好的点。batch_size 设 64 是显存和梯度稳定性的折中显存不够就降到 32同时把学习率按比例调小。验证准确率连续多个 epoch 不涨就该考虑早停别硬跑满。3.3 单张图片推理与结果输出训练完要能对单张图给出预测这一步最容易因为预处理不一致而翻车。from PIL import Image import torch def predict(image_path, model, class_names, size48): model.eval() tf transforms.Compose([ transforms.Grayscale(1), transforms.Resize((size, size)), transforms.ToTensor(), transforms.Normalize([0.5], [0.5]), ]) img Image.open(image_path).convert(RGB) x tf(img).unsqueeze(0).to(device) # 增加 batch 维度 with torch.no_grad(): prob torch.softmax(model(x), dim1)[0] idx prob.argmax().item() return class_names[idx], prob[idx].item() name, score predict(test.jpg, model, train_ds.classes) print(f预测: {name}, 置信度: {score:.3f})逻辑说明unsqueeze(0)把单张图的[C,H,W]变成[1,C,H,W]这是模型 forward 要求的维度。softmax把 logits 转成概率方便看置信度。注意这里的变换必须和验证集完全一致尤其是Grayscale和Normalize两步少一步结果就可能全错。置信度低于某个阈值比如 0.5时工程上更稳妥的做法是输出“不确定”而不是硬给一个类别。4. 训练不收敛、验证虚高、推理翻车的排查清单4.1 损失不下降先查标签和归一化现象训练几个 epoch 后 loss 几乎不动准确率停在随机水平。原因通常有两个——标签映射错乱或者归一化参数离谱。解决先打印一个 batch 的图片和标签肉眼确认图和标签对得上再检查Normalize的均值和方差是否和输入分布匹配灰度图用 0.5/0.5 一般没问题但如果你的图整体偏暗可以统计一下实际均值再设。4.2 验证准确率虚高多半是数据泄漏现象验证集准确率高得反常换一批真实图片就崩。原因常见于同一人的多张照片被分到了训练和验证两侧模型其实在“认人”而不是“认表情”。解决按人划分数据集同一个人只出现在训练或验证一侧如果数据里没有身份信息至少保证同一段视频抽的帧不跨集。4.3 推理结果和训练对不上检查预处理链路现象训练时验证准确率不错单张推理却总是错。原因几乎都是预处理不一致比如训练用了Grayscale推理忘了或者 resize 的插值方式不同。解决把训练验证用的 transform 抽成一个函数训练、验证、推理三处共用同一份从源头杜绝不一致。4.4 显存爆掉从 batch 和输入尺寸下手现象训练刚开始就 OOM。原因要么 batch 太大要么输入分辨率太高要么全连接层没换成全局平均池化。解决先把 batch 降到 16 试跑确认能跑通再往上加输入从 48 起步别一上来就 128确认模型里用的是AdaptiveAvgPool2d而不是直接view展平。4.5 过拟合严重增强和正则一起上现象训练准确率冲到 99%验证卡在 60% 不动。原因就是数据太少、模型容量相对过大。解决加大增强力度随机裁剪、亮度抖动提高 Dropout 比例加权重衰减必要时减掉一层卷积。别指望单靠一个手段解决通常是组合拳才有效。5. 把 EmotionVGGnet 用到自己数据上的三个进阶技巧第一个技巧是迁移学习。如果你只有几百到几千张图从零训 VGG 风格网络基本没戏。常见做法是拿在大型人脸数据上预训练过的权重做初始化把前面几层冻结只训后面几层和分类头学习率调小到 1e-4 量级。这样收敛快小数据上也能有像样的效果。注意预训练权重的输入通道和归一化参数要和你自己的预处理对齐否则迁移的优势会被抵消。第二个技巧是类别合并与阈值策略。真实场景里“厌恶”和“愤怒”经常混淆“惊讶”和“恐惧”也难分。与其硬分七类不如先合并成“积极/中性/消极”三类跑通再逐步细分。推理时对低置信度样本输出“不确定”比强行归类更符合工程实际。下面这个置信度过滤的写法可以直接用def predict_with_threshold(image_path, model, class_names, threshold0.5): name, score predict(image_path, model, class_names) if score threshold: return 不确定, score return name, score逻辑说明threshold是置信度门槛设太高会拒识大量正常样本设太低又失去过滤意义一般从 0.5 起步根据业务对误判的容忍度调整。这个策略在安防、交互类场景里比单纯追求分类准确率更实用。第三个技巧是验证方法要贴近真实分布。别只看整体准确率要画混淆矩阵看哪些类别在互相误判要按性别、光照、是否戴眼镜等维度分组统计找出模型的薄弱子群。我自己的习惯是每次改完模型先跑一遍混淆矩阵再挑出错得最多的两类单独看样本往往能发现标注问题或者数据偏差。情绪识别这行模型结构只是起点数据质量和评估方式才决定它能不能真正用起来。希望帮到你。本文还有配套的精品资源点击获取