Python 深度学习第三版(三)

原文:annas-archive.org/md5/c9f2178832e6a58ea7603b6cecb80b22

译者:飞龙

协议:CC BY-NC-SA 4.0

第八章:深入探索大型语言模型

近年来,学术界、工业界甚至大众对 Transformer 的兴趣急剧上升。当前最前沿的基于 Transformer 的架构被称为大型语言模型LLM)。其最吸引人的特点是文本生成能力,而最著名的例子便是 ChatGPT(chat.openai.com/)。但在其核心,依旧是我们在 第七章中介绍的朴素 Transformer。幸运的是,我们已经建立了坚实的 Transformer 基础。这一架构的一个显著特点是,自从引入以来,它几乎没有发生过太大变化。相反,LLM 的能力随着模型规模的增大而增长(这一点从名称上就可以看出来),这也为“量变导致质变”这一说法提供了有力的佐证。

LLM 的成功进一步推动了该领域的研究(或者是反过来?)。一方面,大型工业实验室(如 Google、Meta、Microsoft 或 OpenAI)投入大量资金,以推动更大规模 LLM 的边界。另一方面,灵活的开源社区也以有限的资源找到创造性的方式,取得了大量进展。

在本章中,我们将从理论和实践两个角度探索当前的 LLM 领域。我们将调查许多最新的 LLM,它们的特性以及训练过程。此外,我们还将看到如何借助 Hugging Face Transformers 库将它们应用于我们的目标。

在本章中,我们将涵盖以下主要内容:

  • 介绍 LLM

  • LLM 架构

  • 训练 LLM

  • LLM 的突现能力

  • 介绍 Hugging Face Transformers

技术要求

我们将使用 Python、PyTorch 和 Hugging Face Transformers 库(github.com/huggingface/transformers)实现本章中的示例。如果你没有安装这些工具的环境,也不必担心——示例可以在 Google Colab 的 Jupyter notebook 中找到。代码示例也在本书的 GitHub 仓库中: github.com/PacktPublishing/Python-Deep-Learning-Third-Edition/tree/main/Chapter08

介绍 LLM

在本节中,我们将采用更系统化的方法,深入探讨基于 Transformer 的架构。正如我们在介绍中提到的,自 2017 年引入以来,Transformer 模块的变化几乎可以说微乎其微。相反,主要的进展体现在模型规模和训练数据集的增大。例如,原始的 GPT 模型(GPT-1)有 1.17 亿个参数,而 GPT-3(语言模型是少样本学习者arxiv.org/abs/2005.14165)则有 1750 亿个参数,增加了 1000 倍。我们可以根据模型的规模区分两类非正式的 Transformer 模型:

  • 预训练语言模型PLMs):具有较少参数的变换器,例如双向编码器表示来自变换器BERT)和生成式预训练变换器GPT)属于这一类别。从 BERT 开始,这些变换器引入了两步预训练/微调(FT)范式。注意力机制和无监督预训练(掩蔽语言建模MLM)或下一个词预测NWP)的结合创造了有效的通用语义特征,我们可以利用这些特征进行多个下游任务。因此,PLMs 比其他自然语言处理NLP)算法(如递归神经网络RNNs))表现得更好。加上它们高度并行化的架构,这激发了很多基于变换器的后续研究,产生了改进的模型,并最终带来了下一个类别。

  • LLMs:这些是具有数十亿参数的变换器模型。LLMs 与 PLMs 在以下几个方面有质的区别:

    • 突现能力:它们能够解决一系列复杂的任务,我们将在LLMs 的突现能力部分进行讨论

    • 提示接口:LLMs 可以通过自然语言与人类互动,而无需特定的 API

    • 研究与工程的融合:LLM 的规模要求研究人员具备强大的大规模数据处理和并行训练的工程能力

目前,LLMs 几乎完全是解码器-only 模型,因为当前 LLMs 的主要应用集中在文本生成(例如,像 ChatGPT 这样的聊天机器人)。这种情况是以牺牲编码器-only 和编码器-解码器架构为代价的。为了更好地理解为什么会这样,我们来看一下聊天机器人的工作原理。它以用户生成的消息(称为提示)开始。提示是解码器模型的初始输入序列,该模型每次生成一个标记作为响应。响应会被添加回输入序列。一个特殊的标记将提示和响应分开。一旦 LLM 生成了响应,用户可以再发出一个新的提示。在这种情况下,我们将新提示与现有序列连接,并要求 LLM 基于扩展的序列创建新的响应。LLM 没有机制去记住现有的聊天会话,除了将其作为输入序列的一部分。这一过程可以无限继续。然而,一旦达到上下文窗口的最大长度,它将开始截断序列的最初部分(我们可以把它看作一个滑动窗口)。

注意

本章部分内容来源于论文《大规模语言模型概览》(arxiv.org/abs/2303.18223)。我们将简单地称之为概览

LLM 架构

第七章中,我们介绍了多头注意力MHA)机制以及三种主要的 Transformer 变体——编码器-解码器、仅编码器和仅解码器(我们使用 BERT 和 GPT 作为典型的编码器和解码器模型)。在本节中,我们将讨论 LLM 架构的各个方面。让我们首先集中注意力(是的——这又是那个老笑话)在注意力机制上。

LLM 注意力变体

到目前为止,我们讨论的注意力机制被称为全局注意力。下图展示了双向全局自注意力机制的连接矩阵(上下文窗口大小为 n=8):

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_08_1.jpg

图 8.1 – 带有上下文窗口大小为 n=8 的全局自注意力

每一行和每一列代表完整的输入令牌序列,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/737.png。虚线彩色对角单元格表示当前输入令牌(查询),https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/738.png。每列中未中断的彩色单元格表示所有令牌(键),这些令牌是https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/338.png可以关注的对象。例如,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/740.png关注所有前面的令牌,[t 1…t 4],

以及所有后续的标记,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/741.png。术语全局意味着https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/742.png会关注所有的标记。因此,所有的单元格都会被着色。正如我们将在稀疏注意力部分看到的,存在一些注意力的变体,并非所有标记都会参与其中。我们将用透明单元格来表示这些标记。该图展示了双向自注意力机制,因为查询可以同时关注前面的(下方)和后面的(上方)元素。在单向情况下,查询只会关注当前输入标记下方的元素。例如,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/740.png只会关注https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/744.png

正如我们将看到的,注意力机制的一个主要挑战是其时间和空间复杂度。

注意力复杂度

尽管注意力机制(特别是全局注意力)有其优点,但也存在一些缺点。其中之一是随着上下文窗口的增大,空间和时间复杂度会呈二次方增长。这是因为该机制是通过矩阵和矩阵乘法实现的。

矩阵乘法的时间复杂度

两个n×n矩阵相乘的时间复杂度是https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/745.png,因为经典实现使用了三重嵌套循环。在实践中,该算法经过优化,复杂度较低。本节目的是使用经典实现的复杂度。

例如,大小为n=4的上下文窗口会产生n×n=4x4 QV 矩阵,每个矩阵有 16 个单元格。但是,大小为n=8的上下文窗口会产生n×n=8x8 QV 矩阵,每个矩阵有 64 个单元格。因此,两倍大的上下文窗口需要四倍的内存。由于矩阵乘法的时间复杂度是https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/746.png,将上下文窗口从n=4增加到n=8会将操作数从https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/747.png增加到https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/748.png

接下来,让我们专注于变压器块,其中包括一个前馈网络FFN),

多头自注意力机制和四个线性投影(全连接FC)层)——三个用于Q/K/V预注意力分离,一个用于合并注意力头的输出。我们将讨论每个组件在模块计算负载中的相对权重。我们用https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/693.png表示嵌入大小,用https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/680.png表示关键维度,用https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/695.png表示值维度(https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/752.png),用上下文窗口大小n,头的数量h,以及 FFN 中隐藏层的大小ffn(通常约定为ffn=4d*)表示。不同组件的时间复杂度如下所示:

该块的完整综合复杂度为 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/757.png。我们可以看到它依赖于上下文窗口长度 n 与嵌入大小 d 之间的比例。如果 d>>n,则线性投影的计算时间将超过注意力头的时间,反之亦然。在实际应用中,d>>n 是最常见的情况。但无论如何,注意力机制至少具有二次空间和时间复杂度。我们来看看一些应对这一挑战的解决方案。

多查询和分组查询注意力

MHA 将输入数据通过每个头的三个线性投影分支到多个头。下图展示了该配置的两种优化:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_08_2.jpg

图 8.2 – 左侧:MHA;中间:多查询注意力(MQA);右侧:分组查询注意力(GQA)(灵感来源于 arxiv.org/abs/2305.13245

我们来讨论它们(除了我们在 第七章 中介绍的 MHA)。

  • MQA快速转换器解码:只需一个写头arxiv.org/abs/1911.02150):不同的头共享键和值投影,而不是 MHA 中的独立投影。由于输入序列相同,所有头共享相同的键值存储,仅在查询上有所不同。这个优化减少了内存和计算需求,且几乎没有性能损失。

  • GQAGQA:从多头检查点训练通用多查询转换器模型arxiv.org/abs/2305.13245):MHA 和 MQA 的混合体,为一组查询头共享单一的键和值头。作者显示,GQA 的速度几乎与 MQA 一致,且质量接近 MHA。

在下一节中,我们将讨论注意力优化,它考虑了 GPU 内存管理的具体细节。

FlashAttention

在本节中,我们将介绍 FlashAttention(FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awarenessarxiv.org/abs/2205.14135; FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioningarxiv.org/abs/2307.08691)。这不是一种新的注意力机制,而是全球注意力的一种实现,考虑了 GPU 硬件的具体特点。GPU 拥有大量的计算核心,可以执行相对简单但高度可并行化的操作(如矩阵乘法)。它有两级内存:小但快速的缓存(L1 和 L2)和大但相对较慢的 高带宽内存 (HBM)。为了执行一个操作,它会将必要的数据从 HBM 转移到缓存。计算核心使用缓存进行计算。操作完成后,结果会存储回 HBM。在这个管道中,主要的瓶颈是数据传输,而不是实际的计算(数据传输越少越好)。

接下来,让我们关注注意力模块,它包含五个操作:1)矩阵乘法 (https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/758.png),2)掩码,3)softmax,4)dropout,和 5)矩阵乘法(V)。标准实现顺序地执行这些操作,从第一个矩阵乘法开始。完成后,它会继续进行掩码操作,依此类推。每个操作涉及 HBM 和缓存之间的双向数据传输。这些传输是多余的,因为操作 i 的结果从缓存传输到 HBM 后,又需要从 HBM 返回到缓存进行操作 i+1。FlashAttention 提出了一个特殊的 融合内核 来解决这种低效问题。它将 Q/K/V 矩阵拆分成可以适配缓存的小块。一旦这些块被传输到缓存中,融合内核将执行所有五个操作,无需中间的数据传输。只有最终结果被发送回 HBM。将矩阵拆分成小块是可行的,因为矩阵乘法是显式并行的。但 FlashAttention 的另一个创新是能够拆分 softmax 操作,这并不像矩阵乘法那样简单(我们不会详细介绍它是如何实现的)。当所有矩阵块通过这个管道时,操作就完成了。

矩阵乘法拆分

假设我们要对矩阵AB进行乘法运算。由于矩阵乘法的运作方式,我们可以按列将B拆分为两个矩阵,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/759.pnghttps://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/760.png。然后,我们在每个设备上执行两个矩阵乘法运算:https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/761.pnghttps://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/762.png。最后,我们将两个操作的输出合并到一个矩阵中,相当于原始乘法产生的矩阵,AB

在下一节中,我们将讨论通过新的注意力机制解决性能问题。

稀疏注意力

稀疏注意力是一类方法,其中输出向量仅关注所有关键向量的一个子集,而不是整个上下文窗口。例如,如果我们可以从八个上下文向量中选择四个感兴趣的向量进行关注,那么我们可以将所需的计算量减少一半。

下图展示了三种双向稀疏注意力机制:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_08_3.jpg

图 8.3 – 左:局部注意力;中:膨胀局部注意力;右:随机注意力;上下文窗口大小 n=12

这些机制与图 8.2中的符号相同,唯一的区别是——透明单元格代表令牌(关键字),查询不会关注这些单元格。

在左侧,我们有双向局部注意力(或滑动窗口注意力),这是在图像 Transformer中首次提出的,arxiv.org/abs/1802.05751。查询仅关注当前标记周围最近的w个键的有限上下文窗口(左边½w,右边½w)。自注意力模块仍然将整个n大小的序列作为输入,但每个标记只关注有限的w大小的局部上下文。这样,内存占用与全局注意力相同,但时间复杂度减少为https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/763.png,而不是https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/764.png

为了理解局部注意力为何有效,我们回顾一下卷积神经网络CNN)。回想一下,CNN 的早期层具有较小的感受野,并捕获较小、更简单的特征。相反,CNN 的更深层具有较大的感受野,能够捕获更大和更复杂的特征。我们可以将相同的原则应用于 Transformer。研究表明,初始的 Transformer 模块学习简单的标记特征和局部语法,而更深层则学习标记语义中更复杂的上下文相关特征。因此,我们可以将局部注意力应用于较浅的 Transformer 模块,而将全局注意力保留给更深的模块,而不会牺牲性能。

扩展注意力图 8.3,中间)是局部注意力的一种修改,工作原理类似于我们在第四章中介绍的扩展卷积。与局部注意力不同,这里上下文窗口不是连续的。相反,每个上下文标记之间有一个间隔(可以是多个单元格)。这使得在相同的计算数量下,可以关注更广泛的上下文。

接下来,我们有双向的 随机注意力 (图 8.3,右),其中当前查询(标记)会关注来自完整上下文窗口的 r 个键(标记)的子集。时间复杂度减少为 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/765.png 而不是 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/766.png。注意力模式可以视为一个有向图。在随机注意力的情况下,这个图也是随机的。也就是说,信息可以在任何一对节点之间迅速流动,而不考虑数据的实际结构,这可能会带有偏见。

也可以结合全局和局部注意力。其中一个例子是 Longformer (Longformer:长文档变换器arxiv.org/abs/2004.05150),如下图所示:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_08_4.jpg

图 8.4 – 结合了局部和全局注意力;左:Longformer 块;右:大鸟块

它在一个未修改的变换器模型中引入了一个可以替代的自注意力块。该块表示全局和局部(或扩展)注意力的结合。它对大多数输入标记应用局部注意力,但少数标记可以使用全局注意力。图 8.4 的左侧显示了结合的自注意力块和一个应用局部和全局注意力的输入标记示例。更具体地说,作者在一个单向 BERT 风格的模型中使用 Longformer 块来解决 MLM 和 [CLS] 在 MLM 任务中的问题。如图所示,全局注意力在两个方向上都有效。特殊标记可以关注所有其他标记,但其他标记除了其局部注意力上下文之外,还可以关注特殊标记。在自回归语言建模(单向模型)的情况下,他们仅应用扩展的局部注意力,因为没有具有特殊意义的标记。完整的 Longformer 模型在较深的层使用扩展注意力和更大的上下文窗口,而较早的层仅使用局部注意力。

大鸟 (图 8.4,右;大鸟:用于长序列的变换器arxiv.org/abs/2007.14062) 类似于 Longformer,但添加了随机注意力。

接下来,我们讨论由 OpenAI 开发的 稀疏变换器 注意力机制(生成长序列的稀疏变换器arxiv.org/abs/1904.10509)。稀疏变换器引入了单向步长和固定注意力机制,如下图所示:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_08_5.jpg

图 8.5 – 左:步长稀疏注意力,l=4;右:固定稀疏注意力;输入图像大小 4×4;序列长度 n=12(灵感来自 arxiv.org/abs/1904.10509

为了理解它们是如何工作的,我们来讨论一下论文的背景。论文提出了一种统一的仅解码器模型,用于生成新的图像、文本或音频。根据使用场景,输入和输出数据可以是二维图像张量(为简化起见,我们省略颜色维度)。然而,变换器接受的是一维序列作为输入。我们可以通过将图像的行连接成一个一维张量来解决这个问题。完成后,我们可以将图像视为常规序列,并将其输入到模型中。图 8.5 显示了一个二维图像(顶部)及其等效的一维连接序列(底部)的步长(左)和固定注意力(右)连接矩阵。需要注意的是,底部扩展的序列与顶部图像的尺寸不匹配——它的长度应为 n=16,对应 4×4 图像,而不是现在的 n=12。由于这是一个生成式解码器模型,它使用单向注意力,尽管图像中的方向性并不像文本中那样明确。

接下来,我们讨论这两种注意力机制。我们从步长注意力开始,其中当前词元关注输入图像的前一行和列。这是两个分别在不同注意力头之间分开的机制:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/769.png

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/772.png

该方案在二维输入数据上表现最佳,例如图像,因为行/列划分反映了底层数据结构。该方案的时间复杂度为https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/773.png

接下来,我们介绍固定注意力,它关注一个固定的列及其最新列元素之后的元素。对于非周期性数据(如文本),它表现更好。再次强调,这是两种独立机制在不同头之间的组合:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/775.png

这里,c 是一个参数(8、16 或 32)。例如,如果 l=64c=16,那么所有大于 64 的位置可以关注 48-64 的位置,所有大于 128 的位置可以关注 112-128 的位置,以此类推。

  • 行头:第一个头与跨步注意力中的行头类似。但不同的是,它不是关注整个行的长度,而是只关注当前列头的位置。行头提供了局部上下文。我们可以将其总结如下:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/776.png

这里,floor 将除法结果向下取整到最接近的整数。

接下来,让我们将注意力集中在一种解码器架构的特殊案例及大语言模型架构的各个方面上(我简直停不下来)。

前缀解码器

在这一节中,我们将介绍前缀(或非因果解码器统一语言模型预训练用于自然语言理解与生成arxiv.org/abs/1905.03197)。这是一种仅包含解码器的模型,提出了一种新的注意力模式,如下图所示:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_08_6.png

图 8.6 – 前缀解码器自注意力模式(灵感来自 https://arxiv.org/abs/1905.03197)

我们将输入序列分成两个部分——https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/777.pnghttps://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/778.png前缀),以及 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/779.pnghttps://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/780.png目标)。源段落的词汇之间可以互相访问。而目标段落的词汇只能单向访问整个(源和目标)输入序列中前面的词汇。例如,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/781.png 是源段落的一部分,可以访问

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/782.pnghttps://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/783.png,和https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/784.png。相反,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/785.png是目标的一部分,只能处理从https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/782.pnghttps://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/787.png(但不能处理https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/788.png)。

前缀解码器是编码器-解码器和解码器模型的混合体。源段充当编码器,目标段充当解码器,但其底层架构基于解码器。

我们可以使用前缀解码器来表示 [SOS] 和序列结束([EOS])标记。例如,我们来看一下文本摘要任务。我们将要总结的文本序列(S1)及其摘要(S2)表示为一个单一序列:[[SOS],S1,[EOS],S2,[EOS]]。源序列 [[SOS],S1,[EOS]] 属于双向注意力模式,而目标序列 [S2,[EOS]] 则属于单向注意力模式。我们通过 MLM(Masked Language Model)预训练模型,其中我们从完整序列中随机遮蔽一些标记。我们通过随机遮蔽目标序列中的一些标记并学习恢复被遮蔽的词语来微调模型。需要注意的是,[EOS] 标记也可以参与遮蔽。通过这种方式,模型学习何时生成 [EOS] 标记,并终止目标序列的生成。

接下来,让我们更详细地了解 LLM 架构的各个方面。

Transformer 的基本构件

以下表格提供了主要 Transformer 网络配置及其变种的详细总结:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_08_7.jpg

图 8.7 – 不同的 Transformer 配置(来源:https://arxiv.org/abs/2303.18223)

我们已经熟悉了其中的许多内容——我们在第七章介绍了三种不同的归一化位置。我们还在第三章介绍了三种归一化方法中的两种。默认情况下,大多数 Transformer 使用 层归一化 (LN)。然而,一些模型使用 RMSNorm,因为它在训练速度和性能上优于 LN。最后但同样重要的是,DeepNorm (DeepNet: 扩展 Transformer 至 1000 层, arxiv.org/abs/2203.00555) 对我们来说是新的。正如论文标题所示,这种归一化帮助构建了一个 1000 层的 Transformer。作者认为,在层归一化 (pre-ln) 架构中,底层的梯度往往大于顶部层的梯度,这导致与 后层归一化 (post-ln) 模型相比性能下降。另一方面,后层归一化模型由于梯度爆炸而不稳定。为了克服这一问题,他们提出了一种简单而有效的残差连接归一化方法:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/789.png

这里,α 是应用在残差连接输出处的常数。其值取决于变压器类型(编码器或解码器)和模型深度(块数)。DeepNorm 的理论基础在于通过这个常数限制模型更新。

接下来,让我们讨论激活函数。更具体地说,我们将讨论前馈网络FFN)子层的第一层激活函数(ActivationFunc),因为这是变压器块中唯一显式的激活函数。作为提醒,我们可以定义原始的 FFN 如下:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/790.png

我们在 第三章 中讨论了大多数激活函数,除了 SwiGLUGeGLUGLU 变体改进变压器arxiv.org/abs/2002.05202)。它们是 门控线性单元GLU)的变体,GLU 更像是层和激活函数的融合,而不是纯粹的激活函数。我们可以定义 GLU 如下:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/791.png

这里,ActivationFunc 是一个特定的激活函数(SwiGLU 对应 SwishGeGLU 对应 GeLU),⊗ 表示两个向量的逐元素乘积,WV 是权重矩阵,表示线性投影(即,全连接层)。GLU 引入了一个额外的线性投影 V,与原始网络路径 W 并行。由于逐元素乘积,带有激活的路径 W 作为来自 V 路径信号的门控。这类似于 长短时记忆LSTM)门。我们现在可以定义带 GLU 激活的前馈网络(FFN):

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/792.png

让我们注意到,作者已经从修改后的 FFN 中排除了偏置。这也是提及不同 LLMs 具有不同偏置配置的一个好地方,下面列出:

  • 在线性投影和注意力块本身都使用偏置。

  • 在线性投影中使用偏置,但在注意力块中不使用。

  • 不要在线性投影或注意力块中使用偏置。

根据一些实验,缺乏偏置可以稳定训练。

接下来,让我们专注于迄今为止未提及的各种类型的位置嵌入。不幸的是(或者幸运的是),详细讨论它们超出了本书的范围。但要记住的重要一点是,我们有绝对(静态)或相对(动态)位置编码。在第一种情况下,我们修改输入令牌嵌入向量。在第二种情况下,我们修改与当前输入令牌位置相关的K/V注意力矩阵。

该调查总结了现有文献中有关详细变压器配置的建议。为了更强的泛化能力,建议使用预先的 RMSNorm 归一化,以及 SwiGLU 或 GeGLU 激活函数。此外,在嵌入层之后立即使用 LN 可能会导致性能下降。至于位置嵌入,Rotary Positional EmbeddingRoPE)或Attention with Linear BiasesAliBi)在处理长序列时比其他方法表现更好。

现在我们对 LLMs 的架构属性已经很熟悉,让我们讨论具体的模型实例。

模型

以下表格总结了一些流行的最近 LLMs:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_08_8.jpg

图 8.8 – 最近大型语言模型的模型卡,包含公开配置详情(修改自 https://arxiv.org/abs/2303.18223p)

这里,PE 表示位置嵌入,#L 表示变换器层数,#H 表示每层的注意力头数,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/707.png 表示隐层状态的大小,MCL 表示训练期间的最大上下文长度。

我们将从 GPT 系列模型开始(由 OpenAI 开发),如以下图所示:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_08_9.jpg

图 8.9 – GPT 系列模型的演变(灵感来源于 https://arxiv.org/abs/2303.18223)

我们已经熟悉 GPT-1,因此让我们继续了解 gpt-3.5-turbo,其上下文长度为 4,096 个标记,gpt-3.5-turbo-16k 的上下文长度为 16,384 个标记。目前的 Copilot 版本基于 GPT-3.5。最新的模型 GPT-4 接受多模态输入(图像和文本),但仅输出文本。它也是封闭的,但可能具有超过 1T 的参数。根据 OpenAI 首席执行官 Sam Altman 的说法,训练 GPT-4 的成本已超过 1 亿美元(https://www.wired.com/story/openai-ceo-sam-altman-the-age-of-giant-ai-models-is-already-over/)。GPT-4 也通过 OpenAI 的 API 提供,有两个子变体——gpt-4,上下文长度为 8,192 个标记,以及 gpt-4-32k,上下文长度为 32,768 个标记。

接下来,让我们讨论 Meta 发布的LlaMa系列预训练(且未微调)模型。第一个版本(LLaMA: Open and Efficient Foundation Language Modelsarxiv.org/abs/2302.13971)有四个变体,参数量从 6B 到 65B 不等。由于 Meta 还发布了其权重(尽管不允许商业使用),这是开源社区中最受欢迎的 LLM 之一。这样,Meta 完成了预训练模型的重担,开源社区则将其作为基础模型使用,因为它可以通过相对较少的计算资源进行微调。最近,Meta 发布了Llama 2——Llama 的更新版(Llama 2: Open Foundation and Fine-Tuned Chat Modelsai.meta.com/research/publications/llama-2-open-foundation-and-fine-tuned-chat-models)。它有三个变体,分别为 7B、13B 和 70B 参数。Llama 2 使用比 Llama 1 多 40%的预训练数据,并且每个变体还有一个使用 RLHF 微调的版本。该模型的许可证允许商业使用(有些限制)。

这就结束了我们对 LLM 架构的调查。接下来,让我们讨论它们的训练。

训练 LLM

由于大多数 LLM 是仅解码器模型,最常见的 LLM 预训练任务是 NWP。模型参数的庞大数量(可达数百亿个)需要相对较大的训练数据集来防止过拟合,并实现模型的全部能力。这一要求带来了两个重大挑战:确保训练数据的质量和处理大量数据的能力。在接下来的部分中,我们将讨论 LLM 训练流水线的各个方面,从训练数据集开始。

训练数据集

我们可以将训练数据分为两大类:

  • 通用:例如网页、书籍或对话文本。LLM 几乎总是基于通用数据进行训练,因为这些数据广泛可用且多样化,能够提升 LLM 的语言建模和泛化能力。

  • 专业:代码、科学文章、教科书或多语言数据,旨在为 LLM 提供特定任务的能力。

以下表格列出了最受欢迎的语言模型数据集:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_08_10.jpg

图 8.10 – 语言建模数据集(修改自 https://arxiv.org/abs/2303.18223)

让我们来讨论一下:

  • 书籍:我们将专注于两个数据集:

    • BookCorpusAligning Books and Movies: Towards Story-like Visual Explanations by Watching Movies and Reading Booksarxiv.org/abs/1506.06724):包含了 11,000 本虚构书籍,约有 10 亿个词(2015 年发布)。

    • 古腾堡计划 (www.gutenberg.org/):包括 70,000 本小说类书籍。

  • Common Crawl (commoncrawl.org/):PB 级别的网络抓取数据库。数据按照获取日期进行分割,从 2008 年开始。最新的档案包含 31 亿网页(390 TiB 的未压缩内容),这些数据来源于 4,400 万个主机或 3,500 万个注册域名。虽然包含大量低质量数据,但也有多个子集包含更高质量的数据:

    • 庞大且清理过的 Common Crawl 版本C4):由 Google 开发的 800 GiB 数据集。原始数据集不可下载,但 Google 已发布工具,以便从 Common Crawl 数据库中重建该数据集。2019 年,艾伦人工智能研究所AI2,https://allenai.org/)发布了该数据集的重建版本,可通过 huggingface.co/datasets/allenai/c4 获取。其最受欢迎的子版本是 en 版本,它移除了所有包含“坏词”列表中单词的文档(“坏词”列表可通过 github.com/LDNOOBW/List-of-Dirty-Naughty-Obscene-and-Otherwise-Bad-Words 查看)。

    • CC-News:来自全球各大新闻网站的文章。

    • RealNews:从 Google News 索引的 5,000 个新闻域名中提取的新闻文章。

    • CC-Stories-R:一个用于常识推理和语言建模的数据集。它由与常识推理任务中的问题具有最大重叠的 n-gram 的 Common Crawl 文档组成。新的训练语料库代表了排名前 1.0% 的高质量文档。

  • Reddit 链接:解决 Common Crawl 低信噪比的一种方法是依赖人工策划的内容。Reddit 就是一个典型平台,用户可以发布文本内容或链接,其他用户可以对这些提交进行点赞(点赞称为 karma)。我们将提到两个基于 Reddit 的数据集:

    • WebText(与 GPT-2 模型一起发布):包含 4,500 万个 Reddit 提交链接,其中 karma 为三次或以上。这些链接背后的文档构成了 LLM 训练数据。WebText 并未公开发布,但有一个开源版本,名为 OpenWebText (github.com/jcpeterson/openwebtext)。

    • Pushshift (arxiv.org/abs/2001.08435):包含所有在 Reddit 上提交的链接和评论。

Reddit API 定价争议

LLMs 的兴起使得 Reddit 的数据比以往更有价值。基于这一点,公司决定对其原本免费的 API 引入费用。这项措施主要针对那些计划利用这些数据训练 LLM 的 AI 公司。然而,这一提案导致许多该网站的志愿版主(Reddit 依赖他们)宣布通过暂时关闭他们所管理的原本开放的社区来进行罢工。截至写作时,双方的争议仍在持续。

  • The Pile (An 800GB Dataset of Diverse Text for Language Modeling, arxiv.org/abs/2101.00027): 由 22 个多样且高质量的数据集组成,来源包括 PubMed、arXiv、GitHub、Stack Exchange、Hacker News、YouTube 等。The Pile 还引入了原始 OpenWebText 和 BookCorpus 数据集的扩展版本 OpenWebText2 和 BookCorpus2。

  • ROOTS (The BigScience ROOTS Corpus: A 1.6TB Composite Multilingual Dataset, arxiv.org/abs/2303.03915): 一个规模庞大的精心策划的数据集,涵盖 46 种自然语言和 13 种编程语言。

  • Wikimedia (dumps.wikimedia.org/): 因其高质量的内容,这是一个优秀的训练数据来源。

  • Stack Exchange (archive.org/details/stackexchange): 一个拥有评分系统的 QA 主题网站网络。最具代表性的站点是Stack Overflow。它每三个月发布一次匿名化数据转储,包含所有用户贡献的内容。

  • arXiv (https://www.kaggle.com/datasets/Cornell-University/arxiv): 主要的科学数据来源,包含超过 22 亿篇科学文章。

  • GitHub: GH Archive 项目 (www.gharchive.org/) 记录、归档并提供公共 GitHub 时间线的访问。

实际上,LLM 的预训练步骤使用的是多个数据集的混合。以下截图展示了几个代表性 LLM 的预训练数据来源分布:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_08_11.jpg

图 8.11 – 现有 LLM 预训练数据中各种数据源的比例(来源:https://arxiv.org/abs/2303.18223)

数据集混合并非一项简单的过程,需要多个处理步骤。我们来讨论一下这些步骤:

  • 移除低质量或无关的数据:例如,网页中包含大量 HTML 标签、JavaScript 或层叠样式表CSS)。然而,我们只对

    人类可读文本(除非我们明确希望训练模型理解 HTML)。在这种情况下,我们必须去除 HTML 和 JavaScript,只保留文本。

  • 移除个人可识别信息(PII):数据通常从网页中提取,而网页中可能包含个人信息。此步骤旨在从训练集中删除此类数据。

  • 分词:我们在第六章中深入讨论了分词,本文不再赘述。

最后,让我们介绍一个实际的变压器缩放法则(神经语言模型的缩放法则, https://arxiv.org/abs/2001.08361)。由于其规模,训练 LLM 可能非常昂贵。因此,避免过度训练或训练不足至关重要。根据经验实验,缩放法则提出了训练计算量(以浮动点操作每秒,或FLOPS表示)、C,模型大小(参数数量)、N,以及训练数据集大小(令牌数量)之间的最佳比例:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/794.png

既然我们已经知道了构建训练集的步骤,接下来让我们专注于实际的预训练。

预训练的特性

与其他神经网络NNs)类似,LLM 的预训练通过梯度下降和反向传播进行。但由于其规模庞大,训练具有一些特定的特性,我们将在本节中讨论这些特性。

Adam 优化器

大多数 LLM 使用 Adam(Adam:一种随机优化方法arxiv.org/abs/1412.6980)或其某些变体。尽管我们在许多示例中使用了它,但至今我们尚未详细讨论它。现在是时候弥补这个遗漏了。

权重更新公式的回顾

第二章,我们学习到使用反向传播来计算损失函数*J(θ)*关于每个参数https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/801.png

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/802.png

在这里,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/803.pnghttps://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/804.png 是具有默认值 0.9 和 0.95 的超参数。 这两个公式与动量公式非常相似。https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/805.png (https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/806.png)

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/807.png (https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/808.png) 作为一种移动平均的模拟。但不同于跨多个前值进行平均,我们只取最新的前一个值,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/809.png (https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/810.png),并为其分配一个权重系数,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/803.png (https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/804.png)。

  1. https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/813.pnghttps://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/187.png 的初始值为 0,因此在训练的初期阶段,它们会对 0 产生偏差。为了理解为什么这可能是个问题,假设在 t=1 时,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/815.png

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/816.png。然后,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/817.png 远小于实际梯度 5。我们可以通过使用偏差校正版本的 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/813.pnghttps://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/187.png 来补偿这个偏差:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/820.png

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/821.png

  1. 使用以下公式进行权重更新:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/822.png

在这里,ε 是一个小值,用于防止除以 0。

AdamW解耦权重衰减正则化arxiv.org/abs/1711.05101)通过解耦的权重衰减改进了 Adam 算法:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/823.png

回顾一下,L2 正则化参与损失函数,并通过导数过程被转移(作为权重衰减)到权重更新公式中。在这种情况下,正则化会经过损失函数的所有变换,并且会受到这些变换的影响。正如名字所示,解耦权重衰减绕过了所有这些变换,直接参与到前面的公式中。

Adam 和 AdamW 的一个问题是增加的内存消耗——优化器为每个模型参数存储至少两个额外的值 (https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/813.pnghttps://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/187.png)。

并行处理

LLMs 的规模需要特别的步骤来进行高效训练。首先,我们将讨论如何在多个设备上训练 LLMs。更具体地,我们将讨论三种不同类型的并行性组合(也称为 3D 并行性):

  • 数据并行性:当模型足够小,可以适配到单个设备上时,它有效:

    1. 在所有设备上创建整个模型及其优化器状态(包括随机种子)的相同副本。

    2. 将每批训练集拆分成唯一的子集(分片),并分配到所有设备上。

    3. 每个设备根据其唯一的输入批次子集计算其梯度。

    4. 将所有设备的梯度汇总为一个单一的梯度更新。

    5. 将聚合的更新分发到各个设备,并在每个设备上执行权重更新。通过这种方式,我们在每个训练步骤开始和结束时,都会使用相同的模型。

  • 模型(或流水线):在操作(层)级别将模型分布到多个设备上。例如,如果我们的模型有 9 层,我们可以将第 1 到第 6 层发送到一台设备,将第 7 到第 9 层发送到另一台设备。通过这种方式,我们可以训练无法在单个设备内存中容纳的模型。不仅如此,我们即使在单个设备上也可以应用这种方法。在这种情况下,我们将加载第一组操作(1-6)并计算它们的输出。然后,我们会卸载这些操作并加载接下来的子集(7-9)。第一组的输出将作为第二组的输入。反向传播以相同的方式进行,但方向相反。模型并行的一个问题是,如果使用多个设备,第二个设备会在第一个设备产生输出之前处于空闲状态。

  • 张量(或水平):在张量级别将模型分布到不同的设备上,从而解决了模型并行中的空闲问题。为了理解这一点,我们回顾一下矩阵乘法是当代神经网络中最计算密集的操作。但正如我们在FlashAttention部分讨论的,它也是极其容易并行化的。因此,我们可以将它分配到不同的设备上。

零冗余优化器

零冗余优化器ZeRO)(ZeRO: 面向训练万亿参数模型的内存优化, https://arxiv.org/abs/1910.02054) 是数据并行和模型并行的混合体。下图展示了 ZeRO 的三个阶段:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_08_12.jpg

图 8.12 – ZeRO(灵感来源于 https://arxiv.org/abs/1910.02054)

第一行表示数据并行系统的情况。每个 GPU 接收输入小批量数据的一个独特分片。它还保存模型参数的副本(GPUi 块的第一个彩色矩形)、梯度(第二个矩形)和优化器状态(第三个矩形)。优化器状态占用的内存最大(例如,Adam 为每个模型参数存储多个值),因此它们在训练过程中占用大部分内存。接下来的三行表示 ZeRO 的三个阶段:

  1. 优化器状态分区 (https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/826.png):每个 GPU 保存整个模型参数及其梯度的副本,但优化器状态在 GPU 之间进行分区,每个 GPU 只保存其中一部分。

  2. 添加梯度分区 (https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/827.png):每个 GPU 持有整个模型参数的副本,但梯度和优化器状态是分区的。

  3. 添加模型参数 (https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/828.png):每个 GPU 保存所有组件的一部分。

为了理解算法的工作原理,我们假设使用https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/829.png,一个包含N层和N个 GPU 的模型。每一层都分布在一个 GPU 上——第一层在https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/830.png,第二层在https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/831.png

以此类推。让我们从前向传播阶段开始。首先,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/832.png 接收 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/833.png。由于它持有模型的第一层,它可以独立地将输入喂入并计算其激活值。同时,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/834.png 将第一层的参数广播到所有其他 GPU。现在,每个 GPU 除了持有自己的部分模型参数外,还持有第一层的参数。通过这种方式,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/835.png 可以处理自己的输入,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/836.png,通过第一层,就像https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/832.png 做的那样。一旦 GPU 计算完第一层的激活值,它会从内存中删除其参数(除了https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/838.png 保留的参数)。我们重复相同的步骤,这次处理第二层。https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/839.png 广播其参数,以便所有 GPU 可以继续前向传播阶段。之后,除了https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/840.png之外,所有其他 GPU 都会删除第二层的参数。这个过程会一直持续,直到所有 GPU 输出模型结果。然后,所有 GPU 的损失函数会被汇总。接下来,反向传播阶段开始,它的工作方式与前向传播相同,但这次 GPU 会同时广播梯度和优化器状态。

混合精度训练

混合精度训练 (arxiv.org/abs/1710.03740)的核心思想是,并非所有的值都必须以 32 位(双精度或全精度)浮动点精度(FP32Float32 数据格式)来存储。研究表明,将部分值存储为 16 位(单精度或半精度)浮动点精度(FP16Float16)不会降低模型的性能。权重、激活、梯度和优化器状态都以 FP16 格式存储。此外,模型保留一个 FP32 的主副本作为权重。前向和反向传播使用的是 FP16 权重,但当进行权重更新操作时,使用的是 FP32 主副本,这样结果是最优的。一个可能的解释是,权重更新公式使用了乘以学习率的梯度,而结果可能会变得太小,无法在 FP16 中表示。另一个解释是,权重值和权重更新的比例非常大,这可能导致权重更新变为零。

Bfloat16 和 TensorFloat32

Google Brain 部门为 机器学习ML)应用开发了 brain floating-point 格式(因此得名 bfloat)。标准的 FP16 格式有一个符号位、五个位的指数部分和十个位的尾数部分。与之不同,bfloat16 具有八个位的指数部分和七个位的尾数部分。其指数部分与 FP32 相同。就 ML 任务的性能而言,bfloat16 与 FP32 相差无几。我们还可以找到 TensorFloat-32TF32)格式——这是 NVIDIA 为 ML 目的开发的 19 位格式,具有 8 位的指数部分和 10 位的尾数部分。

预训练特殊性和总结

在这一节中,我们将讨论一些 LLM 预训练的特殊性。首先从小批量大小开始。理想情况下,我们会在整个训练数据集上计算梯度,然后进行一次权重更新。然而,庞大的数据集和模型使得这种计算方式在实际操作中不可行。另一个极端是对每个训练样本进行一次权重更新,但这样训练就容易受到离群样本的影响,这可能会将损失函数引导到次优的局部最小值。小批量训练是一种折衷方法,使得在有限的计算资源内可以进行训练,并避免离群样本的影响。但从理论上讲,小批量大小越大越好。LLM 训练是分布式在多个设备上进行的,这使得使用大批量大小成为可能(甚至是理想的)。根据模型的不同,批量大小可以在 32K 到 8.25M 个 token 之间变化。此外,批量大小还可以是动态的,并随着训练的进行逐渐增大。实验证明,这种技术可以稳定训练过程。

接下来,我们关注学习率,η。虽然 Adam 实现了自适应学习率,但大多数 LLM 从 预热阶段 开始,以稳定训练。更具体来说,在训练的前 0.1% 到 0.5% 步骤中,学习率从大约 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/841.png 增加到 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/842.png

然后,学习率将按余弦(或线性)衰减策略逐渐降低到最大值的 10% 左右。

LLM 训练还使用了 梯度裁剪——一种防止梯度爆炸问题的技术。一种实现方法是通过值裁剪:

如果 lgl ≥ max_threshold 或 lgl ≤ min_threshold,则 grelevant_threshold

这里,g 是一个包含所有梯度值的向量(lgl 是向量的范数或其绝对值)。首先,我们选择 min_thresholdmax_threshold 值。然后,如果梯度值超出这些边界,我们将在权重更新公式中将其裁剪至阈值。

另一种选择是通过范数裁剪:

如果 lgl ≥ threshold,则 gthreshold * g/lgl

这里,g/lgl 是一个单位向量。它的方向与原向量相同,但长度为 1。单位向量中每个元素的值都在 [0:1] 范围内。通过将其乘以 threshold,每个元素都落在 [0: threshold] 范围内。这样,范数裁剪将梯度限制在预定义的阈值范围内。

下表总结了一些流行 LLM 的训练特性:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_08_13.jpg

图 8.13 – LLM 训练特性(修改自 https://arxiv.org/abs/2303.18223)

这就是我们对 LLM 预训练的介绍。接下来,我们将关注 FT 阶段。

带有 RLHF 的 FT

到目前为止,我们已经关注了 LLM 的预训练阶段。LLM 的预训练目标是基于(主要是)网页训练数据集预测下一个标记。然而,预训练模型可能表现出不良行为。它们经常编造事实,生成偏见或有毒的文本,或者根本不遵循用户指令。然而,它们的目的是以有帮助诚实无害的方式与人类互动。在本节中,我们将讨论 RLHF 技术,这使得微调 LLM 以更好地与人类价值观对齐成为可能(也称为对齐调优)。更具体地,我们将关注 OpenAI 在《用人类反馈训练语言模型以遵循指令》(arxiv.org/abs/2203.02155)中描述的技术。他们在 GPT-3 模型上应用 RLHF,进而推出 GPT-3.5 系列模型。这是使得 ChatGPT 如此擅长与用户互动的秘密之一。

FT 从预训练结束的地方开始——即使用预训练的 LLM。以下图表展示了 RLHF 过程的三步:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_08_14.jpg

图 8.14 – 左侧:监督式 FT;中间:奖励模型训练;右侧:LLM RLHF(灵感来源于 https://arxiv.org/abs/2203.02155)

第一步是[prompt: response]样本,其中promptresponse分别是源和目标的标记序列。该数据集用于微调 LLM,使用与预训练相同的目标——即在给定提示的情况下预测响应的下一个标记。微调后的 LLM 作为下一步的基础。

关于预训练和三步 FT 的必要性

SFT 步骤隐含地回答了一个未被提问的问题——为什么我们需要预训练和三步 FT 来训练我们的模型?原因在于,生成一个人工标注的训练数据集是不可扩展的,并且是一个主要的瓶颈。例如,预训练数据集可以包含超过一万亿个标记;使用人工标注员生成如此规模的提示及其相应的响应是不现实的。因此,我们需要预训练为 LLM 提供一个坚实的基础,而我们可以使用一个较小的数据集来进行微调。

第二步是[(A, B), (A, C), (B, C)]。该数据集训练 RM,基于微调后的 LLM。其输出的下一个标记分类器被替换为一个随机初始化的回归层,该层输出给定响应的预测标量分数。RM 计算每对响应的分数,它们之间的差异参与损失函数的计算。这是迁移学习TL)的一个例子,旨在在原始 LLM 的基础上训练新的回归层。

第三步是使用 RL 通过 RM 和近端策略优化PPO)来训练 LLM(图 8.14—右侧)。

RL 的回顾

为了理解第三步,我们来回顾一下在第一章中的强化学习介绍。我们有一个环境系统和一个智能体。智能体可以采取多种行动之一,这些行动会改变环境的状态。环境会对智能体的行动作出反应,并提供修改后的状态和奖励(或惩罚)信号,帮助智能体决定下一步的行动。智能体的决策算法称为策略。智能体的目标是最大化在整个训练过程中所获得的总奖励。

在这个场景中,智能体的策略由经过微调的 LLM 表示。令牌词汇表表示智能体可以采取的行动——也就是说,智能体的行动是选择序列中的下一个令牌。环境向 LLM 提供一个随机提示,智能体(LLM)生成一个响应。然后,环境的一部分 RM 对生成的响应进行评分。RM 分数是发送给智能体的奖励,并用于更新其参数。

在下一节中,我们将讨论 LLM 与其他模型的不同之处。

LLM 的涌现能力

在本节中,我们将讨论 LLM 的涌现能力现象,该现象首次在arxiv.org/abs/2206.07682中总结。该论文将涌现能力定义如下:

如果某个能力在较小的模型中不存在,但在较大的模型中存在,则该能力为涌现能力。

这些能力代表了大语言模型与小语言模型之间的质的差异,这种差异无法通过外推预测。

我们将从被称为少量示例提示(或上下文学习)的能力开始,这种能力由 GPT-3 普及。在这里,初始用户提示是 LLM 必须通过响应遵循的指令,且无需任何额外的训练。提示本身可能用自然语言描述一个或多个训练示例(因此,称为少量示例)。这是 LLM 在生成响应之前可以用来进行训练的唯一上下文。以下图表展示了一个少量示例提示的例子:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_08_15.jpg

图 8.15 – 一个少量示例提示的例子(灵感来自 arxiv.org/abs/2206.07682

接下来,让我们讨论大型语言模型(LLM)在思维链CoT)提示策略的帮助下解决复杂多步骤推理任务的能力(思维链提示引发大型语言模型的推理能力,https://arxiv.org/abs/2201.11903)。这种提示为 LLM 提供了一系列中间步骤,可以引导模型达到最终任务答案。以下图表展示了常规提示和思维链提示的比较:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_08_16.jpg

图 8.16 – 左:常规的一次性提示;右:链式思维一次性提示(灵感来自 arxiv.org/abs/2201.11903

有人推测,这种能力是通过将源代码包括在训练数据中获得的。

还需要注意的是,我们在 FT with RLHF 部分讨论的对齐微调也是一种紧急能力,因为它仅仅提升了大模型的性能。

下图展示了随着模型规模的增加,模型在各种任务上的性能如何显著提升:

https://arxiv.org/abs/2206.07682)](https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_08_17.jpg)

图 8.17 – 紧急能力仅出现在大规模模型中 (来源:arxiv.org/abs/2206.07682)

x 轴显示每个模型的训练计算时间(以 FLOPS 计),y 轴显示模型的准确性。图表显示了模型在三个不同基准测试上的准确性:

  • 一个算术基准,测试 2 位数乘法,以及 3 位数的加法和减法

  • 涵盖多个主题的 57 项大学级测试,包括数学、历史、法律等

  • 在数学应用题上,链式思维与常规提示的比较,例如 图 8.16 所描述的那种

这就是我们对大语言模型(LLM)的理论介绍。接下来,让我们看看如何在实践中使用它们。

介绍 Hugging Face Transformers

到目前为止,我们已经深入讨论了 LLM 的架构和训练特性。但可悲的事实是,这些模型如此庞大,以至于你或我都不太可能从零开始构建一个。相反,我们很可能会使用一个预训练模型。在本节中,我们将看到如何使用 Hugging Face Transformers 库(github.com/huggingface/transformers)。顾名思义,它的重点是 transformer 架构。它支持三种不同的后端——PyTorch、TensorFlow 和 JAX(像往常一样,我们将重点讨论 PyTorch)。它是开源的,可以用于商业用途。背后的公司 Hugging Face 还开发了 Hugging Face Hub——这是一个与库配套的云平台服务。它支持托管和/或运行 Git 仓库(如 GitHub)、transformer 模型、数据集和 Web 应用程序(用于 概念验证 (POC) 的机器学习应用演示)。好了,让我们继续我们的第一个示例。

我们将从一个基本的使用案例开始——加载一个预训练的 Llama 2 chat 7B 模型,并使用它来生成对用户提示的回答:

  1. 首先,我们加入 import 语句:

    import torch
    from transformers import AutoTokenizer, pipeline
    
  2. 然后,我们在一个变量中定义模型的名称:

    model = "meta-llama/Llama-2-7b-chat-hf"
    

    每个 transformer 模型都有一个唯一的标识符,这对于 Hugging Face 模型中心(Hub)有效。该 Hub 托管所有模型,库可以在后台自动下载模型权重。在这种情况下,我们使用的是最小的 Llama 2 7B RLHF 优化模型,以节省计算资源。

  3. 接下来,让我们加载模型分词器:

    tokenizer = AutoTokenizer.from_pretrained(model)
    

    各种 LLM 模型使用不同的分词器。AutoTokenizer 实例可以根据模型标识符选择正确的分词器。

  4. 让我们通过 print(tokenizer) 打印分词器来查看其属性:

    LlamaTokenizerFast(name_or_path='meta-llama/Llama-2-7b-chat-hf', vocab_size=32000, model_max_length=1000000000000000019884624838656, is_fast=True, padding_side='left', truncation_side='right', special_tokens={'bos_token': AddedToken("<s>", rstrip=False, lstrip=False, single_word=False, normalized=False), 'eos_token': AddedToken("</s>", rstrip=False, lstrip=False, single_word=False, normalized=False), 'unk_token': AddedToken("<unk>", rstrip=False, lstrip=False, single_word=False, normalized=False)}, clean_up_tokenization_spaces=False)
    

    分词器基于字节级字节对编码BPE)。该输出提供了有关标记词汇大小、特殊标记和其他属性的有用信息。

  5. 接下来,我们创建一个 pipeline 实例:

    text_gen_pipeline = pipeline(
        task='text-generation',
        model=model,
        tokenizer=tokenizer,
        torch_dtype=torch.bfloat16,
        device_map='auto',
    )
    

    流水线抽象使得使用模型进行推理变得简单。task 参数决定了解决任务的类型。该库支持多种任务,还涵盖了图像和音频。pipeline 会根据任务返回不同的对象。它还负责下载和初始化模型。此外,我们将数据类型设置为 torch.bfloat16 以减少内存占用。device_map='auto' 参数允许 Accelerate 库(github.com/huggingface/accelerate)自动在任何分布式配置中运行模型。

  6. 我们可以使用以下命令查看模型定义:print(text_gen_pipeline.model)。例如,最大 70B Llama 2 模型 Llama-2-70b-hf 的命令输出如下:

    LlamaForCausalLM(
      (model): LlamaModel(
        (embed_tokens): Embedding(32000, 8192)
        (layers): ModuleList(
          (0-79): 80 x LlamaDecoderLayer(
            (self_attn): LlamaAttention(
              (q_proj): Linear(in=8192, out=8192)
              (k_proj): Linear(in=8192, out=1024)
              (v_proj): Linear(in=8192, out=1024)
              (o_proj): Linear(in=8192, out=8192)
              (rotary_emb): LlamaRotaryEmbedding()
            )
            (mlp): LlamaMLP(
              (gate_proj): Linear(in=8192, out=28672)
              (up_proj): Linear(in=8192, out=28672)
              (down_proj): Linear(in=28672, out=8192)
              (act_fn): SiLUActivation()
            )
            (input_layernorm): LlamaRMSNorm()
            (post_attention_layernorm): LlamaRMSNorm()
          )
        )
        (norm): LlamaRMSNorm()
      )
      (lm_head): Linear(in=8192, out=32000)
    )
    

    为了适应页面行长,我修改了原始输出:in 代表 in_featuresout 代表 out_features,所有线性层都有一个额外的 bias=False 参数。标记词汇大小为 32,000,嵌入大小 (https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/844.png) 为 8,192。模型有 80 个相同的解码器块(LlamaDecoderLayer)。每个块包含一个

    自注意力子层(*_proj 为投影)、一个具有单隐藏层的 FFN(LlamaMLP)、旋转嵌入(LlamaRotaryEmbedding)、LlamaRMSNorm)以及 SiLU 激活(SiLUActivation)。需要注意的是,该激活函数与论文中定义的 SwiGLU 激活函数有所不同。

  7. 然后,我们运行推理:

    sequences = text_gen_pipeline(
        text_inputs='What is the answer to the ultimate question of life, the universe, and everything?',
        max_new_tokens=200,
        num_beams=2,
        top_k=10,
        top_p=0.9,
        do_sample=True,
        num_return_sequences=2,
    )
    

    这里,text_inputs 是用户的提示,作为初始输入序列。num_return_sequences=2 参数表示模型将生成两个独立的响应(稍后会详细介绍)。这是第一个响应:

    Answer: The answer to the ultimate question of life, the universe, and everything is 42.
    Explanation:
    max_new_tokens=200 parameter.
    

让我们分析一下text_gen_pipeline调用中的其余参数,因为它们都与生成新标记的策略相关。LLM 的输出以 softmax 操作结束,该操作对词汇表中的所有标记输出一个概率分布。选择下一个标记的最简单方法是贪心策略,它总是选择概率最高的那个。然而,这通常不是最优的,因为它可能会把高概率的单词隐藏在低概率的单词后面。为了说明这一点,某个标记在当前生成序列的状态下可能会被分配一个低概率,然后会选择另一个标记替代它。这意味着包含当前低概率标记的潜在序列将不会存在。因此,即使它在后续有高概率的标记,我们也永远无法知道,因为低概率标记阻止了它的进一步探索。解决这个问题的一种方法是使用do_sample=True。在这种情况下,算法会考虑整个当前序列的概率,而不仅仅是最新标记的概率。因此,新标记将是最大化整个序列概率的那个,而不是局部概率的最大值。num_beams=2参数表示算法始终保留两个具有最高概率的序列(beams)。因为我们可以有多个输出序列,num_return_sequences=2参数表示返回的序列数量。例如,如果num_beams=5并且num_return_sequences=3,算法将返回五个可用序列中三个最高概率的序列(num_return_sequences > num_beams是无效的参数)。early_stopping=True参数表示当所有 beams 的假设都到达序列结束标记([EOS])时,生成过程结束。top_k=10参数表示算法只会从概率最高的前 10 个标记中进行采样,而不考虑它们的序列概率。top_p=0.9类似于top_k,但它不是只从最可能的k个标记中进行采样,而是从一组最小的标记中选择,这些标记的总概率超过p

这就结束了我们对 Transformers 库的介绍以及整个章节的内容。

总结

LLM(大型语言模型)是非常庞大的变压器模型,具有各种修改以适应其庞大的规模。在这一章中,我们讨论了这些修改,以及 LLM 和普通变压器之间的质的差异。首先,我们重点讲解了它们的架构,包括更高效的注意力机制,如稀疏注意力和前缀解码器。我们还讨论了 LLM 架构的细节。接下来,我们回顾了最新的 LLM 架构,特别关注了 GPT 和 LlaMa 系列模型。然后,我们讨论了 LLM 的训练过程,包括训练数据集、Adam 优化算法以及各种性能提升。我们还讨论了 RLHF 技术和 LLM 的突现能力。最后,我们介绍了 Hugging Face Transformers 库。

在下一章中,我们将讨论计算机视觉CV)的变压器模型、多模态变压器,并继续介绍 Transformers 库。

第九章:大型语言模型的高级应用

在前两章中,我们介绍了变换器架构,并学习了其最新的大规模版本,被称为大型语言模型LLMs)。我们在自然语言处理NLP)任务中讨论了它们。NLP 是变换器最初的应用领域,并且仍然是大型语言模型发展的前沿领域。然而,架构的成功使研究界开始探索变换器在其他领域的应用,如计算机视觉。

在本章中,我们将重点讨论以下内容。我们将讨论将变换器用作卷积网络(CNN,第四章)的替代品,用于图像分类和目标检测等任务。我们还将学习如何将它们用作图像生成模型,而不是像之前那样只用于文本。我们还将实现一个模型微调的示例——这是我们在第八章中未能完成的内容。最后,我们将实现一个基于大型语言模型驱动的新型应用。

在本章中,我们将覆盖以下主要主题:

  • 使用视觉变换器进行图像分类

  • 检测变换器

  • 使用稳定扩散生成图像

  • 微调变换器

  • 利用 LangChain 发挥大型语言模型的力量

技术要求

我们将在本章中使用 Python、PyTorch、Hugging Face 的 Transformers 库(github.com/huggingface/transformers)以及 LangChain 框架(www.langchain.com/github.com/langchain-ai/langchain)来实现示例。如果你没有配置这些工具的环境,也不用担心——该示例可以在 Google Colab 上的 Jupyter Notebook 中找到。代码示例可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Python-Deep-Learning-Third-Edition/tree/main/Chapter09

使用视觉变换器进行图像分类

视觉变换器ViT一张图胜过 16x16 个词:用于大规模图像识别的变换器arxiv.org/abs/2010.11929)通过引入一种巧妙的图像处理技术,证明了注意力机制的适应性。使用变换器处理图像输入的一种方法是通过四个变量对每个像素进行编码——像素强度、行、列和通道位置。每个像素编码是一个简单的神经网络NN)的输入,该网络输出一个https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/845.png维度的嵌入向量。我们可以将三维图像表示为这些嵌入向量的一维序列。它作为模型的输入,方式与令牌嵌入序列相同。每个像素将在注意力块中关注到所有其他像素。

这种方法在输入序列长度(上下文窗口)方面存在一些缺点。与一维文本序列不同,图像具有二维结构(颜色通道不会增加像素数量)。因此,随着图像尺寸的增大,输入序列的长度呈二次方增长。即使是一个小的 64×64 图像,也会导致输入序列的长度为 64*64=4,096。这样一来,一方面使得模型的计算量增加,另一方面,由于每个像素都要关注整个长序列,模型很难学习图像的结构。卷积神经网络(CNN)通过使用滤波器来解决这个问题,滤波器将单位的输入大小限制在其周围的区域内(感受野)。为了理解 ViT 是如何解决这个问题的,让我们从下面的图开始:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_09_1.jpg

图 9.1 – 视觉变换器。灵感来源于 arxiv.org/abs/2010.11929

设输入图像的分辨率为*(H, W),通道数为C*。然后,我们可以将输入图像表示为一个张量,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/846.png。ViT 将图像分割成一系列二维方形图像块,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/847.png (图 9.1)。这里,*(P, P)*是每个图像块的分辨率(P=16),而https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/848.png是图像块的数量(即输入序列的长度)。这些图像块的序列作为输入提供给模型,方式与 token 序列相同。

接下来,输入图像块https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/849.png将作为输入传递给线性投影,输出一个https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/693.png-维的图像块嵌入向量用于每个图像块。这些图像块嵌入形成输入序列https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/851.png。我们可以用以下公式总结图像块到嵌入的过程:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/852.png

这里,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/853.png 是线性投影,和 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/854.png 是静态位置编码(与原始变换器相同)。

一旦我们获得嵌入序列,ViT 会使用标准的仅编码器预归一化变换器进行处理,类似于 BERT( 第七章)。它有三种变体,如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_09_2.jpg

图 9.2 – ViT 变体。基于 https://arxiv.org/abs/2010.11929

编码器架构使用无掩码的自注意力,这允许一个标记关注整个序列,而不仅仅是前面的标记。这是有道理的,因为图像像素之间的关系不像文本序列中的元素顺序那样,前后元素并不携带相同的意义。两种模型的相似之处不仅仅止于此。与 BERT 类似,输入序列以特殊的 [CLS] (https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/855.png) 标记开始(用于分类任务)。模型对 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/856.png 标记的输出是整个图像的输出。通过这种方式,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/856.png 标记关注整个输入序列(即整个图像)。或者,如果我们选择任何其他补丁的模型输出,我们将引入所选补丁与序列中其他部分之间的不平衡。

模仿 BERT,ViT 也有预训练和微调阶段。预训练使用大规模通用图像数据集(例如 ImageNet),而微调则是在较小的任务特定数据集上训练模型。

该模型以 分类头 结束,预训练阶段包含一个隐藏层,而微调阶段则没有隐藏层。

ViT 的一个问题是,当使用非常大的数据集进行预训练时,它的表现最好,例如使用 300M 标签图像的 JFT-300M 数据集(Revisiting Unreasonable Effectiveness of Data in Deep Learning Eraarxiv.org/abs/1707.02968)。这使得训练变得比可比的 CNN 更加计算密集。许多 ViT 的变体尝试解决这一挑战,并对原始模型提出其他改进。你可以在 A Survey on Visual Transformerarxiv.org/abs/2012.12556)中找到更多信息,该文献定期更新领域的最新进展。

这样,我们来看看如何在实际中使用 ViT。

使用 ViT 和 Hugging Face Transformers

在本节中,我们将借助 Hugging Face Transformers 和其pipeline抽象实现一个 ViT 图像分类的基本示例,这一点我们在第8 章中有介绍。让我们开始:

  1. 导入pipeline抽象:

    from transformers import pipeline
    
  2. 创建一个图像分类管道实例。该管道使用 ViT-Base 模型:

    img_classification_pipeline = pipeline(
         task="image-classification",
         model="google/vit-base-patch16-224")
    
  3. 使用来自维基百科的自行车图像运行实例:

    img_classification_pipeline("https://upload.wikimedia.org/wikipedia/commons/thumb/4/41/Left_side_of_Flying_Pigeon.jpg/640px-Left_side_of_Flying_Pigeon.jpg")
    

    这段代码输出以下前五个类别的概率分布(这里只显示第一个类别):

    [{'score': 0.4616938531398773, 'label': 'tricycle,
         trike, velocipede'}]
    

    这个示例足够简单,但让我们深入分析 ViT 模型本身。我们可以使用print(img_classification_pipeline.model)命令来实现,输出如下:

    ViTForImageClassification(
      (vit): ViTModel(
         (embeddings): ViTEmbeddings(
            (patch_embeddings): ViTPatchEmbeddings(
               (projection): Conv2d(3, 768,
                            kernel_size=(16, 16),
                            stride=(16, 16))
            )
            (dropout): Dropout(p=0.0)
         )
         (encoder): ViTEncoder(
            (layer): ModuleList(
               (0-11): 12 x ViTLayer(
                  (attention): ViTAttention(
                     (attention): ViTSelfAttention(
                        (query): Linear(in_f=768,
                                  out_f=768)
                        (key): Linear(in_f=768, out_f=768)
                        (value): Linear(in_f=768,
                                  out_f=768)
                        (dropout): Dropout(p=0.0)
                     )
                     (output): ViTSelfOutput(
                        (dense): Linear(in_f=768,
                                  out_f=768)
                        (dropout): Dropout(p=0.0)
                     )
                  )
                  (intermediate): ViTIntermediate(
                    (dense): Linear(in_f=768, out_f=3072)
                    (intermediate_act_fn):GELUActivation()
                  )
                  (output): ViTOutput(
                    (dense): Linear(in_f=3072, out_f=768)
                    (dropout): Dropout(p=0.0)
                  )
                  (layernorm_before): LayerNorm((768,))
                  (layernorm_after): LayerNorm((768,))
               )
            )
         )
         (layernorm): LayerNorm((768,))
      )
      (classifier): Linear(in_f=768, out_f=1000)
    )
    

    该模型处理 224×224 的输入图像。这里,in_fout_fin_featuresout_features的缩写。与其他模型不同,ViT 在所有Linear层中使用偏置(bias=True输入参数未显示)。让我们按出现顺序讨论模型的组成部分:

    • ViTEmbeddings:补丁嵌入块。它包含一个大小为 16×16 的 2D 卷积滤波器,步幅为 16,三个输入通道(每个颜色一个),以及 768 个输出通道 (https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/858.png)。在每个位置应用卷积滤波器,生成每个输入图像位置的一个 768 维的补丁嵌入。由于补丁形成了一个二维网格(与输入图像相同),所以输出会被展平为一维序列。该块还添加了位置编码信息,这些信息在其字符串表示中没有体现。所有 dropout 实例的丢弃概率为 0,因为该模型在推理模式下运行,而非训练模式。

    • ViTEncoder:主编码器模型包含 12 个ViTLayer预归一化(LayerNorm)编码块实例。每个实例包含以下内容:

      • ViTAttention 注意力块:ViTSelfAttention 多头注意力及其输出线性投影,ViTSelfOutput。所有的ViTIntermediate加上GELUActivationViTOutput
    • 分类头(classifier):在推理模式下,分类头只有一个Linear层,输出 1,000 个结果(因为该模型在 ImageNet 数据集上进行了微调)。

接下来,让我们看看使用 Transformer 进行目标检测是如何工作的。

理解 DEtection TRansformer

DEtection TRansformer (DETR, 端到端目标检测与 Transformer, arxiv.org/abs/2005.12872)引入了一种基于 Transformer 的创新目标检测算法。

快速回顾 YOLO 目标检测算法

我们在第五章中首次介绍了 YOLO。它有三个主要组件。第一个是骨干网络——即一个卷积神经网络(CNN)模型,用于从输入图像中提取特征。接下来是颈部——模型的中间部分,连接骨干网络和头部。最后,头部使用多步骤算法输出检测到的物体。更具体地,它将图像划分为一个网格,每个网格包含若干个具有不同形状的预定义锚框。模型预测这些锚框中是否包含物体以及物体边界框的坐标。许多框会重叠并预测相同的物体。模型通过交并比(IoU)和非极大值抑制(NMS)帮助筛选重叠的物体。

和 YOLO 一样,DetR 也从 CNN 骨干网络开始。然而,它用一个完整的后归一化变换器编码器-解码器替代了颈部和头部。这消除了需要手工设计组件(例如非极大值抑制过程或锚框)的需求。相反,模型输出一组边界框和类标签,用于表示检测到的物体。为了理解它是如何工作的,我们从下图开始,它展示了 DetR 的组件:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_09_3.jpg

图 9.3 – DetR 架构。灵感来源于 https://arxiv.org/abs/2005.12872

首先,主干 CNN 从输入图像中提取特征,和 YOLO 中的操作相同。其输出是最后一个卷积层的特征图。原始输入图像是一个形状为https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/861.png的张量,其中https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/862.png是颜色通道的数量,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/868.png大小的嵌入张量 (https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/869.png)。为了解决这个问题,模型应用了 1×1 瓶颈卷积,将通道数从 C 降采样到 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/870.png,然后进行展平操作。变换后的张量变为 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/871.png,我们可以将其作为变换器的输入序列使用。

接下来,让我们关注实际的变换器,其详细内容显示在下图中:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_09_4.jpg

图 9.4 – DetR 变换器的详细结构。灵感来源于 arxiv.org/abs/2005.12872

编码器将输入序列映射到一系列连续的表示,就像原始编码器一样(第七章)。不同之处在于,模型在每个Q/K张量的所有注意力层中添加了固定的绝对位置编码,而原始变换器仅在初始输入张量中添加了静态位置编码。

解码器是更有趣的地方。首先,我们注意到,固定位置编码也参与了解码器的编码器-解码器注意力块。由于它们参与了编码器的所有自注意力块,我们将它们传递到编码器-解码器注意力层,以使得各部分公平竞争。

接下来,编码器将输入一个N物体查询的序列,这些查询由张量表示,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/872.png

我们可以将它们视为槽位,模型利用这些槽位来检测物体。每个输入物体查询的模型输出代表一个被检测物体的属性(边界框和类别)。拥有N个物体查询意味着模型最多可以检测N个物体。正因为如此,论文的作者提出使用N,其值显著大于图像中通常的物体数量。与原始的变换器不同,解码器的注意力在这里没有被掩盖,因此它可以并行检测所有物体,而不是按顺序进行检测。

在训练过程开始时,物体查询张量是随机初始化的。训练过程中会更新模型权重和查询张量——也就是说,模型在学习权重的同时也在学习物体查询。它们作为检测物体的学习型位置编码,并起到了与初始固定输入位置编码相同的作用。因此,我们将物体查询添加到编码器-解码器注意力层以及解码器块的自注意力层中,方式与将输入位置编码添加到编码器时相同。这种架构存在一个bug——第一个解码器块的第一个自注意力层将重复两次接收相同的物体查询作为输入,这使得该查询变得无用。实验证明,这不会降低模型的性能。为了简化实现,模型没有设计一个没有自注意力的独立第一个解码器块,而是直接使用了标准解码器块。

编码配置

模型可以处理固定编码和学习编码的多种配置:

  • 只将两种类型的编码添加到输入数据;

  • 将固定编码添加到输入数据,并将学习到的编码添加到输入和所有解码器注意力层;

  • 将固定编码添加到数据和所有编码器注意力层,只将学习到的编码添加到解码器输入;

  • 将两种类型的编码添加到输入数据以及编码器和解码器的每个注意力层中。

该模型在第四种配置下表现最佳,但为了简化起见,可以在第一种配置中实现。

物体查询使得不再强制施加诸如网格单元和锚框这样的几何限制(如在 YOLO 中)。相反,我们只指定检测的最大物体数量,并让模型发挥其魔力。学习到的查询通常会专注于图像的不同区域。然而,这归因于训练和训练数据集的特性,而不是手动设计的特征。

该模型以两个头部的组合结束:一个具有 ReLU 激活的三层感知机和一个独立的全连接(FC)层。该感知机被称为 FFN,它与变换器块中的 FFN 不同。它预测检测到的物体边界框的高度、宽度和相对于输入图像的标准化中心坐标。FC 层采用 softmax 激活,预测物体的类别。像 YOLO 一样,它包含一个额外的特殊背景类,表示在该位置没有检测到任何物体。拥有这个类别尤为必要,因为某些位置不可避免地会为空,N 远大于图像中的物体数量。

预测一组无限制的边界框给训练带来了挑战,因为将预测的边界框与真实框匹配并非易事。第一步是为每张图片的真实框添加虚拟条目,使得真实框的数量等于预测框的数量,即 N。接下来,训练使用预测框与真实框之间的 二分匹配。最后,算法监督每个预测框接近与其匹配的真实框。你可以查阅论文以获取有关训练的更多细节。

用于图像分割的 DetR

DetR 的作者扩展了该模型以用于图像分割。DetR 在检测和分割之间的关系类似于 Faster R-CNN 和 Mask R-CNN 之间的关系(见 第五章)。用于分割的 DetR 添加了一个第三个头部,通过上采样卷积实现。它并行生成每个检测物体的二值分割掩码。最终结果通过像素级的 argmax 合并所有掩码。

使用 Hugging Face Transformers 运行 DetR

在本节中,我们将借助 Hugging Face Transformers 和它的 pipeline 抽象,实施一个基本的 DetR 物体检测示例,我们在 第八章 中介绍了该抽象。这个示例遵循 ViT 模式,因此我们将包括完整的代码,而不做任何注释。代码如下:

from transformers import pipeline
obj_detection_pipeline = pipeline(
     task="object-detection",
     model="facebook/detr-resnet-50")
obj_detection_pipeline("https://upload.wikimedia.org/wikipedia/commons/thumb/4/41/Left_side_of_Flying_Pigeon.jpg/640px-Left_side_of_Flying_Pigeon.jpg")

最后一次调用返回以下形式的检测到的物体列表:

{'score': 0.997983455657959,
  'label': 'bicycle',
  'box': {'xmin': 16, 'ymin': 14, 'xmax': 623, 'ymax': 406}}

接下来,我们可以使用print(obj_detection_pipeline.model)命令查看模型定义。这里,in_fout_f分别是in_featuresout_features的缩写。DetR 在所有Linear层中使用偏置(bias=True输入参数未显示)。我们将省略主干网络的定义。

让我们按出现顺序讨论模型元素,从 1×1 瓶颈卷积开始(我们有 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/873.png):

(input_projection): Conv2d(2048, 256,
                   kernel_size=(1, 1),
                   stride=(1, 1))

接下来,我们有对象查询嵌入(N=100)。正如我们所提到的,对象查询会在训练过程中与权重更新一起学习:

(query_position_embeddings): Embedding(100, 256)

以下是带有六个后置-ln 编码器块、ReLU 激活函数和具有一个 2,048 维隐藏层的 FFN 的编码器。注意,位置编码未显示(解码器也同样适用):

(encoder): DetrEncoder(
  (layers): ModuleList(
     (0-5): 6 x DetrEncoderLayer(
        (self_attn): DetrAttention(
           (k_proj): Linear(in_f=256, out_f=256)
           (v_proj): Linear(in_f=256, out_f=256)
           (q_proj): Linear(in_f=256, out_f=256)
           (out_proj): Linear(in_f=256, out_f=256)
        )
        (self_attn_layer_norm): LayerNorm((256,))
        (activation_fn): ReLU()
        (fc1): Linear(in_f=256, out_f=2048)
        (fc2): Linear(in_f=2048, out_f=256)
        (final_layer_norm): LayerNorm((256,))
     )
  )
)

然后,我们有了解码器,带有六个后置-ln 解码器块,属性与编码器相同:

(decoder): DetrDecoder(
  (layers): ModuleList(
     (0-5): 6 x DetrDecoderLayer(
        (self_attn): DetrAttention(
           (k_proj): Linear(in_f=256, out_f=256)
           (v_proj): Linear(in_f=256, out_f=256)
           (q_proj): Linear(in_f=256, out_f=256)
           (out_proj): Linear(in_f=256, out_f=256)
        )
        (activation_fn): ReLU()
        (self_attn_layer_norm): LayerNorm((256,))
        (encoder_attn): DetrAttention(
           (k_proj): Linear(in_f=256, out_f=256)
           (v_proj): Linear(in_f=256, out_f=256)
           (q_proj): Linear(in_f=256, out_f=256)
           (out_proj): Linear(in_f=256, out_f=256)
        )
        (encoder_attn_layer_norm): LayerNorm((256,))
        (fc1): Linear(in_f=256, out_f=2048)
        (fc2): Linear(in_f=2048, out_f=256)
        (final_layer_norm): LayerNorm((256,))
     )
  )
  (layernorm): LayerNorm((256,))
)

最后,我们得到了输出 FFN 和线性层。FFN 输出四个值(边界框坐标),而线性层可以检测 91 个类别和背景:

(class_labels_classifier): Linear(in_f=256, out_f=92)
(bbox_predictor): DetrMLPPredictionHead(
  (layers): ModuleList(
     (0-1): 2 x Linear(in_f=256, out_f=256)
     (2): Linear(in_f=256, out_f=4)
  )
)

接下来,让我们看看如何使用变换器生成新图像。

使用稳定扩散生成图像

在本节中,我们将介绍稳定扩散SD高分辨率图像合成与潜在扩散模型arxiv.org/abs/2112.10752github.com/Stability-AI/stablediffusion)。这是一种生成模型,可以基于文本提示或其他类型的数据合成图像(在本节中,我们将重点讨论文本到图像的场景)。为了理解它的工作原理,让我们从以下图开始:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_09_5.jpg

图 9.5 – 稳定扩散模型与训练。灵感来源于 arxiv.org/abs/2112.10752

SD 结合了自编码器(AE图 9.5 中的像素空间部分),去噪扩散概率模型(DDPM或简写为DM图 9.5 中的潜在分布空间部分,以及第五章),和变换器(图 9.5 中的条件部分)。在我们深入讨论这些组件之前,先概述它们在 SD 的训练和推理管道中的作用。训练涉及所有这些组件——AE 编码器、前向扩散、反向扩散(U-Net第五章)、AE 解码器和条件。推理(从文本生成图像)仅涉及反向扩散、AE 解码器和条件。不要担心如果你没有完全理解刚才所读内容,我们将在接下来的部分详细讨论。我们将从 AE 开始,接着讨论条件变换器,并在讨论扩散过程时将它们结合起来。

自编码器

尽管我们在第一章中简要提到了自编码器(AE),但在这里我们将更详细地介绍这一架构,从以下图示开始:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_09_6.jpg

图 9.6 – 一个 AE

自编码器(AE)是一个前馈神经网络,它试图重建其输入。换句话说,AE 的目标值(标签)y等于输入数据x。我们可以正式地说,它试图学习一个恒等函数,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/874.png(一个重复其输入的函数)。在最基本的形式下,自编码器由隐藏层组成。

(或瓶颈)和输出层(WW’是这些层的权重矩阵)。像 U-Net 一样,我们可以将自编码器看作是两个组件的虚拟组合:

  • 编码器:它将输入数据映射到网络的内部潜在表示。为了简化起见,在这个例子中,编码器是一个单一的全连接瓶颈层。内部状态就是其激活张量z。编码器可以有多个隐藏层,包括卷积层(如 SD 中的情况)。在这种情况下,z是最后一层的激活。

  • 解码器:它试图从网络的内部状态z重建输入。解码器也可以具有复杂的结构,通常与编码器相似。虽然 U-Net 试图将输入图像转换为其他领域的目标图像(例如,分割图),但自编码器仅仅试图重建其输入。

我们可以通过最小化一个损失函数来训练自编码器,这个损失函数被称为重构误差。它衡量原始输入与其重构之间的距离。

潜在张量z是整个自编码器的核心。关键在于瓶颈层的单元数少于输入/输出层的单元数。因为模型试图从较小的特征空间重构输入,我们迫使它只学习数据中最重要的特征。可以将这种紧凑的数据表示看作一种压缩形式(但不是无损的)。我们可以仅使用模型的编码器部分来生成下游任务所需的潜在张量。或者,我们可以仅使用解码器从生成的潜在张量合成新的图像。

在训练过程中,编码器将输入样本映射到潜在空间,在这里每个潜在属性都有一个离散的值。一个输入样本只能有一个潜在表示。因此,解码器只能用一种可能的方式来重构输入。换句话说,我们只能生成一个输入样本的单一重构。然而,我们希望生成基于文本提示的新图像,而不是重新创建原始图像。解决此任务的一种可能方法是变分自编码器VAE)。VAE 可以用概率术语来描述潜在表示。我们将不再使用离散值,而是为每个潜在属性提供一个概率分布,从而使潜在空间变为连续的。我们可以修改潜在张量以影响生成图像的概率分布(即属性)。在 SD 中,DM 组件与条件文本提示相结合,充当这个修改器。

完成这个简短的绕道后,我们来讨论卷积编码器在 SD 中的作用(像素空间部分,见图 9.5)。在训练过程中,AE 编码器创建了一个压缩的初始潜在表示张量,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/875.png,来自输入图像,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/876.png。更具体地说,编码器将图像按因子进行下采样,f = H/h = W/w,其中 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/877.png (m是通过经验实验选择的整数)。然后,整个扩散过程(前向和反向)使用压缩后的z,而不是原始图像x。只有当反向扩散结束时,AE 解码器才会将新生成的表示z上采样成最终生成的图像,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/878.png。通过这种方式,更小的z允许使用更小、更高效的计算 U-Net,这对训练和推理都有好处。论文的作者将这种 AE 与扩散模型的结合称为潜在扩散模型

AE 训练与 U-Net 训练是分开的。因此,我们可以先训练 AE,然后用它在不同的 U-Net 配置下进行多个下游任务。

条件变换器

条件变换器,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/879.png (图 9.5),生成所需图像的文本描述的潜在表示。SD 将此表示提供给 U-Net,以便它可以影响其输出。为了使这一点生效,文本的潜在表示必须与 U-Net 的图像潜在表示处于相同的语义(不仅仅是维度)空间中。为此,SD 的最新版本 2.1 使用 OpenCLIP 开源模型作为条件变换器(可重复的对比语言-图像学习的规模定律,https://arxiv.org/abs/2212.07143)。CLIP 代表 对比语言-图像预训练。这一技术由 OpenAI 提出(从自然语言监督中学习可迁移的视觉模型,https://arxiv.org/abs/2103.00020)。让我们从下面的图开始更详细地讨论:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_09_7.jpg

图 9.7 – CLIP。灵感来源于 https://arxiv.org/abs/2103.00020

它有两个主要组成部分:

  • [EOS] 标记。该标记处的模型输出作为整个序列的嵌入向量。在 SD 的背景下,我们只关注文本编码器,CLIP 系统的所有其他组件仅在其训练时才是必要的。

  • 图像编码器:这可以是 ViT 或 CNN(最常见的是 ResNet)。它以图像作为输入,输出其嵌入向量,i。与文本编码器类似,这也是模型最高层的激活值,而不是任务特定的头部。

为了使 CLIP 起作用,两个编码器的嵌入向量必须具有相同的大小,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/870.png。如果有必要(例如,在 CNN 图像编码器的情况下),编码器的输出张量会被展平为一维向量。如果两个编码器的维度仍然不同,我们可以添加线性投影(FC 层)来使它们相等。

接下来,让我们专注于实际的预训练算法。训练集包含N个文本-图像对,其中每对的文本描述对应图像的内容。我们将所有文本表示输入文本编码器,将图像输入图像编码器,分别产生https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/881.pnghttps://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/882.png 的嵌入。然后,我们计算每两个嵌入向量之间的余弦相似度(共N×N个相似度测量)。在这些测量中,我们有N个正确匹配的文本-图像对(图 9*.5的表对角线)和N×N-N*个不正确的对(表对角线之外的所有对)。训练更新两个编码器的权重,以使正确对的相似度分数最大化,不正确对的分数最小化。如果训练成功,我们将获得对于正确描述图像内容的文本提示的相似嵌入,并且在所有其他情况下具有不相似的嵌入。在 SD 训练期间,我们优化文本编码器以及 U-Net(但不包括完整的 CLIP 系统)。

现在我们知道如何生成语义上正确的文本嵌入,我们可以继续进行实际的扩散模型。

扩散模型

DM 是一种具有正向和反向阶段的生成模型。前向扩散从由 AE 编码器产生的潜在向量z开始(接受图像x作为输入)。然后,通过一系列T步骤逐渐向z添加随机高斯噪声,直到最终的(潜在的)表示,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/883.png

是纯噪声。前向扩散使用加速算法,在一个步骤中生成https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/884.png,而不是T步骤(第五章)。

反向扩散与此相反,从纯噪声开始。它逐渐通过在一系列T去噪步骤中去除少量噪声来恢复原始的潜在张量z。实际上,我们更关心反向扩散,它是基于潜在表示生成图像(前向扩散只参与训练)。它通常使用 U-Net 类型的 CNN 来实现(图 9.5https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/885.png)。

它以噪声张量https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/540.png作为输入,并输出对添加到原始潜在张量z中的噪声的近似值(即仅噪声,而不是张量本身)。然后,我们从当前的 U-Net 输入中减去预测的噪声,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/887.png,并将结果作为新的输入传递给 U-Net,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/888.png

在训练过程中,代价函数衡量预测噪声与实际噪声之间的差异,并在每次去噪步骤后相应地更新 U-Net 的权重。这个过程持续进行,直到(希望)仅剩下原始张量z。然后,AE 解码器使用它生成最终图像。

DM 的纯粹形式无法影响生成图像的属性(这被称为条件化),因为我们从随机噪声开始,导致生成的是随机图像。SD 允许我们做到这一点——一种方法可以将 U-Net 条件化,以根据特定的文本提示或其他数据类型生成图像。为了实现这一点,我们需要将条件化变换器的输出嵌入与去噪 U-Net 结合。假设我们有一个文本提示[EOS]标记,那么我们通过交叉注意力层将其输出映射到 U-Net 的中间层。在这一层中,键和值张量表示条件化变换器的输出,而查询张量表示 U-Net 的中间层(图 9.5):

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/892.png

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/893.png

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/894.png

在这里,i 是第 i 个中间 U-Net 层,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/895.png 是该层的展平激活值,且 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/896.png 是展平激活张量的大小。https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/897.pnghttps://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/898.png,以及 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/899.png 是可学习的投影矩阵,其中 d 是所选择的实际交叉注意嵌入的大小。对于每个带有交叉注意力的 i 中间 U-Net 层,我们有一组独特的三维矩阵。最简单的形式是,在中间 U-Net 层的输出之后添加一个或多个交叉注意力模块。这些模块可以具有残差连接,保留未修改的中间层输出并通过注意力向量进行增强。请注意,中间卷积层的输出有四个维度:[batch, channel, height, width]。然而,标准的注意力模块使用二维输入:[batch, dim]。一种解决方案是,在将其输入到注意力模块之前,先展平卷积输出。或者,我们可以保留通道维度,只展平高度和宽度:[batch, channel, height*width]。在这种情况下,我们可以将每个卷积通道的输出分配给一个注意力头。

注意

图 9.5有一个开关组件,它允许我们将文本提示表示与 U-Net 输入连接,而不是在中间层使用交叉注意力。这个用例适用于文本到图像以外的任务,这是本节的重点。

接下来,让我们看看如何实际使用 SD。

使用 Hugging Face Transformers 进行稳定扩散

在本节中,我们将使用 SD 生成一个基于文本提示的图像。除了 Transformers 库外,我们还需要Diffusers(https://github.com/huggingface/diffusers)—一个用于生成图像和音频的预训练扩散模型的库。请注意,Diffusers 的 SD 实现要求有 GPU。您可以在启用 GPU 的 Google Colab 笔记本中运行此示例。让我们开始:

  1. 进行必要的导入:

    import torch
    from diffusers import StableDiffusionPipeline
    
  2. 使用 SD 版本 2.1 实例化 SD 管道(sd_pipe)。我们不使用前面例子中使用的主变换器pipeline抽象。相反,我们使用来自diffusers库的StableDiffusionPipeline。如果有可用的cuda设备(NVIDIA GPU),我们还将把模型移动到该设备:

    sd_pipe = StableDiffusionPipeline.from_pretrained(
         "stabilityai/stable-diffusion-2-1",
         torch_dtype=torch.float16)
    sd_pipe.to('cuda')
    
  3. 让我们运行sd_pipe进行 100 次去噪步骤,并使用以下文本提示:

    prompt = \
      "High quality photo of a racing car on a track"
    image = sd_pipe(
         prompt,
         num_inference_steps=100).images[0]
    

    生成的image如下:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_09_8.jpg

图 9.8 – SD 生成的图像

不幸的是,AE、U-Net 和条件变换器的描述很长,包含在此处不太实际。不过,它们可以在 Jupyter Notebook 中查看。尽管如此,我们仍然可以通过print(sd_pipe)命令看到整个 SD 管道的简要总结:

StableDiffusionPipeline {
  "safety_checker": [null, null],
  "tokenizer": ["transformers", "CLIPTokenizer"],
  "text_encoder": ["transformers", "CLIPTextModel"],
  "unet": ["diffusers", "UNet2DConditionModel"],
  "vae": ["diffusers", "AutoencoderKL"],
  "scheduler": ["diffusers", "DDIMScheduler"]
}

这里,transformersdiffusers指的是给定组件的源包。

第一个组件是一个可选的safety_checker(未初始化),它可以识别不适合工作环境NSFW)的图像。

接下来,我们有一个基于 BPE 的CLIPTokenizer tokenizer,它的词汇表大小约为 50,000 个词汇。它将文本提示进行标记化并传递给CLIPTextModeltext_encoder。Hugging Face 的CLIPTextModel复制了 OpenAI CLIP 变换器解码器(模型卡可以在 https://huggingface.co/openai/clip-vit-large-patch14 查阅)。

然后,我们有UNet2DConditionModel。U-Net 的卷积部分使用残差块(见第四章)。它有四个下采样块,下采样因子为 2(通过步长为 2 的卷积实现)。前三个块包括text_encoder交叉注意力层。然后,我们有一个单一的中间块,它保持输入大小,并包含一个残差层和一个交叉注意力子层。模型以四个跳跃连接的上采样块结束,结构上与下采样序列对称。最后三个块也包括交叉注意力层。模型使用sigmoid 线性单元SiLU,见第三章)激活函数。

接下来,我们有卷积自编码器AutoencoderKL,包含四个下采样残差块,一个残差中间块(与 U-Net 中的相同),四个上采样残差块(与下采样序列对称),以及 SiLU 激活函数。

最后,让我们聚焦于DDIMSchedulerscheduler,它是diffusers库的一部分。这是多个可用调度器之一。在训练过程中,调度器会向样本添加噪声,以训练 DM。它定义了在推理期间如何根据 U-Net 的输出更新潜在张量。

Stable Diffusion XL

最近,Stability AI 发布了 Stable Diffusion XL(SDXL:改进高分辨率图像合成的潜在扩散模型arxiv.org/abs/2307.01952)。SDXL 使用了三倍大的 U-Net。更大的尺寸源于更多的注意力块和更大的注意力上下文(新版本使用了两个不同文本编码器的连接输出)。它还利用了一个可选的精炼模型refiner)——第二个 U-Net 与第一个处于相同的潜在空间,专注于高质量、高分辨率的数据。它将第一个 U-Net 的输出潜在表示z作为输入,并使用相同的条件文本提示。

至此,我们已完成对 SD 的介绍,以及计算机视觉中变压器的更大主题。接下来,让我们看看如何微调基于变压器的模型。

探索微调变压器

在本节中,我们将使用 PyTorch 微调一个预训练的变压器。更具体地,我们将微调一个Trainer类(huggingface.co/docs/transformers/main_classes/trainer),它实现了基本的训练循环、模型评估、在多个 GPU/TPU 上的分布式训练、混合精度和其他训练特性。这与我们目前在 PyTorch 示例中所做的从头开始实现训练相对立。我们还需要Datasetsgithub.com/huggingface/datasets)和Evaluategithub.com/huggingfahttps://github.com/huggingface/evaluate)包。让我们开始:

  1. 加载数据集,该数据集被划分为trainvalidationtest部分:

    from datasets import load_dataset
    dataset = load_dataset('rotten_tomatoes')
    
  2. 加载 DistilBERT WordPiece 子词tokenizer

    from transformers import AutoTokenizer
    tokenizer = AutoTokenizer.from_pretrained('distilbert-base-uncased')
    
  3. 使用tokenizer对数据集进行分词。此外,它将对每个样本进行填充或截断,直到符合模型所接受的最大长度。batched=True映射通过将数据合并成批次(而不是单个样本)来加速处理。Tokenizers库在批量处理时运行更快,因为它并行化了批次中所有示例的分词过程:

    tok_dataset = dataset.map(
        lambda x: tokenizer(
            text=x['text'],
            padding='max_length',
            truncation=True),
        batched=True)
    
  4. 加载变压器model

    from transformers import AutoModelForSequenceClassification
    model = AutoModelForSequenceClassification.from_pretrained(
        'distilbert-base-uncased')
    

    AutoModelForSequenceClassification类加载 DistilBERT 配置,用于二分类任务——该模型头部有一个隐藏层和一个带有两个单元的输出层。此配置适用于我们的任务,因为我们需要将电影评论分类为两类。

  5. 初始化Trainer实例的TrainingArguments。我们将指定output_dir,作为模型预测和检查点的存储位置。我们还将每个周期运行一次评估:

    from transformers import TrainingArguments
    training_args = TrainingArguments(
        output_dir='test_trainer',
        evaluation_strategy='epoch')
    
  6. 初始化accuracy评估指标:

    import evaluate
    accuracy = evaluate.load('accuracy')
    
  7. 初始化trainer,包括训练和评估所需的所有组件:

    from transformers import Trainer
    import numpy as np
    trainer = Trainer(
        model=model,
        train_dataset=tok_dataset['train'],
        eval_dataset=tok_dataset['test'],
        args=training_args,
        compute_metrics=
            lambda x: accuracy.compute(
                predictions=x[0],
                references=x[1]),
        preprocess_logits_for_metrics=
            lambda x, _: np.argmax(x.cpu(), axis=-1)
    )
    

    它接受模型、训练和评估数据集,以及training_args实例。compute_metrics函数将在每个周期后计算验证准确率。preprocess_logits_for_metrics会将经过独热编码的模型输出(x[0])转换为索引标签,以便与compute_metrics函数中的真实标签(x[1])格式匹配。

  8. 最后,我们可以开始训练:

    trainer.train()
    

    该模型将在三个训练周期内达到大约 85%的准确率。

接下来,让我们看看如何使用 LangChain 框架释放 LLM 的强大能力。

利用 LangChain 释放 LLM 的强大能力

LLM 是强大的工具,但它们也有一些局限性。其中之一是上下文窗口的长度。例如,Llama 2 的最大输入序列为 4,096 个令牌,按单词计数则更少。作为参考,本书大部分章节大约为 10,000 个单词。许多任务的长度会超出这个限制。另一个 LLM 的局限性是,它的所有知识都储存在训练时的模型权重中。它没有直接与外部数据源(如数据库或服务 API)交互的方式。因此,知识可能会过时或不足。LangChain框架可以帮助我们缓解这些问题。它通过以下模块实现:

  • 模型输入/输出:该框架区分经典的 LLM 和聊天模型。在第一种情况下,我们可以通过一个简单的提示来激活模型,模型会生成一个响应。第二种情况则更具互动性——它假设人类和模型之间会有来回的交流。内部来说,两者都是 LLM,区别在于使用了不同的 API。无论模型类型如何,令牌序列都是输入数据的唯一方式。I/O 模块为不同的用例提供了辅助的提示模板。例如,聊天模板会维持一个明确的所有消息列表,而不是将它们合并为一个单一的序列。我们还提供了一个少量示例模板,它为输入查询提供了一个接口,可以在查询中包含一个或多个指令性输入/输出示例。

    该模块还可以解析模型输出(将令牌序列转换为单词)。例如,如果输出是一个 JSON 字符串,JSON 解析器可以将其转换为实际的 JSON 对象。

  • 检索:这是用来获取外部数据并将其输入到模型序列中的功能。其最基本的功能是解析文件格式,例如 CSV 和 JSON。如果文档过大而无法适应上下文窗口大小,它还可以将文档拆分成更小的块。

向量数据库

LLM 和其他神经网络的主要输出(在任何任务特定头部之前)是嵌入向量,我们将这些向量用于下游任务,如分类或文本生成。该数据格式的通用性促使了向量特定数据库(或存储)的创建。如其名称所示,这些存储仅与向量一起使用,并支持快速的向量操作,例如对整个数据库执行不同的相似度度量。我们可以查询输入的嵌入向量与数据库中所有其他向量进行比较,找到最相似的向量。这个概念类似于 Q/K/V 注意力机制,但它是一种外部数据库形式,可以处理比内存注意力更多的数据集。

该检索模块与多个向量数据库集成。这样,我们就可以使用 LLM 生成并存储文档嵌入(文档作为输入序列)。随后,我们可以查询 LLM 为给定查询生成新的嵌入,并将此查询与数据库进行比较,找到最相似的匹配项。在这种情况下,LLM 的作用仅限于生成向量嵌入。

  • :链是将多个 LangChain 组件组合起来,创建单一应用程序的机制。例如,我们可以创建一个链,它接收用户输入,使用特殊的提示模板格式化输入,将其传递给 LLM,并将 LLM 输出解析为 JSON。我们还可以分支链或组合多个链。

  • 记忆:记忆保持输入的标记序列,贯穿整个步骤链或模型与外部世界的交互过程中,它可以动态地修改和扩展该序列。它还可以利用新兴的 LLM 能力来创建当前历史序列的简短摘要。这个简短版本会替代原始输入序列中的内容,用于未来的输入。这种压缩使我们能够更高效地利用上下文窗口,并存储更多信息。

  • 代理:代理是可以采取与环境交互的行动的实体。在当前上下文中,LLM 作为代理的推理引擎,决定代理应该采取哪些行动以及按什么顺序执行。为了帮助完成这一任务,代理/LLM 可以使用特殊的功能,称为 工具。这些工具可以是通用实用程序(例如 API 调用)、其他链,甚至是代理。

  • 回调:我们可以使用回调函数来插入 LLM 应用程序的不同点,这对于日志记录、监控或流式处理非常有用。

接下来,让我们通过一个示例加深对 LangChain 的理解。

实际应用 LangChain

在本节中,我们将使用 LangChain、LangChain Experimental(github.com/langchain-ai/langchain/tree/master/libs/experimental)以及 OpenAI 的gpt-3.5-turbo模型来回答这个问题:海洋最深处和地球最高峰的海拔总和是多少?仅使用公制单位。为了让事情更有趣,我们不会让 LLM 一次生成一个词。相反,我们将要求它将解决方案拆分为步骤,并使用数据查找和计算来找到正确答案。

注意

此示例部分基于python.langchain.com/docs/modules/agents/agent_types/plan_and_execute。它需要访问 OpenAI API(platform.openai.com/)和 SerpAPI(serpapi.com/)。

让我们开始:

  1. 初始化 LangChain 的 API 包装器以使用gpt-3.5-turbo模型。temperature参数(在[0,1]范围内)决定模型如何选择下一个 token。例如,如果temperature=0,它将始终输出概率最高的 token。temperature越接近 1,模型选择低概率 token 的可能性就越大:

    from langchain.chat_models import ChatOpenAI
    model = ChatOpenAI(temperature=0)
    
  2. 定义将帮助我们解决此任务的工具。首先是搜索工具,它使用 SerpAPI 执行 Google 搜索。这允许 LLM 查询 Google,以获取我们问题中关于海洋最深处和最高山峰的海拔:

    # Tools
    from langchain.agents.tools import Tool
    # Search tool
    from langchain import SerpAPIWrapper
    search = Tool(
        name='Search',
        func=SerpAPIWrapper().run,
        description='Google search tool')
    
  3. 接下来是计算器工具,它将允许 LLM 计算海拔的总和。该工具使用一个特殊的少样本学习 LangChain PromptTemplate来查询 LLM 计算数学方程式:

    from langchain import LLMMathChain
    llm_math_chain = LLMMathChain.from_llm(
        llm=model,
        verbose=True)
    calculator = Tool(
        name='Calculator',
        func=llm_math_chain.run,
        description='Calculator tool')
    
  4. 初始化一个特殊的PlanAndExecute agent。它接受 LLM、我们刚才定义的工具,以及plannerexecutor代理作为参数:

    from langchain_experimental.plan_and_execute import PlanAndExecute, load_agent_executor, load_chat_planner
    agent = PlanAndExecute(
        planner=load_chat_planner(
            llm=model),
        executor=load_agent_executor(
            llm=model,
            tools=[search, calculator],
            verbose=True),
        verbose=True)
    

    planner使用一个特殊的 LangChain 文本提示模板来查询 LLM 模型,将任务的解决方案拆分为子任务(步骤)。模型生成一个适合列表格式的字符串,planner解析并返回作为executor(本身是一个agent)执行的步骤列表。

  5. 最后,我们可以运行agent

    agent.run('What is the sum of the elevations of the deepest section of the ocean and the highest peak on Earth? Use metric units only.')
    

    最终答案是海洋最深处的深度(公制单位)为 19,783 米。尽管文本描述有些不准确,但计算结果似乎是正确的。

让我们分析plannerexecutor在达到结果的过程中所采取的部分步骤。首先,planner接受我们的初始查询,并要求 LLM 将其拆分为以下步骤列表:

1\. 'Find the depth of the deepest section of the ocean in metric units.'
2\. 'Find the elevation of the highest peak on Earth in metric units.'
3\. 'Add the depth of the deepest section of the ocean to the elevation of the highest peak on Earth.'
4\. 'Round the sum to an appropriate number of decimal places.'
5\. "Given the above steps taken, respond to the user's original question. \n"

接下来,agent遍历每个步骤,并指派executor执行它。executor有一个内部 LLM 规划器,它也可以将当前步骤拆分为子任务。除了步骤描述外,executor还使用一个特殊的文本提示,指示其 LLMmodel识别它可以用于每个步骤的tools(工具有名称)。例如,executor返回以下结果作为第一步增强版本的输出:

'Action: {
    "action": "Search",
    "action_input": "depth of the deepest section of the ocean in metric units"
}'

Search代理的action表示在当前步骤之后执行的一个新的中间步骤。它将使用搜索工具通过action_input查询 Google。从这个角度来看,链条是动态的,因为一个步骤的输出可以导致额外的步骤被添加到链条中。我们将每个步骤的结果加入到未来步骤的输入序列中,LLM 通过不同的提示模板最终决定下一步的行动。

这就是我们对 LangChain 的介绍——展示了 LLM 的可能性。

总结

在本章中,我们讨论了多个话题。我们从计算机视觉领域的 LLM 开始:ViT 用于图像分类,DetR 用于目标检测,SD 用于文本到图像的生成。接着,我们学习了如何使用 Transformers 库对 LLM 进行微调。最后,我们使用 LangChain 实现了一个新型的 LLM 驱动应用。

在下一章,我们将脱离传统话题,深入探讨 MLOps 的实际应用。

第四部分:

开发

和深度神经网络的部署

在这一单章部分,我们将讨论一些有助于我们开发和部署神经网络模型的技术和工具。

本部分包含以下章节:

  • 第十章机器学习运维(MLOps)

第十章:机器学习

运营(MLOps)

到目前为止,在本书中,我们专注于**神经网络(NNs)**的理论、各种 NN 架构以及我们可以解决的任务。这一章节有些不同,因为我们将专注于 NN 开发的一些实际方面。我们将深入探讨这个主题,因为 ML 模型(特别是 NNs)的开发和生产部署存在一些独特的挑战。我们可以将这个过程分为三个步骤:

  1. 训练数据集创建:数据收集、清理、存储、转换和特征工程。

  2. 模型开发:尝试不同的模型和训练算法,并评估它们。

  3. 部署:在生产环境中部署训练好的模型,并监控其在计算和准确性方面的性能。

这个多步骤的复杂流程预设了解决 ML 任务时的一些挑战:

  • 多样化的软件工具包:每个步骤都有多个竞争工具。

  • 模型开发很难:每个训练实例都有大量的变量。这些可能包括 NN 架构的修改、训练超参数的变化(如学习率或动量)或不同的训练数据分布。此外,NNs 还有随机源,例如权重初始化或数据增强。因此,如果我们无法重现早期的结果,很难找出原因。即使代码中有错误,它可能不会导致易于检测的运行时异常。相反,它可能只是稍微降低模型的准确性。为了不丢失所有实验的记录,我们需要一个强大的跟踪和监控系统。

  • 复杂的部署和监控:NNs 需要 GPU 和批处理组织的数据以达到最佳性能。这些要求可能与实时处理数据或逐样本处理的真实世界要求发生冲突。此外,用户数据的性质可能随时间变化,这可能导致模型漂移

在本章中,我们将涵盖以下主要主题:

  • 理解模型开发

  • 探索模型部署

技术要求

我们将使用 Python、PyTorch、TensorFlow(TF)Hugging Face(HF) Transformers 等工具实现本章中的示例。如果您还没有配置好这些工具的环境,不要担心——示例代码可以在 Google Colab 的 Jupyter Notebook 中找到。您可以在本书的 GitHub 存储库中找到这些代码示例:github.com/PacktPublishing/Python-Deep-Learning-Third-Edition/tree/main/Chapter10

理解模型开发

在本节中,我们将讨论各种工具,这些工具将帮助我们管理 ML 解决方案生命周期的模型开发阶段。让我们从最重要的问题开始——我们应该选择哪个 NN 框架?

选择一个 NN 框架

到目前为止,在本书中我们主要使用了 PyTorch 和 TensorFlow。我们可以将它们称为基础框架,因为它们是整个神经网络软件堆栈中最重要的组件。它们作为机器学习神经网络生态系统中其他组件的基础,比如 Keras 或 HF Transformers,这些组件可以使用它们作为后端(Keras 3.0 将支持多后端)。除了 TensorFlow,Google 还发布了 JAX(github.com/google/jax),这是一个支持 GPU 加速的 NumPy 操作和 Autograd 的基础库。其他流行的库,如 NumPy、pandas 和 scikit-learn(scikit-learn.org)超出了本书的范围,因为它们与神经网络没有直接关系。由于基础库的重要性,它们是我们工具包中的首选和最重要的选择。但如果我们从零开始启动一个项目,应该选择哪一个呢?

PyTorch 与 TensorFlow 与 JAX

让我们来查看这些库在社区中的采用程度。我们的第一站是Papers with Code (paperswithcode.com/),它索引了机器学习论文、代码、数据集和结果。该网站还维护了按框架分组的论文实现趋势(paperswithcode.com/trends)。截至 2023 年 9 月,57%的新论文使用 PyTorch。TF 和 JAX 分别以 3%和 2%的比例位居第二和第三。这个趋势并不新鲜——PyTorch 于 2016 年发布,但它在 2019 年已经超过了 TF。这个特定的数据点表明,PyTorch 主导着前沿研究,而这些研究正是最新的论文。因此,如果你希望始终使用该领域最新和最优秀的技术,选择 PyTorch 是个不错的主意。接下来,我们来看一下托管在 HF 平台上的机器学习模型(huggingface.co/models),我们也可以按项目框架进行筛选。在大约 335,000 个托管模型中,约 131,000 个使用 PyTorch,约 10,000 个使用 TF,约 9,000 个使用 JAX。再次,这一结果强烈支持 PyTorch。然而,这并不是完整的图景,因为这些结果仅适用于公开和开源项目。它们不一定能反映公司在生产环境中的使用情况。更具代表性的可能是 PyPI Stats(pypistats.org/),它提供了Python 软件包索引PyPipypi.org/)上 Python 包的下载汇总信息。这里的情况稍微复杂一些——PyTorch 在过去一个月(2023 年 8 月-9 月)有 11,348,753 次下载,而 TF 为 16,253,288 次,JAX 为 3,041,747 次。然而,我们应该对 PyPi Stats 持谨慎态度,因为许多自动化流程(例如持续集成)可能会使 PyPi 的下载次数膨胀,而这些并不代表真实世界的使用情况。此外,PyTorch 的下载页面建议通过 Conda(conda.io/)安装该库。月度统计数据显示,PyTorch 有 759,291 次下载,而 TF 为 154,504 次,JAX 为 6,260 次。因此,PyTorch 在这里也占据领先地位。总的来说,我的结论是,PyTorch 比 TF 更受欢迎,但这两者都在生产环境中使用。

我的建议是,如果你现在开始一个项目,可以选择 PyTorch,具体如何采纳这个建议可以根据你的实际情况来决定。正因如此,本书在讲解时相对于 TF 更多强调了 PyTorch。这个规则的一个例外是,如果你的项目运行在移动设备或边缘设备(en.wikipedia.org/wiki/Edge_device)上,并且计算能力有限。TF 通过 TF Lite 库(www.tensorflow.org/lite)对这类设备提供了更好的支持。

但最终,你可以使用自己偏好的软件栈进行工作,然后将模型转换为其他库以进行部署。我们将在下一节中看到这如何实现。

开放神经网络交换格式

开放神经网络交换格式ONNXonnx.ai/)提供了一个用于基于神经网络(NN)和传统机器学习(ML)模型的开源格式(我们将在这里专注于神经网络)。它定义了一个可扩展的计算图模型、内置操作符和标准数据类型。换句话说,ONNX 提供了一个通用的神经网络表示格式,允许我们将一个库(例如 PyTorch)实现的模型转换为另一个库(如 TF)的模型,前提是源库和目标库都支持 ONNX。通过这种方式,你可以使用一个库来训练模型,然后在部署到生产环境时将其转换为另一个库。这也很有意义,因为 ONNX 关注推理模式,而不是训练(使用 ONNX 表示训练过程是实验模式)。

ONNX 通过计算onnx!pip install onnx)Python 包来表示一个神经网络。我们开始吧:

  1. 定义图表示的输入(XAB)和输出(Y)变量:

    import numpy as np
    from onnx import TensorProto, numpy_helper
    from onnx.helper import make_tensor_value_info
    X = make_tensor_value_info(
        name='X',
        elem_type=TensorProto.FLOAT,
        shape=[None, None])
    Y = make_tensor_value_info(
        'Y', TensorProto.FLOAT, [None])
    A = numpy_helper.from_array(
        np.array([0.5, -0.6], dtype=np.float32),
        name='A')
    B = numpy_helper.from_array(
        np.array([0.4], dtype=np.float32),
        name='B')
    

    在这里,make_tensor_value_info声明了命名的图输入输出变量(XY),并为其定义了类型(elem_type)和shapeshape=[None]意味着任意形状,而shape=[None, None]意味着没有具体维度大小的二维张量。另一方面,AB是函数参数(权重),我们用 NumPy 数组中的预定义值对它们进行初始化。

  2. 定义图的操作:

    from onnx.helper import make_node
    mat_mul = make_node(
        op_type='MatMul',
        inputs=['X', 'A'],
        outputs=['XA'])
    addition = make_node('Add', ['XA', 'B'], ['Y'])
    

    mat_mul表示输入矩阵XA之间的矩阵乘法(MatMul),并将结果存储到输出变量XA中。additionmat_mul的输出XA与偏置B相加。

ONNX 操作符

这个示例介绍了MatMulAdd的 ONNX 操作符。支持的操作符完整列表(请参见onnx.ai/onnx/operators/)包括许多其他神经网络构建块,如激活函数、卷积、池化以及张量操作符(例如concatpadreshapeflatten)。此外,它还支持所谓的if操作符,根据布尔值执行一个子图或另一个子图。ONNX 本身并不实现这些操作符。相反,支持它的库(如 PyTorch)有自己的实现。反过来,如果你的库模型使用了 ONNX 不支持的操作符,ONNX 转换将会失败。

  1. 现在我们已经具备了定义计算graph的条件:

    from onnx.helper import make_graph
    graph = make_graph(
        nodes=[mat_mul, addition],
        name='Linear regression',
        inputs=[X],
        outputs=[Y],
        initializer=[A, B])
    

    我们可以在以下图中看到我们的计算图:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_10_1.jpg

图 10.1 – 线性回归 ONNX 计算图

  1. 使用graph来创建一个onnx_model实例。该模型允许你向图中添加额外的元数据,如文档字符串、版本、作者和许可证等:

    from onnx.helper import make_model
    onnx_model = make_model(graph)
    onnx_model.doc_string = 'Test model'
    onnx_model.model_version = 1
    
  2. 检查模型的一致性。这可以验证模型组件之间输入类型或形状是否匹配:

    from onnx.checker import check_model
    check_model(onnx_model)
    print(onnx_model)
    
  3. 最后,我们可以使用 ReferenceEvaluator 实例计算两个随机输入样本的模型输出:

    from onnx.reference import ReferenceEvaluator
    sess = ReferenceEvaluator(onnx_model)
    print(sess.run(
        output_names=None,
        feed_inputs={'X': np.random.randn(2, 2).astype(np.float32)}))
    

    计算的结果是一个 NumPy 数组:

    [array([-0.7511951,  1.0294889], dtype=float32)]
    
  4. ONNX 允许我们使用 协议缓冲区 (Protocol Buffersprotobufprotobuf.dev/) 来序列化和反序列化模型结构及其权重。以下是操作方法:

    with open('model.onnx', 'wb') as f:
        f.write(onnx_model.SerializeToString())
    from onnx import load
    with open('model.onnx', 'rb') as f:
        onnx_model = load(f)
    

现在我们已经介绍了 ONNX,接下来看看如何通过将 PyTorch 和 TF 模型导出到 ONNX 来实际应用它。

除了 torchtensorflow,我们还需要 torchvisiononnxtf2onnxgithub.com/onnx/tensorflow-onnx!pip install tf2onnx)包。我们先从 PyTorch 开始:

  1. 加载一个预训练模型(MobileNetV3,参考 第五章):

    import torch
    from torchvision.models import mobilenet_v3_small, MobileNet_V3_Small_Weights
    torch_model = mobilenet_v3_small(
      weights=MobileNet_V3_Small_Weights.DEFAULT)
    
  2. 然后,导出模型:

    torch.onnx.export(
        model=torch_model,
        args=torch.randn(1, 3, 224, 224),
        f="torch_model.onnx",
        export_params=True)
    

    大多数参数不言而喻。args=torch.randn(1, 3, 224, 224) 指定了一个虚拟张量。这是必要的,因为序列化器可能会调用模型一次,以推断图结构和张量的大小。这个虚拟张量将作为调用的输入。然而,这也暴露了转换过程中的一个限制:如果模型包含动态计算图,转换器仅会转换当前调用路径。export_params 告诉导出器在导出模型结构时也包括模型的权重。

  3. 使用 ONNX 加载导出的模型并检查其一致性(剧透:它可以正常工作):

    import onnx
    torch_model_onnx = onnx.load('torch_model.onnx')
    onnx.checker.check_model(torch_model_onnx)
    

接下来,我们也可以使用 TF 执行相同的操作。与 PyTorch 不同,TF 没有开箱即用的 ONNX 序列化支持。相反,我们将使用 tf2onnx 包:

  1. 加载一个预训练的 MobileNetV3 模型:

    import tensorflow as tf
    tf_model = tf.keras.applications.MobileNetV3Small(
      weights='imagenet',
      input_shape=(224, 224, 3),
    )
    
  2. 使用 tf2onnx 序列化模型。它遵循与 PyTorch 相同的原理,包括虚拟输入张量(input_signature),这是调用模型时必需的:

    import tf2onnx
    tf_model_onnx, _ = tf2onnx.convert.from_keras(
      model=tf_model,
      input_signature=[tf.TensorSpec([1, 224, 224, 3])])
    onnx.save(tf_model_onnx, 'tf_model.onnx')
    

我们再次可以使用 ONNX 加载模型,以验证其一致性。

接下来,我们可以使用 torch_model.onnxtf_model.onnx。这是一种用于神经网络和其他机器学习模型的图形查看工具。它可以作为 用户界面 (UI) 的 Web 版,或者作为独立应用程序存在。它支持 ONNX、TensorFlow Lite 和 PyTorch(实验性支持),以及其他一些库。例如,以下图显示了通过 Netron 可视化的初始MobileNetV3层(完整模型的可视化太大,无法在本章中显示):

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_10_2.jpg

图 10.2 – MobileNetV3 ONNX 模型文件的 Netron 可视化

在这里,输入形状为 3×224×224,W 是卷积滤波器的形状,B 是偏置。我们在 第四章 中介绍了其余的卷积属性。

不幸的是,PyTorch 和 TF 都没有集成加载 ONNX 模型的功能。但是,已经有开源包允许我们实现这一点。其中有两个分别为 PyTorch 提供的 onnx2torch (github.com/ENOT-AutoDL/onnx2torch) 和为 TF 提供的 onnx2tf (github.com/PINTO0309/onnx2tf)。

接下来,我们将重点介绍一款能够简化训练过程的工具。

介绍 TensorBoard

TensorBoardTBwww.tensorflow.org/tensorboard/github.com/tensorflow/tensorboard)是一个 TF 补充的基于网页的工具,提供了机器学习实验的可视化和工具支持。它的一些功能如下:

  • 指标(如损失和精度)跟踪和可视化

  • 模型图可视化(类似 Netron)

  • 显示权重、偏差或其他张量随时间变化的时间序列直方图

  • 低维嵌入投影

TensorBoard 可以与 TF/Keras 和 PyTorch 一起使用,但它与 TF 的集成更好(毕竟是由 TF 团队开发的)。在这两种情况下,TensorBoard 在训练过程中并不会直接与模型通信。相反,训练过程会将其状态和当前进度存储在一个特殊的日志文件中。TensorBoard 跟踪该文件的变化,并自动更新其图形界面,展示最新信息。通过这种方式,它可以随着训练的进展实时可视化训练过程。此外,该文件还会存储整个训练历史,即使训练完成后,仍然可以展示这些数据。为了更好地理解其工作原理,我们将把 TensorBoard 添加到我们在 第五章 中介绍的迁移学习计算机视觉示例中。简要回顾一下,我们将从 ImageNet 预训练的 MobileNetV3 模型开始。接着,我们将使用两种迁移学习技术,特征工程微调,来训练这些模型以对 CIFAR-10 数据集进行分类。TensorBoard 将可视化训练过程。

让我们从 Keras 示例开始。我们只会包括相关部分的代码,而不是完整的示例,因为我们在 第五章 中已经讨论过了。更具体地说,我们将专注于 train_model(model, epochs=5) 函数,该函数将预训练的 model 和训练的 epochs 数量作为参数。以下是该函数的主体(请注意,实际的实现有缩进):

初始化 TensorBoard

本示例假设 TensorBoard 已经初始化并正在运行(尽管即使未安装,代码仍然可以正常工作)。我们不会包括 TensorBoard 的初始化代码,因为它取决于环境设置。但它在本示例的 Jupyter Notebook 中是有包含的。

按照以下步骤操作:

  1. 首先,我们将使用 Adam 优化器、二元交叉熵损失函数以及精度跟踪来配置预训练 Keras 模型的训练:

    model.compile(
        optimizer=tf.keras.optimizers.Adam(
            learning_rate=0.0001),
        loss='categorical_crossentropy',
        metrics=['accuracy'])
    
  2. 接下来,我们将添加特殊的tensorboard_callback,它实现了 TB 连接:

    tensorboard_callback = tf.keras.callbacks.TensorBoard(
        log_dir='logs/tb/' + datetime.datetime.now().strftime('%Y%m%d-%H%M%S'),
        update_freq='epoch',
        histogram_freq=1,
        write_graph=True,
        write_images=True,
        write_steps_per_second=True,
        profile_batch=0,
        embeddings_freq=0)
    

    回调参数如下:

    • log_dir:这指示tensorboard_callback将日志文件写入一个唯一的时间戳文件夹,即'logs/tb/' + datetime.datetime.now().strftime('%Y%m%d-%H%M%S'),位于主文件夹'logs/tb/'下。TB 将同时选择'logs/tb/'下所有训练文件夹,并在其 UI 中显示它们作为唯一的训练实例。

    • update_freq=1:每个周期更新日志文件。

    • histogram_freq=1:每个周期计算一次权重直方图。

    • write_graph=True:生成 NN 架构的图形可视化。

    • write_images=True:将模型权重可视化为图像。

    • write_steps_per_second=True:记录每秒训练步骤。

    • profile_batch=1:对第一批次进行分析以采样其计算特性。

    • Embeddings_freq=0:嵌入层将被可视化的频率(以周期为单位)(我们没有嵌入层,因此默认情况下禁用)。

  3. 最后,我们将使用model.fit方法运行训练:

    steps_per_epoch=metadata.splits['train'].num_examples // BATCH_SIZE
    validation_steps=metadata.splits['test'].num_examples // BATCH_SIZE
    model.fit(
        train_batches,
        epochs=epochs,
        validation_data=test_batches,
        callbacks=[tensorboard_callback],
        steps_per_epoch=steps_per_epoch,
        validation_steps=validation_steps)
    

    我们将tensorboard_callback添加到model的回调列表中。训练过程会通知每个回调不同的训练事件:训练开始、训练结束、测试开始、测试结束、周期开始、周期结束、批次开始和批次结束。反过来,tensorboard_callback根据其配置和当前事件更新日志文件。

    TB UI 显示了日志文件中的所有信息。虽然它过于复杂无法在此处包含,但我们仍然可以显示一个关于准确度的片段:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_10_3.jpg

图 10.3 – TB UI 中的准确度

在这里,TB 显示了四个不同实验的准确度 - 特征工程的训练/测试和微调的训练/测试。

接下来,让我们看看 PyTorch 如何与 TB 集成。它提供了一个特殊的torch.utils.tensorboard.SummaryWriter类,它将条目直接写入事件日志文件,以供 TB 消费。它遵循与 Keras 相同的原则。SummaryWriter的高级 API 允许我们在log_dir中创建一个事件文件,并异步向其添加内容。与 Keras 不同的主要区别在于,我们负责添加内容,而不是由自动事件侦听器执行。让我们看看实际操作中是如何工作的。与 Keras 一样,我们将使用计算机视觉迁移学习示例来自第五章。我们只关注相关部分,但您可以在本书的 GitHub 存储库的 Jupyter 笔记本中查看完整示例。

首先,我们将初始化两个SummaryWriter实例,用于特征提取器的微调模式。无论我们在哪里执行它,只要在开始使用它们之前执行即可。与 Keras 一样,每个训练实例都有一个唯一的时间戳文件夹,位于'logs/tb/'下(我们仅显示一个初始化,因为它们都是相同的):

import datetime
from torch.utils.tensorboard import SummaryWriter
writer = SummaryWriter(
log_dir='logs/tb/' + datetime.datetime.now().strftime('%Y%m%d-%H%M%S'))

为了清晰起见,我们将包括初始化 MobileNetV3 预训练模型的代码:

from torchvision.models import (
    MobileNet_V3_Small_Weights, mobilenet_v3_small)
model = mobilenet_v3_small(
    weights=MobileNet_V3_Small_Weights.IMAGENET1K_V1)

接下来,我们将进入训练(或测试)循环,其中 train_loadertorch.utils.data.DataLoader 的一个实例,生成 inputslabels 的小批量数据:

for i, (inputs, labels) in enumerate(data_loader):
    # Training loop goes here

在循环中,我们可以将模型图添加到日志文件中。它以模型和输入张量作为参数生成可视化(因此需要在训练循环中调用 add_graph):

writer.add_graph(model, inputs)

最后,在训练循环的末尾,我们将添加当前 epoch 的损失和准确率作为标量值:

writer.add_scalar(
    tag='train/accuracy',
    scalar_value=total_acc,
    global_step=epoch)
writer.add_scalar(
    tag='train/loss',
    scalar_value=total_loss,
    global_step=epoch)

每个标量值都有一个唯一的 tag(除了代码中的两个标签外,我们还有 tag='validation/loss')。请注意,global_step(等于 epoch)将 scalar_value 存储为同一 tag 下的一个序列。除了图形和标量外,SummaryWriter 还可以添加图像、张量、直方图和嵌入等内容。

本文结束了我们对 TB 的介绍。接下来,我们将学习如何为边缘设备开发神经网络模型。

使用 TF Lite 开发边缘设备的神经网络模型

TF Lite 是一个源自 TF 的工具集,使我们能够在移动设备、嵌入式设备和边缘设备上运行模型。其多功能性是 TF 在工业应用中受欢迎的原因之一(与 PyTorch 主导的研究应用领域相对)。TF Lite 的核心范式是模型在设备上运行,而不是传统的客户端-服务器架构,其中模型部署在远程、更强大的硬件上。这种组织方式有以下影响(包括正面和负面):

  • 低延迟执行:缺少服务器的往返连接显著减少了模型推理时间,使我们能够运行实时应用程序。

  • 隐私:用户数据从不离开设备。

  • 互联网连接:不需要互联网连接。

  • .tflite 文件扩展名。除了文件体积小,它还允许我们直接访问数据,而无需首先解析/解包它。

TF Lite 模型支持 TF Core 操作的子集,并允许我们定义自定义操作:

  • 低功耗:这些设备通常使用电池供电。

  • 训练与推理的差异:神经网络训练比推理需要更多的计算资源。因此,模型训练通常在比实际设备更强大的硬件上进行,而这些设备用于推理。

此外,TF Lite 具有以下关键特性:

  • 支持多平台和多语言,包括 Android(Java)、iOS(Objective-C 和 Swift)设备、Web(JavaScript)以及其他环境的 Python。谷歌提供了一个名为 MediaPipe Solutions 的 TF Lite 封装 API (developers.google.com/mediapipe, github.com/google/mediapipe/),它取代了之前的 TF Lite API。

  • 性能优化。

  • 它具有端到端的解决方案管道。TF Lite 主要面向实际应用,而非研究。因此,它包含了用于常见机器学习任务的不同管道,如图像分类、物体检测、文本分类和问答等。计算机视觉管道使用了修改版的 EfficientNet 或 MobileNet(第四章),自然语言处理管道则使用基于 BERT 的(第七章)模型。

那么,TF Lite 模型开发是如何工作的呢?首先,我们将通过以下方式选择一个模型:

  • 一个已存在的预训练 .tflite 模型(tfhub.dev/s?deployment-format=lite)。

  • 使用 .tflite 模型和自定义训练数据集。Model Maker 仅适用于 Python。

  • 将一个完整的 TF 模型转换为 .tflite 格式。

TFLite 模型元数据

.tflite 模型可能包含三个组件的可选元数据:

可读部分:为模型提供额外的信息。

输入信息:描述输入数据格式以及必要的预处理步骤。

输出信息:描述输出数据格式以及必要的后处理步骤。

最后两部分可以被代码生成器(例如,Android 代码生成器)利用,以在目标平台上创建现成的模型包装器。

接下来,我们来看如何使用 Model Maker 训练一个 .tflite 模型,然后用它来分类图像。我们只会展示相关的代码部分,但完整的示例可以在本书的 GitHub 仓库中的 Jupyter Notebook 中找到。让我们开始吧:

  1. 首先,我们将创建训练和验证数据集:

    from mediapipe_model_maker import image_classifier
    dataset = image_classifier.Dataset.from_folder(dataset_path)
    train_data, validation_data = dataset.split(0.9)
    

    在这里,dataset_path 是 Flowers 数据集的路径(www.tensorflow.org/datasets/catalog/tf_flowers),该数据集包含了 3,670 张低分辨率的 RGB 花卉图片,分为五个类别(每个类别一个子文件夹)。data.split(0.9) 将数据集(image_classifier.Dataset 实例)拆分为 train_data(90%的图片)和 validation_data(10%的图片)两部分。

  2. 接下来,我们将定义训练超参数——训练三轮,使用 mini-batch 大小为 16,并将训练好的模型导出到 export_dir 文件夹(也可以使用其他参数):

    hparams = image_classifier.HParams(
        export_dir='tflite_model',
        epochs=3,
        batch_size=16)
    
  3. 然后,我们将定义模型参数(我们将使用 EfficientNet):

    options = image_classifier.ImageClassifierOptions(    supported_model=image_classifier.SupportedModels.EFFICIENTNET_LITE4,
        hparams=hparams)
    
  4. 最后,我们将创建一个新模型并开始训练:

    model = image_classifier.ImageClassifier.create(
        train_data=train_data,
        validation_data=validation_data,
        options=options,
    )
    

    这个模型在三轮训练中达到了大约 92% 的准确率。训练过程会创建一个与 TB 兼容的日志文件,因此我们可以通过 TB 跟踪进度(在 Jupyter Notebook 中可用)。

  5. 接下来,我们将导出模型为 .tflite 格式,进入示例的下一个阶段:

    model.export_model('model.tflite')
    
  6. 现在我们有了一个训练好的模型,可以用它来分类图像。我们将使用 MediaPipe Python API(与 Model Maker 不同):

    import mediapipe as mp
    from mediapipe.tasks import python
    from mediapipe.tasks.python import vision
    generic_options = python.BaseOptions(
        model_asset_path='/content/tflite_model/model.tflite')
    cls_options = vision.ImageClassifierOptions(
        base_options=generic_options)
    classifier = vision.ImageClassifier.create_from_options(cls_options)
    

    这里,classifier是预训练模型,generic_options包含.tflite模型的文件路径,而cls_options包含特定于分类的选项(我们使用默认值)。

  7. 我们将加载五张随机的花卉图像(每个花卉类别一张,如labels中所列),并将它们存储在一个名为image_paths的列表中(这里不显示)。我们将对每张图像进行分类,并将其预测标签与真实标签进行比较:

    for image_path, label in zip(image_paths, labels):
      image = mp.Image.create_from_file(image_path)
      result = classifier.classify(image)
      top_1 = result.classifications[0].categories[0]
      print(f'Label: {label}; Prediction: {top_1.category_name}')
    

    可以预见,模型能够正确分类所有图像。

接下来,我们将学习如何使用混合精度计算来优化训练过程。

使用 PyTorch 进行混合精度训练

我们在第八章中讨论了 LLM 的混合精度训练。在这一节中,我们将展示如何在实践中使用 PyTorch 来实现它。我们将再次使用第五章中的转移学习 PyTorch 示例作为实现的基础。所有的代码修改都集中在train_model函数中。这里我们只包括train_model,但完整的示例可以在本书的 GitHub 仓库中的 Jupyter Notebook 中找到。以下是该函数定义的简化版本:

def train_model(model, loss_fn, optimizer, data_loader):
    scaler = torch.cuda.amp.GradScaler()
    for i, (inputs, labels) in enumerate(data_loader):
        optimizer.zero_grad()
        with torch.autocast(
            device_type=device,
            dtype=torch.float16):
            # send the input/labels to the GPU
            inputs = inputs.to(device)
            labels = labels.to(device)
            # forward
            outputs = model(inputs)
            loss = loss_fn(outputs, labels)
        # backward with scaler
        scaler.scale(loss).backward()
        scaler.step(optimizer)
        scaler.update()

我们使用两种独立且不相关的机制来进行混合精度训练:

  • torch.autocast:它充当上下文管理器(或装饰器),允许代码的某个区域在混合精度下运行。device_type指定autocast应用的设备,dtype指定 CUDA 操作使用的数据类型。PyTorch 文档建议仅将前向传播和损失计算封装在torch.autocast中,反向传播操作会自动使用与前向传播相同的数据类型。

  • torch.cuda.amp.GradScaler:当前向传播使用float16精度操作时,反向传播也使用相同的精度进行梯度计算。然而,由于较低的精度,一些梯度值可能会变为零。为了防止这种情况,梯度缩放将神经网络的损失乘以一个缩放因子,并用缩放后的值执行反向传播。反向传播时,梯度流也会按相同的因子进行缩放。通过这种方式,整个反向传播过程使用较大的数值,以防止梯度被清零。在权重更新之前,该机制会反缩放梯度值,以确保权重更新时使用的是实际的梯度值。

这就是我们对模型开发工具的介绍。接下来,我们将讨论一些模型部署机制。

探索模型部署

在这一节中,我们将讨论两个基本的模型部署示例。这些示例将帮助你创建简单但功能完整的概念验证应用程序,用于你的实验。我们开始吧。

使用 Flask 部署神经网络模型

在第一个示例中,我们将结合使用 Google Colab 和prompt参数,生成图像,并将其作为结果返回。

根据其主页,Flask 是一个轻量级的localhost,但我们无法访问它。为了解决这个问题,我们需要flask-ngrokngrok.com/docs/using-ngrok-with/flask/),它将使服务器暴露给外界(你需要一个免费的ngrok注册和认证令牌来运行这个示例)。

为了满足所有依赖项,我们需要安装transformersdiffusersaccelerateflask-ngrok包。让我们开始吧:

  1. 首先,我们将以与第九章中相同的方式初始化 SD HF 管道(sd_pipe):

    import torch
    from diffusers import StableDiffusionPipeline
    sd_pipe = StableDiffusionPipeline.from_pretrained(
        "stabilityai/stable-diffusion-2-1",
        torch_dtype=torch.float16)
    sd_pipe.to('cuda')
    
  2. 接下来,我们将初始化我们的 Flask app

    from flask import Flask
    from flask_ngrok import run_with_ngrok
    app = Flask(__name__)
    run_with_ngrok(app)
    

    这里,run_with_ngrok表示应用将使用ngrok运行,但实际的app尚未启动(这将在本示例的最后进行)。由于我们无法访问 Colab 的localhostngrok将使我们能够通过测试客户端访问它。

  3. 然后,我们将实现我们的text-to-image端点,它将处理作为 Web 请求传入的提示,并基于它们生成图像:

    import io
    from flask import Flask, request, send_file, abort
    @app.route('/text-to-image', methods=['POST', 'GET'])
    def predict():
        if request.method in ('POST', 'GET'):
            prompt = request.get_json().get('prompt')
            if prompt and prompt.strip():
                image = sd_pipe(
                    prompt,
                    num_inference_steps=100).images[0]
                image_io = io.BytesIO()
                image.save(image_io, format='PNG')
                image_io.seek(0)
                return send_file(
                    image_io,
                    as_attachment=False,
                    mimetype='image/png'
                )
            else:
                abort(500, description='Invalid prompt')
    

    该端点的名称是/text-to-image,它将处理POSTGET请求(处理流程是相同的)。该函数将解析文本的prompt参数,并将其传递给sd_pipe以生成image参数(与第九章示例中的方式相同)。最后,send_file函数将把image的结果返回给客户端。

  4. 我们现在可以通过app.run()命令启动 Flask 应用。它将初始化 Flask 开发服务器,使我们的端点准备好处理请求。此外,ngrok将通过RANDOM-SEQUENCE.ngrok.io/类型的 URL 将应用暴露给外界。

  5. 我们可以使用这个 URL 发起对text-to-image端点的测试请求(这在 Colab 笔记本外进行):

    import requests
    response = requests.post(
        url='http://RANDOM-SEQUENCE.ngrok.io/text-to-image',
        json={'prompt': 'High quality photo of a racing car on a track'})
    
  6. 我们可以使用以下代码来显示图像:

    from PIL import Image
    import io
    image = Image.open(io.BytesIO(response.content))
    image.show()
    

这就结束了我们的 REST API 示例。接下来,我们将在 Web 环境中部署一个带有 UI 的模型。

使用 Gradio 构建机器学习 Web 应用

Gradio(www.gradio.app/)是一个开源的 Python 库,允许我们为 ML 模型构建互动式 Web 演示。HF Spaces(huggingface.co/spaces)支持托管 Gradio 应用。因此,我们可以在 HF 基础设施上构建一个 Gradio 应用,它不仅包括托管,还可以访问所有可用的 HF 模型(huggingface.co/models)。

我们可以在 huggingface.co/new-space 创建一个 HF 空间。这个空间有一个名字(它也将成为其 URL)、一个许可证和一个 SDK。在写作时,HF Spaces 支持基于 Streamlit 的(streamlit.io/)、基于 Gradio 的和静态实例。然而,你也可以部署自定义的 Docker 容器以获得更多灵活性。

每个新的 HF 空间都有一个关联的 Git 仓库。例如,本文示例的空间位于 huggingface.co/spaces/ivan-vasilev/gradio-demo,这也是其相应 Git 仓库的 URL。基于 Gradio 的空间期望在其根目录中有一个名为 app.py 的 Python 模块(在我们的例子中,整个示例将驻留在 app.py 中)和一个 requirements.txt 文件。每次你推送更改到仓库时,应用程序将自动接收这些更改并重新启动。

注意

若要复制此示例,你需要一个 HF 账户。HF Spaces 提供不同的硬件等级。基本版是免费的,但此特定示例需要启用 GPU 的等级,这会按小时收费。因此,如果你想运行此示例,可以将其复制到自己的账户中并启用 GPU 等级。

Gradio 从一个名为 gradio.Interface 的中央高级类开始。其构造函数接受三个主要参数:

  • fn:主函数,将处理输入并返回输出。

  • inputs:一个或多个 Gradio 输入组件。这些可以是文本输入、文件上传或组合框等。你可以将组件指定为类实例或通过其字符串标签。输入的数量应与 fn 参数的数量匹配。

  • outputs:一个或多个 Gradio 组件,表示 fn 执行结果。输出的数量应与 fn 返回的值的数量匹配。

Gradio 将根据 inputoutput 参数自动实例化并排列 UI 组件。

接下来,我们将实现我们的示例。我们将使用与 使用 Flask 部署神经网络模型 部分相同的文本到图像的 SD 场景。为了避免重复,我们假设 sd_pipe 流水线已被初始化。现在开始:

  1. 首先,我们将实现 generate_image 函数,该函数使用 promptinf_steps 步骤内合成一张图像:

    def generate_image(
            prompt: str,
            inf_steps: int = 100):
        return sd_pipe(
            prompt=prompt,
            num_inference_steps=inf_steps).images[0]
    
  2. 接下来,我们将初始化 gradio.Interface 类:

    import gradio as gr
    interface = gr.Interface(
        fn=generate_image,
        inputs=[
            gr.components.Textbox(label='Prompt'),
            gr.components.Slider(
                minimum=0,
                maximum=100,
                label='Inference Steps')],
        outputs=gr.components.Image(),
        title='Stable Diffusion',
    )
    

    如我们所讨论的,inputsoutputs gr.Interface 参数与 generate_image 函数的输入/输出签名相匹配。

  3. 最后,我们可以使用 interface.launch() 命令运行应用程序。以下是该应用程序响应式 UI 的样子:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/py-dl-3e/img/B19627_10_4.jpg

图 10.4 – SD Gradio 应用程序的响应式 UI,托管在 HF Spaces 上。上方:输入组件;下方:生成的图像

这部分内容总结了我们对 Gradio 和模型部署的介绍。

总结

在这一章中,我们概述了机器学习开发生命周期的三个主要组成部分——训练数据集的创建、模型开发和模型部署。我们主要关注了后两者,从开发开始。首先,我们讨论了基础神经网络框架的流行。接着,我们聚焦于几个模型开发主题——ONNX 通用模型表示格式、TB 监控平台、TF Lite 移动开发库,以及混合精度的 PyTorch 训练。然后,我们讨论了两种基本的模型部署场景——作为 Flask 应用的 REST 服务和使用 Gradio 的交互式 Web 应用。

这章以及本书到此结束。希望你享受这段旅程!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值