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$:
- Forward:模型跑一遍,得到 $L \times V$ 的 Logits。
- Shift(对齐):因为是预测下一个词,所以要把 Logits 往右移一位,对应 Label。
Logits[0]对应预测Label[1]。
- Mask:把 Prompt 部分的 Label 抹掉。
- Reduction:把 Answer 部分所有有效位置的 $-\log(P)$ 加起来求平均。
| |
为什么 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 阶段行不通:
- 梯度无法回传:
generate过程通常包含argmax或随机采样,这些操作在数学上是不可导的。这意味着如果你直接用生成的句子比对,误差(Loss)没法通过反向传播告诉模型参数该怎么改。 - Teacher Forcing(教师强迫):SFT 采用的是 Teacher Forcing 机制。无论模型上一步预测得多么离谱,我们在计算下一步 Loss 时,依然强制给模型输入正确的上文。这样可以防止模型在训练初期因为一步错步步错,导致根本无法收敛。
4. 总结
| 做法 | 结果 |
|---|---|
| 不偏移 (Input=A, Label=A) | 模型学会了简单的复制。 |
| 生成后比对 (RL 方式) | 计算极其缓慢,且梯度断裂,难以训练。 |
| 偏移一位 (Input=A, Label=B) | 模型学会了预测未来,建立了因果逻辑。 |
一句话总结: shift 是为了强迫模型利用“过去”的信息去猜“未来”,而不是让它盯着“现在”看。
你是不是在看 transformers 库里的 LlamaForCausalLM 源码?那里的 shift_logits 逻辑确实困扰过很多人。
如果在 SFT 中,模型预测错了某一个词,会对后续位置的 Loss 产生什么影响吗?