Transformer多头自注意力及掩码机制详解
在本文中我们重点介绍Transformer中的掩码机制及多头自注意力模块的原理以及代码实现。掩码机制是Transformer中非常重要的一个部分,在模型结构图中的三个地方有用到掩码机制,如下图所示。Transformer中的掩码分为两种,分别是填充mask和因果mask。在下图中,1和2所在为位置为填充mask,3所在的位置为因果mask。
系列文章目录
前言
在本文中我们重点介绍Transformer中的掩码机制及多头自注意力模块的原理以及代码实现。
一、self-attention
1. 注意力机制
人在处理信息时,会将注意力放到需要关注的信息上,深度学习中的注意力机制便是源自于此,其允许模型在处理信息时能够聚焦于输入数据的特定部分,从而提高模型的性能和泛化能力。
接下来我们举一个现实生活中的例子。例如我们现在很饿,然后面前有一碗热气腾腾的面条,一本书还有一个水杯,需要我们去选择面前的一个东西,我们很有可能去选择那碗热气腾腾的面条。在这个例子中,我们很饿想吃东西,这是大脑发给我们的自主性提示,引导我们去选择能填报肚子的东西。而热气腾腾的面条,他的外部特征(散发着热气)客观性代表着他是含有热量的食物,这是非自主性提示。我们可以通过眼睛看到面条,书,还有杯子这些东西,这称为感官输入。我们以自主性提示作为引导,然后以面条、书还有水杯这些物体的外部特征作为辅助信息(非自主性提示),最后结合这两种提示,最后选择面条去填饱肚子。
在注意力机制的背景下,自主性提示叫做查询,非自住性查询称为键,而感官输入称为值。给定任何查询,我们通过与键去做注意力汇聚(结合自主性提示和非自主性提示),最终选择合适的感官输入(值)。
在深度学习中,注意力机制就是给定一些键值对,给定一个查询,通过将查询与键送到注意力评分函数中,最终得到对于不同值的权重,然后将这些值做加权和,最终得到关于这个查询的输出。
用数学语言描述,就是假设有一个查询
q
q
q和
m
m
m个键值对(
k
1
k_1
k1,
v
1
v_1
v1) (
k
2
k_2
k2,
v
2
v_2
v2) …(
k
m
k_m
km,
v
m
v_m
vm),注意力汇聚函数
f
f
f就被表示成值的加权和:
其中查询 q q q(向量)和键 k i k_i ki(向量)通过注意力评分函数 α ( q , k i ) \alpha(q,k_i) α(q,ki)求出的值再做softmax操作最后得到 v i v_i vi的注意力权重(标量)。
注意力评分函数分为加性注意力和缩放点积注意力。
加性注意力
α ( q , k ) = w v T tanh ( W q q + W k k ) \alpha(q,k) = w_v^T\tanh(W_qq+W_kk) α(q,k)=wvTtanh(Wqq+Wkk) 其中, W q W_q Wq、 W k W_k Wk和 W v W_v Wv为可以学习的参数,将查询和键分别通过各自的多层感知机后相加,使用 tanh \tanh tanh作为激活函数,接着输出再通过一个多层感知机得到最终输出。
缩放点击注意力
点积操作要求查询和键具有相同的长度。假设两个向量的点积的均值维0,方差维 d d d,且无论向量长度如何,点积的方差再不考虑向量长度的情况下仍然为1,再将点积除以 d \sqrt{d} d进行缩放,数学公式为:α ( q , k ) = q T k / d \alpha(q,k) = q^Tk/\sqrt{d} α(q,k)=qTk/d
2. 自注意力机制
在上文介绍的注意力机制中,查询和键是不同的。当查询、键和值来自同一组输入时,每个查询会关注所有的键值对并生成一个注意力输出,由于查询、键值对来自同一组输入,因此被称为自注意力。
自注意力机制(Self-Attention)是Transformer模型中的核心组成部分之一,它允许模型在处理序列数据时,能够捕捉序列内部的长距离依赖关系。自注意力机制的核心思想是让模型在处理序列的每个元素时,考虑该元素与序列中其他所有元素的关系。具体来说,它通过计算元素之间的相似度,来决定它们之间的权重,从而实现信息的聚合。在Transfoemer中的注意力评分函数使用的是缩放点积注意力。
接下来我们举一下例子来说明在transfomer中的自注意力机制的计算过程。
从图中可以看出,当我们输入一个句子’我爱吃梨‘时,首先将其编码为4x512维,然后其会分别通过三个全连接网络进行映射得到Q,K,V(图中省略了经过全连接层的过程),接着Q乘以K的转置得到4x4的权重图,权重图的每一行会做softmax操作最终得到4x4的注意力权重图,在这个注意力权重图中,第1行的4个值分别代表了句子中的’我’字与’我‘,’爱‘,’吃‘,’梨‘这4个字的关联程度,其与自身的关联程度最大(标注红色),其他几行同理。接着注意力权重图与V相乘得到最终结果。在输出的最终结果中,其第一行的每一个元素都是由其他位置的词向量的对应维度进行加权和得到的。(换句话说,每一行的值是由所有行按照注意力权重加权得到的,即每一行都或多或少的包含了其他行的信息。)
因此,总的来说,Transformers中的自注意力机制使得模型能够同时考虑输入序列中的所有位置,允许模型根据输入序列中的不同位置之间的关系,对每个位置进行加权处理,从而捕捉全局上下文信息。
3. 代码实现
import torch
import torch.nn as nn
import numpy as np
class self_attention(nn.Module):
def __init__(self):
super(self_attention,self).__init__()
def forward(self,q,k,v,att_mask=None):
# q:[batch_size,n_heads,len_q,d_k]
# k:[batch_size,n_heads,len_k,d_k]
# v:[batch_size,n_heads,len_v,d_k]
# attn_mask:[batch_size,n_heads,len_q,len_k]
d_k = q.size(-1)
scores = q @ k.transpose(-1,-2)/np.sqrt(d_k)
if att_mask is not None:
scores.masked_fill_(att_mask,-1e9)
attn = nn.Softmax(dim=-1)(scores)
return attn @ v
q = torch.randn((2,5,6,6))
att = self_attention()
print(att(q,q,q).shape)
在代码中,输入维度为[2,5,6,6],输出维度仍然为[2,5,6,6],因此transform中输入在经过注意力汇聚后输出维度不变。
二、掩码机制
1. 原理介绍
掩码机制是Transformer中非常重要的一个部分,在模型结构图中的三个地方有用到掩码机制,如下图所示。Transformer中的掩码分为两种,分别是填充mask和因果mask。在下图中,1和2所在为位置为填充mask,3所在的位置为因果mask。
填充mask: 我们在给transformer输入句子时通常是一次性输入好几个句子(batch),每个句子的长度不相同,为了transformer能够更好的一次性处理这些长度不同的句子,我们通常要对句子进行填充,使这些句子的长度相同(比如在句子末尾填充0)。但是在计算注意力的时候,这些填充的部分不应该参与计算,所以我们要在注意力权重进行softmax之前要把填充部分进行mask(就是把填充部分变成负无穷),使填充部分的注意力权重在经过softmax后无限接近0。
因果mask: 在解码器训练的过程中,不能让模型知道未来时间步的信息,否则就相当于告诉了模型的最终答案是什么。例如我们给解码器输入’I like eating pears’,当我们在计算’like’这个词与与其他词的注意力权重时,因为解码器是一个单词一个单词预测的,在预测’like‘这个词时是不应该知道后面的单词,所以与’like‘后面的单词不应该产生关联性。
下面两幅图说明了对注意力权重进行mask的过程。在注意力权重图中,黑色部分表示其权重值应为负无穷(softmax之前),红色部分表示权重值最大(肯定是与自身的关联性最大)。
填充mask
因果mask
2. 代码实现
填充mask
def get_pad_mask(seq_q,seq_k):
# seq_q:[batch_size,len_q]
# seq_k:[batch_size,len_k]
len_q,len_k = seq_q.size(1),seq_k.size(1)
pad_att_mask = seq_k.data.eq(0).unsqueeze(1) #[batch_size,1,len_k]
return pad_att_mask.repeat(1,len_q,1) #[batch_size,len_q,len_k]
q = torch.Tensor([[1,2,3,0,0],[3,7,6,5,0]])
k = torch.Tensor([[3,2,5,0],[1,5,6,0]])
mask = get_pad_mask(q,k)
print(mask.shape)
print(mask)
通过上述代码,输入的
q
q
q维度为[2,5],
k
k
k的维度为[2,4],即此时输入还没有进行embedding编码。然后我们根据上图可知,最后q乘k的注意力权重维度为[2,5,4]。(假设没有多头注意力机制,如果有多头注意力机制的话在加一个维度即可)所以我们通过上述代码产生了一个与注意力权重图维度相同的mask,其中为True的变量代表此位置为填充的,需要把此位置处的权重变为负无穷。
说明:按照道理来说,生成的mask最后一行也该为True,但是仔细想一想,最后一行代表查询Q中的填充词与句子中其他词的关联性,填充词最后能否被正确预测对于最终的结果都没有影响,因此mask的最后一行是否为True对于最终的训练结果来说影响不大。
因果mask
def get_causal_mask(seq):
# seq:[batch_size,tgt_len]
att_shape = [seq.size(0),seq.size(1),seq.size(1)] #[batch_size,tgt_len,tgt_len]
causal_mask = np.tril(np.ones(att_shape),k=0) # 上三角矩阵
causal_mask = torch.from_numpy(causl_mask).byte()
return causal_mask #[batch_size,tgt_len,tgt_len]
q = torch.Tensor([[1,2,3,0,0],[3,7,6,5,0]])
mask = get_causal_mask(q)
print(mask.shape)
print(mask)
在上述代码中,np.tril用于生成上三角矩阵。解码器输入句子的维度为[2,5],首先输入句子会进行自注意力计算,然后生成[2,5,5]的注意力权重图,因为我们通过上述代码生成了一个与注意力权重图相同维度的上三角矩阵,上三角矩阵为0的地方代表注意力权重图此位置的权重变成负无穷。
三、多头注意力模块
1. 原理介绍
多头注意力指的是用独立学习得到的h组不同的线性投影来变换查询、键和值,然后将这
h
h
h组变换后的查询、键和值并行地进行注意力权重计算,最后将这
h
h
h组地输出拼接到一起,然后通过另一个可以学习地线性投影进行变换,产生最终输出。
在论文中讲到,将模型分为多个头,形成多个子空间,可以让模型去关注不同方面的信息,最后再将各个方面的信息综合起来。多头的注意力有助于网络捕捉到更丰富的特征和信息,进行多次 attention 综合的结果可以能够起到增强模型的作用,类似于卷积神经网络中的多个卷积核。
在实际实现过程中,为了避免计算代价和参数代价地大幅增长,通常是在输入的
d
m
o
d
e
l
d_{model}
dmodel维进行切割,把他分成多个
n
u
m
_
h
e
a
d
num\_head
num_head个头,以此起到并行计算的作用。例如,如果输入的维度是
[
b
a
t
c
h
s
i
z
e
,
s
e
q
_
l
e
n
,
d
_
m
o
d
e
l
]
[batchsize,seq\_len,d\_model]
[batchsize,seq_len,d_model],首先其通过线性层,输出维度为
[
b
a
t
c
h
s
i
z
e
,
s
e
q
_
l
e
n
,
d
_
m
o
d
e
l
]
[batchsize,seq\_len,d\_model]
[batchsize,seq_len,d_model],如果想分成
n
u
m
_
h
e
a
d
num\_head
num_head个头,则线性层的输出维度的维度被分割成
[
b
a
t
c
h
s
i
z
e
,
s
e
q
_
l
e
n
,
n
u
m
_
h
e
a
d
,
d
_
m
o
d
e
l
/
n
u
m
_
h
e
a
d
]
[batchsize,seq\_len,num\_head,d\_model/num\_head]
[batchsize,seq_len,num_head,d_model/num_head],然后将这
n
u
m
_
h
e
a
d
num\_head
num_head个头分别进行注意力汇集,最后拼接成
[
b
a
t
c
h
s
i
z
e
,
s
e
q
_
l
e
n
,
d
_
m
o
d
e
l
]
[batchsize,seq\_len,d\_model]
[batchsize,seq_len,d_model]维。
2. 代码实现
import torch
import torch.nn as nn
import numpy as np
class MutiheadAttention(nn.Module):
def __init__(self,d_model,n_head):
super(MutiheadAttention,self).__init__()
self.d_model = d_model
self.n_head = n_head
self.w_q = nn.Linear(d_model,d_model)
self.w_k = nn.Linear(d_model,d_model)
self.w_v = nn.Linear(d_model,d_model)
self.w_concat = nn.Linear(d_model,d_model)
self.att = self_attention()
def forward(self,q,k,v,att_mask=None):
# q:[batch_size,len_q,d_model]
# k:[batch_size,len_k,d_model]
# v:[batch_size,len_k,d_model]
batch_size,len_q,len_k,len_v = q.size(0),q.size(1),k.size(1),v.size(1)
n_dim = int(self.d_model / self.n_head)
q,k,v = self.w_q(q),self.w_k(k),self.w_v(v)
q = q.view(batch_size,len_q,self.n_head,n_dim).permute(0,2,1,3)
k = k.view(batch_size,len_k,self.n_head,n_dim).permute(0,2,1,3)
v = v.view(batch_size,len_v,self.n_head,n_dim).permute(0,2,1,3)
context = self.att(q,k,v,att_mask) #[batch_size,n_head,len_q,n_dim]
context = context.permute(0,2,1,3).contiguous().view(batch_size,len_q,self.d_model)
return self.w_concat(context)
muti_head = MutiheadAttention(512,8)
q = torch.randn((5,5,512))
k = torch.randn((5,3,512))
v = k
print(muti_head(q,k,v).shape)
在上述代码中,对于输入的
q
,
k
,
v
q,k,v
q,k,v ([batch_size,seq_len,d_model]维),首先将其分别经过三个全连接网络,然后增加一个维度,变换为[batch_size,n_head,seq_len,d_model/n_head]维,然后进行注意力汇聚(可以理解成输入为(batch_size乘n_head)个句子,然后分别并行进行注意力权重的计算),最后在进行完注意力权重汇聚后,维度重新变换成[batch_size,seq_len,d_model]维,最后在通过一个全连接层进行映射得到最终的结果。同时从代码的输出结果可以看出,在经过多头注意力模块后,输入的维度保持不变。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)