08 位置编码详解:Sinusoidal、RoPE、ALiBi 为什么重要?
在前面的文章中我们已经讲过 Transformer 的整体结构、Self-Attention、Encoder、Decoder。但是这里还有一个非常关键的问题Transformer 是怎么知道 token 顺序的例如下面两个句子我 喜欢 你 你 喜欢 我它们包含的 token 很相似但语义完全不同。如果模型不知道顺序那么它很难区分谁喜欢谁 哪个词在前 哪个词在后 两个 token 相隔多远RNN 天然按时间步读取序列因此顺序信息隐含在递归结构里。CNN 通过卷积窗口和局部结构感知相邻关系。但是 Transformer 的 Self-Attention 是并行计算的。Self-Attention 本身只看 token 之间的相关性这里的计算的是 token 与 token 之间的匹配分数但它并不知道第几个 token 在前、第几个 token 在后。所以 Transformer 必须额外引入位置信息。这就是Positional Encoding也就是位置编码。本文重点讲三种重要位置编码方式Sinusoidal Positional Encoding RoPERotary Position Embedding ALiBiAttention with Linear Biases它们分别代表了三种不同思路Sinusoidal把位置编码加到 token embedding 上 RoPE把位置通过旋转注入到 Q 和 K 中 ALiBi不加位置向量而是在 attention score 上加距离惩罚一、为什么 Transformer 必须要位置编码Self-Attention 的核心计算是它比较的是 token 表示之间的相似度。假设有三个 tokenx1, x2, x3Self-Attention 会让每个 token 和其他 token 计算注意力权重x1 关注 x1, x2, x3 x2 关注 x1, x2, x3 x3 关注 x1, x2, x3但是如果不加入位置编码模型看到的只是一个 token 集合而不是有顺序的序列。例如我 喜欢 你和你 喜欢 我如果只看 token 集合它们都包含我 喜欢 你但顺序不同语义完全不同。所以 Transformer 需要额外告诉模型这个 token 是什么 这个 token 在哪里 这个 token 和其他 token 相隔多远其中token embedding 负责告诉模型这个 token 是什么 position encoding 负责告诉模型这个 token 在哪里最终输入 Transformer 的通常是这就是位置编码最基础的作用。二、绝对位置和相对位置在讲具体方法前我们先区分两个概念绝对位置 相对位置1. 绝对位置绝对位置关注的是当前 token 是第几个 token例如我 喜欢 机器 学习可以给每个 token 一个位置编号我位置 0 喜欢位置 1 机器位置 2 学习位置 3Sinusoidal Positional Encoding 和 learned positional embedding 都属于绝对位置编码思路。它们会给每个位置分配一个位置向量然后加到 token embedding 上。2. 相对位置相对位置关注的是两个 token 之间相隔多远例如第 5 个 token 和第 7 个 token 相距 2 第 5 个 token 和第 100 个 token 相距 95很多语言关系更依赖相对距离而不是绝对编号。例如形容词通常修饰附近的名词 代词可能指向前面某个名词 括号、引号、代码块存在局部或层级关系RoPE 和 ALiBi 都更强调相对位置关系。这也是为什么它们在现代大语言模型中非常重要。三、Sinusoidal Positional Encoding原始 Transformer 的位置编码原始 Transformer 使用的是 Sinusoidal Positional Encoding也就是正弦余弦位置编码。它不是可训练参数而是通过固定公式生成。公式如下其中postoken 在序列中的位置i维度索引模型隐藏维度偶数维使用奇数维使用。例如如果那么位置编码的维度可以理解为第 0 维sin 第 1 维cos 第 2 维sin 第 3 维cos 第 4 维sin 第 5 维cos 第 6 维sin 第 7 维cos不同维度使用不同频率的正弦余弦函数。低维度变化更快高维度变化更慢。这样每个位置都会得到一个独特的位置向量。四、为什么 Sinusoidal 要用不同频率如果只用一个频率模型只能在一个尺度上感知位置。但是语言中的位置关系有不同尺度。例如相邻 token 的关系 短语内部的关系 句子内部的关系 段落内部的关系 长文档中的远距离关系所以位置编码需要同时包含高频和低频信息。高频维度变化快更适合区分相邻位置。低频维度变化慢更适合表示较长距离的位置关系。这就是 Sinusoidal Positional Encoding 使用多频率 sin / cos 的原因。可以直观理解为不同维度相当于不同尺度的位置尺子模型可以根据任务需要选择使用哪种尺度的位置关系。五、Sinusoidal Positional Encoding 代码实现下面是一个 PyTorch 实现。import torch import torch.nn as nn import math class SinusoidalPositionalEncoding(nn.Module): def __init__(self, d_model: int, dropout: float 0.1, max_len: int 5000): super().__init__() self.dropout nn.Dropout(dropout) # pe: [max_len, d_model] pe torch.zeros(max_len, d_model) # position: [max_len, 1] position torch.arange(0, max_len, dtypetorch.float).unsqueeze(1) # div_term: [d_model / 2] div_term torch.exp( torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model) ) # 偶数维使用 sin pe[:, 0::2] torch.sin(position * div_term) # 奇数维使用 cos pe[:, 1::2] torch.cos(position * div_term) # 增加 batch 维度[1, max_len, d_model] pe pe.unsqueeze(0) # pe 不是可训练参数但要随模型保存、加载和移动设备 self.register_buffer(pe, pe) def forward(self, x: torch.Tensor) - torch.Tensor: x: [batch_size, seq_len, d_model] seq_len x.size(1) # 取出当前序列长度对应的位置编码 x x self.pe[:, :seq_len] return self.dropout(x) if __name__ __main__: batch_size 2 seq_len 4 d_model 8 x torch.zeros(batch_size, seq_len, d_model) pe_layer SinusoidalPositionalEncoding(d_modeld_model, dropout0.0) out pe_layer(x) print(output shape:, out.shape) print(out[0])输出形状是[batch_size, seq_len, d_model]也就是[2, 4, 8]位置编码不会改变输入形状只是在 token embedding 上加了位置信息。六、Sinusoidal 的优点和局限Sinusoidal Positional Encoding 的优点是不需要训练 实现简单 可以为任意位置生成编码 和原始 Transformer 结构一致但是它也有一些局限。第一它是绝对位置编码。也就是说它更直接告诉模型这个 token 是第几个位置但很多语言关系其实更依赖相对距离两个 token 相隔多远 谁在谁前面 是否是附近 token第二它是加到 token embedding 上的。输入一开始就把语义信息和位置信息混合在一起。后续 attention 只能从混合后的表示中自己学习位置关系。第三对于长上下文外推Sinusoidal 虽然可以生成训练长度之外的位置编码但模型是否能稳定利用这些新位置并不是天然保证的。这也是后来 RoPE、ALiBi 等方法出现的重要原因。七、RoPE把位置编码变成旋转RoPE 的全称是Rotary Position Embedding中文通常叫旋转位置编码它和 Sinusoidal 最大的不同是RoPE 不把位置编码直接加到 token embedding 上而是把位置信息注入到 Query 和 Key 中。在 Self-Attention 中注意力分数来自其中第 i 个 token 的 Query第 j 个 token 的 Key。RoPE 的做法是根据 token 的位置对和做旋转变换。可以写成其中第 i个位置对应的旋转矩阵第 j个位置对应的旋转矩阵、加入位置信息后的 Query 和 Key。然后 attention score 变成这个设计非常关键因为旋转矩阵有一个重要性质可以转化成与相对距离 j-i有关的形式。也就是说RoPE 虽然使用了绝对位置的旋转角度但在 attention score 中自然体现出相对位置信息。这就是 RoPE 的核心价值。八、RoPE 的二维旋转直观理解为了理解 RoPE可以先看二维向量。假设一个二维向量是旋转角度为旋转矩阵是旋转后的向量是RoPE 会把高维向量拆成很多二维对第 0、1 维作为一组 第 2、3 维作为一组 第 4、5 维作为一组 ...然后对每一组二维向量按照当前位置进行旋转。不同维度组使用不同频率。这和 Sinusoidal 一样也有多尺度位置建模能力。九、RoPE 为什么适合大语言模型RoPE 的优势主要有三点。1. 它直接作用在 Attention 里Sinusoidal 是token embedding position encodingRoPE 是Q 和 K 根据位置旋转也就是说RoPE 直接影响 attention score。由于 Self-Attention 本质上就是通过计算 token 之间的关系所以把位置注入 Q 和 K 非常自然。2. 它天然包含相对位置信息RoPE 通过旋转性质让两个 token 的 attention score 和相对距离有关。这对于语言建模非常重要。因为在文本中很多关系不是取决于绝对位置而是取决于相对距离。例如当前词和前一个词 当前词和前一句中的主语 当前括号和对应的左括号 当前代码缩进和上文结构这些都更依赖相对位置。3. 它适合自回归语言模型GPT 类模型每次生成下一个 token需要持续扩展上下文。RoPE 不需要为每个最大长度学习一个固定位置表而是可以按公式生成不同位置的旋转角度。因此它在现代 Decoder-only 大语言模型中非常常见。十、RoPE 代码实现下面给出一个简化版 RoPE 实现。输入一般是 attention 中的 q 和 k。形状为[batch_size, heads, seq_len, head_dim]代码如下import torch def rotate_half(x): 把最后一维两两分组做二维旋转中的 (-x2, x1) 操作。 x: [..., dim] x_even x[..., 0::2] x_odd x[..., 1::2] # [-x_odd, x_even] x_rotated torch.stack((-x_odd, x_even), dim-1) return x_rotated.flatten(-2) def apply_rope(q, k, base10000): q, k: [batch_size, heads, seq_len, head_dim] batch_size, heads, seq_len, head_dim q.shape device q.device # 每两个维度一组所以取 0,2,4,... dim_idx torch.arange(0, head_dim, 2, devicedevice).float() # 频率项 inv_freq 1.0 / (base ** (dim_idx / head_dim)) # 位置编号 positions torch.arange(seq_len, devicedevice).float() # angles: [seq_len, head_dim/2] angles torch.einsum(i,j-ij, positions, inv_freq) # 扩展成 [seq_len, head_dim] angles torch.repeat_interleave(angles, repeats2, dim-1) # [1, 1, seq_len, head_dim] cos torch.cos(angles).unsqueeze(0).unsqueeze(0) sin torch.sin(angles).unsqueeze(0).unsqueeze(0) q_rope q * cos rotate_half(q) * sin k_rope k * cos rotate_half(k) * sin return q_rope, k_rope if __name__ __main__: batch_size 1 heads 2 seq_len 4 head_dim 8 q torch.randn(batch_size, heads, seq_len, head_dim) k torch.randn(batch_size, heads, seq_len, head_dim) q_rope, k_rope apply_rope(q, k) print(q shape:, q.shape) print(q_rope shape:, q_rope.shape) print(k_rope shape:, k_rope.shape)输出q shape: torch.Size([1, 2, 4, 8]) q_rope shape: torch.Size([1, 2, 4, 8]) k_rope shape: torch.Size([1, 2, 4, 8])可以看到RoPE 不改变张量形状。它只是根据位置对 Q 和 K 做旋转。十一、RoPE 在 Attention 中放在哪里普通 Multi-Head Attention 是输入 x ↓ 线性层生成 Q, K, V ↓ 计算 QK^T ↓ Softmax ↓ 加权求和 V加入 RoPE 后变成输入 x ↓ 线性层生成 Q, K, V ↓ 对 Q, K 应用 RoPE ↓ 计算 Q_rope K_rope^T ↓ Softmax ↓ 加权求和 V注意RoPE 通常作用在 Q 和 K 上不作用在 V 上。因为 Q 和 K 决定 attention score也就是决定“谁关注谁”。V 表示被聚合的内容信息一般不需要旋转位置。代码结构大概是Q self.W_q(x) K self.W_k(x) V self.W_v(x) Q split_heads(Q) K split_heads(K) V split_heads(V) Q, K apply_rope(Q, K) scores torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(d_k) attn torch.softmax(scores, dim-1) out torch.matmul(attn, V)这就是 RoPE 和普通 attention 的主要区别。十二、ALiBi不加位置向量而是给 Attention 分数加偏置ALiBi 的全称是Attention with Linear Biases它的思路和 Sinusoidal、RoPE 都不同。Sinusoidal 是把位置向量加到 token embedding 上RoPE 是把位置通过旋转注入 Q 和 KALiBi 则是直接在 attention score 上加入一个和距离有关的线性偏置普通 attention score 是ALiBi 修改为这里以 causal language model 为例假设。其中i当前 query 位置j被关注的 key 位置两个 token 的距离第 h个 attention head 的斜率距离越远惩罚越大。也就是说ALiBi 会鼓励模型更关注近处 token同时仍然允许模型关注远处 token。十三、ALiBi 的直观理解假设当前 token 是第 5 个位置它可以关注前面的 token位置 0, 1, 2, 3, 4, 5距离分别是5, 4, 3, 2, 1, 0ALiBi 会给更远的位置加更大的负偏置距离 0惩罚最小 距离 1轻微惩罚 距离 2更大惩罚 距离 5最大惩罚这样 attention score 会倾向于近处 token 更容易被关注 远处 token 仍然可以被关注但需要自身 QK 匹配分数足够高这是一种非常简单但有效的位置归纳偏置。十四、ALiBi 为什么有利于长度外推ALiBi 的一个重要优势是它不依赖固定长度的位置 embedding 表。如果使用 learned absolute position embedding模型通常只学习训练长度以内的位置。例如训练时最大长度是 1024那么模型只学习了位置 0 到位置 1023如果测试时输入长度变成 2048后面的位置 embedding 没有训练过就容易出现外推问题。Sinusoidal 可以生成更长位置的编码但模型是否能稳定利用仍然不一定。ALiBi 不需要位置向量表。它只根据距离计算线性偏置。无论序列长度是 1024、2048还是更长只要能计算距离就能生成偏置。所以 ALiBi 在长上下文外推上具有天然优势。十五、ALiBi 代码实现下面给出一个简化版 ALiBi bias 构造代码。import torch def get_alibi_slopes(n_heads): 简化版 head slope 生成方式。 实际论文和开源实现中会使用更细致的 slope 设置。 这里用于教学理解。 return torch.tensor([1.0 / (2 ** i) for i in range(n_heads)]) def build_alibi_bias(batch_size, n_heads, seq_len, devicecpu): 构造 ALiBi bias。 返回形状 [batch_size, n_heads, seq_len, seq_len] slopes get_alibi_slopes(n_heads).to(device) # [heads] positions torch.arange(seq_len, devicedevice) # distance[i, j] i - j distance positions.unsqueeze(1) - positions.unsqueeze(0) # causal 情况下只关注 j i未来位置之后会由 causal mask 屏蔽 distance distance.clamp(min0).float() # bias: [heads, seq_len, seq_len] bias -slopes.view(n_heads, 1, 1) * distance.unsqueeze(0) # [batch_size, heads, seq_len, seq_len] bias bias.unsqueeze(0).expand(batch_size, -1, -1, -1) return bias if __name__ __main__: batch_size 1 n_heads 4 seq_len 5 alibi_bias build_alibi_bias(batch_size, n_heads, seq_len) print(ALiBi bias shape:, alibi_bias.shape) print(Head 0 bias:) print(alibi_bias[0, 0])输出类似ALiBi bias shape: torch.Size([1, 4, 5, 5]) Head 0 bias: tensor([[-0., -0., -0., -0., -0.], [-1., -0., -0., -0., -0.], [-2., -1., -0., -0., -0.], [-3., -2., -1., -0., -0.], [-4., -3., -2., -1., -0.]])注意未来位置还需要配合 causal mask 屏蔽。ALiBi 只是提供距离偏置不替代 causal mask。十六、ALiBi 在 Attention 中怎么用普通 attention score 是scores torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(d_k)加入 ALiBi 后scores scores alibi_bias完整简化代码如下import torch import torch.nn.functional as F import math def attention_with_alibi(Q, K, V, alibi_bias, causal_maskNone): Q, K, V: [batch_size, heads, seq_len, head_dim] alibi_bias: [batch_size, heads, seq_len, seq_len] d_k Q.size(-1) scores torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(d_k) # 加入 ALiBi 位置偏置 scores scores alibi_bias # 仍然需要 causal mask 屏蔽未来位置 if causal_mask is not None: scores scores.masked_fill(causal_mask 0, -1e9) attn F.softmax(scores, dim-1) out torch.matmul(attn, V) return out, attn这就是 ALiBi 的核心实现。它不改变 embedding不旋转 Q/K只是在 attention score 上加了一个距离相关的 bias。十七、Sinusoidal、RoPE、ALiBi 的核心区别现在我们把三者放在一起对比。方法注入位置位置类型是否可训练核心思想代表特点SinusoidalToken Embedding绝对位置否用 sin/cos 生成位置向量加到 embedding 上原始 Transformer简单直观RoPEQuery / Key相对位置效果否根据位置旋转 Q/K使 attention score 体现相对距离适合自回归 LLM常用于现代大模型ALiBiAttention Score相对距离偏置否在 attention 分数上加距离惩罚简单高效有利于长度外推可以这样理解Sinusoidal告诉模型每个 token 在第几个位置 RoPE让 Q 和 K 的匹配天然包含相对距离 ALiBi直接告诉 attention距离越远应该有越大惩罚十八、为什么现代大模型更偏爱 RoPE 或 ALiBi现代大模型通常面对更长上下文、更复杂任务和更强泛化需求。因此位置编码需要满足几个要求能表达 token 顺序 能表达相对距离 能适应长上下文 训练和推理成本不能太高 实现要稳定Sinusoidal 适合作为 Transformer 入门方法也适合讲清楚位置编码的基本思想。但是在大语言模型中仅仅使用绝对位置往往不够。RoPE 和 ALiBi 更强调相对位置关系因此更适合长文本和自回归语言建模。RoPE 的优势是直接作用于 Q/K 相对位置性质自然 和 attention 机制结合紧密 在现代 Decoder-only 模型中非常常见ALiBi 的优势是实现简单 不需要位置 embedding 表 直接支持更长序列 对长度外推友好不过它们也不是没有局限。RoPE 在很长上下文扩展时可能需要额外的缩放或插值策略。ALiBi 的线性距离偏置很简单但表达能力也相对受限。所以今天的大模型位置编码还在不断发展。十九、位置编码和长上下文有什么关系长上下文是大语言模型中的重要问题。如果模型训练时只见过 2048 token但推理时要处理 8192、32768 甚至更长上下文就会遇到位置外推问题。位置编码会直接影响模型能否处理更长文本。如果位置编码依赖固定长度表超过训练长度后就很麻烦。如果位置编码可以通过公式生成或者只依赖相对距离就更容易扩展。这也是 RoPE 和 ALiBi 重要的原因。它们都不是简单地给每个位置学习一个固定向量而是通过更结构化的方式表达位置关系。可以简单理解为短上下文时代只要模型知道 token 大概在第几个位置即可 长上下文时代模型更需要稳定理解相对距离和远距离依赖所以位置编码不只是一个小组件而是长上下文能力的重要基础。