简介:大规模预训练语言模型的数据集构成,是当前模型可复现性与透明度研究的重要议题。这份英文报告系统梳理了2018年至2022年初用于训练GPT-1、GPT-2、GPT-3、GPT-NeoX-20B、Megatron-11B、MT-NLG及Gopher等模型的数据集,逐一拆解维基百科、Common Crawl、书籍语料、Reddit链接等来源,对比各数据集大小、令牌数量与内容占比,指出主流实验室在训练数据披露上的缺失,并强调建立统一文档标准的重要性。报告引用大量参考文献并附有Top 50资源等附录,适合自然语言处理研究者、机器学习工程师及数据科学家作为模型溯源、数据配比优化与数据审计的参考。压缩包为单个PDF文档(2.36MB),结构清晰,便于离线阅读、标注与快速查阅。该资源已有112人学习,对关注大模型底层数据构成、希望提升训练数据可复现性的读者具有较强实用价值。
1. 预训练语言模型的数据集分析:为什么大多数团队输在数据准备这一关
模型选型和训练框架的讨论总被放在最前面,可真正决定一个预训练语言模型能走多远的,往往是被当作“黑匣子”处理的数据集。大规模预训练语言模型数据集的综合分析,不是把语料下下来交给训练脚本,而是对来源、去重、过滤、配比、泄露检测这些环节做一次系统性的盘账。这篇文章把分析拆到可执行:先回顾几代公开数据集的组成逻辑,再给出五个关键质量维度,然后提供一套可以直接跑的 Python 分析脚本,最后梳理最容易误判的几个坑。适合正准备自建语料、评估是否采用某个开源数据集、或是被数据配比问题困扰的算法工程师。
2. 数据集形态的三个时代:从人工清洗到网络快照再到多源混合
2.1 WebText 与 Books:GPT-2/BERT 之前的语料逻辑
现在回看 BERT 那批语料会觉得相当朴素:BooksCorpus 加上英文维基百科,总字数大约是八亿词和二十五亿词这个量级,全部文本经过标准化清洗,句子边界干净,没有网页导航和广告噪音。这种“小但精”的语料是当时构建神经语言模型的基本逻辑——先保证文本质量,再谈规模。今天很多团队在刚开始做数据的时候反而会犯相反的错误,一上来就堆几十 TB 的网页快照,结果过滤阶段就耗费掉大部分人力。
GPT-2 的 WebText 给业界带来第一个真正意义上的“质量信号”概念。它把 Reddit 上被用户投票较高的外部链接作为文本池,本质上是用社区投票当弱标注,把“人类愿意读什么”变成数据筛选器。这个思路影响深远:后续几乎所有数据集都会引入某种人工偏好或领域专家的先验信号,而不是单纯看语法是否正确。对从业者的启发是,数据准备的第一步不是写脚本,而是明确“你认可什么样的文本质量”以及“哪些源头更接近你的目标分布”。
当时还有个容易被忽视的细节:GPT-2 做了一次分量不小的清洗,把网页标签、重复模板、导航文本都去掉了。这说明即使是从高质量来源抓文本,原始 HTML 的污染率也不会低。我一般建议团队在分析任何数据集前,先随机抽两百条原始样本人工过目一遍,对污染类型做分类统计,再决定后续用词级别还是句子级别的过滤器。
2.2 清洗之后的 Common Crawl:C4 与 The Pile
Common Crawl 每月从互联网抓取的海量网页快照,是预训练语料库从“精选小众”转向“海量噪声”的分水岭。直接用原始抓取数据训练几乎不可行,真正有价值的是清洗后的版本。Google 的 C4 是这类清洗链路的典型代表:从 Common Crawl 出发,先丢弃非英文页面,再过滤掉包含“JavaScript”、“法规”等关键词的文本,做一次近似去重,再按语言模型困惑度等信号做质量打分。处理完之后,C4 大约有 750GB 原始文本、十五万级别以上 token,成为 T5 时代的标配语料。
The Pile 则走了一条不同的路:它刻意不追求“全网最大”,而是把 ArXiv、GitHub、Stack Exchange、PubMed、书籍等二十多个高质量子集混合在一起,总原始数据约 800GB。它验证了一件事:领域多样性对模型能力的广度影响巨大,缺代码语料的模型在程序合成任务上会明显偏弱,缺数学语料的模型在推理类任务上也会露出疲态。但 The Pile 也有自己的问题,某些子集包含相当多的噪声标签和作者偏好,如果不做二次过滤,下游任务会被这些隐藏偏差带偏。
| 数据集 | 来源构成 | 规模量级 | 代表性模型 |
|---|---|---|---|
| BooksCorpus | 电子书 | 约 8 亿词 | BERT 系列 |
| C4 | Common Crawl 过滤后英文文本 | 约 750GB / 156B tokens | T5 |
| The Pile | 22 个混合子集 | 约 800GB 原始 | GPT-J / GPT-NeoX |
| RedPajama | 复刻 LLaMA 配方的多源混合 | 1.2T tokens | RedPajama-INCITE |
| SlimPajama | RedPajama 清洗 + 去重 | 627B tokens | OLMo 等 |
| RefinedWeb | Common Crawl 多级过滤 | 数 T tokens 量级 | Falcon |
这张表想说明的不仅是规模增长,更重要的是配方差异:同样一批底层来源,过滤策略和配比不同,产出的模型能力会差出一大截。
2.3 公开复现时代:RedPajama、SlimPajama 与 1T Token 共识
进入 LLaMA 时代之后,数据工程被推到了前所未有的公开透明度上。LLaMA 系列公开了语料配比,Common Crawl 占大头,C4、维基、书籍、ArXiv、Stack Exchange、GitHub 各自占有不同比例,总计约 1.4T tokens。这个配比数据被不少人当成配方表,RedPajama 数据集就是按它复刻出的 1.2T tokens 开源版本。跟着 RedPajama 走了一遍全流程的人会发现,复刻配方容易,复刻清洗质量很难——同样的源,按官方描述做一遍,token 量分布会有细微差异,这些差异在训练收敛曲线上都会被放大。
SlimPajama 给了第二个重要提示:RedPajama 经过全局近似去重和进一步质量过滤之后,体积从 1.2T tokens 降到 627B tokens,减少了将近一半。后续用 SlimPajama 训练的一系列模型并没有因为数据量减半而明显变弱,反而在部分任务上更稳。这说明大语料里被浪费的重复数据远比想象的要多,去重和过滤不是在“削减数据”,而是在“找回原本该有的数据效率”。我一般会建议团队把去重分析放在抽样阶段就做掉,不要等到全量清洗完才开始查重,否则计算和存储都白烧一轮。
2.4 配比与监督信号:多源混合不是堆材料
数据配比是预训练里最像“玄学”的部分,但背后仍有一些可复用的经验。Books、ArXiv、Stack Exchange 这类高质量语料在总 token 里占比很小,却对模型的知识密度和推理能力贡献极大;Common Crawl 占比很大,但单篇文本的平均信息密度低,过度提高它的采样权重会让模型变得“话多而空洞”。常见的做法是先按 token 占比设定一个初始配比,然后用小规模对照实验调倍率,而不是一开始就追求全网最大语料。
在配比之外,监督信号的混合也属于数据集分析的一部分。纯网页文本的监督信号太疏,代码语料提供了更强的结构性信号,维基百科和书籍提供了段落级的逻辑闭环,指令类数据则在后期对齐阶段起作用。把这些信号按训练阶段分离来看,比把它们全部混在一起更健康。尤其是大规模预训练语言模型的数据集分析,必须区分“预训练阶段语料”和“对齐阶段语料”,两者承担的任务不同,清洗标准也不同。
3. 五个必须落地的质量维度:规模、去重、过滤、平衡与泄露检测
3.1 规模统计:Token 计数不等于字节数
预训练数据集的规模统计看起来是最简单的一步,实际翻车率很高。很多人在分析报告里写“数据集共 1.2TB”,但 1.2TB 到底是磁盘占用、文本字符数还是 token 数?这三个数字在中文、代码和多语言混合语料上的差异非常大。同一个数据集用 GPT-2 的 BPE tokenizer 和用 LLaMA 的 SentencePiece tokenizer 统计,结论可能相差 20% 以上。因此分析的第一步必须锁定 tokenizer,并让分析脚本与训练脚本用同一套分词配置。
实际操作中,我会对每个 jsonl 文档统计字符数、token 数、单词数和文档长度分布,并单独观察“超长文档”和“极短文档”两端的占比。极短文档往往是解析残留或对话片段,超长文档则可能是一本书或一个被拼接的网页,两类都值得单独标记,而不是只报一个平均长度。你后边做配比时,这些统计数字直接决定每个来源的采样权重,一旦早期统计失真,后期整个训练计划都会被带偏。
3.2 重复率量化:文档内、文档间与跨数据集的三种重复
重复文本对预训练的影响已经是被反复验证的教训:高重复率会拉高模型对训练样本的记忆程度,导致验证损失曲线出现“看着很低、实际泛化很差”的假象。但重复不能只看“文件是否完全一样”这一种情况。文档内重复指单篇文章中反复出现的段落或句子,常见于 SEO 拼凑页和自动生成内容;文档间重复指不同文档之间存在大段相同文本,比如转载和镜像站点;跨数据集重复则是把多个开源语料混合时,同一批网页也在别的数据集里出现。最后一种最隐蔽,因为单独看每个来源的去重率都很低,混到一起后重复比例突然飙到两位数。
要量化三类重复,常见做法是先做精确 hash 去重,再做 MinHash 近似去重。精确去重用 sha1 或 md5 对整篇文档算指纹,速度快但只能抓住完全一致的副本;近似去重则需要把文本切分成 shingle,再用 MinHash 计算 Jaccard 相似度。很多团队只做了精确去重就开始训练,结果 MinHash 扫描后才发现有大量“标题相同、正文加了一段推广文字”的近似重复文档,这部分对训练信号的污染同样严重。
3.3 质量过滤信号:启发式、语言模型与分类器各自的位置
质量过滤是数据集分析里最需要手工作业的环节。启发式规则是第一道防线,主要处理标点符号比例、HTML 残留、代码片段误混、URL 残留、无意义重复句等显性污染。它不需要模型参与,执行快,适合在大规模语料上做初筛。真正让团队纠结的是后续的“语义级”过滤:哪些文档虽然语法完整但内容空洞?哪些文档领域价值不高?
语言模型困惑度是常用的语义打分手段,做法是拿一个已经训练好的小型语言模型计算每篇文档的困惑度,困惑度过高的文本通常被认为是“不像自然语言”或“领域偏差过大”。但这里有个容易被忽略的坑:困惑度低不代表质量高,一本精心书写的法律文书可能拿到很低的困惑度,但它对通用预训练的价值未必高于一篇科普文章。困惑度更像是一个粗粒度异常检测器,而不是质量排序器。更稳的做法是训练一个 fastText 或小型 Transformer 分类器,用少量人工标注样本区分“高质量领域文本”和“低质 网页噪声”,再把困惑度结果作为特征喂进去。
3.4 语言与领域配比:低资源语料的脆弱性
语言配比直接关系到模型的多语言能力。英语语料在绝大多数开源数据集中占绝对主导,中文、日文、西班牙语等语种加起来的 token 也不一定能超过英语的一半。分析语言分布时,第一件事是统一语言识别工具,fastText 的 lid.176 模型是行业里最常见的选型,它对句子长度敏感,短文本识别误差较大。中文语料还需要额外处理繁简、分词差异和编码残留,否则 GB 级别的统计结果可能包含大量无效字节。
低资源语料在过滤环节特别脆弱。质量分类器通常是在英语样本上训练的,迁移到低资源语言时很容易把正常表达判成噪声。数据量一旦少于某个阈值,过滤后的剩余文本可能连一个小型 tokenizer 都训练不出来。我的建议是给每个语言族设定一个保留底线,分析阶段就把语言 ID、过滤前后 token 量、平均困惑度三个字段写进元数据,后续任何清洗步骤都不能让某类语种低于这个底线,除非你明确决定放弃该语言能力。
3.5 数据泄露检测:与评测集的重叠就是日后翻车的伏笔
数据泄露对大规模预训练语言模型数据集的影响经常被低估。如果评测集里的原题、改写题或长段落已经在预训练语料中出现过,模型在下游评测上的分数会虚高,且这种虚高无法通过增加训练数据来消除。最常见的检测手段是 n-gram 重叠扫描,把评测集文本切成 8-gram 到 13-gram 的片段,再在预训练语料里做倒排索引查询。重叠比例超过一定阈值,就需要逐条审查是合理的领域知识重复,还是真实的任务样本泄漏。
如果检测出高重叠,不是简单删掉那几篇文档就完事。很多评测任务的数据来自网页、维基或 GitHub,这些内容本身就在 Common Crawl 里,完全去除几乎做不到。更合理的处理是把重叠文档按“完全命中、改写命中、语义近似”分成三档,完全命中直接剔除,改写命中做人工复核,语义近似则通过降低该领域采样权重来缓解。这个分析结果同时要写进模型卡里,否则别人在无泄漏数据上复测你的模型时,分数对不上,信任度就崩了。
4. 亲手搭建分析工具链:统计、去重、困惑度与泄漏检测四条脚本
4.1 从 jsonl 读入并产出 Token 级统计
大型预训练语料最常见的存储格式是 jsonl,每行一个 JSON 对象,text 字段存放正文。这一节先从读取和基础统计开始。以下脚本使用 zstandard 处理 .zst 压缩,适合直接从开源数据集目录读取:
import json import zstandard as zstd from tiktoken import get_encoding def open_jsonl(path: str): if path.endswith(".zst"): dctx = zstd.ZstdDecompressor() raw = open(path, "rb") return dctx.stream_reader(raw) return open(path, "rb") enc = get_encoding("o200k_base") # 用训练一致的 tokenizer 效果更准 total_docs = 0 total_chars = 0 total_tokens = 0 short_docs = 0 for line in open_jsonl("pretrain.jsonl.zst"): obj = json.loads(line) text = obj.get("text", "") if len(text) < 80: short_docs += 1 tokens = len(enc.encode(text)) total_docs += 1 total_chars += len(text) total_tokens += tokens print(f"docs={total_docs}") print(f"chars={total_chars}") print(f"tokens={total_tokens}") print(f"short_docs_ratio={short_docs / total_docs:.4f}")这段脚本的关键在 tokenizer 选择。tiktoken 的 o200k_base 只是通用基线,如果你的训练脚本用 LLaMA 的 SentencePiece,这里就要替换成与训练完全一致的分词器,否则 token 统计会失真。short_docs_ratio 这个指标专门用来捕捉“无效短文本”的占比,阈值 80 字符是我常用的初始值,你可以根据语料类型调整。
4.2 用 MinHash 估算近似重复率
精确去重只能抓完全一致的文档,真正的重复大户往往是近似重复。下面这段脚本用 datasketch 做 MinHash 和 LSH 查询,统计文档间的近似重复比例:
from datasketch import MinHash, MinHashLSH NUM_PERM = 128 THRESHOLD = 0.7 SHINGLE_K = 13 def make_minhash(text: str): words = text.split() if len(words) < SHINGLE_K: return None mh = MinHash(num_perm=NUM_PERM) for i in range(len(words) - SHINGLE_K + 1): gram = " ".join(words[i:i + SHINGLE_K]) mh.update(gram.encode("utf-8")) return mh def scan_duplicates(docs): lsh = MinHashLSH(threshold=THRESHOLD, num_perm=NUM_PERM) dup_ids = set() for idx, doc in enumerate(docs): mh = make_minhash(doc) if mh is None: continue matched = lsh.query(mh) if matched: dup_ids.add(idx) lsh.insert(str(idx), mh) return dup_ids参数说明:num_perm 是 MinHash 的哈希函数个数,128 是性价比比较高的选择,256 会更精确但内存翻倍。THRESHOLD 设为 0.7,表示 Jaccard 相似度在 0.7 以上的文档会被判定为近似重复;如果希望只保留非常严厉的去重,可以把阈值提到 0.8 以上。SHINGLE_K 取 13,这是文本去重场景里一个常见经验值,太大会漏掉改写较多的文本,太小会把同主题的正常文章误判为重复。
4.3 用一个小模型给每篇文档打困惑度分
困惑度是质量过滤里的信号之一。用一个已经训练好的小型因果语言模型,对每篇文档计算平均 token 的交叉熵,再取指数得到困惑度。这里用 Hugging Face 的 transformers 实现:
import math import torch from transformers import AutoTokenizer, AutoModelForCausalLM model_name = "gpt2" tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModelForCausalLM.from_pretrained(model_name).to("cuda") model.eval() def score_ppl(text: str): enc = tokenizer(text, return_tensors="pt", truncation=True, max_length=512) if enc["input_ids"].size(1) < 20: return None with torch.no_grad(): logits = model(**enc).logits[:, :-1].contiguous() labels = enc["input_ids"][:, 1:].contiguous() loss = torch.nn.functional.cross_entropy( logits.view(-1, logits.size(-1)), labels.view(-1) ) return math.exp(loss.item())推理逻辑是:模型对每个 token 预测一个概率,交叉熵越小说明这段文本越接近模型学到的语言分布,困惑度就越低。实际使用时要记得两点:一是模型选型必须与目标语料领域接近,拿着英文 GPT-2 给中文医疗文本打分没有参考价值;二是困惑度只建议作为排序特征,不要单独指定一个硬阈值来粗暴删除,否则会把低资源语言和特殊领域文本误杀。
4.4 用 N-gram 碰撞做评测集泄露检查
泄露检测需要把评测集和预训练语料放到同一个 n-gram 空间里做碰撞。下面用离线扫描的方式,把评测集的 n-gram 集合构建成内存字典,再遍历语料文档统计命中次数:
from collections import defaultdict def build_ngram_index(docs, n=8): index = defaultdict(list) for doc_id, doc in enumerate(docs): words = doc.split() if len(words) < n: continue for i in range(len(words) - n + 1): gram = " ".join(words[i:i + n]) index[gram].append(doc_id) return index def check_leakage(corpus_docs, eval_docs, n=8): eval_index = build_ngram_index(eval_docs, n) hit_docs = set() for doc in corpus_docs: words = doc.split() if len(words) < n: continue for i in range(len(words) - n + 1): gram = " ".join(words[i:i + n]) if gram in eval_index: hit_docs.add(i) break return len(hit_docs) / len(corpus_docs)参数上,n 取 8 会让扫描更敏感,适合做“找疑似泄漏”的第一轮筛选;确认阶段建议提高到 13,减少普通知识性文本带来的误报。评测集的 n-gram 索引如果内存吃紧,可以先用 MinHash 对评测集做粗筛,只对有相似嫌疑的文档再做精确 n-gram 碰撞。
5. 数据集分析避坑指南:五个最容易导致误判的坑
5.1 去重阈值过强,把代码和文档一起删掉
现象:做完全局 MinHash 去重后,训练 loss 不降反升,模型在代码生成任务上的表现尤其差。检查数据量,发现删掉了接近一半文本,远超出“重复数据”应有的比例。
原因:Thresold 设到了 0.9,SHINGLE_K 又取到了 5,导致大量同主题、同模板但内容不同的合法文档被识别成近似重复。代码文件里重复的 import 和函数定义特别多,很容易触发高相似度判定。
解决:先把阈值降到 0.7 重新扫描,对比两次删除量的差异;代码语料要单独处理,计算相似度时可以忽略只含 import、include、package 等关键词的行;每次去重都保留一份“被删文档清单”供人工抽查,而不是只看一个删减比例数字。
5.2 困惑度模型与目标领域不匹配,过滤结果完全失真
现象:用英文通用模型给中文法律语料打分,几乎所有领域文本都被标成高困惑度,过滤后语料只剩通用新闻和百科,领域能力明显下降。
原因:困惑度反映的是“这段文本像不像模型训练分布”,而不是“这段文本有没有价值”。目标领域与模型训练分布差异越大,评分失真越严重。
解决:要么在目标领域采样几千条文本,微调一个小型质量分类器;要么把困惑度只作为“离群点检测”信号,结合长度、标点、领域关键词等规则综合判断。不要拿单个通用模型的困惑度分数当质量金标准。
5.3 用全局 token 数推导配比,忽略数据内部的噪音权重
现象:配比表里 Common Crawl 占了 60% token,但训练出来的模型总是显得啰嗦、知识密度低;同一个配比换成书籍和 ArXiv 后,模型在问答任务上反而变好。
原因:全局 token 数没有考虑“有效信息密度”。低质量网页文本虽然 token 多,但大量内容是无意义重复和广告话术,实际贡献的训练信号远低于同 token 数的书籍文本。
解决:在配比计算前,先按质量分层统计每个来源的 token 量,把“高分高质量文本”和“低分噪声文本”分开记录。采样时对低质量层降权,甚至直接丢弃高层里占比最大的那部分。数据配比应该是“有效 token”的配比,不是“原始字符”的配比。
5.4 只做文档级去重,漏了句子级与模板级重复
现象:文档级去重率不到 5%,但模型生成结果里总是出现相同的官方腔开头和固定列表结构,像是背下了某些网页模板。
原因:重复发生在段落或句子层级。很多 SEO 页面由同一个模板生成,每页中间内容不同,但开头介绍和结尾引导语完全一样。文档级去重无法捕捉这种局部重复。
解决:对每篇文档做段落切分,再对所有段落做 MinHash 索引;也可以把高频模板句提取出来建立黑名单,在过滤阶段直接去除。检查模板级重复时,可以把相似度计算单位从整篇文档缩小到连续三到五个句子。
5.5 分析脚本与训练脚本的分词器不一致
现象:分析报告里写语料共 30B tokens,训练时却显示 45B,配比参数完全偏离预期;中文语料的偏差尤其明显。
原因:分析阶段用通用 tokenizer 估算,训练框架里实际用的是另一个词表,两者对中文分词、英文子词切分的结果差异很大。tokenizer 不一致让所有基于 token 的统计、配比、学习率设置都站不住脚。
解决:把 tokenizer 的配置文件作为数据版本的一部分,分析、清洗、训练三个环节必须引用同一个 tokenizer 路径。每次统计时在脚本开头打印 tokenizer 名称和词表大小,方便回溯。
6. 用一个小实验验证你的数据配方:最小可行基线
6.1 三天内建立质量闭环
完整的预训练实验动辄几周,不适合用来快速验证数据是否干净。我一般会从待评估的数据集里随机抽取 20 万到 50 万篇文档作为子集,再用一个参数量在 0.5B 以下的小模型跑一到两万步训练。这个量级在两三天内能看出曲线差异,足够判断某个清洗方案值不值得推广到全量。验证时不只看训练 loss,还要固定一个包含摘要、常识问答和代码补全的小评测池,每五千步测一次。
6.2 自监督与下游任务的双重验证
自监督验证观察的是 loss 收敛速度和稳定性:数据净化做得越干净,验证集 loss 和训练集 loss 之间的差距越小,过拟合到来的步数越晚。但自监督信号只能说明“模型更容易学习这堆数据”,不能说明“学到的东西有用”。所以关键在于第二步——下游任务评测池必须和最终业务场景对齐。如果模型要对中文医学问答做预训练,评测池里医学问答和英文法律文本的比例就要拉开。把离线指标和采集少量的人工评估样本结合,比单独依赖自动指标更能暴露数据配比的问题。
我现在的固定习惯是每次改动数据都记一份版本说明,写明 tokenizer 路径、去重阈值、过滤规则和采样比例。这行记录在分析时觉得很麻烦,但三个月后回看项目,它是唯一能让你快速定位“模型为什么变成这样”的后悔药。数据工程里最贵的成本从来不是计算,而是忘掉自己当初怎么处理数据的。希望这篇文章能帮你在第一次做数据准备时就避开这些弯路。
本文还有配套的精品资源,点击获取