如果单纯的使用词向量作为输入,自注意力机制的计算是忽略了序列中的位置信息的。本质上来看自注意力层计算的中间过程就是矩阵和矩阵之间的基础运算。这虽然极大的提升了计算效率,但是如果我们将序列的顺序打乱并不会影响自注意力层最后的计算结果,而这与序列性数据本身的特性是相悖的。因此作者在transformer架构的输入端引入了位置编码(Position Encoding)来将位置信息注入到词向量编码中,从而使自注意力层在提取特征信息时也能关注到位置这一重要信息。
位置编码加在哪里
位置编码作为transformer架构中唯一的位置信息,他在输入注意力层前(或者说在输入进整个transformer架构前)就被叠加到了原始输入的词向量中,其维度与词向量维度保持一致。
输入时添加方式很直接,直接将两个向量叠加:
从图中可以看到不仅是在输入的时候加了位置编码,在右边将outputs输入进右边的结构(decoder)时也添加了位置编码。如果你不了解transformer的运行机制这很容易让你产生疑问,这里简单解释一下后面在介绍整个transformer架构及其运行机理的时候会详细讲解。transformer在推理时是一个自回归架构,他不会一口气预测出所有的结果,而是会递归性地预测下一个词是什么。也就是说在预测下一个词的时候,之前的预测结果即outputs就变成了已知信息,他们将同样被作为输入输进右边的decoder中以便来使下一个词的预测结果更好。所以这些已经被预测的outputs再被输入到transformer模型中时,也需要被加入位置编码信息。
位置编码的类型
位置编码方法分为两种:绝对位置编码和相对位置编码。原始transformer论文中用到的方法则是绝对位置编码,后面有一些工作在这个基础上改进成了相对位置编码。那这两种编码方式究竟有哪些不同呢?
相对位置编码
相对位置编码主要是为了引入相对位置的信息,而不是绝对位置。这种方法在处理长序列和序列中元素的相对关系时表现得更好。在进行attention计算时,需要引入额外的相对位置信息变量,这个变量并不能被直接注入到词向量中
绝对位置编码
绝对位置编码为序列中的每个元素赋予一个独特的标识符,这些标识符直接映射到元素在序列中的具体位置。简单来说,可以直接通过数字序列(如1, 2, 3, …)来表示元素在序列中的顺序,例如:
你 网 球 打 的 真 厉 害 呀
1 2 3 4 5 6 7 8 9
然而,这种方法存在显著的缺陷。随着句子长度的增加,位置编码的数值也会增大,这可能导致误解,认为位置越靠后,其重要性或权重越大。实际上,这种编码方式并不能准确地反映每个位置的真实权重,因此并不是一个理想的解决方案。
正弦和余弦函数编码
transformer论文中提出了一种新的编码方式:正弦和余弦函数编码。其使用正弦和余弦函数的不同频率的组合来为当前位置生成唯一的位置编码向量。这种方法的优点是能够支持到任意长度的序列,并且模型可以从编码中推断出位置信息。下面是位置编码的计算公式,他针对奇数位和偶数位的向量计算方式不同(注意这里的奇数位和偶数位指的不是该元素的具体位置,而是该元素内特征向量的位数):
P
E
p
o
s
,
2
i
=
sin
(
p
o
s
1000
0
2
i
d
m
o
d
e
l
)
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} = \sin(\frac{pos}{10000^{\frac{2i}{d_{model}}}}) \\\tag*{} PE_{pos, 2i+1} = \cos(\frac{pos}{10000^{\frac{2i}{d_{model}}}})
PEpos,2i=sin(10000dmodel2ipos)PEpos,2i+1=cos(10000dmodel2ipos)
- pos 指该token在序列中的具体位置,即上面提到的12345
- d m o d e l d_{model} dmodel 指该token词向量embedding的维度,如768维
- 2 i 2i 2i 和 2 i + 1 2i+1 2i+1 指token的词向量内某个元素具体的index,如i=0时,就可以计算0和1这两个index元素的位置编码。0是偶数所以其使用的是sin函数,1是奇数使用的是cos函数
将上面的函数整合成一个列表就是:
P
E
p
o
s
=
[
sin
(
w
1
⋅
p
o
s
)
,
cos
(
w
1
⋅
p
o
s
)
,
sin
(
w
2
⋅
p
o
s
)
,
cos
(
w
2
⋅
p
o
s
)
,
.
.
.
,
sin
(
w
d
m
o
d
e
l
/
2
⋅
p
o
s
)
,
cos
(
w
d
m
o
d
e
l
/
2
⋅
p
o
s
)
]
PE_{pos} = \begin{bmatrix} \sin(w_1 \cdot pos), \\ \cos(w_1 \cdot pos), \\ \sin(w_2 \cdot pos), \\ \cos(w_2 \cdot pos), \\ ..., \\ \sin(w_{d_{model}/2} \cdot pos), \\ \cos(w_{d_{model}/2} \cdot pos) \\ \end{bmatrix} \tag*{}
PEpos=
sin(w1⋅pos),cos(w1⋅pos),sin(w2⋅pos),cos(w2⋅pos),...,sin(wdmodel/2⋅pos),cos(wdmodel/2⋅pos)
其中
w
i
=
1
1000
0
2
i
d
m
o
d
e
l
w_i = \frac{1}{10000^{\frac{2i}{d_{model}}}}
wi=10000dmodel2i1
得到的 P E p o s PE_{pos} PEpos 就是某个token的位置编码,其维度就是 ( d m o d e l , 1 ) (d_{model}, 1) (dmodel,1), 最后把 P E p o s PE_{pos} PEpos加到该token的词向量上即可(维度一致直接相加)。
这里举个例子:
假设词向量Embedding的维度是6,即
d
m
o
d
e
l
=
6
d_{model}=6
dmodel=6,根据上面的公式:
pos = 0 的情况
P
E
0
=
[
sin
(
1
1000
0
0
6
⋅
0
)
,
cos
(
1
1000
0
0
6
⋅
0
)
,
sin
(
1
1000
0
2
6
⋅
0
)
,
cos
(
1
1000
0
2
6
⋅
0
)
,
sin
(
1
1000
0
4
6
⋅
0
)
,
cos
(
1
1000
0
4
6
⋅
0
)
]
=
[
0
,
1
,
0
,
1
,
0
,
1
]
PE_{0} = \begin{bmatrix} \sin(\frac{1}{10000^\frac{0}{6}} \cdot 0), \\ \cos(\frac{1}{10000^\frac{0}{6}} \cdot 0), \\ \sin(\frac{1}{10000^\frac{2}{6}} \cdot 0), \\ \cos(\frac{1}{10000^\frac{2}{6}} \cdot 0), \\ \sin(\frac{1}{10000^\frac{4}{6}} \cdot 0), \\ \cos(\frac{1}{10000^\frac{4}{6}}\cdot 0) \\ \end{bmatrix} = \begin{bmatrix} 0, \\ 1, \\0, \\ 1, \\ 0, \\ 1 \end{bmatrix}\tag*{}
PE0=
sin(10000601⋅0),cos(10000601⋅0),sin(10000621⋅0),cos(10000621⋅0),sin(10000641⋅0),cos(10000641⋅0)
=
0,1,0,1,0,1
pos = 1 的情况
P
E
1
=
[
sin
(
1
1000
0
0
6
⋅
1
)
,
cos
(
1
1000
0
0
6
⋅
1
)
,
sin
(
1
1000
0
2
6
⋅
1
)
,
cos
(
1
1000
0
2
6
⋅
1
)
,
sin
(
1
1000
0
4
6
⋅
1
)
,
cos
(
1
1000
0
4
6
⋅
1
)
]
=
[
0.8414709848
,
0.54030230586
,
0.04639922346
,
0.99892297604
,
0.00215443302
,
0.9999976792
]
PE_{1} = \begin{bmatrix} \sin(\frac{1}{10000^\frac{0}{6}} \cdot 1), \\ \cos(\frac{1}{10000^\frac{0}{6}} \cdot 1), \\ \sin(\frac{1}{10000^\frac{2}{6}} \cdot 1), \\ \cos(\frac{1}{10000^\frac{2}{6}} \cdot 1), \\ \sin(\frac{1}{10000^\frac{4}{6}} \cdot 1), \\ \cos(\frac{1}{10000^\frac{4}{6}}\cdot 1) \\ \end{bmatrix} = \begin{bmatrix} 0.8414709848, \\ 0.54030230586, \\0.04639922346, \\ 0.99892297604, \\ 0.00215443302, \\ 0.9999976792 \end{bmatrix}\tag*{}
PE1=
sin(10000601⋅1),cos(10000601⋅1),sin(10000621⋅1),cos(10000621⋅1),sin(10000641⋅1),cos(10000641⋅1)
=
0.8414709848,0.54030230586,0.04639922346,0.99892297604,0.00215443302,0.9999976792
pos = 2 的情况
P
E
1
=
[
sin
(
1
1000
0
0
6
⋅
2
)
,
cos
(
1
1000
0
0
6
⋅
2
)
,
sin
(
1
1000
0
2
6
⋅
2
)
,
cos
(
1
1000
0
2
6
⋅
2
)
,
sin
(
1
1000
0
4
6
⋅
2
)
,
cos
(
1
1000
0
4
6
⋅
2
)
]
=
[
0.90929742682
,
−
0.41614683654
,
0.09269850077
,
0.99569422412
,
0.00430885604
,
0.99999071683
]
PE_{1} = \begin{bmatrix} \sin(\frac{1}{10000^\frac{0}{6}} \cdot 2), \\ \cos(\frac{1}{10000^\frac{0}{6}} \cdot 2), \\ \sin(\frac{1}{10000^\frac{2}{6}} \cdot 2), \\ \cos(\frac{1}{10000^\frac{2}{6}} \cdot 2), \\ \sin(\frac{1}{10000^\frac{4}{6}} \cdot 2), \\ \cos(\frac{1}{10000^\frac{4}{6}}\cdot 2) \\ \end{bmatrix} = \begin{bmatrix} 0.90929742682, \\-0.41614683654, \\0.09269850077, \\ 0.99569422412, \\ 0.00430885604, \\ 0.99999071683\end{bmatrix}\tag*{}
PE1=
sin(10000601⋅2),cos(10000601⋅2),sin(10000621⋅2),cos(10000621⋅2),sin(10000641⋅2),cos(10000641⋅2)
=
0.90929742682,−0.41614683654,0.09269850077,0.99569422412,0.00430885604,0.99999071683
通过计算公式及下图我们可以知道,每一个特征维度都对应一根变化曲线,随着Position的变换该特征维度位置编码的数值也在随之变化。
下图只画出了第4,5,6,7维度的变化曲线,我们随便取一个位置Position的值(绿线),将他穿过的每条曲线所对应的纵轴数值拼接起来,就可以得到该位置完整的位置编码了。每个特征维度的变化都服从该维度的变化曲线(唯一),那么我们将
d
m
o
d
e
l
d_{model}
dmodel个特征维度组成的位置编码送入网络,网络就可以学习到每一组位置编码所代表的位置信息了,从而最终实现将位置信息转换成位置编码,再将位置编码叠加到输入词向量中的完整过程。
除此之外,该方法还有以下特性
- 平滑性:每一个特征维度上的位置编码都是随Position的变化而平滑变化的,有助于模型更好地学习和泛化位置信息
- 正交性:正弦和余弦函数是正交的,这意味着它们在周期内相互正交。这就扩充了低频率部分表达能力,因为如果只使用一种函数的话,随着特征维度的增多,波长会越来越长,到最后的变换可能就会特别特别缓慢,不同position的值可能差距就会特别小,这其实并不利于模型的学习。由于正交性的存在,不同维度上的编码向量相互独立,不会相互干扰。它允许模型更清晰、更准确地学习和识别来自序列中不同位置的特征。例如,如果两个位置的编码在多个维度上都有明显的差异,模型就能够更容易地判断这两个位置是不同的。这种区分能力对于理解序列数据,如文本或时间序列,是非常关键的,因为它使得模型能够捕捉到序列中位置的细微差别,从而提高模型的性能和准确性。
- 相对位置相关性:虽然这种基于三角函数的位置编码是一种绝对位置编码表示,但是由于三角函数的特性也能一定程度的捕捉相对位置信息(具体数学原理有兴趣可以自行搜索,这里不做拓展)。虽然能够间接地反映序列中元素的相对位置关系,但这种表示方式并不是直接或明确的。这就意味着,模型在学习和利用相对位置信息时可能会面临一些挑战,尤其是在处理那些较长的序列和复杂的依赖结构时。由于相对位置信息的表示不够直接,模型可能需要更多的训练数据和更强的学习能力来有效地捕捉和利用这些信息。
代码实现
class PositionalEncoding(nn.Module):
"Implement the PE function."
def __init__(self, d_model, dropout, max_len=5000):
super(PositionalEncoding, self).__init__()
# Compute the positional encodings once in log space.
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len).unsqueeze(1)
div_term = torch.exp(
torch.arange(0, d_model, 2) * -(math.log(10000.0) / d_model)
)
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
pe = pe.unsqueeze(0)
self.register_buffer("pe", pe)
def forward(self, x):
x = x + self.pe[:, : x.size(1)].requires_grad_(False)
return x