大模型中的位置编码

大模型中的位置编码

“我爱着她”,就一定等于“她爱着我”吗?

对语言模型来说,知道一句话里出现了哪些词还不够,它还需要知道这些词按照什么顺序排列。 Transformer 中每一个token的注意力计算与它所在的位置无关。位置编码要做的,正是把这种信息放进模型。

随着大模型不断发展,位置编码的形式也在不断变化。这篇笔记就整理一下位置编码的知识叭🤩

1. 自注意力中的位置信息

2017年,有人说,Attention is all you need:

\[\operatorname{Attention}(Q,K,V) =\operatorname{softmax}\left(\frac{QK^\top}{\sqrt{d_h}}+M\right)V \]

这里的 \(Q\)、\(K\)、\(V\) 分别是输入经过线性变换得到的查询、键和值,\(d_h\) 是一个注意力头里 Q、K 的维度,\(M\) 是掩码:允许关注的位置通常加 \(0\),禁止关注的位置加 \(-\infty\)。

把其中两个位置单独拿出来看,最核心的打分公式就是在比较两个位置token的q,k相似度:

\[s_{m,n}=\frac{q_m^\top k_n}{\sqrt{d_h}} \]

很多地方提到的一种说法是,注意力计算没有引入相对位置关系,假如输入换个顺序,输出也跟着换个顺序,计算规则并没有因此理解新的排列关系,所以需要位置编码。

但是我今天又仔细想了想,发现这样说并不准确。像 GPT 这类自回归模型,是要加上因果注意力掩码的,也就是说前面的 token 看不到后面的 token,这本身好像也是一种前后位置关系的表示。所以,注意力计算不引入位置信息,但因果掩码本身带有方向信息。😃👆

更准确的说法是,单靠内容匹配,模型缺少直接、可控地表达位置和距离的机制。因果掩码提供了前后约束,位置编码让位置与距离成为注意力可以直接利用的信息,减少模型从前缀结构中自行推断的负担。

2. 绝对位置编码

最直接的方案是,给第 \(m\) 个位置准备一个向量 \(p_m\),然后加到 token embedding 上:

\[h_m=e_m+p_m \]

\(e_m\) 表示 token 的内容,\(p_m\) 表示它的位置,两者维度相同。上式省略了某些模型里的 embedding 缩放等细节。原始 Transformer 就是在输入端补充位置编码。参见 Attention Is All You Need,第 3.5 节。

后来绝对位置编码演变出了两条路线,直接学习一张位置表和用正余弦公式生成位置向量。

2.1 可学习的位置向量

假设模型支持的最大位置数是 \(L_{\max}\),隐藏维度是 \(d_{\text{model}}\),就建立一张参数表:

\[P\in\mathbb{R}^{L_{\max}\times d_{\text{model}}} \]

位置 \(0\) 查第 \(0\) 行,位置 \(1\) 查第 \(1\) 行。训练过程中,位置向量和模型其他参数一起更新。BERT 官方实现和 GPT-2 都采用过这种方案;GPT-2 的实现里可以看到 token embedding wte 和位置 embedding wpe 相加。参见 GPT-2 官方模型代码。

它的优点是灵活:每个位置应该长什么样,由训练数据决定。

问题也出在这里:位置编码依赖于训练数据,如果实际遇到了比训练数据更长的序列,模型就蒙了:妹学过呀。

2.2 正余弦位置编码与多频率表示

原始 Transformer 使用的固定正余弦编码是:

\[\begin{aligned} PE(m,2i)&=\sin(m\omega_i)\\ PE(m,2i+1)&=\cos(m\omega_i)\\ \omega_i&=10000^{-2i/d_{\text{model}}} \end{aligned} \]

这里假设 \(d_{\text{model}}\) 为偶数,\(i=0,\ldots,d_{\text{model}}/2-1\)。相邻两个维度是一组,使用相同频率,一个取正弦,一个取余弦。

正余弦编码则把位置变成了多组有界的周期信号:有的变化快,有的变化慢。真的很妙。

正余弦位置编码的热力图与不同频率的变化曲线

图 1:按公式绘制,位置范围为 0~63,编码维度为 64。左图每行是一个位置向量;右图展示几个偶数维度。高频维度变化快,低频维度在这段范围内变化慢。

这套设计还有一个很漂亮的性质。对于固定偏移 \(\Delta\),三角恒等式告诉我们:

\[\sin((m+\Delta)\omega) =\sin(m\omega)\cos(\Delta\omega) +\cos(m\omega)\sin(\Delta\omega) \]

也就是说,从位置 \(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\),那么未缩放的内积可以展开为:

\[\begin{aligned} (e_m+p_m)^\top A(e_n+p_n) ={}&e_m^\top Ae_n+e_m^\top Ap_n\\ &+p_m^\top Ae_n+p_m^\top Ap_n \end{aligned} \]

里面同时存在内容—内容、内容—位置、位置—内容、位置—位置四种交互。后续一些方法正是围绕“哪些交互需要保留,怎样表达更合适”展开的。

4. 相对位置编码

4.1 Shaw:用向量表示相对距离

Shaw 等人在 2018 年提出,把相对位移映射成向量,并放进注意力计算。只看它对 key 的修改,可以写成:

\[s_{m,n} =\frac{q_m^\top\left(k_n+r_{\operatorname{clip}(n-m,-K,K)}\right)}{\sqrt{d_h}} \]

\(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 采用了更轻量的思路:不为相对距离提供完整向量,而是给每个注意力头加一个相对位置偏置:

\[s_{m,n}^{(h)} =\frac{(q_m^{(h)})^\top k_n^{(h)}}{\sqrt{d_h}} +b_{\operatorname{bucket}(n-m)}^{(h)} \]

这里的 \(b\) 是可学习的标量,\(h\) 表示注意力头。bucket 的意思是“分桶”:近距离分得细一些,远距离分得粗一些,足够远的距离可以合并。双向注意力还需要区分左右方向。

直觉上,我们可能很在意“紧挨着”和“隔了两个 token”的差异,却没必要为“相隔 1000”和“相隔 1001”各学一套精确的位置参数。

原始 T5 为每个头学习有限个偏置,同一套偏置参数跨层共享。注意,分桶规则是预先设计的,可学习的是每个桶里的偏置值。参见 T5 论文的位置编码说明。

4.4 DeBERTa:内容与位置的解耦建模

DeBERTa 的关键词是“解耦注意力”。它分别表示内容和相对位置,然后显式计算不同类型的交互。

可以把原论文中保留的主要部分概括为:

\[\text{注意力打分} \leftarrow \text{内容—内容} +\text{内容—相对位置} +\text{相对位置—内容} \]

比如,“当前这个内容倾向于关注左边哪些位置”,和“某种相对位置上出现的内容是否值得关注”,并不完全是同一个问题。它们可以通过不同投影来学习。

原始实现省略了位置—位置项,但也没有把所有绝对位置信息彻底抛弃,而是在掩码预测的解码部分使用了绝对位置信息。参见 DeBERTa 原论文。

5. RoPE:通过旋转表达相对位置

RoPE,全称 Rotary Position Embedding,来自 RoFormer。它的想法非常简洁优雅:给 Q、K 按各自位置做旋转,让内积里的位置因素只依赖相对位移。

标准做法是在每层注意力中,先投影出 Q、K,再对它们应用旋转,通常不旋转 V。参见 RoFormer 原论文。

5.1 二维旋转与相对位置关系

二维空间中,旋转角度 \(\alpha\) 对应的矩阵是:

\[R(\alpha)= \begin{bmatrix} \cos\alpha&-\sin\alpha\\ \sin\alpha&\cos\alpha \end{bmatrix} \]

让位置 \(m\) 的 query 旋转 \(m\omega\),位置 \(n\) 的 key 旋转 \(n\omega\):

\[\widetilde q_m=R(m\omega)q_m,\qquad \widetilde k_n=R(n\omega)k_n \]

两者内积为:

\[\begin{aligned} \widetilde q_m^\top\widetilde k_n &=q_m^\top R(m\omega)^\top R(n\omega)k_n\\ &=q_m^\top R((n-m)\omega)k_n \end{aligned} \]

第二步只用了旋转的两个性质:转置相当于反向旋转,连续旋转的角度可以相加。原来的 \(m\) 和 \(n\),就这样组合成了 \(n-m\)。

这里“只依赖相对位移”指的是旋转引入的位置关系。内积当然还依赖 \(q_m\)、\(k_n\) 的内容,而且深层网络中的内容向量已经包含上下文。

5.2 高维向量中的分组旋转

对于偶数维的注意力头,可以把 \(d_h\) 个维度分成 \(d_h/2\) 组,每组是一个二维平面。第 \(i\) 组使用:

\[\omega_i=b^{-2i/d_h},\qquad i=0,\ldots,d_h/2-1 \]

经典设置使用 \(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\):

\[s_{m,n}^{(h)} =\frac{(q_m^{(h)})^\top k_n^{(h)}}{\sqrt{d_h}} -a_h(m-n) \]

\(a_h>0\) 是第 \(h\) 个头的斜率,原始 ALiBi 为不同头指定不同的固定斜率,不通过训练学习它们。未来位置仍然要通过因果掩码禁止访问。

用 softmax 的指数形式看,这个扣分相当于给未归一化权重乘上:

\[\exp(-a_h(m-n)) \]

假设两个 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'=\frac{m}{s} \]

然后用 \(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 必须沿用同一套位置坐标。这一点对于大模型推理系统是很重要的。常见问题有三类。

  1. 接着生成时,位置不能从头算。 如果已经缓存了前 \(100\) 个 token,位置是 \(0\sim99\),下一个 token 就应使用 \(100\),即使这次只输入它一个。它与位置 \(99\) 的 token 本来只差 \(1\);若误用位置 \(0\),RoPE 计算出的相对位移就会改变,注意力分数也会跟着出错。

  2. 移动缓存,不等于改变逻辑位置。 比如滑动窗口只保留位置 \(80\sim99\) 的 K、V,它们虽然移到了缓存的第 \(0\sim19\) 格,仍然带着原位置的旋转。新 token 可以继续使用位置 \(100\),不能直接拿缓存长度 \(20\) 当作它的位置。如果要把整段重新编号,就需要同步调整缓存中 K 对应的旋转;只修改编号记录是不够的。

  3. 有 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 模型上研究上下文扩展

这几条路线并不是严格的“后一代淘汰前一代”。它们强调的目标不同:表达能力、计算方式、参数共享、推理效率和长度泛化,都可能影响选择。

位置编码研究的重心,逐渐从“怎样标记一个位置”转向“怎样让注意力更好地表达位置关系”,又进一步转向“怎样把训练中学到的位置关系迁移到更长的文本”。感觉这其中还是大有学问的,值得持续关注和学习~

参考资料与延伸阅读

正文中的链接对应具体论点;下面按阅读用途集中列出,方便继续深入。

posted @ 2026-10-16 23:53  JaaaLmg  阅读(19)  评论(0)    收藏  举报