预训练数据的“脏”与“净”:为什么数据清洗、去重与配比决定了模型上限
如果你是做 LLM 应用开发或正在研究大模型预训练,大概率已经听过一句话:数据决定了模型的上限,模型架构和训练技巧只是在逼近这个上限。这句话在学术界和工业界几乎已经变成共识。这次我们来看斯坦福大模型开发课 EP14 的内容,核心就三个词:数据清洗、去重、配比策略。它们不是可有可无的预处理步骤,而是真正决定模型知识容量、泛化能力和训练稳定性的关键环节。
很多初学者下载到开源语料后,第一反应是直接跑分词和训练脚本,结果训练到一半 Loss 抖动、模型输出全是重复文本,或者跑完一个阶段后困惑度死活下不来。问题往往不在模型结构,而在喂进去的数据太脏。这篇内容会把预训练数据工程的完整链路拆开讲清楚,包括数据从采集到筛选、清洗、去重、配比评估的每一步,以及每一步有哪些工程上可行的做法。
从材料看,这门课聚焦的是高质量预训练数据,所以文章会围绕三个核心环节展开:数据清洗解决语料质量问题,去重解决信息冗余问题,配比策略解决知识结构失衡问题。三件事互相独立,但又彼此影响。比如过度去重会丢失低频但关键的领域知识,而如果配比没调好,再怎么清洗也无法提升特定能力。
文章整体偏工程实践向,适合正在准备预训练数据集、复现开源模型训练流程,或者打算用开源语料做领域微调的同学阅读。接下来,从数据工程的整体流程开始拆解。
1. 预训练数据工程核心能力速览
| 能力模块 | 主要内容 | 关键方法/工具思路 | 对模型的影响 |
|---|---|---|---|
| 数据采集 | 获取原始语料 | 爬虫、开源数据集、OCR、语音转写 | 决定数据的上限和覆盖度 |
| 语言过滤 | 去除低质量语言 | FastText 语言识别、启发式规则 | 减少无效 token,提升有效数据密度 |
| 质量清洗 | 去 HTML、去广告、去噪声 | 规则引擎、分类器评分、困惑度过滤 | 降低训练噪声,提升 Loss 收敛效率 |
| 数据去重 | 消除重复和近似重复文本 | 精确去重、MinHash、SimHash 模糊去重 | 防止训练重复、降低记忆过拟合 |
| 隐私/安全过滤 | 移除个人信息和有害内容 | PII 检测、敏感词表、人工抽检 | 合规红线,影响模型安全 |
| 数据配比 | 调整各来源/领域数据比例 | 自然分布、过采样、课程学习 | 控制模型的知识结构和能力偏向 |
| 质量评估 | 验证数据是否可训练 | 小规模训练、Loss 观测、下游任务验证 | 提前发现数据问题,避免浪费算力 |
从这张表可以看到,数据工程不是某一个步骤,而是一条完整的流水线。很多开源项目,比如 RedPajama、RefinedWeb,核心工作就是把这套流程流水线化。斯坦福课程 EP14 的重点,就是讲清楚这条流水线里每一步为什么存在、怎么做、踩过哪些坑。
2. 为什么数据清洗是预训练的第一步
2.1 原始语料到底有多脏
互联网爬取语料看起来体量巨大,但实际质量参差不齐。以 Common Crawl 为例,里面包含大量 HTML 标签残留、重复导航栏、广告文本、乱码字符串、机器生成的垃圾内容甚至恶意注入文本。如果直接把这些数据送进 tokenizer,结果就是模型学到大量无意义的字符模式,训练效率极低。
从课程里的思路来看,数据清洗要解决的是去掉模型不需要学习的内容,而不是去掉所有看起来不像自然语言的内容。比如代码语料中本来就有特殊符号、Markdown 标记、JSON 结构,这些对代码模型反而是有效特征。所以清洗规则必须和数据来源强相关,不能一套规则打天下。
2.2 清洗的典型层级
数据清洗通常分三个层级:
第一层:格式清洗这一步处理最基础的格式问题,包括去 HTML 标签、统一编码为 UTF-8、转换全角半角、清理控制字符、截断过长文本、合并断行等。PySpark 或 pandas 都能做这类批处理,但工程上通常用 PySpark 做分布式处理,因为预训练语料动辄几十 TB,单机处理不现实。
# 简单示例:用 Python 清洗单条文本的 HTML 标签 import re def clean_html(text): # 去掉 script 和 style 标签块 text = re.sub(r'<script.*?</script>', '', text, flags=re.S) text = re.sub(r'<style.*?</style>', '', text, flags=re.S) # 去掉其余 HTML 标签 text = re.sub(r'<[^>]+>', '', text) # 还原常见 HTML 实体 text = text.replace('&', '&').replace('<', '<').replace('>', '>') return text.strip() sample = "<html><body><p>Hello <b>World</b></p></body></html>" print(clean_html(sample)) # 输出: Hello World第二层:语言与内容过滤用 FastText 语言分类模型识别语言,过滤掉非目标语言的文本。内容过滤则针对色情、暴力、赌博、政治敏感等不合规内容,通常用关键词黑名单和分类模型双重过滤。这一层同时也是合规的关键节点。
第三层:质量评分过滤这是清洗的核心升级。很多高质量数据集方案采用一个分类器或质量评分模型,对文本质量进行打分。比如 RefinedWeb 用 Perplexity(困惑度)作为质量指标:困惑度太低的文本往往是重复的垃圾内容,困惑度太高的文本往往是随机噪声,取中间区间的文本作为高质量语料。
课程里强调的一个重要思想是:清洗目标不是“追求最干净”,而是“保留可学习的信息”。过度清洗会删除代码、数学公式、非规范表达等内容,而这些恰恰是让模型具备多样性和泛化能力的东西。正确的做法是分层清洗,对不同来源数据设置不同的规则强度。
2.3 操作层面的清洗流程
实际搭建清洗 Pipeline 时,可以按照下面的顺序操作:
- 解析原始文件格式(WARC、JSONL、Parquet)。
- 提取正文内容,去掉导航、页脚、广告等噪声区。
- 统一字符编码,清理控制字符。
- 语言识别,筛选目标语言文本。
- 规则过滤,去除过短、过长、重复字符比例过高的样本。
- 质量分类器评分,按阈值筛选。
- 输出清洗后数据,按来源和主题分区存储。
# 示例:使用 pyarrow 读取 parquet 格式的语料文件 python -c " import pyarrow.parquet as pq table = pq.read_table('path/to/corpus.parquet') df = table.to_pandas() print(df.shape) print(df.columns) "如果数据条数非常多,建议对清洗前的数据进行抽样检查,先人工查看 100 到 200 条样本,确认噪声类型,再制定清洗规则。这一步能省下大量重复调试时间。
3. 预训练数据去重:精确去重与模糊去重
3.1 为什么要去重
重复数据对预训练的影响常常被低估。一个直观的问题:如果语料中同一个网页出现 10 次,模型会花同样的算力反复学习这段内容,但学习到的信息增量是零。更严重的是,重复数据会导致模型过拟合到某些特定表达方式,降低泛化能力,同时推高训练困惑度。
从另一个角度看,大语言模型的记忆能力很强,训练集中重复出现的样本会被“背下来”。这在生成场景中表现为回答内容大量复述训练语料的原文,而不是理解后组织语言。因此,去重不仅是节省算力,也是在控制模型的记忆与泛化平衡。
3.2 精确去重 vs 模糊去重
精确去重精确重复是指完全相同或只差少量字符的文本。工程上最简单的做法是对文本内容做哈希(比如 MD5、SHA256),然后比较哈希值。但精确哈希对任何一点字符变化都非常敏感,比如正文里多了一个空格或者换行符就判断为不同样本。
更实用的做法是先对文本做归一化,比如去掉所有空白字符、统一大小写、去除标点符号,再做哈希。这样可以把“内容相同但格式不同”的文本识别为重复。
import hashlib def get_dedup_hash(text: str) -> str: normalized = ''.join(text.split()).lower() return hashlib.sha256(normalized.encode('utf-8')).hexdigest()模糊去重互联网语料中,更多重复是近似重复:同一篇新闻被不同网站转载,标题格式不同,正文中加了网站前缀或版权声明。这种情况哈希方法无能为力,需要用到模糊去重算法。
课程里提到的方法是 MinHash(最小哈希)配合 LSH(局部敏感哈希)。核心思路是:把文本切分成 n-gram 集合,对每个 n-gram 做哈希,取集合中最小的几个哈希值作为文本的“指纹”。两篇文本的指纹重叠比例越高,越可能近似重复。
# MinHash 思路演示:生成文本的 n-gram 哈希集合 def shingles(text: str, k: int = 5): tokens = text.split() for i in range(len(tokens) - k + 1): yield ' '.join(tokens[i:i+k]) def minhash_fingerprint(text: str, num_perm: int = 128): import hashlib hashes = [] for shingle in shingles(text): h = int(hashlib.md5(shingle.encode('utf-8')).hexdigest(), 16) hashes.append(h) if not hashes: return [] return sorted(hashes)[:num_perm]这里要注意,上面的代码只是演示算法思路。真实的大规模场景下,需要用到 datasketch 库的 MinHashLSH,或者 Spark 的近似去重实现,而不是自行实现全量哈希。课程强调的重点是理解 MinHash 的相似度语义,而不是重复造轮子。
3.3 去重层级:文档级、段落级、句子级
去重不只是文档层面的操作。课程里区分了三个层级的去重:
- 文档级去重:处理完全重复或近似重复的网页/文档。
- 段落级去重:有些文档内容不重复,但其中某一段落来自重复来源,这时候需要在段落粒度做去重,防止同一段文字在大量文档中以不同上下文出现。
- 句子级去重:用于处理固定表达的泛滥,比如大量网页都有“欢迎关注我们的公众号”这类句子。这类句子对训练无益,优先剔除。
实际工程中可以先用精确去重做一遍粗筛,再用 MinHash 做模糊去重,最后在段落级和句子级做细致清理。
3.4 去重的度:不是越干净越好
课程里特别提醒一个问题:去重力度太强会损害模型的领域知识覆盖。比如某些长尾知识只在极少数页面中出现,如果和类似文本一起被误判为重复,就会被删除。这会让模型在特定领域的能力进一步下降。
更稳妥的做法是:对于通用文本,采用严格的去重策略;对于领域专业文本(比如医学、法律、数学),采用宽松的去重阈值,优先保留多样性。去重阈值通常是基于采样实验确定的,而不是拍脑袋定一个相似度数字。
4. 数据清洗与去重的工程实现:从 HuggingFace 到分布式流水线
4.1 用 HuggingFace Datasets 做中等规模清洗
对于中等规模的数据集(百 GB 级别),直接用 HuggingFace 的 datasets 库就可以完成大多数清洗和去重操作。它的 map 操作支持多进程处理,简单易用。
from datasets import load_dataset dataset = load_dataset('json', data_files='raw_data.jsonl', split='train') # 定义清洗函数 def clean_example(example): text = clean_html(example['text']) # 过滤过短文本 if len(text.split()) < 20: return {'text': None} return {'text': text} # 过滤无效数据 dataset = dataset.map(clean_example) dataset = dataset.filter(lambda x: x['text'] is not None) # 精确去重 def dedup_hash(example): example['hash'] = get_dedup_hash(example['text']) return example dataset = dataset.map(dedup_hash) dataset = dataset.unique('hash') # 这一步会触发全量哈希比较 dataset.save_to_disk('cleaned_data')这只是单机可运行的最小流程。真正需要优化的点是 map 函数的 num_proc 参数和内存管理,避免一次加载过多数据。
4.2 大规模场景下的分布式处理思路
当数据规模到 TB 级别,单机方案就失效了。课程推荐用 Spark 或者 Ray 做分布式数据管道。Spark 的 DataFrame 天然支持大规模数据操作,配合 minhash 库可以搭建分布式去重 Pipeline。
# 用 Spark 做语言过滤的伪代码示例 from pyspark.sql import SparkSession from pyspark.sql.functions import udf from pyspark.sql.types import StringType spark = SparkSession.builder.appName("data_cleaning").getOrCreate() df = spark.read.json("s3://raw_data/") @udf(StringType()) def language_filter(text): # 这里调用 fasttext 模型 lang = detect_language(text) return text if lang == "zh" else None cleaned = df.withColumn("text", language_filter(df["text"])) cleaned.write.parquet("s3://cleaned_data/")Spark 方案的重点不是代码本身,而是资源规划和任务调度。几十 TB 数据的 Full Scan 即使分布式也要数小时甚至数天。务必要先做数据采样验证,确认清洗规则不会误删关键内容,再全量运行。
5. 预训练数据配比策略:自然分布还是人为干预
5.1 为什么不能“有多少喂多少”
很多人会问:语料各来源按原始体量直接混合不就行了吗?课程给的答案是:绝对不行。原因有三个。
第一,不同来源的数据质量差异极大。维基百科质量高但体量小,Common Crawl 质量参差但体量大。如果按自然分布混合,低质量网页会在训练语料中占据绝对主导,模型学到的语言风格会更偏向噪音较多的低质量内容。
第二,数据分布和任务需求不完全一致。如果目标是做一个通用助手,需要更多对话数据和指令数据;如果目标是代码模型,那么通用网页语料再大,也不如代码语料重要。自然分布不会考虑你的模型定位。
第三,过度强调某类数据会导致灾难性遗忘或能力偏移。一个典型例子是,如果训练语料中英文占比超过 95%,中文能力会严重受损。所以需要人为调整配比,保证多语言、多领域的相对平衡。
5.2 课程中的配比思路:奖赏函数与重复采样
配比策略的本质是一个优化问题:在训练 token 预算固定的情况下,如何分配各领域数据,让模型在目标能力上表现最优。常见的做法是用“奖赏函数”评估某个领域的数据在训练后对模型能力的提升幅度,然后按奖赏高低动态调整权重。
具体到实操层面,比常见的策略是:
过采样与降采样对于高质量但量少的数据,比如维基百科、书籍、代码,可以设置较高的采样权重,让它们在训练轮次中出现多次。对于低质量的网页数据,可以降低采样权重,或者在每轮训练中只抽样部分数据。
课程学习(Curriculum Learning)训练前期多喂通用高质量语料,让模型建立基础语言能力;后期逐渐增加领域语料和复杂文本,强化专业知识。这种配比策略和训练阶段绑定,效果好于静态配比。
动态配比根据训练中某个领域数据的 Loss 变化动态调整采样权重。如果某个领域 Loss 下降很快,说明模型已经掌握得差不多,可以降低该领域权重;如果 Loss 一直居高不下,说明数据难度过高,需要增加该领域数据或做数据增强。
5.3 实际配比参考维度
在工程实践中,配比调整通常从以下几个维度切入:
| 维度 | 考虑因素 | 常见做法 |
|---|---|---|
| 语言 | 目标用户使用的主要语言 | 中文模型提高中文数据权重,英文保持基础比例 |
| 来源 | 网页、书籍、论文、代码、对话 | 书籍和论文权重高于普通网页 |
| 领域 | 通用知识、医学、法律、数学、代码 | 根据产品定位增加领域数据 |
| 质量 | 高质量精选 vs 低质量网页 | 高质量数据可重复采样多次 |
| 长度 | 短文本 vs 长文本 | 保证模型能够处理长上下文需要提高长文本比例 |
| 时间 | 新数据 vs 旧数据 | 近期数据比例提高,降低知识过时影响 |
配比不是一次定死,而是需要多轮训练实验验证。如果资源有限,可以从记录 Baseline 开始:固定模型结构不变,只调整数据配比,对比不同配比下的下游任务表现。这是成本最低的验证方式。
6. 数据质量评估:训练前怎么知道数据能不能用
数据清洗、去重、配比都做完后,绝对不能直接启动大规模训练。课程里反复强调Data-Centric Evaluation,即在正式训练前对数据质量做量化评估。主要手段包括:
6.1 困惑度过滤器
困惑度是衡量一段文本在语言模型下“意外程度”的指标。用已经训练好的小型语言模型计算每段文本的困惑度,可以发现异常文本。困惑度极低的文本往往是重复套话,困惑度极高的文本往往是乱码或非自然语言。
# 示例:用 GPT-2 计算文本困惑度 from transformers import GPT2LMHeadModel, GPT2Tokenizer import torch import math model_name = "gpt2" tokenizer = GPT2Tokenizer.from_pretrained(model_name) model = GPT2LMHeadModel.from_pretrained(model_name) model.eval() def compute_perplexity(text: str) -> float: inputs = tokenizer(text, return_tensors="pt") with torch.no_grad(): outputs = model(**inputs, labels=inputs["input_ids"]) loss = outputs.loss return math.exp(loss.item()) text = "这是一个用于测试困惑度的文本。" print(f"Perplexity: {compute_perplexity(text):.2f}")实际项目中,可以拿一小部分模型输出和人工判断做对比,确定一个合理的困惑度阈值范围,再应用到全量数据。
6.2 重复率 Statistic
在去重之后,统计 n-gram 重复率。如果数据集内部重复率依然偏高,说明去重步骤没有生效。一个简单指标是“重复 token 比例”,即训练语料中多少个 token 在前面已经出现过。重复率过高时,模型容易学会复读机行为。
6.3 小型代理训练实验
最可靠的评估方式还是小规模训练。取清洗后数据的 1% 到 5%,跑一个小模型(比如 100M 到 500M 参数),观察训练 Loss 曲线形态。正常情况下,训练 Loss 应平滑下降,验证集困惑度同步下降。如果 Loss 出现周期性尖峰或者验证集上快速过拟合,说明数据存在明显问题。
# 小规模训练验证命令示例(需要按实际框架调整) python train.py \ --model_config small \ --data_path ./cleaned_data_sample \ --max_steps 5000 \ --save_steps 1000 \ --logging_steps 100这个代理模型不需要训练完全收敛,跑几千步就够了。重点观察前 1000 步的 Loss 走势,如果连小模型都无法稳定收敛,后面的全量训练大概率也会出问题。
7. 数据 Pipeline 的模块化设计
课程里涉及的清洗、去重、配比流程,在实际项目中不是一次性脚本,而是长期运行的流水线。最常见的工程架构是:
原始语料 -> Raw Storage -> 解析与格式清洗 -> 语言过滤 -> 质量评分过滤 -> 精确去重 -> 模糊去重 -> 隐私过滤与人工抽检 -> 配比采样器 -> 训练数据包(按 shard 存储)每一层都是独立的模块,可以单独配置、单独重跑。这样做的原因是,数据质量问题是动态的:新增一批语料后,可能只需要重新跑质量过滤和去重,不需要把全流程从头再执行一遍。
实际项目里,建议用 DVC(Data Version Control)或类似工具管理数据管线的版本。每调整一个清洗规则或去重阈值,就记录一次数据版本。训练复现时,确保模型训练代码和数据处理代码版本锁定一致,否则结果很难追溯。
8. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 训练 Loss 不下降 | 数据噪声过大或数据量不足 | 抽样检查清洗后数据,查看 Loss 曲线 | 加强清洗规则,补充高质量语料 |
| Loss 周期性尖峰 | 某些 batch 包含异常文本 | 定位尖峰对应数据批次 | 加强异常检测过滤,调整 batch 大小 |
| 模型输出重复文本 | 语料去重不彻底 | 统计数据 n-gram 重复率 | 增加模糊去重步骤,调整阈值 |
| 模型常见语言偏向严重 | 配比不合理 | 统计语料语言分布 | 调整多语言采样权重 |
| 困惑度评估异常高 | 清洗后仍含乱码或非目标语言文本 | 抽样检查评估异常文本 | 加强语言过滤和质量评分 |
| 特定领域能力差 | 该领域数据占比过低 | 统计领域关键词覆盖度 | 增加领域语料或过采样 |
| 去重后数据量骤减 | 去重阈值过于严格 | 抽样查看被删除文本 | 调整相似度阈值,区分领域保留策略 |
| 全量训练前代理训练已过拟合 | 数据重复过高或多样性不足 | 查看训练集和验证集 Loss 差异 | 加强去重,增加数据来源多样性 |
9. 最佳实践与合规建议
9.1 数据工程的最佳实践
- 先小后大:任何清洗和去重规则,先在一万条样本上验证,再上全量。
- 随时留档:每一版清洗后的数据都要保留样本文件,方便事后追溯数据版本。
- 多维度观测:不只盯着 Loss,还要定期生成模型输出样例,人工判断数据质量是否真实影响生成效果。
- 设置数据预算:预训练 token 总量通常固定,配比调整要在预算内完成,避免数据无限膨胀导致训练成本失控。
- 重复使用已有 Pipeline:优先复用 RedPajama、RefinedWeb 等开源数据管线的经验,而不是从头造轮子。
9.2 版权与隐私边界
在做预训练数据收集和处理时,必须注意数据来源的合法性和合规性。对于有明确版权声明的数据,需要确认是否允许用于模型训练。对于涉及个人信息的内容,要执行 PII(个人身份信息)检测与删除,避免模型在训练后记忆并泄漏用户的隐私信息。
课程里也强调了数据合规在预训练流程中的前置性。数据清洗到后期,隐私过滤不只是技术问题,更是产品上线前必须完成的安全底线。建议在数据管线中加入隐私过滤步骤,并对过滤结果进行人工抽检,确保敏感信息没有被遗漏。
9.3 开源数据的补充使用
实践中可以基于 RedPajama、RefinedWeb、Mc4 等开源数据集做二次清洗和配比调整,而不是从零开始爬取全网数据。这样能避开大量原始数据质量问题的坑,把更多精力放在配比和质量评估上。
10. 总结与下一步
数据清洗、去重与配比策略,是预训练大模型中最容易被轻视却最影响结果的部分。课程 EP14 的核心价值在于把数据工程从“玄学”变成可执行的流程:清洗阶段去除噪声和质量垃圾,去重阶段消除冗余和记忆偏差,配比阶段控制知识覆盖和能力偏向。
对于正准备开始预训练或继续研究大模型的读者,建议按这样的顺序验证:先下载一小批开源语料,跑通清洗、去重和配比的基础 Pipeline,再用一个小模型做代理训练,观察 Loss 和输出效果。走通这一遍后,再考虑扩展数据规模和正式训练。这套流程本身就是最值得收藏的产出物。后续可以继续关注的数据方向包括多模态数据的清洗与配比、动态配比策略的训练时调整方法、以及数据质量与模型可解释性之间的关联分析。