模型能力的上限,往往不在结构里,而在喂进去的数据里。这是我做LLM训练到现在最深的体会。同一个架构、同一份算力预算,训练数据的去重强度、质量过滤策略和领域配比稍作调整,下游效果可以差出好几个点。这篇是LLM Training Lab系列的第17篇,我想把最近一组围绕“数据配比”做的消融实验完整复盘一遍:三个变量各自怎么影响训练效果、组合起来又会有什么坑、最终我们是怎么定出生产配置的。
先说明实验边界。这组消融全部跑在1.2B参数左右的小模型上,中文场景为主,训练数据整理后约55B token。用小模型的逻辑很简单:同样的结论在1B上跑一轮只要几十卡时,在7B上跑一轮是几倍的算力成本。数据配比这种需要多组对比的活儿,先用小模型压低成本把边界探出来,再用7B对少数关键配置做验证,是性价比最高的做法。下面所有数值都是这个规模下的趋势,不能直接当大规模训练的结果抄,但规律本身我认为有参考价值。
1. 实验设计:三个旋钮怎么拆开、怎么对比
消融实验最忌讳一上来就同时动好几个变量。三个维度——去重强度、质量过滤、领域比例——任何一个单独调都能影响最终loss和基准分,同时动两个,出了问题根本说不清是谁干的。所以我把实验拆成三组:
- 第一组:固定质量过滤和领域配比,只扫去重强度的5个档位。
- 第二组:固定去重强度和领域配比,只扫质量过滤的4档阈值。
- 第三组:固定去重强度和质量过滤,只扫5组领域配比。
每组实验跑同样步数(15B token,约训练集总量的四分之一轮),用同一套评估集:通用能力看MMLU、中文理解看CMMLU、代码看HumanEval、数学看GSM8K,外加域内困惑度。中间检查点每500步存一次,方便后面画loss曲线。
基线配置我定了这么一套:去重采用minhash LSH + 阈值0.8的标准操作,质量过滤用perplexity和分类器双通道卡掉最底下约30%的数据,领域配比按原始语料的自然分布(通用:代码:数学:中英其他 ≈ 70:15:8:7)。这个基线不是随便拍的,是参考了实验室之前十几组经验整理出来的“不犯错”配置。消融时每组只偏离其中一个维度,这样任何分数变化都能归因。
还有两个容易被忽略的设计细节。第一,每次实验用完全相同的种子和数据切分顺序,否则loader的采样随机性会把真正的配比效应淹没掉。第二,所有档位的训练都跑满同样的步数和batch size,不做loss早停,因为配比实验关注的是模型后期稳定性和基准分数,不是训练到多少步能停下来。这两点没做到,所谓消融就是自欺欺人。
2. 去重强度:不是越干净越好,遗忘曲线会咬人
2.1 五个档位怎么定义
先把去重强度量化。我们对原始语料做了三个层次的处理,组合出五个档位:
| 档位 | 处理方式 | 语料保留比例 |
|---|---|---|
| 0档 | 完全不去重,只去掉完全相同URL和空行 | 100% (约55B) |
| 1档 | 精确去重:md5完全重复句子剔除 | 约92% |
| 2档 | minhash LSH jaccard≥0.8去重(近似重复剔除) | 约78% |
| 3档 | jaccard≥0.6去重,更激进 | 约65% |
| 4档 | 3档基础上叠句子级前缀去重(公共substring裁掉) | 约51% |
2档是我们之前的基线,所以第一组消融其实是重点看0、1、3、4这几个相邻档位怎么移动。
2.2 核心结果:loss降了,分数未必升
跑完15B token,域内困惑度确实随着去重强度单调下降:0档时验证集困惑度在11.8左右,4档降到了9.6。这个不意外,去重越狠,训练数据里重复样板越少,模型被反复背诵同一段话的概率越低,困惑度自然好看。但下游基准的曲线完全不是单调的:
| 档位 | 验证困惑度 | MMLU | CMMLU | HumanEval | GSM8K |
|---|---|---|---|---|---|
| 0档 | 11.8 | 26.1 | 30.4 | 9.8 | 4.2 |
| 1档 | 10.9 | 27.8 | 31.2 | 11.6 | 5.0 |
| 2档 | 10.2 | 28.4 | 32.6 | 14.1 | 6.5 |
| 3档 | 10.0 | 27.9 | 31.8 | 15.3 | 5.8 |
| 4档 | 9.6 | 27.2 | 30.1 | 12.7 | 4.9 |
3档的HumanEval冲到15.3,但MMLU和GSM8K反而比2档回落了。4档更明显,困惑度全场最低,HumanEval、GSM8K全都崩。这不是噪声,两个种子下结果方向一致。
为什么困惑度一路变好、下游能力却走回头路?我的理解是:激进去重实际在改变数据的“经验分布”。去重把近似重复的表述全部杀掉后,模型看到的语料形态变得更“书面标准化”,少了很多真实世界里的稀疏表达、同义改写、嘈杂上下文。这会让模型在训练集上收敛得很漂亮,但面对评测集那些“不那么干净的问题”时反而泛化减弱。另一个更隐蔽的问题是遗忘:当重复文本从几十份降到一份,模型对这个知识点的记忆强度会被其他大权重样本稀释,训练后期对早期知识的遗忘更明显——这个在GSM8K上尤其突出,受训于第10B到15B token之间的数学文本一旦被后续代码文本挤掉,推理步骤就接不上。
2.3 实操建议:两级去重,不是一竿子到底
踩完这组实验,我们的生产配置最终选了2档,而不是追求更低的困惑度。具体做法是:
- 第一级:minhash LSH jaccard=0.8,拿掉高相似重复文本,这一步能砍掉20%左右冗余。
- 第二级:句子级exact hash去重,针对的是爬虫数据里大量出现的“模板句”——标题、免责声明、公共页脚这类,按整句hash派除掉。
不要对全部语料做jaccard=0.6以下的激进模糊去重。如果你确实需要提高数据“多样性”,用采样加权把稀有来源提上来,比把近似重复文本硬删掉安全得多。代码数据尤其要小心,很多代码仓库自带README模板、MIT许可证长文,激进模糊去重会把不同repo里的许可文本误判成重复,结果就是所有项目的README被抽掉好几份,后面模型写代码时版权声明和注释风格都会异常“干净”——这种干净不是好事,是多样性的流失。
3. 质量过滤:低分数据不是垃圾,是垫脚石
3.1 过滤器怎么搭、阈值怎么扫
质量过滤我们用了两条通道。第一条是规则通道:按文档长度、符号字占比、语言检测、perplexity控制在正常范围。第二条是学习通道:用一个小号的fastText分类器,标注了一批“高质量人工标注样本”和“明显噪声样本”做二分类,输出一个0到1的质量分。两条通道打分后加权合并,然后按阈值切。
这一组扫了4档:
- A档:保留质量分前50%的数据
- B档:保留前70%(删掉最低30%)
- C档:保留前85%
- D档:只删明显乱码和机器生成垃圾(大概是前93%)
实验结果如下:
| 档位 | 保留比例 | MMLU | CMMLU | HumanEval | GSM8K | 训练loss走势 |
|---|---|---|---|---|---|---|
| A | 50% | 29.3 | 33.8 | 13.5 | 7.8 | 全程平稳 |
| B | 70% | 28.4 | 32.6 | 14.1 | 6.5 | 平稳 |
| C | 85% | 27.5 | 31.9 | 15.8 | 7.2 | 中段有波动 |
| D | 93% | 26.0 | 30.7 | 10.2 | 4.8 | 后期loss偏高 |
A档的MMLU比baseline(B档)高了近一个点,GSM8K也上去了,但HumanEval掉了。C档HumanEval最高但通用能力回落。D档全面崩溃,这证实了只删乱码、不做质量过滤是不够的,噪声数据会持续拖住模型。
3.2 一个反直觉的现象:保留30%“低分”反而提升数学推理
这个发现不在计划内。A档数据只有50%,按理说信息量最少,为什么GSM8K反而最好?我们把A档训出来的模型拿来逐题分析,发现它对推理题目的格式泛化能力更强。原因在于质量分的最低30%并不都是垃圾——里面有大量社区问答、论坛求助帖、带错别字的解题过程,这些文本的优势是富含“人的真实推理痕迹”:跳过步骤、甚至结论错误,但思维方式更像人。经过规则和分类器过滤留存下来的50%,全是“标准答案体”,反而让模型学到了模板化的推理输出,对评测集里偏口语化、背景夹缠的数学题适应能力差。
所以我们后面做了一个补充实验:在B档保留数据里,人为往训练集回填2%质量分最低的真实帖子文本(从被删掉的数据里抽样),GSM8K进一步涨到7.6,HumanEval不降。这说明高质量数学代码数据的骨架必须要有,但低分数据提供了“路标”——它让模型知道界限在哪里、什么样的推理路径算走岔,这在数据科学里叫对比信号。全高分的世界没有对比,模型学不出好坏边界。
3.3 分类器训练的三个坑
分类器这条路我们不是第一次走,这次踩了新坑,如果只讲阈值不讲坑,相当于只给地图不给陷阱清单:
- 标注集合不能只标“完美文本”和“乱码文本”,一定要掺入“看起来正常但内容空洞”的文本,比如纯列表堆积、无上下文的问答片断。否则分类器会把空洞内容打出高分。
- 不要用目标模型自己训完的输出去构建负样本,这会造成模型自我强化——它觉得通顺的就是高质量,又绕回“重复舒服文本”的循环。用通用语料库随机文本做负样本更干净。
- 分类器分数和perplexity分数要分开观察,二者相关性过高时说明文本长度一直在干扰判断。长文档perplexity天然偏高,按长度分桶归一化后再合并,才能真实反映质量维度。
4. 领域比例:这里藏着最大的效果杠杆,也最容易顾此失彼
4.1 比例是怎么扫的
领域配比这组实验,我们把语料按领域分成四桶:通用文本、代码、数学推理、中英其他(问答、对话、百科)。基线比例是70:15:8:7,然后扫五组:
| 组别 | 通用:代码:数学:其他 | MMLU | HumanEval | GSM8K |
|---|---|---|---|---|
| R1 | 70:15:8:7(基线) | 28.4 | 14.1 | 6.5 |
| R2 | 50:25:15:10 | 27.9 | 16.8 | 7.9 |
| R3 | 30:40:20:10 | 25.8 | 18.6 | 8.4 |
| R4 | 50:15:25:10 | 26.9 | 13.2 | 9.6 |
| R5 | 60:20:12:8 | 28.1 | 15.6 | 7.4 |
明显看到通用能力(MMLU)与代码、数学比例呈反比,但有意思的是,MMLU跌幅很慢:通用从70%减到30%,MMLU只掉了2.6个点,而HumanEval和GSM8K各涨了4.5和1.9个点。这说明通用语料里面有大量的“冗余支持”——很多通用文本的知识密度低,删掉后对通用任务理解损失没有想象中那么大。但别高兴太早,R3组里CMMLU已经掉到28.9,中文通用理解受损明显,说明通用语料对中文场景仍有不可替代的作用。
4.2 三个细节,决定了领域比例实验成不成立
领域配比实验有三件事做不好,结论基本不可信。
第一,领域分类器要固定且不过拟合。我们用fastText在残差语料上做了8个领域类,训练后手动抽查分类器对“数学题带故事的文本”“代码注释里的中文句子”分得不够稳定,于是改成了规则优先:代码由文件扩展名和AST可解析性兜底,数学由公式密度和解题步骤关键词兜底,分类器只对通用/其他做二次细分。这样分类噪声最大的两栏就被结构性控制了。
第二,采样顺序必须固定。领域比例变了之后,我们仍按同样顺序把各领域数据拼接进global batch,而不是每个batch内重新shuffle。因为领域间分布差异大,如果每个batch都随机shuffle,模型会频繁切换任务模式,损失函数震荡剧烈,配比效应会被高方差掩盖。真实世界里你也不会让模型每步都在法庭文书和GitHub代码之间横跳,保序采样更接近真实的课程式训练。
第三,数学语料按难度分层后再按比例抽取。这里有个容易掉进去的坑:纯按“数学总数占比”扩数学数据,会把大量重复的基础四则运算题灌进去,信息量没增加,“数学字面占比”却上去了。我们在数学桶内部按字符串长度分三层(短题、中题、长题),每层保证固定最低采样量,禁止短题全部占满配额。R4组能做到GSM8K 9.6而不崩HumanEval,和这个分层有很大关系——如果只堆总量,模型常见症状是四则运算熟练、推理题答不上来,分数虚高。
4.3 比例一块调,还是两层调?
做领域配比时一个反直觉的发现是:把数学比例直接拉高到25%,GSM8K确实涨到9.6,但MMLU只掉了1.5个点,表面上赚翻了。可是看分科目细项,MMLU里的STEM部分跌了3.8个点,是社会科学在托底才让总分没那么难看。这提醒我领域比例评估不能只看总分,要拆细项,尤其是当你想给数学加权的场景。
我们后来在实用配置里用了一个折中:先定性别配比,再把每个batch的末尾5%位置替换为数学与代码的“refresher”样本。这些refresher选自被比例削减掉的高质量代码/数学文本,位置固定在batch尾部,让模型在一个batch的最后集中强化推理模式。这个技巧同时从R3和R4各借了一半——训练得到的MMLU回到27.9,GSM8K保持在8.8,两边都满意。如果你在生产里想给某个领域临时加权重,可以考虑类似方式,而不是直接把整体比例拉偏,这样对通用能力伤害最可控。
5. 三个维度的交叉效应:为什么分开调都对、合起来就翻车
5.1 一组交叉验证的失败案例
三组单变量消融做完,我们直接按各自最优档组合了一个“理论最优配置”:去重4档(最激进)、质量过滤A档(保留前50%)、领域比例R4(数学拉高)。结果非常难看:MMLU 24.3、HumanEval 11.9、GSM8K 8.1,连单变量的谷值都比不上。
复盘原因有三层:
第一,激进去重把过滤后仅剩50%数据里的“近似重复群”又压缩了一遍,两个操作合在一起导致语料规模只有原来的25%左右,信息多样性严重不足。
第二,高质量数学数据对去重强度更敏感。数学文本里“因为AD平行于BC,所以角ADB=角CBD”这种标准句式,jaccard=0.8勉强不误判,但到4档jaccard=0.6时就被当成重复文本处理掉了,等于是把最有用的数学逻辑骨架给抽走了。
第三,领域比例拉偏数学后,batch顺序里刷题文本密集排列,模型进入“答题模式”出不来,本身就容易过拟合,叠加激进去重让每道题只剩一个表达,过拟合就更严重,评测里稍微换个问法就不会了。
这组交叉验证告诉我们:单变量的最优解不是全局最优解,数据配比是一个多维约束优化问题,甚至有点像一个脆弱的生态系统,挪一块板子就要检查另外几块有没有塌。
5.2 联合调优的顺序策略
吃了一次亏就长记性了。我们的联合调优顺序改为:
- 先固定去重强度在2档,因为它是三个变量里对下游分数最“钝”的一个,不灵敏就不会引发不可控波动。
- 再调质量过滤阈值,找到“低分数据保留率”上限,让信息量够用又不至于被噪声拖累。
- 最后用领域比例做精细调节,因为领域比例对基准分的影响最直接,放到最后调可以在前两个变量确定的底板上精准找位置。
这个顺序多跑了好几轮,但每一轮都能稳定复现。反过来做的话,领域比例一变,前面两个变量的最优解全变,一天能跑出来的有效实验会少一半。
另一个重要发现是质量过滤和领域配比之间存在“阈值补偿”关系。当我们把数学领域比例从8%拉到15%时,质量过滤B档(删30%)自动变成了A档(删50%)的效果——因为数学语料本身质量方差小,过滤器删掉的多是低分通用文本。但通用比例不降时,同样把质量过滤推到A档,损失就藏在知识广度和CMMLU里。所以换领域比例后,要么重新扫一遍质量过滤梯度,要么接受现结果并压低增删幅度。
5.3 最终生产配置长什么样
经过上面所有轮次,我们锁定的生产配置是:
| 维度 | 配置 |
|---|---|
| 去重 | minhash LSH jaccard=0.8 + 句子级精确去重,不做4档激进模糊去重 |
| 质量过滤 | 保留质量分前70%,另回填2%真实低分帖子文本 |
| 领域比例 | 通用55% : 代码20% : 数学12% : 其他13%,每个batch尾部放5%推理refresher |
这个配置在7B模型上验证过一次,MMLU 39.6、GSM8K 31.5,虽不算顶尖,但在同等算力规模里比最初基线高了近3个点的综合分。更重要的是所有分数稳定,没有出现反复偏离的小方差抖动,这对生产训练很重要——宁可平均分低半档,也不要那种某天版本刷新后忽高忽低的配置。
6. 跑数据配比实验最容易忽略的三件小事
实验本身说完了,最后补几个工程层面的心得。数据配比实验不需要高深的模型知识,但对流程自律要求很高,很多动辄几十卡时的实验就是毁在这些小事上。
第一,每个配比实验单独存一份数据清单。清单至少包含:数据版本的hash、各领域token占比、去重参数、过滤阈值、采样种子。不要只记在你脑海里,也别只写在实验平台描述里——两季之后你根本想不起“5203版本”是什么配比,清单文件能省掉你翻代码翻半天的命。
第二,中途加数据是大忌。我们踩过一次,去重强度实验跑到8B token时发现某个数据源有重复拉取,手动reload了数据流,结果后面所有检查点的loss都带了一次跳变,那一整组实验只能作废。配比实验一旦启动,数据流必须冻结,哪怕发现数据有脏样本也先记下来,下个版本再清理。
第三,评估集要做“防污染预演”。领域比例改变后,数学题一下变多,要提前确认评测集里的题没有出现在任何一个版本的训练数据里。我们用ngram重叠扫描过,从通用语料里抽出来的题和评测集重叠度最高到0.3%,直接导致GSM8K虚高0.7个点。这个量级足以改变消融结论的方向,千万别拿污染过的分数去配置选择。
这三件事在能跑32卡时的团队里最容易被当成“辅助工作”往后排,但实践下来它们对实验质量的影响不小于模型结构上的调整。
数据配比这摊事,本质上是在“让模型见多识广”和“让模型研读经典”之间反复找平衡点。我这组实验做下来最朴素的收尾建议是:每次只动一个变量,动之前把假设写清楚,动完把结果记全。配比实验不产出什么激动人心的新方法,但它决定了你能不能在同样的算力下多挤出几个点,而这几个点,往往就是项目上线与不上线的差别。