简介:资源为东南大学发表于《Journal of Southeast University》的学术论文PDF,系统探讨基于深度神经网络的多模态情感识别方法,面向从事情感计算、人机交互及深度学习应用的研究者与高年级学生。文中重点分析了CNN用于音频与视频低级特征提取、RNN用于时序动态信息建模,并提出了混合CNN与RNN的识别框架;通过频谱特征、能量函数、面部表情与动作特征等多模态融合,在CCPR 2016中文视听情感数据库上取得了41.15%的平均识别精度,比基线提升16.62%。包内文件为1个PDF文档,压缩包大小约714KB,内容包含完整摘要、方法与实验细节,适合直接阅读学习。目前已有233人学习下载,对刚入门多模态情感识别或希望了解CNN/RNN组合建模的读者具有一定的参考价值。
1. 多模态情感识别:这篇 2017 年的论文凭什么涨了 16.62%
把声音和表情同时交给深度神经网络,让机器判断说话的人现在是开心、愤怒还是焦虑——这是多模态情感识别最朴素的目标。这篇 2017 年发表于《东南大学学报(英文版)》第 4 期的论文只有 4 页,作者来自东南大学生物科学与医学工程学院,却用一套“1D CNN + 双向 RNN”处理音频、“2D CNN + RNN”处理视频的完整方案,在 CCPR 2016 多模态情感识别挑战赛的 CHEAVD 中文自然音视频情感数据库上,把多模态平均精度做到了 41.15%,比组委会给出的基线高出 16.62 个百分点。对正在做复杂场景下多模态情感预测建模、或者想找一个 CNN + RNN 端到端融合模板的人来说,这篇论文最大的价值不是数学上多惊艳,而是结构完整、公开参数充足、踩坑点明确,非常适合照着复现再改造。
2. 音频分支:openSMILE 特征 + 1D CNN + B-RNN 怎么串起来
2.1 为什么音频分支要设计成 1D CNN + RNN 的组合
音频情感识别最经典的做法是手工提一大堆统计特征,然后塞进 SVM 或随机森林。这套路子的瓶颈很明显:特征工程决定上限,换个数据集就得重新调特征。论文的做法是把音频当成一维时序信号,先用卷积网络做局部特征抽象,再用循环网络捕捉音调随时间变化的动态,本质上就是语音识别里“CNN 提特征 + RNN 管时序”的迁移。
为什么不是纯 CNN 或纯 RNN?纯 CNN 对局部模式敏感,但音频里的情感表达往往依赖前后语段的音调起伏,卷积核的感受野有限;纯 RNN 能建模时序依赖,但对原始频谱这种高维输入直接建模,收敛速度和特征抽象能力都不如 CNN。把两者串起来,等于让 CNN 先干粗活、把原始频谱压成紧凑的高层特征,RNN 再干细活、专门看这些特征在时间轴上怎么变化。这个分工在情感识别里被反复验证过,是低成本高收益的结构选择。
2.2 openSMILE 特征提取:论文里没说全的细节
这一步是整条链路里最容易出偏差的地方。论文说得很简单:用 openSMILE 按 INTERSPEECH 2010 Paralinguistic Challenge 的配置提取 MFCC 等频带与能量特征。但实际跑起来你会发现,openSMILE 的配置文件和版本会直接影响输出维度,哪怕同一个配置名,不同版本解析出来的特征列表也可能不一样。
openSMILE 的命令行工具是 SMILExtract,常用方式如下:
SMILExtract -C IS10_paraling_compat.conf \ -I audio.wav \ -O audio_features.csv这里-C指定配置文件,-I是输入音频,-O是输出特征文件。IS10_paraling_compat.conf 是 INTERSPEECH 2010 挑战赛的兼容配置,提取的是帧级 MFCC 加上能量、过零率等低级描述符。论文里对音频信号的预处理是“固定帧长、相邻帧共享固定采样点”,这个在 openSMILE 里对应 frameSize 和 frameStep 两个参数,常见做法是 25ms 帧长、10ms 帧移,相邻帧有 15ms 重叠,保证局部和全局信息都不丢。
2.3 音频网络骨架:PyTorch 实现思路与 attention 对齐层
论文的音频网络是四层 1D CNN 加池化,再接双向 RNN,最后加了一个 alignment 层——也就是注意力机制,给每个时间步的隐藏状态分配权重。下面是我按论文结构搭的示意骨架,不是原文代码,但结构和超参对齐了论文 Tab.1 的卷积核数量 32→32→64→64:
import torch import torch.nn as nn class TemporalAttention(nn.Module): """对齐层:给 RNN 每个时间步的隐藏状态加权求和""" def __init__(self, hidden_dim): super().__init__() self.score = nn.Linear(hidden_dim, 1) def forward(self, h): # h: (batch, seq_len, hidden_dim) w = torch.softmax(self.score(h), dim=1) # 每个时间步的权重 return torch.sum(w * h, dim=1) # 加权后的全局表示 class AudioBranch(nn.Module): def __init__(self, feat_dim=158, rnn_hidden=128, num_emotions=8): super().__init__() # 1D CNN 局部特征抽象,通道数按论文 32->32->64->64 self.conv1 = nn.Conv1d(feat_dim, 32, kernel_size=3, padding=1) self.pool1 = nn.MaxPool1d(2) self.conv2 = nn.Conv1d(32, 32, kernel_size=3, padding=1) self.pool2 = nn.MaxPool1d(2) self.conv3 = nn.Conv1d(32, 64, kernel_size=3, padding=1) self.pool3 = nn.MaxPool1d(2) self.conv4 = nn.Conv1d(64, 64, kernel_size=3, padding=1) self.pool4 = nn.MaxPool1d(2) self.rnn = nn.GRU(64, rnn_hidden, bidirectional=True, batch_first=True) self.attn = TemporalAttention(rnn_hidden * 2) self.fc = nn.Linear(rnn_hidden * 2, num_emotions) def forward(self, x): # x: (batch, seq_len, feat_dim) x = x.permute(0, 2, 1) # 转成 (batch, feat_dim, seq_len) x = self.pool1(torch.relu(self.conv1(x))) x = self.pool2(torch.relu(self.conv2(x))) x = self.pool3(torch.relu(self.conv3(x))) x = self.pool4(torch.relu(self.conv4(x))) x = x.permute(0, 2, 1) # 转回 (batch, time, channels) out, _ = self.rnn(x) out = self.attn(out) return self.fc(out)这段代码有两个地方需要特别注意。第一,feat_dim=158是从论文 Tab.1 的输入维度推测的,实际要以你 openSMILE 输出的特征列为准,配置不同这个数字就可能变成 130 多或者 180 多,网络第一层输入必须跟着改。第二,四层池化窗口都是 2,如果输入序列长度本来就是 50,四次池化后时间维只剩下 3,RNN 基本学不到时序依赖。论文里的做法是让部分卷积层步长为 1,池化并不总在时间维上压缩,所以你复现时一定要根据自己的序列长度调整池化次数,这是音频分支最容易翻车的地方。
2.4 论文没有明说的几个参数位置
论文对音频网络结构的描述浓缩在一张表里,关键信息是四层卷积的卷积核数量依次为 32、32、64、64,卷积核大小为 3,池化窗口为 2,但步长是 1 还是 2 原文没写清楚。这里我建议按自己的输入序列长度反推:目标是让进入 RNN 之前的时间维不低于 10,否则双向 RNN 能看到的上下文太短。另一个值得注意的点是 RNN 用的是 BRNN,也就是双向结构,代码里用bidirectional=True实现,hidden 维度要记得乘 2。
alignment 层对应论文里的公式 4 和 5,作用是给每个隐藏状态一个可学习的权重,然后加权求和作为全局表示。这一步在代码里就是 TemporalAttention 做的事,它让网络不再只依赖最后一个时间步的输出,而是综合整段音频的贡献。实践里的收益主要体现在愤怒、焦虑这类情绪转折较多的样本上,因为这些情绪往往在中段爆发,最后一步未必包含全部信息。
3. 视频分支:Faster-RCNN 人脸裁剪与 VGG-Face16 微调的取舍
3.1 Faster-RCNN 人脸裁剪:自动检测 + 人工纠错的平衡
视频分支的第一步不是直接抽帧,而是先把人脸区域裁出来。论文用的是 Faster-RCNN 检测器,把每一帧的人脸框检测出来后裁剪并 resize 到统一尺寸。这一步看似预处理,实际对最终结果影响非常大:如果人脸框偏了,VGG-Face16 提取的特征里混入大量背景,后面的时序模型学到的全是噪声。
比较容易被忽略的是论文里的这句话:裁剪结果需要逐张人工检查,把检测错误的框手动修正。在 CHEAVD 这种自然场景数据集里,人脸遮挡、侧脸、出境是常态,Faster-RCNN 再强也扛不住所有情况。对小规模数据集来说,人工修正几百张图的时间成本是值得的,因为脏特征会顺着整条网络传播到最后的融合层,到时候再回头排查就晚了。
3.2 VGG-Face16 微调:冻结卷积层,只动最后三个 FC
VGG-Face16 是 16 层的 VGG 人脸识别预训练模型,结构是五段卷积网络加三个全连接层加 softmax。论文做了一个很克制的选择:只取最后一个全连接层的输出作为空间特征,微调时冻结前面的卷积层,只把最后三个全连接层随机初始化并更新。
为什么不全量微调?情感识别数据集通常只有几千到几万张人脸图,而 VGG-Face16 是在百万级人脸数据上预训练的。全量微调意味着把已经学好的通用人脸表征推翻重来,在小数据上极易过拟合,表情细节还没学到,loss 就下不去了。只微调 FC 层的逻辑是:浅层卷积已经能提取边缘、纹理、五官结构这些通用特征,真正需要重新学的是“什么样的面部组合对应什么情绪”这个映射关系,而这正是 FC 层的职责。
3.3 帧特征重排:按相邻帧差异挑关键帧的代码思路
视频长度不固定,没法直接拼特征。论文的解决思路很工程化:计算相邻两帧特征的差异,差异太小说明表情几乎没变,直接丢掉;差异大说明情绪在变化,保留这一帧。选完后再按时间顺序排列,如果帧数不够固定长度,就把第一帧复制几份补在开头。
这个逻辑用 NumPy 实现非常直观:
import numpy as np def select_key_frames(feats, min_diff=0.05, max_len=50): """ feats: (T, D) 每帧经过 VGG-Face 提取的特征,T 是帧数 min_diff: 相邻帧特征差异阈值,小于该值视为无变化 max_len: BRNN 输入序列的固定长度 """ keep = [0] for t in range(1, len(feats)): delta = np.linalg.norm(feats[t] - feats[t - 1]) if delta > min_diff: keep.append(t) # 少于固定长度时,复制首帧插入开头,保持时间顺序 if len(keep) < max_len: keep = [0] * (max_len - len(keep)) + keep return np.array(keep[:max_len])min_diff是控制筛选强度的关键超参数,论文没有给出具体数值,经验是从 0.01 开始按十倍粒度扫。阈值设太小,几乎每帧都保留,序列长度依然很长;阈值设太大,关键表情转折帧可能被丢掉。补齐时把第一帧插到开头而不是结尾,是因为情绪表达的铺垫信息通常在视频前段,直接复制首帧放在序列开头能保留这种时间关系,插在结尾会破坏语义顺序。如果你复现时发现多模态结果比单模态还差,先回来看这一步的筛选逻辑。
3.4 视频数据增强:旋转、镜像和 multi-PIE 补样本
CHEAVD 的八类情绪样本数量并不均衡,论文采取了两个策略:一是对选中的视频序列做旋转和镜像,旋转角度取 ±7° 和 ±15°;二是从 multi-PIE 人脸库补充样本,当 multi-PIE 只有 5 类情绪时,就把原本不够的 3 类用 CHEAVD 里对应类别的样本按序号复制,保证八类数量级一致。
这个操作给数据增强提了个醒:旋转和镜像对视频时序模型是安全的,因为人脸在视频里本来就会有轻微平面内旋转;但像缩放、裁剪这类改变人脸空间结构的增强要慎用,VGG-Face16 对对齐后的人脸很敏感。multi-PIE 补数据的大方向没问题,跨数据集补样本时需要注意域差异——multi-PIE 是实验室人脸采集环境,CHEAVD 是影视剧素材,两者的光照和分辨率差异可能导致模型学到数据集特征而非情绪特征。
4. 融合与训练参数:softmax 加权里的 α、β 和数据增强细节
4.1 softmax 加权融合公式与 α、β 的调试顺序
多模态融合是这篇论文的核心卖点。音频网络和视频网络各自输出一个 softmax 分数向量,然后按类别加权求和,取最大值对应的类别作为最终预测。对应论文公式 6:
C = argmax_c ( α · s_audio(c) + β · s_video(c) )
其中 s_audio(c) 和 s_video(c) 分别是音频网络、视频网络对第 c 个类别的 softmax 分数,α 和 β 是两路模态的权重,C 是最终预测标签。
这个公式看起来简单,调起来却有点玄学。我的做法是分三步走:第一步先跑音频单模态和视频单模态,记录各自在验证集上的平均精度;第二步固定较优模态的权重为 1,在 0.1 到 1.0 范围内搜索另一路权重;第三步把 α 和 β 归一化后微调。论文没有明确说 α + β 等于 1,但实际使用时归一化更方便解释。一个值得注意的现象是:如果某一模态单模态精度特别低,它在融合里的贡献往往不是正向的,这时把它的权重压到 0.3 以下反而能整体提升。
4.2 训练超参总表:三套参数分别对应哪一路网络
论文的实验部分给出了三套完全不同的训练参数,分别对应音频子网络、视频微调、BRNN 时序网络。复现时最容易犯的错是把三套参数混用,看到 batch size 有 40、32、256 就以为随便选一个都行,实际上每一套都是按各自网络规模和数据量调过的。
| 网络模块 | batch size | learning rate | momentum | weight decay | 其他关键项 |
|---|---|---|---|---|---|
| 音频子网络 | 40 | 0.01 | 0.9 | 0.01 | epoch 600,lr decay 0.1 |
| 视频微调阶段 | 32 | 0.005 | 0.9 | 0.0015 | dropout 0.5 |
| BRNN 时序网络 | 256 | 0.005 | 0.9 | 0.0005 | 序列长度 50 |
音频子网络用 600 个 epoch 是因为 1D CNN 参数量不大,而且 openSMILE 特征维度有限,需要长训练才能充分收敛。视频微调阶段加了 0.5 的 dropout,这在全连接层上是常规操作,防止 FC 层在情感数据上过拟合。BRNN 的 weight decay 只有 0.0005,比视频微调阶段小了 3 倍,说明时序网络的参数更依赖正则约束,调大反而会让时序依赖学不到位。
4.3 音频与视频的数据增强策略对比
两条分支的数据增强思路完全不同。音频侧,论文把产生噪声的语音信号开头或结尾的 4000 到 8000 个采样点直接切掉,相当于手动去噪加样本扩充,这样能获得大量增强后的语音样本,再从每类中随机抽 800 个,让各类数量大致相等。视频侧则是通过旋转、镜像和跨数据集补样本,让八类情绪序列数量分别达到 798、801、585、614、723、768、737 和 744。
这两套策略的共同点是都在解决类别不平衡,但手段差异很大。音频增强是基于波形层面的,切掉噪声段不会改变说话人的情感表达;视频增强的风险在于旋转角度太大会让面部比例失真,论文选的 ±7° 和 ±15° 是经验和实验折中的结果。另外注意音频样本数量是“每类 800”,视频是“每类 600 到 800 不等”,两者并不严格相等,这说明多模态融合并不要求两路样本数完全一致,只要类别分布不偏就行。
5. 避坑指南:复现 CNN+RNN 情感识别时最容易翻车的五个位置
5.1 坑一:openSMILE 版本配置不一致,特征维度悄悄变了
现象:训练时模型报维度错误,或者特征维度在没改代码的情况下前后两次运行不一致。原因:openSMILE 的配置文件在不同版本之间并不完全兼容,IS10_paraling_compat.conf 在不同版本里解析出的特征列表可能多个或少个几列,特征维度对不上卷积网络的输入层。解决:跑通代码前先固定一个 openSMILE 版本,把配置文件解析出的特征列名导出来存成文件,每次实验前核对维度。
5.2 坑二:视频帧数不够 50 时,补齐位置放错了
现象:多模态融合结果比视频单模态还低,逐层排查发现时序特征混乱。原因:序列补帧时把首帧复制放在了序列末尾,破坏了情感铺垫在时间轴上的顺序,BRNN 读到的“时间顺序”是假的。解决:严格按论文做法,复制首帧插到序列开头,而且要在代码里写明注释,防止后人“好心”帮你改成末尾补齐。
5.3 坑三:直接拿原始分布训练,neutral 把结果带偏
现象:训练 loss 能降,但验证集上 happy 和 surprise 的召回率极低,整体平均精度惨淡。原因:CHEAVD 原始数据里 neutral 类占比偏高,你不做均衡,网络会把多数类当成更容易赚钱的方向学,少数类被彻底牺牲。解决:按论文思路先做音频每类 800 采样、视频用增强手段补数量。补样本时注意不要无脑复制,复制样本加轻微旋转镜像不算作弊,因为帧特征经过 VGG-Face16 之后本来就对小幅几何扰动不敏感。
5.4 坑四:VGG-Face16 全量微调,小数据集直接过拟合
现象:验证集精度先升后降,训练集精度接近 100%,典型过拟合曲线。原因:VGG-Face16 是在百万级人脸数据上预训练的,全量微调让所有卷积核都往小数据集上适配,情感变化没学到,数据集特有噪声倒是学得很透彻。解决:冻结卷积层,只微调最后三个 FC 层。如果效果还不够,再加一层 dropout 0.5,或者把 FC 中间层的输出维度从 4096 降到 512,用信息瓶颈逼模型学更抽象的表征。
5.5 坑五:融合权重一拍脑袋,不如先看单模态成绩
现象:α 和 β 随手设成 0.5/0.5,融合结果比两个单模态都差。原因:softmax 输出的分数分布在不同网络之间没有可比性,音频网络可能偏爱输出高置信度,视频网络输出偏保守,直接等权平均等于把两路错误也平均了。解决:先分别跑通音频和视频单模态,把各自最好的模型存下来,再做权重搜索。我在实际复现时习惯用验证集网格扫描 α 从 0.1 到 0.9、β 从 0.1 到 0.9,找到最高平均精度的组合后再上测试集。
6. 进阶做法:用三个里程碑验证你的复现是否对齐 41.15%
6.1 复现验证的三个里程碑
复现这篇论文不要一上来就冲多模态融合,建议按单模态到多模态的顺序分三个里程碑验证。论文给出的结果表是现成的校验标尺:
| 模态 | 分类准确率 | 平均精度 |
|---|---|---|
| 音频 | 31.53% | 37.06% |
| 视频 | 31.85% | 37.63% |
| 多模态融合 | 34.55% | 41.15% |
第一个里程碑是复现音频分支,目标平均精度 37.06%。如果差得远,优先检查 openSMILE 特征维度和序列长度对不对。第二个里程碑是视频分支,目标平均精度 37.63%,这里重点排查 VGG-Face16 的 FC 层输出和帧筛选阈值。第三个里程碑才是融合,目标平均精度 41.15%,这时再调 α、β。这样做的好处是每个阶段只面对一个变量,翻车时能快速定位问题出在特征、网络还是融合权重。
6.2 从这篇论文能带走的两件东西
如果你是冲着 2026 华为杯 E 题那种复杂场景下多模态情感预测建模来的,这篇论文给你的不是直接能跑的代码,而是两个可以迁移的工程习惯。第一个是“单模态先跑通再谈融合”,它把多模态问题拆成了三个可控子问题,降低排错成本。第二个是帧特征重排的思路,它用相邻帧特征差异做关键帧筛选,比均匀抽帧更能保留情绪转折点,这个技巧在视频片段不标准的任务里都能复用。
论文的结构虽然简单,但每一步都是围绕“数据不干净、长度不统一、类别不平衡”这些真实问题设计的。从那以后,我复现情绪相关的多模态论文时都强制先跑通单模态、再碰融合权重,这个顺序帮我省下了很多次半夜返工。希望帮到你。
本文还有配套的精品资源,点击获取