跳到主要内容

从模型到文本 — 温度、top-k,与把 OpenAI 的权重搬进来

这一章讲两组收尾动作: 模型算出概率清单之后,「怎么挑词」的四个旋钮; 以及「权重」这件事的工程面——怎么存、怎么把别人训好的搬进来。 读完这一章,原书第 5 章(全书的中点)就完整了: 你手里有一台真的会说英语的 GPT,而且每个零件都是自己写的。

1. 这一章讲什么

上一章末尾,模型训完了——但它有一个你可能早就注意到的性质:同一个开头,每次生成的结果一模一样。 因为第 05 章的生成函数每次都挑分最高的词。这一章前半解开这个「太老实」; 后半做另一件事:我们只在 5,145 个词元上训过,水平有限——而 OpenAI 把 GPT-2 的权重公开了, 把它搬进来,直接跳过烧钱阶段。

2. 顶层全景

模型每步给出:词表 50,257 个词元,各一个分数(logits)

四种挑法,一个比一个敢:
贪心 → 永远挑最高分(确定性,会复读)
抽样 → 按概率抽签(有变化,可能抽到离谱的)
温度 → 先给分数除一个数再抽签:小于 1 更保守,大于 1 更狂野
top-k → 抽签前先把长尾砍掉:只留分数最高的 k 个,其余填 -inf

权重工程:
存:state_dict(每层→参数的字典)→ .pth 文件;要续训就连优化器状态一起存
搬:OpenAI 的 GPT-2 权重(TensorFlow 存的)→ 逐层改名换姓 → 搬进 GPTModel
→ 搬对了的检验标准:它开口说人话

图说:前半章管「生成那一刻怎么选」,后半章管「训练成果怎么存与搬」。

3. 核心原理

3.1 主走查:一次抽样实验,看清「按概率挑」长什么样

拿一个 9 词小词表做实验(为排版缩小,词表是 closer/every/effort/forward/inches/moves/pizza/toward/you)。 模型对「every effort moves you」的下一个词给出的 logits 是书里印的真实数1:

closer 4.51 every 0.89 effort -1.90 forward 6.75 inches 1.63
moves -1.62 pizza -1.89 toward 6.28 you 1.79

贪心解码(greedy decoding,第 05 章那个 argmax)永远挑 forward。 换按概率抽样(PyTorch 的 multinomial:概率越大越容易被抽中,但不是每次都被抽中), 同一个分布抽 1,000 次,结果2:

forward 582 次 toward 343 次 closer 73 次 inches 2 次 其余 0 次

这一张表就是「为什么同一句话问两遍答案不一样」的全部机制: 清单没变,变的是挑法。 模型会偶尔写出 "every effort moves you toward" 或 "…closer"—— 多样性不是模型「灵机一动」,是抽样抽出来的3

3.2 温度:拧分布的锐度

温度(temperature)就是「进 softmax 之前,先把 logits 除以这个数」—— 名字唬人,机制就一行除法4:

  • T = 1:等于没动,按原概率抽;
  • T = 0.1(小于 1):除完差距放大,分布变尖——forward 几乎 100% 被挑中,行为逼近贪心;
  • T = 5(大于 1):除完差距抹平,分布变扁——连 pizza 都有约 4% 的机会出头, 于是偶尔会写出 "every effort moves you pizza" 这种胡话5

一句话:温度不动模型、不动清单上的相对名次,只动「名次之间的差距被当成多大」。 第 01 章样板书里那句「0.8 是试出来的,不是推出来的」在这里同样成立:温度没有理论最优值, 要按用途试——严肃任务调低,头脑风暴调高。

3.3 top-k:先把长尾砍掉,再抽

抽样加温度解决了「太老实」,引入了新病:长尾里的离谱词也有了出头之日(pizza 就是这么来的)。 top-k 采样的修法:只留分数最高的 k 个词,其余 logits 全部填成 -inf 再进 softmax—— 第 04 章遮未来用的是同一个技巧,e 的负无穷次方是 0,长尾直接归零6

k=3 的真实走查:三个幸存者是 forward(6.75)、toward(6.28)、closer(4.51), 其余六个位置填 -inf;softmax 后概率变成 forward 0.5775、toward 0.3610、closer 0.0615, 其余为 0——抽样只在幸存者里进行,pizza 永无出头之日7

书里最后把四件套收成一个新的 generate 函数:top_k 过滤 → 温度缩放 → multinomial 抽样;外加一个实用小功能:生成到 <|endoftext|> 就提前收工8。 组合效果:第 06 章那个把小说背下来的模型,用 top_k=25、T=1.4 再生成, 输出变成从没在原文出现过的新句子——抽样策略确实把「背原文」冲淡成了「重新组合」9

3.4 权重的存与取:state_dict 与优化器状态

训练一次要花真金白银,训完不存等于白训。PyTorch 的推荐做法: state_dict——一个「每层 → 参数张量」的字典,torch.save 落成 .pth 文件; 用时新建一个同架构的模型,load_state_dict 灌回去10

一个容易踩的坑:想「继续训练」而不只是「拿来用」,必须连优化器的状态一起存。 AdamW 给每个参数记了一份「近期走势」(第 06 章说的记忆),不存它, 续训时这份记忆清零,模型可能训不动甚至不收敛(损失不再往下降、停在一个高水平上,训不到底)11。 书里的做法是把模型和优化器的 state_dict 打包成一个字典一起存12

3.5 搬权重:把 OpenAI 的 GPT-2 灌进自己写的模型

这一步是全章的高潮,也是这本书的「实战性」所在: 我们自己写的 GPTModel,要能吃下 OpenAI 官方发布的 GPT-2 权重。

先交代为什么这么干:我们的模型只读过一篇短篇小说;OpenAI 的 GPT-2 是几十亿词元训出来的。 架构既然一字不差地复刻了,权重就可以整个搬过来——书里的原话是,这「免去了自己花上万美元重训」13

搬运过程有三个真实障碍,每个都值得记住:

  • 格式障碍:权重是 TensorFlow 存的(2019 年 GPT-2 发布时 PyTorch 还不是主流), 所以加载代码里要装 TensorFlow 来读;124M 模型的权重文件近 500 MB14
  • 口径障碍:两个老设置要改回 OpenAI 的样子。 我们前面把上下文窗口缩到 256 省算力, 现在要改回 1,024;我们的 Q/K/V 投影不带偏置(现代 LLM 的常态), OpenAI 的老权重带,所以 qkv_bias 要设回 True 才能对上15
  • 结构障碍:OpenAI 的注意力把 Q/K/V 存在一个合并矩阵里,加载时要按列切成三份、 各自转置再赋值;输出头则直接指向词元嵌入那张表——第 05 章的 weight tying 在这里现身16

作者在这里有一段罕见的坦白,值得原样记住:这个搬运函数「花了大量猜测」, 而验证手段只有两条——assign 函数的形状检查(尺寸对不上当场报错), 和最终的冒烟测试:搬错了,模型说出来的就不可能是人话17

搬完之后,同一个 "Every effort moves you" 开头,贪心之外加 top_k=50、T=1.5, 输出变成通顺连贯的全新句子18。至此,你的代码里跑的已经是真正预训练过的 GPT-2。

4. 作者的判断与证据

有证据的: 1,000 次抽样的分布(582/343/73/2)、k=3 的三个概率、 加载后的连贯输出——全是书里印的真实运行结果2718

作者的判断:

  • 「加载公开权重替代自己预训练」被当成默认动作——这是成本现实逼出来的教学法13
  • 温度、top-k 没有理论最优,要靠用途试——书里以习题形式让读者自己摸19
  • 「qkv_bias 在现代 LLM 里已不常用,因为不提升性能」——作者的行业观察15

判断(我们的,不是书里的): 「assign 查形状 + 会不会说人话」这套验证法, 是机器学习工程里被低估的一课:深度学习系统里很多错误不会在编译期爆炸, 而是安静地让模型变笨。「冒烟测试 = 生成一段看像不像人话」粗糙但有效, 因为它测的是整条链路的最终产物。 如果错,会错在: 如果权重错得恰好很轻微(比如只有一层转置错),模型可能仍说出像样的话, 错误就漏网了——所以形状检查是主防线,冒烟测试是兜底的。

5. 边界与局限

  • 抽样参数管的是「多样性 vs 靠谱」的取舍,管不了事实对错—— 温度调低只是让胡话说得更笃定,这一点书里没展开,但值得记住。

  • top-p(另一个砍长尾的旋钮):不按个数砍,按累积概率划线—— 从最高分的词开始往下累加,加过一条线就把后面全砍掉。

    这个做法的中文名叫核采样(nucleus sampling,「核」是累加出来的高分核心);这本书只讲了 top-k、没讲它。

    两者目的相同,今天的 API(应用程序接口,你调别人服务时面对的那张参数表)里两个旋钮都常见。

  • 搬权重的方法只对「架构逐层一致」成立;换一个现代模型(RoPE、RMSNorm),这段代码就要重写。

  • Ollama 这类推理工具在后面第 09 章才登场;本章的生成函数是教学实现,没有键值缓存等加速, 别拿去当推理引擎用。

6. 可带走的

  1. 模型输出的清单不变,「怎么挑」是独立的一层:贪心、抽样、温度、top-k 四个旋钮。
  2. 温度 = logits 除以一个数;小于 1 更保守,大于 1 更狂野;没有理论最优,按用途试。
  3. top-k = 长尾填 -inf;和因果掩码是同一个技巧,一处学会两处用。
  4. 「同一问题两次答案不同」的机制:清单没变,抽样抽的——多样性是设计,不是 bug。
  5. 存权重用 state_dict;想续训必须连优化器状态一起存,否则 AdamW 的记忆清零。
  6. 搬别人权重的三个障碍:格式、口径(窗口长、偏置)、结构(合并矩阵怎么切)——逐层对形状,最后看输出像不像人话。
  7. weight tying 不是纸面概念:加载 GPT-2 时输出头真的指向嵌入表。
  8. 「加载公开权重」是这本书的核心动作:预训练太贵,站在公开的肩膀上。

7. 原文地图

主题原书章原文位置
1,000 次抽样实验5 Pretraining on unlabeled datatext/13-ch05-5-pretraining-on-unlabeled-data.txt:1216(搜「582 x forward」)
温度的定义与三个取值同上text/13-ch05-5-pretraining-on-unlabeled-data.txt:1230(搜「dividing the logits」) · text/13-ch05-5-pretraining-on-unlabeled-data.txt:1163(搜「pizza」)
top-k 与 -inf同上text/13-ch05-5-pretraining-on-unlabeled-data.txt:1297(搜「Top-k sampling」) · text/13-ch05-5-pretraining-on-unlabeled-data.txt:1369(搜「0.5775」)
合并版 generate 与新输出同上text/13-ch05-5-pretraining-on-unlabeled-data.txt:1383(搜「def generate」) · text/13-ch05-5-pretraining-on-unlabeled-data.txt:1425(搜「stand to work on surprise」)
state_dict 与优化器状态同上text/13-ch05-5-pretraining-on-unlabeled-data.txt:1473(搜「state_dict」) · text/13-ch05-5-pretraining-on-unlabeled-data.txt:1492(搜「optimizer state」)
加载 OpenAI 权重同上text/13-ch05-5-pretraining-on-unlabeled-data.txt:1528(搜「OpenAI openly shared」) · text/13-ch05-5-pretraining-on-unlabeled-data.txt:100(搜「qkv_bias」)
作者的坦白与冒烟测试同上text/13-ch05-5-pretraining-on-unlabeled-data.txt:1807(搜「took a lot of guesswork」) · text/13-ch05-5-pretraining-on-unlabeled-data.txt:186(搜「coherent text」)

Footnotes

  1. 出处:「5 Pretraining on unlabeled data」第 1173 段(text/13-ch05-5-pretraining-on-unlabeled-data.txt:1173,搜「4.51, 0.89, -1.90」)。

  2. 出处:「5 Pretraining on unlabeled data」第 1216 段(text/13-ch05-5-pretraining-on-unlabeled-data.txt:1216,搜「582 x forward」)。 2

  3. 出处:「5 Pretraining on unlabeled data」第 1222 段(text/13-ch05-5-pretraining-on-unlabeled-data.txt:1222,搜「582 out of 1,000」)。

  4. 出处:「5 Pretraining on unlabeled data」第 1230 段(text/13-ch05-5-pretraining-on-unlabeled-data.txt:1230,搜「fancy description for dividing the logits」)。

  5. 出处:「5 Pretraining on unlabeled data」第 1163 段(text/13-ch05-5-pretraining-on-unlabeled-data.txt:1163,搜「pizza」)。 原文:「using the temperature of 5 results in texts such as every effort moves you pizza about 4% of the time」。

  6. 出处:「5 Pretraining on unlabeled data」第 1297 段(text/13-ch05-5-pretraining-on-unlabeled-data.txt:1297,搜「Top-k sampling」)与第 1326 段(text/13-ch05-5-pretraining-on-unlabeled-data.txt:1326,搜「negative infinity value」)。

  7. 出处:「5 Pretraining on unlabeled data」第 1369 段(text/13-ch05-5-pretraining-on-unlabeled-data.txt:1369,搜「0.5775」)。 2

  8. 出处:「5 Pretraining on unlabeled data」第 1383 段(text/13-ch05-5-pretraining-on-unlabeled-data.txt:1383,搜「def generate」)。

  9. 出处:「5 Pretraining on unlabeled data」第 1425 段(text/13-ch05-5-pretraining-on-unlabeled-data.txt:1425,搜「stand to work on surprise」)。

  10. 出处:「5 Pretraining on unlabeled data」第 1473 段(text/13-ch05-5-pretraining-on-unlabeled-data.txt:1473,搜「state_dict」)。

  11. 出处:「5 Pretraining on unlabeled data」第 1492 段(text/13-ch05-5-pretraining-on-unlabeled-data.txt:1492,搜「optimizer state」)。 原文:「Without it, the optimizer resets, and the model may learn suboptimally or even fail to converge properly」。

  12. 出处:「5 Pretraining on unlabeled data」第 1504 段(text/13-ch05-5-pretraining-on-unlabeled-data.txt:1504,搜「model_and_optimizer.pth」)。

  13. 出处:「5 Pretraining on unlabeled data」第 1528 段(text/13-ch05-5-pretraining-on-unlabeled-data.txt:1528,搜「OpenAI openly shared」)。 2

  14. 出处:「5 Pretraining on unlabeled data」第 1533 段(text/13-ch05-5-pretraining-on-unlabeled-data.txt:1533,搜「TensorFlow」)与第 1575 段(text/13-ch05-5-pretraining-on-unlabeled-data.txt:1575,搜「498M」)。

  15. 出处:「5 Pretraining on unlabeled data」第 1695 段(text/13-ch05-5-pretraining-on-unlabeled-data.txt:1695,搜「1,024-token length」)与第 1700 段(text/13-ch05-5-pretraining-on-unlabeled-data.txt:1700,搜「bias vectors」)。 原文:「Bias vectors are not commonly used in LLMs anymore as they don't improve the modeling performance」。 2

  16. 出处:「5 Pretraining on unlabeled data」第 1741 段(text/13-ch05-5-pretraining-on-unlabeled-data.txt:1741,搜「np.split」)与第 1799 段(text/13-ch05-5-pretraining-on-unlabeled-data.txt:1799,搜「out_head.weight」)。

  17. 出处:「5 Pretraining on unlabeled data」第 1807 段(text/13-ch05-5-pretraining-on-unlabeled-data.txt:1807,搜「took a lot of guesswork」)。

  18. 出处:「5 Pretraining on unlabeled data」第 1833 段(text/13-ch05-5-pretraining-on-unlabeled-data.txt:1833,搜「ideal new way to practice」)。 2

  19. 出处:「5 Pretraining on unlabeled data」第 1433 段(text/13-ch05-5-pretraining-on-unlabeled-data.txt:1433,搜「Exercise 5.2」)。