在前面其实讲过位置编码的完整内容,这次我们具体看看他的数学原理
B站视频讲解
白话transformer(五)
1、位置编码的位置
根据原论文的结构图我们可以看到,位置编码位于embedding后,在正式进入注意力机制前面。
也就是说这个位置编码不同于之前的RNN和LSTM模型是在模型中进行输入的,而是进入模型前输入的。
2、公式
P ( p o s , 2 i ) = s i n ( p o s 1000 0 2 i / d m o d e l ) P(pos,2i) = sin(\frac{pos}{10000^{2i/d_{model}}}) P(pos,2i)=sin(100002i/dmodelpos)
P ( p o s , 2 i + 1 ) = c o s ( p o s 1000 0 2 i / d m o d e l ) P(pos,2i+1) = cos(\frac{pos}{10000^{2i/d_{model}}}) P(pos,2i+1)=cos(100002i/dmodelpos)
我们用之前在讲QKV矩阵中用到的那个例子来看下
因为位置编码和embedding是相加的所以位置编码的形状大小必须和原始的embedding保持一致,应该也是8*10 的
以我
为例来看下,我的位置是在第一行,那么他的pos=1(公式中的pos);
i是指向量长度上的维度,所以需要根据列的数值来确定,因为这里同时使用了sin \cos,所以我们需要注意在计算时i的取值不是embedding的长度10,而是embedding的长度的一半,也就是10/2 = 5;所以我
位置编码的i取值应该是【0,0,2,2,4,4,6,6,8,8,10,10】
所以根据上面的计算方法可以得到其他的位置编码信息,如下:
P = [ sin ( p o s 1000 0 0 / 10 ) cos ( p o s 1000 0 0 / 10 ) sin ( p o s 1000 0 2 / 10 ) cos ( p o s 1000 0 2 / 10 ) … … sin ( p o s 1000 0 0 / 10 ) cos ( p o s 1000 0 0 / 10 ) sin ( p o s 1000 0 2 / 10 ) cos ( p o s 1000 0 2 / 10 ) … … sin ( p o s 1000 0 0 / 10 ) cos ( p o s 1000 0 0 / 10 ) sin ( p o s 1000 0 2 / 10 ) cos ( p o s 1000 0 2 / 10 ) … … … … … … … … … … sin ( p o s 1000 0 0 / 10 ) cos ( p o s 1000 0 0 / 10 ) sin ( p o s 1000 0 2 / 10 ) cos ( p o s 1000 0 2 / 10 ) … … ] P = \left[\begin{array}{cccc} \sin\left(\frac{pos}{10000^{0/10}}\right) & \cos\left(\frac{pos}{10000^{0/10}}\right) & \sin\left(\frac{pos}{10000^{2/10}}\right) & \cos\left(\frac{pos}{10000^{2/10}}\right) &……\\[10pt] \sin\left(\frac{pos}{10000^{0/10}}\right) & \cos\left(\frac{pos}{10000^{0/10}}\right) & \sin\left(\frac{pos}{10000^{2/10}}\right) & \cos\left(\frac{pos}{10000^{2/10}}\right) &……\\[10pt] \sin\left(\frac{pos}{10000^{0/10}}\right) & \cos\left(\frac{pos}{10000^{0/10}}\right) & \sin\left(\frac{pos}{10000^{2/10}}\right) & \cos\left(\frac{pos}{10000^{2/10}}\right) &……\\[10pt] &……&……&……&……\\ \sin\left(\frac{pos}{10000^{0/10}}\right) & \cos\left(\frac{pos}{10000^{0/10}}\right) & \sin\left(\frac{pos}{10000^{2/10}}\right) & \cos\left(\frac{pos}{10000^{2/10}}\right) &……\\[10pt] \end{array}\right] P= sin(100000/10pos)sin(100000/10pos)sin(100000/10pos)sin(100000/10pos)cos(100000/10pos)cos(100000/10pos)cos(100000/10pos)……cos(100000/10pos)sin(100002/10pos)sin(100002/10pos)sin(100002/10pos)……sin(100002/10pos)cos(100002/10pos)cos(100002/10pos)cos(100002/10pos)……cos(100002/10pos)…………………………
3、位置编码的code
def positional_encoding(pos, d_model):
"""
生成位置编码
:param pos: 位置序号 (序列长度)
:param d_model: 模型的维度
:return: 位置编码矩阵 (pos, d_model)
"""
# 初始化位置编码矩阵
pos_encoding = np.zeros((pos, d_model))
# 获取位置索引,并扩展维度以进行计算
position = np.arange(pos)[:, np.newaxis]
# 计算分母中的项
div_term = np.exp(np.arange(0, d_model, 2) * -(np.log(10000.0) / d_model))
# 将正弦应用于偶数索引
pos_encoding[:, 0::2] = np.sin(position * div_term)
# 将余弦应用于奇数索引
pos_encoding[:, 1::2] = np.cos(position * div_term)
return pos_encoding
初始化一个与输入矩阵大小一样的初始矩阵
# 设置位置序号和模型的维度
pos = 100 # 序列长度
d_model = 16 # 模型的维度
# 生成位置编码
pos_encoding = positional_encoding(pos, d_model)
获取位置索引,并扩展维度以进行计算
position = np.arange(pos)[:, np.newaxis]
print(position)
[[0]
[1]
[2]
[3]
[4]
[5]
[6]
[7]]
-
np.arange(pos) 生成一个从 0 到 pos-1 的序列数组。如果 pos 是 8,它会生成一个数组 [0, 1, 2, …, 7]。
-
[:, np.newaxis] 这个部分是一种索引操作,它的作用是增加一个新的轴。: 表示选取数组中的所有项,np.newaxis 是一个特殊的索引器,它会增加一个轴。这里,它被用来将一维数组转换为二维的列向量。
计算分母中的项
div_term = np.exp(np.arange(0, d_model, 2) * -(np.log(10000.0) / d_model))
div_term
-
np.arange(0, d_model, 2) 创建了一个从 0 到 d_model(不包括 d_model)的数组,步长为 2。这意味着它会生成 [0, 2, 4, …, d_model-2](假设 d_model 是偶数)的数组。这个数组用于确定编码的频率:在原始 Transformer 论文中,每个维度的频率是根据这个维度的序号确定的。
-
np.log(10000.0) / d_model 计算了一个常数,这是用于调整频率的比例因子。10000 是一个超参数,可以根据需要调整。这个常数用于控制编码中频率的变化,使其对于模型的不同维度而言是不同的。
- -(np.log(10000.0) / d_model) 将上述创建的数组中的每个元素乘以这个负的比例因子。乘以负数是因为指数函数 np.exp 会随着输入的增加而增加,而在位置编码中,我们希望随着维度的增加而减少频率。
- np.exp(…) 将 e(自然对数的底数)的幂次应用于上面计算的每个元素。结果是对于模型中的每个偶数维度,我们得到了一个减小的指数尺度。
这个 div_term 数组用来缩放位置索引,用于生成位置编码中的波长。随着维度的增加,波长会指数级地减小,这样在模型的较高维度上,位置编码的变化会更加迅速,这有助于模型学习和区分不同位置的信息。
将正弦应用于偶数索引
pos_encoding[:, 0::2] = np.sin(position * div_term)
pos_encoding
偶数列上已经有了数据
将余弦应用于奇数索引
pos_encoding[:, 1::2] = np.cos(position * div_term)
全部数据生成
4、绘图
# 使用更圆滑的线条和不同的颜色绘制位置编码曲线图
plt.figure(figsize=(14, 8))
colors = plt.cm.viridis(np.linspace(0, 1, min(pos, 10)))
for i in range(2,min(pos, 6)): # 只展示前10个位置的编码
plt.plot(pos_encoding[:,i], label=f'Dimension {i}', color=colors[i], linewidth=2, linestyle='-', marker='', markersize=6)
plt.xlabel('Position')
plt.ylabel('PE value')
plt.title('Smooth Positional Encoding Curves for First Few Positions')
plt.legend()
plt.grid(True)
plt.show()
我们可以看到,取了2、3、4、5这4个维度来查看,发现维度2和3这两个形状很像只是发生了位移,因为他们的周期数值是一样的
5、优点
使用正弦(sin)和余弦(cos)函数来生成位置编码在 Transformer 模型中具有几个关键优点:
-
周期性和连续性:
正弦和余弦函数是周期性的,这使得模型能够自然地学习和利用序列的周期性模式。此外,由于这些函数是连续的,模型可以更容易地进行泛化,从而理解未见过的位置。 -
独特的位置信息:
由于 sin 和 cos 函数的值在每个周期内是唯一的,结合两者可以为序列中的每个位置生成独一无二的编码,从而帮助模型捕捉位置信息。 -
可扩展性:
正弦和余弦编码允许模型处理比训练期间遇到的序列更长的序列。由于其周期性,模型可以推断出序列中的相对位置,即使是在训练数据之外的位置。 -
可加性:
正弦和余弦函数使位置编码成为可加的。这意味着模型可以通过叠加两个位置的编码来理解位置偏移,这在某些序列到序列的任务中特别有用,如机器翻译。 -
编码相对位置:
正弦和余弦波的频率可以帮助模型理解位置之间的相对距离。由于这些波的相位差,模型可以通过位置编码的内积来推断两个位置之间的距离,这对于理解单词之间的关系很重要。 -
多尺度的位置编码:
在 Transformer 中,通过使用不同频率的正弦和余弦函数,位置编码可以在多个尺度上编码信息。这使得模型可以同时捕捉短距离和长距离的依赖关系。 -
与学习参数无关:
正弦和余弦位置编码不依赖于模型训练过程中的学习,这意味着它们在训练开始时就是固定的,并且可以作为对学习到的注意力权重的补充。
正弦和余弦波的这些特性使得位置编码不仅可以提供绝对位置信息,还可以帮助模型学习序列中元素的相对位置,这是理解自然语言等复杂序列数据的关键。
6、疑问
虽然我可以说清楚位置编码的流程和方法,但是没有搞明白为什么位置编码会有效,因为他是直接进行相加后再进行训练的,之前学习了大神李沐的课程也貌似没说明白,只是说实践证明确实有效。经过多方查找资料,找到一下信息,仅供参考:
在 Transformer 模型中,位置编码的有效性来自于其能力为模型提供关于输入序列中单词顺序的信息。Transformer 的核心是自注意力机制,它本质上是位置不变的,这意味着如果没有某种方式来考虑输入的顺序,模型将无法知道单词在序列中的位置。为了解决这个问题,位置编码被引入作为序列中每个单词的补充信息。
以下是为什么直接将位置编码加到输入嵌入(embeddings)并训练模型是有效的几个理由:
-
无损信息合并:
位置编码与词嵌入相加不会丢失信息,因为它们都参与模型的训练过程。位置编码被设计成有足够的变化,以确保即使在加到嵌入向量后也能保持唯一性。 -
保持上下文关联:
相加操作后的嵌入维持了单词的原始语义信息,并与位置信息结合,使模型能够关联单词的语义和其在序列中的位置。 -
端到端学习:
Transformer 通过端到端的学习逐渐适应位置编码。模型学习如何结合位置信息和词的含义,以便于执行翻译、问答等任务。 -
训练过程的优化:
位置编码是可微的,这意味着模型可以在训练过程中调整权重,以最优化对位置编码的利用。 -
相对位置关系:
由于正弦和余弦函数具有周期性特征,位置编码能够使模型捕捉到序列中元素的相对位置关系,这在处理诸如长距离依赖这样的语言特征时尤为重要。 -
不需要额外的参数学习:
位置编码是预先定义的,并不需要通过训练来学习,从而减少了模型训练的参数数量,同时也减轻了模型的过拟合风险。
总的来说,位置编码通过直接相加的方式提供了一种简单而有效的方法来将顺序信息融入模型中,而不需要改变原有的架构,也不需要增加额外的参数。这种编码策略证明了其在各种自然语言处理任务中的有效性。