Skip to content

多模态大模型

一句话总结: 多模态大模型通过将视觉编码器与语言模型对齐,让 LLM 不仅能理解文字,还能"看懂"图像,实现跨模态的理解与推理。

为什么需要多模态?

人类认知世界是多模态的 — 我们同时通过视觉、听觉、触觉来理解环境。纯文本 LLM 只能处理语言,这严重限制了它的应用场景。多模态大模型的目标是:

  • 图文理解:看图回答问题、描述图片内容
  • 文档解析:理解包含表格、图表的文档
  • 视觉推理:基于图像内容进行逻辑推理
  • 图像生成:根据文字描述创作图像

ViT(Vision Transformer)深度解析

ViT 是将 Transformer 应用于视觉领域的里程碑工作。核心思想出奇简单:把图像当作"句子"来处理

ViT 的工作流程

输入图像 (224×224×3)

切分为 Patch (16×16 = 196个patch)

每个 Patch 线性投影为向量 (patch embedding, dim=768)

加上位置编码 + [CLS] token

送入标准 Transformer Encoder (L=12, H=12)

[CLS] token 的输出 → 分类头

Patch Embedding 实现

Patch Embedding 的本质是把每个 16x16x3 的图像块通过一个线性层映射为一个 D 维向量。实际实现中常用 Conv2d 来高效完成这个操作:

python
import torch
import torch.nn as nn

class PatchEmbedding(nn.Module):
    """将图像切分为 patch 并投影为嵌入向量"""
    def __init__(self, img_size=224, patch_size=16, in_channels=3, embed_dim=768):
        super().__init__()
        self.num_patches = (img_size // patch_size) ** 2  # 196
        # 用 Conv2d 替代 "展平 + Linear",等价但更高效
        # kernel_size = stride = patch_size,恰好无重叠地切分图像
        self.proj = nn.Conv2d(
            in_channels, embed_dim,
            kernel_size=patch_size, stride=patch_size
        )

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        # x: (B, 3, 224, 224)
        x = self.proj(x)          # (B, 768, 14, 14)
        x = x.flatten(2)          # (B, 768, 196)
        x = x.transpose(1, 2)     # (B, 196, 768)  — 每个 patch 一个 token
        return x

Position Embedding 与 [CLS] Token

python
class ViT(nn.Module):
    def __init__(self, img_size=224, patch_size=16, embed_dim=768,
                 depth=12, num_heads=12, num_classes=1000):
        super().__init__()
        self.patch_embed = PatchEmbedding(img_size, patch_size, 3, embed_dim)
        num_patches = self.patch_embed.num_patches

        # [CLS] token:可学习的分类标记,放在序列最前面
        self.cls_token = nn.Parameter(torch.zeros(1, 1, embed_dim))
        # 位置编码:可学习的绝对位置编码(不是 sinusoidal 的)
        # 长度为 num_patches + 1(包含 [CLS])
        self.pos_embed = nn.Parameter(torch.zeros(1, num_patches + 1, embed_dim))
        nn.init.trunc_normal_(self.pos_embed, std=0.02)

        # Transformer Encoder
        encoder_layer = nn.TransformerEncoderLayer(
            d_model=embed_dim, nhead=num_heads,
            dim_feedforward=embed_dim * 4, activation="gelu",
            batch_first=True, norm_first=True
        )
        self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=depth)
        self.norm = nn.LayerNorm(embed_dim)
        self.head = nn.Linear(embed_dim, num_classes)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        B = x.shape[0]
        x = self.patch_embed(x)                                    # (B, 196, 768)
        cls_tokens = self.cls_token.expand(B, -1, -1)              # (B, 1, 768)
        x = torch.cat([cls_tokens, x], dim=1)                      # (B, 197, 768)
        x = x + self.pos_embed                                      # 加位置编码
        x = self.encoder(x)                                         # Transformer
        x = self.norm(x[:, 0])                                      # 取 [CLS] 输出
        return self.head(x)                                          # 分类

ViT 的关键设计总结

设计点实现方式说明
Patch EmbeddingConv2d(kernel=stride=16)等价于 "展平 + Linear",效率更高
位置编码可学习参数不同于原始 Transformer 的 sinusoidal
[CLS] Token可学习参数聚合全局信息用于分类
归纳偏置几乎没有不假设局部性、平移不变性,需要大量数据

ViT 的启示

ViT 证明了一个重要观点:Transformer 的注意力机制本身足够通用,不需要卷积等视觉特有的归纳偏置。但代价是需要大量数据(ViT 在 JFT-300M 上预训练才超越 CNN)。后续 DeiT 通过蒸馏 + 数据增强证明在 ImageNet 规模也能训练好 ViT。

CLIP(对比语言-图像预训练)深度解析

CLIP(Contrastive Language-Image Pre-training)是连接视觉和语言的桥梁。它学习一个共享的嵌入空间,使得匹配的图文对距离近,不匹配的距离远。

双编码器架构

图像 ──→ [图像编码器(ViT-L/14)] ──→ 图像特征 ──→ [线性投影] ──→ 图像向量 (d=512) ──┐
                                                                                  ├→ 余弦相似度矩阵
文本 ──→ [文本编码器(Transformer)] ──→ [EOS]特征 ──→ [线性投影] ──→ 文本向量 (d=512) ──┘

关键设计点:

  • 图像编码器:ViT-L/14(L 层 Transformer,patch size 14)或 ResNet
  • 文本编码器:12 层 Transformer,最大 77 token
  • 投影层:将两个编码器的输出投影到同一维度空间
  • 归一化:投影后做 L2 归一化,余弦相似度等价于内积

对比学习损失 InfoNCE

给定一个 batch 的 N 个图文对,CLIP 计算 N×N 的相似度矩阵,对角线是正样本对:

        文本1   文本2   文本3   文本4
图像1   [0.9]   0.1    0.05   0.02    ← 对角线是正样本
图像2    0.05  [0.85]  0.08   0.03
图像3    0.02   0.06  [0.88]  0.04
图像4    0.03   0.04   0.02  [0.92]

损失函数是对称的 InfoNCE(Info Noise Contrastive Estimation):

python
def clip_loss(image_features, text_features, temperature):
    """
    InfoNCE 对比学习损失
    Args:
        image_features: (N, D) L2 归一化的图像嵌入
        text_features:  (N, D) L2 归一化的文本嵌入
        temperature:    可学习的温度参数(标量)
    """
    # 计算相似度矩阵 (N, N)
    logits = (image_features @ text_features.T) / temperature

    # 标签:对角线为正样本,即 labels = [0, 1, 2, ..., N-1]
    labels = torch.arange(len(logits), device=logits.device)

    # 对称损失:图像→文本 + 文本→图像
    loss_i2t = F.cross_entropy(logits, labels)       # 每行做 softmax
    loss_t2i = F.cross_entropy(logits.T, labels)     # 每列做 softmax
    return (loss_i2t + loss_t2i) / 2

Temperature 参数的作用

Temperature 是 CLIP 中一个关键的可学习参数:

  • Temperature 小(如 0.01):softmax 分布更尖锐,模型更自信,正负样本分离更明确
  • Temperature 大(如 1.0):softmax 分布更平滑,梯度更均匀
  • CLIP 中初始化为 temperature = nn.Parameter(torch.tensor(1/0.07)),即约 14.3
  • 训练过程中模型自动学习最优温度

Zero-Shot 分类原理

CLIP 最惊艳的能力是零样本分类:无需额外训练就能识别新类别。其原理本质是将分类问题转化为图文匹配问题:

python
import clip

# 加载模型
model, preprocess = clip.load("ViT-B/32", device="cuda")

# 1. 构造文本提示(prompt engineering 很重要)
class_names = ["cat", "dog", "bird", "car", "airplane"]
text_prompts = [f"a photo of a {c}" for c in class_names]

# 2. 编码文本(可以离线计算一次,缓存起来)
text_tokens = clip.tokenize(text_prompts).to("cuda")
with torch.no_grad():
    text_features = model.encode_text(text_tokens)       # (5, 512)
    text_features = text_features / text_features.norm(dim=-1, keepdim=True)

# 3. 编码图像
image = preprocess(PIL.Image.open("cat.jpg")).unsqueeze(0).to("cuda")
with torch.no_grad():
    image_features = model.encode_image(image)            # (1, 512)
    image_features = image_features / image_features.norm(dim=-1, keepdim=True)

# 4. 计算相似度 → 分类
similarities = (image_features @ text_features.T).squeeze(0)  # (5,)
probs = similarities.softmax(dim=0)
# → tensor([0.92, 0.03, 0.02, 0.02, 0.01])  → 预测为 cat

Prompt Engineering 对 CLIP 零样本性能影响巨大

  • "cat" → 准确率较低
  • "a photo of a cat" → 显著提升
  • "a photo of a cat, a type of pet" → 进一步提升
  • CLIP 论文中使用了 80 个 prompt 模板做 ensemble

简化 CLIP 对比学习代码实战

以下是一个可运行的简化 CLIP 训练代码:

python
import torch
import torch.nn as nn
import torch.nn.functional as F

class SimpleCLIP(nn.Module):
    """简化版 CLIP:理解对比学习的核心机制"""
    def __init__(self, image_encoder, text_encoder, embed_dim=256):
        super().__init__()
        self.image_encoder = image_encoder   # 任意图像编码器
        self.text_encoder = text_encoder     # 任意文本编码器
        # 投影头:将编码器输出映射到共享空间
        self.image_proj = nn.Linear(image_encoder.output_dim, embed_dim)
        self.text_proj = nn.Linear(text_encoder.output_dim, embed_dim)
        # 可学习温度参数
        self.temperature = nn.Parameter(torch.tensor(1.0 / 0.07).log())

    def forward(self, images, input_ids, attention_mask):
        # 编码
        image_feat = self.image_encoder(images)                      # (B, img_dim)
        text_feat = self.text_encoder(input_ids, attention_mask)     # (B, txt_dim)

        # 投影 + L2 归一化
        image_embed = F.normalize(self.image_proj(image_feat), dim=-1)  # (B, embed_dim)
        text_embed = F.normalize(self.text_proj(text_feat), dim=-1)     # (B, embed_dim)

        # 计算对比损失
        temperature = self.temperature.exp()                     # 确保正数
        logits = (image_embed @ text_embed.T) * temperature      # (B, B)
        labels = torch.arange(len(logits), device=logits.device)

        loss_i2t = F.cross_entropy(logits, labels)
        loss_t2i = F.cross_entropy(logits.T, labels)
        return (loss_i2t + loss_t2i) / 2

# 训练循环示例
model = SimpleCLIP(image_encoder, text_encoder, embed_dim=256).cuda()
optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4, weight_decay=0.1)

for images, input_ids, attention_mask in dataloader:
    loss = model(images.cuda(), input_ids.cuda(), attention_mask.cuda())
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

LLaVA(大语言与视觉助手)深度解析

LLaVA(Large Language and Vision Assistant)是多模态大模型的代表性工作,展示了一种简洁而有效的架构:视觉编码器 + 投影层 + LLM

LLaVA 架构详解

输入图像 (336×336)

[CLIP ViT-L/14 @336px 视觉编码器]  ← 冻结,不参与训练

视觉特征 (576 tokens, 每个 1024-dim)    ← 24×24 = 576 个 patch

[MLP 投影层: Linear(1024, 4096) → GELU → Linear(4096, 4096)]  ← LLaVA-1.5 使用 2 层 MLP

视觉 Token (576 个, 每个 4096-dim)      ← 与 LLM 的 hidden_dim 对齐

[拼接] ← 系统提示 tokens + 视觉 tokens + 用户指令 tokens

[LLM (Vicuna-7B / Llama-2)]           ← 阶段一冻结,阶段二微调

文本回答(自回归生成)

投影层的演进

  • LLaVA v1:单层 Linear → 效果已不错,证明了简单方案的可行性
  • LLaVA-1.5:2 层 MLP (Linear → GELU → Linear) → 性能显著提升
  • 更复杂的方案(如 Q-Former、Perceiver Resampler)不一定更好

Visual Instruction Tuning:两阶段训练

阶段一:特征对齐预训练

目标:让投影层学会将视觉特征"翻译"为 LLM 能理解的表示
冻结:视觉编码器 ✓, LLM ✓
训练:仅投影层
数据:CC3M 筛选的 595K 图文对 → 生成 caption 任务
     格式:<image> Describe this image in detail. → "A cat sitting on..."
训练时长:约 4 小时 (8× A100)

阶段二:端到端指令微调

目标:让模型学会遵循指令进行多种视觉任务
冻结:视觉编码器 ✓
训练:投影层 + LLM 全部参数
数据:158K 多模态指令数据(GPT-4/GPT-4V 辅助生成)
     包括:详细描述、复杂推理、多轮对话
训练时长:约 10 小时 (8× A100)

指令数据构建的巧妙之处

LLaVA 的核心创新之一是用 GPT-4 生成高质量指令数据。流程为:

  1. 将图像的 caption 和 bounding box 描述输入 GPT-4(纯文本)
  2. GPT-4 基于这些描述生成三类问答:
    • 对话:自然语言问答("图片里有什么?")
    • 详细描述:长篇细致描述
    • 复杂推理:需要多步逻辑的问题

LLaVA 的设计洞察

  1. 投影层是关键:一个简单的 MLP 就能有效连接视觉和语言模态,无需复杂的跨模态注意力
  2. 数据质量 > 数量:用 GPT-4 生成的高质量指令数据,比大量低质量数据更有效
  3. 冻结视觉编码器:利用 CLIP 已学到的强大视觉表示,避免灾难性遗忘
  4. 视觉 token 作为"外语":把视觉 token 直接拼接到文本序列中,LLM 将它们当作一种新的"语言"来理解

国产多模态模型

Qwen-VL(通义千问视觉)

架构:ViT-G/14 (1.9B) + Qwen-7B
关键创新:
├── 多分辨率输入:支持 448×448 高分辨率
├── 多语言支持:中英文双语能力强
├── 细粒度定位:支持 bounding box 输出
└── 多图理解:支持多张图片输入
训练数据:1.4B 图文对(含大量中文数据)

Qwen-VL 的一大特色是支持区域理解——用户可以在图中框选区域提问,模型也可以输出 bounding box 定位目标。

InternVL

架构:InternViT-6B + InternLM-20B(可换)
关键创新:
├── 超大视觉编码器:6B 参数的 ViT,远超 CLIP ViT-L 的 300M
├── 动态分辨率:将图像拆为多个 448×448 的子图
├── Progressive Alignment:渐进式对齐策略
└── 多种 LLM 适配:可接入不同的语言模型

InternVL 的思路是:如果视觉编码器更强大,多模态理解就能更好。通过扩大视觉编码器到 6B 参数并在大规模图文数据上训练,获得了更强的视觉表示。

多模态模型的演进

模型时间视觉编码器LLM关键创新
LLaVA2023.4CLIP ViT-LVicuna-7B简洁的视觉-语言连接
LLaVA-1.52023.10CLIP ViT-L@336Vicuna-7B/13BMLP 投影 + 高分辨率
Qwen-VL2023.8ViT-G/14Qwen-7B多分辨率 + 多语言 + 定位
InternVL2023.12InternViT-6BInternLM超大视觉编码器
GPT-4V2023.9未公开GPT-4商用多模态标杆
GPT-4o2024.5原生多模态GPT-4原生多模态,端到端训练
Qwen2-VL2024.8ViT + NaViTQwen2动态分辨率,视频理解
InternVL2.52024.12InternViT-6BInternLM2.5性能逼近 GPT-4o

多模态训练的关键挑战

模态对齐(Modality Alignment)

视觉和语言是两种截然不同的信息形态。如何让 LLM 真正"理解"视觉信息,而不只是学到浅层的统计对应关系?

核心问题

  • 视觉特征空间和语言特征空间的分布不同
  • 投影层的能力瓶颈:简单的线性/MLP 投影是否足够?
  • 视觉信息的粒度:全局特征 vs 局部特征 vs 区域特征

常见解决方案

  • 分阶段训练:先对齐,再微调
  • Q-Former(BLIP-2):用交叉注意力提取视觉信息
  • Perceiver Resampler:将可变长度的视觉特征压缩为固定数量的 token

多模态数据构建

高质量的多模态数据是稀缺资源:

数据类型规模质量用途
图文对 (LAION)数十亿低(网页抓取)预训练对齐
高质量 caption数百万特征对齐
指令数据 (GPT-4 生成)数十万指令微调
人工标注 VQA数万最高评估/精调

数据构建的实践经验

  • 预训练阶段用量取胜,微调阶段用质取胜
  • GPT-4/4V 辅助生成指令数据是目前最高效的方法
  • 混合不同类型的数据(caption、VQA、推理、对话)训练效果最好

多模态幻觉(Hallucination)

多模态幻觉是当前最突出的问题:模型生成了图像中不存在的内容。

幻觉的类型

  • 物体幻觉:声称看到了图中不存在的物体("图中有一只猫",但实际没有)
  • 属性幻觉:错误描述物体的属性(颜色、大小、位置)
  • 关系幻觉:错误描述物体之间的关系("猫在桌子上",实际在地上)

幻觉的原因

  • 语言先验过强:LLM 倾向于生成"合理"的描述,即使与图像不符
  • 视觉信息损失:投影过程中丢失了细节
  • 训练数据偏差:某些物体共现频率高导致的虚假关联

缓解方法

  • RLHF/DPO 对齐:用人类偏好惩罚幻觉回答
  • 视觉 grounding:让模型输出的内容可以追溯到图像区域
  • 对比解码:比较有图和无图时的输出差异

苏格拉底时刻

请停下来思考以下问题,不急于查看答案:

  1. ViT 将图像切成 16x16 的 patch — 如果一个关键物体恰好被切分到了两个 patch 中,模型能正确识别它吗?注意力机制如何弥补这个问题?
  2. CLIP 通过对比学习对齐图文空间,但它只学习了全局的图文匹配关系。如果你需要模型理解"图片左边的红色球"这样的细粒度空间关系,CLIP 能做到吗?为什么?
  3. LLaVA 用一个简单的线性投影层连接视觉编码器和 LLM — 这似乎太简单了。为什么这能工作?如果换成一个更复杂的跨模态注意力模块,效果一定会更好吗?
  4. 当前的多模态模型大多是"看图说话"模式 — 图像理解是单向的。如果你想让模型能够"指着图中某个区域提问"(视觉定位),架构需要如何改变?
  5. CLIP 的 InfoNCE 损失中,batch size 越大效果越好(OpenAI 用了 32768)。为什么 batch size 对对比学习如此重要?batch 太小会有什么问题?
  6. 多模态幻觉和纯文本 LLM 的幻觉有什么本质区别?为什么多模态幻觉更难解决?

推荐资源

  • Dosovitskiy et al. "An Image is Worth 16x16 Words" — ViT 原始论文
  • Radford et al. "Learning Transferable Visual Models From Natural Language Supervision" — CLIP 论文
  • Liu et al. "Visual Instruction Tuning" — LLaVA 论文
  • Liu et al. "Improved Baselines with Visual Instruction Tuning" — LLaVA-1.5 论文
  • Bai et al. "Qwen-VL: A Versatile Vision-Language Model" — Qwen-VL 论文
  • Chen et al. "InternVL: Scaling up Vision Foundation Models" — InternVL 论文
  • Li et al. "BLIP-2: Bootstrapping Language-Image Pre-training" — Q-Former 架构
  • OpenAI CLIP GitHub — 官方实现与预训练权重
  • LLaVA 项目主页 — Demo 与代码