本文构造一个 极小但完整 的 Transformer 语言模型, 把 每一个公式、每一个矩阵、每一个向量、每一次乘法和加法的中间结果 全部展开, 让你能用一支笔在纸上跟着把整个「下一个 token」的推理过程从头算到尾,然后再看清训练时梯度从哪里产生、权重如何更新。
大型语言模型 (GPT-4, Llama, Qwen…) 在结构上和这里的迷你模型 本质上完全一致,只是数字更多、层数更深。把这里的过程算明白,就掌握了所有大模型最底层的「计算物理」。
给模型输入两个汉字 我爱,让它输出 下一个最有可能的字。期望的正确答案是 你。
| 符号 | 含义 | 数值 |
|---|---|---|
| V | 词表大小 (vocab size) | 6 |
| d | 嵌入维度 / 隐藏维度 (d_model) | 2 |
| h | 注意力头数 (n_heads) | 1 |
| d_k | 每个头的维度 (=d/h) | 2 |
| d_ff | FFN 隐藏维度 | 4 |
| L | Transformer 层数 | 1 |
| T | 输入序列长度 | 2 |
真实大模型有几十亿到上万亿参数,我们这个迷你模型的全部参数清单如下:
| 名称 | 形状 | 用途 | 参数数 |
|---|---|---|---|
| E | V × d = 6×2 | 词嵌入(Token Embedding),输入和输出共享(权重绑定) | 12 |
| W_Q | d × d = 2×2 | 把隐藏向量投影成 Query | 4 |
| W_K | d × d = 2×2 | 把隐藏向量投影成 Key | 4 |
| W_V | d × d = 2×2 | 把隐藏向量投影成 Value | 4 |
| W_1, b_1 | 2×4, 4 | FFN 第一层线性变换 | 12 |
| W_2, b_2 | 4×2, 2 | FFN 第二层线性变换 | 10 |
所有大模型的第一步,就是把人类的字符变成机器能算的数字。这个映射表叫做「词表」(Vocabulary)。
我们手工设计一个包含 6 个 token 的词表 (真实 GPT 的词表是 ~10 万级别)。
| Token ID | 字符 | 含义 |
|---|---|---|
| 0 | <pad> | 填充符 |
| 1 | <bos> | 句首标记 |
| 2 | 我 | — |
| 3 | 爱 | — |
| 4 | 你 | 这是我们希望模型预测的字 |
| 5 | 他 | — |
现在我们把输入字符串「我爱」交给 tokenizer,看它如何变成机器可以矩阵乘法的形式。
One-hot 编码:每个 token ID 变成一个长度为 V 的向量,只有对应位置为 1,其余为 0。整个输入序列就是 T × V = 2 × 6 的矩阵 Xoh。
| pad(0) | bos(1) | 我(2) | 爱(3) | 你(4) | 他(5) | |
|---|---|---|---|---|---|---|
| 位置 t=0(我) | 0 | 0 | 1 | 0 | 0 | 0 |
| 位置 t=1(爱) | 0 | 0 | 0 | 1 | 0 | 0 |
E[token_id])直接取出对应行而已。
所以,把它当作概念上的「向量化」第一步,非常重要。
每个 token ID 需要变成一个 d 维的实数向量(嵌入向量),这样模型才能在连续空间里做几何变换。
嵌入矩阵 E 的形状为 V × d = 6 × 2,第 i 行就是 token ID = i 的嵌入向量。下面这些数值是 随机初始化的具体值(真实训练中由优化器逐步学习得到):
| Token | dim 0 | dim 1 |
|---|---|---|
| 0 <pad> | 0.0 | 0.0 |
| 1 <bos> | 0.1 | 0.2 |
| 2 我 | 0.5 | 0.8 |
| 3 爱 | 0.7 | 0.3 |
| 4 你 | 0.2 | 0.9 |
| 5 他 | 0.6 | 0.4 |
由于 Xoh 的每一行是 one-hot,矩阵乘法的效果就是 「挑出 E 的第 2 行和第 3 行」:
| 位置 | dim 0 | dim 1 |
|---|---|---|
| t=0 (我) | 0.5 | 0.8 |
| t=1 (爱) | 0.7 | 0.3 |
单看嵌入,「我爱」和「爱我」是同一组向量集合 — Transformer 不知道顺序。所以要给每个位置加一个独特的「位置签名」。
当 d=2,只有一对 (sin, cos):i=0,所以分母 = 100000/2 = 1。
| 位置 | dim 0 | dim 1 |
|---|---|---|
| t=0 (我) | 0.500 | 1.800 |
| t=1 (爱) | 1.541 | 0.840 |
这个 H⁽⁰⁾ 就是 Transformer 第一层的真正输入。下面进入核心 — 自注意力。
这一节我们把 Q、K、V 怎么算、注意力分数怎么算、Softmax 怎么算、为什么要除以 √d_k、因果掩码怎么加 — 全部用数字走一遍。
权重矩阵都是 d × d = 2×2,数值是我们提前指定的(代表训练中某个时刻的状态):
| 0.3 | 0.4 |
| 0.5 | 0.2 |
| 0.2 | 0.6 |
| 0.4 | 0.1 |
| 0.1 | 0.5 |
| 0.7 | 0.3 |
对于 h0 = (0.500, 1.800):
对于 h1 = (1.541, 0.840):
| 位置 | q dim 0 | q dim 1 |
|---|---|---|
| t=0 | 1.050 | 0.560 |
| t=1 | 0.882 | 0.784 |
| 位置 | k dim 0 | k dim 1 |
|---|---|---|
| t=0 | 0.820 | 0.480 |
| t=1 | 0.644 | 1.009 |
| 位置 | v dim 0 | v dim 1 |
|---|---|---|
| t=0 | 1.310 | 0.790 |
| t=1 | 0.742 | 1.023 |
每个分数 Si,j 是「位置 i 的 Query 与位置 j 的 Key 的点积」,代表「位置 i 想从位置 j 那里拿走多少信息」。
语言模型(GPT)是 自回归 的 — 预测第 i 个 token 时只能看 ≤ i 的位置。所以把 S 矩阵的上三角(包括严格上三角)设为 -∞,这样 Softmax 之后这些位置的权重就是 0。
| 看 t=0 | 看 t=1 | |
|---|---|---|
| t=0 query | 0.799 | -∞ (masked) |
| t=1 query | 0.777 | 0.961 |
| 权重在 t=0 | 权重在 t=1 | |
|---|---|---|
| t=0 query | 1.000 | 0.000 |
| t=1 query | 0.454 | 0.546 |
| 位置 | dim 0 | dim 1 |
|---|---|---|
| t=0 | 1.310 | 0.790 |
| t=1 | 1.000 | 0.917 |
注意力的输出不能直接送进下一层,要先加回原输入(残差),再做归一化(LayerNorm)。这两个技巧让深层网络得以训练。
我们设 γ = (1, 1), β = (0, 0), ε = 10⁻⁵。
| 位置 | dim 0 | dim 1 |
|---|---|---|
| t=0 | -1.000 | 1.000 |
| t=1 | 1.000 | -1.000 |
注意力让 token 之间 交换信息;FFN 让 每个 token 自己 经过一个非线性「思考」。它通常占 Transformer 一半以上的参数。
| 0.3 | 0.1 | -0.2 | 0.4 |
| 0.2 | -0.3 | 0.5 | 0.1 |
| 0.1 | 0.1 | 0.1 | 0.1 |
| 0.3 | 0.5 |
| 0.4 | -0.2 |
| -0.1 | 0.6 |
| 0.2 | 0.3 |
| 0.0 | 0.0 |
| 位置 | dim 0 | dim 1 |
|---|---|---|
| t=0 | -0.080 | 0.480 |
| t=1 | 0.340 | 0.120 |
和 §6 完全同样的操作,只是这次加在 FFN 输出上。
| 位置 | dim 0 | dim 1 |
|---|---|---|
| t=0 | -1.000 | 1.000 |
| t=1 ← 这一行用于预测! | 1.000 | -1.000 |
把最后一个位置的 d 维向量,变成一个 V 维的「打分向量」,每一维对应词表中的一个 token。
GPT 等模型常常 复用嵌入矩阵 E 的转置 作为输出投影 — 既省参数又有助训练。
| pad | bos | 我 | 爱 | 你 | 他 | |
|---|---|---|---|---|---|---|
| dim 0 | 0.0 | 0.1 | 0.5 | 0.7 | 0.2 | 0.6 |
| dim 1 | 0.0 | 0.2 | 0.8 | 0.3 | 0.9 | 0.4 |
hfinal = (1.000, -1.000),每个 logit 是它和 E 中对应行的点积:
| Token | logit |
|---|---|
| pad | 0.000 |
| bos | -0.100 |
| 我 | -0.300 |
| 爱 | 0.400 (最大) |
| 你 (目标) | -0.700 |
| 他 | 0.200 |
logits 可以是任意实数;Softmax 把它们压成 (0, 1) 之间且加起来等于 1 的概率分布。
| Token | logit | exp(logit) | p (=exp / 5.856) | 百分比 |
|---|---|---|---|---|
| pad | 0.000 | 1.000 | 0.171 | 17.1% |
| bos | -0.100 | 0.905 | 0.155 | 15.5% |
| 我 | -0.300 | 0.741 | 0.127 | 12.7% |
| 爱 | 0.400 | 1.492 | 0.255 | 25.5% |
| 你 (目标) | -0.700 | 0.497 | 0.085 | 8.5% |
| 他 | 0.200 | 1.221 | 0.209 | 20.9% |
有了 6 个字的概率分布,现在要选一个作为输出。不同采样策略会带来不同风格。
T=1 是原始 softmax;T > 1 让分布更平、更随机;T < 1 让分布更尖、更确定。
| 温度 T | p(爱) | p(你) | p(他) | 分布特征 |
|---|---|---|---|---|
| T = 0.5 | 0.396 | 0.045 | 0.266 | 更尖锐(强化最大值) |
| T = 1.0(默认) | 0.255 | 0.085 | 0.209 | 原始分布 |
| T = 2.0 | 0.205 | 0.118 | 0.187 | 更平(增加随机性) |
| T → ∞ | 0.167 | 0.167 | 0.167 | 均匀分布 |
Top-k=3: 保留概率最高的 3 个 token (爱 25.5%、他 20.9%、pad 17.1%) 重新归一化为 (0.405, 0.332, 0.272),再从这 3 个采样。
Top-p=0.5: 按概率从大到小累加,直到累计 ≥ 0.5,只在这部分采样。我们这里前两个累计 0.464,加上第三个达到 0.635,所以从前三个里采样。
假设采样返回 token ID = 4,我们查 §1 的词表:
把这个字符追加在原文后面,得到 「我爱你」。如果是连续生成(autoregressive),把它再喂回模型,重新算 §1~§11,得到下一个字…直到出现 <eos> 或达到长度上限。
推理是用「冻结」的权重往前算;训练是 同样的前向 + 计算 Loss + 反向传播梯度 + 更新权重。我们用刚刚的预测结果当作训练样本走一遍。
交叉熵 + Softmax 组合有一个非常优雅的导数:
其中 y 是 one-hot 标签 (这里 y4=1,其他都是 0)。
| Token | pi | yi | 梯度 ∂L/∂logiti = pi - yi |
|---|---|---|---|
| pad | 0.171 | 0 | +0.171 |
| bos | 0.155 | 0 | +0.155 |
| 我 | 0.127 | 0 | +0.127 |
| 爱 | 0.255 | 0 | +0.255 |
| 你 (目标) | 0.085 | 1 | -0.915 |
| 他 | 0.209 | 0 | +0.209 |
由于权重绑定,logiti = hfinal · Ei,因此:
取 hfinal = (1.000, -1.000):
| Token | ∂L/∂E (dim 0) | ∂L/∂E (dim 1) |
|---|---|---|
| pad | +0.171 | -0.171 |
| bos | +0.155 | -0.155 |
| 我 | +0.127 | -0.127 |
| 爱 | +0.255 | -0.255 |
| 你 (目标) | -0.915 | +0.915 |
| 他 | +0.209 | -0.209 |
对每个参数 θ,梯度通过链式法则一层一层往回传:
PyTorch 的 loss.backward() 一行就完成了上面整条链,内部就是反向遍历每一步,把上一步的梯度乘以这一步的雅可比矩阵。
一图回顾「我爱」→「你」这场完整的数学之旅。
| 步骤 | 输入 | 操作 | 输出形状 | 核心数值(t=1 位置) |
|---|---|---|---|---|
| §2 分词 | "我爱" | 查词表 | [2] (T) | [2, 3] |
| §3 嵌入 | [2, 3] | E 查行 | 2×2 | (0.7, 0.3) |
| §4 位置编码 | X | + PE | 2×2 | (1.541, 0.840) |
| §5 自注意力 | H⁽⁰⁾ | Q·Kᵀ/√d → softmax → ·V | 2×2 | (1.000, 0.917) |
| §6 残差+LN | H⁽⁰⁾, Attn | 加,归一化 | 2×2 | (1.000, -1.000) |
| §7 FFN | H_ln1 | 线性→ReLU→线性 | 2×2 | (0.340, 0.120) |
| §8 残差+LN | H_ln1, FFN | 加,归一化 | 2×2 | (1.000, -1.000) |
| §9 LM Head | h_final | ·Eᵀ | 6 (V) | [0.0,-0.1,-0.3,0.4,-0.7,0.2] |
| §10 Softmax | logits | 归一化指数 | 6 (V) | p(你)=0.085 |
| §11 采样 | p | argmax / 温度 | 1 | (未训练) 选到「爱」 |
| 维度 | 本文迷你模型 | GPT-4 级别 | 倍数 |
|---|---|---|---|
| 词表 V | 6 | ~100,000 | ~17,000× |
| 嵌入维度 d | 2 | ~12,288 | ~6,000× |
| 层数 L | 1 | ~120 | 120× |
| 注意力头数 h | 1 | ~96 | 96× |
| 总参数 | 38 | ~1.8 万亿 | ~10¹¹× |
| 训练 token 数 | 1(教学样本) | ~13 万亿 | — |
| 数学公式 | 完全相同 | 完全相同 | 1× |
把本文的所有公式用 Python + NumPy 写出来,把所有数字对上 — 100 行代码不到。然后:
pip install torch
git clone https://github.com/karpathy/nanoGPT
# 用 200 行 PyTorch 跑一个会写莎士比亚的 GPT