多模态大模型 VLM 架构:从模态对齐到 GPT-4V 的完整技术栈

5808 字
29 分钟
多模态大模型 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-S22M中等轻量 VLM
CLIP ViT-B86M良好LLaVA-1.5 以前
CLIP ViT-L304M优秀LLaVA-1.5, InstructBLIP
EVA-CLIP4B极佳极高InternVL, 大模型
SigLIP1B极佳Gemma-3, 端侧
DINOv286M-304M优秀(无文本监督)视觉任务专用

2.3 视觉编码器配置参数#

@dataclass
class 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)^2

3. 模态对齐层:连接视觉与语言#

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 线性投影(最简方案)#

最简单的对齐:用一层线性变换:

viproj=Wvi+b,WRDl×Dvv_i^{\text{proj}} = W \cdot v_i + b, \quad W \in \mathbb{R}^{D_l \times D_v}
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_out

3.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) ← 固定长度,兼容 LLM

3.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 骨干参数量维度特点
Vicuna7B/13B4096基于 Llama,微调对话
Llama-38B/70B4096/8192强大的基础能力
Mistral7B4096滑动窗口注意力
Qwen-27B/72B3584/8192中文优化
Phi-33.8B3072微软,小而强
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 IDs
img_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 的核心任务是训练投影器,最小化视觉条件下的语言建模损失:

L1=i=1Nlogpθ(yiy<i,v)\mathcal{L}_1 = -\sum_{i=1}^{N} \log p_\theta(y_i \mid y_{<i}, v)

其中 vv 是图像特征,y<iy_{<i} 是已经生成的文本 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_mixture

6. 典型 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_tokens

7.2 视觉 Token 数量控制#

VLM 的一个核心问题是 token 数量爆炸:

@dataclass
class 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 benchmarks

8.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 scenarios

9. 幻觉问题与缓解#

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?"),
]
# 统计准确率和幻觉率
pass

9.3 缓解方法#

def reduce_hallucination_methods():
"""
幻觉缓解方法对比。
"""
methods = {
"RLHF/DPO": {
"方法": "用人类偏好数据训练,让模型学会说"我不知道"",
"效果": "中等",
"代价": "需要大量标注",
},
"Token-Level Loss": {
"方法": "在视觉 token 上加辅助损失,鼓励关注图像",
"效果": "良好",
"代价": "额外训练目标",
},
"ODISE 定位": {
"方法": "先用检测器定位物体,再描述",
"效果": "显著改善",
"代价": "需要额外的检测模型",
},
"BA-CC 约束": {
"方法": "在注意力层加入图像条件约束",
"效果": "良好",
"代价": "架构修改",
},
"LLAVA-RLHF": {
"方法": "用 RLHF 专门对齐视觉和语言",
"效果": "最佳",
"代价": "复杂训练流程",
},
}
return methods

10. 完整 LLaVA-1.5 架构实现#

import torch
import torch.nn as nn
from 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 responses

11. VLM 的未来趋势#

11.1 从专用到全模态#

当前 VLM: 图像 + 文本 → 文本
全模态模型: 图像 + 视频 + 音频 + 文本 + 3D → 文本
代表性工作:
- GPT-4o: 原生多模态,支持音频/视频/图像
- Gemini 1.5: 全模态处理
- Qwen2-Audio: 语音 + 文本
- NVLM: NVIDIA 的全模态 LLM

11.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 (多学科多模态理解基准)

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

多模态大模型 VLM 架构:从模态对齐到 GPT-4V 的完整技术栈
https://aiattnstudio.link/posts/vision-language-model/
作者
Federico
发布于
2026-07-16
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author

Federico

AI Research Lab

Hello, I'm Federico.

关于实验室 / About
公告

欢迎来到Federico的个人博客

分类
标签
站点统计
57文章
7分类
404标签