老师最痛恨的"抄作业",原来是小模型追上大模型的唯一办法
学生时代有一条铁律:抄作业是不行的。
但你一定也见过这种人——他确实抄了,成绩却真的上去了。 因为他抄的不是答案,是过程:老师这一步为什么这么走,那一步为什么排除了另一种可能。
今天整个 AI 行业最赚钱的一件事,干的就是这个,名字叫知识蒸馏。 DeepSeek-R1-Distill、Gemma、Phi、几乎所有能塞进你手机的小模型, 全都是抄大模型作业抄出来的。
这篇文章不谈"模型压缩"这种黑话,全部用能跑起来的 PyTorch 代码, 把"抄作业"这件事,一路接到知识蒸馏。顺手回答四个灵魂拷问:
- 只抄答案和抄过程,差别到底有多大?可以量化吗?
- 为什么"抄"出来的小模型,有时候比自己老老实实学的还强?
- 蒸馏公式里那个"温度 T"到底在调什么?
- 学生有可能超过老师吗?
0. 一句话主线
如果只能留一句话:
标准答案只告诉你"这题选 A";老师的解题过程还告诉你"B 差一点就对了,C 一眼就该排除"。 后面这句话里的信息量,比前面那句大得多——大模型的价值,一大半在这里。**
训练一个模型,标准做法是给它硬标签(hard label):这张图是猫,标签就是 [猫=1, 狗=0, 狐狸=0]。
但一个训练好的大模型看到这张图,输出的是软标签(soft label):[猫=0.90, 狗=0.08, 狐狸=0.02]。
那个 0.08 是金子。它在说:"这张图有点像狗"—— 这是标准答案里永远不会有、但对学习极其宝贵的信息。Hinton 给它起了个名字:暗知识(dark knowledge)。
记住这个画面:硬标签是答案,软标签是解题过程。下面所有东西都挂在这个差别上。
1. 先把"信息量"量出来:答案 vs 过程
差别到底多大?用信息熵直接算:
import torch
hard = torch.tensor([1.0, 0.0, 0.0]) # 标准答案:就是猫
soft = torch.tensor([0.90, 0.08, 0.02]) # 老师的判断:像猫,有点像狗
def entropy(p):
p = p.clamp_min(1e-9)
return -(p * p.log()).sum().item()
print(f"硬标签 熵 = {entropy(hard):.4f}") # 0.0000 —— 零信息,只有一个确定结论
print(f"软标签 熵 = {entropy(soft):.4f}") # 0.3600 —— 携带了"相似度结构"
硬标签的熵是 0。它是一个句号,一个结论,没有任何解释。
软标签的熵大于 0,因为它同时告诉你三件事: 猫最像、狗次之、狐狸最远。这是一张关于"世界长什么样"的地图,而不是一个点。
一道选择题,标准答案给你 1 个 bit; 老师讲这道题的十分钟,给你的是整个知识网络的形状。
2. 每个样本,都在偷偷教你别的类
最妙的地方在于:软标签让"猫"这个样本,顺便教会了模型"狗"和"狐狸"长什么样。
import torch
classes = ["猫", "狗", "狐狸", "汽车"]
soft = torch.tensor([0.90, 0.07, 0.025, 0.005])
for c, p in zip(classes, soft):
print(f"{c:>3}: {p:.3f} {'█' * int(p * 60)}")
猫: 0.900 ██████████████████████████████████████████████████████
狗: 0.070 ████
狐狸: 0.025 █
汽车: 0.005
模型从这一个样本里学到的不只是"这是猫",还有:
猫和狗的距离 < 猫和狐狸的距离 << 猫和汽车的距离。
硬标签把狗、狐狸、汽车统统压成 0,它们之间的远近关系被彻底抹平了。 软标签保留了这套结构。所以蒸馏的数据效率高得离谱——每个样本的信息密度大了一个数量级。
🤔 疑惑点一:为什么"抄"出来的学生,有时比自己学的还强?
因为老师的软标签自带"这题有多难"的信息,等于给学生做了一次难度分级和噪声过滤——而硬标签把所有题都当成同样确定的题在教。
这是最反直觉的一点。想象训练集里有一张模糊到人类都认不清的图,被标成"猫"。
- 硬标签教学:模型被强迫以 100% 的置信度认定它是猫 → 它只能靠死记这张图的像素来达标 → 过拟合。
- 软标签教学:老师输出
[猫=0.4, 狗=0.35, 狐狸=0.25],等于说"这题很难,别太当真" → 学生温和地学一点点。
import torch
# 一张模棱两可的样本
hard = torch.tensor([1.0, 0.0, 0.0]) # 硬标签:必须 100% 确信
soft = torch.tensor([0.40, 0.35, 0.25]) # 老师:我也不太确定
print(f"硬标签要求的置信度: {hard.max():.0%} -> 逼学生死记")
print(f"软标签要求的置信度: {soft.max():.0%} -> 允许学生存疑")
软标签天然是一种正则化:它把老师见过千万样本后形成的"不确定性判断", 免费传给了学生。学生因此更少过拟合、泛化更好。
一句话:硬标签逼你把错题也背成对的;软标签告诉你哪些题本来就没有干净的答案。
3. 温度 T:把"次优选项"放大给学生看
问题来了:真实大模型的输出往往极其自信,[0.998, 0.001, 0.0008, ...]。
那点暗知识被压得几乎为 0,学生根本看不见。
于是需要一个放大镜——温度。做法是在 softmax 之前把 logits 除以 T:
import torch, torch.nn.functional as F
logits = torch.tensor([8.0, 2.0, 1.0, -3.0]) # 老师原始输出,非常自信
for T in [1.0, 2.0, 4.0, 8.0]:
p = F.softmax(logits / T, dim=0)
print(f"T={T:>3}: {[f'{v:.3f}' for v in p]}")
T=1.0: ['0.997', '0.002', '0.001', '0.000'] <- 暗知识被压没了
T=2.0: ['0.947', '0.047', '0.029', '0.004']
T=4.0: ['0.786', '0.175', '0.136', '0.050'] <- 相对关系清晰可见
T=8.0: ['0.554', '0.261', '0.230', '0.126']
T=1 时,学生只看得到"选 A"; T=4 时,学生清楚看到"B 和 C 也有点道理,D 完全不沾边"。
温度不改变谁大谁小,只改变"差距有多明显"。 它做的事,正是老师在讲台上说的那句: "这道题为什么不选 B?B 其实很接近,只差在这一个条件上。"
(如果你读过本系列讲采样温度那篇:同一个 T,采样时用来调随机性,蒸馏时用来调暗知识的可见度——同一个旋钮,两种用法。)
4. 动手:抄答案 vs 抄过程,跑一遍看差距
下面训练一个"老师",再让两个一模一样的"学生"分别抄它—— A 只抄答案(硬标签),B 抄整个概率分布(软标签)。学生的数据量被故意砍得很少:
import torch, torch.nn as nn, torch.nn.functional as F
torch.manual_seed(0)
D, C = 20, 5
# 造一个有结构的任务(类别之间有真实的远近关系)
W = torch.randn(D, C)
def make(n):
X = torch.randn(n, D)
y = (X @ W).argmax(1)
return X, y
# ---- 老师:数据充足,学得很好 ----
Xt, yt = make(6000)
teacher = nn.Sequential(nn.Linear(D, 128), nn.ReLU(), nn.Linear(128, C))
opt = torch.optim.Adam(teacher.parameters(), lr=1e-2)
for _ in range(1200):
loss = F.cross_entropy(teacher(Xt), yt)
opt.zero_grad(); loss.backward(); opt.step()
# ---- 学生:只有 300 条数据(抄作业的典型处境)----
Xs, ys = make(300)
Xe, ye = make(3000) # 考试集
with torch.no_grad():
t_logits = teacher(Xs) # 老师在这 300 条上的"解题过程"
def student(mode, T=4.0, steps=1500):
net = nn.Sequential(nn.Linear(D, 32), nn.ReLU(), nn.Linear(32, C))
opt = torch.optim.Adam(net.parameters(), lr=1e-2)
for _ in range(steps):
out = net(Xs)
if mode == "hard": # A:只抄答案
loss = F.cross_entropy(out, ys)
else: # B:抄整个分布
loss = F.kl_div(F.log_softmax(out / T, 1),
F.softmax(t_logits / T, 1),
reduction="batchmean") * T * T
opt.zero_grad(); loss.backward(); opt.step()
with torch.no_grad():
return (net(Xe).argmax(1) == ye).float().mean().item()
with torch.no_grad():
print(f"老师 考试准确率: {(teacher(Xe).argmax(1) == ye).float().mean():.1%}")
print(f"A 只抄答案 考试准确率: {student('hard'):.1%}")
print(f"B 抄解题过程 考试准确率: {student('soft'):.1%}")
跑完你会看到 B 明显高于 A,而且用的是同样的 300 条数据、同样的网络、同样的步数。
唯一的区别是:A 抄的是句号,B 抄的是整段推导。
代码里那个 T * T 别忽略——除以 T 会让梯度缩小 T² 倍,
乘回去只是为了让学习率保持可比,是个工程补丁,不是理论的一部分。
🤔 疑惑点二:学生有可能超过老师吗?
在原任务上一般不会,但在"单位成本下的表现"上经常远超——而这才是实际有意义的比较。
蒸馏的目标从来不是造一个更强的模型,是造一个便宜到能用的模型:
| 老师 | 学生 | |
|---|---|---|
| 参数量 | 671B | 7B |
| 能不能跑在你手机上 | 不能 | 能 |
| 单次推理成本 | 1× | ~0.01× |
| 原任务准确率 | 100% | 90~97% |
| 每块钱买到的准确率 | 1× | ~50× |
DeepSeek-R1-Distill 系列就是最直白的例子:用 R1 生成的推理过程,去喂 Qwen/Llama 的小模型, 让 7B 的模型学会写长草稿。它当然打不过 R1 本身,但它能跑在一张消费级显卡上。
至于"超过老师",有三种情况确实会发生:
- 老师会犯随机错误,学生学到的是老师的"平均倾向"——噪声被抹平了;
- 多个老师蒸馏给一个学生,学生集成了所有人的判断;
- 学生在老师的输出上再做强化学习,就有机会真正越过老师——这正是当前推理模型迭代的主循环。
所以那句"抄作业没出息",在这里是错的。 抄到解题过程、再自己做一遍并改进,就是青出于蓝。
仓库里的动画脚本把这件事画了出来:
python distillation_visualization.py
左边是硬标签:一根孤零零的柱子,其余全是零,什么结构都没有; 右边是软标签随温度升高逐渐"打开",次优选项一个个浮出水面。 最后一幅图把"训练数据量 vs 准确率"画成两条曲线—— 在数据很少的左半边,抄过程那条线把抄答案那条甩开一大截。
缝合:把所有画面接起来
| 学习概念 | 学生时代怎么讲 | 这篇文章怎么看 | 在大模型里是什么 |
|---|---|---|---|
| 标准答案 | 对照订正 | 熵为 0,零解释(第 1 节) | 硬标签 / 交叉熵 |
| 解题过程 | 老师讲评 | 携带类别间的远近结构(第 2 节) | 软标签 / 暗知识 |
| "为什么不选 B" | 排除法讲解 | 把次优选项放大(第 3 节) | 温度 T |
| 这题本来就难 | 老师说别纠结 | 不确定性即正则化(疑惑点一) | 蒸馏抗过拟合 |
| 抄完自己再做一遍 | 唯一有效的抄法 | 学生可越过老师(疑惑点二) | 蒸馏 + RL |
三句话总结:
硬标签是句号,软标签是推导——后者的信息密度高一个数量级(第 1、2 节); 温度 T 是放大镜,把被压扁的暗知识重新显影给学生看(第 3 节); 于是小模型能用极少的数据追上大模型,这就是你手机里那个模型的来历(第 4 节、疑惑点二)。
所以老师那句"不许抄作业",其实少说了半句。 完整的版本是:不许抄答案——但你要是能把整个解题过程抄进脑子里,那叫学习。
今天几百亿美金的 AI 行业,就建立在这半句话上。
备注(选题/标题): 本篇钩子是"被禁止的行为其实是正解",反转感比纯知识科普更强。 备选标题: 1.《你手机里的那个 AI,是抄作业抄出来的》 2.《为什么抄答案没用、抄过程有用?这个问题价值几百亿美金》