约 3 分钟阅读
Transformer Positional Encoding Explained
理解 Transformer 如何通过 Positional Encoding 注入序列顺序信息,以及正弦函数设计的巧妙之处。
Transformer 的 Self-Attention 机制是排列不变的——打乱输入顺序,输出不变。但语言是有顺序的,“狗咬人”和”人咬狗”含义完全不同。Positional Encoding 就是为了解决这个问题。
Why Positional Encoding?
# Self-Attention 是排列不变的
Attention([A, B, C]) = Attention([C, A, B]) # 输出相同!
# 但语言需要位置信息
"狗 咬 人" ≠ "人 咬 狗"
Sinusoidal Positional Encoding
原始 Transformer 使用正弦和余弦函数:
每个维度的频率不同:
- 维度 0:波长 = ,变化最快
- 维度 :波长 = ,变化最慢
def sinusoidal_encoding(max_len, d_model):
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len).unsqueeze(1)
div_term = torch.exp(
torch.arange(0, d_model, 2) * -(math.log(10000.0) / d_model)
)
pe[:, 0::2] = torch.sin(position * div_term) # 偶数维度
pe[:, 1::2] = torch.cos(position * div_term) # 奇数维度
return pe
Why Sinusoidal?
1. 可以表示相对位置
关键性质: 可以用 的线性变换表示:
其中 是只依赖于 的旋转矩阵。这意味着模型可以学习相对位置关系。
2. 值域有界
不会随着位置增长而发散。
3. 不同维度捕捉不同尺度
维度 0-1: 捕捉局部模式 (相邻词)
维度 2-3: 捕捉短距离模式 (句子内)
...
维度 d-2,d-1: 捕捉长距离模式 (段落间)
Learned Positional Encoding
BERT 和 GPT 使用可学习的位置编码:
class LearnedPositionalEncoding(nn.Module):
def __init__(self, max_len, d_model):
super().__init__()
self.embedding = nn.Embedding(max_len, d_model)
def forward(self, x):
positions = torch.arange(x.size(1))
return self.embedding(positions)
Pros:更灵活,可能学到更好的表示 Cons:不能泛化到训练时未见过的长度
Rotary Positional Encoding (RoPE)
现代模型(如 LLaMA)使用 RoPE,直接在 Attention 计算中编码位置:
def rotate_half(x):
x1, x2 = x.chunk(2, dim=-1)
return torch.cat([-x2, x1], dim=-1)
def apply_rope(q, k, positions, theta=10000.0):
freqs = 1.0 / (theta ** (torch.arange(0, d, 2) / d))
t = positions[:, None] * freqs[None, :]
cos_t = torch.cos(t)
sin_t = torch.sin(t)
q_rot = q * cos_t + rotate_half(q) * sin_t
k_rot = k * cos_t + rotate_half(k) * sin_t
return q_rot, k_rot
RoPE 的优势:
- 天然编码相对位置
- 可以外推到更长序列
- 计算高效
Comparison
| Method | Length Generalization | Training Speed | Implementation |
|---|---|---|---|
| Sinusoidal | Medium | Fast | Simple |
| Learned | Poor | Fast | Simple |
| RoPE | Good | Fast | Medium |
| ALiBi | Best | Fast | Simple |
Practical Advice
- 短序列任务:Learned Encoding 足够
- 长序列任务:选择 RoPE 或 ALiBi
- 需要外推:避免 Learned Encoding
Complete Implementation
import torch
import torch.nn as nn
import math
class TransformerWithPosition(nn.Module):
def __init__(self, d_model=512, max_len=5000, nhead=8):
super().__init__()
self.d_model = d_model
# Positional Encoding
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len).unsqueeze(1)
div_term = torch.exp(
torch.arange(0, d_model, 2) * -(math.log(10000.0) / d_model)
)
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
self.register_buffer('pe', pe.unsqueeze(0))
# Transformer Layer
self.transformer = nn.TransformerEncoder(
nn.TransformerEncoderLayer(d_model, nhead),
num_layers=6
)
def forward(self, x):
# x: (batch, seq_len, d_model)
x = x + self.pe[:, :x.size(1)]
return self.transformer(x)
Summary
Positional Encoding 是 Transformer 的关键组件,它让排列不变的 Attention 机制能够理解序列顺序。正弦编码的巧妙设计展示了数学之美——用简单的三角函数实现了丰富的位置表示。
理解 Positional Encoding,有助于理解为什么现代模型选择 RoPE,以及如何设计更好的序列建模架构。
Further Reading: