# 采样 · 让模型开口

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

- 作者：David（道雾轩）
- 专栏：nanoGPT 源码解读（https://daiw.org/manual/nanogpt.md）
- 最后更新：2026-07-13
- 原文：https://daiw.org/manual/nanogpt/generate-and-sample
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# 采样 · 让模型开口

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

## generate：猜一个、接上、再猜

`model.py` 约 305–330 行：

```python
@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
```

这就是**自回归生成**——[第一站](https://daiw.org/manual/nanogpt/what-is-nanogpt)说的「自己吃自己尾巴」。逐行看这个循环：

- `@torch.no_grad()`：生成不训练，不用算梯度。
- `idx_cond = ... idx[:, -block_size:]`：模型一次最多看 `block_size` 个 token，**上下文超长就只留最后一窗**（前面的看不到了——这正是[《大模型解构》长上下文那章](https://daiw.org/manual/llm-anatomy/positional-encoding)要解决的痛点：窗口就这么大）。
- `logits, _ = self(idx_cond)`：前向。注意**没给 targets**，所以触发了[GPT.forward 的推理期优化](https://daiw.org/manual/nanogpt/gpt-assembly)——只算最后一个位置。
- `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（温度）：调「胆子」

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

softmax 之前除以温度 `T`：

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

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

### top-k：掐掉长尾

```python
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）：

```python
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()
```

和[训练那站](https://daiw.org/manual/nanogpt/train-setup)对称：从 `ckpt.pt` 恢复自己训的模型，或直接拉 GPT-2。`model.eval()` 关掉 dropout。

**2. 准备好 encode / decode**——这里 [meta.pkl](https://daiw.org/manual/nanogpt/data-prepare-char) 又登场了：

```python
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. 编码起始提示，反复生成：**

```python
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` 次，出几段样本。

<Callout type="info">
  跑一下[第一站](https://daiw.org/manual/nanogpt/what-is-nanogpt)那个字符级莎士比亚，你就看到全链路闭环了：**prepare.py 造数据 → train.py 训模型 + 存 ckpt → sample.py 载入 + generate + decode 出文本。** 六百行代码，从一段莎士比亚，真的长出了一个会写伪莎士比亚的 GPT。
</Callout>

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

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

## 参考文献

- [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
