几个月前我用 MindSpore 跑一个中等规模的大模型预训练实验,语料是从多个公开站点抓下来的。前三天 Loss 一直挂在 5.8 上下不动,我换模型结构、调学习率、改优化器参数,折腾了一圈没什么实质变化。最后实在没辙,抽了一千条语料人工逐条看,才知道问题出在数据本身——将近三成是 HTML 残留、重复段落、广告文本和标点符号成片堆砌的“样子货”。把数据清洗掉一轮之后,同样的训练配置在两天内就把 Loss 压到了 4.7。那次经历彻底改变了我的工作方式:大模型预训练的工程链路里,数据质量过滤不是预处理环节的一个可选项,它是决定模型能否快速收敛、下游能否出效果的基础条件。
这篇文章就围绕我在 MindSpore 生态下做大模型预训练数据质量过滤的整套方案展开,包括常见的脏数据形态、分层过滤架构、规则粗筛和模型精筛的具体做法,以及如何用 MindRecord 和 Dataset API 把逻辑落地。适合正在用 MindSpore 跑预训练,或者准备搭建大规模数据管线的团队参考,哪怕你暂时只跑小模型,这套过滤思路同样值得提前沉淀。
1. 数据决定大模型上限?算力之外先过数据这一关
1.1 先搞明白:预训练语料里到底藏着哪些“脏东西”
很多人觉得预训练数据无非是“网上抓的文本”,差不多就行。实际把原始语料摊开看,你会发现里面远不止“干净句子”。我平时会把脏数据大致分成五类:
- 抓取噪声:HTML 标签残留、Cookie 弹窗文案、版权页、站点导航栏、分页器文字。这类内容占原始抓取量的比例通常高得惊人,尤其是从论坛和新闻类站点抓的数据,经常整段都是“上一篇 下一篇 返回列表”这种没有语义的东西。
- 近似重复内容:同一篇新闻被几十个网站转载,或者摘要、正文、评论混在一份文档里。注意这里的重复不一定是完全一样的字符串,更多是“同一意思换了几种表达”,直接哈希很难识别。
- 低语义密度文本:SEO 凑字页面、评论区刷屏、闲聊灌水、随机字符序列。这类文本句子结构看着正常,但单位长度里几乎不含有效信息,模型学到的全是如何“正确地说废话”。
- 机器生成文本:早期机器翻译的硬译稿、某些自动摘要工具吐出来的病句,甚至包括上一代模型生成的重复倒车文本。它们语法未必错,但逻辑链是断裂的,模型会吸收这种“似通非通”的语感。
- 隐私与有害内容:手机号、邮箱、证件号、详细地址这类个人信息,以及不该进入训练语料的恶意内容。模型对这类样本的记忆力很强,一旦进入预训练语料,很可能在生成阶段被原样复述出来,这是非常现实的产品与合规风险。
这些脏数据有一个共同特点:人眼扫一眼就能判断“这不是正常人话”,但程序不会自动帮你识别。所以数据过滤的第一步不是“选择什么高级算法”,而是“用一套机制把不像人话的文本挡在门外”。
1.2 行业里的公开经验都指向同一件事
这不是我一个人拍脑袋得出的结论。你去翻这几年公开的模型训练报告,会发现一个高度一致的规律:规模越大的预训练,对数据质量的处理越细致。
GPT-3 当年对 Common Crawl 做过两轮处理,先按文档与高质量参考集的相似度打分,只保留高分文档,又做了一次模糊去重,把训练集从几十 TB 压到几百 GB;LLaMA 团队在训练 65B 模型时,数据管线里显式放了启发式规则过滤和 PPL(困惑度)打分两步,最终过滤掉了约 9% 的数据;Falcon 模型公开的技术报告里,数据去重的工程细节甚至比模型架构写得还多。国内 MindSpore 社区里一些大模型训练案例,也都在数据清洗部分花了大量篇幅。
这些信息放在一起能说明一个问题:前几年“堆数据就能涨点”的粗放阶段已经过去了。现在的共识是,高质量、多样性、低噪声的数据集,比一味追求 token 总量更划算——单位算力预算下的训练效率完全不同。
1.3 过滤掉 30% 数据反而训练更快?
这里有个反直觉的点,值得展开说。很多人担心过滤会“损失多样性”,觉得数据少了对模型不好。但实践结果恰恰相反:当我把一份语料过滤掉约 35% 的内容后,同等训练配置下 Loss 下降得更快、更稳。
原因不难理解。重复度高的数据会让模型反复拟合相同的局部模式,等于把算力浪费在“不断确认已知内容”上;噪声数据会产生很强的梯度扰动,优化器只能用更低的学习率来硬扛,否则 Loss 会反复震荡。过滤之后,每个 batch 里的样本质量分布更均匀,模型在更少的 step 内接触到更多有效的“信息组合”,收敛自然更快。
而且过滤还有一个隐性好处:它能保证训练过程的稳定性。我以前跑预训练时,最烦的就是某几个 batch 整体是垃圾文本,Loss 突然从 3.2 冲到 5.0,一查样本全是抓取噪声。这类异常步进对学习率调度器的影响很大,一旦触发热启动或者重启机制,浪费的可就不只是几个小时了。
2. 分层过滤架构:粗筛、精筛、去重缺一不可
2.1 一条完整的大模型数据过滤管线是怎样的
我现在使用的标准数据管线大致长这样:
原始语料抓取 → 文本抽取 → 语言识别 → 规则粗筛 → 全局去重与模糊去重 → 质量打分 → 内容安全过滤 → 抽样人工复核 → 转成 MindRecord → 进入训练。
每一道环节都会把结果落成一份中间文件。这样做的目的很简单:任何一个环节出了问题,只需要从对应步骤重新跑,不用把整条链路从头再来一遍。比如后来我发现某批爬取数据的编码识别有误,中文全变成了乱码,因为中间文件还在,直接从语言识别那步开始重跑就行,整个清洗流程两小时就结束了。
2.2 为什么要把过滤拆成粗筛、精筛和去重三层
很多团队最开始做数据清洗,喜欢用一个“大而全”的模型去给所有文本打分,试图一步到位。但我试过之后就放弃了,原因很朴素:数据量太大,模型推理成本扛不住;垃圾种类太多,单一模型很难在“误杀”和“漏网”之间找到平衡。
分层处理的价值在于,把不同成本、不同精度的工具放到合适的位置上:
- 粗筛层只做规则判断,比如语言、长度、字符占比,几乎不消耗 GPU,用多进程跑,吞吐量可以做得很高。它负责干掉那些“明显一眼假”的文本,通常一次能删掉三四成的数据。
- 精筛层用模型给文本打分,成本比规则高一个量级,但更聪明,能识别出“说话通顺但信息量很低”的内容。这一层面对的是粗筛之后剩下的“高度疑似人类写的”文本。
- 去重层单独拎出来处理,是因为它跟“质量好坏”是两个维度。一段高质量文本被转载了十万次,你说它是垃圾还是精品?单看质量它是好的,但放进语料里它就是有害无益的重复项。去重必须在质量过滤的框架里有独立地位。
这三层如果混在一起做,参数耦合会很严重,调一个阈值会殃及另一个目标。分开之后,每层都可以独立迭代、独立回滚,工程上清爽很多。
2.3 分层策略对资源调度的意义
拆分还有一个直接好处:资源调度更灵活。粗筛层全跑 CPU,用 Python 多进程加分片任务就能打满几十个核,不占用宝贵的 GPU;精筛层才上 GPU,但这一层的输入已经小了很多,GPU 利用率能打上去;去重层则放在粗筛之后,避免在大量垃圾文本上白白消耗 MinHash 的签名计算。
在 MindSpore 的分布式训练环境里,我通常把清洗流程放在一个独立的算力集群上跑,跟训练集群错开时间。比如白天训练集群在跑,晚上训练停了,就把清洗任务调度上去。数据管线跟训练管线解耦之后,两边都不会互相拖慢。
3. 规则粗筛层怎么搭:语种识别、统计特征、安全过滤
3.1 语种识别先定边界
如果你训练的目标语种是明确的,比如中文或者英文单语模型,那语种识别就是第一道闸门。这里不需要上太重的大模型,一个 fastText 的 lid.176 模型就够用了,它对常见语种的识别准确率非常高,而且推理速度极快,几百万条文本分分钟跑完。
中文语料有个特殊坑点:日文汉字和韩文中的汉字字符容易被误判成中文。我的做法是在 fastText 识别之后再叠一个规则校验——统计文本中平假名、片假名、谚文音节的比例,超过千分之一就直接丢掉,不跟自己过不去。
如果训练的是多语种模型,语种识别的逻辑会更复杂一点,因为你不能简单按“是否目标语言”来筛。我的建议是至少做到语种打标,后面采样时按比例控制各语种数据量,防止某一种语种的噪音数据趁机冲进来。
3.2 几个有效又廉价的统计特征阈值
粗筛层核心靠统计特征打天下。我常用的几个特征和阈值如下,注意这些阈值不是拍脑袋定的,而是先抽 5000 条样本做分布统计,再按 5% 和 95% 分位点截出来的。
| 特征 | 我的常用阈值 | 说明 |
|---|---|---|
| 平均句长 | 中文 8~40 字;英文 30~200 字符 | 过短可能是碎片,过长可能是截断错误 |
| 非字母数字字符占比 | 超过 50% 丢弃 | 常见于 HTML 残留和符号堆砌 |
| 短行比例 | 少于 10 个字符的行占比超 10% 丢弃 | 抓取噪声大量短行 |
| 三-gram 重复率 | 句子三-gram 重复度高于 30% 丢弃 | 识别洗稿和机械重复 |
| 字符信息熵 | 低于 3.5(中文)丢弃 | 内容极度单一,重复率高 |
这些阈值看起来土,但确实是性价比最高的第一道防线。我见过一个公开数据集的技术报告,里面说单靠“长度过滤 + 符号占比 + 语言识别”这三板斧,就干掉了原始语料里的 16% 内容,而且误杀率极低。脏数据往往在基本统计特征上就藏不住。
3.3 安全过滤和隐私脱敏不能靠堆关键词
安全这块容易走两个极端:一种是完全不处理,另一种是堆一个巨大的关键词表。我个人的经验是,这两者都不够,更合理的是“正则识别隐私 + 关键词表 + 轻量分类模型”三件套。
隐私信息过滤以正则为主,手机号、邮箱、身份证号、银行卡号这类强模式的东西,正则已经能覆盖九成场景。命中隐私特征的样本,是整条删除还是脱敏后保留,取决于你的模型用途——如果做通用生成模型,建议直接删除,避免模型学到“可以原样输出别人手机号”这个行为。
有害内容的识别则不能只靠关键词。关键词表有天然的滞后性,而且“上下文是否越界”得靠模型判断。我通常训练一个很轻量的文本二分类模型,输入文本输出一个风险分数,分数超过阈值就剔除。这类模型不需要很大,一两层的文本分类网络就够,关键是数据标注要做扎实。
注意:安全过滤这一层我建议放在质量打分之后。原因是质量打分会把很多低质噪声文本先干掉,安全模型的输入相对干净,误判率会低很多。如果你的顺序倒过来,让安全模型先去处理一堆乱码和 HTML 标签,它很容易产生奇怪的误杀。
4. 模型精筛层怎么搭:PPL打分、质量分类器、模糊去重
4.1 PPL困惑度打分:原理和筛选陷阱
精筛层我用的第一个工具是困惑度打分。简单解释一下原理:让一个语言模型读一段文本,模型对这段文本能预测得多准,决定了它的困惑度高低。如果文本非常符合语言的常规模式,模型预测得很顺利,PPL 就低;如果文本语无伦次、结构混乱,模型每预测一个词都“很意外”,PPL 就会飙高。
PPL 能自动识别出很多规则层看不出来的问题。比如一段英文机器翻译的中文文本,规则特征完全正常,但读起来就是别扭,PPL 会把这种别扭量化出来。我做这个环节时用的是 MindSpore 上载入的一个小型 Transformer 模型,直接基于要训练的语料本身跑了一个轻量自监督任务做适配,这样可以更贴合目标语料的风格。
但 PPL 有个陷阱:阈值卡太严会让语料“过于通顺”。我曾经在一版实验里把 PPL 上限设得特别低,留下来的全是极其简单直白的句子,结果模型整体风格变得很呆,生成质量反而下降。后来我改成按语料分桶做百分位截断——先统计所有候选文本的 PPL 分布,然后只砍掉分布尾部的 15%,而不是用一个全局硬阈值。这样既能剔除“奇形怪状”的文本,又不会牺牲表达多样性。
4.2 训练一个质量分类器,才是真正贴合数据集的方案
PPL 衡量的是“文本是否流畅”,但流畅不等于有用。一段排版精美的垃圾广告文,PPL 可能很低,可模型真学了它只会增加废话能力。所以精筛层的另一个关键组件是质量分类器。
我训练质量分类器的思路是这样的:构造一个二分类数据集,正样本选高质量书籍、学术论文、头部新闻媒体的正文,负样本选抓取噪声、机器翻译硬译稿、SEO 灌水页面、低质评论。用一个小型预训练模型在这些数据上做微调。这个分类器输出的分数,跟 PPL 结合起来,给每个样本算出一个综合质量分。
这个方案有个额外的好处:它是可以不断迭代的。每跑完一轮清洗,我抽一批被过滤掉的样本回去看,发现有些其实质量不错,就把它们捞回来放进正样本集;发现有些漏网之鱼,就把它们加进负样本集。分类器每迭代一次,对“你这个数据集的好文本长什么样”的理解就更深一层。
4.3 去重的工程细节:精确去重、MinHash、SimHash
去重是老生常谈,但工程细节决定了效果。我从粗到细做三层:
- 精确去重:对整篇文档做 SHA1、MD5 哈希,完全相同的文档直接删掉。这层成本最低,先把“复制粘贴型转载”干掉。
- MinHash 模糊去重:把每个文档切分成 token 窗口,比如中文按 5 个词一组做 shingle,然后对每个 shingle 算多个哈希值,取每个哈希桶的最小值作为文档签名。有了签名之后再用 LSH 分桶,找出候选重复对,最后做精确比对。这套方案对“改了几个字、调整了段落顺序”的转载很有效。
- SimHash 指纹去重:把文本映射成 64 位指纹,汉明距离小于等于 3 的视为近似重复。SimHash 比 MinHash 更省内存,适合在超大规模语料上做一轮粗过滤。
这里有几个实操注意点。一是不要对短文档做 MinHash,文档太短时签名信息量不够,误判率会飙升,我一般限定长度低于 500 字的文档只做精确去重。二是中文 shingle 要按分词后的 token 切,不能按单字切,否则会大量误判。三是去重一定要放在质量精筛之前,因为精筛要跑模型,每处理一条文本都是成本,先把重复项删掉能省不少 GPU 时间。
5. 在 MindSpore 里落地:MindRecord格式、Dataset API与异步管线
5.1 先把清洗结果转成 MindRecord 再进训练
大模型预训练的数据量不是开玩笑的,动辄几十 TB 文本。我强烈不建议把清洗逻辑放在训练脚本里在线执行——每次数据加载都跑一遍规则,训练效率会崩掉。正确做法是离线清洗后,直接转成 MindRecord 格式,训练时只做高效读取。
MindRecord 是 MindSpore 的原生数据格式,它的核心优势是随机读取友好。大模型训练时每个 epoch 要 shuffle 数据,如果直接读一堆散装文本文件,IO 随机访问效率很低;转成 MindRecord 后,数据按块组织,可以配合多进程并行读取,吞吐会明显提升。
转格式的时候要注意分片数量。我第一版图省事把 200GB 数据写成了一个 MindRecord 文件,训练时发现 IO 成了瓶颈。后来改成每个分片 1~2GB,一共拆几十上百个分片,再让多个 Worker 并行读,训练吞吐才跑起来。
5.2 用 Dataset API 的 map/filter 做兜底过滤
清洗可以在离线阶段做彻底,但上线训练时我仍然会在数据管线里留一道轻量过滤,作为兜底。用 MindSpore 的 Dataset API 做这事很方便,下面是我常用的一个模式:
import json import mindspore.dataset as ds def preprocess_and_filter(line): obj = json.loads(line) text = obj.get("text", "") # 这里只保留代价极低的规则,模型打分不放在线 if len(text) < 200: return None if len(text) > 10000: return None if not is_target_language(text): return None return text dataset = ds.GeneratorDataset( source=iter(open("cleaned_samples.jsonl", encoding="utf-8")), column_names=["line"], num_parallel_workers=8, python_multiprocessing=True, ) dataset = dataset.map(operations=preprocess_and_filter, input_columns=["line"]) dataset = dataset.batch(32)这个模式里,map 的预处理函数会返回文本,也可以返回 None 表示丢弃,但实际使用中我更喜欢把过滤逻辑放到 GeneratorDataset 的迭代器内部,这样不需要依赖 map 对 None 的特殊处理,逻辑更可控:
class DatasetIter: def __init__(self, file_path): self.file_path = file_path def __iter__(self): with open(self.file_path, "r", encoding="utf-8") as f: for line in f: obj = json.loads(line) text = obj.get("text", "") if not pass_basic_rule(text): continue yield text, obj.get("score", 0.0) dataset = ds.GeneratorDataset( source=DatasetIter("cleaned_samples.jsonl"), column_names=["text", "score"], num_parallel_workers=8, python_multiprocessing=True, )在线兜底过滤的定位是“防漏网”,不是“做清洗”。所以这层只保留廉价规则,不让任何模型参与打分,避免把训练吞吐拖垮。
5.3 异步数据管线调优:多进程、预取、shuffle
用 MindSpore 跑大模型预训练,数据加载速度经常比 GPU 计算慢,这块一定要调。几个关键参数我踩过不少次坑:
- num_parallel_workers:GeneratorDataset 和 map 里的并行线程数。我之前默认设 4,后来发现 16 左右才有明显提升。注意这个值要跟机器 CPU 核数匹配,不要一上来就写 64,不然线程切换开销反而致命。
- python_multiprocessing:如果处理函数是纯 Python,尽量开成 True,走多进程而不是多线程,否则 GIL 锁会卡死你。但也要注意,进程间传输 Python 对象有序列化开销,处理函数越重,多进程收益越明显。
- prefetch_size:预取缓冲区大小。训练时如果发现 GPU 利用率频繁掉零,优先调大它,我一般设在 16 以上。
- shuffle 的位置:不要在读取原始 JSONL 时就 shuffle,那会打乱文件的随机读取优势。先在 map 之后做 buffer shuffle,让数据进入训练前被打乱即可。
MindDataset 读取 MindRecord 时同样有 num_parallel_workers 的配置。我跑分布式训练时,每个 rank 单独读自己的分片,避免多个 rank 抢同一个文件的 IO。
5.4 过滤参数怎么跟训练超参联动
这是一个容易被忽视的点:数据过滤的结果会直接影响训练超参的选择。过滤后的数据更“干净”,每个 batch 的样本质量更均匀,这时候可以大胆一点把 batch size 调大、把学习率上限调高一档,收敛速度会比脏数据状态下更快。
过滤后总 token 数缩水了,对应的训练步数和 epoch 数也要重新算。我习惯在配置里记录过滤率,比如原始语料 120B token,过滤后只剩 78B,那我训练计划就按 78B 来定,而不是拍脑袋写一个固定的 step 数。如果你的过滤率超过 30%,并且数据集本身是静态的,那就要回头考虑补抓数据了,否则多样性不够,后面几轮训练就是反复看图。
6. 实测效果与踩坑记录:过滤前后的横向对比与经验复盘
6.1 一组我自己跑出来的对比数据
我拿一份从公共网页抓取的约 60GB 原始语料做了一组对比实验,训练目标是同一个参数规模的小模型,训练配置完全一致,只改数据侧的处理流程:
| 数据版本 | 样本量折算 token | 训练到 Loss<3.5 所需 step | 下游 5 个任务平均准确率 |
|---|---|---|---|
| 未过滤原始数据 | 约 12B | 485K | 基准 |
| 仅规则粗筛 | 约 9B | 392K | +3.1% |
| 规则粗筛 + 精筛打分 + 去重 | 约 6.8B | 331K | +4.6% |
这组数据只代表我的某一次实验,不是通用结论,但趋势非常明显:数据量小了 40% 多,收敛速度反而快了三成,下游指标还涨了接近五个点。自那以后我就认准了一件事——过滤不是把数据变少,是让剩下的数据“每一分算力都花在刀刃上”。
6.2 踩坑记录:最典型的几个问题
把这些年踩过的坑整理一下,最典型的有这么几个:
第一,规则阈值照搬英文语料,中文样本被误杀。早年用过一套英文清洗规则,平均句长阈值对中文完全不适用,跑完之后中文语料少了四分之一,抽出来一看大量正常的长句全被当成异常截断处理了。后来每套阈值都用目标语料的小样本来定,不跨语种套用。
第二,PPL 阈值设太严,语料全是“标准答案体”。有一版生成的文本风格非常死板,回头查数据,PPL 上限压得太低,留下的全是简单句和模式化表达。改成按分位截断之后,风格才缓过来。
第三,先精筛后去重,浪费大量 GPU 时间。我第一次搭管线时精筛在前去重在后,等于把重复文本也跑了一遍质量打分,白烧了很多机器。把去重提前到精筛之前,同样的流程省出差不多三分之一的精筛时间。
第四,MindRecord 分片太大,训练 IO 直接拖垮。前面说过,单文件 200GB 的教训还热乎着。现在我的标准是单分片 1GB 左右,宁可多几个文件,也不要一个巨型文件。
第五,在线兜底过滤塞了个大模型,训练吞吐崩了。曾经试图在 Dataset 的 map 里放一个质量分类器兜底,结果每一条文本都要过一遍推理,Step 耗时直接翻倍。现在的原则很明确:训练脚本里只放最廉价的规则过滤,重的活全部离线干完。
6.3 几条稳定有效的实操经验
最后分享几条在实战里被反复验证的经验,都是踩坑之后沉淀下来的。
一是新语料先抽 1000 条人眼扫一遍再定过滤阈值。这个习惯看着原始,但比任何自动化分析都管用。你不用看完一千条,看到两三百条基本上心里就有数了——这批数据是偏论坛风格还是偏新闻风格、噪声主要来自哪种类型、要不要单独调语种规则。
二是过滤参数按数据来源独立配置。不同站点抓下来的数据,脏的性质完全不同。论坛数据主要需要删短句和灌水,新闻数据主要需要去重和去转载,百科数据反而要小心别把冷门条目当噪声误杀。我在配置里给每个内容源单独留一份过滤参数,效果比用一套全局参数好很多。
三是保留“被过滤样本”清单至少一周。清洗完先别急着删被过滤的中间结果,存一份带有过滤原因标记的清单。这样带来一个好处:训练效果不好时,可以反查是不是过滤误杀太多;去重模块被质疑时,可以立马上调一批“被判定为重复”的样本做人工复核。
如果你正打算用 MindSpore 跑大模型预训练,我最大的一个建议是:别急着把模型结构改来改去,先花一周时间把数据管线做扎实。我后来养成的习惯是,每次拿到新语料先抽样本、定规则、跑清水线,再谈训练;这个习惯带来的收敛速度提升,比任何花哨的模型技巧都实在。