Skip to content

背景

长期以来 RNN、LSTM 和 GRU 在序列建模和转换问题上确立为最先进的办法。此后,在对这些旧架构进行性能调优和改进上花费了大量的时间。

这类循环模型通常需要沿着输入和输出序列的符号位置进行计算,也就是说,它必须按照顺序做处理,每次计算都强依赖于前一次计算的结果。假设处理句子 I love you,我们想象整个处理过程是一个 function,那它就是:

go
func RecurseRNN(prevState State, tokens []string, t int) State {
    if t == len(tokens) {
        return prevState
    }
    currentState := RNNCell(prevState, tokens[t])
    return RecurseRNN(currentState, tokens, t+1)
}

在我们想要计算 love 的时候,RNNCell 需要传入上一个状态,也就是从 I 中计算出来的状态。

由于这种强依赖关系,RNN 模型无法进行并行化,当序列长度增加,训练时间和资源占用也会线性增加。随着我们计算资源的发展,多核心已经成为主流,这种串行执行的方式在当今时代非常低效的。


注意力机制

注意力机制和 RNN 的本质不同是它允许对依赖关系进行建模,而无需考虑它们是否是相邻的。

假设把传统的 RNN 看作 LinkedList,找个节点的数据就需要从零开始遍历整个链表。

注意力机制就更像一个 HashMap。无论两个词间隔 500 个词还是 5 个词,都能通过指针直接访问到——O(1) 就能「直通」。

注意:HashMap 的指针是天然写好的,而注意力的「指针」需要先算出来。具体来说,每个词会先对自己和全句其他词做一次打分(QK^T 全匹配),算出「谁和我更相关」,得到一串「软指针」(权重),再用这串权重去加权取 Value。这就像 HashMap 多了一步「先搜索 key 再取值」的过程。

然而,这种注意力机制此前只是一种辅助 RNN 的机制,核心机制还是 RNN 的递归顺序遍历。

而这篇论文的核心就是提出 Transformer 架构,把这个曾经作为辅助的注意力机制完全提升为架构中的核心机制,完全抛弃无法并行化的 RNN。


由于 RNN 的递归太慢,之前的方式是改用 CNN(卷积)。它提供了一定的并行能力。它就像是滑动窗口:

  • 计算第 10 个词的特征时只看 [9, 10, 11] 三个词
  • 计算第 100 个词的特征时,只看 [99, 100, 101] 三个词

这里就会诞生出一个疑问:用 99-101 三个词来计算第 100 个词的特征,结果和从 1 递归到 100 去计算第 100 个词的特征显然不一样——前者只看 3 个词,后者看 100 个词。

CNN 解决这个问题的方法就是堆叠多层

层级能看到的范围
第 1 层[1, 2, 3]
第 2 层[[1,2,3], [2,3,4], [3,4,5]]
...随着一次次堆叠,能看到的范围越来越大

随着一次次堆叠,能看到的范围越来越大,最终覆盖从 1 到 100 的所有词。

随着路径的增加,信息传递的路径也变长了。假设每一层卷积都能保留 90% 的信息,那么 10 层之后就只剩下 0.9¹⁰ = 34% 了。信息损失严重。

这里有三种不同的复杂度指标

  1. 总计算量(Work):需要做多少次算术运算,attention 是 O(n²)
  2. 串行步数(Span / 路径长度):信息从起点到终点要经过多少步传递,attention 直接看全句所以是 O(1)
  3. 实际运行时间(Wall-clock):GPU 并行后,多大的计算量能跑多快

Transformer 在 路径长度 上赢了——长距离依赖不再需要一级级传;总计算量 确实变大了,但 GPU 并行下反而更快。

这个操作通常是 O(log n) 或 O(n) 的复杂度。

而强力的 Transformer 注意力机制将路径长度变成了 O(1)

自注意力机制是一种关联单个序列中不同位置以计算序列表示的注意力机制。可以理解为:让句子里的每一个词都去扫描同一句里的其他词,获得对自己最有用的信息。

你可能会有疑惑:那这样复杂度不是 O(n²) 了吗?确实是的,因为每个句子里有 n 个词,每个词都要和全句的 n 个词做一次匹配,所以会有 n×n 个匹配对。

但是由于每一步操作是可以并行执行的,它们可以同时开始扫描。最终的串行步数是 O(1),因为每个词的计算时间不再依赖其他词的计算结果,而是快速遍历整个句子,找到对自己最有用的信息。这个架构看起来复杂度高了,但由于 GPU 的强大并行计算能力,实际运行时间反而大幅降低——每个词的计算可以同时开始。


模型架构

Transformer 在做什么

Transformer 仍然是 编码器-解码器(Encoder-Decoder) 架构:

组件功能
编码器把整句源语言读完,变成一套「可查询的语义记忆」(一堆向量)
解码器一边生成目标语言,一边用注意力去「查这套记忆」,逐词写出译文

你可以把它想成:编码器先把整篇文章做成一个「可检索数据库」,解码器负责不断发起查询并写答案。


1. 输入端:Embedding + Position Encoding

让词既「有意义」又「有顺序」

1.1 词嵌入(Learned Embeddings):给词一个「数字身份」再翻译成向量

模型拿到的不是「爱」这个字,而是一个 ID(比如 520)。

嵌入层就是一个巨大的查表系统:

  • 输入:ID = 520
  • 输出:一个 512 维向量(假设 d_model=512)

为什么要变成向量?因为向量空间可以表达「相似性」:「爱」和「喜欢」会在空间里更接近——模型就能利用这种几何关系来理解语义,而不是把它们当成两个完全无关的字符串。

1.2 为什么要做 embedding scaling:乘上

在把位置编码加到词向量之前,常见做法是把 embedding 乘上 (例如 ):

  • embedding 初始化时数值幅度往往较小
  • 乘上 可以把向量整体尺度拉到更合适的范围
  • 这样在后续做点积注意力时,分数的尺度更稳定,训练更顺(也避免某些分量被位置编码「压住」或相互尺度不匹配)

如果不这么做,可以想象对于这个词义来说,位置编码可能反而「淹没」了词向量的贡献,导致模型难以学习到有效的表示,而只学到了位置相关的信息,这对做理解是不利的。

不是为了「让位置编码更大」,而是为了把整体数值尺度调到更舒服的工作区间,让后面的点积/softmax 更稳定。

1.3 位置编码(Position Encoding):让「我爱你」和「你爱我」不再一样

如果只有词嵌入,「我爱你」和「你爱我」在「词集合」层面几乎一样,模型很难知道谁在前谁在后。

所以要给每个词加一个同维度的位置向量:

经典 Transformer 用 sin/cos 的不同频率来做 PE(pos),核心好处是:

  • 每个位置都有独特的「波形签名」
  • 相对位置关系可以被表达为一种稳定的变换,因为 sin/cos 有周期性
    • 例如「前后相差 1 个位置」对应到 sin/cos 空间就是一个固定的相位差
  • 所以即使没见过很长的句子,也能更好外推到更远的位置

如果位置只是 1,2,3,4,模型很难自动学会「100 和 99 的关系 ≈ 4 和 3 的关系」。sin/cos 让相对位移对应到可学习的规律,注意力在匹配时更容易捕捉「谁离谁近、差了多少」。


2. 编码器栈(Encoder Stack):把整句变成「可检索语义记忆」

编码器由 N=6 个完全相同的层堆叠(经典设置)。每层有两块核心模块:

  1. 多头自注意力(Multi-Head Self-Attention):让每个词「看全句」,建立上下文关联
  2. 位置前馈网络(Position-wise Feed-Forward Network, FFN):对每个位置独立做「特征加工」

并且每个子层外都有同一套「稳定训练三件套」:

  • 残差连接(Residual):信息直通,避免越堆越深后信息/梯度丢失
  • LayerNorm:把数值分布拉回稳定范围

注意力负责「看懂上下文怎么连」FFN 负责「把每个位置的表示再加工得更有用更特征化」


3. 注意力机制(Attention):用「查询-索引-内容」做一次可微检索

注意力可以理解成:拿一个查询 Q,去一堆 Key/Value 里检索,并把检索到的 Value 加权汇总

概念含义类比
Q(Query)你的搜索请求(例如:「我是谓语,我想找主语」)你要查什么
K(Key)所有词的索引标签(「我是主语/谓语/宾语」)索引字段
V(Value)所有词的实际内容(「我 / 爱 / 你」)真正要取出的内容

对应到「像 HashMap 检索」的:

  • K 像「索引字段」,用于匹配
  • V 像「真正要取出的内容」
  • Q 和 K 必须同维度(d_k),因为要做点积相似度
  • V 的维度(d_v) 可以不同(最后汇总的是内容,不一定和索引同形)

3.1 公式拆解

逐步理解:

  1. QK^T:Q 和所有 K 做点积,得到「匹配分数」(谁更像我想找的)
  2. 除以 :缩放,防止维度大时点积过大导致 softmax 过尖、梯度变差,看不出细微差别
  3. softmax:把分数变成权重分布(像概率一样加起来为 1)
  4. 乘 V:按权重把信息汇总成一个向量输出

3.2 为什么要多头(Multi-Head)

只做一套 QKV 往往不够,因为「相似」不是单一维度的概念。多头做法是:

  • 用不同的投影矩阵把同一份输入投到不同子空间
  • 每个头在自己的子空间里做一次注意力(论文里 h=8)
  • 最后把各头结果拼接,再过一层线性变换融合

我们可以这么理解(便于想象):

  • 有的头更关注语法结构
  • 有的头更关注指代关系
  • 有的头更关注语义搭配/情感色彩
  • 最后合并成更强的表示

本质上:并行做多组不同的投影与匹配,相当于用多个不同的相似度空间同时检索,最终拼起来信息更丰富。模型到底学到了什么分工,是训练结果自学习出来的,不是我们预先规定哪个头必须看什么。

在每个头里都是完整的注意力机制,只不过每个头看的是同一句话的不同「侧面」,通过最后把这些侧面拼起来,模型能获得更丰富的上下文理解。

3.3 编码器自注意力(Encoder Self-Attention):Q/K/V 都来自同一句

在编码器里,Q、K、V 全部来自上一层编码器输出

例子:「我 爱 编程」中处理「编程」这个位置时,它会用注意力扫全句,把「我(主语)」「爱(谓语)」等关键信息融合进「编程」的向量里,让它不仅是「一个词」,而是「在这个句子里它扮演什么角色」。


4. Position-wise FFN:对每个词做同一套「精加工」

注意力解决的是「谁和谁相关」,但相关之后还要「怎么提炼」。

FFN 对每个位置独立运行:

  • 同一套参数,逐位置共享(所以叫 position-wise)
  • 不让不同位置互相混合(混合已经由注意力做过了)
  • 作用是把注意力融合后的表示再变换/筛选,提取更可用的特征

5. 进入解码器:先把已生成的中文也做 Embedding + PE

编码器结束后输出一串向量(整句语义记忆)。

解码器这边的输入不是「源句」,而是已经生成出来的目标句前缀(比如已经生成了「我爱…」),同样:

  • token → embedding(再乘
  • 加位置编码

然后送入解码器栈。


6. 解码器栈(Decoder Stack):一边写,一边查(并且不能偷看未来)

解码器同样堆叠 N=6 层(经典设置),每层三个子层:

  1. Masked Multi-Head Self-Attention:只允许看见「当前位置之前」的已生成词
  2. Encoder-Decoder Attention(Cross-Attention):用解码器的 Q 去查编码器输出的 K/V
  3. Position-wise FFN:同样的逐位置精加工

同样每个子层外面也有 Residual + LayerNorm。

6.1 为什么必须 Mask:防止「抄答案」

推理时解码器是逐词生成的:先吐「我」,再吐「爱」,再吐「你」。

如果在生成「爱」的时候能看到后面的「你」,它就可能学会「直接拷贝未来 token」,训练看起来很准,但生成任务的因果性被破坏。

所以 mask 的做法是:

  • 把当前位置之后的注意力分数设为 -∞
  • softmax 后那些位置权重变成 0
  • 从而确保每步只能利用历史信息

每个词的生成可以当做一个线程,线程只能看到「happened before」的信息,而不该看到「happened after」的信息,即使「happened after」的信息已经被生成。

6.2 Decoder Self-Attention:Q/K/V 来自「已生成部分」,并且带 mask

  • Q/K/V 都来自解码器当前层的输入(目标前缀)
  • mask 保证因果性
    • 例:生成「爱」时只能看「我」,不能看「编程/你」

6.3 Encoder-Decoder Attention:解码器发起查询,去编码器记忆里找证据

  • Q:来自解码器(「我现在要生成下一个词,我需要什么信息?」)
  • K、V:来自编码器最终输出(源句语义记忆库)

当解码器准备写下「爱」时,它用自己的 Q 去编码器的 K 中匹配,找到与源句中 「love」 最相关的位置,再从对应的 V 里取出「love 的语义特征」,从而生成「爱」。

解码器负责写作,编码器负责供稿;cross-attention 就是「写作时查资料」。


7. 输出端:Linear → Softmax(把「人看不懂的向量」变成「词表概率」)

经过解码器最后一层后,每个时间步仍是一个 d_model=512 维向量,人类当然看不懂。

所以要做两步:

  1. 线性层(Linear):把 512 维投影到词表大小 V(比如 30000)

    • 得到长度为 V 的 logits(每个词一个分数)
  2. Softmax:把 logits 变成概率分布

    • 然后选取当前步输出词

7.1 权重共享(Weight Tying)

  • 输入 embedding 矩阵(ID → 向量)
  • 输出投影矩阵(向量 → 词表 logits)

很多实现会共享同一套权重(常见还会把解码器输入 embedding 也一起共享)。核心好处:

  1. 省参数:词表大时,这一块参数非常吓人
  2. 更一致:如果模型「读入时」认为「爱」对应某种特征,那么「写出时」也用同一套特征空间去表达「爱」,读写对齐更自然
  3. 往往还能提升效果:因为输入/输出空间被强约束到同一语义几何结构里

如果在读取的时候认为爱是某一种特征,那么在写入的时候也应该用同样的特征去表达


8. 架构总结

组件功能
Embedding + PE把词变成「有语义、带位置」的向量序列
Encoder(自注意力 + FFN)× N把整句压成「可检索语义记忆库」
Decoder(masked 自注意力 + cross-attention + FFN)× N一边根据历史生成,一边去记忆库查证据
Linear + softmax把向量翻译成「下一词概率」,循环生成整句

为什么选择自注意力

假设句子长度为 n,向量维度为 d。

模型时间复杂度
Self-AttentionO(n² · d)
RNNO(n · d²)

在现代 GPU 核心数量管够的情况下,这个 n² 的复杂度并不会带来太大影响,反而由于 self-attention 可以并行化,它的实际运行时间会远低于 RNN。

不仅在速度上有优势,在长距离依赖的路径长度上(获得第一个词和第一百个词之间的依赖关系),self-attention 的路径长度为 1——因为它是直接扫描全句,而 RNN 的路径长度为 O(n),因为它必须一个词一个词传递下去。

还有一个好处是它更透明。例如在翻译 The animal didn't cross the street because it was too tired. 这个句子的时候,it 指代的是 animal 还是 street

通过 self-attention 我们可以清晰地看到 itanimal 之间的注意力权重更高,所以 it 指代的是 animal。这样证明了模型确实学会了句子中的指代关系,而不是简单地记忆训练数据。


训练

论文中使用的是标准的英语-德语和英语-法语数据集进行训练。其中有一个细节:他不是按照句子数量做的训练,而是按照 token。Token 类似于我们内存管理中的页,也就是 GPU 内存的固定单位。

这是为了避免:当 32 个短句(每句 5 个词)和 32 个长句(每句 50 个词)混合在一起训练时,长句占用的内存远高于短句,导致 GPU 内存不均衡使用,从而影响训练效率。

优化器

论文采用了 Adam 优化器,我们不需要去看他的公式,重点在于:它使用了一个学习率预热和衰减的策略。在前 4000 个 step 当中,学习率线性增加,之后再按照反比例进行衰减。

Dropout

同时为了防止过拟合(模型去死记硬背对应的训练数据),对每个子层的输出应用 Dropout,概率为 0.1,也就意味着随机让 10% 的神经元失活,不参与计算。这样可以让模型学到更通用的特征,而不是死记硬背训练数据。

举个例子:假如一共有 10 个神经元,其中有「超级天才」,它一下就发现了特征,其他 9 个发现已经把活干完了,就开始画水,权重慢慢变小。通过加入这项技术,那个「超级天才」神经元有 10% 的概率被随机失活掉,这样其他神经元就必须自己去发现特征,不能依赖这个天才。最终模型学到的特征会更加通用。

标签平滑

为了提高泛化能力,一般模型会给正确答案接近 1 的概率,而在当前文中,正确的只有 0.9,这样可以显著提高模型的泛化能力,防止过拟合,死记硬背正确答案而不是学到通用特征。


总结

这篇文章围绕 Transformer 架构的核心——自注意力机制,从问题出发讲述了它为何能取代 RNN。

RNN 的致命伤是串行计算:每个词的计算必须等前一个词完成,导致无法并行、训练慢、长距离依赖要一级级传递(O(n) 路径长度)。

Transformer 的解法是用自注意力让每个词直接看全句所有词:

  • 路径长度变为 O(1),长距离依赖不再需要逐级传递
  • 所有匹配计算可以并行执行,GPU 充分利用后实际训练速度大幅提升
  • 通过 Q/K/V 的软指针检索,模型能学到清晰的上下文关联(可解释性)

架构层面,Transformer 沿用编码器-解码器结构:

  • 编码器把句子压成「可检索语义记忆库」
  • 解码器一边生成一边用注意力去「查资料」
  • 多头注意力让模型在多个相似度空间同时检索,信息更丰富
  • FFN 对每个位置做独立精加工

训练技巧上,论文用了 token 级批处理、Adam 学习率预热+衰减、Dropout 防过拟合、标签平滑提升泛化能力——这些细节让大模型训练更稳定高效。

总的来说,Transformer 通过「全连接+并行化」的思路,彻底摆脱了 RNN 的时序瓶颈,奠定了现代大语言模型的基础架构,也正是这篇论文开启了 NLP 领域的新时代。