【Transformer】Positional Encoding

在处理词元序列时,循环神经网络是逐个的重复地处理词元的通过中间状态保存顺序信息, 但是Transformer中使用自注意力并行计算而无法保存序列的顺序信息。

因此在Transformer中为了使用序列的顺序信息,通过在输入表示中添加 位置编码(positional encoding)来在输入中加入位置信息。

位置编码可以通过学习得到也可以直接固定得到。

  • Transformer中使用基于正弦函数和余弦函数的固定位置编码。
  • Bert通过学习的方式获得位置编码

假设输入表示\(X∈R^{n×d}\) 包含一个序列中n个词元的d维嵌入表示。

位置编码使用相同形状的位置嵌入矩阵 \(P∈R^{n×d}\) 输出\(X+P\), 矩阵第i行、第2j列和2j+1列上的元素为: \[ \begin{align} p_{i,2j}&=sin(\frac{i}{10000^{2j/d}})\\ p_{i,2j+1}&=cos(\frac{i}{10000^{2j/d}}) \end{align} \] 在位置嵌入矩阵P中, 行代表词元在序列中的位置(num_step),列代表位置编码的不同维度(d_model/encoding_dim)。

image-20240802184231821
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class PositionalEncoding(nn.Module):
def __init__(self, num_hiddens, dropout, max_len=1000):
super(PositionalEncoding, self).__init__()
self.dropout = nn.Dropout(dropout)
self.P = torch.zeros((1, max_len, num_hiddens))
X = torch.arange(max_len, dtype=torch.float32).reshape(-1, 1) # [max_len, 1]
base = torch.pow(10000, torch.arange(0, num_hiddens, 2, dtype=torch.float32) / num_hiddens) # [num_hiden/2]
X = X / base # [max_len, num_hiden/2] 广播机制

self.P[:, :, 0::2] = torch.sin(X) # 切片获得偶数列
self.P[:, :, 1::2] = torch.cos(X)

def forward(self, X):
X = X + self.P[:, :X.shape[1], :].to(X.device)
return self.dropout(X)

encoding_dim, num_steps = 32, 60
pos_encoding = PositionalEncoding(encoding_dim, 0)
pos_encoding.eval()
X = pos_encoding(torch.zeros((1, num_steps, encoding_dim))) # [batch_size, num_steps, encoding_dim]

绘制P矩阵前60行的6-9列,可以看出P矩阵的第6列和第7列的频率高于第8列和第9列。

第6列和第7列之间的偏移量(第8列和第9列相同)是由于正弦函数和余弦函数的交替。

image-20240802184327328

位置编码中的cos和sin公式对应着embedding维度的一组奇数和偶数的序号的维度

例如0、1一组,2、3一组,分别用上面的 sin 和 cos 函数做处理,从而产生不同的周期性变化

在embedding维度上随着维度序号增大,周期变化会越来越慢,最终产生一种包含位置信息的纹理

通过不同周期的sin和cos函数的取值组合,使得模型学到位置之间的依赖关系和自然语言的时序特性

绘制Positional Encoding P矩阵的热力图如下

纵向观察,可见随着encoding维度序号的增大,位置嵌入函数的周期变化越来越平缓

image-20240805133605164