对注意力研究的总体概述
原文:
machinelearningmastery.com/a-birds-eye-view-of-research-on-attention/
注意力是一个在多个学科中科学研究的概念,包括心理学、神经科学,以及最近的机器学习。虽然各个学科可能对注意力有不同的定义,但它们都一致认为,注意力是使生物和人工神经系统更具灵活性的机制。
在本教程中,你将发现关于注意力研究进展的概述。
完成本教程后,你将了解到:
-
对不同科学学科具有重要意义的注意力概念
-
注意力如何在机器学习中引发革命,特别是在自然语言处理和计算机视觉领域
启动你的项目,请参阅我的书籍 《使用注意力构建变换器模型》。它提供了自学教程和工作代码,指导你构建一个完全可用的变换器模型。
将句子从一种语言翻译成另一种语言…
让我们开始吧。
对注意力研究的总体概述
图片由 Chris Lawton 提供,部分权利保留。
教程概述
本教程分为两个部分,分别是:
-
注意力的概念
-
机器学习中的注意力
-
自然语言处理中的注意力
-
计算机视觉中的注意力
-
注意力的概念
对注意力的研究源于心理学领域。
对注意力的科学研究始于心理学,通过细致的行为实验可以精准展示注意力在不同情况下的倾向和能力。
– 心理学、神经科学和机器学习中的注意力,2020 年。
从这些研究中得出的观察结果可以帮助研究人员推断出这些行为模式背后的心理过程。
尽管心理学、神经科学以及最近的机器学习领域都对注意力有各自的定义,但有一个核心特质对所有领域都具有重要意义:
注意力是对有限计算资源的灵活控制。
– 心理学、神经科学和机器学习中的注意力,2020 年。
鉴于此,接下来的部分将回顾注意力在引领机器学习领域革命中的角色。
机器学习中的注意力
机器学习中的注意力概念非常松散地受到人脑注意力心理机制的启发。
注意力机制在人工神经网络中的使用出现了——就像大脑中对注意力的明显需求一样——作为使神经系统更加灵活的一种手段。
– 心理学、神经科学与机器学习中的注意力,2020。
这个想法是能够处理一种能够在输入可能具有不同长度、大小或结构,甚至处理几个不同任务的人工神经网络。正是在这种精神下,机器学习中的注意力机制被认为是从心理学中获得灵感的,而不是因为它们复制了人脑的生物学。
在最初为人工神经网络(ANNs)开发的注意力形式中,注意力机制在编码器-解码器框架和序列模型的背景下工作……
– 心理学、神经科学与机器学习中的注意力,2020。
编码器的任务是生成输入的向量表示,而解码器的任务是将这个向量表示转换为输出。注意力机制将二者连接起来。
已经有不同的神经网络架构提议实现注意力机制,这些架构也与其应用的特定领域相关。自然语言处理(NLP)和计算机视觉是最受欢迎的应用之一。
自然语言处理中的注意力
在自然语言处理(NLP)中,早期的注意力应用是机器翻译,其目标是将源语言中的输入句子翻译为目标语言中的输出句子。在这个背景下,编码器会生成一组上下文向量,每个词一个。解码器则读取这些上下文向量,以逐字生成目标语言中的输出句子。
在没有注意力的传统编码器-解码器框架中,编码器生成一个固定长度的向量,该向量与输入的长度或特征无关,并且在解码过程中保持静态。
– 心理学、神经科学与机器学习中的注意力,2020。
使用固定长度向量表示输入在处理长序列或结构复杂的序列时尤为棘手,因为这些序列的表示维度必须与较短或较简单序列的表示维度相同。
例如,在某些语言中,如日语,最后一个词可能对预测第一个词非常重要,而将英语翻译成法语可能更容易,因为句子的顺序(句子的组织方式)更相似。
– 心理学、神经科学和机器学习中的注意力,2020 年。
这造成了一个瓶颈,即解码器对由输入提供的信息的访问受到限制,即在固定长度编码向量内可用的信息。另一方面,在编码过程中保持输入序列的长度不变可以使解码器能够灵活地利用其最相关的部分。
注意机制是如何运作的。
注意帮助确定应该使用这些向量中的哪一个来生成输出。由于输出序列是逐个元素动态生成的,注意力可以在每个时间点动态突出显示不同的编码向量。这使得解码器能够灵活地利用输入序列中最相关的部分。
– 第 186 页,深度学习基础,2018 年。
在早期的机器翻译工作中,试图解决由固定长度向量引起的瓶颈问题的工作之一是由Bahdanau et al. (2014)完成的。在他们的工作中,Bahdanau 等人使用递归神经网络(RNNs)进行编码和解码任务:编码器采用双向 RNN 生成一系列注释,每个注释包含前后单词的摘要,可以通过加权和映射到上下文向量;解码器然后基于这些注释和另一个 RNN 的隐藏状态生成输出。由于上下文向量是通过注释的加权和计算得到的,因此 Bahdanau 等人的注意机制是软注意的一个例子。
另一项早期工作是由Sutskever et al. (2014)完成的。他们选择使用多层长短期记忆(LSTM)来编码表示输入序列的向量,并使用另一个 LSTM 来将该向量解码为目标序列。
Luong 等人(2015) 引入了全局与局部注意力的概念。在他们的工作中,他们将全局注意力模型描述为在推导上下文向量时考虑编码器的所有隐藏状态。因此,全局上下文向量的计算基于所有源序列中的词的加权平均。Luong 等人提到,这在计算上是昂贵的,并且可能使全局注意力难以应用于长序列。局部注意力被提出以解决这个问题,通过专注于每个目标词的源序列中的较小子集。Luong 等人解释说,局部注意力在计算上比软注意力更便宜,但比硬注意力更易于训练。
更近期,Vaswani 等人(2017)提出了一种完全不同的架构,已经引导了机器翻译领域的一个新方向。这个被称为Transformer的架构完全舍弃了递归和卷积,但实现了自注意力机制。源序列中的词首先被并行编码以生成键、查询和值表示。键和值被组合以生成注意力权重,从而捕捉每个词与序列中其他词的关系。这些注意力权重随后用于缩放值,以便保持对重要词的关注,并消除无关的词。
输出是通过对值的加权求和来计算的,其中分配给每个值的权重是通过查询与相应键的兼容性函数计算的。
– 《Attention Is All You Need》,2017 年。
Transformer 架构
摘自《Attention Is All You Need》
当时,提出的 Transformer 架构为英语到德语和英语到法语的翻译任务建立了新的最先进的过程。报告称,它的训练速度也比基于递归或卷积层的架构更快。随后,由Devlin 等人(2019)提出的方法 BERT 基于 Vaswani 等人的工作,提出了一个多层双向架构。
如我们很快将看到的那样,Transformer 架构的接受不仅在 NLP 领域迅速增长,而且在计算机视觉领域也迅速扩展。
计算机视觉中的注意力
在计算机视觉中,注意力机制已经在多个应用领域找到了它的位置,例如在图像分类、图像分割和图像描述领域。
例如,如果我们需要将编码器-解码器模型重新构建用于图像描述任务,那么编码器可以是一个卷积神经网络(CNN),它将图像中的显著视觉线索转化为向量表示。解码器则可以是一个 RNN 或 LSTM,将向量表示转换为输出。
此外,正如神经科学文献中所述,这些注意力过程可以分为空间注意力和基于特征的注意力。
– 心理学、神经科学和机器学习中的注意力,2020。
在空间注意力中,不同的空间位置被赋予不同的权重。然而,这些相同的权重在不同的空间位置的所有特征通道中保持不变。
一种基于空间注意力的基本图像描述方法由Xu et al. (2016)提出。他们的模型将 CNN 作为编码器,提取一组特征向量(或注释向量),每个向量对应于图像的不同部分,以便解码器能够有选择地关注特定的图像部分。解码器是一个 LSTM,根据上下文向量、先前的隐藏状态和先前生成的单词生成描述。Xu et al.研究了将硬注意力作为计算其上下文向量的软注意力的替代方法。在这里,软注意力在源图像的所有区域上柔和地施加权重,而硬注意力则只关注单个区域,同时忽略其余部分。他们报告说,在他们的工作中,硬注意力表现更好。
图像描述生成模型
摘自《展示、注意和讲述:带有视觉注意力的神经图像描述生成》
相较之下,特征注意力允许各个特征图赋予自身的权重值。一个这样的例子,亦应用于图像描述,是Chen et al. (2018)的编码器-解码器框架,它在同一个 CNN 中结合了空间和通道注意力。
与 Transformer 迅速成为 NLP 任务的标准架构类似,它最近也被计算机视觉领域采纳并加以改编。
最早这样做的工作是由 Dosovitskiy 等人 (2020) 提出的,他们将 Vision Transformer (ViT) 应用于图像分类任务。他们认为,长期以来对 CNN 的依赖并不是必要的,纯变换器也可以完成同样的任务。Dosovitskiy 等人将输入图像重塑为一系列展平的 2D 图像补丁,然后通过可训练的线性投影将其嵌入,以生成 补丁嵌入。这些补丁嵌入与 位置嵌入 一起,以保留位置信息,被输入到变换器架构的编码器部分,编码器的输出随后被输入到多层感知机 (MLP) 进行分类。
Vision Transformer 架构
摘自《An Image is Worth 16×16 Words: Transformers for Image Recognition at Scale》
受 ViT 启发,并且基于注意力的架构在建模视频中的长程上下文关系时直观有效,我们开发了几个基于变换器的视频分类模型。
– ViViT: A Video Vision Transformer,2021 年。
Arnab 等人 (2021) 随后将 ViT 模型扩展为 ViViT,该模型利用视频中的时空信息进行视频分类任务。他们的方法探索了提取时空数据的不同方法,例如通过独立采样和嵌入每一帧,或提取不重叠的管段(一个跨越多个图像帧的图像补丁,形成一个 管道)并逐一嵌入。他们还研究了对输入视频的空间和时间维度进行分解的不同方法,以提高效率和可扩展性。
视频视觉变换器架构
摘自《ViViT: A Video Vision Transformer》
正确性 · Re
在首次应用于图像分类的情况下,Vision Transformer 已经被应用于多个其他计算机视觉领域,如 动作定位、注视估计 和 图像生成。这种计算机视觉从业者的兴趣激增,预示着一个激动人心的近未来,我们将看到更多对变换器架构的适应和应用。
想开始构建带有注意力机制的变换器模型吗?
现在就参加我的免费 12 天电子邮件速成课程(包含示例代码)。
点击注册,并获取课程的免费 PDF 电子书版本。
进一步阅读
本节提供了更多关于该主题的资源,如果你想深入了解。
书籍
- 深度学习要点,2018 年。
论文
-
心理学、神经科学和机器学习中的注意力,2020 年。
-
通过联合学习对齐和翻译的神经机器翻译,2014 年。
-
序列到序列学习与神经网络,2014 年。
-
基于注意力的神经机器翻译的有效方法,2015 年。
-
注意力机制是你所需的一切,2017 年。
-
BERT:深度双向变换器的预训练用于语言理解,2019 年。
-
展示、关注和讲述:使用视觉注意力的神经图像描述生成,2016 年。
-
SCA-CNN:用于图像描述的卷积网络中的空间和通道注意力,2018 年。
-
一张图像值 16×16 个词:用于大规模图像识别的变换器,2020 年。
-
ViViT:视频视觉变换器,2021 年。
示例应用:
-
时空动作定位中的关系建模,2021 年。
-
使用变换器的注视估计,2021 年。
-
ViTGAN:使用视觉变换器训练 GANs,2021 年。
总结
在本教程中,你了解了关于注意力的研究进展概述。
具体来说,你学到了:
-
注意力的概念对不同科学学科的重要性
-
注意力如何在机器学习中引发革命,特别是在自然语言处理和计算机视觉领域
你有什么问题吗?
请在下方评论中提出你的问题,我会尽力回答。
BERT 简介
正如我们了解了 变换器是什么 和我们可能如何 训练变换器模型,我们注意到它是让计算机理解人类语言的一个很好的工具。然而,变换器最初设计为一个将一种语言翻译成另一种语言的模型。如果我们将其重新用于其他任务,我们可能需要从头开始重新训练整个模型。考虑到训练变换器模型所需的时间非常长,我们希望有一个解决方案,可以使我们能够方便地重用训练好的变换器模型进行多种不同的任务。BERT 就是这样一个模型。它是变换器编码器部分的扩展。
在本教程中,你将了解什么是 BERT 并发现它能做什么。
完成本教程后,你将了解:
-
什么是来自变换器的双向编码表示(BERT)
-
BERT 模型如何被重新用于不同的目的
-
如何使用预训练的 BERT 模型
通过我的书 《构建具有注意力的变换器模型》 启动你的项目。它提供了自学教程和工作代码,指导你构建一个完全可用的变换器模型。
将句子从一种语言翻译成另一种语言…
让我们开始吧。
BERT 简介
图片来源:Samet Erköseoğlu,保留部分权利。
教程概述
本教程分为四个部分;它们是:
-
从变换器模型到 BERT
-
BERT 能做什么?
-
使用预训练的 BERT 模型进行摘要
-
使用预训练的 BERT 模型进行问答
先决条件
在本教程中,我们假设你已经熟悉:
从变换器模型到 BERT
在变换器模型中,编码器和解码器连接在一起形成一个 seq2seq 模型,以便你可以执行翻译任务,例如从英语到德语,正如你之前所见。回想一下注意力方程式说:
attention ( Q , K , V ) = softmax ( Q K ⊤ d k ) V \text{attention}(Q,K,V) = \text{softmax}\Big(\frac{QK^\top}{\sqrt{d_k}}\Big)V attention(Q,K,V)=softmax(dkQK⊤)V
但是上述的 Q Q Q、 K K K 和 V V V 都是通过变换器模型中的权重矩阵转换得到的嵌入向量。训练一个变换器模型意味着找到这些权重矩阵。一旦权重矩阵被学习到,变换器就成为一个语言模型,这意味着它代表了一种理解你用来训练它的语言的方式。
Transformer 架构的编码器-解码器结构
转换器具有编码器和解码器部分。顾名思义,编码器将句子和段落转换为理解上下文的内部格式(一个数值矩阵),而解码器则执行相反的操作。结合编码器和解码器使得转换器可以执行序列到序列的任务,例如翻译。如果你去掉转换器的编码器部分,它可以告诉你一些关于上下文的信息,这可能会带来一些有趣的东西。
双向编码器表示的转换器(BERT)利用注意力模型来更深入地理解语言上下文。BERT 是由多个编码器块堆叠而成。输入文本像在转换器模型中一样被分隔成标记,每个标记在 BERT 输出时会被转换成一个向量。
BERT 能做什么?
BERT 模型同时使用掩码语言模型(MLM)和下一个句子预测(NSP)进行训练。
BERT 模型
每个 BERT 训练样本是一对来自文档的句子。这两个句子可以是文档中的连续句子,也可以不是。第一个句子前会加上一个[CLS]
标记(表示类别),每个句子后会加上一个[SEP]
标记(作为分隔符)。然后,将两个句子拼接成一个标记序列,作为一个训练样本。训练样本中的一小部分标记会用特殊标记[MASK]
掩码或替换为随机标记。
在输入到 BERT 模型之前,训练样本中的标记将被转换成嵌入向量,并添加位置编码,特别是 BERT 还会添加段落嵌入以标记标记是来自第一句还是第二句。
BERT 模型的每个输入词将产生一个输出向量。在训练良好的 BERT 模型中,我们期望:
-
对于被掩码的词,输出结果可以揭示原始词是什么。
-
对应于
[CLS]
标记的输出可以揭示两个句子在文档中是否是连续的。
然后,BERT 模型中训练得到的权重可以很好地理解语言上下文。
一旦你拥有这样的 BERT 模型,你可以将其用于许多下游任务。例如,通过在编码器上添加一个适当的分类层,并仅将一句话输入模型而不是一对句子,你可以将类别标记[CLS]
作为情感分类的输入。这是因为类别标记的输出经过训练,可以聚合整个输入的注意力。
另一个例子是将一个问题作为第一句话,将文本(例如,一个段落)作为第二句话,然后第二句话中的输出标记可以标记出问题答案所在的位置。它有效的原因是每个标记的输出在整个输入的上下文中揭示了有关该标记的一些信息。
使用预训练的 BERT 模型进行摘要
从头开始训练一个 Transformer 模型需要很长时间。BERT 模型则需要更长的时间。但 BERT 的目的是创建一个可以用于多种不同任务的模型。
有一些预训练的 BERT 模型可以直接使用。接下来,你将看到一些使用案例。以下示例使用的文本来自:
理论上,BERT 模型是一个编码器,将每个输入标记映射到一个输出向量,这可以扩展到无限长度的标记序列。在实践中,其他组件的实现会施加限制,限制输入大小。通常,几百个标记应该是可以的,因为并非所有实现都能一次处理数千个标记。你可以将整篇文章保存为 article.txt
(一个副本可以在这里获取)。如果你的模型需要更小的文本,你可以只使用其中的几个段落。
首先,让我们探讨摘要任务。使用 BERT 的想法是从原始文本中 提取 几句话,这些句子代表整个文本。你可以看到这个任务类似于下一句预测,其中如果给定一句话和文本,你希望分类它们是否相关。
为此,你需要使用 Python 模块 bert-extractive-summarizer
pip install bert-extractive-summarizer
这是一些 Hugging Face 模型的包装器,用于提供摘要任务流水线。Hugging Face 是一个允许你发布机器学习模型的平台,主要用于 NLP 任务。
一旦你安装了 bert-extractive-summarizer
,生成摘要只需要几行代码:
from summarizer import Summarizer
text = open("article.txt").read()
model = Summarizer('distilbert-base-uncased')
result = model(text, num_sentences=3)
print(result)
这将产生以下输出:
Amid the political turmoil of outgoing British Prime Minister Liz Truss’s
short-lived government, the Bank of England has found itself in the
fiscal-financial crossfire. Whatever government comes next, it is vital
that the BOE learns the right lessons. According to a statement by the BOE’s Deputy Governor for
Financial Stability, Jon Cunliffe, the MPC was merely “informed of the
issues in the gilt market and briefed in advance of the operation,
including its financial-stability rationale and the temporary and targeted
nature of the purchases.”
这就是完整的代码!在幕后,spaCy 被用于一些预处理,而 Hugging Face 被用于启动模型。使用的模型名为 distilbert-base-uncased
。DistilBERT 是一个简化版的 BERT 模型,可以更快运行并使用更少的内存。该模型是一个“uncased”模型,这意味着输入文本中的大写或小写在转换为嵌入向量后被视为相同。
摘要模型的输出是一个字符串。由于你在调用模型时指定了num_sentences=3
,因此摘要是从文本中选择的三句话。这种方法称为提取式摘要。另一种方法是抽象式摘要,其中摘要是生成的,而不是从文本中提取的。这需要不同于 BERT 的模型。
想要开始构建带有注意力机制的变换器模型吗?
立即参加我的免费 12 天电子邮件速成课程(附样本代码)。
点击注册并获得课程的免费 PDF 电子书版本。
使用预训练 BERT 模型进行问答
使用 BERT 的另一个示例是将问题与答案匹配。你将问题和文本都提供给模型,并从文本中寻找答案的开始 和 结束位置的输出。
一个快速的示例就是如下几行代码,重用前面示例中的相同文本:
from transformers import pipeline
text = open("article.txt").read()
question = "What is BOE doing?"
answering = pipeline("question-answering", model='distilbert-base-uncased-distilled-squad')
result = answering(question=question, context=text)
print(result)
在这里,直接使用了 Hugging Face。如果你已经安装了前面示例中使用的模块,那么 Hugging Face Python 模块是你已经安装的依赖项。否则,你可能需要用pip
进行安装:
pip install transformers
而且为了实际使用 Hugging Face 模型,你还应该安装both PyTorch 和 TensorFlow:
pip install torch tensorflow
上述代码的输出是一个 Python 字典,如下所示:
{'score': 0.42369240522384644,
'start': 1261,
'end': 1344,
'answer': 'to maintain or restore market liquidity in systemically important\nfinancial markets'}
在这里,你可以找到答案(即输入文本中的一句话),以及这个答案在标记顺序中的起始和结束位置。这个分数可以被视为模型对答案适合问题的置信度分数。
在后台,模型所做的是生成一个概率分数,用于确定文本中回答问题的最佳起始位置,以及最佳结束位置。然后通过查找最高概率的位置来提取答案。
进一步阅读
本节提供了更多关于该主题的资源,如果你想深入了解。
论文
总结
在本教程中,你发现了 BERT 是什么以及如何使用预训练的 BERT 模型。
具体来说,你学到了:
-
BERT 如何作为对变换器模型的扩展创建
-
如何使用预训练 BERT 模型进行提取式摘要和问答
变压器模型位置编码的温和介绍,第一部分
在语言中,单词的顺序及其在句子中的位置确实很重要。如果重新排列单词,整个句子的意义可能会发生变化。在实现自然语言处理解决方案时,递归神经网络有一个内置机制来处理序列的顺序。然而,变压器模型不使用递归或卷积,将每个数据点视为彼此独立。因此,模型中明确添加了位置编码,以保留句子中单词的顺序信息。位置编码是一种保持序列中对象顺序知识的方案。
在本教程中,我们将简化 Vaswani 等人那篇卓越论文中使用的符号,Attention Is All You Need。完成本教程后,你将了解:
-
什么是位置编码,为什么重要
-
变压器中的位置编码
-
使用 NumPy 在 Python 中编写并可视化位置编码矩阵
用我的书 Building Transformer Models with Attention 启动你的项目。它提供了自学教程和可运行的代码,指导你构建一个完全可运行的变压器模型
将句子从一种语言翻译成另一种语言…
让我们开始吧。
变压器模型中位置编码的温和介绍
照片由 Muhammad Murtaza Ghani 提供,来自 Unsplash,部分权利保留
教程概述
本教程分为四个部分,它们是:
-
什么是位置编码
-
变压器中位置编码背后的数学
-
使用 NumPy 实现位置编码矩阵
-
理解并可视化位置编码矩阵
什么是位置编码?
位置编码描述了实体在序列中的位置或位置,以便每个位置分配一个唯一的表示。许多原因导致在变压器模型中不使用单一数字(如索引值)来表示项的位置。对于长序列,索引可能会变得非常大。如果将索引值归一化到 0 和 1 之间,则可能会对变长序列造成问题,因为它们会被不同地归一化。
Transformers 使用一种智能的位置编码方案,其中每个位置/索引映射到一个向量。因此,位置编码层的输出是一个矩阵,其中矩阵的每一行表示序列中编码对象与其位置信息的和。下图展示了仅编码位置信息的矩阵示例。
三角函数正弦函数的快速回顾
这是对正弦函数的快速回顾;你也可以用余弦函数进行等效操作。该函数的范围是 [-1,+1]。该波形的频率是每秒完成的周期数。波长是波形重复自身的距离。不同波形的波长和频率如下所示:
想开始构建具有注意力机制的 Transformer 模型吗?
立即参加我的免费 12 天邮件速成课程(包括示例代码)。
点击注册,并且还可以获得课程的免费 PDF 电子书版本。
Transformer 中的位置信息编码层
让我们直接进入正题。假设你有一个长度为 L L L 的输入序列,并且需要该序列中第 k t h k^{th} kth 对象的位置。位置编码由具有不同频率的正弦和余弦函数给出:
\begin{eqnarray}
P(k, 2i) &=& \sin\Big(\frac{k}{n^{2i/d}}\Big)\
P(k, 2i+1) &=& \cos\Big(\frac{k}{n^{2i/d}}\Big)
\end{eqnarray}
这里:
k k k: 输入序列中对象的位置, 0 ≤ k < L / 2 0 \leq k < L/2 0≤k<L/2
d d d: 输出嵌入空间的维度
P ( k , j ) P(k, j) P(k,j): 用于将输入序列中的位置 k k k 映射到位置矩阵的索引 ( k , j ) (k,j) (k,j) 的位置函数
n n n: 用户定义的标量,由 Attention Is All You Need 的作者设定为 10,000。
i i i: 用于映射到列索引 0 ≤ i < d / 2 0 \leq i < d/2 0≤i<d/2,一个单独的 i i i 值同时映射到正弦和余弦函数
在上述表达式中,你可以看到偶数位置对应于正弦函数,而奇数位置对应于余弦函数。
示例
为了理解上述表达式,让我们以短语 “I am a robot” 为例,设定 n=100 和 d=4。下表显示了该短语的位置信息编码矩阵。实际上,对于任何四字母短语,位置信息编码矩阵在 n=100 和 d=4 的情况下都是相同的。
从头开始编码位置编码矩阵
这里是一个简短的 Python 代码示例,用于使用 NumPy 实现位置编码。代码经过简化,以便更容易理解位置编码。
Python
import numpy as np
import matplotlib.pyplot as plt
def getPositionEncoding(seq_len, d, n=10000):
P = np.zeros((seq_len, d))
for k in range(seq_len):
for i in np.arange(int(d/2)):
denominator = np.power(n, 2*i/d)
P[k, 2*i] = np.sin(k/denominator)
P[k, 2*i+1] = np.cos(k/denominator)
return P
P = getPositionEncoding(seq_len=4, d=4, n=100)
print(P)
输出
[[ 0\. 1\. 0\. 1\. ]
[ 0.84147098 0.54030231 0.09983342 0.99500417]
[ 0.90929743 -0.41614684 0.19866933 0.98006658]
[ 0.14112001 -0.9899925 0.29552021 0.95533649]]
理解位置编码矩阵
为了理解位置编码,让我们先来看不同位置的正弦波,n=10,000 和 d=512。Python
def plotSinusoid(k, d=512, n=10000):
x = np.arange(0, 100, 1)
denominator = np.power(n, 2*x/d)
y = np.sin(k/denominator)
plt.plot(x, y)
plt.title('k = ' + str(k))
fig = plt.figure(figsize=(15, 4))
for i in range(4):
plt.subplot(141 + i)
plotSinusoid(i*4)
下图是上述代码的输出:
不同位置索引的正弦波
你可以看到每个位置 k k k 对应一个不同的正弦波,它将单个位置编码成一个向量。如果你仔细查看位置编码函数,你会发现固定的 i i i 的波长由下式给出:
$$
\lambda_{i} = 2 \pi n^{2i/d}
$$
因此,正弦波的波长形成了几何级数,并从 2 π 2\pi 2π 变化到 2 π n 2\pi n 2πn。位置编码方案有许多优点。
-
正弦和余弦函数的值在 [-1, 1] 范围内,这保持了位置编码矩阵值在规范化范围内。
-
由于每个位置的正弦波不同,你有一种唯一的方式来编码每个位置。
-
你有一种方法来衡量或量化不同位置之间的相似性,从而使你能够编码单词的相对位置。
可视化位置矩阵
让我们在更大的数值上可视化位置矩阵。使用 Python 的 matshow()
方法,来自 matplotlib
库。将 n=10,000 设置为原始论文中的值,你会得到如下结果:
Python
P = getPositionEncoding(seq_len=100, d=512, n=10000)
cax = plt.matshow(P)
plt.gcf().colorbar(cax)
对于 n=10,000, d=512, 序列长度=100 的位置编码矩阵
位置编码层的最终输出是什么?
位置编码层将位置向量与单词编码相加,并输出该矩阵以供后续层使用。整个过程如下所示。
Transformer 中的位置编码层
进一步阅读
本节提供了更多关于该主题的资源,如果你希望深入了解。
书籍
- 用于自然语言处理的 Transformers,作者 Denis Rothman。
论文
- Attention Is All You Need,2017 年。
文章
总结
在本教程中,你发现了变压器中的位置编码。
具体来说,你学到了:
-
什么是位置编码,它为何需要。
-
如何使用 NumPy 在 Python 中实现位置编码
-
如何可视化位置编码矩阵
在本文中讨论的位置编码有任何问题吗?请在下面的评论中提出您的问题,我会尽力回答。
注意力机制架构之旅
原文:
machinelearningmastery.com/a-tour-of-attention-based-architectures/
随着注意力在机器学习中的流行,整合注意力机制的神经架构列表也在增长。
在本教程中,您将了解与注意力结合使用的显著神经架构。
完成本教程后,您将更好地理解注意力机制如何被整合到不同的神经架构中,以及其目的。
用我的书 使用注意力构建变形金刚模型 开始您的项目。它提供了自学教程和可工作的代码,帮助您构建一个完全可工作的变形金刚模型,可以
将一种语言的句子翻译成另一种语言…
让我们开始吧。
注意力机制架构之旅
照片由Lucas Clara拍摄,部分权利保留。
教程概述
本教程分为四个部分;它们是:
-
编码器-解码器架构
-
变形金刚
-
图神经网络
-
增强记忆神经网络
编码器-解码器架构
编码器-解码器架构已被广泛应用于序列到序列(seq2seq)任务,例如语言处理中的机器翻译和图像字幕。
注意力最早作为 RNN 基础编码器-解码器框架的一部分用于编码长输入句子[Bahdanau et al. 2015]。因此,注意力在这种架构中被广泛使用。
– 注意模型的关注性调查,2021 年。
在机器翻译的背景下,这样的 seq2seq 任务将涉及将输入序列 I = { A , B , C , < E O S > } I = \{ A, B, C, <EOS> \} I={A,B,C,<EOS>}翻译成长度不同的输出序列 O = { W , X , Y , Z , < E O S > } O = \{ W, X, Y, Z, <EOS> \} O={W,X,Y,Z,<EOS>}。
对于没有注意力的基于 RNN 的编码器-解码器架构,展开每个 RNN将产生以下图表:
未展开的基于 RNN 的编码器和解码器
摘自“神经网络的序列到序列学习“
在这里,编码器一次读取一个词的输入序列,每次更新其内部状态。遇到符号时,表明序列结束。编码器生成的隐藏状态本质上包含了输入序列的向量表示,解码器将处理这个表示。
解码器一次生成一个词的输出序列,以前一个时间步( t t t – 1)的词作为输入生成输出序列中的下一个词。解码端的符号表示解码过程已结束。
正如我们之前提到的,没有注意力机制的编码器-解码器架构的问题在于,当不同长度和复杂性的序列由固定长度的向量表示时,可能会导致解码器遗漏重要信息。
为了解决这个问题,基于注意力的架构在编码器和解码器之间引入了注意力机制。
带有注意力机制的编码器-解码器架构
在这里,注意力机制( ϕ \phi ϕ)学习一组注意力权重,这些权重捕捉编码向量(v)与解码器的隐藏状态(h)之间的关系,通过对编码器所有隐藏状态的加权求和生成上下文向量(c)。这样,解码器能够访问整个输入序列,特别关注生成输出时最相关的输入信息。
Transformer
Transformer 的架构还实现了编码器和解码器。然而,与上述回顾的架构不同,它不依赖于递归神经网络。因此,本文将单独回顾这一架构及其变体。
Transformer 架构摒弃了任何递归,而是完全依赖于自注意力(或内部注意力)机制。
在计算复杂性方面,当序列长度 n 小于表示维度 d 时,自注意力层比递归层更快……
– 高级深度学习与 Python,2019。
自注意力机制依赖于使用查询、键和值,这些是通过将编码器对相同输入序列的表示与不同的权重矩阵相乘生成的。Transformer 使用点积(或乘法)注意力,在生成注意力权重的过程中,每个查询通过点积操作与键的数据库进行匹配。这些权重然后与值相乘,以生成最终的注意力向量。
乘法注意力
取自 “Attention Is All You Need“
直观地说,由于所有查询、键和值都源于相同的输入序列,自注意力机制捕捉了同一序列中不同元素之间的关系,突出显示了彼此之间最相关的元素。
由于 transformer 不依赖于 RNN,序列中每个元素的位置信息可以通过增强编码器对每个元素的表示来保留。这意味着 transformer 架构还可以应用于信息可能不一定按顺序相关的任务,例如图像分类、分割或标注的计算机视觉任务。
Transformers 可以捕捉输入和输出之间的全局/长期依赖,支持并行处理,要求最少的归纳偏置(先验知识),展示了对大序列和数据集的可扩展性,并允许使用类似的处理块进行多模态(文本、图像、语音)的领域无关处理。
– An Attentive Survey of Attention Models,2021 年。
此外,多个注意力层可以并行堆叠,这被称为多头注意力。每个头部在相同输入的不同线性变换上并行工作,然后将头部的输出连接起来生成最终的注意力结果。使用多头模型的好处是每个头部可以关注序列的不同元素。
多头注意力
取自 “Attention Is All You Need“
一些解决原始模型局限性的 transformer 架构变体包括:
*** Transformer-XL:引入了递归机制,使其能够学习超越训练过程中通常使用的碎片化序列的固定长度的长期依赖。
- XLNet:一个双向变换器,它通过引入基于排列的机制在 Transformer-XL 的基础上进行构建,其中训练不仅在输入序列的原始顺序上进行,还包括输入序列顺序的不同排列。
想开始构建带有注意力的变换器模型吗?
立即获取我的免费 12 天电子邮件速成课程(附示例代码)。
点击注册,并免费获得课程的 PDF Ebook 版本。
图神经网络
图可以定义为一组节点(或顶点),它们通过连接(或边)链接在一起。
图是一种多功能的数据结构,非常适合许多现实世界场景中数据的组织方式。
– 深入学习 Python,2019 年。
例如,考虑一个社交网络,其中用户可以通过图中的节点表示,朋友之间的关系通过边表示。或者是一个分子,其中节点是原子,边表示它们之间的化学键。
我们可以将图像视为图形,其中每个像素是一个节点,直接连接到其邻近的像素…
– 深入学习 Python,2019 年。
特别感兴趣的是图注意力网络(GAT),它在图卷积网络(GCN)中使用自注意力机制,其中后者通过在图的节点上执行卷积来更新状态向量。卷积操作应用于中心节点和邻近节点,使用加权滤波器来更新中心节点的表示。GCN 中的滤波器权重可以是固定的或可学习的。
在中心节点(红色)及其邻域节点上的图卷积
摘自“图神经网络的综合调查“
相比之下,GAT 使用注意力分数为邻近节点分配权重。
这些注意力分数的计算遵循与上述 seq2seq 任务中的方法类似的程序:(1)首先计算两个邻近节点的特征向量之间的对齐分数,然后(2)通过应用 softmax 操作计算注意力分数,最后(3)通过对所有邻居的特征向量进行加权组合,计算每个节点的输出特征向量(相当于 seq2seq 任务中的上下文向量)。
多头注意力也可以以非常类似于先前在变换器架构中提议的方式应用。在图中的每个节点会分配多个头,最终层中会对它们的输出进行平均。
一旦生成最终输出,这可以作为后续任务特定层的输入。可以通过图解决的任务包括将个体节点分类到不同的群组中(例如,在预测一个人将决定加入哪些俱乐部时)。或者可以对个体边进行分类,以确定两个节点之间是否存在边(例如,在社交网络中预测两个人是否可能是朋友),甚至可以对整个图进行分类(例如,预测分子是否有毒)。
用我的书 使用注意力构建 Transformer 模型 开始你的项目。它提供了带有工作代码的自学教程,指导你构建一个完全工作的 Transformer 模型。
将一种语言的句子翻译成另一种语言…
记忆增强神经网络
到目前为止审查的基于编码器-解码器的注意力架构中,编码输入序列的向量集可以被视为外部记忆,编码器写入并解码器读取。然而,由于编码器只能写入此内存,解码器只能读取,因此存在限制。
记忆增强神经网络(MANNs)是最近旨在解决这一限制的算法。
神经图灵机(NTM)是 MANN 的一种类型。它由一个神经网络控制器组成,接受输入并产生输出,并对内存执行读写操作。
神经图灵机架构
取自“神经图灵机”
读头执行的操作类似于用于 seq2seq 任务的注意力机制,其中注意力权重指示考虑的向量在形成输出时的重要性。
读头总是读取完整的记忆矩阵,但通过对不同记忆向量进行不同强度的关注来执行此操作。
– Python 深度学习进阶,2019 年。
读操作的输出由记忆向量的加权和定义。
写头还利用注意力向量,与擦除和添加向量一起工作。根据注意力和擦除向量的值擦除内存位置,并通过添加向量写入信息。
MANNs 的应用示例包括问答和聊天机器人,其中外部记忆存储大量序列(或事实),神经网络利用这些信息。关注机制在选择对当前任务更相关的数据库事实时起着关键作用。
进一步阅读
本节提供了更多关于该主题的资源,如果你想深入了解的话。
书籍
-
Python 高级深度学习,2019 年。
-
深度学习精粹,2018 年。
论文
-
注意力模型的综述,2021 年。
-
心理学、神经科学和机器学习中的注意力,2020 年。
摘要
在本教程中,你发现了与注意力机制结合使用的显著神经网络架构。
具体来说,你更好地理解了注意力机制如何融入不同的神经网络架构以及其目的。
你有任何问题吗?
在下面的评论中提问,我会尽力回答。**
向 Keras 中的循环神经网络添加自定义注意力层
原文:
machinelearningmastery.com/adding-a-custom-attention-layer-to-recurrent-neural-network-in-keras/
过去几年来,深度学习网络已经获得了巨大的流行。"注意力机制"被整合到深度学习网络中以提高其性能。在网络中添加注意力组件已经显示出在机器翻译、图像识别、文本摘要等任务中显著的改进。
本教程展示了如何向使用循环神经网络构建的网络添加自定义注意力层。我们将演示如何使用一个非常简单的数据集进行时间序列预测的端到端应用。本教程旨在帮助任何希望了解如何向深度学习网络添加用户定义层,并利用这个简单示例构建更复杂应用程序的人士。
完成本教程后,您将了解:
-
在 Keras 中创建自定义注意力层需要哪些方法
-
如何在使用 SimpleRNN 构建的网络中加入新层
使用我的书《使用注意力构建 Transformer 模型》启动您的项目。它提供了自学教程和完整的工作代码,指导您构建一个完全工作的 Transformer 模型,可以
将句子从一种语言翻译成另一种语言…
让我们开始吧。
在 Keras 中向循环神经网络添加自定义注意力层
照片由 Yahya Ehsan 拍摄,部分权利保留。
教程概述
本教程分为三部分;它们是:
-
为时间序列预测准备一个简单的数据集
-
如何使用 SimpleRNN 构建的网络进行时间序列预测
-
向 SimpleRNN 网络添加自定义注意力层
先决条件
假设您熟悉以下主题。您可以点击下面的链接进行概览。
数据集
本文的重点是了解如何向深度学习网络添加自定义注意力层。为此,让我们以斐波那契数列为例,简单说明一下。斐波那契数列的前 10 个数如下所示:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, …
给定前‘t’个数,能否让机器准确地重构下一个数?这意味着除了最后两个数外,所有之前的输入都将被丢弃,并对最后两个数执行正确的操作。
在本教程中,您将使用t
个时间步来构建训练示例,并将t+1
时刻的值作为目标。例如,如果t=3
,则训练示例和相应的目标值如下所示:
想要开始使用注意力构建 Transformer 模型吗?
现在就参加我的免费 12 天电子邮件速成课程(包含示例代码)。
点击此处注册,并免费获得课程的 PDF 电子书版本。
SimpleRNN 网络
在这一部分,您将编写生成数据集的基本代码,并使用 SimpleRNN 网络来预测斐波那契数列的下一个数字。
导入部分
首先,让我们编写导入部分:
from pandas import read_csv
import numpy as np
from keras import Model
from keras.layers import Layer
import keras.backend as K
from keras.layers import Input, Dense, SimpleRNN
from sklearn.preprocessing import MinMaxScaler
from keras.models import Sequential
from keras.metrics import mean_squared_error
准备数据集
下面的函数生成 n 个斐波那契数列的序列(不包括起始两个值)。如果将scale_data
设置为 True,则还会使用 scikit-learn 中的 MinMaxScaler 将值缩放到 0 到 1 之间。让我们看看n=10
时的输出。
def get_fib_seq(n, scale_data=True):
# Get the Fibonacci sequence
seq = np.zeros(n)
fib_n1 = 0.0
fib_n = 1.0
for i in range(n):
seq[i] = fib_n1 + fib_n
fib_n1 = fib_n
fib_n = seq[i]
scaler = []
if scale_data:
scaler = MinMaxScaler(feature_range=(0, 1))
seq = np.reshape(seq, (n, 1))
seq = scaler.fit_transform(seq).flatten()
return seq, scaler
fib_seq = get_fib_seq(10, False)[0]
print(fib_seq)
[ 1\. 2\. 3\. 5\. 8\. 13\. 21\. 34\. 55\. 89.]
接下来,我们需要一个函数get_fib_XY()
,将序列重新格式化为 Keras 输入层使用的训练示例和目标值。当给定参数time_steps
时,get_fib_XY()
将每行数据集构建为具有time_steps
列的数据。此函数不仅从斐波那契序列构建训练集和测试集,还使用scale_data
参数将训练示例进行洗牌并重新调整到所需的 TensorFlow 格式,即total_samples x time_steps x features
。同时,如果scale_data
设置为True
,函数还返回一个scaler
对象,用于将值缩放到 0 到 1 之间。
让我们生成一个小的训练集,看看它的样子。我们设置了time_steps=3
和total_fib_numbers=12
,大约 70%的示例用于测试。请注意,训练和测试示例已通过permutation()
函数进行了洗牌。
def get_fib_XY(total_fib_numbers, time_steps, train_percent, scale_data=True):
dat, scaler = get_fib_seq(total_fib_numbers, scale_data)
Y_ind = np.arange(time_steps, len(dat), 1)
Y = dat[Y_ind]
rows_x = len(Y)
X = dat[0:rows_x]
for i in range(time_steps-1):
temp = dat[i+1:rows_x+i+1]
X = np.column_stack((X, temp))
# random permutation with fixed seed
rand = np.random.RandomState(seed=13)
idx = rand.permutation(rows_x)
split = int(train_percent*rows_x)
train_ind = idx[0:split]
test_ind = idx[split:]
trainX = X[train_ind]
trainY = Y[train_ind]
testX = X[test_ind]
testY = Y[test_ind]
trainX = np.reshape(trainX, (len(trainX), time_steps, 1))
testX = np.reshape(testX, (len(testX), time_steps, 1))
return trainX, trainY, testX, testY, scaler
trainX, trainY, testX, testY, scaler = get_fib_XY(12, 3, 0.7, False)
print('trainX = ', trainX)
print('trainY = ', trainY)
trainX = [[[ 8.]
[13.]
[21.]]
[[ 5.]
[ 8.]
[13.]]
[[ 2.]
[ 3.]
[ 5.]]
[[13.]
[21.]
[34.]]
[[21.]
[34.]
[55.]]
[[34.]
[55.]
[89.]]]
trainY = [ 34\. 21\. 8\. 55\. 89\. 144.]
设置网络
现在让我们设置一个包含两个层的小型网络。第一层是SimpleRNN
层,第二层是Dense
层。以下是模型的摘要。
# Set up parameters
time_steps = 20
hidden_units = 2
epochs = 30
# Create a traditional RNN network
def create_RNN(hidden_units, dense_units, input_shape, activation):
model = Sequential()
model.add(SimpleRNN(hidden_units, input_shape=input_shape, activation=activation[0]))
model.add(Dense(units=dense_units, activation=activation[1]))
model.compile(loss='mse', optimizer='adam')
return model
model_RNN = create_RNN(hidden_units=hidden_units, dense_units=1, input_shape=(time_steps,1),
activation=['tanh', 'tanh'])
model_RNN.summary()
Model: "sequential_1"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
simple_rnn_3 (SimpleRNN) (None, 2) 8
_________________________________________________________________
dense_3 (Dense) (None, 1) 3
=================================================================
Total params: 11
Trainable params: 11
Non-trainable params: 0
训练网络并评估
接下来的步骤是添加代码以生成数据集、训练网络并评估它。这一次,我们将数据缩放到 0 到 1 之间。由于scale_data
参数的默认值为True
,我们不需要传递该参数。
# Generate the dataset
trainX, trainY, testX, testY, scaler = get_fib_XY(1200, time_steps, 0.7)
model_RNN.fit(trainX, trainY, epochs=epochs, batch_size=1, verbose=2)
# Evalute model
train_mse = model_RNN.evaluate(trainX, trainY)
test_mse = model_RNN.evaluate(testX, testY)
# Print error
print("Train set MSE = ", train_mse)
print("Test set MSE = ", test_mse)
作为输出,你将看到训练进度以及均方误差的以下值:
Train set MSE = 5.631405292660929e-05
Test set MSE = 2.623497312015388e-05
在网络中添加自定义注意力层
在 Keras 中,通过子类化Layer
类很容易创建一个实现注意力的自定义层。Keras 指南列出了通过子类化创建新层的明确步骤。你将在这里使用这些指南。单个层对应的所有权重和偏置由此类封装。你需要编写__init__
方法,并覆盖以下方法:
-
build()
: Keras 指南建议在知道输入大小后通过此方法添加权重。此方法“惰性”创建权重。内置函数add_weight()
可用于添加注意力层的权重和偏置。 -
call()
:call()
方法实现了输入到输出的映射。在训练期间,它应该实现前向传播。
注意力层的调用方法
注意力层的call()
方法必须计算对齐分数、权重和上下文。你可以通过斯特凡尼亚在 从零开始理解注意力机制 文章中详细了解这些参数。你将在call()
方法中实现巴赫达瑙注意力机制。
从 Keras 的 Layer
类继承一个层并通过 add_weights()
方法添加权重的好处在于权重会自动调整。Keras 对 call()
方法的操作/计算进行“逆向工程”,并在训练期间计算梯度。在添加权重时,指定trainable=True
非常重要。如果需要,你还可以为自定义层添加一个train_step()
方法并指定自己的权重训练方法。
下面的代码实现了自定义注意力层。
# Add attention layer to the deep learning network
class attention(Layer):
def __init__(self,**kwargs):
super(attention,self).__init__(**kwargs)
def build(self,input_shape):
self.W=self.add_weight(name='attention_weight', shape=(input_shape[-1],1),
initializer='random_normal', trainable=True)
self.b=self.add_weight(name='attention_bias', shape=(input_shape[1],1),
initializer='zeros', trainable=True)
super(attention, self).build(input_shape)
def call(self,x):
# Alignment scores. Pass them through tanh function
e = K.tanh(K.dot(x,self.W)+self.b)
# Remove dimension of size 1
e = K.squeeze(e, axis=-1)
# Compute the weights
alpha = K.softmax(e)
# Reshape to tensorFlow format
alpha = K.expand_dims(alpha, axis=-1)
# Compute the context vector
context = x * alpha
context = K.sum(context, axis=1)
return context
带有注意力层的 RNN 网络
现在让我们将一个注意力层添加到之前创建的 RNN 网络中。create_RNN_with_attention()
函数现在在网络中指定了一个 RNN 层、一个注意力层和一个稠密层。确保在指定 SimpleRNN 时设置return_sequences=True
,这将返回所有先前时间步的隐藏单元输出。
让我们来看一下带有注意力的模型摘要。
def create_RNN_with_attention(hidden_units, dense_units, input_shape, activation):
x=Input(shape=input_shape)
RNN_layer = SimpleRNN(hidden_units, return_sequences=True, activation=activation)(x)
attention_layer = attention()(RNN_layer)
outputs=Dense(dense_units, trainable=True, activation=activation)(attention_layer)
model=Model(x,outputs)
model.compile(loss='mse', optimizer='adam')
return model
model_attention = create_RNN_with_attention(hidden_units=hidden_units, dense_units=1,
input_shape=(time_steps,1), activation='tanh')
model_attention.summary()
Model: "model_1"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_2 (InputLayer) [(None, 20, 1)] 0
_________________________________________________________________
simple_rnn_2 (SimpleRNN) (None, 20, 2) 8
_________________________________________________________________
attention_1 (attention) (None, 2) 22
_________________________________________________________________
dense_2 (Dense) (None, 1) 3
=================================================================
Total params: 33
Trainable params: 33
Non-trainable params: 0
_________________________________________________________________
使用注意力的深度学习网络进行训练和评估
现在是时候训练和测试你的模型,并查看它在预测序列下一个斐波那契数上的表现如何。
model_attention.fit(trainX, trainY, epochs=epochs, batch_size=1, verbose=2)
# Evalute model
train_mse_attn = model_attention.evaluate(trainX, trainY)
test_mse_attn = model_attention.evaluate(testX, testY)
# Print error
print("Train set MSE with attention = ", train_mse_attn)
print("Test set MSE with attention = ", test_mse_attn)
你将看到训练进度作为输出,以及以下内容:
Train set MSE with attention = 5.3511179430643097e-05
Test set MSE with attention = 9.053358553501312e-06
即使对于这个简单的例子,测试集上的均方误差在使用注意力层后更低。通过调优超参数和模型选择,你可以获得更好的结果。尝试在更复杂的问题上以及通过增加网络层来验证这一点。你还可以使用scaler
对象将数字缩放回原始值。
你可以进一步通过使用 LSTM 代替 SimpleRNN,或者通过卷积和池化层构建网络。如果你愿意,还可以将其改为编码-解码网络。
统一的代码
如果你想尝试,本教程的整个代码如下粘贴。请注意,由于此算法的随机性质,你的输出可能与本教程中给出的不同。
from pandas import read_csv
import numpy as np
from keras import Model
from keras.layers import Layer
import keras.backend as K
from keras.layers import Input, Dense, SimpleRNN
from sklearn.preprocessing import MinMaxScaler
from keras.models import Sequential
from keras.metrics import mean_squared_error
# Prepare data
def get_fib_seq(n, scale_data=True):
# Get the Fibonacci sequence
seq = np.zeros(n)
fib_n1 = 0.0
fib_n = 1.0
for i in range(n):
seq[i] = fib_n1 + fib_n
fib_n1 = fib_n
fib_n = seq[i]
scaler = []
if scale_data:
scaler = MinMaxScaler(feature_range=(0, 1))
seq = np.reshape(seq, (n, 1))
seq = scaler.fit_transform(seq).flatten()
return seq, scaler
def get_fib_XY(total_fib_numbers, time_steps, train_percent, scale_data=True):
dat, scaler = get_fib_seq(total_fib_numbers, scale_data)
Y_ind = np.arange(time_steps, len(dat), 1)
Y = dat[Y_ind]
rows_x = len(Y)
X = dat[0:rows_x]
for i in range(time_steps-1):
temp = dat[i+1:rows_x+i+1]
X = np.column_stack((X, temp))
# random permutation with fixed seed
rand = np.random.RandomState(seed=13)
idx = rand.permutation(rows_x)
split = int(train_percent*rows_x)
train_ind = idx[0:split]
test_ind = idx[split:]
trainX = X[train_ind]
trainY = Y[train_ind]
testX = X[test_ind]
testY = Y[test_ind]
trainX = np.reshape(trainX, (len(trainX), time_steps, 1))
testX = np.reshape(testX, (len(testX), time_steps, 1))
return trainX, trainY, testX, testY, scaler
# Set up parameters
time_steps = 20
hidden_units = 2
epochs = 30
# Create a traditional RNN network
def create_RNN(hidden_units, dense_units, input_shape, activation):
model = Sequential()
model.add(SimpleRNN(hidden_units, input_shape=input_shape, activation=activation[0]))
model.add(Dense(units=dense_units, activation=activation[1]))
model.compile(loss='mse', optimizer='adam')
return model
model_RNN = create_RNN(hidden_units=hidden_units, dense_units=1, input_shape=(time_steps,1),
activation=['tanh', 'tanh'])
# Generate the dataset for the network
trainX, trainY, testX, testY, scaler = get_fib_XY(1200, time_steps, 0.7)
# Train the network
model_RNN.fit(trainX, trainY, epochs=epochs, batch_size=1, verbose=2)
# Evalute model
train_mse = model_RNN.evaluate(trainX, trainY)
test_mse = model_RNN.evaluate(testX, testY)
# Print error
print("Train set MSE = ", train_mse)
print("Test set MSE = ", test_mse)
# Add attention layer to the deep learning network
class attention(Layer):
def __init__(self,**kwargs):
super(attention,self).__init__(**kwargs)
def build(self,input_shape):
self.W=self.add_weight(name='attention_weight', shape=(input_shape[-1],1),
initializer='random_normal', trainable=True)
self.b=self.add_weight(name='attention_bias', shape=(input_shape[1],1),
initializer='zeros', trainable=True)
super(attention, self).build(input_shape)
def call(self,x):
# Alignment scores. Pass them through tanh function
e = K.tanh(K.dot(x,self.W)+self.b)
# Remove dimension of size 1
e = K.squeeze(e, axis=-1)
# Compute the weights
alpha = K.softmax(e)
# Reshape to tensorFlow format
alpha = K.expand_dims(alpha, axis=-1)
# Compute the context vector
context = x * alpha
context = K.sum(context, axis=1)
return context
def create_RNN_with_attention(hidden_units, dense_units, input_shape, activation):
x=Input(shape=input_shape)
RNN_layer = SimpleRNN(hidden_units, return_sequences=True, activation=activation)(x)
attention_layer = attention()(RNN_layer)
outputs=Dense(dense_units, trainable=True, activation=activation)(attention_layer)
model=Model(x,outputs)
model.compile(loss='mse', optimizer='adam')
return model
# Create the model with attention, train and evaluate
model_attention = create_RNN_with_attention(hidden_units=hidden_units, dense_units=1,
input_shape=(time_steps,1), activation='tanh')
model_attention.summary()
model_attention.fit(trainX, trainY, epochs=epochs, batch_size=1, verbose=2)
# Evalute model
train_mse_attn = model_attention.evaluate(trainX, trainY)
test_mse_attn = model_attention.evaluate(testX, testY)
# Print error
print("Train set MSE with attention = ", train_mse_attn)
print("Test set MSE with attention = ", test_mse_attn)
进一步阅读
如果你想深入了解,本节提供了更多相关资源。
书籍
论文
- 通过联合学习对齐和翻译进行神经机器翻译,2014 年。
文章
摘要
在本教程中,你学会了如何向使用 Keras 构建的深度学习网络添加自定义注意力层。
具体来说,你学到了:
-
如何重写 Keras 的
Layer
类。 -
方法
build()
用于向注意力层添加权重。 -
方法
call()
用于指定注意力层输入到输出的映射。 -
如何向使用 SimpleRNN 构建的深度学习网络添加自定义注意力层。
你在本文讨论的循环神经网络有任何问题吗?请在下方评论中提问,我会尽力回答。
循环神经网络及其数学基础介绍
对于序列或时间序列数据,传统的前馈网络无法用于学习和预测。需要一种机制来保留过去或历史信息,以预测未来的值。循环神经网络,简称 RNN,是一种变体的前馈人工神经网络,可以处理序列数据,并且可以被训练以保留有关过去的知识。
完成本教程后,你将了解:
-
循环神经网络
-
展开 RNN 是什么意思
-
RNN 中的权重如何更新
-
各种 RNN 架构
启动你的项目,请参阅我的书籍构建具有注意力机制的变换器模型。它提供了自学教程和可运行的代码,引导你构建一个完全可工作的变换器模型。
将句子从一种语言翻译成另一种语言…
让我们开始吧。
循环神经网络及其数学基础介绍。照片由 Mehreen Saeed 提供,保留部分权利。
教程概述
本教程分为两部分;它们是:
-
RNN 的工作原理
-
时间上的展开
-
反向传播时间算法
-
-
不同的 RNN 架构和变体
先决条件
本教程假设你已经熟悉人工神经网络和反向传播算法。如果没有,你可以阅读 Stefania Cristina 的这篇非常好的教程,Calculus in Action: Neural Networks。该教程还解释了如何使用基于梯度的反向传播算法来训练神经网络。
什么是循环神经网络
循环神经网络(RNN)是一种特殊的人工神经网络,适用于时间序列数据或涉及序列的数据。普通的前馈神经网络仅适用于彼此独立的数据点。然而,如果我们有一个数据序列,其中一个数据点依赖于之前的数据点,我们需要修改神经网络以纳入这些数据点之间的依赖关系。RNN 具有“记忆”的概念,帮助它们存储先前输入的状态或信息,以生成序列的下一个输出。
想开始构建具有注意力机制的变换器模型吗?
立即领取我的免费 12 天电子邮件速成课程(含示例代码)。
点击注册并获取课程的免费 PDF 电子书版本。
循环神经网络的展开
递归神经网络。压缩表示(上),展开网络(下)。
简单的 RNN 有一个反馈回路,如上图的第一个图示所示。图中灰色矩形内的反馈回路可以展开为三个时间步,以产生上图中的第二个网络。当然,你可以改变架构,使得网络展开 k k k 个时间步。在图中,使用了以下符号:
-
x t ∈ R x_t \in R xt∈R 是时间步 t t t 的输入。为了简化问题,我们假设 x t x_t xt 是一个具有单一特征的标量值。你可以将这个概念扩展到一个 d d d 维特征向量。
-
y t ∈ R y_t \in R yt∈R 是网络在时间步 t t t 的输出。我们可以在网络中产生多个输出,但在这个例子中,我们假设只有一个输出。
-
h t ∈ R m h_t \in R^m ht∈Rm 向量存储时间 t t t 的隐藏单元/状态的值。这也称为当前上下文。 m m m 是隐藏单元的数量。 h 0 h_0 h0 向量初始化为零。
-
w x ∈ R m w_x \in R^{m} wx∈Rm 是与递归层中的输入相关的权重
-
w h ∈ R m x m w_h \in R^{mxm} wh∈Rmxm 是与递归层中的隐藏单元相关的权重
-
w y ∈ R m w_y \in R^m wy∈Rm 是与隐藏单元到输出单元相关的权重
-
b h ∈ R m b_h \in R^m bh∈Rm 是与递归层相关的偏置
-
b y ∈ R b_y \in R by∈R 是与前馈层相关的偏置
在每个时间步,我们可以将网络展开 k k k 个时间步,以获取时间步 k + 1 k+1 k+1 的输出。展开的网络非常类似于前馈神经网络。展开网络中的矩形表示一个操作。因此,例如,使用激活函数 f:
h t + 1 = f ( x t , h t , w x , w h , b h ) = f ( w x x t + w h h t + b h ) h_{t+1} = f(x_t, h_t, w_x, w_h, b_h) = f(w_{x} x_t + w_{h} h_t + b_h) ht+1=f(xt,ht,wx,wh,bh)=f(wxxt+whht+bh)
时间 t t t 的输出 y y y 计算如下:
$$
y t = f ( h t , w y ) = f ( w y ⋅ h t + b y ) y_{t} = f(h_t, w_y) = f(w_y \cdot h_t + b_y) yt=f(ht,wy)=f(wy⋅ht+by)
$$
这里, ⋅ \cdot ⋅ 是点积。
因此,在 RNN 的前馈过程中,网络在 k k k 个时间步后计算隐藏单元和输出的值。与网络相关的权重是时间共享的。每个递归层有两组权重:一组用于输入,另一组用于隐藏单元。最后的前馈层,即计算第 k k k 个时间步的最终输出的层,就像传统前馈网络中的普通层一样。
激活函数
我们可以在递归神经网络中使用任何我们喜欢的激活函数。常见的选择有:
-
Sigmoid 函数: 1 1 + e − x \frac{1}{1+e^{-x}} 1+e−x1
-
Tanh 函数: e x − e − x e x + e − x \frac{e^{x}-e^{-x}}{e^{x}+e^{-x}} ex+e−xex−e−x
-
Relu 函数:max ( 0 , x ) (0,x) (0,x)
训练递归神经网络
人工神经网络的反向传播算法被修改以包括时间展开以训练网络的权重。该算法基于计算梯度向量,称为时间反向传播或简称为 BPTT 算法。下面给出了训练的伪代码。用户可以选择训练的 k k k 值。在下面的伪代码中, p t p_t pt 是时间步 t 的目标值:
-
重复直到满足停止准则:
-
将所有的 h h h 设置为零。
-
从 t = 0 到 n-k 重复
-
在展开的网络上向前传播网络 k k k 个时间步以计算所有的 h h h 和 y y y
-
计算误差为: e = y t + k − p t + k e = y_{t+k}-p_{t+k} e=yt+k−pt+k
-
在展开网络上反向传播错误并更新权重
-
-
RNN 的类型
有不同类型的递归神经网络,具有不同的架构。一些示例包括:
一对一
这里有一个单一的 ( x t , y t ) (x_t, y_t) (xt,yt) 对。传统的神经网络采用一对一的架构。
一对多
在一对多网络中,单个输入 x t x_t xt 可以产生多个输出,例如 ( y t 0 , y t 1 , y t 2 ) (y_{t0}, y_{t1}, y_{t2}) (yt0,yt1,yt2)。音乐生成是一对多网络应用的示例领域。
多对一
在这种情况下,来自不同时间步的多个输入产生单个输出。例如, ( x t , x t + 1 , x t + 2 ) (x_t, x_{t+1}, x_{t+2}) (xt,xt+1,xt+2) 可以产生单个输出 y t y_t yt。这样的网络用于情感分析或情绪检测,其中类别标签依赖于一系列单词。
多对多
有许多可能性适用于多对多。上面显示了一个例子,其中两个输入产生三个输出。多对多网络应用于机器翻译,例如英语到法语或其反向翻译系统。
循环神经网络(RNN)的优点和缺点
RNN 具有各种优点,例如:
-
能够处理序列数据
-
能够处理不同长度的输入
-
能够存储或“记忆”历史信息
缺点包括:
-
计算可能非常缓慢。
-
网络在做决策时不考虑未来的输入。
-
梯度消失问题,即用于计算权重更新的梯度可能非常接近零,阻止网络学习新的权重。网络越深,这个问题越显著。
不同的 RNN 架构
不同变体的 RNN 正在实际应用于机器学习问题中:
双向递归神经网络(BRNN)
在 BRNN 中,未来时间步的输入用于提高网络的准确性。这就像知道一句话的首尾词来预测中间的词。
门控递归单元(GRU)
这些网络旨在处理梯度消失问题。它们具有重置和更新门,这些门决定哪些信息将被保留用于未来的预测。
长短期记忆(LSTM)
LSTM 也旨在解决递归神经网络中的梯度消失问题。LSTM 使用三个门,分别是输入门、输出门和遗忘门。类似于 GRU,这些门决定保留哪些信息。
进一步阅读
本节提供了更多相关资源,如果你想深入了解这一主题。
书籍
文章
总结
在本教程中,你发现了递归神经网络及其各种架构。
具体来说,你学到了:
-
递归神经网络如何处理序列数据
-
在递归神经网络中随时间展开
-
什么是时间中的回溯传播
-
递归神经网络的优缺点
-
递归神经网络的各种架构和变体
你对本文讨论的递归神经网络有任何问题吗?请在下面的评论中提出问题,我会尽力回答。
变换器模型与注意力机制速成课程。12 天内构建一个神经机器翻译器
变换器是神经机器翻译中的一种最新突破。自然语言很复杂。一种语言中的一个词可以在另一种语言中翻译成多个词,具体取决于上下文。但上下文究竟是什么,如何教计算机理解上下文是一个大问题。注意力机制的发明解决了如何将上下文编码到一个词中的问题,换句话说,就是如何将一个词和它的上下文一起呈现为一个数值向量。变换器将这一点提升到了一个更高的层次,使我们可以仅使用注意力机制而没有递归结构来构建自然语言翻译的神经网络。这不仅使网络更简单、更容易训练、并且算法上可并行化,还允许构建更复杂的语言模型。因此,我们几乎可以看到计算机翻译的句子几乎完美无瑕。
确实,这样一个强大的深度学习模型并不难构建。在 TensorFlow 和 Keras 中,你几乎可以随时获取所有构建块,训练模型只需几个小时。看到变换器模型被构建和训练是很有趣的。看到一个训练好的模型将句子从一种语言翻译到另一种语言则更有趣。
在这个速成课程中,你将按照原始研究论文的类似设计构建一个变换器模型。
这是一个重要的帖子。你可能想要收藏它。
开始吧。
变换器模型与注意力机制(12 天迷你课程)。
图片由 诺伯特·布劳恩 提供,版权所有。
这个速成课程适合谁?
在开始之前,我们需要确保你在正确的位置。
这个课程适合那些已经熟悉 TensorFlow/Keras 的开发者。课程的内容假设你具备一些前提条件,例如:
-
你知道如何构建自定义模型,包括 Keras 函数式 API
-
你知道如何在 Keras 中训练深度学习模型
-
你知道如何使用训练好的模型进行推理
你不需要是:
-
自然语言处理专家
-
多语言使用者
这个速成课程可以帮助你更深入地理解什么是变换器模型及其功能。
本速成课程假设你已经安装了工作中的 TensorFlow 2.10 环境。如果你需要帮助,可以按照这里的逐步教程进行:
快速课程概述
这个速成课程分为 12 节课。
你可以每天完成一节课(推荐)或者在一天内完成所有课程(高强度)。这真的取决于你可用的时间和你的热情程度。
以下是 12 节课程的列表,它们将帮助你入门并了解 Transformer 模型的构建。
-
课程 01:获取数据
-
课程 02:文本规范化
-
课程 03:向量化和数据集创建
-
课程 04:位置编码矩阵
-
课程 05:位置编码层
-
课程 06:Transformer 构建模块
-
课程 07:Transformer 编码器和解码器
-
课程 08:构建 Transformer
-
课程 09:准备 Transformer 模型进行训练
-
课程 10:训练 Transformer
-
课程 11:Transformer 模型的推理
-
课程 12:改进模型
每节课可能需要你 15 到 60 分钟。花时间按照自己的节奏完成课程。提出问题,甚至在网上发布结果。
课程可能会期望你去查找如何做事的方法。本指南将给你提示,但即使你只是按照课程中的代码,你也可以完成一个效果相当不错的 Transformer 模型。
在评论中发布你的结果;我会为你加油!
坚持下去;不要放弃。
课程 01:获取数据
由于你正在构建一个神经机器翻译器,你需要用于训练和测试的数据。让我们构建一个基于句子的英法翻译器。网络上有许多资源。例如,用户贡献的 Anki 闪卡应用程序的数据。你可以在 ankiweb.net/shared/decks/french
下载一些数据文件。数据文件将是一个包含 SQLite 数据库文件的 ZIP 文件,你可以从中提取英法句对。
不过,你可能会发现拥有文本文件版本更为方便,你可以在 www.manythings.org/anki/
找到这个文件的镜像。Google 也托管了这个文件的镜像
好的,我们将使用这些数据。
以下代码将下载压缩的数据文件并解压它:
import pathlib
import tensorflow as tf
# download dataset provided by Anki: https://www.manythings.org/anki/
text_file = tf.keras.utils.get_file(
fname="fra-eng.zip",
origin="http://storage.googleapis.com/download.tensorflow.org/data/fra-eng.zip",
extract=True,
)
# show where the file is located now
text_file = pathlib.Path(text_file).parent / "fra.txt"
print(text_file)
数据文件将是一个名为 fra.txt
的纯文本文件。其格式将是:
<English sentence><tab character><French sentence>
你的任务
尝试运行上述代码并打开提取的文件。你应该验证每一行的格式是否与上述一致。
在下一节课中,你将处理此文件并准备适合训练和测试的数据集。
课程 02:文本规范化
就像所有 NLP 任务一样,你需要在使用文本之前进行规范化。法语字母有重音符号,这些会以 Unicode 字符表示,但这种表示在 Unicode 中并不唯一。因此,你将把字符串转换为 NFKC(兼容性和组合规范形式)。
接下来,你将对句子进行标记化。每个单词和每个标点符号应该是一个独立的标记。然而,用于缩写形式的标点符号,如 don’t、va-t-il 或 c’est,不会与单词分开。此外,将所有内容转换为小写,期望这会减少词汇中的不同单词数量。
标准化和标记化可以更深入,例如子词标记化、词干提取和词形还原。但为了简化起见,你在这个项目中不会做这些。
从头开始,标准化文本的代码如下。你将使用 Python 模块 unicodedata
将 Unicode 字符串转换为 NFKC 标准形式。然后你将使用正则表达式在标点符号周围添加空格。之后,你将用哨兵 [start]
和 [end]
包裹法语句子(即目标语言)。你将在后续课程中看到哨兵的目的。
import pathlib
import pickle
import random
import re
import unicodedata
import tensorflow as tf
# download dataset provided by Anki: https://www.manythings.org/anki/
text_file = tf.keras.utils.get_file(
fname="fra-eng.zip",
origin="http://storage.googleapis.com/download.tensorflow.org/data/fra-eng.zip",
extract=True,
)
text_file = pathlib.Path(text_file).parent / "fra.txt"
def normalize(line):
"""Normalize a line of text and split into two at the tab character"""
line = unicodedata.normalize("NFKC", line.strip().lower())
line = re.sub(r"^([^ \w])(?!\s)", r"\1 ", line)
line = re.sub(r"(\s[^ \w])(?!\s)", r"\1 ", line)
line = re.sub(r"(?!\s)([^ \w])$", r" \1", line)
line = re.sub(r"(?!\s)([^ \w]\s)", r" \1", line)
eng, fra = line.split("\t")
fra = "[start] " + fra + " [end]"
return eng, fra
# normalize each line and separate into English and French
with open(text_file) as fp:
text_pairs = [normalize(line) for line in fp]
# print some samples
for _ in range(5):
print(random.choice(text_pairs))
with open("text_pairs.pickle", "wb") as fp:
pickle.dump(text_pairs, fp)
当你运行这段代码时,你应该会看到一些样本的结果,如下所示:
('where did you put your key ?', '[start] où est-ce tu as mis ta clé ? [end]')
('you missed a spot .', '[start] tu as loupé une tache . [end]')
("i think we're being followed .", '[start] je pense que nous sommes suivis . [end]')
('i bought a cactus .', "[start] j'ai acheté un cactus . [end]")
('i have more than enough .', "[start] j'en ai plus que marre . [end]")
我们将标准化的句子对保存在一个 pickle 文件中,因此可以在后续步骤中重复使用。
当你将它用于模型时,你希望了解一些关于此数据集的统计信息。特别是,你想看看每种语言中有多少个不同的标记(单词)以及句子的长度。你可以按以下方式找出这些信息:
import pickle
with open("text_pairs.pickle", "rb") as fp:
text_pairs = pickle.load(fp)
# count tokens
eng_tokens, fra_tokens = set(), set()
eng_maxlen, fra_maxlen = 0, 0
for eng, fra in text_pairs:
eng_tok, fra_tok = eng.split(), fra.split()
eng_maxlen = max(eng_maxlen, len(eng_tok))
fra_maxlen = max(fra_maxlen, len(fra_tok))
eng_tokens.update(eng_tok)
fra_tokens.update(fra_tok)
print(f"Total English tokens: {len(eng_tokens)}")
print(f"Total French tokens: {len(fra_tokens)}")
print(f"Max English length: {eng_maxlen}")
print(f"Max French length: {fra_maxlen}")
print(f"{len(text_pairs)} total pairs")
你的任务
运行上述代码。查看不仅是样本句子,还有你收集的统计信息。记住这些输出,因为它们对你的下一节课非常有用。此外,了解句子的最大长度不如了解它们的分布有用。你应该为此绘制一个直方图。试着生成以下图表:
import pickle
import matplotlib.pyplot as plt
with open("text_pairs.pickle", "rb") as fp:
text_pairs = pickle.load(fp)
# histogram of sentence length in tokens
en_lengths = [len(eng.split()) for eng, fra in text_pairs]
fr_lengths = [len(fra.split()) for eng, fra in text_pairs]
plt.hist(en_lengths, label="en", color="red", alpha=0.33)
plt.hist(fr_lengths, label="fr", color="blue", alpha=0.33)
plt.yscale("log") # sentence length fits Benford"s law
plt.ylim(plt.ylim()) # make y-axis consistent for both plots
plt.plot([max(en_lengths), max(en_lengths)], plt.ylim(), color="red")
plt.plot([max(fr_lengths), max(fr_lengths)], plt.ylim(), color="blue")
plt.legend()
plt.title("Examples count vs Token length")
plt.show()
不同语言中的句子长度
在下一个课程中,你将对这些标准化的文本数据进行向量化并创建数据集。
课程 03:向量化和创建数据集
在前面的课程中,你清理了句子,但它们仍然是文本。神经网络只能处理数字。将文本转换为数字的一种方法是通过向量化。这意味着将文本中的标记转换为整数。因此,包含 n n n 个标记(单词)的句子将变成一个 向量,包含 n n n 个整数。
你可以构建自己的向量化器。简单地构建一个每个唯一标记到唯一整数的映射表。当使用时,你一个一个地查找表中的标记,并返回整数形式的向量。
在 Keras 中,你有 TextVectorization
层来节省构建向量化器的工作。它支持填充,即整数 0 被保留用于表示“空”。当你提供一个
m
<
n
m < n
m<n 的标记句子时,但希望向量化器始终返回固定长度
n
n
n 的向量时,这非常有用。
您将首先将句子对拆分为训练、验证和测试集,因为这些是模型训练所需的。然后,创建一个 TextVectorization
层并仅适应于训练集(因为在完成模型训练之前,您不应窥视验证或测试数据集)。
import pickle
import random
from tensorflow.keras.layers import TextVectorization
# Load normalized sentence pairs
with open("text_pairs.pickle", "rb") as fp:
text_pairs = pickle.load(fp)
# train-test-val split of randomized sentence pairs
random.shuffle(text_pairs)
n_val = int(0.15*len(text_pairs))
n_train = len(text_pairs) - 2*n_val
train_pairs = text_pairs[:n_train]
val_pairs = text_pairs[n_train:n_train+n_val]
test_pairs = text_pairs[n_train+n_val:]
# Parameter determined after analyzing the input data
vocab_size_en = 10000
vocab_size_fr = 20000
seq_length = 20
# Create vectorizer
eng_vectorizer = TextVectorization(
max_tokens=vocab_size_en,
standardize=None,
split="whitespace",
output_mode="int",
output_sequence_length=seq_length,
)
fra_vectorizer = TextVectorization(
max_tokens=vocab_size_fr,
standardize=None,
split="whitespace",
output_mode="int",
output_sequence_length=seq_length + 1
)
# train the vectorization layer using training dataset
train_eng_texts = [pair[0] for pair in train_pairs]
train_fra_texts = [pair[1] for pair in train_pairs]
eng_vectorizer.adapt(train_eng_texts)
fra_vectorizer.adapt(train_fra_texts)
# save for subsequent steps
with open("vectorize.pickle", "wb") as fp:
data = {
"train": train_pairs,
"val": val_pairs,
"test": test_pairs,
"engvec_config": eng_vectorizer.get_config(),
"engvec_weights": eng_vectorizer.get_weights(),
"fravec_config": fra_vectorizer.get_config(),
"fravec_weights": fra_vectorizer.get_weights(),
}
pickle.dump(data, fp)
注意,TextVectorization
对象的参数 max_tokens
可以省略以让向量化器自行处理。但是,如果将它们设置为一个比总词汇量更小的值(比如这种情况),则限制了向量化器仅学习更频繁的单词,并将罕见单词标记为超出词汇表(OOV)。这可能对跳过价值不大或拼写错误的单词很有用。您还要固定向量化器的输出长度。我们假设以上句子中的每个句子应不超过 20 个标记。
接下来,您需要使用向量化器并创建一个 TensorFlow 数据集对象。这将有助于您在后续步骤中训练我们的模型。
import pickle
import tensorflow as tf
from tensorflow.keras.layers import TextVectorization
# load text data and vectorizer weights
with open("vectorize.pickle", "rb") as fp:
data = pickle.load(fp)
train_pairs = data["train"]
val_pairs = data["val"]
test_pairs = data["test"] # not used
eng_vectorizer = TextVectorization.from_config(data["engvec_config"])
eng_vectorizer.set_weights(data["engvec_weights"])
fra_vectorizer = TextVectorization.from_config(data["fravec_config"])
fra_vectorizer.set_weights(data["fravec_weights"])
# set up Dataset object
def format_dataset(eng, fra):
"""Take an English and a French sentence pair, convert into input and target.
The input is a dict with keys `encoder_inputs` and `decoder_inputs`, each
is a vector, corresponding to English and French sentences respectively.
The target is also vector of the French sentence, advanced by 1 token. All
vector are in the same length.
The output will be used for training the transformer model. In the model we
will create, the input tensors are named `encoder_inputs` and `decoder_inputs`
which should be matched to the keys in the dictionary for the source part
"""
eng = eng_vectorizer(eng)
fra = fra_vectorizer(fra)
source = {"encoder_inputs": eng,
"decoder_inputs": fra[:, :-1]}
target = fra[:, 1:]
return (source, target)
def make_dataset(pairs, batch_size=64):
"""Create TensorFlow Dataset for the sentence pairs"""
# aggregate sentences using zip(*pairs)
eng_texts, fra_texts = zip(*pairs)
# convert them into list, and then create tensors
dataset = tf.data.Dataset.from_tensor_slices((list(eng_texts), list(fra_texts)))
return dataset.shuffle(2048) \
.batch(batch_size).map(format_dataset) \
.prefetch(16).cache()
train_ds = make_dataset(train_pairs)
val_ds = make_dataset(val_pairs)
# test the dataset
for inputs, targets in train_ds.take(1):
print(f'inputs["encoder_inputs"].shape: {inputs["encoder_inputs"].shape}')
print(f'inputs["encoder_inputs"][0]: {inputs["encoder_inputs"][0]}')
print(f'inputs["decoder_inputs"].shape: {inputs["decoder_inputs"].shape}')
print(f'inputs["decoder_inputs"][0]: {inputs["decoder_inputs"][0]}')
print(f"targets.shape: {targets.shape}")
print(f"targets[0]: {targets[0]}")
稍后您将重复使用此代码以创建 train_ds
和 val_ds
数据集对象。
您的任务
运行上述代码。验证您是否能看到类似以下的输出:
inputs["encoder_inputs"].shape: (64, 20)
inputs["encoder_inputs"][0]: [142 8 263 979 2 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0]
inputs["decoder_inputs"].shape: (64, 20)
inputs["decoder_inputs"][0]: [ 2 15 2496 190 4 3 0 0 0 0
0 0 0 0 0 0 0 0 0 0]
targets.shape: (64, 20)
targets[0]: [ 15 2496 190 4 3 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0]
精确的向量可能不同,但您应该看到形状应该都是(批次大小,序列长度)。以上一些代码是从 François Chollet 的教程中借用的,使用序列到序列 Transformer 进行英语到西班牙语翻译。您可能还想看看他的 Transformer 实现与本小课程有何不同。
在下一课中,您将转到位置编码的主题。
第 04 课:位置编码矩阵
当一个句子被向量化时,您会得到一个整数向量,其中每个整数代表一个单词。这里的整数只是一个标签。我们不能假设两个整数彼此更接近意味着它们表示的单词相关。
为了理解单词的含义以及量化两个单词如何相互关联,您将使用词嵌入技术。但是为了理解上下文,您还需要知道每个单词在句子中的位置。这是通过位置编码来实现的。
在论文Attention Is All You Need中,位置编码使用一个向量来表示每个令牌的位置。向量的元素是正弦波的不同相位和频率的值。具体来说,在位置 k = 0 , 1 , ⋯ , L − 1 k=0, 1, \cdots, L-1 k=0,1,⋯,L−1 处,位置编码向量(长度为 d d d)为
$$
[P(k,0), P(k,1), \cdots, P(k,d-2), P(k,d-1)]
$$
其中对于 i = 0 , 1 , ⋯ , d / 2 i=0, 1, \cdots, d/2 i=0,1,⋯,d/2,
$$
\begin{aligned}
P(k, 2i) &= \sin\Big(\frac{k}{n^{2i/d}}\Big) \
P(k, 2i+1) &= \cos\Big(\frac{k}{n^{2i/d}}\Big)
\end{aligned}
$$
在论文中,他们使用了 n = 10000 n=10000 n=10000。
实现位置编码并不困难,特别是如果您可以使用 NumPy 中的向量函数。
import pickle
import matplotlib.pyplot as plt
import numpy as np
def pos_enc_matrix(L, d, n=10000):
"""Create positional encoding matrix
Args:
L: Input dimension (length)
d: Output dimension (depth), even only
n: Constant for the sinusoidal functions
Returns:
numpy matrix of floats of dimension L-by-d. At element (k,2i) the value
is sin(k/n^(2i/d)) while at element (k,2i+1) the value is cos(k/n^(2i/d))
"""
assert d % 2 == 0, "Output dimension needs to be an even integer"
d2 = d//2
P = np.zeros((L, d))
k = np.arange(L).reshape(-1, 1) # L-column vector
i = np.arange(d2).reshape(1, -1) # d-row vector
denom = np.power(n, -i/d2) # n**(-2*i/d)
args = k * denom # (L,d) matrix
P[:, ::2] = np.sin(args)
P[:, 1::2] = np.cos(args)
return P
# Plot the positional encoding matrix
pos_matrix = pos_enc_matrix(L=2048, d=512)
assert pos_matrix.shape == (2048, 512)
plt.pcolormesh(pos_matrix, cmap='RdBu')
plt.xlabel('Depth')
plt.ylabel('Position')
plt.colorbar()
plt.show()
with open("posenc-2048-512.pickle", "wb") as fp:
pickle.dump(pos_matrix, fp)
你可以看到,我们创建了一个函数来生成位置编码。我们使用 L = 2048 L=2048 L=2048和 d = 512 d=512 d=512进行了测试。输出将是一个 2048 × 512 2048\times 512 2048×512的矩阵。我们还在热图中绘制了编码。这应该看起来像下面这样。
位置编码矩阵的热图表示
你的任务
上述热图可能对你来说不太吸引人。更好的可视化方法是将正弦曲线与余弦曲线分开。尝试下面的代码重新使用序列化的位置编码矩阵,并获得更清晰的可视化:
import pickle
import matplotlib.pyplot as plt
import numpy as np
with open("posenc-2048-512.pickle", "rb") as fp:
pos_matrix = pickle.load(fp)
assert pos_matrix.shape == (2048, 512)
# Plot the positional encoding matrix, alternative way
plt.pcolormesh(np.hstack([pos_matrix[:, ::2], pos_matrix[:, 1::2]]), cmap='RdBu')
plt.xlabel('Depth')
plt.ylabel('Position')
plt.colorbar()
plt.show()
如果你愿意,你可以检查矩阵中不同“深度”代表不同频率的正弦曲线。一个可视化它们的例子如下所示:
...
plt.plot(pos_matrix[:, 155], label="high freq")
plt.plot(pos_matrix[:, 300], label="low freq")
plt.legend()
plt.show()
但是,如果你可视化矩阵的一个“位置”,你会看到一个有趣的曲线:
import pickle
import matplotlib.pyplot as plt
with open("posenc-2048-512.pickle", "rb") as fp:
pos_matrix = pickle.load(fp)
assert pos_matrix.shape == (2048, 512)
# Plot two curves from different position
plt.plot(pos_matrix[100], alpha=0.66, color="red", label="position 100")
plt.legend()
plt.show()
显示给你这个:
一个编码向量
编码矩阵在比较两个编码向量时很有用,可以告诉它们位置有多远。两个归一化向量的点积如果相同则为 1,当它们移动时迅速下降。这种关系可以在下面的可视化中看到:
import pickle
import matplotlib.pyplot as plt
import numpy as np
with open("posenc-2048-512.pickle", "rb") as fp:
pos_matrix = pickle.load(fp)
assert pos_matrix.shape == (2048, 512)
# Show the dot product between different normalized positional vectors
pos_matrix /= np.linalg.norm(pos_matrix, axis=1, keepdims=True)
p = pos_matrix[789] # all vectors compare to vector at position 789
dots = pos_matrix @ p
plt.plot(dots)
plt.ylim([0, 1])
plt.show()
标准化位置编码向量的点积
在下一课中,你将利用位置编码矩阵在 Keras 中构建一个位置编码层。
第 05 课:位置编码层
Transformer 模型
论文注意力机制中的 Transformer 模型如下所示:
位置编码层位于 Transformer 模型的入口处。但是,Keras 库并未提供此功能。你可以创建一个自定义层来实现位置编码,如下所示。
import numpy as np
import tensorflow as tf
def pos_enc_matrix(L, d, n=10000):
"""Create positional encoding matrix
Args:
L: Input dimension (length)
d: Output dimension (depth), even only
n: Constant for the sinusoidal functions
Returns:
numpy matrix of floats of dimension L-by-d. At element (k,2i) the value
is sin(k/n^(2i/d)) while at element (k,2i+1) the value is cos(k/n^(2i/d))
"""
assert d % 2 == 0, "Output dimension needs to be an even integer"
d2 = d//2
P = np.zeros((L, d))
k = np.arange(L).reshape(-1, 1) # L-column vector
i = np.arange(d2).reshape(1, -1) # d-row vector
denom = np.power(n, -i/d2) # n**(-2*i/d)
args = k * denom # (L,d) matrix
P[:, ::2] = np.sin(args)
P[:, 1::2] = np.cos(args)
return P
class PositionalEmbedding(tf.keras.layers.Layer):
"""Positional embedding layer. Assume tokenized input, transform into
embedding and returns positional-encoded output."""
def __init__(self, sequence_length, vocab_size, embed_dim, **kwargs):
"""
Args:
sequence_length: Input sequence length
vocab_size: Input vocab size, for setting up embedding matrix
embed_dim: Embedding vector size, for setting up embedding matrix
"""
super().__init__(**kwargs)
self.sequence_length = sequence_length
self.vocab_size = vocab_size
self.embed_dim = embed_dim # d_model in paper
# token embedding layer: Convert integer token to D-dim float vector
self.token_embeddings = tf.keras.layers.Embedding(
input_dim=vocab_size, output_dim=embed_dim, mask_zero=True
)
# positional embedding layer: a matrix of hard-coded sine values
matrix = pos_enc_matrix(sequence_length, embed_dim)
self.position_embeddings = tf.constant(matrix, dtype="float32")
def call(self, inputs):
"""Input tokens convert into embedding vectors then superimposed
with position vectors"""
embedded_tokens = self.token_embeddings(inputs)
return embedded_tokens + self.position_embeddings
# this layer is using an Embedding layer, which can take a mask
# see https://www.tensorflow.org/guide/keras/masking_and_padding#passing_mask_tensors_directly_to_layers
def compute_mask(self, *args, **kwargs):
return self.token_embeddings.compute_mask(*args, **kwargs)
def get_config(self):
# to make save and load a model using custom layer possible
config = super().get_config()
config.update({
"sequence_length": self.sequence_length,
"vocab_size": self.vocab_size,
"embed_dim": self.embed_dim,
})
return config
这一层确实结合了嵌入层和位置编码。嵌入层创建词嵌入,即将向量化的句子中的整数标记标签转换为能够携带单词含义的向量。通过嵌入,你可以知道两个不同单词在含义上有多接近。
嵌入输出取决于标记化的输入句子。但是位置编码是一个常数矩阵,因为它仅依赖于位置。因此,你在创建此层时创建了一个常量张量。当你将嵌入输出添加到位置编码矩阵中时,TensorFlow 会智能地匹配维度,在call()
函数中。
在上一层定义了两个额外的函数。compute_mask()
函数传递给了嵌入层。这是为了告诉输出的哪些位置是填充的。这将在 Keras 内部使用。get_config()
函数被定义为记住此层的所有配置参数。这是 Keras 中的标准实践,以便记住传递给构造函数的所有参数,并在 get_config()
中返回它们,以便可以保存和加载模型。
你的任务
将上述代码与第 03 课中创建的数据集 train_ds
以及下面的代码片段结合起来:
# From Lesson 03:
# train_ds = make_dataset(train_pairs)
vocab_size_en = 10000
seq_length = 20
# test the dataset
for inputs, targets in train_ds.take(1):
print(inputs["encoder_inputs"])
embed_en = PositionalEmbedding(seq_length, vocab_size_en, embed_dim=512)
en_emb = embed_en(inputs["encoder_inputs"])
print(en_emb.shape)
print(en_emb._keras_mask)
您应该看到类似以下的输出:
tf.Tensor(
[[ 10 4288 607 ... 0 0 0]
[ 28 14 4 ... 0 0 0]
[ 63 23 430 ... 2 0 0]
...
[ 136 315 100 ... 0 0 0]
[ 3 20 19 ... 0 0 0]
[ 44 16 6 ... 0 0 0]], shape=(64, 20), dtype=int64)
(64, 20, 512)
tf.Tensor(
[[ True True True ... False False False]
[ True True True ... False False False]
[ True True True ... True False False]
...
[ True True True ... False False False]
[ True True True ... False False False]
[ True True True ... False False False]], shape=(64, 20), dtype=bool)
您可以看到上面打印的第一个张量是一个批次(64 个样本)的向量化输入句子,填充为长度 20。每个标记是一个整数,但将转换为 512 维的嵌入。因此,en_emb
的形状如上所示为 (64, 20, 512)
。
上面打印的最后一个张量是使用的掩码。这实质上与输入匹配,其中位置不为零。当计算准确率时,必须记住不应计算填充位置。
在下一课中,您将完成 Transformer 模型的另一个构建模块。
第 06 课:Transformer 构建模块
回顾第 05 课中 Transformer 的图表,您将看到除了嵌入和位置编码之外,还有编码器(图表的左半部分)和解码器(图表的右半部分)。它们共享一些相似之处。特别是它们在开始处都有一个多头注意力块,并在末尾有一个前馈块。
如果您将每个构建模块单独创建为子模型,稍后再将它们组合成一个更大的模型将会更容易。
首先,您创建自注意力模型。它位于图中编码器和解码器的底部部分。一个多头注意力层将接受三个输入,即键、值和查询。如果三个输入都相同,我们称这个多头注意力层为自注意力。这个子模型将具有一个加和标准化层,其具有跳跃连接以归一化注意力层的输出。其实现如下:
import tensorflow as tf
def self_attention(input_shape, prefix="att", mask=False, **kwargs):
"""Self-attention layers at transformer encoder and decoder. Assumes its
input is the output from positional encoding layer.
Args:
prefix (str): The prefix added to the layer names
masked (bool): whether to use causal mask. Should be False on encoder and
True on decoder. When True, a mask will be applied such that
each location only has access to the locations before it.
"""
# create layers
inputs = tf.keras.layers.Input(shape=input_shape, dtype='float32',
name=f"{prefix}_in1")
attention = tf.keras.layers.MultiHeadAttention(name=f"{prefix}_attn1", **kwargs)
norm = tf.keras.layers.LayerNormalization(name=f"{prefix}_norm1")
add = tf.keras.layers.Add(name=f"{prefix}_add1")
# functional API to connect input to output
attout = attention(query=inputs, value=inputs, key=inputs,
use_causal_mask=mask)
outputs = norm(add([inputs, attout]))
# create model and return
model = tf.keras.Model(inputs=inputs, outputs=outputs, name=f"{prefix}_att")
return model
seq_length = 20
key_dim = 128
num_heads = 8
model = self_attention(input_shape=(seq_length, key_dim),
num_heads=num_heads, key_dim=key_dim)
tf.keras.utils.plot_model(model, "self-attention.png",
show_shapes=True, show_dtype=True, show_layer_names=True,
rankdir='BT', show_layer_activations=True)
上面定义的函数对编码器和解码器都是通用的。解码器将设置选项 mask=True
来对输入应用因果掩码。
设置一些参数并创建一个模型。绘制的模型将如下所示:
自注意力架构,关键维度=128
在解码器中,您有一个从自注意力模型和编码器输入的交叉注意力模型。在这种情况下,编码器的输出是值和键,而自注意力模型的输出是查询。在高层次上,它基于编码器对源句子上下文的理解,并将解码器输入的部分句子作为查询(可以为空),以预测如何完成句子。这与自注意力模型的唯一区别是,因此代码非常相似:
import tensorflow as tf
def cross_attention(input_shape, context_shape, prefix="att", **kwargs):
"""Cross-attention layers at transformer decoder. Assumes its
input is the output from positional encoding layer at decoder
and context is the final output from encoder.
Args:
prefix (str): The prefix added to the layer names
"""
# create layers
context = tf.keras.layers.Input(shape=context_shape, dtype='float32',
name=f"{prefix}_ctx2")
inputs = tf.keras.layers.Input(shape=input_shape, dtype='float32',
name=f"{prefix}_in2")
attention = tf.keras.layers.MultiHeadAttention(name=f"{prefix}_attn2", **kwargs)
norm = tf.keras.layers.LayerNormalization(name=f"{prefix}_norm2")
add = tf.keras.layers.Add(name=f"{prefix}_add2")
# functional API to connect input to output
attout = attention(query=inputs, value=context, key=context)
outputs = norm(add([attout, inputs]))
# create model and return
model = tf.keras.Model(inputs=[(context, inputs)], outputs=outputs,
name=f"{prefix}_cross")
return model
seq_length = 20
key_dim = 128
num_heads = 8
model = cross_attention(input_shape=(seq_length, key_dim),
context_shape=(seq_length, key_dim),
num_heads=num_heads, key_dim=key_dim)
tf.keras.utils.plot_model(model, "cross-attention.png",
show_shapes=True, show_dtype=True, show_layer_names=True,
rankdir='BT', show_layer_activations=True)
绘制的模型将如下所示。请注意,该模型有两个输入,一个用于上下文,另一个用于自注意力的输入。
具有键维度为 128 的交叉注意力架构
最后,在编码器和解码器的输出处都有前馈模型。在 Keras 中,它实现为Dense
层:
import tensorflow as tf
def feed_forward(input_shape, model_dim, ff_dim, dropout=0.1, prefix="ff"):
"""Feed-forward layers at transformer encoder and decoder. Assumes its
input is the output from an attention layer with add & norm, the output
is the output of one encoder or decoder block
Args:
model_dim (int): Output dimension of the feed-forward layer, which
is also the output dimension of the encoder/decoder
block
ff_dim (int): Internal dimension of the feed-forward layer
dropout (float): Dropout rate
prefix (str): The prefix added to the layer names
"""
# create layers
inputs = tf.keras.layers.Input(shape=input_shape, dtype='float32',
name=f"{prefix}_in3")
dense1 = tf.keras.layers.Dense(ff_dim, name=f"{prefix}_ff1", activation="relu")
dense2 = tf.keras.layers.Dense(model_dim, name=f"{prefix}_ff2")
drop = tf.keras.layers.Dropout(dropout, name=f"{prefix}_drop")
add = tf.keras.layers.Add(name=f"{prefix}_add3")
# functional API to connect input to output
ffout = drop(dense2(dense1(inputs)))
norm = tf.keras.layers.LayerNormalization(name=f"{prefix}_norm3")
outputs = norm(add([inputs, ffout]))
# create model and return
model = tf.keras.Model(inputs=inputs, outputs=outputs, name=f"{prefix}_ff")
return model
seq_length = 20
key_dim = 128
ff_dim = 512
model = feed_forward(input_shape=(seq_length, key_dim),
model_dim=key_dim, ff_dim=ff_dim)
tf.keras.utils.plot_model(model, "feedforward.png",
show_shapes=True, show_dtype=True, show_layer_names=True,
rankdir='BT', show_layer_activations=True)
绘制的模型将如下所示。请注意,第一个Dense
层使用 ReLU 激活,第二个层没有激活。然后添加了一个 dropout 层以进行正则化。
前馈子模型
您的任务
运行上述代码并验证您看到相同的模型图。重要的是您与布局相匹配,因为最终的变压器模型取决于它们。
在上面的代码中,使用了 Keras 的函数式 API。在 Keras 中,您可以使用顺序 API、函数式 API 或者子类化Model
类来构建模型。子类化也可以在这里使用,但顺序 API 不行。您能告诉为什么吗?
在下一节课中,您将利用这些构建块来创建编码器和解码器。
第 07 课:变压器编码器和解码器
再次查看第 05 课中变压器的图表。您会看到编码器是自注意力子模型连接到前馈子模型。另一方面,解码器是一个自注意力子模型,一个交叉注意力子模型,以及一个串联的前馈子模型。
一旦有了这些子模型作为构建块,创建编码器和解码器就不难了。首先,您有编码器。它足够简单,可以使用 Keras 顺序 API 构建编码器模型。
import tensorflow as tf
# the building block functions from Lesson 06
from lesson_06 import self_attention, feed_forward
def encoder(input_shape, key_dim, ff_dim, dropout=0.1, prefix="enc", **kwargs):
"""One encoder unit. The input and output are in the same shape so we can
daisy chain multiple encoder units into one larger encoder"""
model = tf.keras.models.Sequential([
tf.keras.layers.Input(shape=input_shape, dtype='float32', name=f"{prefix}_in0"),
self_attention(input_shape, prefix=prefix, key_dim=key_dim, mask=False, **kwargs),
feed_forward(input_shape, key_dim, ff_dim, dropout, prefix),
], name=prefix)
return model
seq_length = 20
key_dim = 128
ff_dim = 512
num_heads = 8
model = encoder(input_shape=(seq_length, key_dim), key_dim=key_dim, ff_dim=ff_dim,
num_heads=num_heads)
tf.keras.utils.plot_model(model, "encoder.png",
show_shapes=True, show_dtype=True, show_layer_names=True,
rankdir='BT', show_layer_activations=True)
绘制模型会看到它就像下面一样简单:
编码器子模型
解码器有些复杂,因为交叉注意力块还从编码器那里获取输入;因此,这是一个接受两个输入的模型。它实现如下:
import tensorflow as tf
# the three building block functions from Lesson 06
from lesson_06 import self_attention, cross_attention, feed_forward
def decoder(input_shape, key_dim, ff_dim, dropout=0.1, prefix="dec", **kwargs):
"""One decoder unit. The input and output are in the same shape so we can
daisy chain multiple decoder units into one larger decoder. The context
vector is also assumed to be the same shape for convenience"""
inputs = tf.keras.layers.Input(shape=input_shape, dtype='float32',
name=f"{prefix}_in0")
context = tf.keras.layers.Input(shape=input_shape, dtype='float32',
name=f"{prefix}_ctx0")
attmodel = self_attention(input_shape, key_dim=key_dim, mask=True,
prefix=prefix, **kwargs)
crossmodel = cross_attention(input_shape, input_shape, key_dim=key_dim,
prefix=prefix, **kwargs)
ffmodel = feed_forward(input_shape, key_dim, ff_dim, dropout, prefix)
x = attmodel(inputs)
x = crossmodel([(context, x)])
output = ffmodel(x)
model = tf.keras.Model(inputs=[(inputs, context)], outputs=output, name=prefix)
return model
seq_length = 20
key_dim = 128
ff_dim = 512
num_heads = 8
model = decoder(input_shape=(seq_length, key_dim), key_dim=key_dim, ff_dim=ff_dim,
num_heads=num_heads)
tf.keras.utils.plot_model(model, "decoder.png",
show_shapes=True, show_dtype=True, show_layer_names=True,
rankdir='BT', show_layer_activations=True)
模型将如下所示:
解码器子模型
您的任务
从第 06 课中复制三个构建块函数,并运行上述代码以确保您看到与编码器和解码器中所示的相同布局。
在下一节课中,您将使用到目前为止创建的构建块来完成变压器模型。
第 08 课:构建变压器
的确,一个变压器有编码器和解码器部分,每部分不仅仅是一个,而是一系列的编码器或解码器。听起来复杂,但如果你有构建块子模型来隐藏细节,就不那么复杂了。
参见第 05 课的图示,你会看到编码器和解码器部分只是编码器和解码器块的链。只有最后一个编码器块的输出被用作解码器块的输入。
因此,完整的变压器模型可以按如下方式构建:
import tensorflow as tf
# the positional embedding layer from Lesson 05
from lesson_05 import PositionalEmbedding
# the building block functions from Lesson 07
from lesson_07 import encoder, decoder
def transformer(num_layers, num_heads, seq_len, key_dim, ff_dim, vocab_size_src,
vocab_size_tgt, dropout=0.1, name="transformer"):
embed_shape = (seq_len, key_dim) # output shape of the positional embedding layer
# set up layers
input_enc = tf.keras.layers.Input(shape=(seq_len,), dtype="int32",
name="encoder_inputs")
input_dec = tf.keras.layers.Input(shape=(seq_len,), dtype="int32",
name="decoder_inputs")
embed_enc = PositionalEmbedding(seq_len, vocab_size_src, key_dim, name="embed_enc")
embed_dec = PositionalEmbedding(seq_len, vocab_size_tgt, key_dim, name="embed_dec")
encoders = [encoder(input_shape=embed_shape, key_dim=key_dim,
ff_dim=ff_dim, dropout=dropout, prefix=f"enc{i}",
num_heads=num_heads)
for i in range(num_layers)]
decoders = [decoder(input_shape=embed_shape, key_dim=key_dim,
ff_dim=ff_dim, dropout=dropout, prefix=f"dec{i}",
num_heads=num_heads)
for i in range(num_layers)]
final = tf.keras.layers.Dense(vocab_size_tgt, name="linear")
# build output
x1 = embed_enc(input_enc)
x2 = embed_dec(input_dec)
for layer in encoders:
x1 = layer(x1)
for layer in decoders:
x2 = layer([x2, x1])
output = final(x2)
# XXX keep this try-except block
try:
del output._keras_mask
except AttributeError:
pass
model = tf.keras.Model(inputs=[input_enc, input_dec], outputs=output, name=name)
return model
seq_len = 20
num_layers = 4
num_heads = 8
key_dim = 128
ff_dim = 512
dropout = 0.1
vocab_size_en = 10000
vocab_size_fr = 20000
model = transformer(num_layers, num_heads, seq_len, key_dim, ff_dim,
vocab_size_en, vocab_size_fr, dropout)
tf.keras.utils.plot_model(model, "transformer.png",
show_shapes=True, show_dtype=True, show_layer_names=True,
rankdir='BT', show_layer_activations=True)
代码中的 try
–except
块用于处理某些版本的 TensorFlow 中可能导致训练错误计算不正确的 bug。上面绘制的模型将如下所示。虽然不简单,但架构仍然是可处理的。
编码器中有 4 层,解码器中有 4 层的变压器
你的任务
从第 05、06 和 07 课中复制三个构建块函数,以便你可以运行上述代码并生成相同的图示。你将在后续课程中重用此模型。
在下一课中,你将为此模型设置其他训练参数。
第 09 课:准备变压器模型进行训练
在你可以训练你的变压器之前,你需要决定如何训练它。
根据论文 Attention Is All You Need,你正在使用 Adam 作为优化器,但使用了自定义学习率调度,
LR = 1 d model min ( 1 n , n m 3 ) \text{LR} = \frac{1}{\sqrt{d_{\text{model}}}} \min\big(\frac{1}{\sqrt{n}}, \frac{n}{\sqrt{m³}}\big) LR=dmodel1min(n1,m3n)
实现如下:
import matplotlib.pyplot as plt
import tensorflow as tf
class CustomSchedule(tf.keras.optimizers.schedules.LearningRateSchedule):
"Custom learning rate for Adam optimizer"
def __init__(self, key_dim, warmup_steps=4000):
super().__init__()
self.key_dim = key_dim
self.warmup_steps = warmup_steps
self.d = tf.cast(self.key_dim, tf.float32)
def __call__(self, step):
step = tf.cast(step, dtype=tf.float32)
arg1 = tf.math.rsqrt(step)
arg2 = step * (self.warmup_steps ** -1.5)
return tf.math.rsqrt(self.d) * tf.math.minimum(arg1, arg2)
def get_config(self):
# to make save and load a model using custom layer possible0
config = {
"key_dim": self.key_dim,
"warmup_steps": self.warmup_steps,
}
return config
key_dim = 128
lr = CustomSchedule(key_dim)
optimizer = tf.keras.optimizers.Adam(lr, beta_1=0.9, beta_2=0.98, epsilon=1e-9)
plt.plot(lr(tf.range(50000, dtype=tf.float32)))
plt.ylabel('Learning Rate')
plt.xlabel('Train Step')
plt.show()
学习率调度的设计方式是,开始时学习较慢,但随着学习加速。这有助于因为模型在开始时完全是随机的,你甚至无法过多信任输出。但随着你足够训练模型,结果应该足够合理,因此你可以更快地学习以帮助收敛。绘制的学习率看起来如下:
自定义学习率调度
接下来,你还需要定义用于训练的损失度量和准确度量。这个模型很特别,因为你需要对输出应用掩码,仅在非填充元素上计算损失和准确度。从 TensorFlow 的教程 使用 Transformer 和 Keras 进行神经机器翻译 中借用实现:
def masked_loss(label, pred):
mask = label != 0
loss_object = tf.keras.losses.SparseCategoricalCrossentropy(
from_logits=True, reduction='none')
loss = loss_object(label, pred)
mask = tf.cast(mask, dtype=loss.dtype)
loss *= mask
loss = tf.reduce_sum(loss)/tf.reduce_sum(mask)
return loss
def masked_accuracy(label, pred):
pred = tf.argmax(pred, axis=2)
label = tf.cast(label, pred.dtype)
match = label == pred
mask = label != 0
match = match & mask
match = tf.cast(match, dtype=tf.float32)
mask = tf.cast(mask, dtype=tf.float32)
return tf.reduce_sum(match)/tf.reduce_sum(mask)
有了这些,你现在可以如以下方式编译你的 Keras 模型:
vocab_size_en = 10000
vocab_size_fr = 20000
seq_len = 20
num_layers = 4
num_heads = 8
key_dim = 128
ff_dim = 512
dropout = 0.1
model = transformer(num_layers, num_heads, seq_len, key_dim, ff_dim,
vocab_size_en, vocab_size_fr, dropout)
lr = CustomSchedule(key_dim)
optimizer = tf.keras.optimizers.Adam(lr, beta_1=0.9, beta_2=0.98, epsilon=1e-9)
model.compile(loss=masked_loss, optimizer=optimizer, metrics=[masked_accuracy])
model.summary()
你的任务
如果你已正确实现所有功能,你应该能够提供所有构建块函数以使上述代码运行。尽量将到目前为止所做的所有内容保留在一个 Python 脚本或一个 Jupyter 笔记本中,并运行一次以确保没有错误产生且没有引发异常。
如果一切顺利运行,你应该看到 summary()
输出如下:
Model: "transformer"
__________________________________________________________________________________________________
Layer (type) Output Shape Param # Connected to
==================================================================================================
encoder_inputs (InputLayer) [(None, 20)] 0 []
embed_enc (PositionalEmbedding (None, 20, 128) 1280000 ['encoder_inputs[0][0]']
)
enc0 (Sequential) (None, 20, 128) 659712 ['embed_enc[0][0]']
enc1 (Sequential) (None, 20, 128) 659712 ['enc0[0][0]']
decoder_inputs (InputLayer) [(None, 20)] 0 []
enc2 (Sequential) (None, 20, 128) 659712 ['enc1[0][0]']
embed_dec (PositionalEmbedding (None, 20, 128) 2560000 ['decoder_inputs[0][0]']
)
enc3 (Sequential) (None, 20, 128) 659712 ['enc2[0][0]']
dec0 (Functional) (None, 20, 128) 1187456 ['embed_dec[0][0]',
'enc3[0][0]']
dec1 (Functional) (None, 20, 128) 1187456 ['dec0[0][0]',
'enc3[0][0]']
dec2 (Functional) (None, 20, 128) 1187456 ['dec1[0][0]',
'enc3[0][0]']
dec3 (Functional) (None, 20, 128) 1187456 ['dec2[0][0]',
'enc3[0][0]']
linear (Dense) (None, 20, 20000) 2580000 ['dec3[0][0]']
==================================================================================================
Total params: 13,808,672
Trainable params: 13,808,672
Non-trainable params: 0
__________________________________________________________________________________________________
此外,当您查看 Transformer 模型的图表及其在此处的实现时,您应该注意到图表显示了一个 softmax 层作为输出,但我们省略了它。在本课程中确实添加了 softmax。你看到它在哪里了吗?
在下一课中,您将训练此编译模型,该模型有 1400 万个参数,正如我们在上面的摘要中所看到的。
第 10 课:训练 Transformer
训练 Transformer 取决于您在所有先前课程中创建的一切。最重要的是,从第 03 课中的向量化器和数据集必须保存下来,因为它们将在本课程和接下来的课程中重复使用。
import matplotlib.pyplot as plt
import tensorflow as tf
# the dataset objects from Lesson 03
from lesson_03 import train_ds, val_ds
# the building block functions from Lesson 08
from lesson_08 import transformer
# the learning rate schedule, loss, and accuracy functions from Lesson 09
from lesson_09 import CustomSchedule, masked_loss, masked_accuracy
# Create and train the model
seq_len = 20
num_layers = 4
num_heads = 8
key_dim = 128
ff_dim = 512
dropout = 0.1
vocab_size_en = 10000
vocab_size_fr = 20000
model = transformer(num_layers, num_heads, seq_len, key_dim, ff_dim,
vocab_size_en, vocab_size_fr, dropout)
lr = CustomSchedule(key_dim)
optimizer = tf.keras.optimizers.Adam(lr, beta_1=0.9, beta_2=0.98, epsilon=1e-9)
model.compile(loss=masked_loss, optimizer=optimizer, metrics=[masked_accuracy])
epochs = 20
history = model.fit(train_ds, epochs=epochs, validation_data=val_ds)
# Save the trained model
model.save("eng-fra-transformer.h5")
# Plot the loss and accuracy history
fig, axs = plt.subplots(2, figsize=(6, 8), sharex=True)
fig.suptitle('Traininig history')
x = list(range(1, epochs+1))
axs[0].plot(x, history.history["loss"], alpha=0.5, label="loss")
axs[0].plot(x, history.history["val_loss"], alpha=0.5, label="val_loss")
axs[0].set_ylabel("Loss")
axs[0].legend(loc="upper right")
axs[1].plot(x, history.history["masked_accuracy"], alpha=0.5, label="acc")
axs[1].plot(x, history.history["val_masked_accuracy"], alpha=0.5, label="val_acc")
axs[1].set_ylabel("Accuracy")
axs[1].set_xlabel("epoch")
axs[1].legend(loc="lower right")
plt.show()
就是这样!
运行此脚本将需要几个小时,但一旦完成,您将保存模型和损失与准确率绘图。它应该看起来像下面这样:
训练过程中的损失和准确率历史记录
你的任务
在上述的训练设置中,我们没有使用 Keras 中的提前停止和检查点回调功能。在运行之前,请尝试修改上述代码以添加这些回调。
提前停止回调可以在没有进展时中断训练。检查点回调可以帮助您保存最佳分数的模型,而不是仅返回最后一个 epoch 的最终模型。
在下一课中,您将加载这个训练好的模型并进行测试。
第 11 课:从 Transformer 模型进行推断
在第 03 课中,您将原始数据集按 70%-15%-15% 的比例分割为训练、验证和测试集。您在第 10 课的 Transformer 模型训练中使用了训练和验证数据集。而在本课程中,您将使用测试集来查看您训练好的模型表现如何。
您在前一课中保存了您的 Transformer 模型。由于模型中有一些自定义层和函数,您需要创建一个 自定义对象作用域 来加载您保存的模型。
Transformer 模型可以给您一个标记索引。您需要使用向量化器查找此索引代表的单词。为了保持一致性,您必须重用与创建数据集时使用的相同向量化器。
创建一个循环以扫描生成的标记。换句话说,不要使用模型生成整个翻译的句子,而是只考虑直到看到结束标志的下一个生成词。第一个生成的词将是由起始标志生成的词。这也是您在第 02 课中处理目标句子的方式的原因。
以下是代码:
import pickle
import random
import numpy as np
import tensorflow as tf
# the dataset objects from Lesson 03
from lesson_03 import test_pairs, eng_vectorizer, fra_vectorizer
# the positional embedding layer from Lesson 05
from lesson_05 import PositionalEmbedding
# the learning rate schedule, loss, and accuracy functions from Lesson 09
from lesson_09 import CustomSchedule, masked_loss, masked_accuracy
# Load the trained model
custom_objects = {"PositionalEmbedding": PositionalEmbedding,
"CustomSchedule": CustomSchedule,
"masked_loss": masked_loss,
"masked_accuracy": masked_accuracy}
with tf.keras.utils.custom_object_scope(custom_objects):
model = tf.keras.models.load_model("eng-fra-transformer.h5")
# training parameters used
seq_len = 20
vocab_size_en = 10000
vocab_size_fr = 20000
def translate(sentence):
"""Create the translated sentence"""
enc_tokens = eng_vectorizer([sentence])
lookup = list(fra_vectorizer.get_vocabulary())
start_sentinel, end_sentinel = "[start]", "[end]"
output_sentence = [start_sentinel]
# generate the translated sentence word by word
for i in range(seq_len):
vector = fra_vectorizer([" ".join(output_sentence)])
assert vector.shape == (1, seq_len+1)
dec_tokens = vector[:, :-1]
assert dec_tokens.shape == (1, seq_len)
pred = model([enc_tokens, dec_tokens])
assert pred.shape == (1, seq_len, vocab_size_fr)
word = lookup[np.argmax(pred[0, i, :])]
output_sentence.append(word)
if word == end_sentinel:
break
return output_sentence
test_count = 20
for n in range(test_count):
english_sentence, french_sentence = random.choice(test_pairs)
translated = translate(english_sentence)
print(f"Test {n}:")
print(f"{english_sentence}")
print(f"== {french_sentence}")
print(f"-> {' '.join(translated)}")
print()
你的任务
首先,尝试运行此代码并观察推断结果。下面是一些示例:
Test 2:
it rained for three days .
== [start] il a plu pendant trois jours . [end]
-> [start] il a plu pendant trois jours . [end]
Test 3:
two people say they heard a gunshot .
== [start] deux personnes disent qu'elles ont entendu une détonation . [end]
-> [start] deux personnes disent qu'ils ont entendu un coup de feu . [end]
Test 4:
i'm not dead yet .
== [start] je ne suis pas encore mort . [end]
-> [start] je ne suis pas encore mort . [end]
Test 5:
i want us to get back together .
== [start] je veux que nous nous remettions ensemble . [end]
-> [start] je veux que nous nous [UNK] ensemble . [end]
每个测试的第二行是预期输出,而第三行是 Transformer 的输出。
标记[UNK]
表示“未知”或超出词汇范围,这种情况应该很少出现。对比输出,你会看到结果相当准确,但不会完美。例如,英语中的they在法语中可以映射为ils或elles,这取决于性别,而变压器模型并不总能区分这一点。
你逐词生成了翻译句子,但实际上变压器一次性输出整个句子。你应该尝试修改程序,以在 for 循环中解码整个变压器输出pred
,看看当你提供更多的前导词dec_tokens
时,变压器如何为你生成更好的句子。
在下一课中,你将回顾到目前为止的工作,并查看是否可以进行改进。
课程 12:改进模型
你做到了!
让我们回顾一下你做了什么以及可以改进的地方。你创建了一个变压器模型,该模型接受整个英语句子和一个部分法语句子(最多到第 k k k个标记)以预测下一个(第 ( k + 1 ) (k+1) (k+1)个)标记。
在训练中,你观察到最佳准确率为 70%到 80%。你如何改进它?这里有一些想法,但肯定不是详尽无遗的:
-
你为文本输入使用了一个简单的标记器。像 NLTK 这样的库可以提供更好的标记器。此外,你没有使用子词标记化。对于英语,这不是大问题,但对于法语则比较棘手。这就是为什么在你的模型中法语词汇量大得多(例如,l’air(空气)和d’air(的空气)将成为不同的标记)。
-
你用嵌入层训练了自己的词嵌入。已经有现成的预训练嵌入(如 GloVe),它们通常提供更好的质量的嵌入。这可能有助于你的模型更好地理解上下文。
-
你为变压器设计了一些参数。你使用了 8 个头的多头注意力,输出向量维度为 128,句子长度限制为 20 个标记,丢弃率为 0.1,等等。调整这些参数肯定会对变压器产生影响。训练参数同样重要,如轮次数量、学习率调度和损失函数。
你的任务
找出如何更改代码以适应上述变化。但如果我们测试一下,你知道如何判断一个模型是否优于另一个模型吗?
在下面的评论中发布你的答案。我很想看看你提出了什么。
这就是最后一课。
结束!(看看你走了多远)
你做到了。干得好!
花点时间回顾一下你取得的进展。
-
你学会了如何处理一个纯文本句子并将其向量化
-
你根据论文Attention Is All You Need分析了变压器模型的构建块,并使用 Keras 实现了每个构建块。
-
你将构建块连接成一个完整的变压器模型,并进行了训练。
-
最后,你可以见证经过训练的模型以高精度将英语句子翻译成法语。
总结
你在这个迷你课程中表现如何?
你喜欢这个速成课程吗?
你有任何问题吗?是否有遇到难点?
告诉我。请在下面留下评论。