多模态模型原理 — 从 CLIP 到原生多模态
#多模态 · #CLIP · #LLaVA · #视觉语言 · #Vision-Language · #对比学习
多模态模型让 AI 不仅能读文字,还能"看"图像、"听"声音。本文从 CLIP 的对比学习原理出发,逐层深入到 LLaVA 和原生多模态架构。
为什么需要多模态?
纯文本 LLM 的根本局限:世界不只有文字。
mermaid
graph LR
subgraph "纯文本模型"
T1["用户: 这张图里有什么?"] --> LLM1["LLM"]
LLM1 --> ANS1["❌ 无法处理图像"]
end
subgraph "多模态模型"
T2["用户: 这张图里有什么?"] --> MM["多模态模型"]
IMG["📷 图像"] --> MM
MM --> ANS2["✅ 图中有一只在草地上奔跑的金毛犬"]
end
style ANS1 fill:#e74c3c,color:#fff
style ANS2 fill:#2ecc71,color:#fff| 模态 | 应用场景 | 挑战 |
|---|---|---|
| 文本 | 对话、代码、翻译 | 已有成熟方案 |
| 图像 | OCR、医学影像、自动驾驶 | 像素 → 语义的鸿沟 |
| 音频 | 语音助手、会议纪要 | 时序 + 频域双重表示 |
| 视频 | 监控分析、内容审核 | 时空双重维度 |
| 3D/点云 | 机器人、AR/VR | 稀疏、不规则结构 |
CLIP — 多模态革命的起点
核心思想:对比学习桥接文本与图像
CLIP (Contrastive Language-Image Pre-training, OpenAI 2021) 的核心洞察:让图像和文本在同一个向量空间中对齐。一张狗的图片,应该和文字"一只狗"的向量距离很近,和文字"一辆汽车"距离很远。
mermaid
graph TD
subgraph "CLIP 训练流程"
IMG_B["图像批次<br/>N 张图片"] --> IMG_ENC["图像编码器<br/>ViT / ResNet"]
IMG_ENC --> IMG_EMB["图像嵌入 I<br/>(N × d)"]
TXT_B["文本批次<br/>N 条描述"] --> TXT_ENC["文本编码器<br/>Transformer"]
TXT_ENC --> TXT_EMB["文本嵌入 T<br/>(N × d)"]
IMG_EMB --> SIM["相似度矩阵<br/>S = I × Tᵀ<br/>(N × N)"]
TXT_EMB --> SIM
SIM --> LOSS["对比损失<br/>对角线 = 正样本<br/>其余 = 负样本"]
end
style LOSS fill:#e74c3c,color:#fff
style IMG_ENC fill:#3498db,color:#fff
style TXT_ENC fill:#2ecc71,color:#fff数学公式与直觉
CLIP 使用对称交叉熵损失,同时优化图像→文本和文本→图像两个方向:
其中每个方向的损失为:
| 符号 | 含义 | 直觉 |
|---|---|---|
| 第 i 张图与第 j 条文字的余弦相似度 | 配对程度 | |
| 对角线元素:正确的图文对 | 正样本 | |
| 温度参数(可学习) | 控制分布的"锐度" | |
| 正确配对的得分 | 越高越好 | |
| 所有配对的得分之和 | 分母,归一化 |
一句话理解:对每一张图片,模型要在 N 条文字描述中找到它的正确配对——相当于在 N 个选项中做单选题。N 越大训练越难,但最终模型越好。
python
"""
CLIP 对比损失的手工实现
"""
import torch
import torch.nn.functional as F
def clip_loss(image_embeddings: torch.Tensor,
text_embeddings: torch.Tensor,
temperature: float = 0.07) -> torch.Tensor:
"""
Args:
image_embeddings: (N, d) L2 归一化后的图像嵌入
text_embeddings: (N, d) L2 归一化后的文本嵌入
temperature: 温度参数
Returns:
loss: 标量损失
"""
# 计算相似度矩阵: (N, N)
logits = (image_embeddings @ text_embeddings.T) / temperature
# 对角线是正样本: 第 i 张图对应第 i 条文字
labels = torch.arange(len(image_embeddings),
device=image_embeddings.device)
# 对称损失: 图像找文字 + 文字找图像
loss_i2t = F.cross_entropy(logits, labels) # 图像→文本
loss_t2i = F.cross_entropy(logits.T, labels) # 文本→图像
return (loss_i2t + loss_t2i) / 2
# 数值示例: 4 对图文
N, d = 4, 512
torch.manual_seed(42)
img_emb = F.normalize(torch.randn(N, d), dim=-1) # 随机图像嵌入
txt_emb = F.normalize(torch.randn(N, d), dim=-1) # 随机文本嵌入
loss = clip_loss(img_emb, txt_emb)
print(f"CLIP Loss: {loss:.4f}")
# 理想情况: 对角线相似度高,其余低
sim = (img_emb @ txt_emb.T).detach()
print("\n相似度矩阵:")
print(f" {'T0':>6} {'T1':>6} {'T2':>6} {'T3':>6}")
for i in range(N):
vals = " ".join(f"{sim[i,j]:6.3f}" for j in range(N))
print(f"I{i} {vals}")CLIP 的训练规模
| 维度 | 规模 | 说明 |
|---|---|---|
| 训练数据 | 4 亿对图文 | 从互联网抓取,远超人工标注 |
| 图像编码器 | ViT-L/14 或 ResNet-50x64 | Visual Transformer 效果最佳 |
| 文本编码器 | 12 层 Transformer | 63M 参数 |
| Batch Size | 32,768 | 相当于每步做 32768 选 1 |
| 训练时间 | 约 2 周,256 GPU | — |
LLaVA — 面向对话的多模态 LLM
CLIP 的局限性
CLIP 只能判断"图片和文字是否匹配",不能描述图片内容,也不能回答关于图片的问题。LLaVA 将 CLIP 的视觉理解能力接入 LLM,使模型能像聊天一样理解图像。
mermaid
graph TD
subgraph "LLaVA 架构"
IMG["输入图像<br/>224×224"] --> VIT["CLIP ViT-L<br/>视觉编码器<br/>(冻结)"]
VIT --> PROJ["投影层 W<br/>d_visual → d_llm"]
PROJ --> LLM["LLM (Vicuna/LLaMA)<br/>处理图文混合序列"]
TXT["文本指令<br/>'描述这张图'"] --> LLM
LLM --> OUT["生成的回答<br/>'图中有一只在草地上...'"]
end
style VIT fill:#3498db,color:#fff
style PROJ fill:#e74c3c,color:#fff
style LLM fill:#2ecc71,color:#fff两阶段训练
| 阶段 | 训练内容 | 数据 | 目的 |
|---|---|---|---|
| 阶段1: 特征对齐 | 只训练投影层 W | ~558K 图文描述对 | 让视觉特征"说 LLM 的语言" |
| 阶段2: 指令微调 | 训练投影层 + LLM | ~158K 多模态指令 | 学会看图问答/推理 |
python
"""
LLaVA 的核心组件: 视觉投影 + 多模态序列拼接
"""
import torch
import torch.nn as nn
class LLaVAProjector(nn.Module):
"""将视觉特征投影到 LLM 的 embedding 空间"""
def __init__(self, d_visual: int = 1024, d_llm: int = 4096):
super().__init__()
self.proj = nn.Sequential(
nn.Linear(d_visual, d_llm),
nn.GELU(),
nn.Linear(d_llm, d_llm),
)
def forward(self, visual_features: torch.Tensor) -> torch.Tensor:
"""visual_features: (batch, num_patches, d_visual) → (batch, num_patches, d_llm)"""
return self.proj(visual_features)
def build_multimodal_input(
image: torch.Tensor,
prompt: str,
vision_encoder,
projector,
tokenizer,
max_length: int = 2048,
) -> dict:
"""
构建多模态输入: 图像 token + 文本 token
序列结构: [IMG_START] [图像特征1] ... [图像特征N] [IMG_END] [文本 tokens]
"""
# 1. 提取视觉特征
with torch.no_grad():
visual_features = vision_encoder(image) # (B, num_patches, d_visual)
# 2. 投影到 LLM 空间
projected = projector(visual_features) # (B, num_patches, d_llm)
# 3. 构建文本序列
# 格式: "<image>请描述这张图片</image>"
multimodal_prompt = f"<image>\n{prompt}\n</image>"
text_ids = tokenizer.encode(multimodal_prompt)
# 4. 插入图像 token 占位符
# 实际推理时,在 Embedding 层将占位符替换为 projected features
img_token_id = tokenizer.encode("<image>")[0]
return {
"visual_features": projected,
"text_ids": text_ids,
"img_token_positions": [i for i, tid in enumerate(text_ids)
if tid == img_token_id],
}LLaVA 的演进
| 版本 | 关键改进 | 亮点 |
|---|---|---|
| LLaVA 1.0 | CLIP ViT-L + Vicuna-13B | 首个开源多模态对话模型 |
| LLaVA 1.5 | 升级 ViT、MLP 投影、学术数据 | 超越多数闭源模型 |
| LLaVA 1.6 | 动态高分辨率、AnyRes | 支持任意分辨率输入 |
| LLaVA-NeXT | 更强的 LLM + 数据混合 | 接近 GPT-4V 水平 |
原生多模态 vs 拼接式多模态
mermaid
graph TD
subgraph "拼接式 (CLIP → LLM)"
A1["图像"] --> VENC["视觉编码器"]
VENC --> ADPT["适配层"]
A2["文本"] --> LLM_A["LLM"]
ADPT --> LLM_A
LLM_A --> OUT_A["输出"]
end
subgraph "原生多模态 (GPT-5.6 / Claude Fable)"
B1["图像"] --> UNI["统一 Transformer<br/>图文混排输入"]
B2["文本"] --> UNI
B3["音频"] --> UNI
UNI --> OUT_B["输出<br/>可生成文本/图像/音频"]
end
style OUT_B fill:#e74c3c,color:#fff| 维度 | 拼接式 (LLaVA 类) | 原生多模态 (GPT-5.6 类) |
|---|---|---|
| 训练方式 | 分阶段:先对齐再微调 | 从头联合训练 |
| 视觉编码器 | 独立预训练 (CLIP ViT) | Transformer 内部统一处理 |
| Token 化 | 图像→特征向量→投影 | 图像→离散 Token (类似文本) |
| 生成能力 | 仅输出文本 | 可输出文本/图像/音频 |
| 跨模态推理 | 中等 | 强(如看图定位+描述) |
| 部署复杂度 | 两个模型需要协调 | 单一模型 |
多模态模型的关键技术
1. 图像 Tokenization
将图像转换为 LLM 可以处理的 Token 序列:
| 方案 | 原理 | 代表 |
|---|---|---|
| Patch Embedding | 切分图像为固定 patch,线性投影 | ViT, LLaVA |
| 离散 VQ-VAE | 编码 → 在 codebook 中查找 → 离散 ID | DALL-E, GPT-5.6 |
| Resampler | 可学习的查询向量从特征图中提取信息 | Flamingo, Qwen-VL |
| SigLIP + MLP | 更强的对比学习视觉编码 + MLP 投影 | LLaVA 1.5+ |
2. 高分辨率处理
python
"""
LLaVA 1.6 的 AnyRes: 动态切分高分辨率图像
"""
def anyres_split(image, base_size=336, max_tiles=4):
"""
将大图切分为多个 base_size × base_size 的 tile
例如 1024×768 的图:
- 横向: ceil(1024/336) = 4 tiles
- 纵向: ceil(768/336) = 3 tiles
- 最多保留 max_tiles 个 tile
"""
h, w = image.shape[-2:]
# 计算切分数量
num_h = min((h + base_size - 1) // base_size, max_tiles)
num_w = min((w + base_size - 1) // base_size, max_tiles)
tiles = []
for i in range(num_h):
for j in range(num_w):
y1 = i * base_size
x1 = j * base_size
tile = image[:, :, y1:y1+base_size, x1:x1+base_size]
tiles.append(tile)
# 同时保留缩略图(原图 resize 到 base_size)
from torch.nn.functional import interpolate
thumbnail = interpolate(image, size=(base_size, base_size),
mode='bilinear', align_corners=False)
return thumbnail, tiles3. 跨模态注意力模式
| 模式 | 结构 | 适用 |
|---|---|---|
| Cross-Attention | 文本 Q → 图像 K,V | Flamingo |
| Self-Attention 拼接 | 图文序列直接拼接后做自注意力 | LLaVA, GPT-5.6 |
| Q-Former | 可学习 Queries 压缩视觉信息 | BLIP-2 |
| MLP 投影 | 最简单的线性/nonlinear 投影 | LLaVA 1.0-1.5 |
多模态评测基准
| 基准 | 维度 | 说明 |
|---|---|---|
| MMBench | 综合 | 20 个能力维度,中英文双语 |
| MME | 感知 + 认知 | 14 个子任务 |
| SEED-Bench | 空间/时间推理 | 19K 多选题 |
| MMMU | 专家级多学科 | 6 大学科,30+ 子领域 |
| OCRBench | 文字识别 | 29 个 OCR 相关任务 |
| Video-MME | 视频理解 | 多帧视频问答 |
参考
- Learning Transferable Visual Models From Natural Language Supervision (CLIP) — Radford et al., 2021
- Visual Instruction Tuning (LLaVA) — Liu et al., 2023
- LLaVA 1.5 — Liu et al., 2023
- BLIP-2: Bootstrapping Language-Image Pre-training — Li et al., 2023
- Flamingo: a Visual Language Model for Few-Shot Learning — Alayrac et al., 2022
- An Image is Worth 16x16 Words (ViT) — Dosovitskiy et al., 2020
登录后即可发表评论 👇