怎么理解transformer中的位置编码
太棒了!你问到了 Transformer 的"灵魂"之一。
如果没有位置编码(Positional Encoding),Transformer 模型就像一个把所有词扔进搅拌机里的人,它知道有哪些词,但完全不知道它们的顺序。
我们来一步步拆解,看看这个天才的设计是怎么解决问题的。
第一部分:Transformer 的"天生缺陷"------ 秩序的失忆症
我们之前讲过,Transformer 的核心是 Self-Attention(自注意力机制)。
它的工作方式是让句子里的每一个词都和其他所有词"对视"一遍,然后算出它们之间的关系权重。
这就带来一个致命问题: 在它眼里,下面这两句话是完全一样的!
"狗咬了人"
"人咬了狗"
为什么?因为对于"狗"这个词来说,它看到的都是"人"和"了"这两个词,它根本不在乎谁在前面谁在后面。它是一个**"秩序无关(Order-agnostic)"**的模型。
这就好比你把一堆积木扔进一个袋子里,你只能感受到袋子里有几块红的、几块蓝的,但你无法知道它们是怎么排列的。
所以,我们必须想办法告诉 Transformer 每个词的"座位号"。
第二部分:一个"笨办法"以及为什么它不行
最简单的想法是什么?
直接给每个词编个号不就行了?
- 第1个词,我们就给它一个数字 1。
- 第2个词,我们就给它一个数字 2。
- ...
- 第100个词,我们就给它一个数字 100。
然后把这个数字加到每个词的词向量里。
这个办法有两大缺陷:
- 数字会变得非常大: 如果句子有500个词,那第500个词就要加上一个很大的数字,这会让词向量的含义被这个巨大的数字"污染",模型很难学习。
- 模型没见过新长度: 如果训练时最长的句子是100个词,那测试时来了一个101个词的句子,模型根本不知道"101"这个位置号是什么意思,泛化能力很差。
第三部分:天才的解决方案 ------ 用"声波"给位置编码
Transformer 的作者们想出了一个绝妙的主意,他们没有用简单的数字,而是用了一组数学上的波 ------ 正弦波(sin)和余弦波(cos)。
你可以把这个过程想象成:
给每个位置(座位号)谱写一首独一无二的"交响乐"。
-
一个位置,一首歌:
- 第1个位置,是一首歌(一个向量)。
- 第2个位置,是另一首完全不同的歌(另一个向量)。
- 每个位置的"歌"都是独一无二的。
-
这首歌是怎么谱写的?
这首歌由很多不同频率的**音符(sin 和 cos 波)**组成。
- 低音部(慢波): 用来表示大致的、粗略的位置信息。
- 高音部(快波): 用来表示精细的、微小的位置变化。
把这些不同频率的音符组合在一起,就形成了一段独特的旋律,作为这个位置的**"位置指纹"**。
-
为什么要用 sin 和 cos?
这是最天才的地方!因为三角函数有一个神奇的特性:任何一个位置的编码,都可以通过另一个位置的编码进行线性变换来表示。
说人话就是:
模型不需要死记硬背"第5个位置"长什么样。它只需要学会一个规律,比如**"向后移动2个位置"等于"执行某个旋转操作"**。
- 它学会了从位置 3 到位置 5 的"相对移动"方法。
- 那么,它就能自动把这个方法应用到从位置 10 到位置 12,甚至从位置 100 到位置 102。
这让 Transformer 拥有了理解**"相对位置"**的能力,而且泛化能力极强,就算遇到再长的句子,它也能根据规律谱写出新的"位置之歌"。
第四部分:最后一步 ------ 融合
位置编码是怎么和词向量结合的呢?
非常简单:直接相加!
最终输入向量 = 词的含义向量 + 词的位置向量
这就好比我们给每个词发了一张名片:
- 词向量: 名片上写的"我是谁"(比如:我是"苹果")。
- 位置编码: 名片上印的"我的地址"(比如:我在句子的第3个位置)。
模型拿到这张名片后,既知道了这个词是什么,也知道了它在哪里。
总结
如何理解 Transformer 的位置编码?
- 为什么需要它? 因为 Transformer 的核心 Attention 机制是"秩序盲",无法感知单词的顺序。
- 它是什么? 它是一个独特的、由
sin和cos波生成的数学"指纹",被添加到每个词的向量中,用来告诉模型这个词的位置。 - 它为什么好?
- 为每个位置提供了唯一的编码。
- 让模型能轻松学习相对位置关系,而不是死记硬背绝对位置。
- 能很好地泛化到更长的句子。
简单说,位置编码就是给 Transformer 这位"失忆的大师"配上了一副能看清词语顺序的"GPS眼镜"。
这下理解了吗?这确实是整个模型中最精巧的设计之一!
具体解释位置编码函数
好!既然你已经理解了位置编码的宏观原理 (给每个词发名片),那我们现在就戴上显微镜,来解剖一下那个看着有点吓人的数学公式。
别被公式里的符号吓到,只要你跟着我的思路,这其实是一个非常优雅的设计,就像精密的手表齿轮一样。
1. 核心公式展示
在 Transformer 的原论文《Attention Is All You Need》里,位置编码(Positional Encoding, PE)的公式是这样的:
P E ( p o s , 2 i ) = sin ( p o s 1000 0 2 i / d m o d e l ) PE_{(pos, 2i)} = \sin\left(\frac{pos}{10000^{2i/d_{model}}}\right) PE(pos,2i)=sin(100002i/dmodelpos)
P E ( p o s , 2 i + 1 ) = cos ( p o s 1000 0 2 i / d m o d e l ) PE_{(pos, 2i+1)} = \cos\left(\frac{pos}{10000^{2i/d_{model}}}\right) PE(pos,2i+1)=cos(100002i/dmodelpos)
是不是看着头大?没关系,我们把这个公式里的每一个变量都拆开来看。
- p o s pos pos (Position): 这是一句话里词的位置。比如第1个词 pos=0,第2个词 pos=1...
- d m o d e l d_{model} dmodel: 这是整个词向量的长度(维度)。比如 BERT 或 Transformer 通常设为 512。
- i i i: 这是词向量里每一维的索引(第0维、第1维...直到第511维)。
- 2 i 2i 2i 和 2 i + 1 2i+1 2i+1: 意思是偶数维度用 sin 函数,奇数维度用 cos 函数。
2. 为什么要搞这么复杂?(时钟比喻)
为了理解这个公式,我们想象一个多指针的时钟 ,或者你以前见过的机械式水表。
假设一个词向量有 4 个维度( d m o d e l = 4 d_{model}=4 dmodel=4)。
-
第0维和第1维( i = 0 i=0 i=0):
- 公式里的分母 1000 0 0 / 4 = 1 10000^{0/4} = 1 100000/4=1。
- 这里的波长很短,频率很高。
- 比喻: 就像时钟的秒针 ,或者跑得飞快的毫秒位。它对于位置的变化极其敏感,只要位置稍微变一下,这里的数值就狂跳。
-
第2维和第3维( i = 1 i=1 i=1):
- 公式里的分母 1000 0 2 / 4 = 100 10000^{2/4} = 100 100002/4=100。
- 这里的波长变长了,频率变低了。
- 比喻: 就像时钟的时针。你需要走过很多个位置,这里的数值才会慢慢变化一点点。
总结:
- 低维度的编码(左边): 变化极快,用来区分近距离的词(比如第5个词和第6个词)。
- 高维度的编码(右边): 变化极慢,用来区分远距离的词(比如第10个词和第100个词)。
这就像我们要用二进制表示数字:
- 0 001 (1)
- 0 010 (2)
- 0 011 (3)
- 1 00 0 (8)
最后一位(低位)跳得快,最前一位(高位)跳得慢。Transformer 的位置编码就是这个逻辑的"连续平滑版"。
3. 为什么是 1000 0 . . . 10000^{...} 10000...?
公式里的分母是 p o s 1000 0 2 i / d \frac{pos}{10000^{2i/d}} 100002i/dpos。
- 这个 10000 是一个人为设定的"巨大底数"。
- 当 i i i(维度)逐渐变大时,分母会呈指数级爆炸增长。
- 分母越大,正弦波的**波长(Wavelength)**就越长。
这就保证了从第0维到第511维,波长是从 2 π 2\pi 2π 一直拉长到 10000 ⋅ 2 π 10000 \cdot 2\pi 10000⋅2π。这足以覆盖极其长的句子,保证每个位置的编码都不会重复。
4. 为什么要同时用 Sin 和 Cos?(最硬核的数学原理)
这是整个设计最天才的地方!
作者选择 Sin/Cos 不仅仅是因为它们是波,而是因为三角函数有一个著名的**"和角公式"**:
sin ( α + β ) = sin α cos β + cos α sin β \sin(\alpha + \beta) = \sin\alpha \cos\beta + \cos\alpha \sin\beta sin(α+β)=sinαcosβ+cosαsinβ
cos ( α + β ) = cos α cos β − sin α sin β \cos(\alpha + \beta) = \cos\alpha \cos\beta - \sin\alpha \sin\beta cos(α+β)=cosαcosβ−sinαsinβ
这对 AI 意味着什么?
假设我们想知道位置 p o s + k pos+k pos+k 的编码(比如当前位置往后挪 k 个位)。
通过上面这个数学公式,我们可以证明:
P E ( p o s + k ) PE(pos+k) PE(pos+k) 可以直接由 P E ( p o s ) PE(pos) PE(pos) 乘以一个线性的旋转矩阵得到!
翻译成人话:
模型只需要学会一个**"旋转操作"(线性变换),就能理解"往后挪 k 个位置"**这个概念。
这让模型不需要死记硬背绝对坐标(比如"第50个词"),而是学会了相对坐标(比如"它后面第2个词")。这对于处理比训练数据更长的句子至关重要。
5. 举个具体的数值例子
假设 d m o d e l = 4 d_{model}=4 dmodel=4(向量长度为4),我们来看看第 1 个位置(pos=1)的编码长什么样:
- 第0维 (偶数, i = 0 i=0 i=0): P E ( 1 , 0 ) = sin ( 1 / 1000 0 0 ) = sin ( 1 ) ≈ 0.84 PE(1, 0) = \sin(1 / 10000^0) = \sin(1) \approx 0.84 PE(1,0)=sin(1/100000)=sin(1)≈0.84
- 第1维 (奇数, i = 0 i=0 i=0): P E ( 1 , 1 ) = cos ( 1 / 1000 0 0 ) = cos ( 1 ) ≈ 0.54 PE(1, 1) = \cos(1 / 10000^0) = \cos(1) \approx 0.54 PE(1,1)=cos(1/100000)=cos(1)≈0.54
- 第2维 (偶数, i = 1 i=1 i=1): P E ( 1 , 2 ) = sin ( 1 / 1000 0 2 / 4 ) = sin ( 1 / 100 ) ≈ 0.01 PE(1, 2) = \sin(1 / 10000^{2/4}) = \sin(1/100) \approx 0.01 PE(1,2)=sin(1/100002/4)=sin(1/100)≈0.01
- 第3维 (奇数, i = 1 i=1 i=1): P E ( 1 , 3 ) = cos ( 1 / 1000 0 2 / 4 ) = cos ( 1 / 100 ) ≈ 0.99995 PE(1, 3) = \cos(1 / 10000^{2/4}) = \cos(1/100) \approx 0.99995 PE(1,3)=cos(1/100002/4)=cos(1/100)≈0.99995
最终位置编码向量: [ 0.84 , 0.54 , 0.01 , 0.99995 ] [0.84, 0.54, 0.01, 0.99995] [0.84,0.54,0.01,0.99995]
你看,前面的维度数值大(变化剧烈),后面的维度数值接近0或1(变化微小)。
总结
如何通俗地理解这个函数?
- 它是一个多频率的信号发生器: 把位置信息变成了从高频到低频的一组正弦波信号。
- Sin/Cos 交替: 为了利用三角函数的"和差化积"性质,让模型轻松掌握相对位置关系(相对距离)。
- 10000: 仅仅是为了拉长波长,确保即使句子很长,不同位置的编码差异也足够明显,不会混淆。
这个公式,就是把**时间(顺序)刻在空间(向量)**里的魔法。
PE(pos+k) 可以用 PE(pos) 计算得到,具体
这是一个非常硬核的数学推导,但也是 Transformer 位置编码设计中最迷人、最优雅的部分。
简单来说,我们要证明一件事:如果你知道当前位置 p o s pos pos 的编码,你想算出后面第 k k k 个位置(即 p o s + k pos+k pos+k)的编码,只需要乘以一个固定的矩阵就可以了。
这意味着模型不需要重新计算,只需要学会一个**"线性变换"**(Linear Transformation)。
咱们分三步把这个过程推导出来。
第一步:简化符号(准备工作)
原来的公式里有一大坨 1 1000 0 2 i / d \frac{1}{10000^{2i/d}} 100002i/d1,看着太累赘。
我们把它简化为一个符号 ω i \omega_i ωi(角频率)。
那么,对于维度 2 i 2i 2i 和 2 i + 1 2i+1 2i+1,位置 p o s pos pos 的编码就是:
P E ( p o s , 2 i ) = sin ( ω i ⋅ p o s ) PE(pos, 2i) = \sin(\omega_i \cdot pos) PE(pos,2i)=sin(ωi⋅pos)
P E ( p o s , 2 i + 1 ) = cos ( ω i ⋅ p o s ) PE(pos, 2i+1) = \cos(\omega_i \cdot pos) PE(pos,2i+1)=cos(ωi⋅pos)
现在,我们要计算的是 位置 p o s + k pos+k pos+k 的编码:
P E ( p o s + k , 2 i ) = sin ( ω i ⋅ ( p o s + k ) ) PE(pos+k, 2i) = \sin(\omega_i \cdot (pos + k)) PE(pos+k,2i)=sin(ωi⋅(pos+k))
P E ( p o s + k , 2 i + 1 ) = cos ( ω i ⋅ ( p o s + k ) ) PE(pos+k, 2i+1) = \cos(\omega_i \cdot (pos + k)) PE(pos+k,2i+1)=cos(ωi⋅(pos+k))
第二步:召唤三角函数公式(核心魔法)
这里我们需要用到中学数学里最著名的三角函数和角公式:
- sin ( α + β ) = sin α cos β + cos α sin β \sin(\alpha + \beta) = \sin\alpha \cos\beta + \cos\alpha \sin\beta sin(α+β)=sinαcosβ+cosαsinβ
- cos ( α + β ) = cos α cos β − sin α sin β \cos(\alpha + \beta) = \cos\alpha \cos\beta - \sin\alpha \sin\beta cos(α+β)=cosαcosβ−sinαsinβ
令 α = ω i ⋅ p o s \alpha = \omega_i \cdot pos α=ωi⋅pos(当前位置), β = ω i ⋅ k \beta = \omega_i \cdot k β=ωi⋅k(相对距离)。
我们将 P E ( p o s + k ) PE(pos+k) PE(pos+k) 展开:
对于正弦部分(偶数维):
sin ( ω i ( p o s + k ) ) = sin ( ω i p o s ) ⏟ P E ( p o s , 2 i ) ⋅ cos ( ω i k ) + cos ( ω i p o s ) ⏟ P E ( p o s , 2 i + 1 ) ⋅ sin ( ω i k ) \sin(\omega_i(pos+k)) = \underbrace{\sin(\omega_i pos)}{PE(pos, 2i)} \cdot \cos(\omega_i k) + \underbrace{\cos(\omega_i pos)}{PE(pos, 2i+1)} \cdot \sin(\omega_i k) sin(ωi(pos+k))=PE(pos,2i) sin(ωipos)⋅cos(ωik)+PE(pos,2i+1) cos(ωipos)⋅sin(ωik)
对于余弦部分(奇数维):
cos ( ω i ( p o s + k ) ) = cos ( ω i p o s ) ⏟ P E ( p o s , 2 i + 1 ) ⋅ cos ( ω i k ) − sin ( ω i p o s ) ⏟ P E ( p o s , 2 i ) ⋅ sin ( ω i k ) \cos(\omega_i(pos+k)) = \underbrace{\cos(\omega_i pos)}{PE(pos, 2i+1)} \cdot \cos(\omega_i k) - \underbrace{\sin(\omega_i pos)}{PE(pos, 2i)} \cdot \sin(\omega_i k) cos(ωi(pos+k))=PE(pos,2i+1) cos(ωipos)⋅cos(ωik)−PE(pos,2i) sin(ωipos)⋅sin(ωik)
看到没?等号右边用到的全是 P E ( p o s ) PE(pos) PE(pos) 的已知值!
第三步:写成矩阵形式(见证奇迹)
为了看得更清楚,我们把上面的两个等式写成矩阵乘法的形式。
设向量 v ⃗ p o s = [ P E ( p o s , 2 i ) P E ( p o s , 2 i + 1 ) ] = [ sin ( ω i p o s ) cos ( ω i p o s ) ] \vec{v}_{pos} = \begin{bmatrix} PE(pos, 2i) \\ PE(pos, 2i+1) \end{bmatrix} = \begin{bmatrix} \sin(\omega_i pos) \\ \cos(\omega_i pos) \end{bmatrix} v pos=[PE(pos,2i)PE(pos,2i+1)]=[sin(ωipos)cos(ωipos)]
那么 v ⃗ p o s + k \vec{v}_{pos+k} v pos+k 可以表示为:
P E ( p o s + k , 2 i ) P E ( p o s + k , 2 i + 1 ) \] = \[ cos ( ω i k ) sin ( ω i k ) − sin ( ω i k ) cos ( ω i k ) \] ⋅ \[ sin ( ω i p o s ) cos ( ω i p o s ) \] \\begin{bmatrix} PE(pos+k, 2i) \\\\ PE(pos+k, 2i+1) \\end{bmatrix} = \\begin{bmatrix} \\cos(\\omega_i k) \& \\sin(\\omega_i k) \\\\ -\\sin(\\omega_i k) \& \\cos(\\omega_i k) \\end{bmatrix} \\cdot \\begin{bmatrix} \\sin(\\omega_i pos) \\\\ \\cos(\\omega_i pos) \\end{bmatrix} \[PE(pos+k,2i)PE(pos+k,2i+1)\]=\[cos(ωik)−sin(ωik)sin(ωik)cos(ωik)\]⋅\[sin(ωipos)cos(ωipos)
我们把中间那个矩阵记作 M k M_k Mk。
v ⃗ p o s + k = M k ⋅ v ⃗ p o s \vec{v}{pos+k} = M_k \cdot \vec{v}{pos} v pos+k=Mk⋅v pos
这个推导说明了什么?
请仔细看中间那个矩阵 M k M_k Mk:
M k = [ cos ( ω i k ) sin ( ω i k ) − sin ( ω i k ) cos ( ω i k ) ] M_k = \begin{bmatrix} \cos(\omega_i k) & \sin(\omega_i k) \\ -\sin(\omega_i k) & \cos(\omega_i k) \end{bmatrix} Mk=[cos(ωik)−sin(ωik)sin(ωik)cos(ωik)]
你会发现两个惊人的事实:
-
它是一个标准的旋转矩阵(Rotation Matrix):
在线性代数中,这个矩阵的含义就是把原向量在平面上旋转一个角度。
-
它与 p o s pos pos 无关!
矩阵里只有 k k k(相对距离)和 ω i \omega_i ωi(固定的频率)。不管你在句子的第 1 个位置,还是第 100 个位置,只要你想往后看 k k k 个词,你乘的矩阵 M k M_k Mk 都是完全一样的!
总结
为什么说 P E ( p o s + k ) PE(pos+k) PE(pos+k) 可以由 P E ( p o s ) PE(pos) PE(pos) 线性表示?
答案就是:
对于任意的相对距离 k k k,都存在一个固定的旋转矩阵 M k M_k Mk。模型只需要学会这个矩阵(线性变换),就能自动理解"相对位置"的概念,而不需要关心绝对位置在哪里。
这就是为什么 Transformer 即使训练时只见过长度为 50 的句子,在预测时遇到长度为 51 的位置也能猜个八九不离十------因为规律(旋转矩阵)是通用的。
PositionalEncoding代码是什么
这就是业界最标准的 PyTorch 实现版本(参考了哈佛大学《The Annotated Transformer》的经典代码)。
我会把代码拆解开,一行一行带你看懂它是怎么把刚才那个复杂的数学公式变成代码的。
1. 完整代码实现 (PyTorch)
python
import torch
import torch.nn as nn
import math
class PositionalEncoding(nn.Module):
def __init__(self, d_model, max_len=5000, dropout=0.1):
"""
d_model: 词向量的维度 (比如 512)
max_len: 预设的最大句子长度 (比如 5000)
dropout: 防止过拟合的丢弃率
"""
super(PositionalEncoding, self).__init__()
self.dropout = nn.Dropout(p=dropout)
# 1. 初始化一个矩阵 [max_len, d_model],先把里面填满0
pe = torch.zeros(max_len, d_model)
# 2. 生成位置索引 [0, 1, 2, ..., max_len-1]
# shape: [max_len, 1] -> 变成列向量
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
# 3. 计算分母中的频率项 (Divisor Term)
# 公式: 1 / 10000^(2i/d_model)
# 技巧: 在对数空间计算更稳定: exp(-2i * log(10000) / d_model)
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
# 4. 填充矩阵
# 偶数维度 (0, 2, 4...) 用 sin
pe[:, 0::2] = torch.sin(position * div_term)
# 奇数维度 (1, 3, 5...) 用 cos
pe[:, 1::2] = torch.cos(position * div_term)
# 5. 增加一个维度,为了方便后面和 batch 数据相加
# shape 变身: [1, max_len, d_model]
pe = pe.unsqueeze(0)
# 6. 注册为 buffer
# 意思就是:这是模型的状态的一部分,保存模型时要带上它,
# 但是!它不是参数,梯度下降时不要更新它(它是固定的)。
self.register_buffer('pe', pe)
def forward(self, x):
"""
x: [batch_size, seq_len, d_model]
"""
# 取出前 seq_len 个位置的编码与输入相加
# x.size(1) 就是当前句子的实际长度
x = x + self.pe[:, :x.size(1), :]
return self.dropout(x)
2. 关键代码逐行解析
这里面有几个非常"程序员思维"的技巧,如果不解释很难看懂:
技巧一:对数空间的转换 (div_term)
公式里分母是 1000 0 2 i / d m o d e l 10000^{2i/d_{model}} 100002i/dmodel。直接算幂容易溢出或者精度丢失,所以代码里用了 exp 和 log 的转换:
a b = e ln ( a b ) = e b ⋅ ln ( a ) a^b = e^{\ln(a^b)} = e^{b \cdot \ln(a)} ab=eln(ab)=eb⋅ln(a)
代码对应:
python
# arange(0, d_model, 2) 就是 2i
# -math.log(10000.0) / d_model 就是 -ln(10000) / d
torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
这和数学公式是完全等价的,但计算机算起来更舒服。
技巧二:切片操作 (0::2 和 1::2)
怎么把 sin 填进偶数列,cos 填进奇数列?Python 的切片帮了大忙:
pe[:, 0::2]:选中所有行,从第0列开始,每隔2列选一个(0, 2, 4...)。pe[:, 1::2]:选中所有行,从第1列开始,每隔2列选一个(1, 3, 5...)。
技巧三:广播机制 (Broadcasting)
在 forward 函数里:
x 的形状通常是 [32, 10, 512] (Batch Size=32, 句子长度=10)。
pe 的形状是 [1, 5000, 512]。
当我们执行 x + pe[:, :10, :] 时:
PyTorch 会自动把 pe 的第0维(那个1)复制32份,变成 [32, 10, 512],然后再相加。
这就实现了:不论你有多少个句子并行训练,它们用的位置编码都是同一套。
技巧四:register_buffer
为什么要用 register_buffer 而不是直接 self.pe = pe?
- 直接赋值: PyTorch 会以为这只是一个普通的 Python 变量,当你保存模型 (
model.state_dict()) 时,这个pe矩阵不会被保存下来。下次加载模型,位置编码就丢了。 - 用 Parameter: 如果用
nn.Parameter(pe),PyTorch 会把它当成权重,反向传播时会去修改它。但我们需要位置编码是固定死的。 - 用 Buffer: 既保存进模型文件,又不参与梯度更新。完美!
3. 如何使用它?
python
# 假设词向量维度是 512,最大长度 100
pos_encoder = PositionalEncoding(d_model=512, max_len=100)
# 模拟一个输入:2句话,每句话10个词,每个词512维
dummy_input = torch.randn(2, 10, 512)
# 前向传播
output = pos_encoder(dummy_input)
print("输入形状:", dummy_input.shape)
print("输出形状:", output.shape)
# 输出形状依然是 [2, 10, 512],只是数值里混入了位置信息
这段代码就是 Transformer 模型里那个不起眼但至关重要的"GPS 模块"的真身。
LLM 大部分采用了旋转位置编码(Rotary Positional Encoding,RoPE)是什么
这位同学,你观察得非常仔细!
如果你现在去翻看 LLaMA、Mistral、Qwen(通义千问)、PaLM 这些当红大模型的论文或代码,你会发现它们不约而同地抛弃了我们之前讲的"正弦波位置编码(Sinusoidal PE)",全都换成了 RoPE(Rotary Positional Encoding)。
这个 RoPE(旋转位置编码)是由苏剑林(追一科技)大神最早构思,后来被 Google 团队正名并发扬光大的。
简单来说,RoPE 是目前处理长文本、实现相对位置感知的最优解。我们还是用通俗的方式来拆解它。
第一部分:以前的痛点 ------ "加法"的局限
在 RoPE 出现之前(比如 BERT 和 原始 Transformer),我们是怎么加位置信息的?
是**"加法"**。
最终向量 = 词向量 + 位置向量
这就像给每个词发了一个**"门牌号"**。
- 词 A 在第 10 号。
- 词 B 在第 20 号。
痛点在哪里?
Attention 机制的核心是计算 A 和 B 的关系 (点积)。
但是在"加法"体系下,模型想要知道"A 和 B 距离 10 个格子",需要经过比较复杂的数学变换才能学到。模型不仅要记住绝对位置(我是第几),还要费力去算相对位置(我俩差多远)。
这导致模型很难泛化:如果训练时最长只见过 2048 个词,测试时来了个 3000 词的句子,模型直接懵圈,因为它没见过"第 3000 号"门牌长啥样。
第二部分:RoPE 的魔法 ------ 也就是"旋转"
RoPE 的核心思想是:别做加法了,咱们做乘法(旋转)!
它利用了复数(Complex Numbers)和几何旋转的数学性质。
1. 形象的比喻:时钟指针
想象每个词向量(Query 和 Key)原本是坐标系里的一个箭头。
RoPE 不会改变这个箭头的长度 ,而是根据它的位置,把这个箭头旋转一定的角度。
- 第 1 个词: 逆时针转 10 度。
- 第 2 个词: 逆时针转 20 度。
- 第 3 个词: 逆时针转 30 度。
- ...
- 第 m 个词: 逆时针转 m × θ m \times \theta m×θ 度。
2. 为什么"旋转"就能表示"相对位置"?
这是 RoPE 最天才的地方。
Attention 在计算相似度时,算的是两个向量的点积 (这在几何上等于:长度乘积 × \times × 夹角的余弦)。
假设:
- 词 A 在位置 m m m(转了 m m m 度)。
- 词 B 在位置 n n n(转了 n n n 度)。
当我们算 A 和 B 的点积时,数学公式告诉我们,结果只跟它们的角度差 ( m − n ) (m - n) (m−n) 有关!
Attention分数 ∝ cos ( 角度A − 角度B ) = cos ( m θ − n θ ) = cos ( ( m − n ) θ ) \propto \cos(\text{角度A} - \text{角度B}) = \cos(m\theta - n\theta) = \cos((m-n)\theta) ∝cos(角度A−角度B)=cos(mθ−nθ)=cos((m−n)θ)
这意味着什么?
不管 A 和 B 到底在第 100 和 110 位,还是在第 1000 和 1010 位,只要它俩相差 10 位 ,它们的相对角度差就是一样的,Attention 算出来的分数就是由这个相对距离决定的。
结论:
RoPE 通过"旋转",强制让模型只关心词与词之间的相对距离,而忘掉绝对位置。这正是处理自然语言(长文本)最需要的特性。
第三部分:RoPE 具体是怎么做的?
在代码实现上,RoPE 并没有真的去算复数,而是用矩阵运算来模拟旋转。
假设你的词向量维度是 d d d。RoPE 会把这 d d d 个数字,两两分组(比如第0维和第1维一组,第2维和第3维一组...)。
每一组就像一个二维平面上的点 ( x , y ) (x, y) (x,y)。
然后用一个旋转矩阵去乘它:
( cos m θ − sin m θ sin m θ cos m θ ) ( x y ) \begin{pmatrix} \cos m\theta & -\sin m\theta \\ \sin m\theta & \cos m\theta \end{pmatrix} \begin{pmatrix} x \\ y \end{pmatrix} (cosmθsinmθ−sinmθcosmθ)(xy)
这就是你在代码里经常看到的那个神秘操作:两两配对,进行旋转。
第四部分:为什么大家都用它?(RoPE 的优势)
现在 LLM 几乎全员 RoPE,主要原因有三个:
-
完美的相对位置特性(Relative Position):
正如刚才证明的,它天生就让模型懂得"距离"的概念,而不是死记"位置"。这对于理解长文章逻辑至关重要。
-
更好的外推性(Extrapolation):
这是最杀手级的应用!
- 旧方法: 训练长度 2048,推理时想输入 4000?直接报错或乱码。
- RoPE: 因为它是基于旋转角度的。训练时没转过那么多圈,推理时多转几圈,模型大概率也能理解!
- 注: 现在的"长文本技术"(如 NTK-Aware Scaled RoPE, YaRN),能让 LLaMA 原生只支持 4k 长度,不经过训练直接扩展到 16k 甚至 128k,全都是基于 RoPE 的数学特性魔改出来的。
-
兼容性好:
它不需要像 ALiBi 那样修改 Attention 的计算公式(加 Bias),它是直接作用在 Q 和 K 向量上的,可以无缝插入到标准的 Transformer 架构中。
总结
什么是 RoPE?
- 本质: 一种通过**"旋转向量"**来注入位置信息的技术。
- 原理: 第 m m m 个位置就转 m m m 度。两个向量的点积只取决于它们的角度差(相对距离)。
- 地位: 它是现代大模型(LLM)的标配。
一句话记忆:
以前的位置编码是给数据**"贴标签"(做加法),RoPE 是把数据放在 "表盘"上转动(做乘法),让模型通过角度差**来感知距离。
为什么是旋转矩阵,没太理解
这确实是 RoPE 中最反直觉、但也是最数学之美的地方。
为了让你彻底理解**"为什么要用旋转矩阵",我们得把问题拆回到"Attention 到底在算什么"**这个根本问题上。
我们分三步来破解这个谜题。
第一步:Attention 的"怪癖"
Transformer 里的 Attention 机制,核心算的是 Query ( q q q) 和 Key ( k k k) 的点积(Dot Product)。
高中数学告诉我们,两个向量的点积公式是:
q ⃗ ⋅ k ⃗ = ∣ q ⃗ ∣ ∣ k ⃗ ∣ cos ( θ ) \vec{q} \cdot \vec{k} = |\vec{q}| |\vec{k}| \cos(\theta) q ⋅k =∣q ∣∣k ∣cos(θ)
这里有两个关键要素:
- ∣ q ⃗ ∣ |\vec{q}| ∣q ∣ 和 ∣ k ⃗ ∣ |\vec{k}| ∣k ∣: 向量的长度(模)。这代表了词向量的**"强度"**(语义信息)。
- θ \theta θ: 两个向量之间的夹角。
关键点来了:
如果你想把"位置信息"塞进 Attention 里,而且希望 Attention 只关心相对距离 (比如只关心 A 和 B 差了 5 个格子,而不关心它俩是在第 100 位还是第 1000 位),最好的办法就是去操纵这个"夹角 θ \theta θ"!
第二步:把"位置"变成"角度"
假设我们有个魔法,规定:
- 位置 m m m = 角度 m θ m\theta mθ
我们来看两个词:
- 词 A 在位置 m m m。那我就把词 A 的向量旋转 m θ m\theta mθ 度。
- 词 B 在位置 n n n。那我就把词 B 的向量旋转 n θ n\theta nθ 度。
现在,Attention 算它俩的点积时,夹角变成了什么?
新的夹角 = ( m θ ) − ( n θ ) = ( m − n ) θ \text{新的夹角} = (m\theta) - (n\theta) = (m - n)\theta 新的夹角=(mθ)−(nθ)=(m−n)θ
见证奇迹的时刻:
Attention 的结果,只取决于 ( m − n ) (m-n) (m−n),也就是相对距离!
不管 m m m 和 n n n 是多少(是 5 和 10,还是 1005 和 1010),只要它俩差 5,算出来的夹角差就是一样的,Attention 分数也是一样的。
这就是为什么我们要把位置映射成角度。
第三步:怎么实现"改变角度"?------ 必须用旋转矩阵
好了,现在的目标明确了:我们需要把一个向量 x ⃗ \vec{x} x 转动一个角度。
在线性代数里,唯一 能让一个向量旋转,同时绝对不改变它长度 (不破坏词原本的语义强度)的工具,就是旋转矩阵。
在二维平面上,要把向量 ( x y ) \begin{pmatrix} x \\ y \end{pmatrix} (xy) 逆时针旋转 ϕ \phi ϕ 度,标准公式就是乘上这个矩阵:
( x ′ y ′ ) = ( cos ϕ − sin ϕ sin ϕ cos ϕ ) ( x y ) \begin{pmatrix} x' \\ y' \end{pmatrix} = \begin{pmatrix} \cos \phi & -\sin \phi \\ \sin \phi & \cos \phi \end{pmatrix} \begin{pmatrix} x \\ y \end{pmatrix} (x′y′)=(cosϕsinϕ−sinϕcosϕ)(xy)
这就是为什么 RoPE 的公式里全是 sin 和 cos 组成的矩阵。
- 如果不乘这个矩阵: 我们就只能做加法(像以前的 BERT)。那你就改变了向量的长度和方向,而且很难凑出完美的 ( m − n ) (m-n) (m−n) 相对关系。
- 乘了这个矩阵: 我们完美地保留了语义(长度没变),同时把位置信息优雅地藏进了角度里,让点积运算自动解密出相对距离。
总结:逻辑闭环
- 目标: 我们希望 Attention 分数只跟相对距离有关。
- 性质: Attention 是算点积 ,点积由夹角决定。
- 思路: 如果 "位置 = 角度" ,那么 "位置差 = 角度差"。
- 工具: 怎么把位置变成角度?数学上就是旋转。
- 实现: 怎么在计算机里旋转一个向量?用旋转矩阵。
所以,RoPE 不是为了炫技才用旋转矩阵,而是因为在点积运算的框架下,旋转矩阵是实现相对位置编码的唯一且最优的数学解。
这下理解这个逻辑了吗?
可是token不止2维,怎么旋转
这是一个直击灵魂的问题!
确实,我们在图解里画的都是 2 维向量在平面上转圈圈。但 LLM 的词向量(Embedding)动不动就是 4096 维、8192 维,甚至更多。
一个 4096 维的"棍子",在 4096 维的空间里怎么旋转?
答案其实非常简单粗暴,用了四个字:"分而治之"。
简单说:我们不把这 4096 维看作一个整体来转,而是把它切成 2048 个"2维小分队",让每个小分队自己在自己的平面上转。
第一步:切片(两两分组)
假设你的词向量 x x x 有 d d d 维(例如 d = 4 d=4 d=4):
x = [ x 0 , x 1 , x 2 , x 3 ] x = [x_0, x_1, x_2, x_3] x=[x0,x1,x2,x3]
RoPE 的做法是:把它们两两配对。
- 第 1 组: ( x 0 , x 1 ) (x_0, x_1) (x0,x1) ------ 这是一个 2D 向量。
- 第 2 组: ( x 2 , x 3 ) (x_2, x_3) (x2,x3) ------ 这是另一个 2D 向量。
如果有 4096 维,那就分成 2048 组。每一组都构成了一个独立的二维子空间(Subspace)。
第二步:各自旋转(Block Diagonal Matrix)
现在,我们对每一组分别应用那个 2 × 2 2 \times 2 2×2 的旋转矩阵。
- 让 ( x 0 , x 1 ) (x_0, x_1) (x0,x1) 转动角度 θ 1 \theta_1 θ1。
- 让 ( x 2 , x 3 ) (x_2, x_3) (x2,x3) 转动角度 θ 2 \theta_2 θ2。
- ...
在数学上,这等同于乘上一个巨大的"分块对角矩阵":
R = ( cos θ 1 − sin θ 1 0 0 ⋯ sin θ 1 cos θ 1 0 0 ⋯ 0 0 cos θ 2 − sin θ 2 ⋯ 0 0 sin θ 2 cos θ 2 ⋯ ⋮ ⋮ ⋮ ⋮ ⋱ ) R = \begin{pmatrix} \cos\theta_1 & -\sin\theta_1 & 0 & 0 & \cdots \\ \sin\theta_1 & \cos\theta_1 & 0 & 0 & \cdots \\ 0 & 0 & \cos\theta_2 & -\sin\theta_2 & \cdots \\ 0 & 0 & \sin\theta_2 & \cos\theta_2 & \cdots \\ \vdots & \vdots & \vdots & \vdots & \ddots \end{pmatrix} R= cosθ1sinθ100⋮−sinθ1cosθ100⋮00cosθ2sinθ2⋮00−sinθ2cosθ2⋮⋯⋯⋯⋯⋱
你看这个矩阵:
- 主对角线上: 是一堆 2 × 2 2 \times 2 2×2 的小旋转矩阵。
- 其他地方: 全是 0。这意味着第 1 组的旋转完全不会影响第 2 组,大家各转各的,互不干扰。
第三步:关键细节 ------ 大家转的速度不一样!
如果每一组都转一样的角度,那跟没分组也没啥区别。RoPE 的精髓在于:每一组旋转的"速度"(频率)是不同的。
还记得 Transformer 位置编码里的那个 10000 10000 10000 吗?这里又用上了。
- 第 1 组( x 0 , x 1 x_0, x_1 x0,x1): 转得最快(频率最高)。就像时钟的秒针。
- 第 2 组( x 2 , x 3 x_2, x_3 x2,x3): 转得稍微慢点。
- ...
- 最后一组: 转得最慢。就像时钟的时针。
具体公式:
第 i i i 组的旋转角度是位置 m m m 乘以频率:
Angle i = m × 1000 0 − 2 i / d \text{Angle}_i = m \times 10000^{-2i/d} Anglei=m×10000−2i/d
形象的比喻:多维时钟
要理解高维旋转,请想象一下:
你的词向量不是一根单纯的棍子,而是一块拥有 2048 个表盘的超级手表。
当你想表示"我是第 10 个词"时:
- 你把第 1 个表盘的指针拨动 10 × 1 10 \times 1 10×1 度。
- 你把第 2 个表盘的指针拨动 10 × 0.5 10 \times 0.5 10×0.5 度。
- ...
- 你把最后一个表盘的指针拨动 10 × 0.0001 10 \times 0.0001 10×0.0001 度。
这就是高维旋转!
虽然整个 4096 维空间很复杂,但我们把它降维打击成了 2048 个简单的平面旋转。Attention 在计算点积时,会把这 2048 个平面的结果加起来,从而精准地捕捉到相对位置信息。
现在是不是觉得也没那么难了?本质上就是**"同时玩转很多个呼啦圈"**!
相对位置编码不是还有可学习的方式吗,为什么不用那种
这是一个非常深刻的问题!说明你对 NLP 发展的历史很熟悉。
你说的**"可学习的相对位置编码"(Learnable Relative Positional Encoding, RPE)**,最著名的代表就是 Google 的 T5 模型,以及早期的 Transformer-XL。
当时的思路很简单:
"我不搞什么复杂的数学公式了,咱们直接建一个**'查询表'(Lookup Table)**。如果你俩相距 1 格,我就加参数 w 1 w_1 w1;相距 2 格,我就加 w 2 w_2 w2。至于 w 1 , w 2 w_1, w_2 w1,w2 是多少?让模型自己去学!"
这听起来很美好,直观又暴力。但为什么到了 LLaMA、GPT-4 时代,大家把这种方法集体抛弃 ,转而投奔了数学味儿很浓的 RoPE 呢?
主要有三大死穴。
死穴一:遇到了"长度墙"(外推性差)
这是最致命的原因。
可学习 RPE 的逻辑是"死记硬背":
- 训练时,如果你的语料最长只有 2048。
- 模型就会学习一个表,里面存着:距离=1 的偏置、距离=2 的偏置 ...... 直到 距离=2048 的偏置。
到了推理时:
如果突然让你处理一篇文章,Q 和 K 之间的距离是 3000 。
模型去查表:"喂,给我拿一下距离 3000 的参数。"
表:"我没有啊! 我只学到了 2048!"
这时候你怎么办?
- 截断? 强制当成 2048 处理?(误差很大)
- 瞎猜? (完全不可靠)
这就导致了使用 T5 这类位置编码的模型,很难处理比训练长度更长的文本 。这就好比一个只会背九九乘法表的小孩,你问他 12 × 12 12 \times 12 12×12,他就傻了。
反观 RoPE:
它是数学公式 (旋转)。
不管你是 2048 还是 10000,公式都是通用的。虽然没见过,但照着公式算就行了。这让 RoPE 拥有了极强的外推性(Extrapolation)。
死穴二:参数的浪费与"稀疏性"问题
可学习 RPE 需要额外的参数:
你需要维护一个巨大的 Embedding 表来存这些相对距离的 Bias。虽然对于大模型来说这点参数不算啥,但不够优雅。
更重要的是训练数据的稀疏性:
- 短距离(距离 1, 2, 3): 在训练数据里出现得非常频繁,参数训练得很充分,很准。
- 长距离(距离 2000): 在训练数据里出现的次数很少(因为大部分句子没那么长)。
- 结果: 长距离的参数因为训练样本少,根本学不好,甚至可能是噪音。
反观 RoPE:
它不需要任何参数 !
它纯粹是基于逻辑的旋转。不需要训练,不存在"长距离见得少所以学不好"的问题。所有的位置都遵循同一套严谨的数学规律。
死穴三:远程衰减的"归纳偏置"
在自然语言中,有一个普遍规律:"离得越远的词,关系通常越弱"。
我们希望 Attention 分数随着距离增加,能自然地有一种**"衰减"**的趋势。
-
可学习 RPE:
模型必须自己从数据里艰难地学会"距离越远, w w w 越小"这个规律。如果数据不好,它甚至可能学出"距离 1000 比距离 5 更重要"这种反直觉的参数。
-
RoPE:
基于正弦波的旋转,数学上自带一种高频震荡和长程衰减 的特性。随着相对距离变大,点积的期望值会趋向于变小(虽然不是严格单调,但总体有这个趋势)。
这意味着 RoPE 自带了**"远亲不如近邻"**的先天设定(Inductive Bias),这非常符合人类语言的规律,让模型收敛更快。
总结
为什么现在的大模型(LLM)不用"可学习 RPE"了?
| 特性 | 可学习 RPE (T5类) | RoPE (LLaMA类) | 比喻 |
|---|---|---|---|
| 遇到没见过的长度 | 直接歇菜 (查不到表) | 还能一战 (套公式算) | 死记硬背 vs. 掌握公式 |
| 参数量 | 需要额外参数 | 0 参数 | 带小抄 vs. 脑子快 |
| 长距离表现 | 样本少,学不准 | 数学保证,很稳定 | 偏科生 vs. 优等生 |
所以,RoPE 战胜可学习编码,是**"数学逻辑"战胜了"暴力枚举"**。在动辄 100k 上下文的今天,RoPE 这种基于函数的编码方式几乎是唯一的选择。
RoPE:基于正弦波的旋转,数学上自带一种高频震荡和长程衰减的特性。随着相对距离变大,点积的期望值会趋向于变小(虽然不是严格单调,但总体有这个趋势)。理解不了
这确实是一个非常反直觉的结论。
我们通常认为:sin 和 cos 这种波,不是永远在 -1 到 1 之间来回震荡吗?它应该永远在那"跳",怎么会"衰减"(变小)呢?
要解开这个误解,关键在于四个字:多维求和。
RoPE 不是只看一根 正弦波,而是看几百根 不同频率的正弦波加在一起的结果。
我们用一个非常生动的**"拔河比赛"或者"大合唱"**的比喻来解释这个数学现象。
第一步:想象"齐声呐喊"(距离 = 0)
假设你的词向量有 100 维,也就是有 50 个"小分队"(50 个 cos \cos cos 函数)。
当两个词的位置完全重叠(相对距离 k = 0 k=0 k=0)时:
- 所有的 cos ( 0 ) \cos(0) cos(0) 都等于 1。
- 这意味着这 50 个小分队都在向同一个方向使劲。
- 结果: 1 + 1 + ... + 1 = 50(最大值)。
- 含义: 哪怕不用去算,我自己跟自己的相关性肯定是最高的。
第二步:想象"步调不一致"(距离变大)
现在,我们要看相对距离 k k k 变大时发生了什么。
RoPE 的特点是:这 50 个小分队的"转速"(频率)是完全不一样的!
- 第 1 队转得飞快(高频)。
- 第 2 队转得慢点。
- ...
- 第 50 队转得像蜗牛一样慢(低频)。
当距离 k k k 开始增加(比如距离 5、10、20......):
- 高频队(转得快的): 早就转了好几圈了,现在的角度可能正好转到了反方向(负数)。
- 低频队(转得慢的): 可能才转了一点点,还是正数。
这时候把它们加起来求和(算点积):
总分 = cos ( 快 ) + cos ( 中 ) + ⋯ + cos ( 慢 ) \text{总分} = \cos(\text{快}) + \cos(\text{中}) + \dots + \cos(\text{慢}) 总分=cos(快)+cos(中)+⋯+cos(慢)
因为大家的步调彻底乱了,有的在喊"加油"(+1),有的在喊"倒油"(-1),有的在喊"无所谓"(0)。
正负一抵消,总分就变小了!
这就叫**"相消干涉"**(Destructive Interference)。
第三步:想象"完全混乱"(长距离)
当距离 k k k 变得非常大(比如相距 1000 个词)时:
- 这 50 个小分队的角度可以说已经是随机分布了。
- 在概率上,如果你随机扔 50 个 -1 到 1 之间的数,把它们加起来,结果会趋近于 0(或者在 0 附近小范围波动)。
- 它绝对不可能像一开始那样齐心协力凑出 50 这种高分。
这就是"长程衰减"的真相:
不是单个波衰减了(单个波还在疯狂震荡),而是因为大家乱成一团,互相抵消,导致整体的合力衰减了。
第四步:为什么"高频震荡"?
你可能还会问:"那高频震荡体现在哪?"
虽然整体趋势是变小(衰减),但因为它是由很多 cos 组成的,所以它不会像滑滑梯一样平滑地下降,而是像心电图一样,上蹿下跳地下降。
- 距离 10 的时候,可能大家抵消得厉害,分数很低。
- 距离 11 的时候,可能又有几个人凑巧转到了同一个方向,分数突然又高了一点点。
但只要你拉远了看(看长期趋势),距离越远,这种"凑巧"的概率就越低,整体的相关性分数就会越接近于 0。
总结
为什么说 RoPE 自带"长程衰减"?
- 微观上: 每一个维度的 cos \cos cos 都在永不停歇地旋转(没有衰减)。
- 宏观上(求和):
- 近距离时: 大家的角度差不远,合力很大(注意力强)。
- 远距离时: 大家转得乱七八糟,正负互相抵消,合力接近于 0(注意力弱)。
这种特性简直是天赐的礼物!因为在人类语言中,离得越远的词,关系通常就越弱。RoPE 不需要任何额外的参数,光靠数学上的"相位错乱",就完美模拟了这个语言学规律。
这下能get到那个画面了吗?
在绝对位置编码中,如果你知道当前位置pos的编码,你想算出后面第 k个位置(即 pos+k)的编码,只需要乘以一个固定的矩阵就可以了。如果模型学会是不是也具有外推性
这是一个非常深刻且直击本质的问题!你对数学的敏锐度很高。
你的直觉是完全正确的:正弦波绝对位置编码(Sinusoidal APE)在数学上确实拥有和你说的 RoPE 一模一样的线性转换性质。
也就是: P E ( p o s + k ) PE(pos+k) PE(pos+k) 确实可以通过 P E ( p o s ) PE(pos) PE(pos) 乘以一个旋转矩阵得到。
但为什么它不能像 RoPE 那样拥有优秀的外推性呢? 问题的关键不在于"位置编码本身"能不能推导,而在于它和词向量的融合方式。
简而言之:一个是"加法"(污染了语义),一个是"乘法"(解耦了语义)。
我们来深度拆解一下这个细微但致命的区别。
1. 数学上的"亲兄弟"
首先确认你的观点。
正弦波位置编码(Transformer 原版)的公式里也是 sin 和 cos。根据三角函数和角公式,我们完全可以写出:
p ⃗ p o s + k = M k ⋅ p ⃗ p o s \vec{p}_{pos+k} = \mathbf{M}k \cdot \vec{p}{pos} p pos+k=Mk⋅p pos
其中 M k \mathbf{M}_k Mk 就是一个旋转矩阵。
这意味着,绝对位置编码向量本身,确实蕴含了相对位置信息。 这一点你没说错。
2. 致命的"加法" (APE 的困境)
在原始 Transformer 中,位置编码是这样用的:
输入 = 词向量 ( X ) + 位置编码 ( P ) \text{输入} = \text{词向量}(X) + \text{位置编码}(P) 输入=词向量(X)+位置编码(P)
然后这个混合后的东西去做 Attention(算点积)。我们展开 Attention 的公式看看发生了什么:
Attention分数 = ( X i + P i ) T ( X j + P j ) \text{Attention分数} = (X_i + P_i)^T (X_j + P_j) Attention分数=(Xi+Pi)T(Xj+Pj)
把它乘开,会得到 4 项:
- X i T X j X_i^T X_j XiTXj:词跟词的关系 (比如"苹果"和"吃")。------ 这是我们要的。
- P i T P j P_i^T P_j PiTPj:位置跟位置的关系 。因为 P P P 是正弦波,这一项确实等于 cos ( i − j ) \cos(i-j) cos(i−j)(相对距离)。------ 这也是我们要的。
- X i T P j X_i^T P_j XiTPj:词 i i i 跟 绝对位置 j j j 的关系。
- P i T X j P_i^T X_j PiTXj:绝对位置 i i i 跟 词 j j j 的关系。
问题就出在第 3 和第 4 项!
这两项把"词的内容"和"绝对位置"强行绑在了一起。
- 模型在训练时会记住:"'苹果'出现在'第5个位置'时是什么特征"。
- 它学会了 X X X 和 P P P 直接的交互。
当你外推时(遇到没见过的位置):
假设训练时最大长度是 2048。现在来了一个位置 3000。
- 虽然 P 3000 P_{3000} P3000 和 P 2999 P_{2999} P2999 之间的相对关系(第2项)是没问题的。
- 但是!模型从来没见过 P 3000 P_{3000} P3000 这个向量长什么样。
- 模型那一层的权重矩阵( W q , W k W_q, W_k Wq,Wk)从来没有处理过 X + P 3000 X + P_{3000} X+P3000 这种数值分布的向量。
- 结果就是:第 3、4 项(交叉项)算出来的数值直接崩坏,变成了噪音。
比喻:
这就像煮咖啡。
- APE(加法): 把牛奶(位置)倒进咖啡(词)里,搅匀了给模型喝。
- 模型喝惯了"50ml牛奶+咖啡"。突然你给了它一杯"500ml牛奶+咖啡"(长位置),味道全变了,模型直接吐了。
3. RoPE 的"解耦" (乘法的胜利)
RoPE 是怎么做的?它没有做加法,而是直接对 Q Q Q 和 K K K 向量进行旋转。
Q ′ = R p o s ⋅ Q Q' = R_{pos} \cdot Q Q′=Rpos⋅Q
K ′ = R p o s ⋅ K K' = R_{pos} \cdot K K′=Rpos⋅K
Attention 分数计算:
Q ′ T K ′ = ( R i Q ) T ( R j K ) = Q T R i T R j K = Q T R j − i K Q'^T K' = (R_i Q)^T (R_j K) = Q^T R_i^T R_j K = Q^T R_{j-i} K Q′TK′=(RiQ)T(RjK)=QTRiTRjK=QTRj−iK
注意看最后的结果!
在这个公式里,绝对位置 i i i 和 j j j 彻底消失了,只剩下了相对距离 j − i j-i j−i。
- RoPE 没有 X T P X^T P XTP 这种交叉项。
- 它强制让模型只通过相对距离来观察语义关系。
比喻:
- RoPE(乘法): 不往咖啡里加东西,而是把盛咖啡的杯子转一个角度 。
- 不管你是转 10 度(位置 1)还是转 10000 度(位置 3000),咖啡本身的味道(词向量的数值分布)没有被改变。
- 模型只需要判断"两个杯子的角度差",而不需要去尝那个从来没见过的"奇怪混合液体"。
总结
为什么绝对位置编码(APE)虽然有数学性质,却不能外推?
- "加法"破坏了分布: X + P X+P X+P 这种方式,让词向量在长位置时进入了模型权重的"未知领域"(Out-of-Distribution)。
- 交叉项干扰: Attention 计算中包含了"词 vs 绝对位置"的噪声项。一旦绝对位置超出训练范围,这些项就会捣乱。
- RoPE 的优势: 通过旋转(乘法),它把位置信息**完全"隐身"**在相对角度里,不改变词向量的模长(强度),也不产生有害的交叉项。
所以,RoPE 的成功不仅是因为那个旋转矩阵,更是因为它**把位置编码融合进 Attention 的方式(Injecting Method)**发生了革命性的变化。