以聊天格式做 SFT,测量损失掩码改变了什么
目标
把对话切成 MiniMind 的聊天格式(<|im_start|>역할\n…<|im_end|>\n,占位符为角色),并像 generate_labels 那样亲手做出只保留回答(assistant)的标签。以基准预训练权重为起点,用相同条件训练加了掩码的模型和去掉掩码的模型,用数字看在留出的对话上,回答令牌和提问令牌的损失是怎样分开的。
为什么重要
SFT 不是新算法,而是同样的下一个令牌预测。不同的只有一点:把什么当作正确答案。如果把整段对话当作正确答案,模型就要把训练信号分一部分给模仿提问,回答结束后甚至可能自己编出下一个问题。只把回答当作正确答案,提问就成了条件,而且必须把结束标记也放进正确答案,才能学会什么时候停下。 这条界线由标签数组里的几个 -100 决定,出错了也不会报错。所以亲手把标签打印出来,再用两个模型测一测掩码实际改变了什么,是最可靠的确认方法。
步骤
- 把
/opt/mm/data/sft.jsonl的第一段对话转成聊天格式字符串,保存到 /root/mm/sft/sample0.txt。 - 在 /root/mm/sft/sftlib.py 中实现
encode(대화, max_len=64, mask=True)(占位符为对话),并把前 20 段对话的input_ids、labels保存到 /root/mm/sft/labels.json。 - 把整个 SFT 数据中去掉填充的实际令牌数、剩下标签的令牌数及其比例,写入 /root/mm/sft/ratio.json。
- 从基准预训练权重(
/opt/mm/ref/pretrain.pth)出发,用加了掩码的标签做 300 步 SFT,保存为 /root/mm/sft/sft_masked.pth。 - 在相同条件下把标签设为整个输入(只有填充是 -100),保存为 /root/mm/sft/sft_nomask.pth。
- 在留出的对话(
sft_val.jsonl)上,把两个模型的回答令牌损失和提问一侧令牌的损失,写入 /root/mm/sft/compare.json。 - 用加了掩码的模型对留出的 20 个问题用贪心解码生成回答,保存到 /root/mm/sft/answers.jsonl。
- 在 /root/mm/sft/report.md 中写
## 채팅 형식、## 손실 마스킹、## 한계三节(三个标题为韩文,依次意为“聊天格式”“损失掩码”“局限”),并放入加了掩码的模型的提问损失和第 3 步的比例。
参考
- 聊天格式由
mmkit.chat_text(대화)(占位符为对话)生成。回答的起始标记是<|im_start|>assistant\n,结束标记是<|im_end|>\n经分词器切分得到的令牌片段。 - SFT 300 步(批次 16、长度 64、lr 1e-3)在节点上大约 20 秒。
- 常见错误:把结束标记(
<|im_end|>\n)从标签中去掉,导致模型停不下来;把角色标记assistant\n也放进正确答案;在去掉掩码的模型里把填充也当作正确答案。 - 原文:lm_dataset.py — SFTDataset·generate_labels · train_full_sft.py · PyTorch cross_entropy ignore_index
把对话变成一行
把 /opt/mm/data/sft.jsonl 第一行的 conversations 转成 MiniMind 聊天格式(每个回合为 <|im_start|>역할\n내용<|im_end|>\n,占位符依次为角色与内容),保存到 /root/mm/sft/sample0.txt。
mmkit.chat_text(대화)(占位符为对话)会生成这个格式。自己做也行——角色之后换行,内容之后是 <|im_end|> 和换行。不要放思考模式用的 <think> 标签和 system 提示。
只保留回答的标签
在 /root/mm/sft/sftlib.py 中实现 encode(대화, max_len=64, mask=True)(占位符为对话)——用 /opt/mm/ref/tokenizer.json 切分聊天格式,截成 64 个令牌并用填充(0)补齐,得到 input_ids;以及 labels,只有从回答的起始标记之后到结束标记为止是原来的令牌,其余是 -100。把前 20 段对话的结果以 [{"input_ids": […], "labels": […]}, …] 保存到 /root/mm/sft/labels.json。
像 MiniMind 的 generate_labels 那样,遍历令牌序列,找出与起始标记片段相同的区间,从其后开始填标签,直到结束标记片段 结束为止。如果是两轮对话,就会有两段区间。评分器会逐位与按同样规则做出的标签比较。
进入损失的令牌占百分之几
用 encode 转换整个 sft.jsonl,把不是填充的输入令牌数(real_tokens)、不是 -100 的标签数(label_tokens)及其比例(ratio)写入 /root/mm/sft/ratio.json。
如果提问和角色标记只是条件,训练信号就只来自其余部分。即使与预训练的步数相同,SFT 真正学习的令牌也只有这个比例那么多。
加了掩码的 SFT
编写脚本 /root/mm/sft/train_sft.py(参数 --nomask、--out),从 /opt/mm/ref/pretrain.pth 出发,用加了掩码的标签做 300 步(批次 16、长度 64、lr 1e-3、MiniMind 余弦、随机种子 42)SFT,保存到 /root/mm/sft/sft_masked.pth。
相对上一个模块的循环,只有数据变了——model(X[ix], labels=Y[ix])。评分器会在留出的对话上看回答损失是否低于 1.2,以及提问一侧的损失是否 仍然很高(3 以上)。因为加了掩码,提问是没有被学习过的。
去掉掩码的 SFT
其余条件与第 4 步完全相同,只把标签改成 input_ids 原样(只有填充位置是 -100)来训练,保存到 /root/mm/sft/sft_nomask.pth。
如果像 encode(대화, mask=False)(占位符为对话)这样,让一个参数就能做出两种,条件相同这件事就由代码来保证。评分器会看这个模型的提问一侧损失是否很低(低于 1.5)——那就是提问也被学会了的证据。
用留出的对话来测两个模型
把 sft_val.jsonl 的所有对话放入两个模型,把只保留回答区间标签的损失(answer_loss)和只保留非回答位置(不含填充)的损失(prompt_loss),以 {"masked": {…}, "nomask": {…}} 写入 /root/mm/sft/compare.json。
对同样的输入,只把标签用两种方式遮蔽,分别计算两次 cross_entropy(…, ignore_index=-100) 即可。logits 要错开一位来比较(logits[:, :-1] 与 labels[:, 1:])。也请比较一下相同步数下两个模型的回答损失。
回答留出的问题
用加了掩码的模型,对 sft_val.jsonl 前 20 段对话的第一个问题,用贪心解码生成回答,按每行一条,以 {"q": 질문, "a": 답, "ref": 자료의 답}(占位符依次为问题、回答与数据中的回答)保存到 /root/mm/sft/answers.jsonl。
mmkit.greedy(model, tok, 질문)(占位符为问题)会在聊天格式后面接上 <|im_start|>assistant\n 来生成,并在 <|im_end|> 处停下。留出的问题是 SFT 中没见过的提问形式,所以出现格式正确、内容错误的回答是正常的——最后一个模块里会测出原因。
留下掩码的效果
在 /root/mm/sft/report.md 中写 ## 채팅 형식、## 손실 마스킹、## 한계 三节(三个标题为韩文,依次意为“聊天格式”“损失掩码”“局限”),并以数字放入第 6 步中加了掩码的模型的 prompt_loss 和第 3 步的 ratio。
请用一行写出提问损失很高为什么是“好”的信号。局限一节写第 7 步里出错的回答是什么样子。