尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

多模态融合情感分析实战:数据对齐、编码器与注意力融合

多模态融合情感分析实战:数据对齐、编码器与注意力融合 简介这套Python多模态融合情感分析项目针对文本、语音、图片、视频四种输入完成情感倾向判断。项目面向毕业设计、期末大作业及课程设计场景从数据清洗、特征对齐到模型训练与评估均有完整实现可直接部署运行。压缩包内共包含20个文件其中有5个Python源文件覆盖数据预处理、模型构建、主程序运行等模块另有9个预处理特征数据文件、3个数据集压缩包以及设计文档、说明文件和结果展示图整体大小约56.9MB结构划分清晰。所用数据涵盖MOSI、IEMOCAP、MOSEI等主流多模态情感数据集关键代码带有注释便于理解与迁移默认提供多组预提取特征能支持快速复现实验。目前已有101人学习/浏览适合需要完成课程设计报告或搭建演示系统的Python开发者参考。1. 多模态融合情感分析到底是什么从一段视频里挖出真实情绪拿到一段30秒的视频有说话人的表情、语气、台词字幕甚至背景里的情绪音乐。单看文本台词是“我没事”听语音声音在发抖看表情眼眶是红的。人脑会把这几路信息合起来判断“他其实很难过”而单模态模型只会傻乎乎地输出“中性”。多模态融合情感分析做的就是把文本、语音、图片、视频四路信号喂给Python搭的模型让模型学会跨模态地综合证据最终输出比任一单模态更准确的情绪判断。这个方向不算新但要把它跑通——数据对齐、编码器选型、融合策略、调参排错——每一步都有坑。这篇笔记适合正在做情感分析、多模态项目或者手里有数据但不知道怎么拼模型的同学。2. 数据是这行最大的坑把文本/语音/图片/视频整理成对齐样本2.1 先定数据规范统一时间轴、音频采样率和文本转写格式做多模态情感分析第一批劝退人的不是模型是数据。原始数据集里常见的是一份视频文件、一份字幕文件、一份标注表格三者没有统一的时间戳格式。你拿到手第一件事不是写模型而是制定一个全项目通用的“数据规范”否则后面每一步都会因为格式混乱返工。我一般会按下面三条规定来约束所有数据第一统一时间轴。所有模态的特征都以“秒float”为绝对时间基准不允许用帧号、采样点号做跨模态索引。视频帧率可能是25fps音频采样率可能是16kHz或48kHz字幕是时间区间起点/终点三者要齐平唯一可靠的时间单位是秒。第二统一音频参数。无论原始视频里的音轨采样率是44.1kHz还是48kHz进模型前一律重采样到16kHz、单声道因为语音预训练模型Wav2Vec2、Whisper、HuBERT基本都按16kHz单声道做的预训练。采样率不统一会导致特征错位、模型表现忽好忽坏这是很常见的问题。第三统一文本来源。文本可以来自字幕文件也可以来自ASR转写但同一个数据集里不要混用。ASR转写和人工字幕的错字率、时间戳精度完全不一样混用会让模型把“ASR噪声”和“字幕风格”当成情绪线索去学训练时震荡测试时翻车。有了这三条规定再回头看数据集清理就轻松了。视频抽帧按秒对齐音频重采样按秒对齐文本按时间区间切分三路数据全部落到同一个时间坐标系上后面做特征提取时才不会出现“音频比视频前移0.5秒”这种玄学问题。2.2 公开数据集怎么选CMU-MOSI、MELD和自制数据的分工标题里说“数据集”多数从业者想的是“下载一个现成的就能用”。实际上做多模态情感分析公开数据集和自制数据各有各的用途不能互相替代。公开数据集方面最常接触的是这几类CMU-MOSI和CMU-MOSEI是单说话人评论视频表情、语音、文本三模态齐全标注是1到7的情感分数做回归任务很方便MELD是《老友记》的多说话人对话数据集七分类情感标注适合做对话情感分析但语速快、多人重叠说话ASR转写难度大RAVDESS是演员表演的音频和视频情感类别全但表演痕迹重适合做基准测试。我个人的选型经验是第一尝试用CMU-MOSI做原型验证。它的样本短、说话清晰、文本转写质量高能把“模型框架”快速跑通。框架跑通后再在MELD上做对话场景的调整。不建议一上来就在MELD上从零折腾样本长、说话人重叠、GPU显存压力大新手容易被拉进排错的泥潭。自制数据则要务实。现实场景里你手头的视频没有人工字幕就得用ASR先生成文本再手工修正漏转的句子。这里有个提醒自制数据集不要贪大先拿几百条样本把流程走通。我见过不少同事第一步就收集上万条视频结果清洗了一个月还没开始写模型。数据量是后面的事流程通了才能谈规模。为了让你对数据集有个直观印象我按“标注类型、模态覆盖、适合任务”整理一下数据集标注形式模态适合做什么CMU-MOSI情感分数1-7文本语音视觉原型验证、回归/三分类CMU-MOSEI情感分数1-7文本语音视觉大规模训练、细粒度情感MELD七分类标签文本语音视觉对话情感、多人场景RAVDESS类别标签语音视觉单模态基准、消融实验选数据集的本质是选“任务的约束条件”。单说话人、剪辑干净的数据适合验证架构多人对话、有背景噪音的数据适合验证鲁棒性。把这两类数据分开用别混在一个实验里。2.3 数据加载与对齐的落地代码从原始文件到 torch Dataset规范定了数据集选好了接下来是把数据组织成模型能吃的格式。这里我给出一份相对完整的PyTorch Dataset实现它把视频文件切分成样本段每一段对齐文本、音频和视觉特征最终输出三个张量加一个标签。import torch from torch.utils.data import Dataset import pandas as pd import numpy as np import librosa import torchaudio class MultimodalEmotionDataset(Dataset): def __init__(self, meta_csv, video_dir, audio_sr16000, segment_len6.0, transformNone): meta_csv: 样本表包含 video_id, start, end, text, label video_dir: 视频文件目录 audio_sr: 音频统一采样率标准是 16000 segment_len: 切片长度超过该长度按滑窗切 self.meta pd.read_csv(meta_csv) self.video_dir video_dir self.audio_sr audio_sr self.segment_len segment_len def __len__(self): return len(self.meta) def __getitem__(self, idx): row self.meta.iloc[idx] # 1. 文本特征先用简单分词占位BERT 编码在 3.1 节接入 text_ids self._text_to_ids(row[text]) # 2. 音频特征从视频里抽取音轨重采样到 16kHz waveform, sr torchaudio.load( f{self.video_dir}/{row[video_id]}.mp4, channels_firstTrue ) if sr ! self.audio_sr: resampler torchaudio.transforms.Resample(sr, self.audio_sr) waveform resampler(waveform) audio_feat self._audio_to_feature(waveform.mean(dim0)) # 单声道 # 3. 视觉特征按时间轴抽帧每一帧走 ResNet这里先取原始帧 frames self._extract_frames( f{self.video_dir}/{row[video_id]}.mp4, startrow[start], endrow[end] ) label torch.tensor(row[label], dtypetorch.long) return text_ids, audio_feat, frames, label def _text_to_ids(self, text): # 占位实现实际用 transformers 的 tokenizer return torch.tensor([ord(c) for c in text[:200]], dtypetorch.long) def _audio_to_feature(self, wav): # 用 librosa 提 40 维 MFCC时间维度按帧堆叠 mfcc librosa.feature.mfcc( ywav.numpy(), srself.audio_sr, n_mfcc40 ) return torch.tensor(mfcc.T, dtypetorch.float32) def _extract_frames(self, video_path, start, end): # 用 torchvision.io 读视频按 FPS 抽帧 import torchvision.io as io vframes, _, _ io.read_video( video_path, pts_unitsec, start_ptsstart, end_ptsend ) # 每隔 6 帧抽一帧降低显存压力 return vframes[::6].permute(0, 3, 1, 2).float() / 255.0这份代码有几个关键参数需要单独解释。音频统一重采样到16kHz是因为Wav2Vec2的tokenizer和特征提取器都按16kHz训练你用48kHz直接喂进去出来的特征会走样。帧抽取的[::6]是抽帧间隔原始视频25fps时每6帧抽1帧相当于约4fps一个6秒的样本段会得到24帧左右这对ResNet编码器是合理的输入规模。想更密就改成[::3]但显存占用会翻倍。代码里_text_to_ids这个方法是占位实现真实项目里要替换成BERT的tokenizer这个我会在第3章展开。这种做法是在先把数据管线跑通、之后再补模型编码器的意思。先把Dataset写对剩下的都是往里塞新模块不用推倒重来。3. 各模态编码器选型BERT处理文本、Wav2Vec2处理语音、ResNet处理视觉帧3.1 文本模态BERT / RoBERTa做情感编码文本是情感分析里最“强势”的模态常出现“文本一个人就把任务解了”的情况。所以文本编码器的目标不是单纯追求精度而是要产出“能和其他模态对齐”的语义向量。常见做法是用预训练语言模型的[CLS]向量作为整句表示。以bert-base-uncased为例输入token序列取最后一层[CLS]位置的768维向量经过一个投影层到统一的融合维度比如256维。这里有个值得注意的细节不要直接拿最后一层所有token的均值池化情感语义往往集中在少数关键词和一个整体句法结构上[CLS]是模型中专门为“整句语义”设计的汇聚点。from transformers import AutoTokenizer, AutoModel import torch tokenizer AutoTokenizer.from_pretrained(bert-base-uncased) encoder AutoModel.from_pretrained(bert-base-uncased) def encode_text(text_list, max_len128): 批量文本编码返回每句的 CLS 向量。 max_len: 超过截断不足补零。 inputs tokenizer( text_list, paddingTrue, truncationTrue, max_lengthmax_len, return_tensorspt ) with torch.no_grad(): outputs encoder(**inputs) cls_vec outputs.last_hidden_state[:, 0, :] # [batch, 768] return cls_vec参数说明max_len128是在效率和精度之间做权衡。CMU-MOSI里大多数句子不超过20个词128的截断上限已经够用但如果是长对话要提到256。paddingTrue会把同一批次内较短的句子补到同样长度truncationTrue负责超长截断这两个参数不设对的话批量训练时会爆显存或形状不一致。另一个经验是预训练模型的权重用AutoModel.from_pretrained加载后微调时只更新后几层前面的层冻结。这样可以防止小数据集上把预训练知识冲掉。具体做法是对encoder的底层参数设置requires_gradFalse只让后两层的参数参与梯度更新。3.2 语音模态Wav2Vec2或手工特征别一上来就上大模型语音模态有两种路线新手常在两者之间犹豫用Wav2Vec2这类预训练模型端到端提特征还是用librosa手工提MFCC。我的建议是先看任务规模再用“两阶段提特征”的思路去选。路线一手工特征。用librosa提40维MFCC加一阶差分delta得到80维特征序列然后用一个简单GRU编码成固定长度向量。优点是计算量小、显存占用低、不依赖额外预训练模型缺点是表达力有限对背景噪声和语速变化敏感。路线二Wav2Vec2特征。加载facebook/wav2vec2-base把整段语音送入模型取隐藏层序列的均值池化得到固定向量。这里要注意Wav2Vec2的输出特征本身是帧级别的需要池化成句级向量才能和其他模态拼接。from transformers import Wav2Vec2Processor, Wav2Vec2Model import torch import torchaudio processor Wav2Vec2Processor.from_pretrained( facebook/wav2vec2-base-960h ) wav2vec Wav2Vec2Model.from_pretrained( facebook/wav2vec2-base-960h ) def encode_audio(waveform, sample_rate16000): waveform: [1, T] 单声道音频必须是 16kHz 返回句级语音向量 [1, 768] # 先重采样到 16kHz与预训练条件保持一致 if sample_rate ! 16000: resampler torchaudio.transforms.Resample(sample_rate, 16000) waveform resampler(waveform) inputs processor( waveform.squeeze(0), sampling_rate16000, return_tensorspt ) with torch.no_grad(): outputs wav2vec(**inputs) # 帧级特征按时间维度均值池化 pooled outputs.last_hidden_state.mean(dim1) # [1, 768] return pooled参数说明mean(dim1)是对时间维度做均值池化把变长语音统一成固定768维向量。如果你想保留时序信息做更细的融合可以改成max_pool或加一层注意力池化。sampling_rate16000必须传对Wav2Vec2的processor内部会用采样率做归一化传错会导致特征全乱。工程上还有一个判断如果你的数据是电话录音、嘈杂环境Wav2Vec2优势明显如果是剪辑干净的视频人声MFCCGRU已经能打。项目要快跑原型我一般先用MFCC顶住等模型框架稳定了再替换成Wav2Vec2看增益是否值得增加的推理时间。3.3 图片与视频帧ResNet抽帧特征时序池化视频模态的处理常见误区是“直接拿个视频理解模型端到端跑”。对情感分析任务来说这种做法成本高且难以调试。更可靠的工程路线是先抽帧再用图像模型提空间特征最后做时序池化。用ResNet50做帧级特征提取是稳妥的起点。输入是一段视频的若干帧每一帧过ResNet50取avg_pool层输出2048维得到一组帧特征序列再对这些帧特征做时序池化如均值池化或GRU得到视频模态的句级向量。import torch from torchvision.models import resnet50, ResNet50_Weights resnet resnet50(weightsResNet50_Weights.IMAGENET1K_V1) resnet.eval() # 替换最后的全连接层露出2048维特征 resnet.fc torch.nn.Identity() def extract_video_feature(frames_tensor): frames_tensor: [B, frames, C, H, W] 已归一化的帧序列 先合并Batch和帧维度过ResNet再恢复并池化 B, T, C, H, W frames_tensor.shape flat frames_tensor.view(B * T, C, H, W) with torch.no_grad(): features resnet(flat) # [B*T, 2048] features features.view(B, T, 2048) pooled features.mean(dim1) # [B, 2048]均值池化 return pooled参数说明mean(dim1)把多帧特征合并成一个2048维向量这是一种不计时序顺序的做法。如果你的视频样本里情绪是“先平静后爆发”均值池化会丢掉这个演化过程这时可以换成GRUgru torch.nn.GRU(input_size2048, hidden_size256, batch_firstTrue) _, hn gru(features) # features: [B, T, 2048] video_vec hn.squeeze(0) # 取最后一个隐状态[B, 256]选GRU还是均值池化取决于任务里时序信息重不重要。做原型时先用均值池化因为它零参数、不会过拟合如果消融实验证明“情感变化顺序”对结果影响大再换GRU或Transformer编码器。单个模态的编码器做到这一步实际上已经能各自跑分类实验了。但多模态融合的重点在于“多”不同模态特征的维度、语义粒度、噪声模式完全不一样怎么把这些不同尺度的特征合理合起来是第4章的焦点。4. 融合策略怎么选特征级拼接、注意力门控还是决策层投票4.1 为什么不建议上来就做特征拼接最简单粗暴的融合方式是把文本的768维向量、语音的768维向量、视觉的2048维向量直接拼成一个3584维向量再过一个全连接层做分类。这种做法在论文里叫“早融合”early fusion工程上实现最快但效果往往不理想。原因主要有三个。第一特征尺度不一致BERT输出的向量经过层归一化量级较为规整ResNet特征来自卷积网络的池化分布和量级都有明显差异直接把它们拼在一起全连接层会被量级大的特征主导小的特征即使携带情绪信息也被淹没。第二模态间没有交互拼接只是把特征放在同一根线上没有任何机制让模型学会“文本说没事语音发抖难过”这类跨模态推理。第三容易过拟合拼接后维度暴涨对数据集规模不大的情感分析任务全连接层很容易记住训练集噪声。所以我把拼接当成“基线方案”来用它在代码里花不了几行却能提供一个参照——后面的融合模型如果连拼接都赢不了那说明问题不在融合策略而在单模态特征。4.2 方案A特征级拼接映射作为基线class SimpleFusion(torch.nn.Module): def __init__(self, text_dim768, audio_dim768, video_dim2048, hidden_dim256, num_classes3): super().__init__() # 各模态先投影到统一维度把量级拉齐 self.text_proj torch.nn.Linear(text_dim, hidden_dim) self.audio_proj torch.nn.Linear(audio_dim, hidden_dim) self.video_proj torch.nn.Linear(video_dim, hidden_dim) # 拼接后分类 self.classifier torch.nn.Sequential( torch.nn.Linear(hidden_dim * 3, hidden_dim), torch.nn.ReLU(), torch.nn.Dropout(0.3), torch.nn.Linear(hidden_dim, num_classes) ) def forward(self, text_vec, audio_vec, video_vec): t torch.relu(self.text_proj(text_vec)) a torch.relu(self.audio_proj(audio_vec)) v torch.relu(self.video_proj(video_vec)) # 在特征维度上拼接 fused torch.cat([t, a, v], dim1) return self.classifier(fused)每个模态先各自过一个线性层把768/768/2048统一到256维再做拼接。这比直接拼接原始的3584维要合理因为线性层起到了“尺度对齐”的作用。但注意Linear只是逐维度的缩放和平移它没有让模态之间交换信息——所以它仍是早期融合只是做了预处理上的改善。这个模型的输出是[B, hidden_dim * 3]后面接分类层。训练时这个基线模型的loss和常规分类网络没有两样用CrossEntropyLoss加AdamW优化器学习率建议从1e-4起步比单模态微调时BERT用2e-5、Wav2Vec2用1e-5稍大因为这里的线性层是从头训练的需要更快收敛。4.3 方案B跨模态注意力门控融合要让模态之间真正发生交互我建议用“注意力门控”方案。它的核心思想是计算每个模态对最终情绪判断的重要性权重用权重去加权各个模态的特征而不是一视同仁地拼接。具体来说把三个模态的特征向量拼起来过一个注意力层输出三个归一化权重再对各模态特征做加权求和。这个实现加不了几行代码却能让模型学到“这条样本里语音比文本更可信”这种自适应逻辑。class AttentionFusion(torch.nn.Module): def __init__(self, embed_dim256, num_classes3): super().__init__() # 投影到统一维度与4.2保持一致的输入输出 self.text_proj torch.nn.Linear(768, embed_dim) self.audio_proj torch.nn.Linear(768, embed_dim) self.video_proj torch.nn.Linear(2048, embed_dim) # 注意力打分网络 self.attn torch.nn.Sequential( torch.nn.Linear(embed_dim * 3, 128), torch.nn.Tanh(), torch.nn.Linear(128, 3) ) self.classifier torch.nn.Linear(embed_dim, num_classes) def forward(self, text_vec, audio_vec, video_vec): t torch.relu(self.text_proj(text_vec)) a torch.relu(self.audio_proj(audio_vec)) v torch.relu(self.video_proj(video_vec)) # 拼接所有模态计算每个模态的权重 mixed torch.cat([t, a, v], dim1) scores self.attn(mixed) # [B, 3] weights torch.softmax(scores, dim1).unsqueeze(1) # [B, 1, 3] # 按权重加权求和 stack torch.stack([t, a, v], dim2) # [B, embed_dim, 3] fused torch.matmul(stack, weights.transpose(1, 2)).squeeze(2) return self.classifier(fused)这个注意力门控的巧妙之处在于softmax保证了三个模态的权重之和为1。某个模态完全没有信息时模型可以学到一个接近0的权重自动降低它的贡献。训练过程中可以用print(weights.mean(dim0))检查模型是否真的在动态调节权重这一招对排查“某一模态被忽略”很有用。有一点要注意注意力权重是模型自己学出来的它不代表“这个模态在常识意义上的重要程度”只代表“在这个模型看来哪个模态对最终分类的信息量最大”。所以要分析结果时别急着把人脑的直觉套到权重上先看它对验证集的指标有没有帮助。4.4 方案C决策级融合模型投票第三种思路是让每个模态各出一个分类概率再对概率做加权平均或投票。这种方式叫“晚融合”常用于工程上需要解耦部署的场合——三个模态模型是三个独立服务可以分别更新、分别扩量只有最终预测时需要汇总。决策级融合的优点是容错性强某一模态的模型挂了或输入缺失另外两个还能出结果。缺点是丢失了模态间的低层交互。一个文本说“我没事”语音却在哭的样本决策融合只能在“文本说没事”和“语音说难过”之间折中而注意力门控让模态在特征层面直接交互。但对很多实际系统来说可维护性比上限精度更重要所以决策融合并不是“丢人”的方案。融合方式交互粒度可维护性适合场景特征拼接基线无交互高快速跑通、对比参照注意力门控特征级交互中追求精度、单模型集成决策级投票仅概率交互高多服务部署、容错优先三个方案我建议都实现按“基线→注意力→决策”的顺序去对比实验。不要一开始就奔着最复杂的方案去先让基线跑通、让数据处理和训练流程稳定再逐步加复杂度这样出问题时定位范围最小。5. 多模态融合情感分析的高频翻车点现象、原因与排查顺序5.1 现象训练时指标震荡剧烈验证集忽高忽低训练早期会发现loss降不下去验证集准确率在一个区间里来回跳。这个过程会让人怀疑模型设计错了从头检查又看不出问题非常消磨时间。原因一般是学习率过大并且没有warmup。微调预训练模型BERT、Wav2Vec2时底层参数已经收敛在某个区域用大学习率一步跨出去参数就偏离了预训练空间的“好位置”。加上批量里不同样本的模态质量差异大梯度方向来回摆动。解决方法是把优化器换成AdamW学习率调到2e-5到5e-5区间的BERT标准范围Wav2Vec2用1e-5同时加一个线性warmup前10%的训练步数里学习率先从0线性升到目标值。transformers库自带get_linear_schedule_with_warmup直接用就行。另外建议固定随机种子否则你怎么复盘都无法复现实验。5.2 现象截断的文本和音频不同步融合以后反而比单模态差有一回我的融合模型准确率比单模态文本模型低两个点。逐个检查后发现问题出在数据切分逻辑视频样本按6秒切段但文本是“一句话”音频是“整段音轨的起点到终点”。一句话结束了音频还没结束剩余的音频静音或背景噪音也一样被当成语音特征。解决方法是切分时以“文本句子的时间区间”为核心音频和视频都按同一时间区间裁剪。文本句子是从几秒到几秒音频就裁这段视频也裁这段。如果一句文本跨了两个情绪区间可以在标注表里手工标出边界而不是让代码猜。这个对齐问题在公开数据集里经过人工清洗不容易踩到自制数据时会非常明显。5.3 现象视频一多就爆显存训练根本跑不起来把整段视频的每一帧都喂给ResNet50一张12GB的显卡撑不了几个样本。这通常不是代码错而是“抽帧策略批大小”组合得不对。解决路径有三步。第一降低抽帧密度把[::6]改成[::10]一个6秒样本段从24帧降到15帧左右第二用torch.no_grad()先把所有视频帧特征预先提取并缓存到磁盘pkl或npy训练时只加载特征不再过ResNet这是最有效的做法第三不得已时减小batch size到4或2配合梯度累积。视觉特征预提取会让训练速度快好几倍值得作为标准流程纳入工程。5.4 现象推理阶段遇到缺一路输入程序直接崩训练数据永远完整但推理时经常出现音频文件损坏、视频抽不到帧、文本为空等异常。训练好的模型上线后遇到缺模态的输入就抛异常这个现象在真实业务里特别常见。原因在于训练和推理的数据处理逻辑不一致训练时Dataset做了容错过滤推理时的预处理管线没有同样处理。解决方法是把“模态缺失处理”写进模型前向逻辑本身在融合层前加一个valid_mask某一模态缺失时该模态的输入用零向量填充同时注意力权重里把对应位置掩码掉。这样模型在训练时就见过缺失模态的样本不会在部署时因为形状不一致而崩。加一个小概率的随机掩码训练对整体精度损耗很小换来的鲁棒性很值。5.5 现象小数据集上微调大模型过拟合到训练集验证集崩盘情感分析数据集经常只有几百到几千条BERT和Wav2Vec2参数量都是上亿直接全量微调几个epoch之后训练集准确率到95%以上验证集只有60%多非常典型。解决思路是多管齐下冻结预训练模型的底层参数只微调后两层或加一个轻量适配器Dropout调高到0.30.5数据增强方面文本可以用同义词替换或回译音频可以加轻微噪声和音高偏移视频可以做随机裁剪和水平翻转。如果做三分类建议用LabelSmoothing把标签软化避免过分自信的预测。最后要控制训练轮数用小验证集做early stopping最靠谱的指标变化阈值是验证集F1连续3个epoch不再提升就停。6. 验证与调参技巧先跑单模态基线再看融合增益6.1 验证套路四路单模态基线和三个融合方案的对比拿到一套数据和三个融合方案我建议按固定流程做实验先把文本、语音、视觉各单独训练一个分类器记录准确率和F1再把第4章的拼接、注意力门控、决策融合各跑一遍最后对比两张表。results { text_only: {acc: 0.72, f1: 0.70}, audio_only: {acc: 0.58, f1: 0.55}, video_only: {acc: 0.53, f1: 0.50}, concat_baseline: {acc: 0.74, f1: 0.72}, attention_fusion: {acc: 0.78, f1: 0.76}, decision_fusion: {acc: 0.75, f1: 0.73}, }单模态结果决定了这个任务的“天花板预期”。如果文本单模态已经到0.72融合到0.76以上说明模态确实提供了互补信息如果融合结果和文本单模态几乎一样说明注意力权重把文本压到接近1其他模态没有发挥作用这时需要回头检查语音和视觉特征的质量而不是继续调融合层。对比表里的F1值比准确率更有代表性因为情感分类常有类别不均衡比如“中性”样本远多于“愤怒”F1能反映出少数类的表现。6.2 调参顺序先文本后语音再视觉逐路加我习惯按“从强模态到弱模态”的顺序逐步搭建系统。先只跑文本把这个模态的编码器和分类器调好接着把语音接进来做双模态融合确认语音确实带来了增益最后加视觉。每加一路都保留一份模型和指标记录这样出了问题可以二分定位还能对比“加了这路到底值不值”。具体到学习率参数文本BERT用2e-5语音Wav2Vec2用1e-5视觉ResNet冻结不参与更新。融合层的线性层和注意力层学习率可以放宽到1e-4。如果用OneCycle或余弦退火更新周期设成总训练步数的一半配合早停。6.3 最后一招可控实验验证模型到底学到了什么指标好了还要回答“模型是真的学到了跨模态信息还是偷偷靠某个模态单打独斗”。做法是给注意力融合模型做一个“模态屏蔽实验”推理时把某一模态的特征置零重新计算预测准确率。准确率下降最多的那个模态就是你模型的“支柱模态”。如果屏蔽语音后准确率只掉两个点而屏蔽文本后掉二十个点说明语音对最终决策的贡献很小接下来别在语音增强上花时间。另一种可视化做法是统计Softmax权重在“积极/消极”样本上的分布看模型是否在消极样本里更依赖语音和视觉。这个验证习惯能有效避免在错误的方向上继续投入。多模态情感分析这个项目技术栈跨度大数据对齐的坑尤其多但只要先把数据管线和单模态基线跑通融合层就能稳定地往上加。我自己的习惯是每跑完一组实验都记下“哪路特征在什么条件下失效”积累多了下一次拿到新数据时能省下大量试错的时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表