从零实现 CLIP 双编码器架构,掌握对比学习(InfoNCE/SigLIP)、Zero-shot 迁移和图文检索。
第2章的 ViT、MAE、DINO 解决了"看"的问题——它们能把图像编码为高质量的特征向量。但这些特征向量是"无语义的":它们知道图像包含什么,但不知道这些东西叫什么名字,更不知道如何与人类的语言指令对应起来。
对于 VLA 自动驾驶,这是一个致命问题:
- 指令是语言的("在下一个路口右转")
- 感知是视觉的(摄像头图像)
- 规划需要连接两者
CLIP 解决了这个 gap:将视觉和语言映射到同一个语义空间。
CLIP (Radford et al., 2021) 的核心思想极其简单但强大:
1. 收集 4 亿对(图像,文本)数据
2. 训练两个编码器分别处理图像和文本
3. 让匹配的图文对的嵌入向量尽量接近,不匹配的尽量远离
就这么简单。但效果惊人:训练好的模型可以直接用于分类、检索、检测等任务,完全不需要微调。
第1章: Transformer + GPT(语言能力)
第2章: ViT + MAE + DINO(视觉能力)
第3章: CLIP + SigLIP(视觉-语言对齐)← 当前
第4章: LLaVA(多模态对话)
第14章: VLA(视觉-语言-行动)
本章是连接"纯视觉"和"多模态"的关键桥梁。
图像 (224×224) 文本 "a cat sitting on..."
│ │
ViT Encoder Text Transformer
(12 layers) (12 layers, causal)
│ │
[CLS] token [EOS] token
│ │
Linear Projection Linear Projection
│ │
L2 Normalize L2 Normalize
│ │
image_features (512-d) text_features (512-d)
│ │
└──────────┬───────────────────┘
│
Cosine Similarity
× exp(logit_scale)
│
Similarity Matrix (B×B)
CLIP 的视觉编码器与标准 ViT 几乎相同,但有两点关键差异:
1. 更大的 patch size:ViT-B/32 使用 32×32 的 patch(而非 16×16),为了处理 4 亿张图像时的训练效率
2. 输出是归一化嵌入:最后有一个投影层将 [CLS] token 映射到共享空间并 L2 归一化
class CLIPVisionEncoder(nn.Module):
def forward(self, x):
x = self.patch_embed(x) # (B, 3, H, W) → (B, N, D)
x = x + self.pos_embed
for block in self.blocks:
x = block(x)
x = x[:, 0] # [CLS] token
x = self.visual_projection(x) # → projection_dim
x = F.normalize(x, dim=-1) # L2 normalize
return x
CLIP 的文本编码器是一个 GPT-2 风格的因果 Transformer。关键设计选择:
- 使用因果掩码:虽然不需要生成文本,但因果注意力保留了文本的序列结构
- 取 EOS token 的输出:EOS token 能"看到"所有前文,其输出是整个文本的摘要
- max_seq_len=77:这个数字来自对 4 亿条文本的统计
两个编码器的输出维度可能不同(视觉 768,文本 512),需要通过线性投影映射到同一个维度(通常 512 或 768):
self.visual_projection = nn.Linear(vision_dim, projection_dim, bias=False)
self.text_projection = nn.Linear(text_dim, projection_dim, bias=False)
投影之后再 L2 归一化,确保所有嵌入都在单位超球面上。
为什么不训练一个统一的编码器同时处理图像和文本?因为:
- 图像和文本的数据模态完全不同(像素 vs 离散 token)
- 需要不同的处理方式(卷积/patch vs embedding lookup)
- 双编码器更灵活:可以离线预计算图像库的嵌入
给定一个 batch 的 B 对(图像,文本),构建 B×B 的相似度矩阵:
文本1 文本2 文本3 文本4
图像1 ✓ ✗ ✗ ✗ ← 图像1 应该最匹配 文本1
图像2 ✗ ✓ ✗ ✗
图像3 ✗ ✗ ✓ ✗
图像4 ✗ ✗ ✗ ✓
- 对角线(i=j)是正样本对:应该最大化相似度
- 非对角线(i≠j)是负样本对:应该最小化相似度
对于图像 i,它与文本 j 的匹配概率为:
$$P(j|i) = \frac{\exp(S_{ij}/\tau)}{\sum_k \exp(S_{ik}/\tau)}$$
其中 $S_{ij} = \text{cosine\_sim}(I_i, T_j)$,$\tau$ 是温度参数。
InfoNCE 损失是图像→文本 和 文本→图像 两个方向的交叉熵:
$$\mathcal{L} = \frac{1}{2B}\sum_i \left[-\log P(i|i) - \log P(i|i)_{\text{txt}\to\text{img}}\right]$$
直觉上,好的对齐应该是对称的:
- 给出图像,能准确找到对应的文本
- 给出文本,也能准确找到对应的图像
对称损失强制模型在两个方向上都做好。
def infonce_loss(image_features, text_features, logit_scale):
# 相似度矩阵
logits = logit_scale * image_features @ text_features.t()
# 标签:对角线位置
labels = torch.arange(B)
# 双向损失
loss_img = F.cross_entropy(logits, labels) # 图像→文本
loss_txt = F.cross_entropy(logits.t(), labels) # 文本→图像
return (loss_img + loss_txt) / 2
温度 $\tau = 1/\text{logit\_scale}$ 控制 softmax 的"锐度":
- 低温(τ→0):softmax 变成 one-hot → 模型只关注最相似的样本
- 高温(τ→1):softmax 变平 → 所有样本获得相似的梯度
CLIP 将 logit_scale 设为可学习参数,初始值对应 τ=0.07(经验最优值)。
InfoNCE 是 B-way 的 softmax 分类,需要 batch 内有足够的负样本。当 B 较小时(比如训练资源有限),效果会显著下降。
OpenAI 的训练用了 batch_size=32768,这对大多数团队来说完全不可行。
SigLIP (Zhai et al., 2023) 将问题重新定义为 B² 个独立的二分类问题:
对于每个 (i, j) 对:
- 如果 i=j(匹配对),标签 y=1
- 如果 i≠j(不匹配对),标签 y=-1
损失函数:
$$\mathcal{L} = -\frac{1}{B^2}\sum_{i,j} \log\sigma(y_{ij} \cdot (S_{ij} + b))$$
其中 σ 是 sigmoid 函数,b 是可学习的偏置。
| 特性 | InfoNCE | SigLIP |
|---|---|---|
| 损失形式 | B-way softmax | B² 个二分类 |
| batch size 依赖 | 强(需要大 B) | 弱 |
| 训练稳定性 | 中等 | 高 |
| 计算复杂度 | O(B²D) | O(B²D) |
| 额外参数 | logit_scale | logit_scale + logit_bias |
| VLA 中的使用 | 早期模型 | 当前主流(OpenVLA 等) |
def siglip_loss(image_features, text_features, logit_scale, logit_bias):
# 相似度矩阵
logits = logit_scale * image_features @ text_features.t() + logit_bias
# 标签:1(匹配)或 -1(不匹配)
labels = 2 * torch.eye(B) - 1
# Sigmoid loss
loss = -F.logsigmoid(labels * logits).mean()
return loss
在 InfoNCE 中,负样本通过 softmax 分母中的 Σ 竞争。如果负样本太少,这个竞争不充分。
在 SigLIP 中,每个 (i, j) 对是独立判断的,不需要与其他对的比较。因此 batch size 小也不影响单个对的判定质量。
CLIP 的训练数据规模是前所未有的:
- 4 亿(图像,文本)对
- 从互联网收集(alt-text、社交媒体等)
- 包含极其多样的视觉概念
对于自动驾驶场景的 CLIP 微调:
- 可以使用驾驶视频 + 对应的语音/文字指令
- 数据量级远小于 OpenAI,但可以用 SigLIP 弥补
大 batch size:
- 对比学习天然需要负样本多样性
- 如果 batch size 不够 → 用梯度累积或 SigLIP
参数分组 weight decay:
# bias 和 norm 参数不做 decay
decay_params = [p for n, p in model.named_parameters()
if "bias" not in n and "norm" not in n.lower()]
no_decay_params = [p for n, p in model.named_parameters()
if "bias" in n or "norm" in n.lower()]
optimizer = AdamW([{"params": decay_params, "weight_decay": 0.2},
{"params": no_decay_params, "weight_decay": 0.0}])
学习率调度:
- Cosine annealing 从初始 lr 到 0
- 必要时加 warmup(前几百步线性增长)
训练中需要关注的指标:
- Loss:InfoNCE 应该从 ~ln(B) ≈ 4-5 开始,逐渐下降
- Image Accuracy:图像→文本的 top-1 匹配率(in-batch)
- Text Accuracy:文本→图像的 top-1 匹配率
- Temperature:logit_scale 的倒数,通常在训练中逐渐减小
训练好的 CLIP 模型天然支持双向检索:
文本→图像:"找所有包含行人的场景"
text_features = clip.encode_text("a street with pedestrians")
similarities = text_features @ image_database.t()
top_matches = similarities.topk(k=5)
图像→文本:给定一张图像,找最匹配的描述
image_features = clip.encode_image(query_image)
similarities = image_features @ text_database.t()
best_caption = text_database[similarities.argmax()]
场景挖掘:
"a rainy night with heavy traffic and pedestrians crossing"
→ 找到所有类似的 corner case 场景
异常检测:
"a normal highway in clear weather"
→ 匹配度低于阈值?可能是异常场景
数据整理:
为每条驾驶指令自动匹配最相关的图像帧
→ 减少人工标注工作量
训练数据构建:
从海量驾驶日志中筛选出符合特定模式(变道、转弯、跟车)的片段
当图像数据库很大(百万级)时,暴力计算所有相似度不可行。实际使用:
- FAISS(Facebook AI Similarity Search):高效的向量检索
- 分层索引:粗筛 → 精排
- 离线预计算:图像嵌入提前算好存储
不需要任何训练数据,不需要微调,直接用文本描述来做图像分类。
输入图像 → 编码为嵌入向量
类别列表 → 编码为嵌入向量
计算相似度 → 最高分的类别就是预测
def zero_shot_classify(images, class_names, clip_model, tokenizer):
# 1. 将类别名转为 CLIP 风格的 prompt
class_texts = [f"a photo of a {name}" for name in class_names]
# 2. 编码图像和文本
image_features = clip_model.encode_image(images)
text_features = clip_model.encode_text(tokenizer(class_texts))
# 3. 计算相似度并 softmax
logits = image_features @ text_features.t()
probs = softmax(logits)
return probs
CLIP 对 prompt 很敏感。同样的类别,不同的 prompt 可以带来显著的准确率提升:
| Prompt | ImageNet 准确率 |
|---|---|
| "cat" | ~55% |
| "a photo of a cat" | ~63% |
| "a photo of a cat, a type of animal" | ~65% |
这就是为什么 CLIP 论文中使用 80 个不同的 prompt 模板做集成。
CLIP zero-shot 在驾驶场景中的应用:
- 未见过的交通标志:只要有文本描述就能识别
- 异常天气分类:"heavy snow","dense fog"
- 驾驶场景分类:"urban intersection","highway merge"
- 危险情况检测:"pedestrian running across street","vehicle running red light"
1. 缺乏细粒度理解:能判断"这是猫",但不能数"图里有几只猫"
2. 空间关系弱:不能区分"猫在桌上"和"桌在猫上"
3. 幻觉:会在完全无关的图像中"看到"提到的物体
4. 文本能力有限:文本编码器很小,不能进行复杂推理
CLIP 是一个"看+读"的模型,但不能"说"。后续工作将 CLIP 的视觉编码器接入 LLM:
- BLIP-2:用 Q-Former 桥接 CLIP 视觉编码器和 LLM
- LLaVA:直接用线性投影连接 CLIP 视觉编码器和 LLM
- MiniGPT-4:类似 BLIP-2,但训练更简单
这就是第4章的内容。
现代 VLA 模型的典型架构:
图像 → CLIP/SigLIP 视觉编码器 → 视觉 tokens
↓
视觉-语言投影
↓
语言指令 → Tokenizer → ────→ LLM → 行动 token
CLIP 的视觉编码器(特别是 SigLIP 版本)已经经过大规模图文对齐训练,提供的视觉特征天然适合与语言指令融合。
OpenVLA、RT-2 等主流 VLA 模型都使用 SigLIP 作为视觉 backbone。
本章建立的核心能力:
1. ✅ 双编码器架构:视觉+文本独立编码,投影到共享空间
2. ✅ InfoNCE 损失:B-way 对比分类,对角线是正样本
3. ✅ SigLIP 损失:B² 个独立二分类,不依赖大 batch
4. ✅ 可学习温度:logit_scale 自适应调整 softmax 锐度
5. ✅ 图文检索:双向 Top-K 最近邻搜索
6. ✅ Zero-shot 分类:文本描述 → 图像分类,无需微调
7. ✅ CLIP 训练器:参数分组 decay、余弦调度、梯度裁剪
1. 运行 01_clip_model.py,观察相似度矩阵和 loss 值
2. 运行 02_training_retrieval.py,理解训练循环和检索流程
3. 尝试修改 logit_scale 的初始值,观察对 loss 的影响
4. 比较 InfoNCE 和 SigLIP 在不同 batch size 下的表现差异
5. 思考:如果要为自动驾驶构建 CLIP 训练数据,你会如何收集?
下一章:多模态大语言模型 (LLaVA) — 让模型"看图说话"