从模型到文本 — 温度、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。