Loss Masking:为什么只批改“答案”,不批改“题目”
🎯 先猜一猜(下注解锁)
SFT 样本是 [Prompt] + [Response]。构造 labels 时,我们偏偏把 Prompt 那一段全设成 -100。
先判断:把 prompt 段的 label 设成 -100,最主要是为了?
先补的知识
- 回忆预训练 vs SFT:预训练时每个 token 都要预测、都算 loss(读一整本书);SFT 给的是 [Prompt(问题)] + [Response(答案)],我们只想让模型学会“答”,不想让它学“背题干”。
- 做法叫 Loss Masking:把 labels 里属于 prompt 和 padding 的位置全设成 -100。
- -100 是 PyTorch CrossEntropyLoss 的默认 ignore_index:凡是 label=-100 的位置,既不算损失、也不回传梯度,等于“这题不批改”。
- input_ids 和 labels 是两条等长的序列:input_ids 喂给模型看,labels 告诉损失函数“每个位置的正确答案是什么、要不要算分”。
图解原理
把一条 SFT 样本想成一张试卷:prompt 是印好的题干,response 是学生要写的作答。模型读整张卷子(input_ids 完整保留),但老师只批改作答区(labels 里只有 response 是真值,题干和空白都涂成 -100 表示不批改)。这一关就是构造这张“只在答案区留分”的 labels,再处理超长截断和不足填充。
input_ids = prompt_ids + response_ids,整条都保留给模型看。labels 长度一样,但只有 response 段是“真答案”,prompt 段和后面 padding 段都填 -100。
| 位置含义 | input_ids(模型看的) | labels(算分用的) |
|---|---|---|
| prompt 题干 | [10, 20, 30] | [-100, -100, -100] |
| response 答案 | [40, 50, 60, 70] | [40, 50, 60, 70] |
| padding 补位 | [0] | [-100] |
这正是 notebook 测试期望的:labels = [-100,-100,-100, 40,50,60,70, -100](max_len=8)。
如果 prompt 也算 loss,模型会花容量去学“人类爱怎么提问”,而不是“怎么答得好”。把它设成 -100,梯度就只从答案区回传,模型的全部注意力都用在“给定这个问题,下一个词该答什么”。
想一想:padding 位为什么也必须设成 -100,而不是随便填个 pad_id?
padding 只是为了把一个 batch 里长短不一的样本对齐成矩形,它不是真实内容。如果 labels 里 padding 位留着 pad_id,模型就会被要求去“预测 padding”,白白浪费容量、还污染 loss。
所以 input_ids 的 padding 填 pad_id(占位),但 labels 的 padding 必须填 -100(不批改)。两条序列在 padding 段的填法故意不同——这是最容易写错的地方。
拼完可能比 max_len 长(要从末尾截掉)或短(要在末尾补齐)。两条序列同步操作,只是填充值不同:input_ids 补 pad_id,labels 补 -100。
labels[:max_len]不足
input_ids+[pad_id]*n
labels+[-100]*n
面试里“你怎么构造 input_ids 和 labels、为什么 mask 掉 prompt”几乎必问,因为它直接暴露你是不是真做过训练。现实中多轮对话、system prompt、工具调用的 mask 规则更复杂(比如只算最后一轮助手回复),但内核就是这关:用 -100 精确控制“哪些 token 要学、哪些只读”。
一个常见线上事故就是 mask 写错——模型把用户的问法也学了进去,微调完开始“复读”用户,而不是回答。
语法热身:拼接 → 造 labels → 截断/填充
input_ids = prompt_ids + response_ids
labels = [-100] * len(prompt_ids) + response_ids # 题干涂掉,答案保留
if len(input_ids) > max_len: # 超长:末尾截断
input_ids, labels = input_ids[:max_len], labels[:max_len]
else: # 不足:末尾填充
pad = max_len - len(input_ids)
input_ids += [pad_id] * pad # 占位
labels += [-100] * pad # 不批改巩固一下
动手算:prompt=[10,20,30]、response=[40,50,60,70]、max_len=8、pad_id=0,labels 应该是?
回到 notebook 的作业
这里不直接写答案。你已经拿到足够输入,最后用 notebook 的 TODO 做举一反三。