资讯动态

一致性引导提示学习:提升视觉语言模型少样本泛化能力

发布时间:2026/10/26 2:42:35 来源:尧图企业网站定制
做视觉-语言模型下游适配的同行大概都踩过同一个坑拿 CLIP 这类预训练模型做少样本分类手工写的 prompt 稍微换个说法准确率就能上下浮动好几个点。后来 CoOp 一类的提示学习把 prompt 里的上下文词换成可学习向量效果确实上来了但新问题跟着来——在 base 类上拟合得很好换到 novel 类就掉得明显本质是那几条可学习向量把训练样本记住了。ICLR 2024 上这篇关于一致性引导提示学习Consistency-Guided Prompt Learning的工作抓的正是这个痛点与其一味堆正则、把 prompt 往手工先验上拽不如让模型在多种视图、多种模态、多种扰动下保持预测一致。这个思路听起来朴素落地时讲究不少——一致性约束加在哪一层、权重怎么调、增强怎么配每一步都影响最终泛化。这篇内容适合正在做视觉-语言模型少样本适配、提示微调以及想把一致性正则用到多模态任务里的同学我会把方法拆开把可复现的配置和踩坑记录一并写出来。1. 视觉-语言模型提示学习为什么需要一致性引导1.1 从手工 Prompt 到可学习 Prompt 的演进逻辑先把背景捋清楚。CLIP 这类视觉-语言模型的核心能力是把图像和文本映射到同一个特征空间然后通过余弦相似度做匹配。做零样本分类时我们给每个类别拼一句文本比如 “a photo of a dog”把这句话送进文本编码器得到文本特征再和图像特征算相似度取最高的那类作为预测。这里的 “a photo of a” 就是所谓 prompt 模板它本身不参与训练全靠人工设计。问题在于模板对结果极其敏感。我做过一组对照同一批图像模板从 “a photo of a {}” 换成 “a picture of a {}”在细粒度数据集上准确率能差出三到五个点。原因是文本编码器对词序和用词很敏感人工试错成本高而且换一个数据集就得重新调。CoOp 的思路很直接把模板里的上下文词替换成一组可学习的连续向量让模型自己在训练中把这几个向量调到合适的位置类别名仍然作为文本输入保留。这样既保留了 CLIP 文本编码器的结构又把模板设计这件手工活交给了优化器。这个改动的好处是参数量极小。以 16 个上下文 token 为例每个 token 的维度是 512总共也就八千多个可学习参数相比整个 CLIP 动辄上亿的参数几乎可以忽略。参数少意味着训练快、显存友好在只有几张卡的环境里也能跑。但参数少不等于没有过拟合风险这一点后面会详细说。1.2 泛化掉点的真正原因提示向量在记忆样本提示学习在少样本场景下的经典评测协议是 base-to-novel用一批类别base的训练样本学 prompt然后分别在 base 类的新测试样本和完全没见过的 novel 类上评测。理想情况下prompt 学到的应该是“如何描述一个类别”的通用方式而不是“这批训练图像长什么样”。实测下来base 类准确率往往很漂亮novel 类却明显掉队两者的差距就是泛化 gap。我把这个 gap 归因到两点。第一可学习向量虽然参数量小但在只有 16 shot 的情况下它仍然有足够自由度去拟合训练集的偶然特征比如背景、拍摄角度、光照。第二交叉熵损失只关心“预测对不对”不关心“模型对扰动的反应是否稳定”于是模型会走捷径把 prompt 调成对训练分布过度敏感的样子。一致性引导的切入点就在这里。它不去直接限制 prompt 向量本身长什么样而是要求模型面对同一张图的不同增强版本、或者同一语义的不同表达时输出分布保持稳定。这个约束是软的、任务无关的相当于给优化过程加了一条“别对无关变化过度反应”的护栏。相比直接把 prompt 往手工模板上拉的正则一致性约束不引入额外的人工先验泛化收益更干净。2. 一致性引导的核心思路拆解2.1 三种常见的一致性维度与取舍读标题里的 “consistency-guided”第一反应要问一致的是什么东西基于这类方法的常见实践一致性通常可以落在三个维度上各自的作用和代价不同。视图一致性对同一张图像施加两次不同的数据增强随机裁剪、颜色抖动、翻转等分别前向要求两次输出的类别分布接近。这是最常用也最容易实现的一种本质是把半监督里的一致性正则搬到多模态场景。代价是要做两次前向训练时间大约翻倍。模态一致性图像侧做扰动的同时文本侧也对 prompt 做轻微扰动比如对上下文向量加小噪声或者用两种不同的模板表达要求跨模态的匹配关系稳定。这种约束更贴近视觉-语言模型的本质但实现上要小心文本侧的扰动过强会破坏语义。样本或任务一致性在多个数据集、多个任务之间共享 prompt 时要求 prompt 在不同任务上的行为不至于剧烈冲突。这种适合多任务联合训练的场景单数据集少样本实验里用得少。实际落地时视图一致性是性价比最高的起点实现简单、效果稳定。我建议先把这一条做扎实再考虑叠加模态一致性。两者同时上、权重没调好很容易互相干扰训练曲线会变得很难看。2.2 损失函数怎么搭主任务损失加一致性正则整体损失是两项相加L L_ce λ * L_consL_ce是标准的交叉熵用图像特征和各类别文本特征的相似度算 logits。L_cons是一致性项常见做法是两次视图输出分布之间的 KL 散度或均方误差。这里有几个细节值得说清楚。第一用 KL 还是 MSE。KL 散度对分布形状更敏感适合分类任务MSE 实现简单但在类别数多的时候梯度尺度不好控制。我的经验是分类任务优先用对称 KL也就是两个方向各算一次取平均避免单向 KL 把某一侧当成“教师”造成不对称。第二要不要 stop-gradient。如果两次前向用的是同一个模型直接算 KL 会让模型倾向于输出均匀分布来“偷懒”因为均匀分布之间的一致性最高。解决办法有两种一种是对其中一侧做 stop-gradient模仿 Mean Teacher 的思路另一种是给一致性项加一个温度系数把分布锐化后再算。我倾向后者实现更简单不需要维护额外的教师模型。第三λ 的设置。这个权重不能一开始就给大否则一致性项会压过主任务模型学不到分类能力。常见的做法是 warmup前几个 epoch λ 从 0 线性涨到目标值。目标值本身跟数据集难度有关粗略范围在 0.1 到 1.0 之间需要靠消融确定。注意一致性项不是越大越好。λ 超过某个阈值后novel 类准确率反而会下降因为约束过强会让 prompt 失去区分度输出趋于平滑。3. 可复现实现一致性引导提示学习的关键环节3.1 模型骨架与可学习提示的构造先把骨架搭起来。我用 PyTorch 写视觉编码器和文本编码器都冻结只训练上下文向量。下面是提示构造的核心片段省略了 CLIP 加载的样板代码。import torch import torch.nn as nn class PromptLearner(nn.Module): def __init__(self, clip_model, classnames, n_ctx16, ctx_inita photo of a): super().__init__() # 用初始化短语的词向量填充上下文比随机初始化收敛更稳 tokenized clip_model.tokenizer([ctx_init]) with torch.no_grad(): init_emb clip_model.token_embedding(tokenized).type(clip_model.dtype) self.ctx nn.Parameter(init_emb[:, 1:1 n_ctx, :].clone()) self.n_ctx n_ctx # 类别名单独编码不参与梯度 self.class_embeddings self._encode_classes(clip_model, classnames) def _encode_classes(self, clip_model, classnames): prompts [fa photo of a {c}. for c in classnames] tokens clip_model.tokenizer(prompts) with torch.no_grad(): emb clip_model.token_embedding(tokens).type(clip_model.dtype) return emb def forward(self, clip_model): # 拼接可学习上下文 类别词嵌入 prompts torch.cat([self.ctx.expand(len(self.class_embeddings), -1, -1), self.class_embeddings], dim1) # 送入文本编码器得到每个类别的文本特征 text_features clip_model.encode_text_from_embeddings(prompts) return text_features / text_features.norm(dim-1, keepdimTrue)这段代码有两个容易出错的点。上下文向量的初始化用 “a photo of a” 的词向量而不是纯随机收敛速度和最终精度都更稳这是 CoOp 原文就验证过的经验。另外类别词嵌入要和上下文向量在序列维度上正确拼接位置对齐错了模型也能训但效果会莫名其妙地差排查起来很费时间。3.2 双视图前向与一致性损失的计算数据增强部分我用 torchvision 的变换组合做两次独立采样。注意两次增强要来自同一张原图强度适中裁剪比例别太激进否则一致性约束会变成“逼模型在信息缺失的情况下也保持一致”反而有害。def consistency_loss(logits_a, logits_b, temperature2.0): # 温度锐化避免模型靠输出均匀分布来降低一致性损失 p torch.log_softmax(logits_a / temperature, dim-1) q torch.softmax(logits_b / temperature, dim-1) # 对称 KL loss_ab torch.sum(q * (q.log() - p), dim-1).mean() loss_ba torch.sum(p.exp() * (p - q.log()), dim-1).mean() return 0.5 * (loss_ab loss_ba)温度参数这里给 2.0是我在几个数据集上试出来的折中值。温度太低分布太尖锐一致性约束容易被少数高置信样本主导温度太高分布被抹平约束失去区分力。这个值不用精调1.5 到 3.0 之间结果差异不大但一定要设不能直接用原始 logits。3.3 训练循环、优化器与超参配置训练循环的主体结构如下我把 warmup 和梯度裁剪都加上了。optimizer torch.optim.SGD(prompt_learner.parameters(), lr0.002, momentum0.9, weight_decay5e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_maxepochs) lambda_max, warmup_epochs 0.5, 5 for epoch in range(epochs): lam lambda_max * min(1.0, epoch / warmup_epochs) for images, labels in loader: view_a, view_b aug(images), aug(images) # 两次前向共享 prompt 与冻结编码器 logits_a forward(clip_model, prompt_learner, view_a) logits_b forward(clip_model, prompt_learner, view_b) loss_ce F.cross_entropy(logits_a, labels) loss_c consistency_loss(logits_a, logits_b) loss loss_ce lam * loss_c optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(prompt_learner.parameters(), 1.0) optimizer.step() scheduler.step()超参方面优化器用 SGD 而不是 AdamW原因是可学习参数太少AdamW 的逐参数自适应学习率在这个尺度上反而容易震荡这一点跟常规微调经验相反值得注意。学习率 0.002、权重衰减 5e-4 是比较稳的起点。批次大小受显存限制16 到 32 都行太小会让一致性损失的统计噪声变大。epoch 数建议 50 起步少样本场景下 prompt 收敛不算快过早停会误判方法无效。梯度裁剪阈值设 1.0主要是防止一致性项在训练初期梯度爆炸。4. 实验设计与效果验证的要点4.1 base-to-novel 协议的落地细节评测协议要严格对齐否则数字没法比。base 类用全部训练样本学 prompt测试时分别报告 base 类测试集准确率和 novel 类测试集准确率再算两者的调和平均作为综合指标。调和平均比算术平均更能反映平衡性一边高一边低的情况会被压下去。数据集选择上常规做法是覆盖通用物体、细粒度、场景、纹理几类每个数据集按类别划分 base 和 novel比例常见是 1:1 或 base 占多数。划分方式一旦确定就不能改我见过有人为了刷高 novel 准确率偷偷调整划分这种结果没有任何参考价值。4.2 消融实验怎么设计才有信息量消融不能只做“去掉一致性项”这一个对照那样只能说明“有用”说明不了“为什么有用”。我建议至少做三组。消融维度对照设置观察指标预期结论一致性项有无λ0 vs λ0.5base/novel 准确率novel 提升base 基本持平一致性维度仅视图 vs 仅模态 vs 两者训练稳定性、最终精度视图为主模态增益有限λ 敏感性0.1 / 0.5 / 1.0 / 2.0novel 准确率曲线存在明显峰值过大反降温度设置1.0 / 2.0 / 3.0一致性损失收敛速度差异不大2.0 较稳这组消融能回答一个关键问题收益到底来自“正则化”还是来自“多视图带来的数据效率”。如果只加一致性项但不做双视图、改成对同一视图前向两次novel 准确率应该基本不涨这就说明双视图的信息增益是必要的。5. 常见问题与排查实录5.1 一致性损失不降或训练震荡表现是训练日志里loss_cons一直在高位波动不随 epoch 下降。优先查三件事一是数据增强强度裁剪比例如果设到 0.2 以下两个视图信息量差异太大模型无法对齐把裁剪下限提到 0.5 通常就正常了二是温度是否设了没设温度直接用原始 logits 算 KL分布太尖梯度不稳定三是 warmup 是否生效λ 一上来就给 0.5前几个 epoch 主任务还没学好一致性项会把优化方向带偏。还有一个隐蔽原因两次视图如果用了同一个随机种子增强结果完全一样一致性损失恒为零日志看起来“很稳”但实际没有任何约束作用。这个坑我在早期实现里踩过排查了半天才发现增强函数里把种子写死了。5.2 提示向量退化与 novel 类不升反降所谓退化是指训练后期 base 准确率还在涨novel 准确率开始掉。这种情况通常是一致性权重过大或者训练 epoch 太多。我的处理顺序是先把 λ 减半看曲线如果 novel 回升说明约束过强如果没变化再看 epoch把训练轮数砍掉三分之一观察 novel 是否在更早的位置达到峰值。另一个可能是类别词嵌入被意外更新了。类别名部分必须保持冻结如果实现时不小心把它和上下文向量一起放进了优化器参数组等于让模型直接改类别语义base 类会虚高novel 必然崩。检查方法是打印优化器参数组确认只包含ctx。5.3 显存与批次配置的工程问题双视图前向意味着激活值翻倍显存压力比普通提示学习大不少。几个缓解手段一是把视觉编码器的前向用torch.no_grad()包住因为编码器冻结不需要存它的计算图这一条能省下大量显存二是两次视图不要同时保留中间激活可以先后前向、各自 detach 出 logits 再算一致性三是批次从 16 起步稳定后再试着往上加。问题现象可能原因排查动作处理方案loss_cons 不下降增强过强 / 未设温度 / 同种子检查增强参数与温度裁剪下限 0.5温度 2.0novel 准确率下降λ 过大 / epoch 过多做 λ 与 epoch 的单变量扫描λ 减半epoch 砍三分之一base 虚高、novel 崩类别嵌入被更新打印优化器参数组只优化 ctx显存溢出双视图激活翻倍检查是否 no_grad编码器前向禁用梯度训练速度过慢两次前向串行分析耗时分布视图前向分批或异步我个人在实际操作中的体会是一致性引导这类方法的价值不在于单点涨几个点而在于它让提示学习的调参变得不那么脆弱。以前换个数据集、换个 shot 数prompt 的超参就得重调一遍加上一致性约束之后学习率和 epoch 的敏感区间明显变宽同一套配置在多个数据集上都能跑出接近的结果。这对实际项目里没法为每个数据集单独调参的场景意义比论文里那零点几个点的提升更实在。如果你正在做多模态少样本适配建议先把视图一致性这条最简路径跑通确认训练稳定、novel 不掉队再考虑往上叠模态一致性。别一上来就三四种约束全加那种配置在论文里能收敛在你自己的数据上大概率会给你一堆看不出原因的坏曲线。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑