多头注意力

自注意力的意思是,query,key,value都是同一个X。
说明一个词语会咨询所有其他的词元,看其相似度来计算value值。
所以最后演变成下面的结构。

自注意力图
(todo)

与之相比的卷积神经网络图
(todo)

和循环神经网络图
(todo)

可以看出自注意力图能够关联更多信息。

试试看用多头注意力,把q,k,v都设置为同一个输入x。

1
2
3
4
5
6
7
8
num_hiddens, num_heads = 100, 5
attention = d2l.MultiHeadAttention(num_hiddens, num_hiddens, num_hiddens,
num_hiddens, num_heads, 0.5)
attention.eval()

batch_size, num_queries, valid_lens = 2, 4, torch.tensor([3, 2])
X = torch.ones((batch_size, num_queries, num_hiddens))
attention(X, X, X, valid_lens).shape

位置编码

当走上了注意力机制来处理词序列的一刻,就已经失去了时间步的概念,也就根本没有把词的位置信息给放进去。
举个例子:

I do not love you, but I do like your hair color.
I do love you, but I do not like your hair color.
两句话用的一模一样的词,但是意思完全不同,搞错这个意思就完蛋了 :),可能与爱情失之交臂。

所以需要引入位置编码。
举个例子:
I do love you, but I do not like your hair color, can you change ?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

最简单的方式就是把这些位置编码信息带入,比如把1转化为二进制0001,变成一个向量(0,0,0,1)
添加到词元的embedding 向量里去,比如 i = (23, 1, 0, 28) + 位置信息 (0,0,0,1) = (23, 1, 0 ,29)。

但是这样做有严重的问题,遇到特别长的句子,数值会变得很大,遇到训练期没见过的超级长句,泛化能力有限。
所以需要找到一个满足一下条件的位置编码方案:

1 给每个时间步有唯一的编码
2 在一个长句和一个短句,两个时间步的距离应该相等
3 句子长度不影响该编码上界值

继续思考下去:

0: 0 0 0 0
1: 0 0 0 1
2: 0 0 1 0
3: 0 0 1 1
4: 0 1 0 0
5: 0 1 0 1
6: 0 1 1 0
7: 0 1 1 1
8: 1 0 0 0
9: 1 0 0 1
10: 1 0 1 0
11: 1 0 1 1
12: 1 1 0 0
13: 1 1 0 1
14: 1 1 1 0
15: 1 1 1 1

发现了规律没有,对于最后一位,1总是每个1位出现一次,对于倒数第二位,1总是隔两位出现两次
对于倒数第三位,1总是每隔4位出现4次,对于倒数第四位,1总是每隔8位出现一次。
也就是遵循这样的规律,最右来回变化频率快,最左来回变化频率慢, 位置编码沿维度频率在变小或者变大。

所以这个跟cos sin函数很像,引入cos,sin函数遵循了位置的规律。
对每个维度引入位置编码:

t为位置
p(t) = sin( wk * t) 当t等于偶数 t=2k
p(t) = cos( wk * t) 当t等于奇数 t=2k+1
wk = 1/pow(1000, 2k/d)

比如
对于一个embeding维度为4句子:I do love you, but I do not like your hair color.
pt = ( sin(t), cos(t), sin(t/100), cos(t/100) )

代入上面函数
i = ( sin(1), cos(1), sin(1/100), cos(1/100) )

1 这样满足的是最右来回频率慢,最左来回频率慢, 满足了位置的频率变化规律
2 在原论文里面,数学家证明了,位置信息不依赖句子总长度。
3 两个位置编码相减也是满足条件的。

这种位置编码方案就是我们想要的。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class PositionalEncoding(nn.Module):
"Implement the PE function."

def __init__(self, d_model, dropout, max_len=5000):
super(PositionalEncoding, self).__init__()
self.dropout = nn.Dropout(p=dropout)

# 初始化Shape为(max_len, d_model)的PE (positional encoding)
pe = torch.zeros(max_len, d_model)
# 初始化一个tensor [[0, 1, 2, 3, ...]]
position = torch.arange(0, max_len).unsqueeze(1)
# 这里就是sin和cos括号中的内容,通过e和ln进行了变换
div_term = torch.exp(
torch.arange(0, d_model, 2) * -(math.log(10000.0) / d_model)
)
# 计算PE(pos, 2i)
pe[:, 0::2] = torch.sin(position * div_term)
# 计算PE(pos, 2i+1)
pe[:, 1::2] = torch.cos(position * div_term)
# 为了方便计算,在最外面在unsqueeze出一个batch
pe = pe.unsqueeze(0)
# 如果一个参数不参与梯度下降,但又希望保存model的时候将其保存下来
# 这个时候就可以用register_buffer
self.register_buffer("pe", pe)

def forward(self, x):
"""
x 为embedding后的inputs,例如(1,7, 128),batch size为1,7个单词,单词维度为128
"""
# 将x和positional encoding相加。
x = x + self.pe[:, : x.size(1)].requires_grad_(False)
return self.dropout(x)

参考:
https://kazemnejad.com/blog/transformer_architecture_positional_encoding/
https://zhuanlan.zhihu.com/p/106644634
https://zh.d2l.ai/chapter_attention-mechanisms/self-attention-and-positional-encoding.html
https://blog.csdn.net/zhaohongfei_358/article/details/126019181