TT Lab
开始
学习 学习路径 课程

MiniMind — 亲手从头到尾训练一个小型语言模型

以聊天格式做 SFT,测量损失掩码改变了什么

在 TT Lab 中继续学习

目标

把对话切成 MiniMind 的聊天格式(<|im_start|>역할\n…<|im_end|>\n,占位符为角色),并像 generate_labels 那样亲手做出只保留回答(assistant)的标签。以基准预训练权重为起点,用相同条件训练加了掩码的模型和去掉掩码的模型,用数字看在留出的对话上,回答令牌和提问令牌的损失是怎样分开的。

为什么重要

SFT 不是新算法,而是同样的下一个令牌预测。不同的只有一点:把什么当作正确答案。如果把整段对话当作正确答案,模型就要把训练信号分一部分给模仿提问,回答结束后甚至可能自己编出下一个问题。只把回答当作正确答案,提问就成了条件,而且必须把结束标记也放进正确答案,才能学会什么时候停下。 这条界线由标签数组里的几个 -100 决定,出错了也不会报错。所以亲手把标签打印出来,再用两个模型测一测掩码实际改变了什么,是最可靠的确认方法。

步骤

  1. 把 /opt/mm/data/sft.jsonl 的第一段对话转成聊天格式字符串,保存到 /root/mm/sft/sample0.txt。
  2. 在 /root/mm/sft/sftlib.py 中实现 encode(대화, max_len=64, mask=True)(占位符为对话),并把前 20 段对话的 input_ids、labels 保存到 /root/mm/sft/labels.json。
  3. 把整个 SFT 数据中去掉填充的实际令牌数、剩下标签的令牌数及其比例,写入 /root/mm/sft/ratio.json。
  4. 从基准预训练权重(/opt/mm/ref/pretrain.pth)出发,用加了掩码的标签做 300 步 SFT,保存为 /root/mm/sft/sft_masked.pth。
  5. 在相同条件下把标签设为整个输入(只有填充是 -100),保存为 /root/mm/sft/sft_nomask.pth。
  6. 在留出的对话(sft_val.jsonl)上,把两个模型的回答令牌损失和提问一侧令牌的损失,写入 /root/mm/sft/compare.json。
  7. 用加了掩码的模型对留出的 20 个问题用贪心解码生成回答,保存到 /root/mm/sft/answers.jsonl。
  8. 在 /root/mm/sft/report.md 中写 ## 채팅 형식、## 손실 마스킹、## 한계 三节(三个标题为韩文,依次意为“聊天格式”“损失掩码”“局限”),并放入加了掩码的模型的提问损失和第 3 步的比例。

参考

把对话变成一行

把 /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 步里出错的回答是什么样子。