1. 预训练数据集到底在解决什么问题
很多人做大模型训练,眼睛只盯着模型结构和训练参数,觉得把模型搭起来、把 loss 调下去就完事了。但真正跑过一轮完整预训练的人都清楚,数据集的质量和构建方式,直接决定了模型最终能力的上限。你喂给模型什么语料,它就学成什么样子,这件事在预训练阶段体现得淋漓尽致。
预训练数据集构建,说白了就是把互联网上、书籍里、代码仓库中散落的各种文本,经过采集、清洗、去重、过滤、格式化之后,变成模型能直接“吃”的标准数据。这个过程听起来不复杂,但实际操作中涉及的环节非常多,每一步都有坑。比如原始网页里大量导航栏、广告、乱码,你不清理干净,模型就会学到一堆无意义的重复模式;再比如不同来源的数据格式五花八门,有的用 JSON,有的用纯文本,有的用 Markdown,你得统一成训练框架能识别的格式。
这篇文章面向的是已经了解 Transformer 基本结构、跑过至少一次微调、现在想往预训练方向深入的朋友。我会围绕LLamaFactory这个训练框架,把预训练数据集从零构建的完整流程拆开讲清楚。包括数据从哪里来、怎么清洗、怎么去重、怎么格式化、怎么配比、怎么接入训练管道,以及我在实际操作中踩过的那些坑。读完你至少能做到:手里有一批原始语料,能独立把它变成一份合格的预训练数据集,并且知道每一步为什么要这么做。
2. 预训练数据集的核心设计思路
2.1 为什么预训练数据不能直接拿来就用
原始语料和可训练数据之间,差距比很多人想象的大得多。我拿最常见的网页抓取数据举例,一个原始 HTML 页面里,真正有价值的正文可能只占 20% 到 30%,剩下的全是导航、侧边栏、评论区、页脚、广告脚本。如果你不做正文提取,直接把这些内容喂给模型,模型会花大量容量去记忆那些在每个页面上都重复出现的模板文本,这不仅是浪费算力,还会让模型在生成时频繁输出无意义的导航式内容。
除了正文提取,还有几个必须处理的问题。重复数据是预训练里最隐蔽的杀手,互联网上大量内容互相转载,同一篇文章可能在几百个站点出现,如果不去重,模型会在这些重复内容上反复训练,导致过拟合和记忆化。低质量文本同样致命,比如乱码、机器翻译的劣质结果、关键词堆砌的垃圾页面,这些内容会拉低模型的语言理解能力。敏感和有害内容也需要过滤,这不是可选项,而是必须做的合规步骤。
所以预训练数据集构建的核心思路可以概括为一句话:在保证数据多样性的前提下,尽可能提升单位 token 的信息密度。多样性保证模型见过足够多的语言现象,信息密度保证每一份算力都花在刀刃上。
2.2 数据集配比背后的取舍逻辑
预训练数据不是越多越好,而是配比越合理越好。业界常见的做法是把数据分成几个大类:通用网页文本、高质量书籍和论文、代码、多语言文本、领域专有数据。每一类的占比会显著影响模型的能力偏向。
我自己的经验是,通用网页文本通常占大头,大概 60% 到 70%,因为它覆盖面最广,能提供最丰富的语言模式。书籍和论文类高质量文本占 15% 到 20%,这部分数据语法规范、逻辑严密,对提升模型的推理和长文本理解能力帮助很大。代码数据占 10% 到 15%,即使你最终不是要做代码模型,适量代码数据也能提升模型的结构化思维能力。剩下的留给多语言和领域数据。
这个配比不是固定的,要根据你的目标来调。如果你要做的是中文能力强的模型,那中文网页和中文书籍的占比就要往上提。如果你要做的是法律或医疗领域的模型,那领域专有数据的占比可能要拉到 30% 以上。关键是要有意识地去设计配比,而不是把手里所有数据一股脑倒进去。
2.3 为什么选择 LLamaFactory 作为训练框架
LLamaFactory 这两年在开源社区里热度很高,我自己用下来的感受是,它把预训练、微调、推理这条链路整合得比较顺,尤其是对数据格式的抽象做得不错。它支持多种数据格式,包括 Alpaca 格式、ShareGPT 格式,也支持纯文本的预训练格式。对于预训练任务,你只需要把数据整理成它要求的 JSON 或 JSONL 结构,配置好 YAML 文件,就能直接启动训练。
选择 LLamaFactory 的另一个原因是它对显存和分布式训练的支持比较友好。预训练对显存的需求远大于微调,LLamaFactory 内置了梯度累积、混合精度、DeepSpeed 集成等特性,能让你在有限的硬件上跑起更大的模型。当然,如果你用的是其他框架比如 Megatron 或 DeepSpeed 原生流程,数据构建的思路是一样的,只是格式对接方式不同。
3. 数据采集与清洗的实操要点
3.1 数据来源的选择与采集策略
预训练数据的来源大致分几类。公开网页数据是最主要的来源,常见的有 Common Crawl 这类大规模网页快照,也有针对特定语言或领域的爬取数据。书籍和论文可以通过公开的电子书库、学术论文开放获取平台获取。代码数据主要来自开源代码托管平台。百科和问答社区也是高质量文本的重要来源。
采集的时候有几个点要注意。第一是遵守来源的使用条款,不同数据源对使用范围有不同规定,商用和非商用差别很大,这个必须提前确认。第二是控制采集频率,不要对目标站点造成压力,合理设置并发和间隔。第三是保留元数据,比如来源 URL、采集时间、语言标识,这些信息在后续清洗和去重时非常有用。
我一般会把采集下来的原始数据按来源分目录存放,每个目录下再按采集批次分子目录,文件名里带上时间戳。这样做的好处是后续如果发现某个来源的数据质量有问题,可以快速定位并剔除,而不用把整个数据集重新跑一遍。
3.2 正文提取:从 HTML 到纯文本
拿到原始 HTML 之后,第一步是正文提取。常用的工具有trafilatura、readability-lxml、newspaper3k等。我实测下来trafilatura的综合表现比较稳,对中英文页面的正文识别准确率都不错,而且它能同时输出文本和元数据。
正文提取的核心逻辑是识别页面中密度最高、链接最少、标点最丰富的文本块。导航栏和侧边栏通常链接密度极高,评论区往往短句多、重复模式明显,这些都会被算法排除。但没有任何工具能做到 100% 准确,所以提取完之后还需要人工抽检。我的做法是每批数据随机抽 200 条,人工看一遍,如果正文提取的准确率低于 90%,就要调整工具参数或者换工具。
提取出来的文本还要做基础清洗。去除多余空白,把连续多个换行合并成一个,把制表符和连续空格处理掉。去除控制字符,那些不可见的 Unicode 控制字符会干扰 tokenizer。统一标点,比如把全角半角混用的情况规范化。这些操作看起来琐碎,但不做的话后面 tokenize 阶段会出各种奇怪问题。
3.3 质量过滤:把垃圾挡在门外
质量过滤是预训练数据构建里最需要经验的一环。我通常从几个维度来筛。
长度过滤是最简单的,太短的文本(比如少于 50 个字符)通常信息量不足,太长的文本(比如超过 10 万字符)可能是异常拼接,都要处理。重复率过滤,统计文本内部的 n-gram 重复比例,如果一段文本里同一个短语反复出现,大概率是垃圾内容。语言过滤,用语言检测工具确认文本语言,把不符合目标语言的剔除。困惑度过滤,用一个小的参考语言模型计算文本的困惑度,困惑度太高的文本通常语法混乱或质量低下。
还有一个容易被忽略的维度是符号与数字比例。如果一段文本里符号和数字占比超过 50%,那它很可能是表格、代码片段或者乱码,不适合作为通用预训练数据。我一般会把符号数字占比超过 40% 的文本标记出来,单独处理或者直接剔除。
下面这张表是我常用的质量过滤阈值,供参考:
| 过滤维度 | 阈值建议 | 处理方式 |
|---|---|---|
| 最小长度 | 50 字符 | 剔除 |
| 最大长度 | 100000 字符 | 截断或剔除 |
| 符号数字占比 | 40% | 标记复查 |
| 重复 n-gram 比例 | 30% | 剔除 |
| 语言置信度 | 0.9 | 剔除 |
| 困惑度 | 参考模型前 10% | 剔除 |
注意:这些阈值不是绝对的,要根据你的数据来源和目标模型调整。比如代码数据的符号占比天然就高,不能用通用阈值来卡。
4. 去重、格式化与 LLamaFactory 对接
4.1 去重:从精确匹配到语义近似
去重是预训练数据构建里投入产出比最高的环节之一。重复数据不仅浪费算力,还会让模型对某些内容过度记忆,影响泛化能力。去重一般分三个层次。
精确去重最简单,对整段文本做哈希,哈希值相同的只保留一份。这个能干掉完全一样的重复。近似去重用 MinHash 或 SimHash 这类局部敏感哈希算法,能找出内容高度相似但不完全一样的文本。语义去重用嵌入向量计算相似度,能发现换了说法但意思相同的文本,但计算成本最高,一般只在高价值数据上做。
我通常的做法是:先做精确去重,再做 MinHash 近似去重,语义去重只在书籍和论文这类高质量数据上做。MinHash 的关键参数是 n-gram 大小和哈希函数数量,n-gram 一般取 5 到 10,哈希函数数量取 128 到 256。阈值方面,Jaccard 相似度超过 0.8 的我一般会判为重复。
去重的时候要注意,不要跨类别去重。比如代码数据和网页文本即使有相似片段,也不应该互相去重,因为它们属于不同的数据类别,保留下来对模型有不同价值。去重应该在每个类别内部进行。
4.2 格式化成 LLamaFactory 能识别的结构
LLamaFactory 对预训练数据的格式要求比较灵活,最常用的是 JSONL 格式,每行一个 JSON 对象。对于纯文本预训练,结构可以很简单:
{"text": "这里是你的预训练文本内容..."}如果要做多语言或者带元数据的训练,可以扩展成:
{"text": "文本内容", "language": "zh", "source": "web", "category": "general"}LLamaFactory 在读取数据时会根据配置文件里的dataset字段去匹配对应的数据文件。你需要在data/dataset_info.json里注册你的数据集,指定文件名和格式。比如:
{ "my_pretrain_data": { "file_name": "my_pretrain_data.jsonl", "formatting": "text", "columns": { "prompt": "text" } } }这里formatting设为text表示这是纯文本预训练数据,columns里的映射告诉框架从哪个字段读取文本。配置好之后,在训练 YAML 里把dataset指向my_pretrain_data就能用了。
4.3 数据配比与采样策略
格式化完成之后,下一步是配比。如果你有多个类别的数据,需要按设计好的比例混合。LLamaFactory 支持通过interleave方式混合多个数据集,也支持你提前把数据按比例采样好再合并成一个文件。
我一般倾向于提前采样合并,因为这样更可控。具体做法是:先统计每个类别的总 token 数,然后按目标配比计算每个类别应该采样多少 token,再随机采样对应数量的样本,最后合并打乱。打乱这一步很重要,如果不打乱,训练时模型会先看完所有网页数据再看所有书籍数据,这种分布偏移对训练不利。
采样的时候要注意按 token 数采样而不是按样本数采样。不同类别的单条样本长度差异很大,网页文本可能平均 500 token,书籍章节可能平均 5000 token,如果按样本数 1:1 混合,实际 token 比例会严重偏离预期。
5. 常见问题与排查技巧实录
5.1 数据格式报错怎么快速定位
LLamaFactory 在读取数据时如果格式不对,报错信息有时候不够直观。我遇到最多的几类问题:JSONL 文件里有空行导致解析失败、JSON 对象缺少必要字段、文本里包含未转义的特殊字符。
排查的时候,先用python -c逐行读取验证 JSON 合法性:
import json with open("data.jsonl", "r", encoding="utf-8") as f: for i, line in enumerate(f): line = line.strip() if not line: print(f"第 {i} 行为空") continue try: json.loads(line) except json.JSONDecodeError as e: print(f"第 {i} 行解析失败: {e}")这样能快速定位到具体哪一行有问题。空行问题最容易被忽略,很多文本处理工具在输出时会留下尾部空行。
5.2 训练 loss 异常波动的数据侧原因
预训练过程中 loss 突然飙升或者不收敛,很多人第一反应是调学习率,但有时候问题出在数据上。我遇到过几种情况:数据里混入了大量重复样本导致局部过拟合、某个类别的数据质量突然变差、数据没有打乱导致分布偏移。
排查方法是把训练数据按来源和类别分组,分别计算模型在这些组上的 loss。如果某个组的 loss 明显高于其他组,说明这个组的数据可能有问题。另外可以检查训练过程中 loss 的滑动平均,如果 loss 在某个时间点突然变化,对照一下那个时间点对应的是哪批数据。
5.3 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方式 |
|---|---|---|---|
| 数据加载报错 | JSONL 格式错误 | 逐行验证 JSON | 修复或剔除问题行 |
| loss 不下降 | 数据重复率高 | 统计去重前后数量 | 加强去重 |
| loss 波动大 | 数据未打乱 | 检查数据顺序 | 重新打乱 |
| 生成内容重复 | 训练数据模板化严重 | 抽样检查数据 | 增加数据多样性 |
| 显存溢出 | 单条样本过长 | 统计长度分布 | 截断或过滤长样本 |
| 训练速度慢 | 数据文件过大 | 检查文件大小 | 分片存储 |
提示:预训练数据的问题往往在训练开始后才会暴露,所以正式训练前先用小规模数据跑几百步,确认 loss 正常下降再上全量数据,能省下大量时间。
5.4 我踩过的几个坑
第一个坑是低估了去重的重要性。早期我做数据集的时候觉得去重差不多就行,结果模型训练到后期 loss 降得很低但生成质量很差,明显是过拟合了。后来加强去重,同样数据量下模型泛化能力提升明显。
第二个坑是忽略了数据顺序。有一次我偷懒没有打乱数据,直接把网页数据、书籍数据、代码数据按顺序拼接,结果模型在训练前期表现很好,到中期突然变差。原因是分布偏移导致模型需要不断重新适应新分布。打乱之后这个问题就消失了。
第三个坑是tokenizer 和数据不匹配。我用的是一个中文优化的 tokenizer,但数据里混了大量英文和代码,导致英文和代码的 token 效率很低,同样文本量下 token 数暴涨,训练成本增加。后来我调整了数据配比,并且确认 tokenizer 的词汇表覆盖足够。
6. 从数据到训练的完整串联
6.1 构建流程的标准化
把前面所有环节串起来,一个完整的预训练数据集构建流程是这样的:采集原始数据、正文提取、基础清洗、质量过滤、去重、格式化、配比采样、打乱、注册到 LLamaFactory、启动训练。每一步都应该有日志记录,记录输入多少条、输出多少条、剔除了多少、剔除原因是什么。这些日志在后续排查问题时非常关键。
我习惯把整个流程写成一个可配置的 pipeline,每个环节的参数放在配置文件里。这样换一批数据的时候只需要改配置,不用改代码。pipeline 的每个阶段输出到独立目录,方便回溯和复用。
6.2 小规模验证的重要性
正式跑全量预训练之前,一定要用小规模数据做验证。我的做法是从每个类别里采样一小部分,组成一个大概 100 万 token 的小数据集,跑几百步训练,观察 loss 曲线和生成效果。如果小规模验证有问题,全量训练大概率也有问题,而且全量训练的试错成本高得多。
小规模验证主要看几点:loss 是否平稳下降、生成内容是否通顺、有没有明显的重复或模板化输出、不同类别的数据是否都被有效学习。如果发现某个类别拖后腿,可以回到数据侧调整。
6.3 持续迭代的数据策略
预训练数据集不是构建一次就完事的。模型训练过程中你会发现新的问题,比如某类知识覆盖不足、某种语言能力偏弱,这些都需要通过调整数据来改进。我一般会维护一个数据版本记录,每次调整都记录改了什么、为什么改、效果如何。这样积累下来,你对什么样的数据能带来什么样的效果会越来越有感觉。
另外,数据构建和模型训练是相互影响的。不同模型结构对数据的敏感度不同,同样的数据在不同模型上效果可能不一样。所以数据策略要结合具体模型来调,不能照搬别人的配比。
7. 一些实操中的个人体会
预训练数据集构建这件事,技术门槛其实不算特别高,但极其考验耐心和细节把控。我见过太多人把大部分精力花在调模型结构上,数据随便弄弄就开跑,结果训练出来的模型效果不理想,回头找原因发现是数据的问题。
我的建议是,在数据上多花时间绝对值得。一份干净、多样、配比合理的数据集,能让一个普通模型发挥出超出预期的效果;而一份脏乱差的数据集,再好的模型结构也救不回来。具体操作上,去重和质量过滤这两步千万不要省,它们对最终效果的影响远超你的想象。
还有一点,不要追求一步到位。数据构建是一个迭代过程,先跑通流程,再逐步优化每个环节。一开始可以把阈值设得宽松一些,保证数据量,然后根据训练效果逐步收紧。这样比一开始就追求完美、结果卡在某个环节动弹不得要高效得多。
最后分享一个小技巧:在格式化数据的时候,可以顺便统计一下 token 分布、长度分布、语言分布,把这些统计信息存下来。这些数据不仅能帮你判断数据集质量,还能在训练出现问题时提供排查线索。我现在的习惯是每批数据都生成一份统计报告,时间长了之后,看一眼统计数字就能大致判断这批数据能不能用。