Transformer详解(三):Transformer 结构

Transformer结构是谷歌那篇《Attention is all you need》论文中提到的。论文中提出了transformer这么一种新的结构,将其应用在机器翻译的领域上,取得了很好的效果。本文将分析一下Transformer结构的具体组成部分。

1. 背景

自从Attention机制在提出之后,加入Attention的Seq2Seq模型在各个任务上都有了提升,所以现在的seq2seq模型指的都是结合rnn和attention的模型。传统的基于RNN的Seq2Seq模型难以处理长序列的句子,无法实现并行,并且面临对齐的问题。

所以之后这类模型的发展大多数从三个方面入手:

  • input的方向性:单向 -> 双向
  • 深度:单层 -> 多层
  • 类型:RNN -> LSTM GRU

但是依旧收到一些潜在问题的制约,神经网络需要能够将源语句的所有必要信息压缩成固定长度的向量。这可能使得神经网络难以应付长时间的句子,特别是那些比训练语料库中的句子更长的句子;每个时间步的输出需要依赖于前面时间步的输出,这使得模型没有办法并行,效率低;仍然面临对齐问题。

再然后CNN由计算机视觉也被引入到deep NLP中,CNN不能直接用于处理变长的序列样本但可以实现并行计算。完全基于CNN的Seq2Seq模型虽然可以并行实现,但非常占内存,很多的trick,大数据量上参数调整并不容易。

本篇文章创新点在于抛弃了之前传统的encoder-decoder模型必须结合cnn或者rnn的固有模式,只用Attention。文章的主要目的在于减少计算量和提高并行效率的同时不损害最终的实验结果。

2. 整体结构

Transformer 整体结构宏观上看是一个Encoder-Decoder结构,只不过这个结构完全抛弃了常见的RNN,LSTM等结构。

图1 Transformer整体结构

图中左边部分为Encoder 部分,右边部分为Decoder部分和最后的线性输出层。整体模型看上去看上去很复杂,其实这就是一个Seq2Seq模型,左边一个encoder把输入读进去,右边一个decoder得到输出。其中Encoder和Decoder各有6层。

图2 Encoder-Decoder 的6层结构

3. Transformer详细介绍

3.1 输入编码

如图所示,首先通过Word2Vec等词嵌入方法将输入语料转化成特征向量,论文中使用的词嵌入的维度为d_{model}=512

图3 单词的输入编码

3.1.1 Positional Encoding

对于没有Positional Encoding的模型,我们对于输入句子的处理其实忽略了词序问题。
因为按照attention的计算公式,我们只不过是计算了两两之间的attention值。‘I have a dream’ 和 ‘Dream have a I’这两句话对于attention而言是一样。那么截止目前为止,模型还不过是一个复杂的词袋模型,没有考虑到词序。而RNN这种模型的一大特点就是考虑到了词序。为了解决这个问题,transformer模型在数据预处理时就提出了位置编码这个概念。
原文中对于这个位置编码提出了两种方式,第一种是训练出一个位置编码,第二种是原文使用的用三角函数编码的方法。具体公式如下

这里的pos表示单词的位置, i表示embedding的维度。关于位置编码的实现可在Google开源的算法中get_timing_signal_1d()函数找到对应的代码。

位置编码的使用

至于为什么使用三角函数,是三角函数的两条性质可以既考虑到绝对位置又可以考虑到相对位置。

这表明位置k+p的位置向量可以表示为位置k的特征向量的线性变化,这为模型捕捉单词之间的相对位置关系提供了非常大的便利。通过这个公式可以用位置k的线性表达来表示位置k+p。
附上pytorch的代码帮助一下理解。

class PositionalEncoding(nn.Module):
    "Implement the PE function."
    def __init__(self, d_model, dropout, max_len=5000):
        super(PositionalEncoding, self).__init__()
        self.dropout = nn.Dropout(p=dropout)
        
        # Compute the positional encodings once in log space.
        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len).unsqueeze(1)
        div_term = torch.exp(torch.arange(0, d_model, 2) *
                             -(math.log(10000.0) / d_model))
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)
        pe = pe.unsqueeze(0)
        self.register_buffer('pe', pe)
        
    def forward(self, x):
        x = x + Variable(self.pe[:, :x.size(1)], requires_grad=False)
        return self.dropout(x)

3.1.2 输入的下流

在最底层的block中,x+positional encoding 将直接作为Transformer的输入,而在其他层中,输入则是上一个block的输出。在这里输入序列中每个位置的单词都有自己独特的路径流入编码器。在自注意力层中,这些路径之间存在依赖关系。而前馈(feed-forward)层没有这些依赖关系。因此在前馈(feed-forward)层时可以并行执行各种路径。

为了画图更简单,我们使用更简单的例子来表示接下来的过程。

图4 每一层的Encoder

Encoder的输出,会和每一层的Decoder进行结合。我们取其中一层进行详细的展示:

图5 Encoder的输出连接到Decoder的每一层

整体框架细节展示:

图6 整体框架

3.2 Encoder部分

Encoder有N=6层,前面几层的encoder的输出,会作为输入给下一层的encoder。这里要注意,每一个encoder里的两层的输出,都会进入一个add&Norm。最后的encoder会输出给后面的decoder模型。

每层包括两个sub-layers:

第一个sub-layer是multi-head self-attention mechanism,用来计算输入的self-attention
第二个sub-layer是简单的全连接网络。

3.2.1 Add & Normalize

在每个sub-layer我们都模拟了残差网络,每个sub-layer的输出都是:

LayerNorm(x+Sublayer(x))

其中Sublayer(x) 表示Sub-layer对输入 x 做的映射,为了确保连接,所有的sub-layers和embedding layer输出的维数都相同。

图7 Encoder中的每一层

3.2.2 Position-wise Feed-forward Networks

在进行了Attention操作之后,encoder和decoder中的每一层都包含了一个全连接前向网络,对每个position的向量分别进行相同的操作,包括两个线性变换和一个ReLU激活输出(输入输出层的维度都为512,中间层为2048):

FFN(x)=max(0,xW_1+b_1)W_2+b_2

3.3 Decoder部分

Decoder部分和Encoder一样,也是有6层,但是每一个单独的decoder层与encoder层相比,在self-attention层(decoder层中叫masked self-attention)和全连接网络层之间,多了一层Encoder-Decoder-Attention 层。


每层包括3个sub-layers:

  第一个是Masked multi-head self-attention,也是计算输入的self-attention,这个与encoder中的区别是这里是masked-multi-head-self-attention。使用mask的原因是因为在预测句子的时候,当前时刻是无法获取到未来时刻的信息的。上一篇文章提到self-attention会生成一个attention map,并以‘I have a dream’为例生成了一个4*4的attention map,这次生成的4*4的attention map因为有mask的原因,未来的信息全部被隐藏掉了。

图8 当前预测的时候是无法获取未来信息的

       第二个sub-layer是一个正常的multi-head attention层。但是这里Q,K,V来源不同。Q来自于上一个Decoder层的输出,而K,V则来自于encoder的输出。剩下的计算就没有其他的不同了。
  第三个sub-layer是全连接网络,与Encoder相同
  
  关于这两个attention层,可以理解为 mask-self-attention是计算当前翻译的内容和已经翻译的前文之间的关系,而encoder-decoder-attention 是计算当前翻译内容和编码的特征向量之间的关系。

       参考上述总体框架细节图6。

3.3.1 The Final Linear and Softmax Layer

图9 Linear and Softmax

 

3.4 Muti-Head Attention

       模型中使用的是multi-head-Attention。其实就是多个self-attention,可以把每个self-attention理解为一个head,多个self-attention自然就是多头了。在上一篇文章中我们已经提到了self-attention的计算,经过计算,一个self-attention会输出一个结果z。那么,multi-head-attention的输出是什么呢? 答案是把每一个self-attention的输出结果拼接起来。然后输入给后面的全连接网络。

       通过增加一种叫做“多头”注意力(“multi-headed” attention)的机制,论文进一步完善了自注意力层,并在两方面提高了注意力层的性能:

       1.它扩展了模型专注于不同位置的能力。在上面的例子中,虽然每个编码都在图4的z1中有或多或少的体现,但是它可能被实际的单词本身所支配。如果我们翻译一个句子,比如“The animal didn’t cross the street because it was too tired”,我们会想知道“it”指的是哪个词,这时模型的“多头”注意机制会起到作用。

       2.它给出了注意力层的多个“表示子空间”(representation subspaces)。接下来我们将看到,对于“多头”注意机制,我们有多个查询/键/值权重矩阵集(Transformer使用八个注意力头,因此我们对于每个编码器/解码器有八个矩阵集合)。这些集合中的每一个都是随机初始化的,在训练之后,每个集合都被用来将输入词嵌入(或来自较低编码器/解码器的向量)投影到不同的表示子空间中。

    在这里我们以h=8 举例说明。就是说不仅仅只初始化一组Q、K、V的矩阵,而是初始化多组,tranformer是使用了8组,所以最后得到的结果是8个矩阵。

图10 在每一个Attention Head中,为每一个token产生query,key和value

       在“多头”注意机制下,我们为每个头保持独立的查询/键/值权重矩阵,从而产生不同的查询/键/值矩阵。和之前一样,我们拿X乘以WQ/WK/WV矩阵来产生查询/键/值矩阵。

      如图10,X中的每一行,Q_0中的每一行,K_0中的每一行,V_0中的每一行都对应一个token(或者一个词)。

       如果我们做与上述相同的自注意力计算,只需八次不同的权重矩阵运算,我们就会得到八个不同的Z矩阵。

图11 Muti-Head 中计算Attention values

这给我们留下了一个小的挑战,前馈神经网络没法输入8个矩阵呀,这该怎么办呢?所以我们需要一种方式,把8个矩阵降为1个,首先,我们把8个矩阵连在一起,这样会得到一个大的矩阵,再随机初始化一个矩阵W^O(权值矩阵)和这个组合好的矩阵相乘,最后得到一个最终的矩阵。

图12 Multi-head-Attention的拼接过程

这就是multi-headed attention的全部流程了,这里其实已经有很多矩阵了,我们把所有的矩阵放到一张图内看一下总体的流程。

图13 Multi-Head 全部流程

 

这里附上pytorch版本的multi-head attention版本

class MultiHeadedAttention(nn.Module):
    def __init__(self, h, d_model, dropout=0.1):
        "Take in model size and number of heads."
        super(MultiHeadedAttention, self).__init__()
        assert d_model % h == 0
        # We assume d_v always equals d_k
        self.d_k = d_model // h
        self.h = h
        self.linears = clones(nn.Linear(d_model, d_model), 4)
        self.attn = None
        self.dropout = nn.Dropout(p=dropout)
        
    def forward(self, query, key, value, mask=None):
        "Implements Figure 2"
        if mask is not None:
            # Same mask applied to all h heads.
            mask = mask.unsqueeze(1)
        nbatches = query.size(0)
        
        # 1) Do all the linear projections in batch from d_model => h x d_k 
        query, key, value = \
            [l(x).view(nbatches, -1, self.h, self.d_k).transpose(1, 2)
             for l, x in zip(self.linears, (query, key, value))]
        
        # 2) Apply attention on all the projected vectors in batch. 
        x, self.attn = attention(query, key, value, mask=mask, 
                                 dropout=self.dropout)
        
        # 3) "Concat" using a view and apply a final linear. 
        x = x.transpose(1, 2).contiguous() \
             .view(nbatches, -1, self.h * self.d_k)
        return self.linears[-1](x)

以上的过程可以用文章中的图表示。

图14 对应文章中的流程

3.5 动图表示

个人表达能力有限,这里用博客 The Illustrated Transformer中动图来表示decoder阶段,输出第一个词和输出剩下词的过程。

图15 动图表示

3.6 详细解释Transformer输入和输出

3.6.1 Encoder输入输出

让我们从输入开始,再从头理一遍单个encoder这个过程:

  • 输入x    [batch_size, seq_length, embedding_size or 512]
  • x 做一个层归一化: x1 = norm(x)
  • 进入多头self-attention: x2 = self_attention(x1)       #   multi-headed attention (假设8个头)的每个头的Qi/Ki/Vi的参数矩阵WQi/WKi/WVi大小是[batch_size,embedding_size, embedding_size/8]    #x2: [batch_size, seq_length, embedding_size]
  • 残差加成:x3 = x + x2
  • 再做个层归一化:x4 = norm(x3)
  • 经过前馈网络: x5 = feed_forward(x4)
  • 残差加成: x6 = x3 + x5           #[batch_size, seq_length, embedding_size]
  • 输出x6                                         #[batch_size, seq_length, embedding_size]

注意,输入和输出的维度大小是一样的。

3.6.2 Decoder

注意,encoder的输出并没直接作为decoder的直接输入。

训练的时候,1.初始decoder的time step为1时(也就是第一次接收输入),其输入为一个特殊的token,可能是目标序列开始的token(如<BOS>),也可能是源序列结尾的token(如<EOS>),也可能是其它视任务而定的输入等等,不同源码中可能有微小的差异,其目标则是预测翻译后的第1个单词(token)是什么;2.然后<BOS>和预测出来的第1个单词一起,再次作为decoder的输入,得到第2个预测单词;3后续依此类推;

具体的例子如下:

样本:“我/爱/机器/学习”和 "i/ love /machine/ learning"

训练:
1. 把“我/爱/机器/学习”embedding后输入到encoder里去,最后一层的encoder最终输出的outputs [10, 512](假设我们采用的embedding长度为512,而且batch size = 1),此outputs 乘以新的参数矩阵,可以作为decoder里每一层用到的K和V;

2. 将<bos>作为decoder的初始输入,将decoder的最大概率输出词 A1和‘i’做cross entropy计算error。

3. 将<bos>,"i" 作为decoder的输入,将decoder的最大概率输出词 A2 和‘love’做cross entropy计算error。

4. 将<bos>,"i","love" 作为decoder的输入,将decoder的最大概率输出词A3和'machine' 做cross entropy计算error。

5. 将<bos>,"i","love ","machine" 作为decoder的输入,将decoder最大概率输出词A4和‘learning’做cross entropy计算error。

6. 将<bos>,"i","love ","machine","learning" 作为decoder的输入,将decoder最大概率输出词A5和终止符</s>做cross entropy计算error。

Sequence Mask

上述训练过程是挨个单词串行进行的,那么能不能并行进行呢,当然可以。可以看到上述单个句子训练时候,输入到 decoder的分别是

<bos>

<bos>,"i"

<bos>,"i","love"

<bos>,"i","love ","machine"

<bos>,"i","love ","machine","learning"

那么为何不将这些输入组成矩阵,进行输入呢?这些输入组成矩阵形式如下:

【<bos>

<bos>,"i"

<bos>,"i","love"

<bos>,"i","love ","machine"

<bos>,"i","love ","machine","learning" 】

怎么操作得到这个矩阵呢?

将decoder在上述2-6步次的输入补全为一个完整的句子

【<bos>,"i","love ","machine","learning"
<bos>,"i","love ","machine","learning"
<bos>,"i","love ","machine","learning"
<bos>,"i","love ","machine","learning"
<bos>,"i","love ","machine","learning"】

然后将上述矩阵矩阵乘以一个 mask矩阵

【1 0 0 0 0

1 1 0 0 0

1 1 1 0 0

1 1 1 1 0

1 1 1 1 1 】

这样是不是就得到了

【<bos>

<bos>,"i"

<bos>,"i","love"

<bos>,"i","love ","machine"

<bos>,"i","love ","machine","learning" 】

这样的矩阵了 。这就是我们需要输入矩阵。这个mask矩阵就是 sequence mask,其实它和encoder中的padding mask 异曲同工。

这样将这个矩阵输入到decoder(其实你可以想一下,此时这个矩阵是不是类似于批处理,矩阵的每行是一个样本,只是每行的样本长度不一样,每行输入后最终得到一个输出概率分布,作为矩阵输入的话,一下可以得到5个输出概率分布)。

这样我们就可以进行并行计算进行训练了。

测试

训练好模型, 测试的时候,比如用 '机器学习很有趣'当作测试样本,得到其英语翻译。

这一句经过encoder后得到输出tensor,送入到decoder(并不是当作decoder的直接输入):

1.然后用起始符<bos>当作decoder的 输入,得到输出 machine

2. 用<bos> + machine 当作输入得到输出 learning

3.用 <bos> + machine + learning 当作输入得到is

4.用<bos> + machine + learning + is 当作输入得到interesting

5.用<bos> + machine + learning + is + interesting 当作输入得到 结束符号<eos>

我们就得到了完整的翻译 'machine learning is interesting'

可以看到,在测试过程中,只能一个单词一个单词的进行输出,是串行进行的。

4. 扩充内容

4.1 Layer Normalization

在transformer中,每一个子层(self-attetion或者ffnn)之后都会接一个残缺模块,并且有一个Layer normalization,见图7。

残缺模块相信大家都很清楚了,这里不再讲解,主要讲解下Layer normalization。Normalization有很多种,但是它们都有一个共同的目的,那就是把输入转化成均值为0方差为1的数据。我们在把数据送入激活函数之前进行normalization(归一化),因为我们不希望输入数据落在激活函数的饱和区。

说到 normalization,那就肯定得提到 Batch Normalization。BN的主要思想就是:在每一层的每一批数据上进行归一化。我们可能会对输入数据进行归一化,但是经过该网络层的作用后,我们的数据已经不再是归一化的了。随着这种情况的发展,数据的偏差越来越大,我的反向传播需要考虑到这些大的偏差,这就迫使我们只能使用较小的学习率来防止梯度消失或者梯度爆炸。BN的具体做法就是对每一小批数据,在批这个方向上做归一化。如下图所示:

图16 Batch Normaliztion

可以看到,右半边求均值是沿着数据 batch_size的方向进行的,其计算公式如下:

 

BN(x_i)=\alpha \times \frac{x_i-\mu_b}{\sqrt{\sigma^2_B+ \varepsilon }}+ \beta

那么什么是 Layer normalization 呢?它也是归一化数据的一种方式,不过 LN 是在每一个样本上计算均值和方差,而不是BN那种在批方向计算均值和方差!

图17  Layer Normalization

下面看一下 LN 的公式:

LN(x_i)=\alpha \times \frac{x_i-\mu_L}{\sqrt{\sigma^2_L+ \varepsilon }}+ \beta

4.2 Mask

可以看到decoder部分其实和encoder部分大同小异,不过在最下面额外多了一个masked mutil-head attetion,这里的mask也是transformer一个很关键的技术,我们一起来看一下,见图1。

Mask 表示掩码,它对某些值进行掩盖,使其在参数更新时不产生效果。Transformer 模型里面涉及两种 mask,分别是 padding mask 和 sequence mask。其中,padding mask 在所有的 scaled dot-product attention 里面都需要用到,而 sequence mask 只有在 decoder 的 self-attention 里面用到。

 

4.2.1 Padding Mask

什么是 padding mask 呢?因为每个批次输入序列长度是不一样的。也就是说,我们要对输入序列进行对齐。具体来说,就是给在较短的序列后面填充 0。但是如果输入的序列太长,则是截取左边的内容,把多余的直接舍弃。因为这些填充的位置,其实是没什么意义的,所以我们的attention机制不应该把注意力放在这些位置上,所以我们需要进行一些处理。

具体的做法是,把这些位置的值加上一个非常大的负数(负无穷),这样的话,经过 softmax,这些位置的概率就会接近0!而我们的 padding mask 实际上是一个张量,每个值都是一个Boolean,值为 false 的地方就是我们要进行处理的地方

4.2.2 Sequence mask

文章前面也提到,sequence mask 是为了使得 decoder 不能看见未来的信息。也就是对于一个序列,在 time_step 为 t 的时刻,我们的解码输出应该只能依赖于 t 时刻之前的输出,而不能依赖 t 之后的输出。因此我们需要想一个办法,把 t 之后的信息给隐藏起来。

那么具体怎么做呢?也很简单:产生一个上三角矩阵,上三角的值全为0。把这个矩阵作用在每一个序列上,就可以达到我们的目的。

对于 decoder 的 self-attention,里面使用到的 scaled dot-product attention,同时需要padding mask 和 sequence mask 作为 attn_mask,具体实现就是两个mask相加作为attn_mask。

其他情况,attn_mask 一律等于 padding mask。

 

5. 总结

以上就是transformer结构的基本讲解。更深入细节的了解还是要继续研读论文和代码了。

 

参考文献

[1]Attention Is All You Need
[2]详解Transformer (Attention Is All You Need)
[3]The Illustrated Transformer
[4]Visualizing A Neural Machine Translation Model
[5]在NLP中广泛应用的transformer(Self-Attention)剖析笔记
[6]Attention is all you need模型笔记
[7]The Annotated Transformer

  • 17
    点赞
  • 76
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Transformer是一种用于处理序列数据的模型架构,它在自然语言处理领域广泛应用于任务如机器翻译、文本生成和情感分析等。它的核心思想是利用自注意力机制来捕捉输入序列中不同位置的依赖关系。 Transformer由多个相同层级的编码器和解码器组成。下面我将详细介绍Transformer结构: 1. 编码器(Encoder):编码器由N个相同的层级堆叠而成,每个层级都包含两个子层级,分别是多头自注意力机制(Multi-Head Self-Attention)和全连接前馈网络(Feed-Forward Neural Network)。 - 多头自注意力机制:自注意力机制能够计算序列中每个位置与其他位置的相关度,通过对输入序列进行注意力加权求和来获取每个位置的表示。多头表示模块则是并行地进行多次自注意力计算,以捕捉不同的语义信息。 - 全连接前馈网络:在每个位置的表示上应用两个全连接层,这个前馈神经网络可以帮助模型学习更复杂的非线性变换。 2. 解码器(Decoder):解码器也由N个相同的层级堆叠而成,它除了包括编码器中的两个子层级外,还有一个额外的子层级,即编码器-解码器注意力机制(Encoder-Decoder Attention)。解码器通过编码器-解码器注意力机制来获取输入序列的信息,以便更好地生成输出序列。 - 编码器-解码器注意力机制:通过计算输入序列和输出序列之间的相关度,为解码器提供输入序列的上下文信息,以便更好地进行生成。 除了以上主要的结构外,Transformer还引入了一些重要的概念和技术,包括残差连接(Residual Connections)、Layer Normalization(层归一化)和位置编码(Positional Encoding)等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值