1. 大模型预训练里,数据质量过滤到底在解决什么问题
做过大模型预训练的人都有一个共识:模型效果的上限,很大程度上在数据准备阶段就已经被决定了。算力可以堆,并行策略可以调,学习率可以反复试,但如果喂进去的语料本身充满乱码、重复段落、机器翻译腔、广告垃圾和低信息密度的口水文,那训练出来的模型大概率会表现得"什么都懂一点,但什么都不精"。这就是数据质量过滤方案存在的意义。
我接触 MindSpore 这套框架有一段时间了,从最早的 ModelArts 上跑小模型,到后来用 MindSpore 做百亿参数级别的预训练实验,踩过的坑不算少。数据质量过滤这个环节,说实话是最容易被低估、又最容易翻车的一环。很多团队把精力全放在并行策略、混合精度、算子优化上,结果数据管道里混进了大量低质样本,loss 曲线看着挺漂亮,下游评测一塌糊涂。
这篇内容我想聊的是:在 MindSpore 生态下,一套可落地的大模型预训练数据质量过滤方案应该怎么设计、怎么实现、怎么排查问题。核心关键词就是MindSpore、大模型预训练、数据质量过滤。适合谁看?如果你正在用 MindSpore 做预训练,或者准备从其他框架迁移过来,又或者你负责的是数据工程这一块,那这篇内容应该能帮你少走一些弯路。即便你只是刚接触 MindSpore,想了解它的数据处理能力,也能从里面找到可参考的思路。
先说清楚一个前提:数据质量过滤不是一个"跑个脚本就完事"的动作,它是一整套流水线,包含规则过滤、模型打分、去重、配比采样等多个环节,每个环节都有它的取舍逻辑。下面我会按设计思路、核心细节、实操实现、问题排查四个大块来展开,尽量把每个决策背后的"为什么"讲透。
2. 整体方案设计与思路拆解
2.1 为什么不能只靠单一规则过滤
刚入门的时候,很多人会写一堆正则表达式,把 HTML 标签、特殊符号、超短文本过滤掉,觉得这就叫数据清洗了。我早期也这么干过,结果发现两个问题:一是规则永远写不全,新的脏数据类型层出不穷;二是规则过滤太"硬",容易误杀。比如一篇技术博客里包含大量代码符号,正则一刀切下去,好数据也被干掉了。
所以一套靠谱的方案应该是分层过滤的思路。我通常把它分成四层:
- 第一层:格式与编码规范化。统一编码、去除控制字符、修复乱码,这是最基础的,不做后面全是白搭。
- 第二层:规则过滤。基于统计特征和启发式规则,快速筛掉明显不合格的样本,比如长度过短、重复率过高、特殊符号占比异常。
- 第三层:模型打分。用一个轻量级质量分类模型给每条数据打分,区分"高质量自然语言"和"低质内容",这一层是精度提升的关键。
- 第四层:语义去重。用 MinHash、SimHash 或者向量相似度做近似去重,避免模型在重复内容上反复过拟合。
这四层的顺序不能乱。规则过滤放在模型打分前面,是因为模型推理是有成本的,先用便宜的规则砍掉一大半垃圾,能显著降低整体开销。去重放在最后,是因为去重本身计算量也不小,而且它依赖前面已经过滤干净的语料,否则会在垃圾数据上浪费算力。
2.2 MindSpore 在这套方案里的角色定位
MindSpore 本身提供了mindspore.dataset这个数据处理模块,支持map、filter、batch、shuffle等算子,可以构建流式数据管道。但要注意,MindSpore 的 dataset 管道更适合做在线处理和轻量过滤,重度的离线过滤和模型打分建议放在独立的数据工程流程里,比如用 Spark 或者 Ray 做分布式预处理,产出的干净语料再交给 MindSpore 做训练时的加载。
为什么这么设计?因为预训练语料动辄几百 GB 到几 TB,如果全部塞进 MindSpore 的 dataset 管道里做复杂过滤,会拖慢训练时的数据供给速度,GPU/NPU 经常处于等数据的饥饿状态。我实测过,把模型打分环节放进训练管道,吞吐直接掉了一半以上。所以合理的分工是:离线阶段做重过滤,在线阶段只做必要的轻量校验和格式转换。
2.3 过滤强度与数据保留率的平衡
这是最考验经验的地方。过滤太松,垃圾数据进模型;过滤太狠,数据量不够,模型欠拟合。我一般会先做一轮小规模实验,用不同的过滤阈值跑几个 baseline,看下游任务的评测指标变化。
一个实用的经验值是:经过完整过滤后,保留率在 30% 到 60% 之间比较健康。如果保留率低于 20%,说明阈值可能过严,或者原始数据质量太差;如果高于 80%,那基本等于没怎么过滤。当然这个数字跟数据来源强相关,网页爬取的数据保留率通常偏低,而已经经过初步清洗的书籍、论文语料保留率会高一些。
3. 核心细节解析与实操要点
3.1 格式规范化:最容易被跳过却最重要的一步
格式规范化听起来简单,但细节特别多。我列几个必须处理的点:
- 编码统一:全部转成 UTF-8,处理掉 GBK、Latin-1 等混杂编码导致的乱码。Python 里可以用
ftfy库自动修复常见的 mojibake 问题。 - 控制字符清理:去掉
\x00到\x1f之间的不可见控制字符,但要注意保留\n、\t这些有意义的空白符。 - Unicode 规范化:用 NFC 或 NFKC 形式统一,避免同一个字符有多种表示方式导致去重失效。
- HTML 残留清理:即使已经做过 HTML 解析,仍可能有
、&这类实体残留,需要统一替换。
提示:格式规范化一定要在过滤流水线的最前面做,否则后面的长度统计、字符占比计算都会因为编码问题失真。
3.2 规则过滤的关键指标设计
规则过滤的核心是设计一套能区分好坏数据的统计指标。我常用的有这几个:
| 指标 | 含义 | 典型阈值 | 说明 |
|---|---|---|---|
| 字符长度 | 样本总字符数 | 200 ~ 100000 | 太短信息不足,太长可能是拼接垃圾 |
| 平均行长度 | 总字符数/行数 | 10 ~ 2000 | 过短说明碎片化严重 |
| 特殊符号占比 | 非字母数字字符比例 | < 0.3 | 过高可能是代码或乱码 |
| 重复行占比 | 重复行数/总行数 | < 0.3 | 过高说明模板化或爬虫重复 |
| 停用词占比 | 常见停用词出现频率 | > 0.02 | 过低可能是非自然语言 |
| 数字占比 | 数字字符比例 | < 0.2 | 过高可能是日志或表格 |
这些阈值不是拍脑袋定的,而是通过对一批人工标注的好/坏样本做统计分析得出的。我建议你也先标注几百条样本,跑一遍分布统计,再确定适合自己数据的阈值。直接抄别人的阈值,大概率水土不服。
3.3 模型打分:轻量分类器的选型与训练
模型打分这一层,核心是训练一个二分类器,判断样本是"高质量"还是"低质量"。选型上有几个考量:
- 模型不能太大:因为要对全量语料打分,推理成本必须可控。我一般用 1 亿到 3 亿参数的小模型,比如轻量级的 BERT 变体。
- 训练数据要平衡:正样本从高质量来源(书籍、百科、优质问答)采样,负样本从被规则过滤掉的垃圾里采样,比例控制在 1:1 到 1:2。
- 输出用概率而非硬标签:保留打分概率,后续可以按分位数灵活调整阈值,而不是一刀切。
在 MindSpore 里训练这个分类器,可以直接用mindspore.nn.BertModel加一个分类头,损失函数用BCEWithLogitsLoss。训练完成后导出成 MindIR 格式,推理时用mindspore.nn.GraphCell加载,性能比动态图模式好不少。
3.4 语义去重的算法选择
去重这块,MinHash + LSH 是性价比最高的方案。它的原理是把每条文本转成一组哈希签名,通过 Jaccard 相似度判断是否重复。相比精确去重,它能识别"改了几个词但实质相同"的近似重复。
具体参数上,我一般用128 个哈希函数,5 个 band,每个 band 4 行。这个配置在召回率和误判率之间比较平衡。如果数据量特别大,可以先用 SimHash 做粗筛,再用 MinHash 精筛。
注意:去重一定要在文档级别和段落级别各做一次。文档级去重解决整篇重复,段落级去重解决同一段内容在不同文档里反复出现的问题。后者往往更隐蔽,但对模型伤害更大。
4. 实操过程与核心环节实现
4.1 环境准备与依赖安装
先说一下环境。我用的组合是 MindSpore 2.x + Python 3.9 + Ascend 910 或者 GPU 环境。安装 MindSpore 的时候要注意版本和硬件匹配,官方提供了不同硬件对应的安装命令。
pip install mindspore==2.2.0 pip install ftfy langdetect datasketch pip install numpy pandas tqdmftfy用于编码修复,langdetect用于语种识别,datasketch提供 MinHash 和 LSH 的实现。这几个库是数据过滤流水线的常客。
4.2 构建分层过滤流水线
我把整个流水线写成一个可配置的 Python 脚本,每个环节独立成函数,方便单独调试和替换。核心结构大概是这样:
import ftfy import re from datasketch import MinHash, MinHashLSH def normalize_text(text): text = ftfy.fix_text(text) text = re.sub(r'[\x00-\x08\x0b-\x1f]', '', text) text = re.sub(r' |&|<|>', ' ', text) return text.strip() def rule_filter(text, min_len=200, max_len=100000): if len(text) < min_len or len(text) > max_len: return False lines = text.split('\n') if not lines: return False avg_line_len = len(text) / len(lines) if avg_line_len < 10 or avg_line_len > 2000: return False special_ratio = sum(1 for c in text if not c.isalnum() and not c.isspace()) / len(text) if special_ratio > 0.3: return False return True def compute_minhash(text, num_perm=128): m = MinHash(num_perm=num_perm) for token in text.split(): m.update(token.encode('utf-8')) return m这段代码里,normalize_text做格式规范化,rule_filter做规则过滤,compute_minhash生成去重签名。实际使用时,我会把规则过滤的阈值做成配置文件,方便针对不同数据源调整。
4.3 模型打分的 MindSpore 实现
模型打分环节,我用 MindSpore 训练了一个轻量分类器。训练脚本的核心部分:
import mindspore as ms import mindspore.nn as nn import mindspore.ops as ops from mindspore import Tensor class QualityClassifier(nn.Cell): def __init__(self, backbone, num_classes=2): super().__init__() self.backbone = backbone self.classifier = nn.Dense(backbone.config.hidden_size, num_classes) def construct(self, input_ids, attention_mask): outputs = self.backbone(input_ids, attention_mask) pooled = outputs[1] logits = self.classifier(pooled) return logits loss_fn = nn.CrossEntropyLoss() optimizer = nn.Adam(classifier.trainable_params(), learning_rate=2e-5)训练数据我准备了大概 50 万条,正负样本各半。正样本来自维基百科、优质书籍和人工筛选的问答,负样本来自规则过滤掉的垃圾。训练 3 个 epoch 后,验证集准确率能到 92% 左右,这个精度对于数据过滤来说已经够用了。
推理阶段,把模型导出成 MindIR:
ms.export(classifier, input_ids, attention_mask, file_name="quality_cls", file_format="MINDIR")然后批量推理时用ms.load加载,配合batch_size=256跑,单卡吞吐能到每秒几千条,处理 TB 级语料也就几个小时的事。
4.4 去重环节的完整实现
去重我用 MinHash LSH,先建索引再查询。关键代码如下:
from datasketch import MinHashLSH lsh = MinHashLSH(threshold=0.8, num_perm=128) def dedup_documents(docs): unique_docs = [] for idx, doc in enumerate(docs): m = compute_minhash(doc) result = lsh.query(m) if not result: lsh.insert(str(idx), m) unique_docs.append(doc) return unique_docsthreshold=0.8表示相似度超过 0.8 就判定为重复。这个值我调过几次,0.7 太松会误杀,0.9 太严会漏掉近似重复,0.8 是比较稳的中间值。
段落级去重稍微复杂一点,需要先把文档切成段落,对每个段落做 MinHash,然后维护一个全局的段落索引。切段落的时候我一般按换行符切,同时过滤掉长度小于 50 字符的短段落,避免碎片化。
4.5 数据配比与采样策略
过滤完之后还有一步容易被忽略:数据配比。不同来源的语料质量不同,不能简单混合。我一般按来源分层,给每层设定采样权重。比如:
- 书籍和论文:权重 0.3
- 百科和知识库:权重 0.25
- 优质网页:权重 0.25
- 问答和对话:权重 0.2
这个配比不是固定的,要根据下游任务调整。如果模型主要做知识问答,百科和书籍的权重可以调高;如果做对话,问答数据的权重就要上去。我通常会在小规模实验里试几组配比,看评测指标再定。
5. 常见问题与排查技巧实录
5.1 过滤后数据量骤降怎么办
这是最常见的问题。跑完流水线发现保留率只有 10%,第一反应是阈值太严。排查思路是逐层统计保留率,看是哪一层砍得最狠。
我遇到过一次,规则过滤后还剩 70%,模型打分后直接掉到 15%。后来发现是分类器的训练数据分布和实际语料不匹配,正样本太偏向书面语,导致口语化但质量不差的语料被误判。解决办法是补充多样化的正样本重新训练,或者把打分阈值从 0.5 降到 0.3。
提示:每一层过滤都要记录输入输出数量,形成漏斗图。没有这个统计,排查问题就是盲人摸象。
5.2 去重后仍有大量重复内容
如果发现去重后还有重复,通常是两个原因:一是 MinHash 的 num_perm 太小,签名区分度不够;二是去重只做了文档级没做段落级。
我建议 num_perm 至少 128,数据量大就上 256。段落级去重一定要做,尤其是网页爬取的数据,导航栏、页脚、版权声明这些内容会在每个页面重复出现,文档级去重根本抓不到。
5.3 模型打分推理速度慢
推理慢一般是 batch_size 太小或者没开图模式。MindSpore 默认是动态图,推理时切换到静态图模式能快不少:
ms.set_context(mode=ms.GRAPH_MODE, device_target="Ascend")另外,把模型导出成 MindIR 再推理,比直接加载 checkpoint 快 20% 到 30%。如果还是慢,考虑用多卡并行推理,或者把打分环节放到离线集群上跑。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决建议 |
|---|---|---|---|
| 保留率过低 | 阈值过严/分类器偏差 | 逐层统计保留率 | 放宽阈值/重训分类器 |
| 去重不彻底 | num_perm 太小/缺段落级去重 | 检查签名维度 | 提高 num_perm/加段落去重 |
| 推理速度慢 | 动态图模式/批次小 | 检查 context 配置 | 切图模式/导出 MindIR |
| 训练 loss 异常 | 数据配比失衡 | 统计各来源占比 | 调整采样权重 |
| 下游效果差 | 过滤误杀高质量数据 | 人工抽检被过滤样本 | 修正规则/补充正样本 |
5.5 几个踩坑经验
第一个坑是语种识别不能省。我早期做多语种预训练,没做语种过滤,结果英文语料里混进了大量其他语种,模型在英文任务上表现明显下降。后来加了langdetect做语种分流,效果立竿见影。
第二个坑是去重的顺序。我一开始先做模型打分再做去重,结果发现打分模型在重复数据上浪费了大量算力。正确顺序应该是先去重再打分,这样打分环节处理的都是唯一内容。
第三个坑是阈值不能一劳永逸。数据源变了、采集时间变了,合适的阈值也会变。我现在的做法是每次换数据源都重新跑一遍分布统计,动态调整阈值,而不是沿用旧配置。
第四个坑是别忘了保留原始数据。过滤是有损操作,万一发现过滤错了想回滚,没有原始数据就麻烦了。我一般会把原始语料和过滤后的语料分开存储,过滤脚本也做版本管理。
6. 关于数据质量过滤的一些个人体会
做预训练数据过滤这几年,我最大的感受是:这件事没有银弹,只有不断迭代。规则会过时,模型会漂移,数据分布会变化,唯一不变的是"持续监控、持续调整"这个动作本身。
我现在习惯在流水线里加一个抽样人工审核环节,每次过滤完随机抽几百条,人工看看被保留的和被过滤的样本质量。这个动作花不了多少时间,但能发现很多自动化指标看不出来的问题。比如有一次我发现被过滤的样本里有一批质量其实不错的技术文档,原因是特殊符号占比超标,后来专门为技术类语料放宽了这个阈值。
另外,MindSpore 生态在数据处理这块还在快速演进,mindspore.dataset的能力每个版本都在增强。我建议你保持对官方文档的关注,尤其是 dataset 相关的算子更新,有时候一个新算子就能省掉你自己写的一大段代码。
最后分享一个小技巧:如果你的过滤流水线要跑很多次,建议把每个环节的中间结果都落盘缓存。这样调整某一层参数时,不需要从头重跑,直接从缓存加载上一层的输出就行。我靠这个习惯把单次实验的迭代时间从几小时压缩到了几十分钟,效率提升非常明显。