ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

预训练数据集构建全流程:从数据清洗到LLaMAFactory实战

预训练数据集构建全流程:从数据清洗到LLaMAFactory实战 1. 预训练数据集到底在训练流程里扮演什么角色很多人做大模型训练一上来就盯着模型结构、学习率、LoRA 秩这些参数结果跑出来的模型要么答非所问要么翻来覆去说车轱辘话。我踩过最大的坑其实不在训练脚本里而在数据这一层。预训练数据集构建这件事说白了就是给模型准备“教材”。教材选得烂老师再牛也教不出好学生。这一篇我就把预训练数据集从零到可用的完整流程拆开讲包括为什么要这么设计、每一步具体怎么落地、以及那些文档里不会写的坑。先把概念对齐一下。预训练指的是让模型在海量无标注或弱标注文本上学习语言规律、世界知识和推理模式的阶段它决定了模型的“底子”。而微调是在预训练模型基础上用少量高质量指令数据做定向调整。两者对数据的要求完全不同预训练要的是规模大、覆盖广、质量稳微调要的是精准、干净、格式统一。很多人把这两件事的数据混为一谈拿微调数据的标准去筛预训练语料结果数据量直接砍到十分之一模型根本学不动。那预训练数据集构建具体要解决什么问题我总结成三件事第一从哪搞到原始文本第二怎么把脏数据洗成干净数据第三怎么把清洗后的数据组织成训练框架能直接吃的格式。这三件事听起来简单但每一件都有大量细节。比如清洗环节你去掉 HTML 标签的时候顺手把代码块里的尖括号也删了模型后面写代码就会缺胳膊少腿。这种问题只有真正跑过一遍才会发现。适合谁来参考这篇内容如果你正在用 LLaMAFactory 这类框架做预训练或者继续预训练手头有一堆原始文本但不知道怎么处理或者你清洗完数据发现训练 loss 降不下去、模型输出质量差那这篇就是写给你的。哪怕你只是想做领域继续预训练比如拿医疗、法律、代码语料去增强基座模型这套流程同样适用。下面我按实际操作的顺序从设计思路讲到落地实现再讲排查技巧。2. 数据集构建的整体设计与方案选型2.1 为什么预训练数据要分“来源层”和“加工层”我见过太多人把数据构建当成一个脚本搞定的事结果代码越写越乱最后自己都说不清哪份数据是哪来的。正确的做法是把整个流程拆成两层来源层负责收集原始文本加工层负责清洗、去重、格式化。这两层之间用统一的中间格式比如 JSONL隔开好处是任何一层出问题都能单独替换不会牵一发动全身。来源层要考虑的是数据的多样性和合法性。多样性指的是语料要覆盖不同领域、不同文体、不同长度。全是新闻的语料训出来的模型写不了对话全是代码的语料训出来的模型写不了散文。合法性这块不用我多说自己收集的语料要确保来源合规公开数据集要看清 license。加工层要考虑的是可复现性同样的原始数据跑两遍必须得到同样的结果所以清洗规则要写成确定性的代码不能靠人工随机抽检。2.2 方案选型为什么我最终选了 LLaMAFactory 的数据格式市面上做预训练数据处理的方案不少有自己写脚本的有用 HuggingFace datasets 库的也有直接用 LLaMAFactory 内置数据管道的。我试过几种最后稳定在 LLaMAFactory 这套上原因有三个。第一格式统一。LLaMAFactory 对预训练数据的要求很明确就是 JSONL 格式每行一个 JSON 对象核心字段是text。这个格式简单到不会出错而且和它后面的训练脚本无缝衔接。你不需要在训练前再做一次格式转换省掉一个容易出 bug 的环节。第二和微调流程共用一套工具链。你做完预训练大概率还要做微调如果两边用不同的数据格式和不同的框架光是环境切换就够烦的。LLaMAFactory 同时支持预训练和微调数据准备、训练配置、模型导出都在一个体系里维护成本低很多。第三社区活跃坑有人踩过。这一点很实际。你用一个小众框架遇到问题搜都搜不到LLaMAFactory 的 issue 区和讨论区里预训练数据相关的问题基本都有人问过省下大量自己摸索的时间。当然它也不是没缺点。LLaMAFactory 对数据格式的校验比较严格字段名写错、编码不对、有空行都会直接报错。但这其实是好事早报错早发现总比训练到一半崩了强。2.3 数据规模与配比的取舍逻辑预训练数据要多少才够这个问题没有标准答案但有几个经验值可以参考。如果你是从头预训练一个小模型比如 1B 以下几十 GB 的清洗后文本是起步量。如果是做领域继续预训练在原模型基础上增强某个领域的能力那领域数据占比建议在 10% 到 30% 之间剩下的用通用语料填充防止模型遗忘原有能力。配比这件事我踩过坑。有一次我拿 90% 的代码语料去做继续预训练结果模型代码能力确实上去了但日常对话能力明显退化问它“今天天气怎么样”它给我返回一段 Python。后来我把代码比例降到 40%通用中文语料补到 60%两边能力就平衡了。所以配比不是拍脑袋定的要看你最终想要模型具备什么能力然后反推数据比例。还有一个容易被忽略的点是数据长度分布。如果你的语料全是短文本比如微博、评论模型学不会长距离依赖处理长文档就吃力。反过来全是长文档模型对短指令的响应又会变迟钝。理想情况下长度分布要尽量接近你实际使用场景的输入长度分布。3. 核心细节解析与实操要点3.1 原始语料收集的渠道与注意事项原始语料的来源大致分几类公开数据集、自有文档、网页抓取、书籍论文。每一类的处理方式不一样。公开数据集是最省事的比如中文的维基百科 dump、各种开源书籍语料、代码数据集等。用这类数据要注意两点一是license有些数据集标注了仅限研究使用你拿去做商业产品就有风险二是版本同一个数据集不同时间下载的内容可能不一样最好记录下你用的版本号和下载时间方便复现。自有文档这块常见的是公司内部的技术文档、产品手册、客服对话记录。这类数据质量通常很高但格式五花八门有 Word、PDF、Markdown、Confluence 导出页等等。我的做法是统一先转成纯文本或 Markdown再进清洗流程。转的时候注意保留结构信息比如标题层级、列表符号这些对模型理解文档结构有帮助。网页抓取要格外小心。一是要遵守目标网站的 robots 协议二是抓下来的 HTML 里噪音极多导航栏、广告、页脚、评论区混在一起清洗难度大。如果非要用网页数据建议优先选那些本身就以文本为主的站点比如技术博客、文档站。书籍和论文的数据质量高但版权问题要自己把握好。论文的话摘要和公开预印本相对安全全文就要看具体授权了。3.2 数据清洗的核心步骤与参数清洗是预训练数据构建里最耗时间也最影响效果的环节。我把它拆成六步每一步都有具体的判断标准。第一步去除非文本内容。HTML 标签、CSS 样式、JavaScript 代码、Base64 图片编码这些都要去掉。用正则表达式处理 HTML 标签时注意不要误伤正文里的数学公式和代码片段。我的经验是先用成熟的解析库比如 BeautifulSoup把 HTML 转成纯文本再用正则做二次清理比纯正则硬怼靠谱得多。第二步统一编码和换行。所有文本统一成 UTF-8 编码换行符统一成\n。Windows 的\r\n和 Mac 老版本的\r都要转掉否则后面按行处理会出问题。这一步看着简单但不做的话JSONL 里会出现各种诡异的换行训练脚本读的时候直接报错。第三步去除重复和近重复。完全重复的文本直接去重这个用哈希就行。近重复的要用相似度算法比如 MinHash 或者 SimHash。我一般用 MinHash阈值设在 0.8 左右也就是相似度超过 80% 的视为重复。阈值设太低会误删正常内容设太高又去不干净。这个值可以根据你的语料特点微调。第四步过滤低质量文本。什么叫低质量我的判断标准是长度过短少于 50 个字符、特殊符号占比过高超过 30%、重复字符过多比如“哈哈哈哈哈哈”、乱码比例高。这几条规则组合起来能过滤掉大部分垃圾数据。但要注意代码语料里特殊符号本来就多过滤规则要针对不同语料类型分别设置。第五步敏感信息处理。手机号、身份证号、邮箱、银行卡号这类个人信息该脱敏的脱敏该删除的删除。这一步不仅是合规要求也能防止模型学到这些模式后乱输出。用正则匹配加替换就能搞定大部分场景。第六步格式规范化。把清洗后的文本统一成 Markdown 风格标题用#列表用-代码块用三个反引号包裹。为什么选 Markdown因为现在主流大模型对 Markdown 的理解都很好而且 Markdown 本身可读性强人工抽检的时候一眼就能看出结构对不对。3.3 数据去重的策略与阈值选择去重这件事值得单独拿出来讲因为它对训练效果的影响比很多人想象的大。重复数据会让模型过度拟合某些模式表现为对某些话题特别能说对其他话题反应迟钝。完全重复好处理算 MD5 或者 SHA256 哈希一样的丢掉就行。难的是近重复比如同一篇新闻被不同网站转载改了几个词或者同一段代码在不同项目里出现变量名不一样。我用的是 MinHash LSH局部敏感哈希的方案。具体做法是先把每篇文档切成 shingle比如每 5 个词一组然后对每个 shingle 做哈希取最小的 N 个哈希值作为文档签名。两篇文档的签名相似度超过阈值就判为近重复。N 一般取 128 或 256阈值我设在 0.8。这里有个坑如果你的语料里有大量结构相似的模板文本比如电商商品描述、法律条款去重阈值设太低会把正常内容也删掉。我的建议是先用小样本跑一遍人工看看被删掉的内容是不是真的重复再决定最终阈值。3.4 数据格式转换与 LLaMAFactory 对接清洗完的数据要转成 LLaMAFactory 能直接读的格式。预训练数据的标准格式是 JSONL每行一个对象核心字段是text。看起来很简单但有几个细节要注意。第一编码必须是 UTF-8而且不能带 BOM。带 BOM 的文件在某些环境下会读出头几个字节乱码导致第一条数据解析失败。第二每行必须是合法的 JSON。文本里的双引号、反斜杠、换行符都要正确转义。手动拼 JSON 字符串很容易出错建议用编程语言的 JSON 库来序列化比如 Python 的json.dumps。第三不要有空行。有些清洗脚本会在文件末尾留空行LLaMAFactory 读到空行会报错。生成文件后可以用grep -c ^$检查一下有没有空行。第四字段名要完全匹配。LLaMAFactory 预训练数据默认读text字段你写成content或者raw_text它就不认。如果确实需要用别的字段名要在数据集配置里改prompt_column或者自定义数据处理函数。一个典型的 JSONL 行长这样{text: # 标题\n\n这是正文内容包含 Markdown 格式。\n\n- 列表项一\n- 列表项二}生成之后建议用head -n 5和tail -n 5各看一眼确认首尾数据正常。再用wc -l统计行数和预期数据量对一下差太多就说明中间有步骤出问题了。4. 实操过程与核心环节实现4.1 环境准备与依赖安装我假设你用的是 Linux 环境Python 3.10 以上。先建一个干净的虚拟环境避免和系统里的包冲突。python -m venv venv source venv/bin/activate pip install --upgrade pip然后装核心依赖。数据处理这块主要用到datasets、beautifulsoup4、lxml、datasketch做 MinHash 用、tqdm看进度。LLaMAFactory 本身也装一下后面训练要用。pip install datasets beautifulsoup4 lxml datasketch tqdm pip install llamafactory如果你的数据量很大超过 100GB单机内存可能扛不住这时候要考虑用 Spark 或者分片处理。我一般按 10GB 一片来切处理完再合并这样单机 32GB 内存也能跑。4.2 原始数据加载与初步探查拿到原始数据后别急着清洗先做一次探查。看看数据总量、平均长度、编码分布、有没有明显的乱码。这一步能帮你提前发现大问题比如下载的数据集解压出来是空的或者编码全是 GBK 不是 UTF-8。import os from collections import Counter def explore_data(data_dir): total_files 0 total_size 0 encodings Counter() for root, dirs, files in os.walk(data_dir): for f in files: path os.path.join(root, f) total_files 1 total_size os.path.getsize(path) try: with open(path, r, encodingutf-8) as fh: fh.read(1024) encodings[utf-8] 1 except UnicodeDecodeError: encodings[non-utf8] 1 print(f文件数: {total_files}) print(f总大小: {total_size / 1024 / 1024:.2f} MB) print(f编码分布: {encodings}) explore_data(./raw_data)如果发现大量非 UTF-8 文件先统一转码。用chardet检测编码再转成 UTF-8。转码的时候注意捕获异常有些文件可能损坏了读都读不了这种直接跳过并记录。4.3 清洗流水线的代码实现清洗流水线我写成一个可配置的类每一步是一个方法方便单独调试和替换。核心逻辑如下。import re import json from bs4 import BeautifulSoup from datasketch import MinHash, MinHashLSH class DataCleaner: def __init__(self, min_len50, max_special_ratio0.3): self.min_len min_len self.max_special_ratio max_special_ratio self.lsh MinHashLSH(threshold0.8, num_perm128) def remove_html(self, text): soup BeautifulSoup(text, lxml) return soup.get_text(separator\n) def normalize_whitespace(self, text): text text.replace(\r\n, \n).replace(\r, \n) text re.sub(r\n{3,}, \n\n, text) text re.sub(r[ \t]{2,}, , text) return text.strip() def is_low_quality(self, text): if len(text) self.min_len: return True special_chars len(re.findall(r[^\w\s\u4e00-\u9fff], text)) if special_chars / max(len(text), 1) self.max_special_ratio: return True if re.search(r(.)\1{10,}, text): return True return False def dedup(self, text, doc_id): m MinHash(num_perm128) for s in set(text[i:i5] for i in range(len(text)-4)): m.update(s.encode(utf8)) if self.lsh.query(m): return True self.lsh.insert(doc_id, m) return False def clean(self, text, doc_id): text self.remove_html(text) text self.normalize_whitespace(text) if self.is_low_quality(text): return None if self.dedup(text, doc_id): return None return text这段代码里几个参数值得说明。min_len50是过滤过短文本的阈值中文场景下 50 个字符大概是一两句话低于这个长度的文本信息量太少。max_special_ratio0.3是特殊符号占比上限超过 30% 的文本大概率是乱码或者表格残留。threshold0.8是近重复判定阈值前面说过可以根据语料特点调整。4.4 生成 LLaMAFactory 可用的 JSONL 数据清洗完的数据要写成 JSONL。我习惯先写到临时文件全部处理完再重命名避免中途出错留下半成品。import json from tqdm import tqdm def write_jsonl(texts, output_path): tmp_path output_path .tmp count 0 with open(tmp_path, w, encodingutf-8) as f: for text in tqdm(texts): if not text: continue record {text: text} f.write(json.dumps(record, ensure_asciiFalse) \n) count 1 os.rename(tmp_path, output_path) print(f写入 {count} 条数据到 {output_path})注意ensure_asciiFalse这样中文不会被转成\uXXXX的形式文件体积小很多人工查看也方便。写完之后用wc -l确认行数再用 Python 读前几行验证 JSON 合法性。wc -l pretrain_data.jsonl python -c import json with open(pretrain_data.jsonl, r, encodingutf-8) as f: for i, line in enumerate(f): if i 3: break obj json.loads(line) print(list(obj.keys()), len(obj[text])) 4.5 接入 LLaMAFactory 训练配置数据准备好之后在 LLaMAFactory 的dataset_info.json里注册你的数据集。假设你的文件叫pretrain_data.jsonl放在data目录下配置大概是这样{ my_pretrain: { file_name: pretrain_data.jsonl, columns: { prompt: text } } }然后在训练命令里指定dataset为my_pretrainstage设为ptpretrain 的缩写。如果是继续预训练stage也是pt只是加载的基座模型换成你已经有的模型。llamafactory-cli train \ --stage pt \ --model_name_or_path meta-llama/Llama-3-8B \ --dataset my_pretrain \ --template default \ --cutoff_len 4096 \ --max_samples 100000 \ --output_dir ./output/pretrain \ --per_device_train_batch_size 2 \ --gradient_accumulation_steps 8 \ --learning_rate 1e-5 \ --num_train_epochs 1 \ --lr_scheduler_type cosine \ --warmup_ratio 0.03 \ --logging_steps 10 \ --save_steps 500 \ --bf16 true这里cutoff_len设成 4096意思是超过这个长度的文本会被截断。预训练数据里如果有超长文档截断会丢失信息所以清洗阶段最好就把超长文档按段落切开每段控制在 4096 token 以内。learning_rate用 1e-5继续预训练时这个值比较稳太大容易把原模型能力冲掉。5. 常见问题与排查技巧实录5.1 数据清洗常见问题速查表问题现象可能原因排查方法解决方案训练 loss 不下降数据质量差或重复率高抽样人工检查统计去重前后数据量加强清洗提高去重阈值模型输出乱码编码不统一检查 JSONL 文件编码统一转 UTF-8去掉 BOM训练脚本报 JSON 解析错误有空行或非法 JSONgrep -c ^$查空行过滤空行用 json 库序列化模型只擅长某一领域数据配比失衡统计各领域数据占比调整配比补充通用语料显存溢出单条数据过长统计文本长度分布切分超长文本降低 cutoff_len去重后数据量骤减去重阈值过低人工检查被删数据提高阈值到 0.85 以上5.2 实操避坑经验坑一HTML 清洗误伤代码块。我一开始用正则re.sub(r[^], , text)去 HTML 标签结果代码块里的ListString被删成了List。后来改用 BeautifulSoup 解析并且对代码块做特殊标记保护才解决这个问题。如果你语料里有大量代码清洗前先把代码块提取出来单独处理。坑二去重把模板文本全删了。有一次处理法律合同语料去重阈值设了 0.7结果大量正常合同因为结构相似被删。后来我把阈值提到 0.9并且对短文本跳过近重复检测只做完全重复去重数据量才恢复正常。所以阈值不是越低越好要看语料特点。坑三JSONL 文件太大导致内存溢出。我试过用json.load一次性读整个 JSONL 文件文件才 5GB 内存就爆了。正确做法是逐行读、逐行处理用生成器而不是列表。写的时候也是逐行写不要先攒在内存里再一次性 dump。坑四忽略数据 license 导致合规风险。这个不是技术问题但比技术问题更严重。用任何公开数据集之前花五分钟看一下它的 license 和引用要求。有些数据集要求你引用论文有些禁止商业使用这些都要提前确认。坑五清洗完不做抽样检查。我见过有人清洗完直接开训跑了两天才发现数据里混了大量乱码。清洗完一定要随机抽 20 到 50 条人工看一眼确认格式、内容、长度都正常。这个时间投入绝对值得。5.3 数据质量评估的量化指标光靠人工抽检不够还要有一些量化指标来评估数据质量。我常用的几个指标平均长度清洗后文本的平均字符数太低说明过滤太狠太高可能有超长文档没切开。去重率去重删掉的数据量占总量的比例正常在 5% 到 20% 之间超过 30% 说明原始数据重复严重。特殊符号占比所有文本中特殊符号的平均占比超过 15% 要警惕。长度分布用分位数看P50、P90、P99 分别是多少P99 如果超过 cutoff_len 太多说明需要切分。这些指标可以在清洗脚本里顺手统计出来写到日志里。每次调整清洗参数后对比一下指标变化比凭感觉调参靠谱得多。5.4 小数据集快速验证流程如果你不想一上来就处理几百 GB 数据可以先拿一个小样本跑通全流程。我的做法是从原始数据里随机抽 1000 条走一遍清洗、去重、格式化、训练。训练用很小的步数比如 100 步看看 loss 曲线正不正常模型输出有没有明显问题。这个流程半天就能跑完能提前发现 80% 的问题。小样本验证通过后再把清洗脚本放到全量数据上跑。这时候要注意去重这步在小样本和大样本上的行为可能不一样因为 LSH 索引是逐步构建的数据量大了之后误判率会变化。所以全量跑完之后还是要再抽检一次。6. 数据构建完成后的检查清单数据构建完训练开跑之前我习惯过一遍这个清单。别嫌麻烦这几分钟能省下后面几小时的排查时间。JSONL 文件编码是 UTF-8 无 BOM每行是合法 JSON没有空行。字段名和 LLaMAFactory 配置里的prompt字段一致。随机抽 20 条数据人工看过格式和内容正常。统计了数据总量、平均长度、去重率指标在合理范围内。超长文本已经切分单条不超过cutoff_len。数据配比符合预期领域数据和通用数据的比例合理。原始数据和清洗后数据都做了备份清洗脚本和参数有记录。这套流程我用了大半年从最初的手忙脚乱到现在基本能稳定产出可用的预训练数据。最大的体会是数据这件事没有捷径每一步的细节都要抠。你省掉的每一步后面都会以模型效果差的形式还回来。但反过来数据做扎实了训练反而变成最简单的一环参数稍微调调就能出不错的结果。后面如果要做多模态预训练图像和文本的对齐、数据配比又是另一套逻辑那个等下次再聊。
返回列表