Transformers from Scratch(从零开始的Transformers )翻译学习【更新中】

原文链接

Transformers from Scratch

作者前言

“我拖延了几年时间后才开始深入研究Transformers模型。最后因为不了解他们是如何实现而产生的不适感变得对我来说过于强烈(才终于开始着手其中),以下便是我深入研究的内容。

Transformers在2017年的这篇论文中被介绍为序列转换的工具——将一个符号序列转换为另一个符号。该领域最流行的应用是语义翻译,如从英语翻译到德语。它同时也被拓展出实现序列补全的功能-给定一个起始提示,以相同的脉络和风格进行下去。Transformers已迅速成为自然语言处理的研究和产品开发的一个不可或缺的工具。

在我们开始之前先提个醒。我们将讨论很多关于矩阵乘法的内容,并涉及反向传播(用于训练模型的算法),但你不需要事先了解任何内容。我们将逐一添加我们需要的概念,并加以解释。

这将不是一段短暂的学习旅程,但我希望你能乐在其中。”

One-hot encoding(独热编码)

首先是文字处理部分。对于海量的文字,我们的第一步是把所有的字符转换成数据,这样就可以对它们进行数学运算。

假设我们的目标是实现一台可以响应我们语音命令的计算机。我们的工作是建立一个基于Transformer的工具,将一连串的声音转换为一连串的文字。

第一步需要选择我们的词汇表,即在每个序列中要使用的符号集合。在我们的案例中,将有两类不同的符号集合,一类代表声音输入序列,另一类代表输出的文字序列。

现在,假设我们使用英语工作。英语中有数万个单词,也许还有几千个用以涵盖计算机专用术语。这将使我们的词汇量级达到十万之多。将单词转换为数字的一种方法是,从一开始计数,给每个单词分配自己的数字。这样一来,一连串的单词就可以被表示为一串数字。

例如,假设有一种极小众的语言,其词汇量只有三个:files, find, and my。每个词都可以换成一个数字,也许files=1,find=2,my=3。那么,由单词序列[find, my, files]组成的句子 "Find my files "就可以表示为数字序列[2, 3, 1]。

这是一种相当有效的将符号转换为数字的方法,但事实证明,还有一种格式对计算机来说更容易操作,那就是One-hot编码。在单次编码中,一个符号由一个大部分为零的数组表示,与词汇的长度相同,只有一个元素的值为1。数组中的每个元素都对应于一个单独的符号。

另一种定义One-hot编码的方式是:每个词仍然被分配自己的数字,但现在这个数字是一个数组的索引。下图是我们上面的例子,用One-hot编码表示。

在这里插入图片描述

由此,"Find my files "这句话变成了一连串的一维数组,在你把它们压缩在一起后,它们开始看起来像是一个二维数组了。

在这里插入图片描述

注意,我将交替使用 "一维数组 "和 “向量 ”这两个术语。同样,"二维数组 "和 "矩阵 "也是如此。

点积

One-hot表示法的一个真正有用的地方是:它让我们可以计算点积。点积也以其他令人生畏的名字而著称,如内积和标量积。要得到两个向量的点积,需要将它们相应的元素相乘,然后将结果相加。

在这里插入图片描述

点积在我们处理One-hot词的表示时相当有用。任何One-hot向量与自身的点积都是1。

在这里插入图片描述
而任何一个One-hot向量与任何其他One-hot向量的点积都是零。

在这里插入图片描述
前面的两个例子展示了点积如何被用来衡量相似性。再举另一个例子,考虑一个代表不同权重的词的组合的值向量。一个One-hot编码的词可以用点积与之比较,以显示该词的代表性有多强。

在这里插入图片描述

矩阵乘法

点积是矩阵乘法的基础,是组合一对二维数组的一种非常特殊的方式。在最简单的情况下,当A只有一行,B只有一列时,矩阵乘法的结果就是两者的点积。

在这里插入图片描述

请注意,A的列数和B的行数必须相同,这两个数组才能匹配并计算点积。

当A和B的维度开始增长时,矩阵乘法就开始变得棘手了。要处理A中超过一行的情况,可以分别取B与每一行的点积。答案将与A的行数一样多。

在这里插入图片描述

当B的列数更多时,取每一列与A计算点积,并将结果堆叠在连续的列中。

在这里插入图片描述
现在我们可以将其扩展到任意两个矩阵的互乘,只要A的列数与B的行数相同,其结果将与A的行数相同,与B的列数相同。
在这里插入图片描述
如果这是你第一次学习这些内容,可能会觉得没必要这么复杂,但我保证这之后将物有所值。

查表法计算矩阵乘法

注意矩阵乘法在这里(上图)是如何充当查找表的。我们的A矩阵是由一叠One-hot向量组成的。它们在第一列、第四列和第三列分别含有一个数字1。当我们进行矩阵乘法运算时,这有助于依次分离出B矩阵的第一行、第四行和第三行。这种使用One-hot向量来提取矩阵的某一行的技巧是transformers工作的核心。

一阶序列模型

我们可以先把矩阵放在一边,回到我们真正关心的问题上,即词的序列。想象一下,当我们开始开发我们的自然语言计算机界面时,我们只想处理三种可能的命令。

Show me my directories please.
Show me my files please.
Show me my photos please.
*

我们的词汇规模现在是7个。
{directories, files, me, my, photos, please, show}.

表示序列一个有用的方法是使用一个过渡模型。对于词汇表中的每一个词,它都会显示下一个词可能是什么。如果用户有一半的时间询问照片,30%的时间询问文件,其余的时间询问目录,那么过渡模型将是这样的。跳转到任何一个词的过渡总和将恒为1。

在这里插入图片描述
这个特殊的过渡模型被称为马尔科夫链,因为它满足马尔科夫属性,即下一个词的概率只取决于最近的词。更具体地说,它是一个一阶马尔可夫模型,因为它只看最近的一个词。如果它考虑最近的两个词,它就是一个二阶马尔可夫模型。

我们与矩阵的暂别已经结束。事实证明,马尔科夫链可以方便地以矩阵形式表达。现在使用与我们在创建One-hot向量时相同的索引方案,每一行代表我们词汇表中的一个词。每一列也是如此。矩阵过渡模型将矩阵视为一个查找表。找到与你感兴趣的词相对应的那一行。每一列的值显示了该词接下来的概率。因为矩阵中每个元素的值都代表一个概率,它们都将落在0和1之间。因为概率的总和为1,所以每一行的数值加起来总是1。
在这里插入图片描述
在这里的过渡矩阵中,我们可以清楚地看到我们三个句子的结构。几乎所有的过渡概率都是0或1。马尔科夫链中只有一个地方发生了分支。在my之后,directories、files或 photos 这些词可能会出现,每一个都有不同的概率。除此以外,接下来会出现哪个词并不具有不确定性。这种确定性通过过渡矩阵中的大部分1和0来体现。

我们可以重新审视我们的技巧,即使用矩阵与One-hot向量相乘来提取与任何特定单词相关的过渡概率。例如,如果我们只想分离出 "my "之后哪个词的概率,我们可以创建一个代表 "my "的One-hot向量,并将其与我们的过渡矩阵相乘。这样就可以提取相关的行,并向我们展示下一个词的概率分布。
在这里插入图片描述

二阶序列模型

只根据当前的词来预测下一个词是十分困难的。这就像在只得到第一个音符后预测曲子的其余部分。如果我们能至少得到两个音符,我们的机会就会大很多。

我们可以在另一个计算机命令的小型语言模型中看到这一点是如何运作的。我们预计这个模型将只看到两个句子,比例为40/60。

Check whether the battery ran down please.
Check whether the program ran please.

马尔科夫链说明了这是一个一阶模型。

在这里插入图片描述
在这里我们可以看到,如果我们的模型读取的是最近的两个词,而不是只有一个,它可以做得更好。当它遇到battery ran时,它知道下一个词将是down,而当它看到program ran时,下一个词将是please。这就消除了模型中的一个分支,减少了不确定性,增加了可信度。往后看两个词就变成了二阶马尔可夫模型。它提供了更多的背景,可以作为下一个词预测的基础。二阶马尔可夫链的绘制更具挑战性,但其关联性足以证明其价值。

在这里插入图片描述
为了强调两者之间的区别,下图是一阶过渡矩阵。

在这里插入图片描述
而这是二阶过渡矩阵。

在这里插入图片描述
请注意二阶矩阵是如何使每一个词的组合(其中大部分在这里没有显示)都有一个单独的行。这意味着,如果我们从N个词汇量开始,那么过渡矩阵有N^2行。

这使我们对我们的模型更具信心。在二阶模型中,有更多的1和更少的分数。在我们的模型中,只有一行有分数,一个分支。直观地说,看两个词而不是看一个词,可以提供更多的背景,更多的信息,作为猜测下一个词的依据。

使用跳过的二阶序列模型

当我们只需要读取两个词来决定下一个词的时候,二阶模型表现出很好的效果。那么,当我们需要进一步向后读取的时候呢?想象一下,我们正在建立另一个语言模型。这个模型只包含两个句子,每个句子发生的可能性都是一样的。

Check the program log and find out whether it ran please.
Check the battery log and find out whether it ran down please.

在这个例子中,为了确定哪个词应该在run之后,我们必须往前看过去的8个词。如果我们想改进我们的二阶语言模型,我们当然可以考虑使用三阶和高阶模型。然而,对于一个相当大的词汇量,这需要结合创造力和算力来实现。一个简单的八阶模型的执行就会有N^8行,这对于任何合理的词汇量来说都是一个荒谬的数字。

然而我们可以采取一些取巧的方法,设计一个二阶模型,但考虑最近的词与之前的每个词的组合。这仍然是二阶的,因为我们一次只考虑两个词,但它允许我们进一步追溯并捕捉长范围的依赖关系。这个二阶带跳过的模型与无限阶模型之间的区别在于,即便我们放弃了大部分的词序信息和前面词的组合,遗留下的信息仍大有可为。

现在马尔科夫链对我们几乎毫无作用了,但我们仍然可以用其表示每一对前面的词和后面的词之间的联系。在这里,我们省去了数字权重,只显示与非零权重相关的箭头。较大的权重用较重的线条表示。
在这里插入图片描述
以下是它在过渡矩阵中可能出现的情况。
在这里插入图片描述
这个视图只显示与预测ran之后的词有关的行。它显示了最近的词(ran)在词汇表中的每个其他词之前的情况。只显示相关的值。图中所有的空单元格都是零。

首先很明显的是,当我们试图预测run后面的词时,我们不再只看一行,而是观察一整组。我们现在已经走出了马尔科夫的领域。每一行不再代表序列在某一点上的状态。相反,每一行都代表了可能描述序列在某一点的许多特征之一。最近的词与之前出现的每一个词的组合构成了一个适用行的集合,也许是一个大集合。由于这种意义上的变化,矩阵中的每个值不再代表概率,而是一类投票。其投票将被汇总和比较,以决定下一个词的预测结果。

继而显而易见的是,大部分的特征并不重要。绝大多数词都同时出现在两个句子中,所以它们被观察到的事实对预测接下来的内容没有帮助。它们的值都是0.5。唯一的两个例外是battery和program。它们有一些1和0的权重与之相关。battery, ran这一特征表明,ran是最近的一个词,而battery在句子的某处出现。这个特征与down相关的权重为1,与please相关的权重为0。同样地,特征program, ran的权重也是相反的。这种结构表明,这两个词在句子中的早期出现对预测下一个词起着决定性的作用。

为了将这组词的特征转换为下一个词的估计值,需要将所有相关行的数值相加。向下加列,序列 Check the program log and find out whether it ran 产生的所有词的和都是0,除了down的4和please的5。序列Check the battery log and find out whether it ran与之结果相同,除了一个5的down和一个4的please,其他都是一样的。通过选择投票总数最高的词作为下一个词的预测,这个模型让我们得到了相对正确的答案,尽管结果与另外八个词深度依存。

遮蔽

经过更仔细的考虑可以得出,上述结果并不令人满意。总票数为4和5之间的差异相对较小。这表明该模型并不像它预想的那样可信。而在一个更大的、更贴合实际的语言模型中,很容易想象这样一个微小的差异将消失在统计噪音中。

我们可以通过剔除所有不具参考价值的特征票来提高预测的清晰度,除了battery, ran和program, ran之外。在这一点上,记住我们从过渡矩阵中抽出相关行的操作是很有帮助的,我们将其与显示哪些特征当前处于活动状态的向量相乘。到目前为止,在本例子中我们始终使用下图给出的隐含特征向量。
在这里插入图片描述
该向量包含每个特征,是ran与它前面的每个词的一个组合。任何在它之后的词都不包括在特征集中。(在下一个词的预测问题中,这些词还没有被看到,所以用它们来预测接下来的内容是不公平的)。而且这还不包括所有其他可能的单词组合。对于这个例子,我们可以放心忽略这些数据,因为它们都将是零。

为了改善我们的结果,我们可以通过再创建一个掩码将无用的特征强制化为零。这是一个全为1的向量,除了你想隐藏或屏蔽的位置,这些位置将被设置为0。在我们的例子中,我们想屏蔽除了battery, ran 和program, ran,外所有的特征,只有这两组是有帮助的。
在这里插入图片描述
为了应用掩码,我们将这两个向量逐一相乘。在未屏蔽位置的任何特征活动值将被乘以1,并保持不变。在被屏蔽的位置上的任何特征活动值将被乘以0,从而被强制归零
掩码具有隐藏大部分过渡矩阵的效果。它隐藏了除battery和program之外的所有特征的组合,只留下重要的功能。
在这里插入图片描述
在掩盖了无用的特征后,下一个词的预测变得更直接。当battery这个词出现在句子的前面时,在run之后的词被预测为权重为1的down和权重为0的please,原来25%的权重差异变成了无限大的差异,接下来会出现什么词就是毋庸置疑的了。当program在ran前出现时,对please也有同样强烈的预测。

这种选择性遮蔽的过程就是Transformers的原始论文标题中所呼吁的注意力(Attention)。到目前为止,我们所描述的只是对论文中注意力实现方式的一个近似解释,它抓住了其重要的概念,但细节与之不同。之后我们会弥补上这个差距。

中场休息站

恭喜你走到这一步。如果你愿意可以停下来休息一会。至少在解码器方面,建立选择性二阶跳转模型是思考Transformers所做工作的一种有用方式。它初步捕捉到了像OpenAI的GPT-3这样的生成型语言模型正在做的事情。尽管并没有讲述完整的故事,它仍代表了其中心主旨。

接下来的章节进一步填补了这个直观解释和Transformers如何实现之间的差距。它们主要是由三个实际考量因素驱动:

  1. 计算机在计算矩阵乘法方面特别擅长。有一个完整的产业围绕开发计算机硬件尤其是快速矩阵乘法计算打造。任何可以表示为矩阵乘法的计算都可以做到惊人的高效。这就像一列子弹列车。如果你能把你的行李放进去,它就能以惊人的速度把你带到你想去的地方。
  2. 每一步都需要是可微的。到目前为止,我们只是在举儿童玩具般简单的例子,而且还可以手工挑选所有的过渡概率和掩码值–即模型参数。在实践中,这些必须通过反向传播来学习,这取决于每个计算步骤是否可微。这意味着,对于一个参数的任何微小变化,我们都可以计算出模型误差或损失的相应变动。
  3. 梯度需要是平滑的和良态的。所有参数的所有导数的组合就是损失梯度。在实践中,要使反向传播表现良好,需要梯度是平滑的,也就是说,当你在任何方向上做小的步骤时,斜率不会变化的过快。当梯度呈良态时,它们的表现也会好得多,即它不会在一个方向上比另一个大得多。如果你把损失函数想象成一个景观,大峡谷就是一个条件差的景观。无论你是沿着底部旅行,还是沿着侧面旅行,你都将在差别很大的坡度上跋涉。相比之下,经典的Windows屏幕保护程序中的连绵起伏的山丘就会是一个良态的坡度。

如果说架构神经网络的科学是创建可分化的积木,那么它们的艺术就是将这些积木堆叠起来,使梯度不会变化太快,并且在每个方向上都大致相同。

注意力之矩阵乘法

特征权重可以直接通过计算训练中每个词对下一个词出现的转换频率来建立,但注意力掩码并非如此。到此为止,我们已经凭空提出了掩码向量,然而更加重要的是Transformers如何找到相关的掩码。使用某种查找表是个很自然的选择,但现在我们正努力专注于将一切表达为矩阵乘法。我们可以使用与上述介绍同样的查找方法,将每个词的掩码向量堆叠成一个矩阵,并使用最近一个词的One-hot表示法来提取出相关的掩码。

在这里插入图片描述
为了清晰起见,在显示掩码向量集合的矩阵中我们只显示了我们要提取出的那个。

我们终于到了可以开始联系论文的时候了。该查询掩码由注意力方程中的QK^T项表示。
在这里插入图片描述
查询向量Q代表我们感兴趣的特征,矩阵向量K代表掩码的集合。因为它是以列而不是行来存储掩码的,所以需要在乘法之前进行转置(用T运算符)。当我们全部完成的时候,我们会对其进行一些重要的修改,但是在这个层面上,它已经捕获到了Transformers所使用的可区分查找表的概念。

二阶序列模型的矩阵乘法

到目前为止,我们步骤中另一个一直手忙脚乱的部分是构建过渡矩阵。虽然我们已经清楚了这个逻辑,但仍不清楚如何用矩阵乘法来做。

一旦我们得到了注意力步骤中的结果,即一个包括最近的词和之前的一小部分词的向量,就需要把它转化为特征,每一个特征都是一个词对。通过注意力掩蔽我们得到了需要的原始材料,但它并没有建立这些词对的特征。要做到这一点,我们可以使用一个单层的全连接神经网络。

为了解神经网络层是如何创建的,我们将手工制作一个词对。它将呈现人为的干净和风格,它的权重与实践中的权重没有任何相似之处,但它将展示神经网络如何具有建立这两个词对特征所需的表现力。为了保持小而干净,我们将只关注这个例子中的三个被关注的词,battery, program, ran。

在这里插入图片描述
在上面的分层图中,我们可以看到权重是如何将每个词的存在与否状态结合成一个特征集合的。这也可以用矩阵形式来表达。

在这里插入图片描述
而它可以通过与代表从开始所看到的词语集合的向量进行矩阵乘法计算得到。

在这里插入图片描述

battery 和ran 的元素是1,program 元素是0。偏置元素总是1,这是神经网络的一个特点。通过矩阵乘法,代表battery 的元素为1,代表program和ran的元素为-1。其他情况的结果也类似。

在这里插入图片描述
计算这些单词组合特征的最后一步是非线性地应用整流线性单元(ReLU)。这样做的效果是用零来代替任何负值。这样就可以清理这两个结果,使它们代表每个单词组合特征存在(用1)或不存在(用0)。

随着这些智力训练的结束,我们终于完成了一个基于矩阵乘法来创建多字特征的方法。虽然我最初声称这些特征由最近的词和一个较早的词组成,但仔细看看这个方法就会发现它也可以建立其他特征。完成特征创建矩阵的学习而不是硬编码后,其他结构也可以进行学习。即使在这个玩具一般简单的例子中,也没有什么能阻止像battery, program, ran这样的三个词组合的产生。如果这种组合出现得足够普遍,它最终可能会被代表。我们没有办法指出这些词是以什么顺序出现的(至少现在没有),但我们绝对可以利用它们的共同出现来进行预测。甚至有可能利用忽略最近的单词的单词组合,比如battery, program。这些和其他类型的特征可能是在实践中产生的,这体现了我在定义transformers 是一个选择性的二阶带跳的序列模型时所作的过度简化。这里面有更多的细微差别,而现在你可以看到这个细微差别到底是什么。为了纳入更多的模型细节,这不会是我们最后一次改变其定义。

这种形式下,多字特征矩阵已经可以准备再进行一次矩阵乘法,即我们上面构建的带跳过的二阶序列模型。所有的序列所经的:

  1. 特征创建矩阵乘法。
  2. 非线性ReLU
  3. 过渡矩阵乘法

是应用注意力后的前馈处理步骤。论文中的公式2以简明的数学表述显示了这些步骤。

在这里插入图片描述

论文的图1结构图显示,这些被归纳为前馈模块。

在这里插入图片描述

序列补全

到目前为止,我们只谈到了对下一个单词的预测。为了让我们的解码器生成一个长的序列,我们需要添加几个部分。首先是一个提示(prompt),通过输入一些示例性的文本给transformer提供运行的起点和背景,以便建立序列的其余部分。它将被输入到解码器中,即上图中右边的那一列,在那里它被标记为 “Outputs (shifted right)”。选择一个能提供有趣序列的提示本身就是一门艺术,称为提示工程(prompt engineering)。这也是说明人类主要修改自己的行为来支撑算法,而不是反过来的一个很好的例子。

一旦解码器有一个部分序列准备就绪,它就进行前向传递。最终的结果是一组预测的词的概率分布,序列中的每个位置都有一个概率分布。在每个位置上,分布显示了词汇表中每个下一个单词的预测概率。我们并不关心序列中每个已确定的词的预测概率。它们已经被确定了。我们真正关心的是提示结束后下一个词的预测概率。有几种方法来选择这个词,但最直接的方法是贪婪(greedy),即选择概率最高的词。

然后,新的下一个词被添加到序列中,在解码器底部的 "输出 "处被替换进去,这个过程不断重复,直到你对此感到厌倦。

我们还没有准备好详细描述的一个部分是另一种形式的屏蔽,它确保当transformer 进行预测时只关注后面的部分而不是前面。 这个部分被应用于被标记为“屏蔽的多头注意力”的区块中。当我们能更清楚地了解它是如何做到的时候将重新讨论这个问题。

嵌入层(Embeddings)

正如我们到目前为止所描述的那样,transformers过于庞大了。对于一个词汇量为50,000的词汇,所有词对和所有潜在的下一个词之间的转换矩阵将有50,000列和50,000平方(25亿)行,总计超过100万亿元素。即使对于现代硬件平台来说,这仍然是一个难以企及的数字。

而问题不仅仅在于矩阵的大小。为了建立一个稳定的过渡语言模型,我们将不得不至少提供几次阐述每个潜在序列的训练数据。这甚至会远远超过最宏大的训练数据集的容量。

幸运的是,这两个问题都有一个变通的办法,即嵌入。

在一种语言的One-hot表示中,每个词都有一个向量元素。对于一个大小为N的词汇,该向量是一个N维空间。每个词都代表这个空间中的一个点,沿着许多轴中的一个移动,距离原点一个单位。我还没有找到画出高维空间的好方法,但下面有一个粗略的表示。

在这里插入图片描述
在嵌入中,这些词点都被取走并重新排列(用线性代数术语来说,就是映射)到一个较低维度的空间。上图显示了它们在一个二维空间中的样子。例如,现在我们不再需要N个数字来指定一个词,而只需要2个。这些是新空间中每个点的(x,y)坐标。下面是我们的小玩具例子中二维嵌入的样子,以及一些词的坐标。

在这里插入图片描述
一个好的嵌入过程会将具有相似含义的词组合在一起,使用嵌入的模型会在嵌入空间中学习模式。这意味着,无论它对一个词的学习结果如何,都会自动应用于紧挨着它的所有词。这有个额外的好处就是减少了所需的训练数据量。每个例子都提供了一部分学习的输入,而这些学习结果将被应用于整个词的邻域。

在这个插图中,我试图通过把重要的组件放在一个区域(battery, log, program),把介词放在另一个区域(down, out),把动词放在中心附近(check, find, ran)来说明这一点。在实际的嵌入中,分组可能不是那么清晰或直观,但基本概念是相同的。行为类似的词之间的距离是很小的。

嵌入能极大地减少所需的参数数量。然而,嵌入空间的维度越少,关于原词的信息丢失的就越多。一种语言的丰富性仍然需要相当多的空间来放置所有重要的概念,以便它们不会互相拖累彼此。通过选择嵌入空间的大小,我们可以用计算负荷来换取模型的准确性。

你可能不会对此感到惊讶:将单词从它们的One-hot表示映射到一个嵌入空间涉及到矩阵乘法。映射是矩阵最擅长的事情。从一个有一行和N列的One-hot矩阵开始,到一个二维的嵌入空间,投影矩阵将有N行和2列,如图所示。

在这里插入图片描述
这个例子显示了一个One-hot向量,例如用于代表battery,是如何提取与之相关的行的,其中包含嵌入空间中的单词坐标。为了使这一关系更加清晰,One-hot向量中的零被隐藏起来,所有其他没有被拉出投影矩阵的行也是如此。完整的映射矩阵是密集的,每一行都包含它所关联的词的坐标。

映射矩阵可以将原始的One-hot词汇向量集合转换成你想要的任何维度空间中的任何配置。最大的诀窍是找到一个有用的映射,即将相似的词分组在一起的映射,以及一个有足够维度来分散它们的映射。对于常见的语言,如英语,有一些相当不错的预先计算的嵌入。另外,像transformer中的其他东西一样,它也可以在训练中学习。

在原始论文的图1架构图中,这里是进行嵌入的地方。

在这里插入图片描述

位置编码

到目前为止,我们一直假设单词的位置是被忽略的,至少对于在最近的单词之前的任何单词来说是如此。现在我们要用位置嵌入法来解决这个问题。

有几种方法可以将位置信息引入到我们的词的嵌入表中,但在最初的transformer中,它的方法是添加一个循环摆动。

在这里插入图片描述

词在嵌入空间中的位置就像一个圆的中心。它被添加的一个扰动取决于它在词的序列中的位置。不同位置的词被移动相同的距离,但角度不同,当你把词在序列中移动时,会产生一个圆形图案。在序列中相互靠近的词有类似的扰动,但相距较远的词则在不同的方向受到扰动。

由于圆是一个二维的图形,表示一个圆形的摆动需要修改嵌入空间的两个维度。如果嵌入空间由两个以上的维度组成(几乎总是如此),那么圆形摆动就会在所有其他的维度对中重复出现,但是如果旋转的角度频率不同,也就是说,它在每种情况下扫出不同数量的旋转。在一些维度对中,扰动将扫出许多旋转的圆。在其他维度对中,它只会扫出一小部分的旋转。所有这些不同频率的圆周摆动的组合,可以很好地表示一个词在序列中的绝对位置。

我仍然在培养我的直觉,以理解为什么这能起作用。它似乎是以一种不会破坏文字和注意力之间的学习关系的方式将位置信息添加到混合中。如果想更深入地了解数学和意义,我推荐阿米尔霍辛-卡泽姆内贾德的位置编码教程

在transformer典型的结构图中,这些模块显示了生成的位置编码和它添加的嵌入字。
在这里插入图片描述

逆嵌入层

嵌入词使它们的工作效率大大提高,但一旦(嵌入)晚会结束,它们就需要被转换回原始词汇中的词。解除嵌入的方式与嵌入的方式相同,从一个空间投射到另一个空间,也就是矩阵乘法。

逆嵌入矩阵的形状与嵌入矩阵相同,但行数和列数是翻转的。行的数量是我们要转换的空间的维度。在我们所使用的例子中,它是我们嵌入空间的大小,即2。列数是我们要转换的空间的维度–在我们的例子中,是全部词汇One-hot表示法的大小,即13。

在这里插入图片描述

一个好的逆嵌入矩阵中的值并不像嵌入矩阵中的值那样可以直接阐明,但其效果是相似的。当代表程序这个词的嵌入向量与逆嵌入矩阵相乘时,相应位置上的数值就会很高。然而,由于投射到高维空间的工作方式,与其他词相关的值不会是零。在嵌入空间中最接近程序的词也会有较高的值。其他的词将有接近零的价值。而且很可能会有很多负值的词。词汇空间的输出向量将不再是One-hot或稀疏的。它将是密集的,几乎所有的值都不为零。

在这里插入图片描述
这没关系。我们可以通过选择与最高值相关的词来重新创建One-hot向量。这个操作称为argmax,用于给出最高值的参数(元素)。这就是上面提到的如何完成贪婪序列的步骤。这是个不错的第一步,但我们可以做得更好。

如果一个嵌入图很好地映射到几个词,我们可能不想每次都选择最好的那个。它可能只是比其他的选择好一点点,增加一丝变化可以使结果更有趣。另外,有时在确定最后的选择之前,提前看几个词并考虑句子可能的所有方向是很有用的。为了做到这些,我们必须首先将我们的逆嵌入结果转换成概率分布。

Softmax

argmax函数是相当 "顽固 "的,即总是倾向于让最高值获胜,即使它只比其他值稍微大一点。如果我们想同时考虑几种可能性,最好使用一个 相对"软 "的最大值函数,这里我们通过softmax函数实现。要得到一个向量中的值x的“偏软”最大值,可以使用x的指数e^x除以该向量中所有值的指数之和。

softmax的作用很大,原因有三。首先,它将我们的逆嵌套结果向量从一组任意的值转换为一个概率分布。通过概率比较不同的词被选中的可能性变得更加容易,如果我们想进一步展望未来,甚至可以比较多词序列的可能性。

第二,它在顶部附近稀释了极值。如果一个词的得分明显高于其他词,softmax会放大这一差异,使其看起来几乎像一个argmax,获胜的数值接近于1,而其他所有的数值都接近于0。然而,如果有几个词都接近顶部,它将把它们都保留为高概率,而不是人为地压制接近第二位的结果。

第三,softmax是可微调的,这意味着我们可以计算出在任何输入元素发生微小变化的情况下,结果中的每个元素将发生多大的变化。这使我们能够将其与反向传播一起使用,以训练我们的transformer。

如果你想深入了解softmax,(或者如果你因为难以理解它而夜不能寐),这里有一篇关于它的更完整文章

逆嵌入变换(如下图所示为,线性块)和softmax函数共同完成了逆嵌入过程。

在这里插入图片描述

多头注意力

我们已经可以与投影(矩阵乘法)和空间(向量大小)的概念“和解”了,现在让我们重整旗鼓,再次审视核心注意力机制。如果我们能在每个阶段对我们的矩阵的形状进行更具体的说明,这将有助于澄清对其算法的问题。于此,有一个简短的重要用例清单。
N:词汇量大小。在我们的例子中为13。通常是数以万计的。
n:最大序列长度。在我们的例子中为12。在论文中大概为几百个(文中没有详细说明)。GPT-3中为2048。
d_model:整个模型中使用的嵌入空间的维数。论文中为512。

原始输入矩阵是通过从句子中获取每个词的One-hot表示法,并将它们堆叠起来,使每个One-hot向量成为自己的行。由此产生的输入矩阵有n行和N列,我们可以将其缩写为[n x N]。
在这里插入图片描述
正如我们之前所说明的,嵌入矩阵有N行、d_model列,我们可以简写为[N x d_model]。当两个矩阵相乘时,其结果从第一个矩阵中获取其行数,从第二个矩阵中获取其列数。这使得嵌入的词序矩阵的形状为[N x d_model]。

我们可以使用Transformer跟踪矩阵形状的变化,以此来了解正在发生的事情。在进行第一步嵌入之后,位置编码的动作是加法而不是乘法,所以它不会改变事物的维度。然后嵌入的单词序列进入注意层,并在另一端以相同的形状输出。(我们稍后会回顾这些的内部实现)最后,逆嵌入将矩阵恢复到原来的形状,在序列的每个位置为词汇中的每个词提供其对应的概率。

在这里插入图片描述

为什么我们需要一个以上的注意力头

现在终于到了直面我们在第一次解释注意力机制时所作的一些简化假设的时候了。词语被表示为密集的嵌入式矢量,而不是One-hot矢量。注意力不只是1或0,开或关,也可以是介于两者之间的任何地方。为了使结果在0和1之间,我们再次使用softmax技巧。它有两种好处,既可以迫使所有的值都位于我们的[0, 1]注意力范围内,而且有助于强调最高的值,同时积极地压制最小的值。这就是为什么我们之前在解释模型的最终输出时利用的几乎都是argmax的微分形式。

将softmax函数放在注意力中的一个复杂化的后果是,它将倾向于关注单一元素。这是一个以前没有的限制。有时,在预测下一个词时,把前面的几个词预先记录下来是很有用的,而softmax正好剥夺了这个技巧。这是该模型的一个问题。

解决办法是让几个不同的注意力实例,或头同时运行。这让Transformer在预测下一个词时同时考虑之前的几个词。它使我们恢复了在我们将softmax拉入图片之前的能力。

不幸的是,这样做确实增加了计算的负担。注意力已经是计算工作的“大头”了,而我们还要把它乘以我们想用的多个头。为了解决这个问题,我们可以重新使用将所有东西映射到一个低维嵌入空间的技巧。这就缩小了所涉及的矩阵,从而极大地减少了计算时间,难题便迎刃而解。

为了了解这一点,我们可以继续观察矩阵形状。通过多头关注块的分支和延伸来追踪矩阵形状需要以下三个参数:

d_k:用于键和查询的嵌入空间的维数。论文中为64。
d_v: 嵌入空间中用于值的维数。文中为64。
h:头的数量。文中为8。
在这里插入图片描述
嵌入词的[n x d_model]序列是后面一切论述的基础。在每一种情况下,都有一个矩阵,Wv、Wq和Wk,(在架构图中都无意义地显示为 "线性 "块),将原始嵌入词序列转化为值矩阵V、查询矩阵Q和键矩阵K。K和Q的形状相同,[n x d_k],但V可以不同,[n x d_v]。在论文中,d_k和d_v是一样的,这让事情有点混乱,但它们不一定是一样的。这样设置的一个重点是,每个注意头都有自己的Wv、Wq和Wk变换。这意味着每个头可以放大和扩展它想关注的嵌入空间的部分,而且可以与其他每个头关注的内容不同。

每个注意头的结果与V的形状相同。现在我们的问题是有h个不同的结果向量,每个向量关注序列中的不同元素。为了将这些合并成一个,我们利用线性代数的力量,将所有这些结果串联成一个巨大的[n x h *d_v]矩阵。然后,为了确保它以开始时的形状结束,我们再使用一个形状为[h * d_v x d_model]的变换。

下面是以上所有内容简明扼要地说明。
在这里插入图片描述

重新审视单头注意力

上文我们已经完成了关于注意力的一个概念性说明。尽管实现上有点混乱,但我们先前的直觉仍然是有帮助的。queries和key的定义和解释不再容易,因为它们都被映射到自己的特异性子空间上。在我们的概念说明中,queries矩阵中的一行代表了词汇空间中的一个点,由于One-hot表示法,它代表了一个且仅有一个词。在它们的嵌入形式中,queries矩阵中的一行代表嵌入空间中的一个点,它将靠近一群具有类似含义和用法的词。概念图示将一个查询词映射到一组键,反过来过滤掉所有没有被关注的值。在实际实施过程中,每个注意力头都将一个query词映射到另一个较低维度的嵌入空间中的一个点。这样做的结果是,注意力变成了词组之间的关系,而不是单个词之间的关系。它利用语义上的相似性(在嵌入空间中的接近性)来概括它所学到的关于类似的词。

观察注意力计算矩阵的形状有助于跟踪它在做什么。
在这里插入图片描述
queries和key矩阵,Q和K,都是以[n x d_k]的形式出现。由于K在乘法前被转置,Q K^T的结果是一个[n x d_k]*[d_k x n]=[n x n]的矩阵。事实证明,用这个矩阵的每个元素除以d_k的平方根,可以使数值的大小不至于急剧增长,并有助于反向传播的顺利进行。正如我们所提到的,softmax将结果塞进argmax的近似值中,倾向于关注序列中的一个元素而不是其他元素。在这种形式下, [n x n]注意力矩阵大致将序列中的每个元素映射到序列中的另一个元素,表明它应该注意什么,以获得预测下一个元素最相关的背景。它是一个过滤器,最终被应用于价值矩阵V,只留下一个被关注的价值的集合。这样做的效果是忽略了序列中的绝大多数内容,而把注意力集中在最需要注意的一个先前元素上。

在这里插入图片描述
理解这组计算的一个棘手之处在于,它负责计算我们输入序列的每一个元素的注意力和句子中的每一个词,而不仅仅是最近的一个词。同时它也计算早期单词的注意力, 但这些并不我们真正关心的,因为它们的下一个词已经被预测和确定了。同时它也计算对未来词语的注意力。这些值还没有什么用处,因为它们太过遥远了,而且它们的直接前身还没有被选择到。但是通过计算一些间接的路径,可以影响最近的词的注意力,所以我们把它们都包括在内。只是当我们进行到最后,计算出序列中每个位置的单词概率时,我们就会扔掉其中的大部分,只关注下一个单词。

掩码块负责执行约束,即仅对于这个序列完成任务,不能看向未来序列的词。它避免了从假想的未来词中引入任何奇怪的结果。掩码是粗略而有效的——手动将对当前位置之后的所有单词的关注度设置为负无穷大。在The Annotated Transformer(一个对显示逐行Python实现的论文有不可估量的帮助的伙伴)中,掩码矩阵被可视化了。紫色的单元格表示不允许关注的地方。每一行对应于序列中的一个元素。允许第一行关注自己(第一个元素),但不允许关注后面的任何东西;最后一行被允许关注自己(最后一个元素)和前面的一切。掩码是一个[n x n]矩阵。它不使用矩阵乘法,而是用更直接的逐个元素乘法来应用。这样做的效果是,手动进入注意力矩阵,将掩码中的所有紫色元素设置为负无穷。

在这里插入图片描述
注意力实现方式的另一个重要区别是,它利用了单词在序列中呈现给它的顺序,并且不是把注意力表示为单词与单词之间的关系,而是表示为位置与位置之间的关系。这在它的[n x n]形状中是很明显的。它将序列中由行索引表示的每个元素映射到序列中由列索引表示的一些其他元素。这有助于我们更容易直观地了解和解释它在做什么,因为它是在嵌入空间中操作的。我们免去了在嵌入空间中寻找附近的词来表示queries和key之间关系的额外步骤。

Skip connection(残差连接)

注意力是Transformer最基本的部分和其核心机制,我们现在已经在一个相当具体的层面上了解了它。从这里开始,所有的模块都是使它顺利工作所必需的配套模块,是帮助我们拖动沉重注意力负担的其余缰绳。

我们还没有解释的一个模块是残差连接。它大多被放置在多头关注块周围,element-wise的前馈块周围,以及在标有 ""Add and Norm "的模块中。在残差连接中,输入的一个副本被添加到输出的一组计算中。例如注意块的输入被加回到其输出。以及element-wise的前馈块的输入被加到它的输出中。
在这里插入图片描述
使用残差连接有两个目的。

首先,它们有助于保持梯度平滑,这对反向传播有很大帮助。注意力是一个过滤器,这意味着当它正常工作时,它将阻止大部分试图通过它的东西。这样做的结果是,很多输入的微小变化可能不会在输出中产生太大的差异,如果它们恰好落入被阻挡的通道。这就在坡度上产生了鞍点,在坡度图中它是平的,但仍然没有抵达接近谷底的地方。这些鞍点和山脊是反向传播的一个很大的绊脚石。残差连接有助于使这些地方变得平滑。在注意力机制下,即使所有的权重都是零,所有的输入都被阻断,残差连接也会在结果中增加一个输入的副本,确保任何输入的微小变化仍然会在结果中产生明显的变化。这使得梯度下降法不会被卡在离好的解决方案很远的地方。

从ResNet图像分类器的时代开始,残差连接已经变得很流行,因为它们可以显著提高性能。现在它们是神经网络架构中的一个标准特征。从视觉上看,我们可以通过比较有无残差连接的网络来了解其效果。本文的下图显示了一个有和没有连接的ResNet。当使用残差连接时,损失函数山的斜率要温和得多且均匀。如果你想更深入地了解其工作原理和原因,在这篇文章中会有更深入的解释。
在这里插入图片描述
残差连接的第二个目的是Transformer所特有的——保留原始的输入序列。即使模型有很多注意头,也不能保证一个词会“注意”到自己的位置。注意力过滤器有可能完全忘记其最近的单词,而去关注所有可能相关的早期单词。残差连接通过将原始单词手动添加到信号中,保证它不可能被放弃或遗忘。这种稳健性可能是Transformer在这么多不同的序列补全任务中表现良好的原因之一。

Layer normalization(层归一化)

归一化是一个与残差连接搭配良好的步骤。它们并非一定要同时出现,但当它们被放在一组计算之后时,如注意力或前馈神经网络,二者都能发挥很好的作用。

层归一化的简单实现是,矩阵的值被平移使其均值为零,并缩放使其标准差为一。
在这里插入图片描述
更长的版本是,在像Transformer这样的系统中,有很多移动的模块,其中一些不是矩阵乘法(比如softmax运算器或整流线性单元),值的大小以及如何在正负之间平衡是很重要的。如果一切都是线性的,你可以把所有的输入都加倍,那么输出也会加倍,一切都会正常工作。但在神经网络并非如此。它们本质上是非线性的,这使得它们非常富有表现力,但也对信号的幅度和分布很敏感。归一化是一种在多层神经网络中每一步都保持信号值分布一致的技术,它已经被证明对于保证参数值的收敛和提高性能很有效果。

关于归一化我最喜欢的是,除了像我刚刚给出的那种高水平的解释之外,没有人完全确定它为什么如此有效。如果你想在这个兔子洞更深入一点,我写了一篇关于批量归一化的更详细的文章,这是Transformer中使用的层归一化的一个近亲。

  • 12
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值