Series · Transfer Learning · Chapter 8

迁移学习(八):多模态迁移

推导对比学习的 InfoNCE 损失与互信息下界,讲透 CLIP 双塔结构、BLIP-2 的 Q-Former 桥接策略、跨模态检索与三种融合范式,并给出可运行的 PyTorch 实现。

为什么模型从来没见过"缅甸猫"这个标签,却能正确识别一张缅甸猫的照片?传统监督学习需要每个类别有几千上万张标注样本,而 OpenAI 在 2021 年发布的 CLIP 完全绕开了这个限制:它把图像和自然语言描述压到同一个向量空间里,“分类"就退化成"哪句话离这张图最近”——而那些"句子"是你临时写的,不是模型训练时见过的。

关键不在结构,而在监督信号。CLIP 从互联网上爬了 4 亿对(图像,alt-text),用对比学习训练一个简单约束:在每个 batch 里,图像和它真正配对的文本之间的相似度,必须比和其它文本的相似度都高。就这么一条规则,配上海量数据,足以让两个模态对齐到——下游的零样本分类、跨模态检索、图像描述、VQA——几乎都能"白嫖"。

这一篇会推导对齐背后的数学(InfoNCE 与互信息下界),讲清楚 CLIP 与它的继任者 BLIP-2,比较三类融合策略各自适用的场景,最后给一份可以跑起来的 PyTorch 实现。

你将学到什么

  • InfoNCE 损失的互信息解读,以及温度参数 $\tau$ 的作用
  • CLIP 双塔结构与零样本分类的标准协议
  • BLIP / BLIP-2:Q-Former 如何把冻结的 ViT 接到冻结的 LLM 上
  • 跨模态检索($R@K$)、图像描述、VQA、视觉定位
  • 三种融合策略:early、late、cross-attention,怎么选
  • 100 行可跑通的 CLIP 实现

前置知识

  • PyTorch 神经网络训练
  • 余弦相似度、softmax、交叉熵
  • 迁移学习基础(本系列前 6 篇)

一、CLIP:一个双塔视觉语言模型

CLIP 只有两个模块:图像编码器 $f_v$(ViT 或 ResNet)和文本编码器 $f_t$(Transformer)。两者都把输入投到同一个 $d$ 维空间,再做 L2 归一化——这样一来,相似度就是单位超球面上的点积。

CLIP 双塔结构:图像编码器与文本编码器共享一个 L2 归一化的嵌入空间,用对称 InfoNCE 训练。

CLIP 的所有威力,几乎都来自两个设计选择:

  1. 没有分类头。 模型输出的是嵌入向量,而不是某个固定类别集上的 logit。它从未承诺过"我只能识别 ImageNet 那 1000 类",因此可以被应用到任何"能写成一句话"的概念。
  2. 对称对比损失。 图→文 与 文→图 同时训练。模型学到的不是"看图说话"这种单向技能,而是双向对齐——这恰好是零样本分类和检索都需要的能力。

零样本分类的标准协议

给定一张图像和 $K$ 个候选类别:

  1. 把每个类别名套进 prompt 模板:"a photo of a {class}"
  2. 编码图像一次、编码 $K$ 个 prompt 一次。
  3. 取与图像嵌入余弦相似度最高的那个文本,对应的类别就是预测结果。

完了。目标任务上没有任何梯度下降、没有任何标注样本——只需要 $K$ 个类别名。CLIP 用这种方式在 ImageNet 上拿到了 76% 的 top-1,和在 ImageNet 1.28M 张标注图上完整训练的 ResNet-50 持平。

维度监督分类器CLIP 零样本
标注成本每张图都要标部署阶段不需要
输出空间固定 $K$ 类任意文本
新增类别需要重训或微调加一句 prompt
数据规模人工精筛互联网天然存在

二、对比学习:对齐背后的数学

一个 batch 包含 $B$ 个图文对 $\{(\mathbf{v}_i, \mathbf{t}_i)\}$,所有 $B \times B$ 两两相似度构成一个矩阵。对角线是正样本(真正配对的),非对角线是负样本(错配的)。

对比图文对齐:batch 相似度矩阵的对角线被拉高,非对角线被压低。

图→文方向的 InfoNCE 损失,就是按行做 softmax 交叉熵,目标是对角线:

$$ \mathcal{L}_{i \to t} \;=\; -\frac{1}{B}\sum_{i=1}^{B} \log \frac{\exp(\mathbf{v}_i^\top \mathbf{t}_i / \tau)}{\sum_{j=1}^{B} \exp(\mathbf{v}_i^\top \mathbf{t}_j / \tau)} $$

文→图方向 $\mathcal{L}_{t \to i}$ 是按列做的对应版本。CLIP 优化两者的对称平均 $\mathcal{L} = \tfrac{1}{2}(\mathcal{L}_{i \to t} + \mathcal{L}_{t \to i})$。

为什么这个损失"有效":互信息下界

InfoNCE 不是一个拍脑袋的设计——它是图像和文本两个模态之间互信息 $I(V; T)$ 的一个可计算下界(Oord 等人, 2018):

$$ I(V; T) \;\geq\; \log B \;-\; \mathcal{L}_{\text{InfoNCE}} $$

所以最小化损失,就是在最大化“知道图像后能减少多少关于文本的不确定性"这个量。Batch 越大,下界越紧——这正是 CLIP 把 batch size 顶到 32,768 的根本理由。

温度 $\tau$:聚焦还是发散

温度控制 softmax 的尖锐程度:

  • 小 $\tau$(约 0.01):分布尖锐,梯度集中在最难的负样本上。在网络数据这种噪声数据上,容易过拟合到错配。
  • 大 $\tau$(约 1.0):分布平缓,所有负样本贡献差不多,学习信号弱。
  • CLIP 的做法:$\tau = 0.07$,并且让它可学(参数化为 $\log(1/\tau)$ 保证为正),让模型自己找到合适的尖锐度。

Batch size 的作用

每个 anchor 在 batch 里能看到 $B - 1$ 个负样本。Batch 越大,负样本越多,互信息下界越紧,学习信号越强——直到显存吃不消为止。

Batch size备注
256学术 baseline;能跑,但收敛很慢
4,096这个量级 MoCo 风格的负样本队列开始起作用
32,768CLIP 的设置;需要超大规模分布式训练

如果 batch 涨不上去,有两条退路:MoCo 风格的负样本队列(维护一个最近样本的滚动队列)和梯度累积(只对损失累积有效,因为 softmax 的分母必须一次看全所有负样本)。


三、BLIP 与 BLIP-2:把视觉编码器接到 LLM 上

CLIP 给了你一个很好的嵌入,但它生不了文本。要做图像描述、VQA、看图回答指令这些任务,你得有一个生成头。BLIP-2(Li 等人, 2023)给出了一个特别优雅的方案。

BLIP-2 结构:一个小小的 Q-Former 桥接冻结的图像编码器和冻结的 LLM,用三个对比/匹配/生成损失共同训练。

核心思想:把贵的部分(预训练好的 ViT 和预训练好的 LLM)冻住,中间只训练一个小小的 Q-Former(约 1 亿参数)。Q-Former 是一个 Transformer,自带一组可学的 query 向量,它们通过 cross-attention 从图像特征里抽取信息,输出一段"软视觉 prompt”,让 LLM 可以直接消费。

两阶段训练

第一阶段用三个互补的损失训练 Q-Former:

  • ITC(图文对比):CLIP 风格的对齐损失,让 query 学到与文本相关的图像内容。
  • ITM(图文匹配):在 (图,文) 对上做二分类,包含难负样本。强迫模型做对比损失抓不到的细粒度匹配。
  • ITG(图像引导的文本生成):教 Q-Former 抽出"足以生成这条 caption"的视觉信息(基于视觉 query 的自回归语言建模)。

第二阶段把 Q-Former 的输出接到冻结 LLM 的输入嵌入空间,只训练那一层投影,用生成损失。LLM 已经会语言了,让它学会"读"视觉 prompt 几乎是白送的。

这个范式为什么重要

BLIP-2 是几乎所有现代视觉语言模型(LLaVA、MiniGPT-4、Qwen-VL、GPT-4V)的模板:冻结重的部分,训练薄的连接器。相比端到端从头训一个多模态模型,迁移成本能降低 1–2 个数量级。


四、跨模态检索

有了共享嵌入空间,检索就是白送的:把 query 编一次码,把数据库编一次码,按余弦相似度返回 top-$K$ 最近邻。

跨模态检索:图→文 与 文→图 都退化为共享嵌入空间里的最近邻搜索。

标准指标是 Recall@K——在前 $K$ 个结果里命中正确答案的查询比例。现代 VLM 在 MS-COCO(5K 测试图、25K caption)和 Flickr30K 上汇报 $R@1$、$R@5$、$R@10$。

双塔设计带来三个工程红利:

  1. 数据库嵌入可以预计算。 查询时只编码 query,再做 ANN 检索(FAISS / ScaNN),轻松扩展到十亿量级。
  2. 两个方向对称。 同一个索引同时支持 图→文 和 文→图。
  3. 可组合。 可以混合 query:编码一张图加上一段文本修饰词("…但是冬天的"),加权平均后再去检索。

五、跨模态对齐:耦合到什么粒度?

对齐的粒度是个设计旋钮。三种常见档位:

  • 全局对齐(CLIP、ALIGN):一图一向量,一句一向量。简单、可扩展,但抓不到细粒度的空间关系。
  • 区域对齐(OSCAR):检测出图像中的物体区域,把每个区域和 caption 里的名词短语对齐。物体标签充当锚点概念。VQA 这种"针对某个具体物体提问"的任务上更强。
  • 稠密对齐(GLIP、GroundingDINO):像素级或 token 级的对应。视觉定位(“图里哪个女人?")和开放词表检测必须用到。

联合多模态嵌入的有趣性质是:它们按语义概念聚类,而不是按模态聚类。

联合多模态嵌入的 t-SNE:相同概念的图像和文本落进同一个簇。

正是这种聚类结构让零样本迁移成为可能:一张新拍的"沙滩"图,会落到"沙滩"这个概念的附近,无论模型在训练时见没见过描述这张图的特定 caption。


六、下游任务

有了一个好的多模态编码器之后,加一个小小的头——有时候连头都不用——就能撑起一大片任务。

视觉语言下游任务:VQA、图像描述、检索、视觉定位。

任务设置代表模型
零样本分类类别名 prompt 与图像求余弦相似度CLIP、ALIGN
图文检索预计算嵌入 + ANN,看 $R@K$CLIP、ALIGN、BLIP
图像描述图像 → 自回归文本解码器BLIP-2、LLaVA
视觉问答(VQA)(图,问题) → 答案BLIP-2、LLaVA、Flamingo
视觉定位(图,指代表达) → bounding boxGLIP、GroundingDINO
开放词表检测图 + 类别 prompt → 框OWL-ViT、GLIP

七、融合策略

只要要把两个模态混到一起,就得决定在哪一层混。三种范式占了主流。

三种多模态融合策略:早期融合(拼原始特征)、晚期融合(独立编码后再合并)、深度融合(cross-attention 深层交互)。

早期融合(Early fusion):把原始特征拼起来,喂给一个统一的编码器:$\mathbf{h} = f([\mathbf{v}; \mathbf{t}])$。简单,但浪费了所有预训练好的单模态编码器。现代系统里很少见。

晚期融合(Late fusion):两个模态各自独立编码,最后再合并:$\mathbf{h} = g(f_v(\mathbf{v}), f_t(\mathbf{t}))$。CLIP 就是这一派。模块化、可扩展、适合检索,但模态之间的交互很浅——两个编码器互相"看不见”。

深度融合 / 交叉注意力(Cross-attention fusion):编码器中间穿插 cross-attention 层,让一个模态的 query 去 attend 另一个模态的 key/value:

$$ \text{CrossAttn}(\mathbf{V}, \mathbf{T}) = \text{softmax}\!\left(\frac{(\mathbf{V}\mathbf{W}_Q)(\mathbf{T}\mathbf{W}_K)^\top}{\sqrt{d}}\right) \mathbf{T}\mathbf{W}_V $$

ViLBERT、LXMERT、BLIP 都属于这一派。交互更深,在需要细粒度推理的任务(VQA、定位)上更强;但检索很慢,因为每个 (图, 文) 对都得重新前向打分。

策略复用预训练编码器交互深度检索代价
Early高但无章法
Late$O(N + M)$ 次编码后做 ANN
Cross-attention$O(N \cdot M)$——每对都要重打分

实践经验:检索是关键路径用 late(搜索、推荐),推理质量比延迟更重要用 cross-attention(VQA、描述)。


八、实现:极简 CLIP

下面是一份完整、可跑的对比学习实现。图像编码器故意做成了 stub(在伪造的 2048 维特征上跑一个小 MLP),让重点落在对比学习的核心机制上;生产环境直接换成真正的 ViT 即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F


class ImageEncoder(nn.Module):
    """ViT/ResNet 特征提取器 + 投影头的占位实现。"""

    def __init__(self, embed_dim: int = 512, in_dim: int = 2048):
        super().__init__()
        self.proj = nn.Sequential(
            nn.Linear(in_dim, 1024), nn.ReLU(),
            nn.Linear(1024, embed_dim),
        )

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        return F.normalize(self.proj(x), dim=-1)  # 投到单位超球面


class TextEncoder(nn.Module):
    """小型 Transformer 文本编码器;用 [CLS] 位置作为整句汇总。"""

    def __init__(self, vocab_size: int = 10_000, embed_dim: int = 512,
                 max_len: int = 77, n_layers: int = 6, n_heads: int = 8):
        super().__init__()
        self.tok = nn.Embedding(vocab_size, embed_dim)
        self.pos = nn.Parameter(torch.randn(max_len, embed_dim) * 0.02)
        layer = nn.TransformerEncoderLayer(
            d_model=embed_dim, nhead=n_heads,
            dim_feedforward=4 * embed_dim, batch_first=True,
        )
        self.tf = nn.TransformerEncoder(layer, num_layers=n_layers)
        self.proj = nn.Linear(embed_dim, embed_dim)

    def forward(self, tokens: torch.Tensor) -> torch.Tensor:
        x = self.tok(tokens) + self.pos[: tokens.size(1)]
        x = self.tf(x)
        return F.normalize(self.proj(x[:, 0]), dim=-1)  # CLS 池化


class CLIP(nn.Module):
    def __init__(self, embed_dim: int = 512, vocab_size: int = 10_000):
        super().__init__()
        self.image_encoder = ImageEncoder(embed_dim)
        self.text_encoder = TextEncoder(vocab_size, embed_dim)
        # 可学温度,参数化在 log 空间确保为正
        self.logit_scale = nn.Parameter(torch.tensor(np.log(1 / 0.07)))

    def forward(self, images: torch.Tensor, tokens: torch.Tensor):
        v = self.image_encoder(images)            # (B, d)
        t = self.text_encoder(tokens)             # (B, d)
        scale = self.logit_scale.exp().clamp(max=100)
        logits_i2t = scale * v @ t.t()            # (B, B)
        return logits_i2t, logits_i2t.t()


def contrastive_loss(logits_i2t: torch.Tensor,
                     logits_t2i: torch.Tensor) -> torch.Tensor:
    """对称 InfoNCE:正样本目标就是对角线索引。"""
    B = logits_i2t.size(0)
    target = torch.arange(B, device=logits_i2t.device)
    return 0.5 * (F.cross_entropy(logits_i2t, target)
                  + F.cross_entropy(logits_t2i, target))


@torch.no_grad()
def zero_shot_classify(model: CLIP, image: torch.Tensor,
                       class_token_lists: list[torch.Tensor]) -> torch.Tensor:
    """给定 K 个类别 prompt 的 token 序列,对单张图像做零样本分类。"""
    model.eval()
    v = model.image_encoder(image.unsqueeze(0))                      # (1, d)
    t = torch.stack([model.text_encoder(c.unsqueeze(0)).squeeze(0)
                     for c in class_token_lists])                    # (K, d)
    return (model.logit_scale.exp() * v @ t.t()).softmax(-1).squeeze(0)

新手最容易踩的三个坑:

  1. 点积之前一定要 L2 归一化。 否则损失会退化成"把嵌入模长做大"。
  2. logit_scale.exp() 加 clamp。 经验上封顶 100 能稳住后期训练——模型已经几乎完美对齐时,温度容易飘到极端值。
  3. 交叉熵的 target 是 arange(B) 第 $i$ 对的"标签"就是它自己的行/列下标——这就是"第 $i$ 对要和自己匹配"的实现方式。

九、常见问答

Q1:CLIP 的零样本能力到底从哪来? 三件事叠加:(i) 4 亿网络图文对覆盖了极广的概念分布;(ii) 自然语言比离散类别富得多——监督信号是一段描述,不是一个类别号;(iii) 对比学习把两个模态对齐到了同一空间,于是任何文本都能当"类别原型"用。

Q2:为什么 batch size 这么重要? 每个样本能见到 $B-1$ 个负样本,互信息下界随 $B$ 增长而变紧。Batch 低于 1024 会明显掉点,CLIP 直接干到 32,768。Batch 涨不上去就上 MoCo 队列。

Q3:CLIP 在哪些场景下不行? 计数(“几只猫?")、空间关系(“灯左边那只”)、细粒度类别(狗的品种)、图像里的文字内容、抽象概念。对比学习在带噪声的网络文本上,只奖励粗粒度的对应关系。

Q4:怎么在自己的数据上微调 CLIP? 三档:(a) 冻结 + 线性探针,标注 ≤ 1 万;(b) 在注意力层加 LoRA,1 万–10 万;(c) 小学习率全量微调,≥10 万——但要冻住温度,并用对比损失而不是标签上的交叉熵。

Q5:什么时候值得用 cross-attention? 当你需要交互——VQA、定位、多步推理。如果是大规模检索,late fusion(CLIP 风格)更划算,因为可以预计算 + ANN。

Q6:必须爬 4 亿对才能训出有用的视觉语言模型吗? 不必。BLIP-2 范式——冻结预训练视觉编码器预训练 LLM,只训练一个小连接器——在低百万级图文对上就能跑通,也是当下大多数开源 VLM 的做法。


参考文献

  • Radford, A., et al. (2021). Learning transferable visual models from natural language supervision. ICML.
  • Jia, C., et al. (2021). Scaling up visual and vision-language representation learning with noisy text supervision (ALIGN). ICML.
  • Li, J., et al. (2022). BLIP: Bootstrapping language-image pre-training. ICML.
  • Li, J., et al. (2023). BLIP-2: Bootstrapping language-image pre-training with frozen image encoders and large language models. ICML.
  • Li, X., et al. (2020). Oscar: Object-semantics aligned pre-training. ECCV.
  • Lu, J., et al. (2019). ViLBERT: Pretraining task-agnostic visiolinguistic representations. NeurIPS.
  • van den Oord, A., Li, Y., & Vinyals, O. (2018). Representation learning with contrastive predictive coding (InfoNCE). arXiv:1807.03748.

系列导航

部分主题
1基础与核心概念
2预训练与微调
3域适应
4小样本学习
5知识蒸馏
6多任务学习
7零样本学习
8多模态迁移(本文)
9参数高效微调
10持续学习
11跨语言迁移
12工业应用与最佳实践

Liked this piece?

Follow on GitHub for the next one — usually one a week.

GitHub