SFT CrossEntropyLoss 具体计算过程

SFT (Supervised Fine-Tuning) 阶段,模型学习的是“给定上文,预测下一个词”。这个过程本质上是在做一连串的分类任务

计算 CrossEntropyLoss(交叉熵损失)的过程可以拆解为以下几个步骤:


1. 从 Logits 到 概率 (Softmax)

模型的最后一个层(Head)输出的是 Logits。假设词表大小为 $V$(比如 100k),那么对于每一个生成的位点,模型都会输出 $V$ 个实数。

首先,通过 Softmax 函数将这些实数转化为概率分布 $P$:

$$P_i = \frac{\exp(\text{logit}_i)}{\sum_{j=1}^{V} \exp(\text{logit}_j)}$$

此时,每一个词都有一个 $0$ 到 $1$ 之间的概率。


2. 独热编码 (One-Hot) 与 筛选

在 SFT 中,我们有“标准答案”(Ground Truth)。 假设在某个位置,正确的词是“苹果”,它在词表中的索引是 42

  • 标准答案是一个 One-Hot 向量:只有索引 42 的地方是 $1$,其余全是 $0$。
  • CrossEntropy 的核心:它只关心模型给“正确答案”打了多少分。它会把对应索引 42 的那个概率 $P_{42}$ 抠出来。

3. 计算负对数似然 (NLL)

这就是你提到的 $-\log(P)$。

$$\text{Loss} = -\log(P_{\text{target}})$$
  • 如果模型预测正确词的概率是 $0.9$,那么 $\text{Loss} = -\log(0.9) \approx 0.105$(损失很小)。
  • 如果模型预测正确词的概率是 $0.1$,那么 $\text{Loss} = -\log(0.1) \approx 2.302$(损失很大)。

4. SFT 中的特殊处理:Masking(掩码)

这是 SFT 和预训练最重要的一点区别。在 SFT 的数据中,通常包含 Prompt(问题)Answer(回答)

  • 我们不希望模型去学习如何预测 Prompt
  • 做法:在计算 Loss 时,我们会把 Prompt 部分对应的标签设为 -100(这是 PyTorch 中的约定值)。
  • 计算时CrossEntropyLoss 会自动忽略标签为 -100 的位置,只计算 Answer 部分的损失

5. 整体流程总结

假设输入序列长度为 $L$:

  1. Forward:模型跑一遍,得到 $L \times V$ 的 Logits。
  2. Shift(对齐):因为是预测下一个词,所以要把 Logits 往右移一位,对应 Label。
    • Logits[0] 对应预测 Label[1]
  3. Mask:把 Prompt 部分的 Label 抹掉。
  4. Reduction:把 Answer 部分所有有效位置的 $-\log(P)$ 加起来求平均。
1
2
3
4
5
6
7
8
# 伪代码示例
logits = model(input_ids) # (Batch, SeqLen, VocabSize)
# 预测第 i 个词是为了匹配第 i+1 个 label
shift_logits = logits[:, :-1, :].contiguous()
shift_labels = labels[:, 1:].contiguous()

loss_fct = nn.CrossEntropyLoss(ignore_index=-100)
loss = loss_fct(shift_logits.view(-1, V), shift_labels.view(-1))

为什么 SFT 这么高效?

因为 CrossEntropy 这种“指哪打哪”的特性:模型不需要考虑整个句子的空间分布,只需要在每一个时刻把“正确答案”那个维度的能量提上去,把其他的压下来。相比 Flow Matching 需要在连续空间里找路径,SFT 的这个 $-\log(P)$ 简直就是“降维打击”般的简单。

你是在写 SFT 的 Trainer 逻辑,还是在排查 Loss 不下降的问题?

为啥不是模型generate和 sft 中的answer作为label 去做CrossEntropyLoss? 而是预测下一个词,所以要把 Logits 往右移一位,作为 Label ?

这是一个非常经典的学习误区,理解了这一点,你就理解了 自回归模型(Autoregressive Models) 的精髓。

之所以不能直接用“整个 Answer”作为 Label 去做 CrossEntropy,是因为 CrossEntropy 是一个“点对点”的分类损失,它需要明确地知道:在这一时刻,输入是什么,对应的正确输出是什么。

以下是三个核心原因:


1. 任务本质:模型是“单字复读机”

LLM(如 GPT)的本质不是“一键生成整段话”,而是根据已知序列预测下一个字

  • 如果你的 Answer 是 [A, B, C, D]
  • 模型在生成 B 的时候,它的输入必须是 [Prompt, A]
  • 模型在生成 C 的时候,它的输入必须是 [Prompt, A, B]

如果我们不“往右移一位”,而是直接把 Answer 当 Label,模型就会变成:输入 A,预测 A。这叫恒等映射,模型只要学会“抄写”当前输入的最后一个字就能让 Loss 降为 0,但这根本没学会任何逻辑。


2. “偏移”是为了建立因果关系

在代码层面,shift 操作是为了让当前的输入未来的目标对齐。

假设我们要模型学一句话:“北京是首都”。

  • 第 1 步: 输入 [北京] -> 期望输出 [是]
  • 第 2 步: 输入 [北京, 是] -> 期望输出 [首都]

在矩阵计算时,模型是一次性算出所有位置的输出(Logits)的:

  • Logits[0] 对应的是输入第一个词后的预测结果。
  • Labels[1] 才是我们要预测的那个词。 所以,必须把 Logits 的第 $i$ 位和 Labels 的第 $i+1$ 位对齐,这个 Loss 计算才有意义。

3. 为什么不能用“生成的句子”去算 Loss?

你提到的“模型 generate 出来的东西和 Answer 做 CrossEntropy”,这其实是 RLHF(强化学习) 或者某些 Sequence-level Loss 的思路,但在 SFT 阶段行不通:

  1. 梯度无法回传generate 过程通常包含 argmax 或随机采样,这些操作在数学上是不可导的。这意味着如果你直接用生成的句子比对,误差(Loss)没法通过反向传播告诉模型参数该怎么改。
  2. Teacher Forcing(教师强迫):SFT 采用的是 Teacher Forcing 机制。无论模型上一步预测得多么离谱,我们在计算下一步 Loss 时,依然强制给模型输入正确的上文。这样可以防止模型在训练初期因为一步错步步错,导致根本无法收敛。

4. 总结

做法结果
不偏移 (Input=A, Label=A)模型学会了简单的复制
生成后比对 (RL 方式)计算极其缓慢,且梯度断裂,难以训练。
偏移一位 (Input=A, Label=B)模型学会了预测未来,建立了因果逻辑。

一句话总结: shift 是为了强迫模型利用“过去”的信息去猜“未来”,而不是让它盯着“现在”看。

你是不是在看 transformers 库里的 LlamaForCausalLM 源码?那里的 shift_logits 逻辑确实困扰过很多人。

如果在 SFT 中,模型预测错了某一个词,会对后续位置的 Loss 产生什么影响吗?

Licensed under CC BY-NC-SA 4.0
Last updated on April 17, 2026 at 2:05 PM
Built with Hugo
Theme Stack designed by Jimmy