在前面的文章中,我们已经讲过 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 RoPE:Rotary Position Embedding ALiBi:Attention with Linear Biases它们分别代表了三种不同思路:
Sinusoidal:把位置编码加到 token embedding 上 RoPE:把位置通过旋转注入到 Q 和 K 中 ALiBi:不加位置向量,而是在 attention score 上加距离惩罚一、为什么 Transformer 必须要位置编码?
Self-Attention 的核心计算是:
它比较的是 token 表示之间的相似度。假设有三个 token:
x1, 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,也就是正弦余弦位置编码。它不是可训练参数,而是通过固定公式生成。
公式如下:
其中:
pos:token 在序列中的位置;
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, dtype=torch.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_model=d_model, dropout=0.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, base=10000): """ 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, device=device).float() # 频率项 inv_freq = 1.0 / (base ** (dim_idx / head_dim)) # 位置编号 positions = torch.arange(seq_len, device=device).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, repeats=2, 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, device="cpu"): """ 构造 ALiBi bias。 返回形状: [batch_size, n_heads, seq_len, seq_len] """ slopes = get_alibi_slopes(n_heads).to(device) # [heads] positions = torch.arange(seq_len, device=device) # distance[i, j] = i - j distance = positions.unsqueeze(1) - positions.unsqueeze(0) # causal 情况下,只关注 j <= i,未来位置之后会由 causal mask 屏蔽 distance = distance.clamp(min=0).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_mask=None): """ 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 的核心区别
现在我们把三者放在一起对比。
| 方法 | 注入位置 | 位置类型 | 是否可训练 | 核心思想 | 代表特点 |
|---|---|---|---|---|---|
| Sinusoidal | Token Embedding | 绝对位置 | 否 | 用 sin/cos 生成位置向量,加到 embedding 上 | 原始 Transformer,简单直观 |
| RoPE | Query / Key | 相对位置效果 | 否 | 根据位置旋转 Q/K,使 attention score 体现相对距离 | 适合自回归 LLM,常用于现代大模型 |
| ALiBi | Attention 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 大概在第几个位置即可 长上下文时代:模型更需要稳定理解相对距离和远距离依赖所以,位置编码不只是一个小组件,而是长上下文能力的重要基础。