【深度】万字图解,彻底理解 Transformer 架构

637 阅读17分钟

开场图·骑单车的男孩·摄于 2023年 文莱

写在前面

今天是 2024年的第一天。

在刚刚过去的一年我们有幸见证了 OpenAI's ChatGPT 引领的 AI 浪潮席卷全球。

众所周知,ChatGPT 是依托于 LLM (Large Language Model) 构建,如果说 LLM 是 ChatGPT 的皇冠,那么 Transformer 就是皇冠上最耀眼的宝石。

基于 Tranformer 架构的大语言模型

Transformer 整体架构

基本概念

关键词解释举例
TransformerTransformer 模型是由Google Brain团队的研究人员Vaswani等人在2017年提出的。这一模型的引入对自然语言处理和其他序列任务的处理方式产生了革命性的影响。Transformer已经成为众多自然语言处理任务的首选模型,如语言翻译、文本生成和语言理解等。
编码器 (Encoder)编码器是 Tranformer 的一个核心组件,负责将输入序列转化为一系列有意义的表示。输入嵌入(Input Embedding位置编码(Positional Encoding)多头自注意力层(Multi-Head Self-Attention)前馈神经网络层(Feedforward Neural Network)残差连接和层归一化(Residual Connection and Layer Normalization
解码器 (Decoder)解码器是 Tranformer 的另一个核心组件,负责将编码器产生的信息转换为目标序列。输入嵌入(Input Embedding位置编码(Positional Encoding)多头自注意力层(Multi-Head Self-Attention)多头编码-解码注意力层(Multi-Head Encoder-Decoder Attention Layer)前馈神经网络层(Feedforward Neural Network)残差连接和层归一化(Residual Connection and Layer Normalization
嵌入 (Embedding)嵌入是将高维度的离散数据映射到低维度连续向量空间的过程。在深度学习中,embedding通常指的是将离散的符号(如单词、类别等)映射到实数向量的过程。这种映射的目的是学习到数据的稠密表示,使得具有相似语义的数据在向量空间中更加接近。"king"映射到向量0.4,0.7,−0.10.4,0.7,−0.1,"queen"映射到向量0.5,0.7,−0.20.5,0.7,−0.2。这样,模型能够学到王室成员之间的语义关系。
分词 (Tokenization)分词是将文本或语言划分为更小单元(称为"token")的过程。在自然语言处理中,"token"通常是指语言中的基本单元,如单词、子词或字符。对于输入文本:"Tokenization is an important step in NLP."单词级别的Tokenization结果可能是:["Tokenization", "is", "an", "important", "step", "in", "NLP", "."]

图解内容

  1. 概览图解

我们首先将该模型视为一个黑匣子。在机器翻译应用程序中,它将采用一种语言的句子,并以另一种语言输出其翻译。

进一步拆解,我们看到一个编码组件、一个解码组件以及它们之间的连接。

编码器是一堆编码器(本文将其中六个编码器堆叠在一起)。编码器和解码器拥有相同的层数。

这些编码器在结构上都是相同的(但它们不共享权重)。每一层又分为两个子层:

编码器的输入首先流经自注意力层,该层帮助编码器在对特定单词进行编码时查看输入句子中的其他单词。我们将在本文后面仔细研究自我注意力。

自注意力层的输出被馈送到前馈神经网络。完全相同的前馈网络独立应用于每个位置。

解码器具有这两个层,但它们之间是一个注意力层,帮助解码器关注输入句子的相关部分(类似于 seq2seq 模型中注意力的作用)。

  1. 代入实际向量

现在我们已经了解了模型的主要组件,让我们开始了解各种向量/张量以及它们如何在这些组件之间流动以将训练模型的输入转换为输出。

  与 NLP 应用中的一般情况一样,我们首先使用嵌入算法将每个输入单词转换为向量。

  嵌入仅发生在最底部的编码器中。所有编码器共有的抽象是,它们接收每个大小为 512 的向量列表 - 在底部编码器中,这将是单词嵌入,但在其他编码器中,它将是直接位于下面的编码器的输出。这个列表的大小是我们可以设置的超参数——基本上它是我们训练数据集中最长句子的长度。

将单词嵌入到我们的输入序列中后,每个单词都会流经编码器的两层。

在这里,我们开始看到 Transformer 的一个关键属性,即每个位置的单词在编码器中都流经其自己的路径。自注意力层中的这些路径之间存在依赖关系。然而,前馈层不具有这些依赖性,因此各种路径可以在流经前馈层时并行执行。

接下来,我们将示例切换为较短的句子,并查看编码器的每个子层中发生的情况。

  1. 图解编码器

正如我们已经提到的,编码器接收向量列表作为输入。它通过将这些向量传递到“自注意力”层,然后传递到前馈神经网络,然后将输出向上发送到下一个编码器来处理该列表。

  1. 自注意力机制概览

假设以下句子是我们要翻译的输入句子:

The animal didn't cross the street because it was too tired

这句话中的“它”指的是什么?它指的是街道还是动物?这对人类来说是一个简单的问题,但对算法来说就不那么简单了。

当模型处理“it”这个词时,自注意力使其能够将“it”与“动物”联系起来。

当模型处理每个单词(输入序列中的每个位置)时,自注意力允许它查看输入序列中的其他位置以寻找有助于更好地编码该单词的线索。

如果你熟悉 RNN,请想想维护隐藏状态如何允许 RNN 将其先前处理过的单词/向量的表示与当前正在处理的单词/向量合并起来。自注意力是 Transformer 用来将其他相关单词的“理解”融入到我们当前正在处理的单词中的方法。

  1. 自注意力机制详解

我们首先看看如何使用向量计算自注意力,然后继续看看它是如何实际实现的——使用矩阵。

第一步

计算自注意力的第一步是从每个编码器的输入向量(在本例中为每个单词的嵌入)创建三个向量。因此,对于每个单词,我们创建一个查询向量、一个键向量和一个值向量。这些向量是通过将嵌入乘以我们在训练过程中训练的三个矩阵来创建的。

请注意,这些新向量的维度小于嵌入向量。它们的维度为 64,而嵌入和编码器输入/输出向量的维度为 512。它们不必更小,这是一种架构选择,可以使多头注意力的计算(大部分)保持恒定。

第二步

什么是“查询”、“键”和“值”向量?

查询"(Query)、"键"(Key)和"值"(Value)是三个重要的向量,它们用于计算注意力权重并生成上下文表示。这些概念通常出现在自注意力机制和编码-解码注意力机制中,如Transformer模型中。它们是对于计算和思考注意力有用的抽象。一旦你继续阅读下面的注意力是如何计算的,你就会知道几乎所有你需要知道的关于每个向量所扮演的角色。

计算自注意力的第二步是计算分数。假设我们正在计算本例中第一个单词“Thinking”的自注意力。我们需要根据输入句子的每个单词对这个单词进行评分。当我们在某个位置对单词进行编码时,分数决定了对输入句子的其他部分的关注程度。

分数是通过查询向量与我们要评分的各个单词的键向量的点积来计算的。因此,如果我们处理位置 #1 中单词的自注意力,第一个分数将是 q1 和 k1 的点积。第二个分数是 q1 和 k2 的点积。

第三步&第四步

第三步和第四步是将分数除以 8(论文中使用的关键向量维度的平方根 – 64。这会导致梯度更稳定。这里可能还有其他可能的值,但这是默认),然后将结果传递给 softmax 运算。 Softmax 对分数进行归一化,使它们全部为正值并且加起来为 1。

这个softmax分数决定了每个单词在这个位置上的表达量。显然,这个位置的单词将具有最高的 softmax 分数,但有时关注与当前单词相关的另一个单词是有用的。

第五步

第五步是将每个值向量乘以 softmax 分数(准备将它们相加)。这里的直觉是保持我们想要关注的单词的值完整,并淹没不相关的单词(例如,通过将它们乘以 0.001 这样的小数字)。

第六步

第六步是对加权值向量求和。这会在该位置(第一个单词)产生自注意力层的输出。

自注意力计算到此结束。得到的向量是我们可以发送到前馈神经网络的向量。然而,在实际实现中,该计算是以矩阵形式完成的,以便更快地处理。现在我们已经看到了单词级别计算的直观性,让我们来看看。

  1. 自注意力的矩阵计算

第一步 & 最后一步

第一步是计算查询、键和值矩阵。我们通过将嵌入打包到矩阵 X 中,并将其乘以我们训练过的权重矩阵(WQ、WK、WV)来实现这一点。

最后,由于我们处理的是矩阵,我们可以将第二步到第六步压缩为一个公式来计算自注意力层的输出。

  1. 多头注意力

论文通过添加一种称为“多头”注意力的机制进一步细化了自注意力层。这通过两种方式提高了注意力层的性能

它扩展了模型关注不同位置的能力。是的,在上面的示例中,z1 包含一些其他编码,但它可能由实际单词本身主导。如果我们翻译一个句子,比如“The Animal did not cross the street because it was tooert”,那么知道“it”指的是哪个单词会很有用。

它为注意力层提供了多个“表示子空间”。正如我们接下来将看到的,通过多头注意力,我们不仅拥有一组查询/键/值权重矩阵,而且拥有多组查询/键/值权重矩阵(Transformer 使用八个注意力头,因此我们最终为每个编码器/解码器提供八组) 。这些集合中的每一个都是随机初始化的。然后,在训练之后,每个集合用于将输入嵌入(或来自较低编码器/解码器的向量)投影到不同的表示子空间中。

如果我们进行与上面概述相同的自注意力计算,只是使用不同的权重矩阵进行八次不同的时间,我们最终会得到八个不同的 Z 矩阵。

这给我们带来了一些挑战。前馈层不需要八个矩阵——它需要一个矩阵(每个单词一个向量)。所以我们需要一种方法将这八个压缩成一个矩阵。

我们该怎么做呢?我们将矩阵连接起来,然后将它们乘以一个附加的权重矩阵 WO。

这几乎就是多头自注意力的全部内容。我意识到,这是相当多的矩阵。让我尝试将它们全部放在一个视觉效果中,以便我们可以在一个地方查看它们。

现在我们已经触及了注意力头,让我们回顾一下之前的示例,看看当我们在示例句子中对单词“it”进行编码时,不同的注意力头聚焦在哪里:

然而,如果我们将所有注意力头添加到图片中,事情可能会更难以解释:

  1. 位置编码

正如我们到目前为止所描述的,模型中缺少的一件事是一种解释输入序列中单词顺序的方法。

为了解决这个问题,转换器向每个输入嵌入添加一个向量。这些向量遵循模型学习的特定模式,这有助于确定每个单词的位置,或序列中不同单词之间的距离。这里的直觉是,一旦嵌入向量被投影到 Q/K/V 向量中以及在点积注意力期间,将这些值添加到嵌入中可以提供嵌入向量之间有意义的距离。

如果我们假设嵌入的维数为 4,则实际的位置编码将如下所示:

这个模式可能是什么样子?

下图中,每一行对应一个向量的位置编码。因此,第一行将是我们添加到输入序列中第一个单词的嵌入中的向量。每行包含 512 个值 - 每个值都在 1 到 -1 之间。我们对它们进行了颜色编码,以便图案可见。

  1. 残差 (The Residuals)

在继续之前,我们需要提及编码器架构中的一个细节,即每个编码器中的每个子层(自注意力,ffnn)周围都有一个残差连接,并且后面是层归一化步骤。

如果我们要可视化与自注意力相关的向量和层范数操作,它看起来像这样:

这也适用于解码器的子层。如果我们考虑一个由 2 个堆叠编码器和解码器组成的 Transformer,它看起来会是这样的:

  1. 图解解码器

现在我们已经涵盖了编码器方面的大部分概念,我们基本上也知道了解码器的组件是如何工作的。但让我们看看它们是如何协同工作的。

编码器首先处理输入序列。然后,顶部编码器的输出被转换为一组注意力向量 K 和 V。这些向量将由每个解码器在其“编码器-解码器注意力”层中使用,这有助于解码器关注输入序列中的适当位置:

以下步骤重复该过程,直到到达特殊符号,指示变压器解码器已完成其输出。每个步骤的输出在下一个时间步骤中被馈送到底部解码器,并且解码器像编码器一样冒泡其解码结果。就像我们对编码器输入所做的那样,我们将位置编码嵌入并添加到这些解码器输入中以指示每个单词的位置。

解码器中的自关注层的运行方式与编码器中的运行方式略有不同:

在解码器中,自注意力层只允许关注输出序列中较早的位置。这是通过在自注意力计算中的 softmax 步骤之前屏蔽未来位置(将它们设置为 -inf )来完成的。

“编码器-解码器注意力”层的工作方式与多头自注意力类似,只不过它从其下面的层创建查询矩阵,并从编码器堆栈的输出中获取键和值矩阵。

  1. 归一化 (SoftMax)

解码器堆栈输出浮点数向量。我们如何把它变成一个词?这就是最后一个 Linear 层的工作,后面是 Softmax 层。

线性层是一个简单的全连接神经网络,它将解码器堆栈产生的向量投影到一个更大的向量中,称为 logits 向量。

假设我们的模型知道从训练数据集中学习的 10,000 个独特的英语单词(我们模型的“输出词汇”)。这将使 logits 向量有 10,000 个单元格宽——每个单元格对应一个唯一单词的分数。这就是我们解释线性层模型输出的方式。

然后,softmax 层将这些分数转换为概率(全部为正,全部加起来为 1.0)。选择概率最高的单元格,并生成与其关联的单词作为该时间步的输出。

补充

前向传递

现在我们已经通过训练有素的 Transformer 介绍了整个前向传递过程,了解一下训练模型的直觉会很有用。

在训练期间,未经训练的模型将经历完全相同的前向传递。但由于我们是在标记的训练数据集上对其进行训练,因此我们可以将其输出与实际的正确输出进行比较。

为了形象化这一点,我们假设我们的输出词汇仅包含六个单词(“a”、“am”、“i”、“thanks”、“student”和“”(“句子结尾”的缩写))

一旦我们定义了输出词汇表,我们就可以使用相同宽度的向量来表示词汇表中的每个单词。这也称为 one-hot 编码。例如,我们可以使用以下向量表示单词“am”:

在回顾之后,让我们讨论模型的损失函数——我们在训练阶段优化的指标,以形成经过训练的、希望非常准确的模型。

损失函数

假设我们正在训练我们的模型。假设这是我们训练阶段的第一步,我们正在用一个简单的例子来训练它——将“merci”翻译成“thanks”。

这意味着我们希望输出是表示“谢谢”一词的概率分布。但由于该模型尚未经过训练,因此目前还不太可能发生。

但请注意,这是一个过于简单化的示例。更现实的是,我们将使用比一个单词长的句子。例如 – 输入:“je suis étudiant”,预期输出:“我是一名学生”。这真正的意思是,我们希望我们的模型能够连续输出概率分布,其中:

  • 每个概率分布由宽度 vocab_size 的向量表示(在我们的玩具示例中为 6,但更实际的是 30,000 或 50,000 等数字)
  • 第一个概率分布在与单词“i”相关的单元格中具有最高概率
  • 第二个概率分布在与单词“am”相关的单元格中具有最高概率
  • 依此类推,直到第五个输出分布指示“ <end of sentence> ”符号,该符号也有一个来自 10,000 个元素词汇表的与其关联的单元格。

在足够大的数据集上训练模型足够长的时间后,我们希望生成的概率分布如下所示:

现在,由于模型一次产生一个输出,因此我们可以假设模型从该概率分布中选择概率最高的单词,并丢弃其余的单词。这是一种方法(称为贪婪解码)。另一种方法是保留最上面的两个单词(例如“I”和“a”),然后在下一步中运行模型两次:一次假设第一个输出位置是单词“I”,另一次假设第一个输出位置是单词“a”,并且考虑到位置#1和#2,保留产生较少错误的版本。我们对位置 #2 和 #3 等重复此操作。这种方法称为“束搜索”,在我们的示例中,beam_size 为 2(意味着在任何时候,内存中都会保存两个部分假设(未完成的翻译)),top_beams 也是 2(意味着我们将返回两个翻译) )。这些都是您可以试验的超参数。

总结

以上,我们通过详细图解的方式,拆解了 Tranformer 架构的每个模块、每个步骤。旨在为大模型爱好者提供入门的指引,如有纰漏,欢迎大家指正。 2024 年希望能依然热爱人工智能。

恭喜爱学习的你,看到了最后。如能顺手点赞关注,不胜感激。 最后祝新年快乐吧~