返回关卡地图
预判 0 / 3 闯关题:0 / 3 XP 0 / 300

Level 09 | 零基础导学关卡

监督微调训练循环

SFT Training Loop

先通过图解和小例子理解原理,再做预测与练习;不懂的地方可以反复尝试,最后把理解写进官方 Notebook。

本课官方 Notebook ↗
09_SFT_Training_Loop.ipynb 训练微调SFT训练循环
Mission 1 概念练习

先构造 labels:只让 response 与有效位置参与学习

🎯 先猜一猜

prompt 长 3、response 长 4、max_len=8。

labels 的最后一个位置应该填什么?

先猜一个答案,再对照下面的讲解。可以随时修改选择,猜错会得到解释。

先补的知识

  • input_ids 是 prompt_ids + response_ids;它包含模型需要看到的完整上下文。
  • labels 与 input_ids 长度相同,但 prompt 位置写 -100,response 位置保留真实 token id。
  • nn.CrossEntropyLoss(ignore_index=-100) 会跳过标签为 -100 的位置,因此 prompt 与 padding 都不会贡献 loss。
  • 截断时 input_ids 与 labels 必须切同一范围;填充时 input_ids 填 pad_id,而 labels 填 -100。最终两者固定为 max_len 个 long 元素。

图解原理

输入像一张完整试卷:题目和标准答案都要给模型看;labels 像评分模板:题目区域打上“不计分”,只给答案区域评分。Padding 只是把纸张补到统一长度,也必须标成不计分。

同一序列,两种职责

input_ids[prompt tokens, response tokens, pad tokens] labels[-100..., response tokens, -100...] 模型可见prompt、response、padding 都在输入中 参与评分只有 labels 中非 -100 的 response token

Notebook 样例

prompt[10,20,30] response[40,50,60,70] max_len8,需要补 1 个位置 labels[-100,-100,-100,40,50,60,70,-100]
1拼接 input_ids保持 prompt 在前、response 在后
2构造同长度 labelsprompt 用 -100,response 保留 id
3若超长:两者一起 [:max_len]不能只截输入或只截标签
4若不足:计算 pad_len输入补 pad_id,标签补 -100
5转 torch.longtoken id 与类别标签都要求整数类型
Notebook 注释写“从末尾截断”,参考实现实际采用 [:max_len]:保留序列前 max_len 个位置、丢弃末尾超出的部分。页面与测试按这一当前实现合同对齐。

语法热身:给问答卡制作固定长度评分模板

question = [7, 8]
answer = [21, 22, 23]
limit = 7

tokens = question + answer
score_labels = [-100] * len(question) + answer

if len(tokens) > limit:
    tokens = tokens[:limit]
    score_labels = score_labels[:limit]
else:
    missing = limit - len(tokens)
    tokens = tokens + [0] * missing
    score_labels = score_labels + [-100] * missing

独立例子如何迁移回 Notebook

  • question / answer 对应 prompt_ids / response_ids
  • tokens / score_labels 对应 input_ids / labels
  • limit 对应 max_len,输入填充值 0 要改为函数参数 pad_id
  • 返回前把两个列表分别转成 torch.tensor(..., dtype=torch.long);Notebook 已给好这部分。

巩固一下

如果序列长度超过 max_len,为什么 input_ids 和 labels 必须使用同一切片?

学完这一段,试着做

用一个小动作确认自己理解了;最后再进入官方题目。

Mission 2 概念练习

再做 shift:第 t 个预测对齐第 t+1 个标签

🎯 先猜一猜

labels 原位置 3 的值是 response token 40。

哪一个 logits 原位置应该负责预测这个 40?

先猜一个答案,再对照下面的讲解。可以随时修改选择,猜错会得到解释。

先补的知识

  • logits.shape=[batch_size, seq_len, vocab_size];每个位置保存对词表中所有 token 的预测分数。
  • labels.shape=[batch_size, seq_len];每个位置只有一个目标 token id 或 ignore_index=-100。
  • 自回归模型在位置 t 的输出用于预测下一个 token t+1,因此不能让 logits[t] 与 labels[t] 对齐。
  • 切片后的 Tensor 可能不是连续内存;Notebook 使用 contiguous() 后再 view 展平。

图解原理

把 logits 当成每个站点发出的“下一站预测”。0 号站的预测应与 1 号站真实标签比较,1 号站与 2 号站比较。最后一个站没有下一个标签,所以丢掉最后一个 logits;第一个标签没有前一站预测,所以丢掉第一个 label。

logits 位置 0预测 token 1 logits 位置 1预测 token 2 ...继续一位错开 logits 位置 S-2预测 token S-1

两边各切掉一端

原 logits[B,S,V] 去掉最后位置[B,S-1,V] 原 labels[B,S] 去掉第一位置[B,S-1]

Notebook 的准确预测放在哪里

response 第一个 token 是 40,位于 labels[3]。测试把高分放在 logits[2,40],正是因为 shift 后 logits 原位置 2 会与 labels 原位置 3 比较。

logits[2,40]=50 对齐 labels[3]=40 该位置低 loss
不要把 logits 和 labels 向同一方向切。若两边都去掉第一项或都去掉最后一项,shape 仍可能匹配,但语义会变成预测当前位置而不是下一个位置。

语法热身:把“下一天预测”与真实天气错开一位

predictions = torch.tensor([10, 20, 30, 40])
actual = torch.tensor([9, 11, 19, 31])

# 第 0 天的输出用于预测第 1 天
next_predictions = predictions[:-1].contiguous()
next_actual = actual[1:].contiguous()
print(next_predictions) # [10,20,30]
print(next_actual)       # [11,19,31]

独立例子如何迁移回 Notebook

  • predictions[:-1] 对应保留 batch、去掉 logits 的最后一个序列位置,并保留完整词表维。
  • actual[1:] 对应保留 batch、去掉 labels 的第一个序列位置。
  • Notebook 用省略号索引处理任意前置 batch 维;最后别漏掉 .contiguous()
  • 完成后两个结果的有效位置数都必须是 seq_len-1

巩固一下

logits.shape=[1,8,100]、labels.shape=[1,8],shift 后 shape 分别是什么?

学完这一段,试着做

用一个小动作确认自己理解了;最后再进入官方题目。

Mission 3 概念练习

最后展平:把所有有效位置交给交叉熵

🎯 先猜一猜

shift_logits.shape=[1,7,100]。

交给 CrossEntropyLoss 前,正确的 view shape 是什么?

先猜一个答案,再对照下面的讲解。可以随时修改选择,猜错会得到解释。

先补的知识

  • nn.CrossEntropyLoss 的常用输入合同是 logits [N,C]、targets [N];C 是类别数,在语言模型里就是 vocab_size。
  • shift_logits 当前是 [B,S-1,V],要把 B 与 S-1 合并为样本数 N;shift_labels 同样合并为 N。
  • logits 最后一维 V 必须保留,不能把整个 Tensor flatten 成一列。
  • ignore_index=-100 让 prompt 与 padding 对齐到的目标位置不计入平均 loss。

图解原理

交叉熵把“每个有效位置”当成一道 V 选 1 的分类题。B 个序列、每个 S-1 道题可以排成 N 道题,但每道题的 V 个选项必须保持在同一行。labels 则每题只给一个正确选项编号。

展平前后

shift_logits[B,S-1,V] -> [B×(S-1),V] shift_labels[B,S-1] -> [B×(S-1)] 类别维始终保留 V,由 size(-1) 读取 忽略位置target=-100,不参与 loss

为什么测试 loss 很低

测试只在 40、50、60、70 四个 response 目标上放置极高正确 logit;prompt 与 padding 标签为 -100,被 loss 跳过,因此整体 loss 小于 0.01。

prompt
忽略
4 个 response
预测正确
padding
忽略
loss 很低
loss 是 0 维 Tensor 词表维没有被压扁 targets dtype 为 long ignore_index 明确为 -100

语法热身:把两组选择题排成一个题目列表

# 2 位学生,每人 3 题,每题 4 个选项
choice_logits = torch.randn(2, 3, 4)
answers = torch.tensor([
    [1, 0, -100],
    [2, 3, 1],
])

criterion = nn.CrossEntropyLoss(ignore_index=-100)
flat_logits = choice_logits.view(-1, choice_logits.size(-1))
flat_answers = answers.view(-1)
loss = criterion(flat_logits, flat_answers)

独立例子如何迁移回 Notebook

  • choice_logits 对应 shift_logits,最后一维 4 对应词表大小 V。
  • answers 对应 shift_labels,-100 的题目自动跳过。
  • flat_logitsflat_answers 是 TODO 4 展平后的合同。
  • 使用已有变量的 size(-1) 读取 V,不要硬编码测试里的 vocab_size=100。

巩固一下

为什么 labels 中的 padding 要填 -100 而不是 pad_id=0?

学完这一段,试着做

用一个小动作确认自己理解了;最后再进入官方题目。

把理解变成自己的代码

准备好,去官方题目试一试

在本页用小例子建立直觉,再去官方 Notebook 完成实现。先运行你自己的测试,遇到困难时再查看官方提示与参考答案。

前往本课官方 Notebook ↗

这节课会遇到的代码对象

build_sft_data · compute_sft_loss

先找题目里的输入、输出与 TODO,再把本课的手算过程对应进去;以官方题目中的函数说明和测试为准。

练习来源:Datawhale 官方仓库 · 518cc45。这里的入口直接打开官方版本,不读取或分享你的本地 Notebook。

完成 3 道闯关题后,本关即算完成;作业 checklist 用来辅助你回 notebook 练习。
进度只保存在当前浏览器 localStorage,分享 HTML 不会带走你的记录。
上一关:L08 下一关:L10