
简介一套多模态情感分析方向的深度学习模型资源包面向毕业设计、课程项目与科研入门场景重点解决多模态表示学习、多模态融合以及多模态情感分析等下游任务中的建模与实验问题。项目包含多种有代表性的模型实现和多个公开多模态数据集如情感识别、讽刺检测、对话场景样本支持文本、音频、视觉信号的特征提取与融合并提供训练、验证与评估脚本。压缩包共185个文件大小约285.97MB主要包含41个Python源码文件用于模型构建与实验流程21个pickle和8个h5用于保存参数与中间特征18个arff和36个csv用于数据输入与标签12个markdown文档提供说明。已有1495人学习下载。借助清晰的项目目录和PyTorch、Keras、TensorFlow工程读者可以快速复现从数据预处理、特征对齐、模型训练到情感分类评估的完整实验闭环直接修改网络结构或数据路径即可迁移到自有数据集适合作为课题基线或毕业设计基础。1. 多模态情感分析为什么各种融合模型都在抢这个任务一段视频评论里博主嘴上说“这片子我真挺喜欢的”语气却平平的全程没有笑意。只看文本是好评只看语音像敷衍只看表情甚至偏负面——三个模态各说各话。这正是多模态情感分析要解决的问题也是标题里那套“多模态表示学习 多模态融合 下游任务模型”组合包的用武之地。它以CMU-MOSI这类带多维标注的视频语料为基准先分别抽取文本、语音、面部特征再把它们按时间轴对齐、融合最后统一做情感回归或分类评估。适合两类人刚上手想用现成数据和代码把流程跑通的新手以及在固定基准上横向对比自家融合算法的熟手。我当年第一次跑通这个流程时最大的感受是融合模型本身往往不是瓶颈前面特征提取的粒度才是决定上限的玄学。2. 多模态表示学习三种单模态特征提取路线与参数选型在动手碰融合模型之前先想清楚一个事实融合的下限由特征决定上限才由模型决定。文本特征如果只拿一个句向量语音特征只做全局统计视频特征没过滤低质量帧后面无论用多复杂的融合模块都很难补回来。这一章讲这个方向上的默认特征方案每一步都能直接落进代码。2.1 文本模态词级BERT向量比CLS句向量更适合对齐常见做法是用bert-base-uncased取last_hidden_state而不是直接用pooler_output那根向量。pooler_output经过了下游NSP任务设计的Tanh变换句向量是压缩过的做句级分类够用但要做word-level对齐时信息损失明显。更稳妥的是保留每个词的隐状态等语音和视频特征按词时间戳对齐之后再做一次注意力池化。from transformers import BertTokenizer, BertModel text [This movie is surprisingly good] tokenizer BertTokenizer.from_pretrained(bert-base-uncased) model BertModel.from_pretrained(bert-base-uncased) inputs tokenizer(text, paddingTrue, truncationTrue, return_tensorspt, max_length32) with torch.no_grad(): out model(**inputs) word_vec out.last_hidden_state # [batch, seq_len, 768] cls_vec word_vec[:, 0, :] # [batch, 768]逻辑说明last_hidden_state每个位置对应一个subword的768维隐状态cls_vec取第0位。max_length设到32是因为MOSI这类utterance大多在20个词以内给长句留一点余量即可设太大没有收益只增加后面融合的计算量。如果复现时发现长句截断导致特征错位把max_length提到64同时检查最长样本的长度分布。参数说明batch_size一般取32到64显存不够就降到16。BERT只做特征提取时务必包在torch.no_grad()里否则会为无关计算构建计算图白白吃掉显存。模型权重首次加载需要从外网下载如果环境受限先把HF_HOME指向已有的模型缓存目录能省掉很多麻烦。2.2 语音模态openSMILE方便wav2vec 2.0更强语音这块有两条路线。一条是openSMILE提eGeMAPS的88维统计量一条是用wav2vec 2.0提取帧级特征。前者便宜、稳定、跑得快适合做早期拼接后者信息量大保留时序结构适合做中间融合和跨模态注意力。我一般先跑openSMILE当baseline确认文本单模态和音频单模态各自的分数后再决定要不要把音频换成预训练特征。SMILExtract -C eGeMAPS.conf -I utterance.wav -O utterance_eGeMAPS.csv这条命令没有特别复杂的参数核心在配置文件eGeMAPS.conf里它决定了提取哪些低级描述符LLD以及怎么聚合成统计量。默认输出是每段音频一行88维向量作为utterance-level特征可以直接拼接。缺点是它把整段语音压成一个向量丢失了时间结构做word-level对齐时信息不够细。import torchaudio waveform, sr torchaudio.load(utterance.wav) bundle torchaudio.pipelines.WAV2VEC2_BASE model bundle.get_model().eval() with torch.no_grad(): feats, _ model(waveform) # [batch, frames, 768]wav2vec 2.0输出的frames步长约20ms与词时间戳对齐时把落在词边界内的帧做平均池化即可。这里有个容易忽略的细节waveform要求16kHz单声道而视频里的音轨常是48kHz双声道。别省这一步torchaudio.functional.resample否则特征的频率语义会偏后面融合也跟着偏。2.3 视觉模态AU强度特征与帧滤波的默认参数视频模态我习惯用OpenFace先跑一遍得到每帧的面部特征点坐标、头部姿态和17个面部动作单元AU强度。对情感任务来说AU序列比原始图像帧好用得多原始帧经过预训练CNN提取的是物体语义跟情绪相关性太间接AU是脸部肌肉动作的编码更接近标注者打分时的依据。./FaceLandmarkImg -fdir path/to/frames -out_dir path/to/openface_out参数说明-fdir输入帧目录-out_dir输出CSV目录。OpenFace按视频帧顺序逐帧处理每个视频目录输出一个csv行数等于帧数。使用前先做一件事把每帧的confidence过滤掉低于0.5的帧直接丢掉因为这些帧大概率是模糊、遮挡或侧脸AU强度不可信。如果同一段视频连续跑两次低置信度帧上的AU差异很大说明输入帧质量本身就不稳定这时候先别调融合模型回到抽帧环节解决。import numpy as np valid frame_conf 0.5 au_mean au_values[valid].mean(axis0) au_std au_values[valid].std(axis0) video_feat np.concatenate([au_mean, au_std]) # 34维这段逻辑是按话语切片后在时间维度上做均值/标准差聚合。concat均值与标准差是为了把AU的波动也作为一个弱信号保存下来。如果按utterance做回归这个34维向量可以直接用如果按word-level做就不能全局池化得按词时间戳分段池化。视觉特征维度远低于文本和语音融合时如果不做投影均衡很容易被文本模态淹没。3. 多模态融合算法从拼接、门控到跨模态注意力特征都提完之后融合是决定最后分数的临门一脚。这一章按“早期—晚期—中间—注意力”四类路线过一遍每类给最小实现并指出适用边界。明白了这些边界就不会出现“换了个融合模块反而不如拼接”的翻车。3.1 早期融合一个MLP吃掉891维向量的问题早期融合就是把三个模态的特征直接拼接起来然后丢给MLP。优点是极简缺点是问题也很明显76876834这种维度结构里文本占了绝对主导语音和视觉的贡献被稀释且各模态的采样粒度可能不同一个是句级、一个是词级强行对齐会引入误差。import torch import torch.nn as nn class EarlyFusion(nn.Module): def __init__(self, d_text768, d_audio768, d_video34, d_hidden256, num_classes2): super().__init__() self.fc nn.Sequential( nn.Linear(d_text d_audio d_video, d_hidden), nn.ReLU(), nn.Dropout(0.3), nn.Linear(d_hidden, num_classes), ) def forward(self, text, audio, video): h torch.cat([text, audio, video], dim-1) return self.fc(h)参数说明d_hidden256是常见中间层大小Dropout用0.3因为拼接特征维度高、样本量少不丢一下必然过拟合。训练时batch size取64学习率1e-3配Adam配合早停跑30到50轮。早期融合适合当baseline不适合当最终方案原因就是维度失衡和模态间相关性完全没有建模。注意如果只改EarlyFusion里的dropout和hidden size性能很难有质变。问题出在拼接本身不在MLP容量。3.2 晚期融合独立预测再加权先跑通再优化晚期融合是每个模态先单独训练一个分类器最后把三个分类器的输出按权重加起来。这种做法不会让单模态的过拟合互相传染而且每个模态可以各自选择最合适的模型结构比如文本用BERT微调语音用openSMILE特征配线性层视觉用AU序列配LSTM。缺点是它学不到模态间的交互特征对“文本正向但语气负面”这类冲突型样本它只能打平均分而不是理解冲突本身。def late_fusion_logits(text_logits, audio_logits, video_logits, weight(0.5, 0.2, 0.3)): return (weight[0] * text_logits weight[1] * audio_logits weight[2] * video_logits)参数说明权重我一般先在验证集上从0.4/0.2/0.4开始网格搜索步长0.1。注意相加的是logits而不是softmax后的概率概率相加会压缩动态范围logits相加保留差异。晚期融合跑通之后权重就固定下来不要再用训练集调它它本质上是验证集上的超参数调多了就变成对验证集的过拟合。3.3 中间融合门控机制让模型学会“什么时候信谁”中间融合是多数项目里的主力。做法是先把文本和音频等模态映射到同一维度再用一个门控网络计算每个模态在当前位置上的可信度最后做加权组合。门控本质上是连续版本的“软选择”语音质量差的时候门的输出自动降低音频权重这不是手工定的规则而是模型在训练里自己学到的。class GatedFusion(nn.Module): def __init__(self, d_model256): super().__init__() self.text_proj nn.Linear(768, d_model) self.audio_proj nn.Linear(768, d_model) self.video_proj nn.Linear(34, d_model) self.gate nn.Linear(d_model * 3, 3) self.fc nn.Linear(d_model, 2) def forward(self, text, audio, video): ht torch.tanh(self.text_proj(text)) ha torch.tanh(self.audio_proj(audio)) hv torch.tanh(self.video_proj(video)) g torch.softmax(self.gate(torch.cat([ht, ha, hv], dim-1)), dim-1) h g[:, 0:1] * ht g[:, 1:2] * ha g[:, 2:3] * hv return self.fc(h)关键设计三个模态先投影到256维共享空间再算门。不能直接用768维文本向量和34维视觉向量拼在一起算门维度差会让门控网络学到“只看文本维度”的捷径。这里的softmax门比sigmoid门多一个性质三个模态的权重之和为1相当于对输入向量做了凸组合如果想让每个模态独立开关就改成三组sigmoid但参数量更大。d_model是256门控网络只有一层linear足够学习这个可信度映射再加深反而容易在小样本上过拟合。3.4 跨模态注意力以文本为Query的加权池化如果数据量大、预算足跨模态注意力是比门控更强的方案。它让语音和视觉特征按文本每个词的注意力权重做池化得到“文本正在强调哪个词就重点看该词位置附近的表情和语气”的效果。很多多模态融合论文的分类也就是早融合、晚融合、中间融合和注意力这几类注意力是目前公认上限更高的一种。def cross_modal_attention(query, key, value, d_k64): scores torch.matmul(query, key.transpose(-2, -1)) / (d_k ** 0.5) weights torch.softmax(scores, dim-1) return torch.matmul(weights, value)参数说明query来自文本特征投影key和value来自音视频拼接特征投影d_k是query的维度通常取64或128。softmax在最后一个维度上做归一化保证每个词位置上音视频的权重和为1。这个模块需要更长的训练时间学习率要降到5e-5附近否则很容易在最初几步就陷入局部最优。它和门控不冲突可以先用门控做baseline再在门控输出后面接一层跨模态注意力验证性能增量是否值得多出的计算量。最后用一个消融表格收住本章方便对照自己的实验。下面给出的是这类任务在常见配置下的浮动区间以你实际切分和特征为准特征组合acc-2二分类F1说明仅文本60~650.58~0.63容易依赖措辞忽略语气仅音频56~610.54~0.59受背景噪声影响大仅视觉55~600.52~0.57表情与情感相关性较强但信息少文本音频66~700.64~0.68双模态交互明显文本视觉65~690.62~0.66比音频提升略小三模态70~750.68~0.72一般是最优配置如果你把自己的中间融合跑出来后三模态分数低于任意双模态那大概率是特征对齐出了问题不是融合模型的锅。4. 数据集与对齐策略CMU-MOSI、MOSEI、IEMOCAP怎么喂给模型数据集在这类项目里是个双刃剑选对数据集评测有说服力切分策略错了复现出来的分数会完全没法跟历史工作对比。这一章先从数据集分工说起再给一份从原始视频到word-level训练样本的操作流程最后讲特征对齐。4.1 三个常用数据集的分工回归、分类与对话先明确一个概念多模态情感分析不等同于人脸表情识别或语音情绪识别它的核心是“多个模态共同表达一个情感标签”。目前业内最常用来跑这类模型的公开基准是数据集规模与形态标注类型常用指标适合场景CMU-MOSI2199段短视频评论情感评分 [-3,3]Acc-2/F1/MAE/Corr快速验证融合模型CMU-MOSEI23453段视频评论情感倾向 强度Acc-2/F1/MAE/Corr大规模稳定性验证IEMOCAP约12小时双人对话6类离散情绪WAR/Acc、F1对话轮次与上下文建模DEAP32人观看视频的EEG与生理信号维度情感分类/回归精度生理信号音视频融合扩展选型逻辑想快速迭代算法用CMU-MOSI它样本少、跑一轮很快要做严谨的结论必须补CMU-MOSEI规模大10倍对融合鲁棒性的考验更强。IEMOCAP是对话流多个说话人轮流发言如果模型不利用说话人轮次信息很容易在WAR指标上吃亏这是它和MOSI/MOSEI差异最大的地方。做生理信号方向的可以关注DEAP但DEAP的标注来自被试自评噪声大先跑MOSI把流程稳下来更稳妥。4.2 从原始视频到word-level样本抽帧与切片的固定流程这段流程我重复过很多次已经固定成一套标准操作。第一步把视频切成语语第二步用ASR拿到每个词的时间戳第三步按词时间戳取对应的视频帧和音频区间。ffmpeg -i input.mp4 -f segment -segment_time 3 -c copy seg_%03d.mp4这个按固定3秒切的方案只适合快速看数据不适合做训练集。正式做法是先跑ASRwhisper是当前最方便的常见选择拿到每句话的起止时间再按话语起止切。ffmpeg -ss 12.3 -to 18.7 -i input.mp4 -c copy utt_001.mp4参数说明-ss放在-i之前是快速定位再切时间戳精度在关键帧附近可能偏差要更精确就把-ss放在-i之后做重编码速度慢但帧精确。训练集建议用后一种因为词时间戳和帧的对应必须准确否则后面每个word-level的视觉特征都是错位的。抽帧的默认参数也在这里给出来ffprobe -v error -select_streams v:0 -show_entries streamr_frame_rate -of defaultnokey1:noprint_wrappers1 input.mp4先看原始帧率再决定抽帧频率。如果原视频是25fps就直接按25fps抽不要强行抽到30fps否则会产生重复帧后面按时间戳对齐时每帧对应的墙壁时间就乱了。4.3 特征对齐ALI索引与统一长度池化特征对齐是多模态项目里最重要的工程环节。CMU-MOSI和MOSEI官方发布过ALI对齐索引它把每个单词映射到对应的视频帧区间和音频特征区间省去自己基于ASR切词的误差。即使不用ALI也要在自己的流程里保证三点特征的时间分辨率统一、缺失区间有标记、padding后再融合否则融合层会学到“对齐误差”这个假信号。def align_to_fixed(feat, target_len16, modemean): t feat.shape[0] if t 0: return np.zeros((target_len, feat.shape[1]), dtypenp.float32) idx np.linspace(0, t - 1, target_len).astype(int) if mode mean: return feat[idx] if mode avg_pool: seg np.array_split(feat, target_len) return np.stack([s.mean(axis0) for s in seg])逻辑说明第一个分支是均匀索引采样速度快但会丢信息第二个分支是把序列等分成target_len段每段平均更接近时间池化。target_len的选取要和模态对齐文本句子平均词数相近最好16是句级特征的常用值。如果发现某个模态的有效帧数远小于target_len比如一段音频只有2帧有效后面的分段池化会产生大量重复统计量此时应该把这条样本的该模态标记为缺失做掩码而不是硬凑长度。注意无效区间不要用零填充。一旦填零融合模型会学到“模态缺失零向量”而这个零向量本身是一个强特征会导致训练时看似收敛很快、测试时遇到真缺失就崩。用NaN标记然后在融合模块里用mask计算。5. 实验评估避坑指标选错等于白跑的4个常见问题多模态情感分析的公开基准已经比较成熟但不少人在上面跑出来的自测分数很高、横向一比就不可复现。这里把最常见的4个坑按“现象→原因→解决”的顺序写清楚。5.1 切分不一致你的0.82和论文0.82不是同一件事现象复现某组融合结果Acc-2差了3个点以上训练曲线看着很健康但评测分数对不上。原因多数公开基准要求按说话人划分train/valid/test而不是随机切分样本。随机切分会把同一个人的不同片段拆进训练集和测试集模型记住了说话人特征而不是情感表达特征分数虚高。解决加载数据集时打印三件事——训练集说话人数、验证集说话人数、测试集说话人数并确认三者没有交集。for split in [train, valid, test]: speaker_ids set(df[df.split split][speaker_id]) print(split, len(speaker_ids)) if split train: train_ids speaker_ids else: overlap train_ids speaker_ids assert len(overlap) 0, f{split} shares speakers with train这段代码用speaker_id判交集。如果下载的数据里只有video_id而没有speaker_id就需要从视频id前缀推导说话人比如MOSI里很多视频id以yt开头同一说话人的id前缀相同。动手前先核对数据csv里的字段别想当然。MOSI的标准划分大约是51位说话人训练、10位验证、29位测试MOSEI类似但每次下载来源不同标注可能不同所以要自己验证一遍。5.2 用Acc-2还是F1类别不均衡时的两种命运现象训练日志里Acc-2在涨但按分类报告看负类F1只有0.3。原因情感评分转二分类时常把评分大于0标为正、小于等于0标为负MOSI的正负样本天然不均衡模型只需多猜多数类就能把Acc-2撑上去。解决两个指标都报且以加权F1为主要取舍依据。from sklearn.metrics import classification_report, accuracy_score y_true [1, 0, 0, 1, 1, 0] y_pred [1, 0, 0, 0, 1, 0] print(classification_report(y_true, y_pred, digits3)) print(acc-2:, accuracy_score(y_true, y_pred))分类报告会给出每个类别的precision/recall/F1以及macro/weighted平均值。重点看两个数字少数类的F1、weighted avg的F1。如果你的模型在正类上F1很高、负类上F1很低说明特征还没学到“负面情感”的共性表达这时候先别调融合结构回到单模态baseline去看是哪个模态对负类本来就弱。另一个常见做法是保留中性样本做三分类这时候F1比Acc-2更能反映真实能力因为它同时对三个类别的recall做了平均。5.3 回归指标只看MAECorr才是衡量排序能力的标尺现象MAE降了0.1但画出来的预测曲线在几个关键点上完全反向负相关明显。原因MAE衡量的是误差绝对值它小只能说明平均偏差小不能说明预测值的排列顺序和真实值一致而情感评分本身是相对强度排序是否一致同样重要。解决MAE和Corr一起报。from scipy.stats import pearsonr mae float(torch.abs(pred - target).mean()) corr, _ pearsonr(pred.detach().cpu().numpy(), target.detach().cpu().numpy()) print(fMAE{mae:.3f} Corr{corr:.3f})强调一点MAE改进明显但Corr反而下降通常说明模型学会了把预测拉向均值区间牺牲了对极端情感的区分能力。在MOSI/MOSEI的回归设置里这很常见尤其当损失函数只用了L1时。我的习惯是损失函数里加上0.1倍的排序损失或者在训练时同时监控验证集Corr并作为早停依据。还可以检查预测分布的方差如果预测值全部挤在零点附近说明模型已经退化成了“平均预测器”。5.4 特征没对齐就拼接一个经典的翻车案例现象三模态融合分数低于文本单模态且训练集指标也不高。原因最常见的是文本词特征、音频帧特征、视频帧特征在时间上没有对齐。比如文本是句级向量、音视频是词级向量拼接时相当于拿整句话的语义去匹配某个词的语气信息错位。解决回查特征的时间分辨率每个模态都打印shape和对应的时间戳范围确认都是word-level或都是utterance-level后再进融合。另一个隐蔽问题是全零行某段音频无声或视频脸丢了对应特征整行是0拼接时会把这种“伪信息”当成真实输入。我的处理是保留一个valid_mask在融合层对无效位置做masked averaging而不是简单填零。这一步虽然改动很小但它经常就是融合模型能不能超过单模态的分水岭。可以先写一个小函数检查无效行占比def check_valid_ratio(feat, threshold0.5): valid ~(np.abs(feat).sum(axis-1) 0) ratio valid.sum() / feat.shape[0] print(fvalid ratio: {ratio:.2f}) assert ratio threshold, ftoo many empty rows: {1 - ratio:.2%}这段逻辑把整行全零的位置认为是无效帧计算有效比例。阈值0.5是经验值低于这个比例的特征基本不可用。跑融合前对三个模态都执行一遍能过滤掉大多数“融合不如单模态”的乌龙。6. 用模态屏蔽测试验证融合模型学到了什么融合模型训练完先别急着报指标。我每次会多花半小时做一组模态屏蔽测试用来判断模型到底依赖哪个模态。做法很简单推理时把某个模态的输入全部置零观察指标下降幅度。如果屏蔽文本后F1几乎不掉说明模型学到的是文本捷径如果屏蔽视觉后F1反而上升说明视觉特征引入的是噪声。def masked_eval(model, val_loader, mask_key): model.eval() preds, gts [], [] for batch in val_loader: if mask_key in batch: batch[mask_key] torch.zeros_like(batch[mask_key]) logits model(**batch) preds.append(logits.argmax(dim-1)) gts.append(batch[label]) return accuracy_score(torch.cat(gts), torch.cat(preds))这段代码直接复用验证集哪个模态被屏蔽了性能掉得最多哪个就是模型真正依赖的线索。与之配套的做法是单模态有效性排序分别用文本、音频、视觉特征单独训练一个逻辑回归得到单模态F1排序。把两张表对比一下——如果单模态里文本最强屏蔽文本时掉分也最多说明融合模型行为正常如果单模态排序和屏蔽掉分排序不一致就要怀疑融合层里存在某种模态捷径比如投影维度不同导致某个模态梯度占优。我自己踩过的教训是有一版门控融合模型屏蔽文本只掉了1个点屏蔽音频掉了8个点翻看特征才发现音频特征里有大量静音段被错误填补成零向量模型学到的不是语气信息而是“有没有声音”这个二分类信号。从那以后我每个融合实验都固定先跑这条屏蔽测试再决定要不要继续往上加跨模态注意力。别让模型在你不知道的地方抄近道这个习惯比多调一组超参数更值。希望帮到你。本文还有配套的精品资源点击获取