阶段 3 · 处理序列

位置编码:注意力天生
不知道谁先谁后

上一章的自注意力有一个惊人的性质:把输入词的顺序全部打乱,每个词的输出内容一模一样,只是换了个位置。 因为它的核心是"两两点积",而点积不关心顺序。 这意味着"猫追狗"和"狗追猫"在模型眼里是同一句话——除非你把位置信息硬塞进去。

1

为什么必须显式加位置

这个性质有个术语叫"置换不变性"(permutation invariance):顺序随便打乱,每个词拿到的输出内容都不变。

互动 · 打乱输入顺序,看注意力输出变不变

原顺序

打乱顺序

💡 关键观察

想一下你看到的那两排格子:打乱之后,每个词拿到的输出向量完全没变—— 模型根本没有"第几个词"这个概念。
而语言里词序几乎决定一切:"我不喜欢" ≠ "喜欢我不"。 所以位置信息必须从外面注入。

2

五条路线:位置从哪里塞进去

方案怎么注入一句话特点
正弦编码
2017
把不同波长的 sin/cos 加到词向量上(波长 = 隔几个词重复一次) 不用学参数;位置和语义挤在同一串数里
可学习编码
BERT/GPT-2
每个位置一个可训练向量,直接加上去 简单、效果好;长度写死在训练范围里
RoPE
2021 → 主流
按位置旋转 Q、K 不碰词向量,但得改注意力内部的 Q、K;点积自动只看位置差(第 3 节)
ALiBi
2021
不给位置编码,改成给注意力分数减一个随距离变大的数 只表达"越远越不看"这一种偏好,实现极简
NoPE
2023
什么都不加,靠因果掩码自己隐含的一点顺序 因果 LM(从左到右预测下一个词)上不差;双向(能同时看前后文的那类模型,如 BERT)任务明显更弱

示意 · 四种把位置塞进去的写法(NoPE 什么都不加,所以没画)

① 加(正弦) 词向量 + 位置向量 = 同一串数 位置和语义混在一起 ② 查表(可学习) 1 2 3 取第 2 行 再加到词向量上 ③ 旋(RoPE) 只转 Q、K 长度一点没变 ④ 罚(ALiBi) 行、列 = 两个词 越远减得越多
💡 今天的默认答案

大多数现代大模型都用 RoPE——LLaMA、Qwen、DeepSeek、Mistral 这些都在用(BLOOM 用的是 ALiBi,是少数派)。 它被选中是因为三件事:点积只依赖相对位置、不碰词向量、 需要更长的上下文时有成熟的扩展方法(第 4 节讲)。

3

RoPE 到底在做什么:旋转

RoPE 的想法非常几何。把词向量的每两维看成平面上的一个点, 然后按位置把它旋转一个角度——位置越大,转得越多。

互动 · 真实的二维旋转

🎯 类比

像钟表上的指针。位置 = 时刻,向量 = 指针的初始方向。 第 3 个词是"3 点钟方向",第 7 个词是"7 点钟方向"。
关键性质是:两个指针的夹角只取决于它们的时刻差(7 − 3 = 4), 而和现在几点无关。这正是"注意力只需要知道相对距离"所需要的性质。

💡 为什么旋转能得到"相对位置"

设两个向量各旋转 θm 和 θn,然后做点积。 二维旋转的点积结果里,只出现角度差 (θm − θn)——绝对值被消掉了。
而 θ 和位置成正比,所以角度差 ∝ 位置差。 于是:注意力分数自动只依赖两个词的相对距离,这正是我们想要的。

4

训练 4K,怎么用到 128K

模型在 4096 个词上训练,你想让它读 128K 的文档——这叫外推(extrapolation):把模型用到比训练时更长的句子上。 先记住一个词:困惑度(perplexity)——模型预测下一个词时平均在几个候选之间犹豫,越小越好。 长度一超,困惑度就会崩。为什么?

互动 · 外推失败的直观图(示意,不是实测值)

互动 · 为什么一到训练长度就撞墙

方法怎么做代价
直接外推什么都不做 超出训练长度后立刻崩坏,困惑度暴涨
位置插值 PI把 128K 的位置压缩回 0~4K 的范围 短文本性能下降(位置被挤密了)
NTK 感知插值不整体压缩,而是按频率分别处理:转得快的那几对几乎不动,转得慢的那几对多压缩 需要调一个缩放因子
YaRNNTK 插值 + 注意力温度缩放(把注意力分数整体调尖或调平),分频段处理 目前长上下文扩展的主流方案,128K 很稳
滑动窗口注意力只让每个位置看最近 N 个词(N 是固定的窗口大小) 单层只能看最近 N 个词(远距离靠叠层间接传),降低推理成本(只看最近一段,算得少、存得少)
继续训练用长文本继续训练一段 最有效,但要数据和算力
⚠️ 一个必须知道的真相

"支持 128K 上下文"和"真的能有效利用 128K"是两件事。 2023 年那篇《Lost in the Middle》发现: 信息放在上下文中间时,模型的表现会明显下降—— 它其实最擅长用开头和结尾的信息。
所以工程上有个经验:最重要的内容放开头或结尾,别放中间。

M

数学 · 转两下,顺序自己就出来了

钟表类比说完了。下面把它变成一个能拖的东西—— 拖完你会看见一句话:两个词挪到多后面都不重要,只要它们的距离没变,注意力分数就一模一样。

互动 · 两根箭头,一个点积,一条平线

公式卡 · 上面那句话写成数学

三个时刻,夹角一样 → 点积一样

θ 是每一行的转速:低维快、高维慢

到这里收个口:每个词的方向由它的绝对位置决定——位置越大,q、k 转得越多。 但两个词做点积时,两边的绝对位置被约掉,只剩下位置差。「你离我多远」才是真正进了注意力的那个量。

互动 · 为什么旋转不会把意思弄脏

|v|0.877旋转后不变 |v + p|1.296加法后会变

🎬 自己验一遍

把两个位置滑块一起往前挪 5 格(或连点五次「两个一起 +1」)。
左边两根箭头明显转到了别的地方,右边那条橙色平线却一动没动—— 公式里没有 m,也没有 n,只有 (m − n),这就是全部内容。

5

长上下文的三个隐患

隐患说明什么时候真的会痛 → 换什么
① 外推是"借来的"能力 位置编码和训练长度绑死,超出就成了分布外(训练时没见过的样子)。 大多数扩展方法都是在事后打补丁,不是结构上的保证。 要用的长度超过训练长度 → 用 YaRN 这类插值再少量继续训练(第 4 节)
② 长上下文的成本是平方的 128K 的注意力矩阵是 4K 的 1024 倍,显存和算力都按这个倍数涨。 用不到那么长就别开那么长;要长就用省显存的注意力实现(FlashAttention 一类,阶段 6)
③ 长度开了,模型不一定用得上 《Lost in the Middle》(2023)发现:信息放在上下文中间时,模型的表现明显下降。 长文里关键信息在中间 → 调排布(放到开头或结尾),别指望它自己找到
6

小结

上一章(《Transformer 自注意力》)在“它牺牲了什么”那一行写过一句: “位置信息要自己加”。 这一章就是那张账单的逐条明细。

它对应哪条线 ② 没有免费午餐 → 必须有归纳偏置——但这一章站在账单那一面: 它不问你“选哪条偏置”,它告诉你你可以选,但不能不选。
一句话 这一章的做法,本质上是在偿还一笔删掉先验留下的欠款: 注意力为了换来并行和自由度,把“词有先后”这条先验删得干干净净; 删掉的东西不会消失,只会变成一行必须手工加上去的代码。 而这一章的全部技术含量在于:用什么方式把位置加回去代价最小—— 加法把位置和语义挤进同一串数,旋转不碰长度、只改方向。
它牺牲了什么 牺牲了长度的自由:位置编码一旦和训练长度绑死,超出这个长度就成了分布外(训练时没见过的样子), 128K 只能靠事后打补丁(回扣暗线「什么被牺牲了」「它假设了什么」)。 好消息是这笔账很便宜:正弦编码 / RoPE / ALiBi 都是 0 参数—— 与它解锁的长上下文账单(n² 的注意力)相比,位置编码自己的成本可以忽略
💡 检验一下:你现在能指着哪个互动说这句话

回到第 1 节,点一次「🔀 再打乱一次」——同一个词的输出一模一样、只是位置换了,这就是欠款本身; 再回到第 3 节把「Q 的位置」从 0 拖到 32、K 不动,Q·K 只跟着差值变,这就是还款方式:位置不是加进向量的,是旋进去的。

它在暗线里站在哪

暗线这一章的回答
信息流动 数据形状:这一章最特别的地方就是形状一点没变。 可学习位置编码会多出一张 [L, d] 的参数表 (L 个位置、每个位置 d 维,一个位置一行); 而 RoPE 是原地旋转 Q、K——它不增维、不改形状、不加参数, 只是把每两维当成一个平面转一个角度。
三种注入方式落在三个不同的地方: 加法(正弦、可学习)把位置写进向量里; 注意力里减距离(ALiBi)把位置写进分数矩阵; 旋转(RoPE)把位置写进向量之间的关系。
什么被牺牲了 牺牲了“位置是免费先验”的幻觉和长度的自由度。 换回来的是一个很难替代的性质:注意力分数自动只依赖相对距离, 而且不需要一张随长度增长的参数表——这正是 128K 乃至 1M 上下文能在同一个模型里做到的前提。
但得诚实:外推能力是“借来的”,不是结构保证, 它是事后用 NTK 插值 / YaRN 打补丁换的
参数账本 位置编码本身就一个惊人的数字:正弦 0 个参数,RoPE 0 个参数,ALiBi 0 个参数。
作为对照,可学习编码是 [L, d] 一张表: GPT-2 是 1024 × 768 = 78.6 万参数(占 1.24 亿总参数的 0.6%)。
真正的大账在它解锁的那一头:n = 128K 时,注意力分数矩阵有 (128×1024)² ≈ 1.7 × 10¹⁰ 个元素(fp16 半精度、每个数 2 字节,存下来约 34 GB)。 也就是说,那个 0 参数的旋转决定你能不能吃下这份 34 GB 的账(阶段 6 的 FlashAttention / KV cache 就是为它而生)
它假设了什么 三条,一条比一条具体:
① “只有相对距离重要,绝对位置不重要”——RoPE 的点积里 m−n 出现、m 和 n 自己不出现。对语言大体成立, 但遇到“请你数到第 100 个字”这类需求就弱了。
② “位置和语义可以分离”——旋转不改变向量长度, 所以它在注入位置时不扰动“这个词有多强”这个量;加法做不到这一点。 这就是暗线「它假设了什么」在代码级别的一个实例。
③ ALiBi / 滑窗额外假设“越远越不重要”——这是一条很强的语言假设, 而开头的例子、需回溯的人名恰恰常常不成立
违背了哪个直觉 “打乱输入顺序,输出居然不变”——这是全章最反直觉的起点。 一个看起来在“理解语言”的模块,其实对顺序完全不敏感。
第二个更反直觉:什么都不加(NoPE)在一些实验里外推也不差—— 原因是因果掩码本身就隐含了一点顺序信息,多做的那一步反而带来了新的分布外。 “加更多信息”不等于“更好”。
🎯 前后钩子

它接住了上一章的什么:《Transformer 自注意力》 用“任意两个位置都能直接对视”换掉了循环的串行性,代价是丢掉了顺序感。 这一章把丢掉的顺序用一个 0 参数的操作找了回来。

它给下一章留了什么:现在“模型长什么样”这一块已经齐了—— 注意力、位置、前馈、残差。接下来的问题是: 这套结构一口气读完整个互联网之后,到底学会了什么?能不能被一个统一范式描述? 这就是《预训练语言模型》要回答的问题。

一句话带走位置编码

注意力是置换不变的——它天生不知道谁先谁后(第 1 节那两排格子)。
RoPE 用"按位置旋转"这个几何操作,让点积自动只依赖相对距离,是今天的默认答案。
"支持 128K" ≠ "用好 128K":外推仍然靠事后补丁, 而且信息放在中间时模型容易忽略——这是工程上必须知道的坑。

7

拓展阅读

上面讲的都是「够用」的版本。想往下挖,这里有三个入口—— 它们不是必修内容,是给想再往前走一步的读者准备的。

📄 这一章的说法从哪来

💻 工业界怎么写

∑ 更严格的形式