图像由 DALL-E 生成
Transformer 彻底改变了机器学习的格局,从根本上改变了我们处理序列数据的方式。自 2017 年首次亮相以来,这些模型已成为自然语言处理和图像识别的黄金标准。它们是 AI 领域最令人印象深刻的创新之一:大型语言模型 (LLM) 背后的驱动力。在本文中,您将深入研究 Transformer 背后的数学原理,掌握其架构并了解其工作原理。请坚持到最后,因为我们将使用 Python 从头开始?构建一个 Transformer。让我们深入探索现代机器学习中最强大的模型之一。
指数
· 1. 简介
° 1.1 背景
° 1.2 什么是 Transformer?
· 2. Transformer 的架构
° 2.1 整体结构
° 2.2 编码器
° 2.3 解码器
° 2.4 注意力机制
· 3. Transformer 背后的数学
° 3.1 注意力机制
° 3.2 多头注意力
° 3.3 位置前馈网络
° 3.4 层规范化和残差连接
° 3.5 位置编码
° 3.6 编码器
° 3.7 解码器
· 4. 从头开始??实现 Transformer
° 4.1. 多头注意力
° 4.2. 位置编码
° 4.3. 前馈网络
° 4.4.编码器层
° 4.5. 解码器层
° 4.6. Transformer 模型
° 4.7. 前向传递
· 5. 结论
·参考文献
1. 简介
1.1 背景
Transformer 的旅程始于 2017 年,当时 Vaswani 和他的团队发表了论文《注意力就是你所需要的一切》。该模型的出现是为了解决循环神经网络 (RNN) 和长短期记忆网络 (LSTM) 等旧模型所面临的问题,这些模型难以处理序列数据中的长距离依赖关系。
此后,Transformer 的发展经历了几个关键里程碑。2018 年,谷歌推出了BERT(Transformer 的双向编码器表示),通过引入预训练和微调方法,彻底改变了自然语言处理 (NLP) 任务。次年,OpenAI 发布了GPT-2 ,展示了 Transformer 在生成连贯且上下文相关的文本方面的强大功能。2020 年,随着GPT-3的推出,Transformer 的发展又迈进了一步,GPT-3 拥有 1750 亿个参数,在语言理解和生成方面显示出显着的改进。大约在同一时间,Vision Transformers (ViTs) 将 Transformer 模型扩展到图像处理,在图像分类中取得了最先进的结果。
Transformer 与 RNN 和 LSTM 等旧模型有着根本区别。与一次处理一个序列的 RNN 和 LSTM 不同,Transformer 使用自注意力机制来同时处理整个序列。这种并行处理能力使 Transformer 更高效,更善于捕捉长程依赖关系。此外,Transformer 不必费力解决梯度消失问题,从而可以实现更深层次的架构,并提高复杂任务的性能。
在本文中,我们将深入研究 Transformer 架构。虽然您不需要先验知识即可继续阅读,但强烈建议您先了解一下。
1.2 什么是Transformer?
Transformer 是一种深度学习模型,旨在处理顺序数据,因此在自然语言处理任务中尤其有用。Transformer 的独特之处在于其自注意力机制。该机制允许模型确定不同序列部分的重要性,无论它们位于何处。这种专注于输入序列中最相关部分的能力已为各种 NLP 任务带来了重大进步。
Transformer 的应用范围非常广泛。在自然语言处理中,Transformer 用于语言翻译、文本摘要、情感分析、问答系统和聊天机器人等任务。在图像处理中,Vision Transformer (ViT) 彻底改变了图像分类和对象检测。除了这些领域之外,Transformer 还用于预测蛋白质折叠、时间序列预测和构建推荐系统。
可以将 Transformer 视为一个组织良好的项目团队。每个团队成员(或模型的一部分)都有特定的角色,并且知道如何专注于手头任务的最重要方面,无论他们是在项目时间表中何时引入的。这种协调和优先处理信息的能力使 Transformer 在处理和理解复杂数据序列方面异常有效。
2. Transformer 的架构
2.1 总体结构
带有编码器(左)和解码器(右)的 Transformer 架构 — 图片来自作者
想象一下,您正在组织一场有数百名嘉宾参加的大型活动。您不可能一下子跟踪每个人的需求和偏好,因此您可以将其分解为较小的任务并将它们委托给不同的团队。这类似于转换器模型处理信息的方式。它围绕编码器-解码器框架构建,旨在通过一种称为自注意力的巧妙机制高效处理顺序数据并捕获远程依赖关系。
编码器和解码器是转换器的两个主要组件。编码器可以看作是一个团队,负责处理所有客人信息并将其组织成有意义的类别。它获取输入序列并创建编码,以突出显示输入的哪些部分彼此相关。另一方面,解码器就像负责生成最终活动计划的团队,使用来自编码器的信息并关注先前做出的决策,逐步生成输出序列。
如果你查看本节开头的 Transformer 架构图,你会看到左侧有一堆编码器,右侧有一堆解码器。每个编码器和解码器由多个相同的层组成,通常有 6 到 12 个。每个编码器层都有两个主要子层:多头自注意机制和位置完全连接的前馈网络。每个子层都有残差连接和层规范化,有助于稳定和加快训练过程。解码器层类似,但有一个额外的多头注意子层,允许它们关注编码器的输出。
2.2 编码器
编码器架构 — 作者提供的图片
编码器由几个关键组件组成,它们共同处理输入序列并生成有意义的表示。这些组件包括多头注意力、前馈神经网络和带有残差连接的层归一化。
编码器的核心是多头注意力机制。想象一下,它是一组专家,每个专家都专注于输入序列的不同方面。多头注意力机制不是由单个专家组成,而是将输入拆分为多个较小的注意力头。每个头独立地将输入投射到查询、键和值向量中,计算注意力分数,并合并结果。然后,所有头的输出被连接起来并进行转换以产生最终结果。此过程允许模型捕获输入序列中的各种关系。
遵循多头注意力机制,编码器包括一个位置完全连接的前馈神经网络。这部分就像一个分析师,负责获取专家提供的信息并对其进行提炼。它由两个线性变换组成,中间有一个 ReLU 激活函数,在序列中的每个位置上独立运行。这有助于将与会者信息转换为更丰富的表示,为模型添加非线性,并使其能够学习复杂的模式。
编码器中的每个子层(包括多头注意力和前馈网络)后面都有残差连接和层归一化。残差连接(也称为跳跃连接)允许梯度直接流过网络,从而有助于缓解梯度消失问题。这是通过将子层的输入添加到其输出来实现的。然后,层归一化通过对子层的输出进行归一化来稳定和加快训练过程,确保值的分布保持一致。
2.3 解码器
解码器架构 — 作者提供的图片
Transformer 中的解码器与编码器有许多相似之处,但也引入了一些关键差异,以促进其在生成输出序列中的作用。与编码器一样,解码器由多个相同的层组成。这些层中的每一个都包含包括多头注意力和前馈神经网络的子层。编码器和解码器都使用残差连接和层规范化来增强学习并保持稳定的梯度。
然而,编码器和解码器之间存在着重大差异。主要差异是在每个解码器层中增加了第三个子层,称为掩码多头注意力机制。这个额外的子层允许解码器专注于先前生成的标记,同时保持序列生成的自回归特性。此外,解码器的多头注意力机制旨在处理两个信息源:输入序列(来自编码器)和部分生成的输出序列。
掩蔽多头注意力机制是解码器独有的。它的工作原理与编码器中使用的标准多头注意力类似,但包括额外的掩蔽步骤。这种掩蔽确保解码器在训??练期间无法提前查看序列中的未来标记,从而保留模型的自回归性质。在注意力计算期间,将掩码应用于输入序列,将未来标记的注意力分数设置为负无穷大。这有效地防止模型在生成当前标记时考虑未来标记,确保每个标记仅基于过去的标记和编码的输入序列生成。
解码器的多头注意力子层(包括屏蔽多头注意力和标准多头注意力,关注编码器的输出)共同生成输出序列。屏蔽多头注意力允许模型一次生成一个标记,而标准多头注意力则整合来自编码器的信息,使解码器能够产生连贯且上下文准确的输出。
2.4 注意力机制
多头注意力架构——作者提供的图片
注意力机制是 Transformer 如此有效的核心创新。它允许模型动态地关注输入序列的不同部分,使其能够捕获依赖关系,而不管它们在序列中的距离如何。Transformer 中的注意力机制主要有两种:自注意力和交叉注意力。
自注意力机制又称为内部注意力机制,用于编码器和解码器层。在自注意力机制中,序列中的每个元素都会关注所有其他元素,包括其自身。这意味着,例如,句子中的每个单词都可以考虑其他每个单词,以构建更丰富的上下文表示。
另一方面,交叉注意力机制用于解码器处理编码器的输出。这种机制允许解码器在生成输出序列中的每个标记的同时,关注输入序列的相关部分。在交叉注意力机制中,解码器序列的每个元素都会关注编码器输出的元素,整合来自输入序列的信息以产生最终输出。
自注意力和交叉注意力的核心计算都是缩放点积注意力。它的工作原理如下:
首先,将每个输入元素投影到三个向量中:查询 (Q)、键 (K) 和值 (V)。然后将查询向量与键向量相加以计算注意力分数,该分数表示对输入的不同部分给予多少关注。这些点积按键向量维度的平方根缩放,以在训练期间稳定梯度。然后,缩放后的分数通过 softmax 函数将其转换为概率,强调输??入中最相关的部分。最后,用这些注意力分数加权值向量并相加以产生每个输入元素的最终输出。
想象一下,你正在尝试理解一个故事,其中有多个角色和事件同时发生。自我注意力可以帮助你考虑每个角色和事件,了解其他每个角色和事件,从而让你全面了解故事。另一方面,交叉注意力就像专注于故事的具体细节,以确保你在进步的过程中将最相关的信息融入到你的理解中。
3. Transformer 背后的数学
现在让我们深入研究所涉及的数学过程,从输入开始,按照每个步骤到最终的输出。
3.1 注意力机制
正如我们之前所说,注意力机制决定了序列中的每个单词应该给予其他每个单词多少关注。这从计算注意力分数开始。对于输入中的每个单词,我们需要确定它应该给予其他每个单词多少关注。这是使用三个矩阵完成的:查询 (Q)、键 (K)和值 (V)。
首先使用学习到的权重矩阵W_Q、W_K和W_V将输入序列中的每个单词投影到这三个矩阵中:
Q、K 和 V 矩阵计算 — 作者提供的图片
这里,X是输入序列矩阵,其中每一行对应一个词嵌入。例如,如果你有一个包含 10 个单词的句子,并且每个单词由一个 512 维向量表示,则X将是一个 10x512 矩阵。将输入序列X与这些权重矩阵相乘会将每个词嵌入投影到三个新空间(查询、键和值),为注意力机制奠定基础。
接下来,我们通过对查询向量Q与关键字向量K进行点积来计算注意力分数。这可以衡量查询和关键字向量之间的相似性:
注意力得分公式 — 作者图片
此操作生成一个原始注意力得分矩阵,其中每个元素代表一对单词之间的注意力得分。具体来说:
- Q是从XW_Q获得的查询矩阵。Q 中的每一行代表序列中每个单词的查询向量。
- K是从XW_K得到的Key矩阵。K 中的每一行代表序列中每个单词的 Key 向量。
- K^T是Key矩阵的转置。转置K会交换其行和列,这样我们就可以计算与Q 的点积。
因此,上述公式的结果是一个注意力得分矩阵。这个矩阵中的每个元素代表输入序列中一对单词之间的注意力得分,表示一个单词应该给予另一个单词多少关注
然而,随着向量维数的增加,点积值会变得非常大,导致 softmax 运算期间的梯度很小。为了解决这个问题,我们将注意力得分按关键向量d_k维数的平方根缩放:
注意力评分公式 — 作者提供的图片
因此,如果每个关键向量都有 64 个维度,那么关键向量维度的平方根将是 sqrt{64} ?=8。此缩放因子有助于稳定梯度并使 softmax 函数更有效。
然后,我们将 softmax 函数应用于这些缩放分数。softmax 函数将分数转换为概率,确保它们的总和为 1,这突出了单词之间最重要的关系:
注意力权重公式 — 作者图片
softmax 函数对缩放分数进行归一化。当我们绘制 softmax 函数时,这一点更加清晰:
Softmax 图 — 作者提供的图片
其中softmax的函数为:
Softmax 函数 — 作者图片
Softmax 对每个缩放分数求幂,然后除以求幂分数的总和,将分数转换为概率。这会强调最相关的词,同时确保所有概率之和为 1。
注意力机制的最后一步是使用这些注意力权重来计算值向量V的加权和:
注意力输出公式 — 作者提供的图片
这里,Attention Weights是从 softmax 函数获得的注意力权重矩阵。每个元素代表关注特定单词的概率。V是从XW_V获得的值矩阵。V 中的每一行代表序列中每个单词的值向量。
注意力权重与V相乘可得出值向量的加权和。此输出是每个单词的新表示,包含整个输入序列的上下文。
3.2 多头注意力
Transformer 不使用单一的注意力分数集,而是使用多头注意力。这意味着输入序列被分成多个较小的部分,每个部分都通过上面描述的注意力机制独立处理。这些部分中的每一个都称为一个头。
对于每个头部,我们有不同的权重矩阵W_Q^i、W_K^i和W_V^i 。处理完每个头部后,我们将它们的输出连接起来,并使用另一组权重W_O将它们投影回单个向量空间:
多头注意力公式 — 作者提供的图片
这种多头注意力机制使得模型能够同时关注序列的不同部分,从而捕捉到广泛的依赖关系。
3.3 位置前馈网络
在多头注意力机制之后,每个单词的表征将由位置前馈网络进一步处理。该网络对每个单词的表征独立运行,并对每个表征应用相同的变换。
前馈网络由两个线性变换组成,中间有一个 ReLU 激活函数:
前馈网络公式 — 作者提供的图片
这里,x是注意力机制的输入,W_1和W_2是权重矩阵,b_1和b_2是偏差。ReLU 激活函数引入了非线性,使网络能够学习更复杂的模式。
ReLU 图 — 作者提供的图片
事实上,ReLU 就像一个守门人,它让正值不经改变地通过,但阻止负值,将其变为零。
请参阅我的文章“神经网络背后的数学”来了解有关 FNN 的更多信息。
3.4 层归一化和残差连接
为了稳定和加快训练,编码器(多头注意力和前馈网络)中的每个子层都后面跟着一个残差连接和层规范化。
残差连接有助于缓解梯度消失问题,并允许训练更深层的网络。基本思想是将子层的输入添加到其输出中。从数学上讲,它可以表示为:
层归一化公式 — 作者提供的图片
这里:
- x是子层的输入(可以是多头注意力机制,也可以是前馈网络)。
- Sublayer(x)是处理x之后子层的输出。这可以是多头注意力机制或前馈网络的输出。
- x + Sublayer(x)是残差连接,它将原始输入x添加到子层的输出。此添加有助于梯度更容易地流过网络,防止它们变得太小(梯度消失问题)或太大(梯度爆炸问题)。
通过将输入x添加到子层的输出中,模型确保即使子层变换引入了显著的变化,也能保留原始信息。
层归一化应用于残差连接的组合输出,以使值标准化。此过程有助于稳定训练,确保每层的输入具有一致的值分布。层归一化的公式为:
通用层归一化公式 — 作者提供的图片
让我们分解一下这些术语:
- z是层归一化的输入,在本上下文中为x + Sublayer(x)。
- mu是输入z的平均值,计算如下:
均值公式 — 作者图片
其中N是z中元素的数量(隐藏层的维度)。
- sigma是输入z的标准差,计算如下:
标准差公式 — 作者图片
- epsilon是为了数值稳定性而添加的一个小常数,以防止被零除。
- gamma是一个学习到的缩放参数,允许模型调整标准化值。
- beta是一个学习到的转变参数,允许模型调整标准化值。
归一化的输出确保每个子层接收均值为 0、方差为 1 的输入,这有助于保持稳定的梯度并提高训练过程的效率和效果。
3.5 位置编码
Transformer 会同时处理序列中的所有 token,而不是像循环模型 (RNN) 那样逐个处理。虽然这种并行处理效率很高,但这意味着模型本身并不了解 token 的顺序。为了让模型了解位置,我们使用了位置编码。这种编码会将序列中每个 token 的位置信息添加到其嵌入中。
位置编码是在编码器和解码器堆栈处理输入嵌入之前添加到输入嵌入的数学表示。最常见的方法是使用不同频率的正弦和余弦函数来生成这些编码。位置编码的公式为:
对于偶数索引2i:
索引 2i(顶部)和 2i+1(底部)的位置编码 — 作者提供的图片
这里:
- pos是标记在序列中的位置(从 0 开始)。
- i是位置编码的维度索引。
- d_model是模型输入嵌入的维度,与位置编码的维度相同。
这些正弦和余弦函数确保序列中的每个位置都有唯一的编码。此外,使用不同的频率允许模型以平滑和连续的方式区分不同的位置。这对于模型有效地学习标记的相对位置非常重要。
我们来看一个维度较小的例子。假设d_model? 为 4。我们想要计算前几个位置(pos = 0, 1, 2)的位置编码。
对于pos =0:
索引 (0,0)、(0,1)、(0,2) 和 (0,3) 的位置编码 — 作者提供的图片
对于pos =1:
索引 (1,0)、(1,1)、(1,2) 和 (1,3) 的位置编码 — 作者提供的图片
对于pos =2:
索引 (2,0)、(2,1)、(2,2) 和 (2,3) 的位置编码 — 作者提供的图片
这些编码被添加到原始输入嵌入中,以使每个标记了解其在序列中的位置。通过使用具有不同频率的正弦和余弦函数,位置编码可确保每个位置都有唯一的表示,并且编码变化平滑,这有助于模型学习有效地区分不同的位置。
使用正弦和余弦函数有几个好处:
- 周期性:正弦和余弦函数具有周期性,这意味着它们会定期重复其值。这种周期性对于捕获不同长度序列中标记的相对位置非常有用。
- 唯一编码:每个位置(pos)都有唯一的编码,因为每个维度的频率不同。这确保模型可以区分不同的位置。
- 平滑度:正弦和余弦函数的平滑、连续变化使得模型更容易学习位置编码中的模式。
3.6 编码器
编码器将输入序列转换为一组编码表示。首先,将输入序列X嵌入到高维空间中。此嵌入捕获了序列中每个单词的含义。为了帮助模型理解序列中每个单词的位置,我们在这些嵌入中添加了位置编码,从而得到X_pos:
组合序列公式 — 作者图片
接下来,这个组合序列被输入到多头注意力机制中。这使得模型可以同时关注序列的不同部分,并生成输出Z_attn:
多头注意力输出——作者提供的图片
然后,我们将原始输入X_pos添加回此输出(此过程称为残差连接),并应用层归一化来稳定训练。这给了我们Z_add_norm_1:
第一层规范化 — 作者提供的图片
下一步是将此结果传递给位置前馈网络,该网络应用额外的变换。该网络的输出是Z_ffn:
FFN 输出 — 作者提供的图片
我们再次将输入Z_add_norm_1添加回此输出并应用层规范化以获得Z_encoder_output:
第二层归一化后的编码器输出 — 作者提供的图片
最终输出Z_encoder_output是输入序列的编码表示,可供解码器处理。
3.7 解码器
解码器使用编码的输入和先前生成的标记生成输出序列。流程如下:
首先,输入序列Y(右移)被嵌入到高维空间中。让我们关注一下“右移”在这里的含义。
假设您有一个用于解码器的输入标记序列:
输入序列 — 作者提供的图片
当我们“右移”时,我们在开头插入一个特殊的起始标记(通常为 `
输入序列右移 — 作者提供的图片
然后,在训练期间,将此移位序列用作解码器的输入。这样做的目的是确保解码器只能使用已经生成(或预测)的标记,而不能使用任何未来的标记,从而保持模型的自回归特性。
因此,当您读到“右移”时,它意味着准备输入序列,以便模型学习根据先前生成的标记而不是之后的标记来预测每个标记。
接下来,我们将位置编码添加到这些嵌入中以形成Y_pos:
解码器输入的位置编码 — 作者提供的图片
此序列通过掩码多头注意力机制,可防止模型查看序列中的未来标记。输出为Z_masked_attn:
蒙版多头注意力机制 — 作者提供的图片
然后我们将原始输入 Y_pos 添加回此输出并应用层规范化以获得Z_add_norm_2:
层规范化 — 作者提供的图片
接下来,这个输出会经过交叉注意机制,让解码器关注来自编码器的编码输入序列的相关部分。结果是Z_cross_attn:
多头注意力输出——作者提供的图片
我们将输入Z_add_norm_2添加回此输出并应用层规范化,得到Z_add_norm_3:
第二层规范化 — 作者提供的图片
然后将该输出传递通过另一个位置前馈网络,产生Z_ffn_decoder:
FFN 输出 — 作者提供的图片
最后,我们将输入Z_add_norm_3添加回此输出并应用层归一化以获得最终的解码器输出 Z_decoder_output:
第三层归一化后的解码器输出 — 作者提供的图片
然后,该最终输出 Z_decoder_output 用于通过线性层和随后的 softmax 函数生成最终输出序列。
4. 从头开始??实现 Transformer
为了理解转换器的工作原理,让我们逐步分解 Python 代码。在本节中,我们将仅使用从头开始构建转换器架构Numpy。
4.1. 多头注意力
这和我们在“ Transformers 中多头注意力背后的数学”中构建的类是相同的,因此我们将简要介绍一下它。
类初始化
该类MultiHeadAttention使用几个参数进行初始化:
class MultiHeadAttention:
def __init__(self, num_hiddens, num_heads, dropout=0.0, bias=False):
self.num_heads = num_heads
self.num_hiddens = num_hiddens
self.d_k = self.d_v = num_hiddens // num_heads
self.W_q = np.random.rand(num_hiddens, num_hiddens)
self.W_k = np.random.rand(num_hiddens, num_hiddens)
self.W_v = np.random.rand(num_hiddens, num_hiddens)
self.W_o = np.random.rand(num_hiddens, num_hiddens)
if bias:
self.b_q = np.random.rand(num_hiddens)
self.b_k = np.random.rand(num_hiddens)
self.b_v = np.random.rand(num_hiddens)
self.b_o = np.random.rand(num_hiddens)
else:
self.b_q = self.b_k = self.b_v = self.b_o = np.zeros(num_hiddens)
这里,num_heads指定注意力头的数量,而表示num_hiddens隐藏层的维度。和d_k是d_v查询、键和值向量的维度,计算为num_hiddens // num_heads。权重矩阵W_q、、W_k和分别用于查询、键、值和输出投影。如果设置为,则可以添加可选的偏差向量、、和。
W_vW_ob_qb_kb_vb_obiasTrue
转置 QKV
这些方法处理 Q、K 和 V 矩阵的重塑和转置,以实现多头注意力:
def transpose_qkv(self, X):
X = X.reshape(X.shape[0], X.shape[1], self.num_heads, -1)
X = X.transpose(0, 2, 1, 3)
return X.reshape(-1, X.shape[2], X.shape[3])
def transpose_output(self, X):
X = X.reshape(-1, self.num_heads, X.shape[1], X.shape[2])
X = X.transpose(0, 2, 1, 3)
return X.reshape(X.shape[0], X.shape[1], -1)
该transpose_qkv方法将输入矩阵拆分并转置为多个头部。相反,transpose_output恢复转置并将不同头部的输出连接回单个矩阵。
缩放点积注意力
计算注意力分数并应用 softmax 函数:
def scaled_dot_product_attention(self, Q, K, V, valid_lens):
d_k = Q.shape[-1]
scores = np.matmul(Q, K.transpose(0, 2, 1)) / np.sqrt(d_k)
if valid_lens is not None:
mask = np.arange(scores.shape[-1]) < valid_lens[:, None]
scores = np.where(mask[:, None, :], scores, -np.inf)
attention_weights = np.exp(scores - np.max(scores, axis=-1, keepdims=True))
attention_weights /= attention_weights.sum(axis=-1, keepdims=True)
return np.matmul(attention_weights, V)
在此方法中,Q、K和V代表查询、键和值矩阵。valid_lens参数是注意力分数的可选掩码。 该方法通过对分数进行归一化并应用 softmax 函数来确保它们之和为 1,从而计算缩放的点积注意力。
前向
传递通过多头注意机制处理前向传递:
def forward(self, queries, keys, values, valid_lens):
queries = self.transpose_qkv(np.dot(queries, self.W_q) + self.b_q)
keys = self.transpose_qkv(np.dot(keys, self.W_k) + self.b_k)
values = self.transpose_qkv(np.dot(values, self.W_v) + self.b_v)
if valid_lens is not None:
valid_lens = np.repeat(valid_lens, self.num_heads, axis=0)
output = self.scaled_dot_product_attention(queries, keys, values, valid_lens)
output_concat = self.transpose_output(output)
return np.dot(output_concat, self.W_o) + self.b_o
在该forward方法中,查询、键和值使用它们各自的权重矩阵和偏差进行投影。transpose_qkv然后应用该方法为多头注意力准备输入。如果valid_lens提供,则重复执行以匹配头的数量。该
scaled_dot_product_attention方法计算注意力分数,并使用输出权重矩阵和偏差连接和投影输出。
4.2. 位置编码
位置编码为模型提供了有关序列中标记位置的信息。由于 Transformer 模型没有循环或卷积来推断序列顺序,因此位置编码对于为模型提供一些标记位置概念至关重要。该positional_encoding函数的工作原理如下:
def positional_encoding(seq_len, d_model):
pos = np.arange(seq_len)[:, np.newaxis]
i = np.arange(d_model)[np.newaxis, :]
angle_rates = 1 / np.power(10000, (2 * (i // 2)) / np.float32(d_model))
pos_encoding = pos * angle_rates
pos_encoding[:, 0::2] = np.sin(pos_encoding[:, 0::2])
pos_encoding[:, 1::2] = np.cos(pos_encoding[:, 1::2])
return pos_encoding
该函数positional_encoding旨在为序列中的标记生成位置编码,为转换器提供位置信息。让我们逐步分解这个函数:
pos = np.arange(seq_len)[:, np.newaxis]
i = np.arange(d_model)[np.newaxis, :]
- pos是一个形状的数组(seq_len, 1),其中每一行对应于序列中一个标记的位置。
- i是一个形状数组(1, d_model),其中每一列对应模型的维度。
angle_rates = 1 / np.power( 10000 , (2 * (i // 2))/ np.float32(d_model))
此数组定义位置编码的变化率。除以i2 可确保角度速率交替变化,这对于稍后使用的正弦和余弦函数是必需的。
pos_encoding = pos * angle_rates
位置编码是通过将位置pos与相乘来计算的angle_rates。这将生成一个矩阵,其中每个元素对应于特定位置和维度的编码。
pos_encoding[:, 0 :: 2 ] = np.sin(pos_encoding[:, 0 :: 2 ])
pos_encoding[:, 1 :: 2 ] = np.cos(pos_encoding[:, 1 :: 2 ])
此步骤交替将正弦和余弦函数应用于位置编码。正弦函数应用于偶数索引(0::2),余弦函数应用于奇数索引(1::2)。
结果是一个形状为 的矩阵(seq_len, d_model),其中每一行对应于序列中标记的位置编码。然后将这些编码添加到标记嵌入中,为模型提供有关每个标记位置的信息。
4.3. 前馈网络
该类FeedForward实现了一个带有 ReLU 激活的简单前馈神经网络。这种类型的网络用于 Transformer 架构中,以引入非线性并使模型能够学习复杂的表示。以下是该类的详细说明:
class FeedForward:
def __init__(self,d_model,d_ff):
self.W1 = np.random.randn(d_model,d_ff)* np.sqrt(2.0 /(d_model + d_ff))
self.b1 = np.zeros(d_ff)
self.W2 = np.random.randn(d_ff,d_model)* np.sqrt(2.0 /(d_ff + d_model))
self.b2 = np.zeros(d_model)
在初始化方法中__init__,将创建两个权重矩阵W1和W2以及两个偏差向量b1和b2。
- W1是一个形状为 的权重矩阵(d_model, d_ff),其中d_model是输入维度,d_ff是隐藏层维度。它使用输入和输出维度之和的倒数的平方根缩放的随机值进行初始化。
- b1是第一个线性变换的偏差向量,初始化为零,长度为d_ff。
- W2是一个形状为 的权重矩阵(d_ff, d_model),其中d_ff是隐藏层维度,d_model是输出维度。它也用类似缩放的随机值初始化。
- b2是第二个线性变换的偏差向量,初始化为零,长度为d_model。
def __call__ ( self, x ):
return self.forward(x)
def forward ( self, x ):
return np.dot(np.maximum( 0,np.dot(x,self.W1) + self.b1), self.W2) + self.b2
该__call__方法被定义为使FeedForward可调用的实例在内部调用该forward方法。
该forward方法通过前馈网络实现前向传递:
- x首先使用权重矩阵W1和偏差对输入进行线性变换b1。
- 对线性变换的结果应用 ReLU 激活函数。ReLU(整流线性单元)定义为np.maximum(0, x),它将所有负值清零,并保持正值不变。
- W2然后使用第二个权重矩阵和偏差对 ReLU 激活的结果进行线性变换b2。
最终的输出是第二次线性变换的结果,由该方法返回forward。
4.4. 编码器层
该类EncoderLayer将多头注意力机制与前馈神经网络相结合,构成了 Transformer 模型的核心构建块之一。这种组合允许模型关注输入序列的不同部分,同时通过前馈网络对所关注的特征进行转换。
class EncoderLayer:
def __init__(self,d_model,num_heads,d_ff,dropout= 0.0,bias=False):
self.d_model = d_model
self.num_heads = num_heads
self.d_ff = d_ff
self.multi_head_attention = MultiHeadAttention(d_model,num_heads,dropout,bias)
self.feed_forward = FeedForward(d_model,d_ff)
在该__init__方法中,该类使用几个关键参数进行初始化:
- d_model指定模型的维度。
- num_heads表示多头注意力机制中注意力头的数量。
- d_ff定义前馈网络中隐藏单元的数量。
- dropout和bias是用于添加 dropout 正则化和偏差项??的可选参数。
在此初始化过程中,设置了两个主要组件:
- multi_head_attention是类的一个实例MultiHeadAttention,它允许层关注输入序列的不同部分。
- feed_forward是类的一个实例FeedForward,它对焦点特征应用非线性变换。
此类还包括以下方法:
def __call__(self, x, mask=None):
return self.forward(x, mask)
def forward(self, x, mask=None):
attn_output = self.multi_head_attention.forward(x, x, x, mask)
output = self.feed_forward(attn_output)
return output
该__call__方法使EncoderLayer可调用的实例在内部委托给该forward方法。
在forward方法中:
- attn_output通过将多头注意力机制应用于输入来计算x。这涉及将输入投影到查询、键和值中,执行缩放的点积注意力,然后连接和投影结果。
- 注意力机制的结果attn_output随后会通过前馈网络。该网络涉及两个线性变换,中间有一个 ReLU 激活,用于添加非线性并转换关注的特征。
将其视为EncoderLayer一个复杂的过滤器。首先,它查看整个序列并确定哪些部分是重要的(多头注意力)。然后,它处理这些重要部分,使它们对模型更有意义和更有用(前馈网络)。该层在 Transformer 模型中重复多次,以建立对输入序列的深刻理解。
4.5. 解码层
该类DecoderLayer包括两个多头注意机制和一个前馈网络,使其能够对解码器的输入进行自注意,对编码器的输出进行交叉注意。
class DecoderLayer:
def __init__(self, d_model, num_heads, d_ff, dropout=0.0, bias=False):
self.d_model = d_model
self.num_heads = num_heads
self.d_ff = d_ff
self.multi_head_attention_1 = MultiHeadAttention(d_model, num_heads, dropout, bias)
self.multi_head_attention_2 = MultiHeadAttention(d_model, num_heads, dropout, bias)
self.feed_forward = FeedForward(d_model, d_ff)
在该__init__方法中,DecoderLayer该类使用几个参数进行初始化:
- d_model指定模型的维度。
- num_heads表示多头注意力机制中注意力头的数量。
- d_ff定义前馈网络中隐藏单元的数量。
- dropout和bias是 dropout 正则化和偏差项??的可选参数。
在初始化过程中,实例化了三个主要组件:
- multi_head_attention_1是MultiHeadAttention类的一个实例,用于对解码器的输入进行自我注意。
- multi_head_attention_2是该类的另一个实例MultiHeadAttention,用于交叉注意,其中解码器关注编码器的输出。
- feed_forward是类的一个实例FeedForward,它将非线性变换应用于所关注的特征。
def __call__(self, x, enc_output, mask=None):
return self.forward(x, enc_output, mask)
def forward(self, x, enc_output, mask=None):
attn_output1 = self.multi_head_attention_1.forward(x, x, x, mask)
attn_output2 = self.multi_head_attention_2.forward(attn_output1, enc_output, enc_output, mask)
output = self.feed_forward(attn_output2)
return output
该__call__方法使DecoderLayer可调用的实例在内部委托给该forward方法。
在forward方法中:
- attn_output1通过将第一个多头注意力机制(multi_head_attention_1)应用于输入来计算x。这种自注意力机制允许解码器关注其输入序列的不同部分。
- attn_output2通过将第二个多头注意力机制(multi_head_attention_2)应用于attn_output1和来计算enc_output。这种交叉注意力机制使解码器能够关注编码器的输出,整合来自编码输入序列的信息。
- 然后,交叉注意机制的结果attn_output2会通过feed_forward网络传递。该网络涉及两个线性变换,中间有一个 ReLU 激活,用于添加非线性并转换关注的特征。
将其视为DecoderLayer一个多任务助手。首先,它查看任务的当前状态并确定哪些部分需要注意(自我注意)。然后,它检查初始指令或指南(与编码器的输出进行交叉注意)以确保其在正确的轨道上。最后,它处理所有这些信息以产生连贯且上下文准确的输出,为序列中的下一步做好准备。
4.6. Transformer 模型
该类Transformer集成了编码器和解码器层,为序列到序列学习等任务创建了一个综合模型。该类旨在处理通过编码器和解码器的整个数据流,将输入转换为所需的输出,同时捕获数据中复杂的依赖关系。让我们深入了解该类的细节Transformer:
class Transformer:
def __init__(self, d_model, num_heads, d_ff, num_layers, input_vocab_size, target_vocab_size, max_seq_len):
self.d_model = d_model
self.num_heads = num_heads
self.d_ff = d_ff
self.num_layers = num_layers
self.input_vocab_size = input_vocab_size
self.target_vocab_size = target_vocab_size
self.max_seq_len = max_seq_len
self.encoder_layers = [EncoderLayer(d_model, num_heads, d_ff) for _ in range(num_layers)]
self.decoder_layers = [DecoderLayer(d_model, num_heads, d_ff) for _ in range(num_layers)]
self.embedding = np.random.randn(input_vocab_size, d_model) * np.sqrt(2.0 / (input_vocab_size + d_model))
self.pos_encoding = positional_encoding(max_seq_len, d_model)
self.output_layer = np.random.randn(d_model, target_vocab_size) * np.sqrt(2.0 / (d_model + target_vocab_size))
在该__init__方法中,Transformer该类初始化几个关键组件:
- d_model、、、num_heads和分别指定模型的维度、注意力头的数量、前馈网络中d_ff的num_layers隐藏单元的数量以及编码器和解码器层的数量。
- input_vocab_size并target_vocab_size定义输入和目标词汇的大小。
- max_seq_len指定位置编码的最大序列长度。
该模型包括:
- encoder_layers:实例列表EncoderLayer,每个实例代表编码器中的一层。
- decoder_layers:实例列表DecoderLayer,每个实例代表解码器中的一层。
- embedding:输入词汇的嵌入矩阵,用随机值初始化。
- pos_encoding:函数生成的位置编码positional_encoding。
- output_layer:用于将解码器输出投影到目标词汇量的一个权重矩阵。
def __call__(self, input_seq, target_seq, mask=None):
return self.forward(input_seq, target_seq, mask)
def forward(self, input_seq, target_seq, mask=None):
enc_output = self.encode(input_seq, mask)
dec_output = self.decode(target_seq, enc_output, mask)
output = np.dot(dec_output, self.output_layer)
return output
该__call__方法使Transformer类的实例可调用,并在内部委托给该forward方法。
在forward方法中:
- enc_output是通过将输入序列经过编码器获得的。
- dec_output是通过将目标序列和编码器输出经过解码器获得的。
- 最终结果output是通过使用投影解码器输出来计算的output_layer。
def encode(self, input_seq, mask=None):
seq_len = input_seq.shape[1]
x = self.embedding[input_seq] + self.pos_encoding[:seq_len, :]
for layer in self.encoder_layers:
x = layer(x, mask)
return x
该encode方法处理编码过程:
- 它将输入序列嵌入与位置编码相结合。
- 它将结果传递给编码器的每一层,依次应用每一层中定义的转换EncoderLayer。
def decode(self, target_seq, enc_output, mask=None):
seq_len = target_seq.shape[1]
x = self.embedding[target_seq] + self.pos_encoding[:seq_len, :]
for layer in self.decoder_layers:
x = layer(x, enc_output, mask)
return x
该decode方法管理解码过程:
- 它将目标序列嵌入与位置编码相结合。
- 它将结果和编码器输出传递到解码器的每一层,依次应用每层中定义的转换DecoderLayer。
4.7. 前向传播
首先,我们为变压器模型定义几个关键参数:
# Define some parameters
d_model = 512
num_heads = 8
d_ff = 2048
num_layers = 6
input_vocab_size = 10000
target_vocab_size = 10000
max_seq_len = 100
- d_model:模型的维度,设置为512。
- num_heads:注意力头的数量,设置为8。
- d_ff:前馈网络中隐藏单元的数量,设置为2048。
- num_layers:编码器和解码器的层数均为6。
- input_vocab_size:输入词汇表的大小,设置为10000。
- target_vocab_size:目标词汇表的大小,也设置为10000。
- max_seq_len:最大序列长度,设置为100。
我们Transformer用定义的参数实例化一个模型:
transformer = Transformer(d_model, num_heads, d_ff, num_layers, input_vocab_size, target_vocab_size, max_seq_len)
该模型包括编码器和解码器层,每层均包含多头注意力和前馈网络。嵌入和位置编码也在该模型中初始化。
为了模拟通过模型的数据流,我们生成虚拟输入和目标序列:
input_seq = np.random.randint(0, input_vocab_size, (32, 50))
target_seq = np.random.randint(0, target_vocab_size, (32, 50))
- input_seq和target_seq是随机生成的矩阵,形状为(32, 50),其中 32 表示批大小,50 表示序列长度。值是从相应词汇表大小中抽取的整数。
前向传递涉及通过变换器模型处理输入和目标序列以产生输出:
output = transformer(input_seq, target_seq)
print(output.shape) # Should be (batch_size, target_seq_len, target_vocab_size)
- 调用transformer(input_seq, target_seq)将输入和目标序列传递到编码器和解码器层。编码器处理输入序列,生成编码表示。解码器采用目标序列和编码表示来生成最终输出。
- 是output一个形状为 的张量(32, 50, 10000),表示对于目标序列(长度 50)中的每个位置,模型都会为每个批次(32 个样本)生成一个目标词汇表(大小 10000)的分布。
就是这样!现在您有了代码,请深入研究并开始尝试使用它进行序列到序列任务。使用它,调整它,看看你能取得什么惊人的结果。
5. 结论
Transformer 彻底改变了现代机器学习,尤其是在自然语言处理和图像识别领域。它们能够以惊人的效率和准确性处理序列数据,从而推动了各个领域的突破性进步。自注意力机制是 Transformer 成功的关键因素,它允许 Transformer 同时关注输入序列的不同部分。这种捕获长程依赖关系和并行处理序列的能力使 Transformer 有别于 RNN 和 LSTM 等旧模型。
Transformer 已成为众多应用的支柱,从语言翻译和文本摘要到图像分类,甚至蛋白质折叠预测。我鼓励您进一步探索和试验 Transformer。本文提供的代码为您提供了坚实的基础。尝试将其应用于不同的序列到序列任务,调整参数,并查看模型的性能。您试验得越多,您的理解就会越深刻,并且您在利用 Transformer 进行各种应用方面就会越熟练。
参考:
https://medium.com/@cristianleo120/the-math-behind-transformers-6d7710682a1f
https://arxiv.org/abs/1706.03762
https://arxiv.org/abs/1810.04805