趋近智
大师班
监督微调(SFT)通过在高质量的提示-响应示例数据集上训练,使预训练的大型语言模型能够遵循指令或以特定风格生成回复。与预训练不同,预训练侧重于在大量非结构化文本上进行下一个词元预测,而SFT是一种更有针对性的训练形式,旨在使模型的行为与预期结果保持一致。训练过程本身类似于序列到序列任务的标准监督学习,但涉及数据格式、损失计算和超参数选择方面的具体考量。
核心SFT训练循环会遍历批量的提示-响应对,执行前向传播,根据模型预测与目标响应之间的差异计算损失,并通过反向传播更新模型权重。
数据准备: 每个训练示例通常包含一个提示(例如,一个指令或用户查询)和一个期望的响应(例如,指令的答案或一个有用的回复)。这些通常被连接成一个序列,有时会用特殊词元分隔提示和响应部分,或指示对话回合的开始/结束。
# 格式化示例伪代码
prompt = "Instruction: Explain the process of photosynthesis."
response = "Photosynthesis is the process used by plants..."
# 如果模型/分词器需要,添加特殊词元
tokenizer.add_special_tokens({'pad_token': '[PAD]', 'eos_token': '[EOS]'})
# 简单连接示例
input_text = prompt + " " + response + tokenizer.eos_token
# 对组合文本进行分词
tokenized_input = tokenizer(
input_text,
return_tensors="pt",
padding="max_length",
truncation=True,
max_length=512
)
inputs = tokenized_input["input_ids"]
attention_mask = tokenized_input["attention_mask"]
前向传播: 分词后的序列被输入模型,以获取序列中每个词元位置的logits。
# 假设 'model' 是你预训练的Transformer模型
# 且 'inputs'/'attention_mask' 来自上一步
outputs = model(input_ids=inputs, attention_mask=attention_mask)
logits = outputs.logits
损失计算(屏蔽提示): 这是SFT的一个显著特点。目标是教导模型根据提示生成响应。因此,损失通常仅在响应词元上计算。对应于提示的词元被屏蔽,因此它们不参与损失计算或梯度更新。未屏蔽(响应)词元上使用标准交叉熵损失。
import torch
import torch.nn.functional as F
# logits: [批量大小, 序列长度, 词表大小]
# labels: [批量大小, 序列长度] (应为左移后的输入)
labels = inputs.clone()
# 通常,标签会进行位移,以便模型预测下一个词元
logits = logits[:, :-1, :] # 去掉最后一个logit
labels = labels[:, 1:] # 去掉第一个词元(例如,BOS)
# 确定批次中每个项目的提示长度
# 这需要知道提示在哪里结束以及响应在哪里开始
# 为简单起见,假设每个示例的prompt_length是已知的
# prompt_lengths: [批量大小] 张量,包含每个示例的提示词元长度
# 创建损失掩码:-100会被PyTorch的CrossEntropyLoss忽略
loss_mask = torch.ones_like(labels, dtype=torch.long)
for i in range(labels.shape[0]):
# 屏蔽提示词元(如果tokenizer.pad_token_id存在,也屏蔽填充词元)
prompt_end_index = prompt_lengths[i] - 1 # 根据长度定义方式进行调整
loss_mask[i, :prompt_end_index] = -100
if hasattr(tokenizer, 'pad_token_id') and tokenizer.pad_token_id is not None:
loss_mask[i][labels[i] == tokenizer.pad_token_id] = -100 # 屏蔽填充
# 将logits和labels展平以用于CrossEntropyLoss,并应用掩码
# 仅在loss_mask不为-100的词元上计算损失
active_loss = loss_mask.view(-1) != -100
active_logits = logits.view(-1, logits.size(-1))[active_loss]
active_labels = labels.view(-1)[active_loss]
loss = F.cross_entropy(active_logits, active_labels)
实际上,如果数据格式正确(例如,使用特定数据集格式或提供数据收集器),Hugging Face的transformers.Trainer或TRL (trl.SFTTrainer) 等库会在内部处理这种掩码逻辑。
反向传播与优化: 标准反向传播根据计算出的损失来计算梯度。优化器(通常是AdamW)更新模型权重。
# 使用标准PyTorch优化步骤
optimizer.zero_grad()
loss.backward()
# 可选:梯度裁剪
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
if scheduler is not None:
scheduler.step()
选择合适的超参数对SFT的成功很重要。由于SFT调整的是一个已很强大的预训练模型,其设置与预训练时使用的不同。
学习率: SFT通常使用比预训练小得多的学习率。1×10−5 到 5×10−5 范围内的值很常见。较小的学习率可以防止预训练期间获得的知识被灾难性遗忘,同时允许模型适应新的指令遵循目标。学习率调度器,如带有短热身阶段(例如,总步数的0-10%)的余弦衰减,通常会带来好处。
一个典型的SFT学习率调度,包括热身和余弦衰减。
批量大小: 批量大小通常受限于GPU内存。更大的模型需要更多内存,从而限制了每个批次的序列数量。梯度累积常用于在不增加每GPU内存使用量的情况下实现更大的有效批量大小。典型的有效批量大小可能在64到1024之间,具体取决于模型大小和可用硬件。
训练轮次(Epoch)数量: SFT通常只需要少数几个训练轮次(通常1-3个,有时最多5个)。训练时间过长可能导致在特定SFT数据集上过拟合,从而可能降低模型对未见指令的泛化能力或降低其通用知识。监控验证集上的表现很重要。
优化器: AdamW仍然是标准选择,类似于预训练。权重衰减参数可以保持不变或略作调整(例如,0.01到0.1)。Beta参数(β1,β2)通常保持其默认值(例如,0.9,0.999)。
序列长度: 最大序列长度应能容纳SFT数据集中典型提示和响应的组合长度。它可能与预训练期间使用的序列长度不同。将多个短示例打包到一个序列中或使用动态填充可以提高效率。
梯度裁剪: 应用梯度裁剪(例如,将梯度的L2范数裁剪到1.0)有助于稳定训练,尽管与大规模预训练相比,SFT中的不稳定性通常较少发生。
SFT过程微调模型的能力,将模型的预训练知识引导至指令数据集定义的特定交互模式和任务执行格式。对训练循环和超参数的细致管理可确保这种调整有效进行,同时不损害模型内在优势。
这部分内容有帮助吗?
© 2026 ApX Machine Learning用心打造