本人往期文章可查阅: 深度学习总结
在之前的教案中,我们了解到了单词的顺序及其在句子中的位置是非常重要的。 如果重新排列单词,整个句子的意思可能会发生变化。
在实施 NLP 解决方案时,循环神经网络(RNN)具有处理序列的内置机制。Transformer 则是引入位置编码机制保存文本中字符的位置信息。
本文的学习目标:
- 了解什么是位置编码
- Transformer 中的位置编码机制
- 使用 NumPy 实现位置编码矩阵
- 理解和可视化位置编码矩阵
一、什么是位置编码
1.位置编码定义
位置编码记录了文本中字符的位置信息,这里位置信息的记录不使用单个数字(例如索引值)来记录位置信息的原因有很多。对于长序列,索引的大小可能会变大,不利于存储。如果将索引值规范化为介于0~1之间,则可能会为可变长度序列带来问题,因为他们的标准化方式不同
Transformers 使用只能位置编码方案,其中每个位置/索引都映射到一个向量。因此,位置编码层的输出是一个矩阵,其中矩阵的每一行代表序列中的一个编码对象与其位置信息相加。下图显示了仅对位置信息进行编码的矩阵示例。
2.三角函数
这是对正弦函数的快速回顾;你可以等效地使用余弦函数。函数的取值范围是[-1,+1]。该波形的频率是一秒内完成的周期数。波长是波形重复自身的距离。不同波形的波长和频率如下所示:
3.位置编码公式
让我们直接进入这个主题,假设你有一个长度为L的输入序列,要计算第 k 哥元素的位置编码。位置编码由不同频率的正弦和余弦函数给出:
(1)
(2)
参数详解:
- k:对象(即句子中的字符)在输入序列中的位置
- d:输出嵌入空间的维度
- P(k,j):位置函数,用于映射输入序列中 k 处的元素到位置矩阵的 (k,j) 处
- n:用户定义的标量,可任意取值(Attention Is All You Need 作者设置的值是10000)
- i:无实际含义,用于映射到列索引(即
or
,
)
4.位置编码示例
为了理解上面的表达式,让我们以短语 "I am a robot" 为例,其中 n=100,d=4 。下表显示了该短语的位置编码矩阵。事实上,对于任何 n=100 和 d=4 的四字母短语,位置编码矩阵都是相同的。
红色部分的计算结果需要先带入上面的公式1、公式2
二、可视化理解位置编码
1.Python实现位置编码
import math,os,torch
import torch
import torch.nn as nn
class PositionalEncoding(nn.Module):
def __init__(self,embed_dim,max_len=500):
super(PositionalEncoding,self).__init__()
# 创建一个大小为 [max_len,embed_dim] 零张量
pe=torch.zeros(max_len,embed_dim)
# 创建一个形状为 [max_len,1] 的位置索引张量
position=torch.arange(0,max_len,dtype=torch.float).unsqueeze(1)
div_term=torch.exp(torch.arange(0,embed_dim,2).float()*(-math.log(100.0)/embed_dim))
pe[:,0::2]=torch.sin(position * div_term) # 计算 PE(pos,2i)
pe[:,1::2]=torch.cos(position * div_term) # 计算 PE(pos,2i+1)
pe=pe.unsqueeze(0).transpose(0,1)
# 将位置编码张量注册为模型的缓冲区,参数不参与梯度下降,保存model的时候会将其保存下来
self.register_bufferu('pe',pe)
def forward(self,x):
# x 的形状为(seq_len,batch_size,embed_dim)
# 将位置编码添加到输入张量中,注意位置编码的形状
x=x+self.pe[:x.size(0),:]
return x
pe[:,0::2]=torch.sin(position * div_term)
pe[:,1::2]=torch.cos(position * div_term)
代码解释如下:
- pe[:,0::2]:选择所有位置的偶数索引(例如 0,2,4,……)
- torch.sin(position * div_term):计算正弦函数并赋值给 pe 的偶数索引位置
- pe[:,1::2]:选择所有位置的奇数索引(例如 1,3,5,……)
- torch.cos(position * div_term):计算余弦函数并赋值给 pe 的奇数索引位置
2.单个字符可视化
要理解位置编码,让我们从查看 n=10000 和 d=512 的不同位置的正弦波开始。
import matplotlib.pyplot as plt
def plotSinusoid(k,d=512,n=10000):
x=np.arange(0,100,1)
denominator=np.power(n,2*x/d)
y=np.sin(k/denominator)
plt.plot(x,y)
plt.title('k='+str(k))
fig=plt.figure(figsize=(15,4))
for i in range(4):
plt.subplot(141+i)
plotSinusoid(i*4)
输出:
可以看到,每个位置对应于不同的正弦曲线,它将每个位置编码为向量。如果仔细观察位置编码函数,你会发现固定 时对应的波长:
因此,正弦曲线的波长形成几何级数。位置编码方案具有许多优点。
- 正弦和余弦函数的值在 [-1,1] 内,这使位置编码矩阵的值保持在归一化范围内。
- 由于每个位置的正弦曲线都不同,因此你可以采用独特的方式对每个位置进行编码。
- 有一种方法可以测量或量化不同位置之间的相似性,从而使你能够对单词的相对位置进行编码。
3.整句话的位置编码可视化
让我们可视化更大值的位置矩阵。使用 matplotlib 库中的 mashow() 方法。如原始论文中所做的那样设置 ,将得到以下结果:
def getPositionalEncoding(seq_len, d, n=10000):
# 创建一个形状为 [seq_len, d] 的零张量
pe = torch.zeros(seq_len, d)
# 创建一个形状为 [seq_len, 1] 的位置索引张量
position = torch.arange(0, seq_len, dtype=torch.float).unsqueeze(1)
# 计算分母项
div_term = torch.exp(torch.arange(0, d, 2).float() * (-math.log(n) / d))
# 计算位置编码
pe[:, 0::2] = torch.sin(position * div_term) # 计算 PE(pos, 2i)
pe[:, 1::2] = torch.cos(position * div_term) # 计算 PE(pos, 2i+1)
# 返回位置编码
return pe
p=getPositionalEncoding(seq_len=100,d=512,n=10000)
cax=plt.matshow(p)
plt.gcf().colorbar(cax)
输出:
这里使用颜色来表示位置编码中的数值
三、位置编码层的最终输出
Transformer 中的位置编码层把位置向量与单词编码相加,并为后续层输出该矩阵。整个过程如下图所示。