Skip to content

第 16 章 语料整理与 Gutenberg 训练工程 ​

把训练文本从一篇变成一千篇,困难不只是循环多跑几次。文件编码、重复文档、边界标记、分片大小、顺序、验证集和中断恢复都会影响训练。数据管道的错误往往仍会输出一个下降的 loss,因此要先检查数据产物,再启动训练。

上游 Gutenberg bonus 提供整理脚本和按文件训练脚本。本书只分析公开代码,并用六条自造记录验证小型离线管道;没有下载整套 Gutenberg 语料,也没有据此训练模型。固定版本目录

16.1 语料清单应先于合并大文件 ​

每份文档至少需要稳定标识、原始来源、原始字节哈希、解码方式、清理版本、保留/排除原因和训练划分。公开可下载不等于所有使用方式都已获得许可;实际项目还需核验具体材料与使用要求。本书小管道仅使用新写的短句,不依赖第三方正文授权判断。

文件顺序会影响分片与训练顺序。os.walk 的返回顺序不应被当作跨系统稳定顺序,整理前先排序,并在清单记录最终顺序。同一语料内容若因换行风格不同而被当成两份,也会污染统计,因此要区分原始字节哈希和按明确规则规范化后的内容哈希。

16.2 上游清理脚本逐步做了什么 ​

combine_files 按路径读取,先尝试 UTF-8,失败后回退 latin1;用 is_english 的 ASCII 字符占比过滤,再调用外部 Gutenberg cleanup 的 strip_headers,压缩多余空行,估算 UTF-8 字节数,最后用 <|endoftext|> 拼接进大文件。prepare_dataset.py

每一步都有边界。latin1 可以把任意字节解释成字符,因此“成功解码”不证明文本本来就是 latin1,可能产生乱码。ASCII 占比高只能说明字符集合偏向 ASCII,不能证明语言是英语;其他拉丁文字、数字表格或代码也可能通过。空字符串会让占比分母为零。该函数名是教学启发式,不应当作经过验证的语言识别器。

删除头尾元数据可能改善语言建模,但原始出处与处理记录应保留在旁边的清单,不能随训练文本一起丢掉。空行压缩也会改变诗歌、代码或格式化文本结构,是否适用取决于目标。

16.3 分片大小要把分隔符也算进去 ​

上游累加每篇清理后内容的 UTF-8 字节数,却没有把文档间 separator 的字节算入 current_size。因此最终文件可能略超预算。如果第一篇就超过预算,代码会先写当前空列表,产生空分片;超大单文档本身也不能靠“换新文件”缩小。

本书 pack 明确规定:预算包含分隔符;只有当前分片非空时才写出;单篇超大文档独占一个分片,并由调用者标记其超预算状态。这个策略不等于绝对硬限制,若系统要求每片都不超限,还需在合法边界拆文档并记录新边界。

字符数和字节数不同。“山与海”有三个字符,但 UTF-8 为 9 字节。分词后的 token 数又是第三种单位。磁盘分片预算用字节,训练上下文预算用 token,不能互换。

16.4 去重应发生在划分审计中 ​

本书小例子有一份 CRLF 文档和一份 LF 版本,规范化换行后哈希相同。按稳定名字顺序保留第一份,剔除第二份,记录“exact duplicate”。这只做精确去重,不识别近似改写、长段重复或同书不同版本。

六条输入保留四条、剔除一条重复和一条空文档。训练与验证哈希集合不相交。训练三条内容按 64 字节预算打包成两片,实际 42 和 47 字节。分隔符 \n<|endoftext|>\n 本身是 15 字节,测试专门检查不能漏计。

这些小断言能暴露数据管道常见错误,但不能保证真实大语料无重复。更完整的去重需要明确比较单位、相似度规则与误删风险,并对训练验证边界单独审计。

16.5 按文件训练怎样管理进度 ​

上游 read_text_file 读取一个合并文件,再附加结束符。create_dataloaders 在该文件内部按字符比例划分训练和验证,沿用固定长度、stride 等于上下文的窗口。每个文件重新创建 loader,模型和优化器持续保留,所以训练状态跨文件延续。

这种方式降低一次持有全部原文的需求,但仍会把当前大文件完整读入、分词并存窗口。上游 Dataset 还物化了输入与目标张量,可能重复保存大量 ID。真正的大语料实现通常要考虑预分词、内存映射 token 数组、按索引切片或流式读取;本书不把当前逐文件实现描述为常数内存流式训练。

验证 loss 每次来自当前文件后段,会随文件主题变化。若要比较跨训练过程的模型进步,应另保留固定验证集合。否则某个时间点 loss 升高可能只是切到更难的文件,而不一定是训练退化。

16.6 检查点频率代码值得逐字符核对 ​

固定版本训练脚本在文件结束后写:if global_step % save_ckpt_freq:。Python 把非零余数当 True,因此它不是“每逢整除时保存”,而是大多数非整除时保存;位置又在文件循环末尾,无法保证精确每 N 个优化器步骤触发。

若意图按正整数步数定期保存,应在更新循环内检查 (step + 1) % frequency == 0,或先定义清楚 step 从 0 还是 1 开始。本书小测试 frequency=3 时得到步骤 3、6、9。还应在正常完成和中断时保存,并防止异常中尚未定义有效 step。

上游捕获 KeyboardInterrupt 后保存模型权重,这能留住已训练参数,但不等于精确断点续训。需要再保存优化器、epoch、文件索引、文件内批次位置和随机数状态,第 15 章已有最小续训对照。输出目录也应独立命名,避免不同实验覆盖同名检查点。

16.7 ETA 与日志也有计量假设 ​

convert_time 用 divmod 把秒转成时、分、秒。print_eta 用已处理文件平均时间乘剩余文件数,因此假定文件成本近似。若分片大小差很多、首次分词慢或后面发生验证与保存,ETA 只能是粗估。

脚本在多 epoch 情况下用从训练开始的累计时间、当前 epoch 内的文件 index 估算,时间与计数的范围可能不一致。更可靠的是用已处理 token 或总批次数,并明确包含哪些 I/O、验证和生成时间。吞吐统计也应区分训练 token 与评估 token。

16.8 CLI 与测试入口的边界 ​

上游提供 data_dir、output_dir、epochs、采样间隔、评价间隔、保存频率、学习率、batch_size 和 debug 配置。type=bool 解析命令行字符串容易误导:非空字符串 "false" 在 Python 中也是真。更合适的是 store_true 或明确布尔解析。

debug 模型缩小宽度、层数和上下文,但仍保留 GPT-2 的 50,257 词表,且依赖分词器及包接口。上游测试写入重复字符串,调用 debug 脚本,只检查 stdout 中是否出现最终内存提示;它是 smoke test,不是 loss、标签、数据划分或权重恢复的严格验证。重复字符串直接相乘还会使片段末尾与下一段开头贴在一起,若需要边界,必须显式加分隔符。

没有文件时脚本退出;频率参数为零会导致取模错误;过短文件可能造不出窗口。正式入口应在开始前检查这些条件,并把跳过文件和原因记录下来,而不是到训练一半才失败。

16.9 可运行的小管道与练习 ​

运行:

bash
python examples/core_training/corpus_demo.py qa/core_training/corpus_demo.json

程序只处理代码中自造的六条文本,不写外部语料,也不启动模型训练。它验证空输入、空文档、换行规范化、精确重复、划分隔离、UTF-8 字节预算、超大文档策略和保存步数。主训练链路已在第 13 章独立验证,把两者分开有助于定位错误。

补充题一:两个各 10 字节文档、15 字节分隔符,能装进 30 字节分片吗?答案:不能,共 35 字节。补充题二:ASCII 占比 100% 的文档一定是英文吗?答案:不是,例如数字和源代码。补充题三:为什么把所有窗口随机分训练验证可能比按文档分更乐观?答案:重叠上下文和重复内容可能跨集合出现。

参考与覆盖 ​

  • prepare_dataset.py:编码回退、ASCII 启发式、清理、空行、分片和 CLI。
  • pretraining_simple.py:逐文件 loader、训练、评价、生成、保存、ETA、中断、debug。
  • 测试入口:仅说明 smoke test 的覆盖程度,未运行完整 Gutenberg 下载或训练。

保留各书来源与许可 · 示例运行条件以正文为准