跳转至

第 13 章 多模态学习

📌 本章定位视觉任务中的实际应用

本章侧重多模态学习在计算机视觉任务中的实际应用,包括: - CLIP 在视觉任务中的零样本分类与检索应用 - 前沿 VLM 架构对比( LLaVA 、 InternVL 、 Qwen-VL 等) - 多模态 RAG 、 VQA 、图文检索等实际应用场景 - 实战项目与模型部署

🔗 相关章节导航: | 侧重点 | 章节 | 说明 | |--------|------|------| | CV 应用 | 👉 本文档 | VLM 架构对比、实战项目、部署应用 | | 理论原理 | 深度学习/07-多模态学习 | 数学推导、算法原理、融合策略理论 | | 大模型 | LLM 学习/多模态大模型 | 闭源通用多模态模型的能力演进与工程取舍 | | 具身智能 | 具身智能/VLA 模型 | 视觉-语言-动作模型 |

⚠️ 时效性说明:本章涉及前沿模型/价格/榜单等信息,可能随版本快速变化;请以论文原文、官方发布页和 API 文档为准。

📚 章节概述

本章深入讲解多模态学习的核心技术,从经典 CLIP 到近年的视觉语言大模型( VLM ),覆盖 BLIP-2 、 LLaVA 、 InternVL 、 Qwen-VL 等代表性模型的架构对比与实战应用。多模态学习是当前 AI 研究和应用都高度活跃的方向之一。

学习时间: 7-10 天 难度等级:⭐⭐⭐⭐⭐ 前置知识:第 5-6 章( CNN 基础)、第 12 章(视觉 Transformer )、 NLP 基础

🎯 学习目标

完成本章后,你将能够: - 理解多模态融合策略( Early/Late/Cross-Attention ) - 掌握 CLIP 原理与 InfoNCE Loss 推导 - 了解 SigLIP 、 EVA-CLIP 等 CLIP 改进方案 - 深入理解 BLIP-2 的 Q-Former 架构与三阶段训练 - 对比 LLaVA/InternVL/Qwen-VL 等 VLM 的架构差异 - 完成多模态应用项目( VQA 、图文检索、多模态 RAG )


13.1 多模态学习概述

13.1.1 多模态数据类型

模态 数据形式 代表模型
视觉+文本 图像/视频+自然语言 CLIP 、 BLIP-2 、 LLaVA
视觉+音频 视频+语音/音乐 ImageBind
文本+表格 文档+结构化数据 TableGPT
3D+文本 点云/Mesh+语言 PointBind

13.1.2 多模态融合策略

Text Only
┌─────────────────────────────────────────────────────────────────┐
│                    三种融合策略对比                                │
├─────────────────┬─────────────────┬───────────────────────────┤
│  Early Fusion    │  Late Fusion    │  Cross-Attention Fusion   │
│  原始特征拼接     │  各自编码后合并  │  深度交互注意力            │
│                  │                 │                           │
│  [img]+[txt]     │  f(img) ⊕ f(txt)│  Q=img, K=V=txt          │
│     ↓            │      ↓          │       ↓                   │
│  Encoder         │   Classifier    │  Cross-Attn Layers        │
│                  │                 │                           │
│  优点:深度交互   │  优点:灵活独立  │  优点:动态对齐            │
│  缺点:计算量大   │  缺点:交互浅   │  缺点:计算复杂            │
│  代表:ViLBERT   │  代表:CLIP     │  代表:Flamingo/BLIP-2    │
└─────────────────┴─────────────────┴───────────────────────────┘

13.1.3 多模态学习范式演进

Text Only
                    多模态学习范式演进
时间 ──────────────────────────────────────────────→

2021     2022        2023         2024        2025
CLIP  →  BLIP    →   BLIP-2   →   LLaVA-1.5  → Qwen2-VL
         CoCa        InstructBLIP   InternVL     InternVL2.5
                                    Qwen-VL      闭源VLM

范式:  对比学习 → 生成式预训练 → 冻结LLM+桥接 → 端到端VLM

📝 复盘提示:多模态融合有哪些策略、各自优缺点是什么,以及 CLIP 属于哪一类,是读懂后续 VLM 架构的基础。


13.2 CLIP 深度解析

13.2.1 对比学习原理

CLIP ( Contrastive Language-Image Pre-training )是 OpenAI 在 2021 年提出的跨模态对比学习模型。

InfoNCE Loss 推导

给定一个 batch 中的 \(N\) 个图文对 \((I_i, T_i)\),目标是让匹配的图文对相似度高,不匹配的相似度低。

图像到文本方向的损失:

\[\mathcal{L}_{I \to T} = -\frac{1}{N}\sum_{i=1}^{N} \log \frac{\exp(\text{sim}(z_i^I, z_i^T) / \tau)}{\sum_{j=1}^{N} \exp(\text{sim}(z_i^I, z_j^T) / \tau)}\]

其中 \(\text{sim}(a, b) = \frac{a \cdot b}{\|a\| \|b\|}\) 为余弦相似度,\(\tau\) 为可学习温度参数。

文本到图像方向的损失类似:

\[\mathcal{L}_{T \to I} = -\frac{1}{N}\sum_{i=1}^{N} \log \frac{\exp(\text{sim}(z_i^T, z_i^I) / \tau)}{\sum_{j=1}^{N} \exp(\text{sim}(z_i^T, z_j^I) / \tau)}\]

总损失为对称形式:

\[\mathcal{L} = \frac{1}{2}(\mathcal{L}_{I \to T} + \mathcal{L}_{T \to I})\]

温度参数τ的作用: - \(\tau\) 越小 → softmax 分布越尖锐 → 模型更关注最难的负样本 - \(\tau\) 越大 → softmax 分布越平滑 → 最终梯度更均匀 - CLIP 中 \(\tau\) 是可学习参数,初始值为 \(1/0.07 \approx 14.3\)

13.2.2 CLIP 架构

Text Only
┌──────────────────────────────────────────────────┐
│                CLIP 架构                          │
│                                                  │
│  Image ──→ [Vision Encoder] ──→ Projection       │
│              (ViT-B/32)          ──→ z_I         │
│                                     ↕ 余弦相似度  │
│  Text  ──→ [Text Encoder]  ──→ Projection        │
│              (Transformer)       ──→ z_T         │
│                                                  │
│  训练数据:4亿图文对(WebImageText)                │
│  训练方式:对比学习(InfoNCE)                      │
│  推理方式:图像特征 vs 文本模板 → 零样本分类         │
└──────────────────────────────────────────────────┘

关键设计: - 双塔架构:图文编码器完全独立,推理时可以分别编码 - L2 归一化:特征投影后进行 L2 归一化,使相似度范围为[-1, 1] - Prompt 设计:推理时使用"a photo of a {class}"作为文本模板

13.2.3 CLIP 完整实现

Python
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np

class VisionEncoder(nn.Module):  # 继承nn.Module定义网络层
    """简化的Vision Transformer编码器"""
    def __init__(self, image_size=224, patch_size=32, dim=768, depth=12, heads=12):
        super().__init__()  # super()调用父类方法
        num_patches = (image_size // patch_size) ** 2
        self.patch_embed = nn.Conv2d(3, dim, patch_size, patch_size)
        self.cls_token = nn.Parameter(torch.randn(1, 1, dim))
        self.pos_embed = nn.Parameter(torch.randn(1, num_patches + 1, dim))

        encoder_layer = nn.TransformerEncoderLayer(
            d_model=dim, nhead=heads, dim_feedforward=dim * 4,
            batch_first=True, norm_first=True
        )
        self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=depth)
        self.ln = nn.LayerNorm(dim)

    def forward(self, x):
        # x: [B, 3, H, W]
        x = self.patch_embed(x)          # [B, dim, H/P, W/P]
        x = x.flatten(2).transpose(1, 2) # [B, num_patches, dim]
        cls = self.cls_token.expand(x.size(0), -1, -1)
        x = torch.cat([cls, x], dim=1)   # [B, num_patches+1, dim]  # torch.cat沿已有维度拼接张量
        x = x + self.pos_embed
        x = self.transformer(x)
        x = self.ln(x[:, 0])             # 取CLS token
        return x

class TextEncoder(nn.Module):
    """简化的文本编码器"""
    def __init__(self, vocab_size=49408, dim=512, depth=12, heads=8, max_len=77):
        super().__init__()
        self.token_embed = nn.Embedding(vocab_size, dim)
        self.pos_embed = nn.Parameter(torch.randn(1, max_len, dim))

        encoder_layer = nn.TransformerEncoderLayer(
            d_model=dim, nhead=heads, dim_feedforward=dim * 4,
            batch_first=True, norm_first=True
        )
        self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=depth)
        self.ln = nn.LayerNorm(dim)

    def forward(self, tokens):
        x = self.token_embed(tokens) + self.pos_embed[:, :tokens.size(1)]
        x = self.transformer(x)
        # 取EOS token位置(最后一个非padding token)
        x = self.ln(x[torch.arange(x.size(0)), tokens.argmax(dim=-1)])
        return x

class CLIP(nn.Module):
    """完整CLIP模型"""
    def __init__(self, embed_dim=512, vision_dim=768, text_dim=512):
        super().__init__()
        self.visual = VisionEncoder(dim=vision_dim)
        self.text = TextEncoder(dim=text_dim)

        # 投影到共享嵌入空间
        self.image_projection = nn.Linear(vision_dim, embed_dim, bias=False)
        self.text_projection = nn.Linear(text_dim, embed_dim, bias=False)

        # 可学习温度参数(log scale)
        self.logit_scale = nn.Parameter(torch.ones([]) * np.log(1 / 0.07))

    def encode_image(self, image):
        """编码图像为归一化特征"""
        feat = self.visual(image)
        proj = self.image_projection(feat)
        return F.normalize(proj, dim=-1)  # L2归一化

    def encode_text(self, text):
        """编码文本为归一化特征"""
        feat = self.text(text)
        proj = self.text_projection(feat)
        return F.normalize(proj, dim=-1)

    def forward(self, image, text):
        image_features = self.encode_image(image)
        text_features = self.encode_text(text)

        # 缩放余弦相似度
        logit_scale = self.logit_scale.exp()
        logits_per_image = logit_scale * image_features @ text_features.t()
        logits_per_text = logits_per_image.t()

        return logits_per_image, logits_per_text

def clip_loss(logits_per_image, logits_per_text):
    """对称InfoNCE损失"""
    batch_size = logits_per_image.size(0)
    labels = torch.arange(batch_size, device=logits_per_image.device)
    loss_i2t = F.cross_entropy(logits_per_image, labels)  # F.cross_entropy PyTorch函数式交叉熵损失
    loss_t2i = F.cross_entropy(logits_per_text, labels)
    return (loss_i2t + loss_t2i) / 2

# ========== 训练示例 ==========
def train_clip():
    model = CLIP()
    optimizer = torch.optim.AdamW(model.parameters(), lr=5e-4, weight_decay=0.2)

    # 模拟数据
    batch_size = 32
    images = torch.randn(batch_size, 3, 224, 224)
    texts = torch.randint(0, 49408, (batch_size, 77))

    # 前向 + 反向
    logits_per_image, logits_per_text = model(images, texts)
    loss = clip_loss(logits_per_image, logits_per_text)
    loss.backward()  # 反向传播计算梯度
    optimizer.step()  # 更新参数

    print(f"Loss: {loss.item():.4f}")  # 将单元素张量转为Python数值
    print(f"Logit scale: {model.logit_scale.exp().item():.2f}")

# train_clip()

13.2.4 使用预训练 CLIP 进行零样本分类

Python
import clip
from PIL import Image

device = "cuda" if torch.cuda.is_available() else "cpu"
model, preprocess = clip.load("ViT-B/32", device=device)

# 准备输入
image = preprocess(Image.open("cat.jpg")).unsqueeze(0).to(device)  # unsqueeze增加一个维度  # 移至GPU/CPU
text_templates = [
    "a photo of a cat",
    "a photo of a dog",
    "a photo of a car"
]
text = clip.tokenize(text_templates).to(device)

# 零样本推理
with torch.no_grad():  # 禁用梯度计算,节省内存
    image_features = model.encode_image(image)
    text_features = model.encode_text(text)

    # 归一化
    image_features = F.normalize(image_features, dim=-1)
    text_features = F.normalize(text_features, dim=-1)

    # 计算相似度
    similarity = (100.0 * image_features @ text_features.T).softmax(dim=-1)

for i, cls in enumerate(["cat", "dog", "car"]):  # enumerate同时获取索引和元素
    print(f"{cls}: {similarity[0][i]:.4f}")
# 输出示例: cat: 0.9832  dog: 0.0151  car: 0.0017

13.2.5 CLIP 的局限性

局限 说明 例子
组合推理弱 难以理解属性-物体绑定 "红色杯子在蓝色桌上"中的颜色绑定
空间关系弱 不理解位置关系 "左边的猫" vs "右边的猫"
计数能力差 无法精确计数 "三只鸟" vs "五只鸟"
否定理解弱 对否定句鲁棒性差 "不是猫"仍匹配猫图
细粒度差异 相似子类区分弱 不同品种的狗
文本长度限制 最长 77 个 token 长描述被截断

📝 复盘提示:CLIP 的损失函数、温度参数 \(\tau\) 的作用、零样本分类原理,以及 CLIP 的局限性,是这一节最值得反复检查的概念。


13.3 CLIP 后续改进

13.3.1 SigLIP : Sigmoid Loss 替代 Softmax

核心问题: CLIP 的 InfoNCE 损失需要在 batch 内做 Softmax 归一化,要求 batch 内的所有样本对可见,限制了分布式扩展性。

SigLIP 改进:将 Softmax 替换为独立的 Sigmoid ,每个图文对独立判断匹配/不匹配:

\[\mathcal{L} = -\frac{1}{N^2}\sum_{i=1}^{N}\sum_{j=1}^{N} \log \sigma\Big((-1)^{\mathbf{1}[i \neq j]} \cdot (z_i^I \cdot z_j^T / \tau + b)\Big)\]

其中 \(\sigma\) 是 Sigmoid 函数,\(b\) 是可学习偏置。

关键优势

对比维度 CLIP (Softmax) SigLIP (Sigmoid)
归一化范围 整个 batch 单个样本对
分布式通信 需要 all-gather 无需全局同步
Batch Size 扩展 受 Softmax 约束 可自由扩展
负样本权重 Softmax 自适应 均等权重
性能 基线 持平或更优
Python
def siglip_loss(image_features, text_features, temperature, bias):
    """SigLIP损失函数"""
    # [B, D] @ [D, B] -> [B, B]
    logits = image_features @ text_features.T / temperature + bias

    # 标签矩阵:对角线为1(正样本),其余为-1(负样本)
    B = logits.size(0)
    labels = 2 * torch.eye(B, device=logits.device) - 1  # +1 或 -1

    # Sigmoid二分类损失
    loss = -F.logsigmoid(labels * logits).mean()
    return loss

13.3.2 EVA-CLIP

版本 Vision Encoder 参数量 训练数据 ImageNet Zero-shot
CLIP ViT-L/14 428M WIT-400M 75.3%
EVA-CLIP EVA-ViT-G/14 1.1B LAION-2B 78.5%
EVA-02-CLIP EVA-02-ViT-E 4.4B LAION-2B 82.0%

核心创新:使用 EVA ( Masked Image Modeling 预训练)初始化 Vision Encoder ,再进行 CLIP 对比训练。

13.3.3 MetaCLIP

  • 核心贡献:证明数据质量比数据量重要
  • 方法:使用 CLIP 已有知识(元数据)从 CommonCrawl 中筛选高质量图文对
  • 结果: 400M 数据即可匹配原始 CLIP 在 2B 数据上的效果
  • 启示:数据工程是提升 CLIP 的关键

13.3.4 中文 CLIP 模型

模型 开发方 Vision Encoder Text Encoder 训练数据 使用场景
Chinese-CLIP 阿里达摩院 ViT-B/L/H RoBERTa-wwm 2 亿中文图文对 中文图文检索的常见起点
Taiyi-CLIP IDEA 研究院 ViT-B CLIP Text 中英文混合 中英双语场景
CN-CLIP 社区 ViT-B BERT-base-chinese 百万级中文 轻量级部署
Python
# 使用Chinese-CLIP进行中文零样本分类
from cn_clip.clip import load_from_name
import cn_clip.clip as clip

device = "cuda" if torch.cuda.is_available() else "cpu"
model, preprocess = load_from_name("ViT-B-16", device=device)

image = preprocess(Image.open("test.jpg")).unsqueeze(0).to(device)
text = clip.tokenize(["一只猫", "一只狗", "一辆车"]).to(device)

with torch.no_grad():
    image_features = model.encode_image(image)
    text_features = model.encode_text(text)

    image_features = F.normalize(image_features, dim=-1)
    text_features = F.normalize(text_features, dim=-1)

    similarity = (100.0 * image_features @ text_features.T).softmax(dim=-1)

for label, score in zip(["猫", "狗", "车"], similarity[0]):  # zip按位置配对
    print(f"{label}: {score.item():.4f}")

📝 复盘提示:SigLIP 相对 CLIP 的核心改进,以及为什么 Sigmoid Loss 更适合大规模分布式训练,适合与 CLIP 直接对照理解。


13.4 BLIP 与 BLIP-2

13.4.1 BLIP : CapFilt 自举训练

BLIP( Bootstrapping Language-Image Pre-training )的核心创新:

Text Only
网络爬取的嘈杂图文对
┌───────────────────┐
│    BLIP模型训练     │ ← 用嘈杂数据初始训练
└───────────────────┘
┌───────────────────┐     ┌───────────────────┐
│   Captioner       │     │    Filter          │
│   为图像生成描述    │     │   过滤不匹配图文对  │
└───────────────────┘     └───────────────────┘
        ↓                          ↓
    合成高质量图文对            清洗后的网络图文对
        └──────────┬──────────┘
         更高质量的训练数据
         重新训练BLIP → 更好的模型

13.4.2 BLIP-2 架构详解

核心思想:使用轻量级 Q-Former 桥接冻结的 Vision Encoder 和冻结的 LLM ,实现参数高效的多模态对齐。

Text Only
┌────────────────────────────────────────────────────────┐
│                    BLIP-2 架构                          │
│                                                        │
│  Image ──→ [冻结 ViT-G/14 (1.1B)] ──→ 视觉特征         │
│            EVA-CLIP预训练              (257 tokens)     │
│                                          ↓             │
│              ┌───────────────────────────────┐          │
│              │      Q-Former (188M参数)       │          │
│              │   ┌─────────────────────┐     │          │
│              │   │ 32个Learned Queries  │     │          │
│              │   └────────┬────────────┘     │          │
│              │            ↓                  │          │
│              │   Self-Attention              │          │
│              │   (Queries + Text共享)         │          │
│              │            ↓                  │          │
│              │   Cross-Attention ←── 视觉特征 │          │
│              │   (只有Queries参与)             │          │
│              │            ↓                  │          │
│              │   32个压缩后的视觉token         │          │
│              └───────────────────────────────┘          │
│                           ↓                            │
│              Linear Projection → LLM输入空间            │
│                           ↓                            │
│               [冻结 OPT-2.7B / FlanT5-XL] → 文本输出   │
└────────────────────────────────────────────────────────┘

13.4.3 Q-Former 三阶段训练

阶段一: Image-Text Contrastive (ITC)

Text Only
Query tokens ──→ Self-Attn ──→ Cross-Attn(←视觉) ──→ 图像表示
Text tokens  ──→ Self-Attn ──────────────────────→ 文本表示
                                                    ↕ 对比损失
  • Queries 通过 Cross-Attention 提取视觉信息
  • 关键: ITC 阶段不让 Queries 看到文本(单模态对齐)

阶段二: Image-grounded Text Generation (ITG)

Text Only
Query tokens ──→ Self-Attn ──→ Cross-Attn(←视觉) ──→ 条件
Text tokens  ──→ Causal Self-Attn(与Queries共享) ──→ 生成文本
  • Queries 提供视觉条件,引导文本生成
  • 使用因果注意力掩码

阶段三: Image-Text Matching (ITM)

Text Only
[Query + Text] ──→ Self-Attn(双向) ──→ Cross-Attn(←视觉) ──→ 二分类
  • Query 和 Text 双向交互
  • 输出匹配/不匹配的二分类结果
  • 使用 hard negative mining
阶段 任务 Query-Text 交互 目标
ITC 对比学习 不交互 视觉-文本对齐
ITG 文本生成 因果自注意力 生成能力
ITM 匹配判断 双向自注意力 细粒度匹配

13.4.4 Q-Former 简化实现

Python
class QFormer(nn.Module):
    """简化的Q-Former实现"""
    def __init__(self, num_queries=32, dim=768, depth=6, heads=12, visual_dim=1408):
        super().__init__()
        # 可学习查询token
        self.queries = nn.Parameter(torch.randn(1, num_queries, dim))

        # Cross-Attention层(Query与视觉特征交互)
        self.cross_attn_layers = nn.ModuleList([
            nn.MultiheadAttention(dim, heads, batch_first=True)
            for _ in range(depth)
        ])

        # Self-Attention层
        self.self_attn_layers = nn.ModuleList([
            nn.TransformerEncoderLayer(
                d_model=dim, nhead=heads, dim_feedforward=dim * 4,
                batch_first=True, norm_first=True
            )
            for _ in range(depth)
        ])

        # 视觉特征投影(如果维度不匹配)
        self.visual_proj = nn.Linear(visual_dim, dim) if visual_dim != dim else nn.Identity()

        # LayerNorm
        self.cross_attn_norms = nn.ModuleList([nn.LayerNorm(dim) for _ in range(depth)])

    def forward(self, visual_features):
        """
        Args:
            visual_features: [B, num_patches, visual_dim] 来自冻结ViT的特征
        Returns:
            query_output: [B, num_queries, dim] 压缩后的视觉表示
        """
        B = visual_features.size(0)
        visual_features = self.visual_proj(visual_features)

        # 扩展queries到batch维度
        queries = self.queries.expand(B, -1, -1)

        for i in range(len(self.self_attn_layers)):
            # Self-Attention
            queries = self.self_attn_layers[i](queries)

            # Cross-Attention: queries作为Q, 视觉特征作为K,V
            residual = queries
            queries_norm = self.cross_attn_norms[i](queries)
            cross_out, _ = self.cross_attn_layers[i](query=queries_norm,
                key=visual_features,
                value=visual_features)
            queries = residual + cross_out

        return queries  # [B, 32, dim]

class BLIP2(nn.Module):
    """BLIP-2模型简化版"""
    def __init__(self, visual_dim=1408, llm_dim=2560, num_queries=32, qformer_dim=768):
        super().__init__()
        self.qformer = QFormer(
            num_queries=num_queries,
            dim=qformer_dim,
            visual_dim=visual_dim
        )
        # 投影到LLM输入空间
        self.projection = nn.Linear(qformer_dim, llm_dim)

    def forward(self, visual_features):
        """
        Args:
            visual_features: [B, 257, 1408] 来自冻结ViT-G的特征
        Returns:
            llm_input: [B, 32, 2560] 用于送入冻结LLM
        """
        query_output = self.qformer(visual_features)  # [B, 32, 768]
        llm_input = self.projection(query_output)       # [B, 32, 2560]
        return llm_input

# 使用示例
visual_feats = torch.randn(2, 257, 1408)  # 模拟ViT-G输出
blip2 = BLIP2()
llm_input = blip2(visual_feats)
print(f"LLM输入形状: {llm_input.shape}")  # [2, 32, 2560]

13.4.5 使用 BLIP-2 进行视觉问答

Python
from transformers import Blip2Processor, Blip2ForConditionalGeneration
from PIL import Image

# 加载模型(约8GB显存)
processor = Blip2Processor.from_pretrained("Salesforce/blip2-opt-2.7b")
model = Blip2ForConditionalGeneration.from_pretrained(
    "Salesforce/blip2-opt-2.7b",
    torch_dtype=torch.float16,
    device_map="auto"
)

# === VQA任务 ===
image = Image.open("street_scene.jpg")
question = "How many people are in this image?"
inputs = processor(images=image, text=question, return_tensors="pt").to("cuda", torch.float16)
generated_ids = model.generate(**inputs, max_new_tokens=50)
answer = processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
print(f"Q: {question}")
print(f"A: {answer}")

# === 图像描述 ===
inputs = processor(images=image, return_tensors="pt").to("cuda", torch.float16)
generated_ids = model.generate(**inputs, max_new_tokens=100)
caption = processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
print(f"Caption: {caption}")

# === 指令式对话(InstructBLIP) ===
from transformers import InstructBlipProcessor, InstructBlipForConditionalGeneration

instruct_processor = InstructBlipProcessor.from_pretrained("Salesforce/instructblip-vicuna-7b")
instruct_model = InstructBlipForConditionalGeneration.from_pretrained(
    "Salesforce/instructblip-vicuna-7b",
    torch_dtype=torch.float16,
    device_map="auto"
)

prompt = "Describe this image in detail, including colors, objects, and spatial relationships."
inputs = instruct_processor(images=image, text=prompt, return_tensors="pt").to("cuda", torch.float16)
outputs = instruct_model.generate(**inputs, max_new_tokens=200)
print(instruct_processor.decode(outputs[0], skip_special_tokens=True))

📝 复盘提示:BLIP-2 的 Q-Former 解决了什么问题、为什么不直接把视觉特征送入 LLM、32 个 query token 如何工作、三阶段训练分别学什么,是桥接模块部分的主线。


13.5 视觉语言大模型( VLM )深度对比

13.5.1 LLaVA 系列

LLaVA( Large Language and Vision Assistant )的核心特点:极简架构设计。

Text Only
┌──────────────────────────────────────────────────┐
│             LLaVA 架构                            │
│                                                  │
│  Image ──→ [冻结 CLIP ViT-L/14]                  │
│                ↓                                 │
│            [MLP Projection (2层)]  ← 仅此可训练   │
│                ↓                                 │
│            视觉token (576个)                      │
│                ↓                                 │
│  Text  ──→ [Concat] ──→ [LLM (Vicuna/LLaMA)]    │
│                          微调LLM + Projection    │
└──────────────────────────────────────────────────┘

两阶段训练策略

阶段 目标 数据 冻结模块 可训练模块
预训练对齐 对齐视觉-文本空间 558K 图文对 ViT + LLM MLP Projection
指令微调 增强对话能力 665K 指令数据 ViT LLM + MLP

LLaVA 演进路线

版本 投影方式 分辨率 LLM 关键改进
LLaVA Linear 224 LLaMA-7B 首次证明线性投影可行
LLaVA-1.5 2 层 MLP 336 Vicuna-13B MLP+ShareGPT 数据
LLaVA-NeXT 2 层 MLP AnyRes Qwen/LLaMA 动态分辨率
LLaVA-OneVision 2 层 MLP AnyRes 多种 LLM 图像+视频统一

13.5.2 InternVL / InternVL2

核心创新

  1. InternViT-6B:参数规模达 6B 的开源视觉编码器,是目前开源社区中较大规模的视觉编码器之一(GitHub
  2. 动态分辨率( Dynamic Resolution )
Text Only
输入图像 (800×600)
计算更合适的分割方案(适配448×448网格)
┌────┬────┐
│ 子图1│ 子图2│  → 每个子图独立通过ViT编码
├────┼────┤
│ 子图3│ 子图4│  → 拼接所有子图特征
└────┴────┘
    + 缩略图特征
送入LLM
Python
# 使用 InternVL2 进行多模态对话
import torch
from transformers import AutoModel, AutoTokenizer
from PIL import Image

model_name = "OpenGVLab/InternVL2-8B"
model = AutoModel.from_pretrained(
    model_name,
    torch_dtype=torch.bfloat16,
    device_map="auto",
    trust_remote_code=True,
)
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)

# 单图对话
image = Image.open("diagram.png").convert("RGB")
pixel_values = model.transform(image).unsqueeze(0).to(model.device, torch.bfloat16)

question = "请详细描述这张图片中的内容。"
response = model.chat(tokenizer, pixel_values, question)
print(response)

# 多图对比
images = [Image.open(f"img_{i}.jpg").convert("RGB") for i in range(2)]
pixel_values = torch.stack([  # torch.stack沿新维度拼接张量
    model.transform(img) for img in images
]).to(model.device, torch.bfloat16)

question = "请比较这两张图片的异同。"
response = model.chat(tokenizer, pixel_values, question)
print(response)

13.5.3 Qwen-VL / Qwen2-VL

Qwen2-VL 的关键特性

  • Naive Dynamic Resolution:完全不做分割,直接处理任意分辨率
  • 多模态旋转位置编码( M-RoPE ):统一处理图像 2D 位置与文本 1D 位置
  • 视频理解:支持长视频(>20 分钟)理解
  • 多规模: 2B / 7B / 72B ,覆盖端侧到 cloud
Python
import torch
from PIL import Image
from transformers import Qwen2VLForConditionalGeneration, AutoProcessor

def get_input_device(model) -> torch.device:
    if hasattr(model, "hf_device_map"):
        for mapped_device in model.hf_device_map.values():
            if isinstance(mapped_device, int):
                return torch.device(f"cuda:{mapped_device}")
            if isinstance(mapped_device, str) and mapped_device not in {"cpu", "disk"}:
                return torch.device(mapped_device)

    return next(model.parameters()).device

model_name = "Qwen/Qwen2-VL-7B-Instruct"
model = Qwen2VLForConditionalGeneration.from_pretrained(
    model_name,
    torch_dtype=torch.bfloat16,
    device_map="auto",
)
processor = AutoProcessor.from_pretrained(model_name)

messages = [
    {
        "role": "user",
        "content": [
            {"type": "image", "image": "test.jpg"},
            {"type": "text", "text": "这张图片里有什么?请用中文详细描述。"},
        ],
    }
]

prompt = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
image = Image.open("test.jpg").convert("RGB")
inputs = processor(text=[prompt], images=[image], padding=True, return_tensors="pt")
input_device = get_input_device(model)
inputs = {
    name: tensor.to(input_device)
    for name, tensor in inputs.items()
}
output_ids = model.generate(**inputs, max_new_tokens=256)
response = processor.batch_decode(
    output_ids[:, inputs.input_ids.shape[1]:],
    skip_special_tokens=True,
)[0]
print(response)

13.5.4 架构连接方式对比

连接方式 代表模型 可训练参数 视觉 Token 数 优点 缺点
Linear Projection LLaVA ~4M 576 简单高效 token 多、速度慢
2 层 MLP LLaVA-1.5 ~8M 576 非线性映射更好 同上
Q-Former BLIP-2 188M 32 压缩高效 可能丢失细节
Cross-Attention Flamingo 较多 按需 深度交互 计算量大
Perceiver Flamingo 中等 可控 灵活 额外复杂度
Dynamic Res InternVL2 视情况 动态 保留细节 token 数不固定

13.5.5 代表性 VLM 综合对比

📊 说明:下表主要用于比较架构特征与工程取舍,不应被理解为固定性能榜单。VLM 领域变化很快,具体能力、价格、上下文长度和公开基准结果请以官方资料与任务实测为准。

模型 Vision Encoder 连接方式 LLM Backbone 分辨率处理 适合关注点 参数量
LLaVA-1.5-13B CLIP ViT-L/14 2 层 MLP Vicuna-13B 固定分辨率 经典开源桥接范式 ~13B
BLIP-2 ViT-G/14(冻结) Q-Former OPT-2.7B 固定分辨率 低训练成本的桥接思路 ~4B
InternVL2-8B InternViT-6B MLP InternLM2 动态分辨率 开源中文与视觉综合能力 ~8B
Qwen2-VL-7B 定制 ViT Cross-Attn Qwen2-7B 动态分辨率 中文、多图和视频理解 ~8B
Qwen2-VL-72B 定制 ViT Cross-Attn Qwen2-72B 动态分辨率 更大规模的开源多模态理解 ~77B
闭源通用 VLM(代表性产品) 未公开 未公开 未公开 通常支持高分辨率与复杂文档 原型验证、通用问答、多模态 Agent 未公开

选型建议: - 中文与多图任务:优先把 Qwen2-VL、 InternVL 等开源模型放进评测集合,而不是直接凭型号下结论。 - 研究原型: BLIP-2、 LLaVA、 InternVL 代表了不同桥接路线,适合用来理解架构取舍。 - 部署场景:先按显存、延迟、输入分辨率和任务类型筛模型,再用你自己的数据集做对比。

📝 复盘提示:LLaVA 与 BLIP-2 的桥接方式差异、Linear Projection vs Q-Former 的优劣,以及动态分辨率的重要性,是 VLM 系统设计最常回看的比较题。


13.6 多模态应用实战

13.6.1 图文检索系统

Python
import torch
import torch.nn.functional as F
from PIL import Image
from transformers import CLIPProcessor, CLIPModel
import os

class ImageTextRetrieval:
    """基于CLIP的图文检索系统"""
    def __init__(self, model_name="openai/clip-vit-base-patch32"):
        self.model = CLIPModel.from_pretrained(model_name)
        self.processor = CLIPProcessor.from_pretrained(model_name)
        self.model.eval()  # eval()评估模式
        self.image_database = []  # (path, embedding)

    def build_index(self, image_dir):
        """构建图像索引"""
        image_paths = [
            os.path.join(image_dir, f)
            for f in os.listdir(image_dir)
            if f.endswith(('.jpg', '.png', '.jpeg'))
        ]

        for path in image_paths:
            image = Image.open(path).convert("RGB")
            inputs = self.processor(images=image, return_tensors="pt")

            with torch.no_grad():
                embedding = self.model.get_image_features(**inputs)
                embedding = F.normalize(embedding, dim=-1)

            self.image_database.append((path, embedding))

        print(f"索引完成: {len(self.image_database)} 张图片")

    def text_to_image(self, query, top_k=5):
        """文本→图像检索"""
        inputs = self.processor(text=query, return_tensors="pt")

        with torch.no_grad():
            text_embedding = self.model.get_text_features(**inputs)
            text_embedding = F.normalize(text_embedding, dim=-1)

        similarities = []
        for path, img_emb in self.image_database:
            sim = (text_embedding @ img_emb.T).item()
            similarities.append((path, sim))

        similarities.sort(key=lambda x: x[1], reverse=True)  # lambda匿名函数
        return similarities[:top_k]

    def image_to_image(self, query_image_path, top_k=5):
        """图像→图像检索(以图搜图)"""
        image = Image.open(query_image_path).convert("RGB")
        inputs = self.processor(images=image, return_tensors="pt")

        with torch.no_grad():
            query_embedding = self.model.get_image_features(**inputs)
            query_embedding = F.normalize(query_embedding, dim=-1)

        similarities = []
        for path, img_emb in self.image_database:
            sim = (query_embedding @ img_emb.T).item()
            similarities.append((path, sim))

        similarities.sort(key=lambda x: x[1], reverse=True)
        return similarities[:top_k]

# 使用示例
retrieval = ImageTextRetrieval()
retrieval.build_index("./image_gallery/")
results = retrieval.text_to_image("a cat sitting on a sofa")
for path, score in results:
    print(f"{path}: {score:.4f}")

13.6.2 多模态 RAG 系统

Python
"""
多模态RAG系统:结合图像理解和文本检索
"""
import chromadb
from PIL import Image
from transformers import CLIPProcessor, CLIPModel
import torch
import torch.nn.functional as F

class MultimodalRAG:
    """多模态RAG: 支持图像和文本的混合检索"""
    def __init__(self, clip_model="openai/clip-vit-base-patch32"):
        # 视觉-文本编码器
        self.clip = CLIPModel.from_pretrained(clip_model)
        self.processor = CLIPProcessor.from_pretrained(clip_model)
        self.clip.eval()

        # 向量数据库
        self.client = chromadb.Client()
        self.collection = self.client.get_or_create_collection(
            name="multimodal_docs",
            metadata={"hnsw:space": "cosine"},
        )
        self.doc_id = 0

    def add_image(self, image_path, description=""):
        """索引图像"""
        image = Image.open(image_path).convert("RGB")
        inputs = self.processor(images=image, return_tensors="pt")

        with torch.no_grad():
            embedding = self.clip.get_image_features(**inputs)
            embedding = F.normalize(embedding, dim=-1)

        self.collection.add(
            embeddings=[embedding[0].cpu().numpy().tolist()],
            documents=[description or f"Image: {image_path}"],
            metadatas=[{"type": "image", "path": image_path}],
            ids=[f"doc_{self.doc_id}"]
        )
        self.doc_id += 1

    def add_text(self, text, metadata=None):
        """索引文本"""
        inputs = self.processor(text=text, return_tensors="pt", padding=True, truncation=True)

        with torch.no_grad():
            embedding = self.clip.get_text_features(**inputs)
            embedding = F.normalize(embedding, dim=-1)

        self.collection.add(
            embeddings=[embedding[0].cpu().numpy().tolist()],
            documents=[text],
            metadatas=[{"type": "text", **(metadata or {})}],
            ids=[f"doc_{self.doc_id}"]
        )
        self.doc_id += 1

    def query(self, text_query, top_k=5):
        """文本查询,检索相关图像和文本"""
        inputs = self.processor(text=text_query, return_tensors="pt")

        with torch.no_grad():
            query_embedding = self.clip.get_text_features(**inputs)
            query_embedding = F.normalize(query_embedding, dim=-1)

        results = self.collection.query(
            query_embeddings=[query_embedding[0].cpu().numpy().tolist()],
            n_results=top_k
        )
        return results

# 使用示例
rag = MultimodalRAG()

# 索引混合内容
rag.add_image("chart.png", "2024年Q3收入增长趋势图")
rag.add_image("architecture.png", "微服务系统架构图")
rag.add_text("2024年第三季度收入同比增长23%,主要得益于AI产品线。")
rag.add_text("系统采用微服务架构,共包含12个核心服务。")

# 查询
results = rag.query("公司收入增长情况")
for doc, meta in zip(results["documents"][0], results["metadatas"][0]):
    print(f"[{meta['type']}] {doc}")

13.6.3 VQA 实战:使用 VLM 构建问答系统

Python
from transformers import pipeline
from PIL import Image

class VQASystem:
    """基于VLM的视觉问答系统"""
    def __init__(self, model_name="Salesforce/blip2-opt-2.7b"):
        self.pipe = pipeline(
            "visual-question-answering",
            model=model_name,
            torch_dtype=torch.float16,
            device_map="auto"
        )

    def answer(self, image_path, question):
        """回答关于图像的问题"""
        image = Image.open(image_path).convert("RGB")
        result = self.pipe(image, question)
        return result[0]["answer"]

    def batch_answer(self, image_path, questions):
        """批量回答多个问题"""
        image = Image.open(image_path).convert("RGB")
        answers = {}
        for q in questions:
            result = self.pipe(image, q)
            answers[q] = result[0]["answer"]
        return answers

# 使用示例
vqa = VQASystem()
questions = [
    "What color is the car?",
    "How many people are in the image?",
    "What is the weather like?"
]
results = vqa.batch_answer("street.jpg", questions)
for q, a in results.items():
    print(f"Q: {q}\nA: {a}\n")

📝 复盘提示:多模态 RAG 和纯文本 RAG 的差异,以及图像检索该选什么编码模型,是应用落地部分的关键。


13.7 关键复盘问题

Q1: CLIP 使用什么损失函数?温度参数τ的作用

: CLIP 使用对称的 InfoNCE 对比损失。对一个 batch 中 N 个图文对,图像到文本方向:\(\mathcal{L}_{I \to T} = -\frac{1}{N}\sum_{i}\log\frac{\exp(\text{sim}(z_i^I, z_i^T)/\tau)}{\sum_j \exp(\text{sim}(z_i^I, z_j^T)/\tau)}\)。温度参数τ控制分布的"锐利度"——τ越小, softmax 越接近 argmax ,模型越关注最相似的负样本;τ越大,分布越平滑,梯度更均匀。 CLIP 中τ是可学习参数,初始值约 14.3 ( log scale 为 log(1/0.07))。

Q2: BLIP-2 的 Q-Former 核心设计是什么?为什么不直接将视觉特征送入 LLM

: Q-Former 是一个轻量 Transformer ( 188M 参数),使用 32 个可学习 query token 通过 Cross-Attention 从冻结 ViT 的 257 个视觉 token 中提取最相关的信息。不直接送入 LLM 的原因:(1)视觉 token 太多( 257+),显著增加 LLM 的计算和内存开销;(2)视觉和语言表示空间未对齐,直接拼接效果差;(3)Q-Former 起到"信息瓶颈"作用,只保留与语言任务相关的视觉信息,过滤掉噪声;(4)实现参数高效——只训练 188M 的 Q-Former , ViT ( 1.1B )和 LLM 均冻结。

Q3: LLaVA 和 BLIP-2 架构的核心区别

:核心区别在于视觉-语言的桥接方式: - LLaVA:使用 2 层 MLP 将 CLIP 视觉特征全部( 576 个 token )投影到 LLM 空间,保留所有视觉信息 - BLIP-2:使用 Q-Former 将视觉特征压缩为 32 个 token

权衡: LLaVA 保留更多细节但推理更慢( 576 vs 32 个额外 token ),适合需要细粒度理解的任务; BLIP-2 更高效但可能丢失细节。实际效果上, LLaVA-1.5 凭借更好的训练数据和简单架构在多数 benchmark 上超越 BLIP-2 。

Q4: SigLIP 相比 CLIP 的改进是什么

: SigLIP 将 InfoNCE 的 Softmax 替换为 Sigmoid——每个图文对独立做二分类(匹配/不匹配),而非在 batch 内做 softmax 归一化。核心优势:(1)无需 batch 内全局通信,适合大规模分布式训练( CLIP 需要 all-gather 所有 GPU 的 batch 做 softmax );(2)batch size 对损失函数没有数学约束,只影响负样本数量;(3)性能与 CLIP 持平或超越。

Q5: 什么是动态分辨率?为什么对 VLM 重要

:动态分辨率将输入图像根据实际宽高比自适应分割为多个固定大小的子图(如 448×448 ),避免预处理时的强制缩放/裁剪造成的信息损失。重要性:(1)保留图像原始宽高比,避免变形;(2)高分辨率图像(如文档 OCR )可用更多子图保留文字细节;(3)小图不浪费计算资源;(4)InternVL2 和 Qwen2-VL 均采用此方案,已成为 VLM 标配。

Q6: 多模态融合的三种策略分别是什么

:(1)Early Fusion(早期融合):在输入层将多模态数据拼接后送入统一模型,交互深但不灵活,代表 ViLBERT ;(2)Late Fusion(晚期融合):各模态独立编码后在输出层合并,灵活但交互浅, CLIP 属于此类;(3)Cross-Attention Fusion(交叉注意力融合):通过交叉注意力让不同模态特征深度交互,兼顾灵活性和交互深度, Flamingo 、 BLIP-2 属于此类。现代 VLM (如 LLaVA )通过将视觉 token 直接拼入 LLM 输入序列,让 LLM 的 Self-Attention 自然实现跨模态交互。

Q7: CLIP 能做零样本分类的核心原因

: CLIP 在 4 亿图文对上学习了通用的视觉-语言对齐空间。推理时,将待分类类别转化为文本模板(如"a photo of a {class}"),编码为文本特征,再与图像特征计算余弦相似度,相似度最高的类别即为预测结果。本质上将分类问题转化为图文匹配问题,不需要任何目标任务的标注数据。但效果受文本模板设计( prompt engineering )影响较大。

Q8: VLM 推理时视觉 token 过多导致什么问题?有什么解决方案

:视觉 token 过多(如 LLaVA 的 576 个、动态分辨率下可达 2000+个)导致:(1)LLM 推理延迟增加( Self-Attention 复杂度 \(O(n^2)\));(2)KV-Cache 内存消耗线性增长;(3)长文档+多图场景更严重。解决方案: - Q-Former 压缩( BLIP-2 ,压缩到 32 个 token ) - 视觉 Token 剪枝( FastV ,根据 Attention 权重动态删除不重要的 token ) - Token 合并( LLaVA-PruMerge ,基于相似度合并 token ) - 子图策略优化(只对需要高精度的区域使用高分辨率子图)

Q9: 如何评估 VLM 的能力?常用 Benchmark 有哪些

:常用 Benchmark 按能力维度分类: - 通用视觉理解: MMBench 、 MM-Vet 、 SEED-Bench - 文档/OCR: TextVQA 、 DocVQA 、 ChartQA 、 InfoVQA - 数学推理: MathVista 、 MathVerse - 幻觉检测: POPE 、 HallusionBench - 综合排行: OpenCompass Multimodal Leaderboard

评估维度包括:视觉感知、 OCR 识别、空间推理、逻辑推理、幻觉率、多图理解等。

Q10: 什么是多模态幻觉( Hallucination )?如何缓解

:多模态幻觉指 VLM 生成的文本描述了图像中不存在的内容,如图中只有一只猫却说"两只猫在沙发上"。缓解方法: - 数据层面:使用高质量图文对训练,过滤不匹配数据( BLIP 的 CapFilt 思路) - 训练层面: RLHF/DPO 对齐(惩罚幻觉输出),增加负样本训练 - 推理层面:对比解码( Contrastive Decoding )——同时参考有图和无图时的输出分布 - 验证层面: RAG 增强验证——用检索结果交叉验证 VLM 生成内容 - 评估层面: POPE 等 Benchmark 系统化评估幻觉率


13.8 练习与项目

练习 1 : CLIP 变种对比实验

用同一数据集对比 CLIP, SigLIP 的训练效果,分析: - 相同 epoch 下的 zero-shot 准确率 - 不同 batch size 对两种损失的影响 - 温度参数τ和偏置 b 的学习曲线

练习 2 :构建多模态搜索引擎

要求: 1. 支持文本→图像检索、图像→图像检索 2. 使用 Chinese-CLIP 支持中文查询 3. 使用 FAISS 进行大规模向量检索(>10 万图片) 4. 构建简单 Web 界面( Gradio )

练习 3 : VLM 微调

使用 LoRA 对 InternVL2-2B 进行微调: 1. 准备中文 VQA 数据集 2. 使用 LLaMA-Factory 或 ms-swift 进行 LoRA 微调 3. 对比微调前后在自定义数据集上的效果 4. 对比不同 LoRA rank ( 4/16/64 )的效果


13.9 本章小结

核心知识点回顾

概念 要点
多模态融合 Early/Late/Cross-Attention 三种范式
CLIP 对比学习、 InfoNCE Loss 、双塔架构、零样本分类
SigLIP Sigmoid 替代 Softmax 、无需全局通信、支持分布式
BLIP-2 Q-Former 桥接、三阶段训练(ITC/ITG/ITM)、冻结 VE+LLM
LLaVA 极简线性投影、两阶段训练(对齐+指令微调)
InternVL 动态分辨率、 InternViT-6B 、中文强
Qwen-VL 任意分辨率、 M-RoPE 、中文极强
多模态幻觉 VLM 生成不存在内容,需 RLHF/对比解码缓解

技术选型起点

Text Only
                    VLM选型决策树
              ┌─── 中文场景?──── 否 ───→ LLaVA-NeXT / 闭源VLM
              │         │
              │        是
              │         │
              ├── 端侧部署? ─── 是 ───→ Qwen2-VL-2B
              │         │
              │        否
              │         │
              ├── 需要OCR? ─── 是 ───→ Qwen2-VL-7B / InternVL2-8B
              │         │
              │        否
              │         │
              └── 通用对话 ─────────→ InternVL2-8B / Qwen2-VL-7B

下一步

下一章14-自监督学习.md - 学习自监督视觉学习


恭喜完成第 13 章! 🎉


最后更新日期: 2026-04-03