大模型中的位置编码
大模型中的位置编码
“我爱着她”,就一定等于“她爱着我”吗?
对语言模型来说,知道一句话里出现了哪些词还不够,它还需要知道这些词按照什么顺序排列。 Transformer 中每一个token的注意力计算与它所在的位置无关。位置编码要做的,正是把这种信息放进模型。
随着大模型不断发展,位置编码的形式也在不断变化。这篇笔记就整理一下位置编码的知识叭🤩
1. 自注意力中的位置信息
2017年,有人说,Attention is all you need:
这里的 \(Q\)、\(K\)、\(V\) 分别是输入经过线性变换得到的查询、键和值,\(d_h\) 是一个注意力头里 Q、K 的维度,\(M\) 是掩码:允许关注的位置通常加 \(0\),禁止关注的位置加 \(-\infty\)。
把其中两个位置单独拿出来看,最核心的打分公式就是在比较两个位置token的q,k相似度:
很多地方提到的一种说法是,注意力计算没有引入相对位置关系,假如输入换个顺序,输出也跟着换个顺序,计算规则并没有因此理解新的排列关系,所以需要位置编码。
但是我今天又仔细想了想,发现这样说并不准确。像 GPT 这类自回归模型,是要加上因果注意力掩码的,也就是说前面的 token 看不到后面的 token,这本身好像也是一种前后位置关系的表示。所以,注意力计算不引入位置信息,但因果掩码本身带有方向信息。😃👆
更准确的说法是,单靠内容匹配,模型缺少直接、可控地表达位置和距离的机制。因果掩码提供了前后约束,位置编码让位置与距离成为注意力可以直接利用的信息,减少模型从前缀结构中自行推断的负担。
2. 绝对位置编码
最直接的方案是,给第 \(m\) 个位置准备一个向量 \(p_m\),然后加到 token embedding 上:
\(e_m\) 表示 token 的内容,\(p_m\) 表示它的位置,两者维度相同。上式省略了某些模型里的 embedding 缩放等细节。原始 Transformer 就是在输入端补充位置编码。参见 Attention Is All You Need,第 3.5 节。
后来绝对位置编码演变出了两条路线,直接学习一张位置表和用正余弦公式生成位置向量。
2.1 可学习的位置向量
假设模型支持的最大位置数是 \(L_{\max}\),隐藏维度是 \(d_{\text{model}}\),就建立一张参数表:
位置 \(0\) 查第 \(0\) 行,位置 \(1\) 查第 \(1\) 行。训练过程中,位置向量和模型其他参数一起更新。BERT 官方实现和 GPT-2 都采用过这种方案;GPT-2 的实现里可以看到 token embedding wte 和位置 embedding wpe 相加。参见 GPT-2 官方模型代码。
它的优点是灵活:每个位置应该长什么样,由训练数据决定。
问题也出在这里:位置编码依赖于训练数据,如果实际遇到了比训练数据更长的序列,模型就蒙了:妹学过呀。
2.2 正余弦位置编码与多频率表示
原始 Transformer 使用的固定正余弦编码是:
这里假设 \(d_{\text{model}}\) 为偶数,\(i=0,\ldots,d_{\text{model}}/2-1\)。相邻两个维度是一组,使用相同频率,一个取正弦,一个取余弦。
正余弦编码则把位置变成了多组有界的周期信号:有的变化快,有的变化慢。真的很妙。

图 1:按公式绘制,位置范围为 0~63,编码维度为 64。左图每行是一个位置向量;右图展示几个偶数维度。高频维度变化快,低频维度在这段范围内变化慢。
这套设计还有一个很漂亮的性质。对于固定偏移 \(\Delta\),三角恒等式告诉我们:
也就是说,从位置 \(m\) 的一对正余弦值出发,可以通过一个只依赖 \(\Delta\) 的线性变换,得到位置 \(m+\Delta\) 的那一对值。它虽然按绝对位置生成,却提供了学习相对偏移的结构。
3. 从绝对位置转向相对位置
还有一种更直接的设计:让注意力使用两个位置之间的相对关系。
与绝对位置编码相比,相对位置方案把“关系”更直接地写进了计算过程。
下面不太严谨地从绝对位置编码推导一下:
设 \(q_m=W_Q(e_m+p_m)\)、\(k_n=W_K(e_n+p_n)\),记 \(A=W_Q^\top W_K\),那么未缩放的内积可以展开为:
里面同时存在内容—内容、内容—位置、位置—内容、位置—位置四种交互。后续一些方法正是围绕“哪些交互需要保留,怎样表达更合适”展开的。
4. 相对位置编码
4.1 Shaw:用向量表示相对距离
Shaw 等人在 2018 年提出,把相对位移映射成向量,并放进注意力计算。只看它对 key 的修改,可以写成:
\(r_\delta\) 是相对位移 \(\delta\) 对应的向量,\(K\) 是截断范围。比如距离超过 \(K\) 的右侧位置,可以共用最远那一档向量,避免为每种距离都创建参数。相对位移的正负号也区分了方向。
同样是“在我右边两格”,不管当前 token 位于第 \(5\) 还是第 \(50\) 个位置,都可以使用同一个相对位置向量。这就是参数共享带来的结构约束。原论文还讨论了对 value 加入相对表示;上式只抽出了打分部分。参见 Self-Attention with Relative Position Representations。
这种方法增加了相对向量参与的运算。朴素实现如果为所有 token 对展开向量,可能产生 \(T\times T\times d_h\) 的中间张量;通过重排运算、索引和共享可以降低额外开销。
4.2 Transformer-XL:跨片段记忆中的相对位置
Transformer-XL 不只研究位置编码,它还引入了段级递归:处理当前片段时,可以使用之前片段缓存的隐藏状态。
假如每个片段都从位置 \(0\) 重新编号,直接复用带有片段内绝对位置的旧状态,就会遇到位置参照不一致的问题。相对距离则更适合描述“当前查询距离过去的状态有多远”。
Transformer-XL 对注意力打分重新参数化,使内容匹配、内容与相对位置的交互,以及全局偏置分别发挥作用。它在Shaw 的工作基础上,围绕跨段记忆设计了相对位置机制。参见 Transformer-XL,第 3.3 节。
4.3 T5:相对位置分桶与注意力偏置
T5 采用了更轻量的思路:不为相对距离提供完整向量,而是给每个注意力头加一个相对位置偏置:
这里的 \(b\) 是可学习的标量,\(h\) 表示注意力头。bucket 的意思是“分桶”:近距离分得细一些,远距离分得粗一些,足够远的距离可以合并。双向注意力还需要区分左右方向。
直觉上,我们可能很在意“紧挨着”和“隔了两个 token”的差异,却没必要为“相隔 1000”和“相隔 1001”各学一套精确的位置参数。
原始 T5 为每个头学习有限个偏置,同一套偏置参数跨层共享。注意,分桶规则是预先设计的,可学习的是每个桶里的偏置值。参见 T5 论文的位置编码说明。
4.4 DeBERTa:内容与位置的解耦建模
DeBERTa 的关键词是“解耦注意力”。它分别表示内容和相对位置,然后显式计算不同类型的交互。
可以把原论文中保留的主要部分概括为:
比如,“当前这个内容倾向于关注左边哪些位置”,和“某种相对位置上出现的内容是否值得关注”,并不完全是同一个问题。它们可以通过不同投影来学习。
原始实现省略了位置—位置项,但也没有把所有绝对位置信息彻底抛弃,而是在掩码预测的解码部分使用了绝对位置信息。参见 DeBERTa 原论文。
5. RoPE:通过旋转表达相对位置
RoPE,全称 Rotary Position Embedding,来自 RoFormer。它的想法非常简洁优雅:给 Q、K 按各自位置做旋转,让内积里的位置因素只依赖相对位移。
标准做法是在每层注意力中,先投影出 Q、K,再对它们应用旋转,通常不旋转 V。参见 RoFormer 原论文。
5.1 二维旋转与相对位置关系
二维空间中,旋转角度 \(\alpha\) 对应的矩阵是:
让位置 \(m\) 的 query 旋转 \(m\omega\),位置 \(n\) 的 key 旋转 \(n\omega\):
两者内积为:
第二步只用了旋转的两个性质:转置相当于反向旋转,连续旋转的角度可以相加。原来的 \(m\) 和 \(n\),就这样组合成了 \(n-m\)。
这里“只依赖相对位移”指的是旋转引入的位置关系。内积当然还依赖 \(q_m\)、\(k_n\) 的内容,而且深层网络中的内容向量已经包含上下文。
5.2 高维向量中的分组旋转
对于偶数维的注意力头,可以把 \(d_h\) 个维度分成 \(d_h/2\) 组,每组是一个二维平面。第 \(i\) 组使用:
经典设置使用 \(b=10000\),具体模型可以采用其他基数,也可能只对部分维度旋转。这里的维度通常是单个头的旋转维度,往往不是模型的隐藏层维度 \(d_{\text{model}}\)。旋转还会保持向量长度不变。不过,保持长度不等于保持 Q、K 的内积:两者旋转的角度不同,内积恰好会因此受到相对位置影响。
5.3 RoPE 的最小实现
下面使用“相邻维度配成一对”的写法,输入形状约定为 [batch, heads, seq_len, head_dim]。同一批样本共用一组位置索引,且整个 head 都参与旋转:
import torch
def apply_rope(x, position_ids, base=10000.0):
# x: [B, H, T, D];position_ids: [T]
assert x.ndim == 4 and x.shape[-1] % 2 == 0
assert position_ids.ndim == 1
assert position_ids.numel() == x.shape[-2]
dim = x.shape[-1]
# 用 float32 计算角度,随后把三角函数值转回输入精度。
inv_freq = base ** (
-torch.arange(0, dim, 2, device=x.device, dtype=torch.float32) / dim
)
positions = position_ids.to(device=x.device, dtype=torch.float32)
angles = positions[:, None] * inv_freq[None, :] # [T, D/2]
cos = angles.cos().to(x.dtype)[None, None, :, :]
sin = angles.sin().to(x.dtype)[None, None, :, :]
even, odd = x[..., 0::2], x[..., 1::2]
return torch.stack(
(even * cos - odd * sin, even * sin + odd * cos), dim=-1
).flatten(-2)
torch.manual_seed(0)
q = torch.randn(1, 2, 4, 8)
k = torch.randn(1, 2, 4, 8)
positions = torch.arange(4)
q_rot = apply_rope(q, positions)
k_rot = apply_rope(k, positions)
scores = q_rot @ k_rot.transpose(-1, -2) / q.shape[-1] ** 0.5
print(scores.shape) # torch.Size([1, 2, 4, 4])
这里输出的是分数,还没有应用因果掩码和 softmax。实现中也没有构造完整的旋转矩阵,只用了逐元素乘法、加法与重新排列。
6. ALiBi:更加简单粗暴
最开始看到这个名字,还以为是阿里巴巴的团队提出的算法🤣。实际上它的全称是Attention with Linear Biases。
ALiBi 的做法更直接:不添加输入位置向量,也不旋转 Q、K,而是在注意力分数上加入与距离成正比的惩罚,简单到我觉得自己也能想出来(?)。
对只能看当前及过去位置的因果注意力,设 \(n\le m\):
\(a_h>0\) 是第 \(h\) 个头的斜率,原始 ALiBi 为不同头指定不同的固定斜率,不通过训练学习它们。未来位置仍然要通过因果掩码禁止访问。
用 softmax 的指数形式看,这个扣分相当于给未归一化权重乘上:
假设两个 key 的内容匹配分数相同,斜率为 \(0.25\),其中一个比另一个多远 \(4\) 个位置,它的注意力权重就是另一个的 \(e^{-1}\approx0.368\) 倍。这里比较的是同一行 softmax 内的两个权重,因此归一化分母会抵消。
斜率大的头更偏向近处,斜率小的头对远处更宽容。不过,这只是位置偏好;最终注意力还要结合内容分数,远处的重要信息仍可能被关注。参见 Train Short, Test Long: Attention with Linear Biases。
ALiBi 的公式可以用于更大的距离,也不需要为新位置增加可学习表。
7. RoPE的长上下文扩展
7.1 超出训练长度后的泛化问题
假设模型主要在长度 \(4096\) 内训练,推理时突然收到 \(32768\) 个 token。虽然三角函数仍然可以计算,但模型会遇到大量训练中没有充分见过的相位组合、相对距离和注意力竞争关系。
7.2 位置插值:调整位置索引的尺度
Position Interpolation,简称 PI,提供了一个很直观的办法。设原训练长度为 \(L\),希望扩展到大约 \(sL\),可以把位置变为:
然后用 \(m'\) 计算 RoPE 的旋转角度。比如从 \(4096\) 扩到 \(32768\),取 \(s=8\),新序列的末尾会被映射到接近原来的位置范围。
这像是在同一把尺子上塞进更多刻度。代价是相邻 token 的角度间隔也缩小了,原有的局部距离结构发生变化。因此,插值通常需要结合适配训练。参见 Position Interpolation 原论文。
7.3 不同频率的缩放与 YaRN
统一压缩所有频率容易影响局部位置分辨。后续方法因此研究:哪些频率适合插值,哪些频率应该尽量保留,以及是否需要调整注意力分数的尺度。
YaRN 就沿着这个方向,把不同频率范围的缩放与注意力尺度调整结合起来。这里先记一下它解决的问题:在扩大位置覆盖范围时,尽量兼顾模型原有的距离结构。 具体公式和训练配置值得另开一篇文章讨论。参见 YaRN 原论文。
7.4 KV cache 与位置索引的一致性
KV cache 会保存已处理 token 的 K、V,生成下一个 token 时直接复用,避免重复计算。对标准 RoPE 推理来说,还要注意:缓存中的 K 已经按原位置旋转过,新 token 的 Q、K 必须沿用同一套位置坐标。这一点对于大模型推理系统是很重要的。常见问题有三类。
-
接着生成时,位置不能从头算。 如果已经缓存了前 \(100\) 个 token,位置是 \(0\sim99\),下一个 token 就应使用 \(100\),即使这次只输入它一个。它与位置 \(99\) 的 token 本来只差 \(1\);若误用位置 \(0\),RoPE 计算出的相对位移就会改变,注意力分数也会跟着出错。
-
移动缓存,不等于改变逻辑位置。 比如滑动窗口只保留位置 \(80\sim99\) 的 K、V,它们虽然移到了缓存的第 \(0\sim19\) 格,仍然带着原位置的旋转。新 token 可以继续使用位置 \(100\),不能直接拿缓存长度 \(20\) 当作它的位置。如果要把整段重新编号,就需要同步调整缓存中 K 对应的旋转;只修改编号记录是不够的。
-
有 padding 时,列号不一定就是位置。 例如
[PAD, PAD, A, B]中,A、B 可以使用逻辑位置 \(0、1\),下一个 token 接着用 \(2\)。也可以统一加一个偏移,但缓存和后续 token 必须保持同样的约定。若按有效 token 编号,批内样本长度不同时,后续位置也应分别按各自的有效长度续接。
8. 从技术演进到模型实现
| 时间 | 代表工作 | 主要进展 |
|---|---|---|
| 2017 | 原始 Transformer | 给输入加入正余弦位置编码,也比较了可学习位置向量 |
| 2018 | Shaw 等人的相对表示 | 用相对距离向量直接参与注意力 |
| 2019 | Transformer-XL | 将相对位置机制用于跨片段状态复用 |
| 2019 / 2020 | T5 | 用分桶标量偏置表达距离,简化位置建模 |
| 2020 | DeBERTa | 分开表示内容和位置,再建模它们的交互 |
| 2021 | RoPE | 利用旋转内积,自然得到相对位移结构 |
| 2021 | ALiBi | 用固定线性距离偏置探索长度外推 |
| 2023 | PI、YaRN | 在已有 RoPE 模型上研究上下文扩展 |
这几条路线并不是严格的“后一代淘汰前一代”。它们强调的目标不同:表达能力、计算方式、参数共享、推理效率和长度泛化,都可能影响选择。
位置编码研究的重心,逐渐从“怎样标记一个位置”转向“怎样让注意力更好地表达位置关系”,又进一步转向“怎样把训练中学到的位置关系迁移到更长的文本”。感觉这其中还是大有学问的,值得持续关注和学习~
参考资料与延伸阅读
正文中的链接对应具体论点;下面按阅读用途集中列出,方便继续深入。
- Attention Is All You Need(2017):从输入相加和正余弦编码读起。
- Self-Attention with Relative Position Representations(2018):理解相对向量如何进入注意力。
- Transformer-XL(2019):结合跨段记忆理解相对位置的必要性。
- T5(2019 预印本 / 2020 JMLR):阅读相对位置分桶和标量偏置设计。
- DeBERTa(2020 预印本):阅读内容与位置的解耦注意力。
- RoFormer / RoPE(2021 预印本):重点看旋转推导和实现;可对照 作者团队代码。
- ALiBi(2021 预印本 / ICLR 2022):理解线性距离偏置及其外推实验。
- Position Interpolation(2023)、YaRN(2023 预印本):继续阅读 RoPE 模型的上下文扩展。
浙公网安备 33010602011771号