☰
MindSpore大模型预训练数据质量过滤实战:清洗掉点与提速经验
2026/10/2 4:28:50 网站建设 项目流程

先说一个我踩过的坑。去年做一次中文大模型的预训练实验,前两周的loss曲线非常漂亮,稳步下降,到第三周开始忽高忽低,验证集上一些常识问答反而越训越差。排查了很久,最后定位到问题出在数据上——一条爬取来的语料里,同一个招聘广告的模板文本重复出现了几十万次,模型在疯狂背诵这些重复模式而不是学习真实语义。从那以后,我对"数据质量过滤"这件事的态度彻底变了:在大模型预训练里,数据的干净程度和数据规模同等重要,甚至比模型结构更决定最终效果。今天这篇文章,就围绕 MindSpore 框架下的大模型预训练,完整记录我自己在实践数据质量过滤这套方案时的设计思路、落地代码、性能优化和爬坑经验。如果你也正准备用MindSpore训自己的大模型,或者已经在为训练掉点苦恼,这篇内容值得你花几分钟看完。

1. 预训练数据为什么会"越练越差":质量过滤为什么是刚需

1.1 大模型预训练到底需要什么样的数据

很多人刚接触大模型预训练时,第一步就是把能找到的公开语料全部下载下来,拼接成几百GB甚至几TB的数据,然后直接开训。这种做法看似省事,但实际上隐患很大。大模型预训练的本质,是让模型从海量文本中学习语言的统计规律和知识分布。如果输入的数据本身语义不连贯、重复度高、信息密度低,模型学到的就不是"世界知识",而是各种噪声模式和垃圾模板。

你可以把预训练想象成做饭高端食材。数据是原材料,模型架构是厨具,训练框架是火候。原材料不新鲜,厨具再好、火候再准,端上桌的菜也不会好吃。数据质量过滤做的就是"挑菜、洗菜"的工作——把发霉的叶子摘掉,把烂根切掉,留下真正能入锅的部分。

那么什么算"高质量"的预训练数据?我自己的标准有三条:

  • 语义连贯:一篇文章是完整的话题表达,不是碎片拼接和关键词堆砌。
  • 信息密度高:文本里有有效的知识、逻辑或情感表达,而不是大量重复的废话。
  • 来源多样且均衡:领域覆盖广,避免单一来源占比过高导致模型偏向某类文本。

这个标准看似朴素,真正落到代码和流水线上,需要一套完整的过滤机制。

1.2 数据污染的常见类型和实际危害

我在实际清洗数据的项目中,遇到过下面这些典型污染,每种对训练的影响都值得单独说说。

污染类型典型来源对训练的影响
重复文本镜像站、模板站、营销文案批量生成模型死记硬背模板,泛化能力下降,严重时loss出现周期性震荡
低质量内容SEO生成文章、机器翻译痕迹明显的内容模型学到错误语法和逻辑断裂的表达,生成结果前言不搭后语
代码与HTML残留爬虫未解析干净的标签、CSS片段、乱码文本序列里冒出大量无意义符号,浪费token还干扰语义表示
语言混杂同一个文档里中英日韩混排多语言表示空间互相干扰,中文指令遵循能力明显波动
短文本碎片从论坛或评论区截取的片段模型生成内容偏碎片化,缺少上下文衔接能力
隐私与敏感信息个人手机号、身份证、地址模型可能记忆并复述这些信息,带来严重的安全合规风险

其中重复文本是最隐蔽的一类。它不一定是以完全相同的文档出现,更多是"同一个模板 + 换几个词"的形式。这类数据过滤起来难度最大,因为单纯做整篇文档去重完全不够,还要做句子级别和段落级别的近似去重。

还有一个容易被忽略的问题是数据量越大,噪声越难被发现。清洗前你用随机抽样的方式查看1000条数据,可能觉得质量还行,但实际数据池里有千万级别的重复簇,抽样根本看不出来。所以我后来养成了一个习惯:在正式预训练之前,先对数据做一次完整的统计画像,把重复率、长度分布、语言占比、符号占比全部跑出来,再决定过滤规则怎么定。

2. 一套可落地的数据质量过滤流水线,层级设计是关键

2.1 分层过滤架构:先便宜后贵,先粗后细

数据质量过滤最怕的就是"一上来就上最贵的方案"。比如一开始就用大模型打分过滤,几TB的数据跑一遍,计算成本高到难以接受。我自己在实践中的经验是,把过滤拆成多个层级,每一层的成本和效果都做精细控制,优先级从低到高逐级通过。

我使用的分层管线大致如下:

  1. URL与来源层过滤:基于爬虫来源的白名单和黑名单,直接丢弃来自广告站、镜像站、垃圾站的整批文档。这是最便宜的一层,一个URL黑名单就解决。
  2. 规则过滤:用正则和简单统计完成。包括文本长度检查、重复行比例检查、字符类型占比检查、URL/HTML标签残留检查等。这一层处理掉约60%的明显垃圾。
  3. 启发式打分:基于n-gram重复度、困惑度估计、中英字符比例等指标给每个文档打分,低于阈值的直接淘汰。这一层主要干掉"看着像正常文本,实际语义混乱"的文档。
  4. 模型过滤:用一个小的文本分类模型(几十MB级别)对剩余数据做质量判定。这个模型可以用人工标注的小样本训练,专门识别"机器翻译痕迹明显""SEO拼凑"这类规则难以捕捉的问题。
  5. 去重层:在文档级做MinHash/SimHash近似去重,再对句子级做精确去重。这一步保证模型不会在重复数据上浪费算力。
  6. 隐私与合规过滤:用正则匹配手机号、身份证、银行卡等敏感信息,命中即删除对应字段或整条文本。

这个顺序是刻意安排的。每一层只处理自己擅长的问题,越往后成本越高、数据量越小。规则层能用几分钱解决的,绝不用模型层几块钱的成本去过一遍,更不会用大模型去过所有数据。

2.2 关键规则的设置参数与设计思路

规则过滤是整个流水线里性价比最高的一层。下面是我常用的一组规则参数,你可以作为起点再结合自己语料的特点调整:

  • 长度过滤:单条文档小于50个字符的丢弃;大于100万字符的超长文本也值得拆分或丢弃,因为超长文本往往是爬虫拼接产生的。
  • 重复行比例:把文档按行拆分,统计不重复行的比例。一篇文章里70%以上的行都是重复的,基本可以判断是模板生成。
  • 中文字符占比:对于中文预训练语料,我通常要求中文字符占全文比例不低于30%。如果一篇自称中文的文章里面中文不到10%,大概率是乱码或混合语言。
  • HTML标签比例:如果文档中存在明显的<div>、<html>、css等关键字,直接判定为未清洗干净的网页。
  • 标点符号占比:连续出现的标点(如"!!!!""????")、大量无意义符号,直接过滤。

这些规则实现起来都不难,难的是阈值怎么调。我的建议是:先用100万条样本作为小验证集,把规则的每条命中率统计出来,再用人工查看被误杀的样本,逐步调整阈值。这个过程很像做风控模型,避免"宁可错杀一千"也要尽量保留有效数据。

3. MindSpore落地实践:离线清洗脚本与在线数据管线

3.1 离线清洗:把数据洗干净再送进训练管线

在MindSpore里做大模型预训练,我的建议是优先做离线清洗,而不是把过滤逻辑写进训练时实时执行的管线里。原因很简单:训练时每个step都要加载数据,如果中间还要跑一堆正则和去重逻辑,CPU端处理不过来,GPU只能干等,整体吞吐量会非常难看。

离线清洗流程我一般是这样组织的:

import re import json import hashlib from collections import Counter from multiprocessing import Pool MIN_LENGTH = 50 MAX_LENGTH = 100000 MAX_REPEAT_LINE_RATIO = 0.3 MIN_CHINESE_RATIO = 0.3 URL_PATTERN = re.compile(r'https?://\S+') HTML_PATTERN = re.compile(r'<[a-zA-Z/][^>]*>') def is_qualified(text: str) -> bool: if not text or len(text) < MIN_LENGTH: return False if len(text) > MAX_LENGTH: return False lines = [line.strip() for line in text.split('\n') if line.strip()] if not lines: return False repeat_ratio = 1 - len(Counter(lines)) / len(lines) if repeat_ratio > MAX_REPEAT_LINE_RATIO: return False chinese_chars = len(re.findall(r'[\u4e00-\u9fff]', text)) if chinese_chars / max(len(text), 1) < MIN_CHINESE_RATIO: return False if HTML_PATTERN.search(text) or URL_PATTERN.search(text): return False return True def process_line(line: str): try: item = json.loads(line) raw_text = item.get("text", "") if is_qualified(raw_text): return json.dumps({"text": raw_text}, ensure_ascii=False) except Exception: pass return None if __name__ == "__main__": with open("raw_data.jsonl", "r", encoding="utf-8") as fin, \ open("clean_data.jsonl", "w", encoding="utf-8") as fout: with Pool(processes=32) as pool: for result in pool.imap_unordered(process_line, fin, chunksize=256): if result: fout.write(result + "\n")

这里有个关键点:chunksize设置成256而不是默认值1,能有效减少进程间通信的开销,把多进程的并行效率发挥出来。我在实际清洗50GB数据时,32进程大概只需要半小时左右,性能瓶颈主要在磁盘IO和JSON解析上。

3.2 用MindSpore Dataset接口加载清洗后的数据

清洗完的数据基本就是一份干净的JSONL文件。接下来在MindSpore训练侧,我用GeneratorDataset配合map、shuffle、batch来构建数据管线:

import mindspore as ms from mindspore import dataset as ds from tokenizer import MyTokenizer def sample_generator(file_path): tokenizer = MyTokenizer() with open(file_path, "r", encoding="utf-8") as f: for line in f: item = json.loads(line) text = item["text"] input_ids = tokenizer.encode(text) if len(input_ids) < 32: # 过短的句子不进入训练 continue yield input_ids dataset = ds.GeneratorDataset( source=lambda: sample_generator("clean_data.jsonl"), column_names=["input_ids"], shuffle=True, num_parallel_workers=8, ) dataset = dataset.map( operations=lambda ids: ids[:4096], input_columns=["input_ids"], num_parallel_workers=8, ) dataset = dataset.batch(batch_size=16, drop_remainder=True) data_loader = dataset.create_dict_iterator(output_numpy=True)

这里有几个细节值得说清楚。GeneratorDataset的shuffle=True只在缓存池范围内做局部的打乱,如果你的数据本身就存在明显的顺序偏置(比如前100万条都是新闻类,后100万条都是百科类),局部shuffle是不够的。行业里的通用做法是在已经随机打乱的清洗后文件里做shuffle,或者设置一个较大的shuffle_buffer_size。但shuffle_buffer_size设置过大又会增加内存压力,需要在吞吐量和随机性之间做权衡,我一般设置为训练数据总量的0.1%左右,再配合每轮epoch开始前重新打乱文件列表。

另外,map操作里的这个lambda式token化如果比较重,建议单独提取成一个pyfunc函数,并且用MindSpore的map指定num_parallel_workers,会比在generator里做编码更快。我自己测试过,把tokenization从generator挪到map里,整体数据吞吐量能提升30%以上,因为map的并行度控制更灵活。

4. 分布式训练场景下,数据管线的性能瓶颈与调优经验

4.1 数据加载和模型训练必须重叠起来

单独看数据加载接口的吞吐量没有意义,关键要看训练过程中GPU是否在等待数据。我用MindSpore做分布式预训练时,会在ms.context里确认enable_auto_mixed_precision这些基础配置之后,专门检查数据管线的prefetch_size和num_parallel_workers。

MindSpore的Dataset算子链在执行时,数据从文件读取、tokenize、batch,到推送进设备,整个过程是异步的。你可以在构建GeneratorDataset时增加prefetch_size参数,让数据管线提前拉取更多样本到缓冲区。推荐的做法是:

配置项建议值说明
num_parallel_workers8~16取决于机器CPU核数,不是越大越好,过大反而增加调度开销
prefetch_size16~32单位是batch数,太小容易让GPU空等
file_read_buffer_size4~8MB针对大文件读取设计,减少磁盘IO次数
进程内缓存cache可选开启适合重复读取同一批验证集数据

我遇到过一次典型的性能问题:训练日志里每个step的耗时从预计的1.2秒涨到了2.8秒,GPU利用率只有40%。排查后发现GeneratorDataset的num_parallel_workers只有2,文件读取和tokenization全部挤在一起,数据根本喂不上。调整到16后,GPU利用率回到90%以上。所以如果你的GPU在训练时"吃不饱",先不要怀疑模型收敛问题,看看数据管线是不是堵住了。

4.2 注意动态shape和batch size的坑

大模型预训练的数据序列长度差异非常大,有的文档拆出来只有几百个token,有的能到几万。如果你直接对所有样本做batch(drop_remainder=False),同一批次内长度不一致,MindSpore需要跑动态shape,计算图推理和显存分配的开销都会增加,严重时甚至比数据加载的瓶颈还明显。

我的做法分两步:

  • 在离线清洗阶段就把超长文本切分成固定比例的重叠片段,比如每段4096个token,相邻片段重叠256个token,保证文本语义连续性。
  • 在训练管线里用pad_to_batch配合bucket_batch_by_length之类的策略,把相近长度的样本放到同一个batch里。MindSpore的bucket_batch_by_length接口可以直接按长度分桶,少用动态shape,既稳定又高效。

这本质上是把"长度不确定性"挡在训练之前。清洗和切分阶段多花一点时间,能让训练阶段的吞吐量稳定非常多。

4.3 分布式场景下的全局Shuffle策略

当训练卡数多到几十上百张时,每张卡拿到的是切片后的数据。如果你是在每个rank内部独立shuffle,很容易出现的是:同一个样本在不同epoch被反复放在同一个rank上,模型实际上是在"局部数据"上反复训练,没有真正见过全局均匀分布的数据。

MindSpore支持ds.config.set_seed配合dataset.shuffle()做全局性的数据打乱,但真正稳妥的方式是在离线阶段把数据文件打好乱,然后把文件均匀分到各个rank,每个rank内部再做buffer shuffle。我在训练一个7B模型时发现,同一个epoch内,如果每个rank的数据分布明显偏向某个领域,loss曲线会出现周期性波动。把文件整体随机打乱再分发之后,这个问题基本消失了。

还有个进阶做法是每隔几个epoch重新洗一次文件分配方案,确保不同的rank在后面的epoch拿到不一样的数据切片。这对7B以上规模的模型尤其重要,能够有效缓解某些领域数据被单一rank长期垄断的问题。

5. VSCode + MindSpore内核:调试数据过滤代码的环境搭建

5.1 注册MindSpore内核,让Jupyter能正确识别

写数据过滤代码和调参数时,我基本都在VSCode里干活。很多人第一次想在VSCode的Jupyter Notebook里用MindSpore,会遇到"import mindspore失败"或"内核连接失败",根本原因往往是Jupyter没有找到你安装MindSpore的那个Python环境。

解决思路很简单:把MindSpore环境注册为一个独立的Jupyter内核。具体操作如下:

# 1. 创建并激活MindSpore环境 conda create -n mindspore python=3.9 conda activate mindspore # 2. 安装MindSpore(根据你的CUDA版本和平台选择版本) pip install mindspore==2.2.0 # 3. 安装Jupyter和内核管理工具 pip install ipykernel jupyter_client # 4. 注册为Jupyter内核,display-name是你在界面上看到的名称 python -m ipykernel install --user --name mindspore --display-name "MindSpore Env"

注册完成后,打开VSCode的.ipynb文件,右上角选择内核时就能看到"MindSpore Env"。选择它之后,Notebook里import mindspore就能正确导入。

这里有几个容易踩的细节。第一,如果你用的是VSCode远程连接服务器开发,需要在服务器的环境里执行ipykernel install,并且VSCode的Jupyter扩展会通过SSH自动找到远端的内核。第二,如果你的机器上同时有多个conda环境,注册时一定要先conda activate mindspore,否则可能注册成了base环境的内核。

5.2 给过滤脚本加断点:用launch.json跑Python调试

写离线清洗脚本时,我更习惯直接用VSCode的Python调试功能,而不是Notebook。因为脚本处理的是成千上万条数据,用Notebook一行行执行反而低效。

在.vscode/launch.json里配置一个MindSpore环境的调试配置:

{ "version": "0.2.0", "configurations": [ { "name": "MindSpore 数据清洗调试", "type": "debugpy", "request": "launch", "program": "${file}", "console": "integratedTerminal", "python": "/path/to/your/mindspore_env/bin/python", "env": { "PYTHONPATH": "${workspaceFolder}", "CUDA_VISIBLE_DEVICES": "0" } } ] }

把python字段指向MindSpore环境的解释器绝对路径,然后在你的is_qualified函数里打上断点,按F5就可以单步调试每条文本的过滤逻辑。这个方式比盲目跑完整脚本高效多了。特别是当你怀疑某类文本被误杀时,可以用一个只有几十条样本的小文件跑一遍,观察每条规则的实际命中情况,快速定位是哪个条件太激进了。

5.3 先用小样本验证规则,再全量清洗

我在看别人的数据清洗脚本时,经常发现一个问题:规则写了一大堆,但从来没有用真正的小样本验证过过滤效果,直接扔到全量数据上跑。这样做风险极高——很可能某个正则把大量正常内容误删了,或者某个字段解析错误导致整批数据被跳过。

我的建议是:每次修改过滤规则,先构造一个"黄金测试集"——从清洗前的数据里随机抽500条,人工标注其中哪些是优质内容、哪些是垃圾内容,然后跑清洗脚本,对比过滤结果和标注结果的重合度。黄金测试集不要求很大,500条足够覆盖常见的垃圾类型,关键是迭代规则时的验证速度要快。等规则在小样本上精度稳定了,再部署到全量数据上,几乎不会翻车。

6. 实测效果与高频踩坑复盘

6.1 一套典型的清洗前后指标对比

我在一次中文预训练任务中,用上面的分级过滤管线处理了一份约120GB的原始语料,清洗后剩下约86GB。清洗前后的关键指标变化如下:

指标清洗前清洗后
文档总数约1850万约1330万
平均文档token数约650约830
文档级近似重复率约14.5%约1.2%
中文文本占比约72%约91%
规则命中垃圾样本比例100%(定义)低于0.3%

训练侧最直观的变化是:模型收敛更平稳,同等算力下,验证集困惑度比不清洗直接训练降低了1.8左右。更重要的是,下游任务评测里的连贯性指标提升明显,模型生成时重复片段的问题显著减少。这个收益完全来自数据质量过滤,模型结构和训练参数一点没改。

6.2 踩坑记录:结论、根因和修法

下面这几个坑,是我在多次数据清洗实际项目中真实遇到过的问题,每一个都让人头大,写出来希望能帮你跳过。

坑一:规则阈值过于激进,导致有效数据被大量误杀。

有一次我想把重复行比例阈值从0.3调到0.1,觉得这样能更彻底地过滤模板内容。结果清洗完发现所有法律条文类的文档几乎全部被清掉了——法律条文里每一条都是相似的句式,重复行比例天然偏高,被误判成模板。教训是:不要只看规则的"命中率",还要看"误杀率"。调阈值后一定要跑黄金测试集,确认好数据被保留的比例。

坑二:正则匹配HTML标签时误删正常代码内容。

技术文档和代码教程里经常会有<div>、<class>这类字符串,我用re.compile(r'<[a-zA-Z/][^>]*>')匹配HTML标签时,把大量代码示例里的泛型写法和XML片段也删掉了。后来我在过滤逻辑里加了一个前置判断:如果文档属于代码/技术教程类型(可以通过文件来源或开头特征判断),就跳过HTML标签过滤,只做代码块完整性检查。

坑三:MindSpore的map算子里跑Python正则,性能骤降。

我一开始把很多清洗逻辑直接写进了训练时的map算子,正则匹配在Python层跑,每个样本都要经过多个正则,训练吞吐量掉了将近一半。后来把清洗和训练彻底分离,训练管线里只保留tokenization和padding,性能才恢复。你要做的过滤,尽量全放在离线阶段,训练管线里能不用正则就不用正则。

坑四:VSCode里ipykernel install注册的内核被其他conda环境覆盖。

因为我在多个环境里反复安装ipykernel,最后Notebook里出现了多个指向同一个环境的"幽灵内核",每个都报import错误。解决办法是把环境目录下残留的~/.local/share/jupyter/kernels里的旧内核目录清掉,重新注册一遍,并且用jupyter kernelspec list确认当前生效的内核列表。

坑五:小批量验证数据的分布和全量数据不一致。

我曾在某次训练前只抽样看了10万条清洗结果,感觉质量不错,结果全量训练时发现loss明显偏高。事后排查才发现,这10万条抽样来自文件的头部,而头部恰好是某个月份爬取的新闻数据,质量整体较好;全量数据里的尾部有大量未清洗干净的论坛数据,分布差异巨大。现在我做任何抽样验证,一定先从全量文件里做随机抽样,绝不再用文件前N条当样本。

最后说两句真心话

数据质量过滤这件事,看起来不如模型结构创新那么风光,但它带来的收益往往是最直接、最稳定的。我身边的同学经常抱怨预训练效果不好,最后排查来排查去,大多数问题都出在数据上。我的经验是:花在数据清洗上的时间,永远比花在调参上的时间回报率高。

如果你正准备做一个大模型的预训练项目,真心建议先把数据质量过滤方案想清楚、测明白,再开始烧卡。先用几百万条小规模数据跑通清洗流程,把规则和阈值验证好,再全量铺开。过程中多积累规则、多复盘误杀案例,这些东西在未来每一个预训练项目里都能重复使用。等哪天你看到清洗前后训练曲线明显改善的时候,就会明白这套流程的含金量。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询