采样 · 让模型开口

逐行读 generate——自回归地「猜一个、接上、再猜」,以及 temperature 和 top-k 怎么调节模型的「脑洞」。再看 sample.py 怎么载入模型、用 meta.pkl 把吐出的数字翻译回文本。

作者 David更新于 第 11 篇(共 14 篇)

采样 · 让模型开口

训练好的模型,肚子里装着「下一个 token 该是谁」的知识。这一站,我们让它开口——把这知识一步步变成新文本。核心就一个方法:generate。

generate:猜一个、接上、再猜

model.py 约 305–330 行:

@torch.no_grad()
def generate(self, idx, max_new_tokens, temperature=1.0, top_k=None):
    for _ in range(max_new_tokens):
        # 上下文太长就裁到最后 block_size 个
        idx_cond = idx if idx.size(1) <= self.config.block_size else idx[:, -self.config.block_size:]
        # 前向,取最后一个位置的 logits
        logits, _ = self(idx_cond)
        logits = logits[:, -1, :] / temperature
        # 可选:只保留 top_k 个候选
        if top_k is not None:
            v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
            logits[logits < v[:, [-1]]] = -float('Inf')
        # softmax 转概率,按概率采样一个
        probs = F.softmax(logits, dim=-1)
        idx_next = torch.multinomial(probs, num_samples=1)
        # 接到序列末尾,继续
        idx = torch.cat((idx, idx_next), dim=1)
    return idx

这就是自回归生成——第一站说的「自己吃自己尾巴」。逐行看这个循环:

  • @torch.no_grad():生成不训练,不用算梯度。
  • idx_cond = ... idx[:, -block_size:]:模型一次最多看 block_size 个 token,上下文超长就只留最后一窗(前面的看不到了——这正是《大模型解构》长上下文那章要解决的痛点:窗口就这么大)。
  • logits, _ = self(idx_cond):前向。注意没给 targets,所以触发了GPT.forward 的推理期优化——只算最后一个位置。
  • logits = logits[:, -1, :] / temperature:取最后一个位置的分数,除以温度(下面细说)。
  • top_k 分支:只保留最高的 k 个分数,其余打成 -inf(下面细说)。
  • probs = softmax(logits):把分数变成概率分布。
  • idx_next = torch.multinomial(probs, 1):按概率随机抽一个——注意不是取最大(argmax),而是采样,这样每次生成才有变化、有「创造性」。
  • idx = torch.cat((idx, idx_next), dim=1):把抽中的 token 接到序列末尾,进入下一轮。

两个旋钮:temperature 和 top-k

生成质量,全靠这两个旋钮调。

temperature(温度):调「胆子」

logits = logits[:, -1, :] / temperature

softmax 之前除以温度 T:

  • T < 1(如 0.8):相当于把分数拉得更陡,高分的更突出——模型更「保守自信」,爱选最可能的词,文本更连贯但也更平淡。
  • T > 1:把分数压平,低分词也有机会——模型更「放飞」,更有创意但也更容易胡来。
  • T → 0:退化成 argmax,永远选最可能的,完全确定(没有随机性)。

一句话:温度就是模型的「胆子」旋钮——低了循规蹈矩,高了天马行空。

top-k:掐掉长尾

if top_k is not None:
    v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
    logits[logits < v[:, [-1]]] = -float('Inf')

只保留分数最高的 top_k 个候选,其余全部打成 -inf(softmax 后概率为 0)。作用是掐掉概率极低的长尾——防止模型偶尔抽到一个莫名其妙的词把句子带沟里。v[:, [-1]] 是第 k 名的分数,低于它的一律清零。

温度调「分布多陡」,top-k 调「从几个里挑」,两者常配合使用(sample.py 默认 temperature=0.8, top_k=200)。这些采样策略的系统研究,可见 Holtzman et al.(2019)那篇著名的《The Curious Case of Neural Text Degeneration》。

sample.py:把它跑起来

generate 是引擎,sample.py(89 行)是钥匙。它做三件事:

1. 载入模型(resume 或 gpt2):

if init_from == 'resume':
    checkpoint = torch.load(ckpt_path, map_location=device)
    gptconf = GPTConfig(**checkpoint['model_args'])
    model = GPT(gptconf)
    model.load_state_dict(state_dict)
elif init_from.startswith('gpt2'):
    model = GPT.from_pretrained(init_from, dict(dropout=0.0))
model.eval()

和训练那站对称:从 ckpt.pt 恢复自己训的模型,或直接拉 GPT-2。model.eval() 关掉 dropout。

2. 准备好 encode / decode——这里 meta.pkl 又登场了:

if load_meta:  # 字符级:用训练时存的对照表
    stoi, itos = meta['stoi'], meta['itos']
    encode = lambda s: [stoi[c] for c in s]
    decode = lambda l: ''.join([itos[i] for i in l])
else:          # 否则默认用 GPT-2 的 BPE 分词
    enc = tiktoken.get_encoding("gpt2")
    encode = lambda s: enc.encode(s, allowed_special={"<|endoftext|>"})
    decode = lambda l: enc.decode(l)

这就是当初费事存 meta.pkl 的收尾:模型吐出的是一串数字,得靠 itos 把它们翻译回人能读的字符。字符级用那两个字典,GPT-2 级则用 tiktoken。

3. 编码起始提示,反复生成:

start_ids = encode(start)
x = (torch.tensor(start_ids, dtype=torch.long, device=device)[None, ...])
with torch.no_grad():
    with ctx:
        for k in range(num_samples):
            y = model.generate(x, max_new_tokens, temperature=temperature, top_k=top_k)
            print(decode(y[0].tolist()))

把提示词(默认 "\n")编码成 x,喂给 generate,生成 max_new_tokens 个新 token,再 decode 回文本打印。循环 num_samples 次,出几段样本。

跑一下第一站那个字符级莎士比亚,你就看到全链路闭环了:prepare.py 造数据 → train.py 训模型 + 存 ckpt → sample.py 载入 + generate + decode 出文本。 六百行代码,从一段莎士比亚,真的长出了一个会写伪莎士比亚的 GPT。

主线通关,还剩「更大更快更省」

到这里,「数据 → 模型 → 训练 → 采样」的主干你已经全程逐行走完了。剩下的是让它上规模的进阶手艺:怎么装载 OpenAI 的 GPT-2 权重(那个有点烧脑的 Conv1D 转置)、怎么多卡并行、怎么用编译和 Flash Attention 榨干 GPU。这是第五站。👉 装载 GPT-2:from_pretrained 与 Conv1D 转置

参考文献

  • [1] Holtzman et al. “The Curious Case of Neural Text Degeneration.” ICLR 2020. arXiv:1904.09751. —— 温度、top-k、nucleus 采样的系统研究。
  • 源码:Andrej Karpathy, nanoGPT, model.py 的 generate 与 sample.py. https://github.com/karpathy/nanoGPT

本页目录