LLaVA 深度剖析:从视觉指令微调到开源多模态助手的完整技术栈

6604 字
33 分钟
LLaVA 深度剖析:从视觉指令微调到开源多模态助手的完整技术栈

1. LLaVA 的核心洞察#

1.1 为什么需要视觉指令微调?#

在 LLaVA 出现之前,VLM(视觉语言模型)的训练目标主要是图文匹配(如 CLIP)和图像描述(如 BLIP-2)。这些模型能回答”这张图里有什么”,但无法回答更复杂的问题:

CLIP 能做的:
✓ "这张图是猫还是狗?" (二选一分类)
✓ "给这张图一个描述" (简短描述)
CLIP 不能做的:
✗ "图中这只猫在做什么?"
✗ "根据图表趋势,预测下个季度的增长"
✗ "这张 X 光片有什么异常?"
✗ "逐步解释这道数学题的解法"

根本原因:CLIP/BLIP-2 的训练目标是”描述图像”,而不是”根据图像回答问题”。

LLaVA 的核心洞察:把 GPT-4 在语言指令微调上的成功经验,迁移到视觉领域——构建”视觉指令微调”数据集,让 VLM 学会像人类助手一样,根据图像内容回答各种类型的开放性问题。

1.2 GPT-4V 的启发#

2023 年 3 月,OpenAI 发布 GPT-4V,展示了多模态模型的强大能力:

GPT-4V 能做的:
✓ 视觉问答("图里有什么?")
✓ 文档理解(OCR + 理解)
✓ 图表分析(解释图表含义)
✓ 视觉推理(逐步分析图像)
✓ 多模态对话(图像 + 文本 → 多轮对话)
开源模型的差距:
- 没有开源模型能接近 GPT-4V 的能力
- BLIP-2 等模型只能做简单的图像描述
- 缺乏高质量的视觉指令数据

LLaVA 的目标是用开源数据和模型,复现 GPT-4V 的核心能力

1.3 LLaVA 的三个核心贡献#

贡献 1: 视觉指令微调范式
把语言指令微调的思路迁移到视觉领域
训练目标: 根据图像内容回答问题
贡献 2: LLaVA-Instruct 数据集构建方法
用 GPT-4 自动生成高质量的视觉指令数据
不需要昂贵的人工标注
贡献 3: 简洁有效的架构设计
CLIP 视觉编码器 + MLP 投影 + Vicuna LLM
参数量小,数据效率高,效果好

1.4 论文信息#

LLaVA (2023):
论文: "Visual Instruction Tuning"
作者: Haotian Liu, Chunyuan Li, Qingyang Wu, Yong Jae Lee
单位: UW-Madison, MBZUAI
发表于: NeurIPS 2023 (Spotlight)
LLaVA-1.5 (2023):
论文: "Improved Baselines with Visual Instruction Tuning"
作者: Haotian Liu, Chunyuan Li, Yuheng Li, Yong Jae Lee
发表于: NeurIPS 2023 Workshop
LLaVA-1.6 (2024):
改进: 高分辨率、AnyRes、Llava-RLHF

1.5 一句话概括 LLaVA#

LLaVA 的核心是”视觉指令微调”——用 GPT-4 生成高质量的图像-指令-回答三元组数据,然后用这些数据微调一个 CLIP + LLM 的简单架构,使模型学会根据图像内容回答各种开放性视觉问题。

2. 架构设计:简单但有效#

2.1 整体架构#

┌─────────────────────────────────────────────────────────────┐
│ │
│ [图像] → CLIP ViT-L/14 → [IMG patch tokens] │
│ (冻结 ❄️) ↓ │
│ MLP 投影器 │
│ (训练 🔥) │
│ ↓ │
│ [文本] → Vicuna-7B LLM → [文本 tokens] │
│ (训练 🔥) ↓ │
│ 自回归生成 │
│ ↓ │
│ [回答文本] │
│ │
└─────────────────────────────────────────────────────────────┘
LLaVA 架构的核心简洁性:
视觉编码器: CLIP ViT-L/14(冻结,用预训练权重)
模态对齐: 线性投影 → MLP(两层,带 GELU)
LLM 骨干: Vicuna-7B(指令微调后的 Llama-2)

2.2 LLaVA vs BLIP-2 的架构对比#

"""
LLaVA 和 BLIP-2 的核心区别。
BLIP-2: 复杂的多阶段桥接器
CLIP ViT → Q-Former (12 层 attention, 32 queries) → LLM
问题: Q-Former 训练困难,压缩率固定
LLaVA: 简洁的两层 MLP
CLIP ViT → MLP (线性→GELU→线性) → LLM
优点: 参数量小,训练稳定,效果好
"""
维度BLIP-2LLaVA
模态桥接器Q-Former (12 层, 768M)MLP (2 层, ~20M)
训练难度高(Q-Former 训练不稳定)低(简单的两层 MLP)
参数量~3B (Q-Former + LLM)~7B (Vicuna-7B)
数据效率中等高(LLaVA-1.5 用 600K 数据超越)
推理速度慢(Q-Former 额外计算)快(无额外开销)
开源程度部分开源完全开源

2.3 特殊 Token 设计#

LLaVA 在 Vicuna 的 tokenizer 中添加了两个特殊 token 来标记图像位置:

输入格式:
[SYSTEM] You are a helpful assistant.
[USER] [IMG][IMG][IMG]...[IMG] Describe this image. [ASSISTANT]
其中 [IMG] token 位置会被替换为视觉 token 序列。
注意: LLaVA 原始版本使用 4 个 [IMG] token(对应 4 个 grid patches)
LLaVA-1.5 改为 [IMG]...[IMG] 范围标记,由投影器输出长度决定
class LLaVATokenizer:
"""
LLaVA 的 tokenizer 配置。
"""
def __init__(self, base_tokenizer="lmsys/vicuna-7b-v1.5"):
from transformers import AutoTokenizer
self.tokenizer = AutoTokenizer.from_pretrained(base_tokenizer)
# 添加图像标记 token
special_tokens = ["[ IMG ]", "[ / IMG ]"]
num_new = self.tokenizer.add_tokens(special_tokens, special_tokens=True)
self.img_start_id = self.tokenizer("[ IMG ]", add_special_tokens=False).input_ids[0]
self.img_end_id = self.tokenizer("[ / IMG ]", add_special_tokens=False).input_ids[0]
# Vicuna 的系统提示
self.system_prompt = (
"A chat between a curious human and an artificial intelligence assistant. "
"The assistant gives helpful, detailed, and polite answers to the user's questions."
)
def build_input(self, instruction, image_count=1):
"""构建多模态输入。"""
img_tokens = " [ IMG ] " + " [ IMG ] " * (image_count - 1) + " [ / IMG ] "
text = f"{self.system_prompt} [ USER ] {img_tokens} {instruction} [ ASSISTANT ]"
return text

3. 数据集构建:LLaVA-Instruct 的核心创新#

3.1 为什么需要自动生成数据?#

LLaVA 最大的创新不是架构,而是数据集构建方法

传统方法(人工标注):
- 人工标注图像描述: $0.05-0.50/张
- 人工标注 VQA: $0.10-1.00/题
- 人工标注复杂推理: $1.00-5.00/题
- 成本极高,规模受限
LLaVA 方法(GPT-4 自动生成):
- GPT-4 是强大的语言模型
- 可以生成高质量的文本描述
- 可以生成合理的问答对
- 成本低,可扩展到百万级

3.2 GPT-4V 的不可用性困境#

2023 年 3 月 GPT-4V 发布时,OpenAI 尚未开放 GPT-4V 的 API,也没有开源的多模态 GPT-4 模型。

LLaVA 的解决方案:用纯文本的 GPT-4 来生成数据!

核心思路:
- GPT-4 (纯文本) 无法直接"看图"
- 但 GPT-4 可以理解图像的文本描述
- 如果先让 GPT-4 描述一张图,再让它根据描述生成问答
- 那么生成的问题就是"基于图像描述的"
这个方法巧妙地绕过了 GPT-4V 不可用的问题。

3.3 数据生成流程(Stage-by-Step)#

LLaVA-Instruct-150K 数据集的生成分为三个阶段:

┌─────────────────────────────────────────────────────────────┐
│ Stage 1: 图像描述 (Image Captioning) │
│ 输入: 图像 │
│ 方法: BLIP-2 生成初始描述 + GPT-4 改进 │
│ 输出: 详细、准确的图像描述 │
├─────────────────────────────────────────────────────────────┤
│ Stage 2: 指令问题生成 (Instruction Generation) │
│ 输入: 图像描述 │
│ 方法: GPT-4 根据描述生成多样化的指令问题 │
│ 输出: 各种类型的问题(描述/推理/计算/复杂) │
├─────────────────────────────────────────────────────────────┤
│ Stage 3: 回答生成 (Response Generation) │
│ 输入: 图像描述 + 指令问题 │
│ 方法: GPT-4 根据描述和问题的组合生成回答 │
│ 输出: 对应每个问题的详细回答 │
└─────────────────────────────────────────────────────────────┘

3.4 Stage 1:图像描述#

def generate_image_caption(image, blip_model, gpt4_api):
"""
Stage 1: 生成详细的图像描述。
步骤:
1. 用 BLIP-2 生成初始 caption
2. 用 GPT-4 改进 caption
"""
# 1) BLIP-2 生成初始描述
caption_raw = blip_model.generate_caption(image)
# 2) GPT-4 改进描述
prompt = f"""Based on the following image description, generate a more detailed and accurate description:
Raw caption: {caption_raw}
Please provide a more detailed description that includes:
- Main objects and their locations
- Actions and interactions
- Background context
- Style and mood (if applicable)
- Any text or labels visible in the image
Output only the improved description:"""
improved_caption = gpt4_api.complete(prompt)
return improved_caption
def caption_quality_filter(captions, min_length=50, max_repeat=0.3):
"""
过滤低质量描述。
"""
filtered = []
for cap in captions:
# 长度检查
if len(cap.split()) < min_length:
continue
# 重复检查(避免 GPT-4 的循环输出)
words = cap.lower().split()
if len(words) > 0:
unique_ratio = len(set(words)) / len(words)
if unique_ratio < (1 - max_repeat):
continue
filtered.append(cap)
return filtered

3.5 Stage 2:指令问题生成#

GPT-4 根据图像描述,生成四种类型的指令问题:

def generate_instructions(caption, gpt4_api):
"""
Stage 2: 根据描述生成多样化的指令问题。
GPT-4 根据描述,生成四种不同类型的问题。
"""
prompt = f"""You are an AI assistant. Generate diverse question-answer pairs based on the following image description.
Image description: {caption}
Generate questions from FOUR different categories:
1. CONVERSATIONAL (简单对话型):
- 自然、口语化的问题
- 例: "这张图片里有什么?" "这是什么地方?"
2. DETAILED DESCRIPTION (详细描述型):
- 需要详细描述的问题
- 例: "请详细描述图中的人物穿着" "描述这个场景的氛围"
3. COMPLEX REASONING (复杂推理型):
- 需要多步推理的问题
- 例: "图中这个人在做什么,可能会发生什么?"
4. SPATIAL RELATIONSHIPS (空间关系型):
- 关于物体位置和关系的问题
- 例: "红球和蓝球的位置关系是什么?"
For EACH category, generate 1-2 questions.
Output format:
CONVERSATIONAL:
Q: ...
DETAIL:
Q: ...
REASONING:
Q: ...
SPATIAL:
Q: ..."""
response = gpt4_api.complete(prompt)
questions = parse_questions(response)
return questions
def parse_questions(gpt4_output):
"""
解析 GPT-4 的输出,提取问题列表。
"""
questions = []
current_category = None
for line in gpt4_output.split("\n"):
line = line.strip()
if not line:
continue
if line.startswith("CONVERSATIONAL"):
current_category = "conversational"
elif line.startswith("DETAIL"):
current_category = "detail"
elif line.startswith("REASONING"):
current_category = "reasoning"
elif line.startswith("SPATIAL"):
current_category = "spatial"
elif line.startswith("Q:"):
question = line[2:].strip()
if current_category:
questions.append({"type": current_category, "question": question})
return questions

3.6 Stage 3:回答生成#

def generate_response(caption, question, gpt4_api):
"""
Stage 3: 根据描述和问题,生成详细回答。
"""
prompt = f"""You are a helpful AI assistant. Answer the following question based on the image description.
Image description: {caption}
Question: {question}
Please provide a detailed and accurate answer based on the description above.
If the description doesn't contain enough information to answer, say so honestly.
Be specific and informative."""
response = gpt4_api.complete(prompt)
return response
def build_instruction_dataset(images, blip_model, gpt4_api):
"""
完整的数据集构建流程。
"""
dataset = []
for image in tqdm(images):
# Stage 1: 图像描述
caption = generate_image_caption(image, blip_model, gpt4_api)
if not caption:
continue
# Stage 2: 生成问题
questions = generate_instructions(caption, gpt4_api)
# Stage 3: 生成回答
for q_data in questions:
question = q_data["question"]
answer = generate_response(caption, question, gpt4_api)
dataset.append({
"image": image,
"caption": caption,
"instruction": question,
"response": answer,
"type": q_data["type"],
})
return dataset

3.7 LLaVA-Instruct-150K 的数据构成#

def dataset_statistics():
"""
LLaVA-Instruct-150K 数据集统计。
"""
stats = {
"conversational": {
"count": 30_000,
"examples": "简单问答,如'图里有什么'",
"来源": "GPT-4 自动生成",
},
"detailed_description": {
"count": 30_000,
"examples": "详细描述,如'请描述图中的人物'",
"来源": "GPT-4 自动生成",
},
"complex_reasoning": {
"count": 30_000,
"examples": "推理问题,如'图中的因果关系'",
"来源": "GPT-4 自动生成",
},
"spatial_relationships": {
"count": 30_000,
"examples": "空间问题,如'物体相对位置'",
"来源": "GPT-4 自动生成",
},
"llava_lcs_558k": {
"count": 558_000,
"examples": "图像描述对(CC3M 筛选)",
"来源": "BLIP-2 caption + GPT-4 改进",
},
}
return stats

3.8 数据质量控制#

def quality_control(dataset):
"""
LLaVA 的数据质量控制策略。
"""
strategies = {
"长度过滤": {
"caption_min_len": 10, # 描述至少 10 词
"response_min_len": 10, # 回答至少 10 词
"instruction_max_len": 512, # 指令最多 512 词
},
"重复检测": {
"ngram_repeat_threshold": 0.3, # 唯一词比例 > 70%
"copy_from_caption_ratio": 0.8, # 回答不能 80% 直接复制 caption
},
"一致性验证": {
"use_gpt4_validator": True, # 用 GPT-4 检查一致性
"consistency_threshold": 0.7, # 一致性得分 > 0.7
},
"GPT-4 改进": {
"improve_captions": True, # 改进 BLIP-2 的 caption
"gpt4_temperature": 0.0, # 生成用 temperature=0
"gpt4_top_p": 1.0,
},
}
return strategies

4. 两阶段训练:模态对齐 + 指令微调#

4.1 训练流程概览#

┌──────────────────────────────────────────────────────────────┐
│ 阶段 1: 模态预训练 (Modality Alignment) │
│ │
│ 数据: LLaVA-LCS-558K (558K 图像-描述对) │
│ 目标: 让 MLP 学会把 CLIP 视觉特征映射到 LLM 空间 │
│ 方法: 语言建模损失(给定图像,预测描述) │
│ │
│ 视觉编码器: 冻结 ❄️ │
│ LLM: 冻结 ❄️ │
│ MLP: 训练 🔥 ← 唯一要训练的部分! │
│ │
│ 训练: ~1 epoch, batch_size=128, lr=1e-3 │
├──────────────────────────────────────────────────────────────┤
│ 阶段 2: 视觉指令微调 (Visual Instruction Tuning) │
│ │
│ 数据: LLaVA-Instruct-150K (150K 指令问答对) │
│ 目标: 让模型学会根据图像内容回答各种问题 │
│ 方法: 语言建模损失(给定图像+问题,预测回答) │
│ │
│ 视觉编码器: 冻结 ❄️ │
│ LLM: 解冻 🔥 (Vicuna-7B 全量或 LoRA) │
│ MLP: 训练 🔥 │
│ │
│ 训练: ~3 epochs, batch_size=32, lr=2e-5 │
└──────────────────────────────────────────────────────────────┘

4.2 阶段 1:模态预训练#

def pretrain_stage1(projector, vision_encoder, llm, dataset):
"""
阶段 1: 模态预训练。
目标: 训练投影器,让视觉特征和文本特征对齐。
数据: LLaVA-LCS-558K (CC3M 筛选后的图文对)
"""
# 冻结 LLM 和视觉编码器
vision_encoder.requires_grad_(False)
for param in llm.parameters():
param.requires_grad = False
# 只训练投影器
optimizer = torch.optim.AdamW(projector.parameters(), lr=1e-3, weight_decay=0.05)
dataloader = DataLoader(dataset, batch_size=128, shuffle=True)
for epoch in range(1):
for batch in tqdm(dataloader):
images, captions = batch["image"], batch["caption"]
# 1) 视觉编码 (冻结)
with torch.no_grad():
vision_output = vision_encoder(images)
vision_embeds = vision_output.last_hidden_state[:, 1:, :] # 去掉 [CLS]
# 2) 投影
visual_tokens = projector(vision_embeds) # (B, N_v, D_l)
# 3) 构建输入
input_texts = [f"[ IMG ] [ / IMG ] {cap}" for cap in captions]
input_ids = tokenizer(input_texts, return_tensors="pt", padding=True)
input_ids = {k: v.to(device) for k, v in input_ids.items()}
# 4) 在 [IMG] 位置插入视觉 token
inputs_embeds = llm.get_input_embeddings()(input_ids["input_ids"])
for b in range(inputs_embeds.shape[0]):
img_pos = (input_ids["input_ids"][b] == img_start_id).nonzero(as_tuple=True)[0]
end_pos = (input_ids["input_ids"][b] == img_end_id).nonzero(as_tuple=True)[0]
if len(img_pos) == 0:
continue
sp = img_pos[0].item()
ep = end_pos[0].item()
num_v = ep - sp - 1
before = inputs_embeds[b, :sp, :]
after = inputs_embeds[b, ep + 1:, :]
v_tok = visual_tokens[b, :num_v, :]
inputs_embeds[b] = torch.cat([before, v_tok, after], dim=0)
# 5) LLM 前向
outputs = llm(inputs_embeds=inputs_embeds,
attention_mask=input_ids["attention_mask"],
labels=input_ids["input_ids"])
loss = outputs.loss
# 6) 反向传播
optimizer.zero_grad()
loss.backward()
optimizer.step()
return projector

4.3 阶段 2:视觉指令微调#

def finetune_stage2(projector, vision_encoder, llm, dataset):
"""
阶段 2: 视觉指令微调。
目标: 微调 LLM + 投影器,让模型学会回答视觉问题。
数据: LLaVA-Instruct-150K (150K 指令问答对)
方法: 全量微调 Vicuna-7B 或 LoRA 微调
"""
# 解冻 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)
# 视觉编码器仍然冻结
vision_encoder.requires_grad_(False)
optimizer = torch.optim.AdamW(
list(projector.parameters()) + list(llm.parameters()),
lr=2e-5,
weight_decay=0.0,
)
dataloader = DataLoader(dataset, batch_size=32, shuffle=True)
for epoch in range(3):
for batch in tqdm(dataloader):
images, instructions, responses = (
batch["image"], batch["instruction"], batch["response"]
)
# 构建对话格式
# [SYSTEM] ... [USER] [IMG] ... [IMG] {instruction} [ASSISTANT] {response}
prompts = [
f"{SYSTEM_PROMPT} [ USER ] [ IMG ] [ / IMG ] {inst} [ ASSISTANT ]"
for inst in instructions
]
# Tokenize
input_enc = tokenizer(prompts, return_tensors="pt", padding=True)
label_enc = tokenizer(responses, return_tensors="pt", padding=True,
truncation=True, max_length=512)
# 视觉编码 + 投影
vision_output = vision_encoder(images)
vision_embeds = vision_output.last_hidden_state[:, 1:, :]
visual_tokens = projector(vision_embeds)
# 嵌入
inputs_embeds = llm.get_input_embeddings()(input_enc["input_ids"])
# 插入视觉 token
for b in range(inputs_embeds.shape[0]):
sp_pos = (input_enc["input_ids"][b] == img_start_id).nonzero(as_tuple=True)[0]
ep_pos = (input_enc["input_ids"][b] == img_end_id).nonzero(as_tuple=True)[0]
if len(sp_pos) == 0:
continue
sp = sp_pos[0].item()
ep = ep_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)
# LLM 前向
outputs = llm(
inputs_embeds=inputs_embeds,
attention_mask=input_enc["attention_mask"],
labels=label_enc["input_ids"],
)
loss = outputs.loss
optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(llm.parameters(), max_norm=1.0)
optimizer.step()
return projector, llm

4.4 训练超参数对比#

def training_hyperparams():
"""
LLaVA 两个训练阶段的超参数对比。
"""
return {
"Stage 1 - 模态预训练": {
"batch_size": 128,
"learning_rate": 1e-3,
"weight_decay": 0.05,
"epochs": 1,
"LR_schedule": "constant",
"视觉编码器": "冻结",
"LLM": "冻结",
"投影器": "训练",
"数据量": "558K (LLaVA-LCS-558K)",
},
"Stage 2 - 指令微调": {
"batch_size": 32,
"learning_rate": 2e-5,
"weight_decay": 0.0,
"epochs": 3,
"LR_schedule": "cosine",
"视觉编码器": "冻结",
"LLM": "解冻 (LoRA r=64)",
"投影器": "训练",
"数据量": "150K (LLaVA-Instruct-150K)",
},
}

5. LLaVA-1.5:关键改进#

5.1 LLaVA-1.5 的三大改进#

LLaVA-1.5 在 LLaVA 基础上做了三个关键改进:

改进 1: MLP 替换线性投影
- LLaVA: 单层线性投影 W ∈ ℝ^{D_l×D_v}
- LLaVA-1.5: 两层 MLP (Linear → GELU → Linear)
- 效果: +3.5% VQAv2 提升
改进 2: 336px 高分辨率
- LLaVA: 224×224, 196 个 patches
- LLaVA-1.5: 336×336, 576 个 patches (2.25 倍像素)
- 效果: 保留更多细节,尤其是文档和图表
改进 3: 大规模高质量训练数据
- LLaVA-1.5: 665K 视觉指令数据 + 3M 语言数据
- 包括: OCR 数据、图表数据、GQA、VQAv2 等
- 效果: 在多个 benchmark 上达到 SOTA

5.2 MLP 投影的消融实验#

def ablation_mlp_projector():
"""
LLaVA-1.5 论文中的投影器消融实验。
在 ScienceQA 数据集上的准确率。
"""
results = {
"线性投影 (LLaVA)": 66.7,
"MLP 投影 (LLaVA-1.5)": 70.4, # +3.7%
"MLP + LayerNorm": 71.2,
"MLP + GELU": 70.4,
"MLP + SiLU": 70.3,
}
return results
# 结论: MLP 投影比线性投影高 3.7%,LayerNorm 额外 +0.8%

5.3 分辨率的消融实验#

def ablation_resolution():
"""
分辨率对性能的影响。
"""
results = {
"ViT-L/14 @ 224px (LLaVA)": 66.7,
"ViT-L/14 @ 336px (LLaVA-1.5)": 70.4, # +3.7%
"ViT-L/14 @ 448px (更大分辨率)": 72.1, # +5.4%
"EVA-CLIP ViT-L/14 @ 336px": 73.8, # +7.1%
}
return results
# 结论: 分辨率越高越好,但计算量也增加

5.4 训练数据混合#

def llava15_data_mixture():
"""
LLaVA-1.5 的训练数据混合。
总计约 6M 样本。
"""
mixture = {
"视觉语言数据 (总计 ~1.3M)": {
"LLaVA-LCS-558K": {
"数量": 558_000,
"类型": "图像描述对 (预训练)",
"来源": "CC3M 筛选 + GPT-4 改进",
},
"LLaVA-Instruct-665K": {
"数量": 665_000,
"类型": "视觉指令问答 (微调)",
"来源": "GPT-4 生成",
},
},
"纯语言指令数据 (总计 ~4.6M)": {
"LLaVA-Instruct-150K": {
"数量": 150_000,
"类型": "GPT-4 生成的视觉问答",
},
"ShareGPT": {
"数量": 900_000,
"类型": "人类对话数据 (Vicuna 来源)",
},
"Alpaca": {
"数量": 3_000_000,
"类型": "语言指令数据 (GPT-4 生成)",
},
},
"特定能力数据 (总计 ~330K)": {
"OCR/文档": {"数量": 200_000, "来源": "OCR 数据集"},
"图表": {"数量": 100_000, "来源": "ChartQA, DVQA"},
"GQA": {"数量": 80_000, "来源": "VQA 推理"},
"DocVQA": {"数量": 50_000, "来源": "文档问答"},
},
}
return mixture

6. LLaVA 版本演进#

6.1 版本对比总览#

版本发布时间视觉编码器LLM投影器分辨率训练数据特点
LLaVA2023.04CLIP ViT-L/14Vicuna-7B线性224px558K+150K开山之作
LLaVA-1.02023.07CLIP ViT-L/14Vicuna-13B线性224px558K+150K更大 LLM
LLaVA-1.52023.09CLIP ViT-L/14-336Vicuna-7B/13BMLP336px1.3M+3M三大改进
LLaVA-1.62024.02CLIP ViT-L/14-336Vicuna-7B/13BMLP336px/672px1.3M+3MAnyRes
LLaVA-NeXT2024.05SIGLIP-So400mLlama-3 8BMLP动态2M新骨干

6.2 AnyRes:LLaVA-1.6 的高分辨率策略#

LLaVA-1.6 引入了 AnyRes 策略来处理高分辨率图像:

传统方法 (直接放大):
224×224 → 448×448 (4倍像素)
问题: 视觉 token 数量爆炸 (196 → 784)
AnyRes 策略:
原始图像: 任意分辨率
下采样到低分辨率: (H', W') → 编码得到全局特征
切分成子图: (h, w) → 分别编码得到局部特征
全局 + 局部特征拼接 → 丰富的高分辨率表示
示例 (672px, 4×4 grid):
原始: 672×672
下采样到: 336×336 → 全局特征 (576 tokens)
切分成 4×4 = 16 个子图 → 每个 168×168 → 各 144 tokens
总 tokens: 576 + 16×144 = 2880 (可接受)
class AnyResStrategy:
"""
AnyRes 高分辨率策略。
"""
def __init__(self, base_resolution=336, grid_size=2):
self.base_resolution = base_resolution
self.grid_size = grid_size
def split_image(self, image):
"""
把高分辨率图像切分成子图。
"""
W, H = image.size
patch_size = self.base_resolution // self.grid_size # 168
patches = []
for y in range(0, H, patch_size):
for x in range(0, W, patch_size):
patch = image.crop((x, y, x + patch_size, y + patch_size))
patches.append(patch)
# 全局缩略图
global_img = image.resize((self.base_resolution, self.base_resolution))
return global_img, patches
def encode_with_anyres(self, image, vision_encoder, projector):
"""
AnyRes 编码。
"""
global_img, patches = self.split_image(image)
# 编码全局
global_feat = vision_encoder(global_img) # (1, N+1, D)
global_feat = projector(global_feat[:, 1:, :]) # (1, N, D_l)
# 编码每个 patch
patch_feats = []
for patch in patches:
feat = vision_encoder(patch)
feat = projector(feat[:, 1:, :])
patch_feats.append(feat)
# 拼接全局 + 局部
all_feats = torch.cat([global_feat] + patch_feats, dim=1) # (1, N*(grid+1), D_l)
return all_feats

6.3 LLaVA-NeXT(后续版本)#

def llava_next_features():
"""
LLaVA-NeXT 的关键特性。
"""
features = {
"视觉编码器升级": "SIGLIP-So400m (比 CLIP 更强)",
"LLM 升级": "Llama-3 8B (比 Vicuna 更强)",
"分辨率": "动态分辨率,支持任意宽高比",
"训练数据": "~2M 高质量样本",
"关键创新": "Mistral-7B 对比、动态数据过滤",
"性能提升": {
"MMMU": "LLaVA-1.5 7B: 36.7% → LLaVA-NeXT: 42.1%",
"VQAv2": "LLaVA-1.5 7B: 83.1% → LLaVA-NeXT: 85.4%",
},
}
return features

7. 与 GPT-4V 的对比#

7.1 能力对比#

def gpt4v_comparison():
"""
LLaVA-1.5 vs GPT-4V(2023) 能力对比。
数据来源: 2023-2024 公开评测。
"""
comparison = {
"通用视觉问答": {
"GPT-4V": "★★★★★ (接近人类水平)",
"LLaVA-1.5": "★★★★ (较好,但有差距)",
"差距原因": "GPT-4V 有更大规模的预训练",
},
"文档/OCR 理解": {
"GPT-4V": "★★★★★ (极强)",
"LLaVA-1.5": "★★★ (需额外数据)",
"差距原因": "LLaVA-1.5 加入 OCR 数据后缩小差距",
},
"图表分析": {
"GPT-4V": "★★★★★ (多步推理强)",
"LLaVA-1.5": "★★★ (基础图表可以)",
"差距原因": "复杂推理能力差距",
},
"空间推理": {
"GPT-4V": "★★★ (一般)",
"LLaVA-1.5": "★★ (较弱)",
"差距原因": "两者在空间关系上都有局限",
},
"开放世界泛化": {
"GPT-4V": "★★★★★ (极强)",
"LLaVA-1.5": "★★★★ (较好)",
"差距原因": "GPT-4V 数据规模和多样性更大",
},
"代码生成": {
"GPT-4V": "★★★★★ (能生成 matplotlib 等)",
"LLaVA-1.5": "★★ (几乎没有)",
"差距原因": "LLaVA-1.5 没有相关训练数据",
},
}
return comparison

7.2 为什么开源模型与 GPT-4V 有差距?#

def gap_analysis():
"""
开源 VLM 与 GPT-4V 差距的分析。
"""
gaps = {
"预训练数据": {
"GPT-4V": "数万亿图文对 + 内部数据",
"LLaVA-1.5": "CC3M/LAION ~600K 对外公开",
"影响": "数据量差 100-1000 倍",
},
"模型规模": {
"GPT-4V": "GPT-4 (估计 1.8T 参数)",
"LLaVA-1.5": "Vicuna-7B + CLIP ViT-L = ~8B",
"影响": "模型规模差 200 倍",
},
"训练计算": {
"GPT-4V": "数百万 GPU 小时",
"LLaVA-1.5": "数十 GPU 小时 (开源可复现)",
"影响": "训练成本不可比",
},
"对齐数据": {
"GPT-4V": "大量人类标注的偏好数据",
"LLaVA-1.5": "GPT-4 生成数据",
"影响": "GPT-4V 的回答质量更高",
},
}
return gaps

8. 完整实现:LLaVA-1.5 架构#

import torch
import torch.nn as nn
import torch.nn.functional as F
from dataclasses import dataclass
@dataclass
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
patch_size: int = 14
class LLaVA15MLPProjector(nn.Module):
"""
LLaVA-1.5 的 MLP 投影器。
比 LLaVA 的单层线性投影效果更好。
"""
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"
"""
def __init__(self, config: LLaVA15Config):
super().__init__()
self.config = config
# 1) 视觉编码器 (冻结)
from transformers import AutoModel, AutoImageProcessor
self.vision_encoder = AutoModel.from_pretrained(config.vision_model)
self.image_processor = AutoImageProcessor.from_pretrained(config.vision_model)
self.vision_encoder.requires_grad_(False)
# 2) MLP 投影器 (训练)
self.mlp_projector = LLaVA15MLPProjector(
config.vision_hidden_size,
config.llm_hidden_size,
)
# 3) LLM (训练)
from transformers import AutoModelForCausalLM, AutoTokenizer
self.llm = AutoModelForCausalLM.from_pretrained(
config.llm_model,
torch_dtype=torch.float16,
device_map="auto",
trust_remote_code=True,
)
self.tokenizer = AutoTokenizer.from_pretrained(
config.llm_model,
trust_remote_code=True,
)
# 4) 特殊 token
special_tokens = ["[ IMG ]", "[ / IMG ]"]
self.tokenizer.add_tokens(special_tokens, special_tokens=True)
self.img_start_id = self.tokenizer("[ IMG ]", add_special_tokens=False).input_ids[0]
self.img_end_id = self.tokenizer("[ / IMG ]", add_special_tokens=False).input_ids[0]
self.llm.resize_token_embeddings(len(self.tokenizer))
# 5) 系统提示
self.system_prompt = (
"A chat between a curious human and an artificial intelligence assistant. "
"The assistant gives helpful, detailed, and polite answers to the user's questions."
)
def encode_images(self, images):
"""
编码图像,返回投影后的视觉 tokens。
"""
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)
image_embeds = outputs.last_hidden_state # (B, N+1, D_v)
# 去掉 [CLS],投影到 LLM 空间
visual_tokens = image_embeds[:, 1:, :] # (B, N, D_v)
visual_tokens = self.mlp_projector(visual_tokens) # (B, N, D_l)
return visual_tokens
def build_inputs(self, images, instruction, response=None):
"""
构建多模态输入。
"""
if isinstance(instruction, str):
instruction = [instruction]
if not isinstance(images, list):
images = [images] * len(instruction)
# 构建 prompt
prompts = []
for instr in instruction:
if response is not None:
prompt = (
f"{self.system_prompt} [ USER ] [ IMG ] [ / IMG ] "
f"{instr} [ ASSISTANT ] {response}"
)
else:
prompt = (
f"{self.system_prompt} [ USER ] [ IMG ] [ / IMG ] "
f"{instr} [ ASSISTANT ]"
)
prompts.append(prompt)
return images, prompts
def forward(self, images, instruction, response=None, labels=None):
"""
完整前向传播。
"""
# 编码图像
visual_tokens = self.encode_images(images) # (B, N, D_l)
# 构建输入
images, prompts = self.build_inputs(images, instruction, response)
# Tokenize
input_enc = self.tokenizer(
prompts,
return_tensors="pt",
padding=True,
truncation=True,
max_length=2048,
).to(self.llm.device)
# 文本 embedding
inputs_embeds = self.llm.get_input_embeddings()(input_enc["input_ids"])
# 插入视觉 token
B = inputs_embeds.shape[0]
device = inputs_embeds.device
for b in range(B):
sp_pos = (input_enc["input_ids"][b] == self.img_start_id).nonzero(as_tuple=True)[0]
ep_pos = (input_enc["input_ids"][b] == self.img_end_id).nonzero(as_tuple=True)[0]
if len(sp_pos) == 0:
continue
sp = sp_pos[0].item()
ep = ep_pos[0].item()
num_v = ep - sp - 1
before = inputs_embeds[b, :sp, :]
after = inputs_embeds[b, ep + 1:, :]
v_tok = visual_tokens[b, :num_v, :]
inputs_embeds[b] = torch.cat([before, v_tok, after], dim=0)
# 处理 attention mask 和 labels 的长度变化
T = inputs_embeds.shape[1]
new_mask = torch.ones(B, T, device=device)
new_mask[:, :input_enc["attention_mask"].shape[1]] = input_enc["attention_mask"]
if response is not None and labels is None:
# 训练模式:构建 labels
new_labels = input_enc["input_ids"].clone()
# [USER] 之后的内容是回答,需要计算 loss
# 简化:所有非 [IMG] 的 token 都计算 loss
new_labels[new_labels == self.img_start_id] = -100
new_labels[new_labels == self.img_end_id] = -100
else:
new_labels = labels
# LLM 前向
outputs = self.llm(
inputs_embeds=inputs_embeds,
attention_mask=new_mask,
labels=new_labels,
)
return outputs
@torch.no_grad()
def generate(self, images, instruction, max_new_tokens=512, temperature=0.7):
"""
生成回答。
"""
self.eval()
if isinstance(instruction, str):
instruction = [instruction]
if not isinstance(images, list):
images = [images] * len(instruction)
B = len(instruction)
# 编码图像
visual_tokens = self.encode_images(images)
# 构建输入(不带回答)
images_list, prompts = self.build_inputs(images, instruction, response=None)
# Tokenize
input_enc = self.tokenizer(
prompts,
return_tensors="pt",
padding=True,
truncation=True,
max_length=2048,
).to(self.llm.device)
# 嵌入 + 插入视觉 token
inputs_embeds = self.llm.get_input_embeddings()(input_enc["input_ids"])
for b in range(B):
sp_pos = (input_enc["input_ids"][b] == self.img_start_id).nonzero(as_tuple=True)[0]
ep_pos = (input_enc["input_ids"][b] == self.img_end_id).nonzero(as_tuple=True)[0]
if len(sp_pos) == 0:
continue
sp = sp_pos[0].item()
ep = ep_pos[0].item()
num_v = ep - sp - 1
before = inputs_embeds[b, :sp, :]
after = inputs_embeds[b, ep + 1:, :]
v_tok = visual_tokens[b, :num_v, :]
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

9. 微调实践:LoRA 高效训练#

def finetune_llava_lora(llava_model, dataset_path, output_dir):
"""
用 LoRA 高效微调 LLaVA-1.5。
"""
from peft import LoraConfig, get_peft_model
# 只微调 LLM 部分(投影器全量训练)
llava_model.llm = get_peft_model(
llava_model.llm,
LoraConfig(
r=64,
lora_alpha=16,
target_modules=["q_proj", "v_proj", "k_proj", "o_proj", "gate_proj", "up_proj"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
)
)
# 投影器全量训练
optimizer = torch.optim.AdamW(
list(llava_model.mlp_projector.parameters()) +
list(llava_model.llm.parameters()),
lr=2e-5,
weight_decay=0.0,
)
# 数据
dataset = LLaVADataset(dataset_path)
dataloader = DataLoader(dataset, batch_size=8, shuffle=True, num_workers=4)
# 训练
for epoch in range(3):
for batch in tqdm(dataloader):
outputs = llava_model(
images=batch["image"].to(device),
instruction=batch["instruction"],
response=batch["response"],
)
loss = outputs.loss
loss.backward()
torch.nn.utils.clip_grad_norm_(llava_model.parameters(), max_norm=1.0)
optimizer.step()
optimizer.zero_grad()
# 保存
llava_model.save_pretrained(f"{output_dir}/epoch_{epoch}")

10. LLaVA 的影响与遗产#

10.1 LLaVA 对开源多模态生态的贡献#

def llava_legacy():
"""
LLaVA 对开源社区的影响。
"""
impact = {
"视觉指令微调范式": {
"影响": "开创了用 GPT-4 生成多模态指令数据的范式",
"后续": "MiniGPT-4, InstructBLIP, Qwen-VL 等纷纷采用",
},
"简洁架构设计": {
"影响": "证明了简单 MLP + 指令微调可以匹敌复杂 Q-Former",
"后续": "OpenVLA, LLaVA-NeXT 等继续使用 MLP 架构",
},
"开源数据构建方法": {
"影响": "GPT-4 生成数据的pipeline被广泛复制",
"后续": "LLaVA-RLHF, LLaVA-Med 等基于相同方法",
},
"社区复现": {
"影响": "推动了 open-source VLM 的快速发展",
"后续": "8B 模型可以在单卡运行,降低了多模态 AI 的门槛",
},
}
return impact

10.2 LLaVA 衍生的重要工作#

直接衍生:
- MiniGPT-4 (2023): 用更少的参数实现类似 LLaVA 的效果
- InstructBLIP (2023): BLIP-2 + 指令微调
- Qwen-VL (2023): 阿里开源的中文多模态模型
- CogVLM (2024): 清华的深度视觉语言融合
数据方法衍生:
- LLaVA-RLHF (2024): 用 RLHF 进一步对齐
- LLaVA-Med (2024): 医疗领域的多模态模型
- LLaVA-Hailuo (2024): 视频理解版本
架构创新:
- VideoLLaVA (2024): 加入视频理解
- LLaVA-1.6 (2024): AnyRes 高分辨率
- LLaVA-NeXT (2024): 更大规模训练

11. 总结#

11.1 核心要点#

维度关键要点
核心创新视觉指令微调 + GPT-4 自动生成训练数据
架构设计CLIP ViT-L/14 (冻结) + MLP 投影 + Vicuna-7B
训练范式阶段 1(冻 LLM 训投影器)+ 阶段 2(解冻 LLM 指令微调)
数据构建Stage 1: BLIP-2 caption → GPT-4 改进 → Stage 2: GPT-4 生成问答
LLaVA-1.5 改进MLP 投影 + 336px 分辨率 + 大规模高质量数据
开源意义证明了简洁架构 + 高质量数据可以接近 GPT-4V
生态影响开创了开源 VLM 的视觉指令微调范式

11.2 LLaVA vs 其他 VLM#

LLaVA BLIP-2 Qwen-VL
架构 MLP(简单) Q-Former(复杂) 线性+注意力
训练数据 GPT-4生成 人工标注 混合
开源 完全 部分 完全
中文支持 弱 弱 强
分辨率 336px 224px 动态
LLaVA 的优势: 简单、高效、开源、可复现
LLaVA 的劣势: 中文弱、GPT-4数据有上限

11.3 一句话总结#

LLaVA 的核心贡献是”视觉指令微调”范式——用 GPT-4 自动生成高质量的图像-指令-回答三元组数据,然后微调一个 CLIP + MLP + Vicuna 的简洁架构,使开源模型也能回答”这张图里有什么""这张图表说明了什么”这类开放性视觉问题。LLaVA-1.5 通过 MLP 投影、336px 分辨率和大规模数据混合,将开源多模态模型的性能提升到接近 GPT-4V 的水平,成为开源 VLM 领域的里程碑。

11.4 推荐资源#

论文:
- LLaVA (Liu et al., 2023): "Visual Instruction Tuning"
- LLaVA-1.5 (Liu et al., 2023): "Improved Baselines with Visual Instruction Tuning"
- Vicuna (Chiang et al., 2023): "Vicuna: Open-Source Chatbot Impressing GPT-4"
代码:
- haotian-liu/LLaVA (官方实现)
- haotian-liu/LLaVA-Instruct-150K (数据集)
- lmsys/vicuna-7b-delta (LLM 权重)
相关工具:
- BLIP-2 (Salesforce): 用于生成初始 caption
- GPT-4 API: 用于生成指令数据
- peft (HuggingFace): 用于 LoRA 微调

文章分享

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

LLaVA 深度剖析:从视觉指令微调到开源多模态助手的完整技术栈
https://aiattnstudio.link/posts/llava/
作者
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标签