4.1 先学后教与边学边教:离线与在线蒸馏 本节摘要:离线与在线蒸馏是教法组织的两大基本形态:教师是预先练成的现成师傅,还是与学生同堂训练的"同学兼师傅"。本节对比两种形态的损失结构、资源开销与适用前提,给出深度互学习的最小实现——它是在线蒸馏最有代表性的门派,也是"没有教师可用"时的解法之一。 第四章开篇。前三章的蒸馏都默认教师现成:先有老师傅,后有学生。本节松开这个假设,回答课堂组织的第一个问题——教学在时间上怎么排。 两种课堂组织 离线蒸馏:教师预先训练完毕、参数冻结,学生训练全程只做一次教师前向。软目标可以在线打分(教师陪跑)也可以离线打分(预存输出,见 2.1),但无论哪种,教师自身都不再更新。它的优势是训练稳定、可并行、教师可以反复复用;
本节摘要:离线与在线蒸馏是教法组织的两大基本形态:教师是预先练成的现成师傅,还是与学生同堂训练的"同学兼师傅"。本节对比两种形态的损失结构、资源开销与适用前提,给出深度互学习的最小实现——它是在线蒸馏最有代表性的门派,也是"没有教师可用"时的解法之一。
第四章开篇。前三章的蒸馏都默认教师现成:先有老师傅,后有学生。本节松开这个假设,回答课堂组织的第一个问题——教学在时间上怎么排。
离线蒸馏:教师预先训练完毕、参数冻结,学生训练全程只做一次教师前向。软目标可以在线打分(教师陪跑)也可以离线打分(预存输出,见 2.1),但无论哪种,教师自身都不再更新。它的优势是训练稳定、可并行、教师可以反复复用;隐含的上限是——学生的天花板被这位固定的师傅封住。
在线蒸馏:没有现成师傅。训练现场同时有多个模型在学,它们互为师生、共同进步。最经典的形态是深度互学习:两个结构不同的网络从随机初始化同时开训,各自的总损失里都含一项"向对方当前输出学习"的 KL。谁在某一步更强,谁就暂时当师傅;互相拉扯着,两个学生的终局往往都好于各自单练。
import torch import torch.nn.functional as F def mutual_learning_step(net_a, net_b, x, y, T=4.0, beta=0.5): """深度互学习的一步:两个网络互为师生。 beta 是向同伴学习的权重,其余是常规硬标签损失。""" la, lb = net_a(x), net_b(x) loss_a = F.cross_entropy(la, y) loss_b = F.cross_entropy(lb, y) # 各自向"对方当前的软化输出"学习;detach 切断对侧的梯度 peer_a = F.kl_div( F.log_softmax(la / T, dim=-1), F.softmax(lb.detach() / T, dim=-1), reduction="batchmean") * T * T peer_b = F.kl_div( F.log_softmax(lb / T, dim=-1), F.softmax(la.detach() / T, dim=-1), reduction="batchmean") * T * T total_a = (1 - beta) * loss_a + beta * peer_a total_b = (1 - beta) * loss_b + beta * peer_b total_a.backward() total_b.backward() return total_a.item(), total_b.item() # 输出说明:返回两个网络的损失标量。 # 关键在 detach():a 学 b 的当前输出,但这个学习行为不能 # 反过来把 b 往"迁就 a"的方向拽——否则两个网络互相躺平。
在线形态讲完,离线形态还有一个工程上很值钱的变体:预打分。教师对整个训练集的软化输出只需算一次、存成缓存,之后学生训练的每个轮次都直接读缓存——教师前向从"每步都跑"降到"全程一次",多轮实验时训练开销几乎与普通训练持平。
# 离线蒸馏的预打分:教师输出算一次,缓存后反复复用 import torch import torch.nn.functional as F @torch.no_grad() def precompute_teacher_logits(teacher, loader, path_stub): teacher.eval() saved = 0 for x, _y in loader: logits = teacher(x) # 只存 logits,不存软化分布 torch.save(logits.cpu(), f"{path_stub}_{saved:06d}.pt") saved += 1 return saved # 返回缓存分片数 def distill_from_cache(student, x_batch, y_batch, t_logits, T=4.0, alpha=0.9): s_logits = student(x_batch) # 学生的当前批前向 soft = F.kl_div(F.log_softmax(s_logits / T, dim=-1), F.softmax(t_logits / T, dim=-1), reduction="batchmean") * T * T hard = F.cross_entropy(s_logits, y_batch) return alpha * soft + (1 - alpha) * hard # 总损失:软硬两项配比 # 输出示例:10 万样本的教师预打分在单卡上约 25 分钟; # 之后学生每轮训练读缓存,教师前向开销降为 0。 # 注意两点:一是存 logits 而非 softmax 结果——温度要换时不必重算; # 二是数据增强要在打分前决定,增强策略一变,缓存全部作废。
预打分把离线蒸馏的成本结构改写了:教师越大、实验轮数越多,省得越多。反过来说,它也是离线与在线选择的一个隐性砝码——在线蒸馏没有缓存可用,教师(或同伴)输出每步都得现算。
| 维度 | 离线蒸馏 | 在线蒸馏 |
|---|---|---|
| 教师来源 | 必须有现成强教师 | 不需要,同堂互教 |
| 训练期开销 | 教师可预存输出,增量小 | 所有成员全程在训 |
| 稳定性 | 高,教师输出固定 | 中,早期同伴输出噪声大 |
| 精度上限 | 受固定教师封顶 | 可能超过单教师上限 |
| 工程复杂度 | 低 | 中,需协调多个优化器 |
一句话决策:有强教师就用离线(简单稳定收益高),没有教师但训练预算管够才考虑在线。实践中九成生产蒸馏是离线的,在线蒸馏的价值更多在"从零起家的团队怎么起步"和"多模型互相成就"这两个场景。
背景。 某初创团队做工业缺陷分类,数据私有且规模中等(约 20 万张),没有大模型可用,直接训练的最大网络只到 91.4%,业务需要 93%。
操作。 走在线蒸馏:同堂放两个结构互补的网络——一个宽而浅的卷积网、一个窄而深的残差网,各自硬标签损失加向同伴学习的 KL(beta 0.5,温度 4),同步训练 120 轮。训练中期做了一次调度调整:前 40 轮 beta 从 0.1 线性升到 0.5,让两人在各自站稳之后再开始互相影响。
结果。 两个终局网络分别达到 92.8 与 93.1,均超过单练的 91.4;随后把 93.1 的网络当教师做一轮离线蒸馏,教出部署用的轻量学生 92.6,达标上线。
解读。 这条案例展示了在线蒸馏的标准用法:它不是离线蒸馏的替代,而是"无教师冷启动"的垫脚石——先用互学习把现有数据榨出超出单模型的成绩,再把最强者转任教师走成熟的离线路线。beta 预热的细节也值得记:训练早期同伴输出接近随机猜,直接高权重互学会让两个网络把彼此的噪声当真话。
变式。 同堂成员从两个扩展到三四个(多分支或多结构)是常见变体,收益边际递减、协调成本递增;另一种打法是"自榕式"快照集成——同一个网络在训练路上的多个历史快照互为师生,省掉多模型的显存开销,这条思路在 4.3 的自蒸馏里展开。
⚠️ 常见坑:在线蒸馏去掉 detach。两个网络的梯度互相穿透后,损失会找到"互相迁就"的捷径——双方输出趋同、谁都不少学但谁也没多学,表现为互学损失迅速归零而精度停滞。
💡 关键直觉:离线蒸馏是"拜师",在线蒸馏是"同学互助"。没有师傅时,两个同学互相讲题,往往都能比自己闷头学考得好——但别忘了最后还是要有一个人出师带新人。