Transformer 是在论文《Attention is All You Need》中提出的一种基于全部注意力的框架。原文中一些结构由于篇幅限制,并没有给出网络结构详细的解释。在这篇文章中,博主将尝试稍微简化一些事情,并逐一介绍概念,希望能让没有深入了解主题的人更容易理解。 本文是翻译自Jay Alammar的一篇博客,原文地址没法粘贴,不然审核不通过,不知为何。原文标题为《The Illustrated Transformer》。 一、作者 Jay Alammar,是一位机器学习相关的博主,他的很多博客都值得读一读。 二、框架图 让我们先把这个模型看成一个单一的黑盒子。在一个机器翻译应用中,它将接受一种语言的句子,并输出另一种语言的翻译。 ![]() 打开Transformers的核心,我们看到一个编码组件,一个解码组件,以及它们之间的连接。 ![]() 编码部分是一堆编码器(本文将六个编码器堆在一起--六这个数字没有什么神奇之处,人们绝对可以尝试其他安排)。解码部分是一堆相同数量的解码器。 ![]() 编码器的结构都是相同的(但它们并不共享权重)。每一个都被分解成两个子层: ![]() 编码器的输入首先流经一个自注意力层--该层帮助编码器在对特定单词进行编码时查看输入句子中的其他单词。我们将在本篇文章的后面仔细研究自注意力问题。 自注意力层的输出被送入前馈神经网络。完全相同的前馈网络被独立地应用于每个位置。 解码器也有这两个层,但在它们之间还有一个注意力层,帮助解码器专注于输入句子的相关部分(类似于seq2seq模型中注意力的作用)。 ![]() 三、在图片中加入张量 现在我们已经看到了模型的主要组成部分,让我们开始看看各种向量/张量,以及它们如何在这些组成部分之间流动,将训练过的模型的输入转化为输出。 就像一般的NLP应用一样,我们首先使用嵌入算法将每个输入词变成一个向量。 ![]() 嵌入只发生在最底层的编码器中。所有编码器都有一个共同的抽象概念,那就是它们会收到一个大小为512的向量列表--在最底层的编码器中,这将是单词嵌入,但在其他编码器中,这将是正下方编码器的输出。这个列表的大小是我们可以设置的超参数--基本上就是我们训练数据集中最长的句子的长度。 在我们的输入序列中嵌入单词后,每个单词都流经编码器的两层中的每一层。 ![]() 在这里,我们开始看到Transformer的一个关键属性,即每个位置的字在编码器中流经它自己的路径。在自我注意层的这些路径之间存在着依赖关系。然而,前馈层没有这些依赖关系,因此,在流经前馈层时,这些路径可以并行执行。 接下来,我们将把这个例子换成一个较短的句子,我们将看看在编码器的每个子层中发生了什么。 四、编码 正如我们已经提到的,一个编码器接收一个向量列表作为输入。它通过将这些向量传入 '自注意力 '层,然后传入前馈神经网络来处理这个列表,然后将输出向上发送至下一个编码器。 ![]() 每个位置的词都经过一个自注意力处理。然后它们各自通过一个前馈神经网络--每个向量分别流经完全相同的网络 五、自注意力简介 不要被我的 '自注意力 '这个词所迷惑,好像每个人都应该熟悉这个概念。我个人从来没有接触过这个概念,直到读了《Attention is All You Need》一文。让我们来提炼一下它的作用。 假设以下句子是我们要翻译的输入句: “The animal didn't cross the street because it was too tired” 这句话中的 '它 '指的是什么?它是指街道还是指动物?这对人类来说是个简单的问题,但对算法来说就不那么简单了。 当模型在处理 '它 '这个词时,自注意力使它将 '它 '与 '动物 '联系起来。 当模型处理每个词(输入序列中的每个位置)时,自注意力使它能够在输入序列中的其他位置寻找线索,以帮助实现对这个词的更好编码。 如果你熟悉RNN,想想看,维持一个隐藏的状态是如何让RNN把它以前处理过的词/向量的表达与它现在处理的词/向量结合起来的。自注意力是Transformer用来将其他相关词语的 '理解 '纳入我们当前处理的词语的方法。 ![]() 当我们在5号编码器中对 '它 '进行编码时,部分注意力机制集中在 '动物 '上。 请务必查看Tensor2Tensor笔记本,在那里你可以加载一个Transformer模型,并使用这种交互式的可视化方式来检查它。 六、自注意力细节 我们先来看看如何使用向量来计算自注意力,然后再继续看它的实际实现方式--使用矩阵。 计算自注意力的第一步是,从编码器的每个输入向量(在本例中是每个词的嵌入)创建三个向量。因此,对于每个词,我们创建一个Query向量、一个Key向量和一个Value向量。这些向量是通过将嵌入物与我们在训练过程中训练的三个矩阵相乘来创建的。 注意,这些新的向量的维数比嵌入向量要小。它们的维数是64,而嵌入和编码器输入/输出向量的维数是512。它们不一定要更小,这是一个架构选择,以使多头注意力的计算(大部分)恒定。 ![]() x1乘以WQ权重矩阵得q1。我们最终为输入句子中的每个词创建了一个 'query'、一个 'key '和一个 'value '的投影。 什么是 'query'、'key '和 'value '向量? 它们是抽象的,对计算和思考注意力很有用。一旦你继续阅读下面的注意力是如何计算的,你就会知道几乎所有你需要知道的关于这些向量中的每一个所起的作用。 计算自我注意力的第二步是计算一个分数。比如我们要计算本例中第一个词 'Thinking '的自注意力。我们需要对输入句子中的每个词与这个词进行评分。分数决定了当我们在某一位置对一个词进行编码时,要对输入句子的其他部分给予多少关注。 分数的计算方法是将query向量与我们要评分的各个单词的key向量进行点积。因此,如果我们正在处理#1号位置上的单词的自注意力,第一个分数将是q1和k1的点积。第二个分数将是q1和k2的点积。 ![]() 第三和第四步是将分数除以8(本文中使用的关键向量维度的平方根--64。这将导致拥有更稳定的梯度。这里可能有其他可能的值,但这是默认值),然后将结果通过softmax操作。Softmax将分数归一化,所以它们都是正数,加起来是1。 ![]() 这个softmax分数决定了每个词在这个位置的表达程度。显然,这个位置上的词会有最高的softmax分数,但有时关注另一个与当前词相关的词是很有用的。 第五步是将每个值向量乘以softmax分数(准备将它们加起来)。这里的直觉是要保持我们想要关注的词的完整价值,并淹没不相关的词(例如,通过将它们乘以0.001这样的微小数字)。 第六步是将加权的值向量相加。这就产生了自我注意层在这个位置的输出(对于第一个词)。 ![]() 这就结束了自注意力的计算。由此产生的向量是我们可以发送给前馈神经网络的。然而,在实际执行中,这个计算是以矩阵形式进行的,以便更快处理。所以,现在让我们来看看,我们已经看到了单词层面上的计算的直觉。 七、自注意力矩阵计算 第一步是计算Query, Key, 和 Value矩阵。我们通过将我们的嵌入向量打包成一个矩阵X,并将其与我们训练的权重矩阵(WQ、WK、WV)相乘来做到这一点。 ![]() X矩阵的每一行都对应着输入句子中的一个词。我们再次看到嵌入向量(512)和q/k/v向量(64)的大小不同 最后,由于我们正在处理矩阵,我们可以将第二至第六步浓缩在一个公式中,以计算自注意力层的输出。 ![]() 矩阵形式的自注意力计算 八、多头注意力 该论文通过增加一种称为 '多头 '注意力机制进一步完善了自注意力层。这在两个方面提高了注意力层的性能: 1、它扩大了模型对不同位置的关注能力。是的,在上面的例子中,z1包含一点其他的编码,但它可能被实际的单词本身所支配。如果我们要翻译一个像 'The animal didn’t cross the street because it was too tired'这样的句子,如果能知道 '它 '指的是哪个词,那将是非常有用的。 2、它给注意力层提供了多个 '表示子空间'。正如我们接下来要看到的,在多头关注下,我们不仅有一个,而且有多组Query/Key/Value权重矩阵(Transformer使用八个关注头,所以我们最终为每个编码器/解码器提供了八组)。每一组都是随机初始化的。然后,在训练之后,每一组都被用来将输入嵌入向量(或来自下级编码器/解码器的向量)投射到不同的表示子空间。 ![]() 对于多头关注,我们为每个头保持单独的Q/K/V权重矩阵,从而产生不同的Q/K/V矩阵。 如果我们做同样的自注意力计算,只是用不同的权重矩阵做了八次不同的计算,我们最终会得到八个不同的Z矩阵。 ![]() 这给我们留下了一点挑战。前馈层不期望有八个矩阵--它期望的是一个单一的矩阵(每个词都有一个向量)。因此,我们需要一种方法,将这八个矩阵浓缩为一个单一的矩阵。 我们如何做到这一点呢?我们将这些矩阵连接起来,然后再乘以一个额外的权重矩阵。 ![]() 这几乎是多头自注意力的全部内容。我意识到,这是相当多的矩阵。让我试着把它们都放在一个图片中,这样我们就可以在一个地方看它们了。 ![]() 现在我们已经触及到了注意头,让我们重新审视一下之前的例子,看看在我们的例句中对 '它 '这个词进行编码时,不同的注意头都集中在哪里: ![]() 当我们对 'it'这个词进行编码时,一个注意力头最关注的是 'animal',而另一个注意力头关注的是 'tire'。 然而,如果我们把所有的注意力都加到图片上,事情就会更难解释: ![]() 九、用位置编码来表示序列的顺序 到目前为止,我们所描述的模型还缺少一样东西,那就是对输入序列中的词的顺序进行说明的方法。 为了解决这个问题,transformer在每个输入嵌入中添加一个向量。这些向量遵循模型学习的特定模式,这有助于它确定每个词的位置,或序列中不同词之间的距离。这里的直觉是,一旦嵌入向量被投射到Q/K/V向量中,在点乘关注期间,将这些值添加到嵌入向量中,就能提供嵌入向量之间有意义的距离。 ![]() 为了让模型对单词的顺序有所了解,我们增加了位置编码向量--其值遵循特定的模式。 如果我们假设嵌入的维度为4,那么实际的位置编码将看起来像这样: ![]() 一个位置编码的真实例子,小型嵌入尺寸为4 这种模式可能是什么样子的? 在下图中,每一行都对应于一个向量的位置编码。因此,第一行将是我们添加到输入序列中第一个词的嵌入的向量。每一行包含512个值--每个值在1和-1之间。我们对它们进行了颜色编码,所以模式是可见的。 ![]() 一个位置编码的真实例子,20个字(行)的嵌入大小为512(列)。你可以看到,它从中间开始就被分成了两半。 文件中描述了位置编码的公式(3.5节)。你可以在get_timing_signal_1d().中看到生成位置编码的代码。这并不是唯一可能的位置编码的方法。然而,它的优点是能够扩展到未见过的长度的序列(例如,如果我们的训练模型被要求翻译一个比我们训练集中的任何句子都长的句子)。 ![]() 十、残差 在继续之前,我们需要提到编码器结构中的一个细节,那就是每个编码器中的每个子层(自注意力,ffnn)都有一个围绕它的残差连接,并且后面还有一个层规范化的步骤。 ![]() 如果我们要把向量和与自注意力相关的层-规范操作可视化,它看起来会是这样的: ![]() 这对解码器的子层也是如此。如果我们要考虑一个由2个堆叠的编码器和解码器组成的Transformer,它看起来会是这样的: ![]() 十一、解码端 现在我们已经涵盖了编码器方面的大部分概念,我们基本上也知道解码器的组件是如何工作的。但让我们来看看它们是如何一起工作的。 编码器开始处理输入序列。然后,顶部编码器的输出被转化为一组注意力向量K和V。这些向量将被每个解码器用于其 '编码器-解码器注意力 '层,帮助解码器关注输入序列的适当位置: ![]() 完成编码阶段后,我们开始解码阶段。解码阶段的每一步都从输出序列中输出一个元素。 下面的步骤重复这个过程,直到达到一个特殊的符号,表明transformer解码器已经完成了它的输出。每一步的输出都被送入下一个时间步骤的底层解码器,解码器就像编码器那样冒出它们的解码结果。就像我们对编码器输入所做的那样,我们在这些解码器输入中嵌入并添加位置编码,以表明每个字的位置。 ![]() 解码器中的自注意力层的运作方式与编码器中的略有不同: 在解码器中,自注意力层只被允许关注输出序列中的早期位置。这是通过在自注意力计算的softmax步骤之前屏蔽未来的位置(将它们设置为-inf)来实现的。 “编码器-解码器注意力 '层的工作方式与多头自注意力一样,只是它从其下层创建Queries矩阵,并从编码器栈的输出中获取Keys和Values矩阵。 十二、最后的线性层和Softmax层 解码器堆栈输出一个浮点数的向量。我们如何把它变成一个字?这就是最后一个线性层的工作,它的后面是一个Softmax层。 线性层是一个简单的全连接神经网络,它将解码器堆栈产生的向量投射到一个大得多的向量,称为对数向量。 让我们假设我们的模型知道10,000个独特的英语单词(我们模型的 'output vocabulary'),这是它从训练数据集中学到的。这将使logits向量宽为10,000个单元--每个单元对应一个独特的单词的得分。这就是我们如何解释线性层之后的模型输出。 然后,softmax层将这些分数转化为概率(全部为正数,全部加起来为1.0)。选择概率最高的单元,并产生与之相关的词作为这个时间步骤的输出。 ![]() 该图从底部开始,产生的向量作为解码器堆栈的输出。然后,它被转变成一个输出字 十三、训练回顾 现在我们已经涵盖了通过训练好的Transformer的整个前向过程,看一眼训练模型的直觉会很有用。 在训练过程中,一个未经训练的模型会经历完全相同的前向传递。但由于我们是在一个有标签的训练数据集上训练它,我们可以将它的输出与实际的正确输出进行比较。 为了可视化这一点,我们假设我们的输出词汇只包含六个词('a'、'am'、'i'、'thanks'、'student '和''('句末'的缩写))。 ![]() 我们模型的输出词汇是在我们开始训练前的预处理阶段创建的。 一旦我们定义了我们的输出词汇,我们就可以用一个相同宽度的向量来表示我们词汇中的每个词。这也被称为独热编码。因此,例如,我们可以用以下向量来表示 'am '这个词: ![]() 在这个回顾之后,让我们讨论一下模型的损失函数--我们在训练阶段优化的指标,以导致训练后的模型,并希望有一个惊人的精确模型。 十四、Loss函数 假设我们正在训练我们的模型。说这是我们训练阶段的第一步,我们在一个简单的例子上训练它--把 'merci '翻译成 'thanks'。 这意味着,我们希望输出是一个表示 'thanks '这个词的概率分布。但是由于这个模型还没有被训练好,所以现在还不太可能发生这种情况。 ![]() 由于模型的参数(权重)都是随机初始化的,未经训练的模型会在每个词中产生一个随机概率分布。 你如何比较两个概率分布?我们只需用一个减去另一个。更多细节,请看交叉熵和Kullback-Leibler divergence。 但请注意,这是一个过于简化的例子。更现实的是,我们将使用一个长于一个单词的句子。例如--输入: 'Je suis étudiant '和预期输出: 'i am a student'。这实际上意味着,我们希望我们的模型能够连续输出概率分布,其中: 每个概率分布由一个宽度为vocab_size的向量表示(在我们的小例子中为6,但更现实的是一个数字,如30,000或50,000)。 第一个概率分布在与单词 'i '相关的单元中具有最高的概率 第二个概率分布在与单词 'am '相关的单元格中的概率最高。 以此类推,直到第五个输出分布表示'’符号,它也有一个与之相关的10,000元素词汇的单元。 ![]() 我们将在训练实例中针对一个样本句子的目标概率分布来训练我们的模型。 在一个足够大的数据集上训练了足够长的时间后,我们希望产生的概率分布看起来像这样: ![]() 现在,由于该模型一次产生一个输出,我们可以假设该模型是从该概率分布中选择概率最高的词,而丢弃其他的。这是一种方法(称为greedy decoding)。另一种方法是保留,比如说,前两个词(比如说,'I'和'a'),然后在下一步,运行模型两次:一次假设第一个输出位置是'I',另一次假设第一个输出位置是'a',考虑到1号和2号位置,哪个版本产生的误差小就保留。我们对2号和3号位置重复这一步骤......等等。这种方法被称为 'beam search',在我们的例子中,beam_size是两个(意味着在任何时候,两个部分假设(未完成的翻译)都被保存在内存中),top_beams也是两个(意味着我们会返回两个翻译)。这两个都是超参数,你可以进行实验。 |
|
来自: taotao_2016 > 《数学》