理解 Transformer 中的位置编码 Positional Encoding
在学习 Transformer 的自注意力机制时,一个很容易让人困惑的问题是:
Self-Attention 明明可以让每个 token 关注序列中的其他 token,为什么还需要位置编码?
原因在于:Self-Attention 本身并不知道 token 的顺序。
位置编码的作用,就是给每个 token 加入顺序信息,让模型不仅知道“这个 token 是什么”,还知道“这个 token 在哪里”。
1. 为什么 Self-Attention 需要位置编码?
在 RNN 中,序列是按时间步一个一个输入的:
\(x_1 \rightarrow x_2 \rightarrow x_3 \rightarrow x_4\)
因此 RNN 天然知道顺序。第一个 token 先进来,第二个 token 后进来,隐状态也是按时间一步步更新的。
但 Self-Attention 不同。Self-Attention 会让序列中的所有 token 同时参与注意力计算。它关注的是 token 之间的关系,而不是按时间顺序依次处理。
这就带来一个问题。
比如:
“我爱你”
和:
“你爱我”
这两个句子包含的 token 是一样的,但顺序不同,含义也不同。
如果模型只看到 token embedding,而没有任何位置信息,那么它很难区分这两个句子的差异。
因此,Transformer 需要额外给每个 token 加入位置信息。这就是位置编码。
2. Transformer 的输入是什么?
每个 token 原本会被映射成一个 token embedding:\(E(x_t)\),它表示“这个 token 是什么”。
位置编码则为每个位置生成一个向量:\(P_t\),它表示“这个 token 在第几个位置”。
Transformer 的实际输入是二者相加:\(X_t = E(x_t) + P_t\),也就是说,模型看到的不是单纯的 token embedding,而是:
token embedding + position encoding
词义信息 + 位置信息
例如:
\(E(\text{我}) + P_0\)
表示“第 0 个位置上的‘我’”。
\(E(\text{爱}) + P_1\)
表示“第 1 个位置上的‘爱’”。
\(E(\text{你}) + P_2\)
表示“第 2 个位置上的‘你’”。
这样,模型就不仅知道 token 是什么,也知道 token 出现在序列中的哪个位置。
3. 为什么 token embedding 和 position encoding 可以相加?
因为它们的维度相同。
如果 token embedding 是 \(d\) 维:
\(E(x_t) \in \mathbb{R}^d\)
那么 position encoding 也是 \(d\) 维:
\(P_t \in \mathbb{R}^d\)
因此二者可以逐维相加:
\(X_t = E(x_t) + P_t\)
假设:
\(E = [e_1, e_2, e_3, e_4]\)
\(P = [p_1, p_2, p_3, p_4]\)
那么:
\(X = [e_1+p_1, e_2+p_2, e_3+p_3, e_4+p_4]\)
这个操作确实改变了原始 token embedding 的数值。
但这并不意味着它破坏了语义。更准确地说,它把原来的表示从“这个 token 是什么”,变成了“这个 token 是什么,并且它在什么位置”。
也就是说,相加不是抹掉语义,而是给 token 的表示叠加了位置信息。
4. 相加之后,模型怎么利用词义和位置信息?
相加之后,模型并不会显式地把向量拆成两部分:一部分是 token embedding,另一部分是 position encoding。
模型看到的只是一个混合后的向量。
但这并不妨碍模型使用其中的信息。原因在于:位置编码是固定且有规律的,token embedding 是可学习的。模型在训练过程中会逐渐学会如何利用这种混合表示。
例如,Self-Attention 中会计算 Query:
\(Q = XW_Q\)
而:
\(X = E + P\)
所以:
\(Q = (E + P)W_Q\)
根据线性运算的分配律:
\(Q = EW_Q + PW_Q\)
这说明,虽然 token embedding 和 position encoding 已经相加在一起,但它们仍然会以不同方式影响后续计算。
模型不一定需要把二者显式分离出来,它只需要学会从这个“带位置的 token 表示”中提取对任务有用的信息。
5. 正弦/余弦位置编码公式
Transformer 原论文中使用的是正弦/余弦位置编码:
其中:
| 符号 | 含义 |
|---|---|
| \(pos\) | token 在序列中的位置 |
| \(i\) | 维度对的编号,取值范围 \([0, (d/2)-1]\) |
| \(d\) | embedding 的总维度 |
| \(2i\) | 偶数维度 |
| \(2i+1\) | 奇数维度 |
这两个公式表示:
- 偶数维使用 \(\sin\)
- 奇数维使用 \(\cos\)
- 不同维度使用不同频率的位置编码
6. pos 是什么?
\(pos\) 表示 token 在序列中的位置。
如果一个序列长度是 \(T\),那么位置编号通常是:
\(pos = 0, 1, 2, \dots, T-1\)
比如句子:
“我 爱 你”
有 3 个 token,那么:
| token | pos |
|---|---|
| 我 | 0 |
| 爱 | 1 |
| 你 | 2 |
每个位置都会生成一个 \(d\) 维的位置编码向量。
7. i 是什么?
\(i\) 表示位置编码中的“维度对编号”。
因为公式中有:
\(PE(pos, 2i)\)
和:
\(PE(pos, 2i+1)\)
所以每个 \(i\) 对应两个维度。
如果 embedding 维度 \(d\) 是偶数,那么:
\(i = 0, 1, 2, \dots, \frac{d}{2}-1\)
例如,当 \(d=8\) 时:
| \(i\) | 对应维度 |
|---|---|
| 0 | 第 0 维、第 1 维 |
| 1 | 第 2 维、第 3 维 |
| 2 | 第 4 维、第 5 维 |
| 3 | 第 6 维、第 7 维 |
其中:
- 第 \(2i\) 维使用 \(\sin\)
- 第 \(2i+1\) 维使用 \(\cos\)
8. 公式中的 \(10000^{2i/d}\) 是干什么的?
这一部分控制不同维度的位置编码变化速度。
位置编码不是让所有维度都用同样的 \(\sin(pos)\) 或 \(\cos(pos)\),而是让不同维度使用不同频率。
公式是:
\(\sin\left(\frac{pos}{10000^{2i/d}}\right)\)
\(\cos\left(\frac{pos}{10000^{2i/d}}\right)\)
随着 \(i\) 增大:
\(10000^{2i/d}\)
会变大。
因此:
\(\frac{pos}{10000^{2i/d}}\)
会变小。
这意味着,维度编号越靠后,\(\sin\) 和 \(\cos\) 里面的自变量变化越慢。
可以简单理解为:
| 维度 | 变化特点 |
|---|---|
| 低维度 | 变化快,适合区分近距离位置 |
| 高维度 | 变化慢,适合表达远距离位置 |
也就是说,不同维度使用了不同频率的波形。多个频率组合在一起,就可以为每个位置生成一个独特的位置“指纹”。
9. 一个 \(d=8\) 的例子
假设 embedding 维度是:
\(d = 8\)
那么:
\(i = 0, 1, 2, 3\)
位置编码可以展开为:
\(PE(pos) = [\sin(pos), \cos(pos), \sin(pos/10), \cos(pos/10), \sin(pos/100), \cos(pos/100), \sin(pos/1000), \cos(pos/1000)]\)
对于位置 \(pos=1\):
\(PE(1) = [\sin(1), \cos(1), \sin(0.1), \cos(0.1), \sin(0.01), \cos(0.01), \sin(0.001), \cos(0.001)]\)
对于位置 \(pos=2\):
\(PE(2) = [\sin(2), \cos(2), \sin(0.2), \cos(0.2), \sin(0.02), \cos(0.02), \sin(0.002), \cos(0.002)]\)
可以看到,不同位置会得到不同的位置编码向量。
10. 高维度的位置编码数值会更小吗?
不一定。
因为 \(\sin\) 和 \(\cos\) 的输出范围始终是:
\([-1, 1]\)
所以不能说高维度的位置编码数值一定更小。
更准确的说法是:
随着维度编号增大,\(\sin\) 和 \(\cos\) 里面的自变量变小,位置编码随 \(pos\) 的变化更慢。
也就是说,高维度不是“数值更小”,而是“变化频率更低”。
11. 位置编码可以理解成什么?
可以把位置编码理解成每个位置的“指纹”。
第 0 个位置有一组 sin/cos 数值,第 1 个位置有另一组 sin/cos 数值,第 2 个位置又有另一组 sin/cos 数值。
每个位置都有自己的编码模式。
同时,由于这些编码是由多种频率的 sin/cos 组合而成的,不同位置之间的相对关系也具有一定规律。
这对 Transformer 很重要,因为模型不仅需要知道某个 token 在第几个位置,还需要知道两个 token 之间相隔多远。
12. 为什么不用简单的位置编号?
可能有人会问:既然只是表示位置,为什么不直接给第一个 token 加 1,第二个 token 加 2,第三个 token 加 3?
原因是,简单的整数编号不适合直接作为高维向量输入模型。
位置编码需要满足几个要求:
第一,它要和 token embedding 维度一致,方便相加。
第二,不同位置应该有不同表示。
第三,相近位置之间应该有某种连续关系。
第四,模型应该容易从中推断相对位置关系。
正弦/余弦位置编码正好满足这些特点。它既能给每个位置生成不同向量,又具有规律性,方便模型学习位置之间的关系。
13. 总结
位置编码的核心作用是:
给 Self-Attention 加入顺序信息。
Self-Attention 本身不按时间步处理序列,因此它天然缺少顺序感。
所以 Transformer 输入的不是单纯的 token embedding,而是:
\(token\ embedding + position\ encoding\)
也就是:
这个 token 是什么 + 它在什么位置
正弦/余弦位置编码公式为:
可以将其理解为:
- 为每个位置生成一个 \(d\) 维向量
- 偶数维用 \(\sin\)
- 奇数维用 \(\cos\)
- 不同维度使用不同频率
- 低维变化快,高维变化慢
- 最终为每个 token 注入位置信息
一句话概括:
位置编码就是给每个 token embedding 加上一组有规律的位置向量,让 Transformer 能够感知序列顺序。

浙公网安备 33010602011771号