多模态大模型 VLM 架构:从模态对齐到 GPT-4V 的完整技术栈
1. VLM 的诞生:从单模态到多模态
1.1 为什么需要视觉语言模型?
大型语言模型(LLM)在文本理解和生成上取得了巨大成功,但人类世界是多模态的——图像、视频、音频与文本共存。VLM 的目标是让模型能够:
输入: [图像 + 文本] → 输出: 文本典型能力:
- 看图说话(图像描述)
- 视觉问答(VQA)
- 文档理解(OCR + 理解)
- 视觉推理(多步推理)
- 视频理解(帧序列)
1.2 一句话概括 VLM
VLM = 视觉编码器(理解图像)+ 模态对齐层(把图像特征映射到文本空间)+ LLM 骨干(理解和生成文本)。核心挑战是让两个异构模态(像素 vs 词元)在特征空间中对齐。
1.3 VLM 的演进时间线
2017 - Attention Is All You Need (Transformer)2021 - CLIP (Radford et al.) — 视觉语言预训练 - ALIGN (Jia et al.) — 大规模双塔模型
2023 - Flamingo (Alayrac et al.) — 冻结 LLM + Perceiver Resampler - BLIP-2 (Li et al.) — 轻量级 Q-Former 桥接器 - LLaVA (Liu et al.) — MLP 投影 + 指令微调 ★ - InstructBLIP (Liu et al.) — 指令感知 Q-Former - GPT-4V (OpenAI) — 多模态 GPT-4
2024 - LLaVA-1.5 (Liu et al.) — MLP 投影 + OCR 数据 ★ - Qwen-VL (Alibaba) — 大模型 + 全模态支持 - CogVLM (Tsinghua) — 深度视觉语言融合 - InternVL (Shanghai AI Lab) — 大视觉编码器 - MiniGPT-4v2 — 高效多模态2. 视觉编码器:理解图像的第一步
2.1 CLIP 系列视觉编码器
VLM 的视觉编码器几乎都基于 CLIP 风格的 Vision Transformer (ViT)。CLIP 的预训练目标是图文对比学习:
正样本对: (图1, 描述1), (图2, 描述2), ...负样本对: (图1, 描述2), (图2, 描述1), ...
InfoNCE 损失:L = -log exp(sim(图像, 正确文本) / sum_i exp(sim(图像, 文本_i)))CLIP 训练数据规模达 4 亿图文对,模型学会了丰富的视觉语义表示。这种预训练权重是 VLM 视觉编码器的首选。
from transformers import CLIPVisionModel, AutoImageProcessor
class CLIPVisionEncoder: """ 基于 CLIP ViT 的视觉编码器。 输出: patch embeddings + [CLS] token """
def __init__(self, model_name="openai/clip-vit-large-patch14"): self.processor = AutoImageProcessor.from_pretrained(model_name) self.model = CLIPVisionModel.from_pretrained(model_name) # CLIP ViT-L: 1024 维输出, 256 个 patch tokens self.hidden_size = self.model.config.hidden_size # 1024 self.num_patches = 256
def encode(self, images): """ 参数: images: PIL Image 或 list[PIL Image] 或 batch tensor 返回: image_embeds: (B, num_patches+1, hidden_size) [0] 是 [CLS] token, [1:] 是 patch tokens """ if isinstance(images, list): inputs = self.processor(images, return_tensors="pt") else: inputs = self.processor(images, return_tensors="pt")
outputs = self.model(**inputs) # pooled output (CLS) + last hidden state return outputs.last_hidden_state # (B, 257, 1024)2.2 视觉编码器的选择
| 编码器 | 规模 | 视觉表示质量 | 计算量 | 典型用途 |
|---|---|---|---|---|
| CLIP ViT-S | 22M | 中等 | 低 | 轻量 VLM |
| CLIP ViT-B | 86M | 良好 | 中 | LLaVA-1.5 以前 |
| CLIP ViT-L | 304M | 优秀 | 高 | LLaVA-1.5, InstructBLIP |
| EVA-CLIP | 4B | 极佳 | 极高 | InternVL, 大模型 |
| SigLIP | 1B | 极佳 | 高 | Gemma-3, 端侧 |
| DINOv2 | 86M-304M | 优秀(无文本监督) | 中 | 视觉任务专用 |
2.3 视觉编码器配置参数
@dataclassclass VisionEncoderConfig: """视觉编码器超参数。""" model_type: str = "clip" # clip, dinov2, siglip, eva image_size: int = 336 # 输入分辨率 patch_size: int = 14 # patch 大小 (ViT-14) hidden_size: int = 1024 # 隐藏层维度 num_hidden_layers: int = 24 # ViT-L: 24层 num_attention_heads: int = 16 # attention heads intermediate_size: int = 4096 # FFN 中间维度 num_patches: int = None # 自动计算: (image_size/patch_size)^23. 模态对齐层:连接视觉与语言
3.1 为什么需要对齐层?
图像 tokens: 连续特征向量 (B, N_v, D_v) — D_v = 1024 (CLIP ViT-L)文本 tokens: 离散词元嵌入 (B, N_l, D_l) — D_l = 4096 (Llama-3 8B)
问题: D_v ≠ D_l,且语义空间不同方案: 用一个轻量网络把视觉特征映射到 LLM 的输入空间3.2 线性投影(最简方案)
最简单的对齐:用一层线性变换:
class LinearProjector(nn.Module): """线性投影层。""" def __init__(self, vision_dim, language_dim): super().__init__() self.proj = nn.Linear(vision_dim, language_dim)
def forward(self, image_embeds): return self.proj(image_embeds) # (B, N_v, D_l)缺点:表达能力有限,只做仿射变换,无法捕捉复杂的跨模态关系。
3.3 MLP 投影(LLaVA 方案)
LLaVA 发现用两层 MLP(含 GELU 激活)比单层线性投影效果好得多:
class MLPProjector(nn.Module): """ LLaVA 的 MLP 投影器。 vision_dim → intermediate_dim → language_dim """ def __init__(self, vision_dim, language_dim, intermediate_dim=None): super().__init__() if intermediate_dim is None: intermediate_dim = language_dim * 2 self.layers = nn.Sequential( nn.Linear(vision_dim, intermediate_dim), nn.GELU(), nn.Linear(intermediate_dim, language_dim), )
def forward(self, image_embeds): return self.layers(image_embeds)class LLaVAConnector(nn.Module): """ LLaVA-1.5 的模态连接器。 包含: 1. 线性投影 + LayerNorm 2. 2层 MLP + GELU """ def __init__(self, vision_dim, language_dim): super().__init__() self.proj = nn.Sequential( nn.Linear(vision_dim, language_dim), nn.GELU(), nn.Linear(language_dim, language_dim), nn.LayerNorm(language_dim), )
def forward(self, image_embeds): # image_embeds: (B, N_v+1, D_v), 去掉 [CLS] token visual_tokens = image_embeds[:, 1:, :] # (B, N_v, D_v) return self.proj(visual_tokens) # (B, N_v, D_l)3.4 Cross-Attention 投影(Flamingo 方案)
Flamingo 使用门控交叉注意力层把视觉特征注入语言模型:
class FlamingoGatedCrossAttention(nn.Module): """ Flamingo 的门控交叉注意力层。 视觉特征作为 K/V,文本特征作为 Q。 """ def __init__(self, lang_dim, vision_dim, num_heads=8): super().__init__() self.attn = nn.MultiheadAttention(lang_dim, num_heads, batch_first=True) self.q_proj = nn.Linear(lang_dim, lang_dim) self.kv_proj = nn.Linear(vision_dim, lang_dim * 2) # 门控参数(初始化为 0,允许渐进式引入视觉信息) self.gate = nn.Parameter(torch.zeros(1))
def forward(self, lang_embeds, vision_embeds): """ 参数: lang_embeds: (B, N_l, D_l) 语言 token 序列 vision_embeds: (B, N_v, D_v) 视觉 token 序列 返回: attended: (B, N_l, D_l) 带视觉信息的语言表示 """ q = self.q_proj(lang_embeds) # (B, N_l, D_l) kv = self.kv_proj(vision_embeds) # (B, N_v, 2*D_l) k, v = kv.chunk(2, dim=-1) # 各 (B, N_v, D_l)
attn_out, _ = self.attn(q, k, v, need_weights=False) # (B, N_l, D_l) # 门控: 逐步引入视觉信息(训练初期 gate→0,退化为纯文本) return lang_embeds + self.gate.tanh() * attn_out3.5 Q-Former(BLIP-2 方案)
BLIP-2 提出了轻量级 Q-Former 作为模态桥接器:
class QFormer(nn.Module): """ BLIP-2 的 Q-Former。 用 12 层 Cross-Attention 从视觉特征中查询出 32 个表示。 """ def __init__(self, vision_dim, lang_dim, num_queries=32, num_layers=12): super().__init__() self.num_queries = num_queries # 可学习的查询向量 self.query_tokens = nn.Parameter(torch.zeros(1, num_queries, lang_dim)) # 12 层 Cross-Attention + Self-Attention self.layers = nn.ModuleList([ TransformerEncoderLayer( d_model=lang_dim, nhead=8, dim_feedforward=lang_dim * 4, batch_first=True, ) for _ in range(num_layers) ])
def forward(self, vision_embeds): """ vision_embeds: (B, N_v, D_v) 来自视觉编码器 返回: (B, num_queries, D_l) 32 个查询表示 """ B = vision_embeds.shape[0] queries = self.query_tokens.expand(B, -1, -1) # (B, 32, D_l)
for layer in self.layers: # 每层: self-attn → cross-attn(以视觉为 K/V) queries = layer(queries)
return queries # (B, 32, D_l) ← 固定长度,兼容 LLM3.6 四种方案对比
| 方案 | 参数量 | 视觉信息保留 | 与 LLM 的耦合方式 | 典型模型 |
|---|---|---|---|---|
| 线性投影 | ~4M | 最低 | 直接拼接 | 早期 VLM |
| MLP 投影 | ~20M | 中等 | 直接拼接 | LLaVA ★ |
| Cross-Attention | ~50M | 高 | 注入式 | Flamingo |
| Q-Former | ~100M | 高 | 查询式 | BLIP-2 |
4. LLM 骨干:理解与生成的核心
4.1 常用 LLM 骨干
VLM 的 LLM 骨干通常是开源的文本 LLM。典型选择:
| LLM 骨干 | 参数量 | 维度 | 特点 |
|---|---|---|---|
| Vicuna | 7B/13B | 4096 | 基于 Llama,微调对话 |
| Llama-3 | 8B/70B | 4096/8192 | 强大的基础能力 |
| Mistral | 7B | 4096 | 滑动窗口注意力 |
| Qwen-2 | 7B/72B | 3584/8192 | 中文优化 |
| Phi-3 | 3.8B | 3072 | 微软,小而强 |
from transformers import AutoModelForCausalLM, AutoTokenizer
class VLLMBackbone: """VLM 的 LLM 骨干。"""
def __init__(self, model_name="Qwen/Qwen2-7B"): self.tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True) self.model = AutoModelForCausalLM.from_pretrained( model_name, trust_remote_code=True, torch_dtype=torch.float16, device_map="auto", ) self.eos_token_id = self.tokenizer.eos_token_id
def generate(self, input_ids, image_embeds, max_new_tokens=512): """给定文本 token 和图像 embedding,生成文本。""" return self.model.generate( input_ids=input_ids, max_new_tokens=max_new_tokens, do_sample=True, temperature=0.7, )4.2 视觉 token 的注入方式
视觉特征通过特殊 token 注入 LLM:
文本 token 序列:[USER] [IMG] patch patch ... patch [IMG] 请描述这张图 [/IMG] [EOS] ↑视觉特征由投影器映射后插入↑LLaVA 在 tokenizer 中添加了 [ IMG ] 和 [ / IMG ] 两个特殊 token:
special_tokens = ["[ IMG ]", "[ / IMG ]"]num_new_tokens = tokenizer.add_tokens(special_tokens)model.resize_token_embeddings(len(tokenizer))
# 查找 token IDsimg_start_id = tokenizer("[ IMG ]", add_special_tokens=False).input_ids[0]img_end_id = tokenizer("[ / IMG ]", add_special_tokens=False).input_ids[0]5. 训练范式:从预训练到指令微调
5.1 三阶段训练流程
┌─────────────────────────────────────────────────────────┐│ 阶段 1: 模态预训练 (Projector Pretraining) ││ 目标: 让投影器学会把视觉特征映射到 LLM 空间 ││ 数据: 图像-文本对 (CC3M, LAION, 约 600M) ││ 目标: 预测正确文本 (语言建模损失) ││ LLM: 冻结 ❄️ ││ 投影器: 训练 🔥 │├─────────────────────────────────────────────────────────┤│ 阶段 2: 视觉指令微调 (Vision Instruction Tuning) ││ 目标: 让模型学会根据视觉内容回答问题 ││ 数据: 指令微调数据集 (LLaVA-Instr-80K, 等) ││ 目标: 预测正确回答 ││ LLM: 解冻 🔥 (LoRA 或全量) ││ 投影器: 训练 🔥 │├─────────────────────────────────────────────────────────┤│ 阶段 3: 对齐微调 (可选, RLHF/DPO) ││ 目标: 进一步对齐人类偏好 ││ 数据: 人类偏好数据 (LLaVA-RLHF, 等) ││ LLM: 解冻 🔥 │└─────────────────────────────────────────────────────────┘5.2 阶段 1:模态预训练
阶段 1 的核心任务是训练投影器,最小化视觉条件下的语言建模损失:
其中 是图像特征, 是已经生成的文本 token。
def pretrain_stage(projector, vision_encoder, llm, dataset): """ 阶段 1: 模态预训练。 冻结 LLM 和视觉编码器,只训练投影器。 """ vision_encoder.requires_grad_(False) llm.requires_grad_(False) projector.requires_grad_(True)
optimizer = torch.optim.AdamW(projector.parameters(), lr=1e-3, weight_decay=0.05)
for batch in DataLoader(dataset, shuffle=True): images, texts = batch["image"], batch["text"]
# 视觉编码 with torch.no_grad(): vision_embeds = vision_encoder(images) # (B, N_v+1, D_v)
# 投影 visual_tokens = projector(vision_embeds) # (B, N_v, D_l)
# 文本 tokenize input_ids = tokenizer(texts, return_tensors="pt", padding=True, truncation=True) input_ids = {k: v.to(device) for k, v in input_ids.items()}
# 在文本序列中嵌入视觉 token # 简单实现: 在 [CLS] 位置替换为视觉 token input_embeds = llm.get_input_embeddings()(input_ids["input_ids"]) input_embeds[:, 0:1, :] = visual_tokens[:, 0:1, :] # 替换第一个位置
# 前向传播 outputs = llm(inputs_embeds=input_embeds, attention_mask=input_ids["attention_mask"], labels=input_ids["input_ids"]) loss = outputs.loss
loss.backward() optimizer.step() optimizer.zero_grad()5.3 阶段 2:视觉指令微调
阶段 2 解冻 LLM(或用 LoRA),用指令数据训练:
def instruction_tune_stage(projector, vision_encoder, llm, dataset): """ 阶段 2: 视觉指令微调。 解冻 LLM + 投影器,用指令数据训练。 """ # LoRA 配置(节省显存) from peft import LoraConfig, get_peft_model lora_config = LoraConfig( r=64, lora_alpha=16, target_modules=["q_proj", "v_proj", "k_proj", "o_proj"], lora_dropout=0.05, bias="none", task_type="CAUSAL_LM", ) llm = get_peft_model(llm, lora_config)
projector.requires_grad_(True) vision_encoder.requires_grad_(False) # 保持冻结
optimizer = torch.optim.AdamW( list(projector.parameters()) + list(llm.parameters()), lr=2e-5, weight_decay=0.0, )
for batch in DataLoader(dataset): images, instructions, responses = batch["image"], batch["instruction"], batch["response"]
# 构建多模态输入 # 格式: [SYSTEM] + [IMG] + [USER 问题] + [ASSISTANT 回答] prompts = [ f"[ SYSTEM ] 你是一个视觉助手。[ IMG ] [ / IMG ] [ USER ] {inst} [ ASSISTANT ]" for inst in instructions ]
input_ids = tokenizer(prompts, return_tensors="pt", padding=True).to(device) labels = tokenizer(responses, return_tensors="pt", padding=True, truncation=True, max_length=512).to(device)
# 视觉编码 + 投影 vision_embeds = vision_encoder(images) visual_tokens = projector(vision_embeds)
# 获取文本 embedding 并嵌入视觉 token input_embeds = llm.get_input_embeddings()(input_ids["input_ids"])
# 在 [IMG] token 位置插入视觉 token for b in range(input_embeds.shape[0]): img_positions = (input_ids["input_ids"][b] == img_start_id).nonzero(as_tuple=True)[0] if len(img_positions) > 0: img_pos = img_positions[0].item() end_pos = (input_ids["input_ids"][b] == img_end_id).nonzero(as_tuple=True)[0][0].item() num_v_tokens = end_pos - img_pos input_embeds[b, img_pos:img_pos + num_v_tokens, :] = visual_tokens[b, :num_v_tokens, :]
outputs = llm(inputs_embeds=input_embeds, attention_mask=input_ids["attention_mask"], labels=labels["input_ids"]) loss = outputs.loss loss.backward() optimizer.step() optimizer.zero_grad()5.4 训练数据构成
LLaVA-1.5 的训练数据由三部分构成:
def get_training_data(): """ LLaVA-1.5 的训练数据混合。 """ data_mixture = [ # 纯语言指令数据(来自 Vicuna/Llama 训练集) ("llava-instruct-150k", 3_000_000, "语言指令微调"), ("sharegpt", 900_000, "对话数据"),
# 视觉-语言数据 ("llava-lcs-558k", 558_000, "图像描述 (预训练)"), ("llava-instruct-665k", 665_000, "图像指令 (微调)"), ("laion-cc-sbu-558k", 558_000, "图文对 (预训练)"),
# 特定能力数据 ("ocr", 200_000, "文档/OCR 理解"), ("chart", 100_000, "图表理解"), ("docvqa", 50_000, "文档问答"), ("gqa", 80_000, "视觉推理问答"), ] return data_mixture6. 典型 VLM 架构对比
6.1 LLaVA 系列
LLaVA (2023): CLIP ViT-L/14 (冻结) → MLP → Vicuna-7B (全量训练) 架构最简单,数据效率最高
LLaVA-1.5 (2023): CLIP ViT-L/14-336px (冻结) → MLP → Vicuna-7B (LoRA) 关键改进: 1. MLP 替换线性投影 2. 336px 高分辨率 (比 224px 多 2.25 倍像素) 3. 大量 OCR/文档数据训练 4. 更均衡的训练数据混合 结果: 在 MMMU 达到 36.7%, 超越 GPT-4V(2023)6.2 BLIP-2 / InstructBLIP
BLIP-2: EVA-CLIP ViT-G (冻结) → Q-Former (训练) → Llama-2 (冻结) → Flan-T5 (可选) 核心: Q-Former 把 257 个视觉 token 压缩成 32 个查询表示
InstructBLIP: 改进: Q-Former 学会根据指令选择相关视觉区域 训练: 13 个视觉语言数据集联合训练 特点: 多任务通用性强,但推理速度慢(Q-Former 额外计算)6.3 Qwen-VL 系列
Qwen-VL (2023): OpenCLIP ViT-G (冻结) → 线性投影 → Qwen-7B (全量训练)
Qwen-VL2 (2024): 关键创新: 动态分辨率切分 - 图像被切分成多个子图(patch) - 每个子图独立编码 - 避免长序列 attention 爆炸 支持: 图像、视频、文本三模态
Qwen2-VL (2025): - 滑动窗口注意力(处理高分辨率) - 全模态: 图像 + 视频 + 音频 + 文本 - 72B 参数版本6.4 GPT-4V (2023) 架构推测
虽然 OpenAI 未公开 GPT-4V 的架构,但通过系统提示和输出行为分析,可以推测:
GPT-4V 架构推测: 视觉编码器: 类似 CLIP 的大规模 ViT (可能 1B+ 参数) LLM 骨干: GPT-4 的语言模型 模态对齐: 深度融合层(非简单的投影)
关键能力来源: 1. 预训练: 数万亿图文对(远超开源数据规模) 2. 指令微调: 大量人类标注的 VQA 数据 3. 安全对齐: RLHF / Constitutional AI
GPT-4V 能力 vs 开源 VLM 差距: - 视觉推理: GPT-4V 显著更强(复杂多步推理) - OCR / 文档: GPT-4V 相当(开源模型追上) - 视觉描述: GPT-4V 略强(语言生成质量)7. 关键技术细节
7.1 高分辨率图像处理
CLIP ViT 默认处理 224×224 或 336×336 图像。对于高分辨率输入(文档、图表),常用方法:
方法 1: 直接放大 (LLaVA-1.5) - 输入分辨率: 336×336 - 优点: 简单 - 缺点: 丢失细节,计算量随分辨率平方增长
方法 2: 分块编码 (Qwen-VL2) - 原始图像: 1440×960 → 切分成 9 个 480×480 子图 - 每个子图独立编码 → 9x256=2304 个 tokens - 优点: 保留高分辨率细节 - 缺点: token 数量爆炸
方法 3: 注意力池化 (InternVL) - 大视觉编码器 (ViT-L → ViT-G) - 全局 + 局部特征融合 - 自适应 token 数量def split_image_for_high_res(image, patch_size=480, overlap=32): """ 图像分块(Qwen-VL2 风格)。 把高分辨率图像切分成重叠的子图。 """ W, H = image.size patches = [] coords = []
stride = patch_size - overlap for y in range(0, H, stride): for x in range(0, W, stride): # 裁剪子图 patch = image.crop((x, y, x + patch_size, y + patch_size)) patches.append(patch) coords.append((x, y))
return patches, coords
def dynamic_res_encode(images, vision_encoder, projector, max_tokens=1024): """ 动态分辨率编码。 自动选择最佳切分策略。 """ patches, coords = split_image_for_high_res(images[0])
# 批量编码所有 patch all_embeds = [] for patch in patches: embed = vision_encoder(patch) # (1, 257, D_v) proj = projector(embed) # (1, 256, D_l) all_embeds.append(proj[:, 1:, :]) # 去掉 [CLS]
# 拼接所有 patch tokens visual_tokens = torch.cat(all_embeds, dim=1) # (1, N_patches*256, D_l)
# 如果 token 过多,用 attention 池化 if visual_tokens.shape[1] > max_tokens: # 全局 token + 局部 token 混合 global_token = visual_tokens[:, :256, :].mean(dim=1, keepdim=True) local_tokens = visual_tokens[:, :max_tokens-1, :] visual_tokens = torch.cat([global_token, local_tokens], dim=1)
return visual_tokens7.2 视觉 Token 数量控制
VLM 的一个核心问题是 token 数量爆炸:
@dataclassclass TokenBudget: """ 多模态输入的 token 预算管理。 """ max_vision_tokens: int = 1024 # 视觉 token 上限 max_text_tokens: int = 4096 # 文本 token 上限 vision_token_per_image: int = 256 # CLIP ViT-14: 256 patches
def fit_in_budget(self, num_images, num_text_tokens): """检查是否超出预算。""" total = num_images * self.vision_token_per_image + num_text_tokens return total <= (self.max_vision_tokens + self.max_text_tokens)7.3 长上下文视频理解
视频是帧序列 + 音频 + 字幕的多模态流:
视频 token 预算示例 (30 秒 30fps 视频): - 帧采样: 每 1 秒 1 帧 → 30 帧 - 每帧视觉 token: 256 - 总视觉 token: 30 × 256 = 7680 - 加上文本/字幕 → 可能超 LLM 上下文窗口
解决方案: 1. 帧采样: 均匀采样或基于内容重要性采样 2. 视频 token 压缩: 用轻量网络把帧特征压缩 3. 时序建模: VideoLLM 或 VideoChat 的滑动窗口7.4 MoE VLM: 稀疏多模态
class MoEVLM(nn.Module): """ 基于 MoE 的 VLM。 视觉和文本 token 共享 MoE LLM 骨干。 """ def __init__(self, vision_dim, num_experts=8, top_k=2): super().__init__() self.projector = MLPProjector(vision_dim, lang_dim) self.llm = MoELanguageModel( num_experts=num_experts, top_k=top_k, )
def forward(self, image_embeds, text_ids): visual_tokens = self.projector(image_embeds) # (B, N_v, D_l) text_embeds = self.llm.get_input_embeddings()(text_ids) # 拼接视觉和文本 token multimodal_embeds = torch.cat([visual_tokens, text_embeds], dim=1) return self.llm(inputs_embeds=multimodal_embeds)8. 主流 VLM 能力对比
8.1 基准测试对比
def benchmark_comparison(): """ 各 VLM 在主要基准上的表现。 数据来源: 2024-2025 公开评测。 """ results = { # (模型, MMMU, VQAv2, GQA, TextVQA, POPE) "LLaVA-1.5-7B": (36.7, 83.1, 63.3, 61.3, 85.9), "LLaVA-1.5-13B": (40.1, 84.3, 65.1, 62.9, 87.2), "Qwen-VL2-7B": (38.5, 84.1, 64.8, 63.1, 86.5), "Qwen2-VL-72B": (54.8, 88.9, 70.2, 68.9, 91.3), "InternVL2-8B": (42.5, 85.2, 66.1, 64.8, 88.1), "InternVL2-26B": (49.2, 87.1, 68.9, 67.2, 90.4), "CogVLM-17B": (43.8, 84.6, 65.8, 64.1, 87.8), "GPT-4V(2023)": (56.8, 86.4, 67.1, 70.2, 88.9), "GPT-4o": (69.1, 91.8, 74.3, 78.1, 93.2), "Gemini-1.5-Pro": (62.4, 89.2, 72.8, 74.2, 92.1), } return results
def benchmark_descriptions(): """ 各基准测试的含义。 """ benchmarks = { "MMMU": "大规模多学科多模态理解(大学水平)", "VQAv2": "通用视觉问答(MSCOCO 图像)", "GQA": "视觉推理问答(语义图)", "TextVQA": "文本视觉问答(需要 OCR)", "POPE": "物体存在性幻觉评估", } return benchmarks8.2 各模型适用场景
def model_selection_guide(): """ 不同场景下的模型选择建议。 """ scenarios = { "端侧部署 (手机)": { "推荐": "LLaVA-1.5-7B (INT4 量化)", "备选": "MiniGPT-v2, Phi-3-Vision", "参数": "~4GB 显存", }, "服务器单卡": { "推荐": "Qwen2-VL-7B, InternVL2-8B", "备选": "LLaVA-1.5-13B", "参数": "~16GB 显存", }, "高精度场景": { "推荐": "Qwen2-VL-72B, InternVL2-26B", "备选": "GPT-4o API, Gemini-1.5-Pro API", "参数": "多卡 A100/H100", }, "OCR/文档理解": { "推荐": "LLaVA-1.5 + OCR 数据微调", "备选": "Qwen-VL2 (动态分辨率)", "关键": "高分辨率 + 大量文档数据", }, "视频理解": { "推荐": "VideoLLaVA, LLaMA-VID", "备选": "Qwen2-VL (视频支持)", "关键": "时序建模 + 帧采样", }, } return scenarios9. 幻觉问题与缓解
9.1 视觉幻觉的成因
VLM 容易产生”不存在”的物体描述:
图像: 一只猫坐在沙发上VLM 输出: "一只狗在草地上奔跑"
原因: 1. LLM 骨干的"语言先验"过强 2. 视觉编码器特征不足以区分细粒度物体 3. 训练数据中存在错误的图文配对9.2 POPE 评估协议
def pope_evaluation(): """ POPE (Polling-based Object Probability Estimation) 幻觉评估。 通过多选题判断模型是否幻觉。 """ prompt_template = "Is there a {object} in the image? Yes or No."
questions = [ ("cat", "Is there a cat in the image?"), ("dog", "Is there a dog in the image?"), ("person", "Is there a person in the image?"), ] # 统计准确率和幻觉率 pass9.3 缓解方法
def reduce_hallucination_methods(): """ 幻觉缓解方法对比。 """ methods = { "RLHF/DPO": { "方法": "用人类偏好数据训练,让模型学会说"我不知道"", "效果": "中等", "代价": "需要大量标注", }, "Token-Level Loss": { "方法": "在视觉 token 上加辅助损失,鼓励关注图像", "效果": "良好", "代价": "额外训练目标", }, "ODISE 定位": { "方法": "先用检测器定位物体,再描述", "效果": "显著改善", "代价": "需要额外的检测模型", }, "BA-CC 约束": { "方法": "在注意力层加入图像条件约束", "效果": "良好", "代价": "架构修改", }, "LLAVA-RLHF": { "方法": "用 RLHF 专门对齐视觉和语言", "效果": "最佳", "代价": "复杂训练流程", }, } return methods10. 完整 LLaVA-1.5 架构实现
import torchimport torch.nn as nnfrom transformers import AutoModelForCausalLM, AutoTokenizer, CLIPVisionModel, AutoImageProcessor
class LLaVA15Config: """LLaVA-1.5 配置。""" vision_model: str = "openai/clip-vit-large-patch14-336" llm_model: str = "lmsys/vicuna-7b-v1.5" vision_hidden_size: int = 1024 # CLIP ViT-L: 1024 llm_hidden_size: int = 4096 # Vicuna-7B: 4096 image_size: int = 336 # LLaVA-1.5 用 336px
class MLPProjector(nn.Module): """LLaVA-1.5 的模态投影器 (MLP)。""" def __init__(self, vision_dim, llm_dim): super().__init__() self.proj = nn.Sequential( nn.Linear(vision_dim, llm_dim), nn.GELU(), nn.Linear(llm_dim, llm_dim), nn.LayerNorm(llm_dim), )
def forward(self, x): return self.proj(x)
class LLaVA15(nn.Module): """ LLaVA-1.5 完整架构。 论文: "Improved Baselines with Visual Instruction Tuning" (Liu et al., 2023) """
def __init__(self, config: LLaVA15Config): super().__init__() self.config = config
# 1) 视觉编码器 (冻结) self.vision_encoder = CLIPVisionModel.from_pretrained(config.vision_model) self.image_processor = AutoImageProcessor.from_pretrained(config.vision_model) self.vision_encoder.requires_grad_(False)
# 2) 模态投影器 (训练) self.mlp_projector = MLPProjector( config.vision_hidden_size, config.llm_hidden_size, )
# 3) LLM 骨干 (训练) self.llm = AutoModelForCausalLM.from_pretrained( config.llm_model, torch_dtype=torch.float16, device_map="auto", trust_remote_code=True, )
# 4) 特殊 token self.tokenizer = AutoTokenizer.from_pretrained( config.llm_model, trust_remote_code=True, ) img_tokens = ["[ IMG ]", "[ / IMG ]"] num_new = self.tokenizer.add_tokens(img_tokens, special_tokens=True) self.img_start_id = self.tokenizer.encode("[ IMG ]", add_special_tokens=False)[0] self.img_end_id = self.tokenizer.encode("[ / IMG ]", add_special_tokens=False)[0] self.llm.resize_token_embeddings(len(self.tokenizer))
def encode_images(self, images): """ 编码图像。 images: PIL Image, list[PIL Image], 或 tensor (B, C, H, W) 返回: (B, N_v, llm_hidden_size) 视觉 token """ if not isinstance(images, torch.Tensor): inputs = self.image_processor(images, return_tensors="pt") else: inputs = {"pixel_values": images}
inputs = {k: v.to(self.llm.device) for k, v in inputs.items()}
with torch.no_grad(): outputs = self.vision_encoder(**inputs) # last_hidden_state: (B, N+1, 1024) image_embeds = outputs.last_hidden_state
# 去掉 [CLS] token,只保留 patch tokens visual_tokens = image_embeds[:, 1:, :] # (B, N, 1024)
# 投影到 LLM 空间 visual_tokens = self.mlp_projector(visual_tokens) # (B, N, 4096) return visual_tokens
def forward(self, images, input_ids, attention_mask=None, labels=None): """ 完整前向传播。
参数: images: 图像 tensor (B, C, H, W) 或 PIL input_ids: 文本 token IDs (B, L) attention_mask: 注意力掩码 (B, L) labels: 标签 (B, L),用于计算语言建模损失 """ # 编码图像 visual_tokens = self.encode_images(images) # (B, N, D_l)
# 文本 embedding inputs_embeds = self.llm.get_input_embeddings()(input_ids) # (B, L, D_l)
# 在 [IMG] token 位置替换为视觉 token device = inputs_embeds.device B, L = input_ids.shape N = visual_tokens.shape[1]
# 找到每个 batch 中 [IMG] 和 [/IMG] 的位置 for b in range(B): start_positions = (input_ids[b] == self.img_start_id).nonzero(as_tuple=True)[0] end_positions = (input_ids[b] == self.img_end_id).nonzero(as_tuple=True)[0]
if len(start_positions) == 0 or len(end_positions) == 0: continue
start_pos = start_positions[0].item() end_pos = end_positions[0].item() num_v_tokens = end_pos - start_pos - 1
# 提取该范围的文本 embedding before = inputs_embeds[b, :start_pos, :] after = inputs_embeds[b, end_pos + 1:, :]
# 裁剪或填充视觉 token v_tokens = visual_tokens[b, :num_v_tokens, :]
# 拼接 inputs_embeds[b] = torch.cat([before, v_tokens, after], dim=0)
# 对齐维度(可能因视觉 token 数量变化而不同) T = inputs_embeds.shape[1] if attention_mask is not None: # 重建 attention mask(包含视觉 token) new_mask = torch.ones(B, T, device=device) new_mask[:, :attention_mask.shape[1]] = attention_mask
if labels is not None: # 重建 labels # [IMG] 和 [/IMG] 位置的 label 设为 -100(忽略) new_labels = labels.clone() for b in range(B): mask = (input_ids[b] == self.img_start_id) | (input_ids[b] == self.img_end_id) new_labels[b, mask] = -100 else: new_labels = None
outputs = self.llm( inputs_embeds=inputs_embeds, attention_mask=new_mask if attention_mask is not None else None, labels=new_labels, ) return outputs
@torch.no_grad() def generate(self, images, prompts, max_new_tokens=512, temperature=0.7): """ 生成回答。 """ self.eval()
# 构建输入 if isinstance(prompts, str): prompts = [prompts] if not isinstance(images, list): images = [images]
texts = [f"[ IMG ] [ / IMG ] [ USER ] {p} [ ASSISTANT ]" for p in prompts]
input_ids = self.tokenizer(texts, return_tensors="pt", padding=True).to(self.llm.device) visual_tokens = self.encode_images(images)
B = input_ids["input_ids"].shape[0] L = input_ids["input_ids"].shape[1] inputs_embeds = self.llm.get_input_embeddings()(input_ids["input_ids"])
for b in range(B): start_pos = (input_ids["input_ids"][b] == self.img_start_id).nonzero(as_tuple=True)[0] end_pos = (input_ids["input_ids"][b] == self.img_end_id).nonzero(as_tuple=True)[0]
if len(start_pos) == 0: continue
sp = start_pos[0].item() ep = end_pos[0].item() nv = ep - sp - 1
before = inputs_embeds[b, :sp, :] after = inputs_embeds[b, ep + 1:, :] v_tok = visual_tokens[b, :nv, :] inputs_embeds[b] = torch.cat([before, v_tok, after], dim=0)
outputs = self.llm.generate( inputs_embeds=inputs_embeds, max_new_tokens=max_new_tokens, temperature=temperature, do_sample=temperature > 0, pad_token_id=self.tokenizer.pad_token_id, )
responses = self.tokenizer.batch_decode(outputs, skip_special_tokens=True) return responses11. VLM 的未来趋势
11.1 从专用到全模态
当前 VLM: 图像 + 文本 → 文本全模态模型: 图像 + 视频 + 音频 + 文本 + 3D → 文本
代表性工作: - GPT-4o: 原生多模态,支持音频/视频/图像 - Gemini 1.5: 全模态处理 - Qwen2-Audio: 语音 + 文本 - NVLM: NVIDIA 的全模态 LLM11.2 多模态 Agent
VLM + Agent = 视觉 Agent
能力: 1. 视觉感知: 理解图像内容 2. 规划推理: 分解复杂视觉任务 3. 工具使用: 调用图像编辑、OCR、检测等工具 4. 多轮对话: 持续交互和修正
示例任务: - "帮我把图片里这只猫换成狗" - "分析这张财报图表,提取关键数据" - "在视频的 2 分 30 秒处发生了什么"11.3 高效 VLM
优化方向: 1. 视觉 token 压缩: 减少视觉 token 数量 (256 → 64) 2. 动态路由: 只处理图像的相关区域 3. INT4/INT8 量化: 端侧部署 4. 蒸馏: 从大 VLM 蒸馏到小 VLM 5. LoRA/QLoRA: 参数高效微调12. 总结
12.1 核心要点
| 维度 | 关键要点 |
|---|---|
| 视觉编码器 | CLIP/SigLIP/DINOv2 系列 ViT,大规模图文对比预训练 |
| 模态对齐 | MLP 投影(最简)→ Cross-Attention(最灵活)→ Q-Former(压缩) |
| 训练范式 | 预训练(冻 LLM)→ 指令微调(解冻 LLM)→ 可选 DPO/RLHF |
| 架构演进 | 简单 MLP → 深度融合 → 全模态原生 |
| 核心挑战 | 幻觉、高分辨率处理、长上下文视频、端侧部署 |
| 代表模型 | LLaVA(简单高效)、Qwen-VL(中文+动态分辨率)、InternVL(大编码器) |
12.2 一句话总结
VLM 的核心是把”看得见”的视觉编码器和”读得懂”的语言模型通过一个轻量投影器连接起来——预训练阶段训练投影器建立模态对齐,指令微调阶段让对齐后的模型学会”看图说话”。从 LLaVA 的简洁 MLP 到 GPT-4o 的原生多模态,VLM 正在从”图像+文本的拼接”走向”统一的多模态理解”。
12.3 推荐资源
论文: - CLIP (Radford et al., 2021): "Learning Transferable Visual Models From Natural Language" - LLaVA (Liu et al., 2023): "Visual Instruction Tuning" - LLaVA-1.5 (Liu et al., 2023): "Improved Baselines with Visual Instruction Tuning" - BLIP-2 (Li et al., 2023): "Bootstrapping Language-Image Pre-training" - InstructBLIP (Liu et al., 2023): "InstructBLIP" - Qwen-VL (Bai et al., 2023): "Qwen-VL: A Versatile Vision-Language Model" - InternVL (Chen et al., 2024): "InternVL: Scaling Vision Foundation Models"
代码: - llava-vl/llava (LLaVA 官方实现) - salesforce/LAVIS (BLIP 家族) - QwenLM/Qwen-VL - OpenGVLab/InternVL
数据集: - LAION-5B (图文对) - LLaVA-Instruct-150K (指令微调) - MMMU (多学科多模态理解基准)文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!

