JM
← 返回文章
约 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 使用正弦和余弦函数:

PE(pos,2i)=sin⁡(pos100002i/dmodel)PE_{(pos, 2i)} = \sin\left(\frac{pos}{10000^{2i/d_{\text{model}}}}\right)

PE(pos,2i+1)=cos⁡(pos100002i/dmodel)PE_{(pos, 2i+1)} = \cos\left(\frac{pos}{10000^{2i/d_{\text{model}}}}\right)

每个维度的频率不同:

  • 维度 0:波长 = 2π2\pi,变化最快
  • 维度 d−1d-1:波长 = 2π×100002\pi \times 10000,变化最慢
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. 可以表示相对位置

关键性质:PE(pos+k)PE_{(pos+k)} 可以用 PE(pos)PE_{(pos)} 的线性变换表示:

PE(pos+k)=Mk⋅PE(pos)PE_{(pos+k)} = M_k \cdot PE_{(pos)}

其中 MkM_k 是只依赖于 kk 的旋转矩阵。这意味着模型可以学习相对位置关系。

2. 值域有界

−1≤sin⁡(x)≤1-1 \leq \sin(x) \leq 1 −1≤cos⁡(x)≤1-1 \leq \cos(x) \leq 1

不会随着位置增长而发散。

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 计算中编码位置:

RoPE(x,pos)=x⋅cos⁡(pos)+rotate_half(x)⋅sin⁡(pos)\text{RoPE}(x, pos) = x \cdot \cos(pos) + \text{rotate\_half}(x) \cdot \sin(pos)

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

MethodLength GeneralizationTraining SpeedImplementation
SinusoidalMediumFastSimple
LearnedPoorFastSimple
RoPEGoodFastMedium
ALiBiBestFastSimple

Practical Advice

  1. 短序列任务:Learned Encoding 足够
  2. 长序列任务:选择 RoPE 或 ALiBi
  3. 需要外推:避免 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: