对训练的三个核心阶段进行从理论到代码的逐层、公式化拆解。

第一阶段:大规模视觉-语言预训练

目标:建立图像特征到LLM语义空间的“基础语言对齐”。

步骤1:数据准备与模型初始化
  • 论文(3.1节):使用14亿对弱标注的 (图像I, 文本描述T)。LLM(Qwen-7B)冻结,视觉编码器(ViT-bigG)使用OpenCLIP预训练权重初始化,VL适配器随机初始化。
  • 理论实现:此阶段通常有独立的预训练脚本,不在 finetune.py 中。但其核心思想——冻结部分模块——在第三阶段的代码中反向体现。
步骤2:前向传播与序列构建
  • 视觉编码:图像 I (224∗224)(224*224)(224224)经ViT处理,输出特征序列 V∈RN×DvV ∈ R^{N×D_v}VRN×Dv(N≈577)。
  • 适配器压缩:V′=Adapter(V)V' = Adapter(V)V=Adapter(V),其中 V′∈R256×DllmV'∈ R^{256×D_{llm}}VR256×Dllm
    • 适配器是单层交叉注意力:V′=softmax((QWq)(KWk)T/√d)(VWv)V' = softmax((Q W_q) (K W_k)^T / √d) (V W_v)V=softmax((QWq)(KWk)T/√d)(VWv)
    • 关键:键 K = 值 V,且在计算注意力前,为 V的每个空间位置特征显式添加二维绝对位置编码 PE2d(x,y)PE_{2d}(x, y)PE2d(x,y)
  • 序列构建:构建LLM的输入序列X:
    X=[token<img>,V1′,...,V256′,token</img>,tokent1,...,tokentL]X = [token_{<img>}, V'_1, ..., V'_{256}, token_{</img>}, token_{t1}, ..., token_{tL}]X=[token<img>,V1,...,V256,token</img>,tokent1,...,tokentL]
    这里,Vi′V'_iVi 被视为一个独立的“视觉标记”,其嵌入维度与LLM的词嵌入对齐。
步骤3:损失计算与反向传播
  • 前向计算:冻结的LLM接收序列 X,执行标准的自回归Transformer前向传播,输出每个位置的下一个词预测逻辑值 logits。

  • 损失函数:标准的因果语言建模(Causal LM)损失,即下一个词预测的交叉熵,但仅对文本部分计算。
    Lpre=−Σi=offsetL−1logP(tokeni+1∣X[0:i+1])L_{pre} = - Σ_{i=offset}^{L-1} log P(token_{i+1} | X[0:i+1])Lpre=Σi=offsetL1logP(tokeni+1X[0:i+1])
    其中 offset 是视觉标记序列结束后的位置(即 </img> 之后)。视觉标记本身不参与损失计算。

  • 反向传播与更新:

    • 计算损失 LpreL_{pre}Lpre 对模型参数的梯度 ∇L∇LL
    • 由于LLM参数被冻结(requires_grad=False),梯度流在LLM处被阻断。
    • 仅视觉编码器(ViT)和适配器(Adapter)的参数根据梯度 ∇LViT/Adapter∇L_{ViT/Adapter}LViT/Adapter 更新。
  • 代码逻辑映射:此阶段的冻结逻辑与 finetune.py 中以下代码逻辑完全相反但原理一致:

    # finetune.py 中是冻结 ViT,训练 LLM
    if training_args.fix_vit:
        model.transformer.visual.requires_grad_(False)
    # 第一阶段是冻结 LLM,训练 ViT(代码中无直接对应,但训练框架相同)
    # model.transformer.requires_grad_(False) # 假想代码
    

第二阶段:多任务预训练

目标:注入细粒度多任务监督,解锁全模型能力。

步骤1:数据与模型准备
  • 论文(3.2节):输入分辨率提升至 448x448。使用7类高质量数据(VQA、定位、OCR等)打包成交错序列。解锁LLM,全模型端到端训练。
  • 数据格式示例(定位任务):一个样本包含图像 I、问题 Q、答案(包含坐标框)A。答案 A被格式化为:<ref> 物体 (x1,y1),(x2,y2) </ref>。
步骤2:前向传播与复杂序列构建
  • 高分辨率编码:V=ViT(I448)V = ViT(I_{448})V=ViT(I448),得到更长的特征序列(N≈1024)。适配器压缩过程同上,但处理的信息更细粒度。
  • 复杂序列构建:这是本阶段的核心。以定位任务为例,构建的提示序列 X 为:
    KaTeX parse error: Undefined control sequence: \n at position 42: …m_{start}|>user\̲n̲ ̲Q <|im_end|>\n …
    其中 A 包含格式化的坐标文本。所有元素(图片特征、特殊标记、问题、坐标数字)都被线性化并嵌入到一个统一的序列中。
步骤3:统一损失计算与全模型更新
  • 前向计算:整个序列 X 输入已解锁的全模型进行前向传播。
  • 损失函数:仍然是统一的Causal LM损失。
    Lmulti=−Σi∈有效位置logP(X[i+1]∣X[0:i+1])L_{multi} = - Σ_{i ∈ 有效位置} log P(X[i+1] | X[0:i+1])Lmulti=Σi有效位置logP(X[i+1]X[0:i+1])
    “有效位置”现在包括需要模型生成的文本部分,即答案 A 中的所有token(包括坐标数字和特殊标记)。问题和系统提示部分在 labels 中被掩蔽(类似第三阶段代码中的 IGNORE_TOKEN_ID)。
  • 反向传播与更新:
    • 损失 L_{multi} 反向传播。
    • 梯度 ∇L 流经LLM、适配器、ViT。
    • 全模型所有参数同步更新。
  • 代码逻辑映射:finetune.py 中的 preprocess 函数完美体现了这种复杂序列构建和损失掩蔽的精髓,虽然它处理的是对话数据,但核心逻辑一致:将多模态、多任务输入统一为标记序列,并通过精细的 labels 掩蔽来指导模型学习生成特定部分。

第三阶段:监督式微调

目标:对齐人类对话指令,打造 Qwen-VL-Chat

步骤1:模型加载与参数冻结
  • 论文(3.3节):加载第二阶段训练好的 Qwen-VL 检查点。冻结视觉编码器(ViT),仅优化LLM和适配器。使用约35万条指令对话数据。

  • 代码实现:

    # 1. 加载预训练好的 Qwen-VL 模型(包含已训练好的ViT和Adapter)
    model = AutoModelForCausalLM.from_pretrained(model_args.model_name_or_path, ...)
    
    # 2. 冻结视觉编码器(对应论文中的“冻结视觉编码器”)
    if training_args.fix_vit:
        model.transformer.visual.requires_grad_(False)  # ViT主干冻结
        # 可能保持适配器或视觉部分最后一层可训练
        if hasattr(model.transformer.visual, 'attn_pool'):
            model.transformer.visual.attn_pool.requires_grad_(True)
    
    # 3. (可选)配置LoRA进行参数高效微调
    if training_args.use_lora:
        lora_config = LoraConfig(r=lora_args.lora_r, ...)
        model = get_peft_model(model, lora_config) # 仅LoRA参数可训练
    
步骤2:数据预处理与标签构建(代码核心)
  • 原始数据格式:JSON中的多轮对话 conversations: [{"from": "user", "value": "..."}, {"from": "assistant", "value": "..."}]
  • preprocess 函数详解:
    目标:生成 input_ids(模型输入)和 labels(计算损失的目标)。
    1. 添加系统提示:system_tokens = [im_start] + tokenize(‘system\n‘) + tokenize(system_message) + [im_end, \n]
    2. 遍历对话轮次:对于每一句 sentence
      • 编码角色和内容:role_tokens = tokenize(‘<|im_start|>‘ + role + ‘\n‘)
      • _input_id = role_tokens + tokenize(sentence[“value”]) + [im_end, \n]
    3. 关键:生成 labels(损失掩蔽):
      • 对于 用户回合:_target = [im_start] + [IGNORE_TOKEN_ID] * (len(_input_id)-3) + [im_end, \n]。这意味着用户说的话不作为模型需要生成的目标。
      • 对于 助理回合:_target = [im_start] + [IGNORE_TOKEN_ID]*len(role_tokens) + _input_id[len(role_tokens)+1:-2] + [im_end, \n]。这意味着只将助理回复的真实内容(去掉角色标记和结束符) 作为生成目标。
      • IGNORE_TOKEN_ID 在CrossEntropyLoss中会被忽略。
    4. 拼接与填充:将所有轮次的 _input_id_target 分别拼接,然后填充到 max_len
步骤3:训练循环与损失计算
  • 批次数据:DataLoader 提供一个批次的数据:{‘input_ids‘: Tensor[B, L], ‘labels‘: Tensor[B, L], ‘attention_mask‘: ...}
  • 前向传播:logits = model(input_ids=input_ids, attention_mask=attention_mask).logitslogits 形状为 [B, L, Vocab_Size]
  • 损失计算:
    • 标准公式:L_sft = CrossEntropyLoss(logits.view(-1, V), labels.view(-1))
    • PyTorch 内部细节:nn.CrossEntropyLoss(ignore_index=IGNORE_TOKEN_ID) 会自动忽略 labels 中为 IGNORE_TOKEN_ID 的位置,只对有效位置(即助理回复内容)计算损失。
    • 这等价于:L_sft = - (1/N) Σ_{i, 其中 labels[i] != IGNORE_TOKEN_ID} log( softmax(logits[i])[ labels[i] ] )
  • 反向传播与更新:
    • 计算 ∇L_sft
    • 由于ViT被冻结,梯度仅更新LLM的参数和适配器的参数(如果使用LoRA,则仅更新LoRA的少量参数)。
    • 优化器(如AdamW)执行参数更新。

总结:三阶段训练的演进与统一公式

阶段 可训练参数 (θ_train) 冻结参数 数据序列 (X) 构成 损失计算的有效位置
预训练 ViT, Adapter LLM [IMG] 图像特征 [/IMG] 文本描述 文本描述的token
多任务预训练 全模型 (ViT, Adapter, LLM) [IMG] 图像特征 [/IMG] [指令] 问题 [/指令] [答案] 复杂输出 [/答案] 答案中的所有token(文本、坐标等)
监督微调 LLM, Adapter (或 LoRA) ViT [系统] 提示 [/系统] [用户] 问题 [/用户] [助理] 答案 [/助理] 仅 助理回答的真实内容token

贯穿始终的统一训练目标(核心公式):
L(θtrain)=−E(X) D[Σi∈有效位置(X)logPθ(X[i]∣X[0:i])]L(θ_{train}) = - E_{(X) ~ D} [ Σ_{i ∈ 有效位置(X)} log P_θ( X[i] | X[0:i] ) ]L(θtrain)=E(X) D[Σi有效位置(X)logPθ(X[i]X[0:i])]
其中,概率 P_θ 由Transformer语言模型计算,而模型的参数子集 θ_train、序列内容 X 和有效位置集随着训练阶段演变,引导模型从“视觉翻译官”逐步成长为“全能的视觉对话专家”。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐