Skip to content

位置编码

自注意力对词序「视而不见」——它把每个词一视同仁地全连接,所以「猫追狗」和「狗追猫」在纯注意力眼里是一样的。位置编码(Positional Encoding) 就是往每个词向量里注入「我是第几个位置」的信息,把丢失的顺序感补回来。

为什么必须有它

输入:  [猫] [追] [狗]
纯自注意力看到的: 一个"无序集合" {猫, 追, 狗}

"猫追狗" 与 "狗追猫" 表示相同 ❌ 语义完全反了!

必须给每个位置一个「身份证」,让模型能区分先后。位置信息有两种主流注入思路:

  • 绝对位置编码:直接告诉模型「你在第几位」。(原始 Transformer、BERT、GPT-2)
  • 相对位置编码:只强调「你俩隔多远」。(T5、ALiBi、RoPE)

本页先讲透最经典的绝对正弦编码,再对比可学习编码,相对/RoPE 详见 位置编码演进RoPE

正弦/余弦位置编码(Sinusoidal)

原始论文用不同频率的正弦波给每个位置生成一个 d_model 维向量:

[ PE_{(pos,,2i)} = \sin!\left(\frac{pos}{10000^{2i/d_{model}}}\right), \quad PE_{(pos,,2i+1)} = \cos!\left(\frac{pos}{10000^{2i/d_{model}}}\right) ]

  • pos:词在句子中的位置(0, 1, 2, ...)
  • i:向量的维度下标(0 到 d_model/2)

直觉:像二进制计数器一样

想象用一串「指针转速不同的时钟」来给位置编号:

维度0  → 转得飞快的指针(高频)   ┐
维度1  → 稍慢的指针             │ 组合起来,每个 pos 都有独一无二的
维度2  → 更慢...                │ "指纹"; 相邻位置的指纹也很像,
...                           │ 远位置的指纹差异大
维度末尾 → 几乎不动的指针(低频)  ┘
  • 低频维度:变化慢,捕捉「大概在文章哪个区域」(长程位置)。
  • 高频维度:变化快,区分「紧挨着的两个词」(精细位置)。
  • 多频率叠加,就能唯一且平滑地编码任意位置。

3 行代码看懂

python
import torch, math

def positional_encoding(seq_len, d_model):
    pe = torch.zeros(seq_len, d_model)
    pos = torch.arange(seq_len).unsqueeze(1).float()        # [seq_len, 1]
    div = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
    pe[:, 0::2] = torch.sin(pos * div)   # 偶数维用 sin
    pe[:, 1::2] = torch.cos(pos * div)   # 奇数维用 cos
    return pe   # 形状 [seq_len, d_model]

print(positional_encoding(4, 8))

它与词向量怎么「合体」

位置编码不另开一路,而是直接加到词嵌入上

   词嵌入 x      [0.12, -0.34, 0.55, ...]   ← 讲的是"是什么词"
 + 位置编码 PE   [0.00,  0.84, 0.54, ...]   ← 讲的是"在第几位"
 ─────────────────────────────────────────
 = 输入 z       [0.12,  0.50, 1.09, ...]   ← 既含语义又含位置
python
x = embedding(token_ids)            # [B, T, d_model] 语义
x = x + pe[:T].unsqueeze(0)         # 加上位置编码(逐元素相加)

为什么是「加」而不是「拼」?相加不改变维度(仍是 d_model),后续模块无需改动;模型有能力从混合向量里分别解出语义与位置线索。

可学习位置编码(Learned)

BERT、GPT-2 的做法更简单粗暴:把位置也当成一张可学习的嵌入表,和词嵌入一样在训练中学出来。

python
# 位置 0..max_len-1,每个位置一个 d_model 向量,随模型一起训练
self.pos_embedding = nn.Embedding(max_len, d_model)
x = tok_embedding(ids) + self.pos_embedding(position_ids)
方案优点缺点
正弦(固定)无需训练;理论上可外推到未见长度实际外推效果一般;表达力受限
可学习灵活,模型自己决定位置含义超出训练最大长度就没法用;占参数

实践发现:只要序列长度不超出训练上限,可学习位置编码效果通常略好,所以 BERT/GPT-2 都选了它;而现代长上下文模型大多转向 RoPE(见进阶页)。

位置编码能力上限:最大长度

采用可学习编码的模型都有一个 max_position_embeddings,比如 GPT-2 是 1024:

训练时只见过位置 0..1023
    → 推理给到第 1024 个 token,没有对应的位置向量 → 直接报错/效果崩塌

这正是「上下文长度」的物理来源之一,也是后续 RoPE + 位置插值要突破的限制。

小结

  • 自注意力天生「无序」,必须靠位置编码补回顺序信息。
  • 正弦编码:多频率三角函数拼出唯一「位置指纹」,免训练。
  • 可学习编码:查表学出来,序列不超限时更灵活(BERT/GPT-2)。
  • 二者都是加到词嵌入上,不改变 d_model 维度。
  • 想支持更长上下文、更好外推 → 现代方案转向 RoPE

下一步

语义有了、位置有了,接下来是全场最重要的引擎:这些向量如何「互相关注」 → 注意力机制