引言:一次深思熟虑的范式转移
2017 年 Google 发表《Attention Is All You Need》时,很多人以为这又是一篇堆 tricks 的刷榜论文。八年后的今天回看,“Attention Is All You Need” 这个标题不只是标题党——它是一句宣言,宣告了深度学习架构设计的一次根本性转向。
它的深刻之处不在于”不用 RNN 也能做 seq2seq”,而在于它重新回答了序列建模中最基本的问题:信息应该如何在不同位置之间流动。
这个问题在 Transformer 之前已经有了两套答案:
- RNN 说:信息通过隐状态的递归传递,像接力棒一样一站一站传下去
- CNN(如 WaveNet)说:信息通过局部感受野逐层堆叠来扩大覆盖范围
Transformer 给出了第三套答案:所有位置同时可见,由模型动态决定关注什么、忽略什么。
这就是整个变革的起点。下面一步步拆解,为什么这个选择如此强大。
第一原理:为什么需要注意力
RNN 的困局
用一个最简单的例子就能暴露 RNN 的本质问题:
“我在上海的房间里看到一个东西,它特别____。”
让 RNN 预测”它”后面的形容词。要填对这个空,模型需要记住”它”指代的是”东西”,而”东西”在 12 个 token 之前。RNN 的梯度需要反向传播穿过 12 步,每一步还要乘以一个小于 1 的 tanh 导数——梯度早就指数级衰减到零了。
当然,LSTM 用门控机制缓解了这个问题,但它只是把信号路径从”一条笔直的衰减通道”变成了”有岔路和放大器的通道”,信息传递的距离仍然和路径长度线性相关。要让信息从位置 1 传到位置 100,就必须要走过 100 步。
这是 RNN 家族的拓扑缺陷:信息的交互距离等于架构自身的深度。
注意力的核心洞察
注意力的洞察在于:如果让每个位置都能直接看到所有位置,交互距离就变成 O(1) 了。 这不是什么”模拟人类注意力”的心理学类比——它是一个纯粹的计算机科学决策:用 O(n²) 的计算量(对所有 pair 做交互)换取 O(1) 的交互距离。
这个 trade-off 的关键在于:2017 年的时候,GPU 的大规模并行算力已经足够支撑 O(n²) 计算,但内存带宽和序列化操作仍然是 RNN 推理的瓶颈。Transformer 选择”多花算力、压缩步骤” —— 用矩阵乘法(高度并行化)替代了循环(串行化),在当时的硬件条件下是更优的资源配比。
这个决策的背后,有一个很本质的观察:注意力机制本质上是 在集合上的信息路由。
Self-Attention 的数学原理
查询、键、值:信息检索的隐喻
Self-attention 用了一组非常漂亮的抽象:Query、Key、Value。
很多人第一次接触时觉得这是个奇怪的黑话,其实它借用了信息检索系统的基本框架:
你有一个问题(Query)→ 你用它去匹配一堆候选答案的标签(Key)→ 找到最匹配的标签 → 取出对应的完整答案(Value)在自注意力中,序列中的每个 token 同时扮演三个角色:
- 它向别人提问(作为 Query,问”谁和我相关?”)
- 它回应别人的提问(作为 Key,说”我是谁”)
- 它提供实际的内容(作为 Value,说”这是我的信息”)
三个角色通过三个不同的线性变换从同一个输入向量得到:
Q = XW_Q, K = XW_K, V = XW_V这三个权重矩阵是可学习的,模型通过训练数据来学到”一个好的 Query 应该长什么样""一个好的 Key 应该包含什么信息""什么样的 Value 值得被传递”。
Scaled Dot-Product Attention 的直觉
自注意力的核心计算极其简洁:
Attention(Q, K, V) = softmax(QK^T / √d_k) V逐层拆解:
-
QK^T 是一个
n × n的矩阵,第(i, j)个元素是 query_i 和 key_j 的点积。点积越大,两个向量在几何上越”对齐”,模型就认为位置 i 和位置 j 越相关。这个矩阵本质上是 token 之间的交互强度图。 -
除以 √d_k 是很多人容易忽略的关键细节。d_k 是 query/key 向量的维度。为什么这么做?
假设 Q 和 K 的每个分量都是均值为 0、方差为 1 的独立随机变量,那么对于两个 d_k 维向量的点积,其方差等于 d_k。当 d_k 很大时(比如 512),点积的方差就会变得很大,导致 softmax 的输入值集中在极端区域——一个值很大,其余值都非常小。softmax 的梯度在这样的区域几乎为零(梯度饱和),模型学不动了。
除以 √d_k 把点积的方差重新拉回到 1,让 softmax 的输入落在梯度灵敏的区域。这不是一个可选的归一化——它是让 attention 在 d_k 较大时可训练的必要条件。
-
softmax 把每一行的交互强度归一化为概率分布。第 i 行的值表示”位置 i 应该从每个其他位置吸收多少信息”。
-
乘上 V 完成了加权汇聚:位置 i 的新表示 = 所有位置的 V 按 softmax 权重求和。
三次矩阵乘法,没有循环,完全可并行。这个计算模式之所以高效,是因为 GPU 的核心优势就是大规模矩阵乘法。
从一个例子看 attention 的行为
考虑句子:“银行的工作人员在那栋楼里办公。”
当模型在处理”那栋楼”这个 token 时,它的 query 会与句子中所有 token 的 key 做匹配。理想情况下,attention 权重应该给”那栋楼”和”银行”之间较高的权重(楼是银行的物理场所),“工作人员”也可能有一定权重(人在楼里办公),而”在""的""。“等虚词的权重应该接近于零。
模型通过反向传播学习哪些 token 之间应该建立连接。经过大量语料训练后的 BERT/GPT,其 attention 模式已经呈现高度结构化的特征——有的 head 负责语法依赖(动词和主语),有的 head 负责词汇共现(“银行”和”楼”),有的 head 甚至能建模长距离的指代关系。
Multi-Head Attention:并行视角的必要性
一个 attention 机制已经够用了,为什么还要多头?
原因在于:单一 attention 的计算只能在一种表达空间中衡量 token 之间的相似性。
但在自然语言中,“相似性”不是一个单维的概念。两个 token 可能在语法上是主语-动词关系(“他”和”跑”),在语义上是同义关系(“快速”和”迅速”),在位置上是指代关系(“它”和”那栋楼”)。用一种投影空间不可能同时捕捉这些不同维度的关系。
Multi-head 的解决方案很直接:并行地学习多组不同的 Q、K、V 投影矩阵,每组在各自的子空间中计算 attention,然后把结果拼接起来再做一次线性投影。
MultiHead(Q, K, V) = Concat(head_1, ..., head_h) W_O其中 head_i = Attention(QW_i^Q, KW_i^K, VW_i^V)直觉上相当于:用多组不同的”滤镜”去观察同一组 token 之间的关系,每组滤镜各取所需的信息,最后合在一起。 这就是为什么 BERT-base 用了 12 个 head,GPT-2 用了 12 个,而 T5 用了 8 到 128 个不同的 head。
实践中观察到的有趣现象:Transformer 训练完成后,不同 head 会自发地分化出不同的”专业领域”。有的 head 专注局部(相邻几个 token 的语法关系),有的 head 关注全局(长距离语义依赖),有的 head 甚至发展出了类似句法树的层次结构——这没有任何显式的引导,完全通过自监督学习涌现出来。
不过多头也有代价:每个 head 的注意力图 n × n 都要单独计算和存储。好在这些计算完全独立,在 GPU 上可以并行完成。
Positional Encoding:无序集合中的有序世界
这是 Transformer 最优雅的一个设计。
self-attention 的本质是集合操作——它对输入的顺序完全不敏感。把句子”猫追狗”的三个 token 打乱成”狗猫追”,self-attention 会给出完全相同的结果。这是因为注意力交互只取决于 token 之间的语义相似性,而不取决于它们在序列中的位置。
对于语言来说,这显然是不够的。“狗追猫”和”猫追狗”token 相同,但含义完全相反。 Transformer 必须在输入中注入位置信息。
正弦/余弦编码:一种精妙的频率编码
原文用了一种非常有趣的方案:用不同频率的正弦和余弦波来编码位置。
PE(pos, 2i) = sin(pos / 10000^{2i/d_model})PE(pos, 2i+1) = cos(pos / 10000^{2i/d_model})为什么用正弦和余弦波?
这个编码的设计灵感来自傅里叶特征和频率编码的思想。本质上,它在做的事情是:
-
位置信息被编码到不同频率的波上。低频波(i 较小的维度)对大范围位置变化敏感,适合编码绝对位置;高频波(i 较大的维度)只对局部位置变化敏感,适合编码相对距离。
-
这种编码存在一个非常漂亮的代数性质:对于任意固定偏移 k,PE(pos + k) 可以表示为 PE(pos) 的线性变换(旋转矩阵)。这意味着模型可以隐式地学到相对位置关系——两个 token 之间的相对距离和方向信息已经蕴含在编码向量对中。
-
不同频率的组合提供了相对唯一的模式,避免了位置编码之间的混淆。
换句话说,正弦/余弦编码不是随便选的——它确保了位置信息以一种模型容易利用且可泛化到未见长度的方式注入。
从 sinusoidal 到 learned:进化的驱动力
BERT 和 GPT 系列选择了可学习的绝对位置编码(learned positional embeddings),而不是正弦/余弦固定编码。原因是:当你有足够的数据时,“让模型自己学”通常比”用人手设计的公式”更灵活。可学习编码可以为不同位置学习不同的”专有模式”,而不受限于正弦/余弦的固定形式。
缺点是:可学习编码无法泛化到训练时未见过的序列长度。 BERT 在 512 个位置上学了一组编码,给一个 513 长度的序列就不知道怎么处理了。
而正弦/余弦编码理论上可以泛化到任意长度——因为它是用解析公式计算的,不是查表。
RoPE:当代的最佳实践
2023 年以后,旋转位置编码(Rotary Position Embedding, RoPE)逐渐成为新模型的标准选择(LLaMA、Mistral、Qwen 等)。它的思想非常巧妙:
不直接把位置编码加到输入向量上,而是通过旋转 Q 和 K 向量来注入位置信息。
具体来说,RoPE 把位置信息编码为一个旋转矩阵,作用在 Q 和 K 上:
f_q(x_m, m) = R_m · W_q · x_mf_k(x_n, n) = R_n · W_k · x_n然后 attention score 变为:
(f_q^T) · f_k = (R_m · W_q · x_m)^T · (R_n · W_k · x_n) = (W_q · x_m)^T · (R_{-m} · R_n) · (W_k · x_n) = (W_q · x_m)^T · R_{n-m} · (W_k · x_n)关键点:attention score 只取决于 token 的相对距离(n-m),而不是绝对位置(m 和 n 分别的值)。 这意味着:
- 模型学到的”两个 token 相隔 5 个位置时应该怎么交互”的知识,可以泛化到任意序列中任何相隔 5 个位置的两个 token
- 外推到更长序列时,相对位置关系仍然保持,所以 RoPE 本身对长文本有较好的扩展性
RoPE 是目前位置编码的理想方案:不仅能泛化到更长的序列,还保留了相对位置的核心信息,同时不引入额外的位置 embedding 参数。
架构设计深度解析
Encoder-Decoder 的对称美(以及 GPT 为什么打破它)
原始 Transformer 是 encoder-decoder 架构,这是从 seq2seq 时代继承下来的设计:
- Encoder:双向 attention,每个位置可以看到输入序列的所有位置。适合理解任务(分类、阅读理解)
- Decoder:带 mask 的因果 attention(只能看到当前位置及之前的位置)。适合生成任务(翻译、摘要)
中间的 Cross-Attention 连接两边的信息:Decoder 的 query 去 Encoder 输出的 key/value 中检索相关信息。
GPT 的贡献在于一个看似简单却很根本的洞察:如果你只做生成,直接堆叠 Decoder 层就够了。 单向 attention 配合自回归训练(预测下一个 token),在足够多的数据下,模型能学会所有需要的表示能力,不需要显式的双向编码。
这个选择有一个更深的解释:因果语言建模本质上是在学习联合概率分布 p(x) 的因子分解。 当你用自回归的方式(从左到右逐个生成 token)来建模语言时,你学到的隐层表示中同时包含了”理解”和”生成”所需的信息。GPT-3 和后续模型证明了这确实可行。
Feed-Forward Network 不是配角
Transformer 中的 FFN(两层线性变换 + ReLU/GELU 激活)经常被忽视,觉得只是”过一遍 MLP”。实际上它承担了 attention 层做不到的关键功能。
把 attention 层的数学重新审视一下:
Attention output = weighted sum of values = Σ α_ij v_j这是一个线性变换——加权求和,再多的 head 也只是多个线性变换的拼接。如果整个模型只有 attention 层,多层叠加也只是更深的线性变换。
FFN 的激活函数(ReLU/GELU)是非线性变换,它是整个模型中引入非线性能力的核心来源。 每一层 FFN 都在对每个位置的表示做一次独立的非线性变换,让模型能够表达更复杂的函数。
此外,有研究表明 FFN 扮演着记忆存储的角色:
- Attention 层负责路由信息(决定从哪里取、取什么)
- FFN 层负责存储和转换信息(把取来的信息做深度加工)
最近对 LLM 内部机制的探索中,研究者发现 FFN 中的某些神经元会可靠地响应特定的概念或事实(“法国的首都是” → 某个中间神经元激活 → “巴黎”),这种模式被称为知识神经元(Knowledge Neurons)。
Residual Connection 和 Layer Normalization:让训练变深的关键
如果你只堆 6 层 Transformer,残差连接和 LayerNorm 似乎只是”训练小技巧”。但当你堆到 80 层(LLaMA)、96 层(GPT-4 相传)时,这两个组件就是不可少的。
Residual Connection 解决了深层网络的梯度传递问题。梯度可以通过 skip connection 直接回传到浅层,避免因深层堆叠导致的梯度消失。更本质地说,每一层只需要学习相对于输入的残差,而不是直接学习完整的变换。这意味着如果某层学到的变换效果很差(甚至学不动),信息可以直接通过残差路径绕过它,不会破坏模型整体表现。
Layer Normalization 做的事情是:对每个 token 的表示做均值和方差的归一化。这有两个效果:
- 稳定训练——防止激活值过大或过小
- 允许更高的学习率——归一化后的激活值分布可控,不容易梯度爆炸
早期有很多对 LayerNorm 放置位置的争论(Post-LN vs Pre-LN)。Pre-LN(每一层的输入先过 LayerNorm,再进子层计算)是目前的主流选择(GPT、LLaMA 都是 Pre-LN),因为它在训练长序列时更稳定。
Masking 的艺术
Transformer 中有两种 mask:
-
Padding Mask:变长序列要 padding 到相同长度才能做 batch 计算。但 padding 位置没有意义,它们的 attention 权重必须被 mask 掉(置为 -inf,softmax 后为 0)。
-
Causal Mask(也叫 look-ahead mask):Decoder 中,位置 i 只能看到位置 1 到 i,不能看到 i+1 及以后。实现方式是在 attention score 矩阵的上三角填 -inf。
这个设计隐含了自回归生成的核心约束:模型不能作弊,不能提前看到未来要预测的 token。
训练动态:架构之外的智慧
Warmup + Decay 学习率调度
Transformer 原文使用了一个特殊的学习率调度:
lr = d_model^(-0.5) * min(step^(-0.5), step · warmup_steps^(-1.5))前 warmup_steps 步线性增长,之后按 step 的平方根衰减。为什么这样做?
直觉是:模型在训练初期非常脆弱。 刚初始化的参数输出的 attention 分布接近于均匀分布(因为 QK^T 的方差很大,softmax 做不好),这个时候如果用大学习率,模型可能一步就跳到某个严重的局部极小,再也回不来。
Warmup 让模型在参数还比较随机的时候用小学习率,先让 attention 机制”学会基本的对齐”,再用大的学习率加速收敛。平方根衰减则是因为训练后期更大的步数带来的信息增益递减,需要逐步降低学习率来控制噪声。
这个设计和 Adam 优化器配合尤其重要:Adam 的动量和自适应学习率在初期可能因为方差估计不准确而过早稳定,warmup 正好给了 Adam 一个”预热期”来稳定它的统计量。
Label Smoothing
Transformer 原文还用了 label smoothing(标签平滑):把 one-hot 目标分布替换为:
y_k_smooth = y_k · (1 - ε) + ε / V其中 V 是词表大小,ε 通常取 0.1。
这背后的直觉是:one-hot 目标等价于要求模型对自己预测的每个 token 都达到 100% 的置信度,这容易导致过拟合,还会让模型学出过度自信的分布。 Label smoothing 给模型留了”犯错的空间”,让 softmax 输出分布不再追求极端的 0 和 1,而是容忍一定的非目标概率。
有趣的是,label smoothing 在文本生成领域的地位近年来有所下降——GPT 系列和 LLaMA 都不再使用它。有观点认为 label smoothing 会让模型生成时更”发散”,不利于需要精确预测下一个 token 的自回归任务。
从代码理解 Transformer
理论讲了这么多,但代码能帮你建立真实的直觉。下面是一个最小化的 PyTorch Transformer 实现,聚焦最核心的部分:
import torchimport torch.nn as nnimport torch.nn.functional as Fimport math
class MultiHeadAttention(nn.Module): def __init__(self, d_model, n_heads, dropout=0.1): super().__init__() assert d_model % n_heads == 0
self.d_model = d_model self.n_heads = n_heads self.d_k = d_model // n_heads
self.w_q = nn.Linear(d_model, d_model) self.w_k = nn.Linear(d_model, d_model) self.w_v = nn.Linear(d_model, d_model) self.w_o = nn.Linear(d_model, d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, x, mask=None): batch_size, seq_len, _ = x.size()
# 1. 线性投影 + 分头 Q = self.w_q(x).view(batch_size, seq_len, self.n_heads, self.d_k).transpose(1, 2) K = self.w_k(x).view(batch_size, seq_len, self.n_heads, self.d_k).transpose(1, 2) V = self.w_v(x).view(batch_size, seq_len, self.n_heads, self.d_k).transpose(1, 2)
# 2. Scaled Dot-Product Attention scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)
if mask is not None: scores = scores.masked_fill(mask == 0, float('-inf'))
attn_weights = F.softmax(scores, dim=-1) attn_weights = self.dropout(attn_weights)
context = torch.matmul(attn_weights, V)
# 3. 拼接 + 输出投影 context = context.transpose(1, 2).contiguous().view( batch_size, seq_len, self.d_model ) output = self.w_o(context)
return output
class FeedForward(nn.Module): def __init__(self, d_model, d_ff, dropout=0.1): super().__init__() self.linear1 = nn.Linear(d_model, d_ff) self.linear2 = nn.Linear(d_ff, d_model) self.dropout = nn.Dropout(dropout)
def forward(self, x): return self.linear2(self.dropout(F.gelu(self.linear1(x))))
class TransformerBlock(nn.Module): def __init__(self, d_model, n_heads, d_ff, dropout=0.1): super().__init__() self.attention = MultiHeadAttention(d_model, n_heads, dropout) self.ffn = FeedForward(d_model, d_ff, dropout) self.norm1 = nn.LayerNorm(d_model) self.norm2 = nn.LayerNorm(d_model) self.dropout = nn.Dropout(dropout)
def forward(self, x, mask=None): # Pre-LN: 先归一化,再进子层 attn_output = self.attention(self.norm1(x), mask) x = x + self.dropout(attn_output)
ffn_output = self.ffn(self.norm2(x)) x = x + self.dropout(ffn_output)
return x注意上面的代码用了 Pre-LN(先归一化后计算)而非原始论文的 Post-LN。这是当代实践的主流选择。
Transformer 的局限与演化
任何诚实的分析都必须承认:Transformer 不是终极答案。它有三个根本性的局限。
长上下文问题
这是最广为人知的问题。Self-attention 的计算复杂度是 O(n²),意味着序列长度翻倍,计算成本翻四倍。处理 100k token 的上下文时,一个 attention 层的计算量就超过了一些小模型的总参数量。
目前主流方案包括:
- Ring Attention / Flash Attention:通过分块计算(tiling)和重计算(recomputation)降低显存占用,提高硬件利用率。Flash Attention 是实际部署中的标配。
- Sparse Attention(Longformer, BigBird):每个 token 只关注局部邻居 + 少量全局 token,把复杂度降到 O(n)。代价是失去了”所有位置都有交互”的灵活性。
- Contextual/Selective Attention:动态决定哪些 token 需要全量 attention。
二次复杂度的结构矛盾
这是一个更深层的问题:对于需要大量长期依赖的推理任务(如长文档推理、代码理解),O(n²) 不是”浪费”而是”必要的”。 你不能同时又想要所有位置的交互能力同时又抱怨它贵——这两者是同一个性质。
真正的出路可能不在”优化 attention”,而在改变信息交互的基本单元。这导向了状态空间模型(SSM)和 Mamba 的方向。
Mamba 和状态空间模型
Mamba(2023)回到了 RNN 的思路——用固定大小的隐状态来压缩序列信息——但去掉了 RNN 的核心痛点:串行计算。
Mamba 的设计蕴含了一个很有趣的权衡:隐状态大小固定,所以计算复杂度是 O(n),但信息会随着距离增加而模糊(因为隐状态空间有限,必须做选择性压缩)。 Transformer 是”不惜成本保留所有信息”,Mamba 是”用固定预算压缩,压缩策略可学习”。
最早的实验结果(Mamba-2, xLSTM)表明,在小规模上 SSM 可以匹敌甚至超过 Transformer。但在大模型(>70B)的尺度上,当前最强的基准模型(LLaMA-3, GPT-4, DeepSeek-V3)仍然基于 Transformer。
这反映了一个开放问题:O(n) 的复杂度能否在足够大的容量下保持和 O(n²) 同样的表达能力? 如果能,Transformer 会被取代;如果不能,我们会继续在用 Flash Attention 优化的 O(n²) 下演化。
总结
Transformer 的核心贡献不在于具体的组件(attention 不是它发明的,LayerNorm 不是,残差连接也不是),而在于把注意力机制从”补充组件”升级为”主导架构”。它用 attention 替代了 RNN 的信息循环传递,让信息的”路由”不再是距离的函数,而是内容的函数。
这个选择催生了两个规模法则:
- 参数规模法则:数据足够多时,更大的模型永远更好(直到下一个瓶颈)
- 计算规模法则:训练时的计算量是决定模型能力的最关键因素
它们在 ChatGPT/GPT-4 的时代得到了最充分的验证。
Transformer 不会永远统治这个领域。就像 RNN 取代了 n-gram、Transformer 取代了 RNN一样,下一个范式的种子正在出现——也许是 Mamba,也许是 Test-Time Compute 主导的新范式,也许是 Agentic 架构。但 Transformer 留下了一个不可磨灭的知产:“让你的模型自己决定信息怎么流动” 这个思想,会一直延续下去。