简介:基于Python与PyTorch实现的多模态情感分析反事实推理模型框架,面向深度学习初学者与进阶学习者,适合用于毕业设计、课程设计、大作业或工程实训等场景。资源压缩包共包含19个文件,其中17个Python脚本为主要实现部分,分别覆盖模型结构搭建、数据预处理、训练参数配置与主训练流程,另附1个Markdown说明文档和1个Git忽略文件,整体包体大小约36KB,体积轻巧、结构清晰,便于快速阅读和动手复现。当前已有557人学习/下载,具有不错的参考价值。框架内部集成了多种模型模块,如MISA、MAG-BERT、反事实推理模型以及自监督多模态模型,并提供可直接调用的训练脚本和数据生成脚本,能够帮助使用者系统理解反事实推理在多模态情感分析中的具体作用与技术细节;无论是用于课程实践还是作为后续研究的起点,都能获得一套较为完整的工程化参考。
1. 多模态情感分析中的反事实推理:这个框架到底在解决什么问题
我见过不少人在多模态情感分析上翻车,最典型的翻车姿势是:把文本、音频、视频三路特征一股脑拼进一个分类器,然后发现模型越堆越钝,甚至加了视觉模态反而掉分。问题不在于模态不够多,而在于模型学会了模态之间的虚假相关——比如音频里语气一激动,文本里恰好都是褒义词,模型就认定这是正向情感,根本不是在理解语义。这个叫 CLUE_model 的框架,核心就是用反事实推理把「真正的因果」从「碰巧共变」里拆出来。
它基于 PyTorch 实现,训练入口简单到一条python train.py,但内部模型结构完整:MISA 负责把每个模态拆成不变表征和特有表征,causal_model 负责做反事实干预,SelfMM 做自监督辅助,MagBERT 处理文本序列。对做毕设、课程设计、工程实训的人来说,这套代码的价值在于它把「表征解耦 + 因果干预 + 多模态对齐」三条线串成了一个能跑通的项目,既适合新手按流程复现,也适合进阶学习读源码理解多模态情感分析从相关建模走向因果建模的完整思路。
2. 框架结构与模型分工:从 MISA 到因果模块的四层拆解
2.1 先看目录:模型、数据、工具各自管什么
拿到压缩包解压后,先别急着跑训练,把文件归属理清楚。CLUE_model-master 根目录下的文件虽然不多,但分工很明确,我按实际项目里会动的频率给你分个类:
| 文件/目录 | 职责 | 实际使用频度 |
|---|---|---|
train.py | 训练入口,读配置、建模型、跑epoch | 每次训练都动 |
train_config.py/config.py | 超参数和路径配置中心 | 调参必动 |
create_dataset.py | 把原始多模态数据打包成训练集/验证集 | 换数据集时动 |
model_carrier.py | 模型容器,负责把 MISA、因果模块、分类头组装起来 | 改网络结构才动 |
utils/convert.py | 数据格式转换与特征预处理 | 数据格式不匹配时动 |
utils/functions.py | 通用函数,比如指标计算、张量操作 | 默认不动 |
dataset/data_loader.py | DataLoader 封装,负责按 batch 取数据 | 调采样逻辑才动 |
models/misa_model.py | 多模态不变与特有表征模型 | 核心,必读 |
models/causal_model.py | 反事实推理干预模块 | 核心,必读 |
models/magbert_model.py | 文本模态的 BERT 系编码器 | 换文本预训练模型时动 |
models/selfmm_model.py | 自监督多模态辅助训练模型 | 消融实验时动 |
models/subNets/ | 子网络定义,比如单模态编码器内部结构 | 改特征提取层时动 |
model_carrier.py是整个装配口,train.py通过它把 models 目录下的几个模块串起来。新手最容易犯的错是直接改misa_model.py里的层结构,却忘了在model_carrier.py里同步改调用关系,导致运行时维度对不上。改模型前先顺着 train.py -> model_carrier.py -> 具体模型文件这条路读一遍,比什么都强。
2.2 MISA:把每个模态拆成「不变」和「特有」两份表征
MISA 的全称是 Modality-Invariant and Specific Representations,这个思路在 2019 年前后的多模态情感分析里非常有代表性。它的核心假设是:每个模态(文本、音频、视频)的表征应该拆成两部分——跨模态共享的不变表征,和该模态独有的特有表征。不变表征捕获的是三种模态共同表达的情感语义,比如「这段话说的是开心的事」;特有表征捕获的是模态自身的独有线索,比如语音的语调、视频里的表情动作。
# misa_model.py 的核心结构(典型实现) class MISA(nn.Module): def __init__(self, text_dim, audio_dim, video_dim, common_dim): super().__init__() # 每个模态都生成 invariant 与 specific 两组表征 self.invariant_nets = nn.ModuleDict({ 'text': nn.Linear(text_dim, common_dim), 'audio': nn.Linear(audio_dim, common_dim), 'video': nn.Linear(video_dim, common_dim), }) self.specific_nets = nn.ModuleDict({ 'text': nn.Linear(text_dim, common_dim), 'audio': nn.Linear(audio_dim, common_dim), 'video': nn.Linear(video_dim, common_dim), }) # 融合后接情感分类头 self.classifier = nn.Linear(common_dim * 6, num_labels) def forward(self, text_feat, audio_feat, video_feat): text_inv = self.invariant_nets['text'](text_feat) text_spc = self.specific_nets['text'](text_feat) # audio、video 同理 features = torch.cat([text_inv, text_spc, audio_inv, audio_spc, video_inv, video_spc], dim=-1) return self.classifier(features)这段代码里最关键的是分类头输入维度:common_dim * 6,因为三个模态各产出 invariant 和 specific 两份表征后拼接输入。common_dim 就是中间表征的统一维度,这个参数放在train_config.py里,决定模型容量。你的输入模态特征维度如果和预训练用的不一致,要改的是__init__里三个nn.Linear的输入维度,以及它们对应 data_loader 里读进来的特征列。
为什么要拆成两份?这直接为反事实推理铺路。只有把「跨模态共变的部分」和「模态独有的部分」分开,做反事实干预时才知道该动哪一份。如果你只把三模态拼起来,干预任何一个模态都会牵动全局,根本说不清因果。
2.3 causal_model:用反事实干预切断虚假相关
反事实推理要回答的问题形式是:「如果这个人的语气从中性变成愤怒,但文本内容一个字没变,情感标签应该变吗?」如果模型回答「应该变」,说明它把语气和文本内容之间的共现关系当成了因果关系,这就是虚假相关的来源。causal_model.py 的作用就是在训练中主动构造这种「不存在的样本」,强迫模型学习真正的情感语义。
# causal_model.py 的反事实特征构造示意 def forward(self, origin_feature, modality_idx, modality_bank): # origin_feature: 原始样本的多模态融合特征 # modality_idx: 要干预的模态下标,0=文本 1=音频 2=视频 # modality_bank: 从训练集里收集的该模态特征库 cf_feature = origin_feature.clone() # 从特征库里随机采样一个"反事实"特征替换当前模态 sampled = modality_bank[torch.randint(len(modality_bank), (1,))] cf_feature[modality_idx] = sampled # 用干预后的特征重新预测,与原始预测做一致性约束 return self.predict(cf_feature)反事实模块的常见做法就是特征替换或特征重加权。干预后得到的预测如果和原始预测差距过大,说明模型过度依赖这个模态的浅层相关;训练目标就是让干预前后的语义判断保持合理一致。这里要注意,modality_bank的训练方式是有讲究的,通常是训练到中后期,等 MISA 的表征稳定后再加入因果模块,或者通过梯度反转让因果模块只影响特定层。你自己复现时,可以把这类干预写成独立的 loss 项,权重从 0.1 开始调,过大会把模型训练崩。
2.4 MagBERT 与 SelfMM:文本编码与自监督辅助的分工
MagBERT 从命名看是 BERT 的变体,负责文本模态的序列建模。在多模态情感分析里,文本通常走预训练语言模型,输出句向量后对齐到和音频、视频特征相同的维度。如果你要换更强的中文预训练模型,改的是magbert_model.py里的加载逻辑,保持输出维度与train_config.py里配置的文本特征维度一致就行。
SelfMM 走的是自监督路线。常见做法是:在同一段视频里,让文本、音频、视频的表征互相预测,或者做掩码重建。这样 MISA 的输入特征就不是冷启动的,而是带着跨模态对齐信息的。实际跑的时候你会发现:单独训练 MISA 到 70% 准确率左右就上不去了,加上 SelfMM 的辅助损失后才勉强继续涨。如果你的任务是二分类情感正负判断,SelfMM 权重建议给小一点,0.5 以内;如果是细粒度情感强度回归,可以适当调高。
3. 数据准备与配置:从原始多模态数据到可训练样本
3.1 数据集目录规范与 create_dataset.py 的使用
常见的多模态情感分析数据集(比如 MOSI、MOSEI)原始文件是 CSV 或 JSON,里面保存了文本内容、音频特征路径、视频特征路径和情感标签。create_dataset.py做的事就是把这种零散数据整理成框架统一的输入格式,大致步骤是:遍历原始文件 -> 按固定规则切分 train/valid/test -> 把特征路径和标签写入一个索引文件 -> 供 data_loader 按行读取。
python create_dataset.py \ --data_dir ./MOSI/raw \ --output_dir ./dataset \ --train_ratio 0.7 \ --valid_ratio 0.1 \ --test_ratio 0.2这里的train_ratio/valid_ratio/test_ratio是划分比例,注意划分之前最好按说话人或者视频 id 去重,防止同一视频的不同片段同时出现在训练集和测试集里,造成数据泄露。output_dir生成的文件里一般包含三个索引文件和一个特征目录,data_loader 读取时依赖这些路径。换数据集时,尽量保持data_loader.py的返回格式不变——返回(text_feat, audio_feat, video_feat, label)四元组,这样模型层完全不用动。
3.2 特征对齐与采样:音频、视频如何对齐到同一时间轴
多模态情感分析里最耗时间的坑就是对齐。文本天然是离散 token,语音特征一般是 100 帧/秒的连续序列,视频特征可能是 15 帧/秒。三者直接拼接肯定维度错位。框架文档提到用utils/convert.py做转换,常见策略有两种:一种是按固定时间窗口采样,把每段语音切成和文本句子对齐的片段,取片段内特征的均值或最大池化;另一种是利用预训练特征抽取器(比如 Opensmile 提音频特征)对整个句子一次性提特征,这样每个句子就是固定维度的向量。
# utils/convert.py 中的对齐逻辑(典型实现) def align_features(text_feat, audio_feat, video_feat, target_len): # target_len 一般取文本 token 长度或固定长度 aligned_audio = adaptive_avg_pool(audio_feat, target_len) aligned_video = adaptive_avg_pool(video_feat, target_len) assert aligned_audio.shape[0] == aligned_video.shape[0] == len(text_feat) return text_feat, aligned_audio, aligned_videoadaptive_avg_pool是最省事的对齐方式,不管输入帧数多少,输出都固定为目标长度。但要注意,过度的池化会丢掉时序信息,比如语音里的停顿、重音,这些往往是情感判断的关键线索。我一般会把目标长度设成 32 到 64,不要压得太狠。如果你用的是二次特征抽取方案,转换的结果应该提前存成 npy 或 pkl 文件,避免每次训练都在线对齐,那是纯粹浪费时间。
3.3 核心配置项解读:config.py 与 train_config.py 的参数对照表
框架把配置拆成了两个文件,config.py 管理路径和数据基础信息,train_config.py 管理训练超参。下面的表格是按典型反事实多模态项目整理的字段对照,跑之前把这几个参数过一遍,能省掉一半的报错时间。
| 字段 | 归属文件 | 典型值 | 说明 |
|---|---|---|---|
text_dim/audio_dim/video_dim | config.py | 768 / 74 / 35 | 各模态特征维度,必须和数据集实际特征匹配 |
common_dim | config.py | 64 | MISA 中间表征维度,太小欠拟合,太大容易过拟合 |
batch_size | train_config.py | 32 | 显存不够时先降到 16 |
learning_rate | train_config.py | 2e-5 | 用了 BERT 系编码器建议保持这个量级 |
causal_weight | train_config.py | 0.3 | 反事实 loss 的权重,从 0 开始调 |
selfmm_weight | train_config.py | 0.5 | 自监督辅助 loss 权重 |
save_dir | train_config.py | ./checkpoints | 模型保存目录,别和数据集放一起 |
这几个配置里最容易翻车的是text_dim。如果你换了一个 Hugging Face 的预训练模型,输出维度从 768 变成 1024,但 config.py 里没改,训练时模型会直接报维度不匹配。这种错误很隐蔽,因为报错信息通常出现在 MISA 的第一个nn.Linear里,你不会第一时间想到是文本编码器的输出维度变了。
4. 训练复现与避坑排查:把 CLUE 模型跑通到收敛
4.1 环境准备:Python 解释器与 PyTorch 1.3.1 的搭配
项目 README 里写着 Python version 1.7,这明显是笔误,Python 从来没有 1.7 版本,按经验这个项目实际对应的是 Python 3.7 系。PyTorch 1.3.1 是 2019 年底的版本,对应的 torchvision 是 0.4.2,支持 CUDA 10.1。这套组合在今天的机器上直接pip install很可能会遇到依赖冲突,尤其是如果你机器装了新版 CUDA 驱动,老版本 PyTorch 编译好的轮子不一定能用。
conda create -n clue_model python=3.7 conda activate clue_model pip install torch==1.3.1 torchvision==0.4.2 pip install -r requirements.txt先建一个独立的 conda 环境,不要把老版本 PyTorch 装进你日常的开发环境里,否则它会顺手把 numpy、scipy 之类的基础库版本拉低,影响你其他项目。pip install torch==1.3.1会自动匹配当前平台的 CPU 或 CUDA 版本,如果你不打算用 GPU 调试代码,可以先装 CPU 版跑通整个流程,再换 GPU 版做正式训练。定好版本后,在项目根目录跑一遍pip install -r requirements.txt,把剩余依赖补齐。
4.2 启动训练:train.py 的核心参数与两阶段训练
环境就绪后,训练入口是根目录下的 train.py。这个脚本会读取 train_config.py 里的配置,通过 model_carrier.py 组装模型,然后启动训练循环。
python train.py \ --config ./train_config.py \ --model_type misa \ --causal_weight 0.3 \ --resume ./checkpoints/latest.pt--model_type用来切换主模型,除了 misa 应该还有对应的模块名可选,具体看 models 目录下暴露了哪些类。--causal_weight是反事实模块的权重参数,我建议从 0 开始跑一个 epoch 看基准效果,再逐步加到 0.3、0.5,观察验证集指标变化。--resume支持断点续训,路径指向上一次保存的 checkpoint。如果你训练中途断电或者显存爆了,重跑时别从头开始,先看一眼 checkpoints 目录下有没有保存的权重。
使用反事实框架时,一个重要的实操技巧是两阶段训练。第一阶段把causal_weight设成 0,只训练 MISA 和文本编码器,让模态表征稳定下来;第二阶段再打开反事实模块联合训练。直接从头开因果模块,模型一开始的表征还是乱的,反事实干预会把它带偏,训练过程会剧烈震荡。如果你的硬件资源紧张,可以把第一阶段适当缩短,但千万不要省掉。
4.3 三个高频故障:版本冲突、对齐失败、因果损失不降
这一部分是我实际跑反事实多模态项目踩过坑之后整理出来的排查手册,每一条都对应具体的现象,按次序检查基本都能定位。
现象一:pip install -r requirements.txt直接报依赖冲突
原因:项目锁定的 PyTorch 1.3.1 所依赖的 numpy 版本和你环境里的高版本冲突,或者当前 Python 版本太高,老轮子没有对应版本。解决:严格按 4.1 的顺序来,先创建 python=3.7 的 conda 环境,再单独装 torch,最后装 requirements.txt;如果装 torch 失败,去 PyTorch 官网选旧版本的安装命令,不要从 pip 默认源硬装。
现象二:训练到第一个 epoch 结束时报 shape mismatch
原因:典型的模态特征维度没对上。data_loader 返回的 audio_feat 维度是[batch, 74],但 config.py 里写的 audio_dim 是 35,MISA 的线性层吃不下。解决:在 create_dataset.py 跑完后,手动打印一份样本的 shape,和 config.py 里的三个维度逐一核对;这种问题在换数据集时几乎必然出现,提前验证比跑起来再查快得多。
现象三:causal loss 不下降,或者验证集指标完全没变化
原因:反事实模块的梯度没有回传到主模型,或者权重设置太小起不了作用。解决:先确认干预后的输出确实参与了 loss 计算,可以在代码里临时加一行print(cf_feature.requires_grad)验证;如果梯度正常,把 causal_weight 从 0.3 提到 1.0 再观察。如果提权后指标暴跌,说明你的数据量不足以支撑反事实模块训练,这时候优先检查数据集规模,或者用 SelfMM 做预训练热身后再进主训练。
现象四:显存不够,OOM 报错
原因:三个模态的特征和 MISA 的中间表征同时驻留显存,加上 BERT 系文本编码器本身吃显存。解决:先把 batch_size 减半,这是最快的方式;还不行就把文本编码器冻结,只训练后面的层;如果视频帧数很多,检查是不是对齐时没有截断到目标长度,过长的序列会把显存撑爆。
5. 进阶验证:怎么确认反事实模块真的在工作
训练跑通只是第一步,更关键的是验证反事实模块到底起了多大作用。如果你的目标是拿这套框架做毕设或发论文,用下面两个实验给因果模块「验明正身」,说服力比只看准确率强得多。
实验一:模态遮蔽测试(Modal Masking Test)
# 验证脚本的核心逻辑 def modal_mask_test(model, sample, mask_idx): text_feat, audio_feat, video_feat, label = sample if mask_idx == 0: text_feat = torch.zeros_like(text_feat) elif mask_idx == 1: audio_feat = torch.zeros_like(audio_feat) else: video_feat = torch.zeros_like(video_feat) with torch.no_grad(): pred = model(text_feat, audio_feat, video_feat) return pred分别把文本、音频、视频置零,观察预测结果的变化幅度。如果模型对文本模态遮蔽的反应最大,说明它主要依赖文本语义,这是符合预期的;如果遮蔽音频后预测几乎不变,说明音频模态没被有效使用,问题出在特征质量或对齐方式,不一定是模型的错。这一步能帮你快速定位各个模态的贡献度,也是写论文时放的必要实验图。
实验二:反事实一致性检查
从测试集里抽一条样本,保持文本不变,从特征库里随机替换这条样本的音频特征,重复多次,记录标签分布。一个健康的反事实模型应该表现出:文本语义主导时,音频替换不改变预测;文本语义模糊时,音频特征才真正影响预测。如果替换音频 10 次,预测结果 9 次都翻转,说明模型把音频当成了捷径,反事实模块没有真正约束住它。
这个检查看起来简单,但它能暴露一个很隐晦的问题:反事实模块只是形式上接了损失,实际没有改变模型的决策依赖。当初我调第一个版本时就栽在这上面,acc 看着还行,一做模态遮蔽发现音频路径根本是废弃状态,白白占着计算量。从那以后我每次跑这类反事实框架,都会强制走一遍遮蔽测试和一致性检查,把两个实验脚本直接固化在项目里,代替目测猜模型行为。希望这个方法也能帮你在复现和改造这套框架时少走弯路。
本文还有配套的精品资源,点击获取