VL模型演进:从CLIP双塔到原生多模态统一架构

3 阅读13分钟

VL模型演进:从CLIP双塔到原生多模态统一架构

深度拆解Vision-Language模型的三代演进:双塔架构、交叉注意力、原生多模态统一

引言:跨越视觉与语言的鸿沟

问题的本质

人类天生就能理解图像和文字之间的关联:看到一张猫的照片,我们知道"猫"这个词指的就是它。但对机器来说,图像是像素矩阵,文字是token序列,两者存在于完全不同的"世界"。

VL模型要解决的核心问题:

如何让机器理解:
  [一张猫的照片] ≈ "一只猫""cat""小猫咪"

更进一步:
  [猫在沙发上的照片] ≈ "猫坐在沙发上"
  但 ≠ "猫在地板上"

三代演进的本质

第一代(CLIP):
  建立"翻译字典" - 图像和文本映射到同一空间
  就像学会了"这个图等于那个词"

第二代(BLIP-2):
  加入"理解对话" - 模态之间可以交互
  就像不只是查字典,还能根据图片回答问题

第三代(LLaVA):
  完全"融合思考" - 图像变成了语言的一部分
  就像把图片当作特殊的"词",直接参与推理

演进的驱动力

代际能做什么不能做什么突破点
第一代判断图文是否匹配不能生成文本、不能推理对比学习
第二代可以描述图片、回答问题架构复杂、模态融合不够深交叉注意力
第三代深度推理、多轮对话推理成本高统一架构

第一代:双塔架构 - CLIP(2021)

1.1 核心思想:建立"翻译字典"

设计哲学

CLIP的本质是建立一个"图文翻译字典":让语义相近的图像和文本在向量空间中靠近,语义不同的远离。

类比理解:双语词典

想象你在学英语:
  看到苹果图片 → 大脑记住 → "apple"这个词
  看到狗的图片 → 大脑记住 → "dog"这个词

CLIP做的就是这件事:
  训练时看4亿个图文对
  学会:[猫的图片] 和 "cat" 应该很近
       [猫的图片] 和 "dog" 应该很远

架构:两座独立的桥

        图像世界                    文本世界
            ↓                          ↓
      ┌──────────┐              ┌──────────┐
      │ 图像编码器 │              │ 文本编码器 │
      │  (ViT)   │              │  (GPT)   │
      └──────────┘              └──────────┘
            ↓                          ↓
     统一的语义空间(向量空间)
     ┌───────────────────────────┐
     │ [猫图] 靠近 "cat"          │
     │ [狗图] 靠近 "dog"          │
     │ [车图] 靠近 "car"          │
     └───────────────────────────┘

关键设计决策

决策为什么这样做代价
双塔独立图像和文本可以分别预计算,检索时只需计算相似度无法深度理解图文细节关系
对比学习不需要人工标注"这是猫/狗",只要知道图文是否匹配需要海量数据(4亿对)
大规模数据见过足够多的图文对,才能泛化到新概念训练成本极高

1.2 训练原理:对比学习

核心机制

训练目标:让匹配的图文对靠近,不匹配的远离

batch中有N个图文对:
  正例:(图1, 文本1), (图2, 文本2), ...  ← 应该相似
  负例:(图1, 文本2), (图1, 文本3), ...  ← 应该不相似

训练过程:
  1. 分别编码图像和文本 → 得到向量
  2. 计算所有图文对的相似度矩阵(N×N)
  3. 让对角线(正例)的相似度高
  4. 让非对角线(负例)的相似度低

结果:
  语义相关的图文在向量空间中自然聚集

为什么叫"对比学习"?

类比:学习外语时

传统方法(监督学习):
  老师指着猫说:"这是cat,记住!" ← 需要标注

对比学习:
  给你100个物体和100个单词,告诉你哪个配哪个
  你自己通过"对比"学会:
    - "这个毛茸茸的" 对应 "cat"
    - "这个汪汪叫的" 对应 "dog"
  ← 不需要标注具体特征

1.3 零样本能力:突破性创新

传统方法 vs CLIP

维度传统分类模型CLIP
训练时必须定义类别(猫、狗、车...)只看图文对,不需要类别
预测时只能预测训练过的类别可以预测任何用文字描述的概念
扩展性新类别 = 重新训练新类别 = 换个文字描述

零样本的本质

传统模型:
  学会了1000个固定类别的"标签"
  像背单词表,只认识背过的

CLIP:
  学会了"图像语义""文本语义"的通用映射
  像理解了语言逻辑,可以推理新词

示例:
  训练时从未见过"斑马"
  但见过:
    - [马的图片] + "horse"
    - [有条纹的东西] + "striped"

  测试时:
    给出 "a striped horse-like animal"
    CLIP能理解并匹配到斑马图片 ✓

1.4 CLIP的边界

能力边界

能做不能做本质原因
✅ 判断图文是否匹配❌ 理解细粒度关系("左边"vs"右边")双塔独立,无交互
✅ 图文检索(以图搜文/以文搜图)❌ 生成文本描述没有解码器
✅ 零样本分类❌ 复杂推理只是简单匹配

为什么会有这些局限?

双塔架构的本质:
  就像两个人各自看图片和文本,然后汇报
  "我看到的" vs "我读到的" → 比较是否一致

缺少的是:
  两个人一边看图一边讨论
  "你看这个是不是在左边?" → 细粒度交互

这些局限导致了第二代的诞生


第二代:交叉注意力 - Flamingo/BLIP系列(2022-2023)

2.1 动机:为什么需要模态交互?

CLIP的核心问题:独立编码导致信息损失

问题场景:图文细粒度理解

图片:[桌子上有苹果和香蕉,苹果在左边]

问题:"桌子上苹果在哪边?"
  正确答案:"左边"

CLIP的处理:
  图像塔:[桌子、苹果、香蕉、水果...] → 向量
  文本塔:[桌子、苹果、位置、左边...] → 向量

  问题:
    - 图像编码时不知道要关注"位置关系"
    - 文本编码时不知道图片中的具体布局
    - 两个向量点积,无法捕捉细节

解决方案:让模态之间"对话"

交叉注意力机制:

图像特征 → Query: "苹果在哪里?"
               ↓ 交叉注意力
图像特征 ← 关注到苹果和位置信息
               ↓
         生成答案:"左边"

2.2 架构演进:从双塔到交叉注意力

Flamingo (DeepMind, 2022)

┌─────────────────────────────────────────────────────┐
│  Flamingo: Few-shot Learning with Vision            │
├─────────────────────────────────────────────────────┤
│                                                     │
│  图像输入                    文本输入                 │
│  [图片1, 图片2, ...]         "描述这些图片"           │
│       ↓                           ↓                 │
│  Vision Encoder              Frozen LLM             │
│  (预训练好的ViT)              (Chinchilla 70B)      │
│       ↓                           ↓                 │
│  Perceiver Resampler    ┌─────────────────┐        │
│  (压缩视觉特征)          │   Language Model │        │
│       ↓                 │                 │        │
│  视觉tokens              │  每隔N层插入     │        │
│       └─────────────→   │  Cross-Attention │        │
│                         │                 │        │
│                         │  文本 ⟷ 视觉    │        │
│                         └─────────────────┘        │
│                                 ↓                   │
│                           生成文本输出                │
└─────────────────────────────────────────────────────┘

关键创新

  1. Perceiver Resampler:压缩视觉特征

    # 问题:ViT输出太多token(如256个)
    # 解决:用可学习的queries压缩到固定数量(如64个)
    
    visual_features = vision_encoder(image)  # (256, D)
    
    # Perceiver: 用少量queries提取关键信息
    queries = learnable_queries  # (64, D) 可学习
    compressed = cross_attention(
        query=queries,
        key=visual_features,
        value=visual_features
    )  # (64, D)
    
  2. 交叉注意力插入LLM

    # 每隔N层,在LLM中插入交叉注意力
    for layer in language_model.layers:
        # 正常的自注意力(文本内部)
        x = self_attention(x, x, x)
    
        # 每隔N层:交叉注意力(文本关注视觉)
        if layer.id % N == 0:
            x = cross_attention(
                query=x,              # 来自文本
                key=visual_tokens,    # 来自图像
                value=visual_tokens
            )
    
        x = feedforward(x)
    

BLIP-2 (Salesforce, 2023)

┌──────────────────────────────────────────────────┐
│  BLIP-2: Bootstrapping Language-Image Pretraining│
├──────────────────────────────────────────────────┤
│                                                  │
│  图像              Q-Former              LLM     │
│   ↓                  ↓                   ↓      │
│ ViT-L/14      可学习Queries           Frozen     │
│   ↓                  ↓                 OPT-6.7B  │
│ [Frozen]        32个Queries              ↓      │
│   ↓                  ↓                   ↓      │
│ 图像特征  →  交叉注意力  →  线性投影  →  文本生成 │
│  (257, D)      (32, D)      (32, D)      ↓      │
│                                        输出描述    │
└──────────────────────────────────────────────────┘

Q-Former核心思想

# Q-Former: 可学习的查询机制
class QFormer:
    def __init__(self):
        self.queries = nn.Parameter(torch.randn(32, 768))  # 32个可学习查询
        self.cross_attn_layers = nn.ModuleList([...])      # 多层交叉注意力
        self.self_attn_layers = nn.ModuleList([...])       # 自注意力

    def forward(self, image_features):
        # image_features: (257, 1408) 来自ViT

        x = self.queries  # (32, 768) 初始化

        # 多层交互
        for self_attn, cross_attn in zip(self.self_attn_layers, self.cross_attn_layers):
            # 1. Query之间自注意力(提炼信息)
            x = self_attn(x, x, x)

            # 2. Query关注图像特征(提取视觉信息)
            x = cross_attn(
                query=x,                  # (32, 768)
                key=image_features,       # (257, 1408)
                value=image_features
            )

        return x  # (32, 768) 压缩后的视觉-语言特征

为什么Q-Former有效?

传统方法:
  图像 → 257个patch tokens → 直接输入LLM
  问题:token太多,计算量大

Q-Former:
  图像 → 257个patch tokens
         ↓ 交叉注意力
      32个可学习queries(相当于问32个问题)
         ↓
      32个精炼的视觉特征 → 输入LLM

  优势:
    ✓ 大幅减少token数量(257 → 32)
    ✓ 可学习的queries自动学会提取关键信息
    ✓ LLM计算量降低8倍

2.3 训练策略:两阶段预训练

BLIP-2的训练流程

┌─────────────────────────────────────────────────┐
│  Stage 1: 视觉-语言表示学习                       │
├─────────────────────────────────────────────────┤
│  目标:让Q-Former学会提取有用的视觉信息            │
│                                                 │
│  冻结:ViT图像编码器                              │
│  训练:Q-Former                                  │
│  数据:图文对(1.29亿对)                         │
│                                                 │
│  三个损失函数:                                   │
│  1. Image-Text Contrastive (ITC)               │
│     → 图像和文本整体语义对齐                       │
│  2. Image-grounded Text Generation (ITG)        │
│     → 给定图像,生成描述                          │
│  3. Image-Text Matching (ITM)                   │
│     → 判断图文是否匹配(二分类)                   │
└─────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────┐
│  Stage 2: 视觉到语言生成对齐                      │
├─────────────────────────────────────────────────┤
│  目标:让LLM理解Q-Former输出的视觉特征            │
│                                                 │
│  冻结:ViT + LLM                                │
│  训练:Q-Former + 线性投影层                     │
│  数据:图像描述、VQA问答                          │
│                                                 │
│  损失函数:                                       │
│  - Language Modeling Loss                       │
│    → 给定图像特征,生成文本                       │
└─────────────────────────────────────────────────┘

为什么要两阶段?

Stage 1:
  让Q-Former学会"看懂"图片
  - 哪些视觉特征重要?
  - 如何压缩信息?
  - 如何与文本对齐?

Stage 2:
  让LLM学会"理解"Q-Former的输出
  - 将视觉特征映射到LLM的语言空间
  - 学会根据图像生成连贯文本

优势:
  ✓ 分阶段训练,每阶段目标明确
  ✓ 冻结大模型(ViT和LLM),降低计算成本
  ✓ 只训练Q-Former和投影层,参数少

2.4 效果提升

性能对比(Zero-shot Image Captioning)

模型COCO CIDEr参数量训练数据
CLIP-428M4亿对
Flamingo-9B84.39B未公开
Flamingo-80B93.180B未公开
BLIP-2 (OPT-2.7B)103.93.4B1.29亿对
BLIP-2 (OPT-6.7B)113.27.3B1.29亿对

关键观察

BLIP-2的优势:
  ✓ 用更少的数据(1.29亿 vs 4亿+)
  ✓ 更小的模型(7B vs 80B)
  ✓ 达到更好的效果(113.2 vs 93.1)

原因:
  1. Q-Former设计巧妙,高效压缩视觉信息
  2. 两阶段训练,目标更清晰
  3. 充分利用预训练的ViT和LLM

第二代总结

优势局限
✅ 细粒度图文理解❌ 仍需复杂的模块设计(Q-Former)
✅ 可以生成文本❌ 训练流程复杂(两阶段)
✅ 训练成本降低❌ 模态融合仍不够"原生"
✅ 效果大幅提升❌ 推理需要多个模块协作

第三代:原生多模态Transformer - LLaVA/GPT-4V(2023-至今)

3.1 核心理念:统一的多模态语言模型

问题:前两代都是"拼接"架构

第一代 CLIP:
  图像塔 + 文本塔 → 两个独立模型拼接

第二代 BLIP-2:
  ViT + Q-Former + LLM → 三个模块拼接

问题:
  - 需要精心设计连接方式
  - 训练复杂(分阶段、冻结/解冻)
  - 推理需要多个模块协作

第三代思路:把图像当作"视觉tokens"

核心洞察:
  文本是token序列:["我", "喜欢", "猫"]
  图像也可以是token序列:[patch1, patch2, ..., patch256]

那么:
  为什么不能让LLM直接处理图像tokens?

  输入:[图像tokens] + [文本tokens]
  输出:文本回答

  → 真正的"原生"多模态!

3.2 LLaVA: 视觉指令微调

架构设计(极致简洁)

┌────────────────────────────────────────────────┐
  LLaVA: Large Language and Vision Assistant    
├────────────────────────────────────────────────┤
                                                
  图像输入           文本输入                     
                                              
  ViT-L/14         Tokenizer                    
                                              
  图像features      文本tokens                   
  (256, 1024)      (L, 4096)                    
                                              
  Linear投影                                    
  (256, 4096)                                  
                                              
    └────── Concat ────┘                        
                                               
    ┌────────────────────┐                     
      LLaMA-7B/13B                           
      (统一Transformer)                       
    └────────────────────┘                     
                                               
        文本输出                                  
└────────────────────────────────────────────────┘

关键代码实现

class LLaVA(nn.Module):
    def __init__(self):
        # 1. 视觉编码器(冻结预训练ViT)
        self.vision_encoder = CLIPVisionModel.from_pretrained("openai/clip-vit-large-patch14")

        # 2. 语言模型(LLaMA)
        self.language_model = LlamaForCausalLM.from_pretrained("meta-llama/Llama-2-7b")

        # 3. 唯一新增:视觉投影层(将ViT特征投影到LLaMA空间)
        self.vision_projection = nn.Linear(1024, 4096)  # ViT维度 → LLaMA维度

    def forward(self, images, input_text):
        # 1. 编码图像
        vision_features = self.vision_encoder(images)  # (B, 256, 1024)
        vision_tokens = self.vision_projection(vision_features)  # (B, 256, 4096)

        # 2. Tokenize文本
        text_tokens = self.tokenizer(input_text)  # (B, L)
        text_embeddings = self.language_model.get_input_embeddings()(text_tokens)  # (B, L, 4096)

        # 3. 拼接视觉和文本tokens
        # 格式:[图像tokens] [文本tokens]
        inputs_embeds = torch.cat([vision_tokens, text_embeddings], dim=1)  # (B, 256+L, 4096)

        # 4. LLaMA统一处理
        outputs = self.language_model(inputs_embeds=inputs_embeds)

        return outputs

为什么这么简单就能工作?

关键1:LLaMA本身就是强大的序列建模器
  - 训练过万亿tokens
  - 可以处理任意长度的序列
  - 只要输入是"tokens",就能处理

关键2:视觉投影层是桥梁
  - 将ViT的1024维特征投影到LLaMA的4096维空间
  - 相当于"翻译":视觉语言 → LLaMA语言

关键3:指令微调数据
  - 让LLaMA学会"理解"前面256个tokens是图像
  - 通过大量图文问答数据训练

3.3 训练策略:视觉指令微调

训练数据构造

# 构造指令格式的训练数据
def create_instruction_data(image, question, answer):
    """
    输入:
      - image: 图像
      - question: "这张图片中有什么?"
      - answer: "一只猫在沙发上"

    输出:
      - 统一格式的指令数据
    """

    # 格式1:问答
    prompt = f"USER: <image>\n{question}\nASSISTANT: {answer}"

    # 格式2:描述
    prompt = f"USER: <image>\n请描述这张图片。\nASSISTANT: {answer}"

    # 格式3:推理
    prompt = f"USER: <image>\n根据图片,回答:{question}\nASSISTANT: {answer}"

    return prompt

# 训练时
image_tokens = vision_projection(vision_encoder(image))  # (256, 4096)
text_tokens = tokenizer(prompt)  # (L, 4096)

inputs = concat([image_tokens, text_tokens])  # (256+L, 4096)
outputs = language_model(inputs)

# 损失:只计算ASSISTANT回答部分
loss = cross_entropy(outputs[answer_start:], targets)

训练阶段

Stage 1: 特征对齐(~100K图文对)
  冻结:ViT + LLaMA
  训练:视觉投影层
  目标:让视觉特征对齐到LLaMA的token空间

Stage 2: 指令微调(~665K 多模态指令数据)
  冻结:ViT
  训练:LLaMA + 视觉投影层
  数据:
    - 对话:150K(LLaVA-Instruct-150K)
    - VQA:80K
    - 描述:400K
    - 推理:35K
  目标:让LLaMA学会理解视觉输入并回答问题

3.4 关键创新:GPT-4辅助数据生成

问题:如何获得高质量的多模态指令数据?

传统方法:人工标注
  成本:每条数据 $1-5
  质量:不稳定
  规模:难以扩展到百万级

LLaVA的方案:GPT-4辅助生成
  成本:API调用,约 $0.01/条
  质量:接近人类水平
  规模:可以生成百万级数据

数据生成流程

# 1. 输入:图像 + COCO标注(简单描述)
image_caption = "A cat sitting on a couch"

# 2. 让GPT-4生成多样化的问答对
prompt = f"""
给定图像描述:{image_caption}

请生成3种类型的问答对:
1. 对话型:自然的多轮对话
2. 详细描述:要求详细描述图片内容
3. 复杂推理:需要推理才能回答的问题

格式:
Q: [问题]
A: [答案]
"""

gpt4_output = openai.ChatCompletion.create(
    model="gpt-4",
    messages=[{"role": "user", "content": prompt}]
)

# 3. 得到高质量指令数据
# 输出示例:
# Q: 这张图片中的猫看起来怎么样?
# A: 猫看起来很放松,舒适地坐在沙发上,可能在休息或观察周围。

# Q: 为什么猫会选择坐在沙发上?
# A: 猫通常喜欢柔软舒适的地方休息。沙发提供了一个温暖、舒适的环境...

数据质量对比

数据来源质量成本规模多样性
人工标注⭐⭐⭐⭐⭐很高
GPT-4生成⭐⭐⭐⭐
现有数据集⭐⭐⭐免费