在 TensorFlow 和 Keras 中从零开始实现 Transformer 编码器

在了解了如何实现 Transformer 模型的缩放点积注意力多头注意力之后,让我们通过应用其编码器来进一步实现一个完整的 Transformer 模型。我们的最终目标仍然是将整个模型应用于自然语言处理(NLP)。

在本教程中,您将了解如何在 TensorFlow 和 Keras 中从头开始实现 Transformer Encoder。 

完成本教程后,您将了解:

  • 构成 Transformer Encoder 的层。
  • 如何从头开始实现 Transformer Encoder。   

通过我的书《Transformer 模型与注意力机制》来启动您的项目。它提供了自学教程可运行的代码,指导您构建一个功能齐全的 Transformer 模型,该模型可以
将句子从一种语言翻译成另一种语言的完整 Transformer 模型...

让我们开始吧。 

在 TensorFlow 和 Keras 中从头开始实现 Transformer Encoder
照片由 ian dooley 拍摄,部分权利保留。

教程概述

本教程分为三个部分;它们是:

  • Transformer 架构回顾
    • Transformer Encoder
  • 从头开始实现 Transformer Encoder
    • 全连接前馈神经网络和层归一化
    • Encoder 层
    • Transformer Encoder
  • 测试代码

先决条件

本教程假设您已熟悉以下内容:

Transformer 架构回顾

回想一下,Transformer 架构遵循编码器-解码器结构。左侧的编码器负责将输入序列映射到一系列连续表示;右侧的解码器接收编码器的输出以及前一个时间步的解码器输出,以生成输出序列。

Transformer 架构的编码器-解码器结构
摘自“Attention Is All You Need

在生成输出序列时,Transformer 不依赖于循环和卷积。

您已经看到 Transformer 的解码器部分在其架构上与编码器有很多相似之处。在本教程中,您将重点关注构成 Transformer Encoder 的组件。 

Transformer Encoder

Transformer Encoder 由 $N$ 个相同的层组成,每一层又包含两个主要子层。

  • 第一个子层包含一个接收查询、键和值作为输入的自注意力机制。
  • 第二个子层包含一个全连接前馈网络。 

Transformer 架构的 Encoder 块
摘自“Attention Is All You Need

在每个子层之后,都应用了层归一化,其中子层的输入(通过残差连接)和输出被馈入。每个层归一化步骤的输出如下:

LayerNorm(子层输入 + 子层输出)

为了实现这种涉及子层输入和输出之间加法的操作,Vaswani 等人设计了模型中的所有子层和嵌入层,使其输出维度为 $d_{\text{model}}$ = 512。

另外,回想一下,查询、键和值是 Transformer Encoder 的输入。

在这里,查询、键和值承载了经过嵌入并增强了位置信息后的相同输入序列,其中查询和键的维度为 $d_k$,值的维度为 $d_v$。

此外,Vaswani 等人还通过在每个子层(层归一化步骤之前)的输出以及在位置编码馈入编码器之前应用 Dropout 来引入正则化。 

现在让我们看看如何在 TensorFlow 和 Keras 中从头开始实现 Transformer Encoder。

想开始构建带有注意力的 Transformer 模型吗?

立即参加我的免费12天电子邮件速成课程(含示例代码)。

点击注册,同时获得该课程的免费PDF电子书版本。

在 TensorFlow 和 Keras 中从头开始实现 Transformer Encoder

全连接前馈神经网络和层归一化

让我们从创建图中所示的*前馈*和*Add & Norm*层的类开始。

Vaswani 等人指出,全连接前馈网络由两个线性变换组成,中间有一个 ReLU 激活。第一个线性变换的输出维度为 $d_{ff}$ = 2048,第二个线性变换的输出维度为 $d_{\text{model}}$ = 512。

为此,让我们首先创建 FeedForward 类,它继承自 Keras 的 Layer 基类,并初始化密集层和 ReLU 激活。

我们将添加 call() 类方法,该方法接收输入,将其通过两个带 ReLU 激活的全连接层,并返回维度为 512 的输出。

下一步是创建另一个类 AddNormalization,它也继承自 Keras 的 Layer 基类,并初始化一个层归一化层。

在该方法中,包含将子层的输入和输出相加并对结果应用层归一化的类方法。

Encoder 层

接下来,您将实现 Encoder 层,Transformer Encoder 将对其进行 $N$ 次相同的复制。 

为此,让我们创建 EncoderLayer 类,并初始化它包含的所有子层。

在这里,您可能会注意到您已经初始化了 FeedForwardAddNormalization 类的实例,这些类刚刚在上一节中创建,并将它们的输出分别赋给了变量 feed_forwardadd_norm(1 和 2)。Dropout 层不言自明,其中 rate 定义了输入单元被设置为 0 的频率。您在之前的教程中创建了 MultiHeadAttention 类,如果您将代码保存到单独的 Python 脚本中,请不要忘记 import 它。我将我的代码保存在一个名为 *multihead_attention.py* 的 Python 脚本中,因此我需要包含代码行 *from multihead_attention import MultiHeadAttention*。

现在让我们继续创建 call() 类方法,该方法实现所有 Encoder 子层。

除了输入数据外,call() 方法还可以接收填充掩码。简要回顾一下在之前的教程中提到的内容,*填充*掩码是为了防止输入序列中的零填充与实际输入值一起被处理。 

同样的方法也可以接收一个 training 标志,当设置为 True 时,Dropout 层只在训练时应用。

Transformer Encoder

最后一步是为 Transformer Encoder 创建一个类,该类应命名为 Encoder

Transformer Encoder 接收输入序列,该序列在经过词嵌入和位置编码过程后得到。为了计算位置编码,让我们利用 Mehreen Saeed 在这篇教程中描述的 PositionEmbeddingFixedWeights 类。

正如您在前面的部分中所做的那样,在这里,您还将创建一个 call() 类方法,该方法将词嵌入和位置编码应用于输入序列,并将结果馈送给 $N$ 个 Encoder 层。

完整的 Transformer Encoder 代码列表如下:

测试代码

我们将使用 Vaswani 等人 (2017) 的论文 《Attention Is All You Need》中指定的参数值。

至于输入序列,在您到达在另一个教程中 训练完整的 Transformer 模型 的阶段之前,您将使用模拟数据,届时您将使用实际的句子。

接下来,您将创建一个 Encoder 类的实例,将其输出分配给 encoder 变量,然后输入输入参数,并打印结果。您暂时将 padding_mask 参数设置为 None,但当您实现完整的 Transformer 模型时会再回头处理它。

将所有内容结合起来,生成以下代码清单

运行此代码将生成一个形状为 (batch size, sequence length, model dimensionality) 的输出。请注意,由于输入序列的随机初始化和 Dense 层的参数值,您看到的输出可能会有所不同。 

进一步阅读

如果您想深入了解,本节提供了更多关于该主题的资源。

书籍

论文

总结

在本教程中,您学习了如何从头开始在 TensorFlow 和 Keras 中实现 Transformer 编码器。

具体来说,你学到了:

  • 构成 Transformer 编码器的层
  • 如何从头开始实现 Transformer 编码器

你有什么问题吗?
在下面的评论中提出您的问题,我将尽力回答。

学习 Transformer 和注意力!

Building Transformer Models with Attention

教您的深度学习模型阅读句子

...使用带有注意力的 Transformer 模型

在我的新电子书中探索如何实现
使用注意力机制构建 Transformer 模型

它提供了自学教程可运行代码,指导您构建一个可以
将句子从一种语言翻译成另一种语言的完整 Transformer 模型...

为理解人类语言提供神奇力量
您的项目


查看内容

, , ,

5 条对“从头开始在 TensorFlow 和 Keras 中实现 Transformer 编码器”的回复

  1. Ashwanth Kumar D 2023年1月23日晚上9:42 #

    你好 Stefania,
    感谢这篇精彩的文章。我还在阅读《使用注意力构建 Transformer 模型》这本书。我有一个关于“Transformer 的位置编码”的第 14.4 章的问题。

    在这里,我不太理解为什么您要使用

    word_embedding_matrix = self.get_position_encoding(vocab_size, output_dim)

    来初始化 word_embedding_matrix,引用论文“Attention Is All You Need”。

    我不太明白这个“get_position_encoding”方法是如何将单词表示为嵌入的。您能帮我解答一下吗?

    谢谢!!

    • Adrian Tam
      Adrian Tam 2023年1月24日凌晨2:34 #

      这可能有点令人困惑,但您可以尝试将其视为一种生成随机矩阵的方式。词嵌入矩阵只是用于编码单词(大约有 10,000 个),将其映射到更短的向量(例如,50 个浮点数)。我们不希望两个不相关的、不同的单词共享相同的向量。因此,最好将嵌入矩阵随机化,但也要保证它不会“冲突”。碰巧位置编码满足这个属性,因此在这里被滥用了。

      事实上,您也可以使用 numpy.random 来生成一个随机矩阵。但在这种情况下,最好也设置“trainable=True”,让 Keras 进行微调,以避免不必要的冲突。

  2. Ashwanth Kumar D 2023年1月24日凌晨5:10 #

    好的,现在明白了。感谢您的澄清,Adrian。MachineLearningMastery 始终是我学习数据科学的最佳导师。继续加油!!!

  3. Bashir 2023年8月8日晚上6:29 #

    您好,老师,我正在尝试将上述代码应用于单变量风力发电数据。是否可以修改上述代码以用于风力发电(时间序列)数据?

    • James Carmichael 2023年8月9日上午10:04 #

      您好 Bashir…是的,这是可能的。请继续,如果您有任何问题,请告诉我们!

Leave a Reply

Machine Learning Mastery 是 Guiding Tech Media 的一部分,Guiding Tech Media 是一家领先的数字媒体出版商,专注于帮助人们了解技术。访问我们的公司网站以了解更多关于我们的使命和团队的信息。