多模态指令微调

多模态指令微调(Multimodal Instruction Tuning)是赋予视觉-语言模型(VLM)遵循复杂视觉指令能力的关键技术。通过在特定格式的图文数据上进行有监督微调,模型能够理解人类意图并生成符合预期的多模态响应。1

1. 指令微调基础

1.1 视觉指令微调的定义

视觉指令微调(Visual Instruction Tuning)是指在多模态大模型训练过程中,使用包含视觉信息和自然语言指令的数据,让模型学会根据图像内容响应多样化的用户查询。与传统视觉任务不同,指令微调使模型能够处理开放式、复杂化的视觉理解任务。2

核心目标

  • 让模型理解图像中的视觉内容
  • 遵循自然语言指令执行特定任务
  • 生成连贯、准确的多模态响应

1.2 与传统VQA的区别

传统视觉问答(Visual Question Answering, VQA)与视觉指令微调存在本质差异:

维度传统VQA视觉指令微调
问题类型固定形式开放式、自由格式
响应格式短答案/选择题详细描述、推理过程
指令遵循强指令跟随能力
任务泛化单任务多任务统一处理
示例”图中有几只猫?""请详细描述这幅画的艺术风格,并分析其构图特点”

传统VQA的数据集(如VQA v2、CLEVR)通常包含有限的问题类型和固定答案格式。而视觉指令微调强调模型对任意形式视觉指令的理解和执行能力,这与大型语言模型(LLM)的通用性追求一脉相承。3

1.3 指令格式设计

LLaVA等经典工作设计了简洁而有效的指令格式:

# 用户输入格式
[USER] <image>
请描述这张图片的内容。
[ASSISTANT] 这是一张显示[具体描述]的图片。
 
# 多轮对话格式
[USER] <image>
图中的物体是什么?[ASSISTANT] 图中有一个[答案1]。[USER]
它是什么颜色的?[ASSISTANT] 它是[答案2]。[USER]
进一步描述它的特征。[ASSISTANT] [详细描述]

关键设计要素

  • <image>:特殊token标记图像插入位置
  • [USER]/[ASSISTANT]:角色标记便于对话建模
  • <stop>:结束标记控制生成长度
  • 模板化指令:支持数据高效生成

2. 指令数据构建

2.1 LLaVA数据生成方法

LLaVA的数据构建采用半自动化流水线,核心思想是利用GPT-4的语言能力辅助生成高质量指令数据。1

# LLaVA数据生成示意
class LLaVADataGenerator:
    def __init__(self, gpt4_api, image_captioner):
        self.gpt4 = gpt4_api          # GPT-4V用于图像理解
        self.captioner = image_captioner  # COCO Caption等
    
    def generate_instruction_data(self, image_path, num_samples=3):
        """
        为单张图像生成多条指令数据
        """
        # Step 1: 获取图像描述
        captions = self.captioner.generate(image_path)
        
        # Step 2: 使用GPT-4生成多样化指令
        conversation = self.gpt4.create_conversation(
            image_path=image_path,
            context=captions,
            num_turns=num_samples
        )
        
        return conversation

LLaVA-150K数据集构建流程

  1. 图像选择:从CC3M中选取59K张图像,要求具有丰富视觉内容
  2. 描述生成:使用GPT-4V为每张图生成两种描述
    • 简短描述(ShareGPT格式)
    • 详细描述(包含更多视觉细节)
  3. 指令生成:基于描述生成多样化的对话数据

2.2 GPT-4V辅助标注

GPT-4V在数据标注中扮演关键角色,其强大的视觉理解能力显著降低了标注成本:

# GPT-4V辅助标注示例
def gpt4v_annotation_pipeline(image, annotation_type="conversation"):
    """
    使用GPT-4V进行半自动化标注
    """
    if annotation_type == "conversation":
        # 生成多轮对话
        prompt = """
        基于图像内容,生成一段多轮对话,包含:
        1. 关于图像基本内容的问答
        2. 需要推理的复杂问题
        3. 开放性描述请求
        """
    elif annotation_type == "detailed_caption":
        # 生成详细描述
        prompt = """
        详细描述图像中的:物体、场景、颜色、布局、动作、情感氛围等。
        """
    
    response = gpt4v.generate(prompt, image)
    return parse_and_format(response)

GPT-4V标注的优势

  • 高质量视觉理解能力
  • 支持复杂推理问题生成
  • 可生成开放域、长格式响应
  • 降低人工标注成本

2.3 数据质量vs数量权衡

指令微调中,数据质量与数量的权衡是一个核心问题。研究表明这一权衡与模型规模密切相关:

模型规模推荐策略说明
小模型(<7B)质量优先少量高质量数据,避免过拟合
中模型(7B-13B)平衡策略10-100K级别,数据多样化
大模型(>70B)数量优先可从大规模数据中学习

数据质量评估维度

def evaluate_instruction_quality(instruction, response, image):
    """
    评估指令-响应对的质量
    """
    criteria = {
        # 指令质量
        "instruction_diversity": measure_diversity(instruction),  # 指令多样性
        "instruction_clarity": measure_clarity(instruction),       # 指令清晰度
        
        # 响应质量
        "response_accuracy": verify_accuracy(response, image),    # 响应准确性
        "response_relevance": measure_relevance(response, instruction),  # 相关性
        "response_completeness": measure_completeness(response),   # 完整性
        
        # 匹配度
        "alignment": measure_instruction_response_alignment(
            instruction, response
        )
    }
    
    return {k: v for k, v in criteria.items() if v > threshold}

2.4 Self-Instruct方法

Self-Instruct是一种利用模型自身能力生成指令数据的技术,最初由Alpaca提出并被引入多模态领域。4

class SelfInstructMultimodal:
    """
    多模态Self-Instruct方法
    """
    def __init__(self, base_model, image_processor):
        self.model = base_model
        self.image_proc = image_processor
    
    def generate_instructions(self, image, num_instructions=10):
        """
        自举式指令生成
        """
        # Step 1: 获取图像基本信息
        caption = self.model.describe_image(image)
        
        # Step 2: 生成种子指令
        seed_instructions = [
            "描述这张图片",
            "这张图片中的主要物体是什么",
            "图片中有什么有趣的地方",
            # ...更多种子指令
        ]
        
        # Step 3: 使用LLM生成新指令
        generated = []
        for seed in seed_instructions[:num_instructions]:
            new_instructions = self.model.generate(
                prompt=f"""
                基于图像描述:{caption}
                生成3个与以下指令类似但不完全相同的新指令:
                {seed}
                """
            )
            generated.extend(parse_instructions(new_instructions))
        
        # Step 4: 过滤低质量指令
        filtered = self.filter_instructions(generated)
        
        return filtered
    
    def filter_instructions(self, instructions):
        """
        过滤掉无效或低质量的指令
        """
        valid = []
        for inst in instructions:
            # 过滤条件
            if len(inst) < 5:  # 太短
                continue
            if self.is_duplicate(inst, valid):  # 重复
                continue
            if self.is_too_similar_to_seeds(inst):  # 与种子太相似
                continue
            valid.append(inst)
        return valid

Self-Instruct的迭代优化

  1. 初始化:使用人工编写的种子指令(通常5-10条)
  2. 生成:基于种子指令和图像内容生成新指令
  3. 过滤:移除无效、重复或质量较低的指令
  4. 扩展:使用高质量指令进一步扩展数据集

3. 训练策略

3.1 两阶段训练:预训练 + 指令微调

LLaVA等模型采用经典的两阶段训练范式,这是当前多模态指令微调的主流方法:

┌─────────────────────────────────────────────────────────────┐
│                    阶段1:预训练(特征对齐)                   │
├─────────────────────────────────────────────────────────────┤
│  数据:CC3M / LAION-CC-SB (59K-600K图文对)                    │
│  目标:学习视觉特征到语言空间的映射                            │
│  训练:仅更新投影层参数,冻结ViT和LLM                          │
│                                                              │
│       CLIP ViT         投影层W         LLaMA                 │
│    ┌──────────┐     ┌──────────┐     ┌──────────┐           │
│    │  冻结    │ ──→ │  可训练   │ ──→ │  冻结    │           │
│    └──────────┘     └──────────┘     └──────────┘           │
│                                                              │
│    Loss = Σ BCE(image_tokens, text_tokens)                   │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                    阶段2:指令微调                            │
├─────────────────────────────────────────────────────────────┤
│  数据:LLaVA-Instruct-150K(对话数据)                         │
│  目标:让模型学会遵循视觉指令                                  │
│  训练:投影层冻结,微调LLM和特殊token                          │
│                                                              │
│       CLIP ViT         投影层W         LLaMA                 │
│    ┌──────────┐     ┌──────────┐     ┌──────────┐           │
│    │  冻结    │ ──→ │  冻结    │ ──→ │  可训练   │           │
│    └──────────┘     └──────────┘     └──────────┘           │
│                                                              │
│    Loss = Σ CE(output_ids, target_ids)                       │
└─────────────────────────────────────────────────────────────┘

两阶段训练的数学形式化

阶段1的预训练目标:

其中 是图像, 是文本描述, 是投影层参数。

阶段2的指令微调目标:

3.2 全参数微调 vs LoRA适配器

多模态指令微调面临与NLP类似的全参数微调(Full Fine-tuning)与参数高效微调(PEFT)的权衡:

维度全参数微调LoRA/QLoRA
显存需求高(需存储所有梯度)低(仅训练适配器)
训练速度
参数量所有参数1-5%原始参数
泛化能力可能过拟合更好的泛化
硬件要求A100-80G单卡RTX 3090可行

全参数微调代码示例

import torch
from transformers import AutoModelForVision2Seq, AutoProcessor
 
def full_finetune_training_loop(
    model_name: str,
    train_dataset,
    num_epochs: int = 3,
    learning_rate: float = 2e-5,
    warmup_steps: int = 100
):
    """
    全参数微调训练循环
    """
    # 加载模型和处理器
    model = AutoModelForVision2Seq.from_pretrained(
        model_name,
        torch_dtype=torch.bfloat16,
        device_map="auto"
    )
    processor = AutoProcessor.from_pretrained(model_name)
    
    # 配置优化器
    optimizer = torch.optim.AdamW(
        model.parameters(),
        lr=learning_rate,
        weight_decay=0.01
    )
    
    # 学习率调度器
    def lr_lambda(step):
        if step < warmup_steps:
            return step / warmup_steps
        return max(0.1, 0.5 * (1 + math.cos(math.pi * step / total_steps)))
    
    scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda)
    
    # 训练循环
    for epoch in range(num_epochs):
        model.train()
        for batch_idx, batch in enumerate(train_dataset):
            # 前向传播
            outputs = model(
                pixel_values=batch["pixel_values"],
                input_ids=batch["input_ids"],
                attention_mask=batch["attention_mask"],
                labels=batch["labels"]
            )
            
            loss = outputs.loss
            
            # 反向传播
            loss.backward()
            
            # 梯度裁剪
            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
            
            optimizer.step()
            scheduler.step()
            optimizer.zero_grad()
            
            if batch_idx % 100 == 0:
                print(f"Epoch {epoch}, Step {batch_idx}, Loss: {loss.item():.4f}")
    
    return model

LoRA微调代码示例

from peft import LoraConfig, get_peft_model, TaskType
 
def lora_finetune_training_loop(
    model_name: str,
    train_dataset,
    num_epochs: int = 3,
    lora_rank: int = 16,
    lora_alpha: int = 32,
    lora_dropout: float = 0.05,
    learning_rate: float = 1e-4
):
    """
    LoRA参数高效微调训练循环
    """
    # 加载基础模型
    model = AutoModelForVision2Seq.from_pretrained(
        model_name,
        torch_dtype=torch.bfloat16,
        device_map="auto"
    )
    
    # 配置LoRA
    lora_config = LoraConfig(
        task_type=TaskType.CAUSAL_LM,
        r=lora_rank,                    # LoRA秩
        lora_alpha=lora_alpha,          # 缩放因子
        lora_dropout=lora_dropout,
        target_modules=[               # 目标模块
            "q_proj", "v_proj", 
            "k_proj", "o_proj",
            "gate_proj", "up_proj", "down_proj"
        ],
        bias="none"
    )
    
    # 应用LoRA
    model = get_peft_model(model, lora_config)
    model.print_trainable_parameters()
    # 输出:trainable params: 4.2M || all params: 7B || trainable%: 0.06%
    
    # 后续训练循环与全参数微调类似
    # ...

3.3 视觉指令微调最佳实践

基于LLaVA、InstructBLIP等工作的实践经验,以下是视觉指令微调的关键最佳实践:

1. 数据配比策略

class InstructionDataMixer:
    """
    指令微调数据混合策略
    """
    def __init__(self):
        self.data_sources = {
            "conversation": 0.4,      # 对话数据
            "detailed_caption": 0.2,  # 详细描述
            "reasoning": 0.2,         # 推理问题
            "multi_turn": 0.2         # 多轮对话
        }
    
    def create_balanced_batch(self, datasets, batch_size=32):
        """
        创建均衡的batch
        """
        batch = []
        for source, ratio in self.data_sources.items():
            num_samples = int(batch_size * ratio)
            samples = datasets[source].sample(num_samples)
            batch.extend(samples)
        
        # 打乱顺序
        random.shuffle(batch)
        return batch

2. 课程学习(Curriculum Learning)

训练从简单到复杂的指令类型,有助于稳定收敛:

阶段1 → 简单描述:"这张图里有什么?" 
阶段2 → 复杂推理:"为什么图中的人物会这样做?"
阶段3 → 开放式对话:"这幅画反映了什么社会现象?"

3. 训练稳定性技巧

  • 梯度累积:小batch下模拟大批次训练
  • 混合精度:bfloat16/float16减少显存
  • 早停机制:基于验证集loss监控过拟合

3.4 Scaling Instruction-Tuned MLLMs

arXiv:2309.09958系统研究了缩放视觉指令微调的效果,发现了若干重要规律。5

主要发现

  1. 指令数据量与模型能力的关系

    • 规模法则(Scaling Law)在多模态领域同样适用
    • 指令数据规模超过一定阈值后,模型出现”涌现”能力
  2. 指令多样性比数量更重要

    • 多样化的指令类型比单纯增加数据量更有效
    • 覆盖更多任务类型有助于泛化
  3. 模型-数据匹配原则

    • 小模型在大规模指令数据上可能过拟合
    • 大模型能从更多样的指令中受益
# Scaling实验示意
def run_scaling_experiment(
    model_sizes=[7, 13, 34, 70],  # 模型参数量(B)
    data_sizes=[10, 50, 150, 500], # 指令数据量(K)
    eval_tasks=["vqa", "caption", "reasoning"]
):
    """
    模拟scaling实验
    """
    results = []
    
    for model_size in model_sizes:
        for data_size in data_sizes:
            # 训练模型
            model = train_model(size=model_size, data_size=data_size)
            
            # 评估各任务
            task_results = {}
            for task in eval_tasks:
                score = evaluate(model, task)
                task_results[task] = score
            
            results.append({
                "model_size": model_size,
                "data_size": data_size,
                "avg_score": np.mean(list(task_results.values())),
                "task_scores": task_results
            })
    
    # 分析scaling曲线
    analyze_scaling_curves(results)
    
    return results

4. 高效微调方法

4.1 LLaVA-1.5设计选择

LLaVA-1.5在架构设计上做了若干关键改进,显著提升了模型性能:6

核心改进

  1. MLP投影层:使用2层MLP替代线性投影
  2. 高质量数据:引入SFT数据质量过滤
  3. 更丰富的指令类型:VQA、详细描述、推理等多类型数据
class LLaVA15Projector(nn.Module):
    """
    LLaVA-1.5 MLP投影层设计
    """
    def __init__(self, vision_dim: int, llm_dim: int, hidden_dim: int = 4096):
        super().__init__()
        
        # 两层MLP:降维/升维 + GELU激活
        self.projector = nn.Sequential(
            nn.Linear(vision_dim, hidden_dim),
            nn.GELU(),
            nn.Linear(hidden_dim, llm_dim)
        )
    
    def forward(self, vision_features):
        return self.projector(vision_features)
 
 
class LLaVA15Config:
    """
    LLaVA-1.5关键配置
    """
    def __init__(self):
        # 视觉编码器
        self.vision_model = "CLIP-ViT-L/14"
        self.image_size = 336  # 高分辨率
        
        # 投影层
        self.use_mlp_projector = True
        self.mlp_hidden_dim = 4096
        
        # 训练配置
        self.lr = 1e-3          # 预训练阶段高学习率
        self.lr_ft = 2e-5       # 微调阶段低学习率
        
        # 数据配置
        self.instruction_types = [
            "conversation",      # 对话
            "detailed_desc",     # 详细描述
            "complex_reasoning", # 复杂推理
            "regional_caption"   # 区域描述
        ]

4.2 MiniGPT-4对齐策略

MiniGPT-4采用了与LLaVA不同的对齐策略,强调保持预训练知识的同时学习视觉理解:7

核心设计

class MiniGPT4AlignmentStrategy:
    """
    MiniGPT-4对齐策略
    """
    def __init__(self, vision_encoder, llm, q_former=None):
        self.vision_encoder = vision_encoder
        self.llm = llm
        self.q_former = q_former  # 可选的Q-Former中间层
    
    def stage1_pretraining(self, image_text_pairs):
        """
        阶段1:特征对齐预训练
        
        冻结:ViT、Q-Former、LLM
        训练:线性投影层
        """
        for param in self.vision_encoder.parameters():
            param.requires_grad = False
        for param in self.q_former.parameters():
            param.requires_grad = False
        for param in self.llm.parameters():
            param.requires_grad = False
        
        projection = self._init_projection()
        
        # 对齐损失:视觉特征 → 语言空间
        loss = self.compute_alignment_loss(projection)
        return loss
    
    def stage2_instruction_tuning(self, instruction_data):
        """
        阶段2:指令微调
        
        冻结:ViT、Q-Former
        训练:投影层 + LLM(部分层)
        """
        for param in self.vision_encoder.parameters():
            param.requires_grad = False
        
        # 可选:只微调LLM的后面几层
        self._freeze_early_layers(self.llm, num_frozen=30)
        
        # 指令微调损失
        loss = self.compute_instruction_loss(instruction_data)
        return loss

4.3 QLoRA在多模态中的应用

QLoRA(Quantized Low-Rank Adaptation)将量化和LoRA结合,可在消费级GPU上微调大型多模态模型:8

from transformers import BitsAndBytesConfig
from peft import prepare_model_for_kbit_training, get_peft_model
 
def qlora_multimodal_finetune(
    model_name: str,
    train_data,
    quantization_config=None
):
    """
    QLoRA多模态微调
    """
    # 量化配置
    bnb_config = BitsAndBytesConfig(
        load_in_4bit=True,                    # 4bit量化加载
        bnb_4bit_quant_type="nf4",            # NF4量化类型
        bnb_4bit_compute_dtype=torch.bfloat16,
        bnb_4bit_use_double_quant=True        # 双重量化
    )
    
    # 加载量化模型
    model = AutoModelForVision2Seq.from_pretrained(
        model_name,
        quantization_config=bnb_config,
        device_map="auto"
    )
    
    # 准备k-bit训练
    model = prepare_model_for_kbit_training(model)
    
    # LoRA配置
    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=TaskType.CAUSAL_LM
    )
    
    # 应用LoRA
    model = get_peft_model(model, lora_config)
    
    # 训练(显存占用大幅降低)
    trainer = CustomTrainer(model=model, data=train_data)
    return trainer.train()

QLoRA的优势

指标标准微调QLoRA微调
7B模型显存~28GB~6GB
13B模型显存~56GB~12GB
训练速度基准略慢(量化开销)
模型质量基准~99%保持

4.4 视觉适配器设计

视觉适配器(Vision Adapter)是连接视觉编码器和语言模型的关键模块,设计选择直接影响模型性能:

主要类型

# 1. 线性投影(Simple Linear)
class LinearProjector(nn.Module):
    """最简单的投影方式"""
    def __init__(self, vision_dim, llm_dim):
        super().__init__()
        self.proj = nn.Linear(vision_dim, llm_dim)
    
    def forward(self, x):
        return self.proj(x)
 
 
# 2. MLP投影(LLaVA-1.5风格)
class MLPProjector(nn.Module):
    """两层MLP,增强表达能力"""
    def __init__(self, vision_dim, llm_dim, hidden_dim=4096):
        super().__init__()
        self.proj = nn.Sequential(
            nn.Linear(vision_dim, hidden_dim),
            nn.GELU(),
            nn.Linear(hidden_dim, llm_dim)
        )
    
    def forward(self, x):
        return self.proj(x)
 
 
# 3. Q-Former投影(BLIP-2风格)
class QFormerProjector(nn.Module):
    """使用交叉注意力机制"""
    def __init__(self, vision_dim, llm_dim, num_queries=32):
        super().__init__()
        self.q_former = QueryTransformer(vision_dim, llm_dim, num_queries)
        self.num_queries = num_queries
    
    def forward(self, vision_features):
        # vision_features: [B, N, D_vision]
        # 输出: [B, num_queries, D_llm]
        return self.q_former(vision_features)
 
 
# 4. Cross-Attention适配器
class CrossAttentionAdapter(nn.Module):
    """交叉注意力适配器"""
    def __init__(self, vision_dim, llm_dim, num_heads=8):
        super().__init__()
        self.cross_attn = nn.MultiheadAttention(
            embed_dim=llm_dim,
            num_heads=num_heads,
            kdim=vision_dim,
            vdim=vision_dim
        )
        self.norm = nn.LayerNorm(llm_dim)
        self.ffn = nn.Sequential(
            nn.Linear(llm_dim, llm_dim * 4),
            nn.GELU(),
            nn.Linear(llm_dim * 4, llm_dim)
        )
    
    def forward(self, text_features, vision_features):
        # text_features: [B, T, D_llm]
        # vision_features: [B, N, D_vision]
        attn_out, _ = self.cross_attn(
            query=text_features,
            key=vision_features,
            value=vision_features
        )
        return self.ffn(self.norm(attn_out))

5. 多模态RLHF与对齐

5.1 视觉-语言偏好对齐

多模态RLHF(Reinforcement Learning from Human Feedback)将人类反馈机制引入视觉-语言对齐,是提升模型安全性和有用性的关键步骤。9

核心流程

┌─────────────────────────────────────────────────────────────────┐
│                    多模态RLHF流程                                │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  阶段1: 收集人类偏好数据                                         │
│  ┌─────────┐     ┌─────────┐                                    │
│  │ <image> │     │ <image> │                                    │
│  └────┬────┘     └────┬────┘                                    │
│       ↓               ↓                                          │
│  ┌─────────────────────────────┐                                 │
│  │ Response A vs Response B    │ → 人类选择偏好                  │
│  │ 哪个回答更好?(A/B)         │                                 │
│  └─────────────────────────────┘                                 │
│                                                                  │
│  阶段2: 训练多模态奖励模型                                        │
│  ┌──────────────────────────────────────┐                       │
│  │ Reward Model: (image, question, ans) → r ∈ ℝ                │
│  │ 预测人类偏好分数                       │                       │
│  └──────────────────────────────────────┘                       │
│                                                                  │
│  阶段3: 使用RL优化                                               │
│  ┌──────────────────────────────────────┐                       │
│  │ PPO/GRPO优化策略                       │                       │
│  │ 最大化reward同时约束KL散度              │                       │
│  └──────────────────────────────────────┘                       │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

5.2 多模态奖励模型

多模态奖励模型(Reward Model)的设计需要同时理解图像、问题和回答:

class MultimodalRewardModel(nn.Module):
    """
    多模态奖励模型
    """
    def __init__(self, vision_encoder, llm, reward_head_dim=512):
        super().__init__()
        self.vision_encoder = vision_encoder
        self.llm = llm
        
        # 冻结视觉编码器
        for param in self.vision_encoder.parameters():
            param.requires_grad = False
        
        # 投影层
        self.projector = nn.Linear(
            vision_encoder.embed_dim,
            llm.hidden_size
        )
        
        # 奖励头
        self.reward_head = nn.Sequential(
            nn.Linear(llm.hidden_size, reward_head_dim),
            nn.GELU(),
            nn.Linear(reward_head_dim, 1)
        )
    
    def forward(self, images, input_ids, attention_mask):
        """
        计算偏好分数
        """
        # 视觉编码
        vision_features = self.vision_encoder(images)
        vision_tokens = self.projector(vision_features)
        
        # 文本编码
        text_tokens = self.llm.get_input_embeddings()(input_ids)
        
        # 合并并通过LLM
        inputs_embeds = self._merge_inputs(vision_tokens, text_tokens, input_ids)
        
        outputs = self.llm(
            inputs_embeds=inputs_embeds,
            attention_mask=attention_mask,
            output_hidden_states=True
        )
        
        # 使用最后一层最后一个token的表示作为奖励
        last_hidden = outputs.hidden_states[-1]
        reward = self.reward_head(last_hidden[:, -1, :])
        
        return reward.squeeze(-1)
    
    def compute_preference_loss(
        self, 
        chosen_rewards, 
        rejected_rewards,
        margin=0.5
    ):
        """
        Bradley-Terry偏好损失
        
        Loss = -log(σ(r_chosen - r_rejected - margin))
        """
        diff = chosen_rewards - rejected_rewards
        loss = -F.logsigmoid(diff - margin).mean()
        return loss

5.3 多模态RLHF流程

class MultimodalPPOTrainer:
    """
    多模态PPO训练
    """
    def __init__(
        self,
        policy_model,      # 待优化的策略模型
        ref_model,         # 参考模型(约束KL)
        reward_model,      # 奖励模型
        vision_encoder,
        tokenizer
    ):
        self.policy = policy_model
        self.ref = ref_model
        self.reward = reward_model
        self.vision_encoder = vision_encoder
        self.tokenizer = tokenizer
        
        # PPO超参数
        self.clip_eps = 0.2
        self.kl_coef = 0.04
    
    def step(self, batch):
        """
        单步PPO更新
        """
        images, prompts, responses = batch
        
        # 1. 在当前策略下采样响应
        with torch.no_grad():
            log_probs_old = self.get_log_probs(images, prompts, responses)
            values_old = self.get_values(images, prompts, responses)
        
        # 2. 计算奖励
        rewards = self.compute_rewards(images, prompts, responses)
        
        # 3. 计算GAE(广义优势估计)
        advantages, returns = self.compute_gae(rewards, values_old)
        
        # 4. PPO策略更新
        for _ in range(self.ppo_epochs):
            # 当前策略的log_probs
            log_probs = self.get_log_probs(images, prompts, responses)
            
            # 计算比率
            ratio = torch.exp(log_probs - log_probs_old)
            
            # Clipped surrogate loss
            surr1 = ratio * advantages
            surr2 = torch.clamp(ratio, 1-self.clip_eps, 1+self.clip_eps) * advantages
            policy_loss = -torch.min(surr1, surr2).mean()
            
            # KL散度损失
            kl_loss = self.compute_kl_divergence(images, prompts)
            
            # 总损失
            total_loss = policy_loss + self.kl_coef * kl_loss
            
            # 反向传播
            self.optimizer.zero_grad()
            total_loss.backward()
            self.optimizer.step()
        
        return {
            "policy_loss": policy_loss.item(),
            "kl_loss": kl_loss.item(),
            "reward_mean": rewards.mean().item()
        }
    
    def compute_rewards(self, images, prompts, responses):
        """
        计算每个token的reward
        """
        rewards = []
        
        for i in range(len(images)):
            # 拼接prompt和response
            full_text = prompts[i] + responses[i]
            
            # 使用奖励模型评分
            reward_score = self.reward(
                images[i].unsqueeze(0),
                self.tokenizer(full_text, return_tensors="pt")["input_ids"]
            )
            
            # 在响应末尾添加reward
            reward = torch.zeros(len(responses[i]))
            reward[-1] = reward_score
            rewards.append(reward)
        
        return rewards

6. 最新进展

6.1 视觉提示调优

视觉提示调优(Visual Prompt Tuning)将NLP领域的prompt tuning扩展到多模态领域:

class VisualPromptTuning(nn.Module):
    """
    视觉提示调优:在视觉编码器输入端添加可学习提示
    """
    def __init__(self, vision_encoder, num_prompts=8, prompt_dim=None):
        super().__init__()
        self.vision_encoder = vision_encoder
        
        vision_dim = vision_encoder.embed_dim
        self.prompt_dim = prompt_dim or vision_dim
        
        # 可学习的视觉提示
        self.visual_prompts = nn.Parameter(
            torch.randn(1, num_prompts, self.prompt_dim) * 0.02
        )
    
    def forward(self, images):
        # 获取原始视觉特征
        features = self.vision_encoder(images)  # [B, N, D]
        
        # 在特征前添加可学习提示
        prompts = self.visual_prompts.expand(images.size(0), -1, -1)
        enhanced_features = torch.cat([prompts, features], dim=1)
        
        return enhanced_features

6.2 多模态Chain-of-Thought

多模态思维链(Multimodal CoT)将CoT推理能力引入视觉-语言模型:

class MultimodalChainOfThought:
    """
    多模态思维链推理
    """
    def __init__(self, model):
        self.model = model
    
    def think(self, image, question, max_steps=3):
        """
        逐步推理
        """
        context = {
            "image_description": None,
            "relevant_objects": None,
            "reasoning_steps": []
        }
        
        # 步骤1:理解图像内容
        context["image_description"] = self.describe(image)
        
        # 步骤2:识别相关物体
        context["relevant_objects"] = self.identify_objects(
            image, 
            context["image_description"]
        )
        
        # 步骤3:执行推理
        for step in range(max_steps):
            reasoning = self.model.generate(
                prompt=self._build_step_prompt(
                    question, 
                    context,
                    step
                ),
                image=image
            )
            context["reasoning_steps"].append(reasoning)
            
            if self.is_final_answer(reasoning):
                break
        
        # 最终答案
        final_answer = self.model.generate(
            prompt=self._build_answer_prompt(question, context),
            image=image
        )
        
        return {
            "description": context["image_description"],
            "reasoning": context["reasoning_steps"],
            "answer": final_answer
        }

6.3 指令跟随能力提升

提升模型的指令跟随能力是当前研究的热点方向:

主要方法

  1. 数据层面:构建更高质量的指令微调数据
  2. 训练层面:改进训练策略和损失函数
  3. 架构层面:设计更强大的视觉-语言融合模块
class InstructionFollowingEvaluator:
    """
    指令跟随能力评估
    """
    def __init__(self, model, tokenizer):
        self.model = model
        self.tokenizer = tokenizer
        
        # 评估维度
        self.metrics = {
            "format_adherence": self.check_format,
            "content_relevance": self.check_relevance,
            "instruction_clarity": self.check_clarity,
            "task_completion": self.check_completion
        }
    
    def evaluate_instruction_following(
        self, 
        image, 
        instruction, 
        response
    ):
        """
        多维度评估指令跟随能力
        """
        scores = {}
        
        for metric_name, metric_fn in self.metrics.items():
            scores[metric_name] = metric_fn(
                instruction, 
                response
            )
        
        # 综合评分
        scores["overall"] = np.mean(list(scores.values()))
        
        return scores
    
    def check_format(self, instruction, response):
        """
        检查格式遵循度
        """
        # 检查是否遵循指令要求的格式
        # 如:是否使用markdown、是否分点回答等
        format_requirements = self.parse_format_requirements(instruction)
        return self.compute_format_score(response, format_requirements)
    
    def check_relevance(self, instruction, response):
        """
        检查内容相关性
        """
        instruction_entities = self.extract_entities(instruction)
        response_entities = self.extract_entities(response)
        
        overlap = len(instruction_entities & response_entities)
        return overlap / max(len(instruction_entities), 1)

相关内容

参考文献

Footnotes

  1. Liu H, Li C, Wu Q, et al. Visual Instruction Tuning [A]. NeurIPS, 2023. (LLaVA原始论文) 2

  2. Liu H, Li C, Wu Q, et al. Improved Baselines with Visual Instruction Tuning [A]. arXiv, 2024. (LLaVA-1.5)

  3. Zhou S, Wang L, Yao J, et al. Linear Projector: Emerging Architecture in Multimodal Learning [A]. ICLR Workshop, 2024.

  4. Wang Y, Kordi Y, Mishra S, et al. Self-Instruct: Aligning Language Model with Self Generated Instructions [A]. AAAI, 2023.

  5. Sun Q, Cui Y, Xia Z, et al. Multimodal Instruction Tuning [A]. arXiv:2309.09958, 2023.

  6. Liu H, Li C, Zhang Y, et al. LLaVA-1.5: Improved Results with Data Efficiency [A]. arXiv:2310.03744, 2023.

  7. Zhu D, Chen J, Shen X, et al. MiniGPT-4: Enhancing Vision Language Understanding with One Single Projection Layer [A]. arXiv:2304.10592, 2023.

  8. Dettmers T, Pagnoni A, Holtzman A, et al. QLoRA: Efficient Finetuning of Quantized LLMs [A]. NeurIPS, 2023.

  9. Sun Z, Shen S, Cao S, et al. Aligning Large Multimodal Models with Factually Augmented RLHF [A]. arXiv:2309.14304, 2023.