训练 · 预训练之后

SFT

指令微调:只在回答上计算损失

之前
Qwen3-1.7B-Base
之后
Qwen3-1.7B Qwen 的后训练
样本
43 个词元 20 个参与训练

全部步骤

  1. 模型看到的是什么

    模型从来看不到聊天气泡。聊天模板把对话变成一串词元,每个回合前后加上特殊词元:<|im_start|>、角色、正文、<|im_end|>。SFT 在大量这样的序列上训练;这一条共 43 个词元,其中 20 个属于回答。

    43 个词元 · 回答占 20 个

  2. 只给回答打分

    训练时读入整条序列,在每个位置都让模型预测下一个词元,和预训练完全一样。只有回答部分的猜测被打分:它们的 −log p 加总进损失,提示词和模板被掩码。微调之前,Qwen3-1.7B-Base 每个回答词元要付出 2.47 nats。

    loss = 20 个回答词元上 −log p 的平均

  3. 它学会了做什么

    在成千上万个这样的例子上训练之后会怎样:同一个提示词,训练前与训练后。基座模型把对话当成要续写的记录,自己写出说话人标签;微调后的模型直接回答。(Qwen 真实的后训练远不止 SFT。)

    基座模型:续写文本 · 微调模型:回答它

  4. 对自己的措辞很笃定

    在回答上训练,也让模型对自己的措辞非常笃定。在为本页写的回答上,微调后的模型给大多数词元的概率接近 1,但本页的措辞一偏离它自己的说法,概率就跌到几乎为 0。每个词元,它在自己的回答上付出 0.12 nats,在这个回答上付出 4.12。

    后训练让分布变尖锐

  5. 它从没学过的词元

    被掩码的词元得不到梯度,所以没有什么能让模型在那里的猜测保持稳定。后训练之后,这些词元大多变得更可能了,但有几个断崖式下跌:Qwen3 现在给 <|im_start|> 后面的 “assistant” 的概率低于 10⁻¹⁵。它永远不必预测它:模板由程序写出。

    没有损失,就没有训练信号

代码

ids = tokenizer.apply_chat_template([{'role': 'user', 'content': q}, {'role': 'assistant', 'content': a}])
labels = ids.clone()
labels[:n_prompt] = -100                     # prompt and template: masked
logits = model(ids[:-1]).logits
loss = F.cross_entropy(logits, labels[1:], ignore_index=-100)   # the answer only
loss.backward(); opt.step()

延伸阅读