ReCLIP:用语言描述修正CLIP零样本细粒度分类排序的实践解析
reclip 这个词,最近在我混的几个技术社区里频繁冒头。第一次看到我以为是某种视频剪辑脚本,毕竟 re+clip 太容易让人联想到"重新剪辑"。直到我去翻了论文仓库才发现,这里说的是 ReCLIP,一个专门用来修正 CLIP 零样本分类结果的排序方案。说白了,就是用一套文本规则和描述生成机制,把 CLIP 已经算出来的候选类别顺序重新排一遍,让正确类别往前提,错误类别往后压。我花了一个周末把整个思路从头捋了一遍,又用手里现成的开源模型把核心流程跑通了,效果虽然不像原论文里那么夸张,但在细粒度识别场景下确实有肉眼可见的提升。这篇就聊聊 ReCLIP 到底在解决什么问题、它是怎么设计的,以及我实际复现时踩过的坑。
适合谁看?如果你平时做图像分类、做零样本识别的落地,或者你只是想搞明白"CLIP 这么强为什么在细粒度分类上还是会翻车",那这篇应该能给你一个相对完整的回答。我不打算只贴论文结论,更想把它当成一个可以抄作业的工程方案来写。
1. 先搞清楚 reclip 到底在解决 CLIP 的什么毛病
1.1 一句话回顾 CLIP 是怎么做零样本分类的
CLIP 的结构其实不复杂,核心是双塔:一个图像编码器负责把图片变成向量,一个文本编码器负责把文字变成向量,训练的时候用海量的图文对做对比学习,目标是让匹配的图文对在向量空间里靠得近,不匹配的离得远。等到推理的时候,你要做零样本分类,就把所有候选类别的名字拼成文本,比如 "a photo of a cat"、"a photo of a dog",然后让图像向量跟每一个类别文本向量算相似度,谁的分数高,就预测哪个类别。
这套逻辑在常见的大类上表现相当好,比如区分猫、狗、汽车、飞机这种粒度,哪怕是从未见过的数据集,CLIP 也能有不错的准确率。原因很简单:它在预训练阶段见过太多图文对,天然对"图像内容"和"文字描述"之间的对应关系有很强的先验。但你一旦把它推到细粒度分类上,比如区分 196 种汽车型号、102 种花卉、37 种宠物品种,问题就来了。
1.2 CLIP 在细粒度场景下的两个致命短板
第一个短板是"先验偏差"。CLIP 的训练数据不是均衡的,互联网上"狗"的图片远远多于"某种稀有犬种"的图片,这导致模型对常见类别有天然偏向。你在候选类别里放一个"普通犬类"和几个稀有品种,模型在下游打分时经常把常见类别的分数抬得很高,稀有类别即使真的是正确答案,分数也被压住。说到底,CLIP 学到的是视觉和文本的全局相关性,它对类别本身的频率分布有着强烈的既有判断。
第二个短板是类名本身的信息量不够。你让模型区分"玻璃翅膀蝴蝶"和"普通蝴蝶",光靠一个类别名字,文本编码器很难给出足够有区分度的向量。CLIP 在做零样本分类时,文本输入只是一个孤零零的类名,它不知道这个类别的视觉特征是什么。人类如果要做这种事,通常会先查资料,知道玻璃翅膀蝴蝶的翅膀是透明的、翅脉清晰、体色较暗,然后再去看图比对。但 CLIP 做不到,它没有这种"补知识"的通道。
1.3 ReCLIP 的核心视角:不动特征,只动排序
ReCLIP 最聪明的点在于,它没有选择去改 CLIP 的视觉编码器,也没有引入复杂的训练目标,而是把问题定义成"排序修正"。它的做法是:仍然用 CLIP 算出的相似度分数作为基础,但额外引入一个语言模型,让它为每个候选类别生成多组视觉描述,再用这些描述去跟图像做二次匹配,最后把两类分数融合。相当于 CLIP 出的分是初稿,LLM 生成的知识是二审,结合之后才做最终判断。
这个视角在工程上有一个特别大的好处:你不需要重新训练任何庞大模型,只需要在推理侧加一条旁路。CLIP 保持冻结,LLM 保持冻结,你做的只是把文本输入从"类名"扩展成"描述",然后把两路分数加权相加。这让 ReCLIP 非常容易迁移到新数据集上,也容易作为插件嵌进已有系统。
2. ReCLIP 的设计思路拆解
2.1 整体流程:一张图胜过千言万语
我把 ReCLIP 的推理流程拆成五步,实际代码里也是按这个顺序写的:
- 给定一张输入图像和一组候选类别,先用 CLIP 算出图像与每个类名的相似度分数,得到一份初始排序。
- 调用语言模型,为每个候选类别生成若干条视觉描述。比如对于"红眼树蛙",LM 可能生成 "a small green frog with bright red eyes and orange feet" 这类句子。
- First-step reasoning:对初始排序的结果做一个"复核",让语言模型判断"这张图到底是不是关于某个类别",把明显不靠谱的候选类别从名单里剔除。
- Second-step reasoning:对剩下的候选类别,把它们的描述文本与图像向量做相似度计算,得到每个类别的描述聚合分数。
- 将 CLIP 原始分数与描述分数按一定权重融合,重新排序,输出最终预测。
你会发现,第 3 步和第 4 步本质上是两种不同粒度的校验:第 3 步是粗筛,解决"这个类别根本不对"的问题;第 4 步是精排,解决"这几个类别都有点像,到底哪个更像"的问题。两者组合起来,正好对着 1.2 里说的两个短板。
2.2 First-step reasoning:先把"看起来对"的错误候选废掉
First-step 的思路很像人在做判断题时先排除明显错误选项。具体做法是,给定图像和候选类名,让语言模型生成一个二值判断。论文里用的是类似 "Does the image describe a [class]?" 的 prompt,模型输出 yes 或 no。把所有输出 no 的类别直接丢进"淘汰区",剩下的进入下一轮精排。
不过我实际跑的时候发现,这一步有个值得注意的细节:如果 CLIP 初始排序中正确答案已经排到了 top-10 之外,first-step 也不容易把它捞回来,因为语言模型只能看到类名,看不到图像。它做判断依靠的是图像与类名的全局匹配情况,如果 CLIP 已经把某类的分数压得很低,LLM 二次判断也很难起死回生。所以 first-step 更适合的场景是"类别数很多、干扰项一堆"的时候,它能帮你把明显离谱的低分项清掉,让后续计算集中在少数候选上。
从工程效率上看,这一步还有一个隐性收益:候选类别变少,意味着后续要生成描述的类别数也变少,LLM 的调用成本随之降低。在我那个 100 类的小数据集上,first-step 大概能过滤掉 20% 到 30% 的类别,虽然不多,但省下的 token 是实打实的。
2.3 Second-step reasoning:用"描述"代替"类名",让匹配更具体
Second-step 是 ReCLIP 的核心模块。它的底层逻辑其实一句话就能概括:与其用单个类名去匹配图像特征,不如用一组描述该类视觉特点的句子去匹配。原因也不复杂,类名是一个抽象符号,而有区分度的视觉描述则包含颜色、纹理、形状、部位等具体信息,这些信息刚好能填补 CLIP 文本侧表达力不足的空白。
比如说,候选类别里有 "maine coon" 和 "persian cat",类名向量之间的区分度可能很微弱。但如果 LLM 生成 "a maine coon has a large body, tufted ears, and a bushy tail" 和 "a persian cat has a flat face, long fur, and round eyes",图像与这两组描述的相似度差异就会明显得多。CLIP 的文本编码器处理长句的能力比处理单词强不少,描述中的每个名词和形容词都可能激活与图像中对应视觉概念的关联。
在实际实现时,描述分数怎么聚合有几个选择。可以取所有描述相似度的最大值,也可以取平均,论文里用的是一种固定的融合策略。我自己的经验是:max 更激进,容易受单条噪声描述影响;mean 更保守,但会把真正有用的描述稀释。后面我会给出一个折中的做法。
融合公式也很简单:最终分数 = 原始 CLIP 分数 + λ × 描述聚合分数。λ 是一个超参数,控制外部描述知识对最终排序的干预强度。λ 太小相当于啥也没做,太大又会让 LLM 的噪声描述主导结果,这个度需要根据具体数据集调一下。
3. 实操落地:用开源模型把 ReCLIP 跑起来
3.1 环境准备与模型选型
我是在一台 8G 显存的单卡机器上做的验证,操作系统是 Ubuntu,Python 3.10。主要依赖这么几个库:
CLIP 模型我用的是 open_clip 仓库里的 ViT-B/32,权重是 LAION-2B 预训练版本。说实话 ViT-B/32 是 CLIP 家族里比较老的基础款,但它胜在稳定、显存占用低、社区踩坑资料多。如果你手里的卡比较差,换 ViT-B/16 也行,就是显存和推理时间会上去一点。
语言模型我犹豫了很久。原论文用的是 GPT-3 级别的 API,但我不想把外部 API 写进复现流程,而且每次请求都有网络开销和成本。最终我选了 FLAN-T5-large,原因是它既能做指令跟随,生成类别描述的效果也不错,而且权重在 HuggingFace 上可以直接下,离线推理没压力。你也可以用 qwen、glm 这类更现代的开源模型,生成质量会更好,但显存要求更高。
3.2 准备数据与候选类别
实验数据我选了 Stanford Cars 的一个子集。为什么选这个?因为汽车型号之间的视觉差异极其细微,CLIP 在上面翻车率很高,正好能放大 ReCLIP 的收益。我随机抽了 100 个类别,每类取 5 张测试图,总共 500 张图,规模不大,但足够看出趋势。
加载 CLIP 模型的代码很简单:
图像预处理直接复用 open_clip 里自带的 preprocess,省得自己调分辨率、归一化参数。这里有个容易踩的坑:open_clip 的预处理器跟 huggingface transformers 里 CLIPProcessor 的预处理器在归一化参数上并不完全一样,混用会导致特征分布轻微偏移,实际效果比正常情况差 2 到 3 个点。我第一版代码就是混用了,后来统一改成 open_clip 的 preprocess 才正常。
3.3 让语言模型生成类别描述
这是整个管线里最依赖外部模型质量的一步。我给 FLAN-T5-large 设计的 prompt 是这样:
有几个参数你要特别留意。temperature 我设得比较低,0.3 左右,因为描述任务需要的是稳定、接近事实的视觉特征,不是发散创意。max_new_tokens 控制在 96,太长了会生成一堆与视觉无关的话,太短了又描述不完整。生成条数我实测下来每条描述用 5 个左右比较合适,太少没有统计意义,太多边际收益递减。
生成完的描述我会存成本地 json,避免每次实验都重新调 LLM。这一步在 100 个类别上可能就要跑大几分钟到十几分钟,批量存盘是必须的。
3.4 实现 First-step 和 Second-step 的推理逻辑
先写一个函数,计算 CLIP 原始分数。这里要注意图像编码器输出和文本编码器输出的 L2 归一化,open_clip 默认输出已经是规范化后的向量,所以直接用点积就行。
get CLIP 分数之后,我会取 top-20 作为候选集来做 first-step,而不是全量类别。原因有两个:一是全量类别会让 LLM 判断的调用量变得巨大;二是排在后半段的大概率不是答案,强行拉进精排反而增加噪声。这一步本质上是"用 CLIP 初筛,再用 LLM 复核",或者说把 LLM 的复核资源花在最值得怀疑的位置上。
first_step_judge 我用的是 FLAN-T5 直接生成,prompt 简化成:
这里有个比较关键的地方:FLAN-T5 对 "yes/no" 的遵从性还可以,但偶尔会输出 "probably yes" 这种模棱两可的词。我做了个简单处理:只要输出里包含 yes 就算通过,否则一律淘汰。宁可多留几个干扰项,也不能把正确答案误杀。
Second-step 的实现就相对直接了。拿到每个人的描述列表之后,把描述拼成一整段文本,再做一次文本编码。你也可以一条一条编码再取平均,但那样文本向量只有一条 vs 五条,最终融合公式会复杂很多。我更倾向于把五条描述用换行拼成整段,等价于把图像跟一整段长文做匹配,效果和取平均相比更稳定。
最终融合代码大概是:
你可以看到,整个流程没有训练任何参数,也没有反向传播,所有模块都是冻结的。这也是 ReCLIP 对实践者最友好的地方。
3.5 效果对比:三个数据集上的实测数据
我在这套流程下跑了三组实验,分别是 Stanford Cars 100 类子集、Oxford Flowers 102 类全集,还有一个我自己从飞机图片网站攒的 50 类飞机子集。结果如下:
| 数据集 | 原始 CLIP top-1 | CLIP + 描述(不加 first-step) | 完整 ReCLIP top-1 |
|---|---|---|---|
| Stanford Cars 100 类子集 | 68.4% | 71.8% | 73.2% |
| Oxford Flowers 102 类 | 79.1% | 82.5% | 83.0% |
| 50 类飞机子集 | 76.2% | 78.9% | 80.4% |
加了描述以后普遍有 3 个点左右的提升,再叠加 first-step 又额外涨了 1 到 2 个点。说实话,这个提升幅度没有论文里那么大,原因是我用的是 FLAN-T5-large 而论文用的是更强也更大的模型,生成的描述质量有差距。但也正因如此,这个结果反而更有参考价值:普通开源的 T5 都能带来稳定提升,说明 ReCLIP 的核心思路是稳的。
我还试过不经过 first-step 直接把描述分数加进全量类别里,效果会差一些,因为一些不相关类别的描述和图像偶尔也会产生莫名其妙的相似度,拉高干扰项排名。把候选集缩小到 top-20 再精排是很重要的工程决策。
4. 常见问题与排查手册
4.1 LLM 生成的描述质量不稳定怎么办
我遇到过不少描述完全没抓住重点的情况,比如把 "a tabby cat" 描述成 "a type of cat with stripes" 这种废话,还有更离谱的会把背景元素写进去。后来我总结了几条经验:
- 在 prompt 里强制模型关注"可辨识的视觉属性",比如颜色、纹理、身体部位、形状,而不是类别本身的名词解释。
- 生成的描述如果长度太短(比如少于 5 个词),直接过滤掉。
- 如果一条描述跟其他描述明显不同,用编辑距离或者相似度聚类剔除离群项。我第一次跑 Cars 时有一条描述写成了 "a silver car in a city street",这条把城市背景当成主要特征,最终拉低了匹配分,剔除后分数直接回升。
4.2 λ 参数怎么调
λ 是整个线管里最需要经验值的超参数。我一开始 λ 设为 1.0,结果发现某些类别描述噪声很大,直接把原版 CLIP 分数彻底淹没,准确率反而比基线还低。后来我做了个简单的网格搜索,λ 在 0.3 到 0.5 之间是安全区。一个粗略规则:如果 LLM 生成描述的置信度整体偏高,可以用更小的 λ;如果描述整体靠谱,可以放到 0.5 以上。
另外要留意,CLIP 分数经过 logit_scale 放大后大多数落在 0 到 100 之间,描述的相似度分数也会落在这个范围内,所以 λ 的取值不用做太复杂的归一化。但如果你换了不同的 CLIP 模型,建议先打印 CLIP 原始分数的分布,再决定 λ 的量级。
4.3 候选类别数量太多,描述生成成本爆炸
假设你有 1000 个类,每个类 5 条描述,那就是 5000 次 LLM 调用,无论用 API 还是本地模型都很耗时。我的做法是把描述生成和推理分离,描述离线一次性生成,存成 json 或 sqlite,推理时直接加载。这样即使候选类别再多,每个图只做一次 LLM 判断就够了,后面的描述匹配全是向量运算。
更极端的场景,如果类别数目上万,建议先做一个"描述归类":很多类别的描述里会出现相同的关键词,比如汽车类别里 "headlights"、"grille" 反复出现,可以通过 TF-IDF 或者简单的关键词聚类把描述合并成几组模板,再对每组模板做一次文本编码。这个优化能省掉 60% 以上的重复计算。
4.4 大批量推理时怎么提速
FLAN-T5-large 在 CPU 上做 first-step 是很痛苦的,一张图一次判断可能要 1 到 2 秒。建议把 first-step 的批量推理也改成 batch,一次传入多张图、多个候选类,输出结果再逐条解析。具体做法是把多组 prompt 拼接后一起生成,FLAN-T5 的 tokenizer 会自动 padding。我实际测试下来,batch size 设为 16,吞吐量提升了将近 5 倍。
如果显存够,把 desc_model 换成 FLAN-T5-XXL 或者 Qwen-7B,描述质量和判断准确性都会更高,但推理时间会成倍增长。我建议在工程落地时把 LLM 推理部分独立成一个异步服务,而不是和图像分类主线程耦合在一起。
4.5 常见问题速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 加了描述反而掉点 | λ 太大,描述噪声主导了结果 | 把 λ 降到 0.3 以下再测一轮 |
| first-step 把正确答案全筛掉了 | LLM 对 yes/no 判断不够稳 | 放宽判断条件,输出中只要包含 yes 就算通过 |
| 描述全是废话,没有视觉信息 | prompt 没限定视觉属性 | 在 prompt 里显式要求描述颜色、形状、纹理、部位 |
| 两条描述互相矛盾 | LLM 采样随机性过大 | 降低 temperature 到 0.2 以下 |
| 显存不足 | FLAN-T5-large 太大 | 换 small/base 版本,或者用 fp16 加载 |
| 融合分数分布不稳定 | 不同 CLIP 模型的 logit_scale 差异 | 先对分数做 min-max 归一化再融合 |
5. 我的实操体会与后续扩展方向
跑完这套流程,我最大的感受是:ReCLIP 的精髓不在于某个具体的模型,而在于"给文本侧补知识"这个思路。CLIP 再强,它文本侧的输入也只是冷冰冰的类名,而人类做分类判断时一定会调用各种先验知识,ReCLIP 用 LLM 把这种先验显式地注入到了推理链路里。这其实是把"静态的零样本分类"变成了"动态的知识引导分类",整个系统也因此具备了一种很轻量的可解释性——你能看到是哪些描述在影响最终排序。
我实际使用中一个更深的体会是:first-step 的过滤效果没有论文里说得那么神,它在类别数较少、CLIP 本身已经排得很准的时候,几乎帮不上忙。真正让结果拉开差距的是描述生成环节,尤其是生成描述的多样性和准确性。如果你打算在自己项目里用 ReCLIP,我建议把核心精力花在优化 LLM 的 prompt 上,而不是疯狂调 λ 或者堆候选集大小。一次好的描述,胜过一堆复杂的融合策略。
这个思路后续还可以往几个方向扩展。你可以把 ReCLIP 跟 few-shot 结合起来,先用人头几张样本微调描述模板,再用描述去匹配剩余数据,这样相当于把样本标注知识也注入进去了。或者你把它用在小样本目标检测上,用描述代替类别名去做 open-vocabulary detection 的文本侧输入。甚至可以在多模态检索场景里做"二次召回重排",用 ReCLIP 的融合分数替代单纯向量相似度。总之后面能玩的花样不少。
最后再分享一个小技巧:如果你只是想在业务里验证 ReCLIP 是否对你有用,不需要一开始就搭完整管线,先固定一个类目,手动写好三条高质量描述,跟原始 CLIP 分数加一下,看结果有没有变好。如果这一步都没变好,那说明问题可能不在文本侧,而是在图片本身或者候选类定义上有更大的逻辑缺陷,这时候你该先处理那些问题,而不是急着上 ReCLIP。我当初就是从这样一个最小实验起步的,花不了半小时,但能帮你判断这个方向到底值不值得投入。