Transformer从0阅读,从原论文《attention is all you need》开始向你深入浅出的解释注意力机制与Transformer -- 架构补充与广播机制

        在前两天我们给transformer的架构进行了一个比较详细的讲解。具体链接如下:Transformer模型架构icon-default.png?t=N7T8https://blog.csdn.net/m0_62716099/article/details/141289541?spm=1001.2014.3001.5501        我们基本上把所有的内容都讲完了,但是关于模型本身最后还差两个部分。一是Transformer中的掩码操作;二是整个Transformer的架构。确实比较难理解,尤其是对于这个掩码操作,我自己也存在一些疑惑,在总结的过程中我会提出自己的问题,如果有同学比较明白,也欢迎大家一起来探讨讨论。

        嗯,其实我自己回头来看我的上一篇文章,感觉从原理讲到代码,内容太多有的时候反而更不容易理解了。这次我直接一遍敲代码一遍说吧。

代码:Neural-Network-from-zero-to-hero/DiveIntoDL/13_Transformer.ipynb at master · pilipala5/Neural-Network-from-zero-to-hero · GitHub

邮箱:

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很多源码的),应该是没问题的 :)。我觉得逻辑应该就是如下:(以机器翻译为例子),这个我没有很严谨的去想,要是不对还请大家指正!

  1. 读取数据集,进行统计分析,创建tokens -> stoi 和 itos, 两个语言的句子应该不一样
  2. 对于我们的训练句子,利用<pad>补全sentence到max_len的长度,同样的,对于翻译语言的句子,我们要添加<sos> 和 <eos>还有<pad>的符号,pad到eos后面
  3. 然后训练,在训练的过程中,decoder的逻辑跟实际翻译的逻辑不同,实际翻译肯定是以上一个的输入为输出,但是训练的过程中,我们要输入真实的前一个token作为输入
  4. 误差loss的计算,应该也要考虑到掩膜,只计算非pad的误差。

之前有粗糙的写过一篇Seq2Seq机器翻译的博客,应该差不太多的逻辑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

香蕉也是布拉拉

随缘打赏不强求~ 谢谢大家

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值