第13章 多模态学习¶
📌 本章定位:视觉任务中的实际应用
本章侧重多模态学习在计算机视觉任务中的实际应用,包括: - CLIP在视觉任务中的零样本分类与检索应用 - 前沿VLM架构对比(LLaVA、InternVL、Qwen-VL等) - 多模态RAG、VQA、图文检索等实际应用场景 - 实战项目与模型部署
🔗 相关章节导航: | 侧重点 | 章节 | 说明 | |--------|------|------| | CV应用 | 👉 本文档 | VLM架构对比、实战项目、部署应用 | | 理论原理 | 深度学习/07-多模态学习 | 数学推导、算法原理、融合策略理论 | | 大模型 | LLM学习/多模态大模型 | GPT-4o/Gemini等多模态大模型 | | 具身智能 | 具身智能/VLA模型 | 视觉-语言-动作模型 |
⚠️ 时效性说明:本章涉及前沿模型/价格/榜单等信息,可能随版本快速变化;请以论文原文、官方发布页和 API 文档为准。
📚 章节概述¶
本章深入讲解多模态学习的核心技术,从经典CLIP到最新的视觉语言大模型(VLM),覆盖BLIP-2、LLaVA、InternVL、Qwen-VL等前沿模型的架构对比与实战应用。多模态学习是2025-2026年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 多模态融合策略¶
┌─────────────────────────────────────────────────────────────────┐
│ 三种融合策略对比 │
├─────────────────┬─────────────────┬───────────────────────────┤
│ 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 多模态学习范式演进¶
多模态学习范式演进
时间 ──────────────────────────────────────────────→
2021 2022 2023 2024 2025
CLIP → BLIP → BLIP-2 → LLaVA-1.5 → Qwen2-VL
CoCa InstructBLIP InternVL InternVL2.5
Qwen-VL GPT-4o
范式: 对比学习 → 生成式预训练 → 冻结LLM+桥接 → 端到端VLM
📝 面试考点:多模态融合有哪几种策略?各自的优缺点?CLIP属于哪种?
13.2 CLIP 深度解析¶
13.2.1 对比学习原理¶
CLIP(Contrastive Language-Image Pre-training)是OpenAI在2021年提出的跨模态对比学习模型。
InfoNCE Loss 推导:
给定一个batch中的 \(N\) 个图文对 \((I_i, T_i)\),目标是让匹配的图文对相似度高,不匹配的相似度低。
图像到文本方向的损失:
其中 \(\text{sim}(a, b) = \frac{a \cdot b}{\|a\| \|b\|}\) 为余弦相似度,\(\tau\) 为可学习温度参数。
文本到图像方向的损失类似:
总损失为对称形式:
温度参数τ的作用: - \(\tau\) 越小 → softmax分布越尖锐 → 模型更关注最难的负样本 - \(\tau\) 越大 → softmax分布越平滑 → 最终梯度更均匀 - CLIP中 \(\tau\) 是可学习参数,初始值为 \(1/0.07 \approx 14.3\)
13.2.2 CLIP架构¶
┌──────────────────────────────────────────────────┐
│ 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完整实现¶
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进行零样本分类¶
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使用什么损失函数?温度参数τ的作用?为什么CLIP能做零样本分类?CLIP有哪些局限性?
13.3 CLIP后续改进¶
13.3.1 SigLIP:Sigmoid Loss替代Softmax¶
核心问题:CLIP的InfoNCE损失需要在batch内做Softmax归一化,要求batch内的所有样本对可见,限制了分布式扩展性。
SigLIP改进:将Softmax替换为独立的Sigmoid,每个图文对独立判断匹配/不匹配:
其中 \(\sigma\) 是Sigmoid函数,\(b\) 是可学习偏置。
关键优势:
| 对比维度 | CLIP (Softmax) | SigLIP (Sigmoid) |
|---|---|---|
| 归一化范围 | 整个batch | 单个样本对 |
| 分布式通信 | 需要all-gather | 无需全局同步 |
| Batch Size扩展 | 受Softmax约束 | 可自由扩展 |
| 负样本权重 | Softmax自适应 | 均等权重 |
| 性能 | 基线 | 持平或更优 |
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 | 百万级中文 | 轻量级部署 |
# 使用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有利于大规模分布式训练?
13.4 BLIP与BLIP-2¶
13.4.1 BLIP:CapFilt自举训练¶
BLIP(Bootstrapping Language-Image Pre-training)的核心创新:
网络爬取的嘈杂图文对
↓
┌───────────────────┐
│ BLIP模型训练 │ ← 用嘈杂数据初始训练
└───────────────────┘
↓
┌───────────────────┐ ┌───────────────────┐
│ Captioner │ │ Filter │
│ 为图像生成描述 │ │ 过滤不匹配图文对 │
└───────────────────┘ └───────────────────┘
↓ ↓
合成高质量图文对 清洗后的网络图文对
└──────────┬──────────┘
↓
更高质量的训练数据
↓
重新训练BLIP → 更好的模型
13.4.2 BLIP-2架构详解¶
核心思想:使用轻量级Q-Former桥接冻结的Vision Encoder和冻结的LLM,实现参数高效的多模态对齐。
┌────────────────────────────────────────────────────────┐
│ 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)
Query tokens ──→ Self-Attn ──→ Cross-Attn(←视觉) ──→ 图像表示
Text tokens ──→ Self-Attn ──────────────────────→ 文本表示
↕ 对比损失
- Queries通过Cross-Attention提取视觉信息
- 关键:ITC阶段不让Queries看到文本(单模态对齐)
阶段二:Image-grounded Text Generation (ITG)
Query tokens ──→ Self-Attn ──→ Cross-Attn(←视觉) ──→ 条件
Text tokens ──→ Causal Self-Attn(与Queries共享) ──→ 生成文本
- Queries提供视觉条件,引导文本生成
- 使用因果注意力掩码
阶段三:Image-Text Matching (ITM)
- Query和Text双向交互
- 输出匹配/不匹配的二分类结果
- 使用hard negative mining
| 阶段 | 任务 | Query-Text交互 | 目标 |
|---|---|---|---|
| ITC | 对比学习 | 不交互 | 视觉-文本对齐 |
| ITG | 文本生成 | 因果自注意力 | 生成能力 |
| ITM | 匹配判断 | 双向自注意力 | 细粒度匹配 |
13.4.4 Q-Former简化实现¶
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进行视觉问答¶
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?Q-Former的32个query token如何工作?三阶段训练分别学了什么?
13.5 视觉语言大模型(VLM)深度对比¶
13.5.1 LLaVA系列¶
LLaVA(Large Language and Vision Assistant)的核心特点:极简架构设计。
┌──────────────────────────────────────────────────┐
│ 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¶
核心创新:
- InternViT-6B:参数规模达6B的开源视觉编码器,是目前开源社区中最大级别的视觉编码器之一(GitHub)
- 动态分辨率(Dynamic Resolution):
输入图像 (800×600)
↓
计算最优分割方案(适配448×448网格)
↓
┌────┬────┐
│ 子图1│ 子图2│ → 每个子图独立通过ViT编码
├────┼────┤
│ 子图3│ 子图4│ → 拼接所有子图特征
└────┴────┘
+ 缩略图特征
↓
送入LLM
# 使用InternVL2进行多模态对话
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
from transformers import Qwen2VLForConditionalGeneration, AutoProcessor
# 加载Qwen2-VL
model = Qwen2VLForConditionalGeneration.from_pretrained(
"Qwen/Qwen2-VL-7B-Instruct",
torch_dtype=torch.bfloat16,
device_map="auto"
)
processor = AutoProcessor.from_pretrained("Qwen/Qwen2-VL-7B-Instruct")
# 多模态对话
messages = [
{
"role": "user",
"content": [
{"type": "image", "image": "test.jpg"},
{"type": "text", "text": "这张图片里有什么?请用中文详细描述。"}
]
}
]
text = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = processor(text=[text], images=[Image.open("test.jpg")], return_tensors="pt").to("cuda")
output_ids = model.generate(**inputs, max_new_tokens=256)
output = processor.batch_decode(output_ids, skip_special_tokens=True)[0]
print(output)
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综合对比¶
📊 数据时效性说明:以下对比数据截至2026年2月。VLM领域发展极快,性能对比数据可能每月都有变化,请以各模型官方发布页和最新Benchmark为准。
| 模型 | Vision Encoder | 连接方式 | LLM Backbone | 分辨率 | 中/英 | 参数量 |
|---|---|---|---|---|---|---|
| LLaVA-1.5-13B | CLIP ViT-L/14 | 2层MLP | Vicuna-13B | 336 | 弱/强 | ~13B |
| BLIP-2 | ViT-G/14(冻结) | Q-Former | OPT-2.7B | 224 | 弱/中 | ~4B |
| InternVL2-8B | InternViT-6B | MLP | InternLM2 | Dynamic | 强/强 | ~8B |
| Qwen2-VL-7B | ViT(定制) | Cross-Attn | Qwen2-7B | Dynamic | 极强/强 | ~8B |
| Qwen2-VL-72B | ViT(定制) | Cross-Attn | Qwen2-72B | Dynamic | 极强/强 | ~77B |
| GPT-4o | 未公开 | 未公开 | GPT-4 | 高分辨率 | 强/极强 | 未公开 |
| Claude 3.5 | 未公开 | 未公开 | Claude | 高分辨率 | 强/极强 | 未公开 |
| Gemini 1.5 | 未公开 | 未公开 | Gemini | 高分辨率 | 强/极强 | 未公开 |
面试推荐选型: - 中文场景首选:Qwen2-VL > InternVL2 - 开源研究首选:InternVL2 > LLaVA-NeXT - 参数高效方案:BLIP-2(Q-Former压缩) - 工业部署首选:Qwen2-VL-2B(端侧)/ 7B(云端)
📝 面试考点:LLaVA和BLIP-2架构核心区别?Linear Projection vs Q-Former各自优劣?动态分辨率为什么重要?
13.6 多模态应用实战¶
13.6.1 图文检索系统¶
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系统¶
"""
多模态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.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].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].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].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构建问答系统¶
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/对比解码缓解 |
技术选型建议¶
VLM选型决策树
│
┌─── 中文场景?──── 否 ───→ LLaVA-NeXT / GPT-4o
│ │
│ 是
│ │
├── 端侧部署? ─── 是 ───→ Qwen2-VL-2B
│ │
│ 否
│ │
├── 需要OCR? ─── 是 ───→ Qwen2-VL-7B / InternVL2-8B
│ │
│ 否
│ │
└── 通用对话 ─────────→ InternVL2-8B / Qwen2-VL-7B
下一步¶
下一章:14-自监督学习.md - 学习自监督视觉学习
恭喜完成第13章! 🎉