大模型预训练数据质量过滤:MindSpore分层方案与实操

📅 发布时间:2026/9/26 3:05:26
大模型预训练数据质量过滤:MindSpore分层方案与实操
1. 大模型预训练里最容易被低估的环节数据质量过滤做MindSpore大模型预训练的人十有八九把注意力放在并行策略、算子优化、显存占用这些事情上但真正跑过一轮完整预训练的人都知道决定模型最终能力上限的往往不是训练框架本身而是喂进去的数据到底干不干净。我见过太多团队花了两周调并行配置结果loss曲线该抖还是抖最后排查一圈发现是语料里混了大量重复文本、乱码页面和机器翻译腔的低质内容。数据质量过滤这件事说白了就是在预训练开始之前把原始语料里那些“吃了没营养、甚至有毒”的部分筛掉。它解决的核心问题是互联网爬取的原始文本天然含有大量噪声包括HTML残留、广告模板、重复段落、低信息密度内容、语言混杂片段等。如果不做过滤直接训练模型会浪费大量算力去拟合这些噪声最终表现为生成内容重复、逻辑断裂、指令跟随能力差。这套方案适合谁参考如果你正在用MindSpore做百亿参数级别以上的预训练或者准备基于开源语料做继续预训练又或者你负责的是数据工程链路那这篇文章的内容可以直接拿去改改就用。即使你用的是PyTorch生态过滤思路和阈值设定逻辑也是通用的只是实现层面换成MindSpore的Dataset管道而已。2. 整体方案设计与核心思路拆解2.1 为什么过滤要分层做而不是一刀切很多人第一反应是写一个规则脚本把长度小于多少、包含特定字符的文本全删掉。这种做法在数据量小的时候勉强能用但放到TB级别的预训练语料上就会出大问题要么阈值太松过滤完还是脏要么阈值太严把大量有价值的长尾内容也误杀了。我的做法是分层过滤从粗到细一共四层。第一层做格式清洗把原始数据里的HTML标签、URL、特殊控制字符处理掉第二层做启发式规则过滤基于文本统计特征筛掉明显低质的样本第三层做语义级去重解决近似重复的问题第四层做质量打分用一个轻量模型给每条数据打分按分数排序后取头部数据。这样设计的好处是每一层职责单一出问题容易定位。比如你发现过滤后数据量骤降可以逐层看是哪一层的淘汰率异常。而且分层之后每层的阈值可以独立调整不需要牵一发动全身。2.2 MindSpore在这套链路里的角色定位MindSpore在这个方案里主要承担两个职责一是数据管道的构建用mindspore.dataset提供的高效并行加载能力做数据读取和变换二是质量打分模型的训练和推理用MindSpore训练一个小的文本分类模型来给数据打分。为什么不用纯Python脚本做过滤因为当数据量到TB级别时单进程处理的速度完全不可接受。MindSpore的Dataset管道支持多进程并行、内存映射、算子融合等优化在同样硬件条件下吞吐量能差出好几倍。而且过滤完的数据可以直接对接后续的训练管道不需要中间落盘再重新加载省去了大量IO开销。2.3 过滤粒度与算力预算的平衡这里有一个很现实的取舍过滤做得越细消耗的算力越多。语义去重需要计算文本向量质量打分需要跑模型推理这些都不是免费的。我的经验是过滤阶段消耗的算力控制在总训练算力的5%到10%之间比较合理。如果过滤本身比训练还贵那就本末倒置了。具体操作上格式清洗和规则过滤用CPU集群跑就行成本很低。语义去重可以用MinHash加LSH做近似计算避免全量两两比较。质量打分模型选小参数量的比如6层Transformer、隐藏维度256的规模在Ascend上推理速度足够快。3. 核心细节解析与实操要点3.1 格式清洗别小看HTML残留的破坏力原始语料里最常见的噪声就是HTML标签残留。很多爬虫抓下来的文本里混着div、script、style这些标签还有大量的nbsp;、amp;这类实体字符。如果不处理模型会把它们当成正常token学进去生成的时候冷不丁给你吐一段HTML出来。处理方式是用正则表达式做批量替换但要注意几个坑。第一不要用贪婪匹配去删标签否则可能把标签之间的正文也吃掉。第二script和style标签要连同内容一起删而p、br这类结构标签只需要删标签本身保留内容。第三处理完之后要检查一下是否有未闭合的标签残留这些往往是爬虫解析出错导致的。import re def clean_html(text): # 先删script和style及其内容 text re.sub(rscript[^]*.*?/script, , text, flagsre.DOTALL | re.IGNORECASE) text re.sub(rstyle[^]*.*?/style, , text, flagsre.DOTALL | re.IGNORECASE) # 删剩余标签 text re.sub(r[^], , text) # 处理HTML实体 text text.replace(nbsp;, ).replace(amp;, ) text text.replace(lt;, ).replace(gt;, ) # 压缩连续空白 text re.sub(r\s, , text) return text.strip()这段代码看着简单但实际跑的时候要注意正则的性能。Python的re模块在处理超长文本时可能很慢建议先用mindspore.dataset的map操作做并行化每个进程处理一部分数据。3.2 启发式规则过滤哪些指标真正有效启发式规则是过滤的主力军因为它计算成本低、可解释性强。我常用的指标有这么几个文本长度太短的文本信息量不足太长的可能是拼接垃圾。我的经验值是保留200到50000字符之间的样本。低于200字符的通常是标题、导航栏残留高于50000字符的往往是多个页面拼接在一起内部主题不一致。平均行长度正常自然语言文本的平均行长度在30到100字符之间。如果平均行长度低于20说明大量短行可能是代码或列表如果高于200可能是没有换行的长段落阅读体验差。特殊字符比例统计非字母、非数字、非标点字符的占比。如果超过10%说明文本里混了大量乱码或特殊符号。重复行比例把文本按行切分统计重复行占总行数的比例。超过30%的样本直接丢弃这通常是模板化页面。停用词密度中文文本里“的”、“了”、“是”这些高频词的出现频率可以反映文本的自然程度。如果停用词密度异常低可能是机器生成的伪文本。这些指标不是孤立的我一般会组合使用。比如一条文本长度合格但特殊字符比例超标照样丢弃。下面是一个组合过滤的示例def heuristic_filter(text): if len(text) 200 or len(text) 50000: return False lines text.split(\n) avg_line_len sum(len(l) for l in lines) / max(len(lines), 1) if avg_line_len 20 or avg_line_len 200: return False special_ratio sum(1 for c in text if not c.isalnum() and c not in 。《》 \n) / len(text) if special_ratio 0.1: return False unique_lines set(lines) if len(unique_lines) / max(len(lines), 1) 0.7: return False return True注意这些阈值不是拍脑袋定的是我在多个中文语料集上跑过统计分布之后取的合理分位点。你的数据源如果差异很大建议先采样统计一下再定阈值。3.3 语义去重MinHash加LSH的工程实现重复数据是预训练的大敌。模型在重复文本上训练会加剧记忆效应导致生成时倾向于复读。更隐蔽的问题是近似重复——两段文本只差几个字但语义完全一样。这种用精确匹配查不出来必须做语义级去重。我用的方案是MinHash加LSH。MinHash把每段文本映射成一个固定长度的签名向量两个文本的Jaccard相似度可以通过比较签名向量的对应位置来估计。LSH则把签名向量分桶只有落在同一个桶里的文本才需要做精细比较这样就把O(n²)的复杂度降到了近似O(n)。在MindSpore里实现的时候我建议把MinHash计算放在数据管道的map操作里用多进程并行。LSH分桶可以用一个独立的步骤做因为需要全局信息。具体参数上签名长度取128band数量取16每band行数取8这样能在召回率和精度之间取得不错的平衡。from datasketch import MinHash, MinHashLSH def compute_minhash(text, num_perm128): m MinHash(num_permnum_perm) for token in text.split(): m.update(token.encode(utf8)) return m def dedup_with_lsh(texts, threshold0.8): lsh MinHashLSH(thresholdthreshold, num_perm128) minhashes {} for i, text in enumerate(texts): m compute_minhash(text) lsh.insert(fdoc_{i}, m) minhashes[fdoc_{i}] m keep set(range(len(texts))) for i, text in enumerate(texts): key fdoc_{i} if key not in minhashes: continue candidates lsh.query(minhashes[key]) for cand in candidates: if cand ! key: keep.discard(int(cand.split(_)[1])) return [texts[i] for i in sorted(keep)]提示datasketch这个库是纯Python的在大规模数据上可能成为瓶颈。如果数据量超过千万级别建议用C重写核心部分或者用Spark的MinHash实现。3.4 质量打分模型用模型来筛数据前面三层过滤做完之后剩下的数据质量已经不错了但还有进一步优化的空间。这时候可以用一个轻量级的质量打分模型给每条数据打一个0到1的分数然后按分数排序取头部。这个打分模型怎么来我的做法是用少量人工标注的高质量数据作为正样本从被规则过滤掉的低质数据里采样作为负样本训练一个二分类模型。模型结构不用太复杂6层Transformer、隐藏维度256、4个注意力头就够了。训练数据量大概几万条就能收敛。在MindSpore里训练这个模型很直接用nn.TransformerEncoder搭起来损失函数用BCEWithLogitsLoss。推理的时候把模型导出成MindIR格式在数据管道里用mindspore.dataset的map操作批量推理。import mindspore.nn as nn import mindspore.ops as ops class QualityScorer(nn.Cell): def __init__(self, vocab_size30000, hidden_dim256, num_layers6, num_heads4): super().__init__() self.embedding nn.Embedding(vocab_size, hidden_dim) encoder_layer nn.TransformerEncoderLayer( d_modelhidden_dim, nheadnum_heads, dim_feedforwardhidden_dim*4, batch_firstTrue ) self.encoder nn.TransformerEncoder(encoder_layer, num_layersnum_layers) self.classifier nn.Dense(hidden_dim, 1) self.sigmoid ops.Sigmoid() def construct(self, input_ids, attention_mask): x self.embedding(input_ids) x self.encoder(x, src_key_padding_maskattention_mask) # 取平均池化 mask attention_mask.unsqueeze(-1).float() x (x * mask).sum(axis1) / mask.sum(axis1) logits self.classifier(x) return self.sigmoid(logits)注意质量打分模型本身也可能有偏见比如对某些领域文本打分偏低。建议在部署前做一下分领域的效果评估必要时对特定领域做阈值调整。4. 实操过程与核心环节实现4.1 数据管道的MindSpore实现整个过滤链路在MindSpore里的组织方式是这样的先用mindspore.dataset.TextFileDataset读取原始文本文件然后依次挂载格式清洗、规则过滤、去重、质量打分的map操作。每个map操作都可以设置num_parallel_workers来并行化。import mindspore.dataset as ds import mindspore.dataset.text as text def build_filter_pipeline(data_path, batch_size1000, num_workers8): dataset ds.TextFileDataset(data_path, shuffleFalse) dataset dataset.map(operationsclean_html, input_columns[text], num_parallel_workersnum_workers) dataset dataset.map(operationsheuristic_filter_wrapper, input_columns[text], num_parallel_workersnum_workers) dataset dataset.batch(batch_size, drop_remainderFalse) return dataset这里有个细节要注意去重操作需要全局信息不能简单地放在map里。我的做法是先把数据分批读出来每批做局部去重然后对批间做一次全局去重。全局去重可以用一个独立的脚本跑把每批的MinHash签名存下来最后统一做LSH。4.2 参数选择与阈值调优的实操记录阈值调优这件事没有捷径必须基于实际数据的统计分布来定。我的流程是先随机采样10万条数据统计各个指标的分布画出直方图然后根据分位点来定阈值。举个例子文本长度这个指标我统计下来发现5%分位点是180字符95%分位点是48000字符。那我就把下限设在200上限设在50000这样只淘汰掉两端各5%左右的数据。特殊字符比例这个指标90%的数据都在5%以下那我就把阈值设在10%给正常数据留足余量。调优过程中要持续监控淘汰率。如果某一层淘汰率突然飙升说明阈值可能设得太严了。我的经验是每层淘汰率控制在10%到30%之间比较健康总淘汰率在50%到70%之间。如果总淘汰率超过80%就要警惕是不是误杀了太多。4.3 过滤效果评估怎么知道滤得对不对过滤做完之后怎么评估效果最直接的方法是看下游训练的表现。但预训练一轮成本太高不可能反复试。所以我一般用两个代理指标第一个是困惑度。用过滤后的数据训练一个小模型在留出的验证集上算困惑度。困惑度越低说明数据质量越好。我实测下来经过完整过滤的数据比未过滤数据困惑度能低15%到25%。第二个是生成样本的人工评估。从过滤后的数据里随机抽100条人工看一遍统计其中高质量样本的比例。这个比例超过90%就算合格。还有一个技巧是对比过滤前后的数据分布。如果过滤后某个领域的文本几乎消失了说明过滤规则可能对这个领域有偏见需要针对性调整。5. 常见问题与排查技巧实录5.1 过滤后数据量骤降怎么办这是最常见的问题。原因通常有三个阈值设得太严、某个规则有bug、数据源本身质量就差。排查顺序是这样的先逐层看淘汰率定位到是哪一层淘汰最多。如果是规则过滤层把被淘汰的样本随机抽100条出来看看判断是误杀还是该杀。如果是去重层检查MinHash的阈值是不是设得太低导致近似但不重复的文本被误删。如果是质量打分层检查打分模型的训练数据是否有偏。我踩过的一个坑是特殊字符比例的计算里我把中文标点也算成了特殊字符导致大量正常中文文本被误杀。后来把中文标点加入白名单才解决。这种bug很隐蔽一定要用实际数据验证。5.2 去重之后仍然发现重复内容MinHash加LSH是近似算法不是精确去重。如果阈值设得不够严格或者文本长度太短导致签名区分度不够就可能漏掉一些重复。解决办法有两个一是对短文本单独处理长度低于500字符的文本直接用精确匹配去重二是对LSH的候选对做二次验证用编辑距离或余弦相似度做精细比较。还有一个容易被忽略的点不同来源的数据可能有不同的编码格式导致同样的内容看起来不一样。建议在去重之前先统一做Unicode归一化。5.3 质量打分模型推理速度太慢如果打分模型成为瓶颈可以从几个方面优化。第一减小模型规模隐藏维度从256降到128层数从6降到4精度损失通常不大。第二用MindSpore的图算融合和算子优化把推理速度提上去。第三批处理大小调大一些充分利用硬件并行能力。如果还是慢可以考虑用蒸馏的方式把大模型的知识蒸馏到一个小模型上。或者干脆用规则替代模型打分虽然精度差一些但速度快很多。5.4 常见问题速查表问题现象可能原因排查方法解决方案过滤后数据量骤降阈值过严或规则bug逐层统计淘汰率调整阈值检查规则逻辑去重后仍有重复近似算法漏检抽样检查重复对降低阈值增加精确验证打分模型推理慢模型太大或批太小测单条推理耗时减小模型增大batch某领域数据消失规则对该领域有偏分领域统计保留率调整规则领域单独处理过滤后loss仍抖动噪声未完全清除检查剩余数据质量增加过滤层或调整阈值最后分享一个我踩过的坑有一次过滤完之后发现模型生成的内容里频繁出现某个特定网站的域名。排查半天才发现那个网站的页面结构特殊HTML清洗规则没覆盖到导致域名被当成正文保留了。后来我在格式清洗里加了一步把所有URL模式都替换成占位符问题才解决。这种细节问题只有真正跑过完整流程的人才会遇到。5.5 过滤链路的监控与迭代数据过滤不是一次性的工作而是一个持续迭代的过程。我建议在过滤链路里加入监控埋点记录每层处理的输入输出数量、淘汰率、处理耗时等指标。这些指标定期review能帮你及时发现数据源的变化。比如某天你发现规则过滤层的淘汰率从20%突然涨到40%那很可能是数据源更新了引入了新的噪声类型。这时候就需要针对性地补充过滤规则。另外过滤规则本身也需要版本管理。每次调整阈值或增加规则都要记录变更内容和影响范围。这样出问题的时候可以快速回滚。6. 一些实操中的个人体会这套方案我在多个预训练项目里用过最大的感受是数据过滤的投入产出比远高于模型调优。同样一份算力预算花在数据过滤上带来的效果提升往往比花在调并行策略上更明显。还有一个体会是过滤规则要“宁松勿严”。早期我总想把数据滤得特别干净结果把很多有价值的长尾内容也滤掉了。后来发现保留一些中等质量的数据比只保留极高质量的数据效果更好因为多样性对预训练很重要。最后说一个具体技巧在质量打分模型的训练数据里一定要加入一定比例的“困难负样本”也就是那些看起来像高质量但实际有问题的文本。这样训练出来的模型才能更好地区分边界情况。