关于Transformer你不可不知的几个点

这也不知道是第几次阅读《attention is all you need》这篇论文了,下面是这些年阅读这篇论文的一些心得感受,记录一下,各位看官根据自己的关注点点击下面的目录跳转:

目录

1、为什么Transformer是一个完全基于注意力机制的神经网络模型?

2、什么是注意力机制,什么又是自注意力机制,与其他注意力机制有什么区别?

3、Q、K、V代表什么,有哪些好的办法对这些概念进行理解;

4、多头注意力机制又是什么,怎样实现的?

5、解码器为什么要使用编码器的(K,V)值作为输入?

6、位置编码的作用

8、为什么以Transformer为基础的Bert模型,最长序列输入长度是512,与模型的参数设定有关?


1、为什么Transformer是一个完全基于注意力机制的神经网络模型?

        在Transformer出现之前,已经有许多经典的神经网路模型,比如有用于时间序列分析的循环神经网络模型和用于空间图像分析的卷积神经网络模型。循环神经网络的缺陷是处理前面元素时后面的元素无法获知,虽然有双向循环神经网路,但是在长序列建模中,依旧逃避不了模型更加关注当前处理元素的特征,从而导致间隔较远的元素被遗忘。卷积神经网络更像是一个相机,模型本身只能记录输入信息中有什么,而不能为输入数据中的多个特征建立起一个联系。

        Transformer不能说太完美,但是在解决上述两个模型中存在的问题,有了一种更先进的方法。注意力机制在Transformer提出来之前,已经在许多方面取得了显著的进步,通过将模型关注的权重进行分配,可以让模型腾出更多的精力放在重要的输入上。于是便有了“attention is all you ”,构造一个专门用于寻找重要信息点的模型,由于这个模型最早是用于机器翻译,所以这种模型是以Encoder-Decoder的形式存在,用完全的注意力机制关注输入的重点内容,并打包编码传给解码器,解码器也用相同的机制,关注应该在哪些信息上着力解读。这便是Transformer,但是Transformer不是真的完全由注意力机制构成哦!模型中还是使用了去前馈神经网络、残差机制以及全连接映射,虽然有这些结构作为辅助,但是建模的思想确实遵循了完全的注意力机制。

2、什么是注意力机制,什么又是自注意力机制,与其他注意力机制有什么区别?

        注意力:你的眼里只有你看到的东西,虽然其他事物就在旁边。模型处理数据也许要这样,模型的眼里也只有它看到的数据。如果啥都关注,可能啥都没有关注(可能在发呆呢!)。约翰.赫尔曼提出了自我注意机制,不过这是心理学中的内容,我们不关注,而机器学习中的自注意力机制最早就来自与《attention is all you need》,文中的自注意力机制简单说来就是当前的单词与句子中的所有单词进行重组,然后决定当前单词需要以怎样的状态出现才能与其他单词产生联系,大概示意图就如下:

 与其他注意力机制最大的区别就在于,自注意力机制每个时刻都是从自己视角出发,更多的注意点是自己,关注的结果是自己怎样更好的融入整个句子,而其他的注意力机制是从模型的视角出发,关注点是有根据句子的本身的上下文语境来决定的。

3、Q、K、V代表什么,有哪些好的办法对这些概念进行理解;

        最开始阅读论文时,对于Q,K,V具体代表的含义也是非常疑惑,为什么要将同一个输入输入数据拆分成三种不同的表示形式,这样到底有什么好处呢?进过深思熟虑的思考,我想到了一个很好的例子来阐述这样一种思想:

        字典应该是大家都非常熟悉的一个学习工具,我们在用字典查询一个不认识的字时,首先不是直接翻到该字出现的页码,而是对这个字有一个大概的印象,比如这个字的拼音,这个字的部首或者这个字的笔画数,这是都是我们寻找这个字的起始线索,得到这个字的线索后,我们就根据这个线索寻找到这个字以及这个字的页码,最后便是根据这个页码找到目标字,最后就是看这个字的句子含义和各种信息了。Q、K、V所表示的含义是不是与这个过程查字典的过程很相似呢!这个对应关系如下图所示:

 所以要了解一份输入数据的具体信息,我们也不能直接囫囵吞枣,输入的位置信息,我们总是不了解的,类比查字典的过程,也可以将输入的信息分散到三个部分,分三步完成对输入信息的检索。于是模型也会根据输入的信息,首先根据直觉找到该信息的线索,根据线索去寻找该信息的“页码”,最后根据“页码”找到该信息真实的信息载体。公式如下:

softmax(\frac{Q*K}{\sqrt{d}})V

 这个公式的理解,我觉得是因为V中包含了大量的“意象”,具体要取哪一个意象,需要根据Q与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)

4、多头注意力机制又是什么,怎样实现的?

        多头注意力机制,其实可以看作是同时叫多个人去查字典。生活中,对于同一份文章,在遇到陌生的字的时候,每个人的知识盲区可能是不样的,一些人可能不知道这个字怎样读,有的人不知道这个字怎么写,有的人是既不会读也不会写,所以不同的人,会用不同的方式取检索数据。而且,同一个字可能每一个人关注的“意象点”不尽相同,这样导致的最后获取的信息点有差距。实现代码如下:

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

5、解码器为什么要使用编码器的(K,V)值作为输入?

        编码器的作用就是将输入数据经过模型自己的理解构建出一个“字典”,可以将输入数据的大部分“信息”都包含在里面,使得解码器有信息可用。而解码器呢,就是根据已知的或者已经生成的数据,去生成下一个位置可能的查询值Q,然后得到这个Q值,去已经构建好的字典中查询需要得到的信息,最后根据这个信息去解码得到最终的输出。(注意:解码器中的数据是根据已有的前面时刻生成的文字去推理后面出现的文字,所以这里在训练模型的时候,由于输入的是整个句子,为了保持训练与测试相同的使用方式,需要在解码器中添加掩码机制以遮盖不知道文字信息)

6、位置编码的作用

        Transformer本身的注意力机制是没有考虑句子序列顺序的,但是为什么句子在编码和解码过程中都是井然有序,并没有出现翻译顺序混乱不堪的情况呢?其实,无论是编码器的输入还是解码器的输入,在对单词本身进行编码的同时,还对单词出现在句子中的问题进行的附加编码,由于正余弦函数具有很强的位置属性,所以可以选用正余弦函数来实现位置编码。这样在编码解码的时候,也自然引入了位置信息,使得解码出来的文字位置不会错乱。

8、为什么以Transformer为基础的Bert模型,最长序列输入长度是512,与模型的参数设定有关?

        这里申明一点:这里Bert模型的的输入序列长度与Transformer模型选取的向量维度和模型参数没有关系!而是由于自注意力本身机制决定的,由于自注意力机制需要观察输入的每一个元素,所以,序列越长,当前元素关注的对象就越多,使得模型的训练参数和模型体积变得异常庞大。为了折中这种选择,选取了512这个数,并且在模型训练和模型使用的时候都是固定的,如果要改变模型的输入长度,只能自己设定模型规模大小并重新训练一次。

猜你喜欢

转载自blog.csdn.net/weixin_45158611/article/details/129713258