跳到主要内容

LLM 蒸馏

一个好老师不只给标准答案,还会解释思路、在学生犯错时及时纠正。大模型和小模型之间的关系也类似——与其让小模型自己摸索,不如让大模型「教」它。

这一节理解蒸馏的三种方法:Logit-based(学输出分布)、Feature-based(学中间表示)、On-Policy(大模型实时批改),并走通从大模型蒸馏到小模型的完整流程。

在 LLM 上下文中,Teacher 是能力更强的模型(可以是闭源 API、大开源模型或同模型的增强版本),Student 是目标部署模型。传统做法是让 Teacher 写出标准答案,Student 照着学习。

但 Student 在背熟 Teacher 的输出之后,一旦独立生成,质量就会下降。原因在于 Student 的训练数据和它自己生成时的数据分布不同,这个差异称为分布偏移(distribution shift)。

最先出现也最经典的方法是 Logit-based 蒸馏——让 Student 不只学答案,还学 Teacher 对每个词的概率判断。

import numpy as np

np.random.seed(42)

1. 蒸馏的本质

普通 SFT(监督微调):
Teacher 输出: "巴黎"
Student 学习: 输入"法国首都是?" → 输出"巴黎"
问题: 只学了答案,没学推理过程

蒸馏:
Teacher 输出: 每个词的概率分布 [巴黎:0.9, 伦敦:0.05, 柏林:0.03, ...]
Student 学习: 不仅输出"巴黎",还要让整个概率分布接近 Teacher
好处: Student 学到了 Teacher 的「判断力」——知道"巴黎"最可能,"伦敦"也有可能但概率低

为什么概率分布比答案更有价值?

Teacher 说「巴黎 90%,伦敦 5%,柏林 3%」比只说「巴黎」多了两条信息:

  1. 伦敦和柏林也是合理的(只是不太对)——这叫「暗知识」
  2. 其他几百个城市概率接近 0——明确告诉 Student 哪些是错的

这就是 Hinton 等人在知识蒸馏中强调的“软目标/暗知识”直觉。参考:Distilling the Knowledge in a Neural Network

import numpy as np

# 交互演示:硬标签 vs 软标签,用真实概率分布对比
print("=== 硬标签 vs 软标签 ===")
print()

cities = ["巴黎", "伦敦", "柏林", "罗马", "马德里", "东京", "北京", "悉尼"]
teacher_logits = np.array([5.0, 2.0, 1.0, 0.5, 0.1, -3.0, -4.0, -5.0])

# 硬标签 (SFT): one-hot
hard_labels = np.zeros(len(cities))
hard_labels[0] = 1.0

print("问题: 法国的首都是?")
print()
print("硬标签 (SFT):")
for city, prob in zip(cities[:5], hard_labels[:5]):
bar = "█" * int(prob * 40)
print(f" {city}: {prob:.1%} {bar}")
print(" → Student 只知道「巴黎是对的」")
print()

# 软标签 (蒸馏): 概率分布
temperature = 3.0
scaled_logits = teacher_logits / temperature
soft_labels = np.exp(scaled_logits) / np.exp(scaled_logits).sum()

print("软标签 (蒸馏, T=3):")
for city, prob in zip(cities, soft_labels):
bar = "█" * int(prob * 40)
print(f" {city}: {prob:.1%} {bar}")
print(" → Student 学到了:")
print(" 1. 巴黎最对")
print(" 2. 伦敦、柏林也是欧洲首都(相似性知识)")
print(" 3. 东京、北京概率≈0(完全不相关)")
print()

# 量化信息量差异
hard_entropy = -np.sum(hard_labels * np.log(hard_labels + 1e-10))
soft_entropy = -np.sum(soft_labels * np.log(soft_labels + 1e-10))
print(f"硬标签信息熵: {hard_entropy:.2f} bits")
print(f"软标签信息熵: {soft_entropy:.2f} bits")
print(f"→ 软标签包含约 {soft_entropy:.1f} bits 信息,远多于硬标签的 {hard_entropy:.2f} bits!")
=== 硬标签 vs 软标签 ===

问题: 法国的首都是?

硬标签 (SFT):
巴黎: 100.0% ████████████████████████████████████████
伦敦: 0.0%
柏林: 0.0%
罗马: 0.0%
马德里: 0.0%
→ Student 只知道「巴黎是对的」

软标签 (蒸馏, T=3):
巴黎: 45.4% ██████████████████
伦敦: 16.7% ██████
柏林: 12.0% ████
罗马: 10.1% ████
马德里: 8.9% ███
东京: 3.2% █
北京: 2.3%
悉尼: 1.6%
→ Student 学到了:
1. 巴黎最对
2. 伦敦、柏林也是欧洲首都(相似性知识)
3. 东京、北京概率≈0(完全不相关)

硬标签信息熵: -0.00 bits
软标签信息熵: 1.62 bits
→ 软标签包含约 1.6 bits 信息,远多于硬标签的 -0.00 bits!

2. 方法一:Logit 蒸馏(最经典)

让 Student 的输出概率分布逼近 Teacher 的输出概率分布。

Loss 公式

其中:

  • :Student 和正确答案的交叉熵(保证基本正确)
  • :Student 和 Teacher 概率分布的 KL 散度(学习暗知识)
  • :温度参数,越大 Teacher 的分布越「软」(暗知识越明显)
  • :平衡两个 loss 的权重

温度 T 的作用

T=1:  [0.90, 0.05, 0.03, 0.02]  ← 很尖锐,暗知识不明显
T=5: [0.40, 0.25, 0.20, 0.15] ← 软化了,暗知识浮现
T=20: [0.28, 0.26, 0.24, 0.22] ← 太软了,变成均匀分布

T 太大 → 所有词概率差不多 → 没信息量 T 太小 → 和硬标签没区别 → 没暗知识 T=3~10 是常见实验起点,不是固定规则;不同任务、模型和 loss 权重要通过验证集调整。

import numpy as np

# 演示温度对概率分布的影响
print("=== 温度 T 对软标签的影响 ===")
print()

logits = np.array([5.0, 2.0, 1.0, 0.5, 0.1, 0.01, 0.001, 0.0001])
labels = ["巴黎", "伦敦", "柏林", "罗马", "马德里", "维也纳", "布拉格", "华沙"]

for T in [1, 3, 10, 20]:
scaled = logits / T
probs = np.exp(scaled) / np.exp(scaled).sum()

print(f"T={T:2d}: ", end="")
for i in range(5):
bar = "█" * int(probs[i] * 50)
print(f"{labels[i]}:{probs[i]:.3f} {bar} ", end="")
print()

print()
print("T=1: 几乎只有巴黎 → 暗知识被掩盖")
print("T=3: 伦敦、柏林也有一定概率 → 暗知识浮现")
print("T=10: 分布更均匀 → 暗知识丰富但信号变弱")
print("T=20: 几乎均匀 → 信息量太少")
=== 温度 T 对软标签的影响 ===

T= 1: 巴黎:0.903 █████████████████████████████████████████████ 伦敦:0.045 ██ 柏林:0.017 罗马:0.010 马德里:0.007
T= 3: 巴黎:0.382 ███████████████████ 伦敦:0.141 ███████ 柏林:0.101 █████ 罗马:0.085 ████ 马德里:0.075 ███
T=10: 巴黎:0.182 █████████ 伦敦:0.135 ██████ 柏林:0.122 ██████ 罗马:0.116 █████ 马德里:0.112 █████
T=20: 巴黎:0.152 ███████ 伦敦:0.130 ██████ 柏林:0.124 ██████ 罗马:0.121 ██████ 马德里:0.119 █████

T=1: 几乎只有巴黎 → 暗知识被掩盖
T=3: 伦敦、柏林也有一定概率 → 暗知识浮现
T=10: 分布更均匀 → 暗知识丰富但信号变弱
T=20: 几乎均匀 → 信息量太少

3. 方法二:数据蒸馏(最容易落地)

直接做 full-vocab logit 蒸馏时,Student 和 Teacher 最好共享 tokenizer 或能建立可靠的跨 tokenizer 对齐;闭源 API 通常拿不到完整 logits,所以 LLM 场景里更常见的是数据蒸馏或只用 top-k / sampled-token 近似信号。

数据蒸馏绕过了这个问题:让 Teacher 生成训练数据,Student 在这些数据上做 SFT。

Step 1: 收集 prompts(从你的业务场景中)
["写一首关于春天的诗", "解释量子力学", "翻译: Hello → 中文", ...]

Step 2: Teacher 为每个 prompt 生成高质量回答
GPT-4: "春天来了,万物复苏..."
GPT-4: "量子力学是研究微观粒子..."

Step 3: 用 (prompt, teacher_answer) 对训练 Student
Student 做标准 SFT,学习模仿 Teacher 的输出风格和质量

优点:不要求词表相同,任何 Teacher 可以教任何 Student。 缺点:只学到了「答案长什么样」,没学到「概率分布中的暗知识」。

数据蒸馏的进阶技巧

  • 多轮对话蒸馏:Teacher 生成多轮对话,Student 学会对话节奏
  • CoT 蒸馏:Teacher 生成带推理过程的答案,Student 学会推理
  • 拒绝采样:Teacher 生成多个答案,只保留最好的给 Student 学
# 模拟数据蒸馏流程
print("=== 数据蒸馏流程模拟 ===")
print()

prompts = [
"解释什么是机器学习",
"写一首关于秋天的五言诗",
"Python 中 list 和 tuple 的区别",
]

# 模拟 Teacher (GPT-4) 生成
teacher_responses = [
"机器学习是人工智能的一个分支,让计算机从数据中学习模式,而不需要显式编程。",
"秋风扫落叶,霜降百花残。独坐寒窗下,思君衣可单。",
"list 是可变的(可以增删改),tuple 是不可变的(创建后不能修改)。list 用 [],tuple 用 ()。",
]

print("生成训练数据:")
for i, (prompt, response) in enumerate(zip(prompts, teacher_responses)):
print(f"\n--- 样本 {i+1} ---")
print(f"User: {prompt}")
print(f"Assistant: {response}")

print()
print(f"共生成 {len(prompts)} 条训练数据")
print("Student 在这些数据上做 SFT,学习模仿 Teacher 的风格。")
print()
print("实际项目需要多少数据取决于任务宽度、teacher 质量和 student 基座能力;几千条可以跑通概念,几万到更多样本才更可能形成稳定效果。")
=== 数据蒸馏流程模拟 ===

生成训练数据:

--- 样本 1 ---
User: 解释什么是机器学习
Assistant: 机器学习是人工智能的一个分支,让计算机从数据中学习模式,而不需要显式编程。

--- 样本 2 ---
User: 写一首关于秋天的五言诗
Assistant: 秋风扫落叶,霜降百花残。独坐寒窗下,思君衣可单。

--- 样本 3 ---
User: Python 中 list 和 tuple 的区别
Assistant: list 是可变的(可以增删改),tuple 是不可变的(创建后不能修改)。list 用 [],tuple 用 ()。

共生成 3 条训练数据
Student 在这些数据上做 SFT,学习模仿 Teacher 的风格。

实际项目需要多少数据取决于任务宽度、teacher 质量和 student 基座能力;几千条可以跑通概念,几万到更多样本才更可能形成稳定效果。

4. 方法三:特征蒸馏(进阶)

不仅学输出分布,还学中间层的表示。

Teacher (GPT-4, 96层):
Layer 1 → Layer 2 → ... → Layer 48 → ... → Layer 96 → Output

Student (7B, 32层): | 让 Student 第 16 层的输出
Layer 1 → Layer 2 → ... → Layer 16 → ... → Layer 32 → Output 逼近 Teacher 第 48 层

为什么有效? 中间层包含了「怎么理解这句话」的信息,比最终输出更丰富。

为什么少用?

  • 需要访问 Teacher 的内部表示(闭源模型不行)
  • Teacher 和 Student 的维度不同,需要投影矩阵对齐
  • 计算量大,显存消耗高

在闭源 teacher + 开源 student 的 LLM 场景里,数据蒸馏最容易落地;特征蒸馏更多用于能访问 teacher hidden states 的白盒设置,视觉模型和小型语言模型中都能看到类似思路。

5. 实战:蒸馏 7B 模型

前面讲了三种蒸馏方法的原理,现在把它们串成一次完整的蒸馏流程。整个过程分为四步:

  1. 准备训练数据:收集一批高质量的 prompt,覆盖目标领域——数学推理、代码生成、或者通用对话
  2. Teacher 生成:用强 teacher 模型为每个 prompt 生成回答;具体模型和价格会随时间变化,保存为 (prompt, teacher_answer) 对
  3. Student 训练:如果能拿到 teacher logits,可用 KL 做 logit 蒸馏;如果只能拿到文本回答,就用 SFT 做数据蒸馏
  4. 评估对比:用评测 benchmark 对比 Student 在蒸馏前后的分数变化

下面每一步都给出可执行的代码。即使没有真正的 GPT-4 API key,也可以用本地的 MiniGPT 来模拟 Teacher 和 Student 的角色,完整跑通流程。

print("=== 实战:GPT-4 → 7B 蒸馏流程 ===")
print()

steps = [
("Step 1: 选基座模型", [
"推荐: Qwen2.5-7B / Llama-3-8B / Mistral-7B",
"要求: 基座模型本身有一定能力(不能太差)",
"选 Instruct 版本(已经会遵循指令)",
]),
("Step 2: 收集 prompts", [
"来源 1: 你的业务数据(用户真实问题)",
"来源 2: 开源数据集(OpenHermes, ShareGPT, WildChat)",
"来源 3: 自建——用另一个 LLM 生成多样化 prompt",
"数量: 几千条可跑通概念;生产效果通常需要更多高质量、多样化样本,并通过验证集决定是否继续扩充",
]),
("Step 3: Teacher 生成回答", [
"用 GPT-4 API 为每个 prompt 生成回答",
"system prompt: '你是一个有帮助的助手,请详细、准确地回答。'",
"temperature=0.7(保留一定多样性)",
"成本估算要按当日 API 价格、输入/输出 token、重试率和过滤率计算;这里只能作为估算方法,不写固定美元数",
]),
("Step 4: 数据清洗", [
"去掉太短的回答(<20 token)",
"去掉包含 '作为 AI' 等拒绝回答的",
"去掉格式错乱的",
"去重(相似度 > 0.9 的只保留一条)",
]),
("Step 5: SFT 训练", [
"工具: LLaMA-Factory / Axolotl / Firefly",
"格式: ChatML 或 ShareGPT 格式",
"超参: lr=2e-5, epochs=3, batch_size=128",
"硬件/时间: 取决于模型大小、LoRA/全参、序列长度、batch、优化器和并行策略;不要把某个 GPU 数量当成固定要求",
]),
("Step 6: 评测", [
"用 lm-eval 跑标准 benchmark(见支线 19)",
"人工评估 100 条业务数据",
"对比 Student 和 Teacher 的差距",
]),
]

for title, details in steps:
print(title)
for d in details:
print(f" {d}")
print()
=== 实战:GPT-4 → 7B 蒸馏流程 ===

Step 1: 选基座模型
推荐: Qwen2.5-7B / Llama-3-8B / Mistral-7B
要求: 基座模型本身有一定能力(不能太差)
选 Instruct 版本(已经会遵循指令)

Step 2: 收集 prompts
来源 1: 你的业务数据(用户真实问题)
来源 2: 开源数据集(OpenHermes, ShareGPT, WildChat)
来源 3: 自建——用另一个 LLM 生成多样化 prompt
数量: 几千条可跑通概念;生产效果通常需要更多高质量、多样化样本,并通过验证集决定是否继续扩充

Step 3: Teacher 生成回答
用 GPT-4 API 为每个 prompt 生成回答
system prompt: '你是一个有帮助的助手,请详细、准确地回答。'
temperature=0.7(保留一定多样性)
成本估算要按当日 API 价格、输入/输出 token、重试率和过滤率计算;这里只展示估算方法,不写固定美元数

Step 4: 数据清洗
去掉太短的回答(<20 token)
去掉包含 '作为 AI' 等拒绝回答的
去掉格式错乱的
去重(相似度 > 0.9 的只保留一条)

Step 5: SFT 训练
工具: LLaMA-Factory / Axolotl / Firefly
格式: ChatML 或 ShareGPT 格式
超参: lr=2e-5, epochs=3, batch_size=128
硬件/时间: 取决于模型大小、LoRA/全参、序列长度、batch、优化器和并行策略;不要把某个 GPU 数量当成固定要求

Step 6: 评测
用 lm-eval 跑标准 benchmark(见支线 19)
人工评估 100 条业务数据
对比 Student 和 Teacher 的差距

6. 蒸馏与 OPD 对比

维度数据蒸馏OPD(On-Policy Distillation)
Teacher 参与时机训练前(生成数据)训练中(实时打分)
Student 训练数据Teacher 写的标准答案Student 自己写的答案
工程复杂度低(就是 SFT)高(需要 rollout + 实时 Teacher)
Exposure Bias可能存在(训练 prefix 多来自 teacher/数据集)会缓解(在 student 当前 prefix 上训练),但不保证完全消除
词表要求文本级数据蒸馏无要求logit/token 级 OPD 需要同 tokenizer 或跨 tokenizer 对齐;黑盒反馈可绕开部分限制
成本低(Teacher 只跑一次)高(Teacher 每次训练迭代都跑)
适用场景快速原型、预算有限追求极致性能、有工程团队

建议:先用数据蒸馏快速出一个版本,效果好就上线;如果效果不够,再考虑 OPD。

# 蒸馏效果模拟
print("=== 蒸馏效果对比(模拟) ===")
print()

print("假设 Teacher 是 GPT-4,在 MMLU 上得分 86.4")
print()

models = [
("GPT-4 (Teacher)", 86.4, "基线"),
("7B 基座 (无蒸馏)", 64.2, "原始能力"),
("7B + 数据蒸馏", 72.8, "+8.6 分"),
("7B + 数据蒸馏 + CoT", 75.1, "+10.9 分"),
("7B + OPD", 77.3, "+13.1 分(示例:更强但训练更复杂)"),
]

print(f"{'模型':<25s} {'MMLU':>8s} {'提升':>8s}")
print("-" * 45)
for name, score, note in models:
improvement = score - 64.2
print(f"{name:<25s} {score:>6.1f} {improvement:>+6.1f} ({note})")

print()
print("结论:这只是示例曲线。数据蒸馏通常最容易落地;OPD 可能带来更强效果,但训练复杂度、teacher 成本和稳定性风险更高。")
print("实际项目常从数据蒸馏开始;是否加入 CoT、OPD 或 RL,要看任务是否需要推理、teacher 是否可靠、评测是否能验证。")
=== 蒸馏效果对比(模拟) ===

假设 Teacher 是 GPT-4,在 MMLU 上得分 86.4

模型 MMLU 提升
---------------------------------------------
GPT-4 (Teacher) 86.4 +22.2 (基线)
7B 基座 (无蒸馏) 64.2 +0.0 (原始能力)
7B + 数据蒸馏 72.8 +8.6 (+8.6 分)
7B + 数据蒸馏 + CoT 75.1 +10.9 (+10.9 分)
7B + OPD 77.3 +13.1 (+13.1 分(但成本高 10 倍))

结论:这是示例曲线。数据蒸馏通常最容易落地;OPD 可能效果更强,但成本、复杂度和稳定性风险也更高。
实际项目中,数据蒸馏 + CoT 蒸馏是最常用的组合。

7. 蒸馏的常见问题

问题答案
Student 能超过 Teacher 吗?不应把 Teacher 当成绝对上限。Student 可能因为更适合某个任务、更干净的数据、更强解码/工具或额外训练,在局部 benchmark 上超过某个 teacher;但通常难以全面复制 teacher 的能力。
蒸馏会丢失什么?创造性、长尾知识、复杂推理——这些是 Teacher 的「暗知识」中最难蒸馏的部分
需要多少数据?没有固定最少条数。先用小规模验证数据质量和训练链路,再按验证集收益扩充;数据质量、覆盖面和去重往往比单纯数量更关键。
Teacher 和 Student 词表不同怎么办?用数据蒸馏(方法二),不要求词表相同
蒸馏后还需要 RLHF 吗?看场景。蒸馏会继承一部分 teacher 的风格和安全行为,但不能保证完整对齐;仍需要安全评测、拒答边界和业务数据验证。
多 Teacher 蒸馏可行吗?可以。多 teacher 可以按任务取长补短,但需要处理风格冲突、许可证/数据政策和质量过滤;“综合能力更强”必须用评测验证。

小结

  1. 蒸馏本质:学 Teacher 的概率分布(暗知识),而不只是标准答案
  2. Logit 蒸馏:让 Student 的输出分布逼近 Teacher,需要同词表
  3. 数据蒸馏:Teacher 生成训练数据,Student 做 SFT,最容易落地
  4. 特征蒸馏:学中间层表示,适合白盒 teacher;效果取决于层对齐和任务
  5. 实战流程:选基座 → 收集 prompt → Teacher 生成 → 清洗 → SFT → 评测
  6. 蒸馏 vs OPD:数据蒸馏容易落地,OPD 可能更贴近 student 真实生成分布但成本和复杂度更高
  7. CoT 蒸馏:让 Teacher 生成带推理过程的答案,Student 学会推理

一句话总结:蒸馏 = 让大模型当老师,小模型当学生。 最容易落地的方法是数据蒸馏:让强 teacher 生成高质量样本,小模型通过 SFT 学习;样本量、teacher 选择和成本都要按项目重新估算。

作业> 可以让 AI 帮忙解释思路,但不建议直接让 AI "做完这道题"。

作业 1:温度对软标签的影响知识蒸馏中,温度 用于软化 Teacher 的输出分布:。给定 Teacher 对三个词的 logits:。分别计算 时的概率分布。小提示: 时分布尖锐(概率集中在最大值), 时分布平坦(暗知识更明显)。

# 作业 1:温度对软标签的影响import mathlogits = [4.0, 2.0, 1.0]def softmax_with_temp(logits, T):    exps = [math.exp(l / T) for l in logits]    total = sum(exps)    return [e / total for e in exps]# TODO: 计算 T=1 时的概率分布probs_T1 = None  # 在这里计算# TODO: 计算 T=5 时的概率分布probs_T5 = None  # 在这里计算assert probs_T1 is not None, "请先计算 T=1 的概率"assert probs_T5 is not None, "请先计算 T=5 的概率"expected_T1 = softmax_with_temp(logits, 1)expected_T5 = softmax_with_temp(logits, 5)for i, (p, e) in enumerate(zip(probs_T1, expected_T1)):    assert abs(p - e) < 0.01, f"T=1 时 p[{i}] 应为 {e:.4f}"for i, (p, e) in enumerate(zip(probs_T5, expected_T5)):    assert abs(p - e) < 0.01, f"T=5 时 p[{i}] 应为 {e:.4f}"dk_T1 = probs_T1[1] / probs_T1[2]dk_T5 = probs_T5[1] / probs_T5[2]print(f"✅ 作业 1 通过:")print(f"   T=1: {probs_T1} → 概率集中在最大值")print(f"   T=5: {probs_T5} → 概率更均匀,暗知识更明显")print(f"   暗知识比值(次大/最小): T=1={dk_T1:.2f}, T=5={dk_T5:.2f}")print("   温度越高,非正确答案的概率差异越清晰——这就是暗知识。")

作业 2:KL 散度手算KL 散度衡量两个分布的差异:。假设 Teacher 分布 ,Student 分布 。手动计算 KL 散度。小提示:

# 作业 2:KL 散度手算import mathP = [0.7, 0.2, 0.1]Q = [0.5, 0.3, 0.2]# TODO: 计算 KL 散度 D_KL(P || Q)kl_div = None  # 在这里计算assert kl_div is not None, "请先计算 KL 散度"expected = sum(p * math.log(p / q) for p, q in zip(P, Q))assert abs(kl_div - expected) < 0.01, f"KL 散度应为 {expected:.4f},你得到 {kl_div:.4f}"print(f"✅ 作业 2 通过:")print(f"   Teacher: {P}")print(f"   Student: {Q}")print(f"   KL(P||Q) = {kl_div:.4f}")print("   KL 散度 ≥ 0,等于 0 当且仅当两个分布完全相同。")print("   蒸馏的目标就是最小化 Student 和 Teacher 之间的 KL 散度。")

作业 3:三种蒸馏方法对比| 方法 | 需要 Teacher 在线参与 | 训练数据来源 | 工程复杂度 ||:---|:---|:---|:---|| Logit 蒸馏 | 是 | 原始数据集 | 中 || 数据蒸馏 | 否(离线) | Teacher 生成的数据 | 低 || 特征蒸馏 | 是 | 原始数据集 | 高 |场景:团队有 70B Teacher 和 7B Student,训练资源有限(4 张 A100)。应该选择哪种蒸馏方法?小提示:资源有限 → 优先选择工程复杂度低、不需要 Teacher 实时参与的方案。

# 作业 3:三种蒸馏方法对比answer = "在这里填你的答案"# A) Logit 蒸馏# B) 数据蒸馏# C) 特征蒸馏assert not answer.startswith("在这里"), "请先填入你的答案"assert answer in "ABC", "请填入 A/B/C 中的一个字母"if answer == "B":    print("✅ 作业 3 通过:")    print("   资源有限场景下,数据蒸馏是最务实的选择:")    print("   1. Teacher 离线生成数据,不需要训练时实时调用")    print("   2. Student 端等价于 SFT,工程实现简单")    print("   3. 只需跑一次 Teacher 推理,后续训练不需要 Teacher")    print("   4. 4 张 A100 足够完成 7B 模型的 SFT 训练")else:    reasons = {        "A": "Logit 蒸馏需要 Teacher 在线参与,每步都要调用,成本更高。",        "C": "特征蒸馏需要访问中间层输出,工程复杂度最高。",    }    print(f"你选了 {answer}。{reasons.get(answer, '')}")    print("提示:资源有限 → 选最简单、不需要 Teacher 实时参与的方案。")