引言:别人讲概念,我们看真数据

网上讲 Transformer 注意力机制的文章很多。但几乎所有文章都有一个共同的问题:数据是编的

它们会画一个框,写上 “Q”、“K”、“V”,然后说 “Q 是查询向量”、“K 是键向量”、“V 是值向量”——但框里面到底是什么数字?乘完之后变成了什么?softmax 之后注意力权重长什么样?从来不说。

今天我们换个做法。

我们训练一个只有 4192 个参数的迷你 GPT(基于 Karpathy 的 microgpt),喂给它 10000 个英文常用单词,让它学会英语的拼写规律。然后,把这个训练好的模型完全打开——每一个权重矩阵、每一个中间向量、每一个注意力分数,全部用真实数字展示。

本文的所有数字都来自真实训练——不是示意图,不是假数据。你看到的每一个小数点,都是模型在 3000 步训练后的真实参数值。


Demo 速览 — 这不是聊天机器人,不能回答问题。 它做的事情更基础也更纯粹——预测下一个字母。

输入一个英文单词的前几个字母,如 “t”、“th”、“the”
输出下一个字母的概率分布(27 个候选:a-z + 结束符)
目标学会英语拼写规律,能"发明"看起来像真词的新单词
代码量外部依赖硬件要求训练时间
核心 200 行 / Demo ~350 行(纯 Python)普通 CPU,无需 GPU3000 步,~12 分钟

一、先搞清楚:这个模型在做什么?

比如,模型看到 “th” 之后,会输出:

下一个字母的概率:
  'a' → 22.7%    ← that, thank, than, ...
  'o' → 17.0%    ← those, though, ...
  'e' → 16.6%    ← the, them, there, ...
  'i' → 11.4%    ← this, think, third, ...
  ...

这和 ChatGPT 的核心原理完全一样——ChatGPT 也是在预测"下一个 token 最可能是什么",只不过它的 token 是子词而非单个字母,参数是千亿级而非四千个。

实验环境

项目详情
原始代码Karpathy 的 microgpt —— 200 行纯 Python,从零实现 Transformer(含反向传播)
Demo 脚本demo_transformer_internals.py —— 约 350 行,含训练 + 中间数据采集 + 输出
训练数据Google 10000 常用英文单词
模型规模1 层 Transformer,16 维嵌入,4 个注意力头,总共 4192 个参数
硬件普通 CPU(无 GPU),8 核 Intel Xeon,32GB 内存
训练时间3000 步,约 12 分钟
外部依赖——不需要 PyTorch、numpy 或任何第三方库

没错,200 行 Python + 一台普通电脑 + 12 分钟,就能训练出一个完整的 Transformer。这也是为什么选它来做教学——所有东西都小到可以看清。


模型配置

参数microgpt (本文)GPT-2
嵌入维度16768
注意力头数412
每头维度464
层数112
MLP 隐藏层643072
词表大小2750257
总参数4,192~124,000,000

词表很简单:26 个英文字母(a-z)加一个特殊的 <BOS>(Begin of Sequence,序列起始标记)。模型的任务是:给定前面的字母,预测下一个字母是什么。

训练效果

用 10000 个常用英文单词训练 3000 步(约 12 分钟),模型生成的"假词":

conte   tout    stor    costs   ex
derse   alid    ponde   rotins  aron
angir   fure    derits  conen   maliting

其中 conte、tout、stor、costs、ex 都是真词或极其接近真词。其余的如 derse、alid、ponde,虽然不是真词,但完全符合英语的拼写规律——它学到了 “th”、“st”、“ing”、“tion” 这些模式。

4192 个参数,12 分钟训练,就能做到这些。现在,让我们打开引擎盖,看看里面发生了什么。


二、第 1 步:把字母变成向量(Token Embedding)

模型不认识"字母",只认识数字。所以第一步是把每个字母翻译成一个 16 维向量。怎么翻译?查表。

这张表叫 Token Embedding 矩阵(wte),大小是 27 行 x 16 列。每行对应一个字母,每行有 16 个数字。这些数字不是人设计的——它们是训练出来的。

以输入单词 “the” 为例。首先,把它翻译成 token 序列:

<BOS>  →  t  →  h  →  e  →  <BOS>
  26      19     7     4      26

前后各加一个 <BOS> 标记序列的开始和结束。然后查嵌入表:

字母 ’t’ 的嵌入向量(wte 的第 19 行)——这就是 ’t’ 在模型眼中的"样子":

wte['t'] = [ 0.099,  0.107, -0.422,  0.835,  0.152, -0.063,
             0.230,  0.063,  0.169, -0.260,  0.088, -0.251,
             0.415,  0.557, -0.581,  0.013]

16 个浮点数,这就是字母 ’t’ 的全部信息。同样:

wte['h'] = [-0.190, -0.024,  0.020,  0.431, -0.244, -0.080,
            -0.055,  0.212, -0.247,  0.183, -0.130, -0.042,
            -0.158,  0.766, -0.344,  0.105]

wte['e'] = [ 0.305,  0.093,  0.244, -0.408, -0.242, -0.402,
            -0.124,  0.176, -0.345, -0.149,  0.720,  0.176,
             0.412, -0.275,  0.338, -0.281]

加上位置编码

光有"是什么字母"不够,还得知道"在哪个位置"。同一个字母 ’e’ 出现在第 1 位和第 5 位,意义可能完全不同。

所以模型还有一张 位置编码表(wpe),大小 16 x 16(16 个位置,每个位置 16 维)。字母嵌入和位置编码相加,就是模型真正看到的输入。

以位置 1 的 ’t’ 为例:

Token 嵌入 wte['t']:  [ 0.099,  0.107, -0.422,  0.835,  0.152, -0.063, ...]
位置编码   wpe[1]:    [ 0.187,  0.071,  0.037, -0.033, -0.424, -0.085, ...]
──────────────────────────────────────────────────────────────────
相加结果 x:           [ 0.286,  0.179, -0.385,  0.802, -0.272, -0.148, ...]

然后做一次 RMSNorm(归一化,让数值保持在合理范围)。这个 16 维向量 x,就是进入注意力层的输入。


三、第 2 步:QKV 投影——注意力的三件套

现在每个位置都有一个 16 维向量 x。接下来,模型要做一件关键的事:让每个位置"看看"其他位置,决定该关注谁。

这就是注意力机制的核心。它用三组权重矩阵,把每个 x 变成三个不同的向量:

Q = x × W_Q    (Query:  我在找什么?)
K = x × W_K    (Key:    我能提供什么?)
V = x × W_V    (Value:  我的内容是什么?)

W_Q、W_K、W_V 各是一个 16x16 的权重矩阵——它们的每个元素都是训练过程中通过反向传播学到的参数。所以说"W_Q 是参数"和"W_Q 是矩阵"都对:它是一个由可训练参数组成的矩阵

关于符号约定: 本文使用行向量约定,即 Q = x · W_Q(向量 x 在左,矩阵 W 在右),与 Transformer 原始论文和 PyTorch 代码(Q = x @ W_Q)一致。线性代数教科书常用列向量约定 Q = W_Q · x(矩阵在左),两种写法数学上等价,只是矩阵需要转置。

图书馆比喻: 你走进图书馆找书。你手里有一张需求清单(Q)——“我要关于历史的书”。书架上每本书有一个标签(K)——“我是历史类”。清单和标签对上了(Q·K 的点积大),你就把那本书的**内容(V)**取出来。

真实数据:W_Q 权重矩阵长什么样

这是我们训练好的 W_Q 矩阵的前 4 行(一共 16 行,每行 16 个数字,共 256 个参数):

W_Q[0] = [ 0.486, -0.190,  0.927, -0.252, -0.179, -0.152,  0.252,  0.044,
          -0.023, -0.119,  0.209,  0.389,  0.325, -0.083, -0.155, -0.364]
W_Q[1] = [-0.011, -0.067, -0.212,  0.200, -0.450,  0.061, -0.145, -0.091,
          -0.204,  0.306, -0.080,  0.285,  0.486,  0.135, -0.705, -0.325]
W_Q[2] = [ 0.147, -0.066, -0.240, -0.380,  0.110, -0.037, -0.175, -0.254,
           0.295,  0.532, -0.010, -0.093,  0.067,  0.035, -0.263, -0.668]
W_Q[3] = [ 0.074, -0.174,  0.666, -0.313,  0.291, -0.026,  0.022,  0.170,
           0.014, -0.041, -0.053,  0.087,  0.061, -0.058,  0.182, -0.600]
... (共 16 行)

这些数字是随机初始化的,然后在 3000 步训练中被反向传播慢慢调整,最终定型。不是人设计的,是"长出来的"。

真实数据:Q、K、V 投影结果

输入 “the”,每个位置经过 QKV 投影后得到的向量:

位置 0 — <BOS>:
  Q = [ 1.473,  1.605,  3.272,  2.116, -1.330, -0.985, -0.713, -2.096, ...]
  K = [-1.639,  0.890,  0.309, -1.354, -0.483, -0.746,  0.623,  0.189, ...]
  V = [ 0.092,  0.062, -0.102,  0.093,  0.159,  0.113,  0.083,  0.206, ...]

位置 1 — 't':
  Q = [ 0.025,  3.516,  1.232, -1.403, -1.888, -1.277,  1.129,  0.560, ...]
  K = [ 1.219,  1.326,  0.912,  1.092,  0.358,  0.026,  0.005, -0.851, ...]
  V = [-0.029, -0.419,  0.622,  0.132, -0.165, -0.262, -0.168,  0.210, ...]

位置 2 — 'h':
  Q = [ 1.177,  1.676,  0.221, -0.288,  0.026,  0.406,  0.313,  0.439, ...]
  K = [ 1.960,  3.061,  0.669,  0.206,  0.155,  0.619,  0.074, -0.846, ...]
  V = [ 0.346,  0.445, -0.499, -0.237, -0.390, -0.029, -0.190, -0.076, ...]

位置 3 — 'e':
  Q = [ 2.605,  1.533,  1.134,  1.452,  0.315,  0.296, -1.225, -0.970, ...]
  K = [-1.295, -0.088, -1.527, -1.227,  0.858,  0.607,  0.312,  1.019, ...]
  V = [ 0.150,  0.646,  0.456,  0.297,  0.891,  0.388,  0.468,  0.692, ...]

注意:同一个字母 ’t’,经过不同的权重矩阵后,Q、K、V 三个向量完全不同。这就是矩阵乘法的魔力——同一份信息,从三个不同角度被"提问"。


四、第 3 步:多头注意力——4 个头各管一片

Q、K、V 各是 16 维向量。现在要用它们计算注意力。但 Transformer 不会直接用 16 维去算,而是拆成 4 个头,每个头只看 4 维。

Q (16维) → 拆成 4 份:
  头0: Q[0:4]   = [2.605,  1.533,  1.134,  1.452]
  头1: Q[4:8]   = [0.315,  0.296, -1.225, -0.970]
  头2: Q[8:12]  = [-0.477, 0.751, -1.333,  0.168]
  头3: Q[12:16] = [0.505,  0.500, -2.082, -1.242]

K 和 V 同理,每个头 4 维。

为什么要拆? 因为不同的头可以学到不同的"关注模式"。一个头可能学会关注前一个字母,另一个头可能学会关注更远处。多角度看问题,比单一视角更强。

注意力计算:三步走

以位置 3(字母 ’e’)的头 0 为例。’e’ 是序列中最后一个字母,它可以看到所有前面的 token:<BOS>、t、h、e。

Step 1:打分(Q · K)

’e’ 的 Query 和每个历史位置的 Key 做点积,再除以 √d_k = √4 = 2。这个 √d_k 缩放是固定的,不是可调参数——它的作用是防止点积值随向量维度增大而变得过大,导致 softmax 输出趋向极端(全 0 或全 1):

Q_e(头0) = [2.605, 1.533, 1.134, 1.452]

Q_e · K_<BOS> / √4 = -2.261   ← 和 <BOS> 不太相关
Q_e · K_t     / √4 =  3.914   ← 和 't' 比较相关
Q_e · K_h     / √4 =  5.428   ← 和 'h' 非常相关!
Q_e · K_e     / √4 = -3.512   ← 和自己不太相关

Step 2:归一化(softmax)

把分数转成 0 到 1 之间的概率,总和为 1:

softmax([-2.261, 3.914, 5.428, -3.512])

=  [0.0%,  18.0%,  81.9%,  0.0%]
    <BOS>    t       h       e

→ 头 0 把 81.9% 的注意力放在了 'h' 上!

Step 3:加权求和(× V)

用注意力权重对各位置的 Value 加权求和,得到这个头的输出:

头 0 输出 = 0.0% × V_<BOS> + 18.0% × V_t + 81.9% × V_h + 0.0% × V_e

         ≈ V_h    (因为 'h' 的权重占了 82%)

这意味着:当模型处理 ’e’ 时,头 0 主要提取了 ‘h’ 的信息。为什么?因为在英语中 “he” 是一个极其常见的组合——模型学到了 “看到 e,回头找 h”。

4 个头的完整注意力(“the” 中的 ’e’)

<BOS>the解读
头00%18%82%0%紧盯 ‘h’,捕捉 “he” 模式
头112%37%37%14%均匀看 ’t’ 和 ‘h’,综合判断
头27%13%72%8%也紧盯 ‘h’,强化信号
头319%51%28%1%更关注 ’t’,看更远的上下文

看到了吗?4 个头自动学到了不同的策略:头 0 和头 2 聚焦紧邻的 ‘h’,头 3 却去看更远的 ’t’。没人教它们这样分工——这是 3000 步训练后自然"涌现"的行为。

注意力热力图(完整)

下面展示头 0 在处理 “the” 时,每个位置看向哪里(行=当前位置,列=被关注的位置):

头 0 注意力权重:
           <BOS>     t      h      e
  <BOS>  [100% ]
    't'   [ 63%   37%  ]
    'h'   [  2%   12%   86%  ]
    'e'   [  0%   18%   82%    0%  ]

注意对角线的规律:

  • ‘h’ 把 86% 的注意力给了自己——在单独出现时,最重要的信息就是自身
  • ’e’ 把 82% 给了 ‘h’——学到了 “he” 这个强组合
  • 下三角形状说明这是因果注意力(causal attention):每个 token 只能看到前面的,不能偷看后面的

不同单词,不同的注意力模式

换个单词看看。输入 “and”,看 ’d’ 位置的注意力:

<BOS>and解读
头03%5%91%2%几乎只看 ’n’!
头16%21%62%12%也盯着 ’n’
头217%3%63%17%还是 ’n’
头37%0%68%25%同上

处理 “and” 时,所有 4 个头都集中关注 ’n’(62%~91%)。为什么?因为 “nd” 是英语中极其常见的双字母组合(and, end, find, hand, kind…),模型学到了 “看到 n 后面大概率跟 d” 这个模式。

再看 “cat” 中处理 ’t’ 时:

头0: c=87%  a=4%  → 几乎只看 'c'
头3: t=61%  a=24% → 主要看自己

头 0 学到了 “回头看首字母” 的策略——对于判断单词结尾(是否该停下来)非常关键。

拼接 + 输出投影

4 个头各输出 4 维,拼接成 16 维,再乘以一个 W_O 矩阵(16x16)做输出投影,最后加上残差连接(直接把注意力之前的 x 加回来)。

4个头拼接 (16维) → × W_O → 注意力输出 (16维) → + x (残差) → 进入 MLP

残差连接的作用: 即使注意力算出的结果不好,原始信息 x 也不会丢失——总有一条"直通车"保底。这让深层网络的训练变得可能。


五、第 4 步:MLP——注意力之后的"思考"

注意力层负责"看到什么"——从其他位置收集信息。MLP 层负责"怎么理解"——对收集到的信息进行非线性变换。

结构很简单:先升维,再激活,再降维。

x (16维) × W_fc1 (16×64) → hidden (64维)    ← 升维 4 倍
ReLU(hidden)                                   ← 非线性激活
hidden (64维) × W_fc2 (64×16) → out (16维)    ← 降维回来

ReLU:极度稀疏的激活

ReLU 函数很简单:正数不变,负数变成 0。效果是什么?

输入单词最后位置 ReLU 激活激活率
“the” → ’e’2 / 64 个神经元3%
“cat” → ’t’0 / 64 个神经元0%
“stop” → ‘p’1 / 64 个神经元2%
“and” → ’d’0 / 64 个神经元0%

64 个神经元中,通常只有 0~2 个被激活!这叫稀疏激活——模型学会了只让极少数"专家"神经元参与每次计算。

比如处理 “the” 中的 ’e’ 时,64 个神经元中只有 #2(值 0.554)和 #57(值 1.107)被激活。可以把它们想象成两个"专家":一个可能负责检测 “元音在 h 后面” 的模式,另一个可能负责检测 “the 快要结束了” 的信号。

现实类比: 大型模型中也是如此。GPT-2 的 MLP 有 3072 个神经元,但对于任何给定输入,通常只有一小部分被激活。这也是"混合专家"(MoE)架构的理论基础——既然大部分神经元都不活跃,不如直接只激活其中一部分来节省计算。


六、第 5 步:预测下一个字符

经过嵌入 → 注意力 → MLP 之后,每个位置有一个 16 维的输出向量。最后一步是把它映射回词表空间:

x (16维) × lm_head (16×27) → logits (27维) → softmax → 概率分布

输出是 27 个概率值(对应 26 个字母 + <BOS>),表示模型认为下一个字符最可能是什么。

Temperature(温度)在这里: 人们说"调 ChatGPT 的温度",调的就是这一步输出 softmax 的温度,而不是前面注意力层的 √d_k 缩放。具体做法是在 softmax 之前把 logits 除以温度 T:softmax(logits / T)。T < 1 让分布更尖锐(更确定),T > 1 让分布更平坦(更随机)。注意力层里的 √d_k 是固定的数学缩放,和这里的温度是完全不同的概念。

真实预测结果

输入 “the”,看模型在每个位置的预测:

当前字符正确答案Top-3 预测正确答案排名
<BOS>ts(9.1%), c(8.9%), p(8.0%)第 5 (6.6%)
thr(16.5%), o(14.9%), e(14.8%)第 7 (7.0%)
hea(22.7%), o(17.0%), e(16.6%)第 3
e<BOS>(结束)r(12.1%), n(11.2%), s(7.9%)第 4 (7.8%)

几个有趣的观察:

  • <BOS> → 预测首字母: s、c、p 排前三——因为英语中以 s、c、p 开头的单词确实最多
  • t → 预测 h 只排第 7: 因为 t 后面可以跟 r(tr-)、o(to-)、e(te-)、i(ti-)、a(ta-)……选择太多了。但 7.0% 已经不低
  • h → 预测 e 排第 3(16.6%): 这里注意力的效果显现了——前一步模型看到 t→h 组合后,成功提升了 ’e’ 的概率
  • e → 预测结束排第 4: 模型知道短词应该结束,但也考虑了 er-、en-、es- 等常见后缀

记住,这是一个只有 4192 个参数、训练了 12 分钟的模型。它已经掌握了英语拼写的大量统计规律。


七、全景图:一个 Token 的完整旅程

把所有步骤串在一起:

  输入: 字母 'e' (id=4, 在 "the" 的位置 3)
    |
    v
  查表: wte[4] → 16 维向量
    + wpe[3] → 加上位置信息
    → RMSNorm → 归一化
    |
    v
  +------ 多头注意力 -------------------------+
  |                                            |
  |  × W_Q → Q (16维): "我在找什么?"           |
  |  × W_K → K (16维): "我能提供什么?"         |
  |  × W_V → V (16维): "我的内容是什么?"       |
  |                                            |
  |  拆成 4 个头 (每头 4 维):                   |
  |    头0: 82% 看 'h'  → 捕捉 "he" 模式       |
  |    头1: 37% 看 't' + 37% 看 'h'            |
  |    头2: 72% 看 'h'  → 强化 "he" 信号       |
  |    头3: 51% 看 't'  → 看更远的上下文        |
  |                                            |
  |  拼接 → × W_O → 注意力输出 (16维)          |
  +--------------------------------------------+
    + 残差连接 (加上原始 x)
    |
    v
  +------ MLP (前馈网络) ----------------------+
  |                                            |
  |  16维 → × W_fc1(16×64) → 64维 (升维 4 倍)    |
  |  ReLU: 64 个神经元中只有 2 个被激活 (3%)    |
  |  64维 → × W_fc2(64×16) → 16维 (降维)        |
  |                                            |
  +--------------------------------------------+
    + 残差连接
    |
    v
  × lm_head → 27 维 logits → softmax(logits/T) → 概率分布
    |
    v
  输出: r(12.1%), n(11.2%), s(7.9%), <BOS>(7.8%), ...

整个流程涉及的矩阵乘法:

操作矩阵大小参数量
Token 嵌入 (wte)27 x 16432
位置编码 (wpe)16 x 16256
W_Q, W_K, W_V, W_O (各 16x16)4 × (16 x 16)1024
MLP W_fc116 x 641024
MLP W_fc264 x 161024
输出头 (lm_head)16 x 27432
总计4,192

4192 个浮点数,就是这个模型的全部"知识"。没有规则,没有词典,没有语法书——只有 4192 个在训练中被反复微调的数字。


八、从 4192 到 1750 亿:只有规模变了

这是本文最想传达的观点:

GPT-4 和我们这个 4192 参数的模型,用的是完全相同的算法。

同样的 Token Embedding,同样的 QKV 投影,同样的多头注意力,同样的 MLP,同样的残差连接,同样的 softmax。区别只是数字更大了。

本文 demoGPT-2GPT-4 级别
嵌入维度16768~12288
注意力头412~96
层数112~120
词表2750257~100000
总参数4,1921.24 亿~1750 亿
训练时间12 分钟~1 周~数月
能力发明假词写段落理解与推理

这就是深度学习最深刻的特性之一:简单的算法 + 巨大的规模 = 涌现的智能

我们在 4192 个参数中看到注意力头自动分工,看到 MLP 稀疏激活,看到模型学会英语拼写模式。同样的机制放大几万倍,就产生了 ChatGPT 的"理解"和"推理"能力。

核心算法并不神秘。


附录:公式速查

对于想看公式的读者,以下是本文涉及的所有核心计算:

1. 嵌入:     x = wte[token_id] + wpe[position]

2. QKV 投影:  Q = x · W_Q,   K = x · W_K,   V = x · W_V
              (行向量约定: x 在左, W 在右, 与 PyTorch x @ W 一致)

3. 注意力:    score = Q · K^T / √d_k        ← √d_k 是固定缩放, 不是温度
              weight = softmax(score)
              output = weight · V

4. 多头拼接:  MultiHead = Concat(head_0, ..., head_3) · W_O

5. MLP:       hidden = ReLU(x · W_fc1)
              output = hidden · W_fc2

6. 残差连接:  x = x + Attention(x)
              x = x + MLP(x)

7. 预测:      logits = x · lm_head
              probs  = softmax(logits / T)   ← T 就是 temperature (温度)

就这些。没有更多了。


即将发布:视频号 Demo 全过程演示

文字毕竟是静态的。接下来,我会发布一期微信视频号视频,在真实终端上,带你从零走完整个过程:

  • 一行命令启动训练,实时看 loss 从 3.3 降到 2.3
  • 模型从"胡乱拼字母"到"发明像样的假词"的进化过程
  • 逐步展示每一个中间向量——嵌入、QKV、注意力权重、MLP 激活
  • 对照本文的数据,手把手验证每个计算

200 行代码,12 分钟训练,从头到尾没有黑箱。

关注公众号 “AI-lab学习笔记”,第一时间收到视频推送!