
从一次不太顺利的预训练说起。去年我用 MindSpore 启动了一个十亿参数规模的语言模型训练语料是团队从多个开源渠道和网络站点抓下来的也没仔细洗总计大概几个 TB 的文本直接丢进去就跑。训练到一万二千多步的时候loss 已经降得非常慢偶尔还有小尖峰更糟糕的是下游某个分类任务的指标一直在低位徘徊。我后来花了一周时间排查最终坐标落在数据上语料里充斥着页面导航残留、广告文案、重复转载的新闻通稿、以及大量非目标语言的机器翻译内容。把这些数据筛选清洗掉之后同一个实验loss 曲线的收敛速度和对齐情况明显变好。这就是大模型预训练阶段数据质量过滤的价值所在。很多团队把注意力放在模型结构、并行策略和超参数上但语料本身的信噪比才是预训练能否顺利收敛的隐形天花板。这篇文章不堆理论只讲在 MindSpore 生态里一条从规则过滤、语言识别、困惑度打分到语义去重的完整数据清洗链路怎么搭以及我在实际跑数据时踩过的坑和验证方法。适合正在做预训练和数据工程的同学参考尤其是刚把数据管线从 PyTorch 迁到 MindSpore、或者正打算用 MindRecord 重构数据读取的人。1. 为什么预训练语料先要过筛子脏数据对模型的实际影响1.1 预训练语料的典型脏数据画像预训练语料很少是“干净”的。网上抓下来的数据我归纳下来大概是下面这几类网页正文抽取残留导航菜单、页脚版权、cookie 提示、标签属性、图片 alt 文本这类文本大量混在正文里短则几十字长则占整个文档。机器生成内容日志文件、监控告警、自动化返回、爬虫抓取的 JSON 片段特征是大量重复字段、时间戳、固定格式。广告与 SEO 堆砌标题党、关键词密度异常高、句子之间没有语义连贯性甚至整段是关键词的无序排列。转载与镜像内容同一篇新闻、百科词条、论坛长文在几十个站点反复出现正文高度相似部分站点还做了不负责任的改写。代码和非目标语言GitHub README、配置文件、Shell 脚本以及大量低质量机器翻译的中文文本。个人信息与模板文本手机号、邮箱、身份证号片段还有合同模板、简历模板这类高度重复的固定格式内容。这些内容单独看每一类都很容易发现但混合在 TB 级语料里就会变成“沉默的污染源”。我在做数据检查时有一个习惯对任意一个数据分片随机抽 200 条人工浏览一遍。你会发现新鲜抓取的网页数据里真正能算“干净长文本”的往往不到 70%剩下 30% 要么需要清洗要么直接丢弃。脏数据类型容易识别的特征对预训练的主要破坏方式页面模板残留导航词、版权、链接列表拉低语料平均质量浪费 token机器日志与结构化文本大量数字、括号、等号造成训练 loss 波动和记忆噪音SEO 堆砌关键词密度高、句间无关联干扰语义表示容易带偏领域分布近似重复文本转载、改写、镜像重复采样泛化能力下降非目标语言字符分布异常语言分布偏移下游任务变差1.2 脏数据是如何从源头拖垮一个预训练模型的脏数据不是“看起来不舒服”这么简单它会在训练过程中造成几个实质性问题。第一是有效信息密度下降。假设一条文本里有 40% 是导航链接和版权信息那模型在这条样本上学习的有效语义只剩 60%。语料总量看起来很大实际信息量却缩水了。模型参数量越大对这种“看似很多、实则稀薄”的数据越敏感。第二是重复样本导致过拟合和记忆化。同一个新闻稿在不同站点出现几十次相当于同一份数据被重复采样几十次。模型更容易记住那些重复文本的固定表达而不是学到可泛化的语义规律。这个问题在模型规模增大时会越来越明显大模型更倾向于死记硬背。第三是噪声文本对损失函数的影响。机器日志、HTML 碎片、乱码片段会产生大量“不可预期的 token 跳跃”表现为训练曲线的尖峰。数据噪声越大优化器越难稳定收敛尤其在使用大学习率或动态学习率调度时几条异常样本就足以把梯度方向带偏。第四是分布偏移。如果语料中混入大量非目标语言模型会把一部分建模能力“浪费”在那些语言上目标语言的下游表现就会被稀释。这不是说不能有多语种数据而是目标语种和非目标语种的比例必须可控。但这里也有一个反直觉的点数据不是越干净越好。过度过滤会压缩语料的多样性导致模型见到的句式和主题范围变窄泛化能力反而下降。所以数据质量过滤的本质不是“删除数据”而是“提高信噪比并控制分布”。这一条原则贯穿整篇方案的设计。2. 分层过滤策略从规则、语言识别到困惑度打分2.1 规则过滤与清洗的阈值设计我的经验是先做“清洗”再做“过滤”。很多团队把这两件事混在一起结果正则匹配常常被文本里的格式问题干扰。清洗阶段做四件事去控制字符和零宽字符\u200b、\u200c、\u202a这类不可见字符不处理会干扰后续分词。统一 Unicode 规范中文文本按 NFKC 做标准化能合并大量全角/半角差异。HTML 解码处理amp;、nbsp;、lt;nodegt;这类实体不然过滤规则会被伪标签干扰。统一换行符和分隔符把连续的空白压缩到单个空格。清洗完之后再做规则过滤。我常用的规则大概有下面这些阈值可以根据语料情况调整不一定照抄长度过滤纯中文文本至少 200 个字符纯文本少于 50 个 token 的样本语义信息太少直接丢弃。平均句长过滤把文本按中英文句号、问号、感叹号切成句子中文平均句长低于 8 或高于 100 的多半是碎片文本或格式信息。标点密度过滤标点符号占比过高通常是代码、日志或表格乱码。URL 比例过滤文本中http://、www.出现次数过多一般是链接列表或 SEO 内容。关键词堆砌检测对高频关键词做 n-gram 统计如果 Top-5 重复 n-gram 占总 n-gram 的比例超过阈值说明文本是关键词拼接。下面是我在清洗阶段常用的一个 Python 示例规则写起来不复杂但执行顺序很重要import re import unicodedata def clean_text(raw: str) - str: # 去掉控制字符和零宽字符 text re.sub(r[\x00-\x08\x0b\x0c\x0e-\x1f\u200b-\u200f\u202a-\u202e], , raw) # NFKC 规范化处理全角半角差异 text unicodedata.normalize(NFKC, text) # 压缩连续空白和换行 text re.sub(r\s, , text) return text.strip() def rule_based_filter(text: str) - bool: if len(text) 200: return False sentences re.split(r[。.!?], text) avg_sent_len sum(len(s) for s in sentences) / max(len(sentences), 1) if avg_sent_len 8 or avg_sent_len 100: return False punct_ratio len(re.findall(r[。、,.!?;:], text)) / max(len(text), 1) if punct_ratio 0.25: return False url_ratio len(re.findall(rhttps?://, text)) / max(len(text), 1) if url_ratio 0.008: return False return True这套规则是我跑了几个版本语料之后调整出来的不一定适合所有场景。更好的做法是把每条规则的命中统计独立记录到一个 JSON 或表格里这样你能看到是哪个规则删掉了多少数据而不是几层规则嵌套下去最后只剩一个“通过/不通过”的结果。2.2 语言识别用最轻的手段判断文本语种语言识别在预训练语料管线上几乎绕不开。你要训练一个中文为主的模型却混进大量英文、日文、韩文或者低质量机翻文本注意力资源就被稀释了。语言识别有两条路一是用字符表快速判断二是用现成的语言识别模型。工程上我建议两者结合。对于中文语料可以维护一个常用汉字表计算文本中命中常用汉字的比例。如果一段文本长度超过 500 字符常用字覆盖率却低于 60%那基本可以判定它不像是正常中文。这个判断成本极低一条文本只需要几毫秒适合放在规则过滤之后的第一道关口。更精细的语言判断用 fastText 的lid.176.bin语言识别模型它对短文本的识别也够用单条文本速度在毫秒级。需要注意一点fastText 对“中英夹杂”和“低质量机翻”往往会给出含糊的结果不能只看最高置信度要参考置信度差值。如果最高概率和第二高概率相差很小就应该人工抽样确认而不是无脑放行或丢弃。实际操作中我给每条文本输出一个语言标签同时记录目标语言字符的比例。比如目标是中文语料我会对zh类文本进一步区分简体、繁体以及日文汉字干扰。日文文本里也有大量汉字但假名比例会明显拉开差距这个用字符分布就能区分。语言识别完成之后不要只做“删掉非目标语言”一件事。很多团队忽略的是语料配比。如果今天抓到的语料里 10% 是英文你过滤掉之后模型就再也接触不到英文了这不一定符合预训练目标。更合理的做法是在过滤的同时记录各语种的占比然后在采样阶段按目标比例混合。语言识别是“分诊”不是“一刀切”。2.3 困惑度打分让一个小模型替你做初判规则可以杀掉结构性问题但无法判断“这句话读起来是否通顺”。几句话之间逻辑混乱、刻意绕口的低质量文本规则往往看不出来。这时候我会引入困惑度PerplexityPPL打分。原理很简单用一个预训练好的小语言模型计算目标文本的对数似然然后转换成 PPL。PPL 越高说明模型认为这段文本越“意外”。低质量文本、机器生成文本、语无伦次的文本通常会产生更高的 PPL。计算公式本质上是PPL exp(-1/N * sum(log P(token_i | context)))用代码实现的时候我建议按固定窗口滑动计算。如果文本太长直接用全量计算会显著压低得分导致阈值失真。实践中我一般用 512 token 的窗口步长 256把多窗口的 PPL 做平均。打分模型不需要很大。GPT-2 级别、BERT 级别的模型就够了大模型理想但成本太高。以一张 A100 为例对平均长度 1k token 的文档做 PPL 打分一天大概可以跑几十万条这已经能满足中小规模语料的处理需求。如果你面对的是几十 TB 语料建议只对“规则过滤后幸存”的文本做 PPL 打分不要把全部原始语料跑一遍。阈值怎么定我的做法不是拍脑袋选“PPL 5 就保留”而是先对一个有代表性的样本集比如 10 万条画出 PPL 直方图看分位数。一般取 85 到 95 分位作为高 PPL 丢弃线剩下的作为“可疑样本”再结合其他信号判断。还有一个容易踩的坑PPL 对领域非常敏感。用通用语料训练的模型看专业医学文献或代码文档时 PPL 会整体偏高。如果你把这些文本按同一个通用阈值过滤会误杀大量高价值内容。所以我通常会准备一个“领域白名单”对白名单内的文档降低 PPL 要求或者干脆跳过 PPL 打分只做规则和去重。3. 重复文本的歼灭战精确去重与语义去重组合3.1 精确去重与 MinHashLSH 的组合重复文本是最影响预训练质量的因素之一。Precision 不高的话一个长文被几十个站点转载模型等于反复咀嚼同一段内容。去重我习惯分两层做精确去重和近似去重。精确去重最简单先把文本做归一化统一大小写、去空白、只保留正文然后算 MD5 或 SHA1维护一个全局哈希集合遇到重复就丢。这个方案对“完全一样”的文本有效但现实中很多重复文本只是“高度相似”转载时插入了站点水印、改了几个标点、加了一段推荐位。对这类文本精确哈希无能为力。近似去重需要用 MinHash LSH。原理不复杂把一段文本切分成 n-gram 集合比如中文按字符 4-gram 切然后对集合生成一个固定长度的签名。两个文档的集合重叠度越高签名越相似。LSH 的作用是把签名分成若干 band只有当某个 band 中的所有 hash 值完全一致时才把这两个文档标记为候选重复对从而避免全量两两比较。这里有一个实用公式如果你把签名分成b个 band每个 band 有r行两个文档相似度为s那么它们在至少一个 band 内完全一致的概率是1 - (1 - s^r)^b。这个概率曲线的“拐点”大约在(1/b)^(1/r)附近代表设计上的相似度阈值。比如你想重点去掉 80% 以上相似的文档就可以按这个公式反推 band 和 row 的取值。使用 Python 的datasketch库时流程大概是下面这个样子from datasketch import MinHash, MinHashLSH def shingle_set(text: str, k: int 4): # 中文场景按字符 n-gram return {text[i:i k] for i in range(len(text) - k 1)} def minhash_from_text(text: str, num_perm: int 128): m MinHash(num_permnum_perm) for shingle in shingle_set(text): m.update(shingle.encode(utf-8)) return m lsh MinHashLSH(threshold0.85, num_perm128) # 遍历文档依次 update 或者查询后插入 # duplicated_docs lsh.query(mh)实践中有几个细节中文 n-gram 我常用 4-gram太小精确度不够太大对改写文本不敏感。签名维数 128 算是起步追求更稳可以上 256但内存会涨。不要对全部语料一次性建 LSH内存受不了。建议按语料类型分桶处理比如新闻桶、百科桶、论坛桶分开建索引。LSH 只是召回候选对召回之后还要用真实的 Jaccard 相似度或编辑距离做一次二次确认否则会误删仅仅是“局部相似”的文档。3.2 语义级去重值得为哪些语料花这笔钱MinHash 解决“字面相似”的重复但解决不了“换皮”重复。比如一篇技术文章被改写后措辞全变、结构和例子还是原样又比如同一个事件的多篇新闻稿表达方式不同但信息重复度极高。这些内容需要语义级去重。语义去重的基本路径是用文本向量模型给每篇文档算一个 embedding然后用向量索引找相似度超过阈值的文档对。模型我一般用中等规模的中文 embedding 模型比如bge-large-zh或text2vec这一档既能处理中文长文本时效性也不错。索引工具可以用 faiss 或 hnswlib在一台几十 GB 内存的机器上单机跑就够。但这里要泼盆冷水语义去重非常贵不要无脑全量跑。我自己的策略是分优先级第一优先级书籍、论文、高质量百科条目。这类语料信息密度高一旦重复对训练影响大值得花算力做语义去重。第二优先级新闻、论坛长文。它们经过 MinHash 去重后剩下的重复其实有限可以抽样检查后决定是否继续做。第三优先级短文本、评论、口语内容。短文本的 embedding 噪声大语义去重的意义不大。另外长文档做语义去重有一个天然的难题embedding 模型通常有 512 或 1024 的 token 上限直接对整篇长文编码会丢失信息。我的做法是先把长文档分段分别编码然后用“最大相似度”或者“相似段落占比”来判断文档级重复。实操中我常用“相似段落占比超过 30% 即标记为候选重复”这个阈值在书籍和长文数据集上效果不错。做完语义去重建议再做一次抽样人工确认。向量模型的相似度并不等于人类的“重复感”阈值设太高会漏设太低会误杀。抽 100 对重复候选看 20% 以上是不是误判如果是就把阈值调高一点。4. MindSpore 训练管线中的数据质量落地实现4.1 离线清洗与 MindRecord 构建数据过滤跑通之后下一件事是把清洗干净的语料变成 MindSpore 训练时方便读取的格式。这一步如果不做好前面的清洗成果会在读取阶段被浪费掉。我的首选格式是 MindRecord。理由很简单它是 MindSpore 的原生列式存储格式支持多文件分片可以配合分布式训练按num_shards和shard_id切分读取效率也稳定。你当然可以让训练进程直接读取文本文件再在map里做清洗但那样做的问题很明确每次训练都要重复执行高开销的 Python 逻辑训练速度会被数据侧拖慢。离线清洗流水线的大致流程是原始文件 - 规则清洗 - 语言识别 - PPL 打分 - 精去重/近似去重 - 语义去重 - 组装训练样本 - 写入 MindRecord。每个阶段都输出统计日志。写入 MindRecord 时我建议把文本已经分词好的input_ids直接写进去而不是写原始字符串。原因有两个训练时直接读取input_ids省去在线分词的开销速度提升非常明显。MindRecord 的 string 字段读取和序列化开销比 int 数组大存 token id 能显著降低 IO 和内存占用。一个构建 MindRecord 的简化示例from mindspore.mindrecord import FileWriter schema { input_ids: {type: int32, shape: [-1]}, attention_mask: {type: int32, shape: [-1]}, quality_score: {type: float32}, lang: {type: string}, } writer FileWriter(data/pretrain.mindrecord, shard_num16) writer.add_schema(schema, pretrain samples after quality filter) for sample in filtered_samples: writer.write([{ input_ids: sample.input_ids, attention_mask: sample.attention_mask, quality_score: sample.quality_score, lang: sample.lang, }]) writer.commit()shard_num的设置要和训练时的卡数匹配。比如你用 16 张卡训练shard_num最好是 16 的整数倍这样每张卡读到的分片数量均匀。我通常设成卡数的 1 到 2 倍避免单卡数据文件过大的问题。4.2 MindDataset 在线读取与过滤的最小代价原则训练阶段的数据读取我坚持一个原则在线只做“轻过滤”重活全部离线干完。用mindspore.dataset.MindDataset读取 MindRecord 后可以直接做字段投影和 batch 组装。如果还需要在线过滤我一般只按字段做阈值判断。代码大概长这样import mindspore.dataset as ds dataset ds.MindDataset( dataset_filedata/pretrain.mindrecord, num_shardsrank_size, shard_idrank_id, num_parallel_workers8, shuffleTrue, ) # 如果需要在线丢弃极端低质量样本 dataset dataset.filter(predicatelambda x: float(x[quality_score]) 0.5) dataset dataset.project([input_ids, attention_mask]) dataset dataset.batch(batch_size2048, drop_remainderTrue)注意filter里的 lambda 在生产环境不推荐因为序列化、跨进程并发时很容易踩坑。更稳妥的方式是在 Map 操作里做标记或者干脆在离线阶段就把不需要的样本删干净在线只做列裁剪。在线过滤的性能优化有几点值得说num_parallel_workers不是越大越好。建议按 CPU 物理核数的一半起步实测中开太大反而会增加调度开销。如果map操作需要跑 Python 函数开启python_multiprocessingTrue能有效利用多核但如果函数很简单多进程的序列化开销反而更明显。不要把text字符串列读进来再分词。把 token id 直接存进 MindRecord训练时只碰input_ids和attention_mask这是性价比最高的一步。我用过的比较极端的方案是在离线阶段连quality_score、ppl_score都提前算好训练时只按需要决定是否使用这些列。这样一来数据管线的延迟几乎为零训练进程的精力全在计算上。4.3 分布式分片、shuffle 和 batch 的真实坑把过滤和写入 MindRecord 做完之后真正训练时还会遇到一些数据侧的坑这里集中说一下。第一个坑是分布式采样时数据重复。如果你自己写了数据读取逻辑又不小心在各 rank 之间共享了同一个采样器经常会出现同一条样本被两个 rank 同时训练。MindDataset 的num_shards和shard_id是标准的切分方式不要为了“更均匀”自己去切文件。我遇到过一次非常隐蔽的重复离线阶段把同一份数据写入了两个 MindRecord 分片而写入程序没有做全局去重检查结果训练时部分文档被重复采样下游指标始终不稳定。第二个坑是在线过滤导致每个 epoch 的样本数不一致。如果你在filter阶段动态丢样本每个 epoch 剩余样本量可能不同训练总步数就无法提前确定学习率调度也会受影响。解决思路是要么把过滤逻辑做成“固定掩码”提前计算出保留样本的索引要么在离线阶段就完成过滤线上不再动态丢弃。第三个坑是 shuffle 与去重的配合。去重之后理论上每条文本只有一个副本但 shuffle 之后一些语义相似的文本可能出现在同一个 batch 里造成“局部重复”。这个问题用全局数据重排并不能完全解决。我的做法是在构建样本时尽量保证相近文档不在同一 shard 内必要时给训练加入“batch 内去重”的检查逻辑虽然会增加一点计算量但能防止模型在局部窗口内反复看到同一主题。第四个坑是字段类型和 shape。MindRecord 里int32数组的 shape 建议写成[-1]读取时逐条拿到变长数组。如果提前把序列 padding 到固定长度再写入内存会浪费很多在线预处理也会变成负担。把 padding 留给训练时的 batch 阶段而不是写入阶段。下面给出一个简化的数据管线占用对比我实际测过文本态读取和 token 态读取的差异非常明显数据形式单卡读速参考在线额外开销适用场景原始文本文件 在线清洗较低高正则、分词、过滤小规模实验文本存入 MindRecord中等中在线分词中等规模还没有 tokenizer 物化token id 存入 MindRecord较高极低大规模预训练我现在基本只用第三行这种方式。离线多花时间把 token id 物化好训练时省下的时间和省掉的麻烦远大于离线计算的成本。5. 质量过滤的验收指标与回归实验5.1 从数据侧看质量过滤不是玄学过滤做得好不好不能靠“感觉”。我建议每跑完一轮清洗都产出一份“数据存活率报告”让每个过滤阶段的数据量和命中率透明可见。举例说明我曾经处理过一批大约 1.8 亿条文档的原始中文语料清洗流程的数据变化大致如下处理阶段剩余文档量约说明原始抓取1.8 亿含大量模板、日志、导航基础清洗 规则过滤1.1 亿删除率约 39%语言识别过滤0.95 亿排除非中文和低质量机翻精确去重 MinHash0.6 亿重复文本确实严重PPL 打分过滤0.5 亿只丢明显异常段语义去重仅高质量子集0.48 亿此时已经相对干净注意这些数字会随着抓取源和抓取方式变化不一定适用于你的场景但它们能说明一个重要现象重复文本的数量往往比你想象的多得多。仅仅做精确去重已经能砍掉接近三分之一的数据。数据侧的其他指标也很重要重复率抽样计算文档 pair 的相似度估计全局重复文档占比。如果过滤后重复率还高于 5%去重工作需要继续。多样性用 n-gram 覆盖率和 unigram 熵来衡量。如果过滤后语料熵大幅下降很可能过滤规则太重误删了大量有价值内容。语言分布确认目标语言占比稳定在预期区间。长度分布确认没有因为长度规则把长文档全部杀掉。这些指标应该作为过滤管线的“单元测试”。每次改规则、换语料来源都要重跑一遍确保新数据在分布上没有明显变化。5.2 小规模训练回归实验设计数据侧指标再漂亮最终还是要看模型训练效果。我的做法是做“小模型 短训练”的回归实验而不是直接拿大模型全量跑一遍。具体来说固定一个 0.3B 左右的小模型固定 batch size、学习率、warmup 步数、随机种子分别用原始数据和过滤后数据训练相同 token 量比如 5 亿 token。然后对比三组指标训练 loss 曲线的下降速度和平稳度。一组固定评测任务的结果比如 2 到 3 个中文分类任务和 1 个生成任务。在保留验证集上的 PPL 或困惑度差异。这里要特别注意对比时必须保证 token 数量一致或接近而不是“步数一致但数据量不一致”。如果过滤后的数据总量变少你要用更少的数据重复多个 epoch 才能对齐 token 量重复 epoch 本身就会带来过拟合副作用。更干净的做法是从清洗后的数据里随机抽取所需 token 量和原始数据同样也抽取相同 token 量保持训练量完全一致。另一个容易犯的错误是只看 loss 下降快慢。过滤后的语料通常比原始语料更容易拟合所以 loss 下降更快不一定代表模型更好。关键要看评测指标。如果过滤后 loss 更平滑但下游任务指标持平或微降说明过滤可能删掉了一些低质量但对任务交互有用的边缘样本。这种情况我会回到数据侧检查被过滤样本的类型分布尝试把部分“中等质量”样本加回来。我还会做一个小规模的“哨兵训练”在正式大模型训练开始前只用过滤后数据的一小部分比如 2000 到 5000 步快速观察 loss 和梯度范数变化。有问题的话提前半小时就能发现不用等整个训练跑完。5.3 我在几个真实项目中踩过的关键坑最后说几个我在 MindSpore 数据管线里真实踩过的坑希望能帮你少走弯路。第一规则过滤不是一次定死的要带版本号。数据来源、抓取策略一变旧规则的误杀率就可能飙升。我给过滤脚本加了版本号比如filter_v3.2每次推新规则都先在一个 20 万条的样本集上做回归对比确认新规则的命中率和误杀率没有明显劣化后再全量跑。第二PPL 打分模型的 tokenizer 要和训练 tokenizer 保持一致或至少兼容。我做过一次很尴尬的实验PPL 打分模型用的 tokenizer 是英文为主的版本中文文本被打得很碎PPL 整体偏高把大量正常文本误判成低质量。后来统一改成中文友好的 tokenizer分布才恢复正常。第三语义去重不要用在整库上一定要分层。我在一个 3 亿文档的语料上试过全量 embedding 向量化结果只是存储和计算成本就把整个流程拖得很慢。后来改成“MinHash 粗筛 语义确认”并结合优先级策略效率高了非常多。第四MindRecord 写入和读取的字段顺序、shard 数量一旦确定尽量保持稳定。如果中途改字段 schema旧数据文件可能读不出来。我给每个数据版本建一个独立的 manifest 文件记录 schema 版本、过滤规则版本、文档数量、token 量方便回溯和对比。第五在线过滤能不做就不做。刚开始我在 MindDataset 里写了很复杂的 map 函数每步训练来回做正则匹配和清洗训练的吞吐量直接掉了一个量级。后来把重活全部离线完成在线上只保留字段投影和极轻量的 threshold 判断训练速度才恢复正常。现在每收到一批新语料我第一反应不是写训练脚本而是先跑一遍过滤管线看数据存活率和分布报告再决定是否继续训练。这一套流程配合小规模回归实验基本能保证正式预训练时不会因为数据质量问题浪费大量算力。数据质量过滤没有终点每次新语料、新的过滤规则都值得你重新过一遍这套验收流程。