当前位置:   article > 正文

图解transformer_transformer硬件实现第一篇:算法结构拆解

transformer硬件实现第一篇:算法结构拆解

前言

上一节中,讨论了基于RNN结合Attention的seq2seq模型,但这类模型的缺点在于:处理序列输入时,需要按照时间步循环处理输入序列,无法并行

与RNN这类神经网络结构相比,Transformer一个巨大的优点是:模型在处理序列输入时,可以对整个序列输入进行并行计算,不需要按照时间步循环递归处理输入序列。


图解Transformer

下图先便是Transformer整体结构图,Transformer模型结构中的左半部分为编码器(encoder),右半部分为解码器(decoder),下面我们来一步步拆解 Transformer
在这里插入图片描述

Transformer宏观结构

Transformer最开始用来解决机器翻译任务,因此可以看成seq2seq的一种,以机器翻译任务为例,先将Transformer这种特殊的seqseq模型看作一个黑盒,黑盒的输入是法语文本序列,输出是英语文本序列(将之前seq2seq中的编码器和解码器,从RNN模型替换成了Transformer模型)。

在这里插入图片描述
图:Transformer黑盒输入和输出

将上图中的中间部分“THE TRANSFORMER”拆开成seq2seq标准结构,得到下图:左边是编码部分encoders,右边是解码器部分decoders。

在这里插入图片描述
图:encoder-decoder

下面,再将上图中的编码器和解码器细节绘出,得到下图。我们可以看到:

  • 编码部分(encoders)由多层编码器(Encoder)组成(Transformer论文中使用的是6层编码器,这里的层数6并不是固定的,你也可以根据实验效果来修改层数)。
  • 解码部分(decoders)也是由多层的解码器(Decoder)组成(论文里也使用了6层解码器)。每层编码器网络结构是一样的,每层解码器网络结构也是一样的。不同层编码器和解码器网络结构有独立的参数。
    在这里插入图片描述
    图:6层编码和6层解码器

接下来,我们看一下单层encoder,单层encoder主要由以下两部分组成,如下图所示:

  • Self-Attention Layer
  • Feed Forward Neural Network(前馈神经网络,缩写为 FFNN)

编码器的执行流程:

  • 输入文本序列 w 1 , w 2 , . . . , w n w_1, w_2,...,w_n w1,w2,...,wn最开始需要经过embedding转换,得到每个单词的向量表示 x 1 , x 2 , . . . , x n x_1, x_2,...,x_n x1,x2,...,xn,其中 x i ∈ R d x_i \in \mathbb{R}^{d} xiRd是维度为 d d d的向量
  • 然后所有向量经过一个Self-Attention神经网络层进行变换和信息交互得到 h 1 , h 2 , . . . h n h_1, h_2,...h_n h1,h2,...hn,其中 h i ∈ R d h_i \in \mathbb{R}^{d} hiRd是维度为 d d d的向量。【做一个词的上下文信息交换】
  • Self-Attention层的输出会经过前馈神经网络得到新的 x 1 , x 2 , . . , x n x_1, x_2,..,x_n x1,x2,..,xn,依旧是 n n n个维度为 d d d的向量。这些向量将被送入下一层encoder,继续相同的操作。
    在这里插入图片描述
    图:单层decoder

与编码器对应,如下图,解码器在编码器的self-attention和FFNN中间插入了一个Encoder-Decoder Attention层,这个层帮助解码器聚焦于输入序列最相关的部分(类似于seq2seq模型中的 Attention)。

  • Encoder-Decoder Attention:当前解码器输入向量与前面编码器的K做注意力分数计算,然后加权到编码器的V
    在这里插入图片描述
    图:单层decoder

总结一下,我们基本了解了Transformer由编码部分和解码部分组成,而编码部分和解码部分又由多个网络结构相同的编码层和解码层组成。每个编码层由self-attention和FFNN组成,每个解码层由self-attention、FFNN和encoder-decoder attention组成。


Transformer结构细节

学习Transformer如何将文本输入序列转换为向量表示,又如何逐层处理这些向量表示得到最终的输出。

输入处理

机器学习模型处理的是向量,因此需要将文本输入序列转换为一个一个词向量。这一过程被称为 “词嵌入”,实际应用中一般将向量设置成256维或512维。
在Transformer中,由于使用了自注意力机制增加并行度,因此词与词之间的次序就丢失了,我们需要用一种编码来表示词在序列中的位置。

词向量

这里使用4维向量来讲解Transformer的转换过程。

如下图所示,假设我们的输入文本是序列包含了3个词,那么每个词可以通过词嵌入算法得到一个4维向量,于是整个输入被转化成为一个向量序列。

在实际应用中,我们通常会同时给模型输入多个句子,如果每个句子的长度不一样,我们会选择一个合适的长度,作为输入文本序列的最大长度:如果一个句子达不到这个长度,那么就填充先填充一个特殊的“padding”词;如果句子超出这个长度,则做截断。最大序列长度是一个超参数,通常希望越大越好,但是更长的序列往往会占用更大的训练显存/内存,因此需要在模型训练时候视情况进行决定。
在这里插入图片描述
图:3个词和对应的词向量

位置向量

如下图所示,Transformer模型对每个输入的词向量都加上了一个位置向量。这些向量有助于确定每个单词的位置特征,或者句子中不同单词之间的距离特征。词向量加上位置向量背后的直觉是:将这些表示位置的向量添加到词向量中,得到的新向量,可以为模型提供更多有意义的信息,比如词的位置,词之间的距离等。
在这里插入图片描述
图:位置编码向量

依旧假设词向量和位置向量的维度是4,我们在下图中展示一种可能的位置向量+词向量:

在这里插入图片描述
图:位置编码向量

那么,带有位置编码信息的向量遵循什么模式?原论文给出的公式为:
P E ( p o s , 2 i ) = s i n ( p o s / 1000 0 2 i / d model ) P E ( p o s , 2 i + 1 ) = c o s ( p o s / 1000 0 2 i / d model ) PE_{(pos,2i)} = sin(pos / 10000^{2i/d_{\text{model}}}) \\ PE_{(pos,2i+1)} = cos(pos / 10000^{2i/d_{\text{model}}}) PE(pos,2i)=sin(pos/100002i/dmodel)PE(pos,2i+1)=cos(pos/100002i/dmodel)

  • p o s pos pos代表词在序列中的位置
  • d m o d e l d_{model} dmodel代表位置向量的维度 【向量长度】
  • i ∈ [ 0 , d m o d e l ) i \in [0, d_{model}) i[0,dmodel)代表位置 d m o d e l d_{model} dmodel维位置向量第 i i i维 【向量中第几个元素】

根据上述公式,我们可以得到第 p o s pos pos位置的词的 d m o d e l d_{model} dmodel维位置向量。在下图中,我们画出了一种位置向量在第1维度到第8维度、不同位置的的数值大小。横坐标表示位置下标,纵坐标表示数值大小,每条线代表一个向量的维度
在这里插入图片描述
图:位置编码在0-20位置,在 [1,8] 维的数值图示

解释:

x轴每一个pos都代表一个词,可以看到,在[1,8]维的位置编码下,每个词都有对应的一个位置编码 [x,x,x,x,x,x,x,x],这些位置编码通过反函数可以帮助区分pos的位置关系。

这种方法的优点是:可以扩展到未知的序列长度。例如:当我们的模型需要翻译一个句子,而这个句子的长度大于训练集中所有句子的长度,这时,这种位置编码的方法也可以生成一样长的位置编码向量。

此外,三角函数还有一个 s i n ( A + B ) = s i n A c o s B + c o s A s i n B sin(A+B) = sinAcosB+cosAsinB sin(A+B)=sinAcosB+cosAsinB,里面蕴含一个相对的位置信息,比如

s i n ( p o s + k ) = s i n ( p o s ) c o s ( k ) + c o s ( p o s ) s i n ( k ) = s i n ( n e w _ p o s ) sin(pos+k)=sin(pos)cos(k)+cos(pos)sin(k) = sin(new\_pos) sin(pos+k)=sin(pos)cos(k)+cos(pos)sin(k)=sin(new_pos)

这个例子说明new_pos这个词与pos这个词相隔k个词的位置。

编码器encoder

输入序列经过词向量和位置向量编码后,得到一个向量序列输入到编码器encoder中。在Transformer中,编码器有6层结构,接下来介绍向量序列在单层encoder中的具体操作。

下图展示了向量序列在单层encoder中的流动:融合位置信息的词向量进入self-attention层,self-attention的输出每个位置的向量再输入FFNN神经网络得到每个位置的新向量。
在这里插入图片描述
图:单层encoder的序列向量流动

下面再看一个2个单词的例子:

在这里插入图片描述
图:2个单词的例子: x 1 , x 2 → z 1 , z 2 → r 1 , r 2 x_1, x_2 \to z_1, z_2 \to r_1, r_2 x1,x2z1,z2r1,r2

Self-Attention层

下面分析一下,self-Attention的具体机制。

Self-Attention概览

假设我们要翻译的句子是:

The animal didn't cross the street because it was too tired
  • 1

这个句子中的 it 是一个指代词,那么 it 指的是什么呢?它是指 animal 还是street?这个问题对人来说,是很简单的,但是对模型来说并不是那么容易。

但是,如果模型引入了Self Attention机制之后,便能够让模型把it和animal关联起来了。同样的,当模型处理句子中其他词时,Self Attention机制也可以使得模型不仅仅关注当前位置的词,还会关注句子中其他位置的相关的词,进而可以更好地理解当前位置的词。

与RNN对比:
RNN 在处理序列中的一个词时,会考虑句子前面的词传过来的hidden state,而hidden state就包含了前面的词的信息;
Self Attention机制指的是,当前词会直接关注到自己句子中前后相关的所有词语,意思是它不需要等待句子前面传过来的 hidden state,可以并行执行。
如下图 it的例子:
在这里插入图片描述
图:一个词和其他词的attention

上图所示的it是一个真实的例子,是当Transformer在第5层编码器编码“it”时的状态,可视化之后显示it有一部分注意力集中在了“The animal”上,并且把这两个词的信息融合到了"it"中。

Self-Attention细节

通过一个简单的例子来理解一下:什么是“self-attention自注意力机制”?

假设一句话包含两个单词:Thinking Machines。自注意力的一种理解是:Thinking-Thinking,Thinking-Machines,Machines-Thinking,Machines-Machines,共 2 2 2^2 22种两两做attention计算。那么具体如何计算呢?假设Thinking、Machines这两个单词经过词向量算法得到向量是 X 1 , X 2 X_1, X_2 X1,X2​:

第一步:词向量线性变换:

  • Query向量: q 1 , q 2 q_1, q_2 q1,q2
  • Key向量: k 1 , k 2 k_1, k_2 k1,k2
  • Value向量: v 1 , v 2 v_1, v_2 v1,v2

线性投影层 L i n e a r q 1 = X 1 W Q , q 2 = X 2 W Q ; k 1 = X 1 W K , k 2 = X 2 W K ; v 1 = X 1 W V , v 2 = X 2 W V , W Q , W K , W K ∈ R d x × d k 线性投影层Linear\\ q_1 = X_1 W^Q, q_2 = X_2 W^Q; \\ k_1 = X_1 W^K, k_2 = X_2 W^K;\\ v_1 = X_1 W^V, v_2 = X_2 W^V,\\ W^Q, W^K, W^K \in \mathbb{R}^{d_x \times d_k}\\ 线性投影层Linearq1=X1WQ,q2=X2WQ;k1=X1WK,k2=X2WK;v1=X1WV,v2=X2WV,WQ,WK,WKRdx×dk

这3个向量是词向量分别和3个参数矩阵相乘得到的,而这个矩阵也是是模型要学习的参数,也就是说该线性层也是可学习的。
在这里插入图片描述
图:计算Query向量: q 1 , q 2 q_1, q_2 q1,q2,Key向量: k 1 , k 2 k_1, k_2 k1,k2,Value向量: v 1 , v 2 v_1, v_2 v1,v2

Query 向量,Key 向量,Value 向量是什么含义呢?
attention计算的逻辑常常可以描述为:query和key计算的相关性叫attention得分,然后根据attention得分对value进行加权求和。

第二步:计算Attention Score(注意力分数)

假设我们现在计算第一个词Thinking 的Attention Score(注意力分数),需要根据Thinking 对应的词向量,对句子中的其他词向量都计算一个分数。这些分数决定了我们在编码Thinking这个词时,对句子中其他位置词向量的权重。

Attention score是根据"Thinking" 对应的 Query 向量和其他位置的每个词的 Key 向量进行点积得到的。

Thinking的第一个Attention Score就是 q 1 q_1 q1 k 1 k_1 k1的内积,第二个分数就是 q 1 q_1 q1 k 2 k_2 k2的点积。

这个计算过程在下图中进行了展示,下图里的具体得分数据是为了表达方便而自定义的。
在这里插入图片描述
图:Thinking的Attention Score计算

第三步:稳定数值

A t t e n t i o n 得分计算 s c o r e 11 = q 1 ⋅ k 1 d k , s c o r e 12 = q 1 ⋅ k 2 d k ; s c o r e 21 = q 2 ⋅ k 1 d k , s c o r e 22 = q 2 ⋅ k 2 d k ; Attention得分计算\\ score_{11} = \frac{q_1 \cdot k_1}{\sqrt{d_k}} , score_{12} = \frac{q_1 \cdot k_2}{\sqrt{d_k}} ; score_{21} = \frac{q_2 \cdot k_1}{\sqrt{d_k}}, score_{22} = \frac{q_2 \cdot k_2}{\sqrt{d_k}}; \\ Attention得分计算score11=dk q1k1,score12=dk q1k2;score21=dk q2k1,score22=dk q2k2;

把每个分数除以 d k \sqrt{d_k} dk d k d_{k} dk是Key向量的维度。
也可以除以其他数,除以一个数是为了在反向传播时,使得值与值之间不会相差很大,在求梯度时更加稳定。

第四步:Softmax归一化

加权平均,得到权重 s c o r e 11 = e s c o r e 11 e s c o r e 11 + e s c o r e 12 , s c o r e 12 = e s c o r e 12 e s c o r e 11 + e s c o r e 12 ; s c o r e 21 = e s c o r e 21 e s c o r e 21 + e s c o r e 22 , s c o r e 22 = e s c o r e 22 e s c o r e 21 + e s c o r e 22 加权平均,得到权重\\ score_{11} = \frac{e^{score_{11}}}{e^{score_{11}} + e^{score_{12}}}, score_{12} = \frac{e^{score_{12}}}{e^{score_{11}} + e^{score_{12}}}; \\ score_{21} = \frac{e^{score_{21}}}{e^{score_{21}} + e^{score_{22}}}, score_{22} = \frac{e^{score_{22}}}{e^{score_{21}} + e^{score_{22}}} \\ 加权平均,得到权重score11=escore11+escore12escore11,score12=escore11+escore12escore12;score21=escore21+escore22escore21,score22=escore21+escore22escore22

接着把这些分数经过一个Softmax函数,Softmax可以将分数归一化,这样使得分数都是正数并且加起来等于1, 如下图所示。
这些分数决定了Thinking词向量,对其他所有位置的词向量分别有多少的注意力
在这里插入图片描述

图:Thinking的Attention Score计算

第五步:分配权重

得到每个词向量的分数后,将分数分别与对应的Value向量相乘。

这种做法背后的直觉理解就是:对于分数高的位置,相乘后的值就越大,我们把更多的注意力放到了它们身上;对于分数低的位置,相乘后的值就越小,这些位置的词可能是相关性不大的

第六步:求和得到结果

给单词分配权重,实现注意力 z 1 = v 1 × s c o r e 11 + v 2 × s c o r e 12 ; z 2 = v 1 × s c o r e 21 + v 2 × s c o r e 22 给单词分配权重,实现注意力\\ z_1 = v_1 \times score_{11} + v_2 \times score_{12}; \\z_2 = v_1 \times score_{21} + v_2 \times score_{22} 给单词分配权重,实现注意力z1=v1×score11+v2×score12;z2=v1×score21+v2×score22

把第5步得到的Value向量相加,就得到了Self Attention在当前位置(这里的例子是第1个位置)对应的输出 z 1 z_1 z1

最后,在下图展示了 对第一个位置词向量计算Self Attention 的全过程。最终得到的当前位置(这里的例子是第一个位置)词向量会继续输入到前馈神经网络。注意:上面的6个步骤每次只能计算一个位置的输出向量,在实际的代码实现中,Self Attention的计算过程是使用矩阵快速计算的,一次就得到所有位置的输出向量。

在这里插入图片描述
图:第一个位置的词Thinking经过attention之后的向量表示 z 1 z_1 z1

同理,通过 q 2 q_2 q2 k 1 , k 2 k_1,k_2 k1,k2相乘然后除以 d k \sqrt{d_k} dk ,最后经过softmax归一化得到权重,乘上 v 1 , v 2 v_1,v_2 v1,v2后相加得到的 z 2 z_2 z2就是第二个位置的词Machine的向量表示。

Self-Attention 矩阵计算

为了加快计算速度,我们希望利用矩阵一次计算来实现上面的self-Attention计算过程。将self-attention计算6个步骤中的向量放一起,比如 X = [ x 1 ; x 2 ] X=[x_1;x_2] X=[x1;x2]​,便可以进行矩阵计算啦。下面,依旧按步骤展示self-attention的矩阵计算方法:

X = [ x 1 ; x 2 ] Q = X W Q , K = X W K , V = X W V Z = s o f t m a x ( Q K T d k ) V X = [ X 1 ; X 2 ] Q = X W Q , K = X W K , V = X W V Z = s o f t m a x ( Q K T d k ) V X = [x_1;x_2] \\ Q = X W^Q, K = X W^K, V=X W^V \\ Z = softmax(\frac{QK^T}{\sqrt{d_k}}) VX = [X_1;X_2] \\ Q = X W^Q, K = X W^K, V=X W^V \\ Z = softmax(\frac{QK^T}{\sqrt{d_k}}) V X=[x1;x2]Q=XWQ,K=XWK,V=XWVZ=softmax(dk QKT)VX=[X1;X2]Q=XWQ,K=XWK,V=XWVZ=softmax(dk QKT)V

第一步:计算Q、K、V矩阵

矩阵X中的每一行,表示句子中的每一个词的词向量。

首先将所有的词向量放入矩阵X中,然后分别和3个权重矩阵W^K W^V$ 相乘,得到 Q,K,V 矩阵。

Q,K,V 矩阵中的每一行表示每个词对应的 Query向量,Key向量,Value 向量,向量维度是 d k d_k dk
在这里插入图片描述
图:QKV矩阵乘法

第二步:执行矩阵计算,直接得到 Self Attention 的输出

Q矩阵和KT矩阵相乘后,因为Q矩阵的每一行就是一个词(设为 w o r d i word_i wordi)的query向量,KT的每一列也是一个词的key向量,因此他们相乘后得到的 中间矩阵temp的每一行就是 w o r d i word_i wordi对每一个词的注意力分数,然后通过softmax得到权重比例,一行中的第i个元素就是对第i个词的一个权重。然后用该权重乘以V矩阵,由于矩阵乘法有求和的特性,因此最后结果矩阵Z一行就是一个V的缩放。【对历史信息的加权】

在这里插入图片描述图:得到输出 Z Z Z

多头注意力机制

Transformer通过增加多头注意力机制,来进一步完善Self-Attention,这种机制的好处是:

  • 它扩展了模型关注不同位置的能力。在上面的例子中,第一个位置的输出 z 1 z_1 z1​包含了句子中其他每个位置的很小一部分信息,但 z 1 z_1 z1​仅仅是单个向量,所以可能仅由某个位置的信息主导了。而当我们翻译句子:The animal didn’t cross the street because it was too tired时,我们不仅希望模型关注到"it"本身,还希望模型关注到"The"和“animal”,甚至关注到"tired"。意思是 z 1 z_1 z1注意力可能放在animal上,可是tired、The这些单词也与"it"是有很大关联的,因此我们希望有 z 2 、 z 3 z_2、z_3 z2z3等能关注到The、tired等单词。 【这里 z 1 、 z 2 、 z 3 z_1、z_2、z_3 z1z2z3是指多层Self-Attention上同一个词的结果,而不单层Self-Attention上多个词的结果】
  • 多头注意力机制赋予attention层多个“子表示空间”。下面我们会看到,多头注意力机制会有多组 W Q , W K W V W^Q, W^K W^V WQ,WKWV​ 的权重矩阵(在 Transformer 的论文中,使用了 8 组注意力),,因此可以将 X X X​变换到更多种子空间进行表示。接下来我们也使用8组注意力头(attention heads))。每一组注意力的权重矩阵都是随机初始化的,但经过训练之后,每一组注意力的权重 W Q , W K W V W^Q, W^K W^V WQ,WKWV​ 可以把输入的向量映射到一个对应的”子表示空间“。

在这里插入图片描述
图:多头注意力机制

在多头注意力机制中,我们为每组注意力设定单独的 W Q , W K , W V W^Q, W^K, W^V WQ,WK,WV 参数矩阵。将输入X和每组注意力的 W Q , W K , W V W^Q, W^K, W^V WQ,WK,WV 相乘,得到8组 Q, K, V 矩阵。

接着,我们把每组 K, Q, V 计算得到每组的 Z 矩阵,就得到8个Z矩阵。
在这里插入图片描述
图:8 个 Z 矩阵

由于前馈神经网络层FFNN接收的是 1 个矩阵(其中每行的向量表示一个词),而不是 8 个矩阵,所以我们直接把8个子矩阵拼接起来得到一个大的矩阵,然后和另一个权重矩阵 W O W^O WO相乘做一次变换,映射到前馈神经网络层所需要的维度。
在这里插入图片描述
图:拼接8个子矩阵并进行线性变换

总结一下就是:

  1. 把8个矩阵 Z 0 , Z 1 . . . , Z 7 {Z_0,Z_1...,Z_7} Z0,Z1...,Z7拼接起来
  2. 把拼接后的矩阵和 W O W^O WO权重矩阵相乘
  3. 得到最终的矩阵Z,这个矩阵包含了所有 attention heads(注意力头) 的信息。这个矩阵会输入到FFNN (Feed Forward Neural Network)层。

以上就是多头注意力的全部内容。最后将所有内容放到一张图中:

在这里插入图片描述
图:多头注意力机制的矩阵运算

学习了多头注意力机制,让我们再来看下当我们前面提到的it例子,不同的attention heads (注意力头)对应的“it”attention了哪些内容。下图中的绿色和橙色线条分别表示2组不同的attention heads
在这里插入图片描述
图:it的attention

当我们编码单词"it"时,其中一个 attention head (橙色注意力头)最关注的是"the animal",另外一个绿色 attention head 关注的是"tired"。因此在某种意义上,"it"在模型中的表示,融合了"animal"和"tire"的部分表达。

Attention代码实例

下面的代码实现中,张量的第1维是 batch_size,第 2 维是句子长度。代码中进行了详细注释和说明。

class MutliHeadAttention(nn.Module):
	'''
		n_heads:多头注意力的数量
		hid_dim:每个词向量输出的向量维度 [也就是X经过线性层后缩放的维度]
	'''
	def __init__(self, hid_dim, n_heads, dropout):
        super(MultiheadAttention, self).__init__()
        self.hid_dim = hid_dim
        self.n_heads = n_heads

        # 强制规定 hid_dim 必须整除 h [向量维度必须可以整除头数]
        assert hid_dim % n_heads == 0
        # 定义 W_q 矩阵
        self.w_q = nn.Linear(hid_dim, hid_dim)
        # 定义 W_k 矩阵
        self.w_k = nn.Linear(hid_dim, hid_dim)
        # 定义 W_v 矩阵
        self.w_v = nn.Linear(hid_dim, hid_dim)
		# 全连接层 FFNN
        self.fc = nn.Linear(hid_dim, hid_dim)
        # 丢弃层,有dropout的概率丢弃输出值 [置为0]
        self.do = nn.Dropout(dropout)
        # 缩放分母:sqrt(d_k),多头下就是每组自注意力里面的向量长度(hid_dim // n_heads)的开方
        self.scale = torch.sqrt(torch.FloatTensor([hid_dim // n_heads]))

    def forward(self, query, key, value, mask=None):
		'''
	       注意 Q,K,V的在句子长度这一个维度的数值可以一样,可以不一样。
	       K: [64,10,300], 假设batch_size 为 64,有 10 个词,每个词的 Query 向量是 300 维
	       V: [64,10,300], 假设batch_size 为 64,有 10 个词,每个词的 Query 向量是 300 维
	       Q: [64,12,300], 假设batch_size 为 64,有 12 个词,每个词的 Query 向量是 300 维
		'''
        bsz = query.shape[0]
        Q = self.w_q(query)
        K = self.w_k(key)
        V = self.w_v(value)

		'''
		这里把K Q V矩阵拆分为多组注意力
		最后一维就是是用 self.hid_dim // self.n_heads 来得到的,表示每组注意力的向量长度, 每个 head 的向量长度是:300/6=50
		这里:64表示 batch_size,6表示 6组自注意力,10表示 有10个词,50表示 每组注意力的词的向量长度
		********************************** 注意 **********************************
		* 多头注意力的切割是把一个词的向量分割为多组,以此希望每组自注意力能获取不同的信息  *
		* ********************************************************************** *

		K: [64,10,300] 拆分多组注意力 -> [64,10,6,50] 转置得到 -> [64,6,10,50]
        V: [64,10,300] 拆分多组注意力 -> [64,10,6,50] 转置得到 -> [64,6,10,50]
        Q: [64,12,300] 拆分多组注意力 -> [64,12,6,50] 转置得到 -> [64,6,12,50]
        转置是为了把注意力的数量 6 放到前面,把 10 和 50 放到后面,方便下面计算:*** 变成了 10x50 这样的矩阵***
		'''
		Q = Q.view(bsz, -1, self.n_heads, self.hid_dim //
                   self.n_heads).permute(0, 2, 1, 3)
        K = K.view(bsz, -1, self.n_heads, self.hid_dim //
                   self.n_heads).permute(0, 2, 1, 3)
        V = V.view(bsz, -1, self.n_heads, self.hid_dim //
                   self.n_heads).permute(0, 2, 1, 3)

        # 第 1 步:Q 乘以 K的转置,除以scale
        # [64,6,12,50] * [64,6,50,10] = [64,6,12,10]
        # attention:[64,6,12,10]
        attention = torch.matmul(Q, K.permute(0, 1, 3, 2)) / self.scale
        
		# 如果 mask 不为空,那么就把 mask 为 0 的位置的 attention 分数设置为 -1e10,
		# 这里用“0”来指示哪些位置的词向量不能被attention,比如padding位置,当然也可以用“1”或者其他数字来指示,
		# 主要设计下面2行代码的改动。
        if mask is not None:
            attention = attention.masked_fill(mask == 0, -1e10)

        # 第 2 步:计算上一步结果的 softmax,再经过 dropout,得到 attention。
        # 注意,这里是对最后一维做 softmax,也就是在输入序列的维度做 softmax
        # attention: [64,6,12,10]
        attention = self.do(torch.softmax(attention, dim=-1))

        # 第三步,attention结果与V相乘,得到多头注意力的结果
        # [64,6,12,10] * [64,6,10,50] = [64,6,12,50]
        # x: [64,6,12,50]
        x = torch.matmul(attention, V)

        # 因为 query 有 12 个词,所以把 12 放到前面,把 50 和 6 放到后面,方便下面拼接多组的结果
        # x: [64,6,12,50] 转置-> [64,12,6,50]
        x = x.permute(0, 2, 1, 3).contiguous()
        # 这里的矩阵转换就是:把多组注意力的结果拼接起来
        # 最终结果就是 [64,12,300]
        # x: [64,12,6,50] -> [64,12,300],可以看到,X的形状经过前向传播后没有改变
        x = x.view(bsz, -1, self.n_heads * (self.hid_dim // self.n_heads))
        
        # 经过FFNN输出结果
        x = self.fc(x)
        return x


'''
*********** 
*   测试   *
***********
'''
# batch_size 为 64,有 12 个词,每个词的 Query 向量是 300 维
query = torch.rand(64, 12, 300)
# batch_size 为 64,有 12 个词,每个词的 Key 向量是 300 维
key = torch.rand(64, 10, 300)
# batch_size 为 64,有 10 个词,每个词的 Value 向量是 300 维
value = torch.rand(64, 10, 300)
attention = MultiheadAttention(hid_dim=300, n_heads=6, dropout=0.1)
output = attention(query, key, value)
## output: torch.Size([64, 12, 300])
print(output.shape)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
残差连接 & 层归一化

到目前为止,我们计算得到了self-attention的输出向量。而单层encoder里后续还有两个重要的操作:残差连接、层归一化。

编码器的每个子层(Self Attention 层和 FFNN)都有一个残差连接和层归一化(layer-normalization),如下图所示:
在这里插入图片描述
图:残差连接

残差连接就是将原输入矩阵X加到输出矩阵Z上,一起输出

将 Self-Attention 层的层标准化(layer-normalization)和涉及的向量计算细节都进行可视化,如下所示:
在这里插入图片描述
图:归一化细节

层归一化:就是把一个向量中所有的取值映射到[0,1]中,并且所有取值的求和等于1

编码器和和解码器的子层里面都有层归一化(layer-normalization)。假设一个 Transformer 是由 2 层编码器和两层解码器组成的,将全部内部细节展示起来如下图所示
在这里插入图片描述
图:2层Transformer示意图

解码器decoder

现在我们已经介绍了编码器中的大部分概念,我们也基本知道了编码器的原理。现在让我们来看下, 编码器和解码器是如何协同工作的。

编码器一般有多层,第一个编码器的输入是一个序列文本,最后一个编码器输出是一组序列向量,这组序列向量会作为解码器的K、V输入,其中K=V=解码器输出的序列向量表示。 【类似于RNN传递的隐藏状态hidden state到解码器】
这些注意力向量将会输入到每个解码器的Encoder-Decoder Attention层,这有助于解码器把注意力集中到输入序列的合适位置,如下图所示。
在这里插入图片描述

解码(decoding )阶段的每一个时间步都输出一个翻译后的单词(这里的例子是英语翻译),解码器当前时间步的输出又重新作为输入Q和编码器的输出K、V共同作为下一个时间步解码器的输入。然后重复这个过程,直到输出一个结束符。如下图所示:
在这里插入图片描述
动态图:decoder动态图

这里解码器的输入:之前时刻的输出编码器的K、V矩阵(输入序列的信息)

解码器中的 Self Attention 层,和编码器中的 Self Attention 层的区别:

  1. 在解码器里,Self Attention 层只允许关注到输出序列中早于当前位置之前的单词。具体做法是:在 Self Attention 分数经过 Softmax 层之前,屏蔽当前位置之后的那些位置(将attention score设置成-inf)。 【在送进去的时候,后面位置的词的向量做出来的注意力得分设为-inf】
  2. 解码器 Attention层是使用前一层的输出来构造 Query 矩阵,而 Key 矩阵和 Value 矩阵来自于编码器最终的输出。【也就是查询矩阵是解码器前一层是输出,而非输入序列的词嵌入向量】

线性层和softmax

Decoder 最终的输出是一个向量,其中每个元素是浮点数。我们怎么把这个向量转换为单词呢?这是线性层和softmax完成的。

线性层就是一个普通的全连接神经网络,可以把解码器输出的向量,映射到一个更大的向量,这个向量称为 logits 向量:假设我们的模型有 10000 个英语单词(模型的输出词汇表),此 logits 向量便会有 10000 个数字,每个数表示一个单词的分数。

然后,Softmax 层会把这些分数转换为概率(把所有的分数转换为正数,并且加起来等于 1)。然后选择最高概率的那个数字对应的词,就是这个时间步的输出单词。
在这里插入图片描述
图:线性层

损失函数

Transformer训练的时候,需要将解码器的输出和label一同送入损失函数,以获得loss,最终模型根据loss进行方向传播。这一小节,我们用一个简单的例子来说明训练过程的loss计算:把“merci”翻译为“thanks”。

我们希望模型解码器最终输出的概率分布,会指向单词 ”thanks“(在“thanks”这个词的概率最高)。但是,一开始模型还没训练好,它输出的概率分布可能和我们希望的概率分布相差甚远,如下图所示,正确的概率分布应该是“thanks”单词的概率最大。但是,由于模型的参数都是随机初始化的,所示一开始模型预测所有词的概率几乎都是随机的。

在这里插入图片描述
图:概率分布

只要Transformer解码器预测了组概率,我们就可以把这组概率和正确的输出概率做对比,然后使用反向传播来调整模型的权重,使得输出的概率分布更加接近整数输出。

那我们要怎么比较两个概率分布呢?:我们可以简单的用两组概率向量的的空间距离作为loss(向量相减,然后求平方和,再开方),当然也可以使用交叉熵(cross-entropy)和KL 散度(Kullback–Leibler divergence)。

由于上面仅有一个单词的例子太简单了,我们可以再看一个复杂一点的句子。句子输入是:“je suis étudiant” ,输出是:“i am a student”。这意味着,我们的transformer模型解码器要多次输出概率分布向量:

  • 每次输出的概率分布都是一个向量,长度是 vocab_size(前面约定最大vocab size,也就是向量长度是 6,但实际中的vocab size更可能是 30000 或者 50000)
  • 第1次输出的概率分布中,最高概率对应的单词是 “i”
  • 第2次输出的概率分布中,最高概率对应的单词是 “am”
  • 以此类推,直到第 5 个概率分布中,最高概率对应的单词是 “<eos>”,表示没有下一个单词了

于是我们目标的概率分布长下面这个样子:
在这里插入图片描述
图:目标概率分布

我们用例子中的句子训练模型,希望产生图中所示的概率分布
我们的模型在一个足够大的数据集上,经过足够长时间的训练后,希望输出的概率分布如下图所示:
在这里插入图片描述
图:模型训练后输出的多个概率分布

我们希望模型经过训练之后可以输出的概率分布也就对应了正确的翻译。当然,如果你要翻译的句子是训练集中的一部分,那输出的结果并不能说明什么。我们希望模型在没见过的句子上也能够准确翻译。【训练集和验证集的区别、要求泛化能力】

额外提一下greedy decoding和beam search的概念:

  • Greedy decoding:由于模型每个时间步只产生一个输出,我们这样看待:模型是从概率分布中选择概率最大的词,并且丢弃其他词。这种方法叫做贪婪解码(greedy decoding)。
  • Beam search:每个时间步保留k个最高概率的输出词,然后在下一个时间步,根据上一个时间步保留的k个词来确定当前应该保留哪k个词。假设k=2,第一个位置概率最高的两个输出的词是”I“和”a“,这两个词都保留,然后根据第一个词计算第2个位置的词的概率分布,再取出第2个位置上2个概率最高的词。对于第3个位置和第4个位置,我们也重复这个过程。这种方法称为集束搜索(beam search)。

贪婪算法,虽然能保证每一步是最优解(每一步选取最大概率),但是不能保证得到的结果是全局最优。(由于每一步的输出结果都会重新输入到解码器,因此后面词的概率是收到前面的输出结果影响的而非静态的概率)

然而,希望找到全局最优解,穷举法是可以找到的但是开销是最大的。

因此 集束搜索 是贪婪算法和穷举法的一种折中。


总结

本文介绍了Transformer模型,他是基于编码器-解码器结构的用于解决seq2seq的一种架构。

  • 它结合了注意力机制,使得模型并行计算能力增强;
  • 它结合了多头注意力机制,使得模型能够关注到更多方面的信息;
  • 由于并行计算,NLP中词的语序也很重要,因此引入了位置向量对位置进行编码;
  • 在训练解码器的过程中,由于数据集数据的标号(在翻译任务中是翻译结果),也要输入到解码器中,为了防止模型“偷看答案”,引入了一种带掩码的自注意力机制,(也就是将当前输入往后的输入的注意力分数设为-inf),以此提高模型的能力;
  • 在编解码器内部使用了残差连接和层归一化,使得模型可以训练得更深、避免梯度爆炸;
    损失函数采用 交叉熵 或者 KL散度 来衡量两个分布之间的相似性,每次的翻译结果都是一个词汇表的概率;
本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/373230
推荐阅读
相关标签
  

闽ICP备14008679号