学习目标 🍀 了解编码器中各个组成部分的作用. 🍀 掌握编码器中各个组成部分的实现过程.
编码器部分: * 由N个编码器层堆叠而成 * 每个编码器层由两个子层连接结构组成 * 第一个子层连接结构包括一个多头自注意力子层和规范化层以及一个残差连接 * 第二个子层连接结构包括一个前馈全连接子层和规范化层以及一个残差连接
def subsequent_mask(size):
"""生成向后遮掩的掩码张量, 参数size是掩码张量最后两个维度的大小, 它的最后两维形成一个方阵"""
# 在函数中, 首先定义掩码张量的形状
attn_shape = (1, size, size)
# 然后使用np.ones方法向这个形状中添加1元素,形成上三角阵, 最后为了节约空间,
# 再使其中的数据类型变为无符号8位整形unit8
subsequent_mask = np.triu(np.ones(attn_shape), k=1).astype('uint8')
# 最后将numpy类型转化为torch中的tensor, 内部做一个1 - 的操作,
# 在这个其实是做了一个三角阵的反转, subsequent_mask中的每个元素都会被1减,
# 如果是0, subsequent_mask中的该位置由0变成1
# 如果是1, subsequent_mask中的该位置由1变成0
return torch.from_numpy(1 - subsequent_mask)
>>> np.triu([[1,2,3],[4,5,6],[7,8,9],[10,11,12]], k=-1)
array([[ 1, 2, 3],
[ 4, 5, 6],
[ 0, 8, 9],
[ 0, 0, 12]])
>>> np.triu([[1,2,3],[4,5,6],[7,8,9],[10,11,12]], k=0)
array([[ 1, 2, 3],
[ 0, 5, 6],
[ 0, 0, 9],
[ 0, 0, 0]])
>>> np.triu([[1,2,3],[4,5,6],[7,8,9],[10,11,12]], k=1)
array([[ 0, 2, 3],
[ 0, 0, 6],
[ 0, 0, 0],
[ 0, 0, 0]])
# 生成的掩码张量的最后两维的大小
size = 5
sm = subsequent_mask(size)
print("sm:", sm)
# 最后两维形成一个下三角阵
sm: (0 ,.,.) =
1 0 0 0 0
1 1 0 0 0
1 1 1 0 0
1 1 1 1 0
1 1 1 1 1
[torch.ByteTensor of size 1x5x5]
plt.figure(figsize=(5,5))
plt.imshow(subsequent_mask(20)[0])
我们这里使用的注意力的计算规则:
import torch.nn.functional as F
def attention(query, key, value, mask=None, dropout=None):
"""注意力机制的实现, 输入分别是query, key, value, mask: 掩码张量,
dropout是nn.Dropout层的实例化对象, 默认为None"""
# 在函数中, 首先取query的最后一维的大小, 一般情况下就等同于我们的词嵌入维度, 命名为d_k
d_k = query.size(-1)
# 按照注意力公式, 将query与key的转置相乘, 这里面key是将最后两个维度进行转置, 再除以缩放系数根号下d_k, 这种计算方法也称为缩放点积注意力计算.
# 得到注意力得分张量scores
scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)
# 接着判断是否使用掩码张量
if mask is not None:
# 使用tensor的masked_fill方法, 将掩码张量和scores张量每个位置一一比较, 如果掩码张量处为0
# 则对应的scores张量用-1e9这个值来替换, 如下演示
scores = scores.masked_fill(mask == 0, -1e9)
# 对scores的最后一维进行softmax操作, 使用F.softmax方法, 第一个参数是softmax对象, 第二个是目标维度.
# 这样获得最终的注意力张量
p_attn = F.softmax(scores, dim = -1)
# 之后判断是否使用dropout进行随机置0
if dropout is not None:
# 将p_attn传入dropout对象中进行'丢弃'处理
p_attn = dropout(p_attn)
# 最后, 根据公式将p_attn与value张量相乘获得最终的query注意力表示, 同时返回注意力张量
return torch.matmul(p_attn, value), p_attn
>>> input = Variable(torch.randn(5, 5))
>>> input
Variable containing:
2.0344 -0.5450 0.3365 -0.1888 -2.1803
1.5221 -0.3823 0.8414 0.7836 -0.8481
-0.0345 -0.8643 0.6476 -0.2713 1.5645
0.8788 -2.2142 0.4022 0.1997 0.1474
2.9109 0.6006 -0.6745 -1.7262 0.6977
[torch.FloatTensor of size 5x5]
>>> mask = Variable(torch.zeros(5, 5))
>>> mask
Variable containing:
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
[torch.FloatTensor of size 5x5]
>>> input.masked_fill(mask == 0, -1e9)
Variable containing:
-1.0000e+09 -1.0000e+09 -1.0000e+09 -1.0000e+09 -1.0000e+09
-1.0000e+09 -1.0000e+09 -1.0000e+09 -1.0000e+09 -1.0000e+09
-1.0000e+09 -1.0000e+09 -1.0000e+09 -1.0000e+09 -1.0000e+09
-1.0000e+09 -1.0000e+09 -1.0000e+09 -1.0000e+09 -1.0000e+09
-1.0000e+09 -1.0000e+09 -1.0000e+09 -1.0000e+09 -1.0000e+09
[torch.FloatTensor of size 5x5]
# 我们令输入的query, key, value都相同, 位置编码的输出
query = key = value = pe_result
Variable containing:
( 0 ,.,.) =
46.5196 16.2057 -41.5581 ... -16.0242 -17.8929 -43.0405
-32.6040 16.1096 -29.5228 ... 4.2721 20.6034 -1.2747
-18.6235 14.5076 -2.0105 ... 15.6462 -24.6081 -30.3391
0.0000 -66.1486 -11.5123 ... 20.1519 -4.6823 0.4916
( 1 ,.,.) =
-24.8681 7.5495 -5.0765 ... -7.5992 -26.6630 40.9517
13.1581 -3.1918 -30.9001 ... 25.1187 -26.4621 2.9542
-49.7690 -42.5019 8.0198 ... -5.4809 25.9403 -27.4931
-52.2775 10.4006 0.0000 ... -1.9985 7.0106 -0.5189
[torch.FloatTensor of size 2x4x512]
attn, p_attn = attention(query, key, value)
print("attn:", attn)
print("p_attn:", p_attn)
# 将得到两个结果
# query的注意力表示:
attn: Variable containing:
( 0 ,.,.) =
12.8269 7.7403 41.2225 ... 1.4603 27.8559 -12.2600
12.4904 0.0000 24.1575 ... 0.0000 2.5838 18.0647
-32.5959 -4.6252 -29.1050 ... 0.0000 -22.6409 -11.8341
8.9921 -33.0114 -0.7393 ... 4.7871 -5.7735 8.3374
( 1 ,.,.) =
-25.6705 -4.0860 -36.8226 ... 37.2346 -27.3576 2.5497
-16.6674 73.9788 -33.3296 ... 28.5028 -5.5488 -13.7564
0.0000 -29.9039 -3.0405 ... 0.0000 14.4408 14.8579
30.7819 0.0000 21.3908 ... -29.0746 0.0000 -5.8475
[torch.FloatTensor of size 2x4x512]
# 注意力张量:
p_attn: Variable containing:
(0 ,.,.) =
1 0 0 0
0 1 0 0
0 0 1 0
0 0 0 1
(1 ,.,.) =
1 0 0 0
0 1 0 0
0 0 1 0
0 0 0 1
[torch.FloatTensor of size 2x4x4]
query = key = value = pe_result
# 令mask为一个2x4x4的零张量
mask = Variable(torch.zeros(2, 4, 4))
attn, p_attn = attention(query, key, value, mask=mask)
print("attn:", attn)
print("p_attn:", p_attn)
# query的注意力表示:
attn: Variable containing:
( 0 ,.,.) =
0.4284 -7.4741 8.8839 ... 1.5618 0.5063 0.5770
0.4284 -7.4741 8.8839 ... 1.5618 0.5063 0.5770
0.4284 -7.4741 8.8839 ... 1.5618 0.5063 0.5770
0.4284 -7.4741 8.8839 ... 1.5618 0.5063 0.5770
( 1 ,.,.) =
-2.8890 9.9972 -12.9505 ... 9.1657 -4.6164 -0.5491
-2.8890 9.9972 -12.9505 ... 9.1657 -4.6164 -0.5491
-2.8890 9.9972 -12.9505 ... 9.1657 -4.6164 -0.5491
-2.8890 9.9972 -12.9505 ... 9.1657 -4.6164 -0.5491
[torch.FloatTensor of size 2x4x512]
# 注意力张量:
p_attn: Variable containing:
(0 ,.,.) =
0.2500 0.2500 0.2500 0.2500
0.2500 0.2500 0.2500 0.2500
0.2500 0.2500 0.2500 0.2500
0.2500 0.2500 0.2500 0.2500
(1 ,.,.) =
0.2500 0.2500 0.2500 0.2500
0.2500 0.2500 0.2500 0.2500
0.2500 0.2500 0.2500 0.2500
0.2500 0.2500 0.2500 0.2500
[torch.FloatTensor of size 2x4x4]
# 用于深度拷贝的copy工具包
import copy
# 首先需要定义克隆函数, 因为在多头注意力机制的实现中, 用到多个结构相同的线性层.
# 我们将使用clone函数将他们一同初始化在一个网络层列表对象中. 之后的结构中也会用到该函数.
def clones(module, N):
"""用于生成相同网络层的克隆函数, 它的参数module表示要克隆的目标网络层, N代表需要克隆的数量"""
# 在函数中, 我们通过for循环对module进行N次深度拷贝, 使其每个module成为独立的层,
# 然后将其放在nn.ModuleList类型的列表中存放.
return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])
# 我们使用一个类来实现多头注意力机制的处理
class MultiHeadedAttention(nn.Module):
def __init__(self, head, embedding_dim, dropout=0.1):
"""在类的初始化时, 会传入三个参数,head代表头数,embedding_dim代表词嵌入的维度,
dropout代表进行dropout操作时置0比率,默认是0.1."""
super(MultiHeadedAttention, self).__init__()
# 在函数中,首先使用了一个测试中常用的assert语句,判断h是否能被d_model整除,
# 这是因为我们之后要给每个头分配等量的词特征.也就是embedding_dim/head个.
assert embedding_dim % head == 0
# 得到每个头获得的分割词向量维度d_k
self.d_k = embedding_dim // head
# 传入头数h
self.head = head
# 然后获得线性层对象,通过nn的Linear实例化,它的内部变换矩阵是embedding_dim x embedding_dim,然后使用clones函数克隆四个,
# 为什么是四个呢,这是因为在多头注意力中,Q,K,V各需要一个,最后拼接的矩阵还需要一个,因此一共是四个.
self.linears = clones(nn.Linear(embedding_dim, embedding_dim), 4)
# self.attn为None,它代表最后得到的注意力张量,现在还没有结果所以为None.
self.attn = None
# 最后就是一个self.dropout对象,它通过nn中的Dropout实例化而来,置0比率为传进来的参数dropout.
self.dropout = nn.Dropout(p=dropout)
def forward(self, query, key, value, mask=None):
"""前向逻辑函数, 它的输入参数有四个,前三个就是注意力机制需要的Q, K, V,
最后一个是注意力机制中可能需要的mask掩码张量,默认是None. """
# 如果存在掩码张量mask
if mask is not None:
# 使用unsqueeze拓展维度
mask = mask.unsqueeze(0)
# 接着,我们获得一个batch_size的变量,他是query尺寸的第1个数字,代表有多少条样本.
batch_size = query.size(0)
# 之后就进入多头处理环节
# 首先利用zip将输入QKV与三个线性层组到一起,然后使用for循环,将输入QKV分别传到线性层中,
# 做完线性变换后,开始为每个头分割输入,这里使用view方法对线性变换的结果进行维度重塑,多加了一个维度h,代表头数,
# 这样就意味着每个头可以获得一部分词特征组成的句子,其中的-1代表自适应维度,
# 计算机会根据这种变换自动计算这里的值.然后对第二维和第三维进行转置操作,
# 为了让代表句子长度维度和词向量维度能够相邻,这样注意力机制才能找到词义与句子位置的关系,
# 从attention函数中可以看到,利用的是原始输入的倒数第一和第二维.这样我们就得到了每个头的输入.
query, key, value = \
[model(x).view(batch_size, -1, self.head, self.d_k).transpose(1, 2)
for model, x in zip(self.linears, (query, key, value))]
# 得到每个头的输入后,接下来就是将他们传入到attention中,
# 这里直接调用我们之前实现的attention函数.同时也将mask和dropout传入其中.
x, self.attn = attention(query, key, value, mask=mask, dropout=self.dropout)
# 通过多头注意力计算后,我们就得到了每个头计算结果组成的4维张量,我们需要将其转换为输入的形状以方便后续的计算,
# 因此这里开始进行第一步处理环节的逆操作,先对第二和第三维进行转置,然后使用contiguous方法,
# 这个方法的作用就是能够让转置后的张量应用view方法,否则将无法直接使用,
# 所以,下一步就是使用view重塑形状,变成和输入形状相同.
x = x.transpose(1, 2).contiguous().view(batch_size, -1, self.head * self.d_k)
# 最后使用线性层列表中的最后一个线性层对输入进行线性变换得到最终的多头注意力结构的输出.
return self.linears[-1](x)
>>> x = torch.randn(4, 4)
>>> x.size()
torch.Size([4, 4])
>>> y = x.view(16)
>>> y.size()
torch.Size([16])
>>> z = x.view(-1, 8) # the size -1 is inferred from other dimensions
>>> z.size()
torch.Size([2, 8])
>>> a = torch.randn(1, 2, 3, 4)
>>> a.size()
torch.Size([1, 2, 3, 4])
>>> b = a.transpose(1, 2) # Swaps 2nd and 3rd dimension
>>> b.size()
torch.Size([1, 3, 2, 4])
>>> c = a.view(1, 3, 2, 4) # Does not change tensor layout in memory
>>> c.size()
torch.Size([1, 3, 2, 4])
>>> torch.equal(b, c)
False
>>> x = torch.randn(2, 3)
>>> x
tensor([[ 1.0028, -0.9893, 0.5809],
[-0.1669, 0.7299, 0.4942]])
>>> torch.transpose(x, 0, 1)
tensor([[ 1.0028, -0.1669],
[-0.9893, 0.7299],
[ 0.5809, 0.4942]])
# 头数head
head = 8
# 词嵌入维度embedding_dim
embedding_dim = 512
# 置零比率dropout
dropout = 0.2
# 假设输入的Q,K,V仍然相等
query = value = key = pe_result
# 输入的掩码张量mask
mask = Variable(torch.zeros(8, 4, 4))
mha = MultiHeadedAttention(head, embedding_dim, dropout)
mha_result = mha(query, key, value, mask)
print(mha_result)
tensor([[[-0.3075, 1.5687, -2.5693, ..., -1.1098, 0.0878, -3.3609],
[ 3.8065, -2.4538, -0.3708, ..., -1.5205, -1.1488, -1.3984],
[ 2.4190, 0.5376, -2.8475, ..., 1.4218, -0.4488, -0.2984],
[ 2.9356, 0.3620, -3.8722, ..., -0.7996, 0.1468, 1.0345]],
[[ 1.1423, 0.6038, 0.0954, ..., 2.2679, -5.7749, 1.4132],
[ 2.4066, -0.2777, 2.8102, ..., 0.1137, -3.9517, -2.9246],
[ 5.8201, 1.1534, -1.9191, ..., 0.1410, -7.6110, 1.0046],
[ 3.1209, 1.0008, -0.5317, ..., 2.8619, -6.3204, -1.3435]]],
grad_fn=<AddBackward0>)
torch.Size([2, 4, 512])