多模态情感分析大作业实战:从Jupyter到可复现模型全流程
简介本资源为基于Jupyter与Python实现的多模态情感分析模型完整项目包面向计算机、人工智能、自动化等专业的学生与教师可用于期末课程设计、课程大作业或毕业设计也适合希望入门多模态学习的开发者参考。压缩包共约2000个文件以txt数据与说明文件为主另含1个py主程序与1个md说明文档整体约202.69MB目录结构清晰便于按模块查阅与复现。项目涵盖多模态特征融合、模型训练与预测等核心环节代码经过调试测试可稳定运行并附有报告文档与模型文件方便读者理解整体设计思路与实验流程。目前已有187人学习下载具备较高的学习借鉴价值基础较好的读者可在此基础上修改调整实现不同功能。1. 多模态情感分析大作业从 Jupyter 到可复现模型的完整路径期末大作业选多模态情感分析很多人第一反应是找一份现成源码改改交差结果跑起来才发现文本分支和图像分支对不齐、Jupyter 里路径全是绝对路径、换台机器就报 PermissionError。这个标题背后真正要解决的是用 Jupyter Python 把文本和图像两条模态的情感信号融合成一个可训练、可评估、可交付的模型并且附带报告文档和权重文件。适合正在做课程设计、毕设开题或想补一个多模态落地项目的同学。我下面按真实做一遍的顺序拆数据怎么组织、模型怎么搭、Jupyter 里怎么调、报告怎么写、哪些坑必踩。读完你能拿到一套能跑通的最小闭环而不是一堆散落的 notebook 单元格。2. 多模态情感分析的数据管线文本与图像怎么对齐2.1 为什么先定标签体系再碰模型多模态情感分析最常见的翻车不是模型结构而是标签对不上。文本分支用三分类积极/中性/消极图像分支用五分类非常积极到非常消极融合层直接维度爆炸。我一般会先锁死一个统一标签空间比如三分类然后让两个分支都输出 3 维 logits。这样后面融合时只做加权或拼接不用再映射。数据来源通常是两个独立数据集文本用中文情感语料图像用带情感标注的图片集。关键操作是构造一个联合索引表每条样本包含text_id、image_id、label。如果两个数据集没有天然对齐关系就按标签分布做配对采样保证每个 batch 里三类的比例接近 1:1:1。这一步不做训练后期会明显偏向多数类。import pandas as pd import numpy as np # 假设 text_df 有 text, labelimage_df 有 image_path, label text_df pd.read_csv(text_sentiment.csv) image_df pd.read_csv(image_sentiment.csv) # 统一标签为 0/1/2 label_map {消极: 0, 中性: 1, 积极: 2} text_df[label] text_df[label].map(label_map) image_df[label] image_df[label].map(label_map) # 按标签分组后配对保证每类数量一致 paired [] for lbl in [0, 1, 2]: t_sub text_df[text_df[label] lbl].reset_index(dropTrue) i_sub image_df[image_df[label] lbl].reset_index(dropTrue) n min(len(t_sub), len(i_sub)) for i in range(n): paired.append({ text: t_sub.loc[i, text], image_path: i_sub.loc[i, image_path], label: lbl }) paired_df pd.DataFrame(paired).sample(frac1, random_state42).reset_index(dropTrue) paired_df.to_csv(paired_multimodal.csv, indexFalse)逻辑说明先做标签映射再按类配对最后打乱。参数上random_state42保证每次划分一致min(len(t_sub), len(i_sub))防止某一类图像不够导致索引越界。如果图像数据远少于文本可以重复采样图像但要在报告里注明否则评估结果会偏乐观。2.2 文本分支Longformer 中文模型怎么接热搜里出现 longformer中文模型是因为长文本情感分析确实需要处理超过 512 token 的评论。但大作业场景下大部分中文情感语料单条不超过 200 字用 BERT 或 RoBERTa 中文版就够。如果你非要用 Longformer注意它的 attention 窗口参数attention_window默认是 512显存占用比 BERT 大 30% 左右。Jupyter 里跑建议 batch size 降到 8。from transformers import AutoTokenizer, AutoModel import torch import torch.nn as nn class TextEncoder(nn.Module): def __init__(self, model_namehfl/chinese-roberta-wwm-ext, hidden_dim256): super().__init__() self.tokenizer AutoTokenizer.from_pretrained(model_name) self.bert AutoModel.from_pretrained(model_name) self.proj nn.Linear(self.bert.config.hidden_size, hidden_dim) def forward(self, texts): enc self.tokenizer(texts, paddingTrue, truncationTrue, max_length128, return_tensorspt) # 注意Jupyter 里要把 enc 移到和模型同一设备 device next(self.bert.parameters()).device enc {k: v.to(device) for k, v in enc.items()} out self.bert(**enc).last_hidden_state[:, 0, :] # CLS return self.proj(out)逻辑说明max_length128是权衡速度和精度的常用值长文本可以调到 256 但显存翻倍。[:, 0, :]取 CLS 向量作为句子表示。参数hidden_dim256是融合层输入维度太小会丢信息太大容易过拟合。Jupyter 里如果报RuntimeError: Expected all tensors to be on the same device就是这里没把enc移到 GPU。2.3 图像分支轻量 CNN 还是 ViT大作业不建议上 ViT除非你有 16G 以上显存。常见做法是用 ResNet18 或 EfficientNet-B0 做特征提取输出 512 或 1280 维再投影到和文本一样的 256 维。图像预处理统一 resize 到 224x224归一化用 ImageNet 均值方差。from torchvision import models, transforms from PIL import Image class ImageEncoder(nn.Module): def __init__(self, hidden_dim256): super().__init__() self.backbone models.resnet18(pretrainedTrue) self.backbone.fc nn.Identity() # 去掉原分类头 self.proj nn.Linear(512, hidden_dim) self.transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) def forward(self, image_paths): imgs torch.stack([self.transform(Image.open(p).convert(RGB)) for p in image_paths]) device next(self.backbone.parameters()).device imgs imgs.to(device) feat self.backbone(imgs) return self.proj(feat)逻辑说明pretrainedTrue会下载权重Jupyter 里第一次跑会卡在下载建议提前把~/.cache/torch目录配好。fc nn.Identity()去掉 1000 类分类头保留 512 维特征。convert(RGB)防止灰度图或 PNG 带 alpha 通道导致通道数不匹配。3. 融合模型搭建与 Jupyter 训练循环3.1 拼接、加权、门控三种融合方式怎么选融合层是多模态情感分析的核心。最简单是拼接torch.cat([text_feat, image_feat], dim-1)然后接全连接。进阶一点用加权求和权重可学习。再复杂是门控融合用 sigmoid 控制两条模态的贡献。大作业建议从拼接开始跑通后再换门控报告里可以对比三种方式的准确率。class FusionModel(nn.Module): def __init__(self, hidden_dim256, num_classes3, fusionconcat): super().__init__() self.text_enc TextEncoder(hidden_dim) self.image_enc ImageEncoder(hidden_dim) self.fusion fusion if fusion concat: self.classifier nn.Sequential( nn.Linear(hidden_dim * 2, 128), nn.ReLU(), nn.Dropout(0.3), nn.Linear(128, num_classes) ) elif fusion gate: self.gate nn.Sequential( nn.Linear(hidden_dim * 2, 2), nn.Softmax(dim-1) ) self.classifier nn.Linear(hidden_dim, num_classes) def forward(self, texts, image_paths): t_feat self.text_enc(texts) i_feat self.image_enc(image_paths) if self.fusion concat: fused torch.cat([t_feat, i_feat], dim-1) return self.classifier(fused) else: weights self.gate(torch.cat([t_feat, i_feat], dim-1)) fused weights[:, 0:1] * t_feat weights[:, 1:2] * i_feat return self.classifier(fused)逻辑说明Dropout(0.3)是防止过拟合的常用值数据量小于 5000 条时可以调到 0.5。门控融合里Softmax(dim-1)保证两个权重和为 1。注意weights[:, 0:1]而不是weights[:, 0]保持维度一致才能广播。3.2 Jupyter 里的训练循环与显存管理Jupyter notebook 跑训练最大的问题是显存不释放。每次重新运行单元格旧的模型还挂在 GPU 上。我一般会在训练前加torch.cuda.empty_cache()并且把模型定义和训练放在不同单元格方便单独重跑。import torch.optim as optim from torch.utils.data import DataLoader, Dataset class MultiModalDataset(Dataset): def __init__(self, df): self.df df.reset_index(dropTrue) def __len__(self): return len(self.df) def __getitem__(self, idx): row self.df.loc[idx] return row[text], row[image_path], row[label] def train(model, loader, epochs5, lr2e-5): device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) optimizer optim.AdamW(model.parameters(), lrlr) criterion nn.CrossEntropyLoss() for epoch in range(epochs): model.train() total_loss 0 for texts, paths, labels in loader: labels labels.to(device) optimizer.zero_grad() logits model(list(texts), list(paths)) loss criterion(logits, labels) loss.backward() optimizer.step() total_loss loss.item() print(fEpoch {epoch1}, Loss: {total_loss/len(loader):.4f}) torch.cuda.empty_cache() # 每个 epoch 后清缓存 return model逻辑说明lr2e-5是 BERT 类模型微调的经典学习率太大容易震荡。AdamW比 Adam 多了权重衰减适合 Transformer。torch.cuda.empty_cache()放在 epoch 末尾不是必须但能缓解 Jupyter 长时间运行后的显存碎片。如果报CUDA out of memory先把 batch size 减半再考虑冻结文本分支底层参数。3.3 评估指标与报告文档里的必放内容大作业报告文档不能只写准确率。多模态情感分析至少放四样混淆矩阵、F1 分数、两种模态单独预测的对比、融合后的提升幅度。Jupyter 里用sklearn.metrics直接出。from sklearn.metrics import classification_report, confusion_matrix import seaborn as sns import matplotlib.pyplot as plt def evaluate(model, loader): model.eval() all_preds, all_labels [], [] with torch.no_grad(): for texts, paths, labels in loader: logits model(list(texts), list(paths)) preds torch.argmax(logits, dim-1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) print(classification_report(all_labels, all_preds, target_names[消极, 中性, 积极])) cm confusion_matrix(all_labels, all_preds) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabels[消极, 中性, 积极], yticklabels[消极, 中性, 积极]) plt.xlabel(预测) plt.ylabel(真实) plt.show()逻辑说明torch.no_grad()关闭梯度节省显存。classification_report输出每类 precision/recall/F1报告里直接截图。混淆矩阵用 seaborn 画比 matplotlib 原生好看。注意 Jupyter 里中文显示需要设置plt.rcParams[font.sans-serif] [SimHei]否则标签是方块。4. 避坑与排查Jupyter 多模态训练最常见的 5 个翻车现场4.1 PermissionError: [Errno 13] Permission denied现象Jupyter 启动时或保存 notebook 时报这个错。原因通常是 notebook 默认保存路径指向了系统盘受保护目录或者当前用户对工作目录没有写权限。解决先查jupyter notebook --generate-config生成的配置文件找到c.NotebookApp.notebook_dir改成你有写权限的路径比如D:/multimodal_project。Windows 下还要注意路径不要带中文和空格。Linux 下用chmod -R 755给工作目录授权。4.2 图像路径在 Jupyter 里能读换脚本就 FileNotFoundError现象notebook 里Image.open(data/img/1.jpg)正常导出成.py跑就找不到文件。原因是 Jupyter 的工作目录是 notebook 所在目录而脚本的工作目录是你执行命令的目录。解决统一用绝对路径或者在代码开头加os.chdir(os.path.dirname(os.path.abspath(__file__)))。更稳的做法是把数据根目录写成配置变量所有路径基于它拼接。4.3 文本和图像 batch 对不齐导致标签错位现象训练 loss 正常下降但评估准确率一直在 33% 左右等于随机猜。原因多半是 DataLoader 的shuffleTrue同时作用在文本和图像上但两个分支用了不同的 sampler。解决用一个 Dataset 同时返回文本和图像路径如 3.2 节的MultiModalDataset不要分开两个 DataLoader。如果必须分开设置相同的random_seed和shuffleFalse手动打乱索引。4.4 显存溢出但 batch size 已经降到 1现象CUDA out of memory即使 batch size1。原因可能是文本分支 max_length 设太大或者图像分支用了 448x448 输入。解决文本max_length降到 64图像 resize 到 160x160。另外检查是不是在 Jupyter 里反复运行了定义模型的单元格旧模型没释放。加del model; torch.cuda.empty_cache()再重新定义。4.5 报告文档里准确率很高但复现不出来现象报告写准确率 92%答辩时老师让你当场跑结果只有 70%。原因通常是评估时用了训练集或者随机种子没固定。解决训练前固定torch.manual_seed(42)、np.random.seed(42)、random.seed(42)。划分数据集用train_test_split的random_state参数。报告里注明硬件环境和库版本比如torch2.0.1、transformers4.30.0。5. 从大作业到可交付模型保存、加载与报告文档的收尾技巧模型保存别只存state_dict要把文本 tokenizer 的配置和图像预处理参数一起打包。我一般会存一个checkpoint.pt里面包含model_state、optimizer_state、label_map、max_length、image_size。这样换台机器加载时不用翻代码找参数。def save_checkpoint(model, optimizer, pathcheckpoint.pt): torch.save({ model_state: model.state_dict(), optimizer_state: optimizer.state_dict(), label_map: {消极: 0, 中性: 1, 积极: 2}, max_length: 128, image_size: 224, fusion: model.fusion }, path) def load_checkpoint(pathcheckpoint.pt): ckpt torch.load(path, map_locationcpu) model FusionModel(hidden_dim256, num_classes3, fusionckpt[fusion]) model.load_state_dict(ckpt[model_state]) return model, ckpt逻辑说明map_locationcpu保证在没有 GPU 的机器上也能加载。fusion参数从 checkpoint 里读避免加载时结构对不上。报告文档里附上这个 checkpoint 的加载示例老师一看就知道你不是只交了 notebook。报告文档的结构建议按任务定义、数据来源与预处理、模型结构图、训练参数表、评估结果、错误分析、复现步骤。错误分析放 3 到 5 条 bad case比如“反讽文本被预测为积极”“低光照图像情感极性判断错误”。这比堆准确率更有说服力。最后说一个我自己的习惯每次跑完实验在 notebook 最后一个单元格写一段%who和%time记录当前变量和运行耗时。下次打开 notebook 不用猜上次跑到哪。多模态大作业的坑大多不在模型本身而在数据对齐和环境配置。把这两块做扎实源码和报告就是水到渠成的事。希望帮到你。本文还有配套的精品资源点击获取