
简介面向毕业设计、课程设计及项目开发的多模态情感分析完整实现包基于JupyterPython开发整合文本、图像等多模态输入提供从数据清洗、特征提取、模型构建到训练预测与结果评估的端到端流程。项目总计2000个文件压缩包大小约202MB其中1997个txt数据与结果文件便于分阶段测试、扩展训练集或做交叉验证另有项目说明、实验报告文档及主程序脚本目录按数据、脚本、文档划分方便直接定位核心代码。已有522人学习使用源码经过严格测试可独立运行配合项目文档能深入理解多模态特征融合、情感分类等关键环节的实现细节既可支撑课程设计与毕业设计答辩演示也可作为后续智能交互、舆情分析等研究方向的起步模板。内容预览显示包含大量文本样本与预测输出文件读者可对照结果验证模型效果并在现有代码基础上继续延伸应用。1. 多模态情感分析不是三分类JupyterPython把它变成了可上手的项目社交媒体上的负面舆情往往不是靠一段文案表达出来的。语气里压抑的颤抖、画面里的暗沉色调、配上看似中性的文字组合在一起才是真实态度。如果只给模型看文本准确率会卡在75%上下加入语音和图像特征后通常能越过85%。这个项目标题把JupyterPython放在最前面说明它要的并不是一篇paper复现而是从安装环境到模型输出一整套能直接跑通的交付物。对毕业设计和课程设计来说多模态情感分析模型恰好覆盖了数据采集、特征工程、深度学习、可视化展示几个必备环节对想进入多模态方向的开发者来说它又是一个能讲清楚“为什么融合比单模态好”的最小实验台。下面直接进入架构选型把每一步说清楚。2. 多模态情感分析的模型架构文本、音频、图像如何编码与融合2.1 单个模态先编码文本用BERT音频用MFCC加时序网络图像用ResNet多模态情感分析的前提是每个模态有自己的特征形式。文本是token序列音频是采样点或频谱图像是像素。工程上最常见做法是分别用一个预训练或轻量级编码器把输入变成向量再让融合层去组合。文本侧我一般会优先考虑中文预训练模型比如bert-base-chinese取最后一层CLS向量作为句子表示如果训练机器没有显卡退而求其次用Word2Vec把词向量平均再加一层BiLSTM也能保住基本上下文信息。音频侧先计算MFCC特征常见配置是40维、窗长25ms、帧移10ms然后送进一个两层LSTM或小尺寸CNN。图像或视频帧用ResNet18去掉最后的全连接层输出512维特征图再做全局平均池化。下表是我在类似项目里常用的一组替换方案模态输入特征编码器输出维度说明文本token序列max_len128bert-base-chinese768也可换RoBERTa-wwm效果更稳但更吃显存音频MFCC40 x 帧数Conv1D BiLSTM256帧数需要统一到固定长度图像224 x 224 x 3ResNet18512对光照变化敏感需要做归一化选型时不要贪大。课程设计环境里如果只有CPUBERT前向会非常慢此时预设编码器输出维度统一到256再融合是一种更务实的做法。2.2 融合策略不是简单的concat注意力加权更符合情感表达常见融合有早期、中期、晚期。早期把特征拼接后一起过层参数少但面临对齐噪声晚期每个模态先独立出分类结果再投票实现简单却丢失模态间的相关性。我一般使用中期注意力融合也就是先得到各模态的向量再计算一个可学习的权重按加权和得到联合表示。计算上就是把三个向量分别过一层线性映射到相同隐层维度拼接后经过两层全连接产生三个权重softmax归一化再对原始向量加权求和。这里面可以不用太复杂的跨模态注意力文本和音频的语义相关性强于文本和图像但项目迭代时可以先用统一权重后续再观察注意力值变化决定要不要单独给文本模态更高优先级。2.3 特征对齐统一长度、统一采样率、统一隐层维度对齐是融合前最容易出错的地方。文本侧要固定max_len超过截断、不足padding出一个attention_mask。音频侧用librosa把音频重采样到16000Hz然后对MFCC帧轴做padding或直接插值到固定帧数。图像侧只需要把短边resize到256再中心裁剪到224。最后在模型里把三个向量通过Linear层映射到同样的hidden_dim融合层才不需要处理维度不对称。常见误用是直接用各自编码器输出不同维度强行concat高维模态会淹没低维信号另一个误用是没处理音频的静音段导致大量样本帧全是零向量训练时loss先降到很低再反弹。解决方式是计算帧级能量去掉能量低于阈值的帧或在预处理阶段直接过滤静音段。3. 在Jupyter Notebook里搭建Python多模态项目环境3.1 用conda创建独立环境避免jupyter notebook无法运行的版本冲突很多报错都出在“系统里本来就有Python再加装依赖导致内核混乱”这一步。我一般会先装Miniconda然后为项目创建一个独立环境conda create -n msa python3.9 -y conda activate msa python -m pip install --upgrade pip pip install jupyter notebook pip install torch torchaudio torchvision --index-url https://download.pytorch.org/whl/cu118 pip install transformers librosa soundfile opencv-python pandas scikit-learn matplotlib这里把torch系列装成CPU版就删掉--index-url参数反之要确认CUDA版本。安装完在base环境里执行conda install nb_conda_kernels重启jupyter之后就能在新内核里选择msa。如果发现jupyter notebook无法运行或内核列表为空优先检查pip list里有没有ipykernel没有就执行python -m ipykernel install --user --name msa。3.2 数据集目录约定一个样品一个路径标签用CSV统一管理多模态项目最大的坑不是模型是数据对不上。常见做法是每个样本单独编个IDID下放txt、wav、jpg三个文件标签集中放在train_label.csv中data/train/0001.txt data/train/0001.wav data/train/0001.jpg data/train/0002.txt data/train/0002.wav data/train/0002.jpg ... data/label.csvlabel.csv至少三个字段示例为id,text,emotion其中emotion使用数字0负面1中性2正面。这样后面用pandas合并时不需要猜路径。音频文件尽量统一为wav格式不要混用mp3和m4alibrosa在加载mp3时依赖soundfile部分环境会因编码问题读不出文件。3.3 用Python写一个多模态数据读取与预处理类核心是保证每条样本都能取出三个模态对齐后的张量。以下是实际可用的简化版放在dataset.py里import librosa import torch import pandas as pd from torch.utils.data import Dataset from PIL import Image from torchvision import transforms from transformers import BertTokenizer class MultiModalDataset(Dataset): def __init__(self, csv_path, data_dir): self.df pd.read_csv(csv_path) self.data_dir data_dir self.tokenizer BertTokenizer.from_pretrained(bert-base-chinese) self.img_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) def __len__(self): return len(self.df) def __getitem__(self, idx): row self.df.iloc[idx] sample_id row[id] text row[text] text_ids self.tokenizer(text, max_length128, paddingmax_length, truncationTrue, return_tensorspt) audio, sr librosa.load(f{self.data_dir}/{sample_id}.wav, sr16000) mfcc librosa.feature.mfcc(yaudio, srsr, n_mfcc40) mfcc torch.FloatTensor(mfcc).permute(1, 0) # [time, 40] if mfcc.size(0) 157: mfcc torch.nn.functional.pad(mfcc, (0, 0, 0, 157 - mfcc.size(0))) else: mfcc mfcc[:157, :] image Image.open(f{self.data_dir}/{sample_id}.jpg).convert(RGB) image self.img_tf(image) label row[emotion] return text_ids[input_ids].squeeze(0), text_ids[attention_mask].squeeze(0), \ mfcc, image, torch.tensor(label)代码逻辑说明tokenizer返回值是字典直接取input_ids和attention_mask两者都要参与训练。librosa.load强制sr16000把音频统一采样率避免不同采样率的wav被模型当作另一个特征。MFCC维度设置成(time, 40)与PyTorch默认的(seq, feature)一致后续网络不需要每次转置。固定帧数157对应2秒音频按默认hop_length计算出的MFCC帧数样本不足时右侧padding超过则截断。如果音频长度差异很大可以把157调成实际帧数分布的中位数不要在数据集里混用多个长度。图像侧用Resize到224再归一化Normalize的均值方差必须和预训练模型保持一致否则ResNet输出特征会发生漂移。4. Python实现多模态情感分析模型融合网络、训练与评估4.1 先写一个可扩展的融合模型我把三个编码器统一封装在MultiModalFusion里文本用BertModel音频用Conv1d加BiLSTM图像用ResNet18。融合部分采用前面提到的注意力加权import torch import torch.nn as nn from transformers import BertModel from torchvision.models import resnet18 class TextEncoder(nn.Module): def __init__(self): super().__init__() self.bert BertModel.from_pretrained(bert-base-chinese) def forward(self, input_ids, attention_mask): out self.bert(input_idsinput_ids, attention_maskattention_mask) return out.last_hidden_state[:, 0, :] # CLS向量 class AudioEncoder(nn.Module): def __init__(self, input_dim40, hidden128, num_layers2): super().__init__() self.conv nn.Conv1d(input_dim, 64, kernel_size3, padding1) self.lstm nn.LSTM(64, hidden, num_layersnum_layers, batch_firstTrue, bidirectionalTrue) self.fc nn.Linear(hidden * 2, 256) def forward(self, x): x x.permute(0, 2, 1) x torch.relu(self.conv(x)) x x.permute(0, 2, 1) out, _ self.lstm(x) return self.fc(out[:, -1, :]) class ImageEncoder(nn.Module): def __init__(self): super().__init__() self.backbone resnet18(pretrainedTrue) self.backbone.fc nn.Identity() self.fc nn.Linear(512, 256) def forward(self, x): return self.fc(self.backbone(x))然后是把三者融合的模型class MultiModalFusion(nn.Module): def __init__(self, num_classes3): super().__init__() self.text_enc TextEncoder() self.audio_enc AudioEncoder() self.image_enc ImageEncoder() self.attn nn.Sequential( nn.Linear(768 256 256, 384), nn.ReLU(), nn.Linear(384, 3), nn.Softmax(dim-1) ) self.classifier nn.Sequential( nn.Linear(768 256 256, 128), nn.ReLU(), nn.Dropout(0.3), nn.Linear(128, num_classes) ) def forward(self, text_ids, text_mask, audio, image): t self.text_enc(text_ids, text_mask) a self.audio_enc(audio) i self.image_enc(image) h torch.cat([t, a, i], dim-1) attn self.attn(h) fused attn[:, 0:1] * t attn[:, 1:2] * a attn[:, 2:3] * i return self.classifier(fused)参数说明AudioEncoder里的Conv1d输入通道是40对应MFCC维数LSTM的hidden设为128双向后输出256。TextEncoder直接取BERT的CLS向量句长被统一成128因此开销可控。MultiModalFusion的attn在拼接向量上预测三模态权重softmax后对三个特征加权再分类这比简单concat更能体现每种模态对当前样本的贡献。4.2 训练循环、损失函数与超参表的对应关系训练时文本侧BERT通常需要较小的学习率其他层可以稍大因此使用AdamW和分层学习率比较稳妥from transformers import AdamW from torch.utils.data import DataLoader from sklearn.metrics import f1_score, accuracy_score model MultiModalFusion(num_classes3) optimizer AdamW([ {params: model.text_enc.parameters(), lr: 2e-5}, {params: model.audio_enc.parameters(), lr: 1e-4}, {params: model.image_enc.parameters(), lr: 1e-4}, {params: model.attn.parameters(), lr: 1e-4}, {params: model.classifier.parameters(), lr: 1e-4}, ], weight_decay0.01) loss_fn nn.CrossEntropyLoss() for epoch in range(10): model.train() total_loss 0 for text_ids, text_mask, mfcc, images, labels in DataLoader( dataset, batch_size16, shuffleTrue): optimizer.zero_grad() out model(text_ids, text_mask, mfcc, images) loss loss_fn(out, labels) loss.backward() optimizer.step() total_loss loss.item() print(fepoch {epoch1}, loss{total_loss/len(dataset):.4f})这里有几个参数值得现场改batch_size16适合8GB显存显存不足优先降到8而不是调小BERT的max_length。epochs对中文BERT微调来说10到15轮足够看到收敛趋势如果loss震荡把weight_decay从0.01改成0.05。AdamW的weight_decay通常只作用在非bias参数上上面的写法会整体衰减但简单项目里影响不大可以先用。4.3 评估、混淆矩阵与模型保存训练完成后不能只看loss情感分类类别不平衡很常见我用macro F1和混淆矩阵判断哪个类别被模型忽略from sklearn.metrics import classification_report, confusion_matrix model.eval() all_preds, all_labels [], [] with torch.no_grad(): for text_ids, text_mask, mfcc, images, labels in DataLoader(val_ds, batch_size16): out model(text_ids, text_mask, mfcc, images) preds out.argmax(dim-1) all_preds.extend(preds.tolist()) all_labels.extend(labels.tolist()) print(classification_report(all_labels, all_preds, digits4)) print(confusion_matrix(all_labels, all_preds)) torch.save(model.state_dict(), checkpoints/msa_epoch10.pt)说明classification_report输出的macro avg比accuracy更值得写进项目文档混淆矩阵如果显示某类严重偏低说明训练集该模态样本少优先做数据增强而不是改网络。模型保存成state_dict是为了方便部署时只保留权重结构通过模型代码还原。5. 项目文档、源码结构与Jupyter产物让毕设和课设可交付5.1 源码目录把notebook、模型代码和文档分开如果全部堆在一个notebook里最后答辩时很难讲清楚哪个文件负责数据加载、哪个负责训练。我建议把工程化代码和探索代码分开my_project/ ├── README.md ├── requirements.txt ├── config.py ├── dataset.py ├── models.py ├── train.py ├── predict.py └── notebooks/ └── main.ipynbconfig.py集中放路径、超参和随机种子dataset.py和models.py是纯Python类方便命令行训练notebooks/main.ipynb只做演示和可视化不承载核心训练逻辑。这样别人拿到项目能直接跑python train.py而不是先打开每个cell。5.2 README、requirements.txt与docstring三样缺一不可README需要覆盖项目是做什么的、数据集放在哪里、环境如何安装、训练命令是什么、预期结果截图。requirements.txt要注意版本锁定pip freeze requirements.txt生成后手动把torch版本确认到可复现版本例如torch2.1.0。每个类和函数至少要有docstring不要写一大段注释函数内部关键行一两个注释就够了def load_config(): 读取config.py并返回参数字典避免在notebook里手动输入路径。 return config.__dict__docstring的价值在于生成API文档和IDE提示毕设评阅时老师会直接看代码跳转有docstring的代码整体观感会好很多。5.3 用jupyter nbconvert把Notebook导出为无需运行的演示稿答辩现场经常出现kernel忙、依赖丢失等尴尬所以我在提交前会把主notebook导出为两种格式jupyter nbconvert --to html notebooks/main.ipynb jupyter nbconvert --to script notebooks/main.ipynb --output demo_script.py--to html用于演示浏览器打开后不依赖Python环境--to script用于代码走查方便老师直接看逻辑。如果notebook里有耗时训练导出前先把所有cell的输出清理干净只保留关键结果图这样导出文件会小很多加载也快。Another pointnotebook里执行过的变量不会保留到其他文件所以代码评审时最好以train.py为准notebook只放核心中间结果和可视化。6. 端到端复现与验证固定随机种子、导出模型、分析错误样本6.1 固定随机种子让每次训练结果可复现答辩时最怕换一台机器结果大相径庭。我一般在config.py里固定seed并在训练开头调用import random, numpy as np, torch def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed)注意DataLoader的shuffleTrue在不同PyTorch版本下仍可能因为随机算法变化导致结果微小浮动所以最终要锁定整个环境版本号不只是种子。6.2 单样本推理函数验证融合是否真的起作用用state_dict恢复模型后写一个最简单的predict函数对同一条数据分别给文本、给音频、给图像看预测变化def predict_single(model, text, audio_path, image_path): model.eval() text_ids, text_mask, mfcc, image process_one_sample(text, audio_path, image_path) with torch.no_grad(): logits model(text_ids, text_mask, mfcc.unsqueeze(0), image.unsqueeze(0)) return logits.argmax(dim-1).item()如果单独给文本是负面、单独给音频是中性融合后变成正面说明注意力层在起作用如果三个单模态和融合结果全一样说明融合层没有学会加权需要回去检查attention权重是否在训练后被常数覆盖。6.3 用错误样本定位是标注问题还是模态缺失把预测错误样本按字段保存逐个看文本和对应音频波形。很多情况是标注本身有争议比如“这电影还挺逗的”配着生无可恋的语气标注为负面但模型融合后给正面这时优先修数据而不是改网络。让错误样本可视化成为项目文档的一项内容毕设答辩时能直接展示。这个predict_single函数可以直接留在predict.py里后续接API或批量跑测试集时都只需要复用这一个入口。本文还有配套的精品资源点击获取