简介基于Python与TensorFlow开发的多模态融合情感分析工程专注解决文本、语音、图片、视频四类数据的统一情感判别适合作为毕业设计、课程设计或项目开发的基础框架。项目采用分层融合思路先提取各单模态特征再逐步扩展为双模态、三模态向量最终经Softmax分类为喜、怒、哀、其他四种情感能够有效支撑用户分层、舆情监测与目标分析等场景。压缩包共二十一个文件包含五个Python源码文件、九个pickle预处理数据、三个数据集子包另附PDF项目文档、Markdown说明、文本说明与效果图整体大小约五十八MB。环境基于Python三点六与TensorFlow一点七CPU或GPU均可运行部署门槛较低。目前已有六百七十二人学习下载源码经过严格测试可在理解多模态融合机制的基础上自由扩展包内目录清晰自带完整数据集与文档能帮助快速搭建实验环境也适合作为论文或答辩系统的原型参考。1. 为什么情感分析必须走向多模态融合单纯的文本情感分析在遇到反讽、阴阳怪气、崩溃大哭时几乎必翻车——模型看到的字面意思是“我真是谢谢你”实际情绪却是愤怒。把语音的语调、图片的色彩、视频里人物的表情一起喂给模型后同一句话在不同语气下的判定结果会截然不同这也是近年情感分析从纯 NLP 转向多模态融合算法研究的主要原因。这套基于 Python 的多模态融合情感分析项目输入覆盖文本、语音、图片、视频四种形态输出积极、消极、中性等细粒度情感标签并携带完整源码、项目文档和可直接跑通的数据集适合拿来当毕业设计、课程设计也可以作为多模态时序数据融合方法的入门工程。你需要自己装好 Python 3.9 和 PyTorch其余依赖都在源码的 requirements 里。2. 模态对齐与融合文本、语音、图像特征怎么放进同一个向量空间2.1 先理解四种输入为什么不能直接拼接文本经过 tokenizer 后是离散的 token id语音是采样率 16000 Hz 的一维波形图片是三维像素矩阵视频则是“时间维 × 高 × 宽 × 通道”。如果粗暴地把这些特征拼在一起维度对不上语义空间也不一致。常见的做法是让每个模态分别经过自己的特征抽取器先映射成相同维度的向量比如统一到 512 维或 768 维然后再做融合。这个“先抽特征、后融合”的流程决定了整个项目的代码结构。模态对齐要解决的另一个问题是时间尺度一段 10 秒的视频语音帧有 32000 个左右采样点视频只有 300 帧文本可能只有 15 个 token。三者的时间分辨率完全不同直接按位相加没有意义。在这个项目的数据集里我一般按句子级别做对齐也就是整段对话只取一个文本向量、一个语音向量、以及视频帧的平均池化向量。精度要求更高时可以用强制对齐工具把语音和视频帧切到词粒度但初版工程不建议这么干训练难度会大很多。2.2 特征抽取层的选型从文本情感分析到多模态情感分析文本模态的主流选择是 BERT 系列。用bert-base-uncased取出[CLS]位置的输出向量这个向量已经包含整句话的上下文语义维度是 768。语音模态不必自己训练声学模型常见做法是用 openSMILE 提取 IS13 语音特征或者直接对波形做 Mel 频谱图再用一个小的 CNN 或预训练的 wav2vec2 来编码。图片和视频帧用 ResNet50去掉最后的全连接分类层把 2048 维的池化特征作为视觉表示。选择这些模型的原因是它们在各自的单模态任务上已经收敛得足够好多模态训练时只需要微调最后一两层否则显存和时间成本都不可控。如果你用的是更好的显卡可以把 BERT 换成 RoBERTa把 ResNet50 换成 Swin Transformer但注意数据集规模不够大时大模型反而更容易过拟合。2.3 融合算法拼接、注意力加权与门控机制得到三到四个模态向量后最朴素的是拼接后接全连接层公式上就是h concat([h_t, h_a, h_v])再经过两层 MLP 输出分类概率。这个方案在 MOSI 数据集上能跑到 70% 左右的双分类准确率适合作为基线。想要更高就要上跨模态注意力让文本向量作为 Query语音和图像向量作为 Key、Value计算注意力分数后加权融合。门控融合是另一个常用的思路给每个模态学一个 0 到 1 之间的权重表示当前样本里哪个模态更可信。面对视频里人物不说话、只有画面表情的样本门控会自动降低语音模态的权重。实现上就是一个单层线性层加 sigmoid计算量几乎可以忽略。三种方式都值得写进实验对比部分这也是毕业设计评审喜欢看的内容不仅有结果还有选型过程。3. Python 实现从特征抽取到融合分类的完整流程3.1 工程目录结构与依赖安装整个项目建议按下面的结构组织源码包和文档里也基本是这种分层方式multimodal_sentiment/ ├── data/ │ ├── raw/ # 原始视频、文本、音频 │ ├── processed/ # 抽取后的特征 │ └── labels.csv ├── models/ │ ├── text_encoder.py │ ├── audio_encoder.py │ ├── video_encoder.py │ └── fusion_model.py ├── utils/ │ ├── video_utils.py │ └── audio_utils.py ├── train.py ├── predict.py └── requirements.txt依赖部分以 PyTorch 为核心我在requirements.txt里通常固定这样几个库torch、transformers、librosa、opencv-python、numpy、pandas、scikit-learn、flask。安装时直接用pip install -r requirements.txt如果你的 CUDA 环境还没配好记得先按显卡驱动版本装配套的 PyTorch否则 model 跑到 GPU 上会报 CUDA 版本不匹配。3.2 数据预处理视频抽帧、语音重采样、文本清洗拿到一个 mp4 文件第一步是分离出视频帧和音频轨然后对文本内容做清洗。下面的函数处理视频帧抽取import cv2 def extract_frames(video_path, target_fps2): cap cv2.VideoCapture(video_path) frames [] fps cap.get(cv2.CAP_PROP_FPS) frame_interval int(fps / target_fps) # 每两帧取一帧 while True: ret, frame cap.read() if not ret: break frame_id int(cap.get(cv2.CAP_PROP_POS_FRAMES)) if frame_id % frame_interval 0: frame cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) frame cv2.resize(frame, (224, 224)) frames.append(frame) cap.release() return frames参数说明target_fps2表示每秒抽 2 帧对于短视频情感分类已经足够10 秒的视频会得到约 20 帧。frame_interval是根据原始视频帧率动态计算出来的抽帧间隔避免每段视频因为帧率不同导致帧数差异过大。后续 ResNet50 要求输入尺寸为 224×224所以在这里直接 resize省得在模型 forward 里重复做。音频和文本的预处理相对简单。音频用librosa.load(path, sr16000, monoTrue)读取强制单声道和 16k 采样率文本则去掉 URL、特殊符号和多余空格。注意文本清洗不能过度比如“not good”里的否定词不能去掉否则语义就反了。3.3 BERT、ResNet、Mel 频谱三路特征抽取特征抽取是这套工程的核心环节我拆成三个函数分别处理。先看文本from transformers import BertTokenizer, BertModel import torch tokenizer BertTokenizer.from_pretrained(bert-base-uncased) bert BertModel.from_pretrained(bert-base-uncased) def extract_text_feature(text, max_len32): inputs tokenizer( text, truncationTrue, paddingmax_length, max_lengthmax_len, return_tensorspt ) with torch.no_grad(): outputs bert(**inputs) cls_vector outputs.last_hidden_state[:, 0, :] # [CLS] return cls_vector.squeeze(0) # shape: [768]参数说明max_len32对应短视频里一句口语的长度paddingmax_length把短的句子补到 32 token。outputs.last_hidden_state[:, 0, :]取其第一维的第一个位置也就是[CLS]token 的输出BERT 预训练时[CLS]被设计为整句语义的汇总向量。音频和视频特征抽取函数如下import librosa import torch import torch.nn as nn def extract_audio_feature(audio_path, n_mels128): y, sr librosa.load(audio_path, sr16000, monoTrue) mel librosa.feature.melspectrogram( yy, srsr, n_melsn_mels, hop_length160 ) log_mel librosa.power_to_db(mel) # 简单池化把 [n_mels, T] 压成 [128, 1] feature torch.tensor(log_mel.mean(axis1)).float() return feature.view(1, -1) # [1, 128]参数说明n_mels128是 Mel 滤波器组数量hop_length160对应 16k 采样率下每 10ms 一个帧这是语音情感分析里常用的帧移。power_to_db把能量谱转成对数分贝尺度更符合人耳听觉特性。这里用mean(axis1)做时间池化把整段语音压成一个固定维度的向量便于和文本、图像特征拼接。视频帧特征直接用 ResNet50from torchvision import models, transforms resnet models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V1) resnet.fc nn.Identity() # 去掉末层分类头 transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) def extract_video_feature(frames): feats [] with torch.no_grad(): for frame in frames: tensor transform(frame).unsqueeze(0) feat resnet(tensor) # [1, 2048] feats.append(feat) return torch.stack(feats).mean(dim0) # [2048]参数说明weightsIMAGENET1K_V1使用 ImageNet 预训练权重resnet.fc nn.Identity()让输出变成 2048 维特征而不是 1000 类的 logits。对一帧提取一次特征后对所有帧取均值得到视频片段级别的全局视觉特征。这种均值池化在短视频情感分析里是常见的 baseline 做法缺点是会丢失表情的时域变化后续可以换成注意力池化。3.4 融合分类模型与完整训练循环三路特征分别是 768 维、128 维、2048 维先各自过一层 Linear 映射到统一维度fusion_dim256再拼接或做注意力融合import torch.nn as nn class MultimodalFusion(nn.Module): def __init__(self, text_dim768, audio_dim128, video_dim2048, fusion_dim256, num_classes3): super().__init__() self.text_proj nn.Linear(text_dim, fusion_dim) self.audio_proj nn.Linear(audio_dim, fusion_dim) self.video_proj nn.Linear(video_dim, fusion_dim) self.gate nn.Linear(fusion_dim * 3, 3) # 门控权重 self.classifier nn.Sequential( nn.Linear(fusion_dim * 3, 256), nn.ReLU(), nn.Dropout(0.3), nn.Linear(256, num_classes) ) def forward(self, text_feat, audio_feat, video_feat): t torch.relu(self.text_proj(text_feat)) a torch.relu(self.audio_proj(audio_feat)) v torch.relu(self.video_proj(video_feat)) concat torch.cat([t, a, v], dim-1) gate_weight torch.sigmoid(self.gate(concat)) # [B, 3] gated torch.stack([t, a, v], dim1) * gate_weight.unsqueeze(-1) gated gated.view(gated.size(0), -1) # 展平 return self.classifier(gated)逻辑说明text_proj等三个映射层把不同维度统一到 256之后拼接成 768 维门控部分学习三个模态的置信度权重用sigmoid限制在 0 到 1 之间对模态向量做缩放后再展平输入分类器。这个设计比单纯拼接多了可解释性训练结束后打印gate_weight可以看到模型对哪些样本更依赖语音、哪些更依赖文本。训练循环使用标准的交叉熵损失加一点类别权重处理不平衡数据from sklearn.metrics import accuracy_score, f1_score class_counts train_df[label].value_counts().sort_index().values weights 1.0 / torch.tensor(class_counts, dtypetorch.float) weights weights / weights.sum() * len(class_counts) criterion nn.CrossEntropyLoss(weightweights.to(device))操作步骤每个 epoch 遍历 train_loader前向传播得到 logits反向传播更新参数验证集每两个 epoch 算一次准确率和加权 F1保存最佳模型。weights归一化的作用是让少数类样本在损失函数里获得更高权重训练前先print(weights)确认数值范围如果某项大于 5说明类别严重失衡需要再引入 Focal Loss 或者做数据增强。4. 数据集选择、训练调参与踩坑记录4.1 MOSI、MOSEI 与自建数据怎么选这个项目自带的数据集一般整理成labels.csv列包含video_id, text, audio_path, video_path, label。如果你要替换成公开数据集优先考虑 CMU-MOSI 和 CMU-MOSEI它们是多模态情感分析最常用的 benchmark。两者对比如下数据集样本量模态标签粒度适合场景CMU-MOSI2199 段视频文本、语音、视频7 分类或 3 分类快速迭代、课程设计CMU-MOSEI23453 段视频文本、语音、视频7 分类或 3 分类毕业论文、模型调优自建微博/抖音数据自定文本、图片2~3 分类中文场景、业务定制MOSI 样本量小一小时内能跑完一个 baseline 实验MOSEI 是它的十倍规模适合验证融合算法的泛化能力。自建数据时要注意标签噪声多人标注后要做一致性检验常见做法是算 Cohens Kappa低于 0.6 的样本直接丢弃。4.2 损失函数、学习率与批大小的推荐配置在训练时不能照搬单模态情感分析的参数多模态模型的收敛行为差异很大。我这边跑通的基础配置如下超参数推荐值说明optimizerAdamW带权重衰减配合 BERT 微调更稳学习率2e-5如果 BERT 全量微调不要超过 5e-5batch size16视频帧特征显存占用大不够就降到 8warmup steps200前 200 步线性预热避免收敛震荡训练轮数20配早停patience 设为 3dropout0.3防止三路特征拼接后过拟合提示BERT 微调部分的学习率比融合层低一个量级时效果更稳定。比如融合层设5e-4BERT 设2e-5两者用不同的参数组传入优化器。门控层的权重初始化可以偏大一些让初期模型更信任文本模态文本特征通常比语音和视觉更稳定。4.3 训练过程中最常见的四个坑第一个坑是 CUDA 显存不足。三路特征抽取如果都在线做每 GPU 每秒只能处理几个样本。常见做法是先把三路特征离线抽取并保存为.npy文件训练时只加载特征而不加载原始视频和音频显存占用直接降到四分之一。第二个坑是语音和视频帧不匹配。某段视频是无声的librosa.load返回全零向量Mel 特征全是极小值或者视频抽帧失败返回空列表torch.stack直接报错。解决办法是在数据读取阶段做合法性检查空帧用零向量填充同时在labels.csv里标记该样本是否完整。第三个坑是过拟合。MOSI 只有两千多个样本三模态拼接后参数量很大训练集 loss 降到 0.1 而验证集 accuracy 只有 60% 的情况很常见。用早停加上更强的数据增强比如对 Mel 频谱做时间和频率掩码对视频帧做随机裁剪能明显改善。第四个坑是标签分布不均。情感数据往往是中性样本占多数积极和消极偏少。训练时监控每个类别的召回率如果消极类的召回率长期低于 40%不要急着换模型优先调整类别权重或采样策略。5. 视频级联推理与 Web 可视化把模型变成可演示的系统5.1 封装统一的预测接口训练完成后把特征抽取和模型推理串成一个端到端函数。输入一个视频文件路径先抽帧和提取音频再调用extract_text_feature等函数最后进入融合模型输出概率def predict_video(video_path, text): frames extract_frames(video_path) audio_path temp_wav_path(video_path) # ffmpeg 提取音频 text_feat extract_text_feature(text) audio_feat extract_audio_feature(audio_path) video_feat extract_video_feature(frames) with torch.no_grad(): logits model(text_feat, audio_feat, video_feat) prob torch.softmax(logits, dim-1) return { label: int(torch.argmax(prob)), probabilities: prob.tolist() }逻辑说明函数内部先调用第 3 章定义的三个特征抽取函数再把特征传入训练好的MultimodalFusion模型。softmax把 logits 转成概率分布tolist()方便后续 JSON 序列化。temp_wav_path依赖 ffmpegWindows 下要确保 ffmpeg 加入了系统 PATH。5.2 用 Flask 搭建上传和展示页演示环节是毕业设计答辩的加分项。用 Flask 写一个 POST 接口接收上传的 mp4 文件和文本内容返回情感标签和三个模态的概率from flask import Flask, request, jsonify, render_template import os app Flask(__name__) app.route(/upload, methods[POST]) def upload(): video request.files[video] text request.form.get(text, ) save_path os.path.join(tmp, video.filename) video.save(save_path) result predict_video(save_path, text) return jsonify(result) if __name__ __main__: app.run(host0.0.0.0, port8080, debugFalse)参数说明request.files[video]读取前端上传的文件host0.0.0.0表示允许局域网内其他机器访问答辩时可以让老师用手机直接提交测试视频debugFalse避免调试模式暴露完整堆栈。前端页面用一个简单的表单enctypemultipart/form-data提交后把 JSON 结果渲染到页面上。把服务跑起来后用浏览器访问 8080 端口上传一个带情绪表达的短视频就能看到文本、语音、图像三个模态各自的分数和最终融合结果整个流程也就从代码变成了可演示的系统。本文还有配套的精品资源点击获取
