☰
Transformer零基础详解:从自注意力到手算理解架构
2026/10/18 8:36:38 网站建设 项目流程

第一次翻开《Attention Is All You Need》这篇论文的时候,我只撑了不到十分钟就合上了。满屏的公式、矩阵维度和看起来很随意的“缩放点积注意力”,对一个当时只写过几天基础模型代码的人来说,完全是天书。但后来我用了整整三个周末,把它从背景到公式再到多头的每一行都啃了一遍,最关键的是,自己动手把里面那个极其简单的计算流程手算过一次之后,整个架构突然就“通”了。 这篇论文提出了 Transformer 架构,核心观点用标题就能概括:只需要注意力。它把过去处理序列任务离不开的循环结构(RNN)和卷积结构(CNN)全部丢掉,只靠一种叫自注意力的机制,就在翻译、生成和后续几乎所有自然语言处理任务上做出了碾压级的效果。如果你也是零基础,希望真正理解 Transformer 而不是背概念,那这篇文章就是按我当时理解的路径来写的:先搞清楚它解决了什么问题,再手算一次注意力,最后把 Encoder、Decoder 和那些最容易想错的细节全部拆开。

1. 先说清楚:Attention 到底解决了什么老问题

要理解一个模型为什么牛,不能只看它有什么花样,要先看它站在什么位置上,替换掉了什么。

1.1 在 Transformer 之前,序列模型是怎么工作的

在 2017 年之前,主流处理序列任务的方法是循环神经网络(RNN),以及它的进阶版本 LSTM 和 GRU。拿机器翻译举例:输入是一句英文,输出是一句中文。RNN 的做法是一个词一个词地“读”,每读到一个词就更新一次隐藏状态,这个状态就像人脑里暂时记下的“读到这里为止的信息压缩包”。

这个设计的核心问题是:第 t 个词的信息要传到第 t+k 个词,必须老老实实经过 k 次状态更新。句子越长,路径越长,信息损耗越严重,梯度也容易在长距离反向传播中消失。所以大家说 RNN 不适合长距离依赖,就是这个原因。LSTM 用门控机制缓解了这个问题,但它并没有从根本上改变“必须逐级传递”这件事。

另一个让研究者头疼的问题是 RNN 无法并行。因为计算第 t 步的隐藏状态,依赖第 t-1 步的输出,这天然是一个串行过程。在 GPU 时代,等于你有一百个工人却只能让他们排成一队逐个干活,效率上非常吃亏。

1.2 注意力机制:从“按顺序搬砖”变成“全场直接打分”

注意力机制本身不是 Transformer 发明的。早在早几年的机器翻译工作中,研究者就会在 seq2seq 模型上额外加一个注意力模块:解码器在生成某个词时,不再只看最后那个压缩状态,而是回头去扫一遍源语言所有位置的隐藏状态,按“相关度”挑重点信息看。

Transformer 的激进之处在于:连 RNN 都不用了。它直接把注意力从辅助模块提升为主干结构,让任意两个词之间都能一步直达地计算相互关系。这意味着,句子里的第 1 个词和第 100 个词,只要内容足够相关,就能产生一个很大的注意力权重,彻底绕开“逐级传递”的长路径问题;同时,所有位置的相互打分可以在一个矩阵乘法里全部算完,GPU 终于能真正并行地干活。

理解这一个点非常重要,因为它解释了一个经常被问的问题:为什么论文敢叫“Attention Is All You Need”?潜台词是:过去我们认为序列建模必须依赖某个按顺序传递信息的骨架(RNN),现在用一句话来总结 Transformer 的核心发现——不需要骨架,两两直接建联就够了。后面的 Multi-Head、位置编码、残差结构,本质上都是在解决“只用注意力”之后新暴露出来的工程问题。

2. 自注意力的完整计算:从 Q/K/V 到加权求和

如果你打开论文,最核心的公式只有一行:

[ Attention(Q,K,V)=softmax(\frac{QK^T}{\sqrt{d_k}})V ]

别害怕,我不会让你直接跳上去看这个公式,而是把它拆成计算步骤,每一步配一个极简的示例。

2.1 词向量从哪里来,Q/K/V 又是怎么生成的

一切开始于词向量。假设我们有三句话里的三个词:“猫”、“趴”、“垫子”。每个词要先转换成一个固定长度的向量。在真实模型里,这个向量可能是 512 维的,这里我们为了演示方便,假设每个词是一个 3 维向量:

猫 -> [0.5, 1.0, 0.2] 趴 -> [1.0, 0.5, 0.3] 垫子 -> [0.3, 1.0, 0.8]

每个词向量有 3 个数字,代表它初始的语义特征。这一步可以通过随机初始化的 embedding 表,也可以直接用预训练好的词向量。

接下来是 Q、K、V 三胞胎的诞生。每个词向量会分别乘上三个可学习的权重矩阵 Wq、Wk、Wv,得到三个新向量:

q = x @ Wq k = x @ Wk v = x @ Wv

Q 是 Query,可以理解为“我想查什么”;K 是 Key,可以理解为“我身上有什么标签”;V 是 Value,可以理解为“我真正能提供的内容”。为了简化,我先假设三个权重矩阵都是单位矩阵,这样 q、k、v 就等于原来的词向量。现实中它们经过训练会是不同的矩阵,这并不影响你理解后续的计算流程。

2.2 打分、缩放与 Softmax:一次微型手算

注意力最关键的一步,是让每个词去和句子里所有词“互相打分”。

假设我们正在处理“猫”这个词,它就是当前的中心词。我们拿着“猫”的 Query,去和句子里每个词的 Key 做点积:

得分(猫, 猫) = [0.5, 1.0, 0.2] · [0.5, 1.0, 0.2] = 1.29 得分(猫, 趴) = [0.5, 1.0, 0.2] · [1.0, 0.5, 0.3] = 1.06 得分(猫, 垫子) = [0.5, 1.0, 0.2] · [0.3, 1.0, 0.8] = 1.31

点积越大,说明这两个词在当前语义空间里越“相关”。但直接拿这个分数去加权还有问题:当向量维度很高(比如 512 维)时,点积的数值会变得非常大,导致后续 softmax 进入梯度极小的饱和区。所以论文里加了一个缩放因子:

[ score = \frac{q \cdot k}{\sqrt{d_k}} ]

其中 (d_k) 是 Key 向量的维度。在我们例子里是 3,所以每个分数除以 (\sqrt{3} \approx 1.732)。这一步的意义我会在后面专开一节细讲,现在你先记住:除以根号维度是为了控制分数的量级。

然后对这三个分数做 softmax,让它们的总和变成 1,同时保持相对大小。计算过程大致是这样:

exp(1.29/1.732) ≈ 2.101 exp(1.06/1.732) ≈ 1.850 exp(1.31/1.732) ≈ 2.128 总和 ≈ 6.079 softmax 后: 对“猫” ≈ 2.101 / 6.079 ≈ 0.346 对“趴” ≈ 1.850 / 6.079 ≈ 0.304 对“垫子” ≈ 2.128 / 6.079 ≈ 0.350

这三个数就是注意力权重,表示“在理解‘猫’这个词时,我应该分别花多少注意力在‘猫’、‘趴’、‘垫子’上”。你注意到没有,即使只是用简化向量算出来的结果,“猫”和“垫子”的关联分数已经很高了,实际训练好的模型也会自己学会这种关联。

2.3 加权求和:为什么能聚合全局信息

拿到注意力权重后,最后一步是把每个词的 Value 加权求和,得到“猫”这个位置的新向量:

新“猫”向量 = 0.346 * v(猫) + 0.304 * v(趴) + 0.350 * v(垫子)

因为 V 正好等于原始向量,这算出来就是个融合了三个词信息的向量。如果是真实模型,V 经过了可学习的线性变换,这个加权结果就代表“在结合了全句信息之后,‘猫’在这里应该有什么样的特征表达”。

把这套流程对句子里的每个词都执行一遍,你就得到了一整层自注意力的输出。整个过程乘法和加法全是矩阵运算,一次前向传播就能完成所有位置的计算,这正是 Transformer 能高效并行的基础。

3. 把单头变成多头:Multi-Head 与位置编码的意义

前面我们算的是“单头注意力”。论文真正用的是多头注意力(Multi-Head Attention),还要加上位置编码、残差连接和层归一化,否则模型根本训不起来。

3.1 多头注意力在学什么

单头注意力的问题在于,它只能从一种关系维度去看词与词的关联。但自然语言里的关系是多元的:两个词可能在语法上是主谓关系,在语义上是同义关系,在位置上存在修饰关系。一个打分函数学不过来这么多维度的相关性。

多头注意力的做法非常简单粗暴:准备多组 (W_q, W_k, W_v),每组独立地做一次完整注意力计算,得到多组新向量,然后把它们拼起来,再乘一个线性变换到目标维度。论文里用的是 8 个头,也就是 8 组权重。

用生活化的类比:单头注意力是一个人在评估词与词的相关度,他可能只擅长根据“词性”来判断;多头则像叫了 8 个专家,有人看语义,有人看句法,有人看位置,最后开会把意见汇总。每个头只观察到原来 512 维向量中的一个小切片(在论文中每头是 64 维),但它们从不同的子空间去捕捉信息,合起来表达能力就上来了。

实际实现时,为了效率,不是把 8 组矩阵各自循环一遍,而是把 8 个头的 (W_q, W_k, W_v) 堆叠在一起,通过一个巨大的矩阵乘法一次性算出所有注意力结果,再拆分、分别做 softmax 和加权,最后拼接。这也是很多读者看源码时最容易懵的地方——你如果只盯着代码看,会觉得它和公式完全对不上,但把矩阵维度列出来就清楚了。

3.2 位置编码:给模型补上“顺序”这根弦

自注意力有一个致命问题:它本质上把句子当成一个集合(set)来处理。在上面那个三词例子里,你把“猫”、“趴”、“垫子”的顺序打乱成“垫子”、“猫”、“趴”,每一对词之间的点积分数完全不变,注意力权重不变,输出也不变。这对语言来说是无法接受的:“猫咬人”和“人咬猫”完全是两个意思。

循环网络天然知道词序,因为它是按顺序读取的。Transformer 放弃了这层结构,就必须主动把位置信息“灌”进输入。论文用的是正弦和余弦函数编码,公式是这样:

[ PE_{(pos, 2i)} = \sin(pos / 10000^{2i/d_{model}}) ] [ PE_{(pos, 2i+1)} = \cos(pos / 10000^{2i/d_{model}}) ]

其中 (pos) 表示词在句子中的位置,(i) 表示向量维度下标,(d_{model}) 是向量总维度。这些位置向量直接加到词向量上,模型就知道“哪些词在第几个位置”了。

选用三角函数而不是简单的整数位置,背后有两个实在的理由:一是位置编码的值有界,稳定;二是三角函数的相对位置关系可以通过线性变换表示,模型更容易学到“某个词距离当前词 5 个位置”这类相对信息,对捕捉句法结构很有帮助。后续不少模型用可学习的位置编码也能工作,但论文的三角函数方案不需要训练参数,且能应对比训练集更长的序列,在当年是更优雅的选择。

3.3 残差连接与层归一化:让 Transformer 能训练得更稳

注意力输出并不是直接进入下一层,而是先和输入做残差相加,再做层归一化(Layer Normalization)。这个设计在论文里看似一笔带过,实际是模型能稳定堆叠到 6 层、12 层甚至更深的关键。

原因在于:只要注意力输出接近零,残差结构就退化为 (x) 本身。这意味着每一层在最坏情况下不会比上一层的表达能力更差,梯度也可以更顺畅地回传。层归一化则控制每个位置特征向量的均值和方差,让网络各个层的数据分布不过度发散,训练时学习率可以设置得更大胆,收敛也更稳定。

很多零基础的读者看到架构图里有“Add & Norm”这一小条,以为只是个无关紧要的细节,然后自己复现时发现模型怎么训都不收敛,或者一两层还行、一加深就爆炸,才意识到这个残差机制的分量。

4. 从公式到架构:Encoder、Decoder 与训练/推理的差异

理解了单层注意力,你已经有能力看懂论文的框架图了。Transformer 主体是左边 6 层 Encoder、右边 6 层 Decoder,中间用 Cross-Attention 衔接。

4.1 编码器每层到底做了几步

编码器中每一层只有两个核心子层:多头自注意力,和逐位置前馈网络(FFN)。每个子层外面都包着残差连接和层归一化。

自注意力子层我们已经讲清楚了,输入是一句完整句子的全部词向量,输出是每个词融合了全句信息后的新向量。前馈子层是一个对每个位置独立施加的两层全连接网络:

FFN(x) = max(0, x @ W1 + b1) @ W2 + b2

中间层的维度论文里设为 2048,比 512 的模型维度大 4 倍,先升维再降维,引入非线性变换。它不加任何跨位置的交互,就是逐位置地对特征做加工,相当于一个强大的特征提取器。实际操作中,因为所有位置共享同一套参数,它也可以被看成一个 1×1 卷积在逐位置操作。

为什么编码器不用掩码?因为编码器处理源语言句子时,每个词都允许看到全句其他词的信息,这是合理的:读一个英文句子,我们当然可以前后结合着理解每一个词。

4.2 解码器里的掩码自注意力

解码器的结构更复杂一些。它的第一个子层也是自注意力,但这个自注意力是带掩码的:在预测第 t 个词的时候,它只能看到第 1 到第 t 个词的注意力信息,看不到 t 后面的任何词。对应到矩阵里,就是把形状为 (n \times n) 的注意力矩阵右上角(未来的位置)全部替换成负无穷大,这样 softmax 之后的权重就会变成 0。

这个设计的直觉很简单:训练时我们有标准答案,可以一次把整个目标句子的所有位置并行处理,但如果某个位置的注意力能看到后面未来的词,模型就会“作弊”——还没预测的词提前出现在视野里,等于泄露了答案。

解码器的第二个子层是交叉注意力(Cross-Attention),这是 Encoder 和 Decoder 最重要的连接点。它同样有三件套 Q、K、V,但来源完全不同:Q 来自解码器上一个子层的输出,K 和 V 来自编码器最后一层的输出。用通俗的说法是:解码器在生成一个词之前,先拿着自己的当前状态去“查询”输入句子的所有位置,决定该重点参考源语言的哪些词。

第三个子层和编码器一样是 FFN。

4.3 训练时并行、推理时逐词的原因

这是一个很容易想不通的点:既然你说 Transformer 可以并行,为什么生成翻译结果的时候还是一个词一个词往外蹦?

关键要把“训练”和“推理”分开。

训练阶段,我们拥有完整的标准翻译结果。假设目标句是“I love you”,模型在训练时不是先预测 I,再拿 I 去预测 love,而是整个句子的所有位置同时进到解码器里,通过掩码自注意力保证位置 1 只看位置 1,位置 2 只看位置 1 和 2,以此类推。这种“直接把正确答案喂进去,让模型并行预测所有单词”的做法叫教师强制(Teacher Forcing)。因为掩码挡住了未来信息,训练输出是并行的,GPU 可以一次算完。

推理阶段没有标准答案。你第一步只能给解码器输入一个起始标记,让它生成“I”;拿到“I”之后,再把“起始标记 + I”作为输入,生成“love”;接着输入“起始标记 + I + love”,生成“you”。每一步都在越来越长的上下文上重新计算,这就是所谓的自回归生成。所以 Transformer 在训练时并行,在推理时依然是串行的。这也是为什么大语言模型生成文本速度不快,跟你聊天时感觉要等一等。

论文中还有一个细节:解码器输入端的位置也要加上位置编码。因为目标句子也是词序列,掩码自注意力同样没有顺序感,必须显式告诉它每个目标词处于第几个位置。如果你在实现时忘记给解码器输入加位置信息,模型基本不会有像样的生成效果。

5. 零基础最容易想错的四个细节

这个部分是当年我反复琢磨了很久、也在社区里看到无数新人问过的问题。逐个给你讲透。

5.1 为什么缩放因子是根号 d_k

当 (Q) 和 (K) 的每个维度都近似服从均值为 0、方差为 1 的独立分布时,它们的点积 (q \cdot k = \sum_{i=1}^{d_k} q_i k_i) 的期望是 0,方差是 (d_k)。也就是说,向量维度越高,点积分数的绝对数值本身就越大。

Softmax 函数对输入的数值非常敏感:如果两个候选值一个是 20,另一个是 18,指数运算后相差约 8.4 倍,softmax 结果会极度偏向 20,几乎变成 one-hot 分布。在反向传播时,one-hot 区域的梯度非常小,模型很难学习。除以 (\sqrt{d_k}) 后,点积的方差被拉回 1 附近,softmax 的输入分布更平滑,梯度也就更健康,训练稳定得多。

这也是为什么没必要引入其它更复杂的归一化方式——理论上用更高的 sqrt 也行,但论文给出的这个设计既简单又可解释,被后续所有 Transformer 模型沿用至今。

5.2 Masked 注意力在实践中是怎么实现的

很多第一次读源码的人会困惑:代码里怎么没看到“把未来位置替换成负无穷”的步骤?因为实现的时候,一般是用一个上三角矩阵,把未来位置设为一个很大的负数,比如 -1e9,再加到注意力分数上,然后再做 softmax。

要注意的是,不能直接把这些位置设成 0,因为 softmax 里 (e^0 = 1),根本起不到屏蔽作用。必须用一个足够大的负值,让指数运算后趋近于 0。还要小心数值精度问题:负值要足够大但不能大到溢出,-1e9 在常用浮点精度下足够安全。在实际模型代码里你还会看到通过 key_padding_mask 把 padding 位置也一并屏蔽的写法,原理同源。

5.3 Self-Attention 的复杂度问题

自注意力的计算过程里有一处 (QK^T),形状是 ((n, d_k) \times (d_k, n)),结果是一个 (n \times n) 的注意力矩阵。这里的 (n) 是序列长度,所以自注意力时间复杂度是 (O(n^2)),空间复杂度也是 (O(n^2))。

序列短的时候没问题,一旦序列变长,比如处理整本书、长文档或高分辨率图像,(n^2) 的增长会非常快。这也是后续各种高效注意力变体(稀疏注意力、线性注意力等)出现的原因。理解这一点,你才能明白为什么很多新模型需要加“长度外推”技巧,以及为什么输入 token 数会直接影响模型显存占用。

5.4 “并行”到底是什么意思

很多人把 Transformer 的并行理解成“读完整句话后立刻得到完整输出句”,这是不对的。Transformer 的并行体现在两个地方:

第一,编码器处理源文本时,所有词互相打分的注意力计算是一次矩阵乘法完成的,不像 RNN 必须循环执行 (n) 次子步骤。第二,解码器在训练时可以通过掩码一次处理目标句的所有位置,把所有位置的预测损失一起算出来。

但推理生成依旧是逐词的。更准确地说:并行是发生在“给定完整上下文,同时计算多个位置的新特征表示”这个层面;而“生成新内容”这个动作,因为是自回归的,不可避免是串行的。把这两个层面分开,你对 Transformer 的理解就已经超过绝大多数初学者了。

6. 我的学习建议:零基础到能复现的路径

很多读者问我:论文这么难,公式这么多,到底应该怎么读才能真的学会,而不是看完就忘。下面是我自己实际用过的路径,每一步都踩过坑。

6.1 从一行行算完注意力开始

别急着看论文的实验部分,也别急着读源码。先用最简单的方法,找一个只有三到四个词的句子,手算一次注意力的完整过程。你可以完全复制本文第 2 节那个例子,把每个矩阵的维度写出来,把每一步的输出形状标出来。当你亲手在纸上写出 (Q, K, V),再写出 (QK^T) 的形状是 (n, n) 的时候,那个吓人的公式就已经被你降伏了。

我自己的经验是:第一次手算会花大概两三个小时,但算完之后,后面再学多头、掩码、Cross-Attention,每新增一个概念都只是在这棵已经立起来的树干上加枝叶。

6.2 用最小化实现验证理解

在你觉得自己懂了公式之后,写一个最简版的自注意力。这个实现不需要任何训练,只要把前向传播跑通就行。比如我用 PyTorch 写过这样一个最小版本:

import torch import torch.nn.functional as F def simple_attention(x, mask=None): # 假设 x: [batch, seq_len, d_model] # 为了演示,这里直接用同一个线性层生成 q/k/v d_k = x.shape[-1] q = x k = x v = x scores = torch.matmul(q, k.transpose(-2, -1)) / torch.sqrt(torch.tensor(d_k, dtype=torch.float32)) if mask is not None: scores = scores.masked_fill(mask == 0, float('-inf')) weights = F.softmax(scores, dim=-1) out = torch.matmul(weights, v) return out, weights

这段代码和论文公式严格对照,核心就 5 行。跑通之后,把mask参数传入一个第 3 节那种上三角掩码,观察输出怎样变化,你对掩码的理解就是实际经验而不是抽象概念了。

然后再做两件事:加多头(把一个头的逻辑复制 8 份);加位置编码(按论文公式构造一个矩阵加到输入上)。当你能写出一个只有几十行却能处理真实句子的小型 Transformer 编码器时,你已经能看懂大部分论文实现。

6.3 读论文的正确姿势

我自己读论文的顺序和你分享下:先读 Abstract 和 Introduction 中关于 RNN/CNN 困境的段落,然后直接跳到第 3 章看模型架构,尤其只盯着模型结构图和公式 (1) 到 (5) 看。实验部分先跳过,等有真实任务要跑通了再回来看。论文里的机器翻译评测指标对理解核心机制来说并不关键。

遇到不懂的名词,比如“决策级融合”“残差连接的作用”之类,建议先查消息来源可靠的教程,再回来看论文原句。论文的价值在于精确和权威,但它的行文默认你已经有相当的背景知识,宁可先用博客把背景补齐,再打开论文,效率会高很多。

6.4 接下来还能往哪走

当你把 Transformer 本身搞懂后,你会发现几乎所有现代大模型都是它的变体:只保留 Encoder 的模型擅长理解与分类,只保留 Decoder 的模型擅长生成,Encoder-Decoder 结构擅长翻译和摘要。自注意力这个思想还延伸到了图像、音频、多模态领域,几乎随处可见它的影子。

这时候如果还想深入一步,有几个方向值得折腾:把多头注意力的各个头的权重可视化出来,观察它们是否学到了不同的语法或语义关系;实现一个简单的复制任务或翻译小玩具,训练几次看看 loss 是否下降;给模型加不同长度、不同语言的数据,体验位置编码对长度的敏感性。这些实践都会让理论进一步变得血肉丰满。


我在学习这篇论文时最大的教训是:别害怕公式,也别以为看懂公式就完事了。真正的理解来自亲手算一遍、亲手写一次、亲手跑一个任务。Transformer 的每一块设计都有非常直白的动机——从并行、长程依赖、到防作弊掩码,没有一个是为了炫技而存在。你把注意力计算那套“打分—加权求和”的流程印在脑子里,后面遇见的各种模型,本质上都是在问同一个问题:哪些信息该被多看一眼?这个答案,你自己就能找出来。

最后分享一个我至今沿用的操作习惯:拿到任何类似架构的论文,先不看正文里天花乱坠的任务表格,而是把所有矩阵的维度变化列成一张表,顺便问自己“这一步为什么没有掩码,那一步为什么需要掩码”。这几层问题读完,论文基本就被你吃到七成了。然后丢下论文,用 50 行代码跑通一个最小实现——这时候你才真正站在“理解”的门槛上。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询