☰
知识蒸馏中的数据筛选:RSR方法如何挑选黄金数据
2026/10/21 1:22:59 网站建设 项目流程

做模型蒸馏这些年,我最常被问的一句话是:教师模型是不是越强越好?直觉上当然越强越好,但你真正动手把一个 70B 教师的知识压进 7B 学生的时候会发现,事情远没这么简单。前阵子某课题组放出了一套叫 RSR 的数据筛选思路,核心观点非常反直觉:教师模型的能力上限,和它提供给你的蒸馏数据质量,根本不是一回事。简单说,就是教你怎么从一大堆无标注数据里,挑出真正适合学生学习的“黄金数据”。我照着这个思路重新跑了一遍自己的蒸馏管线,有收获也踩了不少坑,今天这篇就把这套方法从头到尾拆开讲。

1. 强师翻车现场:强教师怎么教出弱学生

1.1 蒸馏的实质不是压缩能力上限,而是传递决策边界

很多人把知识蒸馏理解成“让大模型教小模型”,好像教师模型越大、学生学到的就越多。但真正做蒸馏的人会告诉你,蒸馏学的不是教师的能力上限,而是教师在一个具体数据分布上的决策规律。

教师输出的是什么?是软标签,也就是模型在各类别或词表上的概率分布。比如一个简单的分类问题,教师可能给出“猫 0.6、狗 0.35、树 0.05”这样的结果。相比 one-hot 标签,软标签最大的价值在于保留了“猫和狗之间存在竞争关系”这一信息,这比单纯告诉学生“答案是猫”高级得多。

但如果教师对某个样本输出的分布是“猫 0.999、狗 0.0008、树 0.0002”呢?这时候软标签几乎没有提供任何相对关系信息,学生从里面能学到的只有“背下这个答案”,而不是“理解猫和狗在特征空间里的边界”。这就是强教师的第一个隐患:它太自信了,自信到把教学变成了抄答案。

1.2 强教师的三个隐藏毛病

第一个毛病是过度自信的尖峰分布。大模型在自己训练过的分布上,logits 经常拉开非常大的差距,softmax 之后基本是一个像素级的独热分布。你说它错了吗?没错,但这种输出对蒸馏几乎没有信息量。就像让短跑冠军给小学生示范跑姿,他直接按最高速度冲了一遍,小学生看到的只是残影,完全学不到蹬地和摆臂的发力逻辑。

第二个毛病是系统性错误。教师越强,它的错误往往越有规律。它会在某一类样本上持续出现同一种误判,比如把包含特定句式的问题全部导向一个错误答案。这类错误一旦被学生模仿,就会变成很难纠正的偏差。因为学生没有足够的能力判断教师哪里错了,它只会把教师的所有行为都当成标准。

第三个毛病是难度分布失衡。强教师看简单样本和中等样本几乎都是高分,区分度很低。大量样本落在“教师闭眼都能做对”的区域,真正处于“临界区”——也就是学生跳一跳能够到的样本,占的比例反而很小。这时候如果不做筛选,你得到的蒸馏集会被简单样本淹没,学生训练到后期基本没有梯度信号。

所以强师翻车这件事,根子不在教师强弱,而在数据没选对。教师是好的,但喂给学生的“教材”是坏的。RSR 解决的就是这个“教材挑选”问题。

2. 想筛黄金数据,先定义清楚什么叫“可教”

2.1 置信度、一致性、分歧区间三件套

要选数据,首先得有一套判断标准。我拆过几版思路之后发现,基本离不开三个信号:教师置信度、教师预测与标签的一致性、以及教师输出分布的分歧区间。

教师置信度,指的是教师给真实标签分配的概率。它衡量教师对这个样本的把握程度。一致性,则是指教师 argmax 出来的答案和真实标签是否一致。如果教师的高置信答案都是错的,那置信度再高也不能用。分歧区间,我一般用 top1 概率减 top2 概率来度量。这个值如果太小,说明教师自己也在两个答案之间摇摆,输出分布熵很高,学生学起来容易懵;如果太大,说明样本对教师太简单,没有教学价值。

综合来看,一块合适的“黄金数据”,大概率同时满足三个条件:教师对真实标签的置信度处于中高区间(我一般看 0.65 到 0.92),教师 argmax 和标签一致,以及 top2 的差距在一个适中的范围内。这三个条件合起来,等价于说:教师大概率能做对,但并没有做到“闭眼秒杀”,它还保留了一点犹豫和比较空间。而学生要学的,恰恰就是这个空间里的判断逻辑。

2.2 容易样本不是垃圾,难样本也未必是金子

很多团队做蒸馏筛选时会有一个执念:多选难样本,学生才能学得更快。这个想法有一定道理,但直接照搬会出事。

难样本里混着大量噪声标签。尤其在大规模爬取的数据里,很多“难”不是真的知识难,而是标注本身有问题,教师在上面输出混乱,学生硬学只会把错误也学进去。更关键的是,如果蒸馏集里全是困难样本,学生学习曲线的方差会非常大。前几步还在学基础规则,后几步突然被一堆极端样本冲击,训练稳定性很难保证。

容易样本反而承担了一个重要职责:提供稳定的梯度信号。它让学生快速建立对基础模式的识别能力,这部分能力是后续学习困难样本的底座。一个好的蒸馏集,应该是“基础样本 + 临界样本 + 少量高难样本”的混合体,而不是单吊某一类。

所以“可教”这个概念,本质上不是一个绝对难度,而是相对学生当前能力“略高一步”的样本。但问题来了:学生能力在训练过程中是不断变化的,我们没法实时估计,所以只能退而求其次,用教师输出内部的“自我难度”来近似这个“可教区间”。

2.3 校准度在这里比想象中更重要

只看置信度还有一个暗坑:置信度不等于正确率。一个校准不良的教师,可能对某类样本给出 0.9 的置信度,但实际正确率只有 0.7。这时候按置信度排序,你会优先选中那些“教师自信满满但本质上在做错题”的样本。

校准度这个概念从分类模型那边来,指的是模型预测概率和经验频率是否一致。如果模型对所有预测为 0.8 的样本,实际正确率也接近 0.8,那它就是校准良好的。在无标注蒸馏场景里,我们不能直接算校准误差,但可以用教师自身的自洽性做近似,比如看教师在不同温度下是否稳定做同一个预测,或者看教师输出分布和模型整体正确率之间的偏移趋势。

RSR 把校准误差作为一个惩罚项集成进去,本质上就是在说:别只看教师“敢不敢打包票”,还要看它打包票的含金量。

3. RSR 筛选框架的完整拆解

3.1 相对排序到底在排什么

RSR 的全称我习惯理解成 Relative Signal Ranking,它的核心变化在于:不做绝对打分,而是在候选样本内部做相对排序。

为什么相对排序这么重要?因为不同领域、不同来源的样本,教师置信度的天然分布完全不同。某些领域的文本教师随便给到 0.95,另一些领域可能最高只有 0.8。你设一个全局阈值 0.9,就会把第二类领域的数据几乎全部砍掉。相对排序没有这个毛病,它只看每个样本在自己那一批候选者中的位置。

整体分数由几个信号线性加权组合。我使用的版本大致是这样:

RSR_score = w1 * consistency + w2 * true_confidence + w3 * (1 - sharpness) + w4 * (1 - calib_gap)

其中 consistency 是教师 argmax 与标签是否一致,true_confidence 是教师给真实标签的概率,sharpness 用 top1 减 top2 来度量,calib_gap 是教师置信度和经验正确率之间的偏移估计。权重我一般取 0.25、0.4、0.2、0.15,但这不是固定的,不同任务需要自己调。

3.2 分桶采样策略:黄金数据不是“排名前 N”,而是“各段都有一点”

拿到 RSR 分数之后,直觉做法是直接取最高的前 10%。但我强烈不建议这么做。原因有两个:一是同质化,排在最前面的样本大概率看起来特别像,清一色的高置信、低分歧,学生学完容易变成某种模式的特化模型;二是多样性崩塌,很多中低区间的样本虽然单看“教学价值”没那么高,但它覆盖了语言表达的另一种形态,全部丢掉之后模型的泛化能力会退化。

RSR 的做法是把候选集按分数从低到高分成 5 个分位桶,然后每个桶按预设比例采样。我常用的比例是低分段取 10%、中低段取 20%、中段取 40%、中高段取 20%、高段取 10%。这样既保证临界区样本的密度,又留出了基础样本和高难样本的覆盖。

如果候选数据本身有领域或者任务属性,分桶之后还要做一次分层采样,让每个领域在各难度桶里的比例和整体分布保持一致。这一步在实操中对模型最终效果的影响非常大。

3.3 一个 30 行左右的筛选脚本

我把整个筛选过程写成了一个独立脚本,输入是教师模型在候选集上保存的 logits 和对应的标签,输出是筛选后的索引。以下是核心部分,可以直接照着改:

import numpy as np def softmax(logits, temperature=1.0): logits = logits / temperature exp = np.exp(logits - logits.max(axis=-1, keepdims=True)) return exp / exp.sum(axis=-1, keepdims=True) def compute_rsr_scores(logits, labels, temperature=1.0): probs = softmax(logits, temperature=temperature) n = len(logits) true_conf = np.array([probs[i, labels[i]] for i in range(n)]) top_idx = probs.argsort(axis=-1)[:, ::-1][:, :2] top1_conf = probs[np.arange(n), top_idx[:, 0]] top2_conf = probs[np.arange(n), top_idx[:, 1]] sharpness = top1_conf - top2_conf consistency = np.array( [1.0 if top_idx[i, 0] == labels[i] else 0.0 for i in range(n)] ) # 用教师自身的分布熵作为校准偏移的近似估计 entropy = -np.sum(probs * np.log(probs + 1e-9), axis=-1) calib_gap = np.abs(entropy - entropy.mean()) / (entropy.std() + 1e-8) w = [0.25, 0.4, 0.2, 0.15] score = (w[0] * consistency + w[1] * true_conf + w[2] * (1 - sharpness) + w[3] * (1 - calib_gap)) return score def select_by_bucket(scores, ratios, seed=42): rng = np.random.default_rng(seed) order = np.argsort(scores) n = len(scores) bucket_size = n // len(ratios) selected = [] for idx, ratio in enumerate(ratios): bucket = order[idx * bucket_size: (idx + 1) * bucket_size] keep = rng.choice(bucket, size=int(len(bucket) * ratio), replace=False) selected.extend(keep.tolist()) return np.array(selected)

这段代码没有做什么高深的事,它就是把前面定义的三个信号量化,然后按分桶策略采样。实际项目里我会把 logits 预处理成 float16 的 numpy 文件分批读入,避免一次加载几十 G 数据把内存打爆。

4. 把 RSR 塞进真实蒸馏管线:实验配置与效果

4.1 一个 70B 教师带 7B 学生的流程设计

我用于验证 RSR 的蒸馏管线大概是这样的:候选池是一批无标注文本数据,规模在百万级别;教师用一个稠密 70B 模型,学生是 7B;蒸馏目标采用软标签损失和硬标签损失加权组合。

流程分四步走。第一步,教师模型在候选池上做一次前向推理,把所有样本的 logits 按 hash 分片保存到磁盘,这个步骤最贵,但只需要做一次。第二步,用上面的脚本计算 RSR 分数,做分桶采样,得到蒸馏集。第三步,把蒸馏集拆成训练和验证两部分,学生模型训练若干个 epoch。第四步,在任务评估集上做对比。

关键点在于:选数据的过程和训练过程完全解耦。我可以在不重跑教师推理的情况下,反复调整分桶比例和权重,实验效率高很多。

4.2 一次内部对照实验的效果差异

我在一次内部模拟实验里跑了几组对照,筛选比例统一控制在 25% 左右,学生训练步数和超参保持一样,最后看学生在几个任务集合上的平均表现。因为只是内部验证,数值不具备普遍性,但趋势非常明显:

筛选策略数据占比学生平均表现训练稳定性
随机采样25%基准较稳定
只看高置信 top 25%25%略低于基准后期波动明显
只选困难样本 25%25%波动大不稳定
只用中间难度 25%25%略高于基准稳定
RSR 分桶采样 25%25%最高稳定

只看高置信 top 那组,初期学生 loss 降得很快,但后面在下游评测上反而没占到便宜。只选困难样本那组更明显,训练途中出现过一次较大的指标抖动。RSR 分桶那组整体是全场最稳的,它在保留高价值临界样本的同时,没有把基础样本丢光。

4.3 计算成本怎么控制

RSR 本身不增加多少推理成本,真正的成本大头是教师前向推理。规模一旦到百万级,70B 模型全量跑一轮是很贵的。

我有三个控制成本的经验。第一,logits 缓存一定要做,不要每次换筛选策略就重跑一遍教师。第二,先在候选池里随机抽 5% 到 10% 的样本,把教师跑完,估算 RSR 分数的分布和分位点,然后再决定全量筛选的参数。第三,如果候选池特别大,可以先让一个小一点的教师模型做粗筛,把明显不合适的低质量样本过滤掉,再用大教师在剩余样本上精算 RSR。这个两级筛选策略能省下接近一半的推理开销。

5. 复现路上我踩过的坑与修正

5.1 同一个温度处理所有样本是最大的误区

知识蒸馏基本都要用温度系数软化教师分布,但 RSR 的落地过程中,我踩得最狠的一个坑就是统一用固定温度处理所有样本。

不同样本的 logits 尺度差异很大,有的样本教师输出本身就比较平缓,用 T=1 效果尚可;有的样本 logits 差距巨大,T=1 出来的分布还是接近 one-hot,sharpness 依然很高。如果你在计算 RSR 时用的是 T=1,蒸馏训练时又用 T=3,那筛选信号和实际训练信号就对不上了。

我的修正做法是:计算 RSR 之前先统计一批样本 logits 的标准差,设定一个能让 softmax 后熵不至于过低的温度区间,再在 1.0 到 3.0 之间按样本的 sharpness 做自适应调整。简单说就是 sharpness 高的样本用更高的温度摊平,sharpness 低的样本少摊平一点。

5.2 重复采样带来的过拟合

分桶采样本身带有随机性,某些 RSR 分数特别高的样本会在多个桶的边缘反复出现,加上权重采样策略的话,就可能被选中多次。刚开始我没当回事,结果学生训练两三个 epoch 之后明显开始“背”这些重复样本,体现在 loss 曲线上就是反复震荡,验证集表现停滞。

解决方式是在 sampler 里加一个限制:每个样本在一个 epoch 中最多出现两次,并且第二次出现时软标签损失权重减半。这个限制加入之后,重复样本的负面影响很快消失了。

5.3 标签不全的时候怎么维持一致性

我上面提到 consistency 信号需要知道真实标签,但工业生产中很多无标注数据根本没有标签。这时候有两个替代方案。

方案一是用教师自身的自洽性替代:对同一个样本,用温度和输入扰动各跑一次,看教师 argmax 是否稳定。如果教师在扰动下都做同一个预测,那就近似认为它对这个预测是一致的。方案二是用多个教师投票,取两个教师的预测一致性作为 soft consistency 信号。在我自己的场景里,投票方案的效果明显优于单一教师自洽性,因为两个模型在同一处犯同样错误的概率要低很多。

6. 选数据这件事,最后还是要看下游任务说话

6.1 抽一万条先做小实验,别一上来就全量

RSR 里的权重、分桶比例、采样比例,都不是一个固定答案。不同任务的数据分布不同,教师模型的特性也不同,照着别人给的参数直接套,大概率翻车。

我的习惯是:在正式全量筛选前,先从候选池里抽一万条数据,跑一个几百步的小型蒸馏实验,快速对比随机采样、纯高置信采样、困难样本采样、RSR 分桶采样这几种策略在小验证集上的曲线。这个实验整个耗时一个小时以内,但能帮你省掉后面全量训练浪费的几天时间。指标上重点看两个东西:训到后半段验证 loss 是否还在继续下降,以及最终下游评测分数的方差大小。

6.2 RSR 的适用边界,我也摸了一下

我目前主要在分类、抽取、短文本生成这类任务上验证 RSR,效果不错。但在开放式长文本生成场景里,情况要复杂得多,因为这类任务非常依赖输出的多样性和长尾覆盖。RSR 的分桶采样虽然保留了低分段样本,但这种保留比例如果调得不够宽松,依然会把一些“冷门但独特”的表达方式挤掉。

我的处理方式是:在长文本生成任务里降低 RSR 分数中 consistency 和 true_confidence 的权重,同时把低分段样本的保留比例从 10% 提到 30%。本质上就是让筛选的触手短一点,不要过度干预多样性。你可以在自己的任务上试几组权重,找找那个“边界值”。这个平衡点没有标准答案,只能靠实验去摸。

6.3 最后分享一个我自己在用的工程小技巧

所有 RSR 相关的中间结果,我都会按“数据版本 + 教师版本 + 权重配置 + RNG seed”打个全量 hash 存档。这个习惯救过我很多次。因为筛选过程本身就是带随机性的,如果没有固定版本,后续要复现某个训练结果时会非常痛苦。

另外,保存教师 logits 的时候,推荐直接存成 float16 的.npy分片,不要存 pickle,也不要存成文本,不然吃满磁盘后你连删都不知道从哪删起。每次筛选前把 RSR 分数的直方图打出来看一眼,如果某个桶里样本数量明显异常,大概率是数据分布变了或者教师推理环节出了问题。选数据说到底是一个和数据打交道的工程活,保持可追溯、可复现,比追求一时的“最优筛选策略”更重要。

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

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

立即咨询