自2017年推出以来,转换器(Transformers)已成为机器学习领域的一支突出力量,彻底改变了专业翻译和自动完成服务的能力。
最近,随着OpenAI公司的ChatGPT和Meta公司的LLama等大型语言模型的出现,转换器的受欢迎程度飙升。所有上述这些模型都建立在转换器架构的基础上,引起了业界极大的关注。通过利用转换器的力量,这些模型在自然语言理解和生成方面取得了显著突破。
尽管目前网络上已经存储很多很好的资源可以解释转换器的工作方式,但我发现自己仅停留在一个理解转换器数学工作原理的层次上,却很难直观地解释转换器是如何工作的。在进行了多次采访,与我的同事交谈,并就这个问题进行了闪电式的(简短)演讲之后,我发现似乎很多人都存在这样的问题!
在这篇博客文章中,我将努力提供一个关于转换器如何在不依赖代码或数学原理的情况下工作原理的高级解释。我的目标是避免混淆技术术语,避免与以前的体系结构进行比较。虽然我会尽量保持简单,但这并不容易,因为转换器非常复杂,但我希望它能更好地直观地了解它们做什么以及如何做。
转换器是一种神经网络架构,非常适合处理序列作为输入的任务。在这种情况下,序列最常见的例子可能是一个句子,我们可以将其视为一组有序的单词。
这些模型的目的是为序列中的每个元素创建一个数字化表示,用于封装关于元素及其相邻上下文的基本信息。然后,可以将得到的数字表示传递给下游网络,下游网络可以利用这些信息来执行各种任务,包括生成和分类。
通过创建这样丰富的表示,这些模型使下游网络能够更好地理解输入序列中的潜在模式和关系,这增强了它们生成连贯和上下文相关输出的能力。
转换器的关键优势在于它们能够处理序列中的很长范围的依赖关系,并且效率很高;能够并行处理序列。这对于机器翻译、情感分析和文本生成等任务特别有用。
Azure OpenAI服务DALL-E模型生成的图像,其中带有以下提示:“The green and black Matrix code in the shape of Optimus Prime(擎天柱形状的绿色和黑色矩阵代码)”
要将输入馈送到转换器中,我们必须首先将其转换为标记序列——表示我们输入的一组整数。
由于转换器最初应用于自然语言处理领域,所以让我们首先考虑这个场景。将一个句子转换为一系列标记的最简单方法是定义一个词汇表,该词汇表充当查找表,将单词映射为整数;我们可以保留一个特定的数字来表示这个词汇表中不包含的任何单词,这样我们就可以总是分配一个整数值。
在实践中,这是一种过于简单的文本编码方式,因为cat和cats等词会被视为完全不同的标记,尽管它们是对同一动物的单数和复数描述!为了克服这一点,人们设计了不同的标记化策略,如字节对编码,在对单词进行索引之前,将其分解成更小的块。此外,添加特殊的标记来表示句子的开头和结尾等特征,为模型提供额外的上下文,这通常很有用。
让我们考虑下面的例子,以更好地理解标记化过程。
“Hello there, isn’t the weather nice today in Drosval?(你好,德罗斯瓦尔市今天天气好吗?)”
这里,Drosval是GPT-4使用以下提示生成的名称:“Can you create a fictional place name that sounds like it could belong to David Gemmell’s Drenai universe?(你能创建一个听起来可能属于David Gemmell的Drenai宇宙的虚构地名吗?)”;这是故意选择的,因为它不应该出现在任何训练过的机器模型的词汇表中。
借助转换器库中的 bert-base-uncased分词器,将上面的语句转换为以下标记序列:
表示每个单词的整数将根据特定的模型训练和标记化策略而变化。解码后,我们可以看到每个标记所代表的单词:
有趣的是,我们可以看到这与我们当初的输入不同。其中添加了一些特殊的标记,我们的缩写被拆分为多个标记,我们虚构的地名由不同的“块”表示。当我们使用前面所述的bert-base-uncased模型时,我们也失去了所有的大写上下文。
然而,虽然我们在示例中使用了一个句子,但转换器并不局限于文本输入;该体系结构在视觉任务上也取得了良好的效果。为了将图像转换为序列,ViT(译者注:是指转换器在CV领域中的两个经典算法之一,另一个算法是DeiT)的作者将图像切片为不重叠的16x16像素块,并在将其传递到模型中之前将其连接成长向量。如果我们在推荐系统中使用转换器,一种方法可以是使用用户浏览的最后n个项目的项目ID作为我们网络的输入。如果我们能够为我们的域创建一个有意义的输入标记表示,我们就可以将其输入到转换器网络中。
一旦我们有了一个整数序列来表示我们的输入,我们就可以将它们转换为嵌入。嵌入是一种表示信息的方式,可以通过机器学习算法轻松处理;他们的目的是通过将信息表示为一系列数字来捕捉以压缩格式编码的标记的含义。最初,嵌入被初始化为随机数序列,并且在训练期间学习有意义的表示。然而,这些嵌入有一个固有的限制:它们没有考虑到标记出现的上下文。这有两个方面。
一个问题是,根据任务的不同,当我们嵌入标记时,我们可能还希望保留标记的顺序;这在NLP等领域尤其重要;否则,我们基本上会采用单词袋方法。为了克服这一点,我们将位置编码应用于嵌入。虽然有多种方法可以创建位置嵌入,但主要思想是我们有另一组嵌入,它们表示输入序列中每个标记的位置,并与我们的标记嵌入相结合。
另一个问题是,根据周围的标记内容,标记可能会有不同的含义。考虑以下句子:
It’s dark, who turned off the light?(天黑了,谁关灯了?)
Wow, this parcel is really light!(哇,这个包裹真轻!)
在这里,“light”这个词被用于两个不同的上下文,在不同的上下文中它有完全不同的含义!然而,根据标记化策略,嵌入可能是相同的。在转换器中,这是由它的注意力机制来处理的。
转换器架构使用的最重要的机制可能是注意力,它使网络能够了解输入序列的哪些部分与给定任务最相关。对于序列中的每个标记,注意力机制识别哪些其他标记对于理解给定上下文中的当前标记很重要。在我们探索如何在转换器中实现这一点之前,让我们从简单的内容开始,试着理解注意力机制在概念上试图实现什么,以便建立我们的直觉理解基础。
理解注意力的一种方法是将其视为一种方法,该方法将每个标记嵌入替换为包含关于其相邻标记的信息的嵌入;而不是对每个标记使用相同的嵌入,而不管其上下文如何。如果我们知道哪些标记与当前标记相关,那么捕获此上下文的一种方法是创建这些嵌入的加权平均值,或者更一般地说,线性组合。
让我们考虑一个简单的例子,说明如何查找我们前面看到的一个句子。在应用注意力之前,序列中的嵌入没有其邻近的上下文。因此,我们可以将单词“light”的嵌入可视化为以下线性组合。
在这里,我们可以看到,我们的权重只是单位矩阵。在应用我们的注意力机制后,我们想学习一个权重矩阵,这样我们就可以用类似于下面的方式来表达我们的“light”嵌入。
这一次,对与我们选择的标记的序列的最相关部分相对应的嵌入赋予更大的权重;这应当确保在新的嵌入向量中捕获最重要的上下文。
包含当前上下文信息的嵌入有时被称为上下文嵌入,这最终是我们试图创建的。
既然我们已经对注意力试图实现的目标有了很高的理解,那么让我们在下一节中来探讨一下这是如何实际实现的。
注意力有多种类型,主要区别在于用于执行线性组合的权重的计算方式。在这里,我们来考虑一下原始论文中介绍的缩放点积注意力,因为这是最常见的方法。在本节中,假设我们所有的嵌入都已进行了位置编码。
回想一下,我们的目标是使用原始嵌入的线性组合来创建上下文嵌入,让我们从简单的讲解开始,假设我们可以将所需的所有必要信息编码到我们学习的嵌入向量中,我们所需要计算的只是权重。
要计算权重,我们必须首先确定哪些标记彼此相关。为了实现这一点,我们需要在两个嵌入之间建立一个相似性的概念。表示这种相似性的一种方法是使用点积,我们希望学习嵌入,这样得分越高,两个单词就越相似。
对于每个标记,我们需要计算其与序列中其他标记的相关性,我们可以将其推广为矩阵乘法,这为我们提供了权重矩阵;其通常被称为注意力得分。为了确保我们的权重总和为1,我们还应用SoftMax函数。然而,由于矩阵乘法可以产生任意大的数字,这可能导致SoftMax函数对于大的注意力分数返回非常小的梯度;这可能导致训练过程中的梯度消失问题。为了抵消这种影响,在应用SoftMax之前,将注意力分数乘以比例因子。
现在,为了得到我们的上下文嵌入矩阵,我们可以将注意力得分与原始嵌入矩阵相乘;这相当于我们的嵌入的线性组合。
简化的注意力计算:假设嵌入是位置编码的
虽然模型可能学习足够复杂的嵌入,以生成注意力得分和随后的上下文嵌入;我们试图将大量信息压缩到嵌入维度中,嵌入维度通常很小。
因此,为了让模型更容易学习这项任务,让我们介绍一些更容易学习的参数!与其直接使用嵌入矩阵,不如让它通过三个独立的线性层(矩阵乘法);这应该使模型能够“注意”嵌入的不同部分。如下图所示:
缩放后的点积自注意:假设嵌入是位置编码的
从图像中,我们可以看到线性投影被标记为Q、K和V。在最初的论文中,这些投影被命名为Query、Key和Value,据说是从信息检索中获得的灵感。就我个人而言,我从未发现这种类比有助于我的理解,所以我倾向于不关注这一点;为了与文献保持一致,我遵循了这里的术语,并明确表示这些线性层是不同的。
现在,我们了解了这个过程是如何工作的,我们可以把注意力计算看作一个有三个输入的单个块,这些输入将传递给Q、K和V。
当我们将相同的嵌入矩阵传递给Q、K和V时,这被称为自注意。
在实践中,我们经常并行使用多个自注意块,以便使转换器能够同时关注输入序列的不同部分——这被称为多头注意(multi-head attention)。
多头注意力背后的想法很简单,多个独立的自我注意力块的输出被连接在一起,然后通过线性层。这个线性层使模型能够学习组合来自每个注意力头部的上下文信息。
在实践中,每个自注意块中使用的隐藏维度大小通常被选择为原始嵌入大小除以注意头的数量;以保持嵌入矩阵的形状。
尽管介绍转换器的论文(现在臭名昭著)被命名为“注意力”,但这有点令人困惑,因为转换器的组件不仅仅是注意力!
其实,转换器块还包含以下内容:
虽然转换器架构自引入以来一直保持相当稳定,但层规范化块的位置可能因转换器架构而异。原始架构,现在称为后层规范(post-layer norm),如下所示:
如下图所示,在最近的体系结构中,最常见的放置是前层规范(pre-layer norm),它将规范化块放置在跳过连接中的自注意块和FFN块之前。
虽然现在有许多不同的转换器架构,但大多数可以分为三种主要类型。
编码器模型旨在产生可用于下游任务(如分类或命名实体识别)的上下文嵌入,因为注意力机制能够注意整个输入序列;这就是本文迄今为止所探讨的体系结构类型。最流行的编码器专用转换器系列是BERT及其变体。
在将我们的数据通过一个或多个转换器块之后,我们有一个复杂的上下文嵌入矩阵,表示序列中每个标记的嵌入。然而,要将其用于诸如分类之类的下游任务,我们只需要进行一次预测。传统上,第一个标记被获取,并通过分类头部;其通常包含Dropout层和Linear层。可以通过SoftMax函数将这些层的输出转换为类别概率。下面描述了一个这样的例子。
与编码器架构几乎相同,关键区别在于解码器架构采用了屏蔽(或因果)自注意层,因此注意机制只能注意输入序列的当前和先前元素;这意味着生成的上下文嵌入只考虑先前的上下文。流行的仅含解码器的模型包括GPT系列。
这通常是通过用二进制下三角矩阵屏蔽注意力得分,并用负无穷大替换未屏蔽的元素来实现的;当通过以下SoftMax操作时,这将确保这些位置的注意力得分等于零。我们可以更新我们以前的自我注意图,将其包括在内,如下所示:
屏蔽自注意计算:假设采用位置编码嵌入
由于解码器只能从当前位置向后参与计算,因此解码器架构通常用于自回归任务,如序列生成等。然而,当使用上下文嵌入来生成序列时,与使用编码器相比,还有一些额外的考虑因素。下面显示了一个示例。
我们可以注意到,虽然解码器为输入序列中的每个标记生成上下文嵌入,但在生成序列时,我们通常使用与最终标记相对应的嵌入作为后续层的输入。
此外,在将SoftMax函数应用于logits之后,如果不应用过滤方案,我们将在模型词汇表中的每个标记上接收概率分布;这可能非常大!通常,我们希望使用各种过滤策略来减少潜在选项的数量,其中一些最常见的方法是:
最初,转换器是作为机器翻译的一种架构提出的,并使用编码器和解码器来实现这一目标;使用所述编码器来创建中间表示。虽然编码器-解码器转换器已经变得不那么常见,但诸如T5之类的架构展示了如何将诸如问题回答、总结和分类之类的任务构建为序列到序列的问题,并使用这种方法来解决。
与编码器-解码器架构的关键区别在于,解码器使用编码器-解码器注意力,其在注意力计算期间使用编码器的输出(作为K和V)和解码器块的输入(作为Q)。这与自注意形成对比,在自注意中,相同的输入嵌入矩阵用于所有输入。除此之外,整个生成过程与仅使用解码器的架构非常相似。
我们可以将编码器-解码器架构可视化,如下图所示。在这里,为了简化图示,我选择描绘原始论文中所见的转换器的后层规范的变体;其中规范层位于注意块之后。
总之,希望本文能够给您提供一种关于转换器工作原理的直觉理解帮助,有助于您以一种易于理解的方式把握此架构中的一些细节,并成为揭开现代转换器架构神秘面纱的良好起点!