模型全景 · 一个 GPT 只有五个零件
鸟瞰 model.py——LayerNorm、CausalSelfAttention、MLP、Block、GPT 五个类怎么拼成一台 GPT,数据怎么从 token 编号流到「下一个 token 的分数」,以及 GPTConfig 七个超参各自决定什么。
模型全景 · 一个 GPT 只有五个零件
model.py 是整个 nanoGPT 的心脏。约 300 行,定义了「一个 GPT」的全部。在扎进最难的注意力之前,这一章先站高一点,把整台机器的装配图看清楚:有哪几个零件、它们怎么套在一起、数据从哪进从哪出。
五个类,由小到大搭起来
model.py 里就五个类(外加一个存配置的 GPTConfig)。它们的关系是层层嵌套、由小拼大:
| 类 | 是什么 | 由谁组成 |
|---|---|---|
LayerNorm | 归一化层,给数据「校准量纲」 | 最底层零件 |
CausalSelfAttention | 因果自注意力,让每个位置「回看前文」 | 最底层零件 |
MLP | 前馈网络,逐位置「加工」信息 | 最底层零件 |
Block | 一个 Transformer 层 | = LayerNorm × 2 + Attention + MLP |
GPT | 完整模型 | = 词嵌入 + Block × n_layer + 输出层 |
一句话:Block 是把注意力和 MLP 打包成的「一层」,GPT 就是把这样的层叠 n_layer 遍,前面加个「查字典」的入口、后面加个「做预测」的出口。 就这么简单。
数据怎么流过整台机器
比起干看类图,顺着数据流走一遍更有感觉。一批 token 编号 idx 进来,到「下一个 token 的分数」logits 出去,中间是这样:
翻译成大白话:
- 查字典(
wte):每个 token 编号,查一张表换成一个n_embd维的向量。编号13从此变成一串能参与运算的浮点数——这串数字就是模型对这个 token 的「初始理解」。 - 标位置(
wpe):注意力本身分不清「谁在前谁在后」,所以额外查一张位置表,把「你是第 0 个 / 第 1 个 / …… token」也编码成向量,加到词向量上。 - 叠 Block(重头戏):相加后的向量流过
n_layer个Block。每个Block里,注意力让各位置互相看、MLP 再逐位置加工。层数越多,模型能捕捉的上下文关系越复杂。 - 收尾(
ln_f+lm_head):最后归一化一下,再用lm_head把每个位置的向量投影成一个长度为vocab_size的分数向量——这就是「下一个 token 是词表里每一个字的可能性打分」。
第一站我们说「GPT 就是猜下一个字」,现在你看到了:所谓「猜」,就是这条流水线最后吐出的那个 logits——65 个(或 50257 个)分数,谁高就更可能是下一个字。
一个 Block 内部长什么样
Block 是叠出深度的那块积木,它的 forward 只有两行,却是整个 Transformer 的招牌结构:
def forward(self, x):
x = x + self.attn(self.ln_1(x))
x = x + self.mlp(self.ln_2(x))
return x三个要点:
- 先归一化,再进子层:
self.ln_1(x)先把x校准,再喂给注意力。这叫 Pre-LN(把 LayerNorm 放在子层前面),比原始 Transformer 的 Post-LN 训练更稳,是现代 GPT 的标配。 - 残差连接
x + ...:注意力/MLP 的输出不是替换x,而是加回到x上。好处是梯度有一条「高速公路」能直通到底,深层网络也不容易「学不动」。你可以把它理解成:每个子层只负责在原有理解上做增量修正,而不是推倒重来。 - 两个子层,各管一摊:
attn负责「跨位置」交换信息(第 5 个 token 去看第 1、2、3 个),mlp负责「单位置」深加工(把每个位置自己的向量揉得更有表达力)。一个横向、一个纵向,交替叠加。
LayerNorm、MLP 这两个零件都不难,我们在后面的章节顺带讲清;真正需要单开一章、逐行细啃的,是那个 self.attn——因果自注意力。
GPTConfig:七个旋钮
模型的「体格」由一个 dataclass 决定,一共七个超参:
@dataclass
class GPTConfig:
block_size: int = 1024
vocab_size: int = 50304 # GPT-2 vocab_size of 50257, padded up to nearest multiple of 64 for efficiency
n_layer: int = 12
n_head: int = 12
n_embd: int = 768
dropout: float = 0.0
bias: bool = True # True: bias in Linears and LayerNorms, like GPT-2. False: a bit better and faster逐个说清它们各自拧的是什么:
| 超参 | 含义 | 拧大了会怎样 |
|---|---|---|
block_size | 上下文长度:一次最多看多少个 token(默认 1024) | 能看更长的上文,但注意力开销随长度平方增长 |
vocab_size | 词表大小:输出层要在多少个候选里选(默认 50304) | 输出层更宽、更吃显存 |
n_layer | 层数:叠多少个 Block(默认 12) | 模型更深、更能抽象,也更慢更难训 |
n_head | 注意力头数:把注意力拆成几路并行(默认 12) | 能同时关注多种关系,详见下一章 |
n_embd | 嵌入维度:每个 token 向量多长(默认 768) | 表达力更强,参数量随它平方涨 |
dropout | 随机失活比例:训练时随机丢弃的比例(默认 0) | 抗过拟合,但预训练大数据时通常设 0 |
bias | Linear / LayerNorm 要不要偏置项 | False 略快、还略好,是 nanoGPT 相对 GPT-2 的小改动 |
一个有意思的细节:默认 vocab_size = 50304,而 GPT-2 的真实词表是 50257。多出来的 47 个是「凑整」——把词表大小补齐到 64 的整数倍,GPU 上的矩阵运算能对齐得更整齐、跑得更快。这些填充位置永远不会被真正用到,纯粹是为了性能。又一个「逐行读才看得见」的工程小心思。
默认这套(n_layer=12, n_head=12, n_embd=768)正好就是 GPT-2 的最小款(124M 参数) 的规格。把这几个数字调大,就是 GPT-2 medium / large / XL——结构一模一样,只是零件尺寸变了。这也再次说明:读懂 nanoGPT,你就读懂了整个 GPT-2 家族的骨架。
下一步
全景图有了,零件清单也齐了。现在,深吸一口气,我们去啃那块最硬的骨头——CausalSelfAttention。它是 GPT 里最关键、也最容易被「示意图」糊弄过去的部分。我们一行都不跳。👉 注意力:CausalSelfAttention 逐行拆解