
简介这份PDF文档系统梳理了2018年至2022年初GPT-1、GPT-2、GPT-3、GPT-NeoX-20B、Megatron-11B、MT-NLG与Gopher等大规模预训练语言模型所使用数据集的组成情况面向自然语言处理研究人员、机器学习工程师与数据科学家帮助读者理解各模型训练数据的来源、规模与令牌数量差异并直面数据集透明度不足这一核心问题。资源包内含1个PDF文件大小约2.36MB结构上依次覆盖Wikipedia、Books、Journals、Reddit链接、Common Crawl等常见数据源分析以及各模型数据集摘要、The Pile v1分组数据、Gopher的MassiveWeb分析等章节并附有结论、延伸阅读与附录A的Top 50资源清单。作者Alan D. Thompson在文中引用大量参考文献与附录材料对Books1、Books2及Wikipedia等数据集的统计口径提出质疑为后续数据集构建与模型训练提供可复用的评估视角。目前已有112人学习适合需要深入对比主流大模型数据构成、关注数据透明度标准的中高级读者参考。1. 大规模预训练语言模型数据集从 GPT-1 到 GPT-2 的语料账本很多人第一次接触 GPT 系列注意力全在模型结构上——多少层 Transformer、多少注意力头、参数量多大。但真正决定模型能力上限的往往是那本没人细看的语料账本。GPT-1 用了约 7000 本书规模的 BooksCorpus到了 GPT-2语料直接膨胀到 40GB 的 WebText模型参数从 1.17 亿涨到 15 亿。参数量涨了十几倍语料涨了几十倍这两条曲线不是巧合。这篇要讲的是当你手里有一批原始文本怎么把它变成能喂给预训练语言模型的数据集。不是调包跑个 demo而是从数据来源、清洗规则、去重策略、token 统计一路走到可复现的预处理流水线。适合两类人一是准备自己跑小规模预训练或继续预训练continue pretraining的工程师二是需要评估「这个数据集到底够不够、干不干净」的算法负责人。读完你应该能自己搭一条从原始文本到 tokenized shard 的完整链路并且知道每一步的坑在哪。2. 语料从哪来GPT 系列的数据源选型与配比逻辑2.1 为什么 WebText 的「质量过滤」比「规模」更值得抄GPT-2 的 WebText 没有公开完整数据集但论文里透露了构造思路从 Reddit 上抓取获得至少 3 个 karma 的外链页面。这个做法的本质不是「Reddit 数据好」而是用人类投票当了一个廉价的质量过滤器。karma 门槛筛掉的不是内容类型而是大量低质、垃圾、机器生成的页面。我一般会把这个思路抽象成三层过滤过滤层目的常见手段来源层控制初始质量分布白名单域名、社区投票、编辑审核文档层去掉明显垃圾长度阈值、符号比例、重复行检测内容层去掉有害/低信息困惑度过滤、分类器打分、去重来源层最省事但最不灵活内容层最灵活但成本最高。实际项目里来源层能砍掉 60% 以上的噪声文档层再砍 20%内容层只处理剩下的 20%。顺序反了你会把算力浪费在垃圾上。2.2 用 Python 搭一个最小可用的语料采集与初筛脚本下面这段代码做三件事从本地目录读取原始文本、按文档层规则初筛、输出统计报告。不依赖任何外部 API可以直接跑。import os import re import json from collections import Counter def basic_filter(text, min_len200, max_symbol_ratio0.3): 文档层初筛长度 符号比例 if len(text) min_len: return False, too_short # 统计非字母数字字符比例 symbol_count len(re.findall(r[^\w\s], text)) if symbol_count / max(len(text), 1) max_symbol_ratio: return False, too_many_symbols # 重复行检测如果某一行重复超过 5 次判为低质 lines [l.strip() for l in text.split(\n) if l.strip()] if lines: most_common Counter(lines).most_common(1)[0][1] if most_common 5: return False, repetitive_lines return True, ok def scan_corpus(root_dir): stats Counter() kept_docs [] for fname in os.listdir(root_dir): if not fname.endswith(.txt): continue path os.path.join(root_dir, fname) with open(path, r, encodingutf-8, errorsignore) as f: text f.read() ok, reason basic_filter(text) stats[reason] 1 if ok: kept_docs.append({file: fname, chars: len(text)}) return stats, kept_docs if __name__ __main__: stats, kept scan_corpus(./raw_texts) print(过滤统计:, dict(stats)) print(保留文档数:, len(kept)) total_chars sum(d[chars] for d in kept) print(总字符数:, total_chars) with open(filtered_index.json, w, encodingutf-8) as f: json.dump(kept, f, ensure_asciiFalse, indent2)逻辑说明basic_filter里三个判断分别对应长度不足、符号噪声过多、模板化重复内容。min_len200是经验值中文语料可以降到 100英文建议不低于 200。max_symbol_ratio0.3对代码类语料要放宽到 0.5否则会把正常代码全滤掉。repetitive_lines的阈值 5 是防止导航栏、页脚这类模板文本混入。参数怎么改如果你的语料是论坛帖子长度阈值降到 50如果是书籍提到 500。符号比例对中文可以设 0.2因为中文标点占比天然比英文低。这些数字没有标准答案跑一遍看统计分布再定。2.3 数据配比别让某一类语料主导整个预训练GPT-2 的 WebText 虽然来源单一但内容分布很广。如果你自己混合多个来源配比就是第二个关键决策。常见做法是通用网页文本占 60%70%书籍/长文占 15%20%代码占 5%10%问答/对话占 5%。这个比例不是死的取决于你的下游任务。我一般会先跑一个来源分布统计再决定要不要上采样或下采样。如果某个来源的 token 数超过总量 50%要么砍它要么补其他来源。单一来源主导会导致模型在那个领域的困惑度很低但换一个领域就崩。3. 清洗与去重把 40GB 压到 25GB 的实操路径3.1 去重的三个层级文档级、段落级、句子级去重是预训练数据预处理里最容易被低估的一步。Common Crawl 原始数据里重复内容能占到 30% 以上。不去重模型会在重复样本上过拟合浪费算力。三个层级的去重成本和收益完全不同文档级用 MinHash 或 SimHash速度快能去掉完全重复和近似重复的整篇文档。成本低收益高必做。段落级对每个段落做哈希去掉跨文档的重复段落。成本中等能进一步压缩 10%15%。句子级对句子做精确匹配或模糊匹配。成本高收益递减一般只在数据量特别大时才做。我的建议是文档级必做段落级看数据来源句子级除非你有明确证据表明重复严重否则先跳过。3.2 用 MinHash 做文档级去重的完整代码下面用datasketch库实现 MinHash LSH 去重。如果没有这个库pip install datasketch即可。from datasketch import MinHash, MinHashLSH import os def get_minhash(text, num_perm128): 对文本生成 MinHash 签名 m MinHash(num_permnum_perm) # 按空格切词中文可以先分词再传入 for token in set(text.split()): m.update(token.encode(utf-8)) return m def dedup_documents(docs, threshold0.8): docs: list of (doc_id, text) lsh MinHashLSH(thresholdthreshold, num_perm128) minhashes {} for doc_id, text in docs: m get_minhash(text) minhashes[doc_id] m lsh.insert(doc_id, m) keep [] removed set() for doc_id, text in docs: if doc_id in removed: continue keep.append(doc_id) # 查询相似文档并标记删除 candidates lsh.query(minhashes[doc_id]) for c in candidates: if c ! doc_id: removed.add(c) return keep, removed if __name__ __main__: docs [] for i, fname in enumerate(os.listdir(./raw_texts)): with open(f./raw_texts/{fname}, encodingutf-8, errorsignore) as f: docs.append((fdoc_{i}, f.read())) keep, removed dedup_documents(docs, threshold0.8) print(f保留 {len(keep)} 篇去除 {len(removed)} 篇)逻辑说明num_perm128是 MinHash 的排列数越大越精确但越慢。128 在大多数场景够用。threshold0.8表示 Jaccard 相似度超过 0.8 就判为重复。这个阈值对新闻类语料可以设 0.7对技术文档设 0.85。参数怎么改如果语料是短文本如推文num_perm可以降到 64因为短文本的 token 集合小高排列数收益不大。如果语料是长文档num_perm提到 256 更稳。threshold越低去重越激进可能误删不同但相似的文档建议从 0.85 开始往下调观察保留率。3.3 清洗规则表哪些字符该删哪些该留清洗不是把所有非字母数字都删掉。下面这张表是我在多个项目里总结的规则按「必删 / 可删 / 保留」分类类型处理原因HTML 标签必删无语言信息干扰 tokenizer连续空白合并为单空格减少无效 token控制字符必删编码噪声数学符号保留代码和学术语料需要中文标点保留语义边界表情符号视场景对话语料保留正式语料可删URL可替换为占位符保留结构信息但不让模型记具体链接提示清洗规则一旦确定要写成可配置的 YAML 或 JSON不要硬编码在脚本里。不同来源的语料需要不同规则硬编码会让你每次改规则都改代码。4. Tokenization 与分片从原始文本到训练可用的 shard4.1 选 tokenizerBPE、WordPiece 还是 SentencePiece预训练语言模型常用的 tokenizer 就三类BPEGPT 系列、WordPieceBERT 系列、SentencePiece多语言场景。如果你是从头训练选哪个取决于语料语言和词表大小。tokenizer适合场景词表大小经验值BPE英文为主代码3200050257WordPiece英文需要 subword 泛化3000050000SentencePiece中文、多语言、混合语料32000100000GPT-2 用的 BPE 词表是 50257。如果你做中文预训练SentencePiece 的 unigram 模式通常比 BPE 更稳因为中文没有天然空格分词BPE 的合并规则容易产生奇怪的分片。4.2 用 Hugging Face tokenizers 训练自己的 BPE 并编码下面代码用tokenizers库训练一个 BPE tokenizer然后把语料编码成 token id 序列。from tokenizers import Tokenizer, models, trainers, pre_tokenizers from tokenizers.processors import BertProcessing import os def train_bpe(corpus_files, vocab_size32000, save_pathbpe_tokenizer.json): tokenizer Tokenizer(models.BPE(unk_token[UNK])) tokenizer.pre_tokenizer pre_tokenizers.Whitespace() trainer trainers.BpeTrainer( vocab_sizevocab_size, special_tokens[[PAD], [UNK], [CLS], [SEP], [MASK]], min_frequency2 ) tokenizer.train(corpus_files, trainer) tokenizer.save(save_path) return tokenizer def encode_corpus(tokenizer, input_dir, output_path, max_len1024): 把语料编码成定长序列写入二进制文件 import numpy as np all_ids [] for fname in os.listdir(input_dir): if not fname.endswith(.txt): continue with open(os.path.join(input_dir, fname), encodingutf-8, errorsignore) as f: text f.read() encoded tokenizer.encode(text) ids encoded.ids # 按 max_len 切分 for i in range(0, len(ids), max_len): chunk ids[i:imax_len] if len(chunk) max_len: all_ids.append(chunk) arr np.array(all_ids, dtypenp.uint16) arr.tofile(output_path) print(f写入 {len(all_ids)} 个序列总 token 数 {arr.size}) return arr if __name__ __main__: files [f./raw_texts/{f} for f in os.listdir(./raw_texts) if f.endswith(.txt)] tok train_bpe(files, vocab_size32000) encode_corpus(tok, ./raw_texts, ./tokenized.bin, max_len1024)逻辑说明BpeTrainer的min_frequency2表示出现少于 2 次的 token 不进入词表防止词表被罕见词撑大。max_len1024是序列长度GPT-2 用的是 1024你可以根据显存调整到 512 或 2048。np.uint16能表示 065535够 32000 词表用比 int32 省一半空间。参数怎么改vocab_size对中文建议 50000 以上因为汉字组合多。min_frequency对大规模语料可以设 5 或 10进一步压缩词表。max_len要和模型的位置编码匹配不能超过模型支持的最大长度。4.3 分片存储为什么不要把所有 token 塞进一个文件上面代码把所有序列写进一个.bin文件这在数据量小的时候没问题。但如果你有几十 GB token单文件会导致读取时无法并行、内存映射困难、训练中断后难以恢复。常见做法是分片成多个文件每个文件 1GB2GB训练时按 shard 顺序读取。分片命名建议用shard_00001.bin这种零填充格式方便排序。每个 shard 配一个.idx文件记录序列偏移量训练时先读 idx 再随机采样。这套做法在 Megatron-LM 和 GPT-NeoX 的数据加载里都能看到影子。5. 避坑与排查预训练数据预处理里最容易翻车的 5 个点5.1 去重后训练集和验证集出现重叠现象验证集 loss 异常低但下游任务表现差。原因去重只在训练集内部做没有跨训练集和验证集去重。解决把验证集也纳入 MinHash 索引或者在划分数据集之前先做全局去重再切分。5.2 Tokenizer 训练时内存爆掉现象tokenizer.train()跑到一半 OOM。原因tokenizers库默认把整个语料加载进内存做频率统计。解决用train_from_iterator传入生成器分批读取文件不要一次性传文件列表。5.3 编码后的 token 数远少于预期现象原始文本 10GB编码后只有 2GB token。原因清洗阶段删太多或者 tokenizer 的min_frequency设太高导致大量 token 变成[UNK]。解决先跑一遍清洗前后的字符数对比再检查[UNK]比例超过 1% 就要降低min_frequency或换 tokenizer。5.4 分片文件读取顺序错乱现象训练时 loss 震荡剧烈。原因shard 文件名没有零填充shard_1.bin排在shard_10.bin后面导致数据顺序错乱。解决统一用shard_{:05d}.bin格式命名读取时按文件名排序。5.5 多来源语料配比在编码后失真现象明明按 6:2:2 配比混合训练时发现某来源占比超过 50%。原因不同来源的文本长度差异大按文档数配比不等于按 token 数配比。解决配比要在 token 级别做先统计每个来源的 token 总数再按目标比例采样。注意这五个坑里去重和配比是最容易被忽略的。很多人把精力花在 tokenizer 调参上结果数据本身就有问题调什么参数都救不回来。6. 用 1B token 规模验证你的流水线一个可复现的检查清单如果你不想一上来就搞几十 GB可以先跑一个 1B token 的小规模验证。1B token 大约对应 2GB4GB 原始英文文本或者 1.5GB2GB 中文文本。这个规模足够暴露流水线里的绝大多数问题成本又可控。具体做法从你的完整语料里随机采样 1B token 对应的原始文本走一遍完整流程——初筛、去重、tokenizer 训练、编码、分片。然后检查下面这张表检查项健康范围异常时看什么去重保留率60%85%低于 60% 说明来源重复严重UNK 比例 0.5%高于 1% 要扩词表或降 min_frequency平均序列长度接近 max_len 的 80%太低说明文档太短或切分太碎各来源 token 占比与目标配比偏差 5%偏差大说明配比没在 token 级做分片数每片 1GB2GB太多或太少都影响加载效率跑完这张表你基本能判断流水线能不能上大规模。如果 1B token 跑通且各项指标正常放大到 10B 或 40B 只是时间问题。我自己的习惯是每次改清洗规则或 tokenizer 参数都重新跑一遍 1B token 验证对比前后指标。这个习惯帮我省了很多次「上线后才发现数据有问题」的后悔药。预训练数据这活儿玄学不多血泪经验多——大部分翻车都是因为跳过了验证步骤。希望帮到你。本文还有配套的精品资源点击获取