☰
BERT+ResNet多模态情感分析实战:从数据预处理到融合方法
2026/10/7 2:57:22 网站建设 项目流程

简介:面向人工智能课程设计与期末大作业,基于BERT+ResNet的多模态情感分析项目提供了完整源码与文档说明,涵盖两种朴素融合与三种注意力融合方法,并配有网络结构示意图。项目依托Hugging Face与torchvision构建,在Models文件夹中集中实现多种融合策略,同时通过Config.py、Trainer.py及数据预处理模块将配置、训练、数据处理串联起来,适合需要完成高分课设或系统学习图文情感分析的计算机专业学生参考。压缩包共39个文件,以Python源码为主(17个py),另含12个pyc编译文件、3个txt说明、3个png结构图、2个json数据集及1个Markdown文档,整包仅446KB,轻量便携、目录清晰。已有1170人学习下载,对于希望快速上手多模态情感分析并对比不同融合效果的开发者,是一份可直接运行、便于扩展的实用资源。

1. 为什么做多模态情感分析,而不是单模态跑跑就完事

纯文本情感分析有个很明显的盲区:一句“你可真厉害”放在没有表情包的客服对话里是夸奖,配上翻白眼自拍就变成讽刺。图像情感分析反过来,只看图片猜情绪,碰到“手里拿着刀笑”这种画面,很难判断是搞怪还是危险。多模态情感分析要解决的就是这个问题——把文本语义和视觉表情同时交给模型,让判断依据从单通道变成双通道。基于BERT+ResNet做这个任务,是目前大作业和课程设计里性价比最高的方案:BERT吃文本,ResNet吃图像,后端的融合方法有十几种可调,改一处模块就能出一组对比实验,写报告、画图表、答辩都有东西讲。适合想拿高分的 AI 方向学生,也适合刚上手多模态应用的研发。下面按我实际做过的方案,把从数据处理到融合实现、再到排坑的完整路径给你过一遍。

2. 骨架选型:BERT管文本、ResNet管图像,以及融合该从哪里切入

2.1 BERT做文本编码:预训练模型只看特征,别想着从头训

BERT能成为文本骨架,是因为它用 Transformer 的双向编码能力把上下文关系吃透了。拿情感分析来说是直接拿预训练权重做迁移学习,比从零搭建一层 LSTM 节省大量时间。做法一般是:加载预训练模型,把文本通过 tokenizer 转成 input_ids 和 attention_mask,前向传播得到 last_hidden_state,再取序列第一个 token(也就是 CLS)对应的向量作为整句表示,或者对最后一层做平均池化。

具体代码里,我会先用 Hugging Face 的 transformers 库加载模型。模型名用 bert-base-uncased 处理英文,处理中文用 bert-base-chinese,别混。输入文本先做归一化——去首尾空白,把部分 URL 替换成特殊 token,表情符号保留——再交给 tokenizer。实际操作中,很多课设数据是抓来的微博或 Twitter 文本,里面全是 @ 用户、链接、emoji,不做清理,tokenizer 会把一句话劈成几十个碎块,attention_mask越长推理越慢,情感语义还被稀释。

一个关键参数是max_length。文本长度超过max_length会被截断,短于它会填充。常见经验值是 64 到 128,情感分析不像阅读理解需要长距离记忆,句子一般不超过 50 token,取 64 够用。截断方式用truncation=True,填充用padding="max_length"保证 batch 内长度一致。注意别为省显存把max_length压到 32 以下,句子被腰斩后,转折词和否定词丢失,模型会突然把“一点都不喜欢”判成正面。以下是文本编码的基础片段:

from transformers import BertTokenizer, BertModel tokenizer = BertTokenizer.from_pretrained("bert-base-uncased") bert = BertModel.from_pretrained("bert-base-uncased") def encode_text(text: str, max_len: int = 64) -> dict: # 先做归一化,防止 @用户 和 URL 干扰分词 text = text.strip().replace("http\S+", "[URL]").replace("@\w+", "[USER]") encoded = tokenizer( text, max_length=max_len, padding="max_length", truncation=True, return_tensors="pt" ) return { "input_ids": encoded["input_ids"], # shape: [1, max_len] "attention_mask": encoded["attention_mask"] # 1表示真实token,0表示填充 }

这段代码里return_tensors="pt"让结果直接是 PyTorch 张量,省一次转换。attention_mask一定要传入 BERT 前向方法,否则填充位置还会参与注意力计算,等于给模型注入噪声。我在第一次跑的时候忘了传 mask,训练 loss 死活降不下去,后来把 mask 加上,一轮就明显收敛。BERT 输出的last_hidden_state的 shape 是[batch, seq_len, hidden_dim],BERT-base 的hidden_dim是 768。你要拼向量、做注意力,这个维度要记牢。

2.2 ResNet做图像编码:预训练权重够用,最后一层直接砍掉

ResNet 的优势在残差结构,能把网络堆深一点而不至于梯度消失。这个任务里不需要从零训练,直接用 torchvision 的预训练权重。我一般用 ResNet50,输出维度 2048,比 ResNet18 的 512 维更能撑住多模态融合;如果你数据量小、显卡又紧张,先上 ResNet18,后面在意指标再换 50。

这里有个容易搞错的地方:torchvision 里的resnet50(pretrained=True)会自带最后的fc分类层(比如 ImageNet 的 1000 类),我们要拿的是图像特征而不是类别概率,所以把fc层替换成恒等映射,或用model.avgpool之后的输出:

import torch.nn as nn from torchvision import models def build_resnet(version: str = "resnet50", freeze_layers: bool = True): if version == "resnet50": model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2) out_dim = 2048 else: model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) out_dim = 512 # 删掉最后的分类层,保留全局平均池化的输出 model.fc = nn.Identity() # 冻结前90%层,只让最后一两个block参与微调 if freeze_layers: for param in model.parameters(): param.requires_grad = False for param in model.layer4.parameters(): param.requires_grad = True return model, out_dim

一般来说,冻结前几层的原因很简单:浅层学的是边缘、颜色、纹理这种通用视觉特征,用大数据集训好的权重在这些层上已经足够好,微调反而会过拟合。最后把layer4解冻,让网络针对图片里的脸部表情或物体上下文做少量适配。如果你数据量很大(几千张以上)且算力充裕,可以不冻结完全微调,代价是训练时间成倍增长。ResNet 输入前,还要经过归一化,这是后一章的内容。

2.3 融合为什么难:两个特征空间差了十万八千里

BERT 输出的文本特征在语言空间,ResNet 输出的图像特征在视觉空间。BERT-base 的特征向量是 768 维,ResNet50 是 2048 维,两者不仅尺度不同,数据分布也不同。直接把两个向量torch.cat在一起,全连接层会发现文本分支的梯度贡献远小于图像分支,模型被图像特征带着跑。所以融合不是“拼一下就行”,而是要想办法让两个模态在进入分类器前先互相“听懂”。这也是为什么标题里写的“多种融合方法”值得做——不同融合结构在同一个数据集上的指标差异,恰恰是答辩时的亮点。

融合方法大体能按信息交互的时机分三类:早融合(特征提取后立即拼接,交互少),晚融合(先独立得到两个分类得分,最后加权),中间融合(在特征提取过程中就互相穿插,比如注意力)。对课设来说,把三种类型都实现一遍,对比表能填,图也能画。下一章先把数据处理好,再逐个实现融合模块。

3. 预处理与Dataset:让一张图和一句文字对齐成一个样本

3.1 数据表格与目录怎么组织

做多模态情感分析,最怕文件命名混乱。我见过的翻车例子:文本 CSV 里写的是img1.jpg,图片目录里却有IMG_1.JPG、img1(1).jpg,大小写和空格导致读取失败。先把数据目录固定下来,所有样本统一叫id.jpg,文本用 id 关联。目录结构长这样:

dataset/ texts.csv # id,text,label images/ # id.jpg

texts.csv是主表,label取 0、1、2 三分类(负面、中性、正面)或 0、1 二分类。读取时严格用str(id)做 key,避免 int 与字符串错位。写 Dataset 时,把两个模态封装成一个元组:

import os import pandas as pd from PIL import Image from torch.utils.data import Dataset class MultiModalDataset(Dataset): def __init__(self, csv_path, img_dir, text_encoder, img_transform, max_len=64): self.df = pd.read_csv(csv_path) self.img_dir = img_dir self.text_encoder = text_encoder self.img_transform = img_transform self.max_len = max_len def __len__(self): return len(self.df) def __getitem__(self, idx): row = self.df.iloc[idx] image = Image.open(os.path.join(self.img_dir, f"{row['id']}.jpg")).convert("RGB") image = self.img_transform(image) text = self.text_encoder(str(row["text"]), max_len=self.max_len) label = int(row["label"]) return { "image": image, "input_ids": text["input_ids"].squeeze(0), "attention_mask": text["attention_mask"].squeeze(0), "label": torch.tensor(label, dtype=torch.long) }

这里有个细节:PIL 打开图片后统一.convert("RGB"),因为灰度图或 RGBA 图会导致后面Normalize报通道数不匹配。text_encoder返回的是带 batch 维的张量,Dataset 要squeeze(0)把 batch 维去掉,否则后面DataLoader会多套一层 batch,变成[batch, 1, seq_len],模型直接炸形状。这个坑我debug了半小时。

3.2 文本编码的四个必调参数

上一章里encode_text已经出现过,实战中这四个参数每个都别省:max_length、padding、truncation、return_tensors。其中padding="max_length"比padding=True更可控,因为 true 模式是等一个 batch 里最长文本,但 Dataset 是逐条返回,DataLoader 的 collate 如果不做 padding 就会报错。所以统一在 tokenizer 阶段填充到max_length,collate_fn只要简单torch.stack即可。

有的文本很短,只有一两个字,比如“哈哈”。这时 attention_mask 大部分是 0,BERT 会输出一个几乎全是 padding position 的表示,模型判断会退化。常见做法是把这类样本合并扩充,比如“哈哈哈哈哈”,或者直接用真实长度拼接原始文本。课设里不需要太多花活,但max_length=64时,如果一个 batch 里大部分文本实际长度只有 5~10,模型的语义容量是被浪费的。你可以统计text_len分布,把 64 改成 48 或 32,F1 可能会小幅上涨。

3.3 图像预处理:别直接复制ImageNet那套增广

图像的预处理一般包含 Resize、CenterCrop、ToTensor、Normalize。这里最容易踩坑的是:把训练用的随机裁剪、随机翻转直接搬到验证阶段。文本情感和图像情感有强对齐,比如一张摔倒的照片,水平翻转后可能被看作假摔;随机裁剪如果把表情区域裁掉,模型只能看到身体,情感判断直接改变。所以我在验证集和测试集上只用Resize(256)+CenterCrop(224),训练集才加 RandomResizedCrop 和较小的 RandomHorizontalFlip。

归一化的 mean/std 必须和预训练权重的训练数据一致,torchvision 的模型都用 ImageNet 的统计量:

from torchvision import transforms train_transform = transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale=(0.8, 1.0)), transforms.RandomHorizontalFlip(p=0.3), transforms.ToTensor(), transforms.Normalize( mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] ) ]) val_transform = transforms.Compose([ transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize( mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] ) ])

RandomResizedCrop加scale=(0.8, 1.0)是限制裁剪范围,别用默认(0.08, 1.0),那会把一张脸裁成一只耳朵。RandomHorizontalFlip对无方向性的图没问题,但包含文字或左右朝向明显的图要慎用。还有,如果你在 Python 环境里安装 torchvision 时卡在 numpy 版本冲突,先卸载 numpy 再用官方 requirements 重装,这种环境问题通常是 Python 3.8 配新版 torchvision 导致的,血泪经验。

4. 融合方法从简单到复杂:拼接、门控、跨模态注意力和完整模型代码

4.1 基线拼接:先把维度对齐,再谈融合效果

最简单的融合是特征级拼接:文本特征text_featshape[batch, 768],图像特征image_featshape[batch, 2048],直接在最后一维拼成[batch, 2816]。拼接后接一个全连接层降维到分类数。实战里发现直接接全连接层收敛慢,最好先各过一个 BN 层,再接拼接。

class ConcatFusion(nn.Module): def __init__(self, text_dim=768, image_dim=2048, num_classes=3): super().__init__() self.text_bn = nn.BatchNorm1d(text_dim) self.image_bn = nn.BatchNorm1d(image_dim) self.classifier = nn.Sequential( nn.Linear(text_dim + image_dim, 512), nn.ReLU(), nn.Dropout(0.3), nn.Linear(512, num_classes) ) def forward(self, text_feat, image_feat): text_feat = self.text_bn(text_feat) image_feat = self.image_bn(image_feat) fused = torch.cat([text_feat, image_feat], dim=-1) # [batch, 2816] return self.classifier(fused)

这里BatchNorm1d的输入必须是二维,如果你的 text_feat 是[batch, seq_len, hidden],那要先压缩成[batch, hidden]。前一章取 CLS 向量还是平均池化这一步就起作用了。我建议先取平均池化,因为平均池化能保留整句语义,CLS 向量更偏分类语义,在多模态场景里平均池化不容易丢失转折信息。这个模块常作为“基线”出现在消融表里,其他融合方法都要跟它比。

4.2 门控融合:让模型自己决定信文本还是信图像

门控融合的思路是学习出一个权重向量,控制两个模态各自保留多少信息,再相加。用 sigmoid 把权重压到 (0, 1) 区间,形成 soft gating。数学上类似:

g = sigmoid(W_g * [text; image] + b_g) fused = g * text + (1 - g) * image

这里的g由两个模态共同决定,实现了模态间的最浅层交互。实现时要注意门控的输入不要用归一化前的原始特征,先过 BN 再算门,否则梯度在 sigmoid 附近容易饱和。

class GateFusion(nn.Module): def __init__(self, text_dim=768, image_dim=2048, num_classes=3): super().__init__() self.project = nn.Sequential( nn.Linear(text_dim + image_dim, 512), nn.ReLU() ) self.gate = nn.Sequential( nn.Linear(512, 1), nn.Sigmoid() ) self.classifier = nn.Sequential( nn.Linear(text_dim, num_classes) # 门控后维度对齐到text_dim ) def forward(self, text_feat, image_feat): both = torch.cat([text_feat, image_feat], dim=-1) proj = self.project(both) g = self.gate(proj) # [batch, 1] # 门控的结果是插值,要求维度一致,所以先把image投影到text_dim image_proj = nn.Linear(image_feat.shape[1], text_feat.shape[1]).to(image_feat.device)(image_feat) fused = g * text_feat + (1 - g) * image_proj return self.classifier(fused)

上面代码里nn.Linear放在 forward 里不是好习惯,每步都重建参数,实践时应该把 image_proj 定义在__init__。这里为了展示维度匹配逻辑才临时投影。门控融合的优点是参数少、不容易过拟合,尤其适合几百张图的小数据集。缺点是门控机制太过简单,无法建模文本中某个词对应图像中某个部位这种细粒度关系。比它更强的,是注意力融合。

4.3 跨模态注意力:让文本看到图像里的重点区域

跨模态注意力本质是把文本特征作为查询(Query),图像特征作为键值(Key/Value),通过注意力矩阵动态分配图像各区域的权重。比如一句话说“这只猫很可爱”,模型可以把注意力放在猫的脸上而不是背景的书桌上。实现前不要求你对 Transformer 掌握很深,用torch.nn.MultiheadAttention就能搭:

import torch class CrossAttentionFusion(nn.Module): def __init__(self, text_dim=768, image_dim=2048, num_heads=8, num_classes=3): super().__init__() # 需要先把图像维度投影到和文本一致,才能做注意力 self.image_proj = nn.Linear(image_dim, text_dim) self.cross_attn = nn.MultiheadAttention( embed_dim=text_dim, num_heads=num_heads, batch_first=True ) self.layer_norm = nn.LayerNorm(text_dim) self.classifier = nn.Sequential( nn.Linear(text_dim, 256), nn.ReLU(), nn.Dropout(0.2), nn.Linear(256, num_classes) ) def forward(self, text_feat, image_feat): # text_feat: [batch, text_dim] -> 扩展成 [batch, 1, text_dim] text_feat = text_feat.unsqueeze(1) image_feat = self.image_proj(image_feat).unsqueeze(1) # [batch, 1, text_dim] attn_out, attn_weight = self.cross_attn( query=text_feat, key=image_feat, value=image_feat ) # 残差连接 + 层归一化,稳定训练 fused = self.layer_norm(attn_out.squeeze(1) + text_feat.squeeze(1)) return self.classifier(fused), attn_weight

attn_weight就是模型对图像不同位置的注意力分布,可以拿来做可视化。这里图像只有一个全局向量,注意力退化成几乎无意义,效果不如直接把两个特征做向量内积。更有效的做法是把图像特征映射成多个区域,比如 ResNet 倒数第二层特征图是[batch, 2048, 7, 7],可以先池化成[batch, 2048, 7]或[batch, 2048, 49],作为 49 个 key。这样注意力就能知道“图像左上角那块区域更重要”。

不过对课设来说,用单向量版本足够跑通并解释原理。如果你想在答辩里多聊一句“细粒度”,可以看我后面的进阶技巧,把特征图展开成序列。这里给出的是可复现的简单实现。

4.4 把三种融合集成在一个可运行模型里,方便做对比

与其写三个互相独立的 train.py,不如把常见结构封装成一个类,通过fusion_type切换。这样做实验时,你只需改参数,不用改训练循环,也能避免因为代码不一致导致的误差(比如漏了 Dropout、激活函数不对等)。下面是一个整合版本的模型骨架:

import torch.nn as nn class MultimodalModel(nn.Module): def __init__(self, text_dim=768, image_dim=2048, num_classes=3, fusion="concat"): super().__init__() assert fusion in {"concat", "gate", "attention"} self.fusion = fusion self.image_proj = nn.Identity() # 根据fusion类型调整 if fusion == "concat": self.fusion_layer = ConcatFusion(text_dim, image_dim, num_classes) elif fusion == "gate": self.fusion_layer = GateFusion(text_dim, image_dim, num_classes) elif fusion == "attention": self.fusion_layer = CrossAttentionFusion(text_dim, image_dim, num_classes) def forward(self, text_feat, image_feat): if self.fusion == "attention": logits, attn = self.fusion_layer(text_feat, image_feat) return logits, attn return self.fusion_layer(text_feat, image_feat)

训练代码只需要在初始化时传fusion="gate",就能依次跑三组实验。注意每个融合模块内部都已经包含了分类器,所以整体模型里不需要再加一个最后的fc。这也是导致我最初模型参数膨胀、损失不降的一个原因——融合模块里有一个全连接,外面又套一个多层全连接,优化空间变得非常难搜。

5. 训练调参与避坑:损失、学习率、冻结策略和我翻过的四次车

5.1 基础训练配置:双区学习率比统一学习率更稳

BERT 和 ResNet 的预训练程度不同,学习率也应该分开设。BERT 迁移学习中一般给 1e-5 到 3e-5,ResNet 微调层给 1e-4 到 3e-4。如果统一用 3e-4,BERT 很容易被冲得忘掉预训练语义,loss 一开始下降,几个 epoch 后冲高。我用 AdamW 优化器,weight_decay=1e-2,分别设置两组参数:

from transformers import AdamW def build_optimizer(model, bert_model, lr_bert=1e-5, lr_rest=1e-4): bert_params = [] rest_params = [] for name, param in model.named_parameters(): if not param.requires_grad: continue if "bert" in name: bert_params.append(param) else: rest_params.append(param) return AdamW([ {"params": bert_params, "lr": lr_bert}, {"params": rest_params, "lr": lr_rest} ], weight_decay=1e-2)

损失函数用交叉熵时,如果数据集类别不均衡,一定要在CrossEntropyLoss里加weight。我拿到的课设数据里“负面”样本比“正面”多两倍,不处理的话模型把所有样本都判成负面,准确率能到 70%,F1 却不到 10%。权重取各类样本数的倒数再归一化,一次性解决。Batch size 建议 16 或 32,再大显存扛不住,再小 BN 层统计不稳定,容易振荡。

5.2 梯度累积:显卡不够时的后悔药

如果你只有 4GB 显存,batch size 设成 16 会直接 OOM。常见做法是梯度累积,batch size 设 4,累积 4 步再更新一次参数,等效 batch 16。但有个坑:BERT 的 Dropout 在累积时每步都在变化,等效做了一些数据扰动,效果未必差。实现时要手动调用loss.backward(),步数达到累积次数才optimizer.step():

accumulation_steps = 4 for step, batch in enumerate(train_loader): loss = compute_loss(batch) loss = loss / accumulation_steps # 归一化,避免梯度累积后量级变大 loss.backward() if (step + 1) % accumulation_steps == 0: optimizer.step() optimizer.zero_grad()

loss / accumulation_steps这一行别忘,不然等效 batch 变大后学习率没变,收敛容易发飘。梯度裁剪clip_grad_norm_(1.0)也是防 BERT 训练后期梯度爆炸的通用做法。

5.3 我踩过的四个坑,你大概率躲不过

第一个坑:图像翻转导致情感标签反了。现象是训练 loss 下降很快,但验证 F1 一直低。原因是验证集只做了 CenterCrop 没翻转,训练集却用了RandomHorizontalFlip(p=0.5),某些带有左右朝向语义的图片(比如“左边是旧衣服,右边是新衣服”的对比图)翻转后情感极性变化。解决:先看训练集和验证集翻转规则是否一致,把翻转概率降到 0.1,或干脆去掉。

第二个坑:BERT tokenizer 重复加载导致 CPU 内存暴涨。现象是跑两个 epoch 后机器卡死。原因是我在 Dataset 的__getitem__里每次都调用tokenizer.encode,而encode内部加载词表文件,进程一多直接把内存打满。解决:在__init__里一次性初始化 tokenizer,并把它作为参数传入 Dataset,不要在每个样本里重新实例化。

第三个坑:ResNet 冻结后layer4的requires_grad确实设了 True,却加载了model.fc = nn.Identity()导致输出维度对不上。现象是最后一维从 2048 变成 1,全连接层矩阵乘法直接报错。解决:检查model.fc是否真的是Identity(),如果不是,用model.avgpool后的torch.flatten手动获取 2048 维向量。

第四个坑:多个融合模块都用Dropout(0.5),小数据集上训练损失和验证损失差距巨大。原因是很小的样本量配上高 Dropout 会让模型学不稳定。解决:先设Dropout(0.3),如果训练损失比验证损失低一半,说明过拟合,再加早停或 L2;如果验证损失不降,降低 Dropout 到 0.2。

5.4 训练收敛的判断:不要只看准确率

训练到一半,loss 从 1.0 降到 0.3,但验证准确率没变化,别以为模型坏了。情感分类的类别不平衡时,准确率是虚的,重点是 F1。我习惯每两个 epoch 保存一张图,画训练/验证 loss 和 macro-F1 曲线。调参时,先固定融合方法,只改学习率和 batch size,记录三条曲线。改融合方法时,保持同样的数据、随机种子、学习率,否则对比不公平。下面是手工验证一个样本的代码:

def predict_one(model, text_feat, image_feat, class_names): model.eval() with torch.no_grad(): logits, *_ = model(text_feat.unsqueeze(0), image_feat.unsqueeze(0)) pred = logits.argmax(dim=1).item() return class_names[pred]

unsqueeze(0)是为了补 batch 维,因为模型里 BatchNorm 和注意力都要求 batch 维存在。你可以随机挑 10 条数据,把预测概率和文本、图片放在一起看,比任何曲线都直观——比如模型把“有一点点不开心”判成消极,说明它已经理解否定词了。

6. 把实验做成高分课设:评估指标、消融表和 Attention 可视化

验证部分建议按三件事去做:训练集/验证集按 8:2 划分,随机种子固定成 42,每个融合方法跑三遍取平均。报告里不要只给准确率,给 macro-F1 和加权 F1,特别是类别不平衡时,macro-F1 才能反映模型对少数类是否友好。我习惯画一张表格,行是三种融合方法,列是 ACC、macro-F1、参数量、单 epoch 耗时。消融实验除了“只文本”“只图像”“拼接”“门控”“注意力”这几行,再加一行“本文的双模态注意力”,能体现增量贡献。

Attention 可视化是展示多模态模型内部逻辑最直观的技巧。前面CrossAttentionFusion返回了attn_weight,你可以把图片切分成 7×7 的格子(对应 ResNet 特征图),把注意力权重归一化后映射成热力图,叠在原图上。这里有一个细节:attn_weight来自图像区域与文本 CLS token 的注意力,如果权重均匀分布,说明模型没有学到关键区域;如果集中在人脸部位,说明文本提示起了作用。答辩时放三张热力图,比十页文字更有说服力。

最后一件事,保存模型和预测结果时,把文本、图片路径、真实标签、预测标签、置信度都写进 CSV。这样能快速找到模型错在哪里。比如所有错误样本集中在“图像清楚但文本语义反转”的类型,你就能在文档里写“门控融合相比拼接,在反讽场景下 F1 提升了 5%,原因是门控给文本语义分配了更高权重”。这种结论,老师很吃。

这套流程我从课设一直用到做横向项目,唯一的教训是:别在一开始就追求最复杂的融合结构,基线拼接能跑通、结果正常,再往上加门控或注意力。我的第一次方案直接写交叉注意力,结果 BERT 和 ResNet 输入维度匹配就折腾了两天。后来老老实实把三种融合全部跑通,发现最优的往往不是最复杂的,而是让两个模态互相信任的那个。希望这份笔记能帮你在多模态情感分析这条路上少走这些弯路,把精力留在真正有价值的地方——把模型调得更稳,把报告讲得更清楚。

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

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

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

立即咨询