【大模型技术报告】Qwen2-VL大模型训练过程理解
阶段可训练参数 (θ_train)冻结参数数据序列 (X) 构成损失计算的有效位置预训练LLM[IMG] 图像特征 [/IMG] 文本描述文本描述的token多任务预训练全模型 (ViT, Adapter, LLM)无[IMG] 图像特征 [/IMG] [指令] 问题 [/指令] [答案] 复杂输出 [/答案]答案中的所有token(文本、坐标等)监督微调LLM, Adapter (或 LoRA)
对训练的三个核心阶段进行从理论到代码的逐层、公式化拆解。
第一阶段:大规模视觉-语言预训练
目标:建立图像特征到LLM语义空间的“基础语言对齐”。
步骤1:数据准备与模型初始化
- 论文(3.1节):使用14亿对弱标注的 (图像I, 文本描述T)。LLM(Qwen-7B)冻结,视觉编码器(ViT-bigG)使用OpenCLIP预训练权重初始化,VL适配器随机初始化。
- 理论实现:此阶段通常有独立的预训练脚本,不在
finetune.py中。但其核心思想——冻结部分模块——在第三阶段的代码中反向体现。
步骤2:前向传播与序列构建
- 视觉编码:图像 I (224∗224)(224*224)(224∗224)经ViT处理,输出特征序列 V∈RN×DvV ∈ R^{N×D_v}V∈RN×Dv(N≈577)。
- 适配器压缩:V′=Adapter(V)V' = Adapter(V)V′=Adapter(V),其中 V′∈R256×DllmV'∈ R^{256×D_{llm}}V′∈R256×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=offsetL−1logP(tokeni+1∣X[0:i+1])
其中 offset 是视觉标记序列结束后的位置(即 </img> 之后)。视觉标记本身不参与损失计算。 -
反向传播与更新:
- 计算损失 LpreL_{pre}Lpre 对模型参数的梯度 ∇L∇L∇L。
- 由于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(计算损失的目标)。- 添加系统提示:
system_tokens = [im_start] + tokenize(‘system\n‘) + tokenize(system_message) + [im_end, \n] - 遍历对话轮次:对于每一句
sentence:- 编码角色和内容:
role_tokens = tokenize(‘<|im_start|>‘ + role + ‘\n‘) _input_id = role_tokens + tokenize(sentence[“value”]) + [im_end, \n]
- 编码角色和内容:
- 关键:生成
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中会被忽略。
- 对于 用户回合:
- 拼接与填充:将所有轮次的
_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).logits。logits形状为[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 和有效位置集随着训练阶段演变,引导模型从“视觉翻译官”逐步成长为“全能的视觉对话专家”。
更多推荐


所有评论(0)