1. 预训练数据集构建的整体设计思路
1.1 为什么预训练数据集是“地基中的地基”
很多人做大模型训练,眼睛只盯着模型结构、学习率、显卡数量,却忽略了最根本的东西——数据。我见过太多团队,模型代码抄得一模一样,超参调得也差不多,但最终效果差出一大截,问题几乎都出在数据上。预训练数据集构建这件事,说白了就是给模型准备“教材”。教材选得烂,老师再厉害也教不出好学生。
预训练阶段的核心目标只有一个:让模型学会语言的统计规律和世界知识。它不像微调那样有明确的输入输出对,预训练的数据就是一大段一大段的连续文本,模型通过预测下一个token来学习。所以数据的质量、多样性、规模、清洗程度,直接决定了模型的基础能力上限。
一个合格的预训练数据集,通常需要满足几个硬指标:规模足够大(至少几十GB到几TB级别)、领域足够广(网页、书籍、代码、论文、百科等)、噪声足够低(去重、去广告、去乱码)、格式足够统一(便于流式读取和分片)。这四个维度缺一不可,而且它们之间是有取舍的——清洗太狠会损失多样性,清洗太松又会引入大量垃圾。
1.2 从原始语料到训练样本的完整链路
预训练数据集构建不是简单地“把文本收集起来打包”,它是一条完整的流水线。我把它拆成五个阶段:
- 采集阶段:从各种来源获取原始文本,包括公开网页、电子书、学术论文、代码仓库、论坛帖子等。
- 清洗阶段:去除HTML标签、广告、导航栏、重复段落、乱码字符。
- 过滤阶段:基于质量启发式规则和模型打分,筛掉低质量文档。
- 去重阶段:在文档级别和段落级别做去重,避免模型反复背诵同一段内容。
- 打包阶段:将清洗后的文本统一编码,切分成固定长度的序列,写入二进制文件供训练时流式读取。
这条链路里,每一步都有坑。比如去重,很多人只做精确去重,结果模型还是会把近似重复的内容背下来。再比如打包,如果不做shuffle,模型会先学完一个领域再学另一个领域,导致灾难性遗忘。
1.3 方案选型:为什么我最终选择了LLaMA-Factory的数据管线
市面上做预训练数据处理的工具不少,有自己写脚本的,有用HuggingFace datasets的,也有用LLaMA-Factory的。我试过几种组合,最终在实战中稳定用的是LLaMA-Factory + 自定义预处理脚本的方案。
原因有三点。第一,LLaMA-Factory对预训练数据的格式要求非常明确,它支持text字段的JSONL格式,也支持直接读取纯文本目录,接入成本低。第二,它的数据注册机制很清晰,你只需要在dataset_info.json里加一条记录,就能在训练配置里直接引用。第三,它和后续的微调、推理流程是打通的,你不需要为预训练单独维护一套数据格式,后面做SFT的时候可以直接复用同一套数据管理逻辑。
当然,LLaMA-Factory不是万能的。它本身不提供网页爬取和复杂清洗功能,这些还是得自己写脚本。但它的定位很准——做好数据格式的统一和训练时的加载,把脏活累活留给上游。这个分工我很认可。
2. 核心细节解析与实操要点
2.1 数据来源的选择与配比策略
预训练数据来源的选择,直接决定了模型的知识面和偏见。我一般把数据分成六大类,每类给一个目标配比:
| 数据类别 | 典型来源 | 建议占比 | 作用 |
|---|---|---|---|
| 网页文本 | 公开网页抓取 | 50%-60% | 提供广泛的世界知识和语言多样性 |
| 书籍文本 | 电子书、小说、教材 | 10%-15% | 提供长程连贯性和叙事逻辑 |
| 学术论文 | 预印本、期刊 | 5%-10% | 提供专业术语和严谨表达 |
| 代码数据 | 开源代码仓库 | 10%-15% | 提升逻辑推理和结构化生成能力 |
| 百科问答 | 百科条目、问答社区 | 5%-10% | 提供事实性知识和简洁表达 |
| 多语言数据 | 多语种网页和书籍 | 5%-10% | 提升跨语言能力 |
这个配比不是拍脑袋定的,而是根据多个公开模型的技术报告和我的实际训练经验总结的。网页文本占大头是因为它规模最大、覆盖最广,但纯网页数据噪声也最大,所以必须搭配书籍和论文来提升平均质量。代码数据比例不能太低,否则模型在需要结构化输出时会表现很差。
注意:如果你训练的是中文为主的大模型,网页文本里中文占比要控制在合理范围。我见过有人用90%以上中文网页数据训练,结果模型英文能力几乎为零,连基本的英文指令都理解不了。
2.2 文本清洗的关键规则与实现
清洗是预训练数据处理里最耗时的环节,也是最容易出问题的环节。我总结了一套“四层清洗法”:
第一层:HTML剥离。用BeautifulSoup或trafilatura把网页的正文提取出来,去掉所有标签、脚本、样式。这一步的关键是正文提取算法要准,不能把导航栏和评论区也当成正文。
第二层:字符级清洗。去掉控制字符、零宽字符、乱码序列。中文文本里常见的问题是编码错误导致的乱码,比如“锟斤拷”这种,必须用正则批量清除。
第三层:段落级清洗。去掉过短的段落(少于20个字符)、重复的段落、包含大量特殊符号的段落。这一步能过滤掉很多广告和导航文本。
第四层:文档级清洗。基于困惑度或质量分类器给整篇文档打分,低于阈值的直接丢弃。这一步需要一个小型语言模型来做打分,成本较高但效果显著。
import re from bs4 import BeautifulSoup def clean_html(raw_html): soup = BeautifulSoup(raw_html, "html.parser") for tag in soup(["script", "style", "nav", "footer", "header"]): tag.decompose() text = soup.get_text(separator="\n") return text def clean_chars(text): text = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f]", "", text) text = re.sub(r"[\u200b-\u200f\u2028-\u202f]", "", text) text = re.sub(r"锟斤拷|烫烫烫|屯屯屯", "", text) return text def clean_paragraphs(text, min_len=20): paragraphs = text.split("\n") cleaned = [] seen = set() for p in paragraphs: p = p.strip() if len(p) < min_len: continue if p in seen: continue if len(re.findall(r"[^\w\s]", p)) / max(len(p), 1) > 0.5: continue seen.add(p) cleaned.append(p) return "\n".join(cleaned)这三段代码是我实际项目中反复用的,你可以直接抄。clean_html负责剥离标签,clean_chars处理字符级噪声,clean_paragraphs做段落级过滤。注意min_len这个参数,中文和英文要区别对待,中文20字大概相当于英文10个词左右。
2.3 去重策略:精确去重与近似去重
去重是预训练数据处理里最容易被低估的环节。很多人觉得“我数据量大,重复一点没关系”,但实际上重复数据会让模型过度拟合某些片段,降低泛化能力。
精确去重很简单,用哈希就行。把每篇文档或每个段落做MD5,存到集合里,遇到重复的直接跳过。但精确去重只能解决完全一样的情况,对于“改了几个词”的近似重复无能为力。
近似去重我推荐用MinHash + LSH的方案。原理不复杂:把每篇文档表示成一组shingle(连续词片段),然后用多个哈希函数生成签名,最后通过局部敏感哈希找到相似文档。这个方案在datasketch这个库里已经有现成实现。
from datasketch import MinHash, MinHashLSH def get_minhash(text, num_perm=128): m = MinHash(num_perm=num_perm) for i in range(len(text) - 5): m.update(text[i:i+5].encode("utf-8")) return m lsh = MinHashLSH(threshold=0.8, num_perm=128) for doc_id, text in enumerate(documents): m = get_minhash(text) lsh.insert(doc_id, m)threshold=0.8意味着相似度超过80%的文档会被判定为重复。这个阈值可以根据你的数据特点调整,网页数据建议0.8,书籍数据可以放宽到0.9。
实操心得:去重之前一定要先做清洗。如果带着HTML标签去做MinHash,相似度计算会被标签噪声干扰,导致去重效果大打折扣。我踩过这个坑,清洗前去重和清洗后去重,最终数据量差了将近15%。
2.4 数据格式与LLaMA-Factory的对接
LLaMA-Factory对预训练数据的格式要求比较灵活,支持两种方式:
方式一:JSONL格式。每行一个JSON对象,包含text字段。这是最推荐的方式,因为可以在同一个文件里混合多种来源的数据,方便控制配比。
{"text": "这是第一篇文档的完整内容..."} {"text": "这是第二篇文档的完整内容..."}方式二:纯文本目录。把每篇文档存成一个.txt文件,放在一个目录下。LLaMA-Factory会自动读取目录下所有文本文件。这种方式适合文档数量不多、需要保留文件粒度的场景。
我一般用JSONL,因为处理起来更灵活。在dataset_info.json里注册的时候,这样写:
{ "my_pretrain_data": { "file_name": "pretrain_data.jsonl", "formatting": "pretrain", "columns": { "prompt": "text" } } }注意formatting要设为pretrain,这样LLaMA-Factory就知道这是预训练数据,不会去解析instruction和response字段。
3. 实操过程与核心环节实现
3.1 环境准备与依赖安装
我假设你已经有一台带GPU的机器,或者至少有一台大内存的CPU机器来做数据处理。数据处理阶段其实不太吃GPU,主要吃内存和磁盘IO。
conda create -n pretrain-data python=3.10 -y conda activate pretrain-data pip install datasets transformers beautifulsoup4 trafilatura datasketch tqdm pip install llamafactorydatasets用来做数据加载和分片,trafilatura是我用过最好的正文提取库,datasketch做近似去重,tqdm看进度。LLaMA-Factory直接pip装就行。
磁盘空间要提前算好。原始网页数据压缩后可能只有几十GB,但解压清洗后可能膨胀到几百GB。我一般预留原始数据3-5倍的磁盘空间。内存方面,做MinHash去重的时候,如果文档数量上亿,内存消耗会很大,建议分批次处理。
3.2 完整的数据处理流水线
我把整个流水线写成一个可配置的脚本,按阶段执行。每个阶段的输出都落盘,方便中断后继续。
阶段一:原始数据采集与解压。如果你用的是公开数据集,下载后通常是压缩包。解压后先做一次文件级别的去重,把完全一样的文件删掉。
阶段二:HTML清洗与正文提取。对每个HTML文件调用trafilatura.extract,提取正文。这一步比较慢,建议用多进程加速。
from trafilatura import extract from multiprocessing import Pool def process_html(file_path): with open(file_path, "r", encoding="utf-8", errors="ignore") as f: html = f.read() text = extract(html, include_comments=False, include_tables=False) return text with Pool(processes=16) as pool: results = pool.map(process_html, html_files)include_comments=False和include_tables=False很重要,评论区和表格往往是噪声重灾区。
阶段三:字符级和段落级清洗。把上一步提取的文本过一遍前面写的清洗函数。这一步可以合并到阶段二里,减少IO次数。
阶段四:质量过滤。我用一个简单的启发式规则组合:平均词长、标点符号比例、重复行比例、是否包含敏感词。低于阈值的文档直接丢弃。如果你有训练好的质量分类器,这一步可以替换成模型打分。
阶段五:去重。先做精确去重,再做MinHash近似去重。近似去重建议分片做,每片100万篇文档,处理完一片合并一次结果。
阶段六:打包与分片。把最终清洗后的文档写入JSONL文件,每个文件不超过1GB,方便后续加载。同时生成一个索引文件,记录每个分片的文档数量和总token数。
import json import os def write_jsonl(documents, output_dir, max_size_gb=1): os.makedirs(output_dir, exist_ok=True) file_idx = 0 current_size = 0 current_file = open(os.path.join(output_dir, f"data_{file_idx:05d}.jsonl"), "w", encoding="utf-8") for doc in documents: line = json.dumps({"text": doc}, ensure_ascii=False) + "\n" current_file.write(line) current_size += len(line.encode("utf-8")) if current_size > max_size_gb * 1024**3: current_file.close() file_idx += 1 current_size = 0 current_file = open(os.path.join(output_dir, f"data_{file_idx:05d}.jsonl"), "w", encoding="utf-8") current_file.close()3.3 在LLaMA-Factory中配置预训练任务
数据准备好之后,在LLaMA-Factory里配置预训练就很简单了。我以Qwen2.5-7B为例,写一个pretrain.yaml:
model_name_or_path: Qwen/Qwen2.5-7B stage: pretrain do_train: true dataset: my_pretrain_data cutoff_len: 4096 max_samples: 1000000 overwrite_cache: true preprocessing_num_workers: 16 output_dir: outputs/pretrain_qwen2.5_7b logging_steps: 10 save_steps: 500 plot_loss: true overwrite_output_dir: true per_device_train_batch_size: 2 gradient_accumulation_steps: 8 learning_rate: 1.0e-5 num_train_epochs: 1 lr_scheduler_type: cosine warmup_ratio: 0.03 bf16: true gradient_checkpointing: true几个关键参数说明一下。cutoff_len设为4096,意味着每条训练样本会被截断或填充到4096个token。预训练阶段这个值可以设大一点,但受限于显存。max_samples控制最多用多少条样本,调试阶段可以设小一点快速验证。learning_rate用1e-5,预训练的学习率通常比微调要小,因为数据量大,步子迈大了容易跑偏。
启动训练:
llamafactory-cli train pretrain.yaml如果一切正常,你会看到loss从10左右开始缓慢下降。预训练的loss下降很慢,前几百步可能几乎看不出变化,这是正常的。我一般会跑至少几千步再判断数据质量。
3.4 数据质量验证:怎么知道数据好不好
数据构建完了,不能直接上大规模训练,得先做小规模验证。我的做法是:用1%的数据跑一个小的预训练任务,观察loss曲线和生成样本。
验证一:loss曲线是否平滑下降。如果loss剧烈震荡或者不降,说明数据里噪声太多或者格式有问题。
验证二:模型能否生成通顺文本。训练几千步后,让模型续写一段话。如果生成的文本语法混乱、重复严重,说明数据质量不行。
验证三:困惑度对比。在一个干净的验证集上计算困惑度,如果困惑度比基线模型还高,说明预训练数据反而损害了模型能力。
实操心得:我习惯在预处理阶段就抽样1000篇文档人工检查。重点看有没有乱码、有没有重复、有没有敏感内容。人工检查这一步不能省,自动化工具再厉害也会漏掉一些奇怪的东西。
4. 常见问题与排查技巧实录
4.1 数据清洗不干净导致的典型症状
症状一:模型生成内容里出现HTML标签。比如生成“
症状二:模型反复输出同一句话。这是重复数据太多的典型表现。模型在训练时反复看到同一段文本,学会了“偷懒”——直接复制上下文。解决办法是加强去重,尤其是段落级去重。
症状三:模型输出大量无意义符号。比如连续输出“。。。。。。”或者“!!!!!”这种。原因是数据里包含大量低质量文本,比如论坛灌水、弹幕、乱码。解决办法是提高质量过滤阈值,把标点符号比例过高的文档丢掉。
4.2 去重环节的性能瓶颈与优化
MinHash去重是计算密集型任务,文档数量上亿的时候,单机跑非常慢。我试过几种优化方案:
| 优化方案 | 效果 | 代价 |
|---|---|---|
| 增加进程数 | 线性加速,但受限于CPU核数 | 需要多核CPU |
| 减小num_perm | 速度提升,但精度下降 | 去重准确率降低 |
| 分片处理 | 内存可控,可并行 | 需要合并跨片结果 |
| 先用精确去重 | 减少进入MinHash的文档量 | 只能去掉完全重复的 |
我一般组合使用:先用精确去重砍掉30%-40%的重复文档,再用MinHash处理剩下的。num_perm设128,threshold设0.8,这个配置在精度和速度之间比较平衡。
4.3 LLaMA-Factory数据加载报错排查
报错一:KeyError: 'text'。说明JSONL里的字段名不是text,或者dataset_info.json里的columns配置不对。检查你的JSONL第一行,确认字段名,然后检查columns映射。
报错二:RuntimeError: CUDA out of memory。这个不一定是数据问题,可能是cutoff_len设太大了。先把cutoff_len降到2048试试,如果还不行就减小per_device_train_batch_size。
报错三:ValueError: Tokenizer not found。检查model_name_or_path是否写对,以及模型文件是否完整下载。有时候网络中断会导致模型下载不完整,删掉缓存重新下载。
报错四:训练loss一直是nan。这个比较严重,通常是数据里包含非法字符或者空文档。检查你的JSONL里有没有空行,有没有包含\x00这种控制字符。另外检查learning_rate是不是设太大了,预训练阶段1e-5到5e-5之间比较安全。
4.4 数据配比的动态调整经验
数据配比不是一成不变的。我在实际训练中会根据loss曲线和生成效果动态调整。比如:
- 如果模型在代码任务上表现差,就提高代码数据比例。
- 如果模型中文表达不地道,就提高中文网页和书籍比例。
- 如果模型事实性错误多,就提高百科和论文比例。
调整配比的时候,建议一次只调一个维度,调完跑几千步看效果。同时调多个维度,你根本不知道是哪个起了作用。
注意:预训练阶段的数据配比调整,效果显现很慢。不要跑了几百步觉得没变化就放弃,至少跑5000步以上再判断。
4.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| loss不下降 | 学习率太小或数据太差 | 检查loss曲线和样本 | 调大学习率或重新清洗数据 |
| loss震荡严重 | 学习率太大或batch太小 | 观察loss波动幅度 | 减小学习率或增大batch |
| 生成重复内容 | 数据重复率高 | 抽样检查数据 | 加强去重 |
| 生成乱码 | 字符清洗不彻底 | 搜索乱码字符 | 增加字符过滤规则 |
| 训练速度慢 | 数据加载瓶颈 | 看GPU利用率 | 增加预处理进程数或改用流式加载 |
| 显存溢出 | cutoff_len太大 | 看报错信息 | 减小cutoff_len或batch_size |
| 模型遗忘严重 | 数据分布太偏 | 检查各类数据占比 | 调整配比,增加多样性 |
5. 数据版本管理与迭代策略
5.1 为什么预训练数据也需要版本管理
很多人觉得数据嘛,处理完就用,没必要搞版本管理。但我吃过亏。有一次我清洗了三版数据,分别训练了三个模型,结果发现效果最好的那个模型用的是哪版数据我记不清了,因为文件名都是pretrain_data_final.jsonl、pretrain_data_final_v2.jsonl这种。后来我强制自己用版本管理,每次数据处理都打标签。
我的做法很简单:用日期加配置哈希作为版本号。比如20250115_a3f2c1,前面是日期,后面是清洗配置的哈希值。这样一看就知道这版数据是什么时候、用什么配置处理的。同时在目录下放一个README.md,记录这版数据的来源、配比、清洗规则、去重阈值、总token数。
5.2 迭代策略:从粗到细,逐步收紧
预训练数据构建不是一次性的工作,而是一个迭代过程。我的迭代策略是:
第一轮:粗清洗。只做基本的HTML剥离和字符清洗,去重阈值设低一点(0.9),保留尽可能多的数据。这一轮的目的是快速得到一个可用的基线数据集。
第二轮:质量过滤。在第一轮基础上加入质量打分,去掉低质量文档。这一轮数据量会减少20%-30%,但平均质量明显提升。
第三轮:精细去重。把去重阈值降到0.8,做更严格的近似去重。这一轮数据量再减少10%-15%,但重复率大幅下降。
第四轮:配比调整。根据前几轮训练的效果,调整各类数据的比例,补充短板领域的数据。
每一轮迭代都要重新训练一个小模型来验证效果。不要一次性把所有优化都做完再训练,那样你无法判断哪个优化起了作用。
5.3 数据质量监控的长期机制
如果你要持续做预训练,建议建立一个数据质量监控面板。我一般监控这几个指标:
- 文档数量:每版数据的文档总数,突然下降说明清洗规则太严了。
- 平均文档长度:突然变短说明正文提取出了问题。
- 重复率:抽样计算近似重复比例,超过5%就要警惕。
- 字符分布:统计中英文比例、数字比例、标点比例,异常波动说明数据来源有问题。
- token总数:这是最核心的指标,直接决定训练成本和模型规模。
这些指标不需要实时监控,每版数据处理完跑一次就行。但一定要记录,形成趋势图。数据质量的变化往往是渐进的,单看一版数据看不出问题,看趋势才能发现异常。
6. 一些踩坑后的个人体会
预训练数据集构建这件事,技术含量其实没有微调那么高,但它考验的是耐心和细致。我见过太多人在这上面偷懒,最后模型效果不行,回头找原因,发现是数据里混了几万条乱码。
我最大的体会是:数据清洗的投入产出比远高于模型调参。你花一周时间调学习率、调batch size,可能提升1-2个点。但你花一周时间把数据重复率从10%降到2%,可能提升5个点以上。而且数据质量带来的提升是泛化的,不挑任务。
另一个体会是:不要追求一步到位。预训练数据构建是一个迭代过程,第一版数据不可能完美。先跑通流程,得到一个基线模型,然后根据模型表现反推数据问题,再针对性优化。这比一开始就追求完美数据要高效得多。
最后分享一个小技巧:如果你不确定某类数据该不该加,先加进去跑一个小模型试试。预训练阶段的数据实验成本其实不高,用1%的数据跑几千步就能看出趋势。与其纠结,不如快速验证。