news 2026/10/10 21:21:06

EmotionVGGnet情绪识别Python源码实战:从骨干搭建到训练排错

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
EmotionVGGnet情绪识别Python源码实战:从骨干搭建到训练排错

简介:这份资源是面向深度学习初学者与情感识别方向开发者的Python实战源码包,围绕VGGNet卷积神经网络实现情绪识别任务,适合想理解CNN在情感分析中落地流程、需要可复用代码框架的读者。压缩包共11个文件,约12.38MB,以6个py脚本为核心,覆盖数据构建、模型定义、训练与识别推理等环节;另含2个hdf5权重文件、1个json配置、1张结果图和1份说明文档,便于直接复现与二次实验。目前已有309人学习下载。内容涵盖数据预处理、VGGNet模型搭建、训练优化、模型评估与结果可视化等完整链路,读者可据此掌握从数据集构建到情绪识别的实现思路,并借助已有权重快速验证效果,为类似多模态情感识别任务提供可参考的工程结构。

1. EmotionVGGnet 情绪识别:一份 Python 源码能跑出什么

拿到EmotionVGGnet情绪识别-python源码.zip这个包,多数人第一反应是解压、装依赖、跑train.py,然后被一堆形状不匹配和路径报错劝退。它本质上是一个把 VGG 式卷积骨干接到情绪分类头上的小型工程:输入一张人脸图,输出 7 类离散情绪(生气、厌恶、恐惧、开心、中性、难过、惊讶)的概率分布。适合两类人:一是想快速搭一个能演示的情绪识别 demo 的开发者,二是想拿它当骨架、换成自己数据集做迁移的算法同学。它不解决人脸检测,也不解决多人场景,输入得是已经裁好的人脸。这一章先把边界划清楚,后面几章再拆骨干、数据、训练和排错。

2. 骨干与分类头:EmotionVGGnet 到底长什么样

EmotionVGGnet 这个名字里的 VGG 不是指原版 16 层或 19 层,而是借用了 VGG 的堆叠思路:连续的小卷积核(3×3)加池化,逐级把空间尺寸压下去、通道数提上来,最后接全连接做分类。情绪识别和 ImageNet 分类的差别在于,情绪信号集中在眉毛、嘴角、眼角这些局部区域,全局纹理反而没那么重要,所以骨干不需要太深,通道也不用堆到 512 以上。

2.1 为什么用 VGG 式堆叠而不是直接上 ResNet

情绪识别数据集普遍偏小,公开的 FER2013 也就三万多张 48×48 的灰度图,换成 ResNet 这种带残差和 BatchNorm 的大结构,很容易在几百个 step 内就把训练集拟合到 99%,验证集卡在 60% 上下。VGG 式堆叠参数少、感受野增长可控,在小图上反而更稳。常见做法是 4 个卷积块,每块两个 3×3 卷积加一个 2×2 最大池化,通道依次 64、128、256、512,最后全局池化接两层全连接。我一般会把第一层改成 5×5 或加一个 1×1 降维,因为 48×48 的输入用 3×3 第一层感受野太小,边缘信息抓不住。

2.2 用 PyTorch 搭出可复现的骨干

下面这段是骨干的最小实现,直接抄进model.py就能用。注意padding=1保证卷积后尺寸不变,池化才负责降采样。

import torch import torch.nn as nn class EmotionVGGnet(nn.Module): def __init__(self, num_classes=7, in_channels=1): super().__init__() # 4 个卷积块,通道 64->128->256->512 self.features = nn.Sequential( # block1: 48x48 -> 24x24 nn.Conv2d(in_channels, 64, 3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.Conv2d(64, 64, 3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(2), # block2: 24x24 -> 12x12 nn.Conv2d(64, 128, 3, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), nn.Conv2d(128, 128, 3, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), nn.MaxPool2d(2), # block3: 12x12 -> 6x6 nn.Conv2d(128, 256, 3, padding=1), nn.BatchNorm2d(256), nn.ReLU(inplace=True), nn.Conv2d(256, 256, 3, padding=1), nn.BatchNorm2d(256), nn.ReLU(inplace=True), nn.MaxPool2d(2), # block4: 6x6 -> 3x3 nn.Conv2d(256, 512, 3, padding=1), nn.BatchNorm2d(512), nn.ReLU(inplace=True), nn.MaxPool2d(2), ) # 全局平均池化,避免全连接层参数爆炸 self.gap = nn.AdaptiveAvgPool2d(1) self.classifier = nn.Sequential( nn.Dropout(0.5), nn.Linear(512, 256), nn.ReLU(inplace=True), nn.Dropout(0.3), nn.Linear(256, num_classes), ) def forward(self, x): x = self.features(x) x = self.gap(x).flatten(1) # [B, 512] return self.classifier(x)

逻辑说明:features负责特征提取,gap把 3×3×512 压成 512 维向量,classifier做分类。参数上,in_channels=1对应灰度图,如果你的数据是 RGB 就改成 3;num_classes=7是 FER 标准类别数,换成自己的数据集时改这里。Dropout(0.5)放在全连接前是血泪经验,小数据集不加这个,验证集 loss 会在第 5 个 epoch 后开始往上走。

2.3 输入尺寸和通道数怎么定

源码里如果写死了 48×48,你换成 224×224 的图会直接报形状错误。原因是AdaptiveAvgPool2d(1)虽然能吃掉任意空间尺寸,但前面的池化次数决定了最小尺寸不能低于 2 的 4 次方,也就是 16。低于 16 的输入会在某个池化层变成 0。常见做法是统一 resize 到 48×48 或 64×64,前者对齐 FER2013,后者给嘴角细节留更多像素。通道数方面,灰度图省显存、训练快,彩色图在肤色和光照变化大的场景下更稳,但参数量翻三倍,小数据集上不一定划算。

3. 数据管线:从原始人脸到能喂进网络的张量

情绪识别翻车最多的地方不在模型,在数据。你拿到的源码包通常带一个data/目录或一个fer2013.csv,但真实场景里你手里是散落的 jpg,标签在文件名或另一个 csv 里。这一章把从原始图到 DataLoader 的链路拆开。

3.1 标签映射和类别不平衡的处理

FER2013 的 7 类分布极不均匀,开心和中性各占约 25%,厌恶只有 1.5%。直接训练,模型会把所有样本预测成多数类,准确率看着有 25%,实际没用。常见做法有两种:一是用WeightedRandomSampler按类别频率倒数采样,二是损失函数加weight参数。我一般两个都用,采样器管 batch 内平衡,权重管梯度。

import os import pandas as pd from PIL import Image from torch.utils.data import Dataset, DataLoader, WeightedRandomSampler from torchvision import transforms # 7 类情绪,顺序必须和模型输出对齐 EMOTIONS = ['angry', 'disgust', 'fear', 'happy', 'neutral', 'sad', 'surprise'] LABEL2IDX = {e: i for i, e in enumerate(EMOTIONS)} class FERDataset(Dataset): def __init__(self, csv_path, root_dir, transform=None): self.df = pd.read_csv(csv_path) # 至少两列: filename, emotion self.root = root_dir self.transform = transform def __len__(self): return len(self.df) def __getitem__(self, idx): row = self.df.iloc[idx] img = Image.open(os.path.join(self.root, row['filename'])).convert('L') label = LABEL2IDX[row['emotion']] if self.transform: img = self.transform(img) return img, label # 训练集增强:随机翻转 + 小角度旋转 + 归一化 train_tf = transforms.Compose([ transforms.Resize((48, 48)), transforms.RandomHorizontalFlip(p=0.5), transforms.RandomRotation(10), transforms.ToTensor(), transforms.Normalize(mean=[0.5], std=[0.5]), ]) dataset = FERDataset('labels.csv', 'images/', transform=train_tf) # 按类别频率倒数给权重,缓解不平衡 labels = [LABEL2IDX[e] for e in dataset.df['emotion']] class_count = pd.Series(labels).value_counts().sort_index().values class_weights = 1.0 / class_count sample_weights = [class_weights[l] for l in labels] sampler = WeightedRandomSampler(sample_weights, num_samples=len(sample_weights), replacement=True) loader = DataLoader(dataset, batch_size=64, sampler=sampler, num_workers=4)

逻辑说明:convert('L')强制灰度,和模型in_channels=1对齐;Normalize(mean=[0.5], std=[0.5])把像素从 [0,1] 映射到 [-1,1],这是 VGG 式网络的常规操作。WeightedRandomSampler的replacement=True表示有放回采样,少数类会被反复抽到。参数上,batch_size=64在 8G 显存上跑 48×48 够用,num_workers=4在 Windows 上如果报错就改成 0。

3.2 验证集和测试集的划分陷阱

很多人从同一个 csv 里随机切 8:2,结果同一张图的不同增强版本同时进了训练和验证,验证准确率虚高。正确做法是按原始图片 ID 或按人划分,同一个人的人脸不能同时出现在两边。源码包里如果已经切好了train/和test/目录,直接用;如果只有一个大目录,用GroupShuffleSplit按文件名前缀分组。

from sklearn.model_selection import GroupShuffleSplit # 假设文件名格式为 subjectID_xxx.jpg,按 subjectID 分组 groups = dataset.df['filename'].str.split('_').str[0] gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42) train_idx, val_idx = next(gss.split(dataset.df, groups=groups)) train_df = dataset.df.iloc[train_idx].reset_index(drop=True) val_df = dataset.df.iloc[val_idx].reset_index(drop=True)

参数说明:test_size=0.2是验证集比例,random_state=42保证可复现。这一步不做,后面调参全是玄学,验证集涨了测试集掉,你根本不知道信哪个。

4. 训练循环与关键参数:让 loss 真的往下走

模型和数据都齐了,训练脚本写不对照样白搭。这一章给一个能直接跑的训练循环,并把学习率、优化器、早停这几个参数讲透。

4.1 优化器和学习率的选型

情绪识别这种小任务,Adam 比 SGD 收敛快,但最终精度 SGD + momentum 往往高 1 到 2 个点。我一般先用 Adam 跑 20 个 epoch 看趋势,确认模型结构没问题,再换 SGD 精调。学习率初始值 Adam 用 1e-3,SGD 用 1e-2,配合CosineAnnealingLR或ReduceLROnPlateau。

import torch.optim as optim from torch.optim.lr_scheduler import ReduceLROnPlateau device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = EmotionVGGnet(num_classes=7, in_channels=1).to(device) # 类别权重传给损失函数,和采样器双保险 weights = torch.tensor(class_weights, dtype=torch.float32).to(device) criterion = nn.CrossEntropyLoss(weight=weights) optimizer = optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=3) best_val_acc = 0.0 patience_counter = 0 for epoch in range(50): model.train() running_loss = 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() running_loss += loss.item() * imgs.size(0) # 验证 model.eval() correct, total = 0, 0 val_loss = 0.0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels = imgs.to(device), labels.to(device) outputs = model(imgs) val_loss += criterion(outputs, labels).item() * imgs.size(0) preds = outputs.argmax(dim=1) correct += (preds == labels).sum().item() total += labels.size(0) val_acc = correct / total scheduler.step(val_loss) # 早停:验证准确率 5 个 epoch 不涨就停 if val_acc > best_val_acc: best_val_acc = val_acc torch.save(model.state_dict(), 'best.pth') patience_counter = 0 else: patience_counter += 1 if patience_counter >= 5: print(f'Early stop at epoch {epoch}') break print(f'Epoch {epoch}: loss={running_loss/len(dataset):.4f}, val_acc={val_acc:.4f}')

逻辑说明:weight_decay=1e-4是 L2 正则,防止全连接层过拟合;ReduceLROnPlateau在验证 loss 不降时把学习率砍半,patience=3表示等 3 个 epoch。早停的patience_counter >= 5是经验值,数据集越小这个值越要小,否则白跑。参数上,batch_size和lr要联动,batch 翻倍 lr 也可以适当放大,但别超过 5e-3(Adam)。

4.2 训练过程中该盯哪些指标

只看准确率会被不平衡数据骗。必须同时看混淆矩阵和每类的 F1。厌恶类样本少,模型可能一个都预测不对,但整体准确率还有 80%。常见做法是每个 epoch 结束后在验证集上算一次classification_report,重点看 disgust 和 fear 的 recall。

from sklearn.metrics import classification_report, confusion_matrix model.eval() all_preds, all_labels = [], [] with torch.no_grad(): for imgs, labels in val_loader: imgs = imgs.to(device) preds = model(imgs).argmax(dim=1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) print(classification_report(all_labels, all_preds, target_names=EMOTIONS, digits=4)) print(confusion_matrix(all_labels, all_preds))

如果 disgust 的 recall 低于 0.2,说明采样权重还不够,或者这一类本身标注噪声太大,考虑合并到 angry 或直接丢弃。这一步不做,你交付的模型在真实场景里就是个只会说“开心”和“中性”的黑匣子。

5. 避坑与排查:源码跑不起来时先看这几条

这一章列的是我在不同机器上复现这类源码时最常撞的 5 个问题,每条按现象、原因、解决写。

现象:RuntimeError: Given groups=1, weight of size [64, 3, 3, 3], expected input[64, 1, 48, 48] to have 3 channels原因:模型第一层in_channels默认是 3,但你的数据是灰度图,通道数对不上。 解决:把EmotionVGGnet(in_channels=1)显式传进去,或者把数据convert('RGB')。别改源码里的默认值,改调用处,否则换数据集时又乱。

现象:训练 loss 一直卡在 1.9 左右不降,准确率约 14%原因:标签映射错了,模型输出顺序和EMOTIONS列表不一致,或者 csv 里 emotion 列是数字但被当成字符串。 解决:打印dataset[0]看 label 是不是 0 到 6 的整数,再打印LABEL2IDX确认顺序。FER2013 原始 csv 的 emotion 列是 0 到 6,别自己再映射一遍。

现象:验证准确率比训练准确率高 10 个点以上原因:验证集里混进了训练集的增强版本,或者验证集太小(少于 500 张)。 解决:按人分组切分,验证集至少占 15% 且每类不少于 30 张。如果做不到,用交叉验证,别信单次划分的结果。

现象:CUDA out of memory,batch_size 已经降到 16原因:AdaptiveAvgPool2d之前的特征图太大,或者num_workers开太多导致内存泄漏。 解决:先把num_workers=0排除多进程问题,再把输入从 64×64 降到 48×48。还不行就在features里多加一个池化层,把 6×6 压到 3×3。

现象:推理时单张图预测结果和训练时验证结果差很多原因:推理时忘了model.eval(),BatchNorm 用了 batch 统计量;或者归一化参数和训练不一致。 解决:推理前必须model.eval()和torch.no_grad(),归一化的 mean/std 从训练配置里读,别硬编码。

6. 从能跑到好用:换数据集和导出推理的两个技巧

源码跑通只是起点,真正要投入得看它能不能换成你自己的数据、能不能脱离训练脚本做推理。第一个技巧是冻结骨干只训分类头:当你只有几百张标注图时,把features的requires_grad设为 False,只训classifier,学习率用 1e-3,10 个 epoch 就能看到效果,比全量微调稳得多。第二个技巧是导出 ONNX 做部署验证,这一步能暴露很多训练时看不出的问题。

# 冻结骨干,只训分类头 for param in model.features.parameters(): param.requires_grad = False # 只把 classifier 的参数传给优化器 optimizer = optim.Adam(model.classifier.parameters(), lr=1e-3) # 导出 ONNX,输入尺寸必须和训练一致 dummy = torch.randn(1, 1, 48, 48).to(device) torch.onnx.export( model, dummy, 'emotion_vggnet.onnx', input_names=['input'], output_names=['logits'], dynamic_axes={'input': {0: 'batch'}, 'logits': {0: 'batch'}}, opset_version=11, )

导出后拿onnxruntime跑一张图,对比 PyTorch 和 ONNX 的输出,如果最大差值超过 1e-4,说明某层有算子不兼容,常见的是AdaptiveAvgPool2d在旧 opset 上行为不一致,把opset_version提到 12 以上。验证方法很简单:同一张图两边各跑一次,np.allclose过不了就逐层排查。

我自己的习惯是每换一个数据集,先跑 5 个 epoch 看验证集混淆矩阵,如果多数类 recall 超过 0.9 而少数类低于 0.1,直接回去调采样权重,不浪费算力。情绪识别这个方向,模型结构不是瓶颈,数据质量和类别平衡才是。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/10 21:19:09

Java+SSM+Flask在线商品交易平台:电商毕设项目设计思路与实现全解析

每年毕业季我都会被问同一个问题:"老师,在线商品交易平台这种题目是不是太基础了,能做吗?"我的回答通常是不着急,先问清楚对方想要的是什么。这个题目背后涉及的JavaSSMFlask技术栈、源码、论文、调试文档一…

作者头像 李华
网站建设 2026/10/10 21:18:34

647回文子串与516最长回文子序列:区间DP两种典型玩法全解析

各位打卡代码随想录的伙计们,第四十五天来了。今天这两道题——647 回文子串、516 最长回文子序列——看起来名字只差两个字,实际上一个是把字符串切成一段段判断"是不是回文",另一个是允许跳跃地凑出"最长回文有多长"。…

作者头像 李华
网站建设 2026/10/10 21:18:04

ThinkPHP项目迁移Swoole实战:常驻内存、连接池与协程优化

1. 为什么要把 ThinkPHP 项目跑在 Swoole 上先聊个现象。很多 PHP 开发者一说 Swoole 就摇头,觉得那是“常驻内存”的东西,心智负担重,学了容易忘,项目一忙就想扔回 FPM 的怀抱。但真实场景是——你的 ThinkPHP 项目一旦流量上来&…

作者头像 李华
网站建设 2026/10/10 21:12:47

Selenium显式等待优化实战:回归测试耗时降低41%的改造方案

我曾经接过一个支付类后台的回归测试优化任务。套件里有 60 条用例,跑完要一个小时出头,其中大量时间花在界面上转圈、按钮半天不亮、弹窗迟迟不弹这些"无谓等待"上。把耗时明细打出来以后发现,Selenium 的WebDriverWait占了差不多…

作者头像 李华
网站建设 2026/10/10 21:10:06

电子产品设计开发管理流程:从需求到量产的落地指南

简介:这份《电子产品设计开发管理流程》文档面向硬件产品经理、研发工程师及项目管理者,用于规范自主产品从需求到量产的全过程管理。内容围绕角色职责、工程启动、流程图与开发流程展开,明确产品经理、工程经理、软件、硬件、结构、测试、采…

作者头像 李华