系列 · 迁移学习 · 第 8 篇

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

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

一个从未见过“缅甸猫”标签的模型,却能正确分类一张缅甸猫的图片。传统监督学习每个类别需要几百万张标注样本,而 OpenAI 在 2021 年发布的 CLIP 完全规避了这一限制:它将图像与自然语言描述共同映射到同一向量空间,此时“分类”即等价于从用户提供的任意候选句子中选出与该图像嵌入余弦相似度最高的一句。

其核心突破在于监督信号的设计,而非模型架构。CLIP 从网上抓取了 4 亿对(图像,alt-text)数据,训练了一个对比目标:每张图片的正确描述必须比 batch 中其他图片的描述更相似。仅靠这一简单约束和海量数据,即可实现图像与文本模态的高度对齐,从而自然支撑零样本分类、跨模态检索、图像描述生成等下游任务。

这篇文章将推导这种对齐背后的数学原理,详细讲解 CLIP 及其后续版本 BLIP-2,比较三种融合策略的适用场景,并提供一个从零开始实现的 PyTorch 版本 CLIP。


你将学到什么#

  • InfoNCE 损失如何实现互信息最大化,以及温度参数 $\tau$ 的作用
  • CLIP 的双编码器设计和零样本分类协议
  • BLIP / BLIP-2:Q-Former 如何连接冻结的 ViT 和冻结的 LLM
  • 跨模态检索($R@K$ )、图像描述生成、VQA 和视觉定位
  • 三种融合策略:early、late 和 cross-attention,分别适合什么场景
  • 一个仅 100 行的 CLIP 实现,可以直接训练

前置知识#

  • PyTorch 中的神经网络训练
  • 余弦相似度、softmax 和交叉熵
  • 迁移学习基础(第 1 至 6 篇)

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

CLIP 的结构非常简单,只有两个核心组件:图像编码器 $f_v$ (ViT 或 ResNet)和文本编码器 $f_t$ (Transformer)。它们将输入映射到一个共享的 $d$ 维空间,并进行 L2 归一化。这样一来,相似度计算就变成了单位超球面上的点积。

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

CLIP 的强大能力主要归功于两个关键设计:

  1. 没有分类头:模型输出的是嵌入向量,而非限定于固定类别集合的 logits。因此,它不受限于 ImageNet 的 1000 类等预定义范畴,可直接泛化至任意文本可描述的概念。
  2. 对称对比损失:图像到文本与文本到图像两个方向的对比学习同步进行,使模型不仅具备单向能力(如“看图说话”),还具备图像与文本间的双向语义对齐能力,这正好满足零样本分类和检索的需求。

零样本分类的标准流程#

假设有一张图片和 $K$ 个候选类别:

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

就这么简单。不需要在目标任务上做梯度下降,也不需要标注数据,只需要提供 $K$ 个类别名。CLIP 仅凭这种方法,就在 ImageNet 上达到 76% 的 top-1 准确率,性能媲美在 ImageNet 128 万张标注图像上从头训练的 ResNet-50。

维度监督分类器CLIP 零样本
标注需求每张图片都需要标注部署时无需标注
输出范围固定 $K$任意文本
新增类别成本需要重新训练或微调添加一句 prompt 即可
数据规模人工筛选互联网天然存在

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

迁移学习(八):多模态迁移 — 章节小结图

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

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

$$\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})$

为什么有效:互信息下界#

$$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 size,可采用两种替代方案: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 向量,这些向量通过交叉注意力机制提取图像特征,并生成一段固定长度的“软视觉提示”,供 LLM 使用。

两阶段训练#

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

  • ITC(图文对比):类似 CLIP 的对齐损失,让 query 学习捕捉与文本相关的图像内容。
  • ITM(图文匹配):在(图像,文本)对上做二分类,包括难负样本,弥补对比损失无法捕捉的细粒度图文匹配。
  • ITG(图像引导的文本生成):教 Q-Former 提取足够的信息来生成描述(基于视觉 query 的自回归语言建模)。

第二阶段将 Q-Former 的输出映射到冻结 LLM 的输入嵌入空间,仅微调投影层,使用生成损失。因为 LLM 已经掌握了语言能力,学会“读”视觉提示的成本非常低。

为什么这很重要#

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

四、跨模态检索#

有了共享嵌入空间,检索就变得非常简单。把查询内容编码一次,数据库内容编码一次,然后按余弦相似度返回 top-$K$ 最近邻。

跨模态检索:图→文 和 文→图 都归结为共享嵌入空间中的最近邻搜索。

标准评估指标是 Recall@K,也就是正确答案出现在前 $K$ 个结果中的查询比例。现代 VLM 在 MS-COCO(5K 测试图像、25K 标注)和 Flickr30K 上通常会报告 $R@1$$R@5$$R@10$ 的结果。

双塔设计带来了三个工程上的优势:

  1. 数据库嵌入可以提前算好:查询时只需要对 query 编码,然后用 ANN 检索工具(FAISS / ScaNN)快速找到结果,轻松支持十亿级别的数据。
  2. 模态对称:同一个索引既能处理 图→文,也能处理 文→图。
  3. 灵活组合:可以混合查询条件,比如编码一张图片,再加上一段文本修饰(“……但要冬天的”),取加权平均后再检索。

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

对齐的粒度是一个可以调节的设计参数。常见的有三种方式:

  • 全局对齐(CLIP、ALIGN):一张图片一个向量,一句话一个向量。速度快,扩展性好,但无法捕捉细粒度的空间关系。
  • 区域对齐(OSCAR):检测图片中的物体区域,将每个区域与描述中的名词短语对齐。物体标签充当锚点概念,这种对齐方式在 VQA 任务中表现更好,因为问题通常针对某个具体物体。
  • 稠密对齐(GLIP、GroundingDINO):像素级或 token 级的对应关系,视觉定位(“照片里的哪个女人?”)和开放词表检测离不开它。

联合多模态嵌入的特点是按语义概念聚类,而不是按模态分开。

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

这种聚类结构让零样本迁移成为可能。比如,一张新拍的“沙滩”图片会靠近“沙滩”这个概念,无论模型之前见过哪些具体的描述文本。

六、下游任务#

有了一个优秀的多模态编码器,只需要加一个小头——有时甚至不用加头——就能轻松应对各种任务。

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

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

七、融合策略#

把两个模态混在一起时,必须决定在哪一层融合。目前主要有三种主流方法。

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

早期融合:直接拼接原始特征,送入一个统一的编码器:$\mathbf{h} = f([\mathbf{v}; \mathbf{t}])$ 。这种方法简单粗暴,但会丢掉预训练好的单模态编码器,现代系统中很少用。

晚期融合:分别对每个模态独立编码,最后再合并结果:$\mathbf{h} = g(f_v(\mathbf{v}), f_t(\mathbf{t}))$ 。CLIP 就是这么做的。它的优点是模块化、易扩展、检索友好,但模态之间交互很浅——两个编码器互相“看不见”。

$$\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 fusion;如果推理质量比延迟更重要(比如 VQA、描述生成),就用 cross-attention

八、实现:极简 CLIP#

这是一个完整且可运行的对比学习实现。图像编码器故意设计成一个占位模块(在伪造的 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
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() 加上限,实践中封顶到 100 能稳住后期训练——模型快对齐时,温度容易飙到极端值。
  3. 交叉熵的目标是 arange(B),第 $i$ 对的“标签”就是它自己的行/列下标——这就是“第 $i$ 对匹配自己”的实现方式。

九、SigLIP 与后 CLIP 家族#

CLIP 开创了这一类模型,但远没有做到极致。2023 到 2025 年的视觉语言模型浪潮沿着三个对生产环境至关重要的方向优化了 CLIP。

SigLIP:从 softmax 到 sigmoid#

CLIP 的对比目标函数使用整个 batch 的 softmax,这让损失函数天然依赖 batch size——要获得高质量梯度,batch size 得达到几万。SigLIP(Zhai et al., 2023)用逐对的 sigmoid 损失替换了 softmax:每个(图像,文本)对独立判断是否匹配。这解耦了损失函数和 batch size 的关系,即使 batch size 只有 256,也能训练出高质量模型。同时显存占用减半,固定精度下的吞吐量大约翻倍。SigLIP-2(2025 年发布)进一步引入了 masked-prediction 和自蒸馏,缩小了与更大规模 DINOv2 类纯图像编码器的差距。

实战建议:如果在 2025 年从头训练 CLIP 类模型,直接用 SigLIP。它的配方更简单,GPU 成本更低,现成的 checkpoint(Google 提供的 siglip-so400m-patch14-384)在大多数检索和分类任务上都优于 OpenAI 原版 CLIP。

EVA-CLIP 与“谨慎放大”#

EVA-CLIP(Sun et al., 2023)证明了一件事:只要初始化得当——先用 masked image modeling 预训练,再进行 CLIP 微调——可以用十分之一的计算量追平原始 CLIP。关键在于,视觉塔需要在对比对齐之前就具备强几何特征,而不是之后。如果你有图像端预训练预算(哪怕只是在 ImageNet-1K 上跑 MAE),一定要在 CLIP 阶段之前花掉。

LongCLIP 与 77 token 的限制#

原版 CLIP 的文本编码器限制为 77 token,这对图像描述任务够用,但在文档图像检索、OCR 增强搜索和 HTML 截图检索中就是个大问题。LongCLIP(Zhang et al., 2024)通过位置嵌入插值加知识蒸馏,将上下文扩展到 248 token。任何涉及长文本或文档理解的任务,都可以换成 LongCLIP 权重——只需改一行配置,效果提升非常明显。

关于许可证#

后 CLIP 生态并非完全开放。SigLIP 权重采用 Apache 2.0 许可(Google),EVA-CLIP 权重是 MIT 许可(BAAI),但有些变体仅限研究用途(如 LiT 和 SigLIP-2 大模型遵循 Google 的许可)。如果是商业产品,在微调前务必检查许可证——这是我见过的视觉语言部署中最常见的合规问题。

十、我踩过的坑:视觉语言模型的失败模式#

视觉语言模型有一些独特的失败方式,这些在经典论文里看不到。以下是三个我亲自解决的问题。

模态偏置:模型直接忽略图像#

CLIP、BLIP,甚至 GPT-4o 风格的多模态大模型都有这个问题:当文本本身就能给出明确答案时,模型会完全无视图像。比如问“那只猫是什么颜色?”并配上一张的照片,模型很可能回答“橘色”——它从“那只猫”推断出图里一定有猫。原因很简单:预训练数据中的图文对几乎都是高度一致的,模型从未学会如何处理图文矛盾的情况。

怎么解决?推理时把图像和文本分开处理。如果是智能体系统,先跑一遍“描述你看到了什么”,再跑一遍“根据描述回答问题”。这种两阶段提示能消除偏置,代价是多调用一次模型。

BLIP-2 / LLaVA 的对象幻觉问题#

BLIP-2(以及它的后代 LLaVA)会在生成的描述中加入图像里根本不存在的对象,尤其是在少见场景中。即使是最优秀的开源模型,在 POPE 基准测试中幻觉率也高达 15%~25%。根本原因是语言模型主导了联合分布:当视觉信息模糊时,LM 就会依赖自己的先验知识补全内容。

有效的解决方法包括:对比解码(减去一个屏蔽视觉 token 的“盲”前向传播)、基于对象的提示(“先列出你能看到的所有对象,再描述它们的关系”),以及在训练时引入从目标检测错配中挖掘的硬负例。

分辨率敏感性#

大多数 CLIP 变体是在 224×224 或 384×384 分辨率下训练的,换成其他分辨率时性能会急剧下降——不仅是更小的分辨率,更大的分辨率也会出问题。直接把 1024×1024 的图像喂给一个 224 训练的 CLIP,检索得分通常比先降采样还差。高分辨率下的 patch 和学到的位置嵌入会产生混叠效应。

解决办法很简单:永远先把输入调整到模型的训练分辨率。如果确实需要高分辨率理解能力,就用专门设计的模型,比如 NaViT、Idefics2 的任意分辨率支持,或者 LLaVA-NeXT 的分块切分方法。“像素越多信息越多”这个直觉对固定分辨率的视觉 Transformer 并不适用。

最后一点思考#

CLIP 时代教会我们一件事:对比预训练可以生成极其通用的视觉表示。而后 CLIP 时代则让我们认识到,那些粗糙的边缘问题——模态偏置、幻觉、分辨率脆弱性——已经深深嵌入模型之中。如果你要在生产环境中部署视觉语言模型,工作重点不是选对模型,而是围绕已知的失败模式搭建防护措施。

十一、常见问答#

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) 数据量在 1 万到 10 万之间时,在注意力层加 LoRA;(c) 数据量 ≥ 10 万时,用小学习率全量微调,但要固定温度参数,并继续用对比损失,而不是标签上的交叉熵。

Q5:什么时候值得用 cross-attention? 当任务需要交互时,比如 VQA、定位或多步推理。如果是大规模检索,CLIP 风格的 late fusion 更高效,因为可以预计算特征并用 ANN 搜索。

Q6:必须爬 4 亿图文对才能训练出有用的视觉语言模型吗? 不需要。BLIP-2 提供了一种方法:冻结预训练的视觉编码器和预训练的 LLM,只训练一个小连接器。这种方法在低百万级图文对上就能奏效,也是目前大多数开源 VLM 的做法。

在自定义数据上微调 CLIP#

大多数 CLIP “微调”失败,根源在于选错了策略层级。有人拿着一个仅含 5K 对样本的医学影像数据集,直接对 ViT-L/14 进行全参数微调,结果发现模型在域外(OOD)任务上发生了灾难性遗忘。其实正确的做法应该是使用线性探针(linear probe)。

诊断问题始终如一:你的下游任务中究竟包含多少个不同的概念?一个 5K 规模的医学数据集通常只涵盖 20–50 种不同的放射学发现,这完全在线性分类器的能力范围内——毕竟 CLIP 嵌入维度高达 512。全参数微调会让优化器“有权”覆盖那些对你当前任务看似无用的特征,而这些特征恰恰对尚未见过的 OOD 输入至关重要。

以下是一个基于真实项目经验校准的简单决策树:

标注样本对数量推荐策略可训练参数量
< 5K冻结嵌入 + 线性探针~$d \cdot K$
5K – 100K在双塔的 attention q_projv_proj 上应用 LoRA~基模型的 0.5–2%
> 100K低学习率(1e-6 到 1e-5)全参数微调100%

这些阈值并非魔法数字,而是反映了优化问题本质的变化点。当样本少于约 5K 时,在一个 1 亿参数模型上的损失曲面会被少数高梯度样本主导,导致在特征真正适应前就已过拟合;而超过 100K 后,LoRA 的容量开始不足以表达所需的表征偏移。在这两者之间,LoRA 正好处于“甜点区”:你有足够的数据训练适配器,但又不足以安全地移动基础权重。

线性探针严格来说并不算“微调 CLIP”——你冻结两个编码器,一次性预计算嵌入,然后在其上训练一个逻辑回归模型。这种方法成本极低,且在数据稀缺时表现惊人地好。

在双编码器上使用 LoRA 需要注意一个小细节:你必须在视觉和文本两个 Transformer 中分别插入独立的 LoRA 模块,因为两种模态的表征都需要向新领域漂移。跨模态共享 LoRA 权重是一个常见错误——不同模态的特征统计特性和层数都不同。

具体选择哪些投影层进行包装也是一个关键决策。原始 LoRA 论文推荐 q_projv_proj,因为这些矩阵的更新最直接影响注意力行为。对于 CLIP 微调,额外加入 out_proj(注意力后的投影)能带来小幅增益,但扩展到 FFN 层通常无效——FFN 本身容量已很高,若再赋予可训练适配器,容易在小数据集上过拟合。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class LoRALinear(nn.Module):
    """y = W x + (B A) x, 其中 A, B 为低秩矩阵,W 冻结。"""
    def __init__(self, base: nn.Linear, r: int = 8, alpha: int = 16):
        super().__init__()
        self.base = base
        for p in self.base.parameters():
            p.requires_grad_(False)
        self.A = nn.Parameter(torch.randn(r, base.in_features) * 0.01)
        self.B = nn.Parameter(torch.zeros(base.out_features, r))
        self.scale = alpha / r

    def forward(self, x):
        return self.base(x) + self.scale * (x @ self.A.t() @ self.B.t())

def inject_lora(model: CLIP, r: int = 8):
    """在双塔每个 attention block 的 q_proj 和 v_proj 上注入 LoRA。"""
    for tower in (model.image_encoder, model.text_encoder):
        for module in tower.modules():
            if isinstance(module, nn.MultiheadAttention):
                # PyTorch 将 qkv 打包;此处为清晰起见假设 q_proj/v_proj 分离
                module.q_proj = LoRALinear(module.q_proj, r=r)
                module.v_proj = LoRALinear(module.v_proj, r=r)
    return model

一个不太明显的注意事项:保留对比损失(contrastive loss)。常见错误是直接在顶部加一个分类头,切换为交叉熵损失,把 CLIP 当作通用骨干网络。这会破坏嵌入几何结构——你将失去零样本提示、检索等最初选用 CLIP 的核心能力。如果你需要做分类,应将类别名写成文本提示,并继续以对比方式训练。

数据侧也有一个对称性错误:忘记对比微调依赖批次内隐式的负样本对。如果你的微调批次包含 32 张胸部 X 光片,全部配对同一份报告“左下肺叶实变”,那么所有非对角线项实际上都是伪装的正样本,对比损失会坍缩,模型学不到任何东西。解决方案要么确保批次内描述多样性,要么使用基于间隔的损失(如 triplet 或带显式难负样本的 InfoNCE),避免默认将非对角线视为负样本。

来自一个 5K 医学图像-报告配对项目(胸部 X 光 + 放射科报告片段)的具体数据:

设置可训练参数$R@1$ 图像→报告
零样本 OpenAI CLIP012%
线性探针~$d \cdot K$31%
LoRA r=8,双塔~1.4M47%
全参数微调428M44%(过拟合)

全参数微调这一列值得深思。若不采用激进的学习率衰减(1e-6)、基于验证集 $R@1$ 的早停,以及权重 EMA,该指标会跌至 30% 出头,所得模型在医学领域外完全不可用。即使加上所有防护措施,模型也无法再正确回答“这是一张猫的照片吗?”——如果你的产品确实不需要这个能力,那没问题;但这是一种静默的性能退化,不会体现在你的微调指标中。

在此规模下,LoRA 明显胜出,其优势正是避免了灾难性遗忘带来的代价。同样的模式在法律、卫星图像和商品目录等领域反复出现。

还有一个值得显式调优的超参:秩 $r$ 。人们常误以为“越大越好”,但在双编码器 LoRA 中,这一直觉比预期更早失效。在上述胸部 X 光数据上,$r=4$ 达到 44%,$r=8$ 到 47%,$r=16$ 到 48%,而 $r=32$ 反而因过拟合回落至 43%。原因在于:小数据集上的对比微调本质上受限于数据中的概念簇数量,而非适配器的表达能力——一旦 $r$ 超过该内在维度,就开始拟合噪声。建议从 $r=8$ 开始,仅在验证集明确指示时才调整。

将 CLIP 适配到你的领域,这是乐观的故事。悲观的一面是:正是这个便于适配的共享嵌入空间,也让对抗攻击变得异常容易。


CLIP 嵌入的对抗鲁棒性#

CLIP 的超能力也是其攻击面。共享嵌入空间意味着:攻击者只要知道文本编码器,就能写下任意目标描述,将其嵌入,然后优化图像扰动使其嵌入靠近该目标。基于 CLIP 构建的检索系统就会在匹配该描述的查询下返回被扰动的图像——而非图像的真实内容。

这种威胁模型对攻击者异常友好。在任何部署的 CLIP 检索系统中,文本编码器都是公开的部分——即使你的图像嵌入是私有的,文本编码器权重几乎肯定来自公开检查点,且攻击者的损失函数无需查询你的服务。他们可离线生成扰动,上传一次即可。相比之下,攻击黑盒监督分类器要么需要查询访问(可限流),要么依赖迁移攻击(可靠性较低)。

形式化地:给定干净图像 $x$ 、受害者图像编码器 $f_{img}$ ,以及攻击者选定的目标文本(其嵌入为 $t = f_{txt}(\text{caption})$ ),攻击者求解:

$$\min_{\delta} \; \|f_{img}(x + \delta) - t\|_2^2 \;+\; \lambda \|\delta\|_2^2 \quad \text{s.t.} \; \|\delta\|_\infty \le \epsilon$$

第一项将扰动后嵌入拉向目标描述;第二项确保扰动足够小,人类难以察觉。L-BFGS 能高效处理此问题,因为约束范围小且目标函数光滑。

目标函数的两个设计选择很关键。优先使用 $\ell_2$ 对齐项而非余弦相似度,因为 L-BFGS 更偏好光滑的二次损失曲面;而在单位超球面上,$\|f_{img}(x+\delta) - t\|_2^2 = 2 - 2 f_{img}(x+\delta)^\top t$ ,二者仅相差常数——但 $\ell_2$ 避免了 $\cos\theta = -1$ 处的三角奇异性。$\lambda \|\delta\|_2^2$ 正则项在硬 $\ell_\infty$ 投影下主要是美观作用;设为 $10^{-3}$ 通常能生成更平滑的扰动,且不影响攻击成功率。

 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
def clip_attack(model, x, target_caption_tokens, eps=8/255, lam=1e-3,
                steps=200, lr=0.01):
    """使用 L-BFGS 攻击,使 f_img(x+delta) 与目标描述嵌入对齐。"""
    model.eval()
    for p in model.parameters():
        p.requires_grad_(False)

    with torch.no_grad():
        t = model.text_encoder(target_caption_tokens.unsqueeze(0))   # (1, d)

    delta = torch.zeros_like(x, requires_grad=True)
    opt = torch.optim.LBFGS([delta], lr=lr, max_iter=steps,
                            line_search_fn='strong_wolfe')

    def closure():
        opt.zero_grad()
        v = model.image_encoder((x + delta).clamp(0, 1).unsqueeze(0))  # (1, d)
        align = ((v - t) ** 2).sum()
        reg = lam * (delta ** 2).sum()
        loss = align + reg
        loss.backward()
        return loss

    opt.step(closure)
    with torch.no_grad():
        delta.clamp_(-eps, eps)              # 投影回 L-infinity 球
    return (x + delta).clamp(0, 1).detach()

实验表明,在标准对抗预算 $\|\delta\|_\infty \le 8/255$ (肉眼不可察觉)下,该攻击在 ImageNet 上对 OpenAI 的 CLIP ViT-B/32 实现 >90% 的定向检索翻转成功率。攻击者可从数百万词汇中任选目标描述,并可靠地劫持检索结果。更糟的是,扰动具有跨模型迁移性:在 ViT-B/32 上生成的攻击对 ViT-L/14 和 SigLIP 仍保持 60–70% 成功率,因为对比学习目标在不同架构下塑造了相似的嵌入几何。

相比之下,监督基线更难攻击。相同 $\epsilon = 8/255$ 预算下,对普通 ResNet-50 的定向分类攻击成功率仅约 40%——注意这是更难的分类定向攻击,而非嵌入对齐(后者对攻击者更容易)。CLIP 的对比嵌入空间几何更平滑,这正是梯度攻击高效的原因:按设计,嵌入空间中每个方向都是可达的。

缓解措施按性价比排序:

  • 提示集成(Prompt ensembling):对 8–80 个提示模板(如“一张 {c} 的照片”、“一张模糊的 {c} 照片”……)的嵌入取平均,而非单个提示。实测可在零额外训练成本下提升 1.5–3 倍对抗鲁棒性,因为攻击者需同时对齐多个文本方向的均值。
  • 输入变换:JPEG 压缩(质量 75)、随机缩放裁剪或高斯模糊可消除大部分 $\ell_\infty$ 扰动。成本低且可叠加。但攻击者可在攻击优化时模拟相同防御流程(Expectation-over-Transformations),从而恢复大部分成功率。因此,输入变换仅能阻碍机会型攻击者,无法阻挡有动机的对手。
  • 对抗微调:在对比损失上采用 Madry 式 PGD 训练。但在 CLIP 中,此方法的权衡比监督模型更差——鲁棒性提升有限(攻击成功率仅降 ~15%),而干净数据上的零样本准确率却下降 5–10 个百分点。对比几何似乎在训练时对对抗扰动更敏感。

部署前的一个实用诊断:通过采样 $(x, x + \delta)$$\delta$ 为小随机扰动)并计算 $\|f_{img}(x + \delta) - f_{img}(x)\| / \|\delta\|$ ,估算图像编码器的Lipschitz 比率。CLIP 编码器通常在 8–15 之间;ImageNet 上的 ResNet-50 则接近 3–5。高 Lipschitz 常数正是 L-BFGS 攻击快速收敛的原因——微小输入扰动即可引发嵌入空间大幅移动。目前尚无免费解决方案;若需在同一模型中兼顾零样本泛化与对抗鲁棒性,现有方法无法满足,应考虑架构级拆分。

生产团队实际部署的分层防御方案通常包括:(i) 提示集成以提升域内鲁棒性;(ii) 上传时通过感知哈希检测已知对抗模板;(iii) 使用独立的小型“图像是否自然”分类器,识别 $\ell_\infty$ 扰动特有的频域异常特征;(iv) 对高影响力检索结果(如商业查询的首位结果)设置人工审核队列。这些措施单独都无法彻底封堵攻击,但组合起来可将攻击成本提高 2–3 个数量级,足以劝退大多数机会型滥用。

整体来看:CLIP 的开放嵌入空间既是迁移学习的利器,也是对抗鲁棒性的软肋——这一权衡是根本性的,而非可修补的漏洞。若你在内容审核、电商搜索等允许用户提交图像的场景部署 CLIP,请假设嵌入可被操控,并围绕这一假设设计系统,而非寄望于训练技巧能消除风险。

这一模式在 CLIP 之后的整个技术栈中反复出现。SigLIP 的逐对 sigmoid 损失比 softmax InfoNCE 更易受攻击,因为缺乏跨负样本的归一化竞争。BLIP-2 的冻结 Q-Former 继承了底层 ViT 的 Lipschitz 行为。LLaVA 类多模态 LLM 新增了攻击面:对抗图像可通过视觉 token 投射引导文本生成。每一项提升通用多模态推理能力的架构设计,同时也增强了对抗操控能力——并非设计者疏忽,而是这两种特性源于同一表征平滑性。坦诚地说,在当前范式下,对抗鲁棒性与零样本泛化是相互冲突的设计目标,选择其一就意味着接受另一方的折损。

参考文献#

  • 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 .
本系列

迁移学习 12 篇

  1. 01 迁移学习(一):基础与核心概念
  2. 02 迁移学习(二):预训练与微调
  3. 03 迁移学习(三):域适应
  4. 04 迁移学习(四):小样本学习
  5. 05 迁移学习(五):知识蒸馏
  6. 06 迁移学习(六):多任务学习
  7. 07 迁移学习(七):零样本学习
  8. 08 迁移学习(八):多模态迁移 当前
  9. 09 迁移学习(九):参数高效微调
  10. 10 迁移学习(十):持续学习
  11. 11 迁移学习(十一):跨语言迁移
  12. 12 迁移学习(十二):工业应用与最佳实践

读有所得?

GitHub 关注我 → 新文周更

GitHub