
大模型预训练这件事真正烧钱的地方往往不在显卡而在数据。我见过太多团队把八成精力砸在调参和并行策略上结果模型训到一半loss就崩了回头一查训练语料里混着大段乱码、重复的网页模板、还有机器翻译腔的垃圾文本。数据质量过滤不是预训练流程里最亮眼的环节但它决定了你后面所有工作的天花板。这篇内容就围绕 MindSpore 框架下的大模型预训练数据质量过滤方案展开把我在实际项目里踩过的坑、用过的规则、调过的阈值都摊开讲清楚。不管你是刚接触预训练的新手还是已经跑过几轮训练的老手应该都能从里面找到能直接抄作业的东西。1. 为什么数据过滤要在预训练之前就动手1.1 脏数据对预训练的破坏是累积性的很多人对数据质量的理解停留在“有脏数据顶多效果差一点”的层面这个判断在微调阶段勉强成立在预训练阶段完全错误。预训练的本质是让模型从海量文本里学习语言的统计规律和世界知识模型看到的每一个token都会参与梯度更新。如果语料里有百分之五的重复文本模型就会对这百分之五的内容产生过拟合倾向表现为生成时反复输出相似的句子结构。如果语料里混入了编码错误导致的乱码字符这些字符会占据词表位置稀释有效token的学习信号。更隐蔽的问题是低质量文本的“污染扩散”。举个例子网页抓取数据里经常混着导航栏、版权声明、评论区模板这些文本本身语法完整、用词正常但它们是高度重复的。模型在预训练过程中会把这些模板当作高频模式记住导致在生成任务里动不动就冒出“版权所有”“点击查看更多”这类无关内容。这种问题在微调阶段很难彻底纠正因为预训练已经把这些模式刻进了权重里。从成本角度看预训练一次动辄几十万到上百万的算力开销如果因为数据质量问题导致训练失败或者效果不达标重跑的成本是线性叠加的。而数据过滤阶段的投入无论是写规则还是跑分类器相对于训练成本几乎可以忽略不计。这笔账怎么算都划算。1.2 MindSpore 生态下数据处理的特殊性MindSpore 作为国产深度学习框架在数据处理管线上有自己的设计哲学。它提供了 MindRecord 这种高效数据格式以及 mindspore.dataset 模块下的各类数据加载和变换算子。和 PyTorch 生态里大家习惯用 HuggingFace datasets 加自定义脚本不同MindSpore 更强调数据管线的图模式执行和算子融合。这意味着在 MindSpore 里做数据质量过滤不能简单照搬 PyTorch 那边的做法。比如你想在数据加载时动态过滤就需要把过滤逻辑写成 MindSpore 能识别的算子或者提前在数据预处理阶段完成过滤生成干净的 MindRecord 文件。前者对算子编写能力有要求后者更灵活但需要额外的存储空间。我在实际项目里的选择是分两阶段走第一阶段用 Python 脚本做粗过滤和规则过滤把明显有问题的数据干掉第二阶段用 MindSpore 的 dataset 管线做精细过滤和 tokenization。这样既利用了 Python 生态的灵活性又发挥了 MindSpore 在数据加载上的性能优势。1.3 过滤目标的分层定义数据质量过滤不是一刀切的事情需要分层定义目标。我通常把过滤分为三个层次硬性过滤处理编码错误、空文本、超长重复、特殊字符占比过高的样本。这类问题没有商量余地必须直接剔除。软性过滤处理低信息密度文本、模板化内容、机器生成痕迹明显的文本。这类需要设定阈值根据语料整体质量动态调整。语义过滤处理事实错误、逻辑混乱、有害内容。这类最难自动化通常需要结合分类模型和人工抽检。这三个层次的过滤强度和计算成本依次递增实际执行时应该从硬到软逐步推进避免在垃圾数据上浪费算力。2. 硬性过滤先把明显不能用的数据扔掉2.1 编码与字符层面的清洗规则编码问题是中文预训练语料里最常见也最容易被忽视的问题。网页抓取数据经过多次转码后经常出现 UTF-8 和 GBK 混用的情况表现为文本里夹杂着“锟斤拷”这类乱码字符。这类样本如果直接进入训练模型会把这些乱码当作正常字符学习严重影响中文生成质量。我的处理规则是这样的首先统计每个样本中非打印字符和替换字符UFFFD的占比如果超过千分之五就直接丢弃。其次检查文本的字符集分布如果 ASCII 字符占比超过百分之九十但文本长度又超过两百字大概率是英文网页或者编码错误需要进一步判断。对于中文语料我还会统计中文字符占比低于百分之三十的样本通常不是目标语料。import re def check_encoding_quality(text, min_chinese_ratio0.3, max_replacement_ratio0.005): if not text or len(text.strip()) 0: return False total_chars len(text) replacement_count text.count(\ufffd) if replacement_count / total_chars max_replacement_ratio: return False chinese_chars len(re.findall(r[\u4e00-\u9fff], text)) if chinese_chars / total_chars min_chinese_ratio: return False return True这段代码看起来简单但阈值设定有讲究。min_chinese_ratio 设成 0.3 是我在多个项目里试出来的经验值设太高会误杀中英混合的技术文档设太低又过滤不干净。max_replacement_ratio 设成 0.005 是因为替换字符一旦出现往往就是成片的千分之五已经能覆盖绝大多数编码错误样本。2.2 重复文本的检测与去重策略重复数据是预训练里的隐形杀手。它的危害不只是浪费算力更严重的是会让模型对重复内容产生记忆偏好。我见过一个模型在生成技术文档时反复输出同一段话追根溯源就是训练语料里有大量重复的 API 文档。去重策略要分粒度来做。最粗粒度是文档级去重用 SimHash 或者 MinHash 计算文档指纹相似度超过阈值的只保留一份。中等粒度是段落级去重针对网页数据里反复出现的导航栏、版权声明。最细粒度是句子级去重处理那些被多次引用的名言警句。在 MindSpore 项目里我通常用 SimHash 做文档级去重因为它的计算复杂度低适合处理千万级甚至亿级的语料。具体做法是对每个文档提取特征词计算 64 位指纹然后通过汉明距离判断相似度。汉明距离小于等于 3 的视为重复文档。import hashlib from collections import defaultdict def simhash(text, hash_bits64): tokens re.findall(r[\u4e00-\u9fff]|[a-zA-Z], text.lower()) if not tokens: return 0 v [0] * hash_bits for token in tokens: token_hash int(hashlib.md5(token.encode()).hexdigest(), 16) for i in range(hash_bits): bit (token_hash i) 1 v[i] 1 if bit else -1 fingerprint 0 for i in range(hash_bits): if v[i] 0: fingerprint | (1 i) return fingerprint def hamming_distance(hash1, hash2): xor_result hash1 ^ hash2 return bin(xor_result).count(1)实际使用时我会先用 SimHash 做一轮粗去重把汉明距离小于等于 3 的文档聚类每个簇只保留最长的那篇。然后再用精确匹配做一轮句子级去重把重复出现的句子从文档里删掉。两轮下来语料的重复率通常能从百分之十几降到百分之三以下。2.3 长度与信息密度的硬阈值长度过滤看似简单但阈值设定需要结合语料来源。网页数据的长度分布很广从几个字的标题到几万字的長文都有。我的经验是设置一个动态长度区间下限设为 50 个字符上限设为 100000 个字符。低于下限的样本信息量太少高于上限的样本往往是拼接了多个页面的内容质量不可控。信息密度是另一个硬指标。我通常用“有效字符占比”来衡量有效字符包括中文、英文、数字和常用标点排除空白字符、特殊符号和重复标点。有效字符占比低于百分之六十的样本直接丢弃。这个规则能过滤掉大量由表格转换错误、PDF 解析错误产生的垃圾文本。注意长度过滤的阈值不要设得太死。我早期项目里把下限设成 200 字符结果误杀了很多高质量的短文本比如技术术语解释、简短问答对。后来改成 50 字符配合信息密度检查效果就好很多。3. 软性过滤用规则和模型识别低质内容3.1 模板化文本的特征与识别模板化文本是网页抓取数据里的顽疾。它的特点是结构高度一致、用词固定、信息量低。常见的模板包括网站导航栏、面包屑导航、版权声明、相关阅读推荐、评论区通用回复。识别模板化文本有两种思路。一种是基于规则统计文本中是否包含高频模板词比如“版权所有”“转载请注明”“点击查看更多”“上一篇”“下一篇”。如果这些词的出现密度超过阈值就判定为模板文本。另一种是基于聚类把语料里出现频率极高的 n-gram 提取出来包含这些 n-gram 的文本大概率是模板。我在项目里用的是混合策略先用规则快速过滤掉明显的模板文本再用 n-gram 频率分析找出漏网的。具体做法是统计整个语料库里出现频率最高的 1000 个 5-gram如果某个样本包含其中超过 5 个高频 n-gram就标记为疑似模板进入人工抽检队列。from collections import Counter def extract_high_freq_ngrams(texts, n5, top_k1000): ngram_counter Counter() for text in texts: tokens re.findall(r[\u4e00-\u9fff]|[a-zA-Z], text) for i in range(len(tokens) - n 1): ngram .join(tokens[i:in]) ngram_counter[ngram] 1 return set([ngram for ngram, _ in ngram_counter.most_common(top_k)]) def is_template_text(text, high_freq_ngrams, threshold5): tokens re.findall(r[\u4e00-\u9fff]|[a-zA-Z], text) match_count 0 for i in range(len(tokens) - 5 1): ngram .join(tokens[i:i5]) if ngram in high_freq_ngrams: match_count 1 return match_count threshold这套方法在网页语料上效果很好能把模板文本的漏网率控制在百分之五以内。但要注意高频 n-gram 的提取需要在足够大的语料样本上做样本太小统计不准确。3.2 机器生成文本的判别思路预训练语料里混入机器生成文本的情况越来越普遍。这些文本通常来自早期的翻译系统、摘要系统或者模板化写作工具特点是语法正确但表达生硬、用词重复、缺乏连贯性。判别机器生成文本可以从几个维度入手。第一是困惑度用一个小型语言模型计算文本的困惑度机器生成文本的困惑度通常异常低或者异常高。第二是词汇丰富度统计文本的 type-token ratio机器生成文本的词汇丰富度往往偏低。第三是句长分布机器生成文本的句长往往过于均匀。我在实际项目里用的是一个轻量级的分类器方案用人工标注的少量样本训练一个二分类模型特征包括困惑度、词汇丰富度、句长方差、标点分布等。这个分类器不需要很准召回率控制在百分之七十左右即可剩下的交给人工抽检。3.3 阈值调优的实操经验软性过滤的难点不在规则本身而在阈值调优。阈值设得太松过滤不干净设得太紧误杀高质量数据。我的经验是分三步走第一步先在一个小规模语料样本上跑一遍过滤统计每个规则的过滤比例。如果某个规则的过滤比例超过百分之二十说明阈值可能太紧需要放宽。第二步对过滤掉的样本做人工抽检随机抽 100 条看有多少是误杀。如果误杀率超过百分之十就需要调整规则。第三步在正式过滤前先用过滤后的语料训练一个小模型和不过滤的语料做对比。如果过滤后的效果反而更差说明过滤规则有问题。这个流程听起来繁琐但比起训练失败重跑的成本这点投入完全值得。我在最近一个项目里光是阈值调优就花了两天时间但最终语料质量提升了将近三成训练收敛速度明显加快。4. 在 MindSpore 管线里落地过滤逻辑4.1 离线过滤与在线过滤的取舍在 MindSpore 里做数据过滤第一个决策是离线做还是在线做。离线过滤是指用 Python 脚本提前处理原始语料生成干净的 MindRecord 文件训练时直接加载。在线过滤是指在 dataset 管线里动态执行过滤算子训练时实时处理。离线过滤的优点是灵活可以用任意 Python 库调试方便过滤结果可复用。缺点是占用额外存储空间而且过滤规则变更后需要重新生成数据。在线过滤的优点是节省存储规则变更后立即生效。缺点是对算子编写能力有要求调试困难而且过滤逻辑会拖慢数据加载速度。我的建议是硬性过滤和规则过滤放在离线阶段做因为这些规则相对稳定而且过滤后的数据可以复用。语义过滤和模型过滤放在在线阶段做因为这些规则可能需要根据训练效果动态调整。如果存储空间充足全部离线做是最省心的方案。4.2 用 MindSpore dataset 算子实现动态过滤如果选择在线过滤就需要用 MindSpore 的 dataset 算子来实现。MindSpore 提供了 map、filter、batch 等算子其中 filter 算子可以用来做条件过滤。但 filter 算子的表达能力有限复杂的过滤逻辑需要先用 map 算子计算过滤标记再用 filter 算子根据标记过滤。import mindspore.dataset as ds import numpy as np def compute_quality_score(text): # 计算文本质量分数返回 0 到 1 之间的浮点数 if not text or len(text.strip()) 0: return 0.0 chinese_ratio len(re.findall(r[\u4e00-\u9fff], text)) / len(text) replacement_ratio text.count(\ufffd) / len(text) score chinese_ratio * (1 - replacement_ratio) return min(max(score, 0.0), 1.0) def filter_by_quality(text, threshold0.5): score compute_quality_score(text) return score threshold # 构建 dataset 管线 dataset ds.MindDataset(raw_data.mindrecord) dataset dataset.map(operationscompute_quality_score, input_columns[text], output_columns[quality_score]) dataset dataset.filter(predicatelambda score: score 0.5, input_columns[quality_score]) dataset dataset.batch(32)这段代码展示了基本的在线过滤流程。需要注意的是MindSpore 的 filter 算子对 predicate 函数的支持有限复杂的过滤逻辑建议先用 map 算子计算好标记再用简单的条件过滤。4.3 过滤管线的性能优化在线过滤最大的问题是性能。如果过滤逻辑太复杂数据加载会成为训练瓶颈。我在项目里踩过这个坑一开始把 SimHash 去重放在在线阶段做结果数据加载速度慢了十倍GPU 利用率从百分之九十掉到百分之三十。优化思路有几个方向。第一是把复杂计算移到离线阶段在线只做简单的阈值判断。第二是用 MindSpore 的并行加载能力设置 num_parallel_workers 参数让多个线程并行执行过滤逻辑。第三是用缓存机制把过滤结果缓存到磁盘下次训练直接加载。dataset ds.MindDataset(raw_data.mindrecord, num_parallel_workers8) dataset dataset.map(operationscompute_quality_score, input_columns[text], output_columns[quality_score], num_parallel_workers8) dataset dataset.filter(predicatelambda score: score 0.5, input_columns[quality_score], num_parallel_workers8) dataset dataset.batch(32, drop_remainderTrue)num_parallel_workers 的设置需要根据 CPU 核心数调整一般设成核心数的百分之七十到八十比较合适。设太高会导致线程切换开销过大反而降低性能。5. 过滤效果评估与迭代5.1 用训练指标反推数据质量数据过滤做得好不好最终要看训练效果。我通常关注几个指标训练 loss 的下降曲线、验证集 perplexity、下游任务的表现。如果 loss 下降平滑且收敛快说明数据质量不错。如果 loss 震荡严重或者下降缓慢可能是数据里还有较多噪声。另一个有用的指标是梯度范数。如果梯度范数异常大或者异常小往往意味着某个 batch 里混入了异常样本。我习惯在训练脚本里记录每个 batch 的梯度范数如果发现某个 batch 的梯度范数明显偏离均值就回头检查那个 batch 的数据。5.2 人工抽检的组织方式自动化过滤再完善也离不开人工抽检。我的做法是每周组织一次抽检从过滤后的语料里随机抽 200 条让团队成员各自标注质量等级。标注结果汇总后计算过滤规则的准确率和召回率然后针对性调整规则。抽检的样本量不需要很大但要有代表性。我会按照语料来源分层抽样比如网页数据抽 50 条、书籍数据抽 50 条、论文数据抽 50 条、问答数据抽 50 条。这样能发现不同来源数据的特有问题。5.3 过滤规则的版本管理过滤规则是需要迭代的所以版本管理很重要。我习惯把过滤规则写成一个配置文件每次调整都记录变更内容和原因。这样当训练效果出现波动时可以快速定位是哪次规则变更导致的。配置文件里除了规则参数还会记录每条规则的过滤比例和误杀率。这些数据积累下来能帮助判断规则的有效性。比如某条规则的过滤比例从百分之五降到百分之一说明语料质量在提升或者规则需要调整了。6. 几个容易踩的坑和应对方法6.1 过度过滤导致数据多样性下降过滤太狠是新手常犯的错误。我见过一个项目为了追求“干净”语料把过滤阈值设得极高结果过滤后只剩下百分之十的数据而且都是那种四平八稳的新闻稿。用这种语料训练出来的模型生成的内容毫无个性遇到稍微偏门的领域就胡言乱语。应对方法是设置过滤比例的上限。我通常会把总过滤比例控制在百分之三十以内如果超过这个比例就说明阈值太紧了。另外要监控过滤后语料的领域分布确保没有某个领域被过度过滤。6.2 规则冲突与优先级问题多条过滤规则同时使用时可能会出现冲突。比如长度过滤要求文本至少 50 字符但模板过滤可能把一些短小精悍的模板文本标记为低质。如果两条规则都生效可能会误杀一些边界样本。解决方法是给规则设定优先级。硬性规则优先级最高直接决定样本去留。软性规则优先级较低只做标记不做删除最终由综合评分决定。我在项目里用的是一个加权评分系统每条规则输出一个分数最后加权求和低于阈值的才删除。6.3 过滤逻辑与 tokenization 的配合过滤逻辑和 tokenization 的配合也很重要。有些过滤规则在字符层面执行有些在 token 层面执行。如果过滤在 tokenization 之前做可能会把一些 tokenization 后会变成有效 token 的字符过滤掉。比如某些特殊符号在 tokenization 后会被映射到特殊 token但在字符层面看起来像噪声。我的做法是把字符层面的过滤放在 tokenization 之前把 token 层面的过滤放在 tokenization 之后。这样既能过滤掉明显的噪声字符又不会误杀 tokenization 后有效的 token。6.4 分布式训练下的数据一致性在分布式训练场景下数据过滤还要考虑一致性问题。如果每个计算节点独立过滤数据可能会导致节点间的数据分布不一致影响训练稳定性。特别是在使用数据并行时每个节点看到的 batch 应该来自相同的分布。解决方法是把过滤后的数据统一存储然后通过 MindSpore 的分布式采样器来分配数据。这样能保证每个节点看到的数据分布一致。如果必须在线过滤就要确保过滤逻辑是确定性的同样的输入在任何节点上都产生同样的输出。7. 从过滤到增强数据质量的持续提升数据过滤只是数据质量工作的起点不是终点。过滤之后还可以通过数据增强来进一步提升语料质量。常见的手段包括对低质但信息量大的文本进行改写、对重复文本进行合并去重、对缺失字段进行补全。我在项目里尝试过用一个小型语言模型对过滤后的语料做改写把那些语法不通但信息量大的文本改写成通顺的表达。这个做法效果不错但要注意控制改写比例改写太多会引入模型自身的偏见。另一个思路是构建数据质量反馈闭环。把训练过程中发现的低质样本反馈到过滤规则里持续优化过滤逻辑。这个闭环跑通之后数据质量会随着训练轮次逐步提升。数据质量过滤这件事说到底是一个投入产出比极高的环节。花在过滤上的每一分精力都会在训练效果上得到回报。MindSpore 生态提供了足够灵活的工具链关键是要理解过滤的目标和边界不要为了过滤而过滤。我在实际项目里的体会是过滤规则宁少勿多阈值宁松勿紧先保证数据多样性再逐步提升数据质量。这个顺序搞反了后面要花更多时间补救。