☰
Transformer核心解析:注意力机制、多头注意力与位置编码
2026/11/2 17:12:57 网站建设 项目流程

简介:这份文件是NIPS 2017论文《Attention Is All You Need》的中文翻译文档,面向自然语言处理初学者、Transformer入门读者及需要精读原始论文的研究者,用于降低英文阅读门槛、梳理自注意力与序列转导模型的核心概念。压缩包内共1个docx文件,约856KB,以论文中文译文为主体,便于检索、批注和离线阅读。译文覆盖摘要、引言、背景、模型架构、编码器与解码器堆栈、缩放点积注意力、多头注意力等关键章节,对自注意力如何建模长距离依赖、多头机制如何并行计算以及位置表示如何补足顺序信息均有对应阐述,并保留WMT 2014英德、英法翻译任务的实验结论与BLEU结果,可帮助读者理解Transformer完全基于注意力、摒弃递归与卷积的设计动机及并行化优势。目前已有1194人学习下载,适合作为论文精读、课堂汇报或算法复习的对照材料。

1. 一句话说清 Transformer:一篇论文怎么把「按顺序读」这件事从模型里拿掉

想象一个只会逐字读句子的机器在翻译:读到第七个词时,第一个词早被挤到记忆角落,于是长句子总是翻得前松后紧。2017 年的一篇会议论文给出了另一个思路:干脆不按顺序读了,让句子里的每个词直接去「看」其他所有词,自己决定该多看谁几眼。这套架构叫 Transformer,中文里有人译作「变换器」,核心零件只有一个——注意力机制(attention)。论文标题说得很直白:注意力就是你所需要的一切。它值得搞懂,是因为从机器翻译到今天的各类大模型,底层几乎都站着它的影子,理解它就等于拿到了一整类模型的说明书,也顺带搞懂了 self-attention、多头注意力这些高频名词到底指什么。

2. 注意力机制到底在做什么:一次查字典就讲明白

2.1 一句话定义:查询、键、值到底对应现实里的什么

论文对注意力的定义非常克制:把一组查询和一组键值对映射成输出,查询、键、值、输出全都是向量,输出是值的加权和,权重由查询与对应键的「兼容性」算出来。这句话第一次读像绕口令,换成查字典就顺了。

假设你要查一个词,手里捏的是查询(query);字典每一页顶端的索引词是键(key),索引词下面那一段解释是值(value)。你拿查询去和每一个键比对,越像的键,翻到那一页的权重越高,最后交到你手上的不是某一页的原文,而是所有页的解释按匹配程度掺在一起的结果。这就是注意力干的事。

落到句子上:句子「小猫没有过马路,因为它太累了」里,「它」指的是谁?让「它」这个位置生成一个查询,去和全句每个位置的键比对,「小猫」那一格的匹配分数会明显高于「马路」,于是「它」的输出向量里就揉进了大量「小猫」的信息,指代关系就这么被捕捉到了。原文强调的一点很关键:这种依赖关系的建模,和两个词在句子里隔多远无关,隔十个词和隔一个词,走的都是同一套比对动作。

2.2 缩放点积注意力:那个除以根号 d_k 的动作在防什么

具体怎么算匹配分数?论文选了最省事的一种:点积。两个向量点积越大,说明方向越接近,相似度越高。整件事写成一个式子就是:

Attention(Q, K, V) = softmax(QKᵀ / √d_k) · V

拆开看只有四步,代码里更直观:

import numpy as np def scaled_dot_product_attention(Q, K, V, mask=None): """Q: (n, d_k) 查询矩阵,n 为目标序列长度 K: (m, d_k) 键矩阵,m 为源序列长度 V: (m, d_v) 值矩阵 mask: (n, m) 布尔矩阵,True 表示该位置允许被关注 """ d_k = Q.shape[-1] # 键向量维度,缩放因子由它决定 # 1. 打分:每个 query 与所有 key 做点积,得到 (n, m) 的相似度矩阵 scores = Q @ K.T / np.sqrt(d_k) # 除以根号 d_k 是关键一步 # 2. 屏蔽:把不该看的位置压到极小值,softmax 后权重自动趋近 0 if mask is not None: scores = np.where(mask, scores, -1e9) # 3. 归一化:每一行的权重加起来等于 1,变成一组概率 scores = scores - scores.max(axis=-1, keepdims=True) # 减最大值防溢出 weights = np.exp(scores) weights = weights / weights.sum(axis=-1, keepdims=True) # 4. 加权求和:权重乘上对应的 value 再相加 return weights @ V, weights

参数含义值得一提:d_k是键向量的维度,n是查询的个数(目标侧长度),m是键值对的个数(源侧长度),返回的第二个weights就是常被画成热力图的注意力分布,它直接告诉你模型「看了哪几眼」。

现在回答那个分母。点积是逐维相乘再求和,维度越高,累加项越多,结果数值的方差就越大。方差一大,softmax 的输入就会出现「一枝独秀」的局面:最大的那个分数把其他全压成接近 0,函数落进梯度极小的饱和区,反向传播时几乎传不出信号。除以√d_k相当于把方差拉回 1 附近,让 softmax 待在有梯度的区间里。论文里还对比了另一种加法注意力,结论是:d_k小的时候两者表现差不多,d_k大的时候点积注意力会因为数值膨胀而变差,这正是加缩放的原因。顺带说一句,点积注意力在实践中快得多、也省空间,因为它能直接吃高度优化的矩阵乘法。

解码器那一侧还要多做一件事:生成第 i 个词时,不能偷看第 i+1 个及以后的词,否则训练时等于抄答案。做法就是在 softmax 之前把未来位置的分数设成一个极大的负数,代码里的mask干的就是这个。

2.3 多头注意力:为什么要把一件事拆成 8 个视角看

如果只做一次注意力,模型会被迫把所有关注点压进一组权重里:既想盯主谓一致,又想盯指代,还想盯时态,最后只能取个平均,谁的特色都不突出。论文的做法是拆开干。

具体操作是:把查询、键、值分别用 h 组不同的、可学习的线性投影压到低维,每一组独立跑一遍注意力,得到 h 个输出,再把它们拼接起来,最后过一层线性变换还原维度。论文用 h = 8,每组维度d_k = d_v = d_model / h = 64。因为每个头都被压窄了,8 个头加起来的总计算量和一次全维度注意力差不多,并没有变贵。

打个比方,八个人同时看同一句话:一个盯语法结构,一个盯代词指向,一个盯动词时态,最后一个可能盯的是标点节奏。每个人的笔记都很薄,但拼起来信息量比一个人写一大本更丰富。论文在附录里检查过注意力分布,确实看到不同的头在干不同的事,不少头还表现出跟句法、语义结构相关的规律,这也是它常被说成「可解释性更好」的来源。表 3 的消融实验给了个量化参照:单头注意力比最佳设置低了 0.9 BLEU,但头太多同样会掉质量,八不是玄学数字,而是一个被实验调出来的平衡点。

2.4 同一个注意力,在模型里被用了三次

注意力在原始架构里出现三个位置,用途完全不一样,这一点常被忽略。

第一种是编码器自注意力:查询、键、值全部来自编码器上一层的输出,每个位置都能看到输入句子的所有位置,作用是把一句话压成一组带上下文的向量。

第二种是解码器掩码自注意力:查询、键、值都来自解码器自己,但加了掩码,每个位置只能看自己和它左边的词。这是为了保住自回归的性质——预测第 i 个词时只能用前 i-1 个已知输出,不然训练就作弊了。

第三种是编码器-解码器交叉注意力:查询来自解码器上一层,键和值来自编码器的最终输出。它干的是「对齐」这件事:解码器在生成某个词时,去输入句子里找对应的部分。这一步沿用了序列到序列模型的经典设计,只是把里面的循环换成了注意力。

三种用法共用同一套公式,差别只在查询、键、值从哪来。把这条线捋清楚,再去看各种变体架构,就能一眼看出它砍掉了哪一块、保留了哪一块。

3. 为什么非要把循环扔掉:并行、路径长度与被补回来的顺序

3.1 循环网络的死结:一步没算完,下一步就动不了

在 Transformer 之前,序列建模的主流是循环网络,特别是长短期记忆和门控循环单元这一类。它们的计算方式是按位置一步步推进:根据上一个隐藏状态和当前位置的输入,算出当前隐藏状态,再传给下一步。

这个结构本身没毛病,问题出在「串行」上。第 t 步必须等第 t-1 步算完,整条链上没有任何可以并行的余地。序列一长,训练就变成排队:一批样本内部等,样本之间又被显存卡住,批大小上不去,硬件利用率也跟着躺平。论文的原话是,这种固有的顺序性质排除了训练样本内部的并行化,在长序列上尤其致命。

打个比方,循环网络像一场接力赛,棒必须一棒一棒传下去,跑得再快也只有一个在跑。注意力则像把所有人同时放到跑道上,各自跑各自的,最后再统一汇总成绩。前面提到的因子分解技巧、条件计算,都是在接力赛规则里做优化,能提速,但改不了「必须等」这个前提。

3.2 路径长度:信息走的路越短,长距离依赖越好学

除了并行,论文还关心另一个指标:输入序列里任意一个位置,到输出序列里任意一个位置,信号在网络里要走多长的路。路越长,梯度在反向传播中越容易衰减,长距离依赖就越难学——这是循环网络时代反复被讨论的老问题。

论文用一张表把几类层放在一起比了一比,这张表是理解「为什么选注意力」的关键:

层类型每层计算复杂度最少顺序操作数任意两位置间最大路径长度
自注意力O(n²·d)O(1)O(1)
循环层O(n·d²)O(n)O(n)
卷积层O(k·n·d²)O(1)O(log_k n)
受限自注意力O(r·n·d)O(1)O(n/r)

表里 n 是序列长度,d 是表示维度,k 是卷积核宽度,r 是受限注意力只看邻域时的大小。这张表要横向读:自注意力的顺序操作数是常数 1,路径长度也是常数 1——任意两个位置之间,一次比对就能连上。循环层需要 n 次顺序操作,路径长度也是 n。卷积层能并行,但要让首尾两个位置产生联系,得堆 O(log_k n) 层,路还是绕。

复杂度那一列也别忽略。自注意力是 O(n²·d),看着比循环层的 O(n·d²) 多一个 n,但论文指出,在机器翻译这类任务里,句子表示维度 d 通常大于序列长度 n,这个区间内自注意力反而更快。当然,n 一旦远超 d,平方项就会变成负担,这也解释了后来为什么会有那么多针对长序列的稀疏化改造。论文自己也提到了一个折中方案:把注意力限制在以当前位置为中心的 r 邻域内,复杂度降到 O(r·n·d),代价是路径长度涨到 O(n/r)。

3.3 顺序信息从哪来:位置编码这个补丁怎么打

扔掉循环和卷积之后,新问题冒出来了:模型现在对词序完全无感。「猫追狗」和「狗追猫」在它眼里是同一个词袋,这显然不行。所以必须人为地把位置信息塞进去。

论文的方案是在输入嵌入上直接加一组位置编码,两者维度都是d_model = 512,所以能逐元素相加。编码用的是一组不同频率的正弦和余弦函数:

import numpy as np def positional_encoding(max_len, d_model): """生成 (max_len, d_model) 的位置编码表,直接与词嵌入相加""" pe = np.zeros((max_len, d_model)) pos = np.arange(max_len)[:, None] # 位置 0, 1, 2, ... i = np.arange(0, d_model, 2) # 偶数维下标 0, 2, 4, ... denom = np.power(10000.0, i / d_model) # 波长从 2π 到 10000·2π 按几何级数铺开 pe[:, 0::2] = np.sin(pos / denom) # 偶数维用 sin pe[:, 1::2] = np.cos(pos / denom) # 奇数维用 cos return pe

参数里max_len是支持的最大序列长度,d_model是表示维度,必须和词嵌入一致。每个维度对应一条正弦波,波长构成一个从 2π 到 10000·2π 的等比数列——短波负责区分相邻位置,长波负责区分段落级的位置,很像钟表上的秒针、分针、时针分工。

为什么选正弦而不是让模型自己学?论文给的理由是:对任意固定的偏移量 k,位置 pos+k 的编码可以表示成 pos 编码的线性函数,模型因此更容易学会「相对位置」这回事。而且固定公式理论上能外推到比训练时更长的序列。论文也试过可学习的位置嵌入,在表 3 的行 (E) 里,两种做法结果几乎一样,最后还是选了正弦版本。这个细节挺有意思:它说明位置编码的形式不是成败关键,有没有把顺序信息注入进去才是。

3.4 光有注意力还不够:残差、归一化和前馈层在干什么

很多人以为 Transformer 就是堆注意力,其实每个编码器层里有两个子层,每个解码器层里有三个。除了注意力,还有一个逐位置的前馈网络:两次线性变换,中间夹一个 ReLU,写成FFN(x) = max(0, xW₁ + b₁)W₂ + b₂。输入输出维度都是 512,中间那层放大到 2048。它对每个位置独立且相同地作用,等价于两个核大小为 1 的卷积,作用是把注意力汇总来的信息再做一次非线性加工。

每个子层外面还套了一层LayerNorm(x + Sublayer(x)),也就是残差连接加层归一化。残差的作用是给梯度留一条高速公路,让深层网络能训得动;层归一化则是把每个样本的激活值拉回稳定范围。为了让残差能直接相加,模型里所有子层和嵌入层的输出维度统一成 512,这个约束贯穿始终。

编码器和解码器各堆 N = 6 层相同结构。这个数字不算大,但配上残差和归一化,已经足够深到能学到复杂模式,又不至于训崩。训练上还有几处细节:用 Adam 优化器,学习率先线性预热 4000 步,之后按步数的平方根倒数衰减;每个子层输出后加 dropout,基础模型用 0.1;标签平滑也用 0.1,它会压低困惑度这项指标,但能提升准确率和 BLEU。基础模型在 8 块 GPU 上跑约 10 万步、12 小时,大模型跑 30 万步、3.5 天。

4. 容易搞混的几个概念:自注意力、多头和位置编码的常见误区

4.1 误区一:把「注意力机制」和「自注意力」当成一回事

易混点在于两个词高频共现,读论文时几乎每隔一段就出现一次,很容易默认它们是同义词。

之所以会混,是因为名字确实只差一个字,而且论文的核心卖点就是自注意力。但注意力是个更宽的框架:只要你有查询、键、值这三样东西,并且查询和键值来自不同地方,就叫注意力。序列到序列模型里那个经典设计——解码器拿自己的状态去查编码器的输出——属于交叉注意力,它在 Transformer 之前就存在很多年了。

区分的关键看查询、键、值是不是同一路货。三者全部来自同一个序列,就是自注意力;查询来自解码器、键和值来自编码器,就是交叉注意力。再补一句背景:自注意力本身也不是这篇论文发明的,它此前已经在阅读理解、句子表示等任务上被用过。论文真正的贡献是第一个完全依靠自注意力来计算输入和输出表示、彻底不用循环和卷积的转导模型——这个「彻底」才是分水岭。

4.2 误区二:以为多头注意力是多个模型在投票

「多头」这个词听起来太像集成学习,很多人第一反应是训了 8 个模型然后取平均。

会混是因为集成学习的思路太深入人心,而多头确实是 8 份并行计算,形式上像。但它们的输入是同一份,只是各自做了一次不同的线性投影,投影矩阵是学出来的参数,不是 8 个独立模型;输出也不是投票或平均,而是拼接后过一层线性变换融合。

区分方法看参数量和维度:每个头被压到 64 维,8 个头加起来才回到 512 维,总计算成本和一次全维度注意力相当。如果是 8 个独立模型,参数和算力都得翻好几倍。另外多头不是越多越好,论文的表 3 显示单头比最佳设置低 0.9 BLEU,但头数继续加同样会掉质量,它是有最优区间的。

4.3 误区三:以为位置编码是模型自己学出来的

模型里几乎所有东西都是训练出来的,位置信息这么重要的东西,直觉上当然也该是学出来的。

之所以混,是因为这个假设太顺理成章。但论文用的是固定公式算出来的正弦余弦编码,一个参数都不学,直接加到词嵌入上。更反直觉的是,论文确实也试过可学习的位置嵌入,结果和正弦版本几乎一样(表 3 行 E),最后选正弦的理由是它可能外推到训练时没见过的更长序列。

区分时记住两点:一是位置编码和词嵌入是相加,不是拼接,所以维度必须都等于d_model;二是位置编码只在编码器和解码器堆栈的底部加一次,不是每层都加。至于「学的好还是固定的好」,论文的答案挺佛系——差别不大,选哪个都不至于翻车。

4.4 误区四:以为 Transformer 天生只有一个编码器

不少人第一次接触到的 Transformer 是个只有解码器的生成模型,回头再看原始论文,反而觉得陌生。

这是时代造成的混淆。原始架构是完整的编码器-解码器:编码器 6 层,解码器 6 层,解码器比编码器多一个交叉注意力子层,且自注意力带掩码。后来社区按任务把它拆着用——做理解类任务只留编码器,做生成类任务只留解码器,两边各自演化,名字却还叫 Transformer。

区分的办法是先问一句「它要拿什么当输入、产出什么」。输入输出都是序列、且需要对齐关系(比如翻译),就用完整结构;只需要把一段文本压成一个表示(比如分类),编码器就够;只需要从左往右一个个吐词,解码器就够。看到「Transformer」这个词,先确认是哪一半,再谈性能。

4.5 误区五:把 BLEU 和困惑度当成同一种分数

论文的表格里两个数字都出现,都是「分」,很容易被当成同一类指标横向比较。

会混是因为它们都用来衡量模型好坏,而且方向感相反:BLEU 越高越好,困惑度越低越好。但两者量的是完全不同的东西。BLEU 比对的是机器译文和参考译文的重合程度,属于任务级评测;困惑度衡量的是语言模型对下一个词的「犹豫程度」,属于模型内部的概率指标。

区分的关键看它回答什么问题。想知道翻译质量好不好,看 BLEU;想知道模型对词分布的预测有多确定,看困惑度。论文里有个漂亮的例子:标签平滑会让困惑度变差,因为模型被训练得更「不确定」,但准确率和 BLEU 反而提升。一个指标变差、另一个变好,这本身就说明两者不能互相替代。

5. 什么时候值得上这套架构,什么时候别硬套

先给一条判断线:如果你的任务里「任意两个位置可能互相关联」且「序列不算短」,这套架构的优势就明显;如果序列只有十几个词,或者任务本身是严格流式、必须一个词一个词增量处理,硬套反而添麻烦。

值得上的场景大致三类。一是翻译、摘要、改写这类输入输出都要成序列且需要对齐的任务,原始架构就是为它设计的,交叉注意力天生适合干对齐。二是需要并行训练的场合,注意力把顺序操作数压到常数,长序列上的吞吐优势会直接体现在账单上。三是需要看模型「看了哪几眼」的场合,注意力权重本身就是一份可视化材料,出问题时能顺着热力图找线索。

要谨慎的场景也很实在。序列极长时,自注意力的 O(n²·d) 复杂度会变成硬约束,平方项摆在那里,靠调参绕不过去,得考虑受限注意力或别的稀疏化路线。数据量很小的时候,这套架构参数不算少,容易过拟合,dropout 和标签平滑都得往上调。还有一类常被忽略:任务本身只需要判断一句话的情感倾向,用完整编码器-解码器就是杀鸡用牛刀。

场景建议理由
句对翻译、摘要生成用完整编码器-解码器交叉注意力天然负责对齐
文本分类、句向量只用编码器不需要逐步生成,省一半算力
超长文档、日志序列谨慎,先评估复杂度平方项会吃掉显存
严格流式、逐字输出只用带掩码的解码器自回归性质本来就这么设计的

说个踩坑的例子。某开发者要做一个句对相似度判断的小工具,数据只有几千条。他照着论文原图搭了完整编码器-解码器,训了两天死活不收敛,还以为是学习率没调好。后来换成只用编码器,接一个池化层加分类头,半天就跑出了不错的结果。差别不在参数量,而在他根本不需要「生成」这个动作——解码器那一半纯属陪跑,还顺带引入了额外的训练难度。

从那以后我每次上手新任务,都强制先问自己一句:这个任务到底需不需要「生成」,还是只需要「理解」。答案不同,架构该砍的地方完全不同。这份论文的中文翻译版本把编码器-解码器结构、自注意力、多头、位置编码这些零件一个个拆开讲清楚了,配合原文的消融表格对照着看,比零散地刷二手解读扎实得多。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询