简介这是一套基于Python实现的多模态融合情感分析完整项目面向需要完成毕业设计、期末大作业或课程设计的学生以及希望入门多模态学习的开发者。项目支持文本、语音、图片、视频四类输入覆盖MOSI、MOSEI、IEMOCAP等常用数据集可帮助读者理解多模态特征提取与融合建模的完整流程。资源包共20个文件包含5个py源码脚本、9个pickle预处理特征文件、3个zip数据集压缩包以及1份pdf项目文档、1份md说明和1张结果图整体约56.91MB代码注释清晰、结构完整。目前已有96人学习。读者可获得可直接运行的源码、数据预处理脚本、模型定义文件与配套文档便于快速复现实验、撰写论文或在此基础上二次开发适合新手对照学习与调试。1. 多模态情感分析系统一份能跑通文本、语音、图像、视频四路输入的毕设级源码包如果你正在为毕业设计或期末大作业找一个「技术栈够宽、能演示、有数据集、代码能跑」的题目多模态情感分析几乎是命中率最高的方向之一。它同时踩中了情感计算、多模态融合、深度学习部署三个热点答辩时老师问得出问题演示时又能直观看到「同一句话配不同表情模型给出不同情绪判断」的效果。但真正动手时大多数人会卡在三个地方四路模态的数据怎么对齐、融合层到底怎么拼、数据集从哪来。这份资源把文本、语音、图像、视频四条链路的源码、文档和数据集打包在一起省掉了最耗时的数据采集和预处理环节。它适合两类人一是时间紧、需要快速搭出可演示系统的同学二是想理解多模态融合机制、但不想从零写数据管线的开发者。下面按「先跑通、再拆解、后避坑」的顺序把这份包拆开讲清楚。2. 四模态数据管线从原始文件到模型输入的完整链路拿到一个多模态项目最忌讳一上来就翻模型代码。模型结构再花哨数据管线对不上训练时 loss 不动或者直接报 shape 错误你连问题出在哪都找不到。这一章先把四条模态各自的数据处理链路讲透每条链路都落到可执行的代码和参数上。2.1 文本模态分词、截断与嵌入层对齐文本链路的核心是把变长句子变成固定长度的向量序列。这份源码里文本部分用的是预训练词向量加 BiLSTM 的经典组合预处理脚本在data_process/text_pipeline.py。关键参数有三个最大序列长度、词表大小、填充策略。# text_pipeline.py 核心片段 import torch from torch.nn.utils.rnn import pad_sequence from transformers import BertTokenizer # 常见做法是用预训练 tokenizer词表大小 30522 对应 bert-base 中文词表 tokenizer BertTokenizer.from_pretrained(bert-base-chinese) MAX_LEN 128 # 超过截断不足补 PAD def encode_text(sentences, max_lenMAX_LEN): encodings tokenizer( sentences, paddingmax_length, # 统一补齐到 max_len truncationTrue, # 超长直接截断 max_lengthmax_len, return_tensorspt ) return encodings[input_ids], encodings[attention_mask]逻辑说明paddingmax_length保证一个 batch 内所有样本长度一致避免后续pad_sequence再手动处理。attention_mask必须保留因为补出来的 PAD 位置在注意力计算里要屏蔽掉否则模型会把填充符当成真实语义。参数上MAX_LEN128是中文情感分析任务的常见取值短文本数据集如在线评论够用如果你的文本是长影评建议调到 256但显存占用会明显上升。截断策略默认从尾部截如果关键情感词常出现在句尾要改成truncationonly_first并手动保留尾部。2.2 语音与图像梅尔频谱和面部区域裁剪语音链路走的是「音频转梅尔频谱图再当图像处理」的路线这样语音和图像可以共用同一个 CNN 骨干减少参数量。源码里语音预处理在data_process/audio_pipeline.py图像在image_pipeline.py。# audio_pipeline.py 核心片段 import librosa import numpy as np SAMPLE_RATE 16000 # 统一采样率常见语音模型标准 N_MELS 128 # 梅尔滤波器组数量 HOP_LENGTH 512 # 帧移决定时间维度分辨率 def audio_to_mel(wav_path): y, sr librosa.load(wav_path, srSAMPLE_RATE) mel librosa.feature.melspectrogram( yy, srsr, n_melsN_MELS, hop_lengthHOP_LENGTH ) log_mel librosa.power_to_db(mel, refnp.max) # 转 dB压缩动态范围 return log_mel # 形状 (128, T)逻辑说明SAMPLE_RATE16000是语音情感识别的主流采样率高于这个值对情感判别帮助有限反而增加计算量。N_MELS128比常见的 64 保留了更多频带细节适合情感这种需要捕捉韵律变化的任务。power_to_db这一步不能省原始功率谱数值跨度大转 dB 后模型更容易收敛。图像链路则用 MTCNN 或 OpenCV 的 Haar 级联做面部检测裁出人脸区域再缩放到 224×224和语音频谱图一起送进 CNN。这里有个容易忽略的点语音频谱图的时间维度 T 是变化的需要统一 resize 到固定尺寸源码里用的是双线性插值参数在配置文件config.yaml的audio.resize项。2.3 视频模态抽帧、对齐与时间维度压缩视频是最麻烦的一路因为它同时包含视觉和听觉信息而且时间维度最长。源码的处理策略是按固定帧率抽帧每段视频取 16 帧同时提取对应的音频段视觉和听觉分别编码后再做时序融合。# video_pipeline.py 核心片段 import cv2 FRAME_NUM 16 # 每段视频抽帧数 FRAME_SIZE 224 # 单帧缩放尺寸 def extract_frames(video_path, frame_numFRAME_NUM): cap cv2.VideoCapture(video_path) total int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) # 均匀采样避免只取到开头或结尾 indices np.linspace(0, total - 1, frame_num, dtypeint) frames [] for idx in indices: cap.set(cv2.CAP_PROP_POS_FRAMES, idx) ret, frame cap.read() if ret: frame cv2.resize(frame, (FRAME_SIZE, FRAME_SIZE)) frames.append(frame) cap.release() return np.array(frames) # (16, 224, 224, 3)逻辑说明np.linspace做均匀采样是关键如果直接取前 16 帧遇到长视频就只覆盖了开头几秒情感信息严重丢失。FRAME_NUM16是精度和显存的折中调到 32 能捕捉更细的表情变化但 batch size 要相应减半。抽出来的帧序列送进 3D CNN 或 TimeSformer 做时序编码源码默认用的是轻量化的 3D ResNet参数量小适合单卡训练。视频对应的音频段用同样的梅尔频谱流程处理最后视觉和听觉特征在通道维度拼接。提示四条链路的数据必须按样本 ID 严格对齐源码里用sample_id作为主键文本、语音、图像、视频文件命名都带这个 ID。如果你自己换数据集命名规则一定要统一否则训练时会出现「文本配错语音」的玄学问题。3. 融合层设计早期融合、晚期融合还是交叉注意力数据管线跑通后下一个决策点是融合策略。这不是拍脑袋选的不同融合方式对最终指标的影响可能超过模型深度本身。这一章把源码里实现的三种融合方式拆开对比并给出选择依据。3.1 三种融合方式的代码实现与参数量对比源码在models/fusion.py里实现了三种融合早期融合拼接、晚期融合投票、交叉注意力跨模态交互。早期融合最简单把四路特征在通道维度拼起来送进分类器晚期融合是每路单独出预测结果再加权平均交叉注意力让文本特征去 query 语音和图像特征捕捉模态间的关联。# fusion.py 核心片段交叉注意力融合 import torch.nn as nn class CrossAttentionFusion(nn.Module): def __init__(self, dim256, num_heads4): super().__init__() self.text_proj nn.Linear(768, dim) # BERT 输出 768 维 self.audio_proj nn.Linear(128, dim) # 梅尔特征 128 维 self.image_proj nn.Linear(512, dim) # CNN 输出 512 维 self.cross_attn nn.MultiheadAttention(dim, num_heads, batch_firstTrue) self.classifier nn.Linear(dim, 3) # 三分类积极/消极/中性 def forward(self, text_feat, audio_feat, image_feat): t self.text_proj(text_feat).unsqueeze(1) a self.audio_proj(audio_feat).unsqueeze(1) i self.image_proj(image_feat).unsqueeze(1) # 文本作为 query语音和图像作为 key/value fused, _ self.cross_attn(t, torch.cat([a, i], dim1), torch.cat([a, i], dim1)) return self.classifier(fused.squeeze(1))逻辑说明num_heads4是注意力头数的常用起点头数太多在小数据集上容易过拟合。三个投影层把不同模态的特征统一到 256 维这是为了后续注意力计算时维度匹配。文本作为 query 的设计思路是情感判定的主导信息通常来自文本语音和图像起补充和修正作用。如果你的场景里语音韵律更重要比如呼叫中心情绪识别可以把语音设为 query。融合方式参数量训练速度适合场景早期融合最少最快模态对齐好、数据量小晚期融合中等中等某路模态缺失严重交叉注意力最多最慢模态间关联强、数据充足3.2 模态缺失时的降级策略实际部署时经常遇到某路模态缺失的情况比如只有文本没有视频。源码里做了一个降级机制当某路特征全为零时融合层自动跳过对应的投影和注意力计算用剩余模态出结果。这个逻辑在fusion.py的forward里通过 mask 实现。# 模态缺失处理片段 def forward(self, text_feat, audio_featNone, image_featNone): t self.text_proj(text_feat).unsqueeze(1) available [t] if audio_feat is not None: available.append(self.audio_proj(audio_feat).unsqueeze(1)) if image_feat is not None: available.append(self.image_proj(image_feat).unsqueeze(1)) kv torch.cat(available[1:], dim1) if len(available) 1 else t fused, _ self.cross_attn(t, kv, kv) return self.classifier(fused.squeeze(1))逻辑说明available列表动态收集可用的模态特征kv在只有文本时退化为文本自身相当于自注意力。这个设计让同一套模型权重能适应不同模态组合的输入不用为每种组合单独训练。参数上不需要额外调整但要注意如果训练时四路齐全、推理时缺了两路指标会下降建议在训练阶段就随机 drop 掉某路模态做数据增强提升鲁棒性。注意交叉注意力融合的显存占用和模态数量是平方关系四路模态全开时单卡 8G 显存跑 batch size 16 会比较紧张。如果显存不够优先降 batch size不要轻易减注意力头数头数减少对指标的影响更明显。4. 训练与评估损失函数、指标和常见翻车点模型搭好之后训练过程本身也有不少讲究。多模态任务的损失函数设计、评估指标选择、以及训练中的不稳定现象都是实际动手时绕不开的。这一章把源码里的训练配置和评估逻辑讲清楚并给出几个我踩过的坑。4.1 损失函数与类别不平衡处理情感分析数据集天然存在类别不平衡积极样本往往远多于消极样本。源码默认用带类别权重的交叉熵损失权重根据训练集类别频率自动计算。# train.py 损失函数片段 import torch.nn as nn # 假设训练集类别分布积极 6000消极 2000中性 2000 class_counts [6000, 2000, 2000] weights torch.tensor([1.0 / c for c in class_counts]) weights weights / weights.sum() * len(class_counts) # 归一化 criterion nn.CrossEntropyLoss(weightweights)逻辑说明weight参数让少数类样本的损失被放大避免模型偏向多数类。归一化那一步是为了让权重均值保持在 1 附近不改变整体学习率量级。如果你的数据集里中性样本特别少可以把中性类的权重再手动乘 1.5。另一个常见做法是过采样少数类但多模态数据过采样容易导致同一视频被重复抽帧反而过拟合源码里没采用。4.2 评估指标准确率之外的 F1 和混淆矩阵多模态情感分析不能只看准确率。如果消极样本只占 10%模型全预测积极也能有 90% 准确率但完全没用。源码的评估脚本eval.py同时输出宏平均 F1、加权 F1 和混淆矩阵。# eval.py 指标计算片段 from sklearn.metrics import f1_score, confusion_matrix def evaluate(y_true, y_pred): macro_f1 f1_score(y_true, y_pred, averagemacro) weighted_f1 f1_score(y_true, y_pred, averageweighted) cm confusion_matrix(y_true, y_pred) return macro_f1, weighted_f1, cm逻辑说明macro平均对每个类别一视同仁适合关注少数类表现的场景weighted按类别样本数加权更接近整体准确率的直觉。混淆矩阵能看出模型到底把哪两类搞混了比如积极和中性混淆多说明模型对弱情感不敏感可以考虑增加中性类的训练样本或调整分类阈值。参数上没有需要调的但建议每次评估都保存混淆矩阵图答辩时直接展示比单纯报数字有说服力。提示训练时如果发现 loss 震荡厉害先检查学习率是不是太大。多模态融合层的梯度通常比单模态大源码默认学习率 1e-4比纯文本任务的 2e-5 高但比从头训练的 1e-3 低。如果用了预训练 BERT文本分支的学习率要单独调小源码里用参数组实现了差分学习率。5. 避坑与排查四模态项目里最容易翻车的五个地方这一章不讲新功能专门记录我在跑这份源码和类似项目时遇到的真实问题。每条按「现象 → 原因 → 解决」写你遇到类似情况可以直接对照。现象一训练时 loss 一直是 nan。原因语音梅尔频谱转 dB 时出现了 log(0)power_to_db内部有截断但如果音频文件本身是静音或损坏功率谱全零转 dB 后是负无穷。解决在audio_to_mel里加一个极小值保护mel np.maximum(mel, 1e-10)再转 dB。另外检查数据集里有没有时长为零的音频文件。现象二视频模态的准确率远低于文本模态。原因抽帧时用了np.linspace均匀采样但视频里人脸可能只出现在某几秒均匀采样恰好避开了人脸区域。解决先用人脸检测筛出含人脸的帧再在这些帧里均匀采样。源码里提供了一个可选的人脸优先采样开关在config.yaml的video.face_first设为true。现象三四路模态全开时显存溢出。原因交叉注意力融合的中间张量没及时释放加上 BERT 的 768 维输出和 CNN 的 512 维输出拼接后维度很大。解决在融合层前加一个torch.cuda.empty_cache()不解决根本问题正确做法是把文本分支的 BERT 换成蒸馏版如 6 层或者把 batch size 降到 8同时开启梯度累积模拟大 batch。现象四验证集指标比训练集还高。原因数据泄露。四条模态的数据划分没有按样本 ID 统一可能出现同一个样本的文本在训练集、语音在验证集的情况。解决划分数据集时以sample_id为单位整体划分源码的split_dataset.py已经做了这件事但如果你自己换数据务必检查。现象五推理时单条样本预测结果和批量预测不一致。原因批量推理时 padding 和 attention mask 处理正确单条推理时忘了加 batch 维度导致模型把序列长度当成了 batch size。解决单条推理也要unsqueeze(0)加 batch 维并且 attention mask 要同步加。这个坑很隐蔽因为模型不报错只是结果乱掉。6. 从跑通到用起来模型导出、推理加速和一个小技巧把训练跑通只是第一步真正要拿去演示或集成到应用里还得解决导出和推理速度的问题。这一章讲两个实用技巧怎么把 PyTorch 模型导出成 ONNX 提升推理速度以及怎么用置信度阈值过滤低质量预测。6.1 导出 ONNX 并验证数值一致性PyTorch 模型在 Python 环境里推理没问题但如果要集成到其他语言的服务里ONNX 是更通用的格式。导出时最大的坑是动态维度处理多模态输入的序列长度和模态组合都是可变的。# export_onnx.py 核心片段 import torch model.eval() dummy_text torch.randn(1, 128, 768) dummy_audio torch.randn(1, 128) dummy_image torch.randn(1, 512) torch.onnx.export( model, (dummy_text, dummy_audio, dummy_image), multimodal_sentiment.onnx, input_names[text, audio, image], output_names[logits], dynamic_axes{ text: {0: batch, 1: seq_len}, audio: {0: batch}, image: {0: batch} }, opset_version14 )逻辑说明dynamic_axes把 batch 维和文本序列长度标为动态这样导出后的模型能接受不同 batch size 和不同长度的文本。opset_version14对 MultiheadAttention 的支持比较完整低于 12 的版本可能报不支持的操作。导出后一定要用onnxruntime跑一遍同样的输入对比 PyTorch 和 ONNX 的输出差异如果最大绝对误差超过 1e-4说明某些算子导出有问题常见的是 LayerNorm 和 GELU。6.2 置信度阈值过滤与人工复核队列情感分析模型在遇到模棱两可的样本时softmax 输出的最大概率可能只有 0.4 左右这种预测直接采用风险很大。我的习惯是在推理脚本里加一个置信度阈值低于阈值的样本不直接输出结果而是打上「待复核」标签。# inference.py 置信度过滤片段 import torch.nn.functional as F def predict_with_threshold(model, inputs, threshold0.6): logits model(*inputs) probs F.softmax(logits, dim-1) max_prob, pred probs.max(dim-1) if max_prob.item() threshold: return 待复核, max_prob.item() return pred.item(), max_prob.item()逻辑说明threshold0.6是经验值低于这个值说明模型对判断没把握。这个机制在演示时特别有用你可以展示「模型知道自己不确定」的能力比强行输出一个错误结果更可信。阈值调高会减少误判但增加人工工作量调低则相反根据你的场景容忍度来定。我一般会在验证集上画一条阈值-准确率曲线选准确率开始明显下降的拐点作为阈值。从那以后我每次交付多模态项目都会强制走一遍「单条推理一致性检查」和「置信度分布可视化」这两个步骤帮我拦住了至少三次会在演示现场翻车的问题。希望帮到你。本文还有配套的精品资源点击获取