资讯动态

图注入软提示:GIPCOL 组合零样本复现与调参指南

发布时间:2026/10/27 11:08:47 来源:尧图企业网站定制
做组合零样本Compositional Zero-Shot LearningCZSL的同学大概都有过这种体验模型在“红色的车”上表现还行一换成“红色的苹果”就崩明明两个词都认识拼在一起就不认识了。2024年视觉领域有一篇叫 GIPCOL 的工作思路挺对我的胃口——它不去硬改视觉主干也不去造复杂的生成模型而是把“属性与物体的搭配知识”做成一张图用图网络生成软提示soft prompt喂给预训练视觉语言模型。这篇我就按自己复现时的理解路径把组合零样本的核心难点、图注入软提示的设计动机、模块拆解、可落地的实现路径以及我踩过的坑一次性讲透。适合谁看做过 CLIP 类模型微调、想搞清楚提示学习在细粒度/组合任务上怎么用的工程同学正在复现 CZSL 基准、被 seen/unseen 曲线折磨的研究同学以及想把“属性物体”这种组合查询能力搬到自己业务里的开发者。看完你至少能判断这个方法的增益来自哪里复现时哪些参数必须认真对待哪些地方最容易白跑几天实验。1. 先把问题钉死组合零样本到底在考什么1.1 从“见过的搭配”到“没见过的搭配”组合零样本学习的设定很朴素训练集里只出现一部分属性物体组合测试时要预测从未在训练中出现过的组合。举例训练见过“红色的车”“蓝色的车”“红色的苹果”测试要求你判断“蓝色的苹果”。属性词和物体词单独看都是见过的唯独它们的搭配是新的。这跟常规零样本分类的区别在于常规零样本的类别单元是“一个整体标签”没见过就是没见过而 CZSL 的类别单元是“两个已知原子拼出来的新整体”模型必须理解组合语义而不是简单记住一个类别名。换个生活化的说法你认识“辣”和“冰淇淋”也知道“辣”和“火锅”的组合现在让你想象“辣的冰淇淋”长什么样——你靠的不是记忆而是把两个概念的语义关系重新搭建一遍。这个设定带来的第一个硬性约束是搜索空间爆炸。属性数上百、物体数几百理论组合就是数万到数十万而真实数据只覆盖其中很小一部分。第二个约束是视觉纠缠同一个属性在不同物体上的外观差别极大“红的”在车上是漆面高光在苹果上是漫反射果皮在衣服上是布料褶皱。模型如果只学到“红”的某种像素统计迁移到新物体上必然失效。第三个约束是上下文依赖“旧的”可以修饰书很难修饰手机“锋利的”能配刀配云就说不通。这些先验不在图像里只能从数据分布或外部知识里挖。理解这三个约束很重要因为它直接决定了后面所有方案的设计取舍你要么加强组合的语义表示要么显式建模属性与物体的关系要么两者都做。GIPCOL 走的是第二条路并且把关系知识塞进了提示空间。1.2 三条老路线的天花板在哪我梳理过这个方向的演化大致三条主线每条都值得知道它卡在哪。第一条是组合分类器派代表思路是学属性分类器和物体分类器再在推理时把两者的分数组合起来。好处是简单、可解释坏处是它假设属性与物体独立而现实中两者强耦合独立假设直接把“红的苹果”和“红的车”当成同一件事处理。第二条是生成式派用 VAE 或 GAN 为未见组合合成视觉特征再拿合成特征训练分类器。这条路在数据稀缺时有优势但合成质量难控训练不稳定多阶段管线调试成本高超参一多就很难判断增益究竟来自哪一块。第三条是原型/嵌入派把组合映射到某个联合嵌入空间用兼容性函数打分。这类方法效果不错但对嵌入空间的几何结构很敏感属性词向量的初始化方式能左右几个点。三条路线的共同天花板是它们大多从零训练视觉主干或者在 ResNet 级别的特征上做文章语义先验的容量有限。而预训练视觉语言模型CLIP 这一类把“词与图的对应关系”已经压进了参数里等于白送你一个强大的语义底座。问题随之变成怎么让这个底座懂“组合”而不是只懂“词”。1.3 提示学习带来的新变量CLIP 的零样本推理靠模板比如“a photo of a [属性] [物体]”把文本编码成向量去和图像对齐。手工模板的问题很明显措辞一变结果就变而且模板是全局共享的对所有组合一视同仁。CoOp 这类工作把模板里的固定词换成可学习的连续向量软提示在少样本分类上很有效。但直接套到 CZSL 上会撞墙。全局软提示学到的是“整个数据集的最优上下文”它无法针对不同组合给出不同的提示——而 CZSL 的关键恰恰是“每个组合都需要一点专属的语义上下文”。你可以理解为全局提示像一条通用横幅挂在所有商品上而组合任务需要的是每个货架贴一张定制标签。GIPCOL 的切入点就在这儿别让提示全局共享让属性与物体的关系图来决定每个组合的提示长什么样。训练时只更新图网络和提示向量视觉塔和文本塔的主体参数保持冻结参数量小、收敛快也好在小规模卡上跑起来。这套设计让我第一次觉得 CZSL 的复现成本可以压到“单卡几天”的量级。2. GIPCOL 的整体思路让图来决定提示的内容2.1 全局软提示的两个隐患我在复现其他提示学习方案时总结出全局软提示在组合任务上的两个隐患理解这两点基本就理解了 GIPCOL 为什么要引入图。隐患一是提示坍缩。如果所有组合共享同一组可学习向量优化过程会倾向于找到“对大多数训练组合都还过得去”的平均解。平均解在已见组合上损失很小但它把属性信息与物体信息混在一坨连续向量里模型无从分辨哪几位该负责“红”、哪几位该负责“车”。测试时遇到新搭配这坨向量没有可分解的结构泛化就无从谈起。隐患二是数据不均衡被放大。训练集里某些组合比如“破旧的建筑”“自然的风景”样本极多另一些只有个位数。全局提示会被高频组合主导低频组合的语义几乎没被编码进去。而未见组合的泛化恰恰更依赖那些长尾的语义关系。图结构能治这两个病。节点是属性词和物体词边权是共现强度或语义相似度。高频组合在图上就是一条粗边低频组合是一条细边但依然存在信息不会被完全淹没更重要的是图上的邻居关系给出了“哪些属性和哪些物体在语义上接近”的先验未见组合虽然没样本但它在图上不是孤岛。2.2 图注入的两个直觉第一个直觉组合的提示应该由它的两个组成部分共同决定而不是由数据集决定。属性节点和物体节点各自经过图传播后得到上下文增强的表示再融合成这个组合的提示向量。这样“蓝色的苹果”的提示天然带有“蓝”的语义和“苹果”的语义而不是一条通用横幅。第二个直觉图上传播等价于一次语义插值。未见组合的提示可以由它在图上的邻居相近属性、相近物体加权得到。你可以把它想成“没见过辣味冰淇淋但见过辣味巧克力和甜味冰淇淋那就把两者的提示各取一半再微调”。这正是 CZSL 需要的泛化方式——从已知关系外推到未知关系而不是从零学一个新类别。这两点合起来解释了一个常被忽略的事实这个方法的增益主要不在视觉侧而在文本提示的结构化上。它把“提示应该随组合变化”这件事落在了一个可学习、可解释、参数极少的图网络上。2.3 整体架构走一遍按数据流顺序说一遍后面再逐块拆。输入是一张图和它的组合标签属性、物体。图像进冻结的视觉编码器得到图像特征。同时属性词与物体词作为节点进入图编码器图上做两三层消息传递输出两个节点级向量这两个向量拼接或相加后过一个小 MLP投影成若干个提示 token这些 token 与属性词、物体词的原始词嵌入一起按模板顺序拼进文本编码器得到文本特征。最后在共享嵌入空间里做图像-文本对齐配一个组合分类头。训练目标通常两块一块是图像与“正确组合文本”的对比损失batch 内其他组合作负样本一块是组合分类的交叉熵。前者负责把视觉和语言拉到同一个空间后者负责把决策边界压实到组合粒度上。推理时有个细节值得强调测试用的是已见组合和未见组合混在一起的候选集。闭世界设定下候选集由训练中出现过的属性集合和物体集合做笛卡尔积生成开世界设定下还要包含训练中没出现过的属性或物体词。这个区别直接决定了评估代码怎么写也决定了你要不要在构图时给“词表外节点”留位置。3. 核心模块拆解图怎么建、提示怎么生成3.1 构图节点、边权与归一化节点部分很好办属性词表加物体词表每个词取预训练文本编码器输出的词嵌入作为初始特征。用词嵌入而不是随机初始化是为了让图上语义相近的词天然靠近“红”和“深红”即便没有共现边在特征空间里也不远。边权是这套方法的核心设计点也是我最开始做错的地方。最直接的做法是用训练集共现次数当边权但共现次数严重偏向高频物体数据里“人”出现的次数可能是“台灯”的几十倍按原始计数建图几乎所有属性都会连到“人”上图就退化成星形结构。我一开始就是这么干的结果未见组合的准确率比基线还低。更稳的做法是对共现做归一化常用的有两种一种是按行归一化每个属性节点的出边权重和为 1让每个属性把注意力均摊到它出现过的物体上另一种是用点互信息PMI代替原始计数PMI 会惩罚高频共现把“意外搭配”的权重抬起来。我实测 PMI 在长尾组合上更稳代价是要加一个平滑项防止 log 溢出。还有一处容易忽略边要不要对称。属性到物体和物体到属性在语义上并不等价“红”修饰“车”很自然反过来说“车修饰红”就怪但图卷积要求邻接矩阵通常是对称或至少可转置传播。我的处理是建一张有向图做传播但在归一化时用对称归一化 D 的 -1/2 次方乘 A 乘 D 的 -1/2 次方兼顾数值稳定和方向信息。最后是自环。一定要加单位矩阵否则节点在传播中会丢掉自身信息两三层的图就会退化成“全是邻居平均值”。3.2 图编码器与提示向量生成图编码器不用复杂两层 GCN 或者一层 GAT 就够。我试过三层和四层效果反而下降典型的结构过平滑层数一多所有节点表示趋于一致属性之间的区分度被抹平。两层的感受野已经能覆盖“属性的邻居的邻居”对这几个基准的规模足够。传播完得到每个属性节点和物体节点的表示。生成提示时有几种融合方式我按效果从差到好排一下直接相加最省事但会丢身份信息拼接后过线性层好一些我最终用的是“拼接 小 MLP 拆成 L 个 token”L 取 8 到 16 之间。这里有个工程细节token 数不要盲目加超过 16 之后文本侧的上下文长度变长训练成本上升收益却很平。融合后的提示向量要和词嵌入对齐维度。CLIP 文本编码器的输入是 token 嵌入序列软提示就是往序列里插若干连续向量位置决定了它在语义上扮演什么角色。我的经验是把软提示放在属性词之前形如“[软提示 x L][属性][物体]”让提示充当“这个组合的修饰语”比插在两个词中间更稳。插中间会把属性与物体的词序关系打乱文本编码器的位置编码会受影响。还有一处值得说要不要给每个组合单独生成提示还是给每个属性、每个物体各生成一段。前者表达力强但参数随组合数增长退化风险高后者把提示拆成属性段和物体段组合时拼接。我选的是后者理由是它天然保证了“同一属性在不同组合里的表示一致”这与 CZSL 的评测逻辑吻合也让图网络的输出能被复用。3.3 损失函数与训练策略损失这块我用了两路。主路是图像-文本对比损失温度系数可学习初始值用预训练模型的默认值。负样本来自同一 batch 内的其他组合文本batch 越大负样本越充分但显存也吃紧。第二路是组合分类的交叉熵分类头作用在图像特征上类别数是训练集中出现过的组合数。这一路的作用是把“哪些组合是合法的”这条约束显式加进去防止模型把“蓝苹果”和“蓝车”的文本特征拉得太近。还有一个可选的正则对图网络输出的节点表示加一个平滑约束让相邻节点的表示不要差太远。这个正则在小数据集上有用在大数据集上权重调不好会拖累效果我最后把它关了。训练策略上有三个决定值得展开。第一冻结哪些。视觉编码器和文本编码器的主体参数全冻只训图网络、提示向量、融合 MLP 和分类头。可训练参数大概在几百万量级单卡 24G 完全够。好处是避免灾难性遗忘也让不同随机种子的结果更稳定。第二学习率分组。提示向量是随机初始化的新参数需要大学习率图网络是在固定词嵌入上做传播学习率要小一到两个数量级。我一开始用同一个学习率结果提示还没学好图网络已经过拟合到训练组合上了。分组之后收敛曲线明显更干净。第三验证指标怎么选。别用整体准确率选模型要用已见与未见的调和平均。原因是整体准确率会被已见组合主导模型只要把已见组合背熟指标就很好看但未见组合一塌糊涂。这个坑我在早期项目里踩得很深模型在验证集上准确率涨了三个点实际部署时新组合的召回惨不忍睹。3.4 推理阶段闭世界与开世界的分岔闭世界推理简单把所有“训练见过的属性 × 训练见过的物体”组合枚举出来逐个生成提示、编码成文本特征与图像特征算相似度取最大。候选集大小一般是几万全量编码一次能接受但每次改提示参数都要重编码调参阶段建议把文本特征按 epoch 缓存一次。开世界推理麻烦在词表外节点。测试集里可能出现训练时没见过的属性词或物体词这些词在图上是“孤立节点”没有共现边。处理方法有两种一种是用词嵌入相似度给孤立节点补边连到最相近的几个已见节点上另一种是给它一个可学习的“未知节点”表示训练时用掩码模拟。我倾向第一种实现简单效果也够用但要注意相似度阈值阈值太低会引入噪声边。这里有个容易翻车的点开世界评测时候选集是否包含训练未见的词直接决定指标能不能和论文对齐。我看过不少复现仓库把开世界当闭世界跑指标虚高一大截。动手前先确认划分文件里有没有 unseen attribute / unseen object 的标记。4. 复现路径从数据到能跑的第一版4.1 数据准备与组合划分三个常用基准的特点差别很大选的时候要心里有数。MIT-States 属性多、物体多、图像中等规模组合稀疏是最能体现方法泛化能力的一个UT-Zappos 是鞋类数据集属性其实是材质皮革、麂皮之类组合数很少指标容易饱和增益不明显C-GQA 规模最大、属性最细还带开世界评测是检验鲁棒性的首选但训练成本也最高。划分文件一般随数据集提供包含训练组合、验证组合、测试组合三份。拿到之后第一件事是统计训练组合覆盖了多少属性、多少物体测试里有多少组合包含未见的原子。这个统计决定了你要不要开图上的孤立节点处理。预处理上有两个细节。图像侧按预训练模型的标准化参数走别自己乱改否则文本-图像对齐会整体偏移。文本侧要把属性和物体词做一次规范化比如统一小写、把“dark blue”这类多词属性当成一个整体 token 序列处理别拆成两个词否则图上的节点对不上。4.2 代码骨架下面是我复现时用的骨架简化过重点是数据流顺序和参数分组。import torch import torch.nn as nn import torch.nn.functional as F class GraphPrompt(nn.Module): def __init__(self, node_feats, adj, n_tokens16, dim512, hidden1024): super().__init__() # node_feats: [N, dim] 冻结的词嵌入 self.register_buffer(node_feats, node_feats) self.register_buffer(adj, adj) # 对称归一化后的邻接 self.gcn1 nn.Linear(dim, hidden) self.gcn2 nn.Linear(hidden, dim) self.fuse nn.Sequential( nn.Linear(dim * 2, hidden), nn.GELU(), nn.Linear(hidden, n_tokens * dim) ) self.n_tokens, self.dim n_tokens, dim def forward(self, attr_idx, obj_idx): h self.node_feats h F.gelu(self.gcn1(torch.spmm(self.adj, h))) h self.gcn2(torch.spmm(self.adj, h)) a, o h[attr_idx], h[obj_idx] # [B, dim] t self.fuse(torch.cat([a, o], dim-1)) # [B, L*dim] return t.view(-1, self.n_tokens, self.dim) # [B, L, dim]训练循环的关键是把软提示插到文本 token 序列里只对文本侧做一次前向。视觉侧每个 batch 只编码一次图像。def train_step(model, images, attr_idx, obj_idx, labels, optimizer): img_feat model.image_encoder(images) # 冻结 soft model.graph_prompt(attr_idx, obj_idx) # [B, L, D] txt_embed model.text_embedding(attr_idx, obj_idx) # [B, 2, D] inputs torch.cat([soft, txt_embed], dim1) # [B, L2, D] txt_feat model.text_encoder(inputs_embedsinputs) # 冻结主体 logits img_feat txt_feat.t() / model.temperature loss F.cross_entropy(logits, labels) loss loss F.cross_entropy(model.cls_head(img_feat), labels) optimizer.zero_grad(); loss.backward(); optimizer.step()邻接矩阵的构造单独写一段别混在模型里方便调边权方案。def build_adj(cooc, n_nodes, modepmi, eps1e-6): # cooc: [N, N] 共现计数前 n_attr 个是属性节点 if mode pmi: total cooc.sum() p cooc / total pa p.sum(1, keepdimTrue) pb p.sum(0, keepdimTrue) w torch.log((p eps) / (pa pb eps)).clamp(min0) else: w cooc / (cooc.sum(1, keepdimTrue) eps) a w torch.eye(n_nodes, devicew.device) d a.sum(1).pow(-0.5) return d[:, None] * a * d[None, :]4.3 超参和调参顺序参数别一次全调按这个顺序走效率最高。先定提示 token 数扫 8、16、32 三档。我实测 16 是甜点32 在小数据集上明显过拟合。再定图网络层数和隐藏维度。层数固定两层隐藏维度跟着词嵌入维度走别贪大。然后调学习率分组。提示向量用较大的值图网络用较小的值比例大概一百比一。我第一次没分组训练损失降得快但验证集调和平均一直不涨。温度系数建议先固定再放开。可学习温度在训练早期容易跑飞先冻结在默认值训五轮之后再解冻。最后调 batch 大小和训练轮数。数据集小的话 batch 不用太大轮数控制在十几轮按验证集调和平均早停。参数建议起点调整方向备注提示 token 数168 / 16 / 32超过 16 收益递减图网络层数21 / 2 / 33 层开始过平滑提示学习率较大按一个数量级扫需与图网络分组图网络学习率较小比提示小两个数量级过大易过拟合batch 大小中等受显存限制影响对比损失负样本数边权方案PMI计数 / 行归一 / PMIPMI 对长尾更友好早停指标调和平均勿用整体准确率避免已见组合主导5. 排查实录那些让实验白跑两天的坑5.1 症状与原因对照我把复现过程中遇到的和同行反馈过的问题整理成表按症状查会快很多。症状可能原因处理方式已见准确率高、未见准确率接近随机提示坍缩全局共享未生效检查提示是否随组合变化打印不同组合的提示余弦相似度训练损失正常但验证集完全不动图边权被高频节点主导换成 PMI 或行归一化检查度分布开世界指标异常高候选集没包含未见词核对划分文件与候选集构造换随机种子结果波动大提示初始化太随机用模板词嵌入初始化或加 warmup训练后期指标掉头向下过平滑或过拟合降图网络层数加早停显存爆在大 batch每步都重编码全部文本只编码 batch 内组合缓存图像特征相似度分数普遍偏低温度未校准用验证集调温度或加偏置项5.2 几个具体案例案例一提示相似度诊断。我怀疑提示坍缩时会打印同一个 batch 里所有组合的软提示两两余弦相似度。正常情况应该在零点五到零点八之间有明显区分如果全都接近零点九八说明图网络输出已经退化成一个常数向量问题多半出在过平滑或者融合层塌了。这个诊断我强烈建议加进训练脚本比看损失曲线直观得多。案例二度分布的隐蔽影响。有一次未见组合准确率怎么调都上不去最后发现是构图时把“人”这个物体的边权做得极大几乎所有属性节点的主邻居都是它。改成 PMI 之后属性节点的邻居分布明显分散未见组合的提升立刻出现。教训是建完图先画一下节点度分布长尾特别严重的图基本不可用。案例三评测口径偏差。有同学复现时用验证集上的最佳阈值去报测试集指标虚高。正确做法是每个阈值都对应一对已见/未见准确率画成曲线取曲线下面积调和平均则取曲线上使得两者均衡的那个点。这两个指标不能混着报也不能用测试集挑阈值。案例四词表对齐错位。属性词里有“dark blue”这种多词的如果做 tokenize 时被拆成两个词图上的节点索引就和文本输入对不上训练能跑但语义全乱。我的做法是先建一个词表映射文件把每个属性、物体映射到固定索引模型里所有涉及索引的地方都读这个文件杜绝手写偏移。5.3 实操心得关于参数规模别迷信“越大越好”。图网络参数量在百万级已经够表达这几个基准的关系结构再大就是给过拟合开门。关于实验节奏我习惯先在一个小数据集上把管线跑通确认提示真的随组合变化、图传播真的改变了节点表示再上大规模数据。这个顺序能省下大量等待时间。关于结果解读看到已见准确率提升时先别高兴去看未见准确率有没有同步动。如果只有已见涨说明模型在背答案不是学到了组合语义。关于复现心态这个方向的方法增益普遍不大几个点的波动很正常。差异往往来自边权方案、提示位置、评测口径这些“非核心”细节而不是架构本身。把这几处对齐了才谈得上比较。6. 这套思路还能挪到哪些场景6.1 可迁移的三类任务第一类是组合式图像检索。用户在搜索框里输入“复古风格的木质书桌”本质就是属性加物体的组合查询而且查询分布长尾严重。把属性和物体的共现关系建成图生成查询侧的结构化提示可以明显改善冷门组合的召回。我在一个小规模商品库上试过类似做法长尾查询的命中率比纯文本编码高出一截。第二类是开放词表的属性识别。检测或分割任务里经常要给区域打属性标签颜色、材质、状态训练集覆盖不全。图注入的思路可以复用节点是属性词与类别词边来自共现统计输出作为属性分类头的初始化或提示减少对稀有属性的标注依赖。第三类是多模态推荐里的标签生成。商品标题往往由品类词和修饰词拼成新品类不断出现组合爆炸。用图结构约束修饰词与品类词的搭配合理性能在不需要大量标注的前提下过滤掉“蓝色牛仔裤材质的鞋”这类语义不通的组合。6.2 后续可以往下挖的方向一个方向是把图从静态共现升级成可学习的动态图让边权随训练调整而不是一次性从统计量算死。这在数据分布偏移的场景下更稳但要小心过拟合。另一个方向是把图结构对齐到视觉侧的语义现在图完全建在文本词嵌入上如果能把图像聚类得到的视觉原型也作为节点或特征融进去属性与物体的视觉纠缠问题可能缓解得更彻底。还有一个偏工程的扩展把提示生成做成可缓存的查表。推理时组合数有限把每个组合的提示向量离线算好存下来线上只做图像编码和相似度计算延迟能压到很低。做产品落地时这一步比模型结构优化更值钱。我个人在复现这类方法时的体会是组合零样本的瓶颈很少在模型容量上更多在“关系知识有没有被正确表达”。图注入这条路子之所以有效是因为它把一个隐式的、藏在数据分布里的搭配先验变成了一个显式的、可以检查和调试的结构。你可以在图上看到哪些边不合理也可以单独验证某条边对结果的影响这种可干预性在工程上比多涨零点几个点更有价值。真要动手我的建议是先把图画对、把评测口径对齐再去动网络结构——顺序反了容易在错误的地基上雕花。

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

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

免费获取报价 →
↑