大模型预训练这件事,真正跑过一轮的人都会有一个共同感受:模型结构、并行策略、算力调度这些"硬骨头"固然难啃,但最后拖垮训练效果、让loss曲线诡异波动的,往往是数据本身。我前后参与过几个基于MindSpore的中等规模预训练项目,从最初"把能爬到的文本全喂进去"的莽撞阶段,到后来老老实实搭一套数据质量过滤流水线,中间踩的坑足够写一本小册子。这篇就围绕"MindSpore大模型预训练场景下的数据质量过滤方案"展开,把过滤这件事从为什么做、过滤什么、怎么在MindSpore生态里落地、以及实测中那些文档不会告诉你的细节讲透。不管你是刚接触MindSpore数据管道的新手,还是已经跑过几轮预训练、想优化数据质量的老手,都能从里面找到能直接抄作业的部分。
1. 为什么预训练阶段的数据过滤不能"差不多就行"
1.1 脏数据对loss曲线的真实影响
很多人对数据质量的理解停留在"有乱码就清掉"这种朴素层面,但实际预训练里,脏数据的破坏力远比想象中隐蔽。我在一个13B参数规模的实验里做过对照:同一份原始语料,一份只做最基础的去重和长度截断,另一份走了完整的质量过滤流水线,其余超参完全一致。结果前者在训练到约40%进度时,loss开始出现周期性抖动,梯度范数(grad norm)时不时飙到正常值的3到5倍;后者曲线平滑得多,最终验证集困惑度(perplexity)低了将近8%。
这个差异的根源在于,低质量文本会向模型注入"高熵噪声"。比如一段机器翻译残留的中英混杂文本、一段HTML标签没清干净的网页正文、一段重复了上百遍的广告语,它们在token层面呈现出的统计规律和自然语言完全不同。模型为了拟合这些样本,会强行调整一部分注意力权重和FFN参数,而这些调整对真正的语言建模毫无帮助,反而干扰了正常的学习信号。更麻烦的是,这类样本往往集中在某些特定来源,如果采样时没有做来源均衡,它们会在某个训练阶段集中出现,造成loss的阶段性异常。
1.2 过滤不是"越狠越好":召回与精度的权衡
新手最容易犯的第二个错误,是走向另一个极端——过滤规则堆得越多越好,恨不得把90%的数据都扔掉。我见过一个团队,规则写了三十多条,最后留下的语料只有原始的35%,结果模型严重欠拟合,泛化能力极差。这里有个核心认知:数据过滤本质上是一个召回率(recall)和精度(precision)的权衡问题。
过滤得太松,脏数据混进来,训练效率低、效果差;过滤得太紧,把本来有价值的边缘样本(比如口语化表达、专业领域术语密集的文本、短但信息量高的句子)也误杀了,数据多样性和规模都不够。大模型预训练恰恰需要海量且多样的数据,所以过滤方案的设计目标不是"最干净",而是"在可接受的噪声水平下保留最大的有效信息量"。这个平衡点怎么找,后面会结合具体规则讲。
1.3 MindSpore数据管道的特点决定了过滤要"前置"
在PyTorch生态里,很多人习惯用DataLoader的collate_fn或者自定义Sampler做在线过滤。但MindSpore的数据处理范式不太一样,它更强调数据管道的前置化和静态图友好。MindSpore的mindspore.dataset模块提供了一整套算子式的数据变换接口,这些算子在map操作里执行时,如果涉及Python原生逻辑,会受GIL和单线程性能的限制。所以一个关键原则是:能在离线阶段用Spark、Ray或者纯Python批处理做完的重过滤,就不要放到训练时的数据管道里做。
训练时的数据管道只保留轻量的、必须动态执行的过滤(比如基于token长度的截断、简单的特殊字符剔除),而像语言识别、困惑度打分、语义去重这类重计算,全部前置到离线清洗阶段。这样既保证了训练吞吐,又让过滤逻辑更容易调试和复现。这个"前置"思路贯穿整个方案设计,后面每个环节都会体现。
2. 数据质量过滤到底在过滤什么:六类核心问题拆解
2.1 重复数据:不只是"完全一样"那么简单
重复是预训练数据里最普遍也最容易被低估的问题。它分三个层次:
- 完全重复:整篇文档一字不差地出现多次。这种用哈希(如SHA256)就能搞定,但要注意,网页抓取场景下同一篇文章可能因为URL参数不同被存成多条,哈希前要先做URL归一化。
- 近似重复:段落级别的高度相似,比如同一新闻被不同站点转载、只改了几个词。这类要用MinHash + LSH(局部敏感哈希)或者SimHash来做,纯哈希抓不到。
- 模板化重复:大量结构相同、只替换了少量实体的文本,比如"XX公司成立于XXXX年,注册资本XXX万元"这种工商信息模板。这类重复最隐蔽,因为它单看每一条都"像正常文本",但整体分布极度单一。
我在一个项目里统计过,某批网页语料经过MinHash去重后,文档数量从1.2亿降到了6800万,接近腰斩。如果不做这一步,模型会在这些重复内容上反复学习,等效于给这部分数据加了极高的采样权重,严重破坏数据分布的均衡性。
2.2 语言与编码混杂:中文语料里的"隐形杀手"
做中文预训练,最头疼的就是语料里混着大量非目标语言内容。常见的有:英文技术文档片段、日韩字符、乱码、以及各种emoji和特殊符号的堆砌。这些内容如果不清掉,会稀释中文语言建模的信号。
判断语言不能只靠字符集统计,因为中文文本里合法地包含英文单词、数字、标点。我的经验是用fastText的语言识别模型做初筛,再叠加一层基于字符比例的规则兜底。具体来说,对一段文本统计中文字符占比、拉丁字母占比、其他字符占比,如果中文字符占比低于某个阈值(比如30%),且拉丁字母占比很高,就判定为非中文主导,剔除。阈值不能一刀切,技术类、代码类语料里英文占比天然偏高,需要按数据来源分桶设置不同阈值。
编码问题同样常见。GBK、GB2312、UTF-8混用,或者抓取时编码识别错误导致的乱码(比如"锟斤拷"这种经典乱码),必须在清洗早期统一转成UTF-8,并对转换失败的样本直接丢弃。这里有个细节:不要用errors='ignore'静默丢弃非法字节,那样会悄悄改变文本内容,最好用errors='replace'标记出来,再根据替换字符的比例决定是否丢弃整条。
2.3 低信息密度文本:短、乱、无意义的识别
低信息密度文本包括:导航栏文字、版权声明、评论区灌水、纯符号串、超短句等。这类文本单条看没什么危害,但量大之后会显著拉低平均信息量。
识别手段主要有几个维度:长度(字符数或token数低于阈值)、标点符号占比(超过一定比例说明是符号堆砌)、重复字符比例(比如"哈哈哈哈哈哈")、词表覆盖率(用一个小词表统计文本中常见词的比例,太低说明是生僻乱码)。我一般会组合使用:长度小于20个字符且不含句末标点的,直接丢;标点占比超过40%的,丢;连续重复字符超过5个的,做压缩处理而不是直接丢(因为有些正常文本也会有"!!!!!"这种表达)。
2.4 有害与违规内容:必须过滤但需谨慎处理
这部分涉及安全合规,是过滤方案里不可省略的一环。包括辱骂、歧视、暴力、色情等明显违规内容。处理方式通常是关键词黑名单 + 分类模型双层过滤。关键词黑名单负责快速拦截明显违规样本,分类模型负责识别变体表达。
需要强调的是,这类过滤要格外注意误伤。比如医学语料里会出现大量正常的人体描述词汇,如果黑名单太粗暴,会把整个医学领域的数据误杀。我的做法是分领域维护不同的敏感词表,并且对分类模型的判定结果做人工抽检,持续调整阈值。这块内容敏感,具体词表和模型细节这里不展开,核心原则是:宁可漏掉一些边缘样本,也不要大规模误伤正常语料。
2.5 领域失衡:过滤之外的"分布治理"
严格说,领域失衡不算"脏数据",但它对预训练效果的影响和脏数据一样大。如果语料里80%是新闻,10%是论坛,剩下的是杂项,模型在新闻类任务上表现会很好,但一到对话、代码、学术场景就拉胯。
治理手段是按来源和主题打标签,再做重采样。给每条数据标注来源(新闻、论坛、百科、代码、书籍等)和主题(用轻量分类模型或关键词规则),然后在构造训练集时按目标比例混合。比如通用中文大模型,新闻类可能占30%、百科20%、论坛20%、书籍15%、代码10%、其他5%。这个比例没有标准答案,要根据模型的下游用途调整。
2.6 隐私信息:合规红线不能碰
文本里可能包含手机号、身份证号、邮箱、银行卡号等个人隐私信息。这类内容必须用正则表达式做脱敏或剔除。手机号(1开头11位)、身份证(18位)、邮箱这些模式相对固定,正则能覆盖大部分。但要注意,脱敏不是简单替换成星号就完事,因为替换后的文本可能变得不自然,影响语言建模。我的做法是:对隐私信息密集的文档直接丢弃,对偶发出现的做替换处理。
3. 在MindSpore生态里搭建过滤流水线的工程实践
3.1 整体架构:离线重过滤 + 在线轻过滤
前面提过"前置"原则,这里给出具体的架构分层。整个流水线分三层:
| 层级 | 执行时机 | 主要任务 | 技术选型 |
|---|---|---|---|
| 离线粗过滤 | 数据入库前 | 编码统一、完全去重、格式清洗 | Spark / Ray / 多进程Python |
| 离线精过滤 | 训练集构建时 | 语言识别、近似去重、质量打分、隐私脱敏 | fastText + MinHash + 规则引擎 |
| 在线轻过滤 | 训练时数据管道 | 长度截断、特殊字符剔除、动态采样 | mindspore.dataset 算子 |
离线两层用批处理框架跑,产出干净的、带元数据标签的语料;在线层用MindSpore的dataset算子做最后的适配。这样分工的好处是,重计算不占用训练资源,且离线结果可缓存、可复现。
3.2 用mindspore.dataset构建轻量过滤管道
在线层虽然只做轻过滤,但写法有讲究。MindSpore的dataset.map支持num_parallel_workers并行,但Python函数的并行效率受GIL限制,所以在线过滤函数要尽量用numpy或MindSpore内置算子实现,避免纯Python循环。
一个典型的在线过滤管道长这样:
import mindspore.dataset as ds import mindspore.dataset.text as text def build_dataset(data_files, batch_size=32, max_len=2048): dataset = ds.TextFileDataset(data_files, shuffle=True) # 轻量清洗:去除控制字符 dataset = dataset.map(operations=text.NormalizeUTF8(), input_columns=["text"]) # tokenize tokenizer = text.BertTokenizer(vocab_file="vocab.txt") dataset = dataset.map(operations=tokenizer, input_columns=["text"], output_columns=["input_ids"]) # 长度过滤与截断 dataset = dataset.map(operations=lambda x: (x[:max_len],), input_columns=["input_ids"], output_columns=["input_ids"], num_parallel_workers=8) dataset = dataset.batch(batch_size, drop_remainder=True) return dataset这里有个坑:lambda函数在map里并行执行时,如果逻辑复杂,性能会急剧下降。所以长度截断这种简单操作可以放这里,但任何涉及查表、正则、模型推理的逻辑,一律前置到离线阶段。
3.3 离线精过滤的并行化:别让单机成为瓶颈
离线精过滤的数据量动辄TB级,单机跑不现实。我的做法是用Ray做分布式,把每条数据的过滤逻辑封装成一个纯函数,Ray负责调度到多台机器。核心思路是按文件分片,每个分片独立过滤,最后合并。
语言识别和MinHash去重是计算大头。fastText推理很快,单核每秒能处理上万条短文本;MinHash的瓶颈在签名计算和LSH索引构建,可以用datasketch库,配合Ray的并行map。实测下来,8台16核机器处理1TB原始语料,完整精过滤大概需要6到8小时,这个成本相对于训练本身是可以接受的。
3.4 过滤结果的可追溯性:元数据要存好
这一点特别重要但经常被忽略:每条数据经过了哪些过滤、为什么被保留或丢弃,都要有记录。我一般会给每条数据附加一个元数据字典,包含:来源、原始长度、过滤后长度、语言判定结果、质量分、命中的过滤规则列表。
这样做的好处是,当训练效果不理想时,可以回溯是不是某条规则误杀了关键数据;当需要调整过滤强度时,可以基于元数据快速重新筛选,而不用重跑整个流水线。元数据用Parquet格式存储,和语料本身分开,方便用Pandas或PyArrow做分析。
4. 质量打分模型:从规则到学习的进阶方案
4.1 规则打分的局限与适用场景
纯规则打分(长度、标点比例、词表覆盖率加权求和)实现简单、可解释性强,适合作为第一版方案。但它的局限也很明显:规则是人工设计的,很难覆盖所有低质量模式。比如一段语法正确、长度适中、标点正常的文本,但内容空洞、逻辑混乱,规则打分给不出低分。
所以规则打分适合做"粗筛",把明显不合格的样本快速剔除,剩下的交给更精细的手段。
4.2 基于困惑度的质量评估
一个经典做法是用一个在高质量语料上训练的小型语言模型,对每条数据计算困惑度(perplexity)。困惑度高的文本,说明它不符合高质量语言的统计规律,可能是低质量样本。这个方法的原理很直观:高质量文本在好的语言模型下应该"不意外",困惑度低。
实操中,用一个小型GPT或LSTM在维基、书籍等高质量语料上训练,然后对全量数据打分。困惑度阈值需要根据数据分布调,一般取分位数(比如保留困惑度最低的70%)。这个方法的成本比规则高,但比人工标注低得多,效果也明显更好。
4.3 用分类器做质量判别
更进一步,可以训练一个二分类模型,正样本是人工标注的高质量文本,负样本是低质量文本。特征可以用文本的统计特征 + 预训练模型的embedding。这个分类器可以捕捉到规则和困惑度都抓不到的语义层面的质量问题。
不过要注意,分类器的训练数据本身要足够多样,否则会把分类器没见过的正常文本误判为低质量。我一般会用主动学习的方式,先训一版,对边界样本人工复核,迭代几轮。
4.4 打分结果的融合策略
规则分、困惑度分、分类器分,三个分数怎么融合?我的经验是加权 + 分位数截断。先各自归一化到0-1,然后按经验权重(比如规则0.3、困惑度0.4、分类器0.3)加权求和,最后取总分的最低分位数作为过滤阈值。权重和阈值都要在验证集上通过小规模预训练实验来调,不能拍脑袋定。
5. 实测中的坑与调优经验
5.1 去重阈值调过头,数据多样性崩了
MinHash的相似度阈值(Jaccard similarity)设多少合适?我一开始设了0.8,结果发现很多正常的技术文档因为共享大量术语和句式,被误判为重复。后来调到0.9,误杀明显减少。经验是:中文语料的相似度阈值要比英文高一些,因为中文的用词重复度天然更高。另外,去重要分来源做,新闻类可以严一点(0.85),技术文档类要松一点(0.92)。
5.2 语言识别在短文本上翻车
fastText在长文本上准确率很高,但短文本(比如10个字以内)经常判错。我的补救方案是:对短文本不依赖语言识别,直接用字符集规则判断——只要包含中文字符且中文字符占比超过50%,就保留。这个兜底规则救回了不少短但有用的样本。
5.3 过滤后数据量骤降,如何补救
如果过滤后发现数据量不够,有几个补救方向:一是放宽某些规则的阈值,比如长度下限从20降到10;二是从新的数据源补充,比如之前没纳入的垂直领域语料;三是用数据增强,比如对高质量长文本做合理切分,生成更多训练样本。但要注意,补救不能以牺牲质量为代价,宁可多花时间找新数据,也不要把已经过滤掉的脏数据再放回来。
5.4 训练时的动态采样:让过滤效果最大化
过滤完之后,训练时的采样策略也很关键。我一般会用温度采样,给高质量数据更高的采样权重,但保留一定比例的低分数据(比如5%),避免模型对高分数据的分布过拟合。这个比例需要通过实验调,太高了脏数据影响大,太低了多样性不足。
5.5 监控过滤流水线的稳定性
过滤流水线本身也要监控。我见过因为某个正则表达式写错,导致整批数据被误删的事故。所以每次跑完过滤,都要输出统计报告:各规则的命中率、过滤前后数据量对比、各来源的保留率、质量分分布。这些指标异常时能第一时间发现。
6. 一个可复现的最小过滤方案
如果你现在就要上手,我给一个最小可行的方案,按优先级排序:
- 编码统一 + 完全去重:用哈希去重,成本最低,收益最直接。
- 长度和标点规则过滤:剔除超短、符号堆砌的样本。
- 语言识别:用fastText筛掉非目标语言。
- MinHash近似去重:阈值从0.9开始,按来源微调。
- 隐私信息正则脱敏:手机号、身份证、邮箱。
- 质量打分:先用规则打分,有条件再上困惑度模型。
这六步做完,数据质量会有质的提升。后面再根据训练效果,逐步加入分类器打分、领域均衡等进阶手段。
我在实际项目里最大的体会是:数据过滤没有一劳永逸的方案,它是一个需要持续迭代的工程。每换一个数据源、每调整一次模型目标,过滤规则和阈值都可能要重新校准。把它当成一个需要长期维护的模块,而不是一次性的脚本,心态上会从容很多。另外,别迷信"全自动",人工抽检永远是不可省略的一环——机器能帮你处理99%的样本,但剩下1%的边界情况,往往决定了最终效果的上限。