Transformer
Transformer是一个完全依赖注意力(Attention)的模型,最开始主要应用在NLP的翻译任务中,主要的革命对象是RNN为代表的序列模型。不同于传统RNN模型,模型在
t
t
t时刻只能利用到
t
−
1
t-1
t−1时刻的历史信息,Transformer的注意力计算方式一次可以关注到所有的输入信息,因此具备更出色并行计算能力,这对于深度学习训练而言是十分有效的。
Transformer由Encoder和Decoder两个模块组成,Encoder负责接收一个batch的输入序列
[
b
,
n
,
c
]
[b, n, c]
[b,n,c],随后通过一个Embedding层进行特征嵌入
[
b
,
n
,
c
]
−
>
[
b
,
n
,
d
m
o
d
e
l
]
[b,n, c] -> [b, n, d_{model}]
[b,n,c]−>[b,n,dmodel],其中
d
m
o
d
e
l
=
512
d_{model} = 512
dmodel=512,Transformer的注意力计算方式会将每个样本和其他样本都执行一次注意力计算,因此,不管输入序列的顺序几何,都不会对最终的注意力计算结果造成影响。因此在Embedding层后,需要加入一个可以指示位置信息的嵌入,即Positional Encoding,原文的方法是用Sinusoidal Position Encoding:
P
E
(
p
o
s
,
2
i
)
=
s
i
n
(
p
o
s
1000
0
2
i
d
m
o
d
e
l
)
P
E
(
p
o
s
,
2
i
+
1
)
=
c
o
s
(
p
o
s
1000
0
2
i
d
m
o
d
e
l
)
PE_{(pos,2i)} = sin(\frac{pos}{10000^{\frac{2i}{d_{model}}}} ) \ \ \ PE_{(pos,2i+1)} =cos(\frac{pos}{10000^{\frac{2i}{d_{model}}}} )
PE(pos,2i)=sin(10000dmodel2ipos) PE(pos,2i+1)=cos(10000dmodel2ipos)
本质上是用一个角频率为
w
=
1
1000
0
2
i
d
m
o
d
e
l
w = \frac{1}{10000^{\frac{2i}{d_{model}}}}
w=10000dmodel2i1的正、余弦基信号来表征每个维度
i
i
i,这样做的好处是因为
p
o
s
+
k
pos + k
pos+k可以用
p
o
s
pos
pos来线性表征。
s
i
n
(
a
+
b
)
=
s
i
n
a
c
o
s
b
+
c
o
s
a
s
i
n
b
c
o
s
(
a
+
b
)
=
c
o
s
a
c
o
s
b
−
s
i
n
a
s
i
n
b
sin(a + b) = sina \ cosb + cosa \ sinb \ \ \ cos(a+b) = cosa \ cosb - sina \ sinb
sin(a+b)=sina cosb+cosa sinb cos(a+b)=cosa cosb−sina sinb
P
E
(
p
o
s
+
k
,
2
i
)
=
s
i
n
(
w
i
⋅
(
p
o
s
+
k
)
)
=
s
i
n
(
w
i
p
o
s
)
c
o
s
(
w
i
k
)
+
c
o
s
(
w
i
p
o
s
)
s
i
n
(
w
i
k
)
PE_{(pos+k,2i)}=sin(w_i\cdot(pos+k))=sin(w_ipos)cos(w_ik)+cos(w_ipos)sin(w_ik)
PE(pos+k,2i)=sin(wi⋅(pos+k))=sin(wipos)cos(wik)+cos(wipos)sin(wik)
P
E
(
p
o
s
+
k
;
2
i
+
1
)
=
c
o
s
(
w
i
⋅
(
p
o
s
+
k
)
)
=
c
o
s
(
w
i
p
o
s
)
c
o
s
(
w
i
k
)
−
s
i
n
(
w
i
p
o
s
)
s
i
n
(
w
i
k
)
PE_{(pos+k;2i+1)}=cos(w_i\cdot(pos+k))=cos(w_ipos)cos(w_ik)-sin(w_ipos)sin(w_ik)
PE(pos+k;2i+1)=cos(wi⋅(pos+k))=cos(wipos)cos(wik)−sin(wipos)sin(wik)
P
E
(
p
o
s
+
k
,
2
i
)
=
c
o
s
(
w
i
k
)
P
E
(
p
o
s
,
2
i
)
+
s
i
n
(
w
i
k
)
P
E
(
p
o
s
,
2
i
+
1
)
PE_{(pos+k,2i)} =cos(w_i k)PE_{(pos,2i)} +sin(w_i k)PE_{(pos,2i+1)}
PE(pos+k,2i)=cos(wik)PE(pos,2i)+sin(wik)PE(pos,2i+1)
P
E
(
p
o
s
+
k
,
2
i
+
1
)
=
c
o
s
(
w
i
k
)
P
E
(
p
o
s
,
2
i
+
1
)
−
s
i
n
(
w
i
k
)
P
E
(
p
o
s
,
2
i
)
PE_{(pos+k,2i+1)} =cos(w_i k)PE_{(pos,2i+1)} -sin(w_i k)PE_{(pos,2i)}
PE(pos+k,2i+1)=cos(wik)PE(pos,2i+1)−sin(wik)PE(pos,2i)
由上,对于pos+k位置的任意一个维度而言,都可以表示为pos和k的线性组合。
位置编码的向量长度需要和输入Embedding的长度保持一致,即512维,将输入嵌入和位置编码直接相加后执行自注意力,此时每个词就被赋予了一个位置信息。
由于Encoder中的注意力采用的是自注意力(Self-Attention)机制,因此输入的特征会同时作为Q、K和V计算注意力,但是Transformer中的注意力层采用的是多头注意力(Multi-Head Attention),即在计算注意力前先通过
h
h
h个MLP进行投影,这里多头的意义主要是想通过引入可学习的投影参数,使模型可以获得更多可学习参考的视角。
本文采用的注意力属于尺度缩放点乘注意力(Scaled Dot-Product Attention),通过点乘计算每个Q和每个K的相似度矩阵,随后除一个
d
k
\sqrt{d_k}
dk,并通过一个softmax归一化。得到的注意力矩阵本质上就是一些权重,表示为V中的每个样本/元素对最终结果的贡献程度。
Attention
(
Q
,
K
,
V
)
=
softmax
(
Q
K
T
d
k
)
V
\text{Attention}(Q,K,V)=\text{softmax}(\frac{QK^T}{\sqrt{d_k}})V
Attention(Q,K,V)=softmax(dkQKT)V
为什么要加一个尺度放缩?如果
d
k
d_k
dk逐渐变大,那么注意力矩阵的每一个值大概率是会不断变大,如果注意力矩阵中的每个数值之间的差距变得很大,假设此时有一个最大值
x
0
x_0
x0,那他对应的softmax后的概率基本接近1,其他位置的值接近0,这就导致softmax的梯度趋近0,因此作者在注意力矩阵计算时加了一个放缩因子
d
k
\sqrt{d_k}
dk,从而缓解这种情况的发生。
多头注意力中,需要对
h
h
h个头计算的注意力结果进行连接,连接后传输到一个前馈网络中。Transformer默认的注意力头数量是8,同时为了保证输入输出的维度一致(因为还要残差链接),所以会在执行注意力前通过一个MLP将QKV的维度映射到
d
=
d
m
o
d
e
l
/
h
d = d_{model} / h
d=dmodel/h,最后拼接后仍然保持
d
m
o
d
e
l
d_{model}
dmodel不变。
多头注意力的结果和输入的原始特征通过残差加和后,需要通过Layer Norm层进行归一化,在NLP领域中,Layer Norm通常更为常用,CV中的Normalization方法一般是Batch Norm。我的理解是,Layer Norm是对每个Token单独进行归一化,Batch Norm则是对每个Channel在全部Batch内进行归一化。这里的Token在NLP中一般是每个词,而CV的图像处理任务中,每个Token则是图像的每个通道,视频任务中每个Token则是每个时间戳下的样本(每一帧)。
完成注意力计算后,FFN本身就是一个两层的MLP,作者解释的作用是把注意力聚合的特征映射到一个“合理的语义空间中”,感觉像是进一步增加模型可学习的维度/参数,同样在FFN处也有一个残差连结。
Decoder的基本结构和Encoder相似,但是的第一个Attention层是一个带有Masked的注意力层,之所以采用Masked是因为在训练模型阶段,输入内容是完整一个sentence,但是在预测第n个词时,肯定是只能用到
[
1
,
n
−
1
]
[1, n-1]
[1,n−1]个词的信息,后面的词信息是不能被“看到”的,所以需要一个mask来将n以后的词全部都“遮盖”,从而保证训练的严谨性和准确性。
Decoder的第二个Attention模块和之前都有不同,此时需要将输入信息加入进来,将输入的信息作为K、V,而将历史输出信息作为Q进行一个交叉注意力。其他计算方法保持一致。
Code调试
code by Tae Hwan Jung(Jeff Jung) @graykode, Derek Miller @dmmiller612, modify by shwei。
如下是代码构建的Transformer的结构图:Encoder中包括一个embedding层,编码成为一个512维度的向量,每个Encoder中有6层Encoder Layers。
Transformer(
(encoder): Encoder(
(src_emb): Embedding(11, 512)
(pos_emb): PositionalEncoding(
(dropout): Dropout(p=0.1, inplace=False)
)
(layers): ModuleList(
(0-5): 6 x EncoderLayer(
(enc_self_attn): MultiHeadAttention(
(W_Q): Linear(in_features=512, out_features=512, bias=False)
(W_K): Linear(in_features=512, out_features=512, bias=False)
(W_V): Linear(in_features=512, out_features=512, bias=False)
(fc): Linear(in_features=512, out_features=512, bias=False)
)
(pos_ffn): PoswiseFeedForwardNet(
(fc): Sequential(
(0): Linear(in_features=512, out_features=2048, bias=False)
(1): ReLU()
(2): Linear(in_features=2048, out_features=512, bias=False)
)
)
)
)
)
(decoder): Decoder(
(tgt_emb): Embedding(12, 512)
(pos_emb): PositionalEncoding(
(dropout): Dropout(p=0.1, inplace=False)
)
(layers): ModuleList(
(0-5): 6 x DecoderLayer(
(dec_self_attn): MultiHeadAttention(
(W_Q): Linear(in_features=512, out_features=512, bias=False)
(W_K): Linear(in_features=512, out_features=512, bias=False)
(W_V): Linear(in_features=512, out_features=512, bias=False)
(fc): Linear(in_features=512, out_features=512, bias=False)
)
(dec_enc_attn): MultiHeadAttention(
(W_Q): Linear(in_features=512, out_features=512, bias=False)
(W_K): Linear(in_features=512, out_features=512, bias=False)
(W_V): Linear(in_features=512, out_features=512, bias=False)
(fc): Linear(in_features=512, out_features=512, bias=False)
)
(pos_ffn): PoswiseFeedForwardNet(
(fc): Sequential(
(0): Linear(in_features=512, out_features=2048, bias=False)
(1): ReLU()
(2): Linear(in_features=2048, out_features=512, bias=False)
)
)
)
)
)
(projection): Linear(in_features=512, out_features=12, bias=False)
)
本文的Encoder结构如下:
class Encoder(nn.Module):
def __init__(self):
super(Encoder, self).__init__()
self.src_emb = nn.Embedding(src_vocab_size, d_model) # token Embedding
self.pos_emb = PositionalEncoding(
d_model) # Transformer中位置编码时固定的,不需要学习
self.layers = nn.ModuleList([EncoderLayer() for _ in range(n_layers)])
def forward(self, enc_inputs):
"""
enc_inputs: [batch_size, src_len] """
enc_outputs = self.src_emb(
enc_inputs) # [batch_size, src_len, d_model]
enc_outputs = self.pos_emb(enc_outputs.transpose(0, 1)).transpose(
0, 1) # [batch_size, src_len, d_model]
# Encoder输入序列的pad mask矩阵
enc_self_attn_mask = get_attn_pad_mask(
enc_inputs, enc_inputs) # [batch_size, src_len, src_len]
enc_self_attns = [] # 在计算中不需要用到,它主要用来保存你接下来返回的attention的值(这个主要是为了你画热力图等,用来看各个词之间的关系
for layer in self.layers: # for循环访问nn.ModuleList对象
# 上一个block的输出enc_outputs作为当前block的输入
# enc_outputs: [batch_size, src_len, d_model], enc_self_attn: [batch_size, n_heads, src_len, src_len]
enc_outputs, enc_self_attn = layer(enc_outputs,
enc_self_attn_mask) # 传入的enc_outputs其实是input,传入mask矩阵是因为你要做self attention
enc_self_attns.append(enc_self_attn) # 这个只是为了可视化
return enc_outputs, enc_self_attns
本层的输入为enc_inputs,是一个
[
2
,
8
]
[2,8]
[2,8]大小的tensor,其中2为batch size,8是句子长度。输入后的enc_inputs会首先经过一个nn.Embedding层。nn.Embedding是PyTorch中的一个常用模块,其主要作用是将输入的整数序列转换为密集向量表示。NLP任务中,可以将每个单词表示成一个向量,从而方便进行下一步的计算和处理。本文的Embedding层是将每个词转换为一个512维的向量,得到
[
2
,
8
,
512
]
[2, 8, 512]
[2,8,512]。
注意:这里返回的结果记为“enc_outputs”,这是因为后续在计算Attention时需要反复将上一层的输出作为当前层的输入,这样命名便于理解。
经过编码后的特征将会和Positional Encoding的结果进行加和,这里的位置编码采用的是论文中的正余弦位置编码。此外,还有一个需要注意的地方是,经过编码后的特征enc_outputs会先进行一次转置,转置为
[
n
,
b
,
c
]
[n, b, c]
[n,b,c]形状,随后送入Positional Encoding中。
class PositionalEncoding(nn.Module):
def __init__(self, d_model, dropout=0.1, max_len=5000):
super(PositionalEncoding, self).__init__()
self.dropout = nn.Dropout(p=dropout)
pe = torch.zeros(max_len, d_model) # 假设有max_len个词,每个词512维
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1) # 每个词的位置,也需要一个512维度的编码
div_term = torch.exp(torch.arange(
0, d_model, 2).float() * (-math.log(10000.0) / d_model)) # 2i/d_model,构建基频
pe[:, 0::2]= torch.sin(position * div_term) # 偶数位置
pe[:, 1::2] = torch.cos(position * div_term) # 奇数位置
pe = pe.unsqueeze(0).transpose(0, 1)
self.register_buffer('pe', pe)
def forward(self, x):
"""
x: [seq_len, batch_size, d_model]"""
x = x + self.pe[:x.size(0), :] # 此时的x.size(0)不是batch size了,而是句子长度。对每个句子进行位置编码,加在每个batch上。
return self.dropout(x)
此外,我们注意到,在论文中并没有提到在Encoder中也需要进行Mask。
enc_self_attn_mask = get_attn_pad_mask(enc_inputs, enc_inputs) # [batch_size, src_len, src_len]
这里的这句话是给self Attention构建一个mask,实现方式为:
def get_attn_pad_mask(seq_q, seq_k):
# pad mask的作用:在对value向量加权平均的时候,可以让pad对应的alpha_ij=0,这样注意力就不会考虑到pad向量
"""这里的q,k表示的是两个序列(跟注意力机制的q,k没有关系),例如encoder_inputs (x1,x2,..xm)和encoder_inputs (x1,x2..xm)
encoder和decoder都可能调用这个函数,所以seq_len视情况而定
seq_q: [batch_size, seq_len] seq_k: [batch_size, seq_len] seq_len could be src_len or it could be tgt_len seq_len in seq_q and seq_len in seq_k maybe not equal """
batch_size, len_q = seq_q.size() # 这个seq_q只是用来expand维度的
batch_size, len_k = seq_k.size()
# eq(zero) is PAD token
# 例如:seq_k = [[1,2,3,4,0], [1,2,3,5,0]]
# [batch_size, 1, len_k], True is masked
pad_attn_mask = seq_k.data.eq(0).unsqueeze(1)
# [batch_size, len_q, len_k] 构成一个立方体(batch_size个这样的矩阵)
return pad_attn_mask.expand(batch_size, len_q, len_k)
输入的seq_k和seq_q是一样的,都是之前的enc_inputs,pad_attn_mask的结果是将seq_k中非零的位置表征为False,0的位置则为True。这里查了一下资料,解释是:“在计算自注意力的时候,只对有效序列长度进行attention计算”,而NLP任务中,每一个Batch中的sentence的长度不一定一致,如果不一致,会对长度较短的句子补0对齐,因此0的位置属于“无效序列”,需要被mask掉。最后返回的是一个
[
b
,
n
,
n
]
[b, n, n]
[b,n,n]大小的矩阵。为什么是
[
b
,
n
,
n
]
[b, n, n]
[b,n,n]?因为最后得到的注意力矩阵是一个
[
n
,
n
]
[n,n]
[n,n]的矩阵,矩阵上的每个元素
(
i
,
j
)
(i,j)
(i,j)都代表第
i
i
i个字和第
j
j
j个字之间的注意力权重,对于序列为0的位置则不需要计算权重。
接下来将enc_inputs和enc_self_attn_mask通过每一层的Attention和FFN。
for layer in self.layers: # for循环访问nn.ModuleList对象
# 上一个block的输出enc_outputs作为当前block的输入
# enc_outputs: [batch_size, src_len, d_model], enc_self_attn: [batch_size, n_heads, src_len, src_len]
enc_outputs, enc_self_attn = layer(enc_outputs, enc_self_attn_mask) # 传入的enc_outputs其实是input,传入mask矩阵是因为你要做self attention
代码遍历Transformer的每一层,同时将上一层的输出作为下一层的输入,下面是Layer部分的结构:
ModuleList(
(0-5): 6 x EncoderLayer(
(enc_self_attn): MultiHeadAttention(
(W_Q): Linear(in_features=512, out_features=512, bias=False)
(W_K): Linear(in_features=512, out_features=512, bias=False)
(W_V): Linear(in_features=512, out_features=512, bias=False)
(fc): Linear(in_features=512, out_features=512, bias=False)
)
(pos_ffn): PoswiseFeedForwardNet(
(fc): Sequential(
(0): Linear(in_features=512, out_features=2048, bias=False)
(1): ReLU()
(2): Linear(in_features=2048, out_features=512, bias=False)
)
)
)
)
可以看到是6层Attention+FFN,我们以其中一层的操作举例,其他层的操作其实是一致的。
class EncoderLayer(nn.Module):
def __init__(self):
super(EncoderLayer, self).__init__()
self.enc_self_attn = MultiHeadAttention()
self.pos_ffn = PoswiseFeedForwardNet()
def forward(self, enc_inputs, enc_self_attn_mask):
"""E
enc_inputs: [batch_size, src_len, d_model] enc_self_attn_mask: [batch_size, src_len, src_len] mask矩阵(pad mask or sequence mask)
"""
# enc_outputs: [batch_size, src_len, d_model], attn: [batch_size, n_heads, src_len, src_len]
# 第一个enc_inputs * W_Q = Q
# 第二个enc_inputs * W_K = K
# 第三个enc_inputs * W_V = V
enc_outputs, attn = self.enc_self_attn(enc_inputs, enc_inputs, enc_inputs,
enc_self_attn_mask) # enc_inputs to same Q,K,V(未线性变换前)
enc_outputs = self.pos_ffn(enc_outputs)
# enc_outputs: [batch_size, src_len, d_model]
return enc_outputs, attn
其中self.enc_self_attn
是计算多头自注意力,这里放一张图,对照着看。
代码如下:
class MultiHeadAttention(nn.Module):
"""这个Attention类可以实现:
Encoder的Self-Attention
Decoder的Masked Self-Attention
Encoder-Decoder的Attention
输入:seq_len x d_model
输出:seq_len x d_model
"""
def __init__(self):
super(MultiHeadAttention, self).__init__()
self.W_Q = nn.Linear(d_model, d_k * n_heads,
bias=False) # q,k必须维度相同,不然无法做点积
self.W_K = nn.Linear(d_model, d_k * n_heads, bias=False)
self.W_V = nn.Linear(d_model, d_v * n_heads, bias=False)
# 这个全连接层可以保证多头attention的输出仍然是seq_len x d_model
self.fc = nn.Linear(n_heads * d_v, d_model, bias=False)
def forward(self, input_Q, input_K, input_V, attn_mask):
"""
input_Q: [batch_size, len_q, d_model] input_K: [batch_size, len_k, d_model] input_V: [batch_size, len_v(=len_k), d_model] attn_mask: [batch_size, seq_len, seq_len] """
residual, batch_size = input_Q, input_Q.size(0)
# 下面的多头的参数矩阵是放在一起做线性变换的,然后再拆成多个头,这是工程实现的技巧
# B: batch_size, S:seq_len, D: dim
# (B, S, D) -proj-> (B, S, D_new) -split-> (B, S, Head, W) -trans-> (B, Head, S, W) #线性变换拆成多头
# Q: [batch_size, n_heads, len_q, d_k]
Q = self.W_Q(input_Q).view(batch_size, -1,
n_heads, d_k).transpose(1, 2)
# K: [batch_size, n_heads, len_k, d_k] # K和V的长度一定相同,维度可以不同
K = self.W_K(input_K).view(batch_size, -1,
n_heads, d_k).transpose(1, 2)
# V: [batch_size, n_heads, len_v(=len_k), d_v]
V = self.W_V(input_V).view(batch_size, -1,
n_heads, d_v).transpose(1, 2)
# 因为是多头,所以mask矩阵要扩充成4维的
# attn_mask: [batch_size, seq_len, seq_len] -> [batch_size, n_heads, seq_len, seq_len]
attn_mask = attn_mask.unsqueeze(1).repeat(1, n_heads, 1, 1)
# context: [batch_size, n_heads, len_q, d_v], attn: [batch_size, n_heads, len_q, len_k]
context, attn = ScaledDotProductAttention()(Q, K, V, attn_mask)
# 下面将不同头的输出向量拼接在一起
# context: [batch_size, n_heads, len_q, d_v] -> [batch_size, len_q, n_heads * d_v]
context = context.transpose(1, 2).reshape(
batch_size, -1, n_heads * d_v)
# 这个全连接层可以保证多头attention的输出仍然是seq_len x d_model
output = self.fc(context) # [batch_size, len_q, d_model]
return nn.LayerNorm(d_model).to(device)(output + residual), attn
可以看到整个MultiHeadAttention包括四个Linear层,分别是Q、K和V的投影层和最后的FFN。但是在工程实现的角度,并不是从
d
m
o
d
e
l
d_{model}
dmodel投影到
d
m
o
d
e
l
h
e
a
d
\frac{d_{model}}{head}
headdmodel维度,而是先做了线性变换
d
m
o
d
e
l
→
d
m
o
d
e
l
d_{model} \to d_{model}
dmodel→dmodel,然后再拆开成
h
e
a
d
=
8
head=8
head=8个头。
(
B
,
S
,
D
)
−
p
r
o
j
−
>
(
B
,
S
,
D
n
e
w
)
−
s
p
l
i
t
−
>
(
B
,
S
,
H
e
a
d
,
W
)
−
t
r
a
n
s
−
>
(
B
,
H
e
a
d
,
S
,
W
)
(B, S, D) -proj-> (B, S, D_{new}) -split-> (B, S, Head, W) -trans-> (B, Head, S, W)
(B,S,D)−proj−>(B,S,Dnew)−split−>(B,S,Head,W)−trans−>(B,Head,S,W)
实际的代码就是,先将input_Q、input_K和input_V通过对应的Linear层,然后view(batch_size, -1, n_heads, d_k)
变成一个
[
B
,
S
,
H
e
a
d
,
d
k
]
[B, S, Head, d_{k}]
[B,S,Head,dk],再转置transpose(1, 2)
为
[
B
,
H
e
a
d
,
S
,
d
k
]
[B, Head, S, d_{k}]
[B,Head,S,dk]。
由于采用多头注意力,因此需要将mask进行维度扩展,其实就是对mask矩阵在每个头上都进行一次拓展。
attn_mask = attn_mask.unsqueeze(1).repeat(1, n_heads, 1, 1)
随后得到的这个attn_mask和QKV一起送入注意力计算层中,以下是注意力计算代码:
class ScaledDotProductAttention(nn.Module):
def __init__(self):
super(ScaledDotProductAttention, self).__init__()
def forward(self, Q, K, V, attn_mask):
"""
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(=len_k), d_v] attn_mask: [batch_size, n_heads, seq_len, seq_len] 说明:在encoder-decoder的Attention层中len_q(q1,..qt)和len_k(k1,...km)可能不同
"""
scores = torch.matmul(Q, K.transpose(-1, -2)) / np.sqrt(d_k) # scores : [batch_size, n_heads, len_q, len_k]
# mask矩阵填充scores(用-1e9填充scores中与attn_mask中值为1位置相对应的元素)
# Fills elements of self tensor with value where mask is True.
scores.masked_fill_(attn_mask, -1e9)
attn = nn.Softmax(dim=-1)(scores) # 对最后一个维度(v)做softmax
# scores : [batch_size, n_heads, len_q, len_k] * V: [batch_size, n_heads, len_v(=len_k), d_v] # context: [batch_size, n_heads, len_q, d_v]
context = torch.matmul(attn, V)
# context:[[z1,z2,...],[...]]向量, attn注意力稀疏矩阵(用于可视化的)
return context, attn
我们知道Transformer中的注意力计算方法是 Attention ( Q , K , V ) = softmax ( Q K T d k ) V \begin{aligned}\text{Attention}(Q,K,V)=\text{softmax}(\frac{QK^T}{\sqrt{d_k}})V\end{aligned} Attention(Q,K,V)=softmax(dkQKT)V,对应的代码部分:
scores = torch.matmul(Q, K.transpose(-1, -2)) / np.sqrt(d_k)
首先将K的最后两个维度进行转置,然后和Q做矩阵点乘,本文的Q、K的初始维度都是 [ 2 , 8 , 8 , 64 ] [2, 8, 8, 64] [2,8,8,64],经过转置点乘后得到的数据大小为 [ 2 , 8 , 8 , 8 ] [2,8, 8, 8] [2,8,8,8],并除一个 d k \sqrt{d_k} dk进行尺度缩放,随后将需要mask的位置替换为极小值1e-9,并通过一个softmax层,转换为每个词的重要性权重,这个权重最终需要和Value进行加权:
context = torch.matmul(attn, V)
这就是最终经过一层Attention得到的特征,后续需要将8个头的特征全部拼接起来:
context = context.transpose(1, 2).reshape(batch_size, -1, n_heads * d_v)
得到的就是又是一个512维的向量,随后还需要通过一个FC网络(MultiHeadAttention的第48行代码),这个网络一方面保证多头注意力的输出仍然是
d
m
o
d
e
l
=
512
d_{model} = 512
dmodel=512,也是将特征再次投影到一个更适配模型任务的特征空间中。(很玄学…)
注意MultiHeadAttention的第21行代码
residual, batch_size = input_Q, input_Q.size(0)
这里的residual是为了后续的残差连接做准备的,就是在第49行代码:
return nn.LayerNorm(d_model).to(device)(output + residual), attn
这里对应的就是Add & Norm这个操作,这里的norm采用Layer Normalization方法,至此就完成了Encoder中一层Block操作。
对于Decoder,先放一张图,便于和代码对接。可以看到Decoder的输入首先需要经过一层Embedding进行嵌入,随后加入Positional Encoding的位置编码,随后通过一个Masked多头自注意力,并通过Add & Norm后和Encoder的输入进行交叉注意力计算,最后再通过FNN等即可。
现在看一下对于Decoder部分的代码实现:
class Decoder(nn.Module):
def __init__(self):
super(Decoder, self).__init__()
self.tgt_emb = nn.Embedding(
tgt_vocab_size, d_model) # Decoder输入的embed词表
self.pos_emb = PositionalEncoding(d_model)
self.layers = nn.ModuleList([DecoderLayer()
for _ in range(n_layers)]) # Decoder的blocks
def forward(self, dec_inputs, enc_inputs, enc_outputs):
"""
dec_inputs: [batch_size, tgt_len] enc_inputs: [batch_size, src_len] enc_outputs: [batch_size, src_len, d_model] # 用在Encoder-Decoder Attention层
"""
dec_outputs = self.tgt_emb(
dec_inputs) # [batch_size, tgt_len, d_model]
dec_outputs = self.pos_emb(dec_outputs.transpose(0, 1)).transpose(0, 1).to(
device) # [batch_size, tgt_len, d_model]
# Decoder输入序列的pad mask矩阵(这个例子中decoder是没有加pad的,实际应用中都是有pad填充的)
dec_self_attn_pad_mask = get_attn_pad_mask(dec_inputs, dec_inputs).to(
device) # [batch_size, tgt_len, tgt_len]
# Masked Self_Attention:当前时刻是看不到未来的信息的
dec_self_attn_subsequence_mask = get_attn_subsequence_mask(dec_inputs).to(
device) # [batch_size, tgt_len, tgt_len]
# Decoder中把两种mask矩阵相加(既屏蔽了pad的信息,也屏蔽了未来时刻的信息)
dec_self_attn_mask = torch.gt((dec_self_attn_pad_mask + dec_self_attn_subsequence_mask),
0).to(device) # [batch_size, tgt_len, tgt_len]; torch.gt比较两个矩阵的元素,大于则返回1,否则返回0
# 这个mask主要用于encoder-decoder attention层
# get_attn_pad_mask主要是enc_inputs的pad mask矩阵(因为enc是处理K,V的,求Attention时是用v1,v2,..vm去加权的,要把pad对应的v_i的相关系数设为0,这样注意力就不会关注pad向量)
# dec_inputs只是提供expand的size的
dec_enc_attn_mask = get_attn_pad_mask(
dec_inputs, enc_inputs) # [batc_size, tgt_len, src_len]
dec_self_attns, dec_enc_attns = [], []
for layer in self.layers:
# dec_outputs: [batch_size, tgt_len, d_model], dec_self_attn: [batch_size, n_heads, tgt_len, tgt_len], dec_enc_attn: [batch_size, h_heads, tgt_len, src_len]
# Decoder的Block是上一个Block的输出dec_outputs(变化)和Encoder网络的输出enc_outputs(固定)
dec_outputs, dec_self_attn, dec_enc_attn = layer(dec_outputs, enc_outputs, dec_self_attn_mask,
dec_enc_attn_mask)
dec_self_attns.append(dec_self_attn)
dec_enc_attns.append(dec_enc_attn)
# dec_outputs: [batch_size, tgt_len, d_model]
return dec_outputs, dec_self_attns, dec_enc_attns
可以看到,dec_inputs是最开始的句子,随后通过一个Embedding层将dec_inputs编码为512维度的特征嵌入,得到dec_outputs,这里的命名也是为了方便后面的残差连接。
dec_self_attn_pad_mask = get_attn_pad_mask(dec_inputs, dec_inputs).to(device)
这里的pad_mask其实和Encoder中的pad_mask意义是一样的,也是掩蔽掉无效长度。随后就是Decoder中的Masked_Attention部分,让当前时刻看不到未来的信息。以下是函数的实现部分:
def get_attn_subsequence_mask(seq):
"""建议打印出来看看是什么的输出(一目了然)
seq: [batch_size, tgt_len]
"""
attn_shape = [seq.size(0), seq.size(1), seq.size(1)]
# attn_shape: [batch_size, tgt_len, tgt_len]
subsequence_mask = np.triu(np.ones(attn_shape), k=1) # 生成一个上三角矩阵
subsequence_mask = torch.from_numpy(subsequence_mask).byte()
return subsequence_mask # [batch_size, tgt_len, tgt_len]
注意这里的第7行代码,既然要让当前时刻看不到未来的信息,这里的代码采用一个上三角矩阵来表示这种关系:
可以看到,第
i
i
i行代表当前看到的是第
i
i
i个词,此时除了前
i
i
i个位置之外,其余列均为1,表示需要mask。随后代码将两个mask矩阵相加,既屏蔽了pad_mask的信息,也屏蔽了未来时刻的信息。
dec_self_attn_mask = torch.gt((dec_self_attn_pad_mask + dec_self_attn_subsequence_mask), 0).to(device)
torch.gt
的作用是比较两个矩阵的元素,大于则返回1,否则返回0。
随后代码中又计算了一次pad_attn,这个pad_attn是用于encoder-decoder的Attention层(交叉注意力层)。所以输入此时就为dec_inputs和enc_inputs。
dec_enc_attn_mask = get_attn_pad_mask(dec_inputs, enc_inputs)
至此,Decoder全部的数据准备工作就都结束了,下面正式是Decoder Layer的部分:
for layer in self.layers:
# dec_outputs: [batch_size, tgt_len, d_model], dec_self_attn: [batch_size, n_heads, tgt_len, tgt_len], dec_enc_attn: [batch_size, h_heads, tgt_len, src_len]
# Decoder的Block是上一个Block的输出dec_outputs(变化)和Encoder网络的输出enc_outputs(固定)
dec_outputs, dec_self_attn, dec_enc_attn = layer(dec_outputs, enc_outputs, dec_self_attn_mask,
dec_enc_attn_mask)
dec_self_attns.append(dec_self_attn)
dec_enc_attns.append(dec_enc_attn)
# dec_outputs: [batch_size, tgt_len, d_model]
Decoder的输入包括enc_inputs和dec_outputs,enc_inputs本身是的固定的,dec_outputs则是不断变化的。我们还是按照一层Decoder Layer来举例:
class DecoderLayer(nn.Module):
def __init__(self):
super(DecoderLayer, self).__init__()
self.dec_self_attn = MultiHeadAttention()
self.dec_enc_attn = MultiHeadAttention()
self.pos_ffn = PoswiseFeedForwardNet()
def forward(self, dec_inputs, enc_outputs, dec_self_attn_mask, dec_enc_attn_mask):
"""
dec_inputs: [batch_size, tgt_len, d_model]
enc_outputs: [batch_size, src_len, d_model]
dec_self_attn_mask: [batch_size, tgt_len, tgt_len]
dec_enc_attn_mask: [batch_size, tgt_len, src_len] """
# dec_outputs: [batch_size, tgt_len, d_model], dec_self_attn: [batch_size, n_heads, tgt_len, tgt_len]
dec_outputs, dec_self_attn = self.dec_self_attn(dec_inputs, dec_inputs, dec_inputs,
dec_self_attn_mask) # 这里的Q,K,V全是Decoder自己的输入
# dec_outputs: [batch_size, tgt_len, d_model], dec_enc_attn: [batch_size, h_heads, tgt_len, src_len]
dec_outputs, dec_enc_attn = self.dec_enc_attn(dec_outputs, enc_outputs, enc_outputs,
dec_enc_attn_mask) # Attention层的Q(来自decoder) 和 K,V(来自encoder)
# [batch_size, tgt_len, d_model]
dec_outputs = self.pos_ffn(dec_outputs)
# dec_self_attn, dec_enc_attn这两个是为了可视化的
return dec_outputs, dec_self_attn, dec_enc_attn
首先第一层Attention是对于Decoder输入的自注意力层计算,dec_outputs和enc_outputs一起,经过一次交叉注意力计算,其中dec_outputs作为Q,enc_outputs作为K和V,最后通过一个FFN即可完成整个Decoder的计算过程。
Decoder的Attention Layers输出的dec_outputs最后需要通过一个线性层,这个线性层将
d
m
o
d
e
l
d_{model}
dmodel投影到tgt_vocab维度上,这个tgt_vocab是指输出的词字典中有多少个词,后续通过一个Softmax即可得到每个词的概率。
dec_logits = self.projection(dec_outputs)
return dec_logits.view(-1, dec_logits.size(-1)), enc_self_attns, dec_self_attns, dec_enc_attns
输出的概率会和decoder的输入进行损失计算(Transformer用的是交叉熵),至此就完成了一次训练过程。