先从结论说起:预训练模型的天花板,很大程度上不是网络结构决定的,而是数据决定的。我最早用 MindSpore 跑大模型预训练的时候,犯过一个特别典型的错误——拿到几 TB 公开爬虫文本,简单去了个重、去了个 HTML 标签就直接丢进训练流程。结果 7B 模型训到一半,loss 死活降不动,生成出来的中文句子带着乱码和广告文案,推理时还会蹦出“扫码领取红包”。后面排查了很久,问题全出在数据上:编码混乱、重复片段、低质广告、超长无效符号,这些脏数据把整个训练节奏都带崩了。
这篇文章就好好聊聊,在 MindSpore 上做大模型预训练,文本数据清洗全流程到底应该怎么设计、怎么落地。我会把流程拆成几张清晰的工序图,给出可以直接抄作业的代码模板,也会把我踩过的坑和排查思路一并整理出来。无论你是在做 RoBERTa 这类中文预训练模型,还是在搞 Qwen2.5-7B 这种行业大模型微调,数据清洗这一关都是绕不开的,而且越早做、做得越干净,后面省的时间越多。
1. 为什么预训练必须过数据清洗这一关
1.1 数据质量决定模型上限,这是算力换不回来的
很多人对大模型训练有个误解,觉得只要显卡够多、batch size 够大、训练步数够长,模型效果就一定会好。但“Garbage in, garbage out”这句话在大模型领域不是开玩笑,而是最真实的写照。模型参数量再大,如果喂进去的文本是乱码、广告、重复拼接的碎片,学到的就只能是噪声的统计规律。
一个很简单的例子:如果你训练语料里混了大量“SEO 关键词堆砌”页面,模型在生成文本时就会倾向于输出词义重复、语义断裂的句子。你要是混入了大量带 HTML 标签的正文,模型就可能在回答问题时莫名其妙输出一堆 div 标签。这些不是网络结构能修正的,因为模型在预训练阶段学到的就是数据本身的分布。
我个人的体会是,在 MindSpore 上做预训练时,数据清洗的优先级要排在模型调参之前。模型结构抄开源方案就行,优化器超参有成熟经验值,唯独数据这一块,每个数据集都不一样,必须自己动手处理。这也是为什么 OpenAI、Meta 这些团队在公开的技术报告里,都会花大量篇幅讲数据清洗和配比——这不是面子工程,是真真切切影响最终效果的环节。
1.2 脏数据的常见类型与危害程度
在实际的互联网爬虫语料里,脏数据的类型比想象中多得多。我把常见类型整理成了一张表,方便对照排查:
| 脏数据类型 | 典型表现 | 对训练的危害 |
|---|---|---|
| 编码错乱 | é¸Â这类乱码、GBK 与 UTF-8 混用 | 模型学到错误的字符映射,生成大量乱码 |
| HTML 残留 | <div>、 、<p>标签未清除 | 模型输出夹杂标签,语义被割裂 |
| 重复文本 | 同一段落出现几十次、文档间大量重复 | 训练效率降低,模型倾向复读 |
| 低质广告 | “扫码领取”“点击购买”等噪声 | 模型生成广告式文本 |
| 超短碎片 | 几个字的无意义片段 | 拉低训练信号质量,浪费算力 |
| 语种混杂 | 中文里混日文假名、韩文、英文 | 跨语种干扰,生成内容不伦不类 |
| 敏感与隐私 | 手机号、身份证号、恶意言论 | 模型泄露隐私,合规风险大 |
| 标点异常 | 连续几十个感叹号、乱用全角半角 | 模型标点使用混乱,可读性差 |
这里面最容易被忽略的是“重复文本”。你以为去重就是把完全一样的文件删掉,但真实语料里大量存在的是“部分重复”——比如同一篇文章被不同网站转载,中间加了不同的头尾;再比如一个段落被拼接到多个文档里。这种部分重复,靠简单的文件哈希根本查不出来,必须用 MinHash 或 SimHash 这种近似去重方案。
1.3 清洗流程的三大目标:干净、多样、安全
数据清洗不是把数据弄“干净”就完了。一套合格的预训练数据清洗流程,追求的是三个目标同时达成:
第一是干净。数据不能有乱码、HTML 残留、广告噪声、低质重复,这是底线要求,解决的是“模型能学”的问题。
第二是多样。数据不能因为过度清洗而失去多样性。比如你把所有长度小于 50 字的文本都删了,表面上数据变干净了,但模型对短文本的理解能力就废了。再比如你做了激进的近似去重,把相似度 0.7 以上的全删了,可能把同一主题下不同表述的优秀内容也误杀了,导致模型在某类知识上出现空洞。
第三是安全。预训练语料必须做敏感信息过滤和隐私脱敏。手机号、身份证、家庭住址这些信息,如果不处理,模型在推理时有可能被诱导输出。大模型投毒和数据泄露的新闻大家应该都看过,数据侧的安全防线是必须前置的。
这三大目标会贯穿整个流程设计。后面每一步操作,我都会明确告诉你这一步主要在解决哪个目标,避免为了追求某一个目标而牺牲另外两个。
2. 整体流程设计与模块拆解
2.1 七道工序总览:从原始文本到训练样本
我在 MindSpore 上的预训练数据清洗流程,通常拆成七道工序。每一道工序都是独立的处理模块,输入输出都是标准化的文本文件或 JSON 文件,这样既方便单步调试,也方便断点续跑。
原始语料 → 格式统一 → 编码识别与修复 → 精确去重 → 近似去重 → 低质过滤 → 安全合规 → 数据配比与采样这七道工序不是拍脑袋定的顺序,每一步都有它的逻辑:
- 格式统一在最前面,因为后面的所有处理都依赖统一的输入格式。比如原始数据有 JSON、HTML、纯文本、PDF 抽取文本,不统一格式,后面没法批量处理。
- 编码识别与修复紧接着做,因为编码错了,后面的正则匹配、语言判断都会出错。你拿一串乱码去做敏感词过滤,规则根本匹配不上。
- 去重放在过滤之前,因为去重依赖文本相似度计算,而低质过滤会改变文本内容(比如去掉 HTML 标签后文本变短了),先做去重可以避免过滤后再引入新的重复。
- 低质过滤解决内容质量问题,安全合规解决内容安全问题,这两者放在去重之后,是因为去重后数据量大幅下降,做规则过滤和模型分类的开销会小很多。
- 配比与采样放最后,这时候数据已经是干净的,你可以按主题、按长度、按语言做采样,控制最终训练集的分布。
2.2 为什么在 MindSpore 上用 Python 流水线清洗
我知道很多人会问:清洗直接用 Pandas + 多进程不就行了,为什么要用 MindSpore?我的回答是:清洗阶段用 Pandas 完全没有问题,但必须在清洗流程的设计阶段就想清楚,清洗后的数据要怎样交给 MindSpore 做训练。
MindSpore 提供了一套非常完整的mindspore.datasetAPI,支持GeneratorDataset、MindDataset、TFRecordDataset等数据加载方式。我推荐的方案是:清洗阶段用 Python 写独立脚本(可以纯用 Pandas、正则、jieba 这些),清洗完成后把数据写入MindRecord格式,再让 MindSpore 的MindDataset直接加载。这样清洗和训练解耦,清洗脚本跑一遍生成干净数据集,训练时可以反复复用。
这么做有两个明显好处。一是训练时不重复清洗。预训练通常要跑很多轮实验,如果每次都在训练 pipeline 里挂清洗逻辑,等于每次都在重复计算,非常浪费。二是MindRecord 的随机访问和 shuffle 效率非常高,它内部做了索引和分片,支持分布式场景下每个卡读取不同的 shard,不会出现数据倾斜。
当然,如果你的数据量不大(比如几十 GB 级别),也可以直接在GeneratorDataset里挂清洗函数,让清洗和读取同时进行。这个方案后面我会给代码示例,适合快速验证。
2.3 数据规模对流程设计的影响
清洗流程不是一套方案打天下的。数据规模不同,工程实现完全不一样。
百 GB 到 TB 级:单机能处理,重点用正则、规则过滤、SimHash 去重。我通常用 Python 的multiprocessing或多进程 map 做并行清洗,每台机器处理一个分片。
十 TB 到百 TB 级:单机跑不动了,要用 Spark 或 Ray 做分布式清洗。这一步要注意的是数据序列化格式,建议统一为 JSON Lines,每一行一条样本,这样分布式分片和合并都很方便。MindSpore 本身不强依赖 Spark,但清洗完的产出物仍然可以落成 MindRecord 供训练使用。
超大集群场景:这时候要考虑的是数据版本管理和增量更新。你不可能每次训练都从头清洗几百 TB 数据,所以要做数据版本快照,清洗一次、存下来、多次复用。
我自己的经验是,先在小规模数据上把清洗规则调好,再放大到全量数据跑。千万不要直接拿全量数据调试规则,因为一次清洗可能要跑十几个小时,如果规则有漏洞,改完就要重跑,非常痛苦。
3. 核心清洗环节的实操要点
3.1 格式统一与编码修复:这些细节决定了后面所有步骤的成败
格式统一这步看起来很简单,但坑最多。我遇到过原始语料里有三种来源:爬虫 HTML、PDF 抽取文本、用户上传的 DOC 文档。这三种来源的直接产物格式五花八门,必须统一成 JSON Lines 格式再往后面送。
每条样本通常包含这些字段:
{"id": "000001", "text": "正文内容", "source": "web", "title": "标题", "language": "zh", "timestamp": "20240101"}source字段很重要,后续做数据配比的时候,你是要靠这个字段控制不同来源的数据比例的。比如新闻类占比 30%、百科类占比 20%、论坛类占比 15%,这些配比信息在清洗阶段就要保留下来。
编码修复是重灾区。中文互联网语料最常见的编码问题有两个:一是 GBK/GB2312 编码被当成 UTF-8 解码,产生ä¸Â这种典型乱码;二是 UTF-8 的 BOM 头没去掉,第一条文本前面会多个\ufeff字符。修复方案是先用chardet或charset-normalizer做编码探测,再统一转成 UTF-8。
import chardet def detect_and_fix_encoding(text_bytes: bytes) -> str: # 探测原始编码 result = chardet.detect(text_bytes) encoding = result["encoding"] # 统一转成 UTF-8 try: return text_bytes.decode(encoding or "utf-8", errors="replace") except (LookupError, TypeError): return text_bytes.decode("utf-8", errors="replace")有个细节容易被忽略:errors="replace"会把无法解码的字节替换成\ufffd替换符,但替换符本身就是一种噪声。如果文本里大量出现\ufffd,说明原始编码探测可能不准,建议把这些样本直接丢弃,而不是“修复”成带替换符的文本。我一般设定一个阈值:文本里替换符占比超过 1% 直接过滤。
3.2 精确去重与近似去重:从 BloomFilter 到 SimHash 的实战选择
去重是数据清洗里最吃技术含量的一步,也是一个门类齐全的领域。我简单把它分成两层。
精确去重解决的是“完全一样”的重复。最基础的做法是对全文做 SHA-256 哈希,然后查哈希集合。Python 里直接维护一个set就行,但数据量过亿条时,内存会爆。这时候就用 BloomFilter,用极小内存解决“是否存在”的问题。注意 BloomFilter 的特性是“宁可错杀,不可放过”——它可能会把不重复的误判为重复,但不会把重复的漏掉,所以误判率控制很关键。
from bloom_filter import BloomFilter bloom = BloomFilter(max_elements=100_000_000, error_rate=0.001) def is_duplicate_exact(text: str) -> bool: sha = hashlib.sha256(text.encode("utf-8")).hexdigest() if sha in bloom: return True bloom.add(sha) return False近似去重解决的是“部分重复”。这是爬虫语料里最常见的情况:同一篇新闻被改了标题,换了几句话,然后被几十个网站转载。精确去重对它们完全无效。近似去重的经典方案是 MinHash + LSH,有的团队也用 SimHash。两者的思路都是把文本转成一组指纹,然后比较指纹集合的相似度。
SimHash 的做法比较直观:把文本分词后,对每个词计算一个 64 位哈希,按位加权求和,最后二值化为一个 64 位指纹。两篇文本的海明距离越小,说明越相似。下面是简化版实现:
import jieba import hashlib def simhash(text: str, hash_bits: int = 64) -> int: words = jieba.lcut(text) v = [0] * hash_bits for word in words: h = int(hashlib.md5(word.encode("utf-8")).hexdigest(), 16) for i in range(hash_bits): bit = (h >> i) & 1 v[i] += 1 if bit else -1 fingerprint = 0 for i in range(hash_bits): if v[i] > 0: fingerprint |= (1 << i) return fingerprint def hamming_distance(x: int, y: int) -> int: xor_val = x ^ y return bin(xor_val).count("1")实际工程中,如果你用 SimHash,阈值一般设为海明距离 ≤ 3 视为重复;用 MinHash + LSH,Jaccard 相似度 ≥ 0.8 视为重复。具体阈值要抽样本人工看,调得太严会误删好数据,调得太松去重效果又不够。我的建议是先跑一个小批量,把被判定为重复的样本打印出来,肉眼确认 100 条左右再定阈值。
3.3 低质内容过滤:规则过滤、分类器过滤与困惑度过滤
低质内容过滤是清洗流程里最需要“手艺人”感觉的一步。我从粗到细推荐三层方案。
第一层是规则过滤。速度快、可解释性强,适合做批量初筛。常用规则包括:文本长度过滤(比如小于 100 字的直接丢弃)、标点符号占比过滤(标点占比过高说明文本是符号堆砌)、HTML 标签残留检查、URL 链接占比检查、连续重复字符检查(aaaaa、!!!!!这类)。
import re def rule_based_filter(text: str) -> bool: if len(text) < 100: return True # 丢弃 if len(re.findall(r"[,。!?;:、]", text)) / len(text) > 0.3: return True # 标点占比过高 if len(re.findall(r"<[^>]+>", text)) > 5: return True # HTML 残留 if len(re.findall(r"https?://", text)) > 10: return True # 大量链接 if re.search(r"(.)\1{9,}", text): return True # 连续重复 10 次以上 return False第二层是分类器过滤。规则过滤有天花板,有些文本看着合法,但语义上是垃圾。我常用的做法是训练一个二分类模型,把“高质量文本”和“低质噪声”分开。训练数据怎么来?人工标注 + 启发式弱标注。比如从清洗后的数据里抽 5000 条当正样本,从广告页面、乱码文本、机器生成文本里抽 5000 条当负样本。模型不需要很大,BERT-base 或者 RoBERTa 中文预训练模型都可以,fine-tune 一下拿来做分类打分。
这里顺便提一句,很多做预训练的大厂会在数据清洗流水线里串联多个小模型,分别负责“质量打分”“有害内容识别”“语种识别”,而不是只靠规则。因为规则的覆盖度有限,模型能捕获语义层面的低质信号。
第三层是困惑度(PPL)过滤。这个方案很多开源中文预训练模型的清洗流程里都在用。核心思路是:用一个小型的语言模型(比如 GPT-2 或 24 层左右的 transformer)给每篇文本计算困惑度,困惑度越高,说明文本越“不符合语言模型对正常语言的认知”。那些广告语、关键词堆砌、机器拼接文本的困惑度通常显著偏高。
计算困惑度的代码可以用 MindSpore 加载一个小模型实现,也可以用现成的 PPL 计算脚本。举例:
import mindspore as ms from mindspore import nn from transformers import GPT2LMHeadModel, GPT2Tokenizer model = GPT2LMHeadModel.from_pretrained("gpt2") tokenizer = GPT2Tokenizer.from_pretrained("gpt2") def compute_ppl(text: str) -> float: inputs = tokenizer(text, return_tensors="pt", max_length=512, truncation=True) with torch.no_grad(): outputs = model(**inputs, labels=inputs["input_ids"]) loss = outputs.loss return torch.exp(loss).item()注意这里是用了 transformers 的写法,在 MindSpore 生态里,可以换成 MindSpore 版 GPT2 或者加载 MindSpore hub 上的 checkpoint。逻辑是一样的:算 cross-entropy loss,然后取指数。PPL 过滤的阈值我一般设 100~500 之间,具体根据你的语料情况调。如果你在抠细节,建议按不同来源分别统计 PPL 分布,再分别定阈值,不要全局一刀切。
3.4 敏感信息与隐私脱敏:底线中的底线
这部分是绝对不能省的,尤其如果你做的是开源模型或者面向行业落地。手机号、身份证号、银行卡号、家庭住址、车牌号这些信息,都要做脱敏或直接过滤。大模型投毒测试的一个常见手法,就是在公开语料里插入恶意样本,让模型产生特定行为。清洗阶段做好内容安全过滤,能显著降低这类风险。
我的做法是组合拳:正则匹配 + 敏感词库 + 分类模型。正则负责结构化敏感信息,比如手机号:
import re def mask_phone(text: str) -> str: # 匹配 1 开头的 11 位手机号,替换为占位符 return re.sub(r"1[3-9]\d{9}", "[PHONE]", text)敏感词库负责品牌词、垃圾广告词、恶意表述。这个词库要持续维护,建议初始用开源词库 + 自己整理的用户反馈语料,后续不断补充。模型负责的是语义层面的有害内容识别,比如隐晦的诱导言论,用文本分类模型打一个“安全分”,低于阈值的样本丢弃。
脱敏和过滤的策略要区分:手机号、邮箱这类隐私信息做脱敏(保留文本上下文但替换敏感实体),而恶意言论、违法信息直接整篇过滤。不要混为一谈,否则该保留的上下文信息也被删掉了。
4. MindSpore 工程落地:从清洗到训练的无缝衔接
4.1 用 GeneratorDataset 挂载清洗 pipeline
如果你的数据量不是特别大,想快速跑通从清洗到训练的流程,最直接的方式是用GeneratorDataset。思路是写一个 Python 生成器,每次 yield 一条已经清洗过的文本,MindSpore 会自动调度读取。
import mindspore as ms from mindspore.dataset import GeneratorDataset def clean_text_generator(file_path): with open(file_path, "r", encoding="utf-8") as f: for line in f: item = json.loads(line) text = item["text"] # 在这里挂清洗函数 if rule_based_filter(text): continue text = fix_encoding_and_html(text) if len(text) < 50: continue yield text ds = GeneratorDataset( source=clean_text_generator("raw_data.jsonl"), column_names=["text"], num_parallel_workers=8, )这里有个关键参数num_parallel_workers,它决定 MindSpore 用多少个线程并行跑你的生成器。IO 密集型的清洗任务,这个值可以调到 16~32;如果你还在用 GPU 做别的计算,不建议拉太高,容易把 CPU 占满。我的经验是 8 起步,看 CPU 利用率再调。
4.2 用 map / filter 算子组合清洗逻辑
GeneratorDataset的写法适合快速验证,但生产级流程我更推荐把清洗步骤拆成map和filter算子,这样每个算子只干一件事,调试时可以单独注释掉某一步看效果。
import mindspore as ms from mindspore.dataset import text # 依次挂载清洗算子 ds = ds.map(operations=remove_html_tags, input_columns=["text"]) ds = ds.map(operations=fix_encoding, input_columns=["text"]) ds = ds.filter(predicate=lambda text: len(text) >= 50, input_columns=["text"]) ds = ds.map(operations=deduplicate, input_columns=["text"])这里要注意map的是操作顺序:MindSpore 的map是按你调用顺序依次执行的,所以必须把“先修编码、再去 HTML、再过滤”这个顺序在代码层面固定下来。一旦顺序错了,比如先做长度过滤再做 HTML 去除,那些原本长度不足但去掉标签后长度足够的文本就被误删了。
关于 dataset 的 shuffle:预训练数据集通常很大,全量 shuffle 不现实,MindSpore 的做法是设global_random或分片内随机。我的建议是清洗后先做一次全局打乱(把样本顺序打乱,避免同一个来源的文本扎堆),然后训练时再靠 dataset 的 shuffle 机制在每次 epoch 里做局部打乱。
4.3 清洗结果写入 MindRecord,训练时直接加载
在正式做多轮预训练实验时,我强烈建议把清洗后的数据落成 MindRecord。为什么?因为MindDataset读取 MindRecord 的性能远高于别的格式,而且它天然支持多卡分片。
写 MindRecord 的代码如下:
from mindspore.mindrecord import FileWriter # 定义 schema data_schema = {"text": {"type": "string"}, "source": {"type": "string"}} writer = FileWriter(file_name="pretrain_data.mindrecord", shard_num=8) writer.add_schema(data_schema, "pretrain dataset") with open("cleaned_data.jsonl", "r", encoding="utf-8") as f: batch = [] for line in f: item = json.loads(line) batch.append({"text": item["text"], "source": item.get("source", "")}) if len(batch) >= 1000: writer.write_raw_data(batch) batch = [] if batch: writer.write_raw_data(batch) writer.commit()shard_num=8表示把数据分成 8 个分片文件。如果你的分布式训练用了 8 卡,每个卡读一个 shard,IO 不会打架。这个经验很重要,我一开始用单文件 MindRecord,多卡训练时发现每个卡都在抢同一个文件的 IO,训练速度被拖慢了 20% 左右。改成多分片后问题直接消失。
训练时加载就非常简单了:
import mindspore as ms from mindspore.dataset import MindDataset train_dataset = MindDataset( dataset_files="pretrain_data.mindrecord", num_parallel_workers=8, shard_id=rank_id, num_shards=rank_size, ) train_dataset = train_dataset.batch(batch_size=32, drop_remainder=True)shard_id和num_shards是分布式训练分片的关键参数。rank_id是当前卡的编号,rank_size是总卡数。这样每个卡只读自己那部分数据,配合num_parallel_workers做预读取,训练吞吐量会有明显提升。
4.4 分布式并行与数据吞吐优化
在 MindSpore 上做大规模预训练,数据侧的吞吐优化往往比模型侧优化更容易被忽略。我遇到过这样的情况:模型架构没问题,优化器没问题,但 GPU 利用率只有 60%,整卡跑不满。排查之后发现是数据读取的瓶颈——清洗后的数据在磁盘上分散存放,MindDataset 在多个 shard 间随机跳转,磁盘 IO 跟不上。
解决方法有几个:
第一,把数据放到本地 NVMe SSD 上。网络存储虽然容量大,但随机读的性能差,MindDataset 训练时是频繁随机读取样本的,网络存储的延迟会拖垮训练。我之前在 HDFS 上直接训练,GPU 利用率一直上不去,后来把数据拷贝到每台机器的本地盘,利用率立刻回到了 90% 以上。
第二,加大预读取缓冲。MindSpore 的Dataset支持通过config.set_prefetch_size()设置预读取缓冲区大小。默认值可能偏小,我一般调到 64 或 128。
import mindspore.dataset as ds ds.config.set_prefetch_size(128) ds.config.set_num_parallel_workers(16)第三,数据混洗时避免全量 shuffle 的开销。预训练数据集动辄几十亿样本,全量洗一遍开销非常大。MindSpore 提供了一种做法:按文件粒度做 shuffle,文件内保持顺序,然后看重启训练时的全局随机种子。简单地说,你可以把 100 个 MindRecord shard 的顺序随机打乱,每个 shard 内部的样本按顺序读,这样随机性也够用,开销小很多。
5. 我在实际项目中踩过的坑和排查实录
5.1 常见问题速查表
这里整理了一张从清洗到训练阶段最容易遇到的问题速查表,基本覆盖了我做过的所有项目里碰到的情况:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 训练 loss 不降或下降极慢 | 数据里有大量乱码、广告、重复文本 | 检查清洗后的数据质量,单独抽 1000 条肉眼审查 |
| 生成文本夹杂 HTML 标签 | HTML 去除正则写得太宽松 | 增加标签匹配规则,做二次校验 |
| 生成文本出现大量重复句子 | 近似去重阈值设得太松 | 调严 SimHash 海明距离阈值,用 PPL 过滤辅助 |
| 多卡训练时速度忽快忽慢 | 数据分片不均匀,部分卡读到超大文件 | 重新生成 MindRecord,控制每个 shard 大小一致 |
| 中文文本出现繁体/简体混杂 | 未做繁简转换 | 清洗阶段统一转简体,可以用 OpenCC |
| 清洗后发现数据量骤减 40% | 过滤规则太严 | 统计每道工序的丢弃率,逐层调整阈值 |
| 训练 1 epoch 后开始过拟合 | 数据多样性不够 | 检查去重是否过度,适当放宽阈值 |
| 推理时泄露隐私信息 | 隐私脱敏有遗漏 | 增加正则覆盖范围,用模型辅助识别 |
5.2 排查思路:从样本逆推到规则
数据清洗有个非常实用的排查思路:看到模型生成奇怪的内容,不要先调模型,先回数据里找原因。比如模型总是生成“点击这里 http://...”,那就在清洗后的数据里搜一下http,看看是不是链接过滤没做干净。比如模型生成乱码,那就检查编码修复的函数是不是有覆盖不到的编码格式。
清洗规则的调试一定要拿“坏样本”来驱动。我从原始数据里专门切了一个 5 万条的 mini 集,每次调整清洗规则,先在这个 mini 集上跑,统计每道工序的过滤率、对比清洗前后的样本,再决定是否推广到全量数据。这个习惯帮我省下了无数次全量清洗重跑的时间。
另外,每道工序的统计数据一定要留下来。比如编码修复率、去重命中率、规则过滤率、PPL 过滤率,这些数据能告诉你每一批新数据的质量趋势。如果一批新爬取的数据去重命中率突然从 20% 涨到 50%,说明爬虫策略可能抓了大量重复页面,需要调整爬虫侧的逻辑。
5.3 几个容易忽略的隐蔽问题
有一个我特别想提醒的坑:不要只过滤不修复。很多清洗脚本一遇到编码错误、敏感词、HTML 标签,第一反应就是整篇丢弃。但有些文本主体内容质量很高,只是中间嵌了几个订阅链接或一句广告语。这种文本直接丢掉很浪费,尤其是高质量的长文本在预训练语料里非常宝贵。我倾向于优先做“定点摘除”,把广告句子、HTML 片段、链接摘掉,保留正文部分,实在无法修复的才整篇过滤。
还有一个隐蔽问题是多义词与误匹配。敏感词库里的某些词,在正常语境下可能完全是中性的。比如某些品牌词、地域词,在百科或新闻里是正常信息,但因为撞了敏感词被整篇过滤,反而丢掉了高质量样本。所以我做敏感词过滤时,通常不是直接删除,而是打标记,交给下游分类器判断,或者结合上下文做条件判断。
最后,数据清洗不是一次性工程,而是持续迭代的。模型训练到什么阶段、公开语料更新了哪一批、用户反馈中出现了哪些问题,清洗规则都要跟着调。我的建议是给清洗流程保留一个规则配置文件,把正则、阈值、词库都集中管理,每次迭代只改配置、不碰代码,这样能省下很多重复开发的成本。
6. MindSpore 生态里的额外建议
6.1 结合 Model Zoo 与开源工具加速清洗
在清洗流程里,我经常会复用 MindSpore Model Zoo 里的一些预训练模型。比如用 RoBERTa 中文预训练模型做质量分类器的初始权重,用文本相似度模型做近似去重的辅助判断。这些模型在 MindSpore 生态里都有现成的权重文件和推理脚本,不用从零训。
如果你在做具体的微调任务,比如行业大模型微调,清洗完的数据直接喂给微调流程即可。要注意的是,预训练清洗和微调清洗的粒度不一样:预训练追求的是海量、干净、多样,微调追求的是精准、对齐、安全。微调数据的清洗规则通常更严格,需要让你期望模型学会的格式和风格保持一致。
6.2 和 vLLM、Ollama 等推理框架的衔接
清洗好的数据除了用于预训练和微调,还有一种常见用途:作为 RAG 知识库的底料。很多人在本地部署 Qwen、Llama 这类模型做知识问答时,需要把文档清洗后灌入向量库。这时候的清洗流程和预训练清洗有所不同,但核心思想一致:格式统一、去噪、拆分成适合检索的 chunk。
如果你用 vLLM 部署模型做离线批量推理,也可以用清洗好的数据构造评测集。清洗流程的产出物——一份干净、结构一致、带来源标记的语料库——本身就是很好的评测基准。
6.3 数据版本管理建议
清洗流程的另一个隐形需求是可复现性。我每个项目的清洗规则都有自己的版本号,和数据集的版本号一一对应。比如cleandata_v3.2对应规则文件rules_v3.2.yaml。这样跑实验的时候,谁的复现性出了问题,可以直接回退到固定的清洗规则版本,不用重新调参。
数据版本管理我用的是 Git LFS 加配置文件的组合:配置文件进 Git,大文件数据放对象存储,用配置文件里的哈希值关联。听起来很简单,但真的能救命——我踩过“清洗代码更新了、但训练用的还是旧数据”的坑,白白跑了一周实验,从那以后就养成了版本对齐的习惯。
7. 关于这套流程的一些真心话
直接给结论,这套流程跑通之后,预训练模型的收敛速度、生成质量和稳定性都会有肉眼可见的提升。我自己印象最深的一次实验:同一个 7B 模型,用没清洗的数据训,loss 在 2.5 左右就卡住了;清洗完数据后,同样的训练步数把 loss 压到了 1.8 附近,而且生成文本的乱码率从 7% 降到了 0.2% 以下。这个差距不靠调参不靠加算力,纯靠数据清洗。
最后分享一个小技巧:清洗流程里的每个过滤规则,都建议写成独立函数,并且带上统计装饰器,记录它处理了多少样本、过滤了多少样本。这样跑完清洗后,你能一眼看到每条规则的“杀伤力”,定位到是哪一步把数据量砍得太狠。拿这个数据再去调整阈值,就不再是拍脑袋了,每一刀都能下得有依据。
这套流程的适用范围比你想的广。无论是几 GB 的行业数据微调,还是几十 TB 的通用预训练,都可以按这个思路拆解落地;把七道工序理解透了,你手里的任何原始文本都能变成让模型高质量学习的养分。