先把上一章的注意力抽象重述一遍,因为它是 Transformer 的全部语言。注意力是一个“软查找”:用一个查询(Query)去和一组(Key)比相似度,把相似度归一化成权重,再用权重对一组(Value)做加权平均。在 Bahdanau 那里,query 来自解码器、key/value 来自编码器,注意力是连接两个 RNN 的桥。

2017 年《注意力就是你所需要的一切》(Attention Is All You Need)做的事,是把这个机制从“RNN 的桥”提拔成“整座建筑”1。它问了一个极端的问题:如果一个序列里的每个位置,都用注意力直接去看序列里所有其他位置(包括自己),那还需要循环吗?答案是不需要。这种“序列内部各位置互相注意”的机制叫自注意力(self-attention),是 Transformer 的心脏。

抛弃循环换来一个巨大的好处:并行。RNN 的第 tt 个隐藏状态依赖第 t1t-1 个,所以必须顺序算;而自注意力里,所有位置的表示可以同时算出来——本质上就是几个大矩阵乘法,正是 GPU 最擅长的(回顾第 05 章)。这让 Transformer 能在同样的时间里训练大得多的模型、长得多的序列。论文里它在 WMT 2014 英德翻译上拿到 28.4 BLEU、英法 41.8 BLEU,超过此前包括集成模型在内的最佳结果 2 个 BLEU 以上,而训练只用了 8 块 GPU、3.5 天——比当时顶尖的循环/卷积翻译模型训练成本低得多1


自注意力的核心公式,叫缩放点积注意力(scaled dot-product attention)。把输入序列的每个位置先线性投影成三个向量——查询 QQ、键 KK、值 VV(实际是整批位置堆成矩阵 Q,K,VQ, K, V),注意力是:

Attention(Q,K,V)=softmax ⁣(QKdk)V\text{Attention}(Q, K, V) = \text{softmax}!\left(\frac{QK^\top}{\sqrt{d_k}}\right)V

逐项拆解:

  • QKQK^\top:每个 query 和每个 key 做点积,得到一个 L×LL \times L 的分数矩阵(LL 是序列长度),第 (i,j)(i,j) 个元素是“位置 ii 对位置 jj 的相关性”。点积越大表示越相关。这一步用相乘而非 Bahdanau 的相加,所以叫“点积注意力”——它没有额外参数、可以写成纯矩阵乘法,因而极快。
  • /dk/\sqrt{d_k}:除以键向量维度的平方根做缩放。这是个小却关键的细节,下一节专讲。
  • softmax()\text{softmax}(\cdot):对分数矩阵的每一行做 softmax,把每个 query 在所有 key 上的分数变成和为 1 的权重分布。
  • V\cdot,V:用权重对 value 加权求和,得到每个位置的输出表示。

一句话:每个位置的新表示,是序列中所有位置的 value 的加权平均,权重由该位置的 query 和各位置的 key 的匹配度决定。位置 ii 想从位置 jj 取信息,只要它们的 query-key 匹配度高,一步就能取到——不管 iijj 隔了多远。这就是 Transformer 对长程依赖的解法:把 RNN 里“信息要逐步传递 ij|i-j| 步”变成“任意两点一步直达”。


为什么要除以 dk\sqrt{d_k}?这是个值得讲清的数学细节,因为它体现了一种对“数值尺度”的精细控制。

假设 query 和 key 的每个分量是独立的、均值 0 方差 1 的随机变量。那么它们的点积 qk=l=1dkqlkl\mathbf{q}\cdot\mathbf{k} = \sum_{l=1}^{d_k} q_l k_ldkd_k 个独立项之和,其方差正比于 dkd_k——维度越高,点积的数值波动越大,量级约为 dk\sqrt{d_k}

如果不缩放,当 dkd_k 较大时(比如 64),点积会变得很大,送进 softmax 后会把分布推到极端——某一个权重接近 1、其余接近 0。问题在于 softmax 在这种饱和区的梯度极小(回顾第 04 章梯度消失的同源现象),训练会变得困难。除以 dk\sqrt{d_k} 恰好把点积的方差拉回 1 附近,让 softmax 工作在梯度健康的区间。一个除法,稳住了整个训练。


单一注意力有个局限:一次加权平均只能表达“一种”关注模式。但语言里一个词常常需要同时关注多种关系——句法上的(它的主语是谁)、语义上的(它指代什么)、位置上的(紧邻的修饰词)。Transformer 的解法是多头注意力(multi-head attention)。

做法是把 dmodeld_{\text{model}} 维的表示拆成 hh 个较低维的子空间(论文里 dmodel=512d_{\text{model}}=512h=8h=8 个头,每个头 dk=512/8=64d_k = 512/8 = 64 维),每个头用自己独立的 Q,K,VQ,K,V 投影矩阵,各自独立地做一次缩放点积注意力,最后把 hh 个头的输出拼接起来再做一次线性变换:

MultiHead(Q,K,V)=Concat(head1,,headh)WO\text{MultiHead}(Q,K,V) = \text{Concat}(\text{head}_1, \dots, \text{head}_h),W^O headi=Attention(QWiQ,KWiK,VWiV)\text{head}_i = \text{Attention}(QW_i^Q,, KW_i^K,, VW_i^V)

直觉是:每个头可以专注一种关系。事后分析真实 Transformer 的注意力头,确实能看到有的头专门关注相邻词、有的头追踪句法依存(如动词找它的宾语)、有的头处理指代。多头让模型在同一层并行地从多个“角度”看序列。注意计算量并不增加——把一个 512 维的注意力拆成 8 个 64 维的,总维度不变,只是切成了多个子空间。


自注意力有个先天缺陷:它是置换不变的。因为输出是 value 的加权和、权重只看 query-key 匹配度,所以打乱输入顺序,输出只会相应打乱,模型本身分不清“词的位置”。可语序对语言至关重要——“狗咬人”和“人咬狗”词一样、意思相反。RNN 天然知道顺序(它就是按顺序处理的),Transformer 删掉循环后必须显式地把位置信息注入进去

Transformer 的做法是位置编码(positional encoding):给每个位置 pospos 算一个和词向量同维度的向量,加到词向量上。论文用的是一组不同频率的正弦/余弦函数:

PE(pos,2i)=sin ⁣(pos100002i/dmodel),PE(pos,2i+1)=cos ⁣(pos100002i/dmodel)PE_{(pos, 2i)} = \sin!\left(\frac{pos}{10000^{2i/d_{\text{model}}}}\right), \qquad PE_{(pos, 2i+1)} = \cos!\left(\frac{pos}{10000^{2i/d_{\text{model}}}}\right)

每个维度 ii 对应一个不同波长的正弦波(从短到长跨好几个数量级),位置 pospos 在这些波上的取值组合,构成了它独一无二的“位置指纹”。用正弦/余弦的一个巧妙之处是:任意两个位置的相对偏移 kk,可以通过位置编码的线性变换表达(三角恒等式),这让模型容易学到“相对位置”关系,并且能外推到比训练时更长的序列。后来的模型发展出了可学习位置编码、相对位置编码、旋转位置编码(RoPE)等多种变体,但“必须显式注入位置”这个需求是 Transformer 架构的固有属性。


把零件组装成完整的 Transformer。原始论文是一个编码器-解码器结构,各 6 层1。一个编码器层由两个子层组成:

  1. 多头自注意力:序列内各位置互相注意。
  2. 逐位置前馈网络(FFN):对每个位置独立地过一个两层 MLP(5122048512512 \to 2048 \to 512,中间用 ReLU)。这给模型增加非线性变换能力。

每个子层都包着两样东西——残差连接层归一化

output=LayerNorm(x+Sublayer(x))\text{output} = \text{LayerNorm}\big(x + \text{Sublayer}(x)\big)

残差连接 x+Sublayer(x)x + \text{Sublayer}(x) 直接搬用第 09 章会细讲的 ResNet 思想(恒等捷径让梯度畅通、深层可训练);层归一化(LayerNorm)则像第 09 章的 BatchNorm 一样稳定每层的数值分布、加速收敛,只是它在特征维度而非批次维度做归一化,更适合变长序列。没有这两样,6 层乃至后来几十上百层的 Transformer 根本训不动。

解码器层多一个细节:它的自注意力是带因果掩码的(masked self-attention)——生成第 ii 个词时只能看位置 i\le i 的词,不能偷看未来(否则训练时就作弊了)。掩码的实现是把注意力分数矩阵的上三角(未来位置)设成 -\infty,过 softmax 后权重变 0。这正是 GPT 这类自回归生成模型的关键。

用代码看因果掩码(完整文件 code/07_attention.py):

代码 · 07_attention.py
展开代码 · 07_attention.py
"""
第 07/08 章配套代码:
(1) Bahdanau 风格 additive attention(2014)
(2) Transformer 的 scaled dot-product attention + multi-head(2017)
Runnable with: numpy only.  python3 07_attention.py
"""
import numpy as np
rng = np.random.default_rng(0)


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


def additive_attention(query, keys, values, Wq, Wk, v):
    """Bahdanau 2014: score(q, k) = v^T tanh(Wq q + Wk k)。
    query: (dq,)  keys/values: (T, dk)/(T, dv)
    """
    scores = np.array([v @ np.tanh(Wq @ query + Wk @ k) for k in keys])  # (T,)
    alpha = softmax(scores)                  # 注意力权重(对齐分布)
    context = alpha @ values                 # 加权求和得到 context 向量
    return context, alpha


def scaled_dot_product_attention(Q, K, V, mask=None):
    """Transformer 2017: Attention(Q,K,V) = softmax(QK^T / sqrt(d_k)) V
    Q:(Lq,d) K:(Lk,d) V:(Lk,dv)
    """
    d_k = Q.shape[-1]
    scores = Q @ K.T / np.sqrt(d_k)          # (Lq, Lk) 缩放点积
    if mask is not None:
        scores = np.where(mask, scores, -1e9)  # 因果掩码:屏蔽未来位置
    A = softmax(scores, axis=-1)             # 每个 query 在所有 key 上的注意力
    return A @ V, A


def multi_head_attention(X, d_model=8, n_heads=2):
    """把 d_model 拆成 n_heads 个子空间,各自做注意力,再拼接。"""
    L = X.shape[0]; d_head = d_model // n_heads
    out = np.zeros((L, d_model)); attns = []
    for h in range(n_heads):
        Wq, Wk, Wv = [rng.normal(0, .5, (d_model, d_head)) for _ in range(3)]
        Q, K, V = X @ Wq, X @ Wk, X @ Wv
        o, A = scaled_dot_product_attention(Q, K, V)
        out[:, h * d_head:(h + 1) * d_head] = o
        attns.append(A)
    return out, attns


if __name__ == "__main__":
    print("=== Bahdanau additive attention(2014)===")
    T, dk = 4, 5
    keys = rng.normal(0, 1, (T, dk)); values = keys.copy()
    query = keys[2] + rng.normal(0, .1, dk)   # query 接近第 3 个 key
    Wq = np.eye(dk); Wk = np.eye(dk); v = np.ones(dk)
    ctx, alpha = additive_attention(query, keys, values, Wq, Wk, v)
    print("  注意力权重 alpha =", np.round(alpha, 3), "(应在最接近的 key 上最大)")

    print("\n=== Scaled dot-product attention(2017)===")
    L, d = 4, 6
    X = rng.normal(0, 1, (L, d))
    Q = K = V = X
    o, A = scaled_dot_product_attention(Q, K, V)
    print("  注意力矩阵 A (每行和为1):\n", np.round(A, 2))

    print("\n=== 因果掩码(GPT 式自回归,不能看未来)===")
    mask = np.tril(np.ones((L, L))).astype(bool)
    o, A = scaled_dot_product_attention(Q, K, V, mask=mask)
    print("  下三角注意力(位置 i 只能看 <=i):\n", np.round(A, 2))

    print("\n=== Multi-head attention ===")
    out, attns = multi_head_attention(X, d_model=6, n_heads=2)
    print("  输出形状:", out.shape, " head 数:", len(attns))

↓ 下载 07_attention.py

def scaled_dot_product_attention(Q, K, V, mask=None):
    d_k = Q.shape[-1]
    scores = Q @ K.T / np.sqrt(d_k)          # 缩放点积
    if mask is not None:
        scores = np.where(mask, scores, -1e9)  # 因果掩码:屏蔽未来
    A = softmax(scores, axis=-1)
    return A @ V, A

加上下三角掩码后运行:

下三角注意力(位置 i 只能看 <=i):
[[1.00 0.   0.   0.  ]
 [0.43 0.57 0.   0.  ]
 [0.01 0.03 0.96 0.  ]
 [0.01 0.01 0.06 0.92]]

每一行的权重只分布在对角线及左侧(过去和当前),右上方(未来)全是 0——这就是自回归生成“不能看未来”的数学实现。


用一张图把完整的 Transformer 编码器-解码器钉死:

                编码器 ×6                            解码器 ×6
   输入词 ──► 词向量 + 位置编码          输出词(右移) ──► 词向量 + 位置编码
                  │                                      │
            ┌─────▼─────┐                          ┌─────▼──────────┐
            │ 多头自注意力 │                          │ 带掩码多头自注意力 │ (不看未来)
            │ +残差+LN   │                          │ +残差+LN        │
            ├───────────┤                          ├────────────────┤
            │ 前馈FFN    │                          │ 编码器-解码器注意力│◄── 看编码器输出
            │ +残差+LN   │                          │ +残差+LN        │
            └─────┬─────┘                          ├────────────────┤
                  │  ───── 编码器输出(K,V) ───────►  │ 前馈FFN+残差+LN  │
                  ▼                                 └─────┬──────────┘
            (重复6层)                                     ▼
                                                   线性 + softmax ──► 下一个词概率
   核心: Attention(Q,K,V)=softmax(QKᵀ/√d_k)V ; 多头并行; 位置编码注入顺序

配套的 manim 动画 assets/manim/ch08_transformer.pyQKVAttentionPositionalEncoding 两个 Scene)把注意力的内核一步步演出来:某个 token 用自己的 Query 去和所有 token 的 Key 打分(点积),softmax 成权重(线条粗细就是权重大小),再去加权求和所有 token 的 Value,得到这个位置的新表示——这就是 self-attention 的全部。PositionalEncoding 则演示注意力本身不分先后,位置编码如何用不同频率的正余弦给每个位置盖一个全局唯一的“指纹”,把顺序信息加回去。


Transformer 的历史地位,不在于它在 2017 年赢了几个 BLEU 点,而在于它成了一个通用的、可无限堆叠和放大的序列处理底座。它有三个让它统治后续十年的特质:

第一,高度并行,能吃满 GPU/TPU,于是能被训练到巨大规模——这直接接上了第 05 章那条“规模即能力”的暗线,并在下一章成为 scaling laws 的物质基础。第二,任意两位置一步直达,长程依赖不再衰减,适合长文本、长上下文。第三,架构统一、模块整齐,几乎只由注意力、FFN、残差、归一化堆成,容易在不同任务、不同模态(文本、图像 ViT、语音、蛋白质)上复用——“一个架构通吃”成了现实。

正因如此,2018 年之后几乎所有有影响力的模型都建立在 Transformer 上:BERT 用它的编码器做双向预训练,GPT 用它的(带掩码的)解码器做自回归生成。把 Transformer 这个“骨架”和“大规模无监督预训练”这个“训练范式”结合起来,就诞生了我们今天所说的大语言模型。那是下一阶段——预训练、scaling laws 与对齐——的故事。

在抵达那里之前,还有一块拼图要补上:那些让“把网络堆到几十上百层、训练到收敛”成为可能的工程突破。残差连接、批归一化、更好的优化器和激活函数——它们不像 Transformer 那样耀眼,却是一切深层模型能被训练出来的隐形地基。那是下一章。


本质

Transformer 把序列建模归约成了一个极简的原语:每个位置用一个 Query 去和所有位置的 Key 算相似度,按相似度加权地聚合所有位置的 Value。它真正做对的,是把“建模任意两个位置之间的关系”从需要 O(n)O(n) 步顺序传递的循环操作,变成了一次可以完全并行的矩阵乘法——长程依赖不再随距离衰减,算力也能被现代硬件吃满。它的胜利与其说是因为注意力比循环“更聪明”,不如说是因为它更适合被放大:结构整齐、几乎只由注意力与前馈堆成、能复用到任何模态。当一个架构既能表达足够丰富的关系、又能无限并行地堆叠和训练,规模就成了唯一的变量——这正是它统治此后整个大模型时代的根本原因。


参考文献

  1. Vaswani, A., Shazeer, N., Parmar, N., Uszkoreit, J., Jones, L., Gomez, A. N., Kaiser, Ł., & Polosukhin, I. (2017). Attention Is All You Need. NeurIPS 2017. 缩放点积注意力、多头(d_model=512, 8 头)、正弦位置编码、6 层编码器-解码器、残差+LayerNorm、WMT14 英德 28.4 / 英法 41.8 BLEU、8 GPU 3.5 天。arXiv:https://arxiv.org/abs/1706.03762 ;NeurIPS PDF:https://proceedings.neurips.cc/paper/7181-attention-is-all-you-need.pdf ;综述:https://en.wikipedia.org/wiki/Attention_Is_All_You_Need
用一条传送带(嵌入→注意力→前馈→解码)做骨架,放大首尾两端:词经蓝色权重矩阵 W_E 变成灰色数据向量(embedding),最后一个向量经 W_U → softmax 得到下一个词的概率横条(Snape 0.78),从几何上点明 Transformer 本质是『词→向量→反复加工→解码』的流水线,并立起『蓝=权重/灰=数据』的颜色纪律。
用玩具句 fluffy/blue/creature 演示自注意力内核:每个词经蓝矩阵投影成 Q/K/V,Query 与 Key 做点积填进打分网格,逐列 softmax 染成 attention pattern(列和=1),再按权重把 Value 加成增量 ΔE 加回原向量——creature 被『染成蓬松的蓝色生物』,从本质上说明注意力是软查找+加权聚合,且给的是增量而非替换。
多头=同一注意力机制复制几份各看一种关系(用句子上的弧线分别连出相邻修饰/句法主谓/指代);再用『狗咬人 vs 人咬狗』点出自注意力置换不变的先天缺陷,继而用不同频率正余弦在某位置取值组合成全局唯一的『位置指纹』把顺序信息注入回去,从几何上说清多头与位置编码两块拼图。