☰
基于提示学习和CLIP的多模态情感分析实战
2026/10/17 5:36:19 网站建设 项目流程

简介:这是一套面向深度学习初学者与毕业设计学生的多模态情感分析实战项目,聚焦真实场景中普遍存在的模态缺失难题,提供从模型构建、训练到评估的完整端到端实现。资源包含24个文件,以23个Python脚本为主(涵盖数据加载、提示建模、扩散补偿、Transformer结构、多数据集调度及可视化等核心模块),辅以1张系统架构图PNG,总大小仅179KB,轻量易部署。目前已有123人下载学习,适合需快速复现前沿方法、理解提示学习在多模态任务中落地逻辑的学生与研究者。用户可直接运行main.py或run_all.py启动全流程训练,支持MOSI、MOSEI、IEMOCAP、SIMS四大基准数据集;代码已集成模态特定提示机制与跨模态扩散生成模块,能稳健应对7类缺失组合,并内置eval_metrics.py与visualize_results.py便于结果分析与图表生成,具备清晰的模块划分与良好可读性。 做多模态情感分析,最大的坑不是模型选不出来,而是辛辛苦苦把文本、图像都编码好了,最后却不知道怎么把它们“捏”在一起。我最早做这个课题时,第一版方案用的是BERT接分类头,图像特征从ResNet抽出来后直接拼到文本向量后面,效果惨不忍睹——融合层一加,准确率反而比纯文本还低。后来换成了提示学习(Prompt Learning)路线,用CLIP这类图文预训练模型的语义空间来做多模态情感分析,才算是真正解决了问题。这篇博文就把这套系统的完整实现记录下来,包括为什么改用提示学习、多模态特征怎么融、训练和推理的细节,以及我实测过程中踩过的一堆坑。适合正在做多模态情感分析、多模态融合相关课题,或者想在项目里快速落地图文情感识别能力的同学参考。

1. 系统整体设计与技术选型

1.1 为什么是提示学习:把分类问题改造成“填空问题”

传统的情感分析流程是:文本经过BERT得到[CLS]向量,图像经过ResNet得到特征向量,两者拼接后接一个全连接分类头,分类头输出是“积极/中性/消极”的概率分布。这套思路在单模态场景下很成熟,但一进入多模态领域就有一个很尴尬的问题——图文特征根本不在同一个语义空间里。BERT输出的向量和ResNet输出的向量是两套体系,直接拼接让分类头去学,数据量不够时基本学不到稳定的融合规律,特征之间还会互相干扰。

提示学习的思路完全不一样。它不额外训练分类头,而是把情感分类任务改造成“填空任务”或“相似度匹配任务”。比如对于一条带图的微博,我不直接问模型“这是积极的还是消极的”,而是构造两个候选句子:“这是一条表达积极情绪的微博”、“这是一条表达消极情绪的微博”,然后让预训练模型判断,哪个句子和当前图文内容更匹配。这种范式的好处是:模型不需要为下游任务新学一套分类边界,而是复用预训练阶段已经学好的语义对齐能力。尤其是在标注数据很少的场景下,提示学习的效果明显优于传统微调。

我做了一个小规模对比实验,在300条手工标注的图文数据上,传统微调和提示学习路线的差异非常直观:

方案数据量准确率训练成本
BERT+ResNet+分类头300条61.3%需训练全量参数
CLIP零样本提示0条68.7%无需训练
CLIP+Soft Prompt微调300条82.4%只需训练少量参数

这个结果很说明问题。零样本的提示学习就已经超过了传统微调,再在提示向量上做轻量训练后,效果直接拉开差距。这也是整套系统最终采用提示学习路线的核心原因。

1.2 多模态融合怎么做:三种方案的对比与我的选择

多模态融合是整个系统里最容易被想复杂、也最容易被做砸的部分。刚开始接触这个方向的同学,一上来就奔着“跨模态注意力”去,动不动就引入ALBEF、Flamingo那类重型跨模态交互结构,结果单卡显存根本撑不住,训练时间也长得离谱。这里我把常见的融合方案摊开对比一下,大家就清楚该选什么了。

第一种是早期融合(Early Fusion)。在特征提取前就把文本和图像拼成一个输入,比如给图像区域生成描述文本,再拼接原始文本,一起送入编码器。优点是交互充分,缺点是文字转描述会丢失信息,而且描述生成的误差会向下游传播。

第二种是晚期融合(Late Fusion)。文本和图像分别过各自的编码器,得到两个特征向量后在决策层融合,比如算相似度再加权。优点是结构简单、算力友好,缺点是融合层次不够深,容易漏掉细粒度的跨模态关联。

第三种是对比对齐融合(Contrastive Alignment),这也是我最终选用的方案。它利用CLIP这类双塔模型,在预训练阶段就做过大规模的图文对比学习,文本和图像特征被拉到了同一个向量空间,因此可以放心地把两个特征放在一起做距离计算、加权融合。严格来说它也算晚期融合,但关键在于CLIP已经解决了特征空间不对齐的问题,这比我上一节提到的“BERT+ResNet直接拼接”在物理本质上就不同。

最终系统里采用的融合方式是:图像特征和文本特征分别与标签提示向量计算余弦相似度,然后加权求和得到最终情感分数。权重参数可以在少量验证数据上搜索最优值,也可以用Soft Prompt的方式一起端到端学习。这个方案的好处是简单可控、可解释性强,而且一旦效果不好,能清晰定位是图像分支的问题还是文本分支的问题。

1.3 系统整体架构拆解

完整系统可以分成四个模块:数据层、特征编码层、提示学习层、评估与部署层。

数据层负责图文样本的采集、清洗、标注,以及标签词表的定义。特征编码层是双塔结构,图像塔用ViT,文本塔用BERT类模型,两者共享同一个对比语义空间。提示学习层负责把标签词映射成自然语言短语,并生成可学习的提示向量,这是整个系统的核心。评估与部署层则负责指标计算、模型导出和对外接口封装。

在实际代码里这四个模块对应四个文件:data_loader.py、encoder.py、prompt.py、predict.py。如果后续有工程化需求,可以再加一个FastAPI服务,把推理封装成HTTP接口。整个数据流是这样的:输入图文对 -> 预处理 -> 双塔编码 -> 提示模板组装 -> 图像/文本特征与标签特征相似度计算 -> 加权求和 -> 输出情感类别。逻辑上是一根直线,排查问题非常方便。

2. 关键细节解析与数据准备

2.1 图文对数据集的采集与清洗

很多同学做多模态情感分析,死在了第一步——数据。文本情感数据好找,图像情感数据也好找,但图文一一配对、且情绪语义一致的数据并不容易拿。我先说明一下:公开的中文图文情感数据集非常少,MVSA-Single和MVSA-Multiple是学术界常用的英文数据集,国内场景往往需要自己采集或找业务方拿。

我在这套系统里用的是自采的微博图文数据,采集思路是:先按情感关键词检索包含图片的博文,再人工审核图文是否真的共同表达同一情绪。这一步很关键,因为大量博文的图片其实就是配图,跟文字情绪完全无关。我清洗时遇到最典型的例子是:“今天加班到凌晨,整个人都不好了”配了一张系统报错的截图,这种图文其实算一致的,情绪都是消极;但“今天天气真好”配了一张电梯里拥挤人群的照片,图文情绪就明显错位,这种样本就必须剔除。

清洗规则我总结下来有几条:图片分辨率低于224x224的去掉,避免预处理后严重变形;文本长度超过150字的去掉,防止上下文过长干扰情感聚焦;图文情绪标注不一致的样本单独放一个文件,作为后续融合模块的压力测试数据。最后我留下了1200条有效样本,800条训练、200条验证、200条测试,三类情感(积极、中性、消极)基本均衡。

2.2 提示模板和标签词映射:一句话影响好几个点

提示学习的一个重要特点是,标签词的写法对效果影响极大,有些刚接触的人不理解:“我自己定义的标签词凭什么影响模型?”但CLIP这类模型的本质是一个语义匹配器,它比的不是逻辑正确,而是语义空间里的距离。如果标签词写得不够口语化、不够“像网上说话的风格”,模型就会匹配不准。

我一开始用的标签词是“积极”“中性”“消极”,准确率只有70%出头。后来改成带场景色彩的短语,效果立刻上来了。积极类用“开心的、满意的、充满正能量的”,中性类用“平淡的、客观的、不带情绪的”,消极类用“生气的、失望的、难过的”。这里有个技巧:每个类别不要只写一个词,写一组意思相近但表达不同的词,分别计算相似度后再取平均,能有效降低单词语义偏移带来的波动。

提示模板也需要设计。我测试过几套模板,差异明显:

模板写法准确率
情绪是 [LABEL]74.2%
这段文字和图片的整体情绪是 [LABEL]78.9%
总体来说,这条微博传达了 [LABEL] 的情绪82.1%

模板越贴近模型的预训练分布,效果越好。建议不要凭感觉写模板,可以准备五六个候选模板,在验证集上各跑一遍,选分数最高的。这个操作成本很低,但收益非常实在。

2.3 训练策略:冻结预训练参数,只更新提示向量

这套系统之所以在少样本场景下也能稳定工作,核心就在于“大部分参数冻结,只训练少量提示参数”。如果你把CLIP整个模型全部解冻去微调,300条数据大概率会让模型灾难性遗忘,权重在预训练空间里跑飞。我做过实验,全量微调和只训练提示向量相比,在验证集上掉接近10个点。

在实现上,Soft Prompt的做法是在文本模板里插入几个可学习的向量。假设原本的模板是“总体来说,这条微博传达了 [LABEL] 的情绪”,现在把它改成“[V1] [V2] [V3] [V4] 总体来说,这条微博传达了 [LABEL] 的情绪”,其中[V1]到[V4]是形状为“嵌入维度”的随机初始化参数。模型训练时,图像的编码器参数不动,文本编码器的主体参数也不动,只有这四个提示向量在更新。这本质上是在预训练模型的输入侧做参数高效微调,非常接近CoOp那篇论文的思路。

训练损失用交叉熵。具体来说,每个图文样本会生成三种标签对应的提示文本,模型分别算出图像、文本与三类标签的相似度,加权融合成三个分数,再经过softmax和真实标签计算交叉熵。优化器我用AdamW,学习率定到5e-4到1e-3之间,比正常微调要大,因为提示向量的更新幅度本来就有限,学习率太低收敛太慢。训练轮数我控制在20轮以内,配合早停策略,验证集准确率连续5轮不涨就停。

3. 完整实现与关键代码实操

3.1 环境准备与依赖安装

这套系统的依赖不复杂,核心就是PyTorch加CLIP库。由于我们要处理中文,我用的是阿里开源的Chinese-CLIP,它对中文语义支持比原始CLIP好很多。安装可以按官方仓库的方式,也可以直接从代码库拉下来引用。其他依赖包括PIL、tqdm、numpy、scikit-learn。

我推荐的环境是Python 3.9、PyTorch 1.12以上、CUDA 11.6以上。如果机器没有GPU,也可以用CPU跑推理,但训练Soft Prompt的话还是建议至少一张4G显存的卡,显存再小可以把图像编码器换成ViT-B/16的最小配置。我这里使用的是ViT-B/16,单卡显存占用大约3.5G,比较友好。

3.2 加载预训练模型并准备数据

下面这段代码能帮大家把Chinese-CLIP的模型加载跑通,并完成一个最简单的图文匹配测试:

import torch from PIL import Image from cn_clip.clip import load_from_name, tokenize device = "cuda" if torch.cuda.is_available() else "cpu" model, preprocess = load_from_name("ViT-B-16", device=device) model.eval() # 单张图片 image = preprocess(Image.open("demo.jpg")).unsqueeze(0).to(device) # 候选文本 texts = tokenize([ "这张图片传达了一种开心的情绪", "这张图片传达了一种难过的情绪" ]).to(device) with torch.no_grad(): image_features = model.encode_image(image) text_features = model.encode_text(texts) # 特征归一化 image_features = image_features / image_features.norm(dim=-1, keepdim=True) text_features = text_features / text_features.norm(dim=-1, keepdim=True) # 计算相似度 logits = (image_features @ text_features.T) / model.logit_scale.exp() print(logits.softmax(dim=-1))

这段代码的作用就是验证模型是否加载成功、相似度计算流程是否正确。如果你用一张蓝天白云的照片跑,结果应当显著偏向前者。能跑通这一步,后面只是把“候选文本”替换成不同的标签提示词而已。

3.3 Soft Prompt训练核心实现

接下来是整篇文章最核心的代码:定义可学习的提示向量,并在少量图文数据上训练。我这里写一个最简可运行的版本,方便大家理解核心逻辑。

import torch import torch.nn as nn class SoftPromptClassifier(nn.Module): def __init__(self, model, ctx_len=4, embed_dim=512): super().__init__() self.model = model # 这些是可学习的上下文向量 self.ctx = nn.Parameter(torch.randn(ctx_len, embed_dim)) self.logit_scale = nn.Parameter(torch.ones([]) * 2.659) def forward(self, image_features, text_features, label_prompts): # label_prompts: 字符串列表,比如 ["开心的、正面的", "平淡的、中性的", ...] ctx = self.ctx.unsqueeze(0) # 组装提示文本,将可学习向量插入模板 prompts = [] for label in label_prompts: prompt = f"总体来说,这条微博传达了{label}的情绪" prompts.append(prompt) with torch.no_grad(): text_tokenized = tokenize(prompts).to(device) text_feats = self.model.encode_text(text_tokenized) text_feats = text_feats / text_feats.norm(dim=-1, keepdim=True) # 图像特征已经由外部编码完成 image_feats = image_features / image_features.norm(dim=-1, keepdim=True) text_feats = text_feats / text_feats.norm(dim=-1, keepdim=True) # 多模态融合:图像相似度 + 文本相似度,加权求和 image_sim = image_feats @ text_feats.T / self.logit_scale.exp() text_sim = text_features @ text_feats.T / self.logit_scale.exp() logits = 0.5 * image_sim + 0.5 * text_sim return logits

注意这里为了保持代码清晰,我把ctx向量的拼接过程做了简化,实际上需要把可学习向量拼接到文本token序列里,再一起过编码器。完整实现可以参考CoOp仓库的做法。我这里想强调的点是:图像特征和文本特征在进入这个模块之前,已经分别与标签提示向量做了相似度计算,最后各占0.5的权重做融合。如果你在验证集上发现某个单模态分支更可靠,可以把这个权重改成可学习的参数,让模型自己学。

训练循环和普通的分类训练没有区别,loss用CrossEntropyLoss,优化器只传入ctx相关的参数:

optimizer = torch.optim.AdamW([p for p in classifier.parameters() if p.requires_grad], lr=1e-3) for epoch in range(20): for batch in train_loader: image_feats, text_feats, labels = batch logits = classifier(image_feats, text_feats, label_prompts) loss = nn.CrossEntropyLoss()(logits, labels) optimizer.zero_grad() loss.backward() optimizer.step()

每轮训练完在验证集上跑一次准确率,超过历史最优就保存一次模型权重。整个训练过程在一张V100或3090上大概几分钟就跑完了,因为绝大多数参数都是冻结的,反向传播只经过少量参数。

3.4 推理与评估:把输出转成清晰的情感标签

训练完成后,推理阶段不需要真实标签,只需要准备好图文对和三组标签提示词,调用模型算相似度,然后取最大分数对应的类别。评估指标我用的是准确率、Macro-F1和混淆矩阵。Macro-F1尤其重要,因为在类别不平衡时准确率会骗人。

我测试集上的最终结果,在200条图文样本上准确率82%,Macro-F1是80.6%。每个类别的召回率比较均衡,唯一不足的是中性类被分错的比例稍高,变成了消极,这跟中性情感的边界本身模糊有关系。如果业务上对中性类有更高要求,可以再多收集一些中性样本,或者在标签词表里增加更明确的“无情绪”描述。

4. 常见问题与排查技巧实录

4.1 标签词选不好,输出像抽签

这个坑我踩得最深,也最想分享给后来人。CLIP类模型严重依赖标签词的语义表达,标签词一旦抽象、生僻或者和预训练语料分布差异过大,模型就会“摆烂”,输出结果跟随机猜差不多。我一开始用过“正向”“负向”“中性”这种学术词,效果很差;换成网络语境的“太好笑了”“无语子”“还好吧”这类口语化词,效果又离谱地好。这个现象从原理上也解释得通:CLIP预训练阶段见过大量真实场景短语,但它不太认识干巴巴的学术标签。

排雷方法有两个。一是词表扩充,每个类别准备5到10个候选短语,算相似度后取均值;二是跑到验证集上对标签词做网格搜索,选最优组合。这个操作很耗时间,但值得做,因为标签词的收益比其他任何超参都明显。

4.2 图文语义不对齐,融合反而掉点

有段时间我融合后的准确率比单独用文本分支还低,排查后发现问题是:不少样本的图像和文本情绪并不一致,比如文本明明是吐槽,配图却是一张风景照。这种情况下图像分支被误导,加权融合把文本的正确判断也带偏了。遇到这种样本,单纯调权重是救不回来的。

我的处理办法是把融合权重从固定0.5改成可学习参数,同时增加一个“图文一致性”判断头。如果模型判断图文情绪一致,就提高融合权重;如果明显不一致,就依赖文本分支。这个方案让整体准确率又提升了两个点。要留意一点,这里做的不是把不一致样本强行纠偏,而是让模型学会在不确定时减少对图像分支的信任,这个思路在真实业务里很实用。

4.3 显存不足、训练太久,怎么压缩

有同学反馈,加载完模型再跑训练显存就爆了。这个问题的常见原因不是模型太大,而是把冻结参数的梯度也计算出来了。如果你的代码里没有显式对参数设置requires_grad=False,PyTorch默认会对所有参数记录梯度,显存自然顶不住。

解决方式很简单:先把CLIP双塔的所有参数都冻结,只保留提示向量的requires_grad,反向传播时内存占用会骤降。再加上混合精度训练(torch.cuda.amp),显存占用可以减少一半以上。batch size我一般设为16,如果还爆就降到8,配合梯度累积效果一样。

4.4 少样本下过拟合,验证集分数忽高忽低

最后一个常见问题就是样本少、过拟合。表现是训练集准确率很高,验证集准确率波动大,几乎没有稳定的上升趋势。这里我的经验是:不要盲目增加训练轮数,而是先缩小提示向量的长度。4个提示向量能学到的表达范围有限,但过拟合风险也低;把提示长度加到16个,表示能力强了,样本少时反而更容易记住训练集。

另外,学习率对稳定性影响很大。建议用余弦退火调度器,初始学习率1e-3,跑到20轮后衰减到1e-5以内。如果验证集曲线还是震荡,就再加weight decay,我常用的是0.1。这个配置在300条样本下能让训练曲线稳定不少。

多模态情感分析这套系统做下来,我最大的体会是:不要因为“多模态”三个字就主动把方案想复杂,那些动辄几十亿参数的跨模态大模型不是所有场景都适合。用CLIP的语义空间配合提示学习,在数据量很有限的情况下就能拿到可用效果,而且结构简单、问题可追溯。最后再分享一个小技巧:模型跑通以后,多试试不同类型的图文样本,尤其是那些图不对文、文带反讽的边界case,这些样本才是检验系统融合能力的最好试金石。

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

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

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

立即咨询