CLIP论文核心实验复现:零样本图像分类的泛化边界验证

CLIP论文核心实验复现:零样本图像分类的泛化边界验证

CLIP论文核心实验复现:零样本图像分类的泛化边界验证

一、CLIP的零样本能力被高估还是被低估了

CLIP(Contrastive Language-Image Pre-training)自2021年发布以来,其"零样本迁移"能力成为多模态领域讨论的焦点。官方报告在ImageNet上零样本Top-1达到76.2%,与监督训练的ResNet-50持平——这个结果一度被视为"视觉领域的GPT-3时刻"。

但在后续的社区复现中,CLIP的表现高度依赖于评估协议的具体设置:提示词模板的选择("a photo of a {class}" vs "{class}")可以带来2-3个百分点的波动;类别名称的文本预处理方式(是否对CUB-200的鸟类学名做分词优化)影响更大;甚至图像预处理(中心裁剪 vs 直接resize)都会改变排名。

本文复现CLIP的零样本分类实验,重点不在"重现76.2%"这个数字本身,而在于系统性地测量各类因素对结果的影响量级——从而为后续使用CLIP作为基线的研究提供一个可校准的评估协议。

flowchart TB A[CLIP ViT-B/32] --> B{评估变量} B --> C1[提示词模板] C1 --> C1a["a photo of {class}"] C1 --> C1b["a {class} in the wild"] C1 --> C1c[集成: 80个模板平均] B --> C2[图像预处理] C2 --> C2a[CenterCrop 224] C2 --> C2b[Resize 224] C2 --> C2c[保持原比例 + padding] B --> C3[类别名处理] C3 --> C3a[原始类别名] C3 --> C3b[同义词 + 缩写替换] C3 --> C3c[多语言类别名] C1a & C1b & C1c --> D[文本编码器] C2a & C2b & C2c --> E[图像编码器] C3a & C3b & C3c --> D D --> F[余弦相似度矩阵] E --> F F --> G[Top-1/5 准确率] G --> H[各因素影响量级量化]

二、提示词模板集成的效果分解

CLIP在训练时使用的文本是完整的自然语言描述(如"a photo of a dog, a type of animal"),而非单个标签词。因此推理时使用单个词(如"dog")会导致分布偏移——模型从未在单个词的输入上训练过。

官方使用了80个手工设计的提示词模板进行集成(ensemble):对每个类别生成80个文本描述,分别计算与图像的余弦相似度,然后取平均。这是一个有效的技巧,但需要理解每个模板的贡献并非均等。

import torch import torch.nn.functional as F from typing import List, Dict, Tuple import numpy as np # CLIP 官方使用的提示词模板(部分示例) IMAGENET_PROMPTS = [ "a photo of a {}.", "a bad photo of a {}.", "a photo of many {}.", "a sculpture of a {}.", "a photo of the hard to see {}.", "a low resolution photo of the {}.", "a rendering of a {}.", "graffiti of a {}.", "a bad photo of the {}.", "a cropped photo of the {}.", "a photo of a clean {}.", "a photo of a dirty {}.", "a dark photo of the {}.", "a photo of my {}.", "a photo of the cool {}.", "a close-up photo of a {}.", "a black and white photo of the {}.", "a painting of the {}.", "a pixelated photo of the {}.", "a bright photo of the {}.", ] def evaluate_clip_zeroshot( image_features: torch.Tensor, # [N, D] class_names: List[str], text_encoder, tokenizer, templates: List[str] = IMAGENET_PROMPTS, temperature: float = 0.01, # CLIP 训练时的温度参数 ) -> Dict[str, float]: """CLIP 零样本分类的标准评估流程。 为什么使用 temperature 参数: CLIP 训练时使用可学习的 temperature 来缩放 logits。 推理时应使用训练收敛时的 temperature 值, 否则余弦相似度的尺度与模型预期的决策边界不匹配。 """ with torch.no_grad(): # 步骤1:为每个类别生成所有模板的文本特征 text_features_all = [] for class_name in class_names: class_texts = [ template.format(class_name.replace("_", " ")) for template in templates ] # Tokenize 并编码 text_inputs = tokenizer( class_texts, return_tensors="pt", padding=True, truncation=True ) # 移到与图像特征相同的设备 if image_features.device.type != "cpu": text_inputs = { k: v.to(image_features.device) for k, v in text_inputs.items() } text_embeds = text_encoder(**text_inputs) # L2 归一化(CLIP的关键:所有特征必须归一化到单位球面) text_embeds = F.normalize(text_embeds, dim=-1) # 对该类别的所有模板取平均 text_features_all.append(text_embeds.mean(dim=0)) text_features = torch.stack(text_features_all) # [num_classes, D] # 步骤2:计算余弦相似度并缩放 image_features = F.normalize(image_features, dim=-1) logits = (image_features @ text_features.T) / temperature # 步骤3:计算 Top-K 准确率 predictions = logits.argmax(dim=-1) return { "logits": logits.cpu(), "predictions": predictions.cpu() } def analyze_template_contribution( image_features: torch.Tensor, class_names: List[str], text_encoder, tokenizer, templates: List[str] ) -> Dict[str, float]: """分析每个提示词模板对最终预测的独立贡献。 为什么需要模板级别的分析: 集成80个模板掩盖了模板质量的差异。 某些模板(如 'a bad photo of {}')在实际数据上 可能系统性地降低特定类别的相似度。 识别出低质量模板可以精简集成,减少推理计算量。 """ n_templates = len(templates) template_accuracies = {} with torch.no_grad(): text_features_per_template = [] for template in templates: class_texts = [ template.format(c.replace("_", " ")) for c in class_names ] text_inputs = tokenizer( class_texts, return_tensors="pt", padding=True, truncation=True ) if image_features.device.type != "cpu": text_inputs = {k: v.to(image_features.device) for k, v in text_inputs.items()} text_embeds = text_encoder(**text_inputs) text_embeds = F.normalize(text_embeds, dim=-1) text_features_per_template.append(text_embeds) # 评估每个模板 image_norm = F.normalize(image_features, dim=-1) for i, template in enumerate(templates): tf = text_features_per_template[i] logits = image_norm @ tf.T top1 = (logits.argmax(dim=-1) == torch.arange( len(class_names), device=logits.device )).float().mean().item() template_accuracies[template] = top1 return template_accuracies

三、分布偏移:CLIP训练数据与下游任务的隐性差距

CLIP在ImageNet上76.2%的零样本Top-1让人印象深刻——但将同样的CLIP直接应用到专业领域(如医学影像、卫星遥感、工业缺陷检测)时,性能经常暴跌到接近随机猜测。

这种分布偏移的根源在于CLIP的4亿图文对训练数据与这些专业领域的数据分布之间存在系统性差异。具体分为三类:

  1. 视觉风格偏移:CLIP训练数据是自然场景照片,专业图像(如X光片、显微镜图像)的纹理和颜色分布完全不同。
  2. 语言风格偏移:CLIP的文本描述是日常语言,而专业类别的名称通常是术语(如"adenocarcinoma")。CLIP的文本编码器无法正确理解这些术语的语义。
  3. 细粒度区分偏移:CLIP擅长区分语义上差异大的类别(狗 vs 车),但不擅长区分语义接近的细粒度类别(哈士奇 vs 阿拉斯加)。专业领域的类别区分往往是细粒度的。

对于这些场景,零样本CLIP最多只能作为弱基线,不能替代领域内的微调模型。

四、复现的关键教训

  1. 提示词模板是零样本性能的关键杠杆:不使用模板(直接用类名)比集成80个模板低3-5个百分点。
  2. 图像预处理方式需要与训练时一致:CLIP训练时使用随机裁剪,推理时使用中心裁剪——这个差异会引入不可忽视的偏移。
  3. 类别名称的文本处理影响不可忽略:下划线是否替换为空格、是否做子词拆分优化,都会影响文本编码器的输出。
  4. temperature参数必须匹配训练值:社区中一些CLIP复现使用固定的 softmax 温度(T=1)而非CLIP学习的温度,导致相似度分数不可比。

五、总结

CLIP的零样本能力是一项重要技术突破,但其泛化边界需要清醒地认知:

  1. 提示词模板集成是零样本评估的必要组件,而非可选优化。
  2. 评估协议的每个组件(模板、预处理、温度)都对最终分数有不可忽视的影响。
  3. CLIP在专业领域的零样本性能远低于自然图像领域,不能假设它"通用"。
  4. 使用CLIP作为基线时,应报告完整的评估协议,确保后续研究可以在相同条件下对比。