大模型里,藏着一个特别容易被忽略、却又至关重要的细节:
Transformer 本身,是"看不见顺序"的。
你把一句话里的词打乱,再喂给自注意力机制,它计算出来的注意力权重,几乎不受影响——因为自注意力对每个词一视同仁,"猫追老鼠"和"老鼠追猫"在它眼里,是一模一样的一组词。
可谁都知道,这两句话意思完全相反。顺序,是语言里最不能丢的信息之一。
所以,必须想办法把"位置信息"塞进模型。这个"塞"的方法,就叫位置编码(Positional Encoding)。而今天要讲的 RoPE(Rotary Position Embedding,旋转位置编码),是当下 LLaMA、GPT-NeoX、Qwen 等主流大模型都在用的、最流行的一种。这篇,把它一次讲透。
最早的方案:把"位置"直接加进"词向量"
先看最朴素的做法。
Transformer 原文用的是正弦位置编码:给每个位置(第 0 个词、第 1 个词……)生成一个固定向量,然后直接加到那个位置的词向量上。
# 词向量 + 位置向量 = 带位置信息的向量
input = word_embedding[token] + positional_encoding[pos]
思路很直白:既然模型看不见顺序,那我就把"位置"当成一种信息,混进输入里。模型看到"词向量变了",就间接"感知"到了位置。
这个方案能用,但它有一个结构性的短板:位置信息是"加"上去的,位置和词义是"混合"在一起、搅成一团的。 模型得费劲地在这锅"粥"里,同时分辨出"哪部分是词义、哪部分是位置"。
而且,它学到的更多是绝对位置——"第 3 个词"和"第 100 个词"不同。可语言里真正重要的,往往是相对位置:"这个词"和"它前面的那个词"的关系,跟它们在句子的第几位,没太大关系。一段话往后挪了 50 个位置,意思不该变。
于是有人想:能不能直接编码"相对位置"? RoPE,就是这个思路下最漂亮的一个答案。
RoPE 的核心想法:用"旋转"来标记位置
RoPE 的思路,初看有点"奇技淫巧",但理解了会拍案叫绝:
不把位置"加"进词向量,而是根据位置,把词向量"旋转"一个角度。
怎么理解"旋转"?
先想象一个二维平面。一个向量 ( x , y ) (x, y) (x,y),把它逆时针旋转一个角度 θ \theta θ,得到新向量 ( x ′ , y ′ ) (x', y') (x′,y′),数学上就是乘一个旋转矩阵:
$$
\begin{pmatrix} x’ \ y’ \end{pmatrix}
\begin{pmatrix} \cos\theta & -\sin\theta \ \sin\theta & \cos\theta \end{pmatrix}
\begin{pmatrix} x \ y \end{pmatrix}
$$
旋转有个极其宝贵的性质:它不改变向量的长度,只改变方向。 也就是说,旋转"几乎不破坏"向量里原本携带的信息(模长不变),却又能给它打上一个"角度"的标记。
RoPE 要干的事,就是把这个"旋转"用起来:
- 位置 m 的词向量,旋转角度 θ m \theta_m θm;位置 n 的词向量,旋转角度 θ n \theta_n θn;
- 两个词做注意力点积时,它们之间的"位置差",会自然体现为旋转角度之差 ( θ m − θ n ) (\theta_m - \theta_n) (θm−θn)。
这里藏着 RoPE 最精妙的一步,也是它为什么能编码"相对位置"的关键。我们拆一下。
妙就妙在:旋转之后做点积,"相对位置"自己冒出来了
注意力机制的核心运算,是两个向量的点积( q ⋅ k q \cdot k q⋅k,Query 点乘 Key)。RoPE 让这个点积,自动带上位置信息。
这里用到一个三角恒等式(和角公式):两个向量分别旋转 θ m \theta_m θm 和 θ n \theta_n θn 后再做点积,结果等于"原向量点积,再旋转它们角度之差":
$$
\text{Rot}(q, \theta_m) \cdot \text{Rot}(k, \theta_n)
(q \cdot k)\ \text{关于}\ (\theta_m - \theta_n)\ \text{的旋转}
$$
换句话说:旋转后的点积,只跟"两个位置的差 ( θ m − θ n ) (\theta_m - \theta_n) (θm−θn)"有关,而跟它们的"绝对位置"无关。
这正是我们想要的东西!它意味着:
- "第 3 个词"和"第 5 个词"的关系(差 2),
- 跟"第 103 个词"和"第 105 个词"的关系(也差 2),
在 RoPE 眼里是一样的——因为它们的相对距离相同,旋转角度之差相同。相对位置,被"旋转角度之差"优雅地编码了出来,绝对位置被自动抵消了。
这跟正弦位置编码"加绝对位置"的思路,有本质区别:RoPE 不告诉你"你在第几位",只告诉你"你和我隔着多远"。 而后者,恰恰是语言更需要的。
高维向量,怎么"旋转"?
上面为了好懂,只说了二维。可真实的词向量是几百上千维的,怎么旋转一个高维向量?
RoPE 的做法很巧:把高维向量,两两一组,拆成很多个"二维平面",每个平面独立旋转。
比如一个 4 维向量 ( x 1 , x 2 , x 3 , x 4 ) (x_1, x_2, x_3, x_4) (x1,x2,x3,x4),拆成两组: ( x 1 , x 2 ) (x_1, x_2) (x1,x2) 和 ( x 3 , x 4 ) (x_3, x_4) (x3,x4)。第一组在"第 1、2 维张成的平面"上旋转,第二组在"第 3、4 维张成的平面"上旋转。每个平面,用不同的旋转频率。
这样,一个高维向量就被"旋转"起来了,而旋转不改变模长的性质,保证了词向量里原本的语义信息基本不被破坏。
(实际实现里,还会给不同维度组分配不同的旋转频率——低维转得慢、高维转得快,让模型能同时捕捉"短距离"和"长距离"的相对位置。这个细节不必深究,知道"不同维度组、不同频率"就够了。)
为什么 RoPE 能"外推",以及它好在哪
RoPE 相比正弦编码,有几个实打实的优势:
- 天然编码相对位置:不靠"加绝对位置",而是靠"旋转角度之差",模型更容易学到"词与词的相对关系";
- 不增加任何可训练参数:正弦编码也是固定的,RoPE 同样是固定的旋转,不给模型添负担;
- 对长序列的外推更友好:因为旋转是连续的、可插值的,训练时见过的位置和没见过的位置之间,可以平滑过渡——这是后来各种"长度外推"技巧(如 NTK-aware 缩放)能玩得转的基础。
正是这些优点,让 RoPE 成了 LLaMA 之后几乎所有主流开源大模型的位置编码标配。你今天用的 ChatGPT 背后的模型、本地的 LLaMA、Qwen,位置信息的"旋转"大概率都是 RoPE 的功劳。
一个最小化的 numpy 演示
光说不练,永远是隔靴搔痒。下面用 numpy 写一个"二维版 RoPE",感受下"旋转"到底是怎么发生的:
import numpy as np
def rotate(x, theta):
"""把二维向量 x 逆时针旋转 theta 弧度"""
c, s = np.cos(theta), np.sin(theta)
R = np.array([[c, -s],
[s, c]])
return R @ x
# 两个"词向量"(二维演示),位置分别是 m=3 和 n=5
q = np.array([1.0, 0.0])
k = np.array([0.0, 1.0])
theta_m, theta_n = 0.1 * 3, 0.1 * 5 # 位置越大,旋转越多
q_rot = rotate(q, theta_m)
k_rot = rotate(k, theta_n)
dot_before = q @ k # 旋转前的点积
dot_after = q_rot @ k_rot # 旋转后的点积
print(f"旋转前点积: {dot_before:.4f}")
print(f"旋转后点积: {dot_after:.4f}")
print(f"角度差 (theta_m - theta_n): {theta_m - theta_n:.4f}")
# 旋转后的点积,只取决于"角度差",而角度差 = 相对位置 × 频率
你跑一遍就会发现:旋转后的点积,只跟"两个位置的差"有关。 把 m、n 同时各加 100,只要它们的"差"不变,点积就不变——这就是 RoPE"编码相对位置"的直观体现。
结语:换个"运算法则",难题就绕过去了
RoPE 给我最大的启发,是一个关于"怎么表示信息"的思维方式:
同样是"编码位置",有人选择"加"(把位置向量加上去),有人选择"乘"(用旋转矩阵去乘)。换一种运算法则,原本搅成一团的"绝对位置"难题,就绕过去、变成了优雅的"相对位置"。
它再次印证了那个朴素的道理:很多时候,难题不是"解不开",而是"没找对表示它的方式"。 换一个坐标系、换一种编码、换一个角度(字面意义的旋转),答案就浮现了。
位置编码这个小东西,看着不起眼,却是大模型"读懂语言"的地基。而 RoPE 用一场漂亮的"旋转",把这块地基打得更牢、更巧。
想把 RoPE 里的旋转矩阵、点积、三角恒等式这些数学彻底看透,线代和三角是绕不开的底子。推荐 B站【408实验室】的《机器学习数学基础》,把向量、矩阵、内积这些基础补牢——你会发现,大模型里那些"玄乎"的技巧,背后全是干净的数学。
转载自 CSDN-专业IT技术社区




