一
先把上一章的注意力抽象重述一遍,因为它是 Transformer 的全部语言。注意力是一个“软查找”:用一个查询(Query)去和一组键(Key)比相似度,把相似度归一化成权重,再用权重对一组值(Value)做加权平均。在 Bahdanau 那里,query 来自解码器、key/value 来自编码器,注意力是连接两个 RNN 的桥。
2017 年《注意力就是你所需要的一切》(Attention Is All You Need)做的事,是把这个机制从“RNN 的桥”提拔成“整座建筑”1。它问了一个极端的问题:如果一个序列里的每个位置,都用注意力直接去看序列里所有其他位置(包括自己),那还需要循环吗?答案是不需要。这种“序列内部各位置互相注意”的机制叫自注意力(self-attention),是 Transformer 的心脏。
抛弃循环换来一个巨大的好处:并行。RNN 的第 个隐藏状态依赖第 个,所以必须顺序算;而自注意力里,所有位置的表示可以同时算出来——本质上就是几个大矩阵乘法,正是 GPU 最擅长的(回顾第 05 章)。这让 Transformer 能在同样的时间里训练大得多的模型、长得多的序列。论文里它在 WMT 2014 英德翻译上拿到 28.4 BLEU、英法 41.8 BLEU,超过此前包括集成模型在内的最佳结果 2 个 BLEU 以上,而训练只用了 8 块 GPU、3.5 天——比当时顶尖的循环/卷积翻译模型训练成本低得多1。
二
自注意力的核心公式,叫缩放点积注意力(scaled dot-product attention)。把输入序列的每个位置先线性投影成三个向量——查询 、键 、值 (实际是整批位置堆成矩阵 ),注意力是:
逐项拆解:
- :每个 query 和每个 key 做点积,得到一个 的分数矩阵( 是序列长度),第 个元素是“位置 对位置 的相关性”。点积越大表示越相关。这一步用相乘而非 Bahdanau 的相加,所以叫“点积注意力”——它没有额外参数、可以写成纯矩阵乘法,因而极快。
- :除以键向量维度的平方根做缩放。这是个小却关键的细节,下一节专讲。
- :对分数矩阵的每一行做 softmax,把每个 query 在所有 key 上的分数变成和为 1 的权重分布。
- :用权重对 value 加权求和,得到每个位置的输出表示。
一句话:每个位置的新表示,是序列中所有位置的 value 的加权平均,权重由该位置的 query 和各位置的 key 的匹配度决定。位置 想从位置 取信息,只要它们的 query-key 匹配度高,一步就能取到——不管 和 隔了多远。这就是 Transformer 对长程依赖的解法:把 RNN 里“信息要逐步传递 步”变成“任意两点一步直达”。
三
为什么要除以 ?这是个值得讲清的数学细节,因为它体现了一种对“数值尺度”的精细控制。
假设 query 和 key 的每个分量是独立的、均值 0 方差 1 的随机变量。那么它们的点积 是 个独立项之和,其方差正比于 ——维度越高,点积的数值波动越大,量级约为 。
如果不缩放,当 较大时(比如 64),点积会变得很大,送进 softmax 后会把分布推到极端——某一个权重接近 1、其余接近 0。问题在于 softmax 在这种饱和区的梯度极小(回顾第 04 章梯度消失的同源现象),训练会变得困难。除以 恰好把点积的方差拉回 1 附近,让 softmax 工作在梯度健康的区间。一个除法,稳住了整个训练。
四
单一注意力有个局限:一次加权平均只能表达“一种”关注模式。但语言里一个词常常需要同时关注多种关系——句法上的(它的主语是谁)、语义上的(它指代什么)、位置上的(紧邻的修饰词)。Transformer 的解法是多头注意力(multi-head attention)。
做法是把 维的表示拆成 个较低维的子空间(论文里 , 个头,每个头 维),每个头用自己独立的 投影矩阵,各自独立地做一次缩放点积注意力,最后把 个头的输出拼接起来再做一次线性变换:
直觉是:每个头可以专注一种关系。事后分析真实 Transformer 的注意力头,确实能看到有的头专门关注相邻词、有的头追踪句法依存(如动词找它的宾语)、有的头处理指代。多头让模型在同一层并行地从多个“角度”看序列。注意计算量并不增加——把一个 512 维的注意力拆成 8 个 64 维的,总维度不变,只是切成了多个子空间。
五
自注意力有个先天缺陷:它是置换不变的。因为输出是 value 的加权和、权重只看 query-key 匹配度,所以打乱输入顺序,输出只会相应打乱,模型本身分不清“词的位置”。可语序对语言至关重要——“狗咬人”和“人咬狗”词一样、意思相反。RNN 天然知道顺序(它就是按顺序处理的),Transformer 删掉循环后必须显式地把位置信息注入进去。
Transformer 的做法是位置编码(positional encoding):给每个位置 算一个和词向量同维度的向量,加到词向量上。论文用的是一组不同频率的正弦/余弦函数:
每个维度 对应一个不同波长的正弦波(从短到长跨好几个数量级),位置 在这些波上的取值组合,构成了它独一无二的“位置指纹”。用正弦/余弦的一个巧妙之处是:任意两个位置的相对偏移 ,可以通过位置编码的线性变换表达(三角恒等式),这让模型容易学到“相对位置”关系,并且能外推到比训练时更长的序列。后来的模型发展出了可学习位置编码、相对位置编码、旋转位置编码(RoPE)等多种变体,但“必须显式注入位置”这个需求是 Transformer 架构的固有属性。
六
把零件组装成完整的 Transformer。原始论文是一个编码器-解码器结构,各 6 层1。一个编码器层由两个子层组成:
- 多头自注意力:序列内各位置互相注意。
- 逐位置前馈网络(FFN):对每个位置独立地过一个两层 MLP(,中间用 ReLU)。这给模型增加非线性变换能力。
每个子层都包着两样东西——残差连接和层归一化:
残差连接 直接搬用第 09 章会细讲的 ResNet 思想(恒等捷径让梯度畅通、深层可训练);层归一化(LayerNorm)则像第 09 章的 BatchNorm 一样稳定每层的数值分布、加速收敛,只是它在特征维度而非批次维度做归一化,更适合变长序列。没有这两样,6 层乃至后来几十上百层的 Transformer 根本训不动。
解码器层多一个细节:它的自注意力是带因果掩码的(masked self-attention)——生成第 个词时只能看位置 的词,不能偷看未来(否则训练时就作弊了)。掩码的实现是把注意力分数矩阵的上三角(未来位置)设成 ,过 softmax 后权重变 0。这正是 GPT 这类自回归生成模型的关键。
用代码看因果掩码(完整文件 code/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))
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.py(QKVAttention 与 PositionalEncoding 两个 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。它真正做对的,是把“建模任意两个位置之间的关系”从需要 步顺序传递的循环操作,变成了一次可以完全并行的矩阵乘法——长程依赖不再随距离衰减,算力也能被现代硬件吃满。它的胜利与其说是因为注意力比循环“更聪明”,不如说是因为它更适合被放大:结构整齐、几乎只由注意力与前馈堆成、能复用到任何模态。当一个架构既能表达足够丰富的关系、又能无限并行地堆叠和训练,规模就成了唯一的变量——这正是它统治此后整个大模型时代的根本原因。
参考文献
- 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