大模型预训练数据质量过滤实战:基于MindSpore的流水线设计与调参
发布时间:2026/10/2 14:37:36来源:尧图网络
1. 为什么数据质量过滤是大模型预训练的第一道生死线做过大模型预训练的人都有一个共识模型效果的上限在数据准备阶段就已经被决定了。你后面用多少张卡、跑多少天、调多少超参都只是在逼近这个上限而已。而数据质量过滤就是决定这个上限的核心环节。我见过太多团队在预训练上翻车的案例复盘下来八成不是模型结构的问题也不是分布式训练的问题而是数据里混进了大量低质内容——重复文本、乱码、机器翻译腔、广告软文、模板化生成的垃圾。这些东西喂进去模型轻则学出一堆废话重则直接崩掉loss曲线看着正常但生成结果一塌糊涂。MindSpore 作为国产深度学习框架在大模型预训练这条链路上已经比较成熟了但数据质量过滤这块官方文档给的多是接口说明真正落地时怎么设计过滤流水线、每一层过滤的阈值怎么定、过滤完怎么验证效果这些实战细节很少有人系统讲。这篇内容我就围绕 MindSpore 环境下的大模型预训练数据质量过滤方案把整套思路和可复现的操作拆开讲。先明确一下这套方案解决什么问题它要在一批原始语料可能是几百GB到几TB的混合文本进入训练之前通过一系列可配置、可扩展的过滤规则把低质量样本剔除掉同时保留高质量样本最终产出一份可以直接喂给 MindSpore 数据管道的高质量数据集。适合谁看正在或准备用 MindSpore 做大模型预训练的工程师、数据工程师以及想理解数据过滤底层逻辑的算法同学。哪怕你用的是别的框架这套过滤思路也是通用的只是实现载体不同。在展开之前先给一个整体认知数据质量过滤不是单一操作而是一条流水线。它通常包含格式规范化、去重、质量打分、敏感与噪声过滤、长度与语言筛选这几个阶段每个阶段都有明确的输入输出和判定标准。下面我按实际落地顺序一层层拆。2. 过滤流水线的整体架构与各阶段职责划分2.1 从原始语料到训练样本的五个阶段一条完整的数据质量过滤流水线我一般拆成五个阶段顺序不能乱因为后面的阶段依赖前面阶段的输出阶段名称核心职责典型输出1格式规范化统一编码、清理HTML标签、修复乱码干净纯文本2精确去重去除完全重复的文档无重复文本3模糊去重去除高度相似的文档低冗余文本4质量打分过滤用规则或模型给文本打分高质量文本5语言与长度筛选按语言、长度做最终裁剪训练就绪数据这个顺序背后的逻辑是先做便宜的操作再做昂贵的操作。格式规范化和精确去重计算成本极低可以快速砍掉大量垃圾模糊去重和质量打分计算成本高放在后面处理已经瘦身过的数据整体效率最高。如果你反过来先做质量打分那等于对着一堆重复垃圾反复算分纯属浪费算力。2.2 为什么不能只做单一过滤很多人图省事只做一步去重或者只用一个质量分类器就完事。实测下来这种做法问题很大。举个我踩过的坑早期我只用了一个基于困惑度perplexity的质量过滤器结果发现它会把一些专业领域的文本比如法律条文、医学文献误判为低质量因为这些文本的困惑度天然偏高。后来我加了一层基于规则的过滤把是否包含完整句子结构标点符号比例是否正常这些维度补上误杀率才降下来。单一过滤器一定有盲区多层过滤才能互相补位。规则过滤擅长抓明显的垃圾乱码、广告、模板文本模型过滤擅长抓语义层面的低质逻辑混乱、语义不连贯两者结合才稳。2.3 MindSpore 数据管道如何承接过滤结果过滤完的数据最终要喂给 MindSpore 的训练管道。这里有个关键点过滤阶段产出的数据格式要和你训练时用的 Dataset 接口对齐。MindSpore 常用的文本数据加载方式是mindspore.dataset.TextFileDataset或者自定义GeneratorDataset。我一般把过滤后的数据存成每行一个 JSON 的格式jsonl每个样本包含text字段和可选的meta字段记录来源、质量分等。这样在训练时可以用GeneratorDataset流式读取配合batch、map等操作做 tokenization。import mindspore.dataset as ds import json def read_jsonl(path): with open(path, r, encodingutf-8) as f: for line in f: yield json.loads(line)[text] dataset ds.GeneratorDataset( sourceread_jsonl, column_names[text], python_multiprocessingTrue, num_parallel_workers8, shuffleTrue )提示过滤阶段和训练阶段最好解耦。过滤是一次性的离线任务产出静态数据集训练是反复迭代的在线任务。不要把过滤逻辑塞进训练管道里实时跑那样每次训练都要重算一遍得不偿失。3. 格式规范化与精确去重最容易被低估的两步3.1 编码统一与乱码修复的实操细节格式规范化看着简单但坑特别多。最常见的问题是编码不统一有的文件是 UTF-8有的是 GBK还有的是带 BOM 的 UTF-8。如果不统一后面读取时直接报错或者读出乱码。我的处理方式是先做编码探测再统一转成 UTF-8。可以用chardet库做探测但要注意它对短文本的探测准确率不高所以对短文本我一般直接尝试用 UTF-8 解码失败再回退到其他编码。import chardet def normalize_encoding(raw_bytes): # 先尝试 UTF-8 try: return raw_bytes.decode(utf-8) except UnicodeDecodeError: pass # 探测编码 detected chardet.detect(raw_bytes) encoding detected.get(encoding) or utf-8 try: return raw_bytes.decode(encoding, errorsignore) except Exception: return None乱码修复这块除了编码问题还有一类是字符替换错误比如这种替换字符。我的做法是统计替换字符占比如果超过阈值比如 1%就直接丢弃整条样本因为这种文本往往已经损坏严重修复成本高于收益。HTML 标签清理也是重头戏。原始语料里经常混着网页抓取残留的div、script标签。用正则粗暴删除容易误伤正文里的数学符号比如和所以我一般用BeautifulSoup或者lxml做结构化解析只提取文本节点。3.2 精确去重为什么必须用哈希而不是字符串比对精确去重的目标是找出完全相同的文档。最朴素的做法是两两比对字符串但数据量一大比如千万级文档这个复杂度是 O(n²)根本跑不动。正确做法是对每条文档算哈希值用哈希值做去重。哈希函数我推荐用 SHA-256 或者更快的 xxHash。SHA-256 更安全但慢一些xxHash 快很多且碰撞率极低对去重场景完全够用。import xxhash def doc_hash(text): return xxhash.xxh64(text.encode(utf-8)).hexdigest() seen set() unique_docs [] for doc in docs: h doc_hash(doc) if h not in seen: seen.add(h) unique_docs.append(doc)这里有个细节哈希前要不要做归一化比如把连续空格压成一个、统一大小写。我的经验是对于精确去重做轻量归一化压缩空白字符能多去掉一批伪不同的重复文档但不要做过度归一化比如去掉所有标点否则会把语义不同的文档误判为重复。注意精确去重只能去掉完全一样的文档。实际语料里更多的是改了几个字的近似重复这就需要下一节的模糊去重来处理。3.3 精确去重的内存优化技巧千万级文档的哈希集合如果直接放内存可能占用几个 GB。如果内存吃紧可以用布隆过滤器Bloom Filter来降内存代价是有极小的误判率把不重复的判成重复。对于预训练数据这点误判可以接受。另一个技巧是分片处理把数据按哈希前缀分成多个桶每个桶单独去重最后合并。这样内存峰值能降下来适合单机内存有限的场景。4. 模糊去重MinHash 与 SimHash 的选型与调参4.1 为什么模糊去重是数据质量的关键战场精确去重只能解决完全一样的问题但真实语料里大量存在的是近似重复同一篇新闻被不同网站转载时改了标题、同一段代码被复制时改了注释、同一份报告被反复引用时截取了不同片段。这些内容如果不去掉模型会在这些冗余信息上反复学习导致训练效率下降还可能加剧模型的记忆和偏见。模糊去重的核心是用近似算法快速找出相似文档而不是精确计算两两相似度。主流方案有两种MinHash LSH以及 SimHash。两者各有适用场景下面分别讲。4.2 MinHash LSH 的原理与 MindSpore 环境下的实现MinHash 的思路是把文档表示成 n-gram 集合然后用多个哈希函数对集合做最小哈希得到一组签名。两个文档的 MinHash 签名相似度近似等于它们 n-gram 集合的 Jaccard 相似度。LSH局部敏感哈希则用来加速查找把签名分band只有落在同一个band里的文档才需要精确比对相似度避免了两两比对。from datasketch import MinHash, MinHashLSH def build_minhash(text, num_perm128): m MinHash(num_permnum_perm) # 用字符级 5-gram for i in range(len(text) - 4): m.update(text[i:i5].encode(utf-8)) return m lsh MinHashLSH(threshold0.8, num_perm128) for idx, doc in enumerate(docs): m build_minhash(doc) lsh.insert(idx, m) # 查询重复 duplicates set() for idx, doc in enumerate(docs): m build_minhash(doc) result lsh.query(m) for r in result: if r ! idx: duplicates.add(max(idx, r))关键参数是threshold相似度阈值和num_perm哈希函数个数。阈值我一般设在 0.7 到 0.85 之间太低会误杀正常内容太高会漏掉近似重复。num_perm越大越准但越慢128 是个比较平衡的值。4.3 SimHash 的适用场景与对比SimHash 的思路不同它把文档映射成一个固定长度的指纹比如 64 位两个文档的相似度用指纹的汉明距离衡量。汉明距离小于某个阈值比如 3就认为是近似重复。SimHash 的优势是指纹短、存储省、比对快适合超大规模数据的粗筛。劣势是它对短文本不敏感而且阈值不好定容易误判。我的选型经验是场景推荐方案理由文档较长500字MinHash LSH精度高阈值直观文档较短或需极速粗筛SimHash速度快存储省对精度要求极高MinHash 精确验证先粗筛再精算实际落地时我经常两个都用先用 SimHash 做一轮快速粗筛再用 MinHash 对剩下的做精细去重。这样兼顾了速度和精度。4.4 模糊去重的阈值调参经验阈值这个东西没有万能值必须结合你的数据特点调。我的做法是先人工标注一小批样本比如 500 对文档标出哪些是真重复、哪些不是然后画相似度分布图找最佳分割点。另外要注意不同来源的数据相似度分布差异很大。比如新闻类数据重复率高阈值可以设低一点0.7学术论文重复率低阈值可以设高一点0.85。按数据来源分桶分别设阈值效果比全局统一阈值好很多。5. 质量打分过滤规则与模型的双轨方案5.1 规则过滤能抓住哪些模型抓不住的问题规则过滤是最容易被忽视但性价比最高的一层。它不需要训练模型纯靠统计特征和模式匹配速度快、可解释性强。我常用的规则维度包括平均句长过短5字或过长200字的句子占比异常往往是低质文本标点比例标点占比过高30%可能是符号堆砌过低1%可能是无标点长串重复字符比例连续重复字符如哈哈哈哈占比过高特殊符号比例非中英文、非标点的特殊符号占比数字与字母比例纯数字或纯字母占比过高停用词比例正常文本停用词占比在合理区间异常低可能是关键词堆砌import re def rule_score(text): if len(text) 50: return 0.0 # 标点比例 punct len(re.findall(r[。、,.!?;:], text)) punct_ratio punct / len(text) # 重复字符 repeat len(re.findall(r(.)\1{3,}, text)) # 特殊符号 special len(re.findall(r[^\u4e00-\u9fa5a-zA-Z0-9。、,.!?;:\s], text)) special_ratio special / len(text) score 1.0 if punct_ratio 0.3 or punct_ratio 0.005: score - 0.3 if repeat 5: score - 0.3 if special_ratio 0.1: score - 0.4 return max(score, 0.0)这套规则跑下来能砍掉相当一部分明显的垃圾文本而且几乎不误伤正常内容。5.2 基于困惑度的质量打分及其局限困惑度perplexity是衡量文本流畅度的经典指标。用一个在高质量语料上训练的小语言模型对每条文本算困惑度困惑度越低说明文本越像人话。但困惑度有个致命问题它对领域敏感。一个在新闻语料上训练的模型去算医学文献的困惑度结果会偏高导致误杀。所以用困惑度做过滤必须用和目标领域匹配的参考模型或者做领域归一化。我的做法是对每个数据来源单独算困惑度分布取分位数比如去掉最低的 10% 和最高的 5%作为过滤区间。这样比全局统一阈值稳得多。5.3 用轻量分类器做质量判别规则和困惑度都有局限更稳的方案是训练一个轻量质量分类器。思路是人工标注一批高质量和低质量样本用一个小模型比如 TextCNN 或者小 BERT做二分类然后对全量数据打分。在 MindSpore 里训练这个小分类器很方便用nn.Embeddingnn.Conv1d搭个 TextCNN 就行训练数据量不用很大几万条标注样本足够推理速度也快。import mindspore.nn as nn import mindspore.ops as ops class QualityClassifier(nn.Cell): def __init__(self, vocab_size, embed_dim128, num_filters128): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim) self.conv nn.Conv1d(embed_dim, num_filters, kernel_size3, pad_modevalid) self.relu nn.ReLU() self.pool ops.ReduceMax(keep_dimsFalse) self.fc nn.Dense(num_filters, 2) def construct(self, x): x self.embedding(x) # (B, L, E) x x.transpose(0, 2, 1) # (B, E, L) x self.relu(self.conv(x)) # (B, F, L) x self.pool(x, 2) # (B, F) return self.fc(x)分类器的输出概率就是质量分设个阈值比如 0.5做过滤。分类器的优势是能学到规则和困惑度抓不到的语义特征比如逻辑连贯性、信息密度。5.4 双轨方案如何融合打分规则分和模型分怎么融合我一般用加权求和final_score w1 * rule_score w2 * model_score权重根据数据特点调。如果数据里明显垃圾多规则权重高一点w10.6如果数据整体质量还行主要靠模型区分模型权重高一点w20.7。融合后再设一个最终阈值低于阈值的丢弃。阈值同样建议按数据来源分桶设定不要全局一刀切。6. 语言识别、长度裁剪与敏感内容处理6.1 语言识别在混合语料中的必要性如果你的预训练语料是多语言的语言识别就是必须的一步。原因很简单不同语言的 tokenization 策略不同混在一起会互相干扰。而且如果你只想训练中文模型混进大量英文会稀释中文的学习信号。语言识别我一般用fasttext的 lid 模型速度快、准确率高。对每条文本预测语言标签然后按目标语言筛选。import fasttext model fasttext.load_model(lid.176.bin) labels, probs model.predict(text, k1) lang labels[0].replace(__label__, ) if lang ! zh or probs[0] 0.8: # 丢弃或标记 pass注意那个probs[0] 0.8的判断置信度低的样本宁可丢弃因为语言混杂的文本对训练没好处。6.2 长度筛选的上下限怎么定长度筛选看着简单其实有讲究。太短的文本比如少于 50 字信息量不足学不到东西太长的文本比如超过 10000 字可能超出模型上下文窗口而且往往是拼接的垃圾。我的经验值下限中文 50 字英文 100 词上限根据模型上下文长度定一般不超过上下文长度的 2 倍超出部分截断或丢弃但这不是绝对的。代码数据、数学公式数据的长度分布和自然语言完全不同要分开设阈值。我一般按数据类型分桶每桶单独统计长度分布取 5% 和 95% 分位数作为上下限。6.3 敏感与噪声内容的处理原则敏感内容处理这块原则是宁可保守不可激进。具体做法是维护一个敏感词表对命中敏感词的文本做标记然后根据策略决定是丢弃还是脱敏。噪声内容则包括纯导航文本首页 关于我们 联系方式、版权声明、页脚模板等。这些可以用模板匹配的方式识别命中就丢弃。提示敏感词表要定期更新而且要区分硬敏感直接丢弃和软敏感标记后人工复核。不要把所有命中都一刀切丢弃否则容易误伤正常内容。7. 过滤效果验证怎么知道你的过滤方案真的有效7.1 用统计指标做过滤前后的对比过滤完不能直接开训得先验证效果。最基础的验证是过滤前后的统计对比指标过滤前过滤后期望变化文档总数1000万600万下降 30%-50%平均长度320字480字上升重复率25%2%大幅下降特殊符号占比8%1%下降语言纯度70%95%上升如果过滤后平均长度反而下降或者重复率没降下来说明过滤方案有问题得回去查。7.2 人工抽检的正确姿势统计指标只能看整体细节还得靠人工抽检。我的做法是从过滤后的数据里随机抽 200 条人工判断质量统计合格率。合格率低于 90% 就说明过滤不够高于 98% 可能过滤过度把好的也杀了。抽检时要注意分层按数据来源分层抽样不要只从一个来源抽否则结论有偏。7.3 小规模预训练验证法最靠谱的验证是跑一个小规模预训练实验。用过滤后的数据和过滤前的数据各跑一个相同配置的小模型比如 1亿参数对比下游任务效果。如果过滤后的数据训练出的模型效果明显更好说明过滤有效。这个验证成本不低但对于正式的大规模预训练这一步绝对不能省。我见过太多团队跳过验证直接上大规模训练结果跑了几十万卡时才发现数据有问题损失巨大。8. 工程落地中的性能优化与常见坑8.1 用多进程和流式处理扛住大数据量数据过滤是典型的 CPU 密集型任务单进程跑几 TB 数据能跑到天荒地老。必须上多进程。Python 的multiprocessing配合分块读取是标配。我一般把数据切成固定大小的块比如每块 10 万条每个进程处理一块最后合并结果。from multiprocessing import Pool def process_chunk(chunk): return [doc for doc in chunk if pass_filter(doc)] with Pool(processes16) as pool: results pool.map(process_chunk, chunks)注意多进程下要避免共享大对象否则内存会爆。每个进程独立处理自己的块最后合并是最稳的模式。8.2 过滤顺序对整体耗时的影响前面提过过滤顺序要先便宜后昂贵这里再强调一下量化影响。我实测过一个 500GB 的语料先精确去重再质量打分总耗时 8 小时先质量打分再精确去重总耗时 22 小时差了将近 3 倍。原因就是质量打分对重复数据做了大量无用计算。顺序错了算力白烧。8.3 几个我踩过的真实坑坑一去重时用了归一化后的文本做哈希但保留了原始文本。结果去重后同一篇文档的多个变体还在因为哈希是基于归一化文本算的但输出的是原始文本。正确做法是去重和输出用同一份文本。坑二质量分类器的训练数据和实际数据分布不一致。分类器在新闻数据上训的拿去过滤代码数据把好代码全杀了。分类器必须用目标领域的数据训练。坑三过滤阈值定得太死没有留缓冲。比如长度下限设 50 字结果把 49 字的优质短文本也杀了。建议阈值设完后人工看看边界样本确认没有误杀。坑四忘了处理空文档和纯空白文档。这些文档在后续 tokenization 时会报错必须在过滤阶段就清掉。8.4 过滤方案的可复现性保障最后说一个容易被忽视的点过滤方案必须可复现。这意味着你要记录用了哪些过滤规则、每个规则的参数、随机种子、数据版本。否则过几个月你想复现某个数据集根本对不上。我的做法是把过滤配置写成一个 YAML 文件和数据一起版本管理。每次过滤产出都记录配置哈希确保任何一份数据都能追溯到它的生成配置。pipeline: - name: normalize encoding: utf-8 - name: exact_dedup hash: xxh64 - name: fuzzy_dedup method: minhash threshold: 0.8 num_perm: 128 - name: quality_filter rule_weight: 0.4 model_weight: 0.6 threshold: 0.5 - name: length_filter min_len: 50 max_len: 8000这套配置配合数据版本号基本能保证任何一次过滤都可复现、可审计。数据质量过滤这件事说到底是个脏活累活但它对预训练效果的影响是决定性的。我在实际项目里的体会是在数据上多花一天比在模型上调一周更值。上面这套方案不是理论推演是我在多个预训练项目里反复打磨出来的你可以直接拿去改改参数就用。唯一要提醒的是阈值和权重这些一定要结合你自己的数据调别照搬因为数据分布这东西没有两批是一样的。
网站建设高端定制企业官网