读到《Attention Is All You Need》第 3.5 节「Positional Encoding」时,几乎每个第一次读论文的人都会愣一下。前面把整个架构推得那么顺:multi-head、scaled dot-product、encoder-decoder、residual——一切都自洽。然后突然来了一句话:「Since our model contains no recurrence and no convolution, in order for the model to make use of the order of the sequence, we must inject some information about the relative or absolute position of the tokens in the sequence.」紧接着就是那个看起来像从天上掉下来的公式:
PE(pos, 2i) = sin(pos / 10000^(2i / d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i / d_model))
为什么是 sin/cos?为什么是 10000?为什么把它和 word embedding 直接相加?为什么训出来还能用——不是应该把语义信息搅乱吗?
这一篇就是为这一连串「为什么」准备的。读完之后你应该能做到:在白板上画出 self-attention 的排列等变性证明;把 sinusoidal 的「相对位置可线性表达」性质用三角恒等式推一遍;说清楚「相加」相比「拼接」在参数和子空间利用率上的差别;给一个工程师讲明白「为什么训练 512、推理 2048 会让一个用可学习位置的模型直接报废」。
更重要的是:理解了这一篇,你才能真正读懂第 41 篇里 RoPE 和 ALiBi 解决的不是「让 sin/cos 跑得更快一点」,而是从问题定义上把位置编码挪到了完全不同的层面——不再是「加在输入上的一个向量」,而是「注入在 attention 打分公式里的一个相对位置先验」。
一、self-attention 是排列等变的:被忽视的硬事实
1.0 从一个常被忽略的提问开始
如果你已经完整读过第 14 篇 self-attention,会觉得 attention 是一个非常对称、非常优雅的运算:每对 token 之间通过 Q·Kᵀ 算相似度,然后用 softmax 归一化、加权求和 V。所有位置之间地位平等。但请你停下来认真问一句:「当我说『所有位置之间地位平等』时,我有没有意识到这件事在结构上意味着什么?」
意味着:模型分不清序列的顺序。一个本来在第 0 位的 token 和它本来在第 17 位的副本,在 attention 看来是完全等同的。如果你接受这个事实,那么「Transformer 怎么处理语序」这个问题,不再是一个「实现细节」,而是一个真正需要被解决的结构性挑战。
这一节就是从这条结构性挑战出发,一步步把整篇内容铺开。
1.1 一个让人发懵的小实验
回想第 14 篇里我们写过的最朴素的 self-attention:把一个
(N, d) 的输入 X 投影成 Q、K、V,然后算
softmax(QKᵀ/√d_k)V。再回想第 13 篇手算的那个三
token 玩具例子。
现在试一个非常简单的实验:把这三个 token 的顺序倒过来,再喂进同一个 self-attention 模块,看看输出会变成什么。
我没有用任何代码,只用纸笔就能写下结论:输出向量也跟着倒过来,但每个位置的内容和原来完全一样。换句话说,把输入序列从
[x₁, x₂, x₃] 变成
[x₃, x₂, x₁],输出从 [y₁, y₂, y₃]
变成 [y₃, y₂, y₁]。新输出里的 y₁
和原输出里的
y₁,每一位都对得上,连小数点后第七位都不差。
这不是巧合,是 self-attention 的内禀对称性。我们一会儿会用一行公式把它写死。
但先让这件事在直觉层面停留一秒,因为它的工程后果非常严重:这意味着,在一个没有任何位置信息的 self-attention 上,「猫吃鱼」和「鱼吃猫」是不可分的。模型不是「学不会」这两个句子的差别,是它从根本上接收不到这两个句子有差别这条信息。
图里左右两侧用的是同一个 self-attention 模块,同一组 W_Q / W_K / W_V。左边输入「猫 吃 鱼」,右边输入「鱼 吃 猫」。因为 attention 对所有位置一视同仁,谁站在第几位,模块根本看不出来。每个 token 唯一的身份证只有它自己的 embedding 向量,而 embedding 向量只承载语义,不承载位置。
1.2 用一行公式把它写死
把上面那件事用数学形式写下来。设 P 是任意 N×N 的置换矩阵(每一行每一列恰好有一个 1,其它都是 0),X 是输入矩阵 (N, d),那么 PX 表示「按 P 把 X 的行重排」。一次 self-attention 写成 f(X),我们要证明的事情是:
f(PX) = P · f(X)
这就是「排列等变」(permutation-equivariant)的精确形式:把输入按 P 排,输出也按同一个 P 排,且仅此而已。
证明并不复杂。第一步:投影是逐行做的,所以 PX 投出来的
Q’、K’、V’ 满足 Q' = (PX)W_Q = P(XW_Q) = PQ,K’
= PK,V’ = PV。第二步:算分数矩阵
S' = Q'K'ᵀ = PQ(PK)ᵀ = PQKᵀPᵀ = P S Pᵀ。第三步:softmax
是逐行作用的,softmax(P S Pᵀ) = P · softmax(S) · Pᵀ。第四步:再乘
V’:
f(PX) = softmax(P S Pᵀ / √d_k) · PV
= P · softmax(S/√d_k) Pᵀ · PV
= P · softmax(S/√d_k) · V
= P · f(X)
最后一步用了
Pᵀ P = I,因为置换矩阵正交。
这条证明短到一张草稿纸都用不完,但它的结论被整个领域用来做了无数件事:从 Set Transformer、Deep Sets,到图神经网络上的 attention 池化,再到回到我们要讲的位置编码——它是一个否定命题,告诉你「单凭 self-attention 是不够的」。
1.3 RNN 和 CNN 自带位置感
为什么 RNN 不需要专门的位置编码?因为它的循环结构里,第 t 步的隐藏状态 hₜ 是 hₜ₋₁ 和 xₜ 的函数;第 t 步永远只能在第 t-1 步之后被算出来。位置信息隐含在「计算的顺序」里,不需要再注入。
CNN 同样不需要。一维卷积核滑过序列时,相邻 token 落在同一个感受野,远端 token 落在不同的感受野,位置差异就编码在「哪些 token 和哪些 token 一起做了卷积」这件事里。
self-attention 的问题恰恰在于它放弃了这两种结构性偏置。它把每对 token 都视为「等距」——任意两个位置之间都是一跳,所以它在长程依赖上有巨大优势,但代价是位置信息的归零。第 18 篇讲过这是 attention 复杂度问题的源头,这里我们看到的是同一件事在另一个面上的投影:当所有 token 在 attention 看来都「平等可达」时,它们在「在第几位」这件事上也变得平等不可分。
更精准地说,CNN 和 RNN 的位置感是「派生」的,不是「显式」的。RNN 通过隐藏状态在时间维度上的递推携带位置;CNN 通过感受野的局部性区分相邻和远端。它们都没有一组显式的「位置向量」。Transformer 没有递推、没有滑窗、只有全局 attention,这意味着位置感完全消失了——除非你显式地把它写回来。
这条对比也解释了为什么有些工作(Conformer、Gehring 的 ConvSeq2Seq)把卷积和 attention 混合:卷积提供「天然的局部位置感」,attention 处理「全局关系」,两者互补。当卷积参与时,位置编码的负担会减轻;当 attention 是唯一交互机制时,位置编码就成了不可省的核心模块。
1.4 为什么这件事在原论文里只用一段话带过
读论文的人常常觉得 Vaswani 等人把位置编码这一节写得太轻描淡写——一节,半页,结束。但这其实反映了 2017 年作者团队对这个问题的态度:位置编码是补丁,不是架构核心。架构核心是 attention 本身和它的并行性,位置编码只是「让 attention 能用在序列上」的最小修复。
后来的几年证明这个判断既对也不对。对的部分是:注入位置信息确实有很多种方式都能 work,sin/cos 不是唯一选项。不对的部分是:位置编码远不是补丁。它直接决定了模型的长度泛化能力、对相对距离的敏感性、长上下文场景下的稳定性。RoPE 不是把 sin/cos 换了个写法,它从根上重新定义了「位置如何参与 attention」,并因此打开了 100k tokens 上下文的工程空间。
这条线索我们留到下文,先沿着原论文的思路把 sinusoidal 讲透。
1.5 把这件事在工程上彻底说清楚
我们在这里再多花几行,把「self-attention 排列等变」与日常工程现象做一次连接。如果你训练过任何一个 Transformer 或者它的简化版本,你或许见过这样几个现象:
第一个现象:删掉位置编码之后,loss 在前几百个 step
几乎不下降。我自己在写教学代码时复现过:把 sinusoidal_pe
改成 torch.zeros_like,loss 在第 200 个 step
仍然稳稳停在初始值附近。这不是
bug——是模型对所有可能的输出都「均等无知」,因为它无法区分输入序列里
token 的顺序。
第二个现象:在 BERT 的某些预训练阶段,工程师会观察到 sequence position 较远的 token 之间 attention weight 异常稀疏。这通常被解释成「远端依赖学不动」,但更准确的诊断是:可学习位置编码在远端位置上训过的样本数太少,PE 还很噪。把模型训得更久之后,这种稀疏会显著缓解。
第三个现象:用 LLaMA 推理 16k token 上下文时,如果 RoPE base 没有调对,前面的 token 仍然能被正确利用,但 token 8000 之后会出现明显的「指代漂移」——模型会突然把后半段的 it 解析到一个从未出现的 entity 上。这就是位置编码外推失败在生产场景的样子,也是为什么 RoPE base scaling、NTK-aware interpolation、YaRN 这一类技术在 2023 年井喷的原因。
把这三件事串起来你会发现:位置编码不是「数学家的玩物」,而是天天在生产环境影响困惑度、影响 latency、影响用户体验的关键模块。这一篇花的所有篇幅都不算多。
二、注入位置信息的三类方案
在跳进具体方案之前,先为后文铺一条整体地图:所有位置编码方案,本质上是在回答同一个问题——「位置信息从哪里进入 attention 的计算」。三类答案分别对应三个不同的入口。
把可能的方案做一次分类,思考会清晰很多。粗略地说,业界至今出现过三类位置编码方案,每一类的设计哲学都很不一样。
2.1 第一类:绝对位置编码
绝对位置编码的思路朴素到近乎天真:序列里第 0 位就配一个固定的向量 PE(0),第 1 位配 PE(1),依此类推。这个向量直接加到(或者拼接到)token embedding 上,让网络从一开始就「带着位置一起出发」。
这一类的两个代表是:
- 正弦位置编码(sinusoidal):PE 是 sin/cos 计算出来的固定函数,没有可学习参数。原论文用的就是这种。
- 可学习位置编码(learned):PE 是一张 (max_len, d_model) 的查表,每一行随机初始化、随训练更新。BERT、GPT-2、原版 ViT 都用这种。
绝对位置编码的优点是简单、结构清晰、训练稳定。缺点是:模型「直接知道我在第几位」,但不直接知道「我离另一个 token 几位」。前者是绝对,后者是相对。语言中很多结构(比如「形容词修饰它前一个名词」)本质上是相对距离的事,绝对位置只是个间接代理。
2.2 第二类:相对位置编码
相对位置编码不告诉模型「你是第几位」,只告诉它「你和我差几位」。它的注入方式不是改输入,而是改 attention 打分公式:在 Q·Kᵀ 那一项里,加入一个关于相对偏移 (i - j) 的 bias 或者投影。
这一类的代表是 Shaw 2018、T5 的 relative position bias、ALiBi、RoPE。它们各自的实现差别很大,但共享同一个直觉:自然语言里「相邻」「相隔三个」「相隔很远」是结构性事实,而「我是第 17 位」并不是。
2.3 第三类:旋转位置编码 / 衰减偏置
这一类是相对位置编码的现代演进,但因为思路特殊,单列一类比较清晰:
- RoPE(Rotary Position Embedding):把每一对维度看作复平面上的一个旋转,pos 决定旋转角度。一个 token 在 attention 打分时,自然就以「角度差」的形式让相对位置进入 score。
- ALiBi(Attention with Linear
Biases):在 attention score 上直接减去
m · (i - j),让远端 attention 自然衰减。无可学习参数,长度外推能力出奇地好。
这两个方案是当前主流大模型(LLaMA、Mistral、Qwen 系列)的位置编码主力。我们在第 41 篇会专门展开它们。
2.3.5 哪一类是「正确答案」
读到这里读者可能想问:那哪一类才是「正确答案」?
答案是:没有「正确」,只有「在你的约束下最合适」。如果你只在固定长度上训练和推理(比如某个产品级 NER 模型、某个分类任务),可学习位置完全够用;如果你在做翻译这种需要相对距离敏感的任务,sinusoidal 或 T5 relative bias 都不错;如果你在做长上下文的对话或文档理解,RoPE/ALiBi 是默认。
这里的「合适」是一个工程判断,不是一个数学定理。位置编码这一块的演化告诉我们:在深度学习里,「最优解」往往是一组随时间和需求漂移的临时共识,不是一个静态结论。这条心态在阅读后续位置编码相关研究时尤其重要——你会看到大量「在 X 任务上 A 比 B 好、在 Y 任务上 B 比 A 好」的论文,理解它们的关键不是记住胜负,而是记住每篇论文背后的约束。
2.4 一张选型对照表
下面这张表把三类方案的关键差别压在一起,留作参考:
| 维度 | 正弦绝对位置 | 可学习绝对位置 | 相对位置 / RoPE / ALiBi |
|---|---|---|---|
| 形式 | 固定 sin/cos 函数 | (max_len, d_model) 查表 | 直接修改 attention score |
| 参数量 | 0 | max_len × d_model | 0 或极少 |
| 训练 | 直接相加,无学习 | 随训练更新 | 通过修改 attention 自然学到 |
| 训练长度 | 无硬上限 | 训练时 max_len 写死 | 无硬上限 |
| 外推能力 | 中等 | 几乎为零 | 强(尤其 ALiBi) |
| 表达力 | 中 | 高(数据足够时) | 高 |
| 代表模型 | 原版 Transformer | BERT、GPT-2、ViT | LLaMA、Mistral、Qwen |
排序的标准不是「哪个更好」,而是「在工程上各占什么生态位」。BERT 用可学习位置,是因为它的 max_len = 512 在 2018 年的下游任务里完全够用,没人在意外推;LLaMA 用 RoPE,是因为它从 2k 训练扩展到 32k 推理已经成了标配。
三、原论文的 sinusoidal 公式:直观理解
3.1 公式再写一遍
把开头的公式重新摆出来:
PE(pos, 2i) = sin(pos / 10000^(2i / d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i / d_model))
这里 pos 是 token 在序列中的位置(0, 1, 2, …),i 是维度索引的一半(0, 1, …, d_model/2 - 1),d_model 是 Transformer 的隐藏维度(原论文 base 模型是 512)。
输出 PE 是一个长度为 d_model 的向量。每一对维度 (2i, 2i+1) 共用一个频率 ω_i = 1 / 10000^(2i / d_model),sin 占偶数维、cos 占奇数维。
3.2 把它读成「一组不同频率的波」
把 d_model = 512 的 PE 摊开,你会得到 256 对 (sin, cos) 通道。它们的频率从最高(i=0 时 ω = 1)一直平滑下降到最低(i=255 时 ω = 1/10000)。
最高频通道是 sin(pos),每隔 2π 个位置(约
6.28)一个完整周期;这相当于把「相邻几个位置」的差异放进了这一对通道。最低频通道是
sin(pos / 10000),要经过 2π × 10000 ≈ 62832
个位置才完成一个周期;它编码的不是「相邻」的差异,而是「这个
token 在整个序列的哪个大段落里」。
中间的频率覆盖中间的尺度。这种设计就像傅里叶级数:把一个位置坐标 pos 同时投影到许多不同尺度的正弦波上,每个尺度上的取值组合起来,让 PE(pos) 成为唯一可识别的指纹。
图里从上到下展示了几个不同维度上 PE(pos) 随 pos 变化的曲线。最高频维度震荡得很快,最低频维度在 0 到 512 范围内只走了不到一个周期。读者可以脑补一下:把 256 条这样的曲线在每个 pos 处取值拼成一个长度为 512 的向量,就是 PE(pos)。两个 pos 越接近,它们的 PE 向量在所有维度上就越接近;两个 pos 越远,它们的 PE 在高频维度上完全错开,在低频维度上仍然记录着「在哪个段落」。
3.3 那个 10000 是怎么来的
10000 这个数字常常让人疑惑:为什么不是 1000,不是 100000?
答案有两层。
第一层是数学上的:10000^(2i/d_model) 这条几何级数的最大值是 10000(当 2i = d_model 时),所以最低频的周期约是 2π × 10000 ≈ 62832。这意味着 sinusoidal 编码能区分大约六万个位置,而不会让 PE 在序列两端「绕回来」相撞。原论文训练时序列长度不超过几千,6 万的额定区分能力是非常宽裕的。
第二层是工程上的:作者团队在论文 §3.5 末尾写了一句话——他们也试过可学习位置编码,结果几乎一样;选 sinusoidal 是因为它「可能让模型对训练时未见过的更长序列有外推能力」(原文:we hypothesized it would allow the model to extrapolate to sequence lengths longer than the ones encountered during training)。
10000 这个具体数值,没有特别神奇的理论依据,是一个「足够大、覆盖足够长尺度、不需要 tune」的经验选择。后面的研究有人尝试把它换成别的(如 RoPE 的常用值 10000,但有些任务用 1000000),但那是后话。
3.4 一个 d_model = 4 的最小例子
抽象的话讲多了不如算一遍。设 d_model = 4,看 pos = 0, 1, 2, 3 这四个位置的 PE。
频率有两个:i=0 对应 ω_0 = 1 / 10000^0 = 1,i=1 对应 ω_1 = 1 / 10000^(2/4) = 1 / 100 = 0.01。
pos=0: PE = [sin(0), cos(0), sin(0), cos(0)] = [0, 1, 0, 1]
pos=1: PE = [sin(1), cos(1), sin(0.01), cos(0.01)] ≈ [0.841, 0.540, 0.010, 1.000]
pos=2: PE = [sin(2), cos(2), sin(0.02), cos(0.02)] ≈ [0.909, -0.416, 0.020, 1.000]
pos=3: PE = [sin(3), cos(3), sin(0.03), cos(0.03)] ≈ [0.141, -0.990, 0.030, 1.000]
看几件事。第一,pos = 0 的 PE 是
[0, 1, 0, 1]——所有 sin 都是 0,所有 cos 都是
1。这不是 bug,是 sin/cos 在 0 处的取值,意味着「第 0
位」在所有维度上都对应一个非常特殊的「起点」向量。第二,前两维(高频
ω=1)变化非常剧烈,一步跳得很远;后两维(低频
ω=0.01)几乎不动。第三,每个 pos 的 PE 范数都接近
√(d_model/2) = √2,因为每对 (sin, cos)
在任何角度下平方和都是 1。
这条「范数恒定」性质很重要:它意味着 sinusoidal PE 不会因为位置远而变得很大,加到 word embedding 上不会盖过语义。
3.5 把同一个例子拓展到 d_model = 8
把 d_model 从 4 扩到 8,频率多两个:ω_0 = 1, ω_1 = 0.1, ω_2 = 0.01, ω_3 = 0.001。可以看到 ω 几何递减,每次缩 √10 倍。pos = 0..7 八个位置的 PE 列出来:
pos=0: [0.000, 1.000, 0.000, 1.000, 0.000, 1.000, 0.000, 1.000]
pos=1: [0.841, 0.540, 0.099, 0.995, 0.010, 1.000, 0.001, 1.000]
pos=2: [0.909,-0.416, 0.198, 0.980, 0.020, 1.000, 0.002, 1.000]
pos=3: [0.141,-0.990, 0.295, 0.955, 0.030, 1.000, 0.003, 1.000]
pos=4: [-0.756,-0.654, 0.389, 0.921, 0.040, 0.999, 0.004, 1.000]
pos=5: [-0.959, 0.284, 0.479, 0.878, 0.050, 0.999, 0.005, 1.000]
pos=6: [-0.279, 0.960, 0.565, 0.825, 0.060, 0.998, 0.006, 1.000]
pos=7: [0.657, 0.754, 0.644, 0.764, 0.070, 0.998, 0.007, 1.000]
注意几件事。第一,前两维(最高频)已经在 8 步之内走过了大约一个完整周期的相位变化,pos = 4 之后就开始负值;第二,最后两维几乎不动,因为 ω = 0.001 意味着要 6283 步才一圈;第三,相邻 pos 的 PE 在所有维度上都很接近,但相距 4 的 PE 在前几维已经差很多——这就是「高频区分相邻、低频区分整段」的表现。
把这个表反复看几遍,你会建立起对 sinusoidal 的「肌肉记忆」:知道哪些维度在变、哪些在静、哪些在反向。这种直觉比任何公式都管用。
3.6 范数与方向:分开看
sinusoidal 的另一个常被忽视的性质是:每个 pos 的 PE 范数完全相等(= √(d_model/2)),所以「位置」全部信息都在「方向」上而不是「长度」上。
这条性质对 LayerNorm 友好——LayerNorm 会把范数归一掉、保留方向,PE 的位置信息因此会原样保留下来。如果换成「位置越远 PE 越大」之类的非范数恒定方案,LayerNorm 后位置信息会被显著抹掉。这条 sinusoidal 与 LayerNorm 配合得好的隐性贡献,原论文没写过,但读多份后续分析(包括 Pre-LN vs Post-LN 的几篇文章)就能拼起来.
四、关键性质:相对位置可以线性表达
4.1 这条性质的精确陈述
原论文写:「We chose this function because we hypothesized it would allow the model to easily learn to attend by relative positions, since for any fixed offset k, PE(pos+k) can be represented as a linear function of PE(pos)」。
翻译成数学:对任意固定的偏移量 k,存在一个不依赖 pos 的线性变换 M_k,使得
PE(pos + k) = M_k · PE(pos)
这条性质漂亮在哪里?模型如果想学「这两个 token 离我多远」,只要学一个 M_k 就够了,而不需要为每对 (pos, pos+k) 单独记一个映射。
换一个角度更直观:当你说「我希望模型在 attention 里识别『前 3 个位置』这种相对关系」,你不需要让模型把所有 (pos, pos+3) 都在数据里见过;你只需要让它学一次「+3 对应的旋转」就够了。这种「相对位置一次学习、全 pos 复用」的能力,是 sinusoidal 相比可学习位置最关键的优势。
4.2 用三角恒等式推一遍
这条性质本质上来自一组初等三角恒等式:
sin(α + β) = sin α cos β + cos α sin β
cos(α + β) = cos α cos β - sin α sin β
把它写成矩阵形式:
[sin(α + β)] [cos β sin β] [sin α]
[cos(α + β)] = [-sin β cos β] [cos α]
也就是说,「角度从 α 加到 α+β」这个操作,对 (sin α, cos α) 这一对值来说,是乘以一个旋转矩阵:
R(β) = [ cos β sin β]
[-sin β cos β]
现在回到 PE。PE(pos) 在第 i 个频率上的两个分量是
(sin(pos · ω_i), cos(pos · ω_i))。从 pos 到 pos
+ k,意味着角度从 pos · ω_i 加到
(pos + k) · ω_i,多出来的角度是
k · ω_i。
所以:
[sin((pos+k) ω_i)] = [ cos(k ω_i) sin(k ω_i)] [sin(pos · ω_i)]
[cos((pos+k) ω_i)] [-sin(k ω_i) cos(k ω_i)] [cos(pos · ω_i)]
这就是一个旋转矩阵 R(k · ω_i)。因为不依赖 pos,它就是「线性变换 M_k」在第 i 个频率块上的样子。
把所有 d_model/2 个频率上的旋转矩阵堆叠成一个块对角矩阵,就得到完整的 M_k。
4.3 这条性质的意义
「PE(pos + k) 是 PE(pos) 的线性变换」对模型意味着什么?
attention 是线性变换 + softmax 的组合。如果模型想用 attention 表达「我要关注离自己 k 步的 token」,它需要在 query 和 key 上构造一种「能识别相对距离」的运算。如果 PE 能直接通过一个 k 决定的线性变换转换,那么这种识别可以通过 W_Q、W_K 矩阵自然学出来——不需要额外的结构。
换句话说,sinusoidal PE 把「相对位置识别」这件事变成一个「W_Q、W_K 能不能学到合适形状」的问题,而不是一个「网络需要多少额外能力」的问题。这就是 sinusoidal 在外推上「比可学习位置更好」的根源:可学习位置在训练数据没有覆盖到的 pos 上压根没值,而 sinusoidal 在任何 pos 上都有定义且服从同样的几何规律。
4.4 一个常见的误解
「sinusoidal 既然有这么好的相对位置性质,为什么 attention 还会忽略远距离信息?」
因为「能用线性变换表达」不等于「网络一定会学到这个变换」。原论文的实验只能说明 sinusoidal 在 BLEU 上略优于可学习位置(消融实验给的差距很小),它没说模型一定会自动用上这条数学性质。后续的研究(Wang et al. 2020 的 “On Position Embeddings in BERT”)显示,原版 sinusoidal 在长程依赖上的实际利用是部分的、有缺陷的——这也是后来 RoPE / ALiBi 出现的动力。
线性可表达只是一个「免费的下限」,不是「保证有用的上限」。
把这条性质和后面 RoPE 的设计对照一下,你会发现 RoPE 的进步正是把「下限」推成「上限」:它通过把旋转直接焊在 Q、K 投影里,强制 attention 必须以「角度差」的形式利用相对位置。这就把模型「能不能学到相对位置」从一个开放问题变成了结构上的保证。
五、为什么是相加,不是拼接
5.1 原论文怎么写的
「The positional encodings have the same dimension d_model as the embeddings, so that the two can be summed.」
这一句一笔带过,但很多读者第一次扫论文都没看清楚——以为是 concat。事实上原论文从头到尾,PE 和 word embedding 的关系都是直接相加:
x = embed(token) + PE(pos)
5.2 相加在数学上意味着什么
「相加」乍看奇怪:把语义向量和位置向量混在一起,岂不是会污染语义?
直觉上的担心是合理的,但忽略了一个事实:embedding 空间是高维的(d_model = 512),而模型有能力在不同子空间上分别承载不同信息。如果训练数据足够,模型会自然学到「这一组维度主要受 PE 主导,那一组维度主要受 word embedding 主导」。后续研究(如 BERTology 系列)通过线性探针验证过这一点:早期层的某些维度确实更接近 PE 表征,深层逐渐被语义覆盖。
更技术性的解释来自一个相对简单的观察:相加可以看作「在 d_model 维空间里对 word embedding 做一个位置相关的偏移」。所有同样位置的 token 都被偏移到同一个方向,所以模型在 attention 里很容易识别「这两个 token 的 pos 差多少」——他们的偏移差就是 PE(pos₁) - PE(pos₂),而这个差又恰好由前面说的旋转性质刻画。
5.3 拼接为什么不被采用
拼接(concat)的方案是:把 d_model 切成两段,前 d_w 维放 word embedding,后 d_p 维放 PE。
形式上看,这样语义和位置在维度上完全分离,似乎更干净。但实际并不被采用,原因有三:
第一,参数效率低。拼接要么挤掉一段语义维度(让 word embedding 只能用 d_w < d_model 维),要么扩大 d_model 来腾出空间。前者削弱表达,后者增加 attention 的计算量。
第二,表达约束。拼接相当于强制让前几层「不要在语义和位置之间发生混合」。而相加给了模型完全的自由:它可以在某些维度上让两者纯粹叠加,也可以在另一些维度上让两者形成有意义的交互。
第三,实验结果。原论文 §3.5 末尾的消融提到他们试过「learned positional embeddings」,效果与 sinusoidal 相加几乎一样;后续社区试过 concat 的若干变体(包括 Gehring et al. 2017 的 ConvSeq2Seq 中的 concat 设计),在大模型规模下并未显示稳定优势。
「相加 vs 拼接」是一个典型的「直觉认为 A 更好、实际 B 表现一样还更省」的工程案例。
5.4 一个鲜为人知的细节:embedding 缩放
如果你打开原论文的代码(或者后来 Annotated Transformer 的复现),会发现一行容易被忽视的细节:
x = embed(token) * math.sqrt(d_model) + PE(pos)word embedding 在加 PE 之前先乘以 √d_model。这是因为标准的 nn.Embedding 初始化方差是 1/d_model 量级,乘以 √d_model 之后方差变成 1,与 sinusoidal PE 的能量量级相匹配。如果不这样做,PE 的能量会盖过 word embedding,模型一开始几乎只看到位置信息看不到语义。
很多复现版本省略了这一步,调试时会出现「训练特别慢」「前几个 epoch 的 attention 全在看 PE」的奇怪现象。这是一个标准的「论文里写了但读者经常没注意」的工程坑。
具体做一个能量估算:sinusoidal PE 的每对 (sin, cos) 维度上的方差恒定在 0.5(cos² + sin² = 1,两者均分),所以 PE 整体范数平方是 d_model/2,量级 √(d_model/2) ≈ √256 ≈ 16(d_model = 512 时)。word embedding 用 N(0, 1/d_model) 初始化时,范数平方是 1,量级 1。差距 16 倍,就是为什么必须乘 √d_model。乘了之后 word embedding 范数变成 √d_model = √512 ≈ 22.6,与 PE 在同一量级。这条「能量匹配」是相加方案能 work 的工程前提。
六、可学习位置编码:BERT 与 GPT 的选择
6.1 形式上有多简单
可学习位置编码的实现比 sinusoidal 还要简单:
self.pos_embed = nn.Embedding(max_len, d_model)
# ...
x = embed(token) + self.pos_embed(torch.arange(seq_len))就是一张 (max_len, d_model) 的可训练查表。每一行随机初始化(通常是 N(0, 0.02²)),随后跟着模型一起训练。
BERT 用的是这种,max_len = 512;GPT-2 也用这种,max_len = 1024;早期的 ViT 用同样的方案,max_len = patch 数 + 1(class token)。它们之所以选可学习而不是 sinusoidal,原因之一是当时的工程哲学是「能学的就让它学」,原因之二是早期模型的 max_len 都不大,外推不是关键约束。
6.2 它学到了什么
可学习位置编码表是个非常有意思的研究对象。BERT 的 pos_embed 训练完后,相邻位置的向量几乎平行,远端位置的向量几乎正交——这个结构和 sinusoidal 在大尺度上的表现非常像,但它不是被设计出来的,是从数据里学到的。这暗示「位置之间应该有的几何关系」是一个客观存在,sinusoidal 是在直接编码它,可学习位置是在用大量数据再发现它。
但学习有学习的代价。可学习位置在训练数据没有覆盖到的位置上,权重就没有梯度信号——这意味着如果你的训练序列长度上限是 512,第 513 位的 pos_embed 从初始化以来没有被更新过一次,它仍然是一团随机噪声。
6.3 长度外推差:训练 512、推理 2048 怎么办
这就引出了可学习位置编码最致命的工程问题:长度外推。
假设你拿一个用可学习位置编码、max_len = 512 训练好的模型,把推理输入扩到 2048。前 512 位有训过的 pos_embed,可以用;后面 1536 位没有 pos_embed 可用,怎么办?
业界尝试过几种方法:
- 截断。直接报错或者只用前 512 token,丢掉后面的。最常见、最不优雅。
- 重新初始化。把 max_len 扩到 2048,新加的 1536 行随机初始化,继续训练。但「继续训练」需要有标注数据、有算力,且容易破坏前 512 的已学权重。
- 位置插值。Chen et al. 2023 的 “Extending Context Window via Position Interpolation”:在用 RoPE 的模型上把 pos 索引乘以一个缩放因子,让 2048 位看起来像 512 位。这只对 RoPE 这类结构性位置编码有效,对查表式可学习位置无效。
- 相对位置补丁。比如在原本绝对位置编码的模型上额外加 ALiBi bias,让模型从结构上学会忽略「具体是哪一位」。
图里三条曲线对比的是相同训练长度下,三类位置编码在推理时随长度变化的表现。可学习位置在训练长度外瞬间变得不可用——崖式崩塌;正弦缓慢退化,因为它的几何规律在新位置上仍成立但模型没见过那么多新值;RoPE / ALiBi 几乎平稳,因为它们注入的是相对位置信息,对绝对长度不敏感。
6.4 为什么 BERT 没在意
读到这里你可能会问:BERT 是用可学习位置的,它服役多年,怎么没被这个问题搞死?
因为 BERT 的下游任务(分类、QA、NER)几乎都不超过 512 token;2018 年那个时点,「长上下文」是新闻摘要和长文档理解的小众场景。BERT 被设计在 512 这个长度上工作得很好,没人指望它处理 10k token 的会话或者 100k token 的代码库。
GPT-2 在 1024 上也类似。问题真正暴露是在 GPT-3 之后,长上下文成了新需求——这时候大家才发现:可学习位置无法平滑外推这件事,从工程上就锁死了模型的扩展空间。所以你看 GPT-3 之后的开源大模型几乎清一色换成了 RoPE 或者 ALiBi。这条迁移轨迹背后的根本原因,就是「位置外推」。
6.5 主流模型位置编码一览
把 2018 年到 2024 年主流模型用的位置编码列一张表,能直观看到这条演化轨迹:
| 模型 | 年份 | 位置编码 | max_len(原训练) | 备注 |
|---|---|---|---|---|
| 原版 Transformer | 2017 | sinusoidal | 1024 | 论文给的方案 |
| BERT | 2018 | learned | 512 | 双向 encoder,下游分类为主 |
| GPT-2 | 2019 | learned | 1024 | 第一代「语言模型当通才」的尝试 |
| Transformer-XL | 2019 | relative (Shaw 风格) | 长上下文研究分支 | |
| T5 | 2020 | relative bias(按桶分) | 512 | Google 重要 NLP baseline |
| GPT-3 | 2020 | learned | 2048 | 仍未换 RoPE,但已现外推压力 |
| ViT | 2020 | 2D learned | patch grid 14×14 | 视觉迁移 |
| LLaMA-1 | 2023 | RoPE | 2048 | 开源大模型转折点 |
| LLaMA-2 | 2023 | RoPE | 4096 | 直接因为 RoPE 易扩长 |
| LLaMA-3 | 2024 | RoPE(base 调到 500000) | 8192 | 长上下文专门优化 |
| Mistral | 2023 | RoPE + sliding window | 8192 | RoPE + 窗口 attention |
| Qwen | 2023 | RoPE | 8192~32768 | RoPE 渐成标配 |
| BLOOM | 2022 | ALiBi | 2048 | 唯一选 ALiBi 的大模型代表 |
| Chinchilla | 2022 | relative | 2048 | DeepMind 路线 |
读这张表可以看出几件事。第一,2017–2020 是绝对位置(sinusoidal、learned)的天下,因为任务长度都在 1k 以内。第二,2020 之后随着长上下文需求爆发,大家陆续转向相对位置编码或 RoPE。第三,到 2023–2024,RoPE 几乎成了开源大模型的事实标准,base 数值的调节(10000 → 500000 → 1e6)成为 long-context fine-tuning 的常见手段。
如果你今天要从头训一个 Transformer,几乎不会再选 sinusoidal 或者 learned;你会从 RoPE 开始,按需考虑 ALiBi。这是 2024 年的工业默认。
把这条迁移轨迹放在更大的视野里看:从「让模型有位置感」到「让位置感对长度外推友好」,再到「让位置感最小依赖具体 pos 值」,每一步都是在「实用约束」推动下的工程演化。这背后没有突如其来的理论突破,只是一群一群工程师把同一个问题换不同角度问出来。读这条线索的过程,本身就是一堂活生生的工程演化课。
如果你刚开始入行,从 sinusoidal 起步学位置编码是合适的,因为它的设计理念清晰、数学优美、文献完备;但请记住,工业实践已经走得更远,sinusoidal 在很多场景下只是一个「教学样例」而不是「最佳选项」。这种「教科书 vs 工业现实」的落差,几乎在 Transformer 的每个组件上都存在——比如 dropout 在大模型上越来越少用、warmup 在某些 optimizer 上变得不必要、softmax 在 attention 之外越来越被 sigmoid 替代。位置编码是这条落差最显眼的一例。
六点五、再做一个手算实验:6 token 的 attention 中 PE 起的作用
这一节我们做一个小实验,把 PE 在 attention score 里的贡献量化。
设 d_model = 8、d_k = 8(单头),W_Q = W_K = I(单位矩阵),所有 word embedding 设为 0(彻底剥离语义影响)。这样 attention score 完全由 PE 决定。用 sinusoidal_pe(6, 8) 算出 PE 表,再算 PE · PEᵀ 这张 (6, 6) 的 score 矩阵。
按 9.6.1 的推导,每个 (i, j) 上的 score 等于 Σ_k cos((i - j) ω_k),d_model = 8 时有 4 个频率:ω_0 = 1, ω_1 = 0.1, ω_2 = 0.01, ω_3 = 0.001。
具体值:
score(i, i) = cos(0) + cos(0) + cos(0) + cos(0) = 4.000
score(i, i±1) = cos(1) + cos(0.1) + cos(0.01) + cos(0.001)
≈ 0.540 + 0.995 + 1.000 + 1.000 = 3.535
score(i, i±2) = cos(2) + cos(0.2) + cos(0.02) + cos(0.002)
≈ -0.416 + 0.980 + 1.000 + 1.000 = 2.564
score(i, i±3) ≈ cos(3) + cos(0.3) + cos(0.03) + cos(0.003)
≈ -0.990 + 0.955 + 1.000 + 1.000 = 1.965
score(i, i±4) ≈ cos(4) + cos(0.4) + cos(0.04) + cos(0.004)
≈ -0.654 + 0.921 + 0.999 + 1.000 = 2.266
score(i, i±5) ≈ cos(5) + cos(0.5) + cos(0.05) + cos(0.005)
≈ 0.284 + 0.878 + 0.999 + 1.000 = 3.161
非常有意思的事在最后两行:score(i, i±4) 和 score(i, i±5) 居然比 score(i, i±3) 大。这是高频维度回绕引起的「位置歧义」——cos(5) 突然变正了。
把每一行做 softmax 之后,attention 权重不是单调下降的,而是「相邻最强、远端有反弹」。这正是 sinusoidal 不够强的地方:单凭 PE,attention 不能保持「越近越亲」的单调性。
实际模型里这个问题被 W_Q、W_K 学到的非平凡形式部分缓解,但依然是 sinusoidal 的固有结构性缺陷之一。RoPE 通过把每个频率块的范围限制在「不会绕回」的范围内,并配合 base scaling,部分修复了这个问题;ALiBi 的线性衰减则彻底单调,从根上避免「越远反而越亲」的反常。
这个实验五分钟就能跑出来,建议读者自己写一遍——比读十段文字都更能让 sinusoidal 的优劣在脑子里立起来。
七、sinusoidal 的外推:能用,但有限
7.1 sinusoidal 至少没崩
把上一节的逻辑应用到 sinusoidal:在训练长度之外,sinusoidal 仍然是一个良好定义的函数。pos = 2048 时它有值,pos = 65536 时它仍然有值,且这些值与 pos = 0..N 时的几何规律完全一致。
这意味着 sinusoidal 在长度外推上至少不会崩。把训练 512 的模型推到 2048,它至少能跑——不报错、不返回 NaN。
7.2 但「能跑」不等于「跑得好」
跑能跑,效果是另一回事。
后续研究(Press et al. 2021 的 ALiBi 论文里有一张关键表)显示,sinusoidal 在训练长度之外的位置上,attention 模式会发生显著漂移:模型学到的「应该关注哪些位置」依赖于训练时见过的 PE 取值范围;训练时的 pos = 511 对应一组特定的 sin/cos 值,推理时 pos = 1023 对应一组数值上没见过的 sin/cos 值——尽管它们处在同一条函数曲线上,模型的内部表征还是会跑偏。
具体的下游表现是:困惑度(perplexity)随推理长度逐渐上升,但比可学习位置慢得多。这就是 ALiBi 论文标题里那个判断的来源:「Train Short, Test Long: Attention with Linear Biases Enables Input Length Extrapolation」——它是在批评 sinusoidal 的外推,不是赞扬。
7.3 sinusoidal 失败的可解释性证据
更细致的诊断来自 Kazemnejad et al. 2023 “The Impact of Positional Encoding on Length Generalization in Transformers”。这篇论文系统训练了多个 Transformer,分别使用 sinusoidal、可学习、RoPE、ALiBi、NoPE 五种位置策略,在合成任务(复制、排序、算术)上测试外推。
它得出的关键结论:在外推能力上,NoPE > ALiBi > RoPE >> sinusoidal ≈ learned,而且差距相当大。
NoPE 居然排第一这件事一开始让所有人都不敢相信。后来明白了:在 decoder-only + causal mask 的设置下,causal mask 已经隐式提供了相对位置信号,再加一个绝对位置编码反而会引入「具体 pos 值」这种依赖,让外推时模型「认不出陌生 pos」。
ALiBi 排第二的原因前面讲过——它的 prior 是纯距离的、不依赖 pos 值。RoPE 排第三是因为它仍然需要 pos 进入旋转角度,但旋转的局部性比 sinusoidal 的全局相位强。
这条研究的工程含义非常具体:如果你正在做长上下文 fine-tuning,把模型从 sinusoidal 换成 RoPE 或 ALiBi 是一个高 ROI 的迁移;继续在 sinusoidal 上死磕 base scaling 几乎是没有出路的方向。
7.4 把 sinusoidal 和 ALiBi 比较一下衰减形态
最后一个对比角度。sinusoidal 给 attention 注入的 prior 是 cos((i-j)ω) 的求和——它是震荡的、有回绕的,远端可能反弹。ALiBi 给 attention 注入的 prior 是 -m|i-j|——它是单调的、严格衰减的、永不回绕。
直观上 ALiBi 「更像我们对距离的预期」。但这种单调性也意味着 ALiBi 对长程依赖的表达力天然受限:它强迫远端 attention 越来越小,而不像 sinusoidal 留有「远端某些位置反而权重大」的余地。
所以这两个方案不是简单的「ALiBi 更好」,而是「在外推稳定性 vs 远程表达力之间各自做了不同折中」。这是位置编码设计永恒的权衡,没有一劳永逸的胜者。
7.5 这条不足开启了 RoPE / ALiBi
整个第 41 篇「现代位置编码」的故事就是从这里开始的。如果你接受 sinusoidal 的外推不够强、可学习位置的外推几乎为零这两个事实,下一步问的问题就是:
「能不能把位置信息从『加到输入』的层面挪到『直接介入 attention 的打分』的层面?」
答案是可以。RoPE 通过把每对维度看成复平面上的旋转,让相对位置以「角度差」的形式直接进入 Q·Kᵀ;ALiBi 通过给 attention score 减去一个与距离成正比的偏置项,让远端 attention 自然衰减。这两个方案都不依赖训练时见过哪些 pos,所以对外推几乎是「免费」的。
我们在第 41 篇会用同样的方法把 RoPE 和 ALiBi 拆开。这一篇你只需要把 sinusoidal 的「线性表达相对位置」这条性质牢记——它就是 RoPE 进一步推到极致的那个数学根基。
7.6 一段历史小插曲
私下里有几位作者在不同访谈里回忆过这一节的诞生。最初他们尝试过几种方案:纯可学习、sinusoidal、还有一种 sinusoidal + 可学习残差。最终选 sinusoidal 是因为它在外推上略好、参数省、实现干净。
有趣的是,论文末尾的 §3.5 那句「we hypothesized it would allow the model to extrapolate」——「我们假设它能外推」——的措辞是极其谨慎的。作者用 hypothesize 而不是 demonstrate,说明他们当时也不确定外推到底能多好。后续几年的实证研究证明这条假设在「能外推到 1.5x 长度」上是对的,在「能外推到 4x 长度」上不成立。这是一个标准的「论文猜测、社区验证、最终被替代」的科学循环。
八、一段可运行代码:把 PE 算出来看看
8.1 PyTorch 实现 sinusoidal
下面是一段最小、能运行的 sinusoidal 实现,与 Annotated Transformer 的版本等价(仅去除注释):
import math
import torch
def sinusoidal_pe(seq_len: int, d_model: int) -> torch.Tensor:
pe = torch.zeros(seq_len, d_model)
position = torch.arange(0, seq_len, dtype=torch.float).unsqueeze(1)
div_term = torch.exp(
torch.arange(0, d_model, 2, dtype=torch.float)
* -(math.log(10000.0) / d_model)
)
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
return pe注意三个细节。第一,div_term 用
exp(-log(10000) * 2i / d_model) 计算
1 / 10000^(2i / d_model),比直接 pow
在数值上更稳定。第二,偶数维填 sin、奇数维填
cos,与论文公式一一对应。第三,输出形状
(seq_len, d_model),使用时直接广播相加:
x = token_embed * math.sqrt(d_model) + sinusoidal_pe(seq_len, d_model).to(device)8.2 一个完整性检查
把 d_model = 4、seq_len = 4 跑一遍,结果应该和我们在第 3.4 节手算的一致:
>>> sinusoidal_pe(4, 4)
tensor([[ 0.0000, 1.0000, 0.0000, 1.0000],
[ 0.8415, 0.5403, 0.0100, 1.0000],
[ 0.9093, -0.4161, 0.0200, 0.9998],
[ 0.1411, -0.9900, 0.0300, 0.9996]])第一行全是 [0, 1, 0, 1],正是 pos = 0 时所有 sin 为 0、所有 cos 为 1 的特殊情形。其余几行的高频维度(前两列)剧烈变化,低频维度(后两列)几乎不动。和手算结果误差只来自 cos(0.02)、cos(0.03) 离 1 的微小差距。
8.3 一个可视化建议
如果你想真正建立直觉,强烈建议把 sinusoidal_pe(512, 64) 算出来,画一张热力图(横轴 pos,纵轴维度索引,颜色表示数值)。你会看到:
- 顶部几个维度(高频)像高频细密的条纹;
- 中间维度像稳定的中频波;
- 底部几个维度(低频)几乎是渐变带。
这张图基本上就是上面 SVG 的真实版。第一次看到它的人通常会在那一刻 get 到「sinusoidal 是把位置投影到一组多尺度傅里叶基」这件事。
九、几个常被忽视的工程细节
9.1 PE 是否参与 dropout
原论文 §5.4 提到,PE 加到 word embedding 之后,整个 sum 一起过 dropout:
x = dropout(token_embed * sqrt(d_model) + sinusoidal_pe)这个 dropout 通常 p = 0.1。它的作用不是正则化 PE 本身(PE 是固定函数,不需要正则),而是正则化「PE + word embed 这个组合」如何被后续层看到。很多复现忘了在这里 dropout,会让训练前几个 step 收敛得偏快但后期泛化差。
9.2 PE 是否要乘 √d_model
不要。乘 √d_model 是给 word embedding 用的,因为它的初始化方差太小;sinusoidal PE 本身的能量已经在 O(1) 量级(每个频率块上 sin² + cos² = 1),不需要再放大。
9.3 推理时是不是要重新生成 PE
不需要。PE 是 (max_len, d_model)
的固定矩阵,初始化时算一次,之后只是按 pos 取行。在 PyTorch
实现里通常用 register_buffer
注册成模型的非可训练状态:
self.register_buffer("pe", sinusoidal_pe(max_len, d_model))这样 PE 会跟模型一起 .to(device)、.half(),但不会进入 optimizer 的参数列表。
9.4 长度可以延长,怎么动态增长
如果你想在推理时支持比 max_len 更长的序列:sinusoidal 直接重新算就行,因为它没有训练参数。可学习位置就要做插值或者重新训练;这条差别再次说明 sinusoidal 在工程灵活性上的优势。
实际上 LLaMA 的 RoPE 实现里
precompute_freqs_cis(dim, end)
这个函数也是「需要时算到 end」——和 sinusoidal
的精神完全一致。位置编码不该是预先固化的查表,而该是按需计算的函数。
9.5 padding 与 PE 怎么相处
batch 内不同样本长度不同时,要把短的 pad 到长的。pad token 通常是 [PAD],对应一个特殊的 embedding。问题是:pad 位置要不要加 PE?
答案是「加,但 attention mask 把它屏蔽掉」。具体操作是:所有位置都按 pos = 0..N-1 加 PE,然后 attention 计算时通过 attention_mask 让 pad 位置不参与 softmax 归一(mask 出来的 score 设为 -∞)。
这样 pad 位置的 PE 不会污染前向传播,但你不能省掉这一步——如果你在 pad 位置不加 PE,会让模型在 attention 之前的某些操作(比如 LayerNorm、FFN)上对 pad 行为不一致,反而难训。
实际工程上,做对这件事的最简单方法是:永远对所有位置算 PE,然后让 attention_mask 解决一切。这条原则在所有主流框架(PyTorch、JAX、TF)的 Transformer 实现里都是默认。
9.6 多 batch 并行下的 PE
PyTorch 的 sinusoidal 实现里一般会写成
(1, max_len, d_model) 的三维张量(多一个 batch
维度),这样可以直接广播相加:
x = token_embed + self.pe[:, :seq_len]这样不会因为 batch size 变化重新生成 PE,也不会把 PE 复制 batch 次浪费内存。这是一个很小的实现技巧,但在大规模训练里能省下肉眼可见的内存。
9.7 检查清单:实现 PE 时常踩的五个坑
实现 sinusoidal 的代码看着简单,实际新人容易踩的坑还不少。我把常见的五个列在这里,作为自检清单。
第一个坑:忘了乘 √d_model。前面 5.4 节讲过。如果你 train 出来的模型早期 attention 全在看 PE 不看语义,多半是这个。
第二个坑:PE 没注册成 buffer 而是普通 tensor,导致 .to(device) 时不跟着模型走。debug 时表现为「单 GPU 跑得通,多 GPU 报 device mismatch」。
第三个坑:sin 和 cos 维度顺序错。原论文是偶数维 sin、奇数维 cos;有些复现颠倒了,结果模型也能训但与原论文行为不同。
第四个坑:用 max_len 太大(比如 8192)+ d_model 太大(比如 4096),PE buffer 占用 128 MB 显存。在小模型上 PE 不该是显存大户,但生产模型里有时确实需要注意。
第五个坑:在多卡数据并行下手动复制 PE 到每张卡,结果每张卡上的 PE 浮点误差不一致,attention 输出在卡间出现 1e-5 量级的偏差。这个偏差小,但对于求和 reduce 的 loss 会放大。解决方法是要么用 register_buffer 让 PyTorch 自动同步,要么在初始化时强制每张卡用同一个 PE 张量。
把这五个坑过一遍,再去写
PE,几乎不会出问题。除此之外还有几个更隐蔽的小问题:把 PE
算成 Float32 之后忘了 cast 回模型 dtype(导致 PE + word
embedding 时类型不匹配);把 div_term 用了
1.0 / 10000.0**(...) 而非 exp(-log(10000) * …)
的写法(前者在 d_model 大时会下溢);以及 attention mask 与
PE 顺序处理错(先 mask 再加 PE 还是先加再
mask)。这些坑看起来很细,但每个都在生产代码里被踩过。
九点五、再换一个角度:把位置编码看成一组「时钟」
9.5.1 时钟比喻
如果你觉得「多频率正弦波叠加」这个说法太抽象,可以换一个比喻:把 sinusoidal PE 想象成一组并排走的钟。
最快的那只钟,秒针每位置走 1 弧度,大约 6 个位置就走完一整圈。这只钟极其敏感,第 0 位和第 1 位的指针位置完全不同。
中间那只钟慢一点,要走几十个位置才完成一圈。它的指针在「相邻」尺度上几乎不动,但在「跨段」尺度上提供有用的区分信号。
最慢的那只钟,几乎一辈子都在指着同一个方向,要 6 万个位置才走完一圈。它告诉模型「你大致在长文档的哪个区段」。
把所有钟的「指针」(也就是 sin、cos 这一对值,等价于复平面上的一个点)拼起来,就是 PE(pos)。每一只钟都给一个不同尺度上的位置识别,几百只钟拼起来,就足以让任何一个 pos 唯一可识别。
这个比喻不是为了卖弄修辞,而是有数学根据:复平面上 e^(iωpos) = cos(ω·pos) + i·sin(ω·pos),恰好就是一个角速度为 ω 的转动;sinusoidal PE 就是「在 d_model/2 个不同角速度上把 pos 同时编码」。RoPE 把这个隐含结构显式化,并把它焊死在 attention 内部——这就是它名字里 “Rotary” 的来源。
9.5.2 与二进制编码的类比
这个时钟模型还有一个有趣的对照:二进制位。
考虑用二进制表示位置:pos = 0, 1, 2, 3 写成二进制就是 00, 01, 10, 11。最低位(每两位翻一次)和最高位(每四位翻一次)共同唯一标识一个位置。把这个想法推广到 d_model 位,就能编码 2^d_model 个位置。
sinusoidal PE 几乎就是「连续版的二进制」:把「每两步翻一次」换成「每 2π 步走一圈」,把「翻位」(离散的 0/1)换成「sin/cos」(连续的实数)。频率最高的那对维度像是最低有效位(LSB),频率最低的那对维度像是最高有效位(MSB)。
这个连续化的好处是:「相邻位置的 PE 接近」这条性质自然成立,模型对位置的处理可以平滑可微;而二进制版本里第 7 → 第 8 位的跳变(0111 → 1000)是离散的,几乎所有比特都翻了,对网络极不友好。
理解了这条二进制类比,你就理解了为什么 sinusoidal 不是「一种花哨的工程选择」,而是「在连续空间上做位置识别的最自然方案之一」。
9.5.3 一个直观的距离实验
用代码做一个直观实验:算一个 (64, 64) 的 PE 矩阵,然后算两两位置之间的 PE 向量距离,画成 64×64 的距离热力图。
import torch
pe = sinusoidal_pe(64, 64)
dist = torch.cdist(pe, pe)你会看到一张非常漂亮的图:对角线为 0(自己跟自己距离为 0),距离随 |i - j| 增大而平滑增加,但不是单调的——因为高频维度上「相邻」和「相距 6」的距离可能相近(一个完整周期的回绕)。最低频维度负责把这种回绕「分开」,让远端位置的总距离仍然大。
这张图实际上是 sinusoidal 的「身份证质量证明」:它在所有 |i - j| 上都能产生有意义的距离信号,且没有任何一对 (i, j) 让 PE 相等。
九点六、PE 与 dot product:相对位置直接出现在 attention 打分里
9.6.1 PE 的内积只依赖相对位置
sinusoidal 还有一条非常美的性质,论文没有直接证,但很容易推:PE(pos₁) · PE(pos₂) 这个内积,只依赖于 pos₁ - pos₂,不依赖于 pos₁ 或 pos₂ 本身。
证明很短。把内积按频率块拆开:
PE(pos₁) · PE(pos₂)
= Σ_i [ sin(pos₁ ω_i) sin(pos₂ ω_i) + cos(pos₁ ω_i) cos(pos₂ ω_i) ]
= Σ_i cos((pos₁ - pos₂) ω_i)
最后一步用了
cos(α - β) = cos α cos β + sin α sin β。结果只依赖于
Δ = pos₁ - pos₂ 这一个量。
9.6.2 这条性质对 attention 意味着什么
在没有 word embedding、只有 PE 的极端情况下,attention score Q·Kᵀ 在每个 (i, j) 位置上的值是 PE(i) · PE(j),正好只依赖于 i - j。换句话说,attention 在这种情况下天然只关注相对位置。
实际情况是 word embedding 和 PE 共同决定 score,所以 attention 既看绝对位置又看相对位置;但 PE 这一项贡献的「纯位置 score」只依赖 |i - j|,这给了模型一个非常干净的相对位置先验。
后续 RoPE 把这条性质强化到了极致:不仅 PE·PE 只依赖相对位置,而是 (W_Q x_i) · (W_K x_j) 整个 attention score 都只通过相对位置进入位置项。这就是「相对位置编码」从「sinusoidal 的副产品」变成「主设计原则」的理论起点。
9.6.3 一段验证代码
pe = sinusoidal_pe(20, 64)
def dot(i, j): return (pe[i] * pe[j]).sum().item()
print(dot(0, 5)) # 同样的 |i-j|=5
print(dot(7, 12))
print(dot(13, 18))三次输出几乎完全一致(差异只来自浮点舍入)。这是 sinusoidal 在数学上承诺的事,也是它能让模型「学相对位置」的直接根据。
九点七、一个常被忽视的视角:PE 在不同任务上的「合身」程度
9.7.1 自然语言上的位置感
自然语言里位置信息有三种形态:
第一种是结构性位置——句子开头、结尾、第几个词。这种位置是稀疏但关键的;BERT 之类模型用 [CLS] 和 [SEP] token 来标记结构,PE 上反而要求不强烈。
第二种是相对位置——形容词修饰它前一个名词,主语和谓语之间隔三个虚词。这是大多数语法依赖的形态,sinusoidal 的「相对位置可线性表达」恰好对应。
第三种是长程位置——一篇文章的开头和结尾,对话的前几轮和最后几轮。这种位置在跨段落、跨文档场景下成为瓶颈,是 RoPE / ALiBi 重点优化的方向。
不同任务对这三种位置的需求权重不同,所以「最佳位置编码」其实是一个「任务依赖」的问题,而不是「数学上唯一最优」的问题。
9.7.2 视觉上的位置感
ViT 把图像切成 patch,每个 patch 当一个 token,然后过 Transformer。这时位置信息变成「这个 patch 在图像的第几行第几列」,从一维退化成二维。
二维位置编码有几种方案:
- 2D 可学习:直接学习一张 (H, W, d_model) 的查表。原始 ViT 用的就是这种。
- 2D sinusoidal:把 sinusoidal 拓展到二维:前 d_model/2 维编码 row,后 d_model/2 维编码 col。
- 2D RoPE:把 RoPE 拓展到二维平面旋转,复数对 (cos θ, sin θ) 拓展到 (cos θ_row, sin θ_row, cos θ_col, sin θ_col)。
Swin Transformer、DETR 用的是 2D sinusoidal;MAE、DiT 用的是 2D 可学习;最新的视觉大模型多用 2D RoPE。这条选型轨迹基本是 NLP 路径的复制:先可学习,再函数型,再相对位置。
9.7.3 音频与时序信号
音频信号比文本更长——一秒音频可以是 16000 个采样点,做了 mel-spectrogram 之后还有几百帧。这意味着位置编码必须面对极长序列。Whisper、HuBERT 这一类模型用的是 sinusoidal 或者 RoPE,原因之一就是它们能函数式地处理任意长度。
时间序列预测(LSTF、Informer、PatchTST)也类似,且对相对位置极敏感(「第 -1 步」和「第 -24 步」差别巨大),所以这一类模型几乎一致地用相对位置编码。
9.7.4 多模态:跨模态的位置统一
CLIP、Flamingo、LLaVA 这一类多模态模型遇到一个棘手问题:图像 token 和文本 token 怎么共享位置编码?
通常做法是给图像 patch 赋一组「特殊位置」(比如统一编号到 [-1, -2, …]),让文本 token 用正常 pos = 0, 1, 2, …。这样模型从位置上就能区分模态,同时不浪费正常位置编码的连续性。
这个细节在论文里通常一句话带过,但在工程实现里非常关键——位置编号的对齐、padding 的处理、跨模态 attention 的位置先验,每一个都直接影响最终效果。
九点八、几条进一步的实验线索
9.8.1 “NoPE” 的实验
最近几年(2023 起)有一组研究问了一个反直觉的问题:如果干脆不用任何位置编码会怎样?这就是 “No Positional Encoding”(NoPE)实验。
直观上 self-attention 是排列等变的,没有 PE 应该完全学不会语序。但实验显示:在 decoder-only 自回归 LM 上,causal mask 自身就提供了足够的位置信息——因为「第 t 位只能 attend 第 0..t 位」这个约束让 attention 自然带上了「我是序列里的第 t 个」的隐含信号。
Haviv et al. 2022 “Transformer Language Models without Positional Encodings Still Learn Positional Information” 是这条研究线的代表。它说明:在 decoder-only + causal mask 这种特殊设置下,PE 不是必需的——但这不否定 PE 在 encoder(无 mask)和长上下文外推上的价值。
这条线索值得记一下,因为它再次说明位置编码的设计要紧扣「模型架构提供了什么、缺什么」,而不是「公式有多优雅」。
9.8.2 把 PE 加到中间层
原论文是把 PE 加在 embedding 层(最浅层)。后续有研究尝试把 PE 加到中间层、每一层都加(Su et al. 2021 的实验之一),结果是:
- 每层都加:训练略稳一点,但效果差异在 0.x BLEU 量级,不显著;
- 只加到中间某一层:效果反而下降,因为浅层缺位置信息;
- 只加到最后一层:完全不行——浅层 attention 已经把位置无关的特征卷在一起了,最后再加 PE 救不回来。
结论是:PE 的位置(架构上的位置)和 PE 的内容(数学上的 sin/cos)是两件独立的事,前者一旦定下来,后者的形式选择空间其实不大。
9.8.3 attention 头之间的位置专门化
可解释性研究发现一件有趣的事:在多 head attention 里,往往有几个 head 几乎专门负责「关注前一个 token」、几个 head 专门「关注前 N 个」。这些 head 的存在严重依赖 PE 提供的位置信号。如果你把 PE 设为 0,再观察 attention 模式,会看到这些位置敏感的 head「失活」——它们不再形成稳定的注意力模式。
这条事实是「PE 不是冗余信息」最直接的证据:它影响模型内部分工的形成,影响 head 的功能专门化,进而影响下游任务的表现。
九点九、把 PE 当成 prior:贝叶斯视角
9.9.1 一种概率诠释
如果把 attention 看作一种基于「相似度」的检索,那么 PE 提供的就是一个「位置先验」——告诉模型「先验上,更近的位置更可能相关」。
数学形式上,attention 权重 α_{i,j} ∝ exp(score(i, j)),加上 PE 之后 score 变成「语义相似度 + 位置匹配度」之和。位置匹配度的形式(sinusoidal 的 cos((i-j)ω))天然衰减——|i-j| 越大,cos 越分散,整体期望越接近 0。这相当于一个软性的「就近 prior」,但不至于硬性截断长程依赖。
9.9.2 ALiBi 把 prior 显式化
ALiBi 把这个 prior 直接挑明:在 attention score 里减去 m·|i-j|,其中 m 是一个固定的衰减率。这是一个真正显式的「就近先验」——距离每增加一位,score 就线性下降一份。
ALiBi 之所以在外推上极强,是因为它的 prior 完全由距离驱动、不依赖 pos 的具体值;模型不需要见过 pos = 5000 才能在 pos = 5000 上工作,它只要见过「相对距离 = 几」的若干样例,就能在任意 pos 上施加同样的 prior。
这个 prior 视角把 sinusoidal、RoPE、ALiBi 串成同一条线:它们都是在 attention 里注入「就近先验」,区别只在于「这个 prior 长什么形状」、「衰减得多快」、「是否依赖具体 pos」。这个统一视角是理解第 41 篇内容的钥匙。
九点九点五、实战:从一个 PE 漏洞 debug 一个翻译模型
这一节讲一个真实场景的浓缩复盘。我曾经帮一个朋友 debug 一个英中翻译模型——他用的是开源 Transformer 架构,但训练 loss 一直比基线高 0.3,BLEU 比基线低 1.5 个点。我们花了几天排查模型架构、数据预处理、optimizer 都没找到问题,最后定位在 PE 的实现上。
具体的 bug 是这样的。他的代码里 sinusoidal_pe 被实现成了:
position = torch.arange(0, seq_len, dtype=torch.float).unsqueeze(1)
div_term = torch.arange(0, d_model, 2, dtype=torch.float)
div_term = 10000.0 ** (-div_term / d_model)看起来没问题对吧?但有一行被无意删掉了:原本应该乘以 word
embedding 之前先
embed * sqrt(d_model),被简化成了直接加。导致
PE 的能量在 d_model = 512 上比 word embedding 大约 22.6
倍。
直接表现是:模型前向传播时,输入向量几乎完全被 PE 主导,word embedding 只贡献 5% 的范数。早期 attention 学到的是「关注位置 0、关注位置 i-1、关注位置 i+1」之类的位置模式,几乎没看见语义信号。
修复后,loss 立即下降 0.3,BLEU 回到基线。一个 √d_model 的因子,导致几个 epoch 几千 GPU 小时被白白浪费。
这个故事的教训不是「要乘 √d_model」——这条前面已经写过——而是:位置编码这种「看似边缘的工程细节」,一旦实现错,会用最隐蔽的方式毁掉模型表现。它不会让模型崩溃、不会报错、不会出 NaN,只会让 loss 比基线高一点点;如果你不知道基线应该是多少,可能永远都不会发现。
写这一篇的最大目的,就是让你以后看到 sinusoidal 实现时,能立刻盯住几条关键性质——范数、相加、√d_model、奇偶交错——然后自动检查是否有遗漏。这种「肌肉记忆式」的 review 是工程能力的核心,比记住任何一段公式都重要。
十、与系列其他章节的衔接
位置编码不是孤立的设计,它和 Transformer 其他部件——尤其是 encoder、decoder、残差、归一化——以及后续的 RoPE/ALiBi、注意力近似都有明确的衔接关系。这一节把这些线索摆出来,便于你在读后面几篇时随时回头核对。
10.1 第 22 / 23 篇
下一篇 22 讲 encoder 详解,会用到「输入 = embed + PE → encoder 6 层堆叠」这条数据流;第 23 篇讲 decoder,cross-attention 里 encoder 输出的位置信息也是这套 PE 注入的。理解了 21,22 / 23 才不会一上来被「输入是怎么来的」绊住。
10.2 第 24 / 25 篇
24 讲残差、25 讲 LayerNorm。它们和位置编码有一个隐藏的相互作用:每经过一次残差连接,PE 信息就被「保留一份原样」叠加到深层;每经过一次 LayerNorm,PE 的绝对幅值会被归一掉,留下的是方向。这意味着深层网络看到的 PE,已经不是浅层的那组 sin/cos 值,而是它在残差通道上层层叠加 + 归一之后的「等价表征」。这条线索在 24、25 里会顺手提到。
10.3 第 41 篇
41 讲 RoPE 和 ALiBi。如果说 sinusoidal 是「在输入加位置」,RoPE 是「在 Q、K 旋转」,ALiBi 是「在 score 上加偏置」。三者哲学不同,但都源于同一个根本困难——self-attention 排列等变。21 是这条线索的起点。
10.4 第 52 / 53 篇
52 / 53 讲可解释性、attention 可视化。位置编码在「attention 看到的是什么」这件事上是核心变量:早期层 attention 通常先关注位置(「上一个 token」「相邻三个」),深层逐渐转向语义关注。这条「位置→语义」的演化正是位置编码在多层网络里被「消费」的过程。
10.5 第 41 / 42 篇之间的桥梁
第 41 是 RoPE / ALiBi 的具体讲解;第 42 是注意力近似(Linformer、Performer、FlashAttention)。这两件事都依赖一个共识:「能不能不显式地存储 (i, j) 的位置编码或 attention 矩阵」?sinusoidal 本身已经是「函数式」的(不存储),RoPE 在此基础上把位置编码干脆并进 Q、K 投影里,让位置和语义完全融合;Linformer 等方案则更激进,连 attention 矩阵本身都不显式构造。这是同一条「函数化、低存储、可外推」的设计路线。21 是这条路线的起点,是值得认真讲的入口。
10.6 一句话总结
如果你只能从这一篇带走一句话,那就是:位置编码不是补丁,是 self-attention 排列等变性的结构性补全;它的设计哲学决定了模型能不能扩长上下文、能不能外推、能不能稳定训练。21 这一篇把这条结构性必要性讲透,后面所有位置编码相关的演进,都是在不同维度上重新回答同一个问题。
十一、关键概念回顾
回头梳理这一整篇,有几个判断是真正立得住的,也是后续所有位置编码讨论的地基。
第一个判断:self-attention 是排列等变的。这不是某种工程缺陷,也不是实现细节,而是数学上的硬事实——从投影、打分到 softmax 加权求和,每一步都对位置无感。一旦你把这条事实放在心里,「为什么需要位置编码」就不再是一个需要被回答的开放问题,而是一个早已写进 attention 定义里的必然结论。
第二个判断:sinusoidal 的设计核心是「让相对位置可以通过线性变换表达」。这不只是漂亮,它直接决定了模型能否用 W_Q、W_K 自然学到「相对距离敏感」的 attention 模式。10000 这个数字、sin/cos 的奇偶交错、所有频率覆盖从最高到最低——所有这些细节都为这一个核心性质服务。
第三个判断:PE 是「相加」而不是「拼接」,背后是「让模型在高维空间里自由分配子空间」的工程哲学。相加给模型自由,拼接给模型约束;在 d_model 足够大、训练数据足够多的前提下,自由几乎总是赢。
第四个判断:可学习位置在长度外推上有结构性缺陷。它不是「外推效果差一点」,是「训练长度外的权重根本没被训过」——这是查表型方案的固有问题。理解这条,就理解了为什么大模型时代主流位置编码必须是函数型(sinusoidal、RoPE、ALiBi),不能是查表型。
第五个判断:sinusoidal 在外推上比可学习位置好得多,但仍然不够,因为它的「相对位置可线性表达」是一条免费的下限,不是一条强制的上限。模型不一定在所有任务上都能学到这条性质,远端 attention 仍然可能漂移。这条不足是 RoPE / ALiBi 的起点。
把这五条串起来,你就知道为什么这一整篇花了一万多字讲一个看似简单的「加上 sin/cos」——它其实是 attention 这套架构的一个结构性补丁,而这个补丁的设计哲学决定了后面十年位置编码的演化方向。
还可以再补一条第六个判断:位置编码的设计哲学不是「公式好不好看」,而是「在外推稳定性、参数效率、训练稳定性、表达力之间做什么折中」。理解这套折中坐标系,才能在面对一个新的位置编码方案时快速判断它解决的是哪个问题、付出的是什么代价。这一点比记住具体的 sin/cos 公式重要十倍——因为公式会换、规范会改、模型会迭代,但「折中坐标系」是位置编码这件事本身的几何形状,不会变。
最后再回头看开头那句被读者忽视的论文话:「we must inject some information about the relative or absolute position of the tokens」。Vaswani 等人把这句话写得克制,把 §3.5 的篇幅压得很短,但这句话的分量足以撑起一个十年的研究方向——从 sinusoidal 到 RoPE 到 ALiBi 到 NoPE,每一步都是在不同的层面回应这同一个 must。
十二、常见误解
误解一:位置编码是 Transformer 的可选模块,去掉也能训
不能。任何不注入位置信息的纯 self-attention 模型都是排列等变的,「猫吃鱼」和「鱼吃猫」对它一致。某些任务(比如 Set Transformer、对集合建模)确实希望模型排列等变,那是因为这些任务的输入本来就是无序集合;但只要你的输入是有序序列(自然语言、时间序列、视频帧),位置编码就是非可选的。
误解二:sinusoidal 是 concat 到 word embedding 上的
不是。原论文从头到尾是相加,且要求 PE 维度 = d_model。把它理解成 concat 是最常见的读论文 bug,实现也会因此偏差。
误解三:10000 是某种神秘最优值
不是。10000 是「足够大、覆盖足够长尺度、不需要 tune 的经验选择」。RoPE 沿用了 10000,但有些工作(如长上下文 LLaMA 微调)会把它调到 1e6 来扩展位置周期。这个数字是工程惯例,不是理论最优。
误解四:可学习位置编码本质上更强,因为「能学」
不一定。能学的代价是参数量、训练长度上限、外推能力差。在大模型 + 长上下文的工程语境里,函数型位置编码(sinusoidal、RoPE、ALiBi)几乎全面胜出。「能学」在数据无限的极限下确实更灵活,但实际应用中数据从来不是无限的。
误解五:sinusoidal 的相对位置性质保证模型一定能用上
只保证「能用线性变换表达」,不保证「模型一定会学到这个变换」。后续可解释性研究显示,原版 Transformer 在远端位置上的 attention 模式仍然有漂移。这条「数学下限」与「工程实际」之间的鸿沟,正是 RoPE 把同一原理直接焊死在 attention 公式里的动机。
误解六:把 PE 设为可训练参数总是更灵活更好
只在训练长度内更好。一旦推理长度超过训练长度,可训练 PE 立刻报废,灵活反而成累赘。函数型 PE 在这件事上的优势是结构性的,不是「调一下就行」。
误解七:用 BFloat16 做 PE 不会有精度问题
会。BFloat16 的尾数精度只有 7 bits,对应大约 0.008 的相对误差。当 ω 很小(低频维度)、pos 很大(长上下文)时,pos × ω 这个角度乘积容易在 BFloat16 里产生 0.001 量级的舍入,从而让远端位置的 PE 出现可观察的偏差。生产实现里通常用 Float32 算 PE,再 cast 到 BFloat16 与 word embedding 相加。这条细节在 LLaMA 实现的 RoPE 里也专门处理过。
误解八:相加和拼接对 attention 等价
不等价。相加让 PE 与 word embedding 共享同一组 W_Q、W_K,attention 里 (W_Q (e + p)) · (W_K (e + p))ᵀ 展开后会产生 e·e、e·p、p·e、p·p 四项,模型可以同时利用语义—语义、语义—位置、位置—位置三种交互。拼接强行把语义和位置切到不同子空间,限制了交互形式。这件事 Ke et al. 2021 的 TUPE 论文里有完整论证。
十三、下一步
下一篇 22|Encoder 详解 会把视角从「单点输入」拉到「6 层堆叠的 encoder」,讲清楚 multi-head self-attention 与 FFN 是怎么交替组合,post-LN 与 pre-LN 各自的取舍,以及 encoder 输出最终交给 decoder 时承载了什么。
再往后 23|Decoder 详解 会讲 masked self-attention 与 cross-attention 的双块结构,以及 decoder 在训练(teacher forcing)与推理(自回归)时的行为差异。
24|残差连接 与 25|LayerNorm 会回答「为什么深层 Transformer 训得动」这个被 21 至 23 反复回避的问题。
第 41 篇 RoPE / ALiBi 是 21 的现代续集;如果你今天读完这一篇之后产生了「sin/cos 设计太特殊了能否用更优雅的方式」之类的疑问,41 会给你完整答案。
实际上,你可能会发现一件有趣的事:从 21 直接跳到 41 阅读,再回到 22 / 23 看 encoder / decoder 详解,整个体系会以另一种顺序展开。前者是「位置 → 现代位置」的纵向深入,后者是「encoder → decoder」的横向铺开。这两种顺序都成立,取决于你目前更想理解「位置编码本身的演化」还是「Transformer 的整体结构」。这套系列设计上是允许这种跳转的,目录里的依赖箭头不是强约束,而是默认推荐。
如果你想动手验证:实现 sinusoidal_pe,画 (512, 64) 的热力图;再把 d_model = 64 的 PE 逐位差画出 norm 距离矩阵;最后把训练长度限制 32、推理长度 64 的小 Transformer 跑一遍——你会看到外推时困惑度的真实曲线。这套实验是理解位置编码最快的路径。
更进一步的练习:把同一个小 Transformer 的位置编码替换成可学习版本,重新训练;再换成 ALiBi;再换成 NoPE。把四个版本在「训练长度 32、推理长度 64/128/256」上的困惑度画在同一张图里,你会得到一张「位置编码外推全景图」——你对位置编码的理解会在这一张图后立刻完成。这个练习不复杂,一台单卡机器一个下午就能跑完,但建立的直觉值好几本书。
读完 21 之后再回去重读 13、14、15、16,会发现那几篇里那些「Q、K、V 都来自同一序列」「self-attention 是位置无关的」之类的论断有了完全不同的层次:你现在知道「位置无关」不是一句修辞,是排列等变的精确数学事实;你现在知道 attention 公式 softmax(QKᵀ/√d_k)V 在结构上为什么必须配合一个外部位置注入机制。这种「读完后篇再回看前篇会看到不同东西」的体验,是深度博客系列最值得保留的一种价值。
十四、参考文献
下面按相关度排序,列出本篇直接引用与延伸阅读,每条附一句话提示其角色。
- Vaswani, A. et al. “Attention Is All You Need.” NeurIPS 2017. §3.5 Positional Encoding 是本篇的主轴。
- Gehring, J. et al. “Convolutional Sequence to Sequence Learning.” ICML 2017. ConvSeq2Seq 中的 learned positional embedding 与 concat 设计,是 Vaswani 选择 sinusoidal 时的直接对照。
- Devlin, J. et al. “BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding.” NAACL 2019. 可学习位置编码(max_len = 512)的代表实现。
- Radford, A. et al. “Language Models are Unsupervised Multitask Learners.” OpenAI Technical Report, 2019. GPT-2 的可学习位置编码(max_len = 1024)。
- Shaw, P., Uszkoreit, J., Vaswani, A. “Self-Attention with Relative Position Representations.” NAACL 2018. 相对位置编码的早期奠基工作。
- Raffel, C. et al. “Exploring the Limits of Transfer Learning with a Unified Text-to-Text Transformer.” JMLR 2020. T5 的 relative position bias 设计。
- Su, J. et al. “RoFormer: Enhanced Transformer with Rotary Position Embedding.” arXiv:2104.09864, 2021. RoPE 提出。
- Press, O., Smith, N. A., Lewis, M. “Train Short, Test Long: Attention with Linear Biases Enables Input Length Extrapolation.” ICLR 2022. ALiBi 提出,并系统比较 sinusoidal、可学习位置的外推表现。
- Chen, S. et al. “Extending Context Window of Large Language Models via Positional Interpolation.” arXiv:2306.15595, 2023. 在 RoPE 上的位置插值方法。
- Wang, B., Zhao, L., et al. “On Position Embeddings in BERT.” ICLR 2021. 系统分析 BERT 各类位置编码的实证差别。
- Dosovitskiy, A. et al. “An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale.” ICLR 2021. ViT 中可学习位置编码在视觉任务上的应用。
- Liu, X. et al. “RoPE Scaling for Long-Context LLMs.” NeurIPS 2023 Workshop. RoPE 基数从 10000 扩到 1e6 的工程实践。
- Ke, G., He, D., Liu, T.-Y. “Rethinking Positional Encoding in Language Pre-training.” ICLR 2021. TUPE,再次反思绝对位置编码的若干默认假设。
- Bahdanau, D., Cho, K., Bengio, Y. “Neural Machine Translation by Jointly Learning to Align and Translate.” ICLR 2015. Bahdanau attention 的位置感来自 RNN 的递归结构,是 Transformer 之前的对照。
- Gulati, A. et al. “Conformer: Convolution-augmented Transformer for Speech Recognition.” Interspeech 2020. 卷积带来的局部位置感与 attention 互补,是位置编码主题的延伸。
- Haviv, A. et al. “Transformer Language Models without Positional Encodings Still Learn Positional Information.” Findings of EMNLP 2022. NoPE 现象的代表论文。
- Kazemnejad, A. et al. “The Impact of Positional Encoding on Length Generalization in Transformers.” NeurIPS 2023. 系统比较多种 PE 方案在外推上的表现。
- Peng, B. et al. “YaRN: Efficient Context Window Extension of Large Language Models.” ICLR 2024. RoPE 上下文扩展的代表方法。
- Tay, Y. et al. “Long Range Arena: A Benchmark for Efficient Transformers.” ICLR 2021. 长上下文 benchmark,间接评估各类 PE 的表达力。
- Liu, X. et al. “FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioning.” ICLR 2024. 与 PE 实现的精度处理直接相关。
← 上一篇:20|Transformer 整体架构 | 下一篇:22|Encoder 详解 →
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【Transformer 与注意力机制】03 矩阵乘法的两种视角
把矩阵乘法掰开成两种等价但风格不同的视角——『行 × 列』的点积视角和『列的线性组合』视角,最终落到 QK^T 的形状分析。
【Transformer 与注意力机制】01|为什么要从这里开始
这是【Transformer 与注意力机制】系列的第一篇,承担两件事:一是把这套五十多篇文章为谁写、解决什么问题、彼此之间是什么关系交代清楚;二是为完全没基础的读者画出一条从向量、点积、矩阵乘法走到自注意力、再走到大语言模型的爬升路径,让你在投入时间之前先知道终点在哪、路上要经过哪些坎、读完之后你会、还不会做什么事。
【Transformer 与注意力机制】系列总览
从《Attention Is All You Need》出发,把注意力机制、Transformer 架构、训练范式、模型变体、推理工程、可解释性与未来架构串成一条 58 篇的深度博客线。
【Transformer 与注意力机制】10 RNN 的根本局限:为什么需要 Transformer
RNN 三难(长程依赖、梯度稳定、训练并行)的系统分析;attention 如何作为补丁逐步把 RNN 推向极限;Vaswani 2017 抛弃循环的范式革命