构建类似 Llama-2 和 Llama-3 的仅解码器 Transformer 模型

当今的大型语言模型是 Transformer 模型的简化形式。它们被称为仅解码器模型,因为它们的作用类似于 Transformer 的解码器部分,即给定部分序列作为输入,生成输出序列。从架构上讲,它们更接近 Transformer 模型的编码器部分。在这篇文章中,您将构建一个用于文本生成的仅解码器 Transformer 模型,其架构与 Meta 的 Llama-2 或 Llama-3 相同。具体来说,您将学习:

  • 如何构建仅解码器模型
  • 仅解码器模型的架构设计变体
  • 如何训练模型

让我们开始吧。

构建用于文本生成的仅解码器 Transformer 模型
图片来源:Jay。保留部分权利。

概述

本文分为五个部分,它们是:

  • 从完整 Transformer 到仅解码器模型
  • 构建仅解码器模型
  • 用于自监督学习的数据准备
  • 训练模型
  • 扩展

从完整 Transformer 到仅解码器模型

Transformer 模型最初是作为序列到序列 (seq2seq) 模型而出现的,它将输入序列转换为上下文向量,然后使用该向量生成新的序列。在此架构中,编码器部分负责将输入序列转换为上下文向量,而解码器部分则从该上下文向量生成新的序列。

我们能否不使用上下文向量生成一个全新的序列,而是将其投影到表示词汇表中每个标记概率的 logits 向量中?这样,给定部分序列作为输入,模型就可以预测下一个最可能的标记。通过迭代地将序列反馈给模型,我们可以一次生成一个标记的连贯文本,就像文本编辑器中的自动完成功能一样。这种简化架构只专注于预测下一个标记,被称为仅解码器模型。

构建仅解码器模型

仅解码器模型比完整的 Transformer 模型具有更简单的架构。从上一篇文章中讨论的完整 Transformer 架构开始,您可以通过完全移除编码器组件并调整解码器以独立操作来创建仅解码器模型。

此实现重用了完整 Transformer 模型中的大部分代码。`DecoderLayer` 类与之前实现中的 `EncoderLayer` 具有相同的结构。`TextGenerationModel` 类具有简化的 `forward()` 方法,因为它不再需要处理编码器-解码器交互。它只是将输入的 token ID 转换为嵌入,通过堆叠的解码器层进行处理,并将输出投影到表示词汇表中每个 token 概率的 logits。

在图中,该模型如下所示。这与 Meta 提出的 Llama-2/Llama-3 模型的架构设计相同。

遵循 Llama-2/Llama-3 架构的仅解码器模型

用于自监督学习的数据准备

我们的目标是创建一个模型,即使提示只包含一个单词,也能从给定的提示生成连贯的文本段落。为了有效地训练这样的模型,我们需要仔细考虑我们的训练方法和数据要求。

我们将使用的训练技术称为自监督学习。与需要手动标记数据的传统监督学习不同,自监督学习利用文本本身的内在结构。当我们输入一个文本序列时,模型会学习预测下一个标记,而文本中实际的下一个标记则作为真实标签。这消除了手动标记的需要。

训练数据集的大小至关重要。对于包含 $N$ 个标记的词汇表和包含 $M$ 个单词的数据集,每个标记平均出现约 $M/N$ 次。为了确保模型学习所有标记的有意义的表示,这个比率需要足够大。

在这篇文章中,您将从古腾堡计划下载一些小说,并将其用作训练模型的数据集。

这些来自不同作者和不同体裁的公共领域小说提供了一个多样化的数据集,这将帮助我们的模型学习广泛的词汇和写作风格。

下载这些小说后,您可以将主要内容提取为字符串,并将这些字符串保留为列表。

下一步是创建分词器。您可以通过将文本分割成单词来构建一个简单的分词器。您也可以使用字节对编码 (BPE) 算法来创建一个更复杂的分词器,如下所示:

这使用 `tokenizers` 库来训练 BPE 分词器。您调用 `get_dataset_text()` 获取所有小说的文本,然后在此基础上训练分词器。您还需要两个特殊标记:`[pad]` 和 `[eos]`。最重要的是,`[eos]` 标记用于指示序列的结束。如果您的模型生成此标记,您就知道可以停止生成。

训练模型

分词器和数据集准备就绪后,您现在可以训练模型。

首先,您需要创建一个可用于训练模型的 `Dataset` 对象。PyTorch 为此提供了一个框架。

此 `Dataset` 对象用于创建一个 `DataLoader` 对象,该对象可用于训练模型。`DataLoader` 对象将自动批量处理数据并对其进行混洗。

`Dataset` 对象在 `__getitem__()` 方法中生成一对输入和输出序列。它们的长度相同,但偏移了一个标记。当输入序列传递给模型时,模型为序列中的每个位置生成下一个标记。因此,真实输出来自同一源,偏移了一个标记。这就是您如何设置自监督训练。

现在您可以创建并训练模型了。您可以使用此代码创建一个非常大的模型。但是,如果您不期望模型非常强大,您可以设计一个更小的模型。让我们创建一个具有以下特征的模型:

  • 8 层
  • 注意力机制使用 8 个查询头和 4 个键值头
  • 隐藏维度为 768
  • 最大序列长度为 512
  • 注意力机制的 dropout 设置为 0.1
  • 使用 AdamW 优化器训练,初始学习率为 0.0005
  • 学习率调度器具有 2000 步预热,然后是余弦退火
  • 训练 2 个 epoch,批量大小为 32,梯度范数裁剪为 6.0

以上一切都很典型。训练一个仅解码器模型通常需要非常大的数据集,并且 epoch 的数量可能少至 1。重要的是训练步数。训练将使用线性预热在开始时逐渐增加学习率,这可以减少模型初始化方式的影响。然后,余弦退火将逐渐降低学习率,以便在训练结束时,当模型几乎收敛时,它将学习率保持在一个非常小的值以稳定结果。

模型创建和训练的代码如下:

在训练循环中,您执行了常见的前向和后向传播。每当损失改善时,模型都会被保存。为简单起见,没有实现评估。您应该定期(不一定在每个 epoch 之后)评估模型以监控进度。

由于词汇量和序列长度较大,训练过程计算密集。即使在高端 RTX 4090 GPU 上,每个 epoch 也需要大约 10 小时才能完成。

训练完成后,您可以加载模型并生成文本。

模型在 `generate_text()` 函数中用于生成文本。它期望一个不完整的句子作为输入 `prompt`,模型将在 for 循环的每一步中用于生成下一个标记。生成算法使用概率采样,而不是总是选择最可能的标记。这使得模型能够生成更具创造性的文本。`temperature` 参数控制生成文本的创造性水平。

模型的输出是一个 logits 向量,采样过程生成一个 token ID 向量。这个向量将由分词器转换回字符串。

如果您运行此代码,您可能会看到以下输出:

尽管生成的文本显示出一定的连贯性和对语言模式的理解,但它并不完美。然而,考虑到我们模型相对较小的规模和有限的训练数据,这些结果令人鼓舞。

为了完整性,下面是模型和训练的完整代码:

扩展

虽然我们已经成功地实现了一个基本的仅解码器模型,但现代大型语言模型(LLM)要复杂得多。以下是关键的改进领域:

  1. 规模和架构:现代LLM使用更多的层和更大的隐藏维度。它们还融合了我们在这里实现的之外的先进技术,例如专家混合(mixture of experts)。
  2. 数据集大小和多样性:我们目前的数据集,仅包含几兆字节的小说文本,与现代LLM中使用的万亿字节级数据集相比,微不足道。生产模型是在多种语言的各种内容类型上进行训练的。
  3. 训练流程:我们在这里实现的是LLM开发中的“预训练”。生产模型通常会通过额外的微调阶段,使用专门的数据集和定制的训练目标来完成特定任务,例如问答或指令遵循。
  4. 训练基础设施:训练大型模型需要复杂的分布式训练技术,跨多个GPU进行训练、梯度累积以及其他优化,这需要对我们的训练循环进行重大修改。

进一步阅读

以下是一些您可能会觉得有用的链接:

总结

在这篇文章中,您已经了解了构建一个仅解码器Transformer模型进行文本生成的过程。具体来说,您学到了:

  • 如何将完整的Transformer架构简化为仅解码器模型
  • 实现用于文本生成任务的自监督学习
  • 使用训练好的模型创建文本生成流程

这种仅解码器架构是许多现代大型语言模型的基础,使其成为自然语言处理领域中一个至关重要的概念。

暂无评论。

留下评论

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