很多人问我的一个问题:公开的开源数据大家拿到的大差不差,为什么最后训出来的模型效果能差出一截?早期我归因到模型结构和训练框架,后来做多了才发现,真正的差距多半藏在训练数据的“配方”里。这篇我拿去重强度、质量过滤和领域比例三件事,在同一套模型和训练条件下做了一轮完整的消融实验。结论比较直接:这三个开关都不是有就行的关系,各自存在收益递减的区间,配比错了会直接吃掉你几个百分点的下游分数。文章会给出我实际用到的基线设计、实验矩阵、处理管线和踩坑记录,适合正在做预训练数据Pipeline、或者打算调整自有数据配比的团队参考。
1. 为什么数据配比值得专门做一轮消融实验
1.1 数据配方才是真正拉开差距的杠杆
做LLM训练的人都知道一句老话:模型结构决定上限,数据决定你能摸到多高的上限。但真到实操里,大部分团队在模型结构上的讨论投入远高于数据配比。PyTorch版本、张量并行、序列长度、学习率调度,这些能调的都调了个遍,唯独数据配比经常拍脑袋:看某个开源模型报告里写了“代码数据占12%效果好”,自己也照着切12%;听说“去重阈值0.8不错”,自己也写死0.8。
这种抄作业式做法之所以不靠谱,是因为不同团队的原始语料构成、数据源头、清洗深度都不同。同样0.8的MinHash阈值,别人数据里是干净爬虫文本,你数据里全是半生不熟的HTML转文本,过滤后的效果自然天差地别。我这次做消融实验的动机很简单:想用可复现的实验,量化同一批原始语料在不同去重强度、不同质量过滤策略、不同领域配比下的模型性能变化,让后续数据迭代有据可依。
1.2 消融实验的边界:先弄清它回答什么问题
消融实验不是万能药。它最适合回答“单因子变化对结果的边际影响”这类问题,样本是“把去重阈值从0.8抬到0.95,模型在HumanEval上到底涨还是跌”。但它回答不了“整体最佳数据配方是什么”这种全局优化问题,因为因子之间本身有交互作用,比如强去重加严格过滤会让语料量骤减,这时还硬要保持固定的总token数,就必然得引入更多重复遍历或人工混合数据,干扰项就进来了。
所以我这轮实验的设计原则是:一次只动一个变量,其余全部锁死,先看边际方向,再看量级。方向比绝对值更重要——你不需要精确知道代码比例从12%调到16%涨了多少分,但你必须知道它是正收益还是负收益,以及它的收益是否已经进入平台期。
2. 消融实验的基线设计与评估体系
2.1 固定基线:模型、数据、训练参数一锤定音
我先把这次消融实验的公共底座列出来,后续所有实验组都在这个基础上改数据配方,其他什么都不动:
| 固定项 | 设置 |
|---|---|
| 模型规模 | 1.8B参数,decoder-only,标准Transformer |
| 序列长度 | 4096 |
| 优化器 | AdamW,学习率峰值3e-4,余弦衰减 |
| 训练总token | 固定约100B(按预处理后的净token计) |
| 原始语料 | 同一批清洗前的数据:中文网页约55%、英文网页约25%、代码仓库约14%、数学类约4%、其他多语种约2% |
| 评估集 | MMLU、C-Eval、CMMLU、HumanEval、GSM8K,加上中文文本困惑度统计 |
这里有一个容易忽略的细节:训练总token必须固定。如果不固定,某个实验组因为过滤太狠导致token变少,你很难分清下游分数变化是因为配比变了,还是因为模型根本没训够。所以我在数据处理时就做了“token预算对齐”,每个实验组把处理后的语料按比例抽样或过采样到同一个总量。
2.2 评估维度:不止是benchmark分数
只看五个benchmark的平均分会掩盖很多问题。比如强过滤可能让MMLU的常识类分数微涨,但生成类任务或代码任务掉得厉害。所以我的评估分三层看:
- 多任务benchmark:MMLU、C-Eval衡量通用知识,HumanEval、GSM8K衡量代码和数学推理,CMMLU衡量中文深度理解。
- 文本统计指标:中文和英文的分领域perplexity,用来观察模型在特定语料分布上的拟合程度。
- 针对性样例检查:从生成结果里抽样检查多样性和重复度,防止模型“分数漂亮但开口复读机”。
这类检查很费时间,但只有把三层叠在一起看,才能知道数据配比的改动到底改在了哪里。
2.3 预算规划:用1/10数据跑预筛,用全套验证
训练100B token的1.8B模型也不是免费的,如果每个实验组都跑满全套,一轮消融下来成本非常难看。我采用的策略是两阶段:
第一阶段先用原始语料的1/10量级(约10B token)训练同一个模型,跑完所有消融组合。这个量级下模型还没完全收敛,但相对差异的排序已经具备参考价值。第二阶段只挑第一阶段里差异最明显的两三组,用完整的100B token再训一次做确认。实践下来,这个“小规模筛选、大规模确认”的流程能把消融成本控制在直接全套做实验的1/4左右,而且排序稳定性在多数任务上足够可靠。
3. 去重强度怎么消融:MinHash阈值与去重粒度的实际影响
3.1 去重强度和保留率之间的量化关系
去重环节,我用的是MinHash加LSH的经典方案。核心参数是Jaccard相似度阈值,两个文档的MinHash签名相似度超过阈值就判定为重复,保留先出现的,丢弃后出现的。阈值越低,去重越激进,留下的token越少。为了把阈值换算成“保留率”,我在同一批原始语料上做了测试:
| MinHash阈值 | 处理后保留token比例 | 说明 |
|---|---|---|
| 0.95 | 约91% | 几乎只去掉完全重复或轻微改写 |
| 0.85 | 约82% | 常规推荐区间,去掉较大片段重复 |
| 0.8 | 约76% | 我线上常用的起点 |
| 0.7 | 约69% | 明显更激进,文档级相似就会被去 |
| 0.5 | 约58% | 很激进,大面积模板化的文本基本被清掉 |
阈值0.8到0.85之间通常是最甜区。低于0.7后,保留率快速下滑,但token质量并没有同比例提升,反而开始出现误伤——一些内容不同但结构相似的文档被当成重复删掉,比如多个教程都有的“前置知识”段落。
3.2 三档阈值下的实验结果拆解
我把去重强度拆成三档做消融:弱去重(阈值0.95)、中等去重(阈值0.8)、强去重(阈值0.5)。其他配置全部保持基线,质量过滤用同一档规则。
| 实验组 | MMLU | C-Eval | HumanEval | GSM8K |
|---|---|---|---|---|
| 基线(阈值0.8) | 45.8 | 45.1 | 24.9 | 26.7 |
| 弱去重(0.95) | 43.9 | 43.2 | 23.1 | 25.2 |
| 强去重(0.5) | 46.2 | 45.4 | 24.1 | 26.1 |
结果很有代表性。弱去重时,语料里大量重复模板会让模型在训练后期陷入记忆式学习,MMLU直接掉了近2分;强去重时,代码和数学任务不升反降,因为代码仓库里很多问题描述相似但解法不同,激进去重会把一部分带有微妙差异的样本误删,模型在HumanEval上掉了0.8分。这类结构性差异,只有把通用任务和代码任务分开看才看得出来。
3.3 去重粒度:文档级、段落级还是句子级
除了阈值,去重粒度也值得消融。文档级去重实现简单,但粒度太粗,两个网页共享一段版权声明或“相关阅读”列表就会被整体判重丢弃。段落级和句子级去重能更精细化地清理重复片段,但对Pipeline的工程要求高很多。
我这次对比了“文档级0.8”和“文档级0.8加句子级0.9”组合。后者在中英文通用任务上比前者高约0.4分,在困惑度指标上也有改善。句子级去重的主要贡献是清掉了那些长文档内部反复出现的样板句——比如产品页的“点击购买”“支持多平台”这类高频句子。正式跑大规模训练前,强烈建议至少上一层句子级去重。
4. 质量过滤的消融:从启发式规则到分类器再到困惑度
4.1 过滤层级的拆解:Light / Medium / Heavy
质量过滤不是单点操作,而是层层递进的多级管线。我做消融时把它拆成三档:
- Light:纯启发式规则,只看长度、符号占比、乱码率、重复字符占比。
- Medium:在Light基础上,加一个fastText质量分类器。分类器用“高质量百科/新闻语料为正例、低质广告/垃圾文本为负例”训练,输出每个样本属于高质量类的概率,概率低于阈值的样本丢弃。
- Heavy:在Medium基础上加困惑度过滤。用一个小型语言模型计算每个文档的平均Perplexity,过滤掉那些PPL异常高或异常低的文本。异常高说明内容可能驴唇不对马嘴,异常低说明可能是一段重复的模板。
三档过滤的token留存情况:
| 过滤档位 | 留存token比例 | 中文PPL | MMLU | C-Eval | GSM8K |
|---|---|---|---|---|---|
| Light | 约88% | 8.62 | 44.1 | 43.5 | 25.3 |
| Medium | 约76% | 8.31 | 45.8 | 45.1 | 26.7 |
| Heavy | 约67% | 8.16 | 45.3 | 45.8 | 26.2 |
4.2 过滤强度的边际收益
数据最直接的体现是:Light到Medium的提升非常显著,MMLU和C-Eval分别上涨1.7分和1.6分,这笔收益几乎白捡,因为fastText训练成本很低,推理也只占整个Pipeline的一道工序。但Medium到Heavy就进入平台期了,MMLU反而微跌,GSM8K也降了0.5分。
原因是困惑度过滤对复杂推理型文本不友好。数学题的逻辑推理部分,因为省略了很多自然语言连接词,PPL天然偏高;代码文件里变量命名紧凑,PPL分布也跟新闻语料差很多。直接用统一PPL阈值过滤,会误伤数学和代码语料。后来的修正做法是分领域各算各的PPL分布,再按分位数过滤,比全局一刀切靠谱很多。
4.3 一个容易踩的坑:过滤太狠导致领域塌陷
过滤有一个隐蔽风险,就是“领域塌陷”。比如原始语料里数学内容只占4%,如果质量过滤时把大量数学格式文本判定为低质,数学语料可能直接缩到2%。表面上看总token还够,但模型在数学任务上的表现会明显滑坡。所以我在每次调过滤强度之后,都会先按领域统计一遍留存比例,确认没有某个领域被结构性误杀。Medium这档在这个环节表现比较均衡,Heavy则需要逐领域校准阈值。
5. 领域比例的消融:代码、数学与通用语料的棋盘
5.1 先行结论:代码和数学是两个收益最陡的杠杆
在去重和质量过滤都用基线配置的前提下,我重点调了代码和数学两类数据的占比:
| 实验组 | 代码占比 | 数学占比 | 通用占比 | MMLU | HumanEval | GSM8K |
|---|---|---|---|---|---|---|
| 基线 | 12% | 4% | 84% | 45.8 | 24.9 | 26.7 |
| 代码提升 | 20% | 4% | 76% | 45.5 | 27.4 | 27.1 |
| 数学提升 | 12% | 8% | 80% | 45.1 | 25.6 | 29.5 |
| 双提升 | 20% | 8% | 72% | 44.7 | 28.2 | 30.1 |
代码和数学占比分别上调8个点和4个点后,HumanEval和GSM8K分别涨了3.3分和3.4分,这个幅度在1.8B模型上已经非常可观。代价是MMLU掉1分左右,这是典型的通用知识迁移损失。如果你核心场景就是代码生成和数学推理,这个兑换非常划算;如果做的是通用助手,就得谨慎了。
5.2 配比曲线的收益递减现象
为了看清楚“往上加到什么时候就该停”,我额外跑了两组:一组代码占比30%,另一组数学占比16%。试验结果很直白——代码占比从20%顶到30%,HumanEval基本没再涨,GSM8K也原地踏步;数学占比从8%顶到16%,GSM8K反而掉了约0.6分,因为数学语料大量挤占通用语料后,模型在其他任务上的负向迁移开始盖过数学受益。
结论是:代码和数学的收益曲线都是先陡后平的形状。代码在12%到20%区间收益明显,超过20%看不太出差价;数学在4%到8%区间收益显著,超过10%后可能反向。这类结论跟模型规模、基础数据质量都有关系,换到更大规模时可能需要再测一轮,但“先上升、后平台、再下降”这个基本形态是稳定的。
5.3 比例调整如何影响不同语言的迁移
领域配比不只有代码和数学,多语种配比也值得一提。我把中英配比从60:25调整到50:35,英文语料增加后MMLU上涨约0.6分,中文的CMMLU和C-Eval则下滑约1分。这符合直觉:数据里英文占比高,英文知识覆盖就更足,但中文空间的拟合密度被稀释。
如果你的产品主打中文,与其盲目提高英文通用语料,不如先稳住中文核心领域数据,再小幅引入英文高质量知识类语料做补充。那种“中英各一半”的做法在通用场景未必最优。
6. 完整实操流程:从原始语料到结论表
6.1 数据处理流水线的搭建
我这次的Pipeline完全跑在Spark集群上,按“清洗、去重、过滤、配比”四步走,写成可复现的离线任务。清洗阶段先把HTML标签剥离、做Unicode规范化、统一标点,这步很基本但千万别省。去重阶段用datasketch库实现MinHash和LSH,分片处理千万级文档时要调整好band数,避免单个分片数据倾斜。
# 去重阶段:构建MinHash并做LSH查询 from datasketch import MinHash, MinHashLSH def build_minhash(text, num_perm=128, shingle_size=8): m = MinHash(num_perm=num_perm) for i in range(len(text) - shingle_size + 1): m.update(text[i:i+shingle_size].encode("utf-8")) return m lsh = MinHashLSH(threshold=0.8, num_perm=128) for doc_id, text in corpus: m = build_minhash(text) candidates = lsh.query(m) if not candidates: lsh.insert(doc_id, m) keep_doc(doc_id, text)质量过滤阶段,分类器部分我用fastText训练,迭代很快,一份几万条的标注样本就够启动。
# 分类器训练与预测 import fasttext model = fasttext.train_supervised("quality_labels.tsv", lr=1.0, epoch=25, dim=128) for doc_id, text in corpus: label, prob = model.predict(preprocess(text)) if label == "__label__good" and prob > 0.55: emit(doc_id, text)配比阶段按最终需要的领域比例做采抽样。注意代码和数学语料本身占比小,提高配比靠的是对这些语料做多轮重复过采样,而不是凭空造数据。
6.2 六组消融实验的矩阵设计与运行
我最终跑了六组,算是把三件事的关键档位都覆盖到了:
| 实验组 | 去重阈值 | 过滤档位 | 代码比例 | 数学比例 | 训练量级 |
|---|---|---|---|---|---|
| E0 基线 | 0.8 | Medium | 12% | 4% | 10B + 100B确认 |
| E1 弱去重 | 0.95 | Medium | 12% | 4% | 10B |
| E2 强去重 | 0.5 | Medium | 12% | 4% | 10B |
| E3 重过滤 | 0.8 | Heavy | 12% | 4% | 10B |
| E4 代码优先 | 0.8 | Medium | 20% | 4% | 10B + 100B确认 |
| E5 数学优先 | 0.8 | Medium | 12% | 8% | 10B + 100B确认 |
每组先在10B量级上训练,统一用8块A100(80GB)跑两天左右,记录checkpoint并评估。特别地,我会在训练中段(30%和70%)各存一个checkpoint,观察不同配比的收益曲线方向是否一致。
6.3 结果汇总与边际收益分析
综合全部实验,我的最终结论可以用一个简单排序表达:
- 去重从“弱”调到“中等”,收益最大,成本最低,几乎是稳赚。
- 去重从“中等”调到“强”,收益很小,风险上升,谨慎操作。
- 质量过滤从“Light”调到“Medium”,收益明显,建议默认启用。
- 质量过滤从“Medium”调到“Heavy”,收益趋近于零,还需关注领域误伤。
- 代码和数学比例从基线调高,收益大但有上限,且会换掉部分通用分数。
7. 踩坑实录:数据消融实验里最容易被低估的五个问题
7.1 波动大:实验跑不出信噪比
消融实验最大的敌人不是成本,是噪声。1.8B模型在不同随机种子下,同一份数据训出来的MMLU能差1到2分。如果你改一个变量只带来0.3分变化,很可能直接被随机性淹没。我的对策有两个:一是固定训练种子,让所有实验组从同一个起点开始;二是小规模筛选阶段,每个实验组跑两个不同种子取平均,虽然多花一倍时间,但结论可靠性明显提升。
7.2 token总量对不齐
很多时候不是故意不对齐,而是处理管线天然会改变token总量。强过滤后语料少了,为了凑到训练预算只能过采样;那这个过采样本身又引入重复,叠加回数据里。这个问题没有完美解法,只能靠集中在报告里说明清楚,不要掩盖。我习惯在每个实验组数据表后面单独注明“有效token中重复遍历占比”,方便后续分析时扣除这部分干扰。
7.3 只看一个checkpoint就下结论
预训练不同阶段的表现排序可能反转。E3重过滤那组,在小规模上下文中期曾短暂领先,但跑到后期被E0超回去。如果只取最后一步对比,有些规律会失真。所以至少取三个检查点(比如30%、70%、100%)看趋势,不要只盯最终分数。
7.4 单一benchmark迷信
MMLU涨了不代表模型整体变好。那组“数学优先”的配置,GSM8K很好看,但CMMLU和C-Eval都掉了1分上下,说明数学语料挤占了中文通用知识空间。做数据配比决策时,一定把业务目标排在benchmark前面。如果我只做对话助手,MMLU和CMMLU的优先级远高于GSM8K。
7.5 过滤后语料欠拟合
这个坑很反直觉:过滤最狠的实验组,训练loss反而下降更慢。原因不是语料质量不够,而是语料总量骤减后,模型只能反复看同一批样本,泛化能力被削弱。如果你在用强过滤,必须同步检查训练loss和下游任务得分两个指标,一旦出现“loss其实已经收敛但benchmark还在往下掉”的现象,大概率是数据多样性不足了。
跑完这一轮,我个人最大的体会是:数据配比调优不是“找到最优值然后不再变”的静态问题。语料来源会变、业务目标会变、模型规模也会变。与其追求一个万能配方,不如把这套消融实验的方法固化下来,每当数据组成有较大变化时,花一周时间重新跑一遍,用结论指导数据迭代。后续我打算再把“数据配比与模型规模的联动关系”单独拉出来做一轮实验,看看在3B、7B规模上这些结论的迁移程度,等到结果整理清楚再回来更新。