PyTorch 2.x 深度学习专题【左扬精讲】—— 位置编码详解:正弦位置编码、相对位置编码、旋转位置编码(RoPE)
PyTorch 2.x 深度学习专题【左扬精讲】—— 位置编码详解:正弦位置编码、相对位置编码、旋转位置编码(RoPE)
torch.nn.Embedding ← 可学习位置编码的底层实现
torch.nn.functional.scaled_dot_product_attention ← 现代 Attention 支持相对位置偏置
huggingface/transformers ← RotaryEmbedding / RoPE 实现(LLaMA / Mistral / Qwen 等)
facebookresearch/flash-attention ← 高效注意力,支持 RoPE
位置编码 Transformer Attention RoPE 正弦位置编码 相对位置编码 PyTorch 2.x
本文学习重点
- 必须掌握:三种位置编码的数学原理、Why 动机、PyTorch 中的标准实现方式
- 需要理解:正弦位置编码的周期性特征、相对位置编码如何捕获 token 间距离、RoPE 如何通过旋转矩阵实现位置信息注入
- 建议了解:不同位置编码在长序列场景下的优劣对比
目录导航
一、导语:为什么 Transformer 需要位置编码
What — 位置编码是什么?
位置编码(Positional Encoding)是 Transformer 架构中用于为序列中的每个 token 注入位置信息的模块。由于 Transformer 的核心结构——自注意力机制(Self-Attention)——对输入序列是 permutation equivariant 的,即打乱输入序列的顺序不会改变注意力分数的计算结果,这意味着模型本身无法区分 "token A 在位置 1" 和 "token A 在位置 10"。
Why — 为什么需要位置编码?
问题一:Attention 机制本身不感知位置
在自注意力计算中:
Q、K、V 的计算完全基于 token 的内容特征,不包含任何位置信息。因此 "我爱你" 和 "你爱我" 经过注意力计算会产生完全相同的输出(除非 embedding 本身包含位置暗示)。
没有位置编码会发生什么?
- 模型无法区分词语的前后关系,丢失语序信息
- 在机器翻译、文本生成等任务上性能严重下降
- 模型退化为"词袋模型",失去序列建模能力
位置编码的核心思想是将位置信息编码为与 token embedding 同维度的向量,然后与 token embedding 相加(additive)或拼接(concatenation)。形式化表达为:
其中 PE(position) 是位置编码向量,常见实现方式包括:
- 正弦位置编码:使用不同频率的 sin/cos 函数生成
- 可学习位置编码:将位置编码作为可训练参数学习
- 相对位置编码:编码 token 之间的相对距离而非绝对位置
- 旋转位置编码(RoPE):通过旋转矩阵将位置信息注入 Query 和 Key
本节要点
- Self-Attention 本身是 permutation equivariant 的,不感知 token 位置
- 位置编码通过注入位置信息解决这一根本问题
- 主流位置编码分为绝对位置编码和相对位置编码两大类
二、正弦位置编码(Sinusoidal PE)
2.1 理论背景
What — 什么是正弦位置编码?
正弦位置编码源自 Google 2017 年发表的经典论文 "Attention Is All You Need"(Vaswani et al., 2017)。它使用不同频率的正弦和余弦函数为每个位置生成唯一的编码向量。
Why — 为什么选择正弦函数?
动机一:支持任意长度的序列
正弦函数是周期函数,可以泛化到训练时未见过的序列长度。如果训练时最长序列是 512 个 token,测试时来了 1000 个 token,正弦位置编码依然能生成位置 513、514、...、999 的编码向量,因为它只是数学公式,算到哪就生成到哪。而可学习的位置编码在初始化时就固定好了 (max_len, d_model) 的查找表,超出 max_len 的位置根本没有对应的可学习向量,根本无法处理。
动机二:相对位置可以通过线性变换表示
这是正弦位置编码最精妙的设计。观察正弦位置编码的公式:
可以证明,PE(pos + k) 可以写成 PE(pos) 乘以一个与 k 相关的矩阵 M_k 的形式,即 PE(pos + k) = M_k * PE(pos)。这意味着什么呢?模型不需要直接"记住"位置 10 和位置 5 的编码分别是多少,它只需要学会"当前 token 距离目标 token 3 个位置"这个相对关系,就能通过简单的线性变换(矩阵乘法)将当前编码转换为目标的编码。换句话说,模型学会了"翻译"相对距离,而不需要死记硬背每个绝对位置。
没有正弦位置编码会发生什么?
- 模型无法处理变长序列,必须在训练时固定最大长度
- 丢失相对位置信息,句子中词语间的距离关系无法被利用
- 模型退化为词袋模型,无法区分语序
标准实现来自 Hugging Face Transformers 库的 get_sinusoidal_positional_encoding 函数,核心逻辑如下:
import torch
import math
def get_sinusoidal_positional_encoding(seq_len, embed_dim):
# 创建位置编码矩阵,形状为 (seq_len, embed_dim)
pe = torch.zeros(seq_len, embed_dim)
# position 表示位置索引,从 0 到 seq_len-1
position = torch.arange(0, seq_len).unsqueeze(1).float() # (seq_len, 1)
# div_term 用于生成不同频率的缩放因子
# embed_dim 必须为偶数,才能均匀分配给 sin 和 cos
div_term = torch.exp(
torch.arange(0, embed_dim, 2).float() * (-math.log(10000.0) / embed_dim)
) # (embed_dim/2,)
# 偶数维度:sin(position * div_term)
pe[:, 0::2] = torch.sin(position * div_term)
# 奇数维度:cos(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
return pe # 返回形状 (seq_len, embed_dim) 的位置编码张量
# 示例:生成 512 位置、512 维的位置编码
seq_len, embed_dim = 512, 512
pos_encoding = get_sinusoidal_positional_encoding(seq_len, embed_dim)
print(f"位置编码形状: {pos_encoding.shape}") # torch.Size([512, 512])
print(f"位置 0 的编码前 8 维: {pos_encoding[0, :8]}") # 全为 0
print(f"位置 1 的编码前 8 维: {pos_encoding[1, :8]}") # sin(div_term) 值
设计意图:使用指数衰减的频率(div_term),低频维度编码长距离关系,高频维度编码短距离关系,这与自然语言的局部性和长程依赖特性相吻合。
2.2 与 Token Embedding 的结合
What — 如何将位置编码加入 Transformer?
在原始 Transformer 架构中,位置编码与 token embedding 直接相加(additive),然后送入 Encoder 或 Decoder。
import torch
import torch.nn as nn
import math
class SinusoidalPositionalEncoding(nn.Module):
def __init__(self, d_model, max_len=5000):
super().__init__()
# 预计算并注册为 buffer(不参与梯度计算但会随模型保存/加载)
pe = get_sinusoidal_positional_encoding(max_len, d_model)
self.register_buffer('pe', pe) # 注册为 buffer,不计入梯度
def forward(self, x):
# x 形状: (batch_size, seq_len, d_model)
seq_len = x.size(1)
# 直接相加:token_embedding + positional_encoding
x = x + self.pe[:seq_len, :]
return x
# 使用示例
batch_size, seq_len, d_model = 2, 128, 512
token_embeds = torch.randn(batch_size, seq_len, d_model) # 模拟 token embedding
pos_encoder = SinusoidalPositionalEncoding(d_model, max_len=5000)
output = pos_encoder(token_embeds)
print(f"输出形状: {output.shape}") # torch.Size([2, 128, 512])
本节要点
- 正弦位置编码使用 sin/cos 交替函数生成,支持任意长度泛化
- 低频维度编码长距离位置,高频维度编码短距离位置
- 通过与 token embedding 相加的方式注入位置信息
三、可学习位置编码(Learnable PE)
What — 什么是可学习位置编码?
可学习位置编码(Learnable Positional Encoding)将位置编码向量作为可训练的参数,使用 torch.nn.Embedding 创建一个形状为 (max_len, d_model) 的查找表,模型在训练过程中自动学习最优的位置表示。
Why — 什么时候选择可学习位置编码?
优点:
- 灵活性高:位置编码可以适应具体任务数据分布
- 实现简单:直接复用 nn.Embedding 接口
- BERT、GPT 等主流模型广泛采用
缺点与局限:
- 无法泛化到训练时设定的 max_len 之外的序列长度
- 需要更多训练数据来学习位置表示
没有可学习位置编码会发生什么?
- 位置编码的表示能力受限于手工设计的函数形式
- 无法针对特定任务数据自适应调整位置表示
可学习位置编码的实现非常直接,利用 nn.Embedding 的查找表机制:
import torch
import torch.nn as nn
class LearnablePositionalEncoding(nn.Module):
def __init__(self, d_model, max_len=5000, dropout=0.1):
super().__init__()
# 创建可学习的位置编码表,形状 (max_len, d_model)
# 初始化为正态分布,均值为 0,标准差与 d_model 相关
self.position_embeddings = nn.Embedding(max_len, d_model)
nn.init.trunc_normal_(self.position_embeddings.weight, std=0.02)
self.dropout = nn.Dropout(p=dropout)
def forward(self, x):
# x 形状: (batch_size, seq_len, d_model)
batch_size, seq_len, d_model = x.size()
# 生成位置索引:[0, 1, 2, ..., seq_len-1]
position_ids = torch.arange(seq_len, device=x.device).unsqueeze(0).expand(batch_size, -1)
# 通过 Embedding 查找获取位置编码
pos_embeddings = self.position_embeddings(position_ids) # (batch_size, seq_len, d_model)
# 与 token embedding 相加
x = x + pos_embeddings
x = self.dropout(x)
return x
# 使用示例
batch_size, seq_len, d_model = 2, 128, 512
token_embeds = torch.randn(batch_size, seq_len, d_model)
pos_encoder = LearnablePositionalEncoding(d_model, max_len=5000)
output = pos_encoder(token_embeds)
print(f"输出形状: {output.shape}") # torch.Size([2, 128, 512])
设计意图:位置编码作为模型参数,通过反向传播自动优化,适配具体任务的数据分布。注意初始化使用截断正态分布(trunc_normal_),避免过大或过小的初始值影响训练稳定性。
本节要点
- 可学习位置编码使用 nn.Embedding 实现,参数随训练更新
- 灵活性高,但受限于 max_len 的设定
- BERT、GPT 系列模型广泛采用此方式
四、相对位置编码(Relative PE)
4.1 理论背景
What — 什么是相对位置编码?
相对位置编码(Relative Positional Encoding)由 Shaw et al. 在 2018 年的论文 "Self-Attention with Relative Position Representations" 中提出。与绝对位置编码编码 token 的绝对位置不同,相对位置编码编码 token 之间的相对距离,即 "token i 和 token j 之间的距离是 k"。
Why — 为什么需要相对位置编码?
动机一:语言中的相对位置比绝对位置更重要
在自然语言中,"主语在动词左边 3 个词" 比 "主语在位置 5" 更具有语义意义。相对位置直接编码词语之间的距离关系。
动机二:提升长序列建模能力
绝对位置编码在长序列时位置值会变得很大,而相对位置编码的距离值是有界的(通常 clip 到 [-K, K]),更稳定。
没有相对位置编码会发生什么?
- 模型难以学习词语间的距离依赖关系
- 长序列场景下位置值过大,编码质量下降
- 在需要精确相对距离的任务(如代码理解)上表现不佳
相对位置编码的核心修改在 Attention 计算中引入相对位置偏置项:
import torch
import torch.nn as nn
import torch.nn.functional as F
import math
class RelativePositionEmbedding(nn.Module):
def __init__(self, embed_dim, max_distance=128, num_heads=8):
super().__init__()
self.embed_dim = embed_dim
self.num_heads = num_heads
self.max_distance = max_distance
# 创建相对位置的键值对 (2 * max_distance + 1) 个
self.relative_attention_bias = nn.Embedding(2 * max_distance + 1, num_heads)
def compute_relative_positions(self, seq_len):
# 生成相对位置矩阵:position_i - position_j
# 形状: (seq_len, seq_len)
position_ids = torch.arange(seq_len, device='cuda' if torch.cuda.is_available() else 'cpu')
relative_positions = position_ids.unsqueeze(1) - position_ids.unsqueeze(0) # (seq_len, seq_len)
# 将相对位置 clip 到 [-max_distance, max_distance]
relative_positions = relative_positions.clamp(-self.max_distance, self.max_distance)
# 转换为非负索引:[0, 2*max_distance]
relative_positions = relative_positions + self.max_distance
return relative_positions # (seq_len, seq_len)
def forward(self, seq_len):
# 获取相对位置矩阵
relative_positions = self.compute_relative_positions(seq_len) # (seq_len, seq_len)
# 通过 Embedding 获取相对位置偏置,形状 (seq_len, seq_len, num_heads)
relative_attention_bias = self.relative_attention_bias(relative_positions) # (seq_len, seq_len, num_heads)
# 转换为 Attention 兼容的形状 (1, num_heads, seq_len, seq_len)
return relative_attention_bias.permute(2, 0, 1).unsqueeze(0)
# 使用示例
embed_dim, num_heads, seq_len = 512, 8, 128
relative_pos = RelativePositionEmbedding(embed_dim, max_distance=64, num_heads=num_heads)
bias = relative_pos(seq_len) # (1, num_heads, seq_len, seq_len)
print(f"相对位置偏置形状: {bias.shape}") # torch.Size([1, 8, 128, 128])
4.2 相对位置编码在现代 Transformer 中的应用
What — 什么是注意力偏置(Attention Bias)?
在 torch.nn.functional.scaled_dot_product_attention 中,可以通过 attn_mask 参数引入相对位置偏置。现代模型如 Swin Transformer、DeiT 等广泛使用此机制。
import torch
import torch.nn as nn
import torch.nn.functional as F
import math
class RelativeAttention(nn.Module):
def __init__(self, embed_dim, num_heads, max_distance=128):
super().__init__()
self.embed_dim = embed_dim
self.num_heads = num_heads
self.head_dim = embed_dim // num_heads
# QKV 投影层
self.q_proj = nn.Linear(embed_dim, embed_dim)
self.k_proj = nn.Linear(embed_dim, embed_dim)
self.v_proj = nn.Linear(embed_dim, embed_dim)
self.out_proj = nn.Linear(embed_dim, embed_dim)
# 相对位置偏置
self.relative_pos = RelativePositionEmbedding(self.head_dim, max_distance, num_heads)
def forward(self, x):
batch_size, seq_len, _ = x.size()
# QKV 投影
Q = self.q_proj(x).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
K = self.k_proj(x).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
V = self.v_proj(x).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
# 获取相对位置偏置
attn_bias = self.relative_pos(seq_len) # (1, num_heads, seq_len, seq_len)
# 使用 SDPA 计算注意力,加入相对位置偏置
attn_mask = attn_bias # 直接作为 attn_mask 传入
attn_output = F.scaled_dot_product_attention(
Q, K, V, attn_mask=attn_mask
)
# 合并多头输出
attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, seq_len, self.embed_dim)
return self.out_proj(attn_output)
# 使用示例
batch_size, seq_len, embed_dim = 2, 64, 512
x = torch.randn(batch_size, seq_len, embed_dim)
attn = RelativeAttention(embed_dim, num_heads=8, max_distance=32)
output = attn(x)
print(f"输出形状: {output.shape}") # torch.Size([2, 64, 512])
本节要点
- 相对位置编码编码 token 之间的相对距离,而非绝对位置
- 通过在 Attention 计算中加入相对位置偏置实现
- 相对距离通常 clip 到固定范围(如 [-64, 64]),避免参数爆炸
- 现代 Swin Transformer、DeiT 等模型广泛采用
五、旋转位置编码(RoPE)
5.1 理论背景
What — 什么是旋转位置编码(RoPE)?
旋转位置编码(Rotary Positional Encoding,简称 RoPE)由 Su et al. 在 2022 年的论文 "RoFormer: Enhanced Transformer with Rotary Position Embedding" 中提出。RoPE 的核心思想是通过对 Query 和 Key 向量进行旋转操作,将位置信息编码到向量空间中,而不是简单地在 embedding 上叠加。
Why — RoPE 为什么有效?
动机一:内积形式的旋转不变性
RoPE 的关键洞察是:在自注意力中,我们实际计算的是 Q 和 K 的点积。通过对 Q 和 K 应用旋转矩阵,可以使得旋转后的点积包含位置信息:
这意味着旋转后的点积只依赖于相对位置 m-n,而不依赖于绝对位置。
动机二:无需显式添加偏置
与相对位置编码需要在 Attention 计算中添加偏置项不同,RoPE 通过修改 Q 和 K 本身来注入位置信息,计算更加高效。
动机三:支持长上下文
RoPE 天然支持任意长度的序列,无需预设 max_len,是 LLaMA、Mistral、Qwen 等主流 LLM 的核心位置编码方案。
没有 RoPE 会发生什么?
- 无法获得理论上无限的上下文长度支持
- 长序列场景下位置编码质量下降
- 无法享受 FlashAttention 等高效注意力机制的加速
RoPE 将向量按维度两两配对,每对应用二维旋转矩阵:
import torch
import torch.nn as nn
import math
def precompute_freqs_cis(dim, end_idx, theta=10000.0):
# 预计算旋转角度频率
# dim: embedding 维度(应为偶数)
# theta: 缩放因子,控制频率衰减速度
freqs = 1.0 / (theta ** (torch.arange(0, dim, 2).float() / dim)) # (dim/2,)
# 生成角度:position * freqs
idx = torch.arange(0, end_idx, device=freqs.device) # (end_idx,)
angle = idx.unsqueeze(1) * freqs.unsqueeze(0) # (end_idx, dim/2)
# 复数形式:cos + i*sin
freqs_cis = torch.polar(torch.ones_like(angle), angle) # (end_idx, dim/2) 复数
return freqs_cis # 返回复数形式的角度因子
def apply_rotary_emb(x, freqs_cis):
# x: (batch_size, seq_len, num_heads, head_dim) 或 (batch_size, seq_len, head_dim)
# freqs_cis: (seq_len, head_dim/2) 复数
x_complex = torch.view_as_complex(x.float().reshape(*x.shape[:-1], -1, 2)) # 转换为复数形式
freqs_cis = freqs_cis.unsqueeze(0).unsqueeze(0) # (1, 1, seq_len, head_dim/2)
# 旋转:乘以 freqs_cis(复数乘法实现旋转)
x_rotated = x_complex * freqs_cis # (batch_size, seq_len, num_heads, head_dim/2) 复数
# 转回实数形式
x_out = torch.view_as_real(x_rotated).flatten(-2) # (batch_size, seq_len, num_heads, head_dim)
return x_out.type_as(x) # 保持原始 dtype
class RotaryPositionalEmbedding(nn.Module):
def __init__(self, dim, max_seq_len=4096, theta=10000.0):
super().__init__()
self.dim = dim
self.max_seq_len = max_seq_len
self.theta = theta
# 注册为 buffer,与模型一起保存/加载
self.register_buffer('freqs_cis', precompute_freqs_cis(dim, max_seq_len, theta))
def forward(self, seq_len):
# 返回对应序列长度的频率复数
return self.freqs_cis[:seq_len]
# 使用示例
batch_size, seq_len, num_heads, head_dim = 2, 128, 8, 64
x = torch.randn(batch_size, seq_len, num_heads, head_dim) # Q 或 K
rope = RotaryPositionalEmbedding(head_dim, max_seq_len=2048)
freqs_cis = rope(seq_len) # (seq_len, head_dim/2)
x_rotated = apply_rotary_emb(x, freqs_cis)
print(f"输入形状: {x.shape}, 输出形状: {x_rotated.shape}") # torch.Size([2, 128, 8, 64])
5.2 RoPE 在 LLM 中的实际应用
What — LLaMA 等模型如何使用 RoPE?
在实际的 LLM 训练框架(如 Hugging Face Transformers)中,RoPE 被集成到 Attention 模块中,在计算 Q、K 投影后立即应用旋转,然后进行注意力计算。
import torch
import torch.nn.functional as F
import math
class RoPEAttention(nn.Module):
def __init__(self, embed_dim, num_heads, max_seq_len=4096, rope_theta=10000.0):
super().__init__()
self.embed_dim = embed_dim
self.num_heads = num_heads
self.head_dim = embed_dim // num_heads
assert self.head_dim % 2 == 0, "RoPE 要求 head_dim 为偶数"
# QKV 投影
self.q_proj = nn.Linear(embed_dim, embed_dim)
self.k_proj = nn.Linear(embed_dim, embed_dim)
self.v_proj = nn.Linear(embed_dim, embed_dim)
self.out_proj = nn.Linear(embed_dim, embed_dim)
# RoPE 位置编码
self.rotary_emb = RotaryPositionalEmbedding(self.head_dim, max_seq_len, rope_theta)
def forward(self, x, attention_mask=None, position_ids=None):
batch_size, seq_len, _ = x.size()
# QKV 投影
Q = self.q_proj(x).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
K = self.k_proj(x).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
V = self.v_proj(x).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
# 对 Q 和 K 应用 RoPE
freqs_cis = self.rotary_emb(seq_len)
Q = apply_rotary_emb(Q, freqs_cis) # 对 Query 旋转
K = apply_rotary_emb(K, freqs_cis) # 对 Key 旋转
# 使用 SDPA 计算注意力
attn_output = F.scaled_dot_product_attention(Q, K, V, attn_mask=attention_mask)
# 合并多头输出
attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, seq_len, self.embed_dim)
return self.out_proj(attn_output)
# 使用示例:模拟 LLaMA 的 Attention 层
batch_size, seq_len, embed_dim = 1, 256, 4096
x = torch.randn(batch_size, seq_len, embed_dim)
attn = RoPEAttention(embed_dim, num_heads=32, max_seq_len=8192, rope_theta=10000.0)
output = attn(x)
print(f"输入形状: {x.shape}, 输出形状: {output.shape}") # torch.Size([1, 256, 4096])
RoPE 与其他位置编码的关键区别
- 正弦位置编码和可学习位置编码是加到 embedding 上的绝对位置编码
- 相对位置编码是在注意力分数上添加的偏置项
- RoPE 是对 Q 和 K 本身应用旋转操作,直接将位置信息编码到向量中
- RoPE 使得旋转后的 Q、K 点积天然包含相对位置信息,无需额外偏置
本节要点
- RoPE 通过对 Q、K 应用旋转矩阵实现位置编码
- 旋转后的点积天然包含相对位置信息(m-n)
- 支持理论上的无限上下文长度,是 LLaMA、Mistral 等 LLM 的标准选择
- 与 FlashAttention 等高效注意力机制完美兼容
六、三种位置编码的对比与适用场景
What — 如何选择合适的位置编码?
不同的位置编码方案各有优劣,需要根据具体任务和模型架构进行选择。
| 特性 | 正弦位置编码 | 可学习位置编码 | 相对位置编码 | RoPE |
|---|---|---|---|---|
| 位置类型 | 绝对位置 | 绝对位置 | 相对位置 | 相对位置 |
| 序列长度泛化 | 支持任意长度 | 不支持(固定 max_len) | 支持(有界距离 clip) | 支持任意长度 |
| 计算开销 | 低(预计算) | 低(Embedding 查找) | 中等(额外偏置计算) | 低(旋转操作) |
| 与 FlashAttention 兼容 | 是 | 是 | 需要特殊处理 | 完美兼容 |
| 典型应用 | 原始 Transformer | BERT、GPT-2 | Swin Transformer | LLaMA、Mistral、Qwen |
选型建议
- 短序列 + 分类任务:可学习位置编码或正弦位置编码即可
- 长上下文 LLM:优先选择 RoPE,支持长序列且计算高效
- 视觉 Transformer:相对位置编码(如 Swin Transformer 的 Shifted Windows)
- 代码生成模型:RoPE,对精确的相对位置依赖更强
本节要点
- 正弦位置编码适合简单场景,可泛化到任意长度
- 可学习位置编码灵活性高,但受限于 max_len
- 相对位置编码在视觉 Transformer 中广泛使用
- RoPE 是当前 LLM 的主流选择,平衡了性能和效率
七、FAQ(20 组)
以下是关于位置编码的常见问题解答,分为三个部分:基础概念、与正弦/可学习编码相关、RoPE 进阶问题。
第一部分:基础概念
Q1. Transformer 为什么要使用位置编码?
一句话结论:Self-Attention 本身不感知 token 位置,需要额外注入位置信息。 自注意力机制基于矩阵乘法 QK^T 计算相关性,无论 token 出现在序列的哪个位置,只要内容相同,注意力分数就相同。位置编码通过在输入层面或计算过程中注入位置信息,解决了这一根本问题。
Q2. 位置编码可以直接拼接(Concatenation)而不是相加(Addition)吗?
一句话结论:技术上可以,但相加是更常见且高效的选择。 原始 Transformer 论文选择相加而非拼接,主要考虑是参数效率——相加不增加维度,而拼接会使维度翻倍。更深层的原因是可学习的位置编码可以自然地通过线性变换融入 token 表示。
Q3. 位置编码必须和 Token Embedding 放在同一维度空间吗?
一句话结论:不一定,但通常是。 在原始 Transformer 中,PE 和 token embedding 维度相同,可以直接相加。如果维度不同,需要通过线性投影将 PE 投影到 embedding 空间,或将拼接后的向量投影到统一维度。
Q4. 为什么正弦位置编码选择 sin 和 cos 交替的形式?
一句话结论:奇偶维度的 sin/cos 交替使得相邻位置在每个维度上都有不同的表示。 如果全部使用 sin,则偶数和奇数维度完全相同(只差一个相位);交替使用 sin/cos 确保每个位置的编码向量在所有维度上都不同。
Q5. 位置编码可以作用在 Attention 输出上吗?
一句话结论:标准做法是作用在输入层,但理论上可以作用于任何位置。 原始 Transformer 在输入 embedding 后立即添加位置编码,使得整个网络在处理过程中都带有位置信息。一些变体如 "Attention with Relative Position Embeddings" 将相对位置编码加入注意力分数计算。
Q6. 为什么说 Self-Attention 是 permutation equivariant 而不是 permutation invariant?
一句话结论:permutation equivariant 表示输出会随输入顺序变化而相应变化(一一对应),permutation invariant 表示输出完全不变。 对于 Self-Attention,输入 token 打乱顺序后,输出的 token 顺序也跟着打乱——每个输出位置的 token 身份发生了变化(对应到新位置的内容),但整体对应关系保持一致。这是 equivariant 的定义,而非 invariant(完全不变)。例如输入 [A, B, C] 对应输出 [out_A, out_B, out_C],打乱输入顺序后,输出顺序也随之打乱。
Q7. 位置编码的值域范围是多少?
一句话结论:标准正弦位置编码的值域在 [-1, 1] 之间。 由于 sin 和 cos 函数的输出范围是 [-1, 1],所以正弦位置编码的每个维度都在这个范围内。可学习位置编码的值域取决于初始化和训练动态。
Q8. 位置编码和位置嵌入(Positional Embedding)是同一个概念吗?
一句话结论:在大多数语境下是同义词,但严格来说"编码"强调生成方式。 "位置编码"(Positional Encoding)强调通过特定函数(如 sin/cos)生成位置表示;"位置嵌入"(Positional Embedding)强调将位置作为可学习参数。在 Transformer 语境下,两者通常可以互换。
第二部分:正弦与可学习位置编码
Q9. 正弦位置编码可以学习吗?
一句话结论:原始正弦编码不可学习,但可以修改为半可学习版本。 原始 Vaswani 论文中的正弦编码使用固定公式,不包含可学习参数。实践中可以修改为让波长参数 theta 成为可学习的,或在相加后接一个线性变换层引入可学习性。
Q10. 可学习位置编码如何处理训练时未见过的序列长度?
一句话结论:无法直接处理,通常需要外推或微调。 常见策略包括:(1) 在训练时随机 drop 一些位置编码,增强泛化能力;(2) 使用位置插值(Position Interpolation)技术在推理时处理更长序列;(3) 在长序列上进行继续预训练。
Q11. 正弦位置编码为什么使用指数衰减的频率?
一句话结论:指数衰减使得低频维度编码长距离关系,高频维度编码短距离关系。 div_term = theta^(-2i/d) 使得随着 i 增大(向高位维度),频率指数下降。低频(长波长)对应远距离位置,高频(短波长)对应近距离位置,这与语言中局部性和长程依赖的双重需求相匹配。
Q12. 为什么可学习位置编码通常用正态分布初始化?
一句话结论:正态分布初始化保证初始位置编码的稳定性和多样性。 截断正态分布(trunc_normal)确保值不会过大,同时标准差通常设为与 d_model 相关(如 0.02 * sqrt(2/d_model)),使得位置编码的初始量级与 token embedding 相当。
Q13. 位置编码需要 Dropout 吗?
一句话结论:可以加,但不是必须的。 在 transformer 原始实现中,位置编码与 embedding 相加后通常会过 Dropout,但这不是严格的。在大多数预训练模型中,embedding 层的 Dropout 已经覆盖了位置编码。
Q14. 为什么 BERT 使用可学习位置编码而不是正弦编码?
一句话结论:可学习编码更灵活,且 BERT 主要用于分类任务,不需要外推到超长序列。 BERT 的预训练任务是 MLM,不需要生成超长文本,且序列长度固定为 512。可学习编码的参数效率高,实验表明在这类任务上表现良好。
第三部分:RoPE 进阶问题
Q15. RoPE 的旋转角度是如何计算的?
一句话结论:旋转角度 = position * (theta^(-2i/d)),其中 theta 通常为 10000。 角度计算公式源自苏剑林等人的论文,用于生成不同维度的旋转频率。theta=10000 是原始论文的默认值,类似于正弦编码中的 10000。
Q16. RoPE 为什么要求 head_dim 为偶数?
一句话结论:因为 RoPE 需要将向量按 2 维配对进行二维旋转。 二维旋转矩阵作用于 2D 向量,所以 head_dim 必须能被 2 整除。如果 head_dim 是奇数,最后一维无法配对,通常需要将最后一维直接保留或单独处理。
Q17. RoPE 和复数有什么关系?
一句话结论:RoPE 的旋转操作在复数空间中实现更高效。 二维旋转本质上等价于复数乘法:(cosθ + i*sinθ) * (x + iy)。使用复数实现可以避免显式的三角函数计算,利用 torch.polar 和 view_as_complex/view_as_real 实现高效旋转。
Q18. 为什么 RoPE 在长序列场景下优于其他方案?
一句话结论:RoPE 无需预设 max_len,且与 FlashAttention 等高效注意力机制完美兼容。 可学习位置编码受限于 max_len,正弦编码虽然支持外推但效率较低。RoPE 通过预计算频率因子支持任意长度,同时旋转操作可以融合到注意力计算中,享受 FlashAttention 的融合 kernel 加速。
Q19. LLaMA 2 和 LLaMA 3 的 RoPE 有什么改进?
一句话结论:主要改进是增大 rope_theta 参数以提升频率分辨率。 LLaMA 2 将 rope_theta 从默认的 10000 提升到 50000,LLaMA 3 进一步提升。更高的 theta 意味着更慢的频率衰减速度(theta^(-2i/d) 中的分母更大),使得低频维度对应更长波长,模型在更长距离上仍能保持有效的位置感知能力,从而提升长上下文利用率。
Q20. RoPE 可以和其他位置编码结合使用吗?
一句话结论:理论上可以,但实践中通常单独使用 RoPE。 混合位置编码(Hybrid PE)如 "RoPE with ALiBi" 等方案存在,但主流 LLM 选择单一方案以简化实现和保证稳定性。RoPE 本身已经能够很好地捕获相对位置信息。
FAQ 总纲
- 位置编码解决 Self-Attention 不感知位置的根本问题
- 正弦编码支持外推,可学习编码更灵活但受限于 max_len
- RoPE 是当前 LLM 主流方案,支持长序列且计算高效
- 位置编码选择需要根据具体任务和模型架构决定
八、Roadmap 预告
后续专题预告
- 专题十七:FlashAttention 1/2/3 详解:从注意力计算到 IO 优化
- 专题十八:Attention 变体大盘点:Multi-Query Attention、Grouped Query Attention、FlashAttention
- 专题十九:Transformer 训练技巧:从梯度裁剪到混合精度,从 Warmup 到 Cosine Annealing
- 专题二十:大模型分布式训练:DeepSpeed ZeRO、PyTorch FSDP、模型并行策略
欢迎持续关注,共同深入探索深度学习的核心技术。

浙公网安备 33010602011771号