ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

多模态情感识别实战:CNN+RNN融合音频与视频的复现与避坑指南

多模态情感识别实战:CNN+RNN融合音频与视频的复现与避坑指南 简介资源为东南大学发表于《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→64import 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), dim1) # 每个时间步的权重 return torch.sum(w * h, dim1) # 加权后的全局表示 class AudioBranch(nn.Module): def __init__(self, feat_dim158, rnn_hidden128, num_emotions8): super().__init__() # 1D CNN 局部特征抽象通道数按论文 32-32-64-64 self.conv1 nn.Conv1d(feat_dim, 32, kernel_size3, padding1) self.pool1 nn.MaxPool1d(2) self.conv2 nn.Conv1d(32, 32, kernel_size3, padding1) self.pool2 nn.MaxPool1d(2) self.conv3 nn.Conv1d(32, 64, kernel_size3, padding1) self.pool3 nn.MaxPool1d(2) self.conv4 nn.Conv1d(64, 64, kernel_size3, padding1) self.pool4 nn.MaxPool1d(2) self.rnn nn.GRU(64, rnn_hidden, bidirectionalTrue, batch_firstTrue) 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_dim158是从论文 Tab.1 的输入维度推测的实际要以你 openSMILE 输出的特征列为准配置不同这个数字就可能变成 130 多或者 180 多网络第一层输入必须跟着改。第二四层池化窗口都是 2如果输入序列长度本来就是 50四次池化后时间维只剩下 3RNN 基本学不到时序依赖。论文里的做法是让部分卷积层步长为 1池化并不总在时间维上压缩所以你复现时一定要根据自己的序列长度调整池化次数这是音频分支最容易翻车的地方。2.4 论文没有明说的几个参数位置论文对音频网络结构的描述浓缩在一张表里关键信息是四层卷积的卷积核数量依次为 32、32、64、64卷积核大小为 3池化窗口为 2但步长是 1 还是 2 原文没写清楚。这里我建议按自己的输入序列长度反推目标是让进入 RNN 之前的时间维不低于 10否则双向 RNN 能看到的上下文太短。另一个值得注意的点是 RNN 用的是 BRNN也就是双向结构代码里用bidirectionalTrue实现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 微调冻结卷积层只动最后三个 FCVGG-Face16 是 16 层的 VGG 人脸识别预训练模型结构是五段卷积网络加三个全连接层加 softmax。论文做了一个很克制的选择只取最后一个全连接层的输出作为空间特征微调时冻结前面的卷积层只把最后三个全连接层随机初始化并更新。为什么不全量微调情感识别数据集通常只有几千到几万张人脸图而 VGG-Face16 是在百万级人脸数据上预训练的。全量微调意味着把已经学好的通用人脸表征推翻重来在小数据上极易过拟合表情细节还没学到loss 就下不去了。只微调 FC 层的逻辑是浅层卷积已经能提取边缘、纹理、五官结构这些通用特征真正需要重新学的是“什么样的面部组合对应什么情绪”这个映射关系而这正是 FC 层的职责。3.3 帧特征重排按相邻帧差异挑关键帧的代码思路视频长度不固定没法直接拼特征。论文的解决思路很工程化计算相邻两帧特征的差异差异太小说明表情几乎没变直接丢掉差异大说明情绪在变化保留这一帧。选完后再按时间顺序排列如果帧数不够固定长度就把第一帧复制几份补在开头。这个逻辑用 NumPy 实现非常直观import numpy as np def select_key_frames(feats, min_diff0.05, max_len50): 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 分数向量然后按类别加权求和取最大值对应的类别作为最终预测。对应论文公式 6C 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 sizelearning ratemomentumweight decay其他关键项音频子网络400.010.90.01epoch 600lr decay 0.1视频微调阶段320.0050.90.0015dropout 0.5BRNN 时序网络2560.0050.90.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. 避坑指南复现 CNNRNN 情感识别时最容易翻车的五个位置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 题那种复杂场景下多模态情感预测建模来的这篇论文给你的不是直接能跑的代码而是两个可以迁移的工程习惯。第一个是“单模态先跑通再谈融合”它把多模态问题拆成了三个可控子问题降低排错成本。第二个是帧特征重排的思路它用相邻帧特征差异做关键帧筛选比均匀抽帧更能保留情绪转折点这个技巧在视频片段不标准的任务里都能复用。论文的结构虽然简单但每一步都是围绕“数据不干净、长度不统一、类别不平衡”这些真实问题设计的。从那以后我复现情绪相关的多模态论文时都强制先跑通单模态、再碰融合权重这个顺序帮我省下了很多次半夜返工。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进