多模态大模型
一句话总结: 多模态大模型通过将视觉编码器与语言模型对齐,让 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 来高效完成这个操作:
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 xPosition Embedding 与 [CLS] Token
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 Embedding | Conv2d(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):
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) / 2Temperature 参数的作用
Temperature 是 CLIP 中一个关键的可学习参数:
- Temperature 小(如 0.01):softmax 分布更尖锐,模型更自信,正负样本分离更明确
- Temperature 大(如 1.0):softmax 分布更平滑,梯度更均匀
- CLIP 中初始化为
temperature = nn.Parameter(torch.tensor(1/0.07)),即约 14.3 - 训练过程中模型自动学习最优温度
Zero-Shot 分类原理
CLIP 最惊艳的能力是零样本分类:无需额外训练就能识别新类别。其原理本质是将分类问题转化为图文匹配问题:
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]) → 预测为 catPrompt Engineering 对 CLIP 零样本性能影响巨大:
"cat"→ 准确率较低"a photo of a cat"→ 显著提升"a photo of a cat, a type of pet"→ 进一步提升- CLIP 论文中使用了 80 个 prompt 模板做 ensemble
简化 CLIP 对比学习代码实战
以下是一个可运行的简化 CLIP 训练代码:
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 生成高质量指令数据。流程为:
- 将图像的 caption 和 bounding box 描述输入 GPT-4(纯文本)
- GPT-4 基于这些描述生成三类问答:
- 对话:自然语言问答("图片里有什么?")
- 详细描述:长篇细致描述
- 复杂推理:需要多步逻辑的问题
LLaVA 的设计洞察
- 投影层是关键:一个简单的 MLP 就能有效连接视觉和语言模态,无需复杂的跨模态注意力
- 数据质量 > 数量:用 GPT-4 生成的高质量指令数据,比大量低质量数据更有效
- 冻结视觉编码器:利用 CLIP 已学到的强大视觉表示,避免灾难性遗忘
- 视觉 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 | 关键创新 |
|---|---|---|---|---|
| LLaVA | 2023.4 | CLIP ViT-L | Vicuna-7B | 简洁的视觉-语言连接 |
| LLaVA-1.5 | 2023.10 | CLIP ViT-L@336 | Vicuna-7B/13B | MLP 投影 + 高分辨率 |
| Qwen-VL | 2023.8 | ViT-G/14 | Qwen-7B | 多分辨率 + 多语言 + 定位 |
| InternVL | 2023.12 | InternViT-6B | InternLM | 超大视觉编码器 |
| GPT-4V | 2023.9 | 未公开 | GPT-4 | 商用多模态标杆 |
| GPT-4o | 2024.5 | 原生多模态 | GPT-4 | 原生多模态,端到端训练 |
| Qwen2-VL | 2024.8 | ViT + NaViT | Qwen2 | 动态分辨率,视频理解 |
| InternVL2.5 | 2024.12 | InternViT-6B | InternLM2.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:让模型输出的内容可以追溯到图像区域
- 对比解码:比较有图和无图时的输出差异
苏格拉底时刻
请停下来思考以下问题,不急于查看答案:
- ViT 将图像切成 16x16 的 patch — 如果一个关键物体恰好被切分到了两个 patch 中,模型能正确识别它吗?注意力机制如何弥补这个问题?
- CLIP 通过对比学习对齐图文空间,但它只学习了全局的图文匹配关系。如果你需要模型理解"图片左边的红色球"这样的细粒度空间关系,CLIP 能做到吗?为什么?
- LLaVA 用一个简单的线性投影层连接视觉编码器和 LLM — 这似乎太简单了。为什么这能工作?如果换成一个更复杂的跨模态注意力模块,效果一定会更好吗?
- 当前的多模态模型大多是"看图说话"模式 — 图像理解是单向的。如果你想让模型能够"指着图中某个区域提问"(视觉定位),架构需要如何改变?
- CLIP 的 InfoNCE 损失中,batch size 越大效果越好(OpenAI 用了 32768)。为什么 batch size 对对比学习如此重要?batch 太小会有什么问题?
- 多模态幻觉和纯文本 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 与代码