目录
前言
在之前的小节中我们学习了词嵌入层(词向量编码)以及加入了位置编码的输入层的概念和代码实现的学习。在本小节中我们将学习transformer中最重要的部分-注意力机制
掩码张量
我们先学习掩码张量。现提出两个问题:什么是掩码张量?生成掩码张量的过程?
什么是掩码张量
张量尺寸不定,里面只有(0,1)元素,代表位置被遮掩或者不遮掩,它的作用就是让另外一张张量中的一些数值被遮掩,被替换,表现形式是一个张量。
掩码张量的作用
在transformer中掩码张量的主要作用在应用attention时,有一些生成的attention张量中的值计算有可能已知了未来信息而得到的,未来信息被看到是因为训练时会把整个输出结果都一次性的Embedding,但是理论上的解码器的输出却并不是一次就能产生的最终结果的,而是一次次通过上一次结果综合得出的,因此,未来信息可能被提前利用,所以进行遮掩。
生成掩码张量实现
#生成掩码张量的代码分析
def subsequeent_mask(size):
#生成向后遮掩的掩码张量,参数size是掩码张量最后两个维度的大小,形成一个方阵
#在函数中,首先定义掩码张量的形状
attn_shape = (1,size,size)
#使用np.ones方法像这个方阵中添加1元素,形成上三角阵
#节约空间将数据类型变为无符号8位整型数字unit8
subsequeent_mask = np.triu(np.ones(attn_shape),k=1 ).astype('uint8')
#转换成tensor,内部做一个 1 - 操作实现反转
return torch.from_numpy(1 - subsequeent_mask)
size = 5
sm = subsequeent_mask(size)
print('sm:',sm)
#掩码张量的可视化
plt.figure(figsize=(5,5))
plt.imshow(subsequeent_mask(20)[0])
黄色是1的部分,这里代表被遮掩,紫色代表没有被遮掩,横坐标代表目标词汇的位置,纵坐标代表可查的位置。从上往下看,在0的位置看过去都是黄色,都被遮住了,1的位置望过去还是黄色,说明第一次词还没有产生,从第二位置看过去,就能看到位置1的词,其他位置看不到,以此类推
注意力机制
学习目标
掌握注意力计算规则和注意力机制
掌握注意力计算规则的实现过程(最具有辨识度的部分)
注意力计算规则
它需要3个指定的输入Q(query),K(key),V(value),通过公式计算得出注意力的计算结果
query在key和Value的作用下表示:
大家想要了解具体注意力计算规则可以去了解自然语言处理-BERT处理框架-transformer这篇文章,里面有具体的Q,K,V注意力计算规则介绍。
注意力和自注意力
这两者的区别在于Q,K,V矩阵。注意力,刚开始时Q矩阵输入原词向量,而K,V矩阵输入人为添加的已经总结好的特征向量且默认K=V。自注意力,开始时输入Q=K=V,K,V没有人为干扰完全自己去迭代。
注意力机制
之前我们都是纸上谈兵,要使注意力计算规则能够应用在深度学习的网络,成为载体,包括全连接层以及相关张量的处理。
注意力机制在网络中实现的图形表示:
注意力机制计算规则的代码实现
#注意力计算规则的代码分析
def attention(query,key,value,mask=None,dropout=None):
#注意力机制实现:输入分别是query,key,value,mask:掩码张量
#在函数中,首先取query的最后一维的大小,词嵌入的维度d_kd_k,来进行缩放
d_k = query.size(-1)
#按照公式,将query与key的转置相乘,这里面key是将最后两个维度进行转置,再除以缩放系数
#得到得分张量scores
scores = torch.matmul(query,key.transpose(-2,-1)) / math.sqrt(d_k)
#判断是否使用掩码张量
if mask is not None:
#使用tensor的masked_fill方法,将掩码张量和scores张量的每一个位置一一比较,如果等于0就一一替换
#则对应的socers张量用-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
query = key = value = pe_result #自注意力机制
print(query.shape)
mask = Variable(torch.zeros(2,4,4))
attn,p_attn = attention(query,key,value,mask=mask)
print('attn:',attn)
print(attn.shape)
print('p_attn:',p_attn)
print(p_attn.shape)
多头注意力机制
学习目标
了解多头注意力机制的作用,掌握多头注意力机制的实现过程
多头注意力机制结构图:
什么是多头注意力机制
在图中,我们可以看到,有一组Linear层进行线性变换,变换前后的维度不变,就当是一个方阵的张量,每个张量的值不同,那么变化后的结果也不同,特征就丰富起来了。变换后进入注意力计算机制,一组有多少个linear层并行,就代表有几个头,将计算结果最后一维()分割,然后组合成scores。
多头注意力计算机制的作用
这种结构的设计能够让每个注意力机制去优化每个词的不同特征部分,从而均衡同一种注意力机制可能产生的偏差,让词义拥有更多元的表达,实验表明可以提升模型的效果。
多头注意力机制的代码实现
#多头注意力机制的实现
#定义一个克隆函数,因为在多头注意力机制的实现中,用到多个结构相同的线性层
#我们将使用clone函数将他们一同初始化在一个网络层列表对象中,之后的结构中也会使用到该函数
def clones(module,N):
#用于生成相同的网络层的克隆函数,它的参数module表示要克隆的目标网络层,N代表需要克隆的数量
#在函数中,我们通过for循环对module进行N次的深度拷贝,使其每个module成为独立的层
#然后将其放在nn.ModuleList类型的列表中存放
return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])
#使用一个类来实现多头注意力机制的处理
class MutiHeadedAttention(nn.Module):
def __init__(self, head,embedding_dim,dropout=0.1):
#在类的初始化时,会传入3个参数,head代表头数,embedding_dim代表词嵌入的维度
#dropout代表进行dropout操作时置0比率
super(MutiHeadedAttention,self).__init__()
#在函数中,首先使用了一个测试中常用的assert语句,判断h是否能被d_model整除
#这是因为我们之后要每个头分配等量的词特征,也就是emdedding_dim/head个
assert embedding_dim % head == 0
#得到每个头获得的分割词向量的维度d_k
self.d_k = embedding_dim // head
#传入头数
self.head = head
self.embedding_dim = embedding_dim
#然后获得线性层的对象,通过nn.linear实例化,它的内部变换矩阵是embedding_dim x embedding_dim
#为什么是4个呢,这是因为在多头注意力中Q,K,V各需要一个,拼接矩阵也需要一个
self.linears = clones(nn.Linear(embedding_dim,embedding_dim),4)
#self.attn为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拓展维度,代表多头中的第n个头
mask = mask.unsqueeze(1)
#接着,我们获得一个batch_size的变量,他是query尺寸的第一个数字,代表有多少条样本
batch_size = query.size(0)
#之后就进入多头处理环节
#首先利用zip将QKV与三个线性层住到一起,然后使用for循环,将输入的QKV分别传到线性层中
#做完线性变换后,开始为每个头分割输入,这里使用view方法对线性变换的结果进行维度重塑
#这样就意味着每个头可以获得一部分词特征组成的句子,其中的-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))]
#print(query.shape)
#print(key.shape)
#print(value.shape)
#得到每个头的输入后,接下来就是将他们传入到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)
#实例化参数
head = 8
embedding_dim = 512
dropout = 0.2
#若干输入参数的初始化
query = key = value = pe_result
mask = Variable(torch.zeros(2,4,4))
mha = MutiHeadedAttention(head,embedding_dim,dropout)
mha_result = mha(query,key,value,mask)
print(mha_result)
print(mha_result.shape)