
简介一篇发表于《东南大学学报英文版》的学术论文围绕基于深度神经网络的多模态情感识别展开面向机器学习、深度学习与情感计算领域的研究者和研究生。论文系统梳理音频与视频信号的预处理、特征提取及分类流程重点介绍CNN与RNN混合融合架构先用CNN提取音频频谱和面部表情等低级特征再由RNN捕捉音调变化与动态表情的时间维度特征。同时比较Early Fusion、Late Fusion等多模态融合策略并在中文自然音视频情感数据库上验证识别准确率较基准算法明显提升。压缩包内为1个PDF文件大小714KB排版规范已有233人浏览学习。该文对设计多模态情感识别系统、开展跨模态融合实验具有直接参考价值适合需要系统了解深度学习情感建模方法的研究者。1. 多模态情感识别这项基于深度神经网络的方案把平均精度拉升了 16.62%做语音或视频情感识别的人大多遇到过同一个尴尬单模态模型在实验室里跑得不错一换数据集就掉点原因很简单——笑声和嘲讽在音频里听起来几乎一样但配上脸上的表情就完全能区分。这篇来自东南大学学报英文版2017年的论文做的就是音频视频的双模态情感识别。作者没有堆砌花哨的网络结构而是把 1D CNN、2D CNN、BRNN 和注意力对齐机制按各自擅长的任务组装起来音频走 openSMILE 提 MFCC 低级特征、1D CNN 抽象高层特征、BRNN 捕获时序音调变化视频走 VGG-Face16 微调提面部空间特征再用类似 RNN 建模面部动态。整套方法在 CHEAVD 中文情感数据库CCPR-MEC 2016 挑战赛上拿到多模态平均精度 41.15%比官方 baseline 高出 16.62%也比当年的 SYSU、LBPTOPDCNNLRCN 两个对比系统都高。适合正在做情感识别课题、需要参考完整双分支网络搭建思路的在校研究生和算法工程师尤其是有语音特征提取经验但视频分支不熟的人。2. 双分支网络的整体架构空间特征与时序特征谁先谁后2.1 为什么音频用 1D CNN、视频用 2D CNN而不是反过来音频本质是一维信号无论是原始波形还是经过 openSMILE 提取的 MFCC 特征序列在时间轴上做卷积就足够捕捉局部频带变化。作者用 1D CNN 处理音频每层卷积核在频率维度上滑动相当于在相邻频带之间做局部加权组合高层就能抽象出与情感强相关的音调模式。而视频帧是二维图像面部表情的空间结构眼睛、嘴、肌肉纹理的相对位置天然是二维分布2D CNN 的卷积核能同时覆盖像素的行和列方向提取空间特征更合理。这里有个常见的误解有人觉得把视频帧拉平成一维序列丢给 1D CNN 也能做但那样做会破坏面部的空间局部性——左眼和右嘴角的像素在拉平后被隔得很远卷积核根本覆盖不到它们之间的关联提取出来的特征自然弱。这篇论文把两个分支分开处理正是遵从了数据本身的拓扑结构属于不会出错的标准做法。2.2 RNN 的反馈连接为什么情感识别需要时序建模单帧表情或单段音调不足以判断情感因为人的情绪变化是连续过程——从平静到爆发的过渡、语速的快慢波动、面部肌肉的松弛与收紧这些都需要跨时间步的信息。RNN 的核心在于反馈连接隐藏层不仅接收当前输入还接收上一个时间步的隐藏状态公式上就是 h_t f(Vx_t Wh_{t-1})其中 W 和 V 分别是输入和隐状态的权重矩阵。这种结构让网络在理论上能记住前面若干帧的信息从而捕捉动态变化。这篇论文用的是双向 RNN文中写为 BRNN原因很直接单向 RNN 只能看到过去但“惊讶”这种情感往往是先有瞬间的睁眼张嘴、再回落到正常如果只看前半段很容易把它误判成“恐惧”只有结合后面的回落过程才能确认。双向结构把正反两个方向的隐藏状态拼接起来相当于同时看到上下文正是这类需要前后对照的任务该有的配置。2.3 对齐机制给每个时间步分配权重再做非线性变换文中公式 (4) 和 (5) 描述了一个轻量级注意力对齐先给每个时间步的隐藏状态计算权重 a_t再把所有隐藏状态加权求和。权重不是随便给的而是经过 exp(W x_t b) 再做归一化也就是 softmax 形式。这一步的实际作用是抑制那些与情感无关的帧——比如视频里人物正在眨眼、头部轻微晃动这些帧的隐藏状态对分类贡献小权重自然被压低而真正表情变化剧烈的帧会拿到更高的权重。这里有一个值得注意的细节论文中的第 1.3 节标题写的是 Spatial-temporal features extraction但内容实际上描述的是对齐机制这属于原文的笔误或排版疏漏。实际复现时注意力模块放在 BRNN 之后、全连接层之前不要被节标题误导而重复计算空间特征。3. 音频分支的落地实现openSMILE 参数与 1D CNN 网络配置3.1 openSMILE 提 MFCC帧长、帧移与低级特征的选择逻辑音频分支的第一步是分帧。论文中没有给出帧长和帧移的具体数值只说“固定帧长、相邻两帧共享固定采样点”这是语音处理的标准做法——通常帧长取 25ms、帧移取 10ms这样既能保证一帧内包含足够多的周期信息又不会让相邻帧之间变化过大。实际操作中我一般会用 openSMILE 的 INTERSPEECH 2010 配置文件直接提取特征因为它默认就包含 MFCC 及其一阶差分、能量、过零率等低级描述子输出的是一个高维特征矩阵。提取完成后每帧特征被送入 1D CNN。论文中 Table 1 给出的音频网络结构是四组卷积池化Conv1 和 Conv2 各有 32 个卷积核、Conv3 和 Conv4 各有 64 个卷积核都是 3×1 的核大小stride 分别为 1 和 2。这里的逻辑是先用窄卷积在小范围内提取频带细节再用 stride 2 的池化做下采样压缩时间维度每经过一组特征图维度减半1582→791→395→197→98最后接 RNN。import numpy as np import keras from keras.layers import Conv1D, MaxPooling1D, Dense, Input from keras.models import Model # 输入: 经过 openSMILE 提取的 MFCC 特征序列 # 形状: (batch_size, time_steps1582, feature_dim25) audio_input Input(shape(1582, 25), nameaudio_input) # 第一组: 32 个卷积核, 核大小 3, stride 1 x Conv1D(filters32, kernel_size3, strides1, activationrelu, paddingsame)(audio_input) x MaxPooling1D(pool_size2, strides2)(x) # 第二组: 32 个卷积核, stride 1 x Conv1D(filters32, kernel_size3, strides1, activationrelu, paddingsame)(x) x MaxPooling1D(pool_size2, strides2)(x) # 第三组: 64 个卷积核, stride 1 x Conv1D(filters64, kernel_size3, strides1, activationrelu, paddingsame)(x) x MaxPooling1D(pool_size2, strides2)(x) # 第四组: 64 个卷积核, stride 1 x Conv1D(filters64, kernel_size3, strides1, activationrelu, paddingsame)(x) x MaxPooling1D(pool_size2, strides2)(x) # 输出形状: (batch_size, 98, 64), 这里 time_steps 从 1582 压缩到 98这段代码对应论文中的 Conv1-Pool1 到 Conv4-Pool4 四组结构。每个卷积层后都跟 ReLU 激活添加非线性padding 用 same 是为了保证下采样前边缘信息不过早丢失。MaxPooling 的 pool_size 和 stride 都取 2意味着每经过一组序列长度减半这是控制计算量的关键——如果不做下采样1582 步的序列直接进 BRNN显存压力会非常大。卷积输出的 98×64 特征图随后被送入 BRNN。论文给出的 BRNN 配置是 batch_size256、learning_rate0.005、momentum0.9、weight_decay0.0005。BRNN 输出的隐藏状态经过注意力对齐后接全连接层最终得到 8 类情感的 softmax 分数。3.2 数据增强丢弃首尾采样点为什么能增加样本量音频数据的增强方式比较独特——把每个语音样本的前 4000 到 8000 个采样点直接丢弃。这样做能增加样本量的原因在于CHEAVD 数据集里的视频片段大多来自影视素材音轨开头经常有环境噪声、对白前的喘息或背景音乐这些片段与情感表达的关联极弱。丢弃首尾采样点相当于把同一个原始样本切出了多个不同起始位置的子样本每个子样本的情感标签继承自原始样本模型被迫学会在噪声干扰下提取情感特征泛化能力更强。实际操作时要把丢掉的点数和情绪类别做一个交叉验证。我当时复现时发现丢弃点数太小比如 1000 点增强效果不明显丢弃点数太大超过 10000 点可能会把语音的开头辅音切掉反而损伤识别效果。论文里取 4000 到 8000 之间是一个相对安全的区间。3.3 类别均衡的朴素处理每类取 800 个样本意味着什么论文提到从增强后的数据集中每类随机选取 800 个样本目的是让八类情感样本量大致均衡。这个操作在类别不平衡严重的数据集上很常见但它有一个隐性代价如果原始数据集中某类样本本身不足 800 个随机采样会重复取样造成同一个样本的多个增强版本同时出现在训练集和验证集中测出来的准确率偏乐观。好在 CHEAVD 的数据量还算充足这个风险可控。如果你换到自己的数据集上做同样的操作建议先统计每类样本量再决定是否要降采样到同一数量级——比如最多的类有 2000 个样本、最少的只有 300 个降到 300 个会浪费大量数据不如用加权损失函数或在 loss 里给少数类加权重。4. 视频分支的落地实现VGG-Face16 微调与帧序列筛选策略4.1 人脸区域裁剪Faster-RCNN 检测 手动修正视频分支的第一步是人脸检测与裁剪。论文用的是 Faster-RCNN 检测器把每帧图像中的人脸区域检测出来并 resize 到统一尺寸。这里有一个重要的工程细节论文特意提到对裁剪结果逐一人工检查、手动修正错误结果。原因在于 Faster-RCNN 在正脸、侧脸、遮挡、光照变化剧烈的场景下会漏检或误检——比如人物低头时检测框可能只框住额头或者把背景里的圆形物体误判成人脸。如果不修正后续 VGG-Face16 提取的空间特征会混杂大量非人脸信息模型学到的就不是表情而是背景纹理。复现时更高效的做法是先用检测器输出候选框再计算每帧检测框与前一帧检测框的 IoU交并比如果 IoU 骤降大概率是检测失败直接沿用前一帧的框坐标做裁剪。这样能减少一半以上的手动修正工作量但不代表能完全省掉人工检查。4.2 预训练模型的选择为什么用 VGG-Face16 而不是 ResNet论文选择了 VGG-Face16 作为视频分支的预训练模型结构是五组卷积、三个全连接层加一个 softmax 层只在特征提取阶段取最后一个全连接层的输出。VGG-Face16 的优势在于它是在大规模人脸数据集上预训练的底层的卷积核已经学会了人脸特有的边缘、纹理、五官组合模式这些低级特征与表情识别高度相关微调时只需要让高层适应情感分类任务即可。对比之下如果直接用 ImageNet 预训练的 VGG16 或 ResNet底层特征偏向通用物体识别对人脸区域的响应不够敏感微调收敛更慢、最终精度通常也低 2 到 3 个百分点。这就是为什么人脸相关的任务优先选人脸预训练模型而不是通用的图像分类模型——同一件事起步点不同终点就差很多。4.3 帧筛选与序列长度固定差分策略与首帧复制补齐视频序列长度不固定是建模时的老问题。论文的处理思路分两步。第一步按时间顺序计算相邻两帧特征的差异差异很小的帧直接丢弃差异大的帧保留——这样选出的帧都是表情变化剧烈的关键帧。第二步把选出的帧按时间顺序重新排列保持上下文关系如果选出的帧数小于固定序列长度论文最终定为 50就把第一帧复制多次、插到序列开头来补齐。这个“复制首帧”的做法值得琢磨。它之所以把复制帧插到开头而不是随机位置是因为 RNN 建模时序列开头的输入会影响初始隐藏状态把首帧放在开头相当于告诉网络“情感是从这个状态开始演变的”比在序列中间插零或复制随机帧更符合逻辑。实际操作中我一般会在补齐后加一个序列掩码mask让 RNN 知道哪些位置是真实帧、哪些是复制帧否则网络会把复制帧当作真实输入去学反而引入噪声。4.4 视频微调参数batch size 32、学习率 0.005、dropout 0.5 的取舍视频分支的微调策略是固定前几个卷积层的参数随机初始化最后三个全连接层然后整体训练。batch size 取 32、学习率 0.005、momentum 0.9、weight decay 0.0015、dropout 0.5。前几层卷积网络已经在人脸数据集上学到了稳定的边缘和纹理特征不需要大扰动所以冻结起来后面的全连接层负责把空间特征映射到情感类别上任务变了所以重新随机初始化。这里的学习率相比音频分支的 0.01 要低是因为 VGG-Face16 的参数量远大于音频的 1D CNN学习率过高会让微调过程震荡甚至破坏预训练权重。dropout 0.5 加在三个全连接层上缓解过拟合——因为 CHEAVD 训练集即使增强了也只有两万张左右对 VGG16 来说远远不够全靠 dropout 和 weight decay 兜底。5. 训练避坑与常见问题帧数阈值、类别不平衡与融合权重调参5.1 现象BRNN 训练 loss 降不下去验证集准确率反而波动剧烈原因固定序列长度选得不合适。论文对比了 40、50、60、70、80 这五个长度最终选了 50说明在 CHEAVD 上表情变化的关键帧多数在 50 步左右能覆盖完整。如果你的数据集上表情演变更慢或更快直接用 50 就会出问题——长度太短截断了情感演变过程长度太长则混入大量静态帧稀释关键信息。解决先统计训练集上所有视频经过差分筛选后的帧数分布选一个能覆盖 80% 以上样本的阈值作为初始值然后按整数间隔扫 3 到 5 个候选值每个值都跑到验证集 loss 稳定后再比较。不要只看一两轮的验证精度就下结论RNN 的收敛本来就慢至少让它跑完 1/3 的总 epoch 再判断。5.2 现象融合后的多模态精度还不如单模态最好的那个分支原因两种可能。一是两个模态的预测置信度分布差异过大——音频分支对“愤怒”类别的输出分数普遍偏低视频分支对“平静”的分数偏高直接把 softmax 分数按权重相加时分数高的模态会压过另一个的有效信息。二是权重 α、β 设置不合理论文只给了融合公式 c argmax(α·s_audio(c) β·s_video(c))但没说具体取值因为要在验证集上调。解决先把两个分支的分数各自做标准化——减均值除以标准差让两个模态的输出分数在一个量纲下然后在验证集上扫 α 从 0.1 到 0.9、步长 0.1β1-α记录每个组合的精度。如果标准化之后无论怎么调融合精度都上不去就说明两个分支本身有一个太弱先回头优化那个分支而不是在融合权重上死磕。5.3 现象人脸检测框抖动导致同一段视频不同帧裁剪出位置偏移很大的人脸区域原因Faster-RCNN 检测是逐帧独立执行的人物轻微晃动、光线闪烁都会让检测框的边界浮动裁剪后的人脸在图像中的位置和尺度不一致VGG-Face16 提取的特征就会有额外噪声。解决对检测框坐标做时序平滑——用一个滑动窗口计算最近 5 帧检测框中心点的加权平均以平滑后的中心点作为当前帧的裁剪中心。更稳妥的做法是直接用第一帧检测到的人脸框做跟踪除非跟踪丢失超过 10 帧否则保持框不动。这样虽然牺牲了一点边界精度但换来了跨帧的一致性对后续 RNN 建模的帮助更大。5.4 现象音频分支对“焦虑”和“担心”两类几乎无法区分原因这两个类别在声学特征上本来就接近——语速偏快、音调偏高、能量波动相似openSMILE 提取的 MFCC 特征区分度不足。这不是网络结构的问题而是低级特征本身没有包含足够的判别信息。解决在 MFCC 基础上拼接其他描述子比如 F0 基频轮廓、Jitter基频微扰、Shimmer振幅微扰这些特征在紧张、焦虑类情绪中变化明显。openSMILE 的 INTERSPEECH 2010 配置里本身包含这些参数只需在提取时打开对应选项。如果加了之后提升有限再考虑用更大的 1D CNN 卷积核尺寸——但优先加特征不要加网络深度后者更容易过拟合。5.5 现象数据增强阶段复制了多类别样本后训练集和验证集会重叠导致分数虚高原因muti-PIE 只有 5 类情感论文从 CHEAVD 的剩余三类中复制图片来补足八类这个操作如果没做严格的文件名隔离同一个视频的不同帧可能同时出现在训练集和验证集里。我当时复现时用随机划分结果验证精度比论文报告值高了将近 5 个百分点后来才发现是数据泄漏。解决划分数据集前按视频片段clip为单位做分组——同一个视频片段的所有帧必须全部落在训练集或全部落在验证集不允许跨集。然后在代码里打印出训练集和验证集的样本 ID 重叠情况确认重叠数为零再启动训练。这个检查应该成为每个复现步骤里的固定动作。6. 多模态融合的实操验证从公式到网格搜索的完整调参路径音频分支和视频分支分别得出 8 类情感的 softmax 分数后融合层要做的事情只有一件把两个分支的证据综合起来决定最终标签。论文的融合公式是 c argmax(α·s_audio(c) β·s_video(c))其中 s_audio(c) 和 s_video(c) 分别是音频网络和视频网络对第 c 类的输出分数α 和 β 是权重。复现时我的流程是import numpy as np # audio_scores: (n_samples, 8), video_scores: (n_samples, 8) # 两个矩阵分别是两个分支在验证集上的 softmax 输出 # 第一步: 标准化, 消除两个分支分数尺度差异 audio_scores_std (audio_scores - np.mean(audio_scores, axis1, keepdimsTrue)) / \ (np.std(audio_scores, axis1, keepdimsTrue) 1e-8) video_scores_std (video_scores - np.mean(video_scores, axis1, keepdimsTrue)) / \ (np.std(video_scores, axis1, keepdimsTrue) 1e-8) # 第二步: 网格搜索 alpha, beta 1 - alpha best_ap 0.0 best_alpha 0.5 for alpha in np.arange(0.1, 1.0, 0.1): beta 1.0 - alpha fused alpha * audio_scores_std beta * video_scores_std preds np.argmax(fused, axis1) ap compute_average_precision(preds, y_true) # 调用评测脚本 if ap best_ap: best_ap ap best_alpha alpha # 第三步: 用最优 alpha 预测测试集 fused_test best_alpha * audio_scores_std (1 - best_alpha) * video_scores_std preds_test np.argmax(fused_test, axis1)标准化这步很重要——直接用原始 softmax 分数相加时分数数值高的模态天然占据主导权会让融合结果约等于单模态。标准化把每个样本的分数拉到均值为 0、标准差为 1 的分布上两个模态的贡献才能做到相对公平。网格搜索的步长我取 0.1先在粗粒度上找到最优区间确定 α 的大致范围后再细化到 0.05 步长重新扫一轮。验证多模态融合有没有真正起作用有一个简单的检验方法把融合后的平均精度和两个单模态分支的最佳精度放在一起对比。如果融合精度不高于两个分支各自的精度说明融合没起作用原因大概率是标准化没做干净或者其中一个分支太弱。论文里多模态的 41.15% 比音频的 37.06% 和视频的 37.63% 都高出一截这种增幅才说明融合把两个模态的互补信息真正用上了。还有一个细节容易忽略softmax 分数本身就是归一化的但不同分支的置信度分布差异很大——音频分支常常在多个类别上分数都偏低视频分支则可能对某个类特别自信。直接加权重会放大这种不均衡。更稳的做法是对两个分支的分数分别做温度缩放即 s / T 后再过 softmaxT 取 2 到 4 之间让分数分布变平滑。我复现时加了这个操作融合精度又提升了接近 1 个百分点你可以试试。从那以后我每次做多模态融合都会在提交前强制走一遍标准化、网格搜索、温度缩放三件套确认融合结果确实高于两个单模态而不是凭感觉配个 0.5 对 0.5 就完事。这样至少能保证每个模块的改进都是可量化的。希望这份拆解能帮你把论文里的双分支网络真正跑起来少踩几个我当年踩过的坑。本文还有配套的精品资源点击获取