上一篇从 tokenizer 讲起:文本会被切成 token,再变成一串整数 input_ids。但真正进入训练循环之前,这些 token 还要被组织成模型能学习的样本。
在 MiniMind 里,这部分主要由 dataset/lm_dataset.py 负责。这个文件看起来只是 Dataset 代码,但它其实定义了不同训练阶段的“学习题型”:
- 预训练阶段:模型学习普通文本续写。
- SFT 阶段:模型学习在对话格式下回答用户。
- DPO 阶段:模型学习偏好 chosen,远离 rejected。
- RLAIF / Agent RL 阶段:模型先生成,再根据奖励信号调整。
理解 Dataset 的输出,是理解训练目标的第一步。
1. 模型真正吃进去的是什么
LLM 不直接吃字符串,它吃的是 token id。
一个最常见的训练样本可以抽象成:
{
"input_ids": [...],
"labels": [...]
}
在 causal language model 里,训练目标是 next-token prediction:
给定前面的 token,预测下一个 token。
因此如果原始 token 序列是:
[t0, t1, t2, t3, t4]
模型实际学习的是:
看到 [t0] -> 预测 t1 看到 [t0, t1] -> 预测 t2 看到 [t0..t2] -> 预测 t3 看到 [t0..t3] -> 预测 t4
工程上通常会写成:
x = input_ids[:-1] y = input_ids[1:]
这就是前面经常看到的 x/y。
2. PretrainDataset:学习文本续写
PretrainDataset 面向预训练数据。
它的目标很直接:给模型大量普通文本,让模型学习语言本身的统计规律。
可以把它理解成:
输入:一段连续文本 目标:预测每个位置的下一个 token
例如原文是:
春天来了,花开了。
tokenizer 后会得到一串 token id。Dataset 再把它切成:
x = input_ids[:-1] y = input_ids[1:]
模型看到 春天来了,,要预测后面的 花;看到 春天来了,花,要预测 开。
预训练阶段并不关心“用户”和“助手”这种角色,它只是让模型学会语言、知识、常识、句法和基本续写能力。
3. SFTDataset:学习按指令回答
SFT 是 supervised fine-tuning,也就是监督微调。
它和预训练最大的区别是:样本不再只是普通文本,而是对话。
常见原始数据长这样:
[
{"role": "user", "content": "解释一下什么是机器学习"},
{"role": "assistant", "content": "机器学习是让计算机从数据中学习规律的方法..."}
]
但模型不能直接理解 role=user 这种结构化字段。它需要通过 chat template 转成一段线性文本:
<|im_start|>user 解释一下什么是机器学习 <|im_end|> <|im_start|>assistant 机器学习是让计算机从数据中学习规律的方法... <|im_end|>
然后再 tokenizer,变成 token id。
SFT 的核心是:通常只让模型在 assistant 回答部分计算 loss。
也就是说:
user 问题部分:作为条件,不要求模型学习复述。 assistant 回答部分:作为目标,要求模型学会生成。
这就是为什么 Dataset 里会出现 loss_mask。它用来标记哪些 token 要参与 loss,哪些 token 只是上下文。
4. pad_token_id 和 -100
训练时,一个 batch 里样本长度通常不一样。
比如:
样本 A 长度 8 样本 B 长度 5
为了组成 tensor,需要把短样本补齐:
A: [a0, a1, a2, a3, a4, a5, a6, a7] B: [b0, b1, b2, b3, b4, PAD, PAD, PAD]
PAD 对应的 id 就是:
self.tokenizer.pad_token_id
但是 padding 位置不是有效训练目标,不能让模型学习“预测 PAD”。所以 labels 里通常会把这些位置设成:
-100
PyTorch 的 CrossEntropyLoss 默认会忽略 ignore_index=-100 的位置。
于是:
pad_token_id:输入里用来补齐长度。 -100:label 里用来告诉 loss 忽略这个位置。
这两个概念很容易混,但作用不同。
5. DPODataset:学习偏好
DPO 的原始数据不是单条答案,而是偏好对:
{
"prompt": "如何保持健康?",
"chosen": "保持规律作息、均衡饮食、适量运动。",
"rejected": "每天只喝咖啡就可以。"
}
这里不是简单地说 chosen 是标准答案,rejected 是错误答案。更准确地说:
在同一个 prompt 下,chosen 比 rejected 更符合偏好。
DPODataset 会分别构造两条序列:
prompt + chosen prompt + rejected
然后分别得到:
{
"x_chosen": ...,
"y_chosen": ...,
"mask_chosen": ...,
"x_rejected": ...,
"y_rejected": ...,
"mask_rejected": ...
}
x/y 仍然是 next-token prediction 的结构;mask 用来控制哪些位置参与答案概率计算。
DPO 训练时比较的是:
模型生成 chosen 的概率 vs 模型生成 rejected 的概率
并推动策略模型更偏向 chosen。
6. RLAIFDataset 和 AgentRLDataset
RLAIF / Agent RL 和 SFT、DPO 又不一样。
SFT / DPO 的答案已经在数据里:
模型直接对已有答案学习。
而 RLAIF / Agent RL 更像:
给模型一个 prompt -> 模型自己生成 response -> 用 reward model、规则函数或环境反馈打分 -> 根据分数更新模型
所以 RLAIFDataset 输出的重点是 prompt,而不是 labels:
{
"prompt": prompt,
"answer": ""
}
AgentRLDataset 还会包含工具和标准答案:
{
"messages": messages,
"tools": tools,
"gt": sample["gt"]
}
这里的 tools 是工具定义,不是工具返回结果。它告诉模型有哪些函数可以调用、参数是什么、应该按什么格式输出 tool call。
7. 一条主线串起来
从 Dataset 角度看,几个阶段的差异非常清楚:
| 阶段 | 数据形态 | 模型在学什么 |
|---|---|---|
| Pretrain | 普通文本 | 语言续写、知识和基本表达。 |
| SFT | user / assistant 对话 | 按指令回答。 |
| DPO | prompt + chosen / rejected | 更偏好好答案,远离差答案。 |
| RLAIF | prompt + reward | 根据奖励信号优化回答。 |
| Agent RL | messages + tools + gt | 学会多轮工具调用和任务完成。 |
这也是 LLM 训练链路的基本层次:
先学会语言 -> 再学会听指令 -> 再学会偏好 -> 再学会在环境中行动
8. 小结
Dataset 不是简单的数据读取器,它决定了模型看到什么、预测什么、哪些位置计算 loss。
如果你刚开始看 LLM 工程,可以先记住这几个点:
input_ids是模型输入。labels是预测目标。x = input_ids[:-1],y = input_ids[1:]是 next-token prediction 的常见组织方式。loss_mask控制哪些 token 真正参与训练。pad_token_id用于补齐输入,-100用于忽略 loss。- 不同 Dataset 对应不同训练阶段,也对应不同学习目标。
理解了 Dataset,后面看 train_pretrain.py、SFT、DPO 和 RL 训练脚本时,就不会只看到一堆 tensor,而是能看出模型到底在学什么。