☰
大模型预训练数据集构建实战:清洗去重、配比与Token化全流程
2026/10/1 5:16:53 网站建设 项目流程

直接开工。这篇是系列第十六篇,前几篇我们把模型架构、分布式框架、并行策略、超参调优都聊了个遍,但说实话,模型这条路走到越深,我越确信一件事:预训练数据集才是大模型能力的真正天花板。参数结构决定了下限,数据规模和数据质量才决定上限。这一篇就专门把“预训练数据集构建”这件事掰开揉碎,从整体设计思路、数据获取、清洗去重、配比混合、Token化、质量验证到踩坑记录,给你一份可以直接抄作业的完整实战方案。

我默认你准备训练的是 10B~30B 参数规模的模型,目标 Token 量在 1T~2T 左右。如果你做的是 1B 以内的小模型,流程可以等比缩水;如果是 100B 以上超大模型,流程差不多,但工程复杂度会再往上翻几倍。这篇文章的方案基于我们自己在真实集群上调试多轮后的沉淀,很多细节是公开论文里不会写、但实操中一定躲不开的。

1. 预训练数据集构建的整体思路与设计拆解

1.1 为什么说数据决定了大模型的“智力天花板”

我在初学阶段有过一段错误认知:以为模型效果主要靠网络结构、注意力机制、层数堆叠。后来亲手把同样的 LLaMA 结构用不同数据去训练,才真正理解那句话——数据质量决定模型上限,模型结构只是逼近这个上限的手段。

举一个特别直观的例子:同样都是 7B 参数,A 组用 1.2T 清洗严谨、配比合理的 Token 训练,B 组用 300B 没怎么去重、直接从网上爬下来就喂进去的 Token 训练。前者在常识问答、代码生成、多语言能力上会全面碾压后者,甚至极端情况下 B 的效果连一些微调过的 3B 模型都不如。原因很简单:

  • 重复数据导致模型反复“背诵”同一句话,浪费了宝贵的参数量去记忆冗余模式;
  • 低质量数据(杂音、乱码、机器生成文本)会干扰语言模型的统计规律,让生成变得语无伦次;
  • 领域配比失衡会让模型出现“偏科”——代码语料多了,写代码流畅但闲聊降智;学术论文多了,说话文绉绉但常识缺失。

预训练数据集构建的核心目标就三个:规模足够大、质量足够高、配比足够合理。规模不够,模型学不到足够的世界知识;质量不够,模型学到的全是噪声;配比不合理,模型的综合能力就会失衡。所以这一步绝对不能省,也绝对不能靠运气。

1.2 数据集构建的完整流程与规模估算

一套可落地的预训练数据流水线,在我这里大致分 7 个环节:

  1. 原始语料获取与合规审查
  2. 内容提取与标准化
  3. 语言识别与按需筛选
  4. 质量过滤(分类器 + 困惑度)
  5. 多级去重(精确去重 + 近似去重)
  6. 领域配比与样本权重调整
  7. 分词器训练、Token 化与数据落盘

每个环节都会直接或间接地决定最终训练效果。以我们训练 20B 模型、目标 1.2T Token 为例,做一下规模估算,你会对数据量级有更直观的概念:

  • 目标 Token:1.2T,这里我按一个 Token 约等于 0.75 个英文单词、或者约 0.6 个汉字来估算。
  • 原始文本量:要产出 1.2T Token,原始文本差不多需要 1.6T~2T 字节。因为清洗、去重和过滤环节至少会砍掉 40%~60% 的原始语料,所以上游采集量通常要准备目标数据量的 2 倍以上,也就是 3T~4T Token 的原始材料。
  • 存储成本:原始 JSON 文本按 4T 算,三副本存储约 12T,配合 SSD 做随机读取加速,这部分成本在预算里占比不小,但省不了。Token 化后的二进制数据(我习惯用 Memmap 格式)大概在 2.5T,同样三副本存储。

这里给出一个我自己常用的快速估算公式:

原始语料预估体积 = 目标Token数 × 平均每Token字节数 ÷ (1 - 预估损耗率) 举例:1.2T Token × 1.4字节 ÷ (1 - 0.5) ≈ 3.36T 字节

损耗率取 0.5 是一个比较保守的经验值,如果数据源本身比较干净(比如以书籍和论文为主),损耗率可以放宽到 0.3;如果以 Common Crawl 这类网页数据为主,损耗率往 0.6 以上走。拿不准的时候,先用小批量样本试跑完整流程,再推算整体损耗,比拍脑袋靠谱得多。

2. 数据获取与初筛:从源头选料的门道

2.1 常见公开数据源对比与选择逻辑

数据源的选择基本决定了预训练数据的“基因”。我整理了一张常用数据源对比表,方便你根据模型定位做取舍:

数据源规模量级优点缺点适合场景
Common Crawl 网页快照PB 级体量大、覆盖广、免费噪声多、低质量内容多、需要重度清洗作为底座数据撑规模
Wikipedia数十亿 Token质量高、结构规范、百科知识密集规模有限、风格单一知识型底座补充
书籍(开源书库)百亿 Token 级长文本能力强、叙事连贯版权敏感、风格偏文学提升长文生成与推理能力
学术论文(arXiv、PubMed)百亿 Token 级逻辑性强、术语丰富风格单一、公式识别困难提升专业领域能力
代码仓库(GitHub 等开源代码)千亿 Token 级代码能力刚需注释噪声大、重复率高代码模型必备
多语言语料(多语种新闻、论坛)百亿 Token 级多语言泛化低资源语言数据稀缺多语言模型必备
对话/论坛数据(Reddit、贴吧等)百亿 Token 级口语化、问答形式多样质量参差、隐私风险提升对话流畅度

我的建议是:底座用 Common Crawl,精粮用书籍+论文+Wikipedia,代码模型再加 GitHub 代码,多语言模型按目标语言占比补多语料。这套组合拳打下来,模型的知识密度和文体多样性都有保障。底层数据源的比例建议控制在 70% 左右,精粮 30% 左右,太高精粮比例会限制模型规模扩展,太低又会让模型“营养不良”。

2.2 数据获取、提取与初筛实操

Common Crawl 不需要自己去爬网页,直接下载月度快照的 WARC 文件即可。文件巨大,通常用分布式任务并行下载解压。注意 WARC 里除了网页正文还包含请求头和响应头,需要用warcio这类库解析出干净的<main>或<article>正文。这块工作量大,但没太多技术含量,核心是别丢字段。

自己做针对性爬虫的话,优先采集目标领域的头部站点和高质量长文站点,爬取时注意规范 robots、限速、去重 URL,避免给源站造成压力。采集的原始 HTML 建议保留成 JSON 格式,字段至少包含url、html、timestamp、lang,方便后面追溯数据来源和分析问题。

初筛阶段三个动作要一起做:

  • PII 过滤:用正则加实体识别,把身份证号、手机号、邮箱、家庭住址、银行卡号等个人信息替换或删除。这一步不只是合规问题——模型会“记忆”训练数据,如果里面有真实个人信息,推理时可能泄漏,这是非常严重的隐私事故。
  • 有害内容过滤:基于关键词黑名单 + 分类模型双通道,把暴力、色情、仇恨言论等有毒内容剔除。不要只用正则,效果差,至少配套一个 fastText 或 BERT 的分类模型做二次判断。
  • 语言识别:推荐用 fastText 的lid.176.bin语言识别模型,速度快、效果好,支持 170 多种语言。实践中我会对整篇文本和文本前 500 字符分别做一次语言判定,防止多语言混排的文档被误判。

初筛这一环节不要追求一步到位,目的只是把明显不该进训练集的东西清掉,真正的质量关在下一步。

3. 清洗与去重:决定数据质量的硬核环节

3.1 从精确去重到 MinHash 近似去重

去重为什么这么关键?重复数据在预训练里不是“无害冗余”,而是危险内容。假设一条文本重复出现 100 次,模型会在学习中不断强化这条文本的权重,把它当成更重要的规律。结果是模型对重复内容的输出概率异常高、泛化能力下降,甚至出现“复读机”现象。

去重分两层:精确去重和近似去重。

精确去重最简单:对每篇文档做 sha256(或 md5),哈希值相同的文档只保留一份。注意要做全局去重,不能只对单个数据源内部去重,不同来源之间完全可能重复,比如同一篇文章被多个网站转载。但精确去重只能处理“完全一样”的情况,对“改了改标题、调换段落顺序、加了几个错字”的重复就无能为力了,所以需要近似去重。

近似去重我在实战中最常用的是 MinHash。概念理解起来不难:把文档切成若干个连续 n-gram(一般取 5-gram 或 6-gram),把每个 n-gram 哈希后得到一个集合,两个文档的相似程度可以用 Jaccard 相似度来度量:

Jaccard 相似度 = 两个文档 n-gram 集合的交集大小 ÷ 并集大小

但直接算所有文档两两之间的 Jaccard 复杂度太高了。MinHash 的秘密在于:通过对集合做多次随机排列,取每次排列后集合的最小哈希值,生成固定长度的“签名向量”。签名向量中相等的分量比例,就是原集合 Jaccard 相似度的无偏估计。然后用 LSH(局部敏感哈希)把签名按 band 分桶,只对映射到相同桶的文档对做进一步的精确相似度计算,极大压缩了计算量。

实际动手时直接用datasketch这个库就能实现 MinHash + LSH。我把核心思路贴出来:

from datasketch import MinHash, MinHashLSH # 取 5-gram 并做初步哈希 def ngrams(text, n=5): tokens = text.split() for i in range(len(tokens) - n + 1): yield " ".join(tokens[i:i+n]) # 生成文档的 MinHash 签名 def minhash_from_text(text, num_perm=128): m = MinHash(num_perm=num_perm) for gram in ngrams(text): m.update(gram.encode("utf-8")) return m # 建 LSH 索引,先只对疑似重复对做精查 lsh = MinHashLSH(threshold=0.7, num_perm=128)

注意 threshold 参数要根据你的业务语义调节。经验值:文档级去重 threshold=0.7~0.8 比较合适,低于 0.7 会把主题相同但内容差异较大的文档误杀;句子级去重 threshold 建议 0.8~0.9,抓完全重复或近似复述的句子时效果更好。如果你训练集规模特别大,可以先按 64-gram、128-gram 分段做“局部指纹”,再在指纹级去重,效率更高。

还能再加一个后缀数组(Suffix Array)来精确找出长公共子串,对代码语料效果尤其好,因为代码仓库经常有大量复制粘贴的代码块。常用工具是Borg或者你自己实现一个简单版本。想省事的话,text-dedup这个开源库把 MinHash、SimHash、精确去重都封装好了,可以直接集成到自己的流程里。

3.2 质量过滤:分类器打分与困惑度阈值双通道

网页数据里充斥着广告、SEO 堆砌、机器翻译、乱码、无意义符号,这类低质量内容光靠规则清不完,需要建一个质量过滤器。我采用的是分类器 + 困惑度两条腿走路。

分类器方案:人工抽几千条高质量文本和几千条低质量文本,训练一个 fastText 二分类器。特征用 n-gram 词袋,训练速度很快。分类器输出P(高质量)作为文本质量分,设定阈值过滤。比如我习惯把分数低于 0.4(fastText 默认概率校准不算完美,但排序能力没问题)的文本直接丢掉,对 0.4~0.7 的中间带做二次格外的规则检查。

困惑度(PPL)方案:用一个小规模但训练充分的语言模型(比如 GPT-2 或一个 1B 自训模型),计算每条文本的平均困惑度。困惑度越低说明文本越符合自然语言规律,越高越可疑。实际操作时,把 PPL > 某个阈值的文本过滤掉。这个阈值需要在自己语料上跑分布统计后定——先随机抽 10 万条样文算 PPL,画个直方图,看尾部在哪个位置断层,把 cutoff 放在断层附近。

这两种方案建议并行使用,不要只用其中一种。我遇到过纯用 PPL 漏掉“流利但无意义”的文本(例如顺着话术不断说车轱辘话,PPL 反而很低),也遇到过纯用分类器漏掉“不常见话题但质量正常”的内容(低资源语言语料尤其明显)。双通道联合后,误杀率和漏网率都可以降到可接受范围。

顺带提一句,长度过滤也不能省。我一般过滤掉小于 200 字符的“碎片文本”(几乎都是导航栏、评论片段、验证码提示串),因为碎片序列无法提供完整语义上下文;但对超大文本要小心整页拆出的超长文本(比如整本书一次性塞一个文件),可以切片到 2048 token 以下再做长度过滤,以免误伤书籍这类高价值长文本。

4. 数据配比、分词器训练与数据落盘

4.1 领域配比:不同来源的“菜谱”怎么定

数据配比会直接改变模型的下游能力分布。同一批清洗后的语料,A 配比训出的模型代码能力强但逻辑推理弱,B 配比训出的模型百科知识丰富但写代码像呓语,这不是玄学,是统计学上的必然——模型只会从它见过的词序列分布里学模式。

公开的知名模型配比是很好的参考起点。GPT-3 用了约 60% 的 Common Crawl + 22% WebText2 + 8% 书籍 + 3% Wikipedia + 其他;LLaMA 系列则在维基、书籍、论文上给了更高权重;代码模型(CodeLlama、StarCoder)会把代码语料占比提到 30% 以上甚至更高。我不是让你照抄,而是建议按自己的应用场景倒推配比:

  • 通用助手型:核心诉求是知识问答和对话,50% 网页 + 15% 书籍 + 10% 论文 + 10% 代码 + 10% 多语言 + 5% 其他。
  • 代码开发型:目标是自动补全和代码生成,35% 代码 + 30% 网页(技术博客/文档/问答)+ 15% 书籍 + 10% 论文 + 10% 其他。
  • 垂直领域型:比如医疗或法律,领域语料建议直接拉到 50% 以上,否则模型很难在专业文本上形成深层的领域能力。

配比还有个容易忽略的细节:按 Token 量算而不是按文档数算。一份代码文档平均 Token 数可能只有网页的一半,相同的文档数下代码语料的贡献度远不如网页语料。我习惯先把各领域语料 Token 数统计出来,再按目标占比倒推每类的体积和份数。

再提一个进阶玩法:数据退火(Data Annealing)。做法是在训练的最后 5%~10% 步数里,把高质量领域(书籍、论文、代码)的采样权重临时提高,让模型在收敛末期接触到更大比例的“精品内容”。这像炒菜最后撒一把香菜——整个训练都在积累基础味道,最后阶段集中强化高品质细节。实证下来,数据退火对常识能力、代码能力的提升都很可观,而且只需要改采样器权重,不增加任何训练成本。

4.2 分词器训练:BPE 与词汇表选择

数据配比确定后,就要做 Token 化。先训练分词器,再对全量数据做 Token 化落盘。

现在主流是 BPE(Byte Pair Encoding)和 Unigram 两种。BPE 按字符/字节对高频共现逐步合并,擅长处理多语言混合场景;Unigram 用概率模型从候选子词集合里挑最优切分,对形态复杂的语言(中文、日文)表现更稳。如果训练数据是多语言的,我建议用 Unigram;如果纯英文或纯代码,BPE 完全够用,可选方案是直接用 SentencePiece 库实现。

词汇表大小一般选 32K、64K 或 96K。参数规模小于 3B 时 32K 性价比最高,10B 以上建议 64K,代码模型可以试试 96K(代码 token 种类多,更大的词表能降低序列长度)。词表太大也有代价:嵌入矩阵和输出头的参数会膨胀,增加显存开销和收敛难度。我的经验是 20B 模型搭配 64K 词表比较均衡,序列长度 4096 时每一步训练成本相对可控。

分词器训练时加入一个操作,能让你后期省很多事:用全量语料的 0.1%~1% 抽样来训练分词器,样本要覆盖所有领域和语言,别只拿网页语料去训。如果分词器没见过足够多的代码和低资源语言,实际 Token 化时会产生大量 UNK 或切得很碎。

# SentencePiece 训练示例:字符覆盖模型、vocab size 64K spm_train \ --input=sampled_corpus.txt \ --model_prefix=sp_64k \ --vocab_size=64000 \ --model_type=unigram \ --character_coverage=0.9995 \ --shuffle_input_sentence=true \ --input_sentence_size=3000000

4.3 数据落盘:切片、混洗与检查点

数据预处理最后一步是把 Token 化结果落盘成训练可直接读取的格式。我一般不直接把每篇文档拼成一个超长序列,而是按固定长度切片,比如每片 2048 Token,然后做全局随机混洗。直接按原始文档顺序排列会有两个问题:一是相邻文档主题高度相关(同一网站上连续爬取的页面),导致训练出现局部偏差;二是同一文档切出的连续序列容易过长,让模型在长程依赖上偷懒。

落盘格式我用的是二进制 Memmap:

# 伪代码示意:把 token ids 写入 uint16 数组,按 2048 切片 import numpy as np tokens_all = [...] # 全量 token id seq_len = 2048 num_seq = len(tokens_all) // seq_len data = np.array(tokens_all[:num_seq * seq_len], dtype=np.uint16) data = data.reshape(num_seq, seq_len) np.save("pretrain_data.npy", data) # 后续训练 torch.from_numpy(np.load(...)) 即可

注意 Token id 超过 65535 就要改成 uint32,否则溢出。另外我习惯把原始 JSON 文本、清洗后文本、Token 化数据分三层存储,每一层都加版本号。数据版本管理是预训练工程里最容易被低估的环节——模型训练到一半发现数据有 bug,如果没版本号,想回溯到底哪批数据出了问题,会非常酸爽。用 Git LFS 管小文件,大文件用清单文件(manifest.csv)记录每批数据的生成时间和清洗参数。

到这里,数据集的构建已经走完一大半。但千万别急着开训,先做一轮质量验证。

5. 数据质量验证与迭代:别等训完才发现数据有问题

5.1 训练前的小规模试训与三向诊断

我见过不少团队把数据集构建当成“一次性产出”,清洗完直接丢给训练脚本,跑了两周以后 loss 曲线开始异常,回头查才发现某批数据中间混入了大量重复文本或者语言标签打错了。等训练到一半再返工,等于前面的 GPU 费用全部打水漂。

所以我的习惯是:大规模训练前,先跑一个小规模试训。试训配置可以很轻量:用一个 100M~300M 的模型,在 1B~2B Token 的小样本上训练几百步,重点观察三个指标:

  • Loss 曲线形状:正常情况下 loss 应该稳定下降、无明显震荡。如果 mid 阶段突然出现 loss 低谷后又反弹,很可能是混入了高重复数据导致模型先“背”后“忘”。
  • 生成质量抽查:训练几百步后手动生成几条文本,看看是否语句通顺、有无乱码、是否出现莫名其妙的人名地名。生成结果语无伦次,先别怀疑模型大小,优先怀疑数据质量。
  • 重复率检测:用小模型在验证集上算重复 n-gram 比例,如果重复率明显高于正常水平,说明去重环节还缺火候。

5.2 常见诊断脚本与指标参考

下面这个脚本虽然简单,但我每次做数据集验证都会跑一遍:

from datasets import load_from_disk from collections import Counter ds = load_from_disk("your_tokenized_data") seqs = [s for s in ds["input_ids"]][:10000] # 统计连续重复 2-gram 的比例 repeat_count = 0 total_count = 0 for seq in seqs: grams = list(zip(seq, seq[1:])) total_count += len(grams) dup = {g for g, c in Counter(grams).items() if c > 1} repeat_count += len(dup) print(f"重复2-gram占比: {repeat_count / total_count:.4f}")

经验上,在线网页类数据中重复 2-gram 占比超过 1% 就需要警惕,超过 2% 基本可以判定去重不彻底。但代码类数据重复率天然偏高,因为有大量重复语法结构,这个阈值要放宽到 4%~5% 再下结论。

5.3 数据版本管理与回流机制

数据不可能一次做到完美,所以我一向把数据集当作“活的产物”,而不是“一次性交付物”。我在团队里推行的管理方式:

  • 每次清洗流程变更都要生成新版本目录,目录名包含日期和核心参数,例如pretrain_v3_20250301_minhash07_thresh04;
  • 用一张manifest.csv记录每个版本对应的清洗参数、各领域占比、过滤阈值、具体数据源清单;
  • 试训发现问题后,直接回到清洗 pipeline 调整参数、生成新版本、重新试训,直到诊断指标通过再开大规模训练。

整个过程有点像一个“数据炼油厂”——从原油到成品油要多道工序反复调试,急不得。

6. 预训练数据集实战中的常见问题与排查技巧

6.1 问题速查表:现象、原因与解法

我把自己踩过以及在社区里高频看到的问题整理成了速查表,遇到异常现象可以先在这里定位:

问题现象可能原因排查方式解决方案
训练 loss 后期开始反复震荡重复数据混入、数据顺序未被全局洗牌检查重复 token 占比、shuffle 随机种子重新做全局去重 + 切分后全局 shuffle
模型生成内容出现大量“复读机”某条文本在语料中重复次数过高检索生成文本与训练集相似度加强近似去重、对高频句块做全局去重
多语言语料测试效果很差语言识别误判导致语料失衡按语言统计 token 占比与 PPL 分布用多语言分类器重筛,人工抽查低资源语言
模型在隐私内容上表现出“记忆”PII 过滤不彻底、直接训练了原文对生成文本回测训练集检索增强 PII 过滤 + 去重后按需重训
代码补全能力显著低于预期代码语料占比偏低或代码文本解析错误统计代码 token 占比增加代码语料并修复注释/文档字符串解析
小规模试训 loss 突降后反弹存在大量完全重复样本检查重复率指标重新去重或提高 LSH 阈值

6.2 几则实操踩坑记录(血泪教训)

踩坑一:测试集被污染,模型“高分低能”。有一次我们拿公开 benchmark 测模型,分数非常漂亮,但人工一测就露馅。后来定位到原因:我们用的训练语料里包含了 benchmark 测试集的原文,模型相当于“背了答案”。从那以后,我在数据清洗的最后一步增加了一个“黑名单过滤”,把常见评估集、已知测验集、网上公开答案的文档哈希提前建库,在预训练数据里全局剔除。这个动作成本极低,但能避免后续整个评估环节失真。

踩坑二:清洗过度,把长尾知识也洗没了。早期我为了追求数据纯净度,把过滤阈值调得特别高,PPL cutoff 一路收紧。结果模型训练完以后,主流知识没问题,但问一些偏门但真实存在的事实(比如小众开源项目、地方志、特定人物生平),模型完全哑火。原因就是过度过滤把长尾但真实的知识样本误杀掉了。现在我对过滤策略多了一分谨慎:核心目标是去掉“不真实、无意义”的内容,而不是只保留“高频、主流”的内容。只有 4 分以上再丢,中段 6~7 分的内容可以留一部分给长尾多样性。

踩坑三:分词器只拿网页语料训练,中文和代码被切得稀碎。早期图省事,直接用 Common Crawl 抽样的英文数据训练 SentencePiece,结果中文语料 Token 化后序列长度暴涨,训练效率直线下降。后来重新用多领域多语言混合样本训练分词器,中文的 Token 化效率明显改善。这件事的教训就是:分词器的训练数据分布必须贴近最终训练数据分布。

踩坑四:忘做跨数据集全局去重。我第一次构建数据时对 Wikipedia、书籍、网页分别做了内部去重,但跨数据集的重复一个没管。后来发现大量书籍内容在网页上被全文转载,导致等价内容在训练集里出现了多遍。全局去重和单源去重的计算量不在一个量级,但这件事不能省。建议按数据源分布拆分任务,用 MinHash LSH 在那批最大的语料上建索引,把其他所有语料逐条去比对,能省不少时间。

6.3 工具链与流程自动化建议

数据集构建涉及的环节多、数据量大,全手动跑根本不现实。我推荐把整个流程串成 DAG 工作流,比如用 Airflow、Argo Workflows 或者干脆就是一组 Makefile + shell 脚本,把每个环节做成独立任务,中间产物落盘,失败自动重试。这样做的好处是:每个中间产物都能被监控、审计、回溯。

另外强烈建议在流程里记录所有“关键指标”,比如每轮清洗去重后剩余 token 数、各领域占比变化、去重率、过滤率。训练结束后再回看这些指标,对于分析模型行为和排查数据问题有巨大价值——我很多次定位模型奇怪的偏差,都是通过回看数据指标而不是重读文档解决的。

整个预训练数据集构建写下来,我自己最大的体会是:这是一项 70% 工程化 + 30% 研究品味的工作。工程化的部分——分布式清洗、去重算法、存储优化——都是可复制的;但那 30% 的品味,比如过滤阈值放在哪、配比怎么微调、什么数据该留什么该丢,只能靠一轮轮试错去积累手感。所以别指望第一次就把数据集做到完美,把流水线和验证机制搭好,反复迭代,你手里的数据质量会一版比一版好。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询