目录
一、位置编码的意义
二、位置编码方法
三、代码实现
一、位置编码的意义
在标准的注意力机制中,每个查询都会关注所有的键-值对并生成一个注意力输出,模型并没有考虑到输入序列每个token的顺序关系。
以["我",“爱”,“你”]和["你",“爱”,“我”]为例,假如输入序列为:["我",“爱”,“你”],这个输入序列经过embedding层编码后的向量为:
"我" →
[1, 0]
"爱" →
[0, 1]
"你" →
[0, 0]
接下来,模型会计算每个词与其他词的相似度。例如,计算“爱”对“我”和“你”的注意力权重(缩放点积)时:
"爱" vs "我":
(0*1) + (1*0) = 0
"爱" vs "你":
(0*0) + (1*0) = 0
此时,模型可能认为“爱”和“我”“你”都无关(相似度为0),但显然不合理,因为“爱”在序列中的位置(中间)应该关联前后词。如果我们变换“我”和“你”的位置,相似度依然会是0,说明此时在模型眼中,“我爱你”与“你爱我”并没有差别。
如果我们在embedding层编码时同时加入各个token的位置信息(位置编码):
位置0("我") →
[0.1, 0.2]
位置1("爱") →
[0.3, 0.4]
位置2("你") →
[0.5, 0.6]
新的词表示(embedding层编码结果 + 位置编码结果):
"我" →
[1+0.1, 0+0.2] = [1.1, 0.2]
"爱" →
[0+0.3, 1+0.4] = [0.3, 1.4]
"你" →
[0+0.5, 0+0.6] = [0.5, 0.6]
重新计算注意力权重(以“爱”为中心):
"爱" vs "我":
(0.3*1.1) + (1.4*0.2) = 0.33 + 0.28 = 0.61
"爱" vs "你":
(0.3*0.5) + (1.4*0.6) = 0.15 + 0.84 = 0.99
此时,“爱”对“你”的注意力权重更高,符合“爱”在中间位置时更关注后面的“你”。如果交换“我”和“你”的位置,因为位置关系变化了,计算得到的注意力权重也会发生变化,也就说明此时模型能够区分“我爱你”与“你爱我”的语义差别了。
从上面的例子可以看出,位置编码可以帮助注意力模型理解输入序列中各个token的顺序关系。
二、位置编码方法
假设embedding层输出的词嵌入矩阵为X,token个数为n,向量长度为d,即;位置编码输出矩阵为P,
。P中第i行,第2j列和第2j+1列的元素为:
最终将使用X+P作为整个编码流程的输出结果。
至于为什么要用上面的公式来进行位置编码,感觉自己还是一知半解,后面学会了再记录下来。
三、代码实现
import torch
from torch import nn
"""位置编码"""
class PositionalEncoding(nn.Module):
# 参数max_len >= 每个batch的token个数n;参数num_hiddens对应于编码向量长度d
def __init__(self, num_hiddens, dropout, max_len=1000):
super(PositionalEncoding, self).__init__()
self.dropout = nn.Dropout(dropout)
# 位置编码矩阵P
self.P = torch.zeros((1, max_len, num_hiddens))
# 分子
molecule = torch.arange(max_len, dtype=torch.float32).reshape(-1, 1)
# 分母
denominator = torch.pow(10000, torch.arange(0, num_hiddens, 2, dtype=torch.float32) / num_hiddens)
X = molecule / denominator
# 从索引0开始,每隔两个元素取一个值(即取所有偶数索引的元素)
self.P[:, :, 0::2] = torch.sin(X)
# 从索引1开始,每隔两个元素取一个值(即取所有奇数索引的元素)
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)
if __name__ == "__main__":
encoding_dim, num_steps = 32, 60
pos_encoding = PositionalEncoding(encoding_dim, 0)
pos_encoding.eval()
# 模拟embedding层输出的词嵌入矩阵,(batch_size, 查询个数, 向量长度) -> (1, 60, 32)
X = torch.zeros((1, num_steps, encoding_dim))
# embedding+位置编码的结果,(batch_size, 查询个数, 向量长度) -> (1, 60, 32)
X = pos_encoding(X)
# 仅位置编码的结果,(batch_size, 查询个数, 向量长度) -> (1, 60, 32)
P = pos_encoding.P[:, :X.shape[1], :]
参考链接:
《动手学深度学习》 — 动手学深度学习 2.0.0 documentationhttps://zh-v2.d2l.ai/