当你现在阅读关于自然语言处理中的机器学习时,你只会听到一个词:Transformer。基于这种深度学习架构的模型自2017年以来已经在NLP界引起了轰动。事实上,它们是今天最常用的方法之一。许多方法在某种程度上都是在最初的Transformer基础上构建的。
变压器并不简单。最初的Transformer模型相当复杂,很多后续变体也是一样。因此,我们将仔细看看Vaswani等人在2017年提出的原始Transformer架构。它是许多其他架构的起点。这篇文章不会太多数学公式,而是用更直观的方式来解释,让大家更容易了解它的内部工作机制。
本文的结构如下。首先,我们将看看为什么Transformer会出现——通过看看他们的前辈,特别是LSTMs和GRUs存在的问题。接下来,我们将从整体上看看Transformer的架构。然后,我们看看编码部分,然后看看解码部分。最后,我们将看看如何训练一个Transformer。
准备好了没?那咱们开始吧!
为什么不使用Transformer? 为什么不使用Transformer?改为:
为什么使用Transformer?在自然语言处理领域中,机器学习传统上使用循环神经网络来完成。这里的“循环”是指,处理一个序列时,用于生成某个标记预测的隐藏状态(或称为“记忆”)会传递给下一个预测使用,以用于生成下一个预测。
循环神经网络(RNN)是一类人工神经网络,节点间的连接形成一个沿时间序列的有向图。这使它具有时间动态行为的能力。RNN是由前馈神经网络发展而来的,可以利用内部状态(记忆)来处理长度不固定的输入序列。
维基百科(2005)
循环网络已经存在了一段时间。最早的之一是简单的或称为“基础”的循环网络,也称作基础RNN。下面这个画廊顶部的图就是它。如你所见,生成预测后,更新的隐藏状态会传递给自身,以便用于后续的预测。展开来看,我们可以清晰地看到它是如何处理各种输入和输出预测的。
虽然当时循环网络在自然语言处理领域取得了进步,但也遇到了一些瓶颈。
- 由于隐藏状态传递的方式,RNN 对 梯度消失问题 特别敏感。特别是在较长的序列中,这种情况下,用于优化的梯度链可能会很长,以至于第一层的实际梯度几乎消失。换句话说,和任何遇到梯度消失问题的网络一样,最上游的层学不到任何东西。
- 对于记忆也是如此,可以说:隐藏状态传递到下一个预测步骤,这意味着绝大多数上下文信息与模型短期看到的内容相关。因此,经典 RNN 在短期记忆表现上表现良好,但在长期记忆方面表现较差。
- 处理是按顺序进行的。也就是说,短语中的每个词都要依次通过循环网络,并返回一个预测结果。由于循环网络在计算需求方面可能很大,生成输出预测可能需要较长时间。这是循环网络的一个固有缺点。
幸运的是,在2010年代初期,长短期记忆网络(LSTM)(LSTM,中图)和门控循环单元(GRU,下图)被研究并应用于帮助解决了上述问题中的部分。特别是LSTM,通过类似内存保留的单元结构,LSTM由于其结构特点,对梯度消失问题具有较强的鲁棒性。此外,由于记忆现在与前一时间步的输出(例如LSTM图中的$c_{t}$流)分开维持,两者都能够存储更长时间的信息。
尤其是当注意力机制在这种机制下被发明出来之后,提供的是一个加权上下文向量而不是隐藏状态,该向量对所有先前预测步骤的输出进行加权,长期记忆问题迅速得到解决。唯一的问题是处理必须按顺序进行,这在训练自然语言处理模型时形成了显著的资源瓶颈。
一个完全递归的网络。由 fdeloche 在 Wikipedia 上创建,采用 CC BY-SA 4.0 许可,未作任何修改。
一个LSTM单元图。由Guillaume Chevalier(Ketograff绘制了SVG图)创建于Wikipedia,采用CC BY 4.0许可证。
) 一个GRU细胞。由Jeblad在Wikipedia上创建,授权为CC BY-SA 4.0(未做任何修改)。
Transformer是什么?在一项2017年的标志性工作中,Vaswani等人声称“注意力就够了”——换句话说,在深度学习模型中,在自然语言处理任务中表现良好无需递归组件。他们提出了一种新的架构,即Transformer,这种架构能够在处理序列时保持注意力机制的同时进行并行处理:所有词一起处理,而不仅仅是逐字处理。参见《注意力就是你所需要的》
这种架构已经消除了上述提到的三个问题中的最后一个,即序列必须按顺序处理,从而带来了很高的计算成本。有了Transformer,真正的并行处理已经成为现实。
正如我们将在不同文章中所看到的,基于Transformer的架构有多种版本。在传统的Transformer架构基础上,研究人员和工程师进行了大量实验并带来了很多变化。然而,原始的Transformer架构如下:
参考文献来源:Vaswani et al. (2017)。
正如我们所见,它有两个交织在一起的段落。
- 编码器段,从源语言输入,为其生成嵌入,编码位置,计算每个单词在多上下文中的关注点,然后输出一些中间表示。
- 解码器段,从目标语言输入,生成嵌入并编码位置,计算每个单词的关注点,然后将编码器输出与之前产生的内容结合。通过Softmax和因此的argmax类别预测(其中每个令牌或单词都是一个类别),预测下一个令牌。
最初的Transformer因此是一个经典的序列到序列翻译模型。
需要注意的是,Transformer能应用于多种语言任务,从自然语言理解(NLU)到自然语言生成(NLG)。有时,源语言和目标语言可能是相同的,但这并不一定如此。
如果你现在觉得有点手忙脚乱,我能理解。我第一次读到关于Transformer的内容时也有这样的反应。所以我们现在分别看看编码器和解码器的部分,仔细观察每个步骤。我们会尽量用翻译类比来直观解释这些步骤。
翻译者的比喻假设我们的目标是构建一个能够将德文翻译成英文的语言模型。在经典的场景中,采用更经典的方法,我们会构建一个可以直接从德文翻译成英文的模型。换句话说,我们正在教一个能从德文翻译成英文的翻译器。换句话说,这个翻译器需要能够流利地掌握并翻译这两种语言,理解这两种语言中单词之间的关系等等。虽然这样可以做到,但这种方法不可扩展。
Transformer的工作方式不同,因为它采用了编码器-解码器架构。可以把它想象成有两个翻译在工作。第一个翻译员可以将德语翻译成一种中间的通用语言。另一个翻译员可以将这种语言翻译成英语。然而,在每次翻译任务中,翻译都会先经过中间语言。这在产生可用结果方面(如传统方法)同样有效。然而,这种方法也更具有可扩展性,比如,我们可以利用中间语言来训练一个用于总结文本的模型,例如。我们不再需要专门针对第一个翻译任务进行训练。
在不同的文章中,我们会看到这种预训练和微调的方法在今天非常流行,尤其是在像BERT这样的架构中,它们在原始Transformer中使用编码器部分,在大规模数据集上进行预训练,并允许人们自己对各种任务进行微调。然而,目前我们还是专注于原始的Transformer。在这个类比中,{德语} -> {中介语言}的翻译任务将由编码器部分执行,生成的中介状态相当于中介语言。接下来,{中介语言} 到 {英语} 的翻译任务则由解码器部分来完成。
我们现在来仔细看看这两个片段吧。
编码器段Transformer的编码器部分负责将输入转换为某种中间的高维表示形式。从结构上看,它如下图所示。编码器部分由几个独立的模块组成:
- 输入的嵌入,将分词后的输入转换为向量格式以便使用。Vaswani 等人 (2017) 使用的是 学习到的嵌入,这意味着分词到向量的转换过程是通过与主要的机器学习任务(即学习序列到序列模型)一起训练来完成的。
- 位置编码,对嵌入层的向量输出进行微调,向这些向量中添加位置信息。
- 实际的编码器部分,学习输出输入向量的加权表示,并由以下子部分组成:
- 多头注意力部分,执行多头自注意力,添加残差连接,然后执行层归一化。
- 前馈段,为每个标记生成编码器输出。
- 编码器部分可以重复 N 次;Vaswani 等人 (2017) 选择 N = 6。
我们现在来看看编码器的各个模块,更仔细地了解它们。
从文本数据到输入嵌入的转换你将用一个文本数据集来训练Transformer。就像你预料的那样,这样的数据集包括短语(通常是一对配对的短语)。比如,英语短语I go to the store
在法语中对应着Je vais au magasin
。
文本切分,即将连续的文本分割成单独的词语
但是,我们不能直接将文本输入到机器学习模型中,比如说TensorFlow,因为它是一个数值处理库,优化技术也基于数字。
因此,我们要找到一种方法,将文本转换为数字。我们可以使用分词化来实现这一点,它将文本分解成整数列表。例如,tf.keras
中的Tokenizer工具可以帮助我们完成这两项任务(Nuric, 2018)。
- 根据文本生成词汇表。我们从一个空的Python字典
{}
开始,一点一点地用每个不同的词填充它,例如,dictionary["I"] = 1
,dictionary["go"] = 2
,以此类推。 - 利用词汇表将单词转换为整数。有了包含许多词汇的词汇表,我们可以将短语转换为基于整数的序列。比如,短语
I go to the store
通过["I", "go", "to", "the", "store"]
可以变成[1, 2, 39, 49, 128]
。显然,这里的数字是由词汇表的生成方式决定的。
用独热编码表示单词不太实用
假设我们用分词器(Tokenizer)生成了一个包含45,000个不同单词的词典。然后我们有一个包含45,000个键的Python字典,其中每个键对应一个单词,所以 len(keys) = 45000
。下一步就是对数据集中的每一句话进行分词处理,例如,句子 ["I", "go", "to", "the", "store"]
被转换为 [1, 2, 39, 49, 128]
,同样,句子 ["I", "will", "go", "now"]
被转换为 [1, 589, 2, 37588]
。这里的数字是分词器随机分配的。
因为这些变量是分类的,我们必须用不同的方式来表示它们——例如通过one-hot编码(KDNuggets,n.d)。然而,在非常大的词汇量的情况下,这种方法效率很低。例如,在我们上面提到的字典中,每个词元将变成一个45,000维的向量!因此,对于较小的词汇量,one-hot编码可以是一种很好的表达文本的方式。对于更大的词库,我们需要采用不同的方法。
词嵌入技术不过,我们有个解决办法。
我们可以使用词向量:
词嵌入技术是自然语言处理(NLP)中的一系列语言建模和特征学习方法,其中词汇中的词或短语被映射到实数值向量。从概念上看,它涉及从高维词空间映射到低维连续向量空间的数学转换。
维基百科(2014年)
换句话说,如果我们能够学会将词元映射到向量,我们就可以为每个单词找到一个独一无二的向量,这个向量存在于一个维度大大减少的空间。如下的可视化所示,对于10,000个单词,我们可以在一个三维空间中进行可视化(仅通过应用PCA,只损失少量信息),而如果我们使用一热编码,就需要使用10,000维的向量。
来自Word2Vec 10K数据集的一个图表,使用[嵌入投影仪],在三维空间中绘制了三个主要成分。单词“routine”被标注为突出。
vanilla transformers 使用学习到的输入嵌入,
注:通常在中文中,“vanilla transformers” 会保持英文原样,除非有特定的翻译习惯。这里假设保持英文术语原样。
vanilla Transformers 使用一个动态学习的输入嵌入层(Vaswani et al., 2017)。这意味着嵌入是动态学习的,而不是使用预训练的嵌入(例如预训练的Word2Vec嵌入,这也是一种选择)。动态学习嵌入可以确保每个词都被准确地映射为向量,从而提高效果,确保不会遗漏任何词。
学到的嵌入生成的向量维度为 d{model},Vaswani 等人 (2017) 设 d{model} 为 512。d_{model} 也是模型中所有子层输出的维度。
根据Vaswani等人(2017)的研究成果,输入嵌入层和输出嵌入层之间的权重矩阵被共享,同时预softmax线性变换也共享。权重还会乘以SQRT(d_model)以提高稳定性。这并非严格必须,有时甚至可能对性能产生负面影响,如Ncasas解释的那样。
源词嵌入和目标词嵌入可以共享,也可以不共享。这是一个设计决定。通常,如果词表是共享的,那么嵌入也会共享,这种情况通常发生在使用相同书写体系的语言上,比如都使用拉丁字母的语言。如果你的源语言和目标语言分别是英语和中文,由于它们的书写体系不同,词表通常不会共享词汇,因此嵌入也不会共享。
Ncasas (2020)
位置编码法
传统的语言理解和生成模型构建方法得益于顺序处理。由于单词必须按顺序处理,模型能识别出常见的顺序(如 我 是
),因为在处理 是
之前,总是传递包含 我
的隐藏状态。
使用Transformer时,情况变得不同了,因为我们知道这类模型没有递归结构,而是完全依赖注意力机制。当一个完整的短语输入到Transformer模型时,它不一定按照顺序进行处理,因此模型并不了解短语内部的顺序。
使用位置编码,我们向由嵌入层生成的词向量中添加一个指示单词相对位置信息的向量。这是一个简单的向量乘法操作:v {encoded} = v {embedding} x v _{encoding}。你可以将其想象成一种重组过程,使得常用的向量更接近。
Vaswani 等人 (2017 年) 使用基于数学(更具体说是基于正弦和余弦)的方法来进行位置编码。通过让位置和维度根据其奇偶性经过正弦或余弦函数,我们可以生成位置编码,用于对嵌入输出进行位置编码。这一步的结果是一个保留了许多嵌入信息的向量,同时也包含了一些关于相对位置的信息(即单词之间的关系)。
N个编码段生成输入嵌入并添加位置编码是预备步骤,使我们能够在Transformer模型中使用文本数据。现在让我们来看真正的编码器部分。
我们必须首先注意到,我们讨论的内容可以重复N次;换句话说,这些内容可以堆叠。在堆叠编码器的情况下,每个编码器的输出用作下一个编码器的输入,从而生成越来越抽象的编码信息。尽管堆叠编码器能通过泛化提升模型性能,但这会增加计算负担。Vaswani等人(2017年)选择使用6个编码器进行堆叠。
每个编码器部分由以下组件组成:
- 一个多头注意力块。该块允许我们对每个序列执行自注意力(这意味着对于每个输入模型的短语,确定每个词(token)在读取时应关注哪些其他词(token))。
- 一个前馈块。生成了每个词(token)的注意力后,我们还需要生成一个512维的向量来编码该词(token)。前馈块负责执行此操作。
- 残差连接。残差连接是指不通过复杂过程的连接。这里有两个残差连接:一个从输入到第一个Add & Norm块;另一个从第一个块到第二个块。残差连接可以使得模型能更高效地进行优化,因为从技术上讲,梯度可以自由地从模型的末端流向前端。
- Add & Norm 块(加法与归一化块)。在这些块中,多头注意力块或前馈块的输出通过相加与残差合并,然后再进行一次层归一化。
由于输入到编码器段的要么是嵌入并进行了位置归一化的标记,要么是来自前一个编码器段的输出,因此编码器会生成一个考虑上下文的中间表示(即编码过程)。通过自注意力机制实现的上下文感知,Transformer能够发挥其魔力。
多头注意力机制
我们现在来详细看看编码器部分的各个组件。输入会经过一个多头注意力块。它由多个所谓的缩放点积注意力组成,我们现在来仔细看看这些注意力。
从视觉上看,按比例缩放的块看起来像这样(Vaswani et al., 2017)。
缩放点积注意力机制
你可以看到它有三个输入——分别代表查询(Q)、键(K)和值(V)。这意味着带位置编码的输入向量首先被拆分为三个独立的流,即形成三个矩阵;我们将看到,这通过三个不同的线性变换来实现。
在 Vaswani 等人 (2017) 的研究中,这些 Q 值、K 值和 V 值被描述为:
注意力函数可以描述为将一个查询和一组键值对映射到一个输出向量,其中查询、键、值和输出均是向量。输出是值的加权总和,其中,每个值的权重是通过查询与对应键的兼容性函数计算得出的。
Vaswani et al. (2017)
不过,Dontloo (2019) 给出了一个更直观的描述,
关键词、值和查询的概念来自检索系统。例如,当你在YouTube上搜索某个视频时输入查询,搜索引擎会将你的查询与数据库中候选视频相关联的关键词(如视频标题、描述等)进行匹配,然后向你展示最佳匹配的视频结果。
Dontloo (2019)
希望这能让你更清楚地了解查询、键和值的。在Transformer中的角色。
重要说明:在上面,我写的是 _vector_s 和 matrices ,因为所有的 token 都是并行处理的!这意味着所有的位置编码输入向量会通过这三个线性层,形成一个矩阵。理解这一点非常重要,即它们是被联合处理的,这样才能理解接下来通过得分矩阵进行自注意力机制的工作原理。
然而,如果我们真的想展示最匹配的视频,我们需要找出关注点(在给定某些输入的情况下,哪些视频最相关?)。这就是为什么在上面的图片中,你会看到查询和键之间有一个 MatMul
操作。这实际上是一个矩阵乘法,通过将查询输出与键矩阵相乘来生成一个分数矩阵。
评分矩阵可以是这样的:
它说明了短语中某个词的重要程度。然而,它们目前还不可以直接比较。传统上,可以使用Softmax函数(未来的博客文章)来生成(伪)概率,从而使这些值具有可比性。
然而,如果你看一下上面的流程图示,你可以看到在应用Softmax之前,我们首先应用了一个缩放函数。我们应用这个缩放函数是因为Softmax可能对梯度消失敏感,这是我们不想看到的。我们通过将所有值除以sqrt(d_k)来缩放,其中d_k是查询和键的维度dk。
我们接着计算Softmax结果,这立即显示出在某个词的上下文中哪些其他词是重要的。
最后一步是将带注意力权重的评分矩阵与值相乘,从而有效地保留那些模型认为最重要的值。
这是自注意力的工作方式,但经过了缩放处理——因此Vaswani等人(2017)将其称为缩放点积自注意力。
多个脑袋
然而,编码器块被称为多头注意力。这些“多头”又是指什么呢?让我们来看看视觉上的解释:
多头注意力机制通过复制用于生成查询、键和值矩阵的线性层,让它们拥有不同的权重,这样我们就可以学习这些查询、键和值的不同表示方式。
在人类的语言中,你可以把它想象成从不同的角度看同一个问题,而不仅仅是从一个角度(即我们刚刚提到的自我注意机制)。通过学习多种表示,Transformer 变得越来越善于理解上下文。如你所见,线性层的输出被发送到单独的缩放点积注意力模块中,这些模块输出值的权重;这些输出被连接起来并通过另一个线性层处理。
每个组件的配置称为一个注意力头。由于在一个编码器段中存在多个注意力头,这个块被称为多头注意力块。它为每个块执行缩放点积注意力操作,然后拼接所有输出,并通过一个线性层,再次生成一个512维输出。
注意,每个注意力头的维数是 d_{model}/h,其中 h 是注意力头的数量。Vaswani 等人 (2017) 使用了 512 的模型维数,并使用了 8 个平行的头,因此,每个注意力头的维数是 64(即 512/8 = 64)。
添加残差和层归一化多头注意力块的输出首先与残差连接相加,需要指出的是,残差连接指的是未经变换的原始输入嵌入向量。这是一个简单的加法操作。加完之后,执行一次layer normalization操作,然后传递给前向部分。应用层归一化稳定了训练过程,而添加残差连接也起到稳定作用。
前馈层
完成层归一化之后,输入传递到一系列前馈层。根据Vaswani等人(2017),每个令牌都会单独通过这个网络:“单独且相同地应用到每个位置”。每个前馈网络包含两个线性层,中间插入了一个ReLU激活函数(我们将在另一篇文章中详细讨论这些激活函数)。
添加残差连接和层归一化在前馈网络的情况下,同样会从输入中分支出一个残差,以便梯度顺畅流动。该残差将被添加到前馈网络的输出中,然后进行层归一化处理。
这是编码输入离开编码器部分之前的最后一个步骤。现在它可以在后续操作中被使用(例如在BERT中,我们会在另一篇文章中讲解)或作为原始Transformer解码部分的(部分)输入。我们会在这个系列的第二部分中解释Transformer。
(注:讨论比我预期的要长一些。因此,我把它分成两部分系列。)如果您感兴趣的话,请继续看Decoder部分的训练和Transformer的训练内容。如果您想了解更多,请继续看Decoder部分的训练和Transformer的训练内容。
总结Transformer模型正在自然语言处理界引起轰动。但它们的架构相当复杂,理解起来要花不少时间。这就是为什么在这篇文章里,我们探讨了Vaswani等人在2017年论文中提出的原始Transformer架构。
这种架构是当今所有与Transformer相关的活动的基础,解决了序列到序列模型中的最后一个难题:即如何进行序列处理。不再需要递归处理,这意味着网络可以受益于并行处理,从而大大加快了训练过程。如今的Transformer都是使用数以百万计的序列进行训练的,有时甚至更多。
为了提供必要的背景信息,我们首先介绍了Transformer是什么以及它们为何必要。接着我们继续研究了编码器模块。
在编码器部分,我们看到,输入首先通过一个(学习过的)输入嵌入,这将基于整数的token转换为低维度的向量。然后通过正弦和余弦函数进行位置编码,以向嵌入添加关于token相对位置的信息——这种信息在传统的序列模型中是自然存在的,但在并行处理中丢失了。经过这些准备步骤后,输入然后被送入编码器,编码器学习应用自注意力。换句话说,当特定单词被查看时,该模型学会识别短语中哪些部分重要。这是通过多头注意力机制和前向网络实现的。
在第二部分,我们将看到解码器部分以类似但略有不同的方式运行。首先,输出被嵌入并加了位置编码,随后再通过一个多头注意力块。然而,这个块在生成分数矩阵时应用了前瞻掩码,以确保模型在预测当前单词时不能看到后面的单词。换句话说,它只能参考过去的单词。随后,又添加了一个多头注意力块,将编码的输入作为查询和键,与注意力输出值结合在一起。这种组合被传递到一个前馈网络,最终通过一个额外的线性层和一个Softmax激活函数生成一个词元预测。
vanilla版本的Transformers 如果用于翻译任务的话,是基于双语数据集进行训练的。一个这样的数据集例子是 WMT 2014 英德翻译数据集,该数据集包含英文和德文的短语;Vaswani 等 (2014) 使用该数据集来训练他们的 Transformer。
希望你从今天的文章中学到一些东西。欢迎任何评论、问题或建议。谢谢阅读。如果你喜欢这种类型的内容,请点赞并关注。
参考资料维基百科。(2005年4月7日)。“循环神经网络”条目。来自维基百科。2020年12月23日访问https://en.wikipedia.org/wiki/Recurrent_neural_network。
Vaswani, A., Shazeer, N., Parmar, N., Uszkoreit, J., Jones, L., Gomez, A. N., … & Polosukhin, I. (2017). 《注意力就够了》. Advances in neural information processing systems , 30 , 5998–6008.
Nuric. (2018). Keras Tokenizer方法到底做了什么? Stack Overflow. https://stackoverflow.com/a/51956230
KDNuggets. (未知日期). 自然语言处理任务中的数据表示方法. KDnuggets. https://www.kdnuggets.com/2018/11/data-representation-natural-language-processing.html
维基百科。 (2014, 8月14日). 词向量。 自由百科全书维基百科。 从2020年12月24日访问 https://en.wikipedia.org/wiki/Word_embedding
Ncasas. (2020). Transformer模型各部分共享的权重. 数据科学Stack Exchange, https://datascience.stackexchange.com/a/86363
Dontloo. (2019). 在注意力机制中,“键”、“查询”和“值”到底是什么? Cross Validated. https://stats.stackexchange.com/a/424127
维基百科。(2002年10月22日)。矩阵乘法。自由的在线百科全书维基百科。2020年12月24日从https://en.wikipedia.org/wiki/Matrix_multiplication
斯坦福自然语言处理研究小组。 斯坦福自然语言处理研究小组。 https://nlp.stanford.edu/projects/nmt/