自注意力机制和位置编码
理论
自注意力的自不是自己和自己做Attention,之前理解的有问题,是query,key,value都是自己。
y i = f ( x i , ( x 1 , x 1 ) , . . . , ( x n , x n ) ) ∈ R d y_i=f(x_i,(x_1,x_1),...,(x_n,x_n))\in\mathbb{R}^d yi=f(xi,(x1,x1),...,(xn,xn))∈Rd
CNN,RNN,Attention比较
但和CNN,RNN不一样,Attention有一个问题是它并没有包含序列的位置信息,比如机器翻译任务,把句子打乱了,翻译出来的每个词的内容也一样。注意不是对整个结果没影响,I love you->我爱你,love I you->爱我你,顺序是有影响的,但对于把love翻译成爱是没影响的。
所以为了加入词的位置信息,所以提出了位置编码。
位置编码直接将信息注入到输入里
-
假设长度为n的序列是 X ∈ R n × d X\in\mathbb{R}^{n\times d} X∈Rn×d,那么使用位置编码矩阵 P ∈ R n × d P\in\mathbb{R}^{n\times d} P∈Rn×d来输出 X + P X+P X+P作为自编码的输入
-
P的元素计算如下
p i , 2 j = s i n ( i 1000 0 2 j / d ) p_{i,2j}=sin(\frac{i}{10000^{2j/d}}) pi,2j=sin(100002j/di), p i , 2 j + 1 = c o s ( i 1000 0 2 j / d ) p_{i,2j+1}=cos(\frac{i}{10000^{2j/d}}) pi,2j+1=cos(100002j/di)
个人理解:横轴是每个词,不同的线是每个词的列也就是特征,纵轴是这些特征所加的值的。
竖着切一刀,能看到每个词所有特征的位置编码P,沿着每条线是不同的词的第j个特征的P,能发现越往后的特征周期越长。
这就像计算机的二进制编码,越小的位频率越高。
热图表示
纵轴是每个词,横轴是它的各个特征,颜色是位置编码值。现在终于理解transformer那个黄绿色的位置编码图是啥意思了。
相对位置信息
最后一个问题,为什么我们要用sin,cos函数表示这个位置信息呢,用别的呢?
-
位置于 i + δ i+\delta i+δ处的位置编码可以线性投影位置 i i i处的位置编码来表示
简单来说,举个例子,第三个词和第五个词,可以通过一个矩阵找到他们的映射关系,且这个矩阵不受这两个词绝对位置的影响,即把他两移到第一个词和第三个词,依然可以通过这个矩阵映射出来。
[ c o s ( δ w j ) s i n ( δ w j ) − s i n ( δ w j ) c o s ( δ w j ) ] [ p i + δ , 2 j p i + δ , 2 j + 1 ] = [ p i + δ , 2 j p i + δ , 2 j + 1 ] \begin{bmatrix} cos(\delta w_j) & sin(\delta w_j)\\ -sin(\delta w_j) & cos(\delta w_j) \end{bmatrix}\begin{bmatrix} p_{i+\delta,2j} \\ p_{i+\delta,2j+1} \end{bmatrix}=\begin{bmatrix} p_{i+\delta,2j} \\ p_{i+\delta,2j+1} \end{bmatrix} [cos(δwj)−sin(δwj)sin(δwj)cos(δwj)][pi+δ,2jpi+δ,2j+1]=[pi+δ,2jpi+δ,2j+1]
代码
import math
import torch
from torch import nn
from d2l import torch as d2l
num_hiddens, num_heads = 100, 5
attention = d2l.MultiHeadAttention(num_hiddens, num_hiddens, num_hiddens,
num_hiddens, num_heads, 0.5)
attention.eval()
MultiHeadAttention(
(attention): DotProductAttention(
(dropout): Dropout(p=0.5, inplace=False)
)
(W_q): Linear(in_features=100, out_features=100, bias=False)
(W_k): Linear(in_features=100, out_features=100, bias=False)
(W_v): Linear(in_features=100, out_features=100, bias=False)
(W_o): Linear(in_features=100, out_features=100, bias=False)
)
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
torch.Size([2, 4, 100])
#@save
class PositionalEncoding(nn.Module):
"""位置编码"""
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))
X = torch.arange(max_len, dtype=torch.float32).reshape(
-1, 1) / torch.pow(10000, torch.arange(
0, num_hiddens, 2, dtype=torch.float32) / num_hiddens)
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)
self.P = torch.zeros((1, max_len, num_hiddens))
max_len:序列长度
num_hiddens:特征维度
套公式
dropout是为了防止对位置编码太敏感
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)))
P = pos_encoding.P[:, :X.shape[1], :]
d2l.plot(torch.arange(num_steps), P[0, :, 6:10].T, xlabel='Row (position)',
figsize=(6, 2.5), legend=["Col %d" % d for d in torch.arange(6, 10)])
for i in range(8):
print(f'{i}的二进制是:{i:>03b}')
0的二进制是:000
1的二进制是:001
2的二进制是:010
3的二进制是:011
4的二进制是:100
5的二进制是:101
6的二进制是:110
7的二进制是:111
P = P[0, :, :].unsqueeze(0).unsqueeze(0)
d2l.show_heatmaps(P, xlabel='Column (encoding dimension)',
ylabel='Row (position)', figsize=(3.5, 4), cmap='Blues')
下一节就是激动人心的Transformer了。