你真的了解Transformer了吗1. 背景为什么要看这篇文章呢LZ最近在接触NLP的相关算法按照最近大火的BERT来看Transformer其实就是这个算法的一个核心所以本着先把论文读厚的过程还是得把这块详细的理解清楚除了知识的输入阶段还要再强行输出一遍这样才能比较好的掌握对应的知识。主要参考http://nlp.seas.harvard.edu/2018/04/03/attention.htmlhttps://jalammar.github.io/illustrated-transformer/原始PaperAttention is All You Need2. 理论知识2.1 宏观角度首先我们可以把一个模型看成是一个黑箱子的状态那么在一个机器翻译应用中这个黑箱子的输入是一种语言的句子然后经过这个黑箱子的处理用另外一种语言输出对应的翻译。那么具体示例如下直接扩展一下在我们通常使用的例如goole翻译百度翻译就是这种类似的技术我们揭开这个黑箱子来看下这个黑箱子下的结构可以从下图中看到两部分一个是编码部分另一个是解码部分并且编码后的信息会传递给解码部分。我们再进一步看一下编码部分和解码部分的内部情况吧。由下图可以看出编码部分是由一系列编码器堆叠而成论文中显示的使用六个编码器但实际上在BERT中Base版本使用了12个编码器在large版本中使用了24个编码器与编码器相对应的也存在相同数量的解码器通常情况下编码器的数量和解码器的数量是一致的那么每个编码器中是什么结构呢实际上每个编码器的结构都是一致的但是他们不是权值共享的每个编码器又可以分成两个子层自注意力层和前向神经网络这两部分。编码器的输入首先会流经自注意力层这一层是帮助编码器在编码特定单词时可以和句子中其他的词进行联系后续文章中会详细说明。自注意力层的输出会通过前馈神经网络完全相同的前馈神经网络独立应用在每个位置。解码器也包含这两层但是他们之间是一个注意力层帮助解码器专注于输入句子的相关部分类似于注意力在Seq2seq模型中的作用用图像来表示张量目前为止我们已经看到了模型的主要部分现在我们来看看各种向量/张量看一下我们的输入是怎么经过对应的模块来得到模型的输出结果的。与 NLP 应用程序中的一般情况一样我们首先使用以下方法将每个输入词转换为向量如果像了解词向量这方面的信息可以看一下这篇论文这里lz可以简单介绍一下什么是word embedding以中文为例小时候我们都用过新华字典通过对应的字我们可以找到对应字的解释近义词反义词常用句子等word embedding就可以看作是另外一种新华字典对应每个字都有对应维度的向量0.1 , 0.00003 , − 0.00001... 0.1, 0.00003, -0.00001 ...0.1,0.00003,−0.00001...类似这种编码信息而这些数字代表什么呢其实也可以看作是这个字的近义词反义词特征常用句子等一些原来用文字进行表示的信息转换成了连续的数组编码。这样其实在高维空间中意思相近的词或者字可能会距离比较近而意思不相干的词或者字的embedding距离会比较远一些。这个只是一个比较通俗的理解啦同样按照上述的情况我们会有一个Embedding字典通过字典查询到对应的embedding如果没有对应的字就是UNKNOWN,一般可能就是随机初始化一个符合高斯分布的embedding。embedding一般都是在最底层也就是最开始的编码器中使用这点其实是可以类比图像的一般比较原始的图像都是输入底层的网络来进行特征的提取的比较抽象的来讲所有的编码其都是接受一个向量列表每个向量的大小都是512维唯一的区别可能是底部的编码器的输入是我们刚刚提到的词向量而后续的编码器就是就是接收前一个编码器的输出。其中我们提到了列表的大小是怎么确定的呢训练过图像的小伙伴应该都知道在训练的时候我们一个batch一个batch的进行训练的那么输入的图像的尺寸都是一致的这样才能concat到一起。那么对于不一样长的句子我们要怎么处理呢首先我们会设置一个输入的最大长度如果超过这个长度的文本会进行截断当然也可以有其他操作例如滑动窗口之类的方式其他的短的句子会按照最长的句子的部分进行pad当然如果最长的句子长度是100那么短句子pad到100就可以了。又跑远了快回来我们还是继续来看transformer里面的encoder部分下图展示的是最底层的encoder的状态可以看到输入是word embedding每个embedding经过自注意力层会产生对应的z1向量然后再传入到feed forward层在上图中我们可以看到Transformer的一个关键属性即每个位置的单词在编码其中都流经其对应的路径在自注意力层中这个路径之间会存在相互依赖的关系在后续会介绍词与词之间到底是怎么联系的。然而从图中我们还是可以看到前馈层并没有这些依赖关系因此各种路径可以在流经前馈层时并行执行。接下来我们将使用更短的句子来仔细的看下编码器的每个子层中发生的情况2.2 编码器的内部正如我们已经提到的编码器接收一个向量列表作为输入。 它通过将这些向量传递到一个自注意力层然后传递到一个前馈神经网络最后将输出传送到下一个编码器来处理这个列表。每个位置的单词都经过一个自注意力层然后再经过一个前馈神经网络2.3 宏观上来看自注意力哈哈原文中说“Don’t be fooled by me throwing around the word “self-attention” like it’s a concept everyone should be familiar with.”其实好像之前CV中都会说是注意力机制但和这里的自注意力机制还是不同的小伙伴们要注意区分哈。这不到这篇文章我们就可以好好理解一下自注意力机制了假设我们有下面的句子作为输入然后我们想得到下面输入句子的翻译“The animal didn’t cross the street because it was too tired”皮一下我们来看下goole翻译怎么说呢感觉百度的好像更加准确一些因为增加了对动物的单复数区分实际上对于算法来说中间有一个问题是“it”到底指动物还是指街道呢对于小伙伴来说肯定是一个非常简单的问题但是对于算法也许就不那么简单了并且从位置上来说可以看到“it”实际上距离“animal”会更加远一些。实际上当模型处理“it”这个词时self-attention 会把“it”和“animal”联系起来。当模型处理每个单词输入序列中的每个位置时自注意力会让它查看输入序列中的其他位置来寻找有助于对该单词进行更好编码的线索。相信了解过RNN的小伙伴都知道RNN是如何把之前的词的信息传递给之后的单词或者换句话说就是增加词与词之间联系可以RNN一个比较大的缺点就是词的位置如果相差很远的词与词之间的关系就会被弱化这也是RNN面对长句的一个弊端。那么自注意力其实就是Transformer用来理解我们当前处理词和其他词之间的联系。小伙伴如果想自己玩一下这个功能可以在这个链接上自己尝试下https://colab.research.google.com/github/tensorflow/tensor2tensor/blob/master/tensor2tensor/notebooks/hello_t2t.ipynb#scrollToOJKU36QAfqOC2.4 仔细看看到底什么是自注意力敲黑板啦 重头戏来了让我们首先看看如何使用向量计算自注意力然后继续看看它是如何实际实现的——使用矩阵。计算自注意力的第一步是从每个编码器的输入向量在本例中为每个词的嵌入创建三个向量。 因此对于每个单词我们创建一个 Query 向量、一个 Key 向量和一个 Value 向量。 这些向量是通过将嵌入乘以我们在训练过程中训练的三个矩阵来创建的。请注意这些新向量的维度比嵌入向量小。 它们的维数是 64而嵌入和编码器输入/输出向量的维数是 512我们使用注意力多头机制机制这样就可以保持一致了。看到上面的图小伙伴们肯定会有疑惑“query”“key”和“value”这些向量到底是什么呢简单来说他们是可以用计算和思考注意力的抽象特征我们可以通过下面的阅读来明白注意力到底是怎么计算的。计算self-attention的第二步是计算一个score。 假设我们正在计算本例中第一个单词“Thinking”的自注意力。 我们需要根据这个词对输入句子的每个词进行评分。 当我们在某个位置对单词进行编码时分数决定了将多少注意力放在输入句子的其他部分上。得分是通过将查询向量与我们正在评分的各个单词的键向量进行点积来计算的。 因此如果我们正在处理位置 #1 中单词的自注意力第一个分数将是 q1 和 k1 的点积。 第二个分数是 q1 和 k2 的点积。具体的我们可以看下图操作第三步和第四步是将上一步得到的分数除以 8论文中使用的关键向量维度的平方根也就是64的平方根。这会导致梯度更稳定。这里可能还有其他可能的值但8是默认的值。然后通过 softmax 。 Softmax 将分数归一化因此它们都是正数并且加起来为1。从这个 softmax分数我们看出来比如在第一个位置上“Thinking”这个词肯定是本身自己的关联度最高那么经过softmax之后就会得到一个比较高的分数换句话说是比较高的联系度。而其他位置与这个词的联系度也可以通过这个分数来体现出来。第五步是将每个值向量乘以 softmax 分数准备将它们相加。 这里的直觉是保持我们想要关注的单词的值不变并选择性的忽略不相关的单词例如通过将它们乘以像 0.001 这样的小数字。第六步是对加权值向量求和。 这会在这个位置对于第一个词产生自注意力层的输出。自注意力计算到此结束。 结果向量也就是上图中z1、z2是我们可以传送到前馈神经网络的向量。 然而在实际实现中为了更快的处理这个计算通常是以矩阵形式完成的。我们再来以矩阵的形式理解一下自注意力的流程。自注意力的矩阵计算其实就是向量的扩充版本为了加深记忆咱们再以矩阵的形式理解一下对应的流程第一步是计算查询、键和值矩阵。通过将我们的词嵌入写成到一个矩阵 X 中并将其乘以我们训练过的权重矩阵WQ、WK、WV来实现。X矩阵中的每一行其实都是对应输入词的embedding。然后我们把上面的向量的2-6步合并成如下一步2.5 多头机制在论文中通过添加一种称为“多头”注意力的机制进一步细化了自注意力层。具体是通过两种方式来提高注意力层的性能多头的机制扩展了模型专注于不同位置的能力。在上面的例子中 z1 包含一点点其他编码但它可能由实际单词本身主导。如果我们翻译成为“动物没有过马路因为它太累了”这样的句子其实感官上也是很有用但是我们还是想知道“它”指的是哪个词。多头的机制为注意力层提供了多个“表示子空间”。正如我们接下来将看到的对于多头注意力我们不仅有一个而且还有多组查询/键/值权重矩阵在论文中Transformer有八个注意力头所以我们最终为每个编码器/解码器提供了八组向量这些集合中的权重每一个都是随机初始化的。然后在训练之后每组用于将输入嵌入或来自较低编码器/解码器的向量投影到不同的表示子空间中。如果我们进行上面描述的自注意力计算方式那么只需使用不同的权重矩阵进行8次不同的计算我们最终会得到8个不同的Z矩阵但是多头机制也会产生一个问题就是作为前馈网络的输入它还是希望可以接收到一个矩阵而不是八个头对应的八个矩阵相信小伙伴们肯定想到了对应的措施就是把对应的八个矩阵concat起来然后再通过一个矩阵来进行融合就得到了下面的那张图我们把整个流程总结起来就是下面这张图啦2.6 使用位置编码表示序列的顺序从上面的计算流程来看这个算法其实对单词在句子中的位置并不感知也就是随意调换单词的位置对实际网络推断的影响并没有很大但是我们在讲述一个句子的时候词的顺序不同表达的意思也是不同的以中文为例如果把牙和刷这两个词顺序相调换意思是不一样的。为了解决这个问题我们在做输入的时候就不能只把word embedding的信息传入到编码器中而是还需要在初始情况下加上对应的位置信息编码假设我们的word embedding的长度为4 那么对应的位置的embedding的长度也为4小伙伴可能又有疑惑了到底位置的embedding到底是什么样子的呢在下图中每一行对应一个向量的一个位置编码。 因此第一行是我们添加到输入序列中第一个词的嵌入中的向量。 每行包含 512 个值——每个值都在1到-1之间。我们对它们进行了颜色编码因此图案是可见的。在论文中的具体编码方式如下当然也可以使用其他的位置编码方式但是位置编码也需要注意几个问题就是它的范围要尽量是一定的并且针对每个位置具有特定的特征同时不能增加很多的计算开销2.7 残差连接在继续之前我们需要提及的编码器架构中的一个细节是每个编码器中的每个子层self-attentionffnn周围都有一个残差连接然后是层归一化步骤。残差连接也是增加了信息的传递这边要注意的是会有一个层归一化操作也许有小伙伴们会问为什么不用BN层相信小伙伴们肯定记得我们是要对输入的句子进行组batch的那么这种操作会对短句子进行padding那么对于bn计算的时候会产生大量无效的计算并且计算得到的结果也并不是很好的反映数据的分布状态所以这里就步进行BN层的操作了为什么要用LN层因为网络结构中会存在一系列的残差操作这些操作其实会对数据的分布产生一定的影响并且保证层与层之间的分布比较稳定所以就采用了LR层未完待续。。。这文章写的太长了欲知后事如何且听下回分解