ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MindSpore大模型预训练数据质量过滤实战:分层流水线设计与去重优化

MindSpore大模型预训练数据质量过滤实战:分层流水线设计与去重优化 1. 大模型预训练里数据质量过滤为什么是生死线做过大模型预训练的人都有一个共识模型效果的上限很大程度上在数据准备阶段就已经被决定了。你后面用多少张卡、调多细的学习率、换多花哨的并行策略都很难把一个被脏数据喂坏的模型救回来。我在实际项目里见过太多这样的情况——团队花了两周时间清洗出来 500GB 语料训练 loss 曲线看着挺漂亮结果一推理就露馅重复输出、答非所问、时不时蹦出乱码和广告。回头一查问题全出在数据质量过滤这一环没做扎实。这篇内容就是围绕MindSpore 大模型预训练中的数据质量过滤方案展开的。我会把整套过滤流程拆成可落地的模块讲清楚每一步为什么这么做、参数怎么定、坑在哪里。适合正在用 MindSpore 做预训练或者微调的工程师也适合刚接触大模型数据工程、想搞清楚“数据到底该怎么洗”的读者。哪怕你之前只用过 PyTorch 那一套这里面的思路也是通用的只是工具链换成了 MindSpore 生态。先说清楚一个基本判断数据质量过滤不是一道工序而是一条流水线。它至少包含四个层次——文档级过滤、段落级过滤、句子级过滤、以及 token 级的去重与规范化。很多人只做了第一层觉得把 HTML 标签去掉、把乱码删掉就算完事结果模型还是学不到东西。真正拉开差距的是后面几层的精细度。我个人的经验是一个 100GB 的原始语料经过完整过滤后能剩下 30GB 到 50GB 的“干净可用数据”就已经很不错了。如果你过滤完还剩 90GB那大概率是过滤强度不够脏数据还在里面。这个比例可以作为你自查的一个参考线。下面我按“整体设计思路 → 核心细节解析 → 实操流程 → 问题排查”的顺序把整套方案讲透。2. 整体方案设计与思路拆解2.1 为什么过滤要分层而不是一把梭刚上手的人容易犯一个错误写一个巨大的正则表达式想把所有脏东西一次性干掉。我早期也这么干过结果就是规则越加越多互相冲突最后连正常文本都被误杀。后来我改成分层过滤的思路每一层只负责一类问题层与层之间解耦这样调试起来清晰出了问题也能快速定位是哪一层的锅。具体分层是这样的第一层格式规范化。把各种来源的原始数据统一成纯文本去掉 HTML、Markdown 残留、控制字符、异常空白。第二层文档级质量过滤。判断整篇文档值不值得留比如太短的、符号占比过高的、语言不对的直接整篇丢弃。第三层段落级与句子级过滤。文档留下了但里面可能混着广告、导航栏、版权声明这些要在段落和句子粒度上剔除。第四层去重与近似去重。重复数据是预训练的大敌会让模型对某些内容过拟合必须做精确去重加近似去重。第五层敏感与低质内容兜底。最后再过一遍黑名单和启发式规则确保没有明显不该出现的内容。这个分层的好处是每一层的输出都可以单独采样检查你能清楚知道每一层砍掉了多少数据、砍得对不对。我习惯在每一层后面加一个统计日志记录输入条数、输出条数、丢弃原因分布这个日志在后期调参时非常有用。2.2 工具选型为什么用 MindSpore 生态而不是纯 Python 脚本有人会问数据过滤用 pandas 加正则不就行了为什么要扯上 MindSpore这里要区分两个阶段。纯 CPU 的文本清洗确实用 Python 脚本就够了但到了去重和向量化相似度计算这一步数据量一大纯 Python 就扛不住了。这时候用 MindSpore 的算子来做批量计算能利用上 GPU 或者 Ascend 的算力速度差距是数量级的。具体来说我在这套方案里用到的 MindSpore 相关能力包括用mindspore.dataset做数据管道把清洗和过滤串成流水线支持多进程并行。用 MindSpore 的 Tensor 运算做 MinHash 和 SimHash 的批量计算加速近似去重。用mindspore.nn里现成的 embedding 能力做语义层面的相似度筛查这一步可选数据量特别大时才值得上。如果你只是做小规模实验纯 Python 也能跑通但一旦语料上了几十 GB我强烈建议把重计算的部分迁到 MindSpore 上。这也是为什么标题里强调 MindSpore 的原因——它不是噱头而是真的能解决性能瓶颈。2.3 过滤强度的权衡宁可错杀还是宁可放过这是整个方案里最需要经验判断的地方。过滤太松脏数据进模型过滤太狠数据量不够模型欠拟合。我的原则是在预训练阶段宁可稍微严一点。因为预训练数据量本来就大砍掉一部分不影响整体规模但脏数据的负面影响是会被放大的。不过“严”也要有度。我见过有人把包含数字的句子全删了理由是“数字可能是乱码”这就过头了。合理的做法是给每一类过滤规则设一个阈值然后在小批量数据上人工抽样验证确认误杀率在可接受范围内再全量跑。3. 核心细节解析与实操要点3.1 格式规范化把五花八门的输入拉齐原始语料的来源通常很杂有网页抓取的、有 PDF 转的、有日志导出的。格式规范化的目标就是把这些统一成干净的纯文本。这一步看着简单但细节特别多。我常用的处理顺序是这样的编码统一。全部转成 UTF-8遇到无法解码的字节用errorsignore跳过同时记录跳过的比例。如果某个文件跳过比例超过 5%直接整篇丢弃说明这个文件本身编码就有问题。HTML 标签剥离。用lxml或者BeautifulSoup解析只取正文文本。注意不要用正则去删标签正则处理嵌套标签会出错。控制字符清理。把\x00到\x1f这些不可见字符删掉但保留\n和\t。空白规范化。连续多个空格压成一个连续多个换行压成两个。全半角统一。把全角字母数字转成半角避免同一个词因为全半角不同被当成两个词。这里有个容易忽略的点换行符的处理。有些语料用\r\n有些用\n还有的用\r。如果不统一后面按行切分的时候会出乱子。我一般统一转成\n。import re import unicodedata def normalize_text(text): # 统一换行符 text text.replace(\r\n, \n).replace(\r, \n) # 去除控制字符保留换行和制表 text .join(ch for ch in text if ch \n or ch \t or unicodedata.category(ch)[0] ! C) # 全角转半角 text unicodedata.normalize(NFKC, text) # 压缩空白 text re.sub(r[ \t], , text) text re.sub(r\n{3,}, \n\n, text) return text.strip()这段代码我用了很久稳定可靠。NFKC规范化会顺带处理掉很多兼容字符比如全角括号、特殊空格一举两得。3.2 文档级过滤哪些整篇文档该直接扔文档级过滤是最粗粒度的一层判断标准也比较直接。我通常用下面几个指标指标阈值建议说明文档长度少于 50 字符丢弃太短没有语义价值平均行长度少于 10 字符丢弃说明大量短行可能是列表或导航符号占比超过 40% 丢弃符号太多说明不是正常文本字母数字占比低于 50% 丢弃正常文本字母数字应占多数重复行占比超过 30% 丢弃大量重复行说明是模板页语言置信度低于 0.8 丢弃用语言检测模型判断这些阈值不是拍脑袋定的是我在多个语料集上抽样统计后总结出来的。比如符号占比这个我统计过正常新闻、技术文档、小说符号占比普遍在 15% 到 25% 之间超过 40% 的基本都是代码片段或者乱码。语言检测我用的是fasttext的 lid 模型速度快、准确率够用。注意一点中英混合的文档不要直接丢很多技术文档就是中英混排的。我的做法是判断主语言只要主语言置信度够高就保留。3.3 段落级与句子级过滤把混在正文里的垃圾挑出来文档整体合格不代表每一段都合格。网页抓取的数据里正文周围经常裹着导航栏、广告、评论区、版权声明。这些内容如果不清掉模型会学到一堆“点击这里”“版权所有”之类的废话。段落级过滤我主要看这几个信号段落长度少于 20 字符的段落大概率是标题碎片或者按钮文字丢弃。重复模式如果某个段落在整个语料里高频出现说明是模板内容丢弃。关键词命中维护一个黑名单词表比如“点击”“关注”“扫码”“版权所有”“免责声明”段落命中就丢弃。标点密度正常段落标点密度在合理范围如果一段话几乎没有标点可能是关键词堆砌。句子级过滤更细主要处理段落内部的噪声。比如一句话里包含大量 URL、邮箱、电话号码或者一句话里特殊符号占比过高就把这句话删掉保留段落里其他正常的句子。这里我要强调一个经验句子级过滤要谨慎。因为删句子会破坏段落的连贯性如果删得太多剩下的段落读起来支离破碎反而影响模型学习。我的做法是如果一段话里超过一半的句子被判定为噪声就整段丢弃而不是只删那几句。3.4 去重精确去重和近似去重两手都要抓重复数据是预训练里最隐蔽的杀手。它不会让 loss 曲线变难看但会让模型对重复内容过度记忆泛化能力下降。去重分两种精确去重就是完全一样的文档只留一份。用哈希就行把文档内容算个 MD5放进集合里重复的直接跳过。这一步简单但能砍掉不少数据尤其是从多个来源抓的语料交叉重复很常见。近似去重才是难点。两篇文档可能只差几个字或者段落顺序调换了精确哈希抓不出来。这时候要用 MinHash 或者 SimHash。我一般用 MinHash因为它对集合相似度Jaccard的估计比较准。MinHash 的核心思路是把文档切成 n-gram 集合用多个哈希函数对集合做最小哈希得到签名向量然后比较两个签名的相似度。签名长度一般取 128 或 256越长越准但越慢。import hashlib import numpy as np def minhash_signature(text, num_hashes128, ngram5): tokens [text[i:ingram] for i in range(len(text) - ngram 1)] if not tokens: return None signature np.full(num_hashes, np.inf) for token in set(tokens): for i in range(num_hashes): h int(hashlib.md5(f{i}_{token}.encode()).hexdigest(), 16) if h signature[i]: signature[i] h return signature def jaccard_estimate(sig1, sig2): return np.mean(sig1 sig2)这段代码是纯 Python 版跑小数据没问题。数据量大了之后我把内层循环改成 MindSpore 的 Tensor 运算把哈希值批量算出来速度能提升十几倍。具体做法是把 token 列表转成 Tensor用 MindSpore 的ops做向量化哈希这里就不展开代码了思路是一样的。去重的阈值我一般设在 0.8也就是相似度超过 0.8 就认为是重复。这个值可以调语料质量差就调低一点质量好就调高一点。3.5 敏感与低质内容兜底最后一层是兜底。维护一个黑名单词表包含明显不该出现在训练数据里的内容。这一层不需要太复杂就是简单的字符串匹配命中就丢弃。词表要定期更新根据实际语料里发现的问题往里加。另外我还会做一个“低质内容”的启发式判断比如整篇文档全是问句没有陈述句。整篇文档全是感叹号结尾。文档里包含大量重复的同一句话。这些模式往往对应着垃圾内容直接丢弃。4. 实操过程与核心环节实现4.1 用 MindSpore Dataset 搭建过滤流水线前面讲的都是单个模块现在把它们串起来。MindSpore 的mindspore.dataset提供了很好的流水线能力可以把多个处理步骤串成一条链支持并行和缓存。整体流程是这样的import mindspore.dataset as ds from mindspore.dataset import text def build_pipeline(data_files, batch_size1000): # 读取原始文本文件 dataset ds.TextFileDataset(data_files, shuffleFalse) # 格式规范化 dataset dataset.map(operationsnormalize_text, input_columns[text]) # 文档级过滤 dataset dataset.filter(predicatedoc_level_filter, input_columns[text]) # 段落级过滤 dataset dataset.map(operationsparagraph_filter, input_columns[text]) # 句子级过滤 dataset dataset.map(operationssentence_filter, input_columns[text]) # 批处理 dataset dataset.batch(batch_size, drop_remainderFalse) return dataset这里有几个实操要点第一num_parallel_workers要设对。默认是 1太慢了。我一般设成 CPU 核数的 70% 左右比如 32 核的机器设 24。设太高反而会因为上下文切换变慢。第二filter和map的顺序有讲究。先做便宜的过滤比如长度判断再做贵的处理比如正则替换这样能减少后面步骤的数据量整体更快。第三缓存中间结果。如果某一层特别慢可以用dataset.save()把中间结果存下来下次直接读缓存省得重复计算。4.2 去重环节的工程实现去重是整条流水线里最耗时的部分我单独拿出来讲。精确去重简单用集合就行seen_hashes set() def exact_dedup(text): h hashlib.md5(text.encode()).hexdigest() if h in seen_hashes: return False seen_hashes.add(h) return True注意这个seen_hashes集合会随着数据量增长而变大100GB 语料大概需要几十 GB 内存来存哈希。如果内存不够可以用布隆过滤器代价是有极小的误判率但能省大量内存。近似去重我用的是分桶加局部敏感哈希LSH的思路。先把所有文档的 MinHash 签名算出来然后按签名分桶同一个桶里的文档两两比较。这样避免了全量两两比较的 O(n²) 复杂度。def lsh_bucketing(signatures, band_size16): buckets {} for doc_id, sig in enumerate(signatures): for i in range(0, len(sig), band_size): band tuple(sig[i:iband_size]) key hash(band) if key not in buckets: buckets[key] [] buckets[key].append(doc_id) return bucketsband_size的选择影响召回率和准确率。band 越小越容易分到同一个桶召回率高但误判多band 越大越严格。我一般用 16配合 128 长度的签名效果比较均衡。4.3 参数计算过滤阈值到底怎么定很多人问我阈值怎么定我的答案是用数据说话不要拍脑袋。具体做法是从原始语料里随机抽 1000 篇文档。人工标注哪些是好的、哪些是坏的。对每个指标画分布图看好坏样本的分界线在哪。选一个能过滤掉大部分坏样本、同时误杀好样本最少的阈值。举个例子文档长度这个指标我统计过好样本的长度分布中位数在 800 字符左右5% 分位数在 120 字符。坏样本里长度小于 50 的占了 60%。所以我定 50 作为阈值能砍掉大部分坏样本误杀的好样本不到 2%。这个流程虽然要花点时间但比拍脑袋定阈值靠谱得多。而且这个统计结果可以复用到后续项目不用每次都重新做。4.4 过滤效果验证怎么知道洗得对不对过滤跑完了怎么验证效果我一般做三件事第一看统计日志。每一层砍掉了多少、丢弃原因分布如何这些数字要合理。如果某一层砍掉了 90% 的数据那大概率是阈值设错了。第二人工抽样。从过滤后的数据里随机抽 100 条逐条读看有没有明显的脏数据残留。这一步不能省机器判断再准也有盲区。第三下游验证。用过滤后的数据训一个小模型和用未过滤数据训的模型对比。如果过滤有效小模型在验证集上的表现应该更好。这一步成本高但最有说服力。我自己的经验是前两步能发现 90% 的问题第三步是最终确认。5. 常见问题与排查技巧实录5.1 过滤后数据量骤减怎么办这是最常见的问题。本来 100GB 语料过滤完只剩 10GB肯定哪里出问题了。排查思路是逐层看日志找到砍得最狠的那一层。如果问题出在文档级过滤大概率是长度阈值或者符号占比阈值设太严了。把阈值放宽一点重新抽样验证。如果问题出在去重可能是相似度阈值设太低了。0.8 是个比较安全的起点如果设成 0.6那稍微有点像的文档都会被当成重复数据量自然暴跌。如果问题出在语言检测可能是语言模型对某些领域文本判断不准。比如技术文档里代码多语言检测可能判成“未知语言”。这种情况可以把语言检测的置信度阈值调低或者对特定领域文本跳过语言检测。5.2 过滤后还有脏数据残留反过来如果过滤完发现还有脏数据说明规则不够。这时候要做的是收集漏网的脏数据样本分析它们的共同特征然后针对性加规则。我遇到过的典型漏网情况包括用特殊字符伪装的广告比如用零宽字符隔开关键词绕过黑名单匹配。解决办法是在匹配前先把零宽字符去掉。用图片代替文字的广告文本里只剩“点击查看”。这种靠文本规则很难处理只能靠关键词黑名单兜底。编码错误导致的乱码看起来像正常文本但实际是乱码。这种要靠语言检测和字符分布统计来识别。5.3 去重速度太慢怎么优化去重慢通常是因为两两比较的复杂度太高。优化方向有三个第一用 LSH 分桶把 O(n²) 降到接近 O(n)。这是最有效的优化。第二把 MinHash 计算迁到 GPU。用 MindSpore 的 Tensor 运算批量算哈希比纯 Python 快十几倍。第三减少签名长度。128 长度的签名已经够用没必要上 256。签名短一半计算量和存储都减半。我实测过一个 50GB 的语料纯 Python 去重要跑 8 个小时迁到 MindSpore 加 LSH 之后40 分钟就跑完了。这个提升是很可观的。5.4 常见问题速查表问题现象可能原因排查方向解决办法数据量骤减阈值过严看各层丢弃日志放宽阈值重新抽样脏数据残留规则不足收集漏网样本针对性加规则去重太慢复杂度高看是否用了 LSH上 LSH 加 GPU 加速内存爆了哈希集合太大看内存占用用布隆过滤器过滤后文本不连贯句子级过滤太狠看段落完整性整段丢弃代替删句语言检测误判领域文本特殊看误判样本调低阈值或跳过5.5 几个我踩过的坑坑一正则表达式回溯爆炸。我写过一个匹配 URL 的正则在长文本上跑得特别慢后来发现是回溯导致的。解决办法是用更精确的正则或者直接用字符串查找代替正则。坑二多进程共享状态。用multiprocessing做并行过滤时去重用的集合如果放在全局多个进程之间不共享会导致去重失效。解决办法是用Manager或者把去重单独放一个进程做。坑三编码问题导致的静默失败。有些文件用errorsignore解码后内容全没了但不报错。解决办法是记录解码前后的字节数差异过大就报警。坑四阈值硬编码。一开始我把阈值写死在代码里后来换个语料就得改代码。现在我把所有阈值放到配置文件里改起来方便也方便做实验对比。6. 一些延伸思考这套方案我在几个项目里都用过效果比较稳定。但数据质量过滤这件事没有终点语料在变脏数据的形态也在变规则需要持续迭代。一个我觉得值得尝试的方向是用模型来过滤数据。比如训一个小的分类模型判断文档质量高低用它来代替部分人工规则。这样能处理一些规则覆盖不到的复杂情况。不过这个方法的成本比较高而且要小心模型本身的偏见适合数据量特别大、规则已经优化到瓶颈的场景。另一个方向是把过滤和训练打通做成动态的。训练过程中监控模型在某些验证集上的表现如果发现某类问题反过来调整过滤规则。这个思路比较前沿工程复杂度也高但长期看是有价值的。最后分享一个小技巧过滤规则要版本化。每次改规则都记下来改了什么、为什么改、效果如何。这样过几个月回头看能清楚知道哪条规则是有效的、哪条是多余的。我吃过没做版本管理的亏后来补上之后调优效率高了很多。
返回列表