跳转至

第八章 预训练:数据、目标、规模与训练稳定性

预训练让模型从大规模语料中学习语言统计、知识与通用表示。它不是把网页“存进参数”,而是在下一个 token 等目标上优化概率分布。完整预训练工程包括数据治理、tokenizer、模型设计、规模实验、分布式训练、监控、评估和发布。

预训练流水线

图 8-1 预训练是可版本化的数据—模型—评估闭环,数据治理贯穿全程。

8.1 训练目标与架构

先建立直觉。 预训练把大量 token 变成自监督样本。Decoder-only 用前缀预测下一个 token,Encoder-only 常用掩码恢复,Encoder-Decoder 可用去噪重建;目标决定可用信息与适合任务。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。

把过程拆开看:

  1. 文本经过 tokenizer。
  2. 构造输入与标签。
  3. 模型输出每位置 logits。
  4. 只在有效标签上计算损失。

最小例子。 长度 T 的自回归序列可产生 T-1 个预测目标,训练时并行计算,推理时只能按前缀逐步生成。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。

容易踩坑。 把训练并行误解为推理并行、标签未右移、EOS 与 padding 混淆、把 perplexity 跨 tokenizer 直接比较,都会误导。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。

动手任务:对同一短句分别写出 causal LM、masked LM 与 seq2seq 去噪的输入和标签。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。

Decoder-only 模型最常用因果语言建模:

$$ \mathcal{L}=-\sum_{t=1}^{T}\log p_\theta(x_t\mid x_{<t}). $$ Encoder-only 模型可用掩码语言建模,适合表征与理解;Encoder-Decoder 可用 span corruption,把被遮蔽片段作为生成目标。目标决定可见信息与下游归纳偏置,不能只比较模型参数量。

训练时采用 teacher forcing:所有位置的真实前缀同时输入,在因果掩码下并行预测。标签通常是输入右移一位;padding 或跨样本拼接位置要设为忽略值,避免计算无效损失。

8.2 数据获取与合规

预训练数据治理管线

图 08-2 来源登记、去重和安全过滤都应保留可审计记录。 从问题出发。 数据质量、合法性和可追踪性是预训练的第一层模型能力。采集只是开始,还要做格式识别、语言过滤、去重、质量评分、隐私与版权处理。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。

沿数据流逐步检查:

  1. 建立来源与许可清单。
  2. 抽取正文并过滤模板噪声。
  3. 文档级与段落级去重。
  4. PII/恶意内容处理并分层抽样。

用小数据走一遍。 同一新闻被数千站点转载会放大记忆和数据泄漏风险;MinHash 可近似发现相似文档,但阈值需要通过人工样本校准。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。

这里最容易出现的误解。 只按关键词过滤、训练集与评测集去重不足、删除数据后索引仍保留、来源权重无记录,都会造成不可解释风险。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。

小实验:为 1 万篇文档设计数据卡,记录来源、许可、语言、质量、去重簇和过滤原因。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。

数据首先要有可用权利和清楚来源。建立数据卡记录来源、许可、语言、时间、清洗规则、PII 处理、已知偏差与删除流程。公开可抓取不等于可随意训练或再分发。

清洗通常包括编码修复、语言识别、文档质量过滤、样板/广告去除、PII 与恶意内容处理。去重分精确去重与近似去重;训练/评估集之间还要做污染检测。过度过滤会损失方言、少数语言和领域多样性,质量规则应抽样审计。

混合数据时,不能只按原始体量采样,否则高资源语种和重复网页会主导训练。可按领域、语言与质量分层设权重,并跟踪每类 token 的实际占比。

8.3 Tokenizer 训练

先看它解决什么。 Tokenizer 决定模型看到的基本符号、序列长度和多语言效率。BPE、Unigram 等算法从字符或字节出发学习可复用子词,不是寻找唯一正确的词语边界。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。

可以把实现分成以下环节:

  1. 规范化与预分词。
  2. 统计候选片段。
  3. 迭代合并或概率选择。
  4. 加入特殊 token 并验证可逆性。

一个可以手算的例子。 代码缩进、中文、生僻字符和 emoji 都应能无损编码;byte fallback 可避免未知字符,但可能增加 token 数。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。

排错时先看这些地方。 在全量测试集上训练词表、特殊 token id 与模型配置不一致、只看词表大小不看压缩率,会产生隐患。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。

现在动手:训练两个不同词表大小的 tokenizer,比较中英代码语料的字符/token 比和未知字符处理。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。

Tokenizer 词表过小会产生长序列,过大增加嵌入和输出层参数并稀释低频 token。中文、代码、数学和多语言对分词有不同需求。评估指标包括平均每字符 token 数、不同语种压缩率、特殊符号和空白保真、未知/字节回退行为。

特殊 token 的 id、对话模板和 BOS/EOS 规则必须固定并写入模型配置。预训练完成后随意更换 tokenizer 相当于改变输入语义。

8.4 Scaling Laws:用小实验规划大训练

抓住这一节的主线。 Scaling Law 用小规模实验估计模型、数据和计算之间的趋势,帮助在固定预算下选择参数量与 token 数。它是经验规律,不是对所有架构和数据永恒不变的定律。 遇到新术语,先找它在系统中的位置:它读取什么、保存什么、改变什么,以及失败时会留下什么信号。

真正动手时按这个顺序走:

  1. 选择多个模型和数据规模。
  2. 保持训练配方可比。
  3. 拟合 loss 与 compute 曲线。
  4. 用外推结果规划并留安全余量。

先做最小实验。 只训练更大的模型但 token 不足可能处于欠训练状态;更多高质量 token 有时比继续增加参数更划算。 把随机性固定并保留中间量,这个例子就能成为后续优化时的回归基线。

别被表面现象带偏。 用单个实验点拟合、跨 tokenizer 比 perplexity、把训练 FLOPs 当总成本、忽略数据质量变化,都会破坏结论。 先区分定义错误、实现错误、数据问题和性能瓶颈。四类问题需要的证据不同,不能靠盲目调参混在一起处理。

验证任务:用三个小模型和三种 token 预算记录验证损失,画 compute—loss 图并说明不确定性。 实验结束后写三句话:观察到了什么、这些证据支持什么结论、还有哪种解释尚未排除。

Scaling Law 描述损失与模型规模、数据量和计算量之间的经验幂律关系。Chinchilla 工作指出,在给定计算预算下,模型参数和训练 token 应协调扩展;它不是“永远固定 20 token/参数”的硬定律,具体最优点受数据质量、架构、优化器和目标影响。

实践中先训练多组小模型,保持数据和训练设置可比,拟合损失曲线,再估算目标规模。外推存在误差,应留预算和容错。只扩大参数而数据不足会欠训练;只增加数据而模型太小也会进入收益递减。

8.5 一个微型自回归模型

先把概念落到可观察对象上。 微型自回归模型把完整链路缩小到单机可观察尺度:embedding、多个 Decoder Block、最终归一化和词表投影。它用于验证实现,不代表真实大模型训练配方。 这一节的重点是建立因果链。先说明问题,再看计算或流程,最后用可重复实验验证结论。

把抽象概念还原成操作:

  1. token/position 进入隐藏状态。
  2. 逐层注意力与 FFN。
  3. 输出投影到词表。
  4. 交叉熵训练并采样生成。

把它缩小到能逐项检查。 权重绑定可让输入 embedding 与输出 projection 共用参数;是否使用必须与架构配置和维度一致。 如果这个小例子还不能解释清楚,扩大数据只会让错误更难发现。

需要特别守住的边界。 位置长度超界、因果 mask 错误、初始化过大、训练语料太小却期望知识能力,都会导致误判。 保留完整 trace 比猜原因更重要。找到第一次偏离预期的位置,通常比分析最终错误输出更高效。

本节练习:让模型在一小段文本上过拟合,确认 loss 接近零并能复述,再换验证文本观察泛化差距。 除代码外,请保留一份结果说明,标明环境、随机种子、输入规模和你主动检查过的边界。

import torch
from torch import nn

class TinyLM(nn.Module):
    def __init__(self, vocab_size=256, dim=128, layers=4, heads=4, max_len=512):
        super().__init__()
        self.token_emb = nn.Embedding(vocab_size, dim)
        self.pos_emb = nn.Embedding(max_len, dim)  # 教学版用绝对位置
        layer = nn.TransformerEncoderLayer(
            d_model=dim, nhead=heads, dim_feedforward=4 * dim,
            batch_first=True, norm_first=True, activation="gelu"
        )
        self.blocks = nn.TransformerEncoder(layer, num_layers=layers)
        self.norm = nn.LayerNorm(dim)
        self.lm_head = nn.Linear(dim, vocab_size, bias=False)
        self.lm_head.weight = self.token_emb.weight

    def forward(self, tokens):
        b, t = tokens.shape
        pos = torch.arange(t, device=tokens.device)
        x = self.token_emb(tokens) + self.pos_emb(pos)[None]
        causal = torch.triu(torch.ones(t, t, device=tokens.device, dtype=torch.bool), 1)
        x = self.blocks(x, mask=causal)
        return self.lm_head(self.norm(x))

def next_token_loss(model, batch):
    logits = model(batch[:, :-1])
    labels = batch[:, 1:]
    return nn.functional.cross_entropy(logits.reshape(-1, logits.size(-1)),
                                       labels.reshape(-1))

这里借用 TransformerEncoder 但传入因果 mask,使其表现为因果堆叠;命名容易误导,因此生产实现应使用明确的 Decoder Block。权重绑定让输入嵌入与输出投影共享参数。reshape 前要确认 logits 与 labels 的位置严格对齐。

8.6 数据打包与有效 token

先建立直觉。 打包的目标是减少 padding,让每个设备处理尽可能多的有效 token。样本边界、EOS、document mask 与 position_ids 必须与训练目标一致。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。

把过程拆开看:

  1. 按长度分桶或流式拼接。
  2. 插入 EOS 保留边界。
  3. 构造 attention 与 label mask。
  4. 统计有效 token 比例。

最小例子。 两个短文档拼成一条序列时,若允许跨文档注意力,模型可能把无关结尾当作下一文档上下文;是否隔离要显式选择。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。

容易踩坑。 把 padding 计入 loss、最后残片大量丢弃、packing 与缓存模板不一致、只报告 batch size 不报告 token 数,都会影响训练。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。

动手任务:实现 fixed-length packer,验证每个原文 token 恰好出现一次且标签边界正确。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。

把短文逐条 padding 会浪费计算,可将多个文档 token 拼接再切固定长度。文档边界是否允许相互注意取决于训练设计;至少要插入 EOS,必要时使用 block-diagonal mask。记录 tokens_per_second 应只统计非 padding、真正参与损失的 token。

数据加载器要能断点续训:保存数据 shard、样本游标、随机数状态和混合权重。若恢复后数据顺序改变,训练虽能继续,却不再严格复现。

8.7 优化器与学习率

从问题出发。 AdamW、学习率预热和衰减共同控制更新尺度。预热降低训练初期不稳定,权重衰减与 Adam 的自适应矩估计解耦。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。

沿数据流逐步检查:

  1. 计算梯度并做全局裁剪。
  2. 优化器更新一阶/二阶矩。
  3. 按 schedule 得到当前学习率。
  4. 记录参数与更新范数。

用小数据走一遍。 相同学习率在不同有效 batch、参数化和精度下含义不同;可观察 update/weight ratio 判断更新是否过激。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。

这里最容易出现的误解。 把 weight decay 施加到所有 bias/norm、梯度累积时错误缩放 loss、恢复 checkpoint 后学习率重置,都会影响收敛。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。

小实验:比较 constant、linear warmup+cosine 两种曲线,并解释训练早期 loss 的差异。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。

常见配置为 AdamW、warmup 后余弦衰减、梯度裁剪和 BF16。Warmup 在训练初期逐渐提高学习率,减少随机初始化下的剧烈更新。全局批量大小 = 每卡微批量 × 梯度累积 × 数据并行卡数。

大批量可提高吞吐,但可能需要调整学习率并影响泛化。梯度累积只减少单步显存,不减少总计算。BF16 相比 FP16 有更大指数范围,通常更稳定,但硬件支持和部分算子仍需检查。

8.8 监控与异常处理

先看它解决什么。 长训练必须把异常当预期事件处理。监控不仅有 loss,还要有学习率、梯度范数、吞吐、有效 token、显存、数据延迟和硬件错误。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。

可以把实现分成以下环节:

  1. 每步记录核心标量。
  2. 定期保存可恢复 checkpoint。
  3. 异常时保留最近数据批次。
  4. 恢复后验证状态连续。

一个可以手算的例子。 loss spike 若只出现在某个数据 shard,可能是异常长序列或坏文本;若所有 rank 同时出现则可能是学习率或数值问题。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。

排错时先看这些地方。 只保存权重不保存优化器和 RNG、告警阈值固定不分阶段、遇 NaN 自动跳过且不留样本,会掩盖根因。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。

现在动手:为训练循环加入非有限值检查和故障包,保存 step、样本 id、配置与张量统计。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。

至少监控训练/验证 loss、学习率、梯度范数、参数范数、吞吐、MFU、数据等待、显存、通信和异常样本。loss 突增可能来自坏数据、数值溢出、学习率、恢复错误或硬件故障,不能只回滚而不定位。

Checkpoint 要包含模型、优化器、调度器、梯度缩放器、随机状态和数据位置。周期性做恢复演练,确认文件存在不等于可以恢复。大集群还需处理节点抢占、慢卡、网络错误和检查点写入瓶颈。

8.9 评估与涌现

抓住这一节的主线。 验证 loss 衡量平均预测难度,不能完整代表知识、推理、安全和生成质量。涌现现象还会受到指标阈值、提示和评测规模影响。 遇到新术语,先找它在系统中的位置:它读取什么、保存什么、改变什么,以及失败时会留下什么信号。

真正动手时按这个顺序走:

  1. 使用无泄漏验证语料。
  2. 定期跑稳定 benchmark。
  3. 检查生成与记忆样例。
  4. 按能力、安全和语言分桶。

先做最小实验。 选择题准确率从随机附近跨过阈值可能看似突然,但底层连续概率已有平滑变化。 把随机性固定并保留中间量,这个例子就能成为后续优化时的回归基线。

别被表面现象带偏。 评测集进入训练、只报最好 checkpoint、用不同 prompt 比模型、把 benchmark 提升直接等同真实业务提升,都会夸大结果。 先区分定义错误、实现错误、数据问题和性能瓶颈。四类问题需要的证据不同,不能靠盲目调参混在一起处理。

验证任务:建立预训练评估表,分别包含 loss、常识、阅读、代码、记忆和安全,并写局限。 实验结束后写三句话:观察到了什么、这些证据支持什么结论、还有哪种解释尚未排除。

验证困惑度反映预测分布,但不等同于指令遵循、事实性或推理。建立多层评估:语言建模损失、基础任务、领域任务、生成质量、安全和污染检查。所谓“涌现”有时受指标阈值与尺度影响,应报告连续曲线和不确定性,避免把单个跳变过度神秘化。

8.10 从 Nano 模型走向真实预训练

先把概念落到可观察对象上。 从 Nano 模型到真实预训练,变化的不只是规模,还包括分布式数据、容错、精度、通信、评估、治理与成本。应通过阶段门逐步放大,而不是一次跳到昂贵训练。 这一节的重点是建立因果链。先说明问题,再看计算或流程,最后用可重复实验验证结论。

把抽象概念还原成操作:

  1. 单批次过拟合验证实现。
  2. 单机小语料验证稳定性。
  3. 多卡短跑验证通信与恢复。
  4. 小比例数据验证完整管线。

把它缩小到能逐项检查。 先用 0.1% 数据跑完加载—训练—保存—恢复—评估,能提前发现比模型结构更常见的工程错误。 如果这个小例子还不能解释清楚,扩大数据只会让错误更难发现。

需要特别守住的边界。 未经缩放实验直接投入全部算力、没有停止标准、训练中频繁更改数据配方、评估代码不版本化,都会浪费预算。 保留完整 trace 比猜原因更重要。找到第一次偏离预期的位置,通常比分析最终错误输出更高效。

本节练习:为一次真实预训练写四级放大计划,每级规定通过指标和最大预算。 除代码外,请保留一份结果说明,标明环境、随机种子、输入规模和你主动检查过的边界。

微型项目应完成 tokenizer、数据 shard、模型、训练、验证、生成、checkpoint 和实验记录。目标不是复现大模型能力,而是验证全链路。扩到多卡前先确保单卡能过拟合一个小批次;若连小批次都学不会,增加 GPU 只会更快地产生错误。

练习:训练字符级 TinyLM 并观察上下文长度变化;实现断点续训并比较恢复前后下一个 batch;做三组模型/数据规模实验,绘制验证 loss 与计算量的关系。

延伸阅读:Training Compute-Optimal Large Language ModelsLlama 3 技术报告DeepSeek-V3 报告

本章配套代码

下面的脚本与正文使用相同符号。建议先在代码中打印形状和中间量,再运行断言;若依赖尚未安装,至少先阅读入口函数、输入输出和测试部分。

本章端到端实验:把知识变成可复现证据

本实验不是把本章代码重新抄一遍,而是把概念、实现、测试和解释串成一个小型工程。请新建独立目录,保存 README.md、环境文件、源代码、测试、运行日志和结果图。README 至少说明任务、输入输出、运行命令、预期现象、已知限制和复现条件。

实验步骤

  1. 对同一短句分别写出 causal LM、masked LM 与 seq2seq 去噪的输入和标签。
  2. 为 1 万篇文档设计数据卡,记录来源、许可、语言、质量、去重簇和过滤原因。
  3. 训练两个不同词表大小的 tokenizer,比较中英代码语料的字符/token 比和未知字符处理。
  4. 用三个小模型和三种 token 预算记录验证损失,画 compute—loss 图并说明不确定性。
  5. 让模型在一小段文本上过拟合,确认 loss 接近零并能复述,再换验证文本观察泛化差距。
  6. 实现 fixed-length packer,验证每个原文 token 恰好出现一次且标签边界正确。

每完成一步,先写下预期,再运行代码。若结果与预期不一致,不要覆盖旧日志;建立 failures.md,记录现象、假设、证据、修复和回归测试。这样得到的不是一次性 Demo,而是一份能证明你真正理解本章内容的实验档案。

验收标准

  • 全新环境能够按照 README 从头运行,依赖和随机种子已记录。
  • 关键函数至少有正常、边界和错误输入三类测试;涉及数值计算时检查有限值与合理误差。
  • 结果包含一个基线和至少一个受控改动,能够说明变化来自哪里。
  • 日志保留输入规模、耗时、内存或显存、软件版本和失败样本。
  • 结论区分“实验直接证明的事实”“根据事实做出的推断”和“仍未验证的猜想”。

本章自测

  1. 不看正文,用自己的话解释“预训练把大量 token 变成自监督样本”,并给出一个可以证伪的测试。
  2. 不看正文,用自己的话解释“数据质量、合法性和可追踪性是预训练的第一层模型能力”,并给出一个可以证伪的测试。
  3. 不看正文,用自己的话解释“Tokenizer 决定模型看到的基本符号、序列长度和多语言效率”,并给出一个可以证伪的测试。
  4. 不看正文,用自己的话解释“Scaling Law 用小规模实验估计模型、数据和计算之间的趋势,帮助在固定预算下选择参数量与 token 数”,并给出一个可以证伪的测试。
  5. 不看正文,用自己的话解释“微型自回归模型把完整链路缩小到单机可观察尺度:embedding、多个 Decoder Block、最终归一化和词表投影”,并给出一个可以证伪的测试。
  6. 不看正文,用自己的话解释“打包的目标是减少 padding,让每个设备处理尽可能多的有效 token”,并给出一个可以证伪的测试。

回答时先画图或写形状,再给结论。若只能说出术语而不能给出最小例子、边界条件和验证方法,说明这一节仍需要回到代码中重做。