小王算钱

Interview Prep · ML Infra

模型架构:
从 N-gram 到 Transformer

先用一条时间线看每一种架构在解决上一种的什么问题,然后把 Transformer 拆成十步,每一步都有图,attention 可以亲手算。后面有形状和参数量、对比表、Staff 级的面试题、一个能跑的练习和一页速查表。

时间线

从数数到 attention,每一步在解决上一步的什么问题

  1. 出处

    Google,论文《Attention Is All You Need》,Vaswani 等八位作者

    它要解决的问题

    RNN 不能并行,训练慢;远处的词之间信息要一步步传。当时的任务是机器翻译。
    一个 Transformer 层(decoder-only,现代写法) 的结构,数据从上往下流
    • 输入每个 token 一个向量
    • 第一个子层
      • Norm
      • masked self-attention每个位置只能看自己和前面
      • 加回输入残差连接
    • 第二个子层
      • Norm
      • MLP先放大到 4 倍宽,再缩回来
      • 加回输入残差连接
    • 输出形状和输入一样,交给下一层

    怎么工作

    • self-attention:每个词直接看句子里所有的词,按相关程度把它们的信息加权拿过来。任意两个词之间只隔一步。
    • 所有位置的计算互不依赖,可以一次算完,适合 GPU。
    • attention 本身不知道词的顺序,所以要给每个位置加上位置编码。
    • 每一层是:attention,然后一个 MLP。两者外面都有残差连接和 LayerNorm。
    • 原始论文是 encoder 加 decoder。base 模型 6 层,向量 512 维,8 个头,约 6500 万参数。

    训练的过程

    1. 把一段文本的所有位置一次送进去。
    2. 用 mask 保证每个位置看不到后面。
    3. 每个位置都预测下一个词。
    4. 把所有位置的误差加起来,更新参数。

    推理的过程

    1. 把已有的文本送进去,得到最后一个位置对下一个词的概率。
    2. 按概率选一个词。
    3. 把它接到末尾,再算一次。
    4. 之前算过的 key 和 value 存在缓存里,不用重算。

    优点

    • 所有位置同时算,训练能充分利用 GPU。
    • 任意两个词之间只隔一步,长距离依赖容易学。
    • 结构统一,模型和数据越大效果越好。

    缺点

    • attention 的计算量和序列长度的平方成正比,长文本很贵。
    • 没有内置的顺序概念,要靠位置编码补。
    • 它自带的假设少,需要大量数据才能发挥出来。在图像上,数据少的时候不如 CNN。

    用在哪里,现在还在哪里用

    现在几乎所有的大语言模型、多数视觉和语音模型。

    被什么取代,为什么

    没有被取代。新的结构多数是在它上面改,或者和它混合使用。

    面试时,一句话

    只用 attention 的序列模型:并行计算,任意两个位置直接相连。

    面试时,两分钟

    Transformer 把 RNN 拿掉,只用 self-attention。每个词直接看所有其他的词,按相关程度取信息,所以任意两个词之间只隔一步,而且所有位置可以同时算。因为 attention 不知道顺序,要另外加位置编码。每一层是 attention 加 MLP,外面有残差连接和归一化。代价是计算量和长度的平方成正比。它成为主流,是因为并行让它能在大数据上训练,而它的效果随规模持续变好。

    值得补一句的

    下面有一整节把它拆开讲。面试里最常被追问的是三件事:为什么要除以根号 d,为什么需要位置编码,以及计算量为什么是平方。

先懂这五个概念

整条时间线都在这几件事上做取舍

token 和 embedding
文本先切成 token(词或者词的一部分),每个 token 查表变成一个向量。
模型只能算数字。之后所有的计算都在这些向量上进行,模型的「宽度」d_model 就是这个向量的维数。
上下文能看多远
预测一个词的时候,模型能用到多远以前的信息。
N-gram 是几个词,RNN 理论上无限但实际记不住,Transformer 是整个上下文窗口。这条线贯穿了整个时间线。
能不能并行
一句话里的各个位置,是必须一个接一个算,还是可以同时算。
GPU 擅长同时算很多东西。RNN 必须顺序算,Transformer 可以同时算,这是它能用上大数据的直接原因。
残差连接
每一层的输出 = 这一层的输入 + 这一层算出来的修改量。
它给梯度留了一条直通路,没有它,几十层的网络训练不起来。可以把整个模型想成一条主线,每层往上面加一点东西。
自回归
一次生成一个 token,把它接到输入后面,再生成下一个。
这是 GPT 类模型生成文本的方式,也是它输出越长越慢、需要 KV cache 的原因。

重点 · 先看论文里那张图

2017 年的经典结构:encoder 加 decoder

这是《Attention Is All You Need》的图 1,面试和教材里说「Transformer 的结构」,指的通常就是它。当时的任务是翻译:左边的 encoder 读原文,右边的 decoder 写译文。图从下往上看。

encoder:读原文decoder:写译文下一个词的概率Softmax把分数变成概率Linear词表里每个词一个分数重复 N 次(论文里 N = 6)Add & Norm加上残差,再归一化Feed ForwardMLPAdd & Norm加上残差,再归一化Multi-Head Attentioncross-attention:看 encoder 的输出Add & Norm加上残差,再归一化Masked Multi-Head Attentionself-attention:只能看前面重复 N 次Add & Norm加上残差,再归一化Feed ForwardMLPAdd & Norm加上残差,再归一化Multi-Head Attentionself-attention:前后的词都能看encoder 的输出:原文每个词一个向量+Positional Encoding(位置编码)+Positional Encoding(位置编码)Input Embedding原文的词变成向量Output Embedding译文的词变成向量输入:原文我 爱 猫输入:已经写出来的译文,前面加一个开始符号<开始> I love

图比较宽,可以左右滑动。

论文的原图里,每个 Add & Norm 旁边还画了一条绕过子层的线,那就是残差连接。这里为了清楚没有画,写在了方框的说明里。

翻译一句话,数据怎么走

  1. 原文「我 爱 猫」从左下角进去,变成向量,加上位置编码。
  2. 经过 6 层 encoder。每层先做 self-attention(三个词互相看),再过一个 MLP。出来的是三个向量,每个词一个,已经带上了整句话的信息。encoder 只算这一次。
  3. decoder 从右下角开始。第一步的输入只有一个「开始」符号。
  4. 每层 decoder 做三件事:先在已经写出来的词之间做 self-attention,带 mask,只能看前面;再做 cross-attention,回头看 encoder 输出的那三个向量;最后过 MLP。6 层 decoder 看的都是 encoder 最后一层的输出。
  5. 最上面的 Linear 和 Softmax 给出下一个词的概率,从中选出「I」。论文里用的是 beam search,同时保留几个候选,不是每一步只取概率最大的。
  6. 把「I」接到 decoder 的输入后面,重复第 4、5 步,直到输出结束符号。

cross-attention 和 self-attention 的区别

公式完全一样,区别只在 Q、K、V 从哪来。self-attention 里三者来自同一串词。cross-attention 里,Q 来自 decoder 这一层里每个位置的向量(刚做完带 mask 的 self-attention),K 和 V 来自 encoder 的输出。意思是:decoder 拿着「我现在要写什么」去原文里找相关的词。这就是 2014 年那个 attention 的 Transformer 版本,下面「追问」的第 4 条有动图。

图里的每个零件,在下面哪一步讲

图里的零件在十步里的位置现在的 LLM 里
Input / Output Embedding第 2 步保留,只剩一张表
Positional Encoding第 3 步多数公开的模型换成了 RoPE
encoder 的 Multi-Head Attention第 4、5 步,不带 mask没有 encoder,这部分去掉了
Masked Multi-Head Attention第 4、5、9 步保留,整个模型只剩这一种 attention
decoder 中间的 Multi-Head Attention上面这一小节没有 encoder 可看,去掉了
Add & Norm第 6 步保留,Norm 挪到了子层前面
Feed Forward第 7 步保留,有的模型换成 MoE
Linear 和 Softmax第 8 步保留

把这张图的左半边和 decoder 中间那个 attention 去掉,剩下的就是现在大语言模型的骨架,零件的改动见上表。下面的十步按这个精简后的结构来讲,因为零件更少,而且是现在实际在用的形状。每一步讲的零件和这张图里的是同一个。

重点 · Transformer 拆开看

十步,从一句话到下一个 token

1先看整体:一个 decoder-only 的 Transformer

现在的大语言模型都是这个形状。文本从上面进去,一路往下,最后出来的是「下一个 token 是什么」的概率。中间那一块重复 N 次,GPT-2 small 是 12 次。

数据从上往下流
  • 文本「小猫 追 老鼠 它」
  • 分词切成 token,每个 token 换成一个编号
  • token embedding每个编号查表,变成一个向量
  • 加上位置信息告诉模型每个 token 排在第几个
  • Transformer 层,重复 N 次
    • Norm → masked self-attention → 加回输入从别的 token 那里取信息
    • Norm → MLP → 加回输入每个 token 各自加工取来的信息
  • 最后一次 Norm
  • 输出层每个位置给词表里的每个 token 打一个分
  • softmax把分数变成概率,取最后一个位置的,就是下一个 token

有一个理解方式很有用:把每个 token 的向量想成一条从上到下的主线。每一层都不替换它,只往上面加一点东西。attention 负责从别的 token 那里拿信息,MLP 负责加工。

2分词和 embedding:把文字变成向量

模型只能算数字。第一步是把文本切成 token,每个 token 对应词表里的一个编号。然后拿编号去 embedding 表里查出一个向量。这张表有「词表大小」行、「d_model」列,是训练出来的。

四个 token,各自变成一个编号,再变成一个向量。编号是为了举例随便写的。
token
小猫追老鼠它
编号
37218921504456
向量
768 个数768 个数768 个数768 个数

token 不一定是一个完整的词。常见的词是一个 token,少见的词会被切成几段。GPT-2 的词表有 50,257 个 token。

3位置编码:告诉模型谁排在第几个

attention 对每个词做的事情是一样的:看所有的词,按相似度加权。它不知道词的顺序,「狗咬人」和「人咬狗」在它眼里是同一组词。所以要把位置信息另外放进去。

原始论文的做法是给每个位置算一个和 embedding 一样长的向量,加到 embedding 上。这个向量的每一维是一个正弦或余弦波,维数越靠后,波越长。

原始论文的正弦位置编码,这里画的是 16 维、前 12 个位置。每个格子的颜色是实际算出来的值。
位置 0
位置 1
位置 2
位置 3
位置 4
位置 5
位置 6
位置 7
位置 8
位置 9
位置 10
位置 11
−1+1每一行是一个位置的 16 维编码,从左到右是第 0 到第 15 维

看这张图:左边几维变化很快,相邻的位置颜色就不同;右边几维变化很慢,要隔很远才看得出差别。合在一起,每个位置都有一个独一无二的图案。现在的模型多用 RoPE,它不加在输入上,而是在 attention 里起作用,后面会讲。

4self-attention:每个词去看别的词

这是 Transformer 的核心。每个词要回答一个问题:句子里哪些词和我有关,我该从它们那里拿什么信息?

做法是把每个词的向量变成三个新的向量:

名字作用打个比方
query(Q)我在找什么搜索框里输入的词
key(K)我是什么,能被什么找到每个网页的标题
value(V)被找到之后,我提供什么内容网页的正文

下面用四个词「小猫、追、老鼠、它」把整个计算摆出来,每个数字都写明是怎么来的。

三张表:每个词的 query、key、value。这是整个计算的全部输入。
词query(我在找什么)key(我能被什么找到)value(我提供什么)
小猫[1, 0][2, 0][1, 0]
追[1, 1][0, 1][0, 1]
老鼠[0, 1][1, 1][0.5, 0.5]
它[2, 0.5][0, 0.5][0, 0]

真实模型里,这三个向量是算出来的:拿这个词当前的向量 x,分别乘三个训练出来的矩阵,q = x·W_Q,k = x·W_K,v = x·W_V。这里跳过这一步,直接手写结果,每个只有 2 维,方便心算。

公式是 softmax(Q·Kᵀ / √d_k)·V,从里往外一共四步。下面只跟着「它」这一个词,把四步走完。

跟着「它」走一遍。它的 query 是 [2, 0.5]。

第 1 步:和每个词的 key 做点积Q·Kᵀ

点积就是对应位置相乘再相加,结果越大说明越相关。

  • 它 · 小猫 = 2×2 + 0.5×0 = 4
  • 它 · 追 = 2×0 + 0.5×1 = 0.5
  • 它 · 老鼠 = 2×1 + 0.5×1 = 2.5
  • 它 · 它 = 2×0 + 0.5×0.5 = 0.25

第 2 步:除以 √d_k/ √d_k

这里 d_k 是 2,√2 约等于 1.414。

  • 小猫:4 ÷ 1.414 = 2.828
  • 追:0.5 ÷ 1.414 = 0.354
  • 老鼠:2.5 ÷ 1.414 = 1.768
  • 它:0.25 ÷ 1.414 = 0.177

第 3 步:softmax,变成加起来等于 1 的权重softmax(…)

先把每个数 x 换成 e 的 x 次方,再各自除以这四个结果的总和 25.39。

  • 小猫:e^2.828 = 16.92,16.92 ÷ 25.39 = 66.6%
  • 追:e^0.354 = 1.42,1.42 ÷ 25.39 = 5.6%
  • 老鼠:e^1.768 = 5.86,5.86 ÷ 25.39 = 23.1%
  • 它:e^0.177 = 1.19,1.19 ÷ 25.39 = 4.7%

第 4 步:按权重把每个词的 value 加起来· V

0.666×[1, 0] + 0.056×[0, 1] + 0.231×[0.5, 0.5] + 0.047×[0, 0] ≈ [0.782, 0.171]

这就是「它」经过这一层 attention 之后得到的新信息,主要来自「小猫」的 value。

四个词同时做这件事,就是矩阵运算。下面的表里,每一行是一个词在看别的词;「它」那一行就是上面算出来的数。这里先不加 mask;加上之后,每一行只剩自己和左边的词有权重。

第 1 步的结果 Q·Kᵀ:每个词的 query 和每个词的 key 的点积。
谁在看 ↓ / 看谁 →小猫追老鼠它
小猫2010
追2120.5
老鼠0110.5
它40.52.50.25
第 3 步的结果:attention 权重。每一行加起来是 100%。
谁在看 ↓ / 看谁 →小猫追老鼠它
小猫50.5%12.3%24.9%12.3%
追35.2%17.4%35.2%12.2%
老鼠15.4%31.3%31.3%22.0%
它66.6%5.6%23.1%4.7%
第 4 步的结果:每个词的新向量。
词输出
小猫[0.630, 0.247]
追[0.528, 0.350]
老鼠[0.311, 0.469]
它[0.782, 0.171]

换一个词,或者加上 causal mask,结果会怎么变?下面这张表可以自己点。

「它」把 67% 的注意力放在了「小猫」上

「它」的 query 是 [2, 0.5]。它和每个词的 key 做点积,除以 √2,再过 softmax。

被看的词它的 key点积÷ √2权重
小猫[2, 0]42.83
66.6%
追[0, 1]0.50.35
5.6%
老鼠[1, 1]2.51.77
23.1%
它[0, 0.5]0.250.18
4.7%

最后按权重把每个词的 value 加起来,「它」的新向量是 [0.782, 0.171]。

换一个词来看
causal mask

这里的向量是手写的 2 维数字,为的是能心算。真实模型里它们有几百到几千维,是训练出来的。

为什么要除以 √d:这里的 d 是每个 head 里 key 的维数,论文里写作 d_k,这个例子里是 2,GPT-2 small 里是 64。维数越大,点积的数值波动越大。数值太大时 softmax 会变成「一个是 1,其余是 0」,这时梯度几乎是 0,模型学不动。除以 √d 是把数值拉回正常的范围。

5multi-head:同时用几种方式去看

一组 Q、K、V 只能表达一种「相关」。但词和词之间的关系有很多种:谁是主语,指代的是谁,修饰的是谁。所以用 12 组不同的投影矩阵,把同一个 768 维的向量各自投影成 64 维的 Q、K、V,每组各做一次 attention,叫一个 head。

GPT-2 small 的一层:12 个 head,每个都把 768 维投影成 64 维,各看各的,最后拼回 768 维。
  1. 输入

    每个 token 768 维

  2. 12 个 head

    每个 head 用自己的矩阵,把 768 维投影成 64 维的 Q、K、V

  3. 拼起来

    12 × 64 = 768 维

  4. 再过一个线性层

    输出 768 维

总的计算量和一个 768 维的大 head 差不多,但 12 个 head 可以各自关注不同的东西。

6残差连接和归一化:让几十层能叠起来

attention 的输出不是直接替换原来的向量,而是加回去。这就是残差连接,它来自 ResNet。归一化则把每个 token 的向量调整到均值 0、方差 1 附近,再做一次可学习的缩放和平移,防止数值一层层越变越大或越变越小。

同一个子层的两种写法。区别只在归一化放在哪。

Post-Norm(2017 年的原始论文)

  1. 输入 x

  2. 子层

    attention 或 MLP

  3. 加上 x

  4. Norm

    归一化挡在主线上

Pre-Norm(现在的做法)

  1. 输入 x

  2. Norm

    只归一化送进子层的那一份

  3. 子层

    attention 或 MLP

  4. 加上 x

    主线上没有东西挡着

Pre-Norm 下,x 这条主线从第一层到最后一层是畅通的,梯度可以原样传回去,所以层数很深时更容易训练。

7MLP:每个词各自加工信息

attention 之后是一个两层的小网络,也叫 feed-forward。它对每个 token 单独做同样的事:先把向量放大到 4 倍宽,过一个非线性函数,再缩回原来的宽度。

GPT-2 small 的 MLP。它不看别的 token,只处理自己这一个。
  1. 输入

    768 维

  2. 线性层,放大

    3072 维

  3. 非线性

    GELU

  4. 线性层,缩回

    768 维

分工是这样的:attention 负责在 token 之间搬运信息,MLP 负责对搬来的信息做计算。每一层里,MLP 的参数大约是 attention 的两倍,占这一层的三分之二。算上 embedding,GPT-2 small 整个模型里 MLP 约占 46%,模型越大越接近三分之二。

8输出:下一个 token 的概率

过完所有的层,每个位置还是一个 d_model 维的向量。最后一个线性层把它变成「词表大小」个分数,叫 logit,每个 token 一个。softmax 把分数变成概率。生成时只看最后一个位置。

怎么从概率里选一个 token,由采样方式决定。最常调的参数是 temperature。

「我 爱 吃」后面,「苹果」的概率是 62.1%

  • 苹果3
    62.1%
  • 香蕉2
    22.9%
  • 米饭1.5
    13.9%
  • 石头-1
    1.1%
temperature

词后面的数字是模型给它打的分(logit),是为这个例子手写的。分数先除以 temperature 再过 softmax:temperature 小,高分的词更突出;temperature 大,各个词的概率更接近。

temperature 接近 0 时,几乎总是选分数最高的那个,输出稳定但死板。调高之后,分数低的词也有机会被选中,输出更多样,也更容易出错。

9训练:每个位置都预测下一个 token

训练时把一整段文本一次送进去,让每个位置都预测它后面的那个 token。答案就是文本本身往后错一位。

输入和答案。一段 4 个 token 的文本,提供了 3 道题。
输入
小猫追老鼠
答案
追老鼠它

这里有一个问题:预测第 2 个 token 时,如果模型能看到第 2 个,就是在抄答案。causal mask 解决这个问题:在 attention 里把后面的位置遮住,每个 token 只能看自己和前面的。

causal mask。每一行是一个 token,蓝色是它能看到的,× 是被遮住的。
小猫追老鼠它
小猫看×××
追看看××
老鼠看看看×
它看看看看

有了 mask,所有位置可以同时算,互不泄露。这就是 Transformer 训练快的原因:RNN 要一步步来,它一次算完整段。误差用交叉熵:模型给正确答案的概率越低,误差越大。

10生成:一个一个往外写,以及 KV cache

生成时没有答案可以对。模型输出一个 token,把它接到输入后面,再跑一次,得到下一个。这叫自回归。

朴素的做法是每次都把整段重新算一遍。但前面那些 token 的 key 和 value 和上一次完全一样,因为它们只看得到自己前面的内容。把它们存下来,每次只算新的那一个,这就是 KV cache。

生成第 4 个 token 时。粗框是这一步真正要算的。

不用缓存:四个都重算

key、value
小猫追老鼠它

用缓存:前三个直接取,只算新的

从缓存取
小猫追老鼠
新算
它

前面那些位置的 MLP 也不用重算,因为它们每一层的输出都没变。代价是显存:每一层都要存每个 token 的 key 和 value,上下文越长占得越多。

四个追问

时间线里一句话带过的地方,展开讲

1N-gram 是怎么做平滑的?

问题是这样的:语料里没出现过的组合,数出来的概率是 0。一句话里只要有一个这样的组合,整句话的概率就成了 0。平滑就是从见过的组合那里匀一点概率,分给没见过的。

最简单的是加一平滑:假装每个组合都多见过一次。假设词表里只有三个词,「我 爱」后面跟它们的次数是 30、10、0:

加一平滑。分子加 1,分母加上词表的大小(这里是 3)。
「我 爱」后面的词出现次数不平滑加一平滑
你3030 ÷ 40 = 75%31 ÷ 43 = 72.1%
吃1010 ÷ 40 = 25%11 ÷ 43 = 25.6%
跑00 ÷ 40 = 0%1 ÷ 43 = 2.3%

加一平滑太粗糙:真实的词表有几万个词,没见过的组合太多,匀出去的概率就太多了。实际用的是另外两个思路。一是回退:「我 爱 跑」没见过,就退一步看「爱 跑」,再没有就只看「跑」。二是插值:把 3-gram、2-gram、1-gram 的概率按比例混在一起。回退时要先从见过的组合里扣一点概率,再按比例分下去,不然加起来会超过 1。经典方法里效果最好的是 Kneser-Ney 平滑(在 Chen 和 Goodman 的比较里,是它的改进版)。它先从每个见过的组合的次数里减掉一个固定的小数,把省下的概率分给低阶模型;低阶模型不看一个词出现了多少次,而看它跟在多少种不同的词后面。

2RNN 训练时的梯度裁剪,具体怎么做?

梯度是一个向量,告诉每个参数往哪边调、调多少。梯度爆炸时这个向量变得极长,参数一步迈得太大,训练就崩了。裁剪的做法是:方向不变,只把长度压到一个上限以内。

按长度裁剪。阈值设为 1 时的一个例子。
  1. 算出梯度

    [3, 4]

  2. 算它的长度

    √(3² + 4²) = 5

  3. 超过阈值 1

    每个数乘以 1 ÷ 5

  4. 裁剪后的梯度

    [0.6, 0.8],长度是 1

长度没超过阈值的梯度不动。这里的长度是把所有参数的梯度拼在一起算的。注意它只管梯度爆炸,管不了梯度消失:梯度消失不能靠放大来救,要靠 LSTM 的 cell state 这条直通路来缓解。现在很多公开的 Transformer 训练配置也用梯度裁剪,阈值常设为 1,比如 LLaMA。

3信号与系统里学过卷积,它在这里到底解决了什么问题?

运算是同一个:拿一个小的「核」在信号上滑动,每到一个位置,对应的数相乘再加起来。区别在核从哪来。信号与系统里,核是系统的冲激响应,由系统本身决定,或者是人设计的滤波器,比如低通滤波器。神经网络里,核里的数是训练出来的。

核是 [−1, 1],在一串数上滑过去。它在数值发生变化的地方输出非零,相当于一个「找边缘」的检测器。
000555
(−1)×0 + 1×0 = 0
000555
(−1)×0 + 1×0 = 0
000555
(−1)×0 + 1×5 = 5
000555
(−1)×5 + 1×5 = 0
000555
(−1)×5 + 1×5 = 0

这个核只有两个数,却能在任何位置找出边缘。卷积的好处就在这里,一共三点:

好处意思例子
参数少同一个核在所有位置重复使用一层 64 个 3×3 的核,处理彩色图片只要 1,728 个权重(不算 bias)
位置共享(平移等变)猫在图片左边还是右边,用的是同一个检测器;猫挪了位置,检测到的特征跟着挪不用为每个位置各学一遍
利用局部性相邻的像素关系最紧密,先看小范围一层层叠上去,看到的范围越来越大

对比一下不用卷积的做法:一张 224×224 的彩色图片有约 15 万个数,如果下一层每个神经元都和它们全部相连,1,000 个神经元就要 1.5 亿个权重。一个小细节:神经网络里的「卷积」没有把核翻转,严格说是互相关,但核是学出来的,翻不翻没有区别。

4「decoder 每生成一个词,都给原文每个位置打分,再加权求和」是什么意思?

用翻译「我 爱 猫」来看。encoder 把原文读完,三个词各留下一个向量,可以理解成「这个词在这句话里的意思」。decoder 要一个词一个词地写出译文。

最早的做法是只把 encoder 最后留下的一个固定长度的向量交给 decoder,相当于让它凭一句话的总印象来翻译。Attention 的做法是:每写一个词之前,回头把原文的三个词都看一遍,决定这一步该重点参考哪个。

写「I」的时候,decoder 把 85% 的注意力放在「我」上

encoder 读完原文,每个词留下一个状态decoder 一个词一个词地写译文我85%爱10%猫5%I??

这一步的参考 = 0.85 ×「我」的状态 + 0.10 ×「爱」的状态 + 0.05 ×「猫」的状态

线越粗,表示这一步越看重原文的那个词。这里的权重是为了说明手写的,不是真实模型的输出。

每写一个词,做三件事:

步骤做什么写「love」时
打分拿 decoder 写这个词之前的状态,和原文每个词的状态一起送进一个小网络,各算出一个分数我:低,爱:高,猫:低
变成权重对这些分数做 softmax,加起来等于 18%、84%、8%
加权求和按权重把原文每个词的状态加起来,得到一个向量0.08×「我」+ 0.84×「爱」+ 0.08×「猫」

最后得到的那个向量就是「这一步的参考」,decoder 结合它和自己的状态,输出「love」。下一个词再重新打一次分。Transformer 图里 decoder 中间的那个 attention,做的就是这件事,只是打分从小网络换成了点积,而且每一层都做一次。

形状走一遍

一句 5 个 token 的话,在 GPT-2 small 里变成什么形状

面试时能把每一步的形状说对,说明真的懂了。GPT-2 small:12 层,d_model 是 768,12 个 head,词表 50,257。

步骤形状含义
token 编号(5)5 个整数
embedding(5, 768)每个 token 一个 768 维的向量
加上位置(5, 768)形状不变
每个 head 的 Q、K、V12 × (5, 64)每个 head 把 768 维投影成 64 维
attention 权重12 × (5, 5)每个 head 里,每个 token 对每个 token 一个权重
attention 输出(5, 768)12 个 head 拼回来
MLP 中间(5, 3072)放大到 4 倍
一层的输出(5, 768)和输入一样,所以能一层层叠
12 层之后(5, 768)还是这个形状
logit(5, 50257)每个位置给每个 token 一个分数
下一个 token 的概率(50257)只取最后一个位置,过 softmax

注意 attention 权重那一行:它是 5×5。句子长度变成 n,它就是 n×n。这就是「计算量和长度的平方成正比」的来源。

参数量

参数都在哪里

选一个型号,看参数怎么分布。这个公式算出来的四个型号,和用 Hugging Face 的 GPT-2 实现数出来的参数量完全一致,small 是 124,439,808。OpenAI 最初报的 117M 是数错了。

参数总数

124,439,808

约 124 M,其中 MLP 占 46%,attention 占 23%

  • MLP56,669,184
  • attention28,348,416
  • token embedding38,597,376
  • 位置 embedding786,432
  • 归一化38,400

心算的办法:每一层大约是 12 × d²(attention 4 个 d×d 的矩阵,MLP 相当于 8 个),再加上词表 × d 的 embedding。

为什么是它赢了

和 RNN、CNN 放在一起比

这张表来自原始论文。n 是序列长度,d 是向量宽度,k 是卷积窗口的大小。

层的类型每层的计算量必须顺序做的步数两个词之间最远隔几步
self-attentionn² · d11
RNNn · d²nn
卷积k · n · d²1log_k(n)

后两列是关键。「必须顺序做的步数」是 1,说明可以并行;「最远隔几步」是 1,说明再远的两个词也能直接互相影响。代价在第一列:n 很大时,self-attention 的计算量会超过 RNN 和卷积。卷积那一行的 log_k(n) 指的是带空洞的卷积;普通卷积要叠 n/k 层才能让最远的两个词相遇。

三种用法

encoder-onlydecoder-onlyencoder-decoder
代表BERTGPT原始 Transformer、T5
能看到哪里两边都能看只能看前面encoder 看两边,decoder 看前面和 encoder
训练任务完形填空预测下一个 token给输入,生成输出
擅长理解、检索、分类生成、通用任务翻译、语音转文字这类一段进一段出的任务

2017 年的原版和现在的 LLM

零件原始论文现在常见的做法
整体encoder + decoder只有 decoder
归一化的位置子层后面(Post-Norm)子层前面(Pre-Norm)
归一化的种类LayerNormRMSNorm
位置信息正弦编码,加在输入上RoPE,在 attention 里旋转 Q 和 K
MLP 的激活函数ReLUGELU 或 SwiGLU
attention 的头每个头有自己的 K、VGQA:几个头共用一组 K、V
MLP每层一个大模型里常换成 MoE

横向对比

同样的几个维度,放在一起看

架构能看多远能否并行计算量怎么知道顺序最适合
N-gram前面 N−1 个词不涉及查表靠固定的窗口很小很快的基线
Word2Vec训练时看前后几个词;用的时候不看上下文不涉及查表不知道顺序词的向量表示
RNN理论上全部,实际几十个词不能和长度成正比按顺序读,天然知道很小的序列模型
LSTM / GRU上百个词不能和长度成正比按顺序读,天然知道时间序列、小模型
CNN一层只看附近,要叠很多层能和长度成正比靠窗口的位置图像、端侧设备
Seq2Seq + Attentiondecoder 能直接看原文的每个位置不能原文长度 × 译文长度按顺序读,天然知道2017 年以前的机器翻译
Transformer整个上下文窗口,一步到位训练时能和长度的平方成正比要另外加位置编码大规模的语言、视觉、多模态模型
BERT整句,双向能和长度的平方成正比学出来的位置向量检索、分类
GPT前面的全部训练时能,生成时逐个来和长度的平方成正比GPT-1 和 GPT-2 是学出来的位置向量;后来的开源 decoder-only 模型多用 RoPE生成、通用模型
ViT整张图的所有小块,双向能和小块数量的平方成正比学出来的位置向量图像理解、多模态里的视觉部分
现代 LLM前面的全部,窗口可以到几十万 token训练时能,生成时逐个来和长度的平方成正比,常数更小RoPE通用的大语言模型
Diffusion / DiT整张图一步之内能,步与步之间不能每一步跑一遍网络,要很多步由去噪的网络决定图像、视频、音频生成
Mamba全部,但压缩在固定大小的状态里训练时能和长度成正比按顺序读,天然知道很长的序列

业界最新的方向 · 2026 年 10 月核对

现在大家在做什么

  • MoE 成了大模型的常见做法

    把每层的 MLP 换成很多个专家,每个 token 只走其中几个。总参数可以很大,每次计算用到的却不多。DeepSeek-V3 和 Qwen3 里最大的 235B 模型都是这种结构。

  • attention 和线性代价的层混合使用

    为了更长的上下文,一些模型不再每层都用完整的 attention,而是大部分层用线性代价的结构,隔几层放一层完整的 attention。Sebastian Raschka 在 2026 年 6 月的论文清单里说,这种交替的混合结构是当年比较流行的方向;他 2025 年 11 月的文章里举的例子是 Qwen3-Next 和 Kimi Linear:三层线性 attention 配一层完整 attention。

  • 让 attention 本身更省

    GQA 让多个头共用 key 和 value,缩小 KV cache。FlashAttention 不把整个 attention 矩阵写进显存。这些不改变平方这个量级,但让长上下文在工程上可行。

  • 用 diffusion 生成文本

    自回归一次只出一个 token。diffusion 语言模型想一次并行地生成很多 token,再反复修改。它还没有取代自回归,是一个活跃的研究方向。

常见误解

这几句话听起来对,其实不对

  • 误解:Transformer 发明了 attention。

    attention 在 2014 年就用在了 RNN 的机器翻译上。Transformer 的贡献是把 RNN 去掉,只用 attention。

  • 误解:attention 的权重就是模型的解释,权重大就说明那个词重要。

    权重只说明这一层这一个头从哪里取了信息。一个模型有很多层、很多头,后面还有 MLP 和残差,单看一个头的权重不能当作模型决策的解释。

  • 误解:Transformer 能并行,所以生成也是并行的。

    并行说的是训练,以及处理已经给定的输入。生成时仍然是一个 token 一个 token 来,每个 token 依赖前一个。

  • 误解:BERT 和 GPT 是两种不同的架构。

    它们用的是同一种层。区别在于 attention 能不能看到后面的词,以及训练任务是完形填空还是预测下一个词。

  • 误解:Diffusion 是 Transformer 的替代品。

    一个是生成的方法,一个是网络的结构。DiT 就是用 Transformer 做 diffusion 里的去噪网络。

  • 误解:参数主要在 attention 里。

    标准的层里,MLP 的参数大约是 attention 的两倍:attention 是 4 个 d×d 的矩阵,MLP 相当于 8 个。

  • 误解:位置编码是加在输入上的一个固定向量。

    原始论文是这样。现在的主流是 RoPE:不加在输入上,而是在每一层的 attention 里旋转 query 和 key。

Staff 级面试题

先自己答,再点开看

▶attention 里为什么要除以根号 d?
  • 这里的 d 是每个 head 里 key 的维数 d_k,不是 d_model。GPT-2 small 里除的是 √64 = 8,不是 √768。
  • 点积是把 d_k 对数两两相乘,再把这些乘积加起来。如果每一维的均值是 0、方差是 1,点积的方差就是 d_k,维数越大波动越大。
  • 点积很大时,softmax 的输出会接近「一个是 1,其余是 0」。这时梯度几乎为 0,模型学不动。
  • 除以 √d_k 之后方差回到 1,和维数无关,softmax 不会饱和。
  • 这只是让初始状态合理的一种办法。要不要这样缩放、缩放多少,取决于向量的尺度:有的模型把 query 和 key 先做归一化,再乘一个可学习的系数,目的是一样的。
▶Transformer 为什么需要位置编码?RNN 为什么不需要?
  • self-attention 对每个词做的事情是一样的:看所有词,按相似度加权。把输入的词打乱,每个词得到的结果不变,只是跟着换了位置。它本身分不出「狗咬人」和「人咬狗」。
  • RNN 是按顺序读的,顺序已经体现在计算过程里。
  • 所以 Transformer 要把位置信息显式地放进去。原始论文是加在输入向量上,现在常用的 RoPE 是在 attention 里作用在 query 和 key 上。
  • 补充一点:上面的论证只对不带 mask 的 attention 成立。加了 causal mask,每个位置能看到的词的数量不同,模型可以从中推出一部分位置信息。用哪种位置编码取决于需求:要外推到更长的上下文,相对位置(RoPE)比绝对位置好;上下文很短且固定时,差别不大。
▶self-attention 的计算量是多少?瓶颈在哪?
  • 长度 n、维数 d 时,attention 是 n 的平方乘以 d:每个位置都要和每个位置算一次。MLP 是 n 乘以 d 的平方。
  • 带上常数看:attention 里和 n² 有关的部分大约是 2n²d,MLP 是 8nd²,attention 的几个投影矩阵是 4nd²。两边相等大约在 n 等于 4 到 6 倍的 d。GPT-2 small(d = 768)是三千到五千个 token,d = 4096 的模型是一万六到两万四。比这短,MLP 占大头;比这长,attention 占大头。
  • 长上下文真正的瓶颈常常是内存:n×n 的 attention 矩阵,以及推理时的 KV cache。
  • 优化方向取决于瓶颈在哪:FlashAttention 减少显存读写,GQA 缩小 KV cache,稀疏或线性的结构改变平方这个量级但会损失精度。
▶为什么现在的大模型都是 decoder-only,而不是 encoder-decoder?
  • 任务统一:所有任务都能写成「给前文,往下接」,不需要区分输入和输出两段。
  • 训练信号密:每个位置都在预测下一个词。
  • 结构简单,容易做大。输入和输出在同一个序列里,一份 KV cache 同时覆盖两者,相同的开头还可以在不同请求之间复用。
  • encoder-decoder 并没有错。输入和输出明显是两种东西时(比如语音转文字),它仍然合理。选哪种取决于任务是不是天然的「一段进、一段出」,以及是不是要一个模型做所有事。
▶Pre-Norm 和 Post-Norm 有什么区别?为什么现在都用 Pre-Norm?
  • Post-Norm 是原始论文的做法:先算子层,加上残差,再归一化。Pre-Norm 是先归一化,再算子层,最后加残差。
  • Pre-Norm 下,残差这条主线上没有归一化挡着,梯度可以原样传回最前面,深层网络更容易训练。
  • Post-Norm 在层数不多、调得好的时候效果不差。选哪个主要取决于层数:几十层以上基本都用 Pre-Norm。
▶KV cache 是什么?它省了什么,代价是什么?
  • 生成第 t 个词时,前面 t−1 个位置在每一层的输出都和上一步完全一样,因为它们只看得到自己前面的词。所以它们的 key 和 value 不用重算,它们的 MLP 也不用重算。
  • 把它们存下来,每一步只需要算新词的 query、key、value,再和缓存里的 key 做一次 attention。每一步的计算从「和长度的平方成正比」降到「和长度成正比」。
  • 代价是显存。缓存的大小是:层数 × token 数 × 存 key 和 value 的头数 × 每个头的维数。上下文越长占得越多,长上下文下它经常是显存的大头。
  • 所以才有 GQA(让多个 query 头共用一组 key 和 value,直接减少要存的头数)和各种缓存压缩的做法。
  • 要不要压缩缓存、压多少,取决于瓶颈是算力还是显存,以及并发的请求有多少。
▶给你一个模型的配置(层数、宽度、词表大小),怎么估它的参数量?
  • 每一层大约是 12 乘以 d 的平方:attention 是 4 个 d×d 的矩阵,MLP 是 d×4d 和 4d×d 两个矩阵,一共 8 个 d×d。
  • embedding 是词表大小乘以 d。
  • 总数约等于 12 × 层数 × d² + 词表 × d。拿 GPT-2 small 验算:12 × 12 × 768² 约 8500 万,加上 50257 × 768 约 3900 万,一共约 1.24 亿,和公开的数字一致。
  • 实际数字还取决于几个细节:输出层是否和 embedding 共用参数、位置编码是不是学出来的、MLP 是不是 4 倍宽、有没有用 MoE。
▶上下文窗口从 8K 加到 1M,哪些地方会出问题?
  • 计算:处理输入时 attention 是平方的,长度涨 125 倍,这部分计算涨一万五千多倍(125²)。有 KV cache 时,之后每生成一个 token 的计算和长度成正比。
  • 显存:KV cache 和长度成正比,每层每个头都要存。
  • 位置编码:训练时没见过这么远的位置,模型可能不会用。RoPE 这类相对位置编码外推得好一些,但也有限。
  • 质量:窗口够长不等于模型用得好,中间的内容容易被忽略。
  • 怎么解决取决于目标:只是想让这么长的输入装进显存,就用更省显存的 attention 实现和缓存压缩;要便宜,可以用检索只放进相关的部分;要结构上解决,就是混合线性代价的层。
▶设计题:要部署一个延迟敏感的生成服务,你会从架构上关注什么?
  • 先分清两个指标:首个 token 的延迟取决于处理输入的速度,和输入长度有关;之后每个 token 的延迟取决于单步生成的速度。
  • 处理输入可以并行,一次算完。生成只能一个一个来,KV cache 是必须的。
  • 显存决定一台机器能同时服务多少请求。KV cache 的大小和层数、上下文长度、存 key 和 value 的头数成正比,GQA 和量化能直接降低它。
  • MoE 模型每个 token 用到的参数少,但全部参数都要在显存里。
  • 怎么取舍要看请求是什么样的:输入长输出短,优化输入处理和缓存复用;输出长,优化单步速度和批处理。

动手练习 · 约 5 分钟

用 NumPy 写一遍 self-attention

需要 Python 和 NumPy。把下面的代码存成 attention.py 再运行。核心只有四行。

import numpy as np

np.set_printoptions(precision=3, suppress=True)

tokens = ["小猫", "追", "老鼠", "它"]

# 每个词的 query、key、value 向量。这里是手写的 2 维向量,方便心算;
# 真实模型里它们由 embedding 乘以三个训练出来的矩阵得到。
Q = np.array([[1, 0], [1, 1], [0, 1], [2, 0.5]])
K = np.array([[2, 0], [0, 1], [1, 1], [0, 0.5]])
V = np.array([[1, 0], [0, 1], [0.5, 0.5], [0, 0]])


def softmax(x):
    e = np.exp(x - x.max(axis=-1, keepdims=True))
    return e / e.sum(axis=-1, keepdims=True)


def attention(Q, K, V, causal):
    d = K.shape[-1]
    scores = Q @ K.T / np.sqrt(d)          # 第 1 步:每个 query 和每个 key 算相似度
    if causal:                              # 第 2 步:只能看自己和前面的词
        hidden = np.triu(np.ones_like(scores), k=1).astype(bool)
        scores = np.where(hidden, -np.inf, scores)
    weights = softmax(scores)               # 第 3 步:每一行变成加起来等于 1 的权重
    return weights, weights @ V             # 第 4 步:按权重把 value 加起来


weights, output = attention(Q, K, V, causal=False)
print("不加 mask 的权重(每一行是一个词在看谁)")
print(weights)
print("「它」这一行:", dict(zip(tokens, weights[3].round(3).tolist())))
print("「它」的输出向量:", output[3])

weights, output = attention(Q, K, V, causal=True)
print("\n加上 causal mask 的权重")
print(weights)
print("每一行的和:", weights.sum(axis=1))

在 NumPy 1.26 上的实际输出:

不加 mask 的权重(每一行是一个词在看谁)
[[0.505 0.123 0.249 0.123]
 [0.352 0.174 0.352 0.122]
 [0.154 0.313 0.313 0.22 ]
 [0.666 0.056 0.231 0.047]]
「它」这一行: {'小猫': 0.666, '追': 0.056, '老鼠': 0.231, '它': 0.047}
「它」的输出向量: [0.782 0.171]

加上 causal mask 的权重
[[1.    0.    0.    0.   ]
 [0.67  0.33  0.    0.   ]
 [0.198 0.401 0.401 0.   ]
 [0.666 0.056 0.231 0.047]]
每一行的和: [1. 1. 1. 1.]
  • 这里的数字和上面「亲手算一次 attention」里的完全一样,用的是同一组向量。可以对着那张表一格一格核对。
  • 加上 mask 之后,矩阵的右上角全是 0:每个词只能看自己和前面的词。第一行只剩自己,所以权重是 1。
  • 「它」是最后一个词,它后面没有词,所以加不加 mask 这一行都一样。
  • 自己改一改:把除以 np.sqrt(d) 去掉,看「它」这一行会不会更集中在「小猫」上;把 Q 里「它」的向量改成 [0.5, 2],看它转而去看谁。

速查表

考前十分钟过一遍

N-gram
数数。只看前几个词,词之间没有相似度。
Word2Vec
词变向量。一个词一个向量,不看上下文。
RNN
顺序读,一个隐藏状态。梯度消失,不能并行。
LSTM / GRU
加门和直通路,记得更远。还是不能并行。
Seq2Seq + Attention
encoder 读,decoder 写,attention 回头看原文。
ResNet
输出 = 输入 + 修改量。深层网络能训练的前提。
Transformer
只用 attention。并行,任意两个词一步直达,代价是计算量随长度的平方增长。
self-attention
softmax(QKᵀ / √d_k) · V。除以 √d_k 是为了不让 softmax 饱和、梯度消失。
BERT
encoder-only,双向,完形填空。理解和检索。
GPT
decoder-only,causal mask,预测下一个词。生成。
现代 LLM
Pre-Norm、RMSNorm、RoPE、SwiGLU、GQA、MoE、KV cache。
参数量
约 12 × 层数 × d² + 词表 × d。
Diffusion
逐步去噪。是生成方法,网络可以是 Transformer。
Mamba
计算量和长度成正比,状态按输入内容决定记什么。常和 attention 混用。

来源

「业界最新的方向」在 2026 年 10 月核对过,之后可能已经变化。页面里可以交互的数字(attention 的权重、位置编码、参数量)都是当场算出来的,没有写死。

留言

说说你的看法

有想法或问题都可以写在这里。留言会立刻显示;想收到回复通知再填邮箱。

还没有留言,你可以写第一条。