预训练数据集构建实战:从采集到LLamaFactory格式化的完整流程 1. 预训练数据集构建的整体设计思路1.1 为什么预训练数据集是“地基中的地基”很多人做大模型训练眼睛只盯着模型结构、学习率、batch size觉得数据集就是“把文本喂进去”这么简单。我刚开始接触大模型训练的时候也这么想直到有一次用了一份自己爬的所谓“高质量语料”去跑LLamaFactory的预训练任务loss曲线前200步看着还行到后面直接崩成一条直线生成出来的东西全是重复的“的的的的的”。排查了三天最后发现问题出在数据上——语料里混入了大量重复的模板文本和乱码字符清洗环节几乎等于没做。这件事让我彻底明白一个道理预训练数据集的质量直接决定了大模型能力的上限。你后面做SFT、做RLHF、做提示词工程所有花活都建立在预训练阶段打下的底子上。底子歪了后面怎么调都是白费力气。那预训练数据集构建到底在做什么简单说就是把互联网上、书籍里、代码仓库中散落的各种文本经过采集、清洗、去重、过滤、格式化、配比、打包这一整套流程变成模型能直接“吃”的标准化数据。这个过程听起来不复杂但每一步都有大量细节可以决定成败。1.2 数据集构建的核心流程拆解我习惯把整个预训练数据集构建分成六个阶段每个阶段都有明确的输入和输出阶段核心任务输出产物常见工具采集从多源获取原始文本原始语料池爬虫框架、公开数据集下载清洗去除噪声、乱码、模板文本干净文本正则、启发式规则去重消除重复和近似重复去重后语料MinHash、SimHash过滤按质量、安全、领域筛选高质量子集分类器、规则引擎配比按领域和来源调整比例混合语料采样脚本格式化转成训练框架需要的格式JSONL/Arrow等LLamaFactory内置工具这个流程不是线性的很多时候需要反复迭代。比如你在过滤阶段发现某个来源的数据质量特别差可能就要回到采集阶段重新调整策略。我在实际项目中这六个阶段来回跑了不下五轮才得到一份勉强满意的预训练语料。1.3 工具选型为什么我最终选了LLamaFactory市面上做预训练数据处理的工具不少有直接用HuggingFace datasets库手写脚本的有用Spark做大规模清洗的也有用专门的数据处理平台的。我试过几种方案最后在中小规模预训练场景下还是觉得LLamaFactory最顺手。原因有几个第一它把数据格式定义得很清楚你只要按照它的规范准备好JSONL文件后面训练脚本直接就能读省去了大量格式转换的麻烦。第二它内置了一些数据预处理工具比如分词、打包、截断虽然不算特别强大但胜在开箱即用。第三它的社区活跃遇到问题搜一下基本都能找到答案。当然如果你要做千亿参数级别的预训练LLamaFactory可能就不够用了那时候你需要自己写分布式数据处理流水线。但对于大多数个人开发者和中小团队来说LLamaFactory的性价比是最高的。提示不要一上来就追求“全自动流水线”。我见过太多人花两周搭了一套看起来很酷的自动化数据处理系统结果发现清洗规则没调好跑出来的数据还不如手动处理的质量高。先把每个环节的手动流程跑通再考虑自动化。2. 核心细节解析与实操要点2.1 数据采集来源决定上限预训练数据的来源直接决定了模型的知识广度和语言能力。我一般把数据来源分成几大类通用网页文本覆盖面最广但噪声也最大。需要重点清洗。书籍和论文语言规范、逻辑性强但获取成本高版权要注意。代码仓库对代码生成能力至关重要但需要处理许可证问题。百科和词典结构化程度高适合做知识注入。对话和论坛口语化表达丰富但需要过滤低质量互动。我在实际项目中的配比经验是通用网页文本占60%左右书籍和论文占15%代码占15%百科和词典占5%对话和论坛占5%。这个比例不是固定的要根据你的模型定位来调整。如果你要做的是一个代码助手那代码比例可能要拉到40%以上。采集环节有个容易被忽视的点元数据记录。每一条数据从哪里来的、采集时间是什么、原始URL是什么这些信息一定要保留。后面做数据溯源、质量分析、去重的时候元数据能帮你大忙。我习惯在采集阶段就给每条数据打上来源标签后面过滤的时候可以直接按标签做分层处理。2.2 数据清洗把“垃圾”挡在门外清洗是预训练数据处理中最耗时、最考验耐心的环节。我总结了几类必须处理的问题第一类是编码问题。不同来源的文本编码格式五花八门UTF-8、GBK、Latin-1混在一起。如果不统一转成UTF-8后面分词的时候会出现大量乱码。我的做法是先用chardet检测编码然后统一转换转换失败的直接丢弃。第二类是HTML标签和特殊字符。从网页爬下来的文本里全是div、span、nbsp;这些东西。用正则表达式批量清理是最直接的办法但要注意不要误删正文内容。我一般会先用BeautifulSoup把HTML解析成纯文本再用正则处理残留的特殊字符。第三类是模板文本和导航栏。很多网页的页眉、页脚、侧边栏都是固定模板爬下来就是一堆重复的“首页 | 关于我们 | 联系我们”。这类文本如果不清理模型会学到大量无意义的重复模式。我的做法是统计高频n-gram把出现频率异常高的短句加入黑名单。第四类是低质量内容。比如乱码、火星文、纯符号、过短的文本。我一般会设置几个硬性阈值文本长度少于50个字符的丢弃非中英文字符占比超过30%的丢弃重复字符连续出现超过10次的丢弃。import re def clean_text(text): # 去除HTML标签 text re.sub(r[^], , text) # 去除URL text re.sub(rhttp[s]?://\S, , text) # 去除多余空白 text re.sub(r\s, , text) # 去除控制字符 text re.sub(r[\x00-\x1f\x7f-\x9f], , text) # 去除连续重复字符 text re.sub(r(.)\1{9,}, r\1, text) return text.strip()这段代码看起来简单但实际跑起来你会发现不同来源的数据需要不同的清洗规则。网页数据要重点处理HTMLPDF提取的文本要处理换行和分页符代码数据要保留缩进和特殊符号。一刀切的清洗规则一定会出问题必须分来源定制。2.3 数据去重别让模型“复读”重复数据是预训练的大敌。模型在重复数据上训练久了会变得只会“复读”生成多样性急剧下降。去重分两个层次精确去重比较简单就是完全相同的文本只保留一条。用哈希表就能搞定速度快内存占用也可控。我一般用MD5或者SHA256对文本做哈希然后去重。近似去重才是真正的挑战。互联网上大量内容是改头换面的重复比如同一篇新闻被不同网站转载只改了标题和几个词。这种用精确去重搞不定需要用MinHash或者SimHash。MinHash的原理我打个比方假设你有两篇文章你想知道它们有多相似。你把每篇文章拆成很多个小片段shingle然后对每个片段做哈希取最小的几个哈希值作为这篇文章的“指纹”。两篇文章的指纹重合度越高说明它们越相似。这个方法的好处是计算快而且可以通过调整哈希函数数量来控制精度。我在实际项目中的做法是先用精确去重去掉完全相同的再用MinHash做近似去重相似度阈值设在0.8左右。阈值太高会漏掉很多近似重复太低会误删有价值的相似内容。这个值需要根据你的数据特点来调。注意去重不是越彻底越好。有些领域本身就有大量相似表达比如法律条文、医学病例。过度去重会导致这些领域的样本过少模型在该领域的能力下降。我一般会按领域分别设置去重阈值通用领域严一些专业领域松一些。2.4 数据过滤质量比数量重要过滤环节的目标是把低质量、不安全、不相关的数据剔除掉。我一般从三个维度做过滤质量维度用困惑度perplexity来评估文本的“通顺程度”。具体做法是先用一个小型语言模型比如GPT-2或者RoBERTa计算每条文本的困惑度困惑度太高的说明文本不通顺可能是机器生成的或者乱码。我一般把阈值设在 perplexity 1000 的丢弃。安全维度用关键词黑名单和分类器结合的方式过滤掉包含敏感信息的文本。关键词黑名单覆盖常见的敏感词分类器用来识别更隐蔽的不安全内容。这个环节要特别小心宁可错杀不可放过。领域维度如果你要做的是垂直领域模型比如医疗或者法律那还需要用领域分类器把不相关的文本过滤掉。我一般会训练一个轻量级的文本分类器把数据分成“相关”和“不相关”两类只保留相关的那部分。过滤完之后一定要做人工抽检。我习惯随机抽1000条数据一条一条看统计各类问题的比例。如果某个问题的比例超过5%就要回到清洗或过滤环节重新调整规则。这个步骤很枯燥但绝对不能省。2.5 数据配比不是越多越好很多人觉得预训练数据越多越好恨不得把整个互联网都爬下来。但实际上数据的质量和配比比数量重要得多。Meta在训练LLaMA的时候用了1.4万亿tokens但他们的数据配比是经过大量实验调出来的。我的一般原则是高质量来源书籍、论文、百科占比不低于30%代码数据占比根据模型定位调整通用模型10%-15%代码模型30%-50%对话数据占比不超过10%否则模型会变得过于“话痨”单一来源占比不超过40%避免模型偏向某个特定领域配比确定之后用采样脚本按比例抽取数据。这里有个技巧不要一次性把所有数据混在一起。我习惯按来源分别存储训练的时候再用数据加载器按比例采样。这样后面调整配比的时候不用重新处理整个数据集。2.6 格式化让LLamaFactory能直接读LLamaFactory对预训练数据的格式要求是JSONL每行一个JSON对象包含text字段。看起来很简单但实际处理的时候有几个坑第一个坑是特殊字符转义。JSON对引号、反斜杠、换行符有严格的转义要求。如果文本里包含这些字符直接写进去会导致JSON解析失败。我一般用Python的json.dumps来生成每一行确保转义正确。第二个坑是长文本截断。LLamaFactory默认的最大序列长度是2048或者4096超过这个长度的文本会被截断。但截断的位置很关键如果在句子中间截断模型会学到不完整的语言模式。我的做法是优先在段落边界截断如果段落太长就在句子边界截断。第三个坑是数据打包。为了提高训练效率通常会把多条短文本打包成一个长序列。LLamaFactory支持这种打包方式但需要你在数据配置里开启。打包的时候要注意不同来源的文本不要混在同一个序列里否则模型会学到奇怪的跨文档关联。import json def format_for_llamafactory(texts, output_path, max_length4096): with open(output_path, w, encodingutf-8) as f: buffer [] buffer_len 0 for text in texts: if len(text) max_length: # 长文本单独处理按句子边界截断 chunks split_by_sentence(text, max_length) for chunk in chunks: f.write(json.dumps({text: chunk}, ensure_asciiFalse) \n) else: if buffer_len len(text) max_length: f.write(json.dumps({text: .join(buffer)}, ensure_asciiFalse) \n) buffer [text] buffer_len len(text) else: buffer.append(text) buffer_len len(text) if buffer: f.write(json.dumps({text: .join(buffer)}, ensure_asciiFalse) \n)这段代码是我实际项目中用的简化版核心逻辑就是控制每个JSON对象的文本长度不超过模型的最大序列长度同时尽量在句子边界做切分。3. 实操过程与核心环节实现3.1 环境准备与依赖安装在开始处理数据之前先把环境搭好。我一般用conda创建一个独立的Python环境避免和系统里的其他包冲突。conda create -n pretrain-data python3.10 conda activate pretrain-data pip install datasets transformers llamafactory pip install jieba nltk beautifulsoup4 lxml pip install datasketch # 用于MinHash去重 pip install fasttext # 用于质量分类这里重点说一下datasketch和fasttext。datasketch是MinHash的Python实现去重的时候直接用它的API就行不用自己从头写。fasttext用来训练质量分类器速度快效果也不错。LLamaFactory的安装要注意版本兼容性。我遇到过transformers版本太新导致LLamaFactory报错的情况后来锁定在4.36.0版本就稳定了。如果你也遇到类似问题可以试试降级transformers。3.2 从零构建一份中文预训练语料的完整流程假设我们现在要构建一份中文通用预训练语料目标是500万条文本总token数在20亿左右。下面是我实际操作的完整流程。第一步采集数据。我从几个公开数据源获取原始文本包括中文维基百科、开源书籍数据集、中文新闻语料、GitHub上的中文代码仓库。每个来源单独存一个文件方便后面分别处理。第二步统一编码。写一个脚本遍历所有文件检测编码并统一转成UTF-8。这一步会丢掉一些编码检测失败的文件但没办法总比后面出现乱码强。第三步分来源清洗。针对不同来源写不同的清洗规则。维基百科要处理wiki标记新闻要处理HTML标签代码要保留缩进。我一般会为每个来源写一个清洗函数然后用多进程并行处理。第四步精确去重。用MD5哈希对所有文本去重。这一步很快500万条数据大概几分钟就能跑完。第五步近似去重。用MinHash对去重后的数据再做一次近似去重。这一步比较慢500万条数据大概要跑几个小时。我一般会先在小样本上测试阈值确定合适的参数后再全量跑。第六步质量过滤。用训练好的fasttext分类器给每条文本打质量分低于阈值的丢弃。同时用关键词黑名单过滤不安全内容。第七步配比混合。按照前面说的比例从各个来源抽取相应数量的数据混合成一个大的数据集。第八步格式化输出。用前面写的format_for_llamafactory函数把数据转成JSONL格式每条文本控制在4096个字符以内。第九步抽样验证。随机抽1000条数据人工检查质量。同时用LLamaFactory跑一个小的预训练任务看loss曲线是否正常。整个流程跑下来500万条原始数据最终能剩下大概300万条高质量数据压缩比在60%左右。这个比例是正常的如果压缩比太高比如只剩10%说明清洗规则太严了可能误删了大量有价值的数据。3.3 关键参数的计算与选择在数据处理过程中有几个参数需要仔细计算和选择MinHash的哈希函数数量。这个参数决定了去重的精度和速度。哈希函数越多精度越高但计算越慢。我一般用128个哈希函数在精度和速度之间取得平衡。如果你对精度要求特别高可以用256个但速度会慢一倍。质量分类器的阈值。fasttext分类器输出的是概率值我需要设定一个阈值来决定哪些文本保留。我一般会先人工标注500条数据作为验证集然后画ROC曲线找到F1分数最高的阈值点。这个过程听起来麻烦但一次调好后面就不用反复调了。最大序列长度的选择。LLamaFactory支持的最大序列长度取决于你的显存大小。我一般用4096因为大部分中文文本的段落长度都在这个范围内。如果你的显存比较小可以用2048但要注意截断可能会损失一些长距离依赖信息。数据配比的具体数值。这个没有标准答案需要根据你的模型定位来定。我的经验是先按通用配比跑一版然后看模型在验证集上的表现哪个领域表现差就增加哪个领域的数据比例。这个过程可能需要迭代两三次。3.4 用LLamaFactory跑通预训练任务数据准备好之后用LLamaFactory跑一个小的预训练任务来验证数据质量。我一般会用一个小模型比如Qwen-0.5B或者TinyLlama先跑几百步看loss曲线和生成效果。LLamaFactory的配置文件大概长这样model_name_or_path: Qwen/Qwen2-0.5B stage: pt do_train: true dataset: my_pretrain_data dataset_dir: data template: default cutoff_len: 4096 max_samples: 100000 preprocessing_num_workers: 8 output_dir: outputs/pretrain_test logging_steps: 10 save_steps: 500 learning_rate: 1e-4 lr_scheduler_type: cosine warmup_ratio: 0.03 num_train_epochs: 1 per_device_train_batch_size: 4 gradient_accumulation_steps: 8这里有几个参数需要特别注意cutoff_len要和数据处理时的最大长度一致否则会出现截断不一致的问题。preprocessing_num_workers根据你的CPU核心数来设一般设成核心数的80%左右。learning_rate预训练阶段一般用1e-4到5e-5比SFT阶段要大一些。warmup_ratio设0.03左右让学习率慢慢升上去避免一开始就震荡。跑起来之后重点看loss曲线。正常的预训练loss应该是一个平滑下降的曲线如果出现剧烈震荡或者突然上升大概率是数据有问题。我遇到过loss突然飙升的情况排查后发现是某批数据里混入了大量重复文本模型在那批数据上过拟合了。4. 常见问题与排查技巧实录4.1 数据清洗中的典型问题与解决方案在实际操作中我踩过的坑比想象的多。下面整理几个最常见的问题和我的解决方法问题现象可能原因排查方法解决方案分词后出现大量UNK编码不统一或包含特殊字符抽样检查原始文本编码统一转UTF-8过滤控制字符模型生成重复内容训练数据中重复文本过多统计n-gram重复率加强去重提高MinHash阈值loss曲线震荡剧烈数据质量参差不齐按来源分别统计loss分来源过滤提高质量阈值模型偏向某个领域数据配比失衡统计各领域token占比调整采样比例增加弱势领域数据训练速度突然变慢数据中存在超长文本统计文本长度分布截断超长文本设置最大长度限制这个表格里的每一个问题我都实际遇到过其中“模型生成重复内容”是最常见的。有一次我偷懒没做近似去重结果模型训练完之后不管输入什么输出都是“感谢您的提问我会尽力回答您的问题”。后来加了MinHash去重问题就解决了。4.2 去重环节的独家避坑技巧去重看起来简单但有几个细节很容易被忽视第一个细节是去重的粒度。我一般按段落去重而不是按整篇文档去重。因为很多文档只有部分段落是重复的按整篇去重会漏掉这些部分重复。按段落去重能更彻底地消除重复但要注意不要把一个文档内部的正常重复也去掉。第二个细节是去重的顺序。我习惯先做精确去重再做近似去重。因为精确去重速度快能先去掉一大批完全相同的文本减轻后面近似去重的压力。如果反过来近似去重会把精确重复的文本也当成近似重复处理浪费计算资源。第三个细节是去重后的采样。去重之后各个来源的数据量会发生变化。比如新闻数据可能被去掉了80%而书籍数据只去掉了20%。这时候如果还按原来的比例采样新闻数据的占比就会大幅下降。我一般会在去重后重新统计各来源的数据量然后按新的比例重新配比。提示去重不是一劳永逸的。每次新增数据源都要重新跑一遍去重流程。我见过有人把新数据直接追加到已去重的数据集里结果引入了大量重复模型训练效果大打折扣。4.3 质量过滤的实操心得质量过滤是最主观的环节不同的人对“高质量”的定义不一样。我的一般原则是宁可少一点也要好一点。预训练数据少几百万条对模型影响不大但混入大量低质量数据会严重拖累模型表现。我常用的质量评估维度包括语言通顺度用困惑度衡量困惑度越低越通顺。信息密度用停用词占比衡量停用词占比太高的文本信息密度低。重复率用n-gram重复率衡量重复率太高的文本质量差。长度太短和太长的文本都要谨慎处理。这几个维度可以组合成一个综合质量分然后按分数排序取前N%的数据。我一般会保留质量分在前70%的数据后30%的丢弃。这个比例可以根据你的数据总量来调整数据多就严一点数据少就松一点。4.4 与LLamaFactory配合的注意事项LLamaFactory虽然好用但有几个地方需要特别注意数据路径配置。LLamaFactory默认从data目录读取数据你需要在dataset_info.json里注册你的数据集。这个文件很容易写错特别是file_name字段必须和实际文件名完全一致包括扩展名。模板选择。预训练阶段一般用default模板不需要特殊处理。但如果你用的是对话数据做预训练可能需要用qwen或者llama2模板。模板选错了会导致数据格式不对训练效果大打折扣。多卡训练的数据分片。如果你用多卡训练LLamaFactory会自动做数据分片。但要注意分片是在文件级别做的如果你的数据文件只有一个那分片效果可能不理想。我一般会把数据拆成多个文件每个文件大概10万条左右这样多卡训练时负载更均衡。训练中断后的恢复。LLamaFactory支持从checkpoint恢复训练但要注意数据加载器的状态可能不会完全恢复。如果训练中断后恢复最好检查一下loss曲线是否连续。我遇到过恢复后loss突然跳变的情况后来发现是数据加载器的随机种子没设对。4.5 数据质量验证的实用方法数据准备好之后怎么验证质量我一般用三种方法第一种是人工抽检。随机抽1000条数据一条一条看统计各类问题的比例。这个方法最直接但最耗时。我一般只在关键节点做比如数据配比调整之后。第二种是小模型验证。用一个小模型比如0.5B参数在数据上跑几百步看loss曲线和生成效果。如果loss下降平滑生成内容通顺说明数据质量不错。如果loss震荡或者生成内容重复说明数据有问题。第三种是下游任务验证。如果你有下游任务的验证集可以直接用预训练后的模型跑一下看效果提升是否明显。这个方法最准确但需要等预训练完成才能做。我一般会结合前两种方法在数据处理阶段就用小模型快速验证等数据确定后再跑完整的预训练。4.6 常见问题速查表最后整理一份速查表方便遇到问题时快速定位问题排查方向快速解决数据加载报错JSONL格式是否正确用json.loads逐行验证训练loss不下降学习率是否太小尝试提高学习率到5e-5生成内容重复数据去重是否彻底加强MinHash去重显存溢出序列长度是否太长降低cutoff_len到2048训练速度慢数据预处理是否耗时增加preprocessing_num_workers模型偏向英文中英文配比是否失衡调整中英文数据比例验证集效果差训练数据是否过拟合增加数据多样性降低重复率这份速查表是我在实际项目中慢慢积累的基本上覆盖了80%的常见问题。剩下的20%需要具体问题具体分析但只要你把数据处理的每个环节都做扎实了大部分问题都不会出现。我个人在实际操作中的体会是预训练数据集构建这件事慢就是快。花三天时间认真清洗数据比花三天时间调模型参数带来的提升要大得多。很多人急于求成数据随便处理一下就开跑结果训练出来的模型各种问题回头再改数据浪费的时间更多。把数据这一关把住了后面的训练和调优都会顺利很多。