预训练数据流水线


文档摘要

预训练数据流水线 本节摘要:模型是一面镜子,你喂什么它就照什么——喂垃圾,它就用完美的流畅度照出垃圾。大多数人以为训练 LLM 靠架构,其实靠数据。Llama 3 用了 15.6 万亿 token,GPT-3 用 3000 亿,DeepSeek-V2 用 8.1 万亿——三者架构大同小异,输出质量的天壤之别几乎全来自数据。本节带你搭一条流式数据流水线:清洗(去 HTML、语言检测、质量过滤)、MinHash 近似去重、序列打包、按批随机化,快到 8 卡集群永远不等下一批。你会理解 Chinchilla 缩放定律为什么说「大多数模型欠训练」,以及为什么 Llama 3 故意违背它。

预训练数据流水线

本节摘要:模型是一面镜子,你喂什么它就照什么——喂垃圾,它就用完美的流畅度照出垃圾。大多数人以为训练 LLM 靠架构,其实靠数据。Llama 3 用了 15.6 万亿 token,GPT-3 用 3000 亿,DeepSeek-V2 用 8.1 万亿——三者架构大同小异,输出质量的天壤之别几乎全来自数据。本节带你搭一条流式数据流水线:清洗(去 HTML、语言检测、质量过滤)、MinHash 近似去重、序列打包、按批随机化,快到 8 卡集群永远不等下一批。你会理解 Chinchilla 缩放定律为什么说「大多数模型欠训练」,以及为什么 Llama 3 故意违背它。

学习目标

阅读完本节,你应当能够:

  1. 构建流式数据流水线,在不全部载入内存的情况下对 TB 级文本完成分词、切块、洗牌、成批。
  2. 实现真实预训练流水线中的数据质量过滤(去重、语言检测、内容过滤)。
  3. 用正确的注意力掩码与文档边界处理,创建定长训练序列。
  4. 对流水线吞吐做性能分析,确保数据加载跟得上 GPU 训练速度。

一、问题与直觉

你有了分词器。现在需要数据。

不是一个数据集,不是一个 CSV,而是数 TB 文本——已清洗、已去重、已按质量过滤、已分词成定长序列、按随机批次供给,快到 8 卡集群永远不等下一批。

多数人以为训练 LLM 是模型架构的事。其实不是。Llama 3 用 15.6 万亿 token,GPT-3 用 3000 亿,DeepSeek-V2 用 8.1 万亿——三者的架构大致相同:堆叠的 Transformer 块加注意力和前馈层。输出质量的差异压倒性地来自数据。DeepMind 的 Chinchilla 论文把这说得很精确:给定算力预算,模型参数与训练 token 有一个最优比率。Chinchilla 表明 2022 年大多数模型都严重欠训练——参数太多、数据太少。一个 70B 模型在 1.4 万亿 token 上训练(Chinchilla 最优)能打败一个 280B 模型在 3000 亿 token 上训练(Gopher)。

你的数据流水线,决定了模型学的是语言还是学的是噪声。

数据从哪来

每个大语言模型都在多种来源的混合上训练。确切配比是各实验室严守的秘密,但类别我们清楚。

来源 体量 质量 使用者
Common Crawl 原始约 250 TB 低(需重度过滤) GPT-3、Llama、多数开源模型
维基百科 约 20 GB 每个主流 LLM
GitHub 代码 1 TB+ 中(大量重复、死代码) StarCoder、CodeLlama、DeepSeek-Coder
书籍(BookCorpus、Pile) 约 100 GB GPT-2、GPT-3、早期模型
学术论文(arXiv、S2ORC) 约 100 GB STEM 高 Llama、Galactica
StackOverflow、Reddit 约 100 GB Llama、Falcon
精选网页(C4、RefinedWeb) 约 5 TB 中高(已预过滤) T5、Falcon

Llama 3 公开了数据配比:约 50% 网页、25% 代码、13% 书籍与论文、8% 数学、4% 多语言网页。配比与总量同等重要——网页太多,模型变成 Reddit 鹦鹉;代码太少,不会编程;数学太少,推理不行。调好这个混合是训练 LLM 最难的环节之一,没有公式,只能靠实验和评估。

数据清洗

原始网页数据极脏。一份典型 Common Crawl 转储含:HTML 标签与 JavaScript、样板页眉页脚导航、重复页(精确与近似)、机器生成垃圾、个人身份信息(PII)、低质文本(关键词列表、SEO 垃圾)、编码成文本的非文本内容。清洗不是可选项,它是「生成连贯段落」与「输出混杂 HTML 与商品列表」之间的分水岭。

  • 去 HTML:移除所有标记,只留可见文本。trafilaturareadability 等库可抽取正文、丢弃导航广告样板。
  • 语言检测:用 fastText 的语言识别模型(lid.176.bin)分类每篇文档,过滤到目标语言。置信度低于 0.8 的「英语」文档大概率不干净。
  • 质量过滤:这是有趣之处。RefinedWeb(Falcon 背后的数据集)用困惑度过滤——在维基百科上训一个小语言模型,给每篇文档打分。困惑度高意味着这篇文档不像维基,可能是垃圾、关键词列表或机器生成;超过阈值的移除。
  • 去重:影响最大的单一清洗步骤。Common Crawl 含海量重复页——法律免责声明、Cookie 通告、服务条款。在重复数据上训练浪费算力,还会让模型逐字记忆特定段落。
  • 去 PII:姓名、邮箱、电话、身份证号。结构化 PII 用正则,上下文中的姓名用 NER 模型。

用 MinHash 去重

精确去重很简单:给每篇文档算哈希,删重复。但真正麻烦的是近似重复。同一篇新闻的两份拷贝,周围广告略不同,内容 95% 相同,逐字节却不同。MinHash + 局部敏感哈希(LSH)能高效解决。

思路:(1) Shingling——把文档转成 n-gram 集合(如 5-gram);(2) MinHash——对每个 shingle 集合算 k 个哈希值,每个哈希取所有 shingle 的最小值,得到固定大小「签名」,近似两文档间的 Jaccard 相似度;(3) LSH——按 MinHash 签名的「带」分桶,同桶文档是近似重复候选,避免了 O(n²) 的两两比较;(4) 验证——对每对候选算精确 Jaccard,超阈值(通常 0.8)就删一份。Llama 团队报告通过去重移除了约 38% 的网页数据——Common Crawl 超过三分之一是重复或近似重复。

序列打包

模型期望定长输入序列,文档却是变长的——有的 50 token,有的 5 万。朴素做法:每篇填到最大长度,在填充 token 上浪费巨量算力。更好做法:把多篇文档用 [EOS] 分隔,拼进同一条序列。一条 2048-token 序列可能含三篇短文档拼接。注意力掩码必须正确设置:文档 A 的 token 不该注意到同序列里文档 B 的 token,需要块对角注意力掩码。长文档在序列边界处截断或切块,切点尽量对齐段落或句子边界。

Chinchilla 缩放定律

固定算力预算 C(以 FLOPs 计),最优模型大小 N 与数据大小 D 满足:N_opt ~ C^0.5D_opt ~ C^0.5。即模型与数据要大致等比放大——参数多 10 倍的模型,需要约多 10 倍的训练 token 才能到同等损失。

模型 参数 训练 token Chinchilla 最优?
GPT-3 175B 300B 否(欠训练 3~4 倍)
Chinchilla 70B 1.4T 是(设计如此)
Llama 2 70B 2T 过训练(有意)
Llama 3 70B 15T 严重过训练

💡 关键概念:Llama 3 故意违背 Chinchilla——Meta 发现,用更多数据过训练,能产出更适合推理的模型。额外训练成本只付一次,但更小的模型永久更省服务成本。这被称为「推理最优」缩放,自 2024 年起成为业界标准。

二、从零实现

Step 1:文本清洗

去 HTML、规整空白、移除非文本内容。用公有领域文本(古腾堡计划)做小语料。

import re def clean_text(text): text = re.sub(r"<[^>]+>", "", text) # 去 HTML 标签 text = re.sub(r"http\S+", "", text) # 去 URL text = re.sub(r"[^\x20-\x7E\n]", "", text) # 只留可打印 ASCII text = re.sub(r"\n{3,}", "\n\n", text) # 压多余空行 text = re.sub(r" {2,}", " ", text) # 压多余空格 return text.strip() def quality_filter(text, min_words=50, max_ratio_caps=0.3, max_ratio_special=0.1): words = text.split() if len(words) < min_words: return False # 太短(存根页) if sum(w.isupper() for w in words)/len(words) > max_ratio_caps: return False # 全大写(SEO) special = sum(1 for c in text if not c.isalnum() and not c.isspace()) if special/max(len(text),1) > max_ratio_special: return False # 特殊字符多(机生噪声) return True

三个检查就能移除惊人数量的网页垃圾。

Step 2:MinHash 去重

只用 hashlib,从零实现 MinHash。

import hashlib def get_shingles(text, k=5): words = text.lower().split() return {" ".join(words[i:i+k]) for i in range(len(words)-k+1)} if len(words)>=k else set() def minhash_signature(shingles, num_hashes=128): sig = [] for i in range(num_hashes): mn = min(int(hashlib.sha256(f"{i}:{s}".encode()).hexdigest(),16) for s in shingles) if shingles else 0 sig.append(mn) return sig def lsh_buckets(sig, bands=16): rpb = len(sig)//bands return [(b, hashlib.md5(str(sig[b*rpb:b*rpb+rpb]).encode()).hexdigest()) for b in range(bands)]

num_hashes=128bands=16 控制精确率与召回率的权衡:哈希越多估计越准,带越多召回越高但误报越多。同桶的候选对再算精确 Jaccard,超 0.8 阈值删一份。

Step 3:分词与序列打包

def tokenize_corpus(docs, tokenizer): ids = [] for d in docs: ids.extend(tokenizer.encode(d)); ids.append(tokenizer.eos_id) return ids def pack_sequences(token_ids, seq_len, pad_id=0): seqs, masks = [], [] for i in range(0, len(token_ids), seq_len): s = token_ids[i:i+seq_len]; m = [1]*len(s) if len(s) < seq_len: s += [pad_id]*(seq_len-len(s)); m += [0]*(seq_len-len(s)) seqs.append(s); masks.append(m) return seqs, masks

Step 4:训练用 DataLoader

产出随机化批次的打包序列,供训练循环消费。

import random class PreTrainingDataLoader: def __init__(self, sequences, masks, batch_size, shuffle=True): self.sequences, self.masks, self.batch_size, self.shuffle = sequences, masks, batch_size, shuffle def __iter__(self): idx = list(range(len(self.sequences))) if self.shuffle: random.shuffle(idx) for s in range(0, len(idx), self.batch_size): b = idx[s:s+self.batch_size] yield [self.sequences[i] for i in b], [self.masks[i] for i in b]

Step 5:数据集统计

算关键数字:总 token、唯一 token、压缩比、文档长度分布、序列利用率。压缩比告诉你分词器在本语料上多有效——英文通常压到约每 token 3~4 字符。序列利用率告诉你打包序列里多少是真数据而非填充——低于 90% 说明打包低效。

三、框架对比

对比 HuggingFace Datasets

from datasets import load_dataset from transformers import AutoTokenizer import time ds = load_dataset("wikitext", "wikitext-2-raw-v1", split="train") tok = AutoTokenizer.from_pretrained("meta-llama/Meta-Llama-3-8B") t0 = time.time() tok_ds = ds.map(lambda x: tok(x["text"], truncation=True, max_length=2048), batched=True, num_proc=4) print(f"{sum(len(t) for t in tok_ds['input_ids']):,} tokens in {time.time()-t0:.2f}s")

HuggingFace 底层用 Rust 分词器、4 核并行。你的纯 Python 流水线慢 10~50 倍。这就是生产团队用编译分词器的原因——算法相同,实现语言是差距。

四、可复用产物

本节产出 outputs/prompt-data-quality-checker.md——一个用于校验与调试 LLM 训练流水线数据质量的提示。Python 代码是独立工具,接口稳定。

五、练习

  1. (Easy) 给清洗流水线加语言检测(用字符集分析的简单启发式),只留英文文档,统计移除了多少篇。

  2. (Medium) 在 MinHash 近似去重旁再加 SHA-256 精确去重,对比两种方法在网页语料上各抓出多少重复。

  3. (Hard) 构建困惑度质量过滤器:在维基文本上训一个小二元语言模型,给每篇文档打困惑度,移除最差的 20%,对比过滤与未过滤数据上的模型输出质量。

本节要点回顾

  1. 输出质量压倒性来自数据:Llama 3(15.6T token)、GPT-3(300B)、DeepSeek-V2(8.1T)架构大同小异,差距在数据。
  2. Chinchilla 最优 = 参数与数据等比:多数 2022 年模型欠训练;70B 配 1.4T token 能打败 280B 配 300B。
  3. Llama 3 故意过训练:更多数据换来更适合推理的小模型,「推理最优」缩放成业界标准。
  4. 清洗五步:去 HTML、语言检测、质量过滤(困惑度)、去重(影响最大)、去 PII。
  5. MinHash+LSH 高效去近似重:用固定大小签名近似 Jaccard 相似度,把 O(n²) 比较降到近线性;Llama 去掉约 38% 网页数据。
  6. 序列打包省填充:多篇文档用 [EOS] 拼进定长序列,块对角注意力掩码防止跨文档注意。
  7. 数据配比无公式:网页/代码/数学/多语言的比例靠实验与评估,调好比调大难。
  8. 困惑度过滤器抓垃圾:小模型在干净参考上训,给文档打分,高分=不像干净数据=移除。
  9. 注意力掩码标边界:打包序列里,token 只该注意到同文档 token。
  10. 生产用编译分词器:Rust/C++ 比纯 Python 快 10~50 倍;算法相同,语言是差距。

下一节,我们用上一章的分词器与本节的数据,从零训练一个 124M 参数的迷你 GPT——每一行 numpy 都看得见。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U