训练数据长什么样(GPT 训练分步学习·第一步)

本文是「GPT 训练分步学习路径」的第一步,基于 GPT_teacher-3.37M-cn 这个教学项目,讲清楚「喂给模型的训练数据到底长什么样」。文中所有示例均来自本项目真实的 data/train.jsonlsrc/data.py

分步学习路径共五步:① 看懂数据长什么样 → ② 模型核心(注意力+因果掩码)→ ③ 训练循环 → ④ 验收评估 → ⑤ 推理与部署。本文是第一步。


目录

  1. 这一步属于哪个环节
  2. 原始数据长什么样
  3. 从一条问答到一串数字:拼接
  4. 输入与目标:右移一位的秘密
  5. -100 是什么:让模型只学答案
  6. 真实数据实测:一条样本的完整对齐表
  7. 超长怎么办:截断策略
  8. 组批:collate 如何对齐一个 batch
  9. 核心结论速览

1. 这一步属于哪个环节

整个项目的全流程是:

1
构建分词器 → 数据处理 → 训练 → 验收 → Web 演示

本文讲的是 数据处理 环节,对应代码 src/data.py。它的作用只有一个:

把人类能读的问答对(prompt / completion),加工成模型能吃的两条等长数字序列 —— 输入 x目标 y

这一步是「构建分词器」的下游(要用分词器把文字变数字),也是「训练」的上游(训练循环直接吃它产出的 x / y)。


2. 原始数据长什么样

打开 data/train.jsonl,每一行就是一条 JSON,一个问答对:

1
2
{"prompt": "你知道什么是因果掩码?", "completion": "下三角矩阵,把未来位置权重设为负无穷,确保模型只看到历史信息。"}
{"prompt": "请问RMSNorm 和 LayerNorm 有什么区别?", "completion": "RMSNorm 只做均方根缩放不减均值,比 LayerNorm 更简洁高效。"}
  • prompt:用户的问题。
  • completion:期望模型给出的答案。

这就是所谓的 指令微调(instruction tuning) 数据格式:一问一答,教模型「看到这样的问题,该这样回答」。


3. 从一条问答到一串数字:拼接

模型不认识文字,只认识数字 id。src/data.pyInstructDataset.__init__ 把一条问答拼成一串 id:

1
2
3
prefix = [self.tok.bos_id] + self.tok.encode("用户:" + prompt + "\n助手:", add_special_tokens=False)
comp = self.tok.encode(completion, add_special_tokens=False)
ids = prefix + comp + [self.tok.eos_id]

拆开看,一条样本被拼成这样的结构:

1
2
[BOS]  用户: {问题} \n助手:   {答案}   [EOS]
└──────── prefix ────────┘ └─ comp ─┘ └结束┘
  • [BOS]:开始符(begin of sentence),告诉模型「一段话开始了」。
  • 用户:...\n助手::把问题包进一个固定模板,模型靠这个格式知道「现在轮到助手回答了」。
  • comp:答案本身。
  • [EOS]:结束符(end of sentence),告诉模型「答到这里就停」。

我们把 [BOS] + 用户:问题\n助手: 这一整段叫 prefix(前缀),它是「已知条件」;把答案 comp 叫「要学着生成的部分」。这个区分是下面所有逻辑的基础。


4. 输入与目标:右移一位的秘密

GPT 的训练目标是 预测下一个 token:看着前面所有字,猜下一个字是什么。所以对同一串 ids,要造出两条序列:

1
tar = ids[1:] + [self.tok.eos_id]
  • 输入 ids[A, B, C, D]
  • 目标 tar[B, C, D, EOS] —— 就是把输入整体 左移/右错一位

对齐起来看:

位置 输入(模型看到) 目标(要预测的下一个)
0 A B
1 B C
2 C D
3 D EOS

也就是「在位置 i 看到输入的第 i 个字,去预测第 i+1 个字」。这就是所谓的 teacher forcing:训练时不用模型自己上一步的输出,而是直接拿标准答案的前一个字当输入,效率高、收敛稳。


5. -100 是什么:让模型只学答案

现在有个问题:输入里既有「问题」又有「答案」,我们只想让模型学会写答案,不想让它去背问题(问题是用户给的,不需要模型生成)。

怎么办?用 -100 把「问题部分」的目标屏蔽掉:

1
2
ignore = min(max(0, len(prefix) - 1), len(tar))
tar[:ignore] = [-100] * ignore
  • -100 是 PyTorch CrossEntropyLoss(ignore_index=-100) 约定的「忽略暗号」。目标位置只要是 -100,这个位置就 不计算损失、不参与训练
  • 注意它和数值 100 毫无关系,纯粹是个约定的特殊标记。

为什么是 len(prefix) - 1(减 1)?

因为目标 tar 已经整体右移了一位。原本前缀占 [0, len(prefix)-1] 这些输入位置,右移后「预测答案第一个字」这件事落在了索引 len(prefix)-1 上 —— 这个位置我们要保留(它负责生成答案的第一个字)。所以只屏蔽它前面的 len(prefix)-1 个位置。

一句话:问题段的目标全设成 -100(不批改),答案段保留真实目标(要批改)。模型只为「答案答得对不对」挨罚。


6. 真实数据实测:一条样本的完整对齐表

光看代码抽象,我们拿项目里第一条真实数据跑一遍(用真实分词器),看看到底长什么样。

样本:prompt = "你知道什么是因果掩码?"completion = "下三角矩阵,把未来位置权重设为负无穷,确保模型只看到历史信息。"

实测结果:prefix 长度 = 8,拼完 ids 总长 = 15,ignore = 7。对齐表如下(截取关键部分):

1
2
3
4
5
6
7
8
9
10
11
12
13
索引  输入(ids)        目标(tar)      是否算损失
[0] BOS -100 ✗(问题段,忽略)
[1] 用户 -100 ✗
[2] : -100 ✗
[3] 你知道 -100 ✗
[4] 什么是 -100 ✗
[5] 因果 -100 ✗
[6] 掩码?\n助手 -100 ✗ ← len(prefix)-1 之前全忽略
[7] : 下三角 ✓(答案段,开始批改!)
[8] 下三角 矩阵 ✓
...
[13] 信息 。 ✓
[14] 。 EOS ✓ ← 学会在这里收尾

从这张表能一眼看懂三件事:

  1. [0]~[6] 目标全是 -100:问题段不参与训练,模型不会去”背题目”。
  2. [7] 是第一个要学的答案位置:输入是 助手: 的最后一个 token,目标是答案的第一个字「下三角」—— 这正是「看到 助手: 就该开口答」的能力。
  3. 最后一位目标是 EOS:模型还要学会「答完就停」,否则会啰嗦不止。

提示:想自己复现这张表,可以用项目里的分词器对这条样本走一遍 InstructDataset 的处理逻辑打印出来。


7. 超长怎么办:截断策略

模型有最大长度限制(本项目 seq_len=128)。如果一条问答拼完超过了这个长度,src/data.py 会截断:

1
2
3
4
5
6
if len(ids) > seq_len:
max_comp_len = seq_len - len(prefix) - 1
if max_comp_len > 0:
ids = prefix + comp[:max_comp_len] + [self.tok.eos_id] # 优先砍答案
else:
ids = prefix[:seq_len-1] + [self.tok.eos_id] # 前缀都装不下,砍前缀

策略很有讲究:

  • 优先保留完整的问题(prefix),砍掉过长的答案尾巴 —— 因为问题不完整的话,模型根本无从答起。
  • 只有当问题本身就超长、连 prefix 都放不下时,才退而砍前缀。
  • 无论怎么截,结尾都补上 [EOS],保证「结束」这件事永远被学到。

8. 组批:collate 如何对齐一个 batch

训练时是一批(batch)样本一起算的,但每条样本长度不一,得先「对齐」成同样长度,才能堆成一个张量。这就是 collate 干的事:

1
2
3
4
def collate(batch, seq_len, pad_id):
for a, b in batch:
pa = a + [pad_id] * (seq_len - len(a)) # 输入用 pad_id 补齐
pb = b + [-100] * (seq_len - len(b)) # 目标用 -100 补齐

两个关键点:

  • 输入 xpad_id 补齐:补的是「填充符」,占位用,不影响语义。
  • 目标 y-100 补齐:这些填充位置同样不算损失 —— 又一次用上了 -100 这个忽略暗号。

补齐后,一个 batch 就是形状 [batch_size, seq_len] 的整齐张量,可以直接喂进模型了。


9. 核心结论速览

  1. 数据处理是全流程第二环节,作用是把问答对加工成模型能吃的输入 x 和目标 y
  2. 原始数据是 jsonl,每行一条 {prompt, completion} 问答对(指令微调格式)。
  3. 一条样本被拼成 [BOS] 用户:问题\n助手: 答案 [EOS],前半段叫 prefix。
  4. 目标 tar 是输入右移一位得到的(预测下一个 token,teacher forcing)。
  5. -100 屏蔽问题段目标,让模型 只学答案、不背题目-100 是忽略暗号,与数值无关。
  6. 屏蔽长度是 len(prefix)-1(因为目标右移了一位,答案第一个字的预测位要保留)。
  7. 超长时优先砍答案、保住问题,且结尾永远补 [EOS]
  8. collate 把一个 batch 补齐成等长张量:输入补 pad_id,目标补 -100

一句话:这一步就是把「人话问答」翻译成「模型作业本」——输入是题面,目标是标准答案,-100 圈出「只批改答案区、不批改题目区」。


下一步预告(第二步):模型核心。我们会打开 src/model.py,看注意力是怎么算 Q·K 的、因果掩码长什么样,以及模型如何一步步把这些 id 变成「下一个字的预测」。