5. 编码器-解码器架构
- 课程地址:https://www.cloudskillsboost.google/course_templates/543
- 视频学习:https://youtu.be/qZxtqYlZHnM
生成式AI和编码器-解码器架构
作者:Benoit Dherin,Google高级解决方案实验室的机器学习工程师
引言
大家好,我叫 Benoit Dherin。在Google的高级解决方案实验室,我担任机器学习工程师。目前,围绕生成式AI和新的进步(包括新的Vertex AI功能,如GenAI Studio、Model Garden、Gen AI API)有很多令人兴奋的事情。
目标
我们在这些短期课程中的目标是让您扎实地了解一些使所有GenAI魔法成为可能的基本概念。
编码器-解码器架构概述
- 编码器-解码器架构是一个序列到序列的架构。
- 这意味着它需要一个单词序列作为输入,并输出对应的翻译序列。
例如: 输入:英语中的句子“The cat ate the mouse” 输出:法语翻译“Le chat a mangé la souris”。
架构细节
- 编码器阶段产生输入语句的向量表示。
- 解码器阶段使用这个向量表示创建序列输出。
- 编码器和解码器都可以用不同的内部架构来实现,比如递归神经网络(RNN)或更复杂的转换器块。
递归神经网络(RNN)编码器
- 一次获取输入序列中的每个标记,并生成表示该标记以及先前摄取的标记的状态。
- 将所有输入标记摄入RNN后,输出一个向量,代表完整的输入句子。
RNN 解码器
- 采用输入语句的向量表示,并从该表示中生成输出语句。
- 分步进行,使用当前状态和到目前为止已解码的内容一次解码输出一个标记。
训练阶段
- 要训练模型,您需要一个数据集,它是输入/输出对的集合,您想让您的模型模仿。
- 在训练期间,模型将根据它在数据集中的给定输入上产生的错误来校正其权重。
- 这个错误本质上是神经网络在给定输入序列的情况下生成的结果与数据集中的真实输出序列之间的差异。
如何产生训练数据集
- 在编码器-解码器架构的情况下,您需要一组输入和输出文本。
- 在翻译的情况下,这将是一个句子在源语言的句子对和目标语言。
- 您会将源语言句子提供给编码器,然后计算解码器生成的内容与实际翻译之间的误差。
训练编码器-解码器模型:教师强制
- 训练过程中的陷阱: 解码器在训练时也需要自己的输入!
- 您需要为解码器提供正确的先前翻译的标记作为输入以生成下一个标记,而不是解码器到目前为止生成的标记。
- 这种训练方法称为教师强制,因为您强制解码器从正确的前一个标记生成下一个标记。
输入准备:两个输入句子
- 一个是输入到编码器的原始句子。
- 另一个是您将输入到解码器的、向左移动的原始句子。
生成标记:贪婪搜索与波束搜索
- 解码器在每一步只生成词汇表中每个标记是下一个标记的概率。
- 使用这些概率,您必须选择一个词。
- 贪婪搜索: 生成概率最高的标记。
- 波束搜索: 使用解码器生成的概率来评估句子块而不是单个单词的概率,并在每一步都保留最有可能生成的块。
服务阶段:生成新的翻译或响应
- 训练结束后,在服务时间,您将从提供编码器表示开始。
- 给解码器一个特殊的起始标记,如“GO”,这将提示解码器生成第一个单词。
生成阶段细节
- 起始标记的嵌入: 使用嵌入层将起始标记转化为向量表示。
- 循环层的状态更新: 将编码器产生的先前状态更新为新状态。
- 生成单词概率: 这个状态将被传递到一个密集的softmax层以产生单词概率。
- 选择单词: 最后这个单词是通过贪婪搜索或波束搜索的最高概率块选择的。
与大型语言模型的比较
- 我们刚刚了解的架构与大型语言模型中的架构之间的区别在于编码器和解码器块内部的内容。
- 简单的RNN网络被transformer块取代,这是一种在谷歌发现的、基于注意力机制的架构。
更多学习资源
- 如果您有兴趣了解更多关于这些主题的信息,我们在该系列中还有两门概览课程:注意力机制:概览,以及Transformer模型和BERT模型:概览。
实验室演练
- 如果您喜欢今天的课程,请查看编码器-解码器架构:实验室演练,我将在其中向您展示如何使用我们在本概述中看到的概念在代码中生成诗歌。
谢谢你的时间!祝你有美好的一天!
使用编码器-解码器架构构建诗歌生成器
作者:Benoit Dherin,Google高级解决方案实验室的机器学习工程师
引言
大家好!我叫 Benoit Dherin,是 Google 高级解决方案实验室的机器学习工程师。如果您想了解更多有关高级解决方案实验室的信息,请点击下面描述框中的链接。
生成式 AI 的新进展
目前围绕生成式 AI 和新进展有很多令人兴奋的事情,包括新的 Vertex AI 功能,例如 GenAI Studio、Model Garden、GenAI API。
本次会议目标
在这个简短的会议中,我们的目标是让您对一些使所有 Gen AI 魔法成为可能的基本概念有一个坚实的基础。
编码器-解码器架构概述
今天,我将回顾一下互补的代码到同一系列的“编码器-解码器架构概述”课程。我们将一起了解如何使用编码器-解码器架构从头开始构建诗歌生成器。使用这种解码器架构,您会在我们的 GitHub 存储库中找到设置说明。
代码实验室
好的,现在让我们看一下代码。要访问我们的实验室,请进入 asl-ml-immersion
文件夹。然后是 notebooks
文件夹。然后是 text_models
文件夹。在解决方案中,您会找到文本生成笔记本。
文本生成器
在本实验中,我们将实现一个基于字符的文本生成器,基于编码器解码器架构。基于字符意味着网络消耗和生成的令牌是字符而不是单词。我们将使用 plays 作为数据集。他们有一个特殊的结构,就是人们互相交谈的结构。
网络的特点
在这里,您可以看到一段由经过训练的神经网络生成的文本示例,当句子不一定有意义或语法不正确时。这在很多方面都是非凡的。首先,请记住,它是基于角色的。这意味着它学会了只预测最可能的字符。尽管如此,它还是能够很好地学习由空格分隔的单词的概念,以及角色相互交谈的戏剧的基本结构。
因此,这是一个非常小的网络。正如您将看到的,它基于 RNN 架构,并且仅在 Vertex AI Workbench 中训练了 30 个时期,这是一个非常快的训练时间。
现在进入代码
现在让我们看一下代码。所以第一件事就是导入我们需要的库。特别是,我们可以使用 TensorFlow Keras 或编码器解码器架构来传达这一点。
然后我们使用 TF Keras utils.get_file
下载我们的数据集。所以现在数据集在磁盘上,我们只需要将它加载到一个可变的内存中。所以文本变量现在包含代表莎士比亚数据集中的所有戏剧。
我可以快速看一下它是什么吗?你看,我们是否打印了前 250 个字符。你有第一批公民与他们交谈,每个人和其他每个人都在和第一位公民说话。
该单元格计算我们在文本数据集中拥有的唯一字符的数量,我们看到我们有 65 个独特的字符,对吗?这些字符将是神经网络将在训练期间使用并在服务期间生成的标记。
所以现在的第一步是矢量化文本。
从字符串到字符序列
首先,我们需要从实际的字符串中提取字符序列。我们可以通过使用 TensorFlow 的 TF 字符串 Unicode 拆分
来实现这一点。
转换为数字
现在,例如,这里的文本被转换为字符序列列表。神经网络不能直接处理这些字符。我们需要将其转化为数字。
字符到 ID 的映射
我们需要简单地将每个字符映射到给定的 ID。为此,我们使用 TF Keras 层字符串查找
,只需要将其传递给您的词汇表。我们的语料库中有 65 个独特的字符,我们生成一个层,当字符通过时将生成相应的 ID。
在这个层中,你有一个在字符和 ID 之间生成的映射。要获得逆向映射,您使用同一层字符串查找,使用与您使用 get vocabulary
从词汇表检索到的完全相同的参数,但是将 invert
参数设置为 True
。这将计算反向映射,即从 ID 到字符的映射。实际上,如果您将 ID 的这个映射序列传递给它,它会返回给您相应的字符。
准备训练数据集
现在,让我们将这些字符序列作为训练神经网络的数据集。为此,我们正在使用 TF 数据数据集 API
。它允许我们将大量的切片(例如,戏剧文本的整个语料库)转换为 TensorFlow 数据集。
在这一点上,这些数据集的元素只是单个字符。但我们想为我们的神经网络提供相同长度的序列,而不仅仅是一个字符。我们需要预测下一个字符。幸运的是,数据集 API 有一个很好的函数 batch
,可以为我们做到这一点。例如,如果我们在 ID 数据集上调用 batch
方法,并将给定的序列长度(在这里是100)传递给它,现在我们的数据集中的元素不再是单个字符,而是100个字符的序列。
创建输入和目标序列
我们仍然需要创建要传递给解码器的输入序列以及我们想要预测的序列。例如,如果我们有序列 "TensorFlow",那么我们的输入序列可能是 "TensorFlo",而我们要预测的目标序列将是 "ensorFlow"。这个小函数就是这样做的,它需要一个原始序列,通过截断该序列从中创建一个输入序列,并通过从开始添加第一个字符开始创建目标序列。我们只需将 split_input_target
函数映射到我们的序列数据集。
构建模型
首先,我们设置了一些变量,例如词汇量大小和我们想要表示字符的向量大小(这里设为256)。对于模型本身,这是一个相对简单的模型。我们使用 Keras 子类 API 创建它。
我们只创建一个名为 MyModel
的新类,它从 TF Keras Model
继承。这个类需要覆盖两个函数:构造函数和调用函数。构造函数采用模型的超参数(如词汇量大小、嵌入维度、循环层的神经元数量)并初始化需要的层。调用函数指定这些层如何连接,即定义网络的体系结构。
训练模型
在我们训练模型之前,我们需要一个损失函数。由于这本质上是一个多类分类问题(每个可能的下一个字符是一个类),因此损失将是稀疏的分类交叉熵损失。我们配置这个损失是从 logits(不是直接的概率)计算的。
一旦我们有了损失,我们就可以编译我们的模型,这意味着我们将损失和优化器联系在一起。优化器将在训练期间更新权重以尽可能减少损失。
最后,我们开始训练模型。我们选择了一些我们想要进行训练的时期。一个时期是对数据集的完整传递。我们在这里进行了多次训练,并提供了回调以确保在训练期间保存权重。
首先,我们需要从实际的字符串中提取字符序列。为了做到这一点,我们可以使用 TensorFlow 的 TF 字符串 Unicode 拆分
功能。
例如,此时文本已经被转换为字符序列列表。但是,神经网络不能直接处理这些字符序列。因此,我们需要将这些字符序列转化为数字。
为了将每个字符映射到给定的 ID,我们可以使用 TF Keras
层中的 StringLookup
函数。您只需要将您的词汇表传递给这个函数。在我们的例子中,语料库包含 65 个独特的字符。我们生成一个层,当字符通过这个层时,它将生成相应的 ID。
在这个层中,你会有一个从字符到 ID 的映射。要获得反向映射(即从 ID 到字符的映射),您可以使用同一层的 StringLookup
函数,但需要将参数 invert
设置为 True
。
现在让我们将这些处理后的数据作为神经网络的训练数据集。
为此,我们使用 TF Data Dataset API
。这个 API 有一个优秀的方法,可以将整个戏剧文本语料库(它代表了大量的实例)作为 ID 存储,以便获取一个数据集。
在此阶段,数据集的元素仅是单个字符,这对我们来说不够。我们希望为神经网络提供相同长度的序列,而不仅仅是一个字符,并需要预测下一个字符。
幸运的是,Dataset API
有一个非常有效的 batch
函数。当我们在 ID 数据集上调用这个 batch
方法时,我们可以传递给它一个指定的序列长度,例如 100。这样,我们的数据集中存储的元素将不再是单个字符,而是一个包含 100 个字符的序列。
但我们还没有完全完成。
我们仍然需要创建要传递给解码器的输入序列以及我们想要预测的目标序列。例如,假设我们有一个序列 TensorFlow
,那么我们可以创建一个输入序列 tens-or-flow
,而目标序列是相同的序列,但向右移动了一个字符,变成 ensor-flow
。
为了实现这个目的,我们需要一个小函数。这个函数接受一个原始序列,通过截断最后一个字符来创建输入序列,并且通过从开始处添加第一个字符来创建目标序列。我们只需将这个 split_input_target
函数映射到我们的序列数据集上。
现在,让我们看看如何构建模型。
首先,我们设置了一些变量,包括词汇量大小、向量大小(我们想要用来表示字符的向量大小,这里设置为 256),以及我们所需的一些神经元或循环层。
对于模型本身,这是一个相对简单的模型。我们使用 Keras Subclass API
来创建它。我们创建一个名为 MyModel
的新类,并从 TF Keras Model
类继承。当你这样做时,你只需要覆盖两个函数:构造函数和调用函数。
构造函数主要接受模型的超参数,包括词汇量大小、嵌入维度和循环层的神经元数量,并将你需要的层存储为类的变量。
call
函数指定了整个网络的架构。这里,输入是代表字符的 ID 序列。我们首先有一个嵌入层,为每个 ID 创建一个表示向量。这是一个可以训练的层,因此,随着训练的进行,这些向量将开始变得越来越有意义。然后,我们将这些字符的向量表示传递给一个循环层。
最后,我们将循环层的输出传递给一个密集层。这个密集层将输出与我们词汇表大小相同的分数,这意味着为 65 个可能的字符中的每一个打分。这些分数表示该字符是下一个字符的概率。
完成模型构建后,我们可以使用 model.summary()
查看模型的结构。
创建初始向量层
我们有一个第一层,我们将为每个初始状态(init)创建一个表示它的向量。这就是训练层。随着训练的进行,这个向量逐渐开始代表了字符。
字符向量的演进
我们期待这些字符向量将开始变得越来越有意义。这些字符的静态表示将被传递给循环层,在这里我们将以某种方式修改这些表示。根据当前上下文与之前的内容,生成相应的状态,这将是下一步的重用。
构建密集输出层
最后,我们将循环层的输出传递给一个密集层。这个层将输出与我们词汇表中一样多的数字,这意味着为 65 个可能的字符中的每一个打分。这些分数表示该字符是下一个字符的概率。这就是模型所做的一切。接下来,我们开始实例化这个模型。
查看和理解模型结构
完成后,我们可以使用模型摘要查看模型的结构。在这里,你会看到我们正在构建的层,包括循环层和我们刚刚编码的密集层。这些层已经在我们的模型中实现了。
模型的训练准备
在我们开始训练模型之前,我们需要一个损失函数。这是我们将模型的输出与实际情况进行比较的工具。由于这本质上是一个分类问题,每一类对应于下一个可能的字符,所以我们选择使用稀疏分类交叉熵作为损失函数。而且,因为神经网络的输出(logits)不是直接的概率,我们配置这个损失函数是基于 logits 计算的,而不是概率。
结合模型与优化器
一旦我们有了损失函数,我们就可以组装我们的模型。这基本上意味着我们将损失函数和优化器联系在一起。这将在训练期间更新模型的权重,以尽可能减少损失。此外,我们设置了一些回调函数,用于在训练期间保存模型的权重。
开始训练
这是一个重要的步骤。我们现在准备好开始训练了。我们对数据集进行了模型拟合操作,并选择了一定数量的训练周期。每一个训练周期意味着模型对整个数据集的一次完整遍历。在这个例子中,我们将数据集遍历了多次。同时,我们设置了回调以确保在训练期间保存权重。
使用训练好的模型进行文本生成
现在我们有了一个训练好的模型,下一步我们要用它做什么呢?在这个阶段,事情变得有点复杂。我们不能直接使用训练好的模型来生成文本,我们需要编写一个解码函数。这个解码函数将使用训练好的模型,一步一步地生成文本。在这种情况下,我们选择将这个解码功能实现为一个 Keras 模型。
继承 TF Keras 模型并实现生成步骤
我们从 TF Keras 模型继承。在该模型中,主要方法是生成一个步骤。它接受输入,这些输入可以作为初始提示(initial prompt)来触发模型完成字符序列的预测,从而生成新的字符。
文本预处理和模型调用
你需要将输入文本转换为字符序列,然后再将这些字符序列转换为 ID 序列。接下来,我们调用我们之前训练过的编码器-解码器模型。这个模型将接受这些 ID 输入,并输出预测的 logits。
温度调整和字符选择
在选择最有可能的字符之前,有一个技巧:我们将 logits 除以一个称为“温度”的参数。如果温度很高,每个可能字符的分数会变得相似,从而产生更多的随机性和创造性。如果温度很低,模型将更有可能选择得分最高的字符,从而表现出更确定的行为。
从 Logits 到最终字符
有了预测的 logits 后,我们使用 TF.random.categorical
函数从这些概率分数中抽样,以确定下一个最有可能的字符。然后我们将这个选择的字符 ID 转换回字符,这就是我们最终返回的内容。
解码函数的实现
这就是解码函数的本质,大多数解码函数都遵循相似的结构。此外,这个温度调整技巧可以被看作是大型语言模型的一个重要参数。
使用解码函数生成文本
在实践中,我们通常在一个循环中使用解码函数。例如,为了生成一个包含 1000 个字符的文本,我们可以循环地调用解码函数,每次都将之前预测的内容和状态提供给该函数,并预测下一个字符和新状态。
示例与实践
例如,我们可以开始这个过程,使用一些初始提示,如“罗密欧”,然后观察神经网络生成的文本。虽然初步的训练结果可能不完美,但我们可以看到模型已经能够从输入数据的结构中提取一定的信息。
结语
如果你喜欢这个演示,可以在我们的 ASL GitHub 存储库中找到更多信息。该存储库包含 90 多个机器学习相关的笔记本示例。如果你觉得这些内容有用,不要忘记给我们的 GitHub 仓库加星标。谢谢你的时间!
github:
https://github.com/GoogleCloudPlatform/asl-ml-immersion/blob/master/notebooks/text_models/solutions/text_generation.ipynb