Transformer 位置编码指南
目录
- 背景与动机
- 位置编码概述
- 绝对位置编码 (Absolute Positional Encoding)
- 旋转位置编码 (RoPE - Rotary Position Embedding)
- 相对位置编码 (Relative Positional Encoding)
- ALiBi (Attention with Linear Biases)
- 可学习位置编码 (Learned Positional Encoding)
- 方法对比与选择建议
- 常见误区与注意事项
- 扩展阅读与前沿研究
1. 背景与动机
1.1 为什么 Transformer 需要位置编码?
核心问题 :Transformer 的自注意力机制(Self-Attention)本质上是一个集合操作(set operation),它对输入序列的顺序不敏感。
具体来说:
- 自注意力计算的是 Query、Key、Value 之间的相似度和加权和
- 如果打乱输入序列的顺序,注意力的计算结果是相同的
- 这导致模型无法区分词的位置关系
举例说明:
句子A: "我爱自然语言处理"
句子B: "自然语言处理爱我"
如果没有位置信息,纯粹的自注意力会认为这两个句子是等价的,因为它们包含相同的词。
1.2 位置编码的作用
位置编码通过为每个位置注入唯一的位置信息,使模型能够:
- 区分不同位置的 token
- 捕捉序列的顺序关系
- 建模相对距离和绝对位置
2. 位置编码概述
2.1 位置编码的分类
位置编码可以从多个维度分类:
按编码方式:
- 固定编码(Fixed):使用数学函数生成,不需要训练(如 Sinusoidal PE、ALiBi)
- 可学习编码(Learned):作为参数随模型训练(如 BERT、GPT)
按位置表示:
- 绝对位置编码:直接编码每个 token 的绝对位置(位置 0, 1, 2, ...)
- 相对位置编码:编码 token 之间的相对距离(如 i - j)
按注入方式:
- 输入层注入:在 Embedding 后直接相加(如 Sinusoidal、Learned)
- 注意力层注入:在计算注意力分数时注入(如 RoPE、ALiBi、Relative PE)
2.2 理想位置编码的特性
一个好的位置编码应该具备:
- 唯一性:每个位置都有独特的表示
- 有界性:编码值在合理范围内,避免数值不稳定
- 外推性:能够处理比训练时更长的序列
- 距离感知:能够表达 token 之间的距离关系
- 旋转不变性(对某些任务):对序列的平移具有一定不变性
3. 绝对位置编码 (Absolute Positional Encoding)
3.1 Sinusoidal 位置编码(原始 Transformer)
提出论文 :Attention is All You Need (Vaswani et al., 2017)
3.1.1 核心思想
使用正弦和余弦函数生成位置编码,为每个位置和每个维度生成唯一的值。
3.1.2 数学公式
对于位置 pos 和维度 i(维度索引从 0 到 d_model-1):
PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))
其中:
pos:token 在序列中的位置(0, 1, 2, ...)i:编码向量的维度索引d_model:模型的隐藏层维度- 偶数维度使用
sin,奇数维度使用cos
3.1.3 直觉理解
- 不同频率的波形:低维度使用高频波形(快速变化),高维度使用低频波形(缓慢变化)
- 唯一性:不同位置在不同维度上的组合是唯一的
- 相对位置信息:由于三角函数的性质,PE(pos+k) 可以表示为 PE(pos) 的线性函数
3.1.4 代码实现
python
import torch
import math
def sinusoidal_positional_encoding(seq_len, d_model):
"""
生成 Sinusoidal 位置编码
Args:
seq_len: 序列长度
d_model: 模型维度
Returns:
pe: [seq_len, d_model] 的位置编码矩阵
"""
# 创建位置索引 [0, 1, 2, ..., seq_len-1]
position = torch.arange(seq_len).unsqueeze(1) # [seq_len, 1]
# 创建维度索引的除数项
div_term = torch.exp(torch.arange(0, d_model, 2) *
-(math.log(10000.0) / d_model)) # [d_model/2]
# 初始化位置编码矩阵
pe = torch.zeros(seq_len, d_model)
# 偶数维度使用 sin
pe[:, 0::2] = torch.sin(position * div_term)
# 奇数维度使用 cos
pe[:, 1::2] = torch.cos(position * div_term)
return pe
# 使用示例
seq_len = 100
d_model = 512
pe = sinusoidal_positional_encoding(seq_len, d_model)
print(f"位置编码形状: {pe.shape}") # [100, 512]
# 与输入相加
# x: [batch_size, seq_len, d_model]
# x = x + pe[:x.size(1), :]
3.1.5 优缺点
优点:
- ✅ 无需训练:完全由数学函数生成,节省参数
- ✅ 外推性好:可以处理任意长度的序列(超过训练长度)
- ✅ 相对位置信息:包含了一定的相对位置关系
缺点:
- ❌ 固定模式:无法根据数据自适应调整
- ❌ 长序列效果下降:对于非常长的序列,编码可能不够精确
- ❌ 维度耦合:不同维度之间的关系是预定义的
4. 旋转位置编码 (RoPE - Rotary Position Embedding)
提出论文 :RoFormer: Enhanced Transformer with Rotary Position Embedding (Su et al., 2021)
4.1 核心思想
RoPE 不是在输入层添加位置编码,而是在注意力计算内部 ,通过旋转矩阵对 Query 和 Key 进行变换,从而注入位置信息。
关键创新:
- 将位置信息编码为复平面上的旋转
- 通过旋转角度来表示位置关系
- 自然地编码了相对位置信息
4.2 数学原理
4.2.1 二维情况的直觉
在二维空间中,将向量 ( x , y ) (x, y) (x,y) 旋转 θ \theta θ 角度:
[x'] [cos(θ) -sin(θ)] [x]
[y'] = [sin(θ) cos(θ)] × [y]
4.2.2 RoPE 的核心公式
对于位置 m m m 的 query 向量 q m \mathbf{q}_m qm 和位置 n n n 的 key 向量 k n \mathbf{k}_n kn:
q_m' = R(m) · q_m
k_n' = R(n) · k_n
其中 R ( m ) R(m) R(m) 是旋转矩阵,使得:
Attention Score = (R(m)·q_m)^T (R(n)·k_n) = q_m^T R^T(m) R(n) k_n = q_m^T R(n-m) k_n
核心性质 :注意力分数只依赖于相对位置 n − m n-m n−m,而不是绝对位置。
4.2.3 高维推广
对于 d d d 维向量,RoPE 将其分为 d / 2 d/2 d/2 对,每对应用不同频率的旋转:
对于维度 2i 和 2i+1 (i = 0, 1, ..., d/2-1):
[q_{2i} ] [cos(m·θ_i) -sin(m·θ_i)] [q_{2i} ]
[q_{2i+1}] = [sin(m·θ_i) cos(m·θ_i)] × [q_{2i+1}]
其中 θ_i = 10000^(-2i/d)
4.3 代码实现
python
import torch
import torch.nn as nn
class RoPE(nn.Module):
def __init__(self, dim, max_seq_len=2048, base=10000):
"""
RoPE 位置编码
Args:
dim: 特征维度 (必须是偶数)
max_seq_len: 最大序列长度
base: 频率基数
"""
super().__init__()
self.dim = dim
self.max_seq_len = max_seq_len
self.base = base
# 预计算旋转频率
inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2).float() / dim))
self.register_buffer('inv_freq', inv_freq)
# 预计算旋转矩阵
self._cache_rotations(max_seq_len)
def _cache_rotations(self, seq_len):
"""预计算旋转矩阵"""
t = torch.arange(seq_len).type_as(self.inv_freq)
freqs = torch.einsum('i,j->ij', t, self.inv_freq) # [seq_len, dim/2]
# 生成 sin 和 cos
emb = torch.cat([freqs, freqs], dim=-1) # [seq_len, dim]
self.register_buffer('cos_cached', emb.cos(), persistent=False)
self.register_buffer('sin_cached', emb.sin(), persistent=False)
def rotate_half(self, x):
"""将向量的前半部分和后半部分交换并取负"""
x1, x2 = x[..., :x.shape[-1]//2], x[..., x.shape[-1]//2:]
return torch.cat([-x2, x1], dim=-1)
def forward(self, q, k, seq_len=None):
"""
应用 RoPE 到 query 和 key
Args:
q: [batch, heads, seq_len, dim]
k: [batch, heads, seq_len, dim]
Returns:
q_rot, k_rot: 旋转后的 query 和 key
"""
if seq_len is None:
seq_len = q.shape[2]
# 获取 sin 和 cos
cos = self.cos_cached[:seq_len, ...].unsqueeze(0).unsqueeze(0) # [1, 1, seq_len, dim]
sin = self.sin_cached[:seq_len, ...].unsqueeze(0).unsqueeze(0)
# 应用旋转
q_rot = (q * cos) + (self.rotate_half(q) * sin)
k_rot = (k * cos) + (self.rotate_half(k) * sin)
return q_rot, k_rot
# 使用示例
batch_size = 2
num_heads = 8
seq_len = 128
head_dim = 64
# 初始化 RoPE
rope = RoPE(dim=head_dim, max_seq_len=2048)
# 模拟 query 和 key
q = torch.randn(batch_size, num_heads, seq_len, head_dim)
k = torch.randn(batch_size, num_heads, seq_len, head_dim)
# 应用 RoPE
q_rot, k_rot = rope(q, k)
# 计算注意力分数
attn_scores = torch.matmul(q_rot, k_rot.transpose(-2, -1)) / (head_dim ** 0.5)
print(f"注意力分数形状: {attn_scores.shape}") # [2, 8, 128, 128]
4.4 RoPE 的优势
优点:
- ✅ 相对位置编码:注意力分数自然依赖于相对位置
- ✅ 外推性强:可以较好地处理比训练时更长的序列
- ✅ 计算高效:不增加额外参数,计算开销小
- ✅ 理论优美:基于旋转变换的几何解释清晰
- ✅ 远程衰减:自然地对远距离 token 的注意力进行衰减
缺点:
- ❌ 实现复杂度:相比简单相加的位置编码,实现较复杂
- ❌ 维度要求:要求特征维度是偶数
4.5 应用案例
- LLaMA / LLaMA 2:Meta 的开源大模型
- GPT-NeoX:EleutherAI 的大规模语言模型
- PaLM:Google 的 5400 亿参数模型
- CodeLlama:代码生成模型
5. 相对位置编码 (Relative Positional Encoding)
5.1 核心思想
相对位置编码不直接编码每个 token 的绝对位置,而是在计算注意力时,显式地加入 token 之间的相对距离信息。
关键理念:
- 对于许多 NLP 任务,相对位置比绝对位置更重要
- 例如:"我"和"爱"之间的距离是 1,这比它们的绝对位置更有意义
5.2 主要变体
5.2.1 Shaw et al. (2018) - 基础相对位置编码
公式:
Attention(Q, K, V) = softmax((QK^T + R) / √d_k) V
其中 R_{ij} 是位置 i 和 j 之间的相对位置偏置
相对位置计算:
R_{ij} = a_{clip(i-j)}
clip(x) = max(-k, min(k, x)) # 将相对距离截断在 [-k, k] 范围内
其中 { a − k , . . . , a 0 , . . . , a k } \{a_{-k}, ..., a_0, ..., a_k\} {a−k,...,a0,...,ak} 是可学习的参数。
5.2.2 T5 相对位置编码
提出论文 :Exploring the Limits of Transfer Learning with a Unified Text-to-Text Transformer (Raffel et al., 2020)
T5 使用了简化的相对位置编码:
公式:
对于相对位置偏移 offset = i - j:
- 将 offset 映射到有限个桶(buckets)
- 每个桶对应一个可学习的偏置值
桶划分策略:
python
def relative_position_bucket(relative_position,
num_buckets=32,
max_distance=128):
"""
T5 的相对位置桶划分
- 前一半桶:精确表示小的相对距离 (0 到 num_buckets//2 - 1)
- 后一半桶:对数刻度表示大的相对距离
"""
ret = 0
n = -relative_position # 使用负值,因为关注的是向前看的距离
# 处理双向注意力(前向和后向)
num_buckets //= 2
ret += (n < 0).long() * num_buckets
n = torch.abs(n)
# 小距离:精确桶
max_exact = num_buckets // 2
is_small = n < max_exact
# 大距离:对数桶
val_if_large = max_exact + (
torch.log(n.float() / max_exact) /
math.log(max_distance / max_exact) *
(num_buckets - max_exact)
).long()
val_if_large = torch.min(val_if_large,
torch.full_like(val_if_large, num_buckets - 1))
ret += torch.where(is_small, n, val_if_large)
return ret
5.3 完整代码实现
python
import torch
import torch.nn as nn
class RelativePositionBias(nn.Module):
def __init__(self, num_heads, num_buckets=32, max_distance=128):
"""
T5 风格的相对位置编码
Args:
num_heads: 注意力头数
num_buckets: 相对位置桶的数量
max_distance: 最大相对距离
"""
super().__init__()
self.num_heads = num_heads
self.num_buckets = num_buckets
self.max_distance = max_distance
# 可学习的相对位置偏置参数
self.relative_attention_bias = nn.Embedding(num_buckets, num_heads)
def _relative_position_bucket(self, relative_position):
"""计算相对位置的桶索引"""
ret = 0
n = -relative_position
# 双向注意力
num_buckets = self.num_buckets
num_buckets //= 2
ret += (n < 0).long() * num_buckets
n = torch.abs(n)
# 小距离精确,大距离对数
max_exact = num_buckets // 2
is_small = n < max_exact
val_if_large = max_exact + (
torch.log(n.float() / max_exact) /
torch.log(self.max_distance / max_exact) *
(num_buckets - max_exact)
).long()
val_if_large = torch.min(
val_if_large,
torch.full_like(val_if_large, num_buckets - 1)
)
ret += torch.where(is_small, n, val_if_large)
return ret
def forward(self, query_length, key_length):
"""
计算相对位置偏置
Returns:
bias: [num_heads, query_length, key_length]
"""
# 创建位置索引矩阵
q_pos = torch.arange(query_length, dtype=torch.long)
k_pos = torch.arange(key_length, dtype=torch.long)
# 计算相对位置 [query_length, key_length]
relative_position = q_pos[:, None] - k_pos[None, :]
# 映射到桶
rp_bucket = self._relative_position_bucket(relative_position)
# 获取偏置值 [query_length, key_length, num_heads]
values = self.relative_attention_bias(rp_bucket)
# 调整维度 [num_heads, query_length, key_length]
return values.permute(2, 0, 1)
# 在注意力计算中使用
class AttentionWithRelativePE(nn.Module):
def __init__(self, d_model, num_heads):
super().__init__()
self.num_heads = num_heads
self.head_dim = d_model // num_heads
self.q_proj = nn.Linear(d_model, d_model)
self.k_proj = nn.Linear(d_model, d_model)
self.v_proj = nn.Linear(d_model, d_model)
self.out_proj = nn.Linear(d_model, d_model)
# 相对位置偏置
self.relative_pe = RelativePositionBias(num_heads)
def forward(self, x):
batch_size, seq_len, d_model = x.shape
# 投影并重塑
q = self.q_proj(x).view(batch_size, seq_len, self.num_heads, self.head_dim)
k = self.k_proj(x).view(batch_size, seq_len, self.num_heads, self.head_dim)
v = self.v_proj(x).view(batch_size, seq_len, self.num_heads, self.head_dim)
# [batch, heads, seq_len, head_dim]
q = q.transpose(1, 2)
k = k.transpose(1, 2)
v = v.transpose(1, 2)
# 计算注意力分数
scores = torch.matmul(q, k.transpose(-2, -1)) / (self.head_dim ** 0.5)
# [batch, heads, seq_len, seq_len]
# 添加相对位置偏置
rel_bias = self.relative_pe(seq_len, seq_len) # [heads, seq_len, seq_len]
scores = scores + rel_bias.unsqueeze(0) # 广播到 batch
# 应用 softmax 和加权求和
attn_weights = torch.softmax(scores, dim=-1)
out = torch.matmul(attn_weights, v)
# 重塑并投影输出
out = out.transpose(1, 2).contiguous().view(batch_size, seq_len, d_model)
return self.out_proj(out)
# 使用示例
model = AttentionWithRelativePE(d_model=512, num_heads=8)
x = torch.randn(2, 100, 512)
output = model(x)
print(f"输出形状: {output.shape}") # [2, 100, 512]
5.4 优缺点分析
优点:
- ✅ 关注相对关系:更符合 NLP 任务的特性
- ✅ 长度泛化:可以较好地处理不同长度的序列
- ✅ 可学习:偏置参数可以根据数据自适应
- ✅ 灵活性:可以与其他位置编码组合使用
缺点:
- ❌ 额外参数:需要存储可学习的偏置矩阵
- ❌ 计算开销:需要为每个注意力层计算偏置
- ❌ 实现复杂:相比简单的位置编码,实现更复杂
5.5 应用案例
- T5:Google 的文本到文本转换模型
- DeBERTa:Microsoft 的增强 BERT 模型
- XLNET:结合了相对位置编码的自回归模型
6. ALiBi (Attention with Linear Biases)
提出论文 :Train Short, Test Long: Attention with Linear Biases Enables Input Length Extrapolation (Press et al., 2022)
6.1 核心思想
ALiBi 是一种极其简单但有效的位置编码方法:在计算注意力分数时,直接减去一个与距离成正比的惩罚项,使得距离越远的 token 注意力越低。
关键创新:
- 不在输入层添加位置编码
- 而是直接修改注意力矩阵
- 使用简单的线性偏置(斜率)来表示距离衰减
6.2 数学公式
标准注意力:
Attention(Q, K, V) = softmax(QK^T / √d_k) V
ALiBi 注意力:
Attention(Q, K, V) = softmax(QK^T / √d_k + m · D) V
其中:
- D_{ij} = -(i - j) 当 i ≥ j(因果掩码)
- m 是每个注意力头的斜率(slope),不同头使用不同斜率
偏置矩阵示例(假设 m = -1):
位置: 0 1 2 3 4
0 [ 0 -1 -2 -3 -4 ]
1 [ - 0 -1 -2 -3 ]
2 [ - - 0 -1 -2 ]
3 [ - - - 0 -1 ]
4 [ - - - - 0 ]
6.3 斜率的设计
对于 n 个注意力头,斜率按照几何级数生成:
m_i = 2^(-8i/n) 对于 i = 1, 2, ..., n
例如,8 个头的斜率:
head 1: 2^(-8*1/8) = 2^(-1) = 0.5
head 2: 2^(-8*2/8) = 2^(-2) = 0.25
head 3: 2^(-8*3/8) = 2^(-3) = 0.125
...
head 8: 2^(-8*8/8) = 2^(-8) ≈ 0.0039
设计理念:
- 不同头使用不同的衰减速率
- 有的头关注近距离依赖(大斜率)
- 有的头关注远距离依赖(小斜率)
6.4 代码实现
python
import torch
import torch.nn as nn
import math
class ALiBi(nn.Module):
def __init__(self, num_heads, max_seq_len=2048):
"""
ALiBi 位置编码
Args:
num_heads: 注意力头数
max_seq_len: 最大序列长度
"""
super().__init__()
self.num_heads = num_heads
self.max_seq_len = max_seq_len
# 计算每个头的斜率
slopes = self._get_slopes(num_heads)
self.register_buffer('slopes', slopes)
# 预计算偏置矩阵
self._build_alibi_bias(max_seq_len)
def _get_slopes(self, num_heads):
"""
计算 ALiBi 斜率
Returns:
slopes: [num_heads] 的斜率向量
"""
def get_slopes_power_of_2(n):
start = 2 ** (-8 / n)
ratio = start
return [start * (ratio ** i) for i in range(n)]
# 如果头数是 2 的幂次
if math.log2(num_heads).is_integer():
return torch.tensor(get_slopes_power_of_2(num_heads))
# 如果不是,需要额外处理
closest_power_of_2 = 2 ** math.floor(math.log2(num_heads))
slopes_a = get_slopes_power_of_2(closest_power_of_2)
slopes_b = self._get_slopes(2 * closest_power_of_2)[::2][:num_heads - closest_power_of_2]
return torch.tensor(slopes_a + slopes_b)
def _build_alibi_bias(self, seq_len):
"""
构建 ALiBi 偏置矩阵
Returns:
bias: [num_heads, seq_len, seq_len]
"""
# 创建距离矩阵
position = torch.arange(seq_len).unsqueeze(0) # [1, seq_len]
distance = position.T - position # [seq_len, seq_len]
# 只保留因果部分(上三角设为很大的负数)
distance = torch.tril(distance)
distance = distance.masked_fill(distance == 0, 0)
distance = torch.where(distance != 0, -distance, distance)
# 应用斜率 [num_heads, 1, 1] * [1, seq_len, seq_len]
bias = self.slopes.view(-1, 1, 1) * distance.unsqueeze(0)
self.register_buffer('bias', bias, persistent=False)
def forward(self, seq_len):
"""
返回 ALiBi 偏置
Args:
seq_len: 当前序列长度
Returns:
bias: [num_heads, seq_len, seq_len]
"""
if seq_len <= self.max_seq_len:
return self.bias[:, :seq_len, :seq_len]
else:
# 动态生成更长的偏置
self._build_alibi_bias(seq_len)
return self.bias
# 在注意力计算中使用
class AttentionWithALiBi(nn.Module):
def __init__(self, d_model, num_heads, max_seq_len=2048):
super().__init__()
self.num_heads = num_heads
self.head_dim = d_model // num_heads
self.q_proj = nn.Linear(d_model, d_model)
self.k_proj = nn.Linear(d_model, d_model)
self.v_proj = nn.Linear(d_model, d_model)
self.out_proj = nn.Linear(d_model, d_model)
# ALiBi 偏置
self.alibi = ALiBi(num_heads, max_seq_len)
def forward(self, x):
batch_size, seq_len, d_model = x.shape
# 投影(注意:不添加位置编码)
q = self.q_proj(x).view(batch_size, seq_len, self.num_heads, self.head_dim)
k = self.k_proj(x).view(batch_size, seq_len, self.num_heads, self.head_dim)
v = self.v_proj(x).view(batch_size, seq_len, self.num_heads, self.head_dim)
q = q.transpose(1, 2) # [batch, heads, seq_len, head_dim]
k = k.transpose(1, 2)
v = v.transpose(1, 2)
# 计算注意力分数
scores = torch.matmul(q, k.transpose(-2, -1)) / (self.head_dim ** 0.5)
# 添加 ALiBi 偏置
alibi_bias = self.alibi(seq_len) # [heads, seq_len, seq_len]
scores = scores + alibi_bias.unsqueeze(0) # [batch, heads, seq_len, seq_len]
# Softmax 和加权求和
attn_weights = torch.softmax(scores, dim=-1)
out = torch.matmul(attn_weights, v)
out = out.transpose(1, 2).contiguous().view(batch_size, seq_len, d_model)
return self.out_proj(out)
# 使用示例
model = AttentionWithALiBi(d_model=512, num_heads=8)
x = torch.randn(2, 100, 512)
output = model(x)
print(f"输出形状: {output.shape}") # [2, 100, 512]
# 测试长度外推
x_long = torch.randn(2, 500, 512)
output_long = model(x_long)
print(f"长序列输出形状: {output_long.shape}") # [2, 500, 512]
6.5 关键优势
优点:
- ✅ 极简设计:无额外参数,实现简单
- ✅ 外推性强:在长序列上表现出色("Train Short, Test Long")
- ✅ 无位置编码:输入层完全不需要位置编码
- ✅ 计算高效:偏置矩阵可以预计算和缓存
- ✅ 自然衰减:远距离注意力自动降低
缺点:
- ❌ 固定模式:衰减模式是预定义的,无法学习
- ❌ 单向信息:主要设计用于因果(自回归)模型
- ❌ 缺乏灵活性:不能像学习型编码那样适应数据
6.6 实验结果(论文数据)
论文中的关键发现:
- 在训练长度的 2-10 倍上依然有效
- 例如:在 1024 tokens 上训练,在 8192 tokens 上测试仍能保持性能
- 相比 Sinusoidal PE,困惑度(perplexity)显著降低
6.7 应用案例
- BLOOM:BigScience 的 176B 参数多语言模型
- MPT:MosaicML 的开源模型系列
- Falcon:Technology Innovation Institute 的模型
7. 可学习位置编码 (Learned Positional Encoding)
7.1 核心思想
最直接的方法:将位置编码作为可训练的 Embedding 矩阵,让模型自己学习最优的位置表示。
7.2 实现方式
python
import torch
import torch.nn as nn
class LearnedPositionalEncoding(nn.Module):
def __init__(self, max_seq_len, d_model):
"""
可学习位置编码
Args:
max_seq_len: 最大序列长度
d_model: 模型维度
"""
super().__init__()
# 创建可学习的位置 embedding
self.position_embeddings = nn.Embedding(max_seq_len, d_model)
self.max_seq_len = max_seq_len
def forward(self, x):
"""
Args:
x: [batch_size, seq_len, d_model]
Returns:
x + position_encoding: [batch_size, seq_len, d_model]
"""
seq_len = x.size(1)
# 创建位置索引
position_ids = torch.arange(seq_len, dtype=torch.long, device=x.device)
position_ids = position_ids.unsqueeze(0).expand(x.size(0), -1) # [batch, seq_len]
# 获取位置编码
position_embeddings = self.position_embeddings(position_ids)
# 与输入相加
return x + position_embeddings
# 使用示例
max_len = 512
d_model = 768
learned_pe = LearnedPositionalEncoding(max_len, d_model)
x = torch.randn(2, 100, d_model)
output = learned_pe(x)
print(f"输出形状: {output.shape}") # [2, 100, 768]
7.3 优缺点
优点:
- ✅ 数据自适应:可以学习任务特定的位置模式
- ✅ 实现简单:只需要一个 Embedding 层
- ✅ 灵活性高:可以捕捉任意的位置关系
缺点:
- ❌ 外推性差:无法处理超过 max_seq_len 的序列
- ❌ 额外参数:需要存储 max_seq_len × d_model 的参数矩阵
- ❌ 冷启动问题:初始化时没有任何位置信息
7.4 应用案例
- BERT:使用可学习位置编码(最大长度 512)
- GPT-2/GPT-3:使用可学习位置编码(GPT-3 最大长度 2048)
- ViT (Vision Transformer):图像块的位置编码也是可学习的
8. 方法对比与选择建议
8.1 完整对比表
| 特性 | Sinusoidal | Learned | RoPE | Relative | ALiBi |
|---|---|---|---|---|---|
| 参数量 | 0 | O(L×d) | 0 | O(n_heads×n_buckets) | 0 |
| 外推能力 | ✅ 好 | ❌ 差 | ✅ 很好 | ⚠️ 中等 | ✅ 最好 |
| 训练成本 | 低 | 低 | 中 | 中 | 低 |
| 实现复杂度 | 低 | 低 | 中 | 高 | 低 |
| 长序列性能 | ⚠️ 中等 | ❌ 差 | ✅ 好 | ✅ 好 | ✅ 最好 |
| 相对位置编码 | ⚠️ 隐式 | ❌ 无 | ✅ 显式 | ✅ 显式 | ✅ 显式 |
| 计算开销 | 低 | 低 | 中 | 高 | 低 |
| 适用模型类型 | 通用 | 通用 | 自回归 | 通用 | 自回归 |
8.2 应用场景建议
场景 1:短序列 NLP 任务(<512 tokens)
推荐:Learned PE 或 Sinusoidal
- 理由:序列短,外推不是问题;Learned PE 可以学习任务特定模式
- 典型应用:BERT 风格的分类、命名实体识别、问答系统
场景 2:长文本生成(>2048 tokens)
推荐:ALiBi > RoPE > Relative PE
- 理由:需要优秀的外推能力
- 典型应用:长文档生成、书籍写作、长对话系统
场景 3:代码生成
推荐:RoPE 或 Relative PE
- 理由:代码结构依赖于相对位置(如缩进、语法树)
- 典型应用:Codex、CodeLlama、StarCoder
场景 4:多语言 / 跨语言模型
推荐:ALiBi 或 RoPE
- 理由:不同语言的序列长度差异大,需要好的泛化能力
- 典型应用:BLOOM、mT5
场景 5:视觉 Transformer
推荐:Learned PE 或 Sinusoidal
- 理由:图像块的位置关系可能与文本不同
- 典型应用:ViT、DETR、Swin Transformer
场景 6:资源受限设备
推荐:ALiBi 或 Sinusoidal
- 理由:无额外参数,计算高效
- 典型应用:边缘设备、移动端模型
8.3 混合策略
一些先进模型会结合多种位置编码:
示例 1:绝对 + 相对
输入层:Sinusoidal PE
注意力层:Relative Position Bias
优势:同时捕捉绝对和相对位置信息
示例 2:RoPE + ALiBi
低层:RoPE(精细的位置信息)
高层:ALiBi(长距离依赖)
优势:兼顾局部和全局结构
8.4 选择流程图
开始
|
├─ 序列长度 < 512?
| └─ 是 → Learned PE 或 Sinusoidal
|
├─ 需要长序列外推?
| └─ 是 → ALiBi (最优) 或 RoPE
|
├─ 关注相对位置?
| └─ 是 → RoPE 或 Relative PE
|
├─ 资源受限?
| └─ 是 → ALiBi 或 Sinusoidal
|
└─ 默认推荐 → RoPE(平衡性能和效果)
9. 常见误区与注意事项
9.1 常见误区
❌ 误区 1:"位置编码越复杂越好"
真相:ALiBi 用最简单的线性偏置就达到了最好的外推效果。复杂度不等于性能。
❌ 误区 2:"可学习位置编码总是更好"
真相:可学习编码在训练数据上可能更好,但泛化能力(特别是长度外推)通常不如固定编码。
❌ 误区 3:"位置编码可以随意切换"
真相:不同位置编码的训练收敛速度和最优超参数差异很大。切换位置编码可能需要重新调优整个模型。
❌ 误区 4:"RoPE 和 ALiBi 可以直接组合"
真相:这两种方法在注意力层的作用机制不同,组合需要仔细设计,否则可能冲突。
❌ 误区 5:"位置编码只影响第一层"
真相:位置信息会通过层层传递影响整个模型。不同的注入方式(输入层 vs 注意力层)会导致不同的信息流动模式。
9.2 实现注意事项
1. 数值稳定性
python
# ❌ 错误:直接计算可能溢出
scores = torch.matmul(q, k.transpose(-2, -1))
# ✅ 正确:缩放防止梯度爆炸
scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(d_k)
2. 缓存机制
对于固定的位置编码(Sinusoidal、ALiBi),应该预计算并缓存:
python
# 在 __init__ 中预计算
self.register_buffer('pe', self._compute_pe(max_len), persistent=False)
# 而不是每次 forward 都重新计算
3. 长度外推的测试
python
# 训练时使用短序列
train_seq_len = 512
# 测试时逐步增加长度
test_seq_lens = [512, 1024, 2048, 4096]
for test_len in test_seq_lens:
# 评估困惑度或其他指标
evaluate(model, test_len)
4. 混合精度训练
使用 float16 时,位置编码的计算可能需要特别注意:
python
with torch.cuda.amp.autocast():
# 某些位置编码计算可能需要 float32
pe = compute_pe(...).float()
x = x + pe
9.3 调试技巧
技巧 1:可视化注意力矩阵
python
import matplotlib.pyplot as plt
import seaborn as sns
# 提取注意力权重
attn_weights = model.get_attention_weights(x) # [heads, seq_len, seq_len]
# 可视化第一个头
plt.figure(figsize=(10, 8))
sns.heatmap(attn_weights[0].detach().cpu().numpy(), cmap='viridis')
plt.title('Attention Pattern (Head 0)')
plt.xlabel('Key Position')
plt.ylabel('Query Position')
plt.show()
观察要点:
- 对角线:是否有局部注意力模式?
- 衰减:远距离注意力是否合理衰减?
- 异常:是否有不符合预期的注意力峰值?
技巧 2:位置编码相似度分析
python
# 对于 Sinusoidal PE
pe = sinusoidal_pe(seq_len=100, d_model=512)
# 计算不同位置编码之间的余弦相似度
similarity = torch.cosine_similarity(
pe[0:1, :].expand(100, -1),
pe,
dim=1
)
plt.plot(similarity.numpy())
plt.xlabel('Position')
plt.ylabel('Cosine Similarity with Position 0')
plt.title('Position Encoding Similarity')
plt.show()
10. 扩展阅读与前沿研究
10.1 重要论文
经典论文
-
Attention is All You Need (Vaswani et al., 2017)
- 原始 Transformer 和 Sinusoidal PE
- arXiv:1706.03762
-
Self-Attention with Relative Position Representations (Shaw et al., 2018)
- 第一个相对位置编码方法
- arXiv:1803.02155
-
RoFormer: Enhanced Transformer with Rotary Position Embedding (Su et al., 2021)
- RoPE 的原始论文
- arXiv:2104.09864
-
Train Short, Test Long: Attention with Linear Biases (Press et al., 2022)
- ALiBi 论文
- arXiv:2108.12409
进阶论文
-
Rethinking Positional Encoding in Language Pre-training (Su et al., 2021)
- 位置编码的系统性研究
- arXiv:2006.15595
-
Your Transformer May Not be as Powerful as You Expect (Liu et al., 2022)
- 位置编码对模型能力的影响分析
- arXiv:2205.13401
10.2 前沿研究方向
1. 基于内容的自适应位置编码
- 根据输入内容动态调整位置编码
- 例如:对重要 token 加强位置信息
2. 层级位置编码
- 不同层使用不同的位置编码策略
- 低层关注局部,高层关注全局
3. 位置编码的理论分析
- 为什么某些位置编码外推性好?
- 位置编码与模型表达能力的关系
4. 无位置编码的 Transformer
- 探索完全不需要显式位置编码的架构
- 通过结构归纳偏置隐式编码位置
5. 多模态位置编码
- 如何为图像-文本、音频-文本等多模态数据设计统一的位置编码?
10.3 实用工具和资源
代码库
-
Hugging Face Transformers:包含各种位置编码的实现
-
Flash Attention:高效注意力实现,支持 RoPE
教程和博客
-
The Illustrated Transformer (Jay Alammar)
- 可视化解释 Transformer 和位置编码
-
LLM 位置编码全景 (知乎/CSDN)
- 中文社区的详细讨论
实验平台
- Colab/Kaggle:快速实验不同位置编码
- Weights & Biases:跟踪不同位置编码的训练曲线
总结
位置编码是 Transformer 架构中不可或缺的组成部分。本笔记覆盖了从经典的 Sinusoidal PE 到最新的 ALiBi 等主流方法:
核心要点回顾:
- Sinusoidal PE:经典方法,简单高效,外推性好
- Learned PE:数据自适应,但外推性差
- RoPE:相对位置编码,外推性强,应用广泛
- Relative PE:显式建模相对距离,灵活性高
- ALiBi:极简设计,外推性最优,计算高效
选择建议:
- 短序列任务:Learned 或 Sinusoidal
- 长序列生成:ALiBi 或 RoPE
- 代码/结构化数据:RoPE 或 Relative
- 资源受限:ALiBi 或 Sinusoidal
未来展望 :
位置编码仍是活跃的研究领域,未来可能会出现更加高效、泛化能力更强的新方法。理解现有方法的原理和特性,将帮助我们更好地应用和改进它们。
附录:快速参考代码模板
A. 完整的位置编码模块
python
import torch
import torch.nn as nn
import math
class PositionalEncodingFactory:
"""位置编码工厂类"""
@staticmethod
def create(pe_type, **kwargs):
"""
创建位置编码
Args:
pe_type: 'sinusoidal' | 'learned' | 'rope' | 'alibi'
**kwargs: 各种位置编码的特定参数
"""
if pe_type == 'sinusoidal':
return SinusoidalPE(**kwargs)
elif pe_type == 'learned':
return LearnedPE(**kwargs)
elif pe_type == 'rope':
return RoPE(**kwargs)
elif pe_type == 'alibi':
return ALiBi(**kwargs)
else:
raise ValueError(f"Unknown PE type: {pe_type}")
# 使用示例
pe = PositionalEncodingFactory.create(
pe_type='rope',
dim=64,
max_seq_len=2048
)
B. 位置编码对比实验模板
python
def compare_positional_encodings(seq_lengths=[128, 512, 2048, 8192]):
"""对比不同位置编码在不同序列长度上的表现"""
pe_methods = {
'Sinusoidal': SinusoidalPE(d_model=512),
'Learned': LearnedPE(max_len=512, d_model=512),
'RoPE': RoPE(dim=64, max_seq_len=8192),
'ALiBi': ALiBi(num_heads=8, max_seq_len=8192)
}
results = {}
for name, pe in pe_methods.items():
results[name] = []
for seq_len in seq_lengths:
# 运行评估
score = evaluate_on_length(model, pe, seq_len)
results[name].append(score)
# 可视化结果
plot_comparison(results, seq_lengths)