开放词表视觉 —— CLIP
一起训练一个图像编码器和一个文本编码器,让匹配的(图像,标题)对落在共享空间里的同一个点上。这就是全部诀窍。
类型: Build + Use 语言: Python 前置要求: 阶段 4 第 14 课(ViT)、阶段 4 第 17 课(自监督) 预计时间: ~45 分钟
学习目标
- 解释 CLIP 的双塔架构和对比训练目标
- 用一个预训练的 CLIP(或 SigLIP)做零样本分类,不做任何任务特定训练
- 从零实现零样本分类:编码类别 prompt、算余弦相似度、取 argmax
- 区分 CLIP、SigLIP、OpenCLIP 和 LLaVA/LLaMA-vision 模型——2026 年各自用于什么
问题背景
传统分类器是闭词表的:一个 1000 类的 ImageNet 模型只能预测 1000 个标签。每个新类别都需要带标签数据和一个重训的头。
CLIP(Radford 等人,OpenAI 2021)证明:在从网上爬来的 4 亿个(图像,标题)对上训练,产出的模型在推理时能分类到任意一组类别,这些类别纯用自然语言描述。你写一个句子就给了它一个新类别。
那个能力——零样本迁移——就是为什么每个现代视觉系统都从一个 CLIP 家族的 checkpoint 起步。检测(Grounding DINO、OWL-ViT)、分割(CLIPSeg、SAM)、检索、内容审核、VLM 和文本到图像生成,全都建在 CLIP 风格的联合嵌入之上。
核心概念
两座塔
flowchart LR
IMG["图像"] --> IENC["图像编码器<br/>(ViT-L/14)"] --> IEMB["图像嵌入<br/>(1024,)"]
TXT["标题"] --> TENC["文本编码器<br/>(transformer)"] --> TEMB["文本嵌入<br/>(1024,)"]
IEMB --> SIM["余弦相似度"]
TEMB --> SIM
style IENC fill:#dbeafe,stroke:#2563eb
style TENC fill:#fef3c7,stroke:#d97706
style SIM fill:#dcfce7,stroke:#16a34a两个编码器都以一个到相同嵌入维度的线性投影收尾(CLIP-B/32 是 512,CLIP-L/14 是 1024)。做 L2 归一化,算余弦相似度。
目标
给定一个 batch 的 N 个(图像,标题)对,构建一个 NxN 的相似度矩阵。训练两个编码器,让对角线(匹配的对)相似度高、非对角线(不匹配的)相似度低。
sim_matrix = image_embeddings @ text_embeddings.T / tau
loss_i2t = cross_entropy(sim_matrix, targets=arange(N))
loss_t2i = cross_entropy(sim_matrix.T, targets=arange(N))
loss = (loss_i2t + loss_t2i) / 2对称,因为图到文和文到图的检索都该行得通。tau(温度)通常作为一个标量参数学出来,初始化为 0.07。
SigLIP:更好的损失
SigLIP(Zhai 等人,2023)把 softmax 换成了逐对 sigmoid:
loss = 对所有对求 log(1 + exp(-y_ij * sim_ij)) 的均值
y_ij = 匹配则 +1,否则 -1逐对损失去掉了 CLIP 需要的 batch 级归一化。SigLIP 在小 batch 下训得更好,在同等数据下匹敌或超过 CLIP。
零样本分类
给定一个训练好的 CLIP:
- 为每个类别组一个 prompt:"a photo of a {class}"。
- 用文本编码器编码所有类别 prompt ->
T,形状 (C, d)。 - 编码测试图像 ->
I,形状 (1, d)。 - 相似度 =
I @ T.T,形状 (1, C)。 - Argmax -> 预测类别。
Prompt engineering 要紧。OpenAI 为 ImageNet 发布了 80 个 prompt 模板("a photo of a {}"、"a blurry photo of a {}"、"a sketch of a {}"……)。把每个类别所有模板的嵌入平均,额外多 1-3% 的 top-1 准确率。
2026 年 CLIP 风格模型用在哪
- 零样本分类 —— 直接用。
- 图像检索 —— 把所有图像编码一次,推理时嵌入查询。
- 文本条件检测 —— Grounding DINO、OWL-ViT 把一个 CLIP 文本塔包在检测器外面。
- 文本条件分割 —— CLIPSeg;SAM 通过 CLIP 使用文本 prompt 输入。
- VLM —— LLaVA、Qwen-VL、InternVL 把一个 CLIP 家族的视觉编码器接进一个 LLM。
- 文本到图像生成 —— Stable Diffusion、DALL-E 3 以 CLIP 文本嵌入为条件。
一旦你有了一个共享嵌入空间,每个视觉+语言任务都变成了一次距离计算。
clip-contrastive动手构建
第 1 步:一个微型双塔模型
真正的 CLIP 是 ViT + transformer。这一课的塔是预提取特征上的小 MLP,好让训练信号在 CPU 上看得见。
import torch
import torch.nn as nn
import torch.nn.functional as F
class TwoTower(nn.Module):
def __init__(self, img_in=128, txt_in=64, emb=64):
super().__init__()
self.image_proj = nn.Sequential(nn.Linear(img_in, 128), nn.ReLU(), nn.Linear(128, emb))
self.text_proj = nn.Sequential(nn.Linear(txt_in, 128), nn.ReLU(), nn.Linear(128, emb))
self.logit_scale = nn.Parameter(torch.ones([]) * 2.6592) # ln(1/0.07)
def forward(self, img_feats, txt_feats):
i = F.normalize(self.image_proj(img_feats), dim=-1)
t = F.normalize(self.text_proj(txt_feats), dim=-1)
return i, t, self.logit_scale.exp()两个投影、共享维度输出、学习出来的温度。和真正的 CLIP API 形状相同。
第 2 步:对比损失
def clip_loss(image_emb, text_emb, logit_scale):
N = image_emb.size(0)
sim = logit_scale * image_emb @ text_emb.T
targets = torch.arange(N, device=sim.device)
l_i = F.cross_entropy(sim, targets)
l_t = F.cross_entropy(sim.T, targets)
return (l_i + l_t) / 2对称。logit_scale 越高 = softmax 越尖锐 = 越自信但有不稳定风险。
第 3 步:零样本分类器
@torch.no_grad()
def zero_shot_classify(model, image_feats, class_text_feats, class_names):
"""
image_feats: (N, img_in)
class_text_feats: (C, txt_in) 每个类别一个平均嵌入
"""
i = F.normalize(model.image_proj(image_feats), dim=-1)
t = F.normalize(model.text_proj(class_text_feats), dim=-1)
sim = i @ t.T
pred = sim.argmax(dim=-1)
return [class_names[p] for p in pred.tolist()]每步一行。这就是配生产 CLIP checkpoint 用的精确零样本流程。
第 4 步:合理性检查
torch.manual_seed(0)
model = TwoTower()
img = torch.randn(8, 128)
txt = torch.randn(8, 64)
i, t, scale = model(img, txt)
loss = clip_loss(i, t, scale)
print(f"batch size: {i.size(0)} loss: {loss.item():.3f}")对一个随机初始化的模型,损失应该接近 log(N) = log(8) = 2.08——还没学到结构时对称交叉熵的目标值。
实际使用
2026 年 OpenCLIP 是社区默认:
import open_clip
import torch
from PIL import Image
model, _, preprocess = open_clip.create_model_and_transforms("ViT-B-32", pretrained="laion2b_s34b_b79k")
tokenizer = open_clip.get_tokenizer("ViT-B-32")
image = preprocess(Image.open("dog.jpg")).unsqueeze(0)
text = tokenizer(["a photo of a dog", "a photo of a cat", "a photo of a car"])
with torch.no_grad():
image_features = model.encode_image(image)
text_features = model.encode_text(text)
image_features = image_features / image_features.norm(dim=-1, keepdim=True)
text_features = text_features / text_features.norm(dim=-1, keepdim=True)
probs = (100.0 * image_features @ text_features.T).softmax(dim=-1)
print(probs)SigLIP 更新,在小规模上训得更好,新工作更推荐它:google/siglip-base-patch16-224。Hugging Face 两个都提供。
拿去用
这一课产出:
outputs/prompt-zero-shot-class-picker.md—— 一个 prompt,给定一份类别列表和一个领域,为零样本 CLIP 设计类别模板。outputs/skill-image-text-retriever.md—— 一个 skill,用任意 CLIP checkpoint 构建图像嵌入索引,支持按文本查询和按图像查询。
练习
- (简单) 用一个预训练的 OpenCLIP ViT-B/32,配 80 模板 prompt 集,在 CIFAR-10 上做零样本分类。报告 top-1 准确率;应该在 85-90% 左右。
- (中等) 在同样的 CIFAR-10 任务上,对比单模板("a photo of a {}")和 80 模板平均嵌入。量化差距并解释为什么模板有帮助。
- (困难) 构建一个零样本图像检索索引:用 CLIP 嵌入 1,000 张图像,建一个 FAISS 索引,用自然语言描述查询。对你手写的 20 个留出查询报告检索 recall@5。
关键术语
| 术语 | 大家嘴上怎么说 | 它实际是什么 |
|---|---|---|
| 双塔 | "双编码器" | 分开的图像和文本编码器,都以一个共享维度的投影头收尾 |
| 零样本 | "无任务特定训练" | 推理时分类到仅用文本描述的类别;不碰任何标签 |
| 温度 / logit_scale | "tau" | 学习出来的标量,在 softmax 前缩放相似度矩阵 |
| Prompt 模板 | "A photo of a {}" | 类别名外面的自然语言包装;平均多个模板提升零样本准确率 |
| CLIP | "图像+文本模型" | 2021 年的 OpenAI 模型;2026 年这个领域的通用语 |
| SigLIP | "Sigmoid CLIP" | 把 softmax 换成逐对 sigmoid;在小 batch 下训得更好 |
| OpenCLIP | "开放复现" | 社区在 LAION 上训练的 CLIP 变体;开源流水线的生产默认 |
| VLM | "视觉-语言模型" | 一个 CLIP 家族编码器加一个 LLM,被训练来回答关于图像的问题 |