做视觉-语言模型的少样本分类,最绕不开的一类技术就是提示学习(Prompt Learning)。从CoOp、CoCoOp一路到VPT,大家都在想同一个问题:CLIP虽然强大,但直接端到端微调又贵又容易过拟合,能不能只往输入里加一些可学习的提示词,让模型自己“调频道”?MaPLe,全称Multi-modal Prompt Learning,就是在这一串名字里很有代表性的一个方法。它既不像CoOp/CoCoOp那样只在文本分支加提示,也不像VPT那样只在视觉分支加提示,而是同时在CLIP的文本编码器和视觉编码器的多个Transformer层中维护两组提示,并且用耦合函数把语言提示映射到视觉提示,让两个分支在训练过程中真正协同起来。这篇内容适合正在做少样本图像分类、想复现提示学习论文、或准备把CLIP接到自己任务上的研究者,我会把方法思路、核心设计、复现经验和容易踩的坑一次说清楚。
1. 项目概述与核心思路拆解
1.1 从CLIP和提示学习说起
CLIP的思想很简单:用海量图文对做对比学习,把图像和文本映射到同一个向量空间。训练完以后,你给一张图片,再给一串候选文本描述,比如“a photo of a cat”和“a photo of a dog”,它就能算出图片跟哪个文本更接近。这种零样本能力很惊艳,但实际问题里,类别名往往不是“cat”这种日常词,而是医学影像、卫星场景、工业缺陷等专业概念。直接让CLIP零样本分类,效果经常不够用,所以才有了提示学习这条路。
提示学习的核心假设是:CLIP预训练时的文本模板是有固定形式的,比如“a photo of a {class}”。如果把这些模板里的词换成一串可学习的向量,并且只训练这些向量,就能用极少样本把CLIP适配到新任务上。CoOp把文本模板中的上下文词改成可学习向量,CoCoOp进一步让这些向量依赖图像内容,VPT则转向在视觉分支的输入序列里加可学习token。这些方法的共同点都是只调整一侧的表示,另一侧完全冻结。
等到MaPLe出现,思路又往前走了一步:只调文本侧,视觉特征还是原始CLIP空间里的老样子;只调视觉侧,文本侧又是老样子。可CLIP的图文空间并没有完美对齐,尤其是下游数据分布和预训练数据差别大的时候,单侧提示很难同时把两侧拉到合适的语义位置。那为什么不两边一起提示,还让两边提示相互通个气呢?这正是MaPLe想做的事。
1.2 MaPLe到底在解决什么问题
MaPLe要解决的核心问题可以概括成两个。
第一,单模态提示的表示能力有限。文本分支有可学习上下文向量时,模型只能调整文本侧的类别描述;视觉分支有可学习prompt时,模型只能调整图像侧的局部特征。但下游任务真正需要的是“新的视觉概念”和“新的语义描述”一起变化。单侧调参本质上是在固定一个空间,只移动另一个空间,遇到分布偏移时容易顾此失彼。
第二,如果两侧都加提示,但各学各的,依然有问题。视觉提示和语言提示在深层特征空间里如果没有交互,训练时看似都在更新,实际上还是两条互不干扰的平行线。MaPLe的做法是显式地在语言提示和视觉提示之间建立一个映射关系,也就是耦合函数。语言提示经过耦合函数变成视觉提示的初始化或补充,这样梯度可以跨分支传播,文本和视觉的优化就绑在了一起。
从实验结果来看,这种设计对小样本场景特别有效。基类上训练的样本量很小,如果只是单纯增加可学习参数,很容易过拟合成训练集里的纹理和背景。MaPLe把视觉提示限制为语言提示的某种函数,等于给视觉分支的调整加了一个先验约束:图像侧的改动要跟语义侧的改动有关联,不能乱动。这个先验在基类到新类的泛化中起到了明显的稳定作用。
1.3 多模态提示学习的含义
很多人在看到项目名时会把MaPLe顺手读成Maple,这也挺好记的。它全称是Multi-modal Prompt Learning,重点是multi-modal这个词。多模态在MaPLe里有两层含义。
第一层是指模型输入天然跨模态。CLIP本来就有图像和文本两条分支,所以提示学习天然可以分成语言提示和视觉提示。第二层是指可学习参数本身也是跨模态的。MaPLe不只是在两条分支里分别加了一组参数,而是让两组参数通过耦合函数产生关联,形成共同的可学习空间。你更新语言提示时,会影响视觉提示的生成;你更新耦合函数时,又同时改变了两侧的表达。这种“共轭更新”才是多模态提示学习和简单双分支提示的核心差别。
理解这一点很重要,因为所有后续关于MaPLe的复现和变体,本质都是围绕“如何定义视觉和语言提示之间的关系”做文章。
2. 方法拆解:MaPLe的核心设计
2.1 冻结CLIP,只学提示和耦合函数
MaPLe没有改动CLIP的预训练权重,主干网络保持冻结。需要学习的参数只有三类:语言分支各层的提示向量、视觉分支各层的提示向量,以及把语言提示映射到视觉提示的耦合函数。这样做的最大好处是训练开销极小,一张消费级显卡就能跑小规模实验。
从参数效率角度看,MaPLe比全量微调或LoRA式微调都更接近“提示学习”的本意。它没有引入大量额外分支,耦合函数通常就是一个轻量MLP或线性映射,总体新增参数量远小于模型本身。官方实验中,即便只用16个样本的基类支持集,MaPLe也能稳定训练,这背后就是冻结主干带来的强先验。
不过,冻结主干也意味着CLIP本身对输入分布很敏感。如果你的图像预处理和CLIP预训练时不一致,比如分辨率不是224x224,或者归一化参数不对,后续所有提示都白学。复现时第一件事就是检查image transform,而不是急着调超参数。
2.2 语言分支的深层提示
CoOp只在文本编码器的第一层输入位置加入一组上下文向量,后面所有层共享同一个输入序列,但transformer层层加深时,交互方式其实已经固化。MaPLe更彻底:在多个Transformer层各设置一组独立的语言提示。
具体来说,文本输入仍然是“[CLASS]”前面拼接一些token,例如模板“a photo of a [CLASS]”中的“a photo of a”会被替换成一组可学习的上下文向量。区别在于,这些上下文向量不是只在第一层存在,而是每一层都有一份专属的提示。第1层的提示和第6层的提示可以学到不同抽象层级的语义,类别信息能在早期就参与特征交互,而不是在最后一层才发挥影响。
这里有一个很直观的类比:如果只改第一层提示,相当于你在给一个陌生人讲任务时,只调整了第一句话,后面全让对方自由发挥。多层提示则是在对话的每个环节都递小纸条,不断把任务意图重新注入。对CLIP这种深层Transformer来说,越深层引入提示越能直接作用于最后用于对比的特征,这也是MaPLe提升性能的关键原因之一。
语言提示的长度一般取M,比如4或8。长度太短,表示能力不足;长度太长,可学习参数变多,小样本下容易过拟合,而且会和文本原有的token抢位置。实践中我一般从4开始试,如果训练集稍大再调到8。
2.3 视觉分支的提示与耦合函数
视觉分支这边,MaPLe在每层Transformer的patch token序列前面或中间插入视觉提示。因为图像被切成的patch patch数量是固定的,插入的提示token数量可以比patch数量少很多。视觉提示的维度和patch embedding一致,ViT-B/16通常是768维左右,而文本提示的维度是CLIP文本编码器的隐藏维度,通常512。两者维度不同,所以不能直接复用参数。
这里就是耦合函数发挥作用的地方。MaPLe会把当前层的语言提示送入一个耦合函数,由耦合函数映射出一个视觉提示。这个映射可能包含线性投影和非线性变换,目的就是让视觉分支的初始提示带上语言分支的语义信息。除了这个映射生成的提示,视觉分支也可以再保留一小部分可学习增量,用于吸收语言映射表达不了的信息。
如果去掉耦合函数,让视觉提示和语言提示各自随机初始化、各自更新,这其实就是一个更强版的双分支提示方法,效果也能提升,但如果加上耦合,训练时反向传播路径就通了:损失函数对视觉提示求梯度,视觉提示又来源于语言提示和耦合函数,于是梯度能传到语言提示上。语言分支的更新不再只依赖文本分类结果,而是同时受到视觉特征的影响,两个模态才真正开始协作。
2.4 训练和推理流程
训练阶段和大部分CLIP提示学习方法一样。输入一张图像,经过视觉编码器得到图像特征;输入一组类别文本,比如“a photo of a cat”,经过文本编码器得到文本特征。两者做点积相似度,再接一个交叉熵损失。反向传播时,CLIP主干不动,只更新各层提示和耦合函数。
关键区别在于,MaPLe的视觉特征和文本特征都已经被多层提示“修饰”过。图像分支的每一层都注入了来自语言提示映射的视觉提示,文本分支的每一层也都注入了可学习的上下文向量。因此在最终特征空间里,两个模态的表示都向当前任务做了协同偏移。
推理阶段更简单。把所有候选类别的文本特征算一遍并缓存起来,然后对任意一张测试图,只跑一次视觉编码器,算相似度取最大值即可。这种解耦式的分类器设计,使得新增一个类别时不需要重新训练,只要重新生成文本特征就行。实际工程里这个特性很实用,比如你给一个系统增加新的SKU,只需要改类别名列表,不用碰模型。
3. 实验效果与评价协议
3.1 少样本分类
MaPLe的官评协议沿用了提示学习领域常用的少样本评估方法,在ImageNet以及Caltech101、OxfordPets、StanfordCars、Flowers102、Food101、FGVCAircraft、SUN397、EuroSAT、UCF101等一系列数据集上,分别用1、2、4、8、16个样本做训练。每个数据集类别数不同,所以总训练量也不同,但协议统一。
从结论来看,MaPLe在大多数数据集上都稳定超过CoOp、CoCoOp和VPT。尤其突出的是在细粒度数据集上,比如StanfordCars和FGVCAircraft,这些数据集的类别差异很小,单靠文本侧提示很难把语义区分开。MaPLe因为在视觉分支的多层都注入了提示,视觉特征也能针对任务做局部调整,效果明显更稳。
要注意的一点是,这么多数据集的平均分只能说明整体趋势,不同数据集上的增益差异很大。有些数据集本身和CLIP预训练分布很接近,提升幅度可能就1个点左右;有些域差距很大的数据集,提升能到5个点以上。你复现的时候如果只看某个单一数据集,感受到的增益可能和论文里平均分不完全一致,这是正常的,不代表复现出了问题。
3.2 基类到新类泛化
基类到新类泛化是这个领域最关注的指标之一。训练时只给模型看部分类别的少量样本,测试时却要求模型同时识别训练过的基类和没训练过的新类。这非常考验模型有没有充分利用CLIP本身的开放词汇能力,而不是单纯记住训练集类别。
MaPLe在这个协议下有一个很明显的特点:新类准确率比CoOp高出不少。CoOp容易过拟合到基类,新类效果经常掉得厉害;CoCoOp用图像条件缓解了一部分,但依然受限于只在文本侧做提示。MaPLe因为视觉提示和语言提示是耦合的,训练过程中模型把更多精力放在如何调整任务语义上,而不是如何过度拟合训练集纹理。实验里它的基类准确率可能不是最高的,但新类表现往往会更好,基类和新类的平均分更均衡。
如果你实际要做的是一个小样本分类系统,上线后可能不断出现新类别,那就不应该只看基类测试集上的准确率,而要认真跑一下基类到新类评估。这种评估更能反映模型在真实开放场景里的可用性。
3.3 领域泛化与鲁棒性
除了常规少样本分类,MaPLe还在ImageNetV2、ImageNet-Sketch、ImageNet-A、ImageNet-R这些鲁棒性数据集上做过测试。这些数据集的共同点是和ImageNet训练集分布有明显偏移,比如素描风格、对抗样本风格、自然变化等。
提示学习一个容易踩的问题就是:微调后虽然目标数据集准确率涨了,但CLIP原本的鲁棒性被破坏。为什么会这样?因为可学习提示如果把太多注意力放在训练集特有的统计规律上,就会丢失CLIP原始特征空间里的通用知识。MaPLe在这类评估中相对稳健,原因是它没有直接改CLIP主干,视觉提示又受到语言提示的约束,整体对特征空间的扰动更小。这个结论对实际工程很有价值:如果你的任务需要应对各种摄像头、光线、清晰度变化,最好别选过拟合能力太强的微调方案。
3.4 消融实验带来的启发
MaPLe的消融实验里有几个点很值得关注。
第一,提示层数的影响。只在第一层加提示,性能提升有限;随着加提示的层数增多,性能逐步上升,但到一定深度后继续增加收益变小,甚至可能轻微下降。这说明深层提示有效,但不是越多越好,需要平衡参数量和过拟合风险。
第二,耦合函数是否存在的对比。去掉耦合函数、视觉和语言提示独立更新,效果比完整版要差。这直接证明了耦合设计不是锦上添花,而是核心贡献。
第三,视觉提示的初始化方式。用随机初始化的视觉提示,不如用耦合函数从语言提示生成的提示效果好。这个结果告诉我们,在少样本条件下,视觉提示的“起点”非常重要,合理初始化相当于给优化提供了更好的先验,比单纯增加参数容量更划算。
4. 实操复现与超参数经验
4.1 环境与代码库选择
MaPLe官方代码基于PyTorch实现,仓库名称就叫mapl-prompting,核心依赖是open_clip或官方CLIP模型。复现时不需要额外安装重型框架,只要把CLIP权重下载下来就行。
显存方面,ViT-B/16作为视觉主干时,单卡16GB完全够用;如果用ViT-L/14,建议batch size调小,或者用梯度累积。我第一次复现时用的是一张RTX 3090,batch size设4,16-shot的ImageNet实验并没有太大压力。提示学习相关参数都很少,真正占显存的是CLIP主干的前向激活值。
如果你不想从零开始训练,也可以在官方代码基础上做改动。源码里把数据集加载、FewShot采样、模型构建都封装好了,改起来比较顺手。不建议自己重新搭一套CLIP提示学习框架,因为很容易在数据划分和文本模板这些细节上出错。
4.2 关键超参数设置原则
MaPLe里最核心的超参数是prompt长度M、提示层数D、学习率和训练轮数。
从我复现的经验看,M取4是比较稳的起点。M=1时提示过于单薄,每层只有一个token,很难承载复杂语义;M=8在某些数据集上效果更好,但训练集只有1-shot或2-shot时会显著过拟合。D一般取总层数的一部分,ViT-B/16有12层,D取9是比较常见的选择。D取12时所有层都有提示,参数更多但提升不明显,训练也慢一些。
学习率方面,提示向量和耦合函数可以设置不同学习率。耦合函数相当于一个小网络,学习率太大容易震荡;提示向量本质上是embedding类参数,通常可以用稍大一些的学习率。官方配置一般用SGD加动量,学习率范围在1e-3到5e-3之间。如果你用AdamW,学习率要相应调低,否则前期loss会直接飞起来。
训练轮数一般设定在20个epoch以内。因为少样本数据量很小,跑太多轮只会让模型记住支持集,新类泛化反而变差。每轮迭代次数也少,20个epoch很快就能跑完。要特别注意的是随机种子对结果影响可能非常大,同一个配置换一个seed,准确率波动1到2个百分点并不罕见,所以对比方法时一定要多跑几个seed取平均。
4.3 复现中容易踩的坑
第一个坑是文本模板不一致。MaPLe对文本编码器输入的处理会直接影响结果。很多人复现时会随手写一个“a photo of a {class}”的模板,但不同数据集、不同类名拼写,甚至类名前面是否加冠词,都会显著影响特征质量。建议直接沿用官方代码里的模板,不要自行发挥。
第二个坑是图像分辨率。CLIP预训练有固定分辨率,ViT-B/16是224x224。有些数据集原生图很大,如果resize策略和CLIP的预处理不一致,等于给模型喂了分布外的输入。复现时最好把图像统一处理成224x224,再按CLIP自带的normalize参数做标准化。
第三个坑是冻结参数没有完全冻结。很多改代码的人会把CLIP的train()/eval()模式切换写错,导致BatchNorm或Dropout状态异常。CLIP主干用的是LayerNorm和Attention,一般情况下不需要因为训练集太小而切换状态,但一定要确认自己是只更新prompt和耦合函数,不要误把backbone的requires_grad设为True。
第四个坑是损失函数和类别顺序。少样本训练时,每个batch通常只包含当前batch内的类别,而不是全量类别。如果直接写一个所有类别的分类器,会无意中让模型看到未来类别的文本特征,造成信息泄漏,基类到新类评估结果会虚高。
4.4 如何把MaPLe迁移到自己的任务
如果你不想跑论文里的公开数据集,而是要把MaPLe用在自己的业务场景,步骤其实不复杂。
先准备好类别名列表。类别名最好是人类可读的自然语言,比如“scratch on metal surface”“broken capacitor”这类描述。然后构造文本模板。可以是简单的“a photo of {class}”,也可以根据领域改成“a satellite image of {class}”“a histological image of {class}”。
接下来用少量标注图像训练提示和耦合函数。不需要标注大量数据,16-shot每类16张图就能启动。训练完以后,保存各层的提示向量和耦合函数参数。在线推理时,先对所有类别编码文本特征,再对图像编码视觉特征,做矩阵乘法就能得到分类分数。
如果你的任务不是分类,而是图文检索或视觉问答,MaPLe的核心思想同样可以借用。只要你有两条模态分支,并且希望同时调整两侧的提示,就可以参考耦合函数的设计。不过要注意,MaPLe是在CLIP框架下提出的,换成其他双塔模型时,维度、层数、token插入位置都需要按新模型重新调。
5. 常见问题与后续扩展
5.1 常见问题速查表
实际操作中大家问得最多的问题,我整理成了一张表,方便对照排查。
| 问题现象 | 可能的排查方向 |
|---|---|
| loss持续不下降 | 学习率太大或太小;文本模板错误;类别名拼写异常;耦合函数权重初始化异常 |
| 基类准确率高但新类很低 | 训练轮数太多;prompt长度偏长;提示层数过多,导致过拟合;只优化基类分类器 |
| 两个分支效果都不如单侧提示 | 没有正确设置多层提示;耦合函数路径没有梯度;视觉提示和语言提示维度不一致 |
| 换一个seed结果波动很大 | 少样本本身方差大;建议多seed平均;检查数据采样逻辑是否固定 |
| 显存不足 | 减小batch size;减少提示层数;降低输入分辨率(但会影响结果);用梯度累积 |
| 复现结果和论文不一致 | 检查CLIP模型版本;检查数据集划分是否和官方一致;检查是否误用了全量文本类别 |
这些坑里,最隐蔽的是数据划分问题。提示学习领域的少样本划分不是简单随机采样,很多论文会固定一个划分文件。如果你自己随机采样,很可能采样到的类别子集和官方不一致,导致结果没有可比性。
5.2 MaPLe与其他提示学习方法的取舍
做一个简单对比,帮你判断什么场景下选谁更合适。
| 方法 | 提示位置 | 是否跨模态耦合 | 适合场景 |
|---|---|---|---|
| CoOp | 仅文本输入层 | 否 | 文本模板固定、类别语义清晰的任务;实现最简单 |
| CoCoOp | 仅文本输入层,依赖图像 | 否 | 类别变化多、需要动态文本特征的任务;比CoOp泛化好 |
| VPT | 仅视觉各层 | 否 | 视觉特征需要精细调整,文本语义不太重要的任务 |
| MaPLe | 文本和视觉多Transformer层 | 是 | 少样本、基类到新类、跨模态需要协同对齐的任务 |
从部署复杂度看,CoOp最轻量,MaPLe多了一个耦合函数和一组视觉提示,但总体仍然比微调CLIP轻得多。如果你的任务只是固定类别集合,CoOp已经够用;如果你的任务需要频繁加新类,或者训练样本极少且域差距大,MaPLe的泛化优势会更明显。
5.3 MaPLe之后的一些扩展方向
MaPLe之后,提示学习领域出现了很多变体,比如改善耦合函数设计的、用大语言模型生成提示的、或者把prompt学习和adapter结合的。整体脉络都是沿着“如何在保持CLIP泛化能力的同时,用更少的可学习参数,让两个模态更好对齐”这个方向走。
我觉得最有价值的扩展方向不是改耦合函数的网络结构,而是把跨模态提示用于生成任务。MaPLe本质上是为分类任务设计的,但它“视觉提示由语言提示映射而来”的思路,天然适合图文生成、视觉问答、零样本检测等需要对齐语义和视觉细节的场景。比如做开放词汇检测时,语言侧提示可以增强类别描述,视觉侧提示可以增强区域特征,两者耦合后可能比分开调优更自然。
另一个方向是和时间维度结合。视频理解里每一帧都有对应的文本描述,如果每一帧的视觉提示都由全局视频级语言提示来生成,可能比独立处理每一帧更稳定。这类扩展目前在研究社区里已经有了一些尝试,说明MaPLe的核心思想还有不少可挖掘的空间。
写在最后的一点个人体会
我从第一次读MaPLe论文到实际复现,最深的感受是“耦合”这个设计看着简单,但带来的改变是全方位的。以前做视觉-语言提示学习,总习惯把图像和文本当成两个独立模块分别调优,MaPLe提醒了我,预训练模型虽然有两个分支,但它们最终是在一个共享语义空间里工作的。只要让模型知道“图像侧应该怎么改,应该参考文本侧怎么改”,少样本条件下的对齐就会稳很多。最后再分享一个小技巧:如果你在某个数据集上复现效果不理想,先别急着调耦合函数结构,把提示层数从浅到深扫一遍,再检查文本模板,大多数问题都出在这两个地方。