首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >手撕 GPT#09:验证 loss 从 340 降到 6.5,模型却只会说“对对对”——小模型蒸馏的“不可能三角”

手撕 GPT#09:验证 loss 从 340 降到 6.5,模型却只会说“对对对”——小模型蒸馏的“不可能三角”

作者头像
烟雨平生
发布2026-06-01 18:50:19
发布2026-06-01 18:50:19
650
举报

手撕 GPT 系列第 9 篇。前 8 篇我们训练了一个 3M 模型,发现天花板在容量。上一篇最后选了知识蒸馏这条路——让大模型教小模型。

这篇记录蒸馏探索的完整过程:三个 teacher,三个方向,全部失败。还有一路上踩的工程坑。

失败不可怕。可怕的是失败了不知道为什么。

知识蒸馏听起来很美好:让一个 102M 的大模型当老师,把知识"传授"给我们的 3M 小模型。大模型知道的多,小模型学得快,双赢。

Industry 里确实有成功的案例——DistilBERT 把 BERT 压缩了 40%,只掉了 3% 的性能。DeepSeek-R1 蒸馏系列把 R1 的推理能力塞进了 1.5B 的小模型。

但那些都是 100M+ 级别的 student。3M 级别的小模型,蒸馏这条路走得通吗?

我们选了三个方向,逐一实验。

先解决一个问题:参数量是瓶颈吗?

第 7 篇说"瓶颈在容量",但那个结论是在 3M 模型上得出的。如果把模型从 3M 扩到 12M,容量翻 4 倍,情况会不会不一样?

这是蒸馏的起点——student 至少要够大才能接收 teacher 的知识。

▪ 实验

同一个 byte-level tokenizer (vocab=259),同一份 600 条训练数据,唯一变量是模型宽度:

配置

n_embd

n_head

参数量

训练时间

验证 loss

原始

256

4

3.02M

~20min

0.005

加宽

512

8

12.22M

8.6min

0.0035

验证 loss 降低了 30%,训练反而更快(MPS 对大矩阵运算效率更高)。验收测试 6/6 通过

但我用 15 个验收外的问题测试泛化能力:

Q: 什么是深度学习? → RoPE 是旋转位置编码... Q: CPU和GPU有什么区别? → RoPE 是旋转位置编码... Q: 地球到月球有多远? → RoPE 是旋转位置编码... Q: 什么是神经网络? → RoPE 是旋转位置编码...

15 个问题,全部返回同一个答案。置信度 100%。和 3M 模型一模一样的行为。

▪ 结论

参数量从 3M 增到 12M(4 倍),泛化能力零提升。

更大的模型只是把训练数据背得更熟了,并没有变得更"聪明"。泛化的瓶颈在数据多样性(只有 12 个主题),不在模型大小。

不过这个实验不白做——它告诉我们:如果要继续蒸馏,12M 是一个合理的 student 起点。 至少验收表现不比 3M 差。增大模型容量的方向已有结论,下面来看看看看另外两个方向。

方向一:让大模型生成训练数据

▪ 思路

最直觉的蒸馏方式:用 teacher 模型对每个问题生成答案,用这些答案替换原始手写答案,训练 student。

如果 teacher 的答案质量更高、覆盖更广,student 应该能学到更多。

▪ 踩坑一:选错了 teacher

第一次选了 uer/gpt2-chinese-cluecorpussmall(102M 参数)。理由很充分:中文 GPT-2,参数量是 student 的 30 倍。

结果它生成的答案:

问题

teacher 的回答

RoPE 是什么?

是我!

太阳系有哪些行星?

..................................................................

什么是 Python?

。。。。。。。。。。。。。。。。。。。。。。。。

15×6 等于多少?

6 等于多少,多少乘以 6 等于多少

它不是在答题,是在续写文本。 问它"RoPE 是什么?",它从"是什么"后面续写,写出"是我!"。问它"太阳系有哪些行星?",它输出一堆点号——因为训练数据里点号后面经常跟着更多文本。

⚠️ 踩坑提醒

做知识蒸馏,teacher 必须是指令微调模型(Instruct)。Base 模型只会 next-token prediction(续写),不会按 Q&A 格式答题。用它当 teacher 等于让一个不会数学的人教数学——不是教得不好,是根本不会。

▪ 换 teacher:Qwen2.5-1.5B-Instruct

第二次选了 Qwen2.5-1.5B-Instruct——真正的指令微调模型。

这次好多了:

问题

Qwen 的回答

正确?

15×6 等于多少?

90

太阳系有哪些行星?

八大行星:水星、金星、地球、火星、木星、土星、天王星、海王星。

什么是 Python?

Python是一种高级编程语言。

RoPE 是什么?

罗伯特·恩格尔系数

RoPE 的全称是什么?

Robots, Operations, and Environments.

Qwen 答对了常识题,但对专业 ML 概念(RoPE = 旋转位置编码)完全无知。它不知道 RoPE 是 Rotary Position Embedding,以为是什么经济学名词。

这暴露了一个问题:1.5B 的模型,知识面有边界。 超出边界的内容,它会自信地胡说八道。

▪ 用 Qwen 数据训练 student

用 Qwen 生成的 171 条数据,训练 12M student(byte-level tokenizer)。

1.7 分钟,early stopping 触发——验证 loss 从第一步就不降。推理输出全是乱码。

▪ 为什么失败?

根因不在 teacher 质量,在 tokenizer 不匹配

Qwen 用 tiktoken tokenizer(vocab=151,936)生成中文文本,每个汉字约 1-2 个 token。但这些文本被我们的 byte-level tokenizer(vocab=259)重新编码成 UTF-8 字节流,每个汉字变成 3 个字节。

"什么是深度学习?" Qwen tokenizer: [20002, 25, 106582, 102217, 100134, 94432] → 6 tokens Byte tokenizer: [233, 160, 168, 228, 185, 136, 230, 183, 177, 229, 186, 166, 229, 173, 166, 239, 189, 159] → 18 tokens

12M 模型要从这些字节流中学到有意义的中文模式——就像让你看摩尔斯电码学中文。信息还在,但密度太低了。

⚠️ 踩坑提醒:数据级蒸馏(teacher 生成文本 → student 学习)的前提是 student 的 tokenizer 能高效表示这些文本。byte-level tokenizer 每个中文字符 3 个 token,序列长度直接爆表,模型学不到任何有用的模式。

方向二:标准 logits 级蒸馏

▪ 思路

方向一失败的原因是 tokenizer 不匹配——teacher 生成文本,student 用另一个 tokenizer 重新编码,信息失真。

标准做法是:teacher 和 student 共享同一个 tokenizer,直接对齐输出层的概率分布。 这就是 Hinton 2015 年提出的经典知识蒸馏方法。

DeepSeek-R1-Distill-Qwen-1.5B 做 teacher。它本身就是 DeepSeek 从 R1 蒸馏出来的,推理能力强,而且和 Qwen 系列共享 tokenizer。

▪ 踩坑二:vocab_size 不一致

写代码时遇到了第一个工程坑。

tok = AutoTokenizer.from_pretrained("deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B") print(tok.vocab_size) # 151,936 print(model.config.vocab_size) # 151,643

tokenizer 报告的 vocab_size 和模型实际输出的维度不一样! tokenizer 包含一些特殊 token(比如 chat template 的控制 token),但这些 token 没有对应的模型权重。

tok.vocab_size 构建 student,计算 KL 散度时报错:

RuntimeError: The size of tensor a (151936) must match the size of tensor b (151643) at non-singleton dimension 2

正确做法是用 model.config.vocab_size,不是 tokenizer.vocab_size

⚠️ 踩坑提醒

HuggingFace 的 tokenizer.vocabsize 和 model.config.vocabsize 不一定相同。做 logits 级蒸馏时,一定要用 model.config.vocab_size 构建 student。

▪ 踩坑三:Python 的 min() 陷阱

训练数据编码时,需要把 prompt 部分的 target 设为 -100(不计算 loss)。代码:

# 计算 mask 长度 n = min(prompt_len - 1, len(y)) # 生成 masked target y_masked = [-100] * n + y[n:]

看起来没问题。但 Python 的 min() 在只传一个参数时,会尝试把那个参数当 iterable 来迭代。如果 len(y) 恰好等于 prompt_len - 1,你可能写出 min(prompt_len - 1) 这种单参数调用——直接报 TypeError: 'int' object is not iterable

这个 bug 藏得很深,因为大多数情况下 min(a, b) 有两个参数没问题,只有边界条件才会触发。

▪ 实验

蒸馏损失 = α × CE(student, 硬标签) + (1-α) × KL(student 分布, teacher 分布)

配置

参数量

Teacher: DeepSeek-R1-Distill-Qwen-1.5B

1777M

Student: GPT (vocab=151,643)

41.8M

这是真正的标准知识蒸馏——teacher 和 student 输出维度完全一致,可以直接计算 KL 散度。

训练 loss 从 340 降到 6.5,验证 loss 也稳步下降。133 分钟训练完成。看起来很成功。

▪ 结果

Q: 什么是深度学习? A: 除以 除以 5 是多少,而不需要人工人工人工人工人工... Q: 什么是注意力机制? A: 分 分 分 分 分 分 分 分 分 分 分 分... Q: Python? A: Python 乘以 66 30,对对对对对对对对对对...

全是乱码和重复词。 没有一个问题回答正确。

▪ 为什么 loss 降了但输出是垃圾?

数字揭示了真相。41.8M 参数的 student 模型:

组件

参数量

占比

Embedding 层

38.9M

93%

Transformer 层(4层)

2.9M

7%

93% 的参数用来记忆 151,643 个 token 的嵌入向量,只剩 7% 做实际的语言理解。

KL 散度确实在驱动 student 的 151,643 维输出去模仿 teacher——loss 在降不是假的。但 student 的 transformer 层只有 2.9M 参数,它在用 2.9M 的"大脑"去模仿 1.77B 的"大脑"输出的 15 万维概率分布。

它能做的只有一件事:学会哪些 token 出现频率高,然后反复输出它们。 "除以"、"人工"、"对"——这些是训练数据中的高频词。loss 下降是因为模型学会了输出高频 token 来"匹配" teacher 的概率分布,不是因为它理解了语言。

⚠️ 踩坑提醒验证 loss 下降 ≠ 模型在学有用的东西。 当 embedding 占比超过 90% 时,loss 下降可能只是模型学会了"输出高频 token"。一定要跑推理验证,不要被 loss 曲线骗了。

为什么 DistilBERT 能成功?

你可能会问:DistilBERT 也做了蒸馏,凭什么它成功我们失败?

模型

参数量

vocab

embedding 占比

transformer 占比

DistilBERT

66M

30,522

36%

64%

我们的student

41.8M

151,643

93%

7%

DistilBERT 有 64% 的参数在做"理解"。

我们只有 7%

不是蒸馏这个技术不行。是我们的 vocab(151K)太大了,embedding 把"理解"的空间全吃光了。DistilBERT 的 vocab 只有 30K,所以 embedding 只占 36%,剩下 64% 都给了 transformer。

三个失败指向同一个结论

方向

做法

结果

根因

增大容量

3M→12M

6/6 验收,0/15 OOD

瓶颈在数据多样性

数据级蒸馏

Qwen 数据 + byte tokenizer

0/6 全乱码

tokenizer 不匹配,信息密度太低

logits 级蒸馏

DeepSeek + 共享 vocab(151K)

全乱码+重复词

embedding占 93%,transformer 没容量

三个方向失败的原因各不相同,但指向同一个结论:

小模型做知识蒸馏,同时面临三个约束,无法同时满足。

  1. 模型容量 — 参数太少学不到复杂模式
  2. 词表大小 — 大词表吃掉 embedding 容量;小词表无法与 teacher 对齐
  3. Tokenizer 效率 — byte-level(vocab=259)每个汉字 3 token,太浪费;大 vocab(151K)又吃光参数

小 vocab(方向一)→ embedding 小 → 有容量训练 → 但无法从 teacher 学习;

大 vocab(方向二)→ 能对齐 teacher → embedding 占满容量 → 没能力理解语言。

这就是小模型蒸馏的不可能三角

工程踩坑清单

一路踩了不少代码层面的坑,记录下来:

现象

解法

adapt_tokenizer返回未定义变量

return tok_id 引用不存在的变量

改成 return tok

tokenizer.vocabsize ≠ model.vocabsize

KL 散度维度不匹配

用 model.config.vocab_size

min() 单参数传 int

TypeError: 'int' object is not iterable

确保始终传两个参数

Python 后台跑训练输出缓冲

看不到实时输出

PYTHONUNBUFFERED=1

MPS 间歇性暂停

elapsed从1732s跳到 2693s

GPU 资源竞争,无解,等就好

base 模型当 teacher

输出全是续写不是答题

只用Instruct版本

⚠️ 踩坑提醒:做蒸馏项目最常遇到的两个 bug——tokenizer 和 model 的 vocab 不对齐teacher 类型选错(base vs instruct)。先把这两个确认了再开始训练,能省很多调试时间。

这些失败有什么价值?

三个全失败,有什么好写的?

三个失败各有各的价值:

  1. 参数量不等于泛化能力。 4 倍参数量换不来 1 个新能力。盲目增大模型只会更好地记住训练数据。
  2. 好的 teacher ≠ 好的蒸馏效果。 如果 student 的 tokenizer 无法高效表示 teacher 的输出,再好的 teacher 也传不过去知识。还有,1.5B 的 teacher 对专业概念会自信地胡说八道——teacher 的知识边界就是蒸馏效果的天花板。
  3. loss 下降 ≠ 模型在学。 验证 loss 从 340 降到 6.5,曲线很漂亮,但模型只学会了输出高频 token。当 embedding 占比超过 90% 时,loss 曲线是骗人的。

你不是也这么想过:找个大模型当 teacher,小模型就能学会大模型的本事?

现在你知道了:知识蒸馏不是传功,是上课。student 太小,坐都坐不住,怎么听课?

下一步,我们准备换个思路——不依赖 teacher 的知识传递,而是从 student 自身的架构和 tokenizer 下手。看看能不能找到一条绕过不可能三角的路。

💡 一句话带走:小模型蒸馏的不可能三角——容量、词表、tokenizer 效率,三者不可兼得。不是蒸馏不行,是你的 student 还没大到能"听课"。

这是「手撕 GPT」系列第 9 篇。系列全部文章:

从0到1手搓484行代码,用普通CPU训练一个3.37M中文GPT模型,耗时不到20分钟,回答的效果很不错,欢迎各位老师检查作业

训练完AI模型才发现,最难的不是训练

手撕 GPT#01:五分钟上手,手把手带你用CPU 原生训练中文GPT模型,“我没有 GPU”的问题解了!!!

手撕 GPT#02:从乱码到说人话,模型经历了什么?

手撕 GPT#03:GPT 的核心代码只有 100 行,并且还支持注意力、权重、loss可视化哦

手撕 GPT#04:我用CPU花20分钟训练了一个满分模型,问它一个问题,后悔了

手撕 GPT#05:316 万个参数、比照片还小的AI,为什么还能学会“说”中文?

手撕 GPT#06:手把手 30 分钟:零基础跑通你的第一个 GPT

手撕 GPT#07:我试了四个方向终于摸到了3M模型的天花板,最后还是成功了

手撕 GPT#08:你已经能训练模型了——接下来“撕”什么?

项目地址:https://github.com/helloworldtang/GPT_teacher-3.37M-cn

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-05-31,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 的数字化之路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档