监督微调(Supervised Fine-Tuning, SFT)是将预训练的视觉语言模型适配到特定任务的关键步骤。与纯语言模型不同,VLM 的 SFT 需要同时考虑视觉和语言两种模态的对齐,这带来了独特的挑战:如何设计有效的指令格式?如何平衡不同任务的损失?如何在有限的计算资源下高效微调?本章将系统介绍 VLM SFT 的核心技术,从指令设计到训练优化,帮助您掌握将通用 VLM 转化为任务专家的完整流程。
指令微调的核心在于教会模型理解和遵循人类指令。对于 VLM,这意味着模型不仅要理解文本指令,还要将其与视觉输入关联起来。设计良好的指令格式是成功微调的第一步。
VLM 的指令模板需要明确标识图像位置、用户指令和模型响应的边界。常见的模板格式包括:
基础单轮对话模板:
<image>
User: {instruction}
Assistant: {response}
带系统提示的模板:
System: {system_prompt}
<image>
User: {instruction}
Assistant: {response}
多图像交织模板:
User: 比较这两张图片 <image1> 和 <image2>,{instruction}
Assistant: {response}
关键设计原则:
<image>、<|im_start|>)标记图像嵌入位置<|im_end|>)Token 化示例:
输入文本: "<image>\nUser: 描述这张图片\nAssistant: "
Token IDs: [32000, 13, 2659, 29901, 29871, 31904, 30810, 30775, 30998, 13, 7900, 22137, 29901, 29871]
↑图像占位 ↑换行 ↑User: ↑描述这张图片 ↑换行 ↑Assistant:
系统提示词(System Prompt)定义模型的角色和行为准则,对 VLM 的表现有显著影响:
通用视觉助手提示:
你是一个专业的视觉语言助手。请准确描述图像内容,回答用户关于图像的问题。
如果图像中包含文字,请准确识别并转录。避免猜测或编造不存在的内容。
任务特定提示(OCR场景):
你是一个OCR专家。请:
1. 识别图像中的所有文字
2. 保持原始格式和布局
3. 标注不确定的字符为[?]
4. 忽略装饰性元素,专注文字内容
系统提示的优化技巧:
VLM 的多轮对话需要处理历史上下文和新图像输入的关系:
策略1:图像持久化
# 第一轮
messages = [
{"role": "user", "content": "<image> 这是什么动物?"},
{"role": "assistant", "content": "这是一只橙色的猫。"}
]
# 第二轮(引用同一图像)
messages.append({"role": "user", "content": "它在做什么?"})
# 模型需要记住之前的图像上下文
策略2:显式图像引用
# 使用图像ID系统
messages = [
{"role": "user", "content": "<image id='img1'> 描述第一张图"},
{"role": "assistant", "content": "第一张图显示..."},
{"role": "user", "content": "<image id='img2'> 比较img1和img2的差异"},
]
上下文窗口管理:
最大上下文 = 4096 tokens
├── 系统提示: ~100 tokens
├── 图像嵌入: 576 tokens × N张图
├── 历史对话: 可变长度
└── 当前回复: 预留 500-1000 tokens
确保视觉理解与语言生成的一致性是 VLM SFT 的核心挑战:
对齐层次:
对齐技术:
视觉特征对齐矩阵:
物体 属性 关系 场景
________________
视觉 | 1.0 0.8 0.6 0.7 | <- 视觉编码器输出
语言 | 0.9 0.9 0.7 0.8 | <- 语言模型理解
‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
对角线值越接近1.0,对齐越好
细粒度对齐示例:
# Grounding 标注格式
instruction = "找出<click>红色的球</click>在哪里"
response = "红色的球位于<box>[[125, 235, 200, 310]]</box>图像的左下角。"
# Referring 标注格式
instruction = "描述位于<region>[[x1,y1,x2,y2]]</region>的物体"
response = "这是一个红色的篮球,表面有黑色的线条纹理。"
损失函数设计直接影响模型的学习目标和收敛行为。VLM 的 SFT 需要精心设计损失函数来平衡不同类型的预测任务。
VLM 的核心损失是自回归语言建模损失,即预测下一个 token 的交叉熵损失:
\[\mathcal{L}_{LM} = -\sum_{t=1}^{T} \log P(x_t | x_{<t}, I)\]其中 $x_t$ 是第 $t$ 个 token,$I$ 是输入图像,$x_{<t}$ 是之前的所有 token。
实现细节:
def compute_lm_loss(logits, labels, vocab_size=32000):
"""
logits: [batch_size, seq_len, vocab_size]
labels: [batch_size, seq_len]
"""
# Shift:预测位置和标签错位
shift_logits = logits[..., :-1, :].contiguous()
shift_labels = labels[..., 1:].contiguous()
# Flatten 便于计算
shift_logits = shift_logits.view(-1, vocab_size)
shift_labels = shift_labels.view(-1)
# 交叉熵损失
loss = F.cross_entropy(
shift_logits,
shift_labels,
ignore_index=-100, # 忽略 padding
reduction='mean'
)
return loss
注意力掩码的影响:
序列: [IMG] User: 描述图片 Assistant: 这是一只猫 [EOS]
掩码: 0 0 0 1 1 1
只在 Assistant 响应部分计算损失
不同部分的 token 对学习的重要性不同,通过掩码和权重调整可以优化训练效果:
1. 响应掩码(Response Masking):
def create_response_mask(input_ids, response_start_token_id):
"""只在模型响应部分计算损失"""
batch_size, seq_len = input_ids.shape
mask = torch.zeros_like(input_ids, dtype=torch.bool)
for i in range(batch_size):
# 找到响应开始位置
response_start = (input_ids[i] == response_start_token_id).nonzero()
if len(response_start) > 0:
start_idx = response_start[0].item()
mask[i, start_idx:] = True
return mask
2. Token 级别权重:
# 不同类型 token 的权重
token_weights = {
"special_tokens": 0.0, # <image>, <pad> 等
"instruction": 0.0, # 用户指令部分
"response": 1.0, # 助手响应
"grounding_box": 2.0, # 坐标预测
"key_entities": 1.5 # 关键实体名词
}
3. 动态权重调整:
早期训练(epoch 1-3):
- 所有 token 权重 = 1.0
- 让模型学习基础的语言模式
中期训练(epoch 4-8):
- 指令部分权重 = 0.5
- 响应部分权重 = 1.0
- 强化指令遵循能力
后期训练(epoch 9-10):
- 只计算响应损失
- 精细调整生成质量
VLM 通常需要同时处理多个任务,如图像描述、VQA、OCR 等。多任务损失平衡是关键:
损失组合策略: \(\mathcal{L}_{total} = \sum_{i=1}^{N} w_i \mathcal{L}_i\)
自适应权重方法:
其中 $\sigma_i$ 是可学习的任务不确定性参数。
def gradnorm_weights(losses, shared_params, alpha=1.5):
"""根据梯度大小动态调整任务权重"""
# 计算每个任务的梯度范数
grad_norms = []
for loss in losses:
grads = torch.autograd.grad(loss, shared_params, retain_graph=True)
grad_norm = torch.norm(torch.cat([g.flatten() for g in grads]))
grad_norms.append(grad_norm)
# 计算平均梯度范数
mean_norm = torch.stack(grad_norms).mean()
# 调整权重
weights = []
for i, norm in enumerate(grad_norms):
relative_norm = norm / mean_norm
weight = relative_norm ** alpha
weights.append(weight)
return F.softmax(torch.stack(weights), dim=0)
任务采样策略:
批次构建策略:
├── 均匀采样: 每个 batch 包含所有任务
├── 任务分组: 相似任务放在同一 batch
└── 温度采样: P(task_i) ∝ (1/loss_i)^T
温度 T 控制采样分布:
- T → 0: 只采样损失最大的任务
- T = 1: 根据损失反比采样
- T → ∞: 均匀采样所有任务
对于需要定位的任务(如目标检测、referring segmentation),需要专门的损失设计:
1. 边界框回归损失:
def box_loss(pred_boxes, gt_boxes):
"""
pred_boxes: [batch, num_queries, 4] # (x1, y1, x2, y2) 归一化坐标
gt_boxes: [batch, num_targets, 4]
"""
# L1 损失
l1_loss = F.l1_loss(pred_boxes, gt_boxes)
# GIoU 损失
giou_loss = 1 - compute_giou(pred_boxes, gt_boxes)
return l1_loss + giou_loss
2. 坐标 Token 化策略:
方法1:连续坐标离散化
[0, 1] → [0, 999] → token_id ∈ [32000, 32999]
方法2:区域编码
图像分成 32×32 网格 → 每个网格一个 token
方法3:特殊数字 token
<x>0.123</x> <y>0.456</y> → 解析时提取
3. Referring 损失设计:
def referring_loss(pred_mask, gt_mask, pred_box, gt_box):
"""组合分割和检测损失"""
# 像素级分割损失
seg_loss = F.binary_cross_entropy_with_logits(pred_mask, gt_mask)
# 边界框损失
box_loss = compute_box_loss(pred_box, gt_box)
# 一致性损失:确保 mask 和 box 对应
mask_from_box = box_to_mask(pred_box)
consistency_loss = F.mse_loss(pred_mask, mask_from_box)
return seg_loss + 0.5 * box_loss + 0.1 * consistency_loss
4. 负样本处理:
Grounding 任务的负样本策略:
├── Hard Negative: 选择最容易混淆的物体
├── Random Negative: 随机选择其他物体
└── Background: 选择背景区域
负样本比例建议:
- 正负比 1:3 for 目标检测
- 正负比 1:1 for referring expression
- 动态调整based on 难度
参数高效微调(PEFT)方法允许在有限的计算资源下微调大规模 VLM。这些方法通过只更新少量参数来实现与全量微调相近的效果。
LoRA(Low-Rank Adaptation)通过低秩分解来近似权重更新:
核心原理: \(W' = W + \Delta W = W + BA\)
其中 $B \in \mathbb{R}^{d \times r}$,$A \in \mathbb{R}^{r \times k}$,$r \ll \min(d, k)$。
VLM 中的 LoRA 配置:
class LoRAConfig:
# 语言模型部分
lm_target_modules = [
"q_proj", "v_proj", # 注意力层
"k_proj", "o_proj",
"gate_proj", "up_proj", "down_proj" # FFN 层
]
# 视觉编码器部分(可选)
vision_target_modules = [
"qkv", # ViT 的 QKV 投影
"proj", # 输出投影
"mlp.fc1", "mlp.fc2" # MLP 层
]
# 关键超参数
r = 16 # rank,常用 8/16/32/64
alpha = 16 # 缩放因子,通常 = r
dropout = 0.1
动态 Rank 选择:
不同模块的重要性分析:
模块类型 建议 rank 参数占比
-----------------------------------------
Q, K 投影 8-16 ~15%
V, O 投影 16-32 ~20%
FFN 上投影 32-64 ~35%
FFN 下投影 16-32 ~25%
Cross-Attn 32-64 ~5% (如果有)
实现细节:
class LoRALayer(nn.Module):
def __init__(self, in_features, out_features, rank=16, alpha=16):
super().__init__()
self.scaling = alpha / rank
# 低秩矩阵
self.lora_A = nn.Parameter(torch.randn(rank, in_features))
self.lora_B = nn.Parameter(torch.zeros(out_features, rank))
# 初始化
nn.init.kaiming_uniform_(self.lora_A, a=math.sqrt(5))
def forward(self, x, base_output):
# base_output 是原始层的输出
lora_output = (x @ self.lora_A.T @ self.lora_B.T) * self.scaling
return base_output + lora_output
QLoRA 结合 4-bit 量化和 LoRA,大幅降低显存占用:
量化流程:
原始模型 (16-bit) → NF4 量化 (4-bit) + LoRA 适配器 (16-bit)
显存节省: ~75% (相比全精度)
NF4(NormalFloat4)量化:
def quantize_nf4(tensor):
"""4-bit NormalFloat 量化"""
# 1. 归一化到 [-1, 1]
absmax = tensor.abs().max()
tensor_normalized = tensor / absmax
# 2. 量化到 16 个级别
quantization_levels = [
-1.0, -0.6961, -0.5250, -0.3949,
-0.2844, -0.1848, -0.0911, 0.0,
0.0796, 0.1609, 0.2461, 0.3379,
0.4407, 0.5626, 0.7230, 1.0
]
# 3. 找最近的量化级别
quantized = quantize_to_nearest(tensor_normalized, quantization_levels)
return quantized, absmax # 保存 scale 用于反量化
双重量化(Double Quantization):
第一次量化: 模型权重 → 4-bit
第二次量化: 量化常数 → 8-bit
额外节省: ~0.37 bit/参数
QLoRA 训练配置:
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True,
)
# Paged Optimizer 节省优化器内存
optimizer = PagedAdamW32bit(
model.parameters(),
lr=2e-4,
weight_decay=0.01,
optim_bits=32 # 优化器状态保持 32-bit
)
Adapter 通过插入小型网络模块来实现参数高效微调:
标准 Adapter 架构:
输入 → LayerNorm → Down-projection → 激活 → Up-projection → 残差连接
↓ ↑
└──────────────────────────────────────────────────────────────┘
VLM 中的 Adapter 变体:
class SequentialAdapter(nn.Module):
def __init__(self, dim, reduction_factor=16):
super().__init__()
hidden_dim = dim // reduction_factor
self.down_proj = nn.Linear(dim, hidden_dim)
self.activation = nn.GELU()
self.up_proj = nn.Linear(hidden_dim, dim)
def forward(self, x):
residual = x
x = self.down_proj(x)
x = self.activation(x)
x = self.up_proj(x)
return x + residual
class ParallelAdapter(nn.Module):
"""并行处理,减少延迟"""
def forward(self, x, original_output):
adapter_output = self.adapter(x)
return original_output + self.scale * adapter_output
class CrossModalAdapter(nn.Module):
"""专门处理视觉-语言交互"""
def __init__(self, vision_dim, text_dim, hidden_dim):
super().__init__()
self.vision_proj = nn.Linear(vision_dim, hidden_dim)
self.text_proj = nn.Linear(text_dim, hidden_dim)
self.fusion = nn.MultiheadAttention(hidden_dim, num_heads=8)
def forward(self, vision_features, text_features):
v = self.vision_proj(vision_features)
t = self.text_proj(text_features)
fused, _ = self.fusion(t, v, v) # text 作 query
return fused
性能对比表:
方法 参数量 显存占用 训练速度 效果(相对全量)
---------------------------------------------------------
全量微调 100% 100% 1.0x 100%
LoRA 0.1-1% ~60% 1.5x 95-98%
QLoRA 0.1-1% ~25% 1.2x 92-96%
Adapter 1-5% ~70% 1.3x 93-97%
Prefix <0.1% ~50% 1.8x 85-92%
IA3 <0.01% ~55% 1.6x 88-94%
选择决策树:
显存限制严格?
├─ 是 → QLoRA(4-bit量化 + LoRA)
└─ 否 → 需要最佳性能?
├─ 是 → 全量微调 or LoRA (r=64)
└─ 否 → 推理速度优先?
├─ 是 → LoRA (可合并权重)
└─ 否 → Adapter (灵活性高)
组合策略:
# 混合 PEFT:不同层使用不同方法
config = {
"vision_encoder": "frozen", # 冻结
"projection": "full", # 全量微调
"llm_layers_0_16": "lora", # 底层用 LoRA
"llm_layers_16_32": "adapter", # 高层用 Adapter
}
实践建议:
训练大规模 VLM 时经常遇到不稳定问题:损失突然爆炸、梯度消失、收敛缓慢等。本节介绍实用的稳定性技巧。
VLM 常用调度器:
def cosine_schedule_with_warmup(optimizer, num_warmup_steps, num_training_steps):
def lr_lambda(current_step):
if current_step < num_warmup_steps:
# 线性 warmup
return float(current_step) / float(max(1, num_warmup_steps))
# Cosine 衰减
progress = float(current_step - num_warmup_steps) / \
float(max(1, num_training_steps - num_warmup_steps))
return max(0.0, 0.5 * (1.0 + math.cos(math.pi * progress)))
return LambdaLR(optimizer, lr_lambda)
阶段1(预热): lr = 1e-6 → 2e-4 (线性增长)
阶段2(主训练): lr = 2e-4 (恒定或缓慢衰减)
阶段3(精调): lr = 2e-4 → 1e-5 (cosine衰减)
视觉编码器特殊处理:
# 不同组件不同学习率
param_groups = [
{"params": vision_encoder.parameters(), "lr": 1e-5}, # 更小
{"params": projection.parameters(), "lr": 5e-4}, # 更大
{"params": language_model.parameters(), "lr": 2e-4}, # 标准
]
梯度裁剪策略:
# 1. 全局梯度裁剪(推荐)
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
# 2. 分层梯度裁剪
for name, param in model.named_parameters():
if "vision" in name:
torch.nn.utils.clip_grad_norm_([param], max_norm=0.5)
else:
torch.nn.utils.clip_grad_norm_([param], max_norm=1.0)
梯度监控:
def monitor_gradients(model):
"""监控梯度统计信息"""
grad_stats = {}
for name, param in model.named_parameters():
if param.grad is not None:
grad_stats[name] = {
"mean": param.grad.mean().item(),
"std": param.grad.std().item(),
"max": param.grad.abs().max().item(),
}
return grad_stats
# 异常检测
if any(stat["max"] > 100 for stat in grad_stats.values()):
logger.warning("梯度爆炸风险!")
关键组件初始化:
def init_vlm_weights(model):
# 1. 投影层:Xavier 初始化
if hasattr(model, 'visual_projection'):
nn.init.xavier_uniform_(model.visual_projection.weight)
nn.init.zeros_(model.visual_projection.bias)
# 2. LoRA 层:接近零初始化
for name, param in model.named_parameters():
if "lora_B" in name:
nn.init.zeros_(param) # B 矩阵初始化为0
elif "lora_A" in name:
nn.init.kaiming_uniform_(param, a=math.sqrt(5))
# 3. Layer Scale:小值初始化
if hasattr(model, 'layer_scale'):
nn.init.constant_(model.layer_scale, 1e-4)
稳定性技巧:
初始化检查清单:
□ 投影层不能太大(std < 0.02)
□ LoRA B 矩阵初始为 0
□ Layer Norm 权重 = 1, 偏置 = 0
□ 新增 token embedding 用已有 token 平均值
智能 Checkpoint:
class SmartCheckpointer:
def __init__(self, patience=3, delta=0.001):
self.patience = patience
self.delta = delta
self.best_score = None
self.counter = 0
def should_save(self, val_loss):
if self.best_score is None:
self.best_score = val_loss
return True
if val_loss < self.best_score - self.delta:
self.best_score = val_loss
self.counter = 0
return True
else:
self.counter += 1
return False
def should_stop(self):
return self.counter >= self.patience
Checkpoint 管理:
checkpoint = {
'epoch': epoch,
'model_state_dict': model.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
'scheduler_state_dict': scheduler.state_dict(),
'best_val_loss': best_val_loss,
'training_history': {
'train_losses': train_losses,
'val_losses': val_losses,
'learning_rates': lrs,
}
}
# 保存策略
save_strategies = {
"best": "checkpoint_best.pt", # 最佳验证性能
"latest": "checkpoint_latest.pt", # 最新状态
"periodic": f"checkpoint_epoch_{epoch}.pt", # 定期保存
}
Qwen-VL 采用渐进式三阶段训练策略,从大规模预训练到精细指令微调,实现了优秀的多模态性能。
目标:建立基础的视觉-语言对齐能力
数据配置:
总量:1.4B 图文对
├── LAION-400M: 40%(网络爬取)
├── COYO-700M: 30%(韩语+英语)
├── CC12M: 15%(概念描述)
└── 内部数据: 15%(高质量筛选)
训练配置:
stage1_config = {
"vision_encoder": "frozen", # OpenCLIP ViT-G/14
"projection": "trainable", # 新增的 Resampler
"language_model": "trainable", # Qwen-7B
"batch_size": 2048,
"learning_rate": 1e-4,
"warmup_steps": 2000,
"total_steps": 50000,
}
目标:学习多样化的视觉任务能力
任务分布:
任务类型 数据量 损失权重
--------------------------------------
图像描述 50M 0.3
VQA 30M 0.2
OCR 20M 0.2
Grounding 15M 0.15
Referring 10M 0.15
关键技术:
# 动态分辨率处理
def dynamic_resolution(image, min_pixels=224*224, max_pixels=1024*1024):
"""保持宽高比的动态分辨率"""
h, w = image.shape[:2]
current_pixels = h * w
if current_pixels < min_pixels:
scale = math.sqrt(min_pixels / current_pixels)
elif current_pixels > max_pixels:
scale = math.sqrt(max_pixels / current_pixels)
else:
scale = 1.0
new_h, new_w = int(h * scale), int(w * scale)
# 确保是 14 的倍数(ViT patch size)
new_h = (new_h // 14) * 14
new_w = (new_w // 14) * 14
return resize(image, (new_h, new_w))
目标:优化指令遵循和对话能力
数据构成:
sft_data = {
"high_quality_vqa": 200k, # 人工标注
"complex_reasoning": 150k, # GPT-4V 生成
"multi_turn_dialog": 100k, # 多轮对话
"rejection_sampling": 50k, # 负样本
}
LoRA 微调配置:
lora_config = LoRAConfig(
r=64, # 较大的 rank
lora_alpha=16,
target_modules=[
"c_attn", # Qwen 的注意力模块
"c_proj",
"w1", "w2", # MLP
],
lora_dropout=0.05,
task_type="CAUSAL_LM",
)
# 只微调语言模型部分
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
total_params = sum(p.numel() for p in model.parameters())
print(f"可训练参数: {trainable_params/1e6:.2f}M ({100*trainable_params/total_params:.2f}%)")
# 输出: 可训练参数: 384.00M (4.92%)
训练曲线监控:
Loss
3.5 |
3.0 | Stage 1
2.5 | ╲___
2.0 | ╲__ Stage 2
1.5 | ╲____
1.0 | ╲___ Stage 3
0.5 | ╲________
|__|__|__|__|__|__|__|__|__|__|__
10k 20k 30k 40k 50k 60k Steps
解冻策略对比:
策略 优点 缺点 适用场景
-----------------------------------------------------------------
始终冻结 省显存、训练快 可能欠拟合 数据与预训练相似
从头解冻 充分适应新任务 易过拟合、慢 大规模新领域数据
阶段性解冻 平衡性能与效率 需要经验调参 通用场景(推荐)
阶段性解冻实践:
def staged_unfreeze(model, current_step, total_steps):
"""渐进解冻视觉编码器"""
progress = current_step / total_steps
if progress < 0.5:
# 前 50%: 全部冻结
freeze_vision_encoder(model)
elif progress < 0.8:
# 50-80%: 解冻最后 4 层
for i, layer in enumerate(model.vision_encoder.layers):
if i < len(model.vision_encoder.layers) - 4:
freeze_layer(layer)
else:
unfreeze_layer(layer)
else:
# 最后 20%: 全部解冻,但用更小学习率
unfreeze_vision_encoder(model)
# 视觉编码器学习率 = 0.1 * 基础学习率
基于重要性的 Rank 分配:
def compute_layer_importance(model, dataloader, num_samples=100):
"""计算各层的 Fisher 信息矩阵迹"""
importance_scores = {}
for batch in dataloader[:num_samples]:
outputs = model(batch)
loss = compute_loss(outputs, batch['labels'])
for name, param in model.named_parameters():
if param.requires_grad:
grad = torch.autograd.grad(loss, param, retain_graph=True)[0]
if name not in importance_scores:
importance_scores[name] = 0
importance_scores[name] += (grad ** 2).sum().item()
# 归一化
total = sum(importance_scores.values())
for name in importance_scores:
importance_scores[name] /= total
return importance_scores
# 根据重要性分配 rank
def adaptive_rank_allocation(importance_scores, total_rank_budget=512):
rank_allocation = {}
for name, score in importance_scores.items():
# rank ∈ [4, 64]
rank = min(64, max(4, int(score * total_rank_budget)))
# 确保是 4 的倍数(硬件友好)
rank = (rank // 4) * 4
rank_allocation[name] = rank
return rank_allocation
BF16 vs FP16 选择:
特性 FP16 BF16
-----------------------------------------
动态范围 ±65504 ±3.4e38
精度 高 中
硬件支持 广泛 A100+
溢出风险 高 极低
推荐场景 推理为主 训练为主
混合精度最佳实践:
# 自动混合精度配置
from torch.cuda.amp import autocast, GradScaler
scaler = GradScaler(
init_scale=2.**16, # 初始缩放因子
growth_factor=2.0, # 增长因子
backoff_factor=0.5, # 回退因子
growth_interval=2000, # 增长间隔
)
# 训练循环
for batch in dataloader:
optimizer.zero_grad()
with autocast(dtype=torch.bfloat16): # 或 torch.float16
outputs = model(batch)
loss = compute_loss(outputs, batch['labels'])
# 梯度缩放
scaler.scale(loss).backward()
# 梯度裁剪(在缩放空间)
scaler.unscale_(optimizer)
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
# 优化器步骤
scaler.step(optimizer)
scaler.update()
# 监控溢出
if scaler.get_scale() < 1.0:
logger.warning(f"梯度溢出,当前 scale: {scaler.get_scale()}")
本章系统介绍了 VLM 的监督微调策略,涵盖了从指令设计到训练优化的完整流程:
核心要点回顾:
关键公式汇总:
| 语言模型损失:$\mathcal{L}{LM} = -\sum{t=1}^{T} \log P(x_t | x_{<t}, I)$ |
题 1:指令模板设计 设计一个支持多图像输入和 CoT(Chain of Thought)推理的指令模板。要求能够处理图像间的比较任务。