在前两天我们给transformer的架构进行了一个比较详细的讲解。具体链接如下:Transformer模型架构https://blog.csdn.net/m0_62716099/article/details/141289541?spm=1001.2014.3001.5501 我们基本上把所有的内容都讲完了,但是关于模型本身最后还差两个部分。一是Transformer中的掩码操作;二是整个Transformer的架构。确实比较难理解,尤其是对于这个掩码操作,我自己也存在一些疑惑,在总结的过程中我会提出自己的问题,如果有同学比较明白,也欢迎大家一起来探讨讨论。
嗯,其实我自己回头来看我的上一篇文章,感觉从原理讲到代码,内容太多有的时候反而更不容易理解了。这次我直接一遍敲代码一遍说吧。
邮箱:
yuhan.huang@whu.edu.cn
掩码操作
在掩码操作之前,我们回顾一下Transformer中的多头注意力机制,原理还是去上一篇文章来看,这里我们直接看代码:
# scaled dot-product attention
class ScaledDotProductAttention(nn.Module):
def __init__(self):
super().__init__()
self.softmax = nn.Softmax(dim=-1)
def forward(self, q, k, v, mask=None):
# (batch_size, h, max_len, dk)
# Attention(Q,K,V) = softmax(Q @ K_T / sqrt(dk)) @ V
_, _, _, dk = k.shape
k_T = k.transpose(2, 3) # (batch_size, h, dk, max_len)
scores = torch.matmul(q, k_T) / math.sqrt(dk) # (batch_size, h, max_len, max_len)
if mask is not None: # (batch_size, 1, 1, max_len) or (batch_size, 1, max_len, 1)
scores = scores.masked_fill(mask == 0, -10000)
scores = self.softmax(scores) # (batch_size, h, max_len, max_len)
outputs = torch.matmul(scores, v) # (batch_size, h, max_len, dv)
return outputs, scores
这里可以看见,我们的mask主要是对于 q @ k_T 得到的矩阵进行一个掩码操作,熟悉原理的同学都知道,以 max_len 个token来举例,scores矩阵的每一行 都是一个token作为q,每一列都是每一个token作为k来计算余弦相似度。
既然我们得到了scores,我们在训练的时候对于一些数据是需要进行补全或者切割处理的。所以,我们要把 "<pad>"这个token不参与到scores中的计算的。这个不懂的还是去看一下李宏毅老师的课程。具体的掩膜操作,在Encoder和Decoder中又有部分不同。我们可以看Transformer的架构图,主要是三个Attention机制:1. Encoder中的自注意力机制; 2.Decoder中的自注意力机制 3. Encoder 和 Decoder中的交叉注意力机制
友情提示:所有的代码,看不懂的,可以像我一样,自己敲几个代码print一下,自然就心知肚明了。
广播机制
首先我们要知道,广播机制存在几条核心规则:
1. 如果两个张量的维度数量不同,则将维度较少的张量的形状在前面补齐1
这里很简单,比如说我 (3, 5)的tensor 要加上一个(5, )的tensor,那么我就会在5前面的维度补一个1.
2. 从尾部维度开始,对齐每个维度维度。(大小相同或有一个为1)
这里也是同样的思路,我要把两个tensor从末尾的维度开始判断能否对齐,若两个tensor的某一个维度大小不同,且均不为1,那么就无法对齐,会报错。
3. 广播的结果维度是两个张量中每个维度的最大值。
这个很简单哈,我就不过多解释。敲代码就知道了。
我直接给大家一个简单的案例,下面的注释片段代码是内部逻辑的可视化。并且思考为什么第二个案例会广播失败? 就当作小练习了哦 : )
print((torch.randn([3, 5]) + torch.randn([5])).shape) # 不会报错 (3, 5)
print((torch.randn([3, 5]) + torch.randn([3])).shape) # 无法对齐 会报错
'''
tensor (3, 5) + tensor (5, )
how to align and broadcast
1. tensor(3, 5) -> tensor(3, 5)
tensor(5, ) -> tensor( , 5) -> tensor(1, 5)
2. tensor(3, 5) -> tensor(3, 5)
tensor(1, 5) -> tensor(3, 5)
'''
Encoder
在Encoder中,我们对于我们的输入会有一个获取掩膜的操作,我们先随机生成我们的一个案例数据。
a = torch.tensor([[1, 2, 3, 4, 0, 0, 0], # (2, 7) -> (batch_size, max_len)
[1, 9, 2, 0, 0, 0, 0]])
src_mask = (a != 0).unsqueeze(1).unsqueeze(2).to(torch.long) # (batch_size, 1, 1, max_len) -> (2, 1, 1, 7)
scores = torch.randn([2, 8, 7, 7])
_scores = scores.masked_fill(src_mask==0, -10000)
其中a就是我们输入的原始数据,批次为2,有7个词元,并且token "<pad>"用0来代替。因此我们得到的掩膜即为src_mask,至于为什么要增加两个维度呢?那自然就是跟广播机制有关了。
可以知道,我们这里的src_mask是一个(2, 1, 1, 7)的tensor,而scores则是我们注意力机制中q @ k_T 的结果,其形状为(2, 8, 7, 7), 8代表的是头部的数量 -> 这一部分在原理篇我已经讲过了,就不重复了。
那么我们就要对tensor进行一个masked_fill操作(就是一个把掩膜中为0的位置替换成-10000,softmax后的概率自然就是0)。当然,这其中就会实现一个广播机制了。我们如何形象的理解呢?我们可以不管前两个维度是怎么广播的,因为实际上的掩膜操作是二维上的操作,其余都是二维的不断重复嘛。我们就相当于把掩膜的这一行不断重复,形成一个二维的矩阵,所以softmax后得到的结果就是这个样子。
但是,我在这里是具有疑惑的,这个代码是Github上一个高star数的代码,但是他这样其实是把<pad>也当作q来进行余弦相似度的计算了,我不知道大家能不能理解,就是说,我觉得这个掩码实际上应该是一个(batch_size, 1, max_len, max_len)的矩阵,大概是这个样子。百思不得其解,希望大家指导一下。
Decoder
Decoder其实也就是在自注意力机制的一步,其关于pad的掩膜跟Encoder看着相同,实际大不相同,他在这里的代码反而成这样了。意思是说在广播过程中,其是把掩膜当成列向量,然后不断横向重复得到一个矩阵。反正我挺奇怪的。
trg = torch.tensor([1, 3, 8, 2, 0, 0, 0]).view(1, -1) # (1, 7) 1 -> sos ; 2-> eos; 0->pad (batch_size, max_len)
max_len = trg.shape[1]
trg_pad_mask = (trg != 0).unsqueeze(1).unsqueeze(3) # (batch_size, 1, max_len, 1)
当然除了这一步外,还有一步,由于Decoder是输出结果的一步,所以我们不能得到其位置之后的词元组合的信息,因而,我们需要进行这一步操作。
trg_sub_mask = torch.tril(torch.ones(max_len, max_len)).to(torch.long) # (max_len, max_len)
trg_mask = trg_pad_mask & trg_sub_mask # (batch_size, 1, 1, max_len)
sub_mask的形状也很简单,就是一个下三角矩阵。
因此,我们最终得到的就是两个掩膜的交集。
Transformer整体架构
整体架构就是把上一章的代码拼接一下就好了,并不复杂,这里直接把代码给大家。
class Transformer(nn.Module):
def __init__(self, src_pad_idx, trg_pad_idx, trg_bos_idx, enc_voc_size, dev_voc_size, d_model, n_head, max_len,
ffn_hidden, n_layers, drop_prob):
super().__init__()
self.src_pad_idx = src_pad_idx
self.trg_pad_idx = trg_pad_idx
self.trg_bos_idx = trg_bos_idx
self.encoder = Encoder(d_model=d_model,
n_head=n_head,
max_len=max_len,
ffn_hidden=ffn_hidden,
enc_vocab_size=enc_voc_size,
drop_prob=drop_prob,
n_layers=n_layers)
self.decoder = Decoder(d_model=d_model,
n_head=n_head,
max_len=max_len,
ffn_hidden=ffn_hidden,
dec_vocab_size=dev_voc_size,
drop_prob=drop_prob,
n_layers=n_layers)
def forward(self, src, trg):
src_mask = self.make_src_mask(src)
trg_mask = self.make_trg_mask(src)
enc_src = self.encoder(src, src_mask)
output = self.decoder(trg, enc_src, trg_mask, src_mask)
def make_src_mask(self, src):
src_mask = (src != self.src_pad_idx).unsqueeze(1).unsqueeze(2).to(torch.long)
return src_mask
def make_trg_mask(self, trg):
trg_pad_mask = (trg != self.trg_pad_idx).unsqueeze(1).unsqueeze(3)
trg_len = trg.shape[1]
trg_sub_mask = torch.tril(torch.ones(trg_len, trg_len)).type(torch.ByteTensor).to("cuda")
trg_mask = trg_pad_mask & trg_sub_mask
return trg_mask
这里我提一嘴训练,由于懒得找数据集,还有最近的事情比较多(bushi),我就没有自己跑这个模型了,但是我参考了很多github代码(Github一艘transformer很多源码的),应该是没问题的 :)。我觉得逻辑应该就是如下:(以机器翻译为例子),这个我没有很严谨的去想,要是不对还请大家指正!
- 读取数据集,进行统计分析,创建tokens -> stoi 和 itos, 两个语言的句子应该不一样
- 对于我们的训练句子,利用<pad>补全sentence到max_len的长度,同样的,对于翻译语言的句子,我们要添加<sos> 和 <eos>还有<pad>的符号,pad到eos后面
- 然后训练,在训练的过程中,decoder的逻辑跟实际翻译的逻辑不同,实际翻译肯定是以上一个的输入为输出,但是训练的过程中,我们要输入真实的前一个token作为输入
- 误差loss的计算,应该也要考虑到掩膜,只计算非pad的误差。
之前有粗糙的写过一篇Seq2Seq机器翻译的博客,应该差不太多的逻辑。