
1. 大模型预训练数据清洗到底在洗什么很多人一提到大模型预训练第一反应就是堆显卡、调并行策略、跑通训练脚本。但真正在工业级预训练里摸爬滚打过的人都知道决定模型最终能力的上限往往不是模型结构而是喂进去的数据质量。一个 7B 的模型如果预训练语料干净、去重彻底、噪声控制得当它在下游任务上的表现可以轻松超过一个用脏数据硬喂出来的 13B 模型。这不是玄学是大量实验反复验证过的结论。我这次要拆解的是基于 MindSpore 框架做的大模型预训练文本数据清洗全流程。简单说就是把互联网上抓下来的原始文本——网页、论坛帖子、代码仓库、电子书、百科条目——经过一系列处理变成可以直接喂给预训练任务的干净语料。这套流程解决的核心问题是原始数据里充斥着 HTML 标签、重复段落、乱码、广告、模板化文本、低信息密度内容如果不处理直接训练模型会学到大量垃圾模式浪费算力不说还会导致生成质量严重下降。这套流程适合谁参考如果你正在用 MindSpore 做大模型预训练或者你手上有几十 GB 到几 TB 的原始文本需要处理又或者你只是想搞清楚工业界的数据清洗管线到底长什么样那这篇内容应该能给你一个可以直接抄作业的框架。我会尽量把每一步的“为什么这么做”讲清楚而不是只丢一堆代码。2. 整体管线设计与核心思路拆解2.1 为什么清洗流程要分阶段而不是一把梭新手最容易犯的错误就是写一个大脚本从原始文件读进来一路 filter 到底最后输出。这种写法在小数据量上跑得通但一旦数据量上到 TB 级别问题就全暴露了某个过滤规则写错了你得从头再跑一遍中间某一步想调整阈值整条链路都要重来更致命的是你根本不知道每一步到底过滤掉了多少数据、过滤掉了什么。所以工业界的标准做法是分阶段、可落盘、可回溯。每一阶段处理完的数据都写到磁盘上下一阶段从磁盘读。这样做的好处很直接调试的时候只需要重跑出问题的那一阶段可以随时抽样检查中间结果不同阶段可以用不同的并行策略和资源规格。我这次设计的管线大致分成六个阶段原始数据解析与格式统一基础清洗去标签、去乱码、去控制字符质量过滤启发式规则 统计特征去重精确去重 近似去重敏感与有害内容过滤最终格式化与分片每个阶段之间用统一的中间格式我选的是 JSONL每行一个文档对象带 metadata串联。选 JSONL 而不是 Parquet 或者 TFRecord主要是考虑到 MindSpore 的MindDataset对文本类数据的读取灵活性以及调试时肉眼可读的便利性。当然如果你的数据量特别大Parquet 在存储效率上会更好这个后面会细说。2.2 清洗规则的设计哲学宁可错杀还是宁可放过这是数据清洗里最核心的取舍问题。过滤太狠数据量骤减模型欠拟合过滤太松噪声混进去模型学歪。我的经验是在预训练阶段宁可稍微放过一点也不要错杀太多。原因是预训练本身就是一个大规模学习的过程少量噪声会被海量干净数据稀释掉但如果因为规则太严导致数据分布偏移比如把某个领域的文本全过滤没了模型在这个领域的能力就会直接塌掉。具体到阈值设定上我一般会先在小样本比如 10 万条上跑一遍统计每条规则的命中率和过滤后的数据分布然后人工抽样检查被过滤掉的内容确认没有大规模误杀再放到全量数据上跑。这个“小样本试跑 人工抽检”的步骤是我踩过好几次坑之后养成的习惯强烈建议你不要跳过。2.3 MindSpore 在这套流程里扮演什么角色需要说清楚一点数据清洗本身大部分是 CPU 密集型的文本处理MindSpore 在这里的主要价值不是做计算而是和后续训练流程无缝衔接。清洗完的数据最终要喂给 MindSpore 的MindDataset或者GeneratorDataset如果清洗阶段就用 MindSpore 的数据处理算子来做格式和接口上会省很多事。另外MindSpore 提供的一些并行数据处理能力在去重这种需要全局计算的步骤上能派上用场。比如近似去重里的 MinHash LSH如果自己用 Python 多进程写内存和调度都很容易出问题而借助 MindSpore 的分布式能力或者至少是它的数据并行接口可以更稳地跑完。3. 核心细节解析与实操要点3.1 原始数据解析别小看这一步原始数据的来源五花八门最常见的是 Common Crawl 的 WARC 格式、各种爬虫抓下来的 HTML、以及已经半结构化的 JSON。这一步的核心任务是把它们统一成“纯文本 元数据”的形式。HTML 解析我推荐用trafilatura或者readability-lxml而不是直接用 BeautifulSoup 暴力去标签。原因很简单暴力去标签会把导航栏、页脚、广告文案全都留下来而 trafilatura 这类工具是专门做正文抽取的能识别出页面的主体内容区域。实测下来用 trafilatura 抽出来的正文后续质量过滤的负担能减轻一大半。import trafilatura def extract_main_text(html_content): text trafilatura.extract( html_content, include_commentsFalse, include_tablesFalse, no_fallbackFalse ) return text注意trafilatura 的no_fallback参数建议设为 False这样在正文抽取失败时会回退到简单模式避免整条数据被丢弃。宁可拿到稍微脏一点的文本也不要直接丢数据。元数据这块至少要保留来源 URL 的域名、抓取时间、原始语言、文档长度。这些在后面做质量过滤和数据分析时都会用到。特别是域名可以用来做基于来源的过滤比如某些站点整体质量低可以整站降权。3.2 基础清洗控制字符和乱码处理这一步看起来简单但细节很多。控制字符比如\x00到\x08必须清掉否则后续写入文件或者喂给模型时会出各种诡异错误。Unicode 里的零宽字符\u200b、\u200c等也要处理它们肉眼看不见但会干扰分词。乱码处理要小心。常见的乱码是编码错误导致的比如 UTF-8 被当成 GBK 解码。我的做法是先尝试用ftfy库修复修复不了的再根据乱码比例决定是否丢弃。import ftfy import unicodedata def clean_text(text): text ftfy.fix_text(text) text .join( ch for ch in text if unicodedata.category(ch)[0] ! C or ch in \n\t ) return text这里保留\n和\t是因为它们在后续的段落切分和格式判断里有用。其他控制字符一律清掉。3.3 质量过滤启发式规则怎么定质量过滤是整个管线里最需要经验的一步。我一般用这几类规则长度过滤太短的文档比如少于 50 个字符信息量太低直接丢。太长的比如超过 10 万字符可能是拼接了多个页面的脏数据需要截断或者单独处理。符号比例过滤统计文档里非字母数字字符的比例如果超过 30%大概率是代码、表格或者乱码需要根据目标领域决定是否保留。重复行比例如果一个文档里超过 50% 的行是重复的说明这是模板化文本比如商品列表页信息密度极低。困惑度过滤用一个小的语言模型比如 KenLM 训练的 n-gram 模型计算文档的困惑度困惑度太高的文档通常是乱码或者机器生成的垃圾。这个规则效果很好但计算成本也高建议放在管线的后半段。规则类型阈值建议主要过滤目标最小长度50 字符碎片化文本最大长度100000 字符拼接脏数据符号比例 30%代码、表格、乱码重复行比例 50%模板化页面困惑度领域相关机器生成垃圾提示阈值不是固定的一定要根据你的目标领域调整。比如你要训练一个代码大模型符号比例阈值就得放宽到 60% 以上。3.4 去重精确去重和近似去重的分工去重是数据清洗里收益最高的一步。互联网文本的重复率惊人很多内容被转载、镜像、拼接了无数次。不去重的话模型会在这些重复内容上反复学习导致记忆化严重泛化能力下降。精确去重用文档的 SHA256 哈希就行简单粗暴能干掉完全相同的文档。但精确去重只能处理 100% 相同的对于改了几个字的转载无能为力。近似去重才是重头戏。主流方案是 MinHash LSH对每个文档计算一组 MinHash 签名然后用 LSH 把可能相似的文档分到同一个桶里再在桶内做精细比对。相似度阈值一般设在 0.8 左右超过就认为是重复保留其中一条。from datasketch import MinHash, MinHashLSH def build_minhash(text, num_perm128): m MinHash(num_permnum_perm) for token in text.split(): m.update(token.encode(utf8)) return m lsh MinHashLSH(threshold0.8, num_perm128)这里有个坑MinHash 是基于 token 的中文需要先分词。我一般用 jieba 做粗分词或者直接用字符级 n-gram比如 3-gram后者对中文效果更稳不依赖分词质量。3.5 敏感与有害内容过滤这一步的规则设计需要非常谨慎既要过滤掉真正有害的内容又要避免误伤正常文本。我的做法是维护一个多层次的过滤词表分成“直接丢弃”和“降权保留”两档。直接丢弃的是明确有害的内容降权保留的是边界模糊的可以在后续采样时降低权重而不是直接删掉。技术上可以用 Aho-Corasick 自动机做高效的多模式匹配比逐个关键词去in判断快几个数量级。4. 实操过程与核心环节实现4.1 环境准备与依赖安装先把环境搭起来。MindSpore 的安装根据你的硬件平台选对应版本CPU 版本就够跑数据清洗GPU 版本在困惑度计算那一步会快一些。pip install mindspore pip install trafilatura ftfy datasketch jieba pip install pyahocorasick如果你要用 KenLM 做困惑度过滤还需要单独编译安装这一步稍微麻烦一点后面会细说。4.2 分阶段脚本的组织方式我习惯把每个阶段写成一个独立的 Python 脚本用命令行参数控制输入输出路径和配置。这样可以用 shell 脚本串起来也可以用任务调度系统管理。python stage1_parse.py --input /data/raw --output /data/stage1 python stage2_clean.py --input /data/stage1 --output /data/stage2 python stage3_quality.py --input /data/stage2 --output /data/stage3 python stage4_dedup.py --input /data/stage3 --output /data/stage4 python stage5_safety.py --input /data/stage4 --output /data/stage5 python stage6_format.py --input /data/stage5 --output /data/final每个脚本都支持多进程用multiprocessing.Pool或者concurrent.futures都行。我一般用后者接口更清爽。4.3 质量过滤的并行实现质量过滤是 CPU 密集型的多进程加速效果明显。但要注意困惑度计算如果用的是 KenLM模型对象不能跨进程共享需要在每个进程里单独加载。from concurrent.futures import ProcessPoolExecutor def process_chunk(lines): results [] for line in lines: doc json.loads(line) if passes_quality_filter(doc): results.append(line) return results with ProcessPoolExecutor(max_workers16) as executor: futures [] for chunk in read_in_chunks(input_path, chunk_size10000): futures.append(executor.submit(process_chunk, chunk)) for future in as_completed(futures): write_results(future.result(), output_path)分块大小建议设在 5000 到 20000 行之间。太小了进程调度开销大太大了内存吃紧。4.4 近似去重的分布式处理近似去重是整条管线里最耗资源的一步。如果数据量在千万级文档以上单机跑 MinHash LSH 会非常慢。这时候有两个选择一是用 MindSpore 的分布式能力把 MinHash 计算分散到多个节点二是用 Spark 之类的工具先做一轮粗筛。我这次用的是 MindSpore 的数据并行接口把文档分片后并行计算 MinHash 签名然后汇总到主节点做 LSH 分桶。LSH 分桶本身是内存密集型的如果内存不够可以调低num_perm比如从 128 降到 64代价是相似度判断的精度会下降一些。注意LSH 的 threshold 参数直接决定了去重的激进程度。设成 0.7 会干掉很多“疑似重复”的文档设成 0.9 则只干掉几乎完全一样的。我一般先用 0.8 跑一遍然后抽样检查被判定为重复的文档对看看有没有误杀。4.5 最终格式化与 MindSpore 数据集对接清洗完的数据最终要能被 MindSpore 高效读取。我一般输出成两种格式一种是 JSONL用于存档和调试另一种是 MindSpore 的MindRecord格式用于训练时高速读取。from mindspore.mindrecord import FileWriter schema { text: {type: string}, domain: {type: string}, length: {type: int32} } writer FileWriter(final.mindrecord, shard_num16) writer.add_schema(schema, pretrain_data) # 逐条写入... writer.commit()shard_num建议设成训练时并行读取的 worker 数量的整数倍这样每个 worker 都能均匀分到数据分片避免 IO 瓶颈。5. 常见问题与排查技巧实录5.1 内存爆了怎么办近似去重那一步最容易爆内存。LSH 的桶结构在文档量大时会占用大量内存。我的经验是如果内存不够先把num_perm降到 64再把 LSH 的桶数量调大减少每个桶里的文档数最后考虑分批次处理——把数据分成几批每批单独做 LSH批与批之间用布隆过滤器做粗略的跨批去重。5.2 过滤后数据量骤减这是新手最慌的情况。先别急着放宽阈值先做两件事一是统计每条规则的命中率看看是哪条规则在大量杀数据二是抽样检查被过滤掉的文档确认是不是真的该过滤。我遇到过好几次发现是某条规则的阈值设错了比如符号比例阈值设成了 0.1改过来数据量就正常了。5.3 中文分词导致的去重误判用 jieba 分词做 MinHash 时如果分词结果不稳定比如同一段文本两次分词结果不同会导致 MinHash 签名不一致去重失效。解决办法是固定分词词典和分词模式或者干脆用字符级 n-gram 替代分词。5.4 困惑度计算太慢KenLM 的困惑度计算是单线程的在千万级文档上会非常慢。我的做法是先用轻量规则长度、符号比例过滤掉大部分明显低质的文档只对剩下的文档算困惑度。另外可以把 KenLM 模型量化牺牲一点精度换速度。问题现象可能原因排查方向内存溢出LSH 桶过大降低 num_perm分批处理数据量骤减阈值过严统计各规则命中率去重失效分词不稳定改用字符 n-gram处理太慢困惑度计算瓶颈前置轻量过滤5.5 一个容易被忽略的坑编码问题原始数据里混着各种编码的文本如果不在第一步统一成 UTF-8后面每一步都可能出问题。我的做法是在解析阶段就用chardet检测编码然后统一转成 UTF-8转不了的直接丢弃。这一步虽然简单但能省掉后面无数的诡异 bug。6. 我在这套流程上踩过的坑和总结的经验说几个只有真正跑过全流程才会知道的细节。第一去重一定要放在质量过滤之后。如果先做去重那些低质但重复的文档会占用大量去重计算资源而且去重后的结果里还是混着低质内容。先过滤再去的顺序能让去重阶段的输入量减少 30% 到 50%。第二每个阶段都要输出统计报告。我一般会在每个脚本结束时打印输入文档数、输出文档数、过滤率、各规则命中数。这些数字在排查问题时是救命稻草。有一次我发现某个阶段输出量异常低一看统计报告发现是某条规则的命中率高达 90%明显不正常一查果然是阈值写错了。第三抽样检查要贯穿始终。不要等到最后才看数据长什么样。每个阶段结束后抽 100 条看看花不了几分钟但能提前发现大问题。我习惯用shuf -n 100 output.jsonl | less快速抽样。第四MindSpore 的 MindRecord 写入要注意分片大小。单个分片建议不要超过 2GB否则读取时会有性能问题。如果数据量特别大就多分几个 shard。这套流程我在几个不同规模的数据集上都跑过从几十 GB 到接近 1TB整体框架是稳的。真正需要根据具体情况调整的主要是质量过滤的阈值和去重的相似度阈值。这两个参数没有万能值只能根据你的目标领域和数据特点去试。我的建议是先用小样本把参数调到一个合理的范围再放到全量数据上跑这样能省下大量返工时间。