☰
大模型预训练数据质量过滤:MindSpore分层方案与实操
2026/9/26 3:05:21 网站建设 项目流程

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(r'<script[^>]*>.*?</script>', '', text, flags=re.DOTALL | re.IGNORECASE) text = re.sub(r'<style[^>]*>.*?</style>', '', text, flags=re.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分桶可以用一个独立的步骤做,因为需要全局信息。具体参数上,签名长度取128,band数量取16,每band行数取8,这样能在召回率和精度之间取得不错的平衡。

from datasketch import MinHash, MinHashLSH def compute_minhash(text, num_perm=128): m = MinHash(num_perm=num_perm) for token in text.split(): m.update(token.encode('utf8')) return m def dedup_with_lsh(texts, threshold=0.8): lsh = MinHashLSH(threshold=threshold, num_perm=128) minhashes = {} for i, text in enumerate(texts): m = compute_minhash(text) lsh.insert(f"doc_{i}", m) minhashes[f"doc_{i}"] = m keep = set(range(len(texts))) for i, text in enumerate(texts): key = f"doc_{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_size=30000, hidden_dim=256, num_layers=6, num_heads=4): super().__init__() self.embedding = nn.Embedding(vocab_size, hidden_dim) encoder_layer = nn.TransformerEncoderLayer( d_model=hidden_dim, nhead=num_heads, dim_feedforward=hidden_dim*4, batch_first=True ) self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_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_mask=attention_mask) # 取平均池化 mask = attention_mask.unsqueeze(-1).float() x = (x * mask).sum(axis=1) / mask.sum(axis=1) 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_size=1000, num_workers=8): dataset = ds.TextFileDataset(data_path, shuffle=False) dataset = dataset.map(operations=clean_html, input_columns=["text"], num_parallel_workers=num_workers) dataset = dataset.map(operations=heuristic_filter_wrapper, input_columns=["text"], num_parallel_workers=num_workers) dataset = dataset.batch(batch_size, drop_remainder=False) 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. 一些实操中的个人体会

这套方案我在多个预训练项目里用过,最大的感受是:数据过滤的投入产出比远高于模型调优。同样一份算力预算,花在数据过滤上带来的效果提升,往往比花在调并行策略上更明显。

还有一个体会是,过滤规则要“宁松勿严”。早期我总想把数据滤得特别干净,结果把很多有价值的长尾内容也滤掉了。后来发现,保留一些中等质量的数据,比只保留极高质量的数据效果更好,因为多样性对预训练很重要。

最后说一个具体技巧:在质量打分模型的训练数据里,一定要加入一定比例的“困难负样本”,也就是那些看起来像高质量但实际有问题的文本。这样训练出来的模型才能更好地区分边界情况。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询