☰
多模态情感分析大作业实战:从Jupyter到可复现模型全流程
2026/10/8 21:10:25 网站建设 项目流程

简介:本资源为基于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, label;image_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(drop=True) i_sub = image_df[image_df["label"] == lbl].reset_index(drop=True) 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(frac=1, random_state=42).reset_index(drop=True) paired_df.to_csv("paired_multimodal.csv", index=False)

逻辑说明:先做标签映射,再按类配对,最后打乱。参数上random_state=42保证每次划分一致,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_name="hfl/chinese-roberta-wwm-ext", hidden_dim=256): 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, padding=True, truncation=True, max_length=128, return_tensors="pt") # 注意: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_length=128是权衡速度和精度的常用值,长文本可以调到 256 但显存翻倍。[:, 0, :]取 CLS 向量作为句子表示。参数hidden_dim=256是融合层输入维度,太小会丢信息,太大容易过拟合。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_dim=256): super().__init__() self.backbone = models.resnet18(pretrained=True) 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)

逻辑说明:pretrained=True会下载权重,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_dim=256, num_classes=3, fusion="concat"): 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(drop=True) 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, epochs=5, lr=2e-5): device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model.to(device) optimizer = optim.AdamW(model.parameters(), lr=lr) 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(f"Epoch {epoch+1}, Loss: {total_loss/len(loader):.4f}") torch.cuda.empty_cache() # 每个 epoch 后清缓存 return model

逻辑说明:lr=2e-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, annot=True, fmt="d", cmap="Blues", 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 的shuffle=True同时作用在文本和图像上,但两个分支用了不同的 sampler。解决:用一个 Dataset 同时返回文本和图像路径,如 3.2 节的MultiModalDataset,不要分开两个 DataLoader。如果必须分开,设置相同的random_seed和shuffle=False,手动打乱索引。

4.4 显存溢出但 batch size 已经降到 1

现象:CUDA out of memory即使 batch size=1。原因可能是文本分支 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参数。报告里注明硬件环境和库版本,比如torch==2.0.1、transformers==4.30.0。

5. 从大作业到可交付:模型保存、加载与报告文档的收尾技巧

模型保存别只存state_dict,要把文本 tokenizer 的配置和图像预处理参数一起打包。我一般会存一个checkpoint.pt,里面包含model_state、optimizer_state、label_map、max_length、image_size。这样换台机器加载时不用翻代码找参数。

def save_checkpoint(model, optimizer, path="checkpoint.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(path="checkpoint.pt"): ckpt = torch.load(path, map_location="cpu") model = FusionModel(hidden_dim=256, num_classes=3, fusion=ckpt["fusion"]) model.load_state_dict(ckpt["model_state"]) return model, ckpt

逻辑说明:map_location="cpu"保证在没有 GPU 的机器上也能加载。fusion参数从 checkpoint 里读,避免加载时结构对不上。报告文档里附上这个 checkpoint 的加载示例,老师一看就知道你不是只交了 notebook。

报告文档的结构建议按:任务定义、数据来源与预处理、模型结构图、训练参数表、评估结果、错误分析、复现步骤。错误分析放 3 到 5 条 bad case,比如“反讽文本被预测为积极”“低光照图像情感极性判断错误”。这比堆准确率更有说服力。

最后说一个我自己的习惯:每次跑完实验,在 notebook 最后一个单元格写一段%who和%time,记录当前变量和运行耗时。下次打开 notebook 不用猜上次跑到哪。多模态大作业的坑大多不在模型本身,而在数据对齐和环境配置。把这两块做扎实,源码和报告就是水到渠成的事。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询