
简介《基于深度神经网络的多模态情感识别》英文版PDF是一篇发表于《东南大学学报英文版》的学术论文主要面向深度学习、情感计算、人机交互等领域的研究者、研究生及高年级本科生旨在解决如何将音频与视频双模态情感信息有效融合并提高识别准确率的问题。论文详细介绍了卷积神经网络在语音频谱和面部表情低级特征提取中的具体用法循环神经网络对音调变化及时序表情动态的捕捉能力并提出了混合卷积与循环网络框架同时讨论了音频信号处理、视频特征选择以及早期融合与晚期融合等多模态融合策略。全文为英文原文包含基于中文情感数据库的实验过程、准确率对比与结论分析可供模型复现、实验参考或论文写作时引用。资源仅有1个PDF文件大小约714KB体积紧凑便于全文精读。目前已有233人学习下载适合作为深度学习相关课程设计、毕业设计和大作业的文献研读材料。1. 多模态情感识别为什么值得做光听声音远远不够不管是英文论文还是工业落地项目里基于深度神经网络的多模态情感识别已经是人机交互、智能客服质检、车载情感感知等场景的标配方案。我经常碰到有人拿着英文论文来问单模态的准确率已经不错了为什么还要折腾三个模态一个很典型的反直觉结论是真实场景里的情感从来不是单一信号承载的语音里说的“干得漂亮”配上不屑的语气和翻白眼的微表情单看文本或单听音频都会误判成正面只有把文本、音频、视觉三路信息一起送进深度神经网络讽刺、愤怒、焦虑这些复合情感才分得清。这篇文章就按我实际做过的方案把这个方向从数据预处理、模态融合、网络结构、训练参数到推理落地完整拆开讲一遍适合想做情感计算、智能交互标签系统或复现英文论文实验的工程师直接参照。2. 三模态选型与数据预处理文本、音频、视觉各自的坑2.1 为什么是文本、音频、视觉三路输入多模态情感识别里模态数量不是越多越好。公开数据集里最常见的组合是文本-音频-视觉三模态原因很本质三个模态分别覆盖情感表达的三个侧面语义内容靠文本韵律和语调靠音频面部表情和头部姿态靠视觉。加上生理信号比如心电、皮电理论上信息更全但采集成本高、标注困难工程上很难落地我一般不推荐在第一个版本就上。文本模态负责“说了什么”常见做法是先用自动语音识别把音频转成文本再对文本做词向量编码用预训练语言模型提特征比从头训词向量稳定得多。音频模态负责“怎么说的”语速、音高、能量起伏这些韵律特征用openSMILE或librosa提取低层描述符就够用。视觉模态负责“表情怎么变”面部动作单元和表情分类特征可以用openface或mediapipe提取。三条数据源做好预处理和对齐之后才轮到深度神经网络。2.2 音频特征提取从波形到低层描述符音频特征提取是整个流程里最成熟也最容易出问题的一步。我用librosa比较多这里给出一个能直接用的代码片段import librosa import numpy as np def extract_audio_features(wav_path, sr16000, n_mfcc13, max_len200): # 统一采样率到16kHz中文语音识别和情感识别的关键参数 y, sr librosa.load(wav_path, srsr) # 提取MFCC13维通常够用40维会引入更多噪声 mfcc librosa.feature.mfcc(yy, srsr, n_mfccn_mfcc) # 加入一阶差分捕捉音高和能量的动态变化 delta librosa.feature.delta(mfcc) delta2 librosa.feature.delta(mfcc, order2) features np.concatenate([mfcc, delta, delta2], axis0) # 时间步对齐固定到200帧不足补零超过截断 if features.shape[1] max_len: features np.pad(features, ((0, 0), (0, max_len - features.shape[1])), modeconstant) else: features features[:, :max_len] return features.T # 返回形状 (时间步, 特征维度)这里的核心参数是采样率16kHz英文论文里很多用44.1kHz但实际跑下来16kHz在情感识别任务里损失很小训练速度却能快三分之一。MFCC取13维是经验值加上差分后变成39维既能描述短时频谱包络也能捕捉动态变化。固定时间步到200帧对应约5秒音频比这个更长的音频建议先做切片——情感通常是按句或按段变化的整段平均会把关键信息稀释掉。2.3 文本与视觉特征预训练Embedding与面部动作单元文本模态的常规操作是加载一个预训练模型做序列编码。我一般用中文场景下的BERT类模型取最后一层隐状态的平均池化作为整个句子的语义向量。如果你用的英文数据集直接加载英文预训练模型同理。代码示意如下import torch from transformers import AutoTokenizer, AutoModel def extract_text_features(text, model_namebert-base-chinese, max_len128): # 先把模型和分词器下载到本地避免每次实验重复联网 tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModel.from_pretrained(model_name) inputs tokenizer(text, max_lengthmax_len, paddingmax_length, truncationTrue, return_tensorspt) with torch.no_grad(): outputs model(**inputs) # 取所有token隐状态的均值作为句子向量 sentence_embedding outputs.last_hidden_state.mean(dim1) return sentence_embedding.squeeze().numpy()视觉模态我用mediapipe提取面部动作单元和关键点坐标它比openface部署简单效果在近距离会议场景下足够。提取到的特征要按帧记录时间戳这是后面做模态对齐的基础。文本特征这里有一个很容易踩的坑如果数据本身就是英文文本就不要加载中文预训练模型跨语言嵌入在情感任务里表现会明显下滑。预训练模型的下载路径和版本我会在工程脚本里固定好避免实验之间特征分布漂移。2.4 模态对齐把三条时间线拧到同一根轴上这是多模态情感识别预处理里最不受重视却最容易翻车的一步。音频采样率是16000Hz视频帧率通常是25帧每秒或30帧每秒文本是按词按句出现的三个模态的时间刻度完全不同。不对齐就做融合深度神经网络会学到乱七八糟的时间偏移效果反而不如单模态。我采用的做法是按话语段对齐先根据音频端点检测切出每个说话片段再把文本按时间戳映射到对应片段最后把视频帧和音频统一重采样到同一个时间分辨率。代码逻辑如下def align_modalities(audio_feats, video_feats, text_feats, video_fps25, target_fps10): # 将视频帧从原始fps重采样到目标fps old_indices np.linspace(0, video_feats.shape[0] - 1, int(video_feats.shape[0] / video_fps * target_fps)) new_indices np.linspace(0, video_feats.shape[0] - 1, video_feats.shape[0]) aligned_video np.interp(old_indices, new_indices, video_feats, axis0) # 音频按时间步裁切到和目标fps同样的时间长度 audio_per_frame audio_feats.shape[0] / video_feats.shape[0] aligned_audio audio_feats[::int(audio_per_frame)] # 用最短时序长度截断所有模态防止维度不一致 min_len min(aligned_audio.shape[0], aligned_video.shape[0], text_feats.shape[0]) return aligned_audio[:min_len], aligned_video[:min_len], text_feats[:min_len]对齐之后有个验证习惯值得养成把三个模态的特征打印成热力图肉眼检查时间轴上的峰谷是否对应同一个情感爆发点。这一步虽然麻烦但能过滤掉数据管线里百分之八十的时序错位问题。3. 模态融合策略选型早期、晚期、混合融合怎么定3.1 早期融合特征拼接的特征维度爆炸早期融合又叫特征级融合最直观的做法就是把三个模态的向量拼在一起喂给深度神经网络。比如文本得到768维向量音频得到39维视觉得到80维直接拼接成887维输入。这个方案看起来省事但实际使用中很快会遇到问题三个模态的特征分布差异极大文本embedding的值域和MFCC差出好几个数量级拼接后网络会不自觉地把数值大的特征当成主导信号训练出来的模型几乎只靠某一个模态在猜。更麻烦的是维度爆炸。如果按时间序列特征做拼接而不是句子级拼接每个时间步都有几百维叠加网络参数后小数据集很容易过拟合。我见过有人把三模态特征全拼在一起丢给LSTM结果在验证集上训了二十轮还在震荡。所以早期融合在工业落地里并不受宠除非你的数据集足够大而且先做了特征归一化和降维。如下做法会稳很多先把每个模态各自过一层全连接压缩到64维再拼接成192维。本质上已经变成混合融合的思路了。3.2 晚期融合各模态独立出分再投票晚期融合也叫决策级融合每个模态先训练一个独立的情感分类模型最后把三个模型的输出分数融合成最终结果。这个方案最大的优点是容错性强某一个模态信号质量差时其他模态仍然能正常工作。在真实线上环境里一段视频可能画面模糊、麦克风噪音又大这时三个模型同时翻车的概率比单模型翻车小。这也是我建议作为系统第一版落地的融合方式因为三个单模态模型是独立训练的整个流程简单又直观。晚期融合最常见的做法是加权求和。每个类别的最终得分等于三个模态概率输出的加权和def late_fusion(text_score, audio_score, visual_score, weights(0.3, 0.3, 0.4)): # text_score/audio_score/visual_score 是各模态输出的类别概率向量 # weights 是各模态的置信度权重通过验证集搜索得出 final_score weights[0] * text_score weights[1] * audio_score weights[2] * visual_score predicted_class int(np.argmax(final_score)) return predicted_class, final_score权重怎么定是个关键问题。常见做法是拿验证集做网格搜索词搜在一个小范围里。如果数据本身质量均衡直接用等权重往往也不差。我在做智能客服质检时遇到过一个特殊情况音频里混着大量背景音乐音频模态的置信度总是偏小我把音频权重调到0.15之后整体准确率反而提高了两个点。所以权重不是拍脑袋定的而是根据每个模态在验证集上的表现单独搜索出来的。3.3 混合融合用注意力给模态分配权重混合融合是目前效果最稳的做法也是英文论文里最常见的架构。它同时保留模态特异的特征和模态间的交互并用一个注意力模块动态决定当前样本应该更相信哪个模态。跟晚期融合的固定权重相比注意力机制能做到根据输入内容实时调整权重——同一个系统里句子“我没事”配愤怒语调时文本权重低配平静语调时文本权重高。我常用的简化注意力的实现是三个模态经过各自的编码器得到特征向量然后把三者堆叠成二维矩阵过一层可学习的注意力打分层再对三个模态做加权求和最后送进分类头。代码骨架如下import torch.nn as nn import torch.nn.functional as F class AttentionFusion(nn.Module): def __init__(self, embed_dim, num_modalities3): super().__init__() # 打分网络把每个模态的特征映射成一个标量权重 self.attn nn.Sequential( nn.Linear(embed_dim, embed_dim // 2), nn.ReLU(), nn.Linear(embed_dim // 2, 1) ) self.classifier nn.Linear(embed_dim, num_classes) def forward(self, modality_features): # 输入形状 (batch, num_modalities, embed_dim) scores self.attn(modality_features).squeeze(-1) # (batch, num_modalities) attn_weights F.softmax(scores, dim1) # 加权求和得到融合特征 fused (modality_features * attn_weights.unsqueeze(-1)).sum(dim1) return self.classifier(fused)混合融合的训练难度比晚期融合高因为它需要三个模态一起反向传播特征提取部分的收敛节奏不一致时可能出现一个模态学好了另一个还没收敛。这时候需要用较小的学习率配合分阶段训练先冻结其他模态只训练融合层再解开全部参数微调。我实际做下来这个方案能比晚期融合在加权准确率上高出三到五个百分点代价是训练时间和调参成本多了一倍不止。4. 深度神经网络结构落实CNN、双向GRU与注意力模块怎么拼4.1 卷积神经网络在文本与频谱里的局部建模把深度神经网络结构落实的时候卷积神经网络在文本和音频特征上的局部建模能力非常实用。文本的局部n-gram语义、音频频谱上的局部模式都适合用一维卷积去抓。常见做法是对文本序列或音频特征做一维卷积再加池化层逐层提炼。比如文本用卷积核大小为3和5的两路卷积分别抓取二元和多元词组特征音频在时间维上做卷积捕捉语调变化片段。import torch.nn as nn class ConvFeatureExtractor(nn.Module): def __init__(self, input_dim, conv_channels64): super().__init__() # 两个不同大小的卷积核并行提取局部特征类似N-gram效果 self.conv1 nn.Conv1d(input_dim, conv_channels, kernel_size3, padding1) self.conv2 nn.Conv1d(input_dim, conv_channels, kernel_size5, padding2) self.pool nn.AdaptiveMaxPool1d(1) def forward(self, x): # x形状: (batch, seq_len, input_dim)需要转成通道优先 x x.transpose(1, 2) h1 torch.relu(self.conv1(x)) h2 torch.relu(self.conv2(x)) h torch.cat([h1, h2], dim1) h self.pool(h).squeeze(-1) return h卷积核大小和通道数是这个模块最值得调的两个参数。经验值是多模态任务用64通道起步文本特征比音频特征更依赖大卷积核因为语义模式跨度更大。从英文论文的对比实验来看卷积核3和5的并联结构是一个性价比很高的基线。需要注意的是这里的输入必须是已经对齐到同一时间刻度的序列特征不然卷积的感受野在时间维上是错位的。4.2 双向GRU与时序建模情感在时间序列上的变化非常依赖上下文前半句平铺直叙后半句突然爆发这种动态模式是单向时序模型捕捉不到的特征。双向GRU在效果和训练速度之间比其他时序模型均衡比双向LSTM省参数、收敛快适合数据量不算特别大的场景。class BiGRUEncoder(nn.Module): def __init__(self, input_size, hidden_size128, num_layers2, dropout0.3): super().__init__() self.gru nn.GRU(input_size, hidden_size, num_layersnum_layers, bidirectionalTrue, dropoutdropout, batch_firstTrue) self.output_proj nn.Linear(hidden_size * 2, hidden_size) def forward(self, x): # x形状: (batch, seq_len, input_size) outputs, hidden self.gru(x) # 取最后一个时间步的双向拼接结果 last_step outputs[:, -1, :] return torch.relu(self.output_proj(last_step))双向GRU在这里把每个时间步的过去和未来信息都编码进隐状态取最后一步的输出作为全片段的语义浓缩。隐层大小128是情感识别任务里常见的起点如果序列长度特别长比如超过500帧可以尝试加到256。层数不是越多越好两层是我在多数数据集上的经验上限再加深在几千规模的训练集上几乎必过拟合。dropout这里设0.3是针对中等规模数据的推荐值如果你发现验证集损失震荡加剧优先提高dropout而不是降低学习率。4.3 完整模型骨架与训练参数推荐把特征提取、时序编码和融合模块拼成一个端到端网络是复现英文论文实验的标准姿势。常见架构是各模态先用卷积提局部特征再过双向GRU编码时序然后进注意力融合层最后过全连接分类头。核心代码如下class MultimodalEmotionNet(nn.Module): def __init__(self, audio_dim39, text_dim768, visual_dim80, num_classes7): super().__init__() self.audio_conv ConvFeatureExtractor(audio_dim, 64) self.text_conv ConvFeatureExtractor(text_dim, 64) self.visual_conv ConvFeatureExtractor(visual_dim, 64) self.audio_gru BiGRUEncoder(64, 128) self.text_gru BiGRUEncoder(64, 128) self.visual_gru BiGRUEncoder(64, 128) self.attention_fusion AttentionFusion(128) def forward(self, audio, text, visual): a self.audio_gru(self.audio_conv(audio)) t self.text_gru(self.text_conv(text)) v self.visual_gru(self.visual_conv(visual)) features torch.stack([a, t, v], dim1) # (batch, 3, hidden) return self.attention_fusion(features)训练参数上我一般用AdamW优化器初始学习率在3e-4到5e-4之间batch size取32或16视显存而定。学习率调度用余弦退火配合前两步的warmup损失函数对七分类情感用交叉熵如果是valence-arousal这种维度模型就换MSE或Huber。显存不够时不要硬上大batch可以考虑梯度累积效果稳定且几乎不损失精度。训练过程里面还有一个很关键的细节音频和视觉特征的序列长度往往比文本长需要在进入融合层之前确保三路特征维度完全一致否则torch.stack直接报错。5. 五种常见故障排查血泪经验里的翻车现场5.1 模态缺失视频没声音模型直接摆烂现象是线上推理时某个样本只有文本和画面没有音频流模型输出概率分布错乱甚至崩溃。原因是训练时三模态输入总是完整的模型没有见过模态缺失的情况推理时缺了一路输入后融合权重计算出的分布完全失真。解决方法是训练阶段做模态随机丢弃以一定概率把某个模态的输入置零让注意力融合层学会在缺路时重新分配权重推理阶段缺失的模态同样补零。我一般把丢弃概率设为0.2太低学不到鲁棒性太高会拖累正常样本的收敛。5.2 标签噪声同一个样本不同标注者打分差异极大情感标注天然带有主观性同一个音频片段有人标愤怒有人标焦虑原因是情感类别的边界本身模糊加上标注者不同文化背景差异。英文论文里一般会报告标注一致性指标实践中如果Kappa系数低于0.4说明标注质量已经不可信了。解决方法是做标注清洗把标注者一致率低的样本剔除或改为软标签用多轮投票而不是一个标注者的结果。这个步骤虽然耗时间但直接影响模型上限数据不行再好的网络结构也白搭。5.3 数据不均衡生气样本太少模型永远预测中性情感数据天然长尾分布中性类占大头生气惊讶恐惧这类类别占比往往不到百分之五。如果不做任何处理模型学到的就是“永远预测中性”的解法整体准确率看着不低但实际任务里用户真正关心的正是那些少数的负面情绪。解决办法有两个最直接的是用类别加权交叉熵给少类样本更大的损失权重进阶的做法是Focal Loss让模型把注意力集中在难分类的样本上我用下来在少类别上的F1能提升十个百分点左右。数据增强对音频加噪和视频裁剪也能缓解但效果不如损失函数来得直接。5.4 特征维度不一致MFCC、词向量、面部特征量纲差出两个数量级这个问题的现象是训练初期loss掉得很快但验证准确率一直原地踏步查看注意力权重发现模型几乎只盯着数值尺度最大的那个模态。原因是文本预训练embedding的数值范围一般在正负几之间而MFCC特征动辄几十上百数值更大的特征天然主导了距离计算和梯度更新。解决方法是在特征进入网络之前做z-score标准化或用BatchNorm前置层标准化的均值和方差必须从训练集统计不要把测试集混进来计算否则会引入数据泄漏。5.5 融合后反而不如单模态网络结构越复杂效果越差这个现象我在实验里遇到过好几次多模态模型在验证集上打不过最好的单模态模型融合个寂寞。原因通常是数据量撑不起模型的参数量三模态网络比单模态网络的参数量大了好几倍几千条样本根本不够学。解决办法是先检查每个单模态模型的表现找最强的那个作为基线然后从晚期融合起步逐步加复杂度。如果数据集规模有限混合融合不一定优于晚期融合这时候果断降级方案而不是死磕结构。思路清晰的话先用轻量网络跑通全流程再考虑上重模型方向不能跑偏。6. 评估指标与部署优化模型好不如下线稳6.1 评估指标看WA、UA、F1和混淆矩阵多模态情感识别里只盯着整体准确率一定会栽跟头。数据不均衡的背景下整体准确率会掩盖少数类完全不可用的事实。我一般同时看加权准确率、未加权准确率和宏平均F1三个指标其中未加权准确率对类别均衡性最敏感能及时暴露模型是否放弃少数类。混淆矩阵更是排查必备能直接看出哪些情感类别之间容易被混淆最常见的混淆对是“愤怒”和“厌恶”“中性”和“平静”也经常分不开这往往不是模型问题而是标注定义不清晰。6.2 推理加速与断点续训上线前必须做的两件事深度神经网络模型训练完只是第一步落地上线前一定要做推理优化。PyTorch模型直接部署会浪费大量性能和显存常见的做法是先转TorchScript简单几行代码就能让推理速度提升两到三倍。核心代码import torch # 必须在模型所在的同一环境下执行确保结构和参数一致 model.eval() example_input { audio: torch.randn(1, 200, 39), text: torch.randn(1, 200, 768), visual: torch.randn(1, 200, 80) } traced_model torch.jit.trace(model, tuple(example_input.values())) traced_model.save(emotion_model.pt)另外长时间训练必须定期保存检查点我习惯每五个epoch存一次同时记录最优验证指标对应的权重。哪怕训练中途断掉也能从检查点续跑不浪费此前几十个小时的计算。这个习惯在调参阶段特别重要很多次是改了学习率发现效果变差回滚到最优检查点后重新小步微调才稳住结果。6.3 从单模态基线开始一套能复用的迭代习惯我这套方案做下来最大的体会是不要一开始就调三模态融合的大模型而是先把每个单模态的模型各自调到最好。先让文本、音频、视觉三个单模态模型分别出基线再把它们组合起来做融合实验。因为多模态融合效果的上限通常由最强的单模态决定如果单模态基线本身就很差融合层再努力也救不回来。数据质量永远是第一位我吃过太多教训在数据没清洗干净时反复调模型参数最后发现是数据泄漏导致指标虚高。每次实验固定随机种子、固定数据划分、记录训练配置这些看似枯燥的习惯能让你在复现调整时省下无数时间。希望帮到你动手之前先把数据管线跑通再谈模型优化也不迟。本文还有配套的精品资源点击获取