ARTICLE DETAIL

资讯详情

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

大模型预训练数据质量过滤实战:MindSpore管道构建与去重方案

大模型预训练数据质量过滤实战:MindSpore管道构建与去重方案 去年我在跑一批MindSpore大模型预训练任务时被一个数据问题折磨了将近一周loss曲线一路向下看起来一切正常但模型生成的文本却经常出现重复片段和不知所云的表达。查了很久才发现问题根本不在模型结构或超参数而是训练语料里混进了大量重复文档、机器翻译垃圾和乱码片段模型把脏数据背得滚瓜烂熟。从那次之后数据质量过滤正式成为我所有预训练流程里雷打不动的前置环节也逐步沉淀出一套能在MindSpore上直接落地执行的过滤方案。这篇文章主要分享三件事为什么预训练之前必须认真做数据质量过滤过滤到底要过滤哪些东西以及怎么用MindSpore的数据管道把整套过滤流程跑起来。内容面向正在做预训练、继续预训练或者微调前清洗数据的工程同学也适合刚接触大模型、想搞明白数据工程到底解决什么问题的初学者。1. 为什么要单独把数据质量拎出来1.1 低质量数据把模型带偏的真实场景早期做数据清洗时我犯过一个低级错误从多个公开来源抓取语料后只做了简单的HTML标签剥离和空行删除就直接送进了预训练管道。数据集里有一条问答型样本被重复抓取了500多次模型训练结束后你问它相关的问题它几乎只会背诵那一条固定答案其他答法完全不会生成。这背后的原因并不复杂。大模型预训练本质是让模型学习数据的统计规律如果一份文本在语料里反复出现模型会认为这段文字在真实世界中出现的概率极高于是把大量模型容量花在“死记硬背”上而不是学习通用的语言模式。类似的还有跨文档重复同一篇新闻稿被几十个网站转载内容只有标题或结尾稍微变了一下全量去重前我完全没意识到这类数据的占比居然能超过15%。另一个容易被忽视的问题是机器翻译垃圾和乱码。网络上存在大量通过机器翻译生成的中文内容语法勉强通顺但表达僵硬伴随大量错误的代词和量词用法。模型学多了这类文本生成结果就会带上一股“翻译腔”。而乱码文本更危险它会把模型的token分布搅乱直接拉低下游任务的收敛速度。1TB粗清洗语料和300GB精过滤语料模型最终效果哪个更好我自己的实验结果是精过滤版本在下游平均分上反超了约6个百分点。这说明数据质量对模型能力的约束往往比想象中还要明显。1.2 大模型预训练语料里常见的脏数据我在项目里把常见的脏数据归纳成了七类每类都有比较典型的特征和对应的过滤手段。污染类型典型表现对训练的影响过滤手段行级重复同一条文本在数据集中出现多次模型背诵、过拟合明显精确指纹去重跨文档重复转载内容高度相似字面有差异有效信息密度大幅下降MinHash近似去重乱码噪声控制字符、异常Unicode、解码错误语言模型生成撕裂正则规则字符熵检查机器生成痕迹翻译腔、模板化句式、表达不自然生成流畅度下降PPL打分、质量分类器语言混杂中文文本夹杂大量英文和乱码符号中文表示被干扰字符集比例检测超短无意义片段导航栏、标签、版权声明等网页杂物引入大量无效噪声长度规则URL占比规则敏感违规内容色情、暴力、赌博等不良信息合规风险影响生成安全敏感词库分类过滤每条规则单独看都很简单难的是如何组合起来在不误杀正常样本的前提下把污染样本尽可能剔除。这个平衡问题贯穿整个过滤管线设计。1.3 过滤方案的整体设计目标数据清洗不是简单跑一个脚本就完事我给自己定了四个目标后续所有环节都在围绕这四个目标展开。效率优先过滤环节要能并行处理整体吞吐不能成为预训练前的瓶颈。可回滚可统计每个过滤阶段都要留下样本数和过滤比例一旦效果变差能快速定位是哪一步出了问题。保留多样性过滤后语料不能只剩一种风格要防止模型在口语化、垂直领域、多轮对话等场景下泛化能力受损。分层渐进先做低成本规则过滤再做语义打分最后做去重各环节相对独立又能串联成流水线。这套顺序我后来叫它“先粗后细再查重”。规则过滤先解决80%肉眼可见的垃圾模型打分处理规则覆盖不了的语义问题去重解决数据冗余最后再用分布控制保证语料不过度偏斜。2. 数据质量过滤的四个核心维度2.1 启发式规则过滤低成本干掉大部分显性垃圾大模型预训练的数据量动辄上百GB甚至几TB规则过滤阶段必须简单、快速、可解释。我在项目里用到的主要规则有五类。文本长度规则低于50个字符的样本基本没有学习价值高于8192字符的样本训练窗口放不下而且大概率是爬虫拼接出来的错误页面。中英文混排场景下长度阈值应该按字符数而非token数来定因为tokenizer的行为在不同语言上差异很大。字符重复率用来识别“哈哈哈哈哈哈”或者“这是一个测试这是一个测试”这类无意义内容。我常用n-gram重复度实现把文本切成长度为4的连续子串统计最高频子串出现的比例如果前几个高频子串占比太高就判定为重复型垃圾。标点与符号密度日志文件、代码片段、乱码文本的标点密度通常远高于自然语言。一段合理的中文文本标点占比一般在5%到15%之间超出太多值得警惕太低又可能是无标点的纯文本堆砌。URL与链接占比网页抓取的文本经常携带大量链接地址模型会把“每句话后面跟着一串http开头的字符串”当作常见模式学进去严重影响生成质量。HTML标签和特殊字符div、nbsp;、\u200b这类残余物要在清洗阶段处理干净清理后仍然残留大量标签碎片的文本直接过滤。规则阈值最忌讳拍脑袋。我习惯先挑一万条自己人工确认过的高质量文本跑一遍上述所有特征取P10到P90的分位数区间作为阈值边界。这样做的好处是阈值有据可依后面每次调整规则都能拿这份基线统计做参照减少主观性。2.2 语义质量打分让模型判断“像不像人写的”规则过滤处理不了机器翻译腔、内容重复但字面不同、通篇空话这类语义问题。到了这个层面需要引入模型的判断力。我最常用的两个信号是困惑度和质量分类器。困惑度PPL用一个小规模语言模型计算文档的平均负对数似然损失再取指数。PPL越高说明这段文本越不符合自然语言的分布特征。中文高质量语料的PPL通常在50到150之间机器翻译内容很容易飙到300以上而乱码文本经常破千。实际使用中PPL有一个明显短板它对重复拼凑的长文本不敏感。一段话反复说三遍和无意义的重复语言模型照样能给出较低的PPL因为预测难度确实不高。所以PPL需要和n-gram重复率这类信号搭配使用不能单打独斗。质量分类器是另一个方向。人工标注一部分高质量和低质量样本再自动构造一批噪声负样本训练一个小型Transformer做二分类输出0到1的质量分。这个分类器的训练数据可以来自规则过滤被剔除和保留的样本相当于让规则先做一次自动标注再用模型去泛化规则覆盖不到的情况。PPL打分模型本身完全可以基于MindSpore训练。打分阶段放到离线任务里跑不占用训练集群这样即便一个亿级参数的模型对千万条文本打分也只是时间问题不会影响主训练流程的进度。2.3 大规模去重减少数据冗余的关键文本去重是大模型预训练公认不可跳过的一环也是我踩坑最密集的部分。最常用的方案是MinHash和SimHash两者的适用场景有明确差异。维度MinHash LSHSimHash核心思路文本转成shingle集合多个哈希函数取最小值生成签名分桶后桶内精确比较特征词加权哈希后累加生成64位或128位指纹相似度判定Jaccard相似度阈值可控海明距离一般小于等于3视为相似适合场景精确去重、识别转载和拼凑内容近似去重、指纹比对速度极快工程复杂度需要理解分桶策略稍复杂更简单但误判稍高我的实际做法是两阶段去重。第一阶段对全文做MinHash去重Jaccard相似度大于0.8的文档直接视为重复第二阶段对剩余语料做精确去重处理完全相同的文档。第一阶段解决“高度相似但不完全相同”的转载和洗稿第二阶段解决“一模一样但可能被不同文件重复收录”的原始重复。去重和MindSpore框架本身没有绑定关系我在项目里用Spark跑MinHash因为分桶后天然可以分布式并行。如果你没有Spark环境用Python多进程也能处理千万级文本只是要把签名矩阵分块保存不要全部塞进内存。2.4 多样性与分布控制过滤做过头是新手最容易犯的错误也是我自己的惨痛教训。第一次迭代时我把过滤规则拉得很严PPL阈值卡得很低结果大部分口语化问答、网络小说、短对话都被误杀了留下的语料几乎只剩规范的新闻稿和百科条目。模型训出来之后一本正经一旦有人用口语化方式提问回答质量就急剧下降。后来我在过滤流程后面加了一层分布控制。先按来源字段或者用轻量分类器给文档打上领域标签统计百科、新闻、问答、小说、对话等类别的占比。如果过滤后某一类占比偏离目标太多就对保留样本做欠采样或加权重采样让最终语料分布接近期望分布。加了这个环节之后第二次迭代时我专门导出了过滤前后的类别占比对比发现问答类数据之前被过滤掉了约35%调整PPL阈值和长度规则后保留率提升了20%左右模型在对话相关任务上的表现立刻就有了肉眼可见的变化。数据过滤不能只盯着质量多样性同样是模型能力的保障。3. 在MindSpore上落地过滤管线3.1 整体管线结构我设计的过滤管线以离线为主整体分六步读取原始数据规则过滤PPL打分MinHash去重分布控制重采样最后将过滤结果写入MindRecord供训练直接加载。原始数据我建议统一成JSONL格式一行一条记录至少包含id、text、source三个字段。source字段在分布控制阶段会派上大用场别省。MindRecord是最终输出格式MindSpore对它有底层读取优化内存占用和加载速度都比直接读JSONL好很多。而且MindDataset直接支持shuffle、分布式分片训练脚本里省掉不少格式转换的代码。3.2 用MindSpore Dataset API实现规则过滤器MindSpore的mindspore.dataset模块提供了map和filter操作非常适合做逐条文本处理。一个基本的规则过滤管道长这样import json import re from collections import Counter import mindspore.dataset as ds _URL_RE re.compile(rhttps?://\S|www\.\S) _HTML_RE re.compile(r[^]) _BAD_CHAR_RE re.compile(r[\x00-\x08\x0b\x0c\x0e-\x1f]) def clean_doc(text): text _URL_RE.sub(, text) text _HTML_RE.sub(, text) text _BAD_CHAR_RE.sub(, text) return re.sub(r\s, , text).strip() def doc_passes(text, min_len80, max_len8192, min_zh_ratio0.4, max_dup_ratio0.5): if len(text) min_len or len(text) max_len: return False zh sum(1 for ch in text if \u4e00 ch \u9fff) if zh / max(len(text), 1) min_zh_ratio: return False sub_count Counter(text[i:i4] for i in range(len(text) - 3)) top sum(sorted(sub_count.values(), reverseTrue)[:5]) if top / max(len(sub_count), 1) max_dup_ratio: return False return True def generator(): for file in file_list: with open(file, r, encodingutf-8) as f: for line in f: record json.loads(line) yield record[id], clean_doc(record[text]), record.get(source, ) dataset ds.GeneratorDataset(generator(), column_names[id, text, source]) dataset dataset.filter( predicatelambda id, text, source: doc_passes(text), input_columns[id, text, source], num_parallel_workers16, )这段代码有几个关键点。filter的predicate必须返回Python原生bool如果你在函数里返回的是np.bool_管道会报类型相关错误。我在实际项目里被这个问题坑过一次后来所有过滤函数的最后一行都统一写return bool(...)。另一个坑是clean_doc被generator和doc_passes重复调用等于一份文本清洗了两遍。工程优化时我会先把清洗结果单独落一版中间文件后续所有过滤环节都从这个清洗后的版本读取节省大量重复计算。3.3 并行化与性能调优数据量一大单进程跑过滤必然成为瓶颈。我从实际调试中总结出四个最有效的调优手段。num_parallel_workers设为CPU核数的2到4倍不要无脑拉满否则磁盘IO会成为新的瓶颈。buffer_size可以适当调大比如4096让管道在内存里多积压一些样本减少各阶段等待。重计算操作不要塞进map回调同步执行。最典型的错误就是在map里加载PPL模型推理每处理一条样本就要加载一次模型管道直接卡死。纯Python函数尽量开启python_multiprocessingTrue用多进程并行替代单进程速度提升非常明显。规则过滤阶段在32核机器上能做到每秒处理几千条短文本实际瓶颈几乎都出现在JSONL解析上。所以如果数据量到了上亿条强烈建议原始数据先转成Parquet或者二进制格式解析开销能降一个量级。3.4 在VSCode里用MindSpore内核调试过滤脚本拿到海量数据之前先别急着全量跑。我在调试阶段的做法是在VSCode里配置好MindSpore环境新建Jupyter Notebook把内核切换到对应的Python解释器然后手动构造二三十条脏样本和干净样本逐条跑过滤函数看输出。Notebook每个单元格跑完都能看到中间结果等于一份隐形的调试日志比直接写死脚本然后反复print要直观得多。这种方式对调整过滤阈值特别实用。比如发现规则把“短小精悍的问答”误杀了就在Notebook里临时修改最小长度参数重新跑一遍确认无误后再同步回正式脚本。数据过滤是规则密集型的开发任务这种交互式调试能省下大量试错时间。4. 实操过程与关键环节实现4.1 准备数据与加载我的原始数据统一成JSONL格式一行一条字段结构如下{id: 10001, text: ……, source: news}如果数据量在百万级以下GeneratorDataset直接读没有问题。到了千万级以上建议先把原始文件切分成多个shard每shard一个文件减少单文件的IO压力。最终通过过滤的数据统一转成MindRecord后续训练用mindspore.dataset.MindDataset来读。4.2 从零实现一个完整过滤脚本在一次实际项目中我把规则过滤、PPL打分、去重三个阶段串成了完整流程。规则过滤脚本已经在3.2节给出重点是PPL打分的实现方式。PPL计算可以分为离线批量打分和管道内过滤两步。离线打分脚本的核心逻辑是import mindspore as ms from mindspore import ops def compute_ppl(model, tokenizer, text): input_ids tokenizer(text)[input_ids] logits model(ms.Tensor(input_ids)) log_probs ops.log_softmax(logits, axis-1) loss -log_probs[:, :-1].gather(1, input_ids[:, 1:].expand_dims(-1)).mean() return ops.exp(loss).asnumpy().item()这段代码只是示意实际工程中要按batch推理一次处理数百条文本并且把结果保存成id - ppl的映射文件。模型加载一次常驻内存避免每条文本都重新加载权重。Multi-阶段串联的完整流程如下第一遍跑规则过滤输出保留样本的中间文件第二遍对中间文件批量计算PPL产出打分映射表第三遍读中间文件用filter根据PPL阈值做二次过滤第四遍在分布式环境跑MinHash去重第五遍做分布控制重采样最终写入MindRecord。每个阶段都会把输入和输出条数写进统计日志方便后面对照。4.3 将过滤后的高质量数据写入MindRecord所有过滤环节跑完后把数据写进MindRecord是收尾的关键一步。代码如下from mindspore.mindrecord import FileWriter writer FileWriter(file_nametrain_data.mindrecord, shard_num8) schema { id: {type: int32}, text: {type: string}, source: {type: string} } writer.add_schema(schema, descfiltered corpus) for sample in filtered_iter(): writer.write_row(sample) writer.commit()写入时有几个细节值得注意。schema里text字段的类型定义成string传入时必须用Python字符串而不是bytes。shard_num的取值最好能和后续训练的卡数对齐比如8卡训练就分成8个shard否则加载时容易出现文件大小不均。写完MindRecord之后我在训练启动前会先跑一段抽样读代码验证格式。直接用MindDataset读几行出来看一眼字段和内容确认没有任何问题再进正式训练。这一步虽然简单但能避免训练启动后才发现的低级错误。4.4 对过滤结果做质量抽查过滤管线跑完不代表工作结束我一定会做一份质量抽查报告。报告包含四项内容每个过滤环节的输入条数、输出条数和过滤比例文本长度分布和PPL分布直方图随机抽500条保留样本做人工评估估算误杀率按source字段统计各来源的保留率。这份报告的价值在于它能帮你发现规则是不是有偏。我曾经遇到过一个问题URL占比规则把一小部分包含代码块的技术文章整篇干掉了原因是代码块里的路径和链接拉高了URL占比。如果没做人工抽查我根本意识不到这个规则对技术类语料的杀伤力。数据过滤是一场需要反复迭代的工程统计报告留存下来每次调整规则都能有据可依。5. 常见问题与排查技巧实录5.1 Dataset管道OOM跑超大语料过滤时OOM是最常见的问题。我总结的排查顺序是这样的buffer_size设太大导致内存积压过多先调到1024或512试试map回调里保留了全局大对象比如词典或模型参数这类对象要么挪到外部进程要么每次进程加载一次而不是每条数据加载一次GeneratorDataset一次性把过多文件读进内存改成每个文件单独迭代worker数量开太多导致每个进程都复制一份Python环境32核机器开到8到16就够了。5.2 filter回调返回类型报错MindSpore的filter对predicate返回值类型要求严格返回np.bool_偶尔会报类型错误。解决方法是在所有返回路径上统一包一层bool()。另外一个隐蔽问题是函数分支漏写返回值导致返回None。写长函数时每个分支都要仔细检查最好用and/or把多个判断合并成一条表达式从结构上减少漏返回值的情况。5.3 去重任务在超大语料上跑不动MinHash和SimHash在超大数据集上都会遇到内存问题。我的解决办法是分shard先做局部去重再做跨shard去重避免全量比较用banding策略粗筛候选对只在桶内计算精确相似度签名和指纹用numpy数组不要用Python list内存节省非常明显指纹落盘存储不要全部放进内存。去重失败的项目我见过不少几乎都是因为想一步到位构建全量相似度矩阵结果内存直接爆掉。分桶分批是最稳妥的做法。5.4 PPL打分模型推理太慢PPL打分成为瓶颈时核心优化思路是批量推理。一次把一个batch的文本喂给模型而不是逐条推理。同时在管道设计上先用规则过滤掉明显低质量的样本只对存疑样本跑PPL。我的实际项目里PPL打分只覆盖全量样本的30%规则过滤帮我砍掉了70%的工作量。5.5 过滤后训练出现震荡过滤前后语料分布变化过大训练初期loss会出现波动甚至反弹。这个问题的处理方式是渐进式切换先按旧分布训练一定比例的step再平滑切换到新语料。在MindSpore里可以在训练脚本中动态替换MindDataset的数据源同时保存检查点一旦效果异常可以回滚到切换前的模型状态。5.6 问题排查速查表现象直接原因快速解法训练loss下降但生成质量差重复数据过多或低质量数据残留检查去重效果重新抽样看文本数据管道OOMbuffer、worker或大对象占用过高调小buffer减少进程数filter报类型错误返回了np.bool_或None统一bool()转换补齐分支去重跑不动内存占用过大、算法不匹配分桶、两级去重、指纹落盘PPL过滤太慢单条推理、样本量过大批量推理、先粗过滤再打分过滤后训练震荡分布突变、有效样本减少渐进切换、检查点回滚6. 我对这套方案的个人体会数据清洗这块工作我从一开始的“不愿意做”变成了现在“不敢不做”。每次调整过滤规则我都会顺手把过滤统计报告存成带时间戳的文件方便后面做实验对比时精确知道某个规则改动对应的是哪个版本的数据集。以前我懒得留这些记录结果有一次换了规则之后模型效果明显变差排查半天才定位到是数据分布变化导致的白白浪费了几天时间。数据质量过滤从来没有一劳永逸的版本规则需要随着语料来源和应用场景不断迭代。把整套流程跑通以后后续做继续预训练、领域微调基本上可以直接复用同一套清洗框架要改的只是过滤目标和具体阈值。最后再提一个小建议不管用什么框架清洗数据时尽量保留中间产物宁可多占点磁盘也不要删得太干净因为你永远不知道下一次调规则时哪些“被丢弃的数据”会有重新利用的价值。
返回列表