MiniMind 学习笔记 02:LLM 的数据如何进入模型

上一篇从 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普通文本语言续写、知识和基本表达。
SFTuser / assistant 对话按指令回答。
DPOprompt + chosen / rejected更偏好好答案,远离差答案。
RLAIFprompt + reward根据奖励信号优化回答。
Agent RLmessages + 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,而是能看出模型到底在学什么。

发表评论