
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_len20): 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_perm128): m MinHash(num_permnum_perm) for i in range(len(text) - 5): m.update(text[i:i5].encode(utf-8)) return m lsh MinHashLSH(threshold0.8, num_perm128) for doc_id, text in enumerate(documents): m get_minhash(text) lsh.insert(doc_id, m)threshold0.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 python3.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, encodingutf-8, errorsignore) as f: html f.read() text extract(html, include_commentsFalse, include_tablesFalse) return text with Pool(processes16) as pool: results pool.map(process_html, html_files)include_commentsFalse和include_tablesFalse很重要评论区和表格往往是噪声重灾区。阶段三字符级和段落级清洗。把上一步提取的文本过一遍前面写的清洗函数。这一步可以合并到阶段二里减少IO次数。阶段四质量过滤。我用一个简单的启发式规则组合平均词长、标点符号比例、重复行比例、是否包含敏感词。低于阈值的文档直接丢弃。如果你有训练好的质量分类器这一步可以替换成模型打分。阶段五去重。先做精确去重再做MinHash近似去重。近似去重建议分片做每片100万篇文档处理完一片合并一次结果。阶段六打包与分片。把最终清洗后的文档写入JSONL文件每个文件不超过1GB方便后续加载。同时生成一个索引文件记录每个分片的文档数量和总token数。import json import os def write_jsonl(documents, output_dir, max_size_gb1): os.makedirs(output_dir, exist_okTrue) file_idx 0 current_size 0 current_file open(os.path.join(output_dir, fdata_{file_idx:05d}.jsonl), w, encodingutf-8) for doc in documents: line json.dumps({text: doc}, ensure_asciiFalse) \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, fdata_{file_idx:05d}.jsonl), w, encodingutf-8) current_file.close()3.3 在LLaMA-Factory中配置预训练任务数据准备好之后在LLaMA-Factory里配置预训练就很简单了。我以Qwen2.5-7B为例写一个pretrain.yamlmodel_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标签。比如生成“”、“ ”这种。原因是HTML剥离不彻底有些标签藏在JavaScript字符串里没被BeautifulSoup处理掉。解决办法是在清洗后加一轮正则把所有尖括号包裹的内容删掉。症状二模型反复输出同一句话。这是重复数据太多的典型表现。模型在训练时反复看到同一段文本学会了“偷懒”——直接复制上下文。解决办法是加强去重尤其是段落级去重。症状三模型输出大量无意义符号。比如连续输出“。。。。。。”或者“”这种。原因是数据里包含大量低质量文本比如论坛灌水、弹幕、乱码。解决办法是提高质量过滤阈值把标点符号比例过高的文档丢掉。4.2 去重环节的性能瓶颈与优化MinHash去重是计算密集型任务文档数量上亿的时候单机跑非常慢。我试过几种优化方案优化方案效果代价增加进程数线性加速但受限于CPU核数需要多核CPU减小num_perm速度提升但精度下降去重准确率降低分片处理内存可控可并行需要合并跨片结果先用精确去重减少进入MinHash的文档量只能去掉完全重复的我一般组合使用先用精确去重砍掉30%-40%的重复文档再用MinHash处理剩下的。num_perm设128threshold设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%的数据跑几千步就能看出趋势。与其纠结不如快速验证。