
简介面向深度学习与多模态情感分析研究者和工程师的PyTorch可运行源码包整合语音与文本双模态借助预训练多语言BERT与Wav2Vec2分别提取语义和语音深层特征由注意力机制完成跨模态融合针对negative、neutral、positive三类情感分类场景适合快速搭建可用的情感分析原型。整个压缩包共31个文件、约183KB包含2个Python脚本、9组wav音频、9个mp4视频、10个txt说明及1个inscode配置脚本覆盖样例视频生成与模型训练主流程说明文件对样本标签和数据组织给出提示依赖清单则列出所需环境便于直接配置运行环境。目前已有175人学习/下载可作为入门多模态情感分析并对照代码理解注意力融合的实践参考。通过该项目可理清多模态融合的实现思路掌握EATD_Corpus情感数据集的目录组织方式运行自带样例即可体验从数据预处理、特征提取到训练评估的完整链路也为后续扩展更多情感类别或提升模型泛化能力留下清晰切入点。 多模态情感分析说白了就是让计算机同时看你的表情、听你的语气、读你的文字然后综合判断你现在到底是开心、愤怒、惊讶还是焦虑。我这次整理的这套多模态情感分析开发项目最大的特点就是给你一套可以直接跑起来的源码而不是那种只丢给你几个模型文件就完事的半吊子工程。文本、音频、视频三条模态的完整链路都有从数据预处理到模型训练再到推理预测一键执行。这个项目适合谁如果你是刚接触多模态方向的学生或者工作中需要做舆情监控、客服质检、用户满意度分析又不想从零开始啃论文和框架源码那这套东西可以帮你省下至少两周的摸索时间。先别急着复制粘贴代码我会把每条链路的设计逻辑、为什么选这个模型、跑起来会遇到哪些坑一次讲清楚。1. 多模态情感分析的整体设计与方案选型1.1 为什么单模态不够用情感分析的核心瓶颈做过传统文本情感分析的朋友应该都有体会单靠文字判断情绪很容易翻车。比如“你可真厉害”这句话放在真心夸赞的语境里是正向情感放在阴阳怪气的语气里就是负向情感光看文本根本分不清。类似的问题在语音和图像里同样存在一个人的表情可以假装语气可以控制但三者放在一起产生矛盾时往往能暴露真实情绪。多模态情感分析的目的就是解决这个“信息不对等”问题。它借鉴的是人脑的认知方式我们判断一个人开不开心本来就是同时看表情、听语调、读文字而不是只看某一个信号。放到工程实现里就需要把文本、音频、视频三类数据分别提取特征再设计融合策略让模型综合决策。1.2 三条模态的模型选型逻辑这个项目里我采用的具体模型组合如下模态输入形式特征提取模型选型理由文本转录文本/字幕BERT-base-uncased预训练语义表示能力强HuggingFace生态完善微调成本低音频16kHz原始波形Wav2Vec2-base直接吃波形无需手动提取MFCC自监督预训练效果稳视频连续帧图像ResNet-50训练速度快中等规模数据集不容易过拟合部署友好这组选型不是唯一答案却是“开箱即用”性价比最高的组合。BERT负责吃透文本语义Wav2Vec2把语音的节奏、语调、停顿信息抽出来ResNet-50提取面部表情和视觉上下文。三者的输出维度不同后面需要专门的融合层对齐。我实测对比过用VGG16替代ResNet-50效果差不多但模型体积大了近三倍训练速度明显拖慢所以最后固定在ResNet-50。2. 数据准备与预处理细节2.1 训练数据集怎么选MOSI与自建数据的取舍训练多模态情感分析首选公开数据集CMU-MOSI和CMU-MOSEI。这两个数据集里的每条样本都包含一段短视频、对应音频和人工转录文本情感标签是-3到3的连续值负数偏消极正数偏积极0为中性。MOSI规模较小、样本干净适合快速验证模型MOSEI样本量更大、噪声更多适合做正式训练和评测。如果你要处理的是中文场景网上也有一部分中文多模态数据集但规模普遍不够我的建议是先用MOSI把整个流程跑通再用自己的业务数据做领域微调。项目源码里默认加载的是MOSI的预处理版本输入是已经切分好的文本、音频npy文件和视频帧目录不需要自己去YouTube上下原始视频这能给你省掉大量数据清洗时间。2.2 三个模态的对齐与归一化多模态任务最容易踩的坑就是“模态不对齐”。一段视频里说话人的嘴型和语音不同步或者文本长度和语音长度对应不上都会让融合层学到错误的相关性。这个项目里我对齐策略是这样处理的文本按子词切分使用BERT的tokenizer得到每个token的边界时间戳音频按帧frame提取特征每帧对应10ms的语音视频按场景切帧每秒钟采样2帧然后记录每帧对应的时间点。实际操作中我定义了一个align_to_text函数以文本token的时间戳为基准把音频特征和视频特征通过线性插值映射到同样的时间长度上。如果某段文本没有对应的语音或视频信号就直接丢弃这个token位置的跨模态对齐信息避免把空白特征硬塞给模型。注意永远不要为了对齐而强行pad。尤其是音频不要用0向量去补齐缺失片段否则模型会把“无声音”学成“负面情绪”。我早期测试时犯过这个错误损失函数死活降不下去最后排查发现是噪声特征污染了语义表示。3. 模型训练与核心实现3.1 特征提取层的HuggingFace工程实现文本特征用BERT代码很简单。但要注意一点BERT输出是[CLS]向量还是全序列token向量后续融合方式完全不同。这个项目里因为要做时间对齐融合我取的是全序列token级别的hidden_state而不是[CLS]。from transformers import BertTokenizer, BertModel tokenizer BertTokenizer.from_pretrained(bert-base-uncased) text_model BertModel.from_pretrained(bert-base-uncased) def extract_text_features(text): inputs tokenizer(text, return_tensorspt, paddingTrue, truncationTrue, max_length64) outputs text_model(**inputs) # 保留序列维度不要取 pooler_output return outputs.last_hidden_state # [batch, seq_len, 768]音频特征用Wav2Vec2同样从HuggingFace加载预训练权重。这条链路里最容易被忽略的是采样率统一Wav2Vec2要求16kHz输入如果原始视频的音频是48kHz必须先重采样否则提取出来的特征基本是废的。import torchaudio from transformers import Wav2Vec2Processor, Wav2Vec2Model processor Wav2Vec2Processor.from_pretrained(facebook/wav2vec2-base) audio_model Wav2Vec2Model.from_pretrained(facebook/wav2vec2-base) def extract_audio_features(waveform_path): waveform, sr torchaudio.load(waveform_path) if sr ! 16000: waveform torchaudio.functional.resample(waveform, sr, 16000) inputs processor(waveform.squeeze(0), sampling_rate16000, return_tensorspt) outputs audio_model(**inputs).last_hidden_state return outputs # [batch, seq_len, 768]视频帧用ResNet-50提取。这里有个工程小技巧不要每帧都跑一次ResNet太慢了。我会先用OpenCV把视频抽帧成numpy数组然后统一batch送入ResNet最终拿倒数第二层的全局特征。3.2 融合策略的对比与最终选择多模态融合有早融合把特征拼起来再进分类器、晚融合每个模态单独分类再平均得分和中间融合在某个隐藏层拼接特征三种主流做法。这个项目里我迭代过三个版本融合方式结构验证集准确率问题早融合直接拼接3个特征向量输入MLP58.2%模态间尺度差异大训练不稳定晚融合三个独立模型预测结果取加权平均61.5%无法捕捉模态间的交互关系中间融合用Transformer跨模态注意力拼接68.7%训练时间略长但效果提升明显最终采用中间融合。思路是把文本、音频、视频三路特征拼成一个序列当作一个简单的token序列输入到一层Transformer encoder里让模型自己学习哪个模态在当前语境下权重更高。这个设计的直观解释是当你看到一个人面无表情但声音发抖时模型应该理解“音频信号更重要”这种跨模态的注意力分配是简单拼接做不到的。3.3 训练参数与损失函数设计因为是连续值情感分数回归任务损失函数用的是MSE输出层是一个不带激活函数的线性层。训练参数参考了我多次实验后的稳定配置参数数值说明batch_size8视频帧特征显存占用高调大容易爆显存learning_rate2e-5BERT微调用小学习率防止破坏预训练语义epochs10用EarlyStoppingpatience3optimizerAdamW配合weight_decay0.01学习率调度linear_warmup前5%步热身避免大模型震荡训练过程里最应该关注的是三路特征提取器的“梯度传播范围”。我做了冻结设置前5个epoch冻结ResNet-50和Wav2Vec2的backbone只训练BERT末层和融合Transformer第6个epoch起解冻Wav2Vec2继续解冻ResNet-50。这样避免一开始多模态参数同时更新导致特征提取器互相干扰、损失爆炸。4. 源码的核心模块与运行步骤4.1 项目结构说明这套可运行源码的组织方式很清晰核心就四个目录加一个入口脚本multimodal-sentiment/ ├── data/ # 数据存放目录 │ ├── raw/ # 原始视频/音频/文本 │ └── processed/ # 预处理后的npy特征文件 ├── models/ │ ├── text_encoder.py # BERT文本编码 │ ├── audio_encoder.py # Wav2Vec2音频编码 │ ├── video_encoder.py # ResNet50视频编码 │ └── fusion_module.py # Transformer跨模态融合 ├── utils/ │ ├── align.py # 模态特征时间对齐 │ └── config.py # 全局配置参数 ├── train.py # 训练入口 ├── predict.py # 单条样本推理入口 └── requirements.txt我故意没有把代码逻辑拆得太散每个编码器一个独立文件方便你替换成自己的模型。比如你想把文本编码器从BERT换成RoBERTa或者中文的BERT-wwm只需要改text_encoder.py里的模型加载逻辑其他模块不需要动。4.2 从零跑通训练流程打开终端按顺序执行以下命令即可# 1. 创建虚拟环境并安装依赖 python -m venv .venv source .venv/bin/activate pip install -r requirements.txt # 2. 跑数据预处理把原始数据转成特征 python preprocess.py # 3. 开始训练 python train.py --config ./utils/config.py # 4. 用训练好的模型预测单条样本 python predict.py --video test_video.mp4 --text I am so happy today训练日志会实时打印每个epoch的损失、验证集MAE和准确率二分类正负向。训练完成后模型权重会保存到checkpoints/目录predict.py会自动加载最优权重进行推理。4.3 推理时的数据流推理的时候用户输入是一段视频和一句文本系统内部会做这么几件事用OpenCV抽帧并每2秒抽取一帧送入ResNet生成视觉特征用ffmpeg从视频里剥离音频轨道重采样到16kHz送入Wav2Vec2生成音频特征文本直接进BERT得到token序列特征三路特征对齐后拼接成统一序列进Transformer融合融合结果通过线性层映射成[-3, 3]的连续分数再做sigmoid或阈值映射输出正负情感类别。整条链路封装在predict.py里不依赖GPU也能跑CPU推理只是速度慢一些单条视频大约需要3-5秒。5. 常见问题排查与性能优化5.1 训练时的典型报错速查表报错信息大概率原因解决方案CUDA out of memorybatch_size过大或视频帧数过多降低batch_size或减少每秒抽帧数expected shape mismatch in fused feature模态特征长度不一致检查align.py里的时间戳映射逻辑BERT token indices out of range文本过长超出max_length调大max_length或对文本做截断audio waveform sample rate mismatch输入采样率不是16kHz统一调用torchaudio重采样loss不下降且震荡严重多模态同时解冻导致梯度冲突把冻结策略调成“先文本后音视频”5.2 三个实际踩过的坑第一个坑是ResNet-50的BatchNorm在batch_size很小4或8的时候统计量非常不稳定训练集和验证集效果差异巨大。解决办法是在融合阶段加一个LayerNorm把BatchNorm对batch size的依赖隔离开实测验证集波动明显变小。第二个坑是Wav2Vec2的序列长度太长。一段60秒的音频提取出来的特征长度可能达到几百甚至上千和文本token序列拼接后Transformer注意力矩阵会爆内存。我的处理方式是给音频特征做窗口降采样每5帧取平均把长度压到和文本序列差不多的量级。当然如果你用的是更长的视频建议改用卷积层做降维而不是简单平均。第三个坑是和预训练模型库的版本兼容性。HuggingFace的transformers版本更新很频繁不同版本的API有细微差异比如有些版本里Wav2Vec2Model返回值是BaseModelOutput对象有些版本直接返回tuple。我项目里锁定了transformers4.30.0、torch2.0.1、torchaudio2.0.2复现的时候不要随意升级大版本否则很可能遇到莫名其妙的报错。5.3 推理速度优化思路如果你要部署到线上训练时的模型结构需要做几个调整。第一把ResNet-50和Wav2Vec2离线抽特征不要在推理时实时跑这样能减少70%以上的计算开销第二把整套模型导出成ONNX格式融合Transformer的部分在CPU上的推理速度能提升3-5倍第三如果实时性要求更高可以砍掉视频模态只保留文本音频精度下降不大但速度翻倍。6. 从demo走向实际业务这套源码的直接用途是跑通流程和实验验证但落到真实业务里还差两步扩展。第一步是换数据。把MOSI换成你自己的业务数据后只需要保留原有目录格式然后重新运行preprocess.py即可。第二步是改任务。如果你不想做回归分数预测而是要做四分类开心、难过、愤怒、中性把train.py里的loss_fn从MSELoss换成CrossEntropyLoss再把输出层维度从1改成4其他部分基本可以无缝复用。我觉得比较有价值的一个扩展方向是把它接到舆情监控系统里线上用户的评论是文本客服通话录音是音频App用户录制的反馈视频是视觉文本音频三模态。三者拆开分析都会漏掉很多信息合在一起能够更精准地识别用户情绪尤其是“强颜欢笑”这类复杂情绪。项目源码里留了自定义数据集的加载接口直接按目录结构替换数据就行不用改模型代码。最后再分享一个小技巧多模态模型的质量评估不能只看一个指标一定要同时记录回归MAE和分类准确率。有时候MAE略微上升但二分类准确率提升说明模型把模糊的中性样本学得更果断这在业务上往往是更需要的。如果你准备拿这份源码做自己的项目建议从MOSI小数据开始跑通然后用MOSEI正式训练最后再迁移到自己的业务数据上。多模态方向不是每个参数都要自己调但每个模态的“对齐逻辑”一定要亲手过一遍这部分才是坑最深的地方。本文还有配套的精品资源点击获取