位置编码解决 Transformer 自注意力机制的「顺序失忆症」,为模型注入序列位置信息。从绝对编码到 RoPE,相对位置建模成为主流。

为什么需要位置编码

自注意力机制具有置换不变性。交换序列元素位置不影响注意力权重。

位置编码打破这种不变性:

位置编码类型对比

类型原理优点缺点代表模型
可学习绝对编码每个位置一个可学习向量简单有效无法外推超长序列BERT, GPT-2
正弦编码固定三角函数生成可外推、无需训练极长序列效果下降Transformer, ViT
相对位置编码编码元素间相对距离长序列表现好实现复杂T5, DeBERTa
RoPE旋转向量融合位置优雅、外推性好-LLaMA, ChatGLM
ALiBi注意力分数加线性偏置极简单、外推强-BLOOM, Falcon

正弦位置编码

数学公式

核心性质

  1. 唯一性:不同频率正弦波组合形成唯一编码
  2. 相对位置线性表达可由通过旋转矩阵变换得到
  3. 多尺度表示:高频编码局部位置,低频编码全局位置

线性变换推导(和差角公式):

RoPE:旋转位置编码

RoPE 通过复数旋转将位置信息嵌入:

核心优势:注意力分数仅依赖相对位置差

二维旋转操作

设计原则

  1. 唯一性:每个位置编码唯一
  2. 外推性:能处理比训练更长的序列
  3. 相对位置不变性:相对距离编码不随绝对位置剧烈变化
  4. 效率:计算不成为瓶颈

代码实现

正弦位置编码

import torch
import torch.nn as nn
import math
 
class PositionalEncoding(nn.Module):
    def __init__(self, d_model: int, dropout: float = 0.1, max_len: int = 5000):
        super().__init__()
        self.dropout = nn.Dropout(p=dropout)
 
        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
        div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
 
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)
        pe = pe.unsqueeze(0)
 
        self.register_buffer('pe', pe)
 
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        x = x + self.pe[:, :x.size(1)]
        return self.dropout(x)

RoPE 实现

class RotaryEmbedding(nn.Module):
    def __init__(self, dim):
        super().__init__()
        self.inv_freq = 1.0 / (10000 ** (torch.arange(0, dim, 2).float() / dim))
 
    def forward(self, x):
        seq_len = x.shape[1]
        t = torch.arange(seq_len, device=x.device).type_as(self.inv_freq)
        freqs = torch.einsum('i,j->ij', t, self.inv_freq)
        return torch.cat((freqs, freqs), dim=-1)

融合方式争议

传统采用加法融合

替代方案:

  • 拼接:可能破坏语义空间连续性
  • 相乘,理论上有潜力但缺乏广泛验证

学习资源