PyTorch 生成式人工智能学习指南(一)

如何用Python打造一个生成式AI驱动的未来应用 生成式AI(Generative AI)作为人工智能的重要分支,正在塑造未来技术的格局。它不仅可以生成高质量的图像、文本和音频,还能模拟复杂的系统和环境,为各行业带来创新的解决方案。Python,以其简洁的语法和强大的生态系统,成为开发生成式AI应用的首选语言。本文将详细介绍如何用Python打造一个生成式AI驱动的未来应用,从理论基础、工具选择、开发流程到实际案例分析。 阅读详情

原文:zh.annas-archive.org/md5/ea442d0490aad3d193551ec97eaf414e

译者:飞龙

协议:CC BY-NC-SA 4.0

第一部分:生成式 AI 简介

什么是生成式 AI?它与非生成式对应物,即判别模型有何不同?为什么我们在这本书中选择 PyTorch 作为 AI 框架?

在本部分中,我们回答这些问题。此外,本书中所有生成式 AI 模型都是深度神经网络。因此,你将学习如何使用 PyTorch 创建深度神经网络以执行二分类和多分类,以便你能够熟练掌握深度学习和分类任务。目的是让你为即将到来的章节做好准备,在这些章节中,你将使用 PyTorch 中的深度神经网络创建各种生成模型。你还将学习如何使用 PyTorch 构建和训练生成对抗网络以生成形状和数字序列。

第一章:什么是生成式 AI,为什么是 PyTorch?

本章涵盖

  • 生成式 AI 与非生成式 AI 的比较

  • 为什么 PyTorch 是深度学习和生成式 AI 的理想选择

  • 生成对抗网络的概念

  • 注意力机制和 Transformers 的好处

  • 从头创建生成式 AI 模型的优势

生成式 AI 自 2022 年 11 月 ChatGPT 问世以来,对全球格局产生了重大影响,引起了广泛关注,并成为焦点。这项技术进步彻底改变了日常生活的许多方面,迎来了技术新时代,并激发了许多初创公司探索各种生成模型提供的广泛可能性。

考虑一下 Midjourney 这家先驱公司取得的进步,它现在可以从简短的文字输入中创建高分辨率、逼真的图像。同样,软件公司 Freshworks 显著加速了应用程序的开发,将平均所需时间从 10 周缩短到仅仅几天,这是通过 ChatGPT 的能力实现的(参见Forbes文章“2023 年公司如何使用 ChatGPT 的 10 个惊人的真实世界例子”,作者 Bernard Barr,2023 年,mng.bz/Bgx0)。为了举例说明,这篇引言的一些内容已经通过生成式 AI 得到了增强,展示了其精炼内容以使其更具吸引力的能力。

注意事项:有什么比让生成式 AI 自己解释生成式 AI 更好的方法呢?在最终确定之前,我让 ChatGPT 重新撰写了这篇引言的早期草稿,以“更具吸引力”的方式呈现。

这项技术进步的影响远远超出了这些例子。由于生成式 AI 的先进能力,各行业正经历着重大的颠覆。这项技术现在可以创作出与人类写作相当的文章,创作出令人联想到古典作品的音乐,并快速生成复杂的法律文件,这些任务通常需要大量的人类努力和时间。ChatGPT 发布后,教育平台 CheggMate 的股价出现了显著下降。此外,美国编剧工会最近的一次罢工中,达成了一致意见,要为 AI 在剧本写作和编辑方面的侵犯设定界限(参见Wired文章“好莱坞编剧达成 AI 协议,将改写历史”,作者 Will Bedingfield,2023 年,mng.bz/1ajj)。

注意事项:CheggMate 向大学生收费,让他们的问题由人类专家回答。现在,许多这些工作可以通过 ChatGPT 或类似工具以极低成本完成。

这引发了一系列问题:什么是生成式 AI,它与其他 AI 技术有何不同?为什么它在各个行业引起了如此广泛的颠覆?生成式 AI 的潜在机制是什么,为什么了解它很重要?

本书深入探讨了生成式人工智能,这是一种通过从现有数据中学习模式来创建新内容(如文本、图像或音乐)的人工智能类型。它与专注于区分不同数据实例之间的差异并学习类别之间边界的判别模型不同。图 1.1 展示了这两种建模方法之间的差异。例如,当面对包含狗和猫的图像数组时,判别模型通过捕捉区分两者的几个关键特征(例如,猫有小的鼻子和尖耳朵)来确定每张图像描绘的是狗还是猫。如图表的上半部分所示,判别模型将数据作为输入,并产生不同标签的概率,我们用 Prob(狗)和 Prob(猫)表示。然后我们可以根据最高的预测概率对输入进行标记。

所有这些模型都是基于深度神经网络,你将使用 Python 和 PyTorch 来构建、训练和使用这些模型。我们选择 Python 是因为其用户友好的语法、跨平台兼容性和广泛的社区支持。我们还选择了 PyTorch 而不是 TensorFlow 等其他框架,因为它易于使用并且能够适应各种模型架构。Python 被广泛认为是机器学习(ML)的主要工具,PyTorch 在人工智能领域也越来越受欢迎。因此,使用 Python 和 PyTorch 可以使你跟上生成式人工智能的新发展。因为 PyTorch 允许使用图形处理单元(GPU)进行训练加速,所以你可以在几分钟或几小时内训练这些模型,并见证生成式人工智能的实际应用!

1.1 介绍生成式人工智能和 PyTorch

本节解释了什么是生成式人工智能以及它与非生成式人工智能(如判别模型)的不同之处。生成式人工智能是一类具有非凡能力,能够产生各种形式的新内容的技术,包括文本、图像、音频、视频、源代码和复杂模式。生成式人工智能能够创造全新的内容世界;ChatGPT 是一个显著的例子。相比之下,判别建模主要关注的是识别和分类现有内容的工作。

1.1.1 什么是生成式人工智能?

生成式人工智能是一种通过从现有数据中学习模式来创建新内容(如文本、图像或音乐)的人工智能类型。它与专注于区分不同数据实例之间的差异并学习类别之间边界的判别模型不同。图 1.1 展示了这两种建模方法之间的差异。例如,当面对包含狗和猫的图像数组时,判别模型通过捕捉区分两者的几个关键特征(例如,猫有小的鼻子和尖耳朵)来确定每张图像描绘的是狗还是猫。如图表的上半部分所示,判别模型将数据作为输入,并产生不同标签的概率,我们用 Prob(狗)和 Prob(猫)表示。然后我们可以根据最高的预测概率对输入进行标记。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH01_F01_Liu.png

图 1.1 生成模型与判别模型的比较。判别模型(图的上半部分)将数据作为输入,并产生不同标签的概率,我们用 Prob(dog)和 Prob(cat)表示。相反,生成模型(图的下半部分)通过深入了解这些图像的显著特征来合成代表狗和猫的新图像。

相比之下,生成模型展现出生成数据新实例的独特能力。在我们的狗和猫示例中,生成模型通过深入了解这些图像的显著特征来合成代表狗和猫的新图像。如图 1.1 的下半部分所示,生成模型将任务描述(例如,在潜在空间中变化的值导致生成的图像具有不同的特征,我们将在第四章到第六章中详细讨论)作为输入,并产生狗和猫的全新图像。

从统计学的角度来看,当面对具有特征 X 的数据示例,这些特征描述了输入和相应的各种标签 Y 时,判别模型承担预测条件概率的责任,具体来说是预测 Y|X 的概率。相反,生成模型试图学习输入特征 X 和目标变量 Y 的联合概率分布,表示为 prob(X, Y)。凭借这种知识,它们从分布中采样,以产生 X 的新实例。

根据你想要创建的具体内容形式,存在不同类型的生成模型。在这本书中,我们主要关注两种突出的技术:生成对抗网络(GANs)和转换器(虽然我们也会涵盖变分自编码器和扩散模型)。在 GANs 中的“对抗”一词指的是两个神经网络在零和博弈框架中相互竞争的事实:生成网络试图创建与真实样本不可区分的数据实例,而判别网络则试图从真实样本中识别生成的样本。两个网络之间的竞争导致两者都得到改进,最终使生成器能够创建高度逼真的数据。转换器是能够高效解决序列到序列预测任务的深度神经网络,我们将在本章后面更详细地解释它们。

GANs 因其易于实现和多功能性而备受赞誉,使连对深度学习只有初步了解的人也能从头开始构建他们的生成模型。这些多才多艺的模型可以产生各种各样的创作,从本书第三章中展示的几何形状和复杂图案,到第四章中将学习生成的高质量彩色图像,如人类面孔。此外,GANs 还表现出转换图像内容的能力,无缝地将金发人类面孔图像转变为黑发人类面孔图像,这在第六章中进行了讨论。值得注意的是,它们将它们的创造力扩展到音乐生成领域,产生听起来逼真的音乐作品,如第十三章中所示。

与形状、数量或图像生成相比,文本生成的艺术面临巨大的挑战,这主要归因于文本信息的序列性质,其中单个字符和单词的顺序和排列具有重大意义。为了应对这种复杂性,我们转向 Transformer,这是一种专为高效处理序列到序列预测任务而设计的深度神经网络。与它们的 predecessors,如循环神经网络(RNN)或卷积神经网络(CNN)不同,Transformer 在捕捉输入和输出序列中固有的复杂、长距离依赖关系方面表现出色。值得注意的是,它们并行训练的能力(一种在多个设备上同时训练模型的多设备训练方法)大大减少了训练时间,使我们能够在大量数据上训练 Transformer。

Transformer 的革命性架构是大型语言模型(LLMs;具有大量参数并在大型数据集上训练的深度神经网络)的出现的基础,包括 ChatGPT、BERT、DALL-E 和 T5。这种变革性的架构是近年来 AI 进步激增的基石,ChatGPT 和其他生成预训练 Transformer(GPT)模型的引入开启了这一进步。

在接下来的章节中,我们将深入了解这两种开创性技术的全面内部工作原理:它们的底层机制和它们解锁的众多可能性。

1.1.2 Python 编程语言

我假设您已经具备 Python 的基本知识。为了跟随书中的内容,您需要了解 Python 的基础知识,例如函数、类、列表、字典等。如果您还没有,网上有大量的免费资源可以帮助您入门。按照附录 A 中的说明安装 Python。之后,为本书创建一个虚拟环境,并安装 Jupyter Notebook 作为本书项目中计算环境。

自 2018 年后期以来,Python 已经成为全球领先的编程语言,正如《经济学人》杂志所记录的(参见《经济学人》数据团队撰写的文章“Python Is Becoming the World’s Most Popular Coding Language”,2018 年,mng.bz/2gj0)。Python 不仅对每个人都是免费的,还允许其他用户创建和调整库。Python 有一个庞大的社区驱动的生态系统,因此你可以轻松找到来自其他 Python 爱好者的资源和帮助。此外,Python 程序员喜欢分享他们的代码,所以你不必重新发明轮子,你可以导入现成的库,并与 Python 社区分享你的代码。

无论你是在 Windows、Mac 还是 Linux 上,Python 都能满足你的需求。它是一种跨平台语言,尽管安装软件和库的过程可能因操作系统而异——但不用担心;我会在附录 A 中向你展示如何操作。一旦一切准备就绪,Python 代码在不同系统上表现一致。

Python 是一种适合通用应用开发的表达性语言。它的语法易于理解,使得 AI 爱好者能够轻松理解和操作。如果你在这本书中提到的 Python 库遇到任何问题,你可以在 Python 论坛上搜索或访问像 Stack Overflow(stackoverflow.com/questions/tagged/python)这样的网站寻找答案。如果所有其他方法都失败了,请不要犹豫,向我寻求帮助。

最后,Python 提供了一大批库,使得创建生成模型变得容易(相对于 C++或 R 等其他语言)。在这个旅程中,我们将独家使用 PyTorch 作为我们的 AI 框架,我将在稍后解释为什么我们选择它而不是像 TensorFlow 这样的竞争对手。

1.1.3 使用 PyTorch 作为我们的 AI 框架

现在我们已经决定将 Python 作为本书的编程语言,我们将选择一个适合生成建模的 AI 框架。在 Python 中,最受欢迎的两个 AI 框架是 PyTorch 和 TensorFlow。在这本书中,我们选择 PyTorch 而不是 TensorFlow,因为它易于使用,并且我强烈建议你也这样做。

PyTorch 是由 Meta 的 AI 研究实验室开发的开源 ML 库。它建立在 Python 编程语言和 Torch 库的基础上,旨在提供一个灵活且直观的平台,用于创建和训练深度学习模型。Torch 是 PyTorch 的前身,是一个用 C 语言构建深度神经网络并带有 Lua 包装器的 ML 库,但它的开发已经停止。PyTorch 旨在通过提供一个更用户友好和适应性强的框架来满足研究人员和开发者的需求。

计算图是深度学习中的一个基本概念,在高效计算复杂数学运算中扮演着至关重要的角色,尤其是涉及多维数组或张量的运算。计算图是一种有向图,其中的节点代表数学运算,而边则代表在这些运算之间流动的数据。计算图的关键用途之一是在实现反向传播和梯度下降算法时计算偏导数。图结构允许高效地计算在训练过程中更新模型参数所需的梯度。PyTorch 能够动态创建和修改图,这被称为动态计算图。这使得它能够更好地适应不断变化的结构模型,并简化了调试过程。此外,就像 TensorFlow 一样,PyTorch 通过 GPU 训练提供加速计算,与中央处理器(CPU)训练相比,可以显著减少训练时间。

PyTorch 的设计与 Python 编程语言相得益彰。其语法简洁易懂,使得新手和经验丰富的开发者都能轻松上手。研究人员和开发者都欣赏 PyTorch 的灵活性。它使他们能够快速实验新想法,这得益于其动态计算图和简单的接口。这种灵活性在快速发展的生成人工智能领域至关重要。PyTorch 还拥有一个快速发展的社区,积极为其发展做出贡献。这导致了一个庞大的生态系统,包括库、工具和资源,为开发者提供支持。

PyTorch 在迁移学习方面表现出色,这是一种将针对通用任务设计的预训练模型微调以适应特定任务的技术。研究人员和实践者可以轻松利用预训练模型,节省时间和计算资源。这一特性在预训练大型语言模型(LLMs)的时代尤为重要,它使我们能够将 LLMs 应用于下游任务,如分类、文本摘要和文本生成。

PyTorch 与其它 Python 库兼容,如 NumPy 和 Matplotlib。这种互操作性允许数据科学家和工程师无缝地将 PyTorch 集成到现有的工作流程中,提高生产力。PyTorch 还以其对社区驱动开发的承诺而闻名。它发展迅速,定期根据实际使用和用户反馈进行更新和改进,确保其始终处于人工智能研究和开发的前沿。

附录 A 提供了如何在您的计算机上安装 PyTorch 的详细说明。按照说明在本书的虚拟环境中安装 PyTorch。如果您计算机上没有安装支持 Compute Unified Device Architecture (CUDA)的 GPU,本书中的所有程序也兼容 CPU 训练。更好的是,我将在本书的 GitHub 仓库github.com/markhliu/DGAI上提供训练好的模型,以便您可以看到训练好的模型在实际中的应用(如果训练模型太大,我将在我的个人网站上提供gattonweb.uky.edu/faculty/lium/))。在第二章中,您将深入探索 PyTorch。您首先将学习 PyTorch 中的数据结构,Tensor,它包含数字和矩阵,并提供执行操作的功能。然后,您将学习如何使用 PyTorch 执行端到端的深度学习项目。具体来说,您将在 PyTorch 中创建一个神经网络,并使用服装物品图像及其相应的标签来训练网络。完成之后,您将使用训练好的模型将服装物品分类到 10 种不同的标签类型中。这个项目将使您为在后续章节中使用 PyTorch 构建和训练各种生成模型做好准备。

1.2 GANs

本节首先概述 GANs 的工作原理。然后,我们以生成动漫人脸图像为例,向您展示 GANs 的内部工作原理。最后,我们将讨论 GANs 的实际应用。

1.2.1 GANs 的高级概述

GANs 代表一类生成模型,最初由 Ian Goodfellow 及其合作者于 2014 年提出(“Generative Adversarial Nets,” arxiv.org/abs/1406.2661)。近年来,GANs 因其易于构建和训练,并且能够生成各种内容而变得极为流行。您将从下一小节中的说明示例中看到,GANs 采用一个双网络架构,包括一个生成模型,其任务是捕捉潜在的数据分布以生成内容,以及一个判别模型,其作用是估计给定样本是否来自真实的训练数据集(被认为是“真实”)而不是生成模型的产物(被认为是“虚假”)。模型的主要目标是产生与训练数据集中的数据实例非常相似的新数据实例。GANs 生成数据的性质取决于训练数据集的组成。例如,如果训练数据由服装物品的灰度图像组成,合成的图像将非常类似于这样的服装物品。相反,如果训练数据集包含人类面部彩色图像,生成的图像也将类似于人类面部。

看一下图 1.2——我们 GAN 的架构及其组件。为了训练模型,我们将真实样本(如图 1.2 顶部所示)和生成器创建的虚假样本(左侧)都展示给判别器(中间)。生成器的主要目的是创建与训练数据集中找到的示例几乎无法区分的数据实例。相反,判别器努力区分生成器生成的虚假样本和真实样本。这两个网络进行着一种持续的竞争过程,类似于猫捉老鼠的游戏,试图迭代地超越对方。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH01_F02_Liu.png

图 1.2 GAN 架构及其组件。GAN 采用双网络架构,包括一个生成模型(左侧),其任务是捕捉潜在的数据分布,以及一个判别模型(中间),其目的是估计一个给定的样本是来自真实的训练数据集(被认为是“真实”)而不是生成模型的产物(被认为是“虚假”)的可能性。

GAN 模型的训练过程涉及多个迭代。在每个迭代中,生成器接受某种形式的任务描述(步骤 1)并使用它来创建虚假图像(步骤 2)。虚假图像以及来自训练集的真实图像被展示给判别器(步骤 3)。判别器试图将每个样本分类为真实或虚假。然后,它将分类与实际标签、真实情况(步骤 4)进行比较。判别器和生成器都从分类中接收反馈(步骤 5),并提高它们的能力:判别器适应其识别虚假样本的能力,而生成器学习提高其生成能够欺骗判别器的令人信服样本的能力。随着训练的进行,当两个网络都无法进一步改进时,达到平衡。此时,生成器能够产生与真实样本几乎无法区分的数据实例。

为了确切了解 GAN 是如何工作的,让我们来看一个说明性例子。

1.2.2 一个说明性例子:生成动漫面孔

想象一下:你是一个热衷的动漫爱好者,你正在使用一种称为深度卷积 GAN(简称 DCGAN)的强大工具进行一次激动人心的探索,以创建你自己的动漫面孔。别担心,我们将在第四章中深入探讨这一点。

如果你观察图 1.2 的顶部中间部分,你会看到一个写着“实像”的图片。我们将使用 63,632 张动漫面孔的彩色图片作为我们的训练数据集。如果你翻到图 1.3,你会看到我们训练集中的 32 个示例。这些特殊图片在形成我们判别网络一半输入方面起着至关重要的作用。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH01_F03_Liu.png

图 1.3 动漫面孔训练数据集示例

图 1.2 的左侧是生成器网络。为了每次生成不同的图像,生成器从潜在空间中输入一个向量 Z。我们可以将这个向量视为“任务描述”。在训练过程中,我们从潜在空间中抽取不同的 Z 向量,因此网络每次都会生成不同的图像。这些假图像是鉴别器网络输入的一半。

注意:通过改变向量 Z 的值,我们可以生成不同的输出。在第五章,你将学习如何选择向量 Z 来生成具有特定特征(例如,男性或女性特征)的图像。

但这里有个转折:在我们教我们的两个网络创作和检测的艺术之前,生成器产生的图像,嗯,简直是胡言乱语!它们看起来根本不像图 1.3 中你看到的那些逼真的动漫面孔。事实上,它们看起来就像电视屏幕上的静态画面(你将在第四章亲自见证这一点)。

我们对模型进行了多次迭代训练。在每次迭代中,我们向鉴别器展示一组由生成器创建的图像,以及一组来自我们的训练集的动漫面孔图像。我们要求鉴别器预测每张图像是由生成器(伪造)还是来自训练集(真实)创建的。

你可能会想:鉴别器和生成器在每次训练迭代中是如何学习的?一旦做出预测,鉴别器不会坐以待毙;它会从每个图像的预测错误中学习。有了这种新获得的知识,它调整自己的参数,以在下一轮中做出更好的预测。生成器也不是闲着的。它从自己的图像生成过程和鉴别器的预测结果中汲取经验。有了这些知识在手,它调整自己的网络参数,力求在下一轮迭代中创造越来越逼真的图像。目标?降低鉴别器发现其伪造品的几率。

随着我们穿越这些迭代,一个显著的变化发生了。生成器网络在进化,产生的动漫面孔越来越逼真,类似于我们的训练集合中的那些。同时,鉴别器网络也在磨练自己的技能,成为在识别伪造品方面的老练侦探。这是创作与检测之间的一场迷人的舞蹈。

渐渐地,一个神奇的时刻到来了。达到了一种平衡,或者说是一种完美的平衡。生成器创造的图像变得如此逼真,以至于它们与我们在训练档案中的真实动漫面孔无法区分。在这个时候,鉴别器如此困惑,以至于它将每个图像的真实性赋予 50%的机会,无论它是来自我们的训练集还是由生成器制作的。

最后,请看一些生成器艺术作品的例子,如图 1.4 所示:它们看起来确实与我们的训练集中的那些无法区分。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH01_F04_Liu.png

图 1.4 DCGAN 训练好的生成器生成的动漫人脸图像

1.2.3 你为什么应该关注 GANs?

GANs 易于实现且用途广泛:仅在本书中,你将学会生成几何形状、复杂图案、高分辨率图像和听起来逼真的音乐。

GANs 的实用用途不仅限于生成逼真的数据。GANs 还可以将一个图像域的属性转换到另一个域。正如你在第六章中将会看到的,你可以训练一个 CycleGAN(GAN 家族中的一种生成模型)将人脸图像中的金色头发转换为黑色头发。同样的训练模型也可以将黑色头发转换为金色头发。图 1.5 显示了四行图像。第一行是带有金色头发的原始图像。训练好的 CycleGAN 将它们转换为带有黑色头发的图像(第二行)。最后两行是带有黑色头发的原始图像和分别转换为金色头发的转换图像。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH01_F05_Liu.png

图 1.5 使用 CycleGAN 改变发色。如果我们将带有金色头发的图像(第一行)输入到一个训练好的 CycleGAN 模型中,该模型将这些图像中的金色头发转换为黑色头发(第二行)。同样的训练模型也可以将黑色头发(第三行)转换为金色头发(底部行)。

想想你在训练 GANs 时会掌握的所有令人惊叹的技能——它们不仅酷炫,而且超级实用!假设你经营一家采用“按需定制”策略的在线服装店(这允许用户在制造前定制他们的购买)。你的网站展示了大量独特的款式供客户选择,但这里有个问题:你只有在有人下单时才会制作衣服。由于你必须制作商品并拍照,制作这些衣服的高质量图像可能相当昂贵。

GANs 来拯救!你不需要大量的制造服装物品及其图像;相反,你可以使用类似 CycleGAN 的东西将一组图像的特征转换到另一组,从而创建一系列全新的风格。这只是使用 GANs 的一种巧妙方式。可能性是无限的,因为这些模型非常灵活,可以处理各种类型的数据——使它们在实用应用中成为变革者。

1.3 变换器

变换器是擅长序列到序列预测问题的深度神经网络,例如,输入一个句子并预测最可能的下一个单词。本节将向您介绍 Transformers 的关键创新:自注意力机制。然后我们将讨论 Transformer 架构和不同类型的 Transformers。最后,我们将讨论 Transformers 的一些最新发展,如多模态模型(输入不仅包括文本,还包括其他数据类型,如音频和图像的 Transformers)和预训练的 LLMs(在大规模文本数据上训练的模型,可以执行各种下游任务)。

在 2017 年由一组谷歌研究人员(Vaswani 等人,“Attention Is All You Need”,arxiv.org/abs/1706.03762)发明 Transformer 架构之前,自然语言处理(NLP)和其他序列到序列预测任务主要是由循环神经网络(RNNs)处理的。然而,RNNs 在保留序列中早期元素的信息方面存在困难,这阻碍了它们捕捉长期依赖关系的能力。即使是像长短期记忆(LSTM)网络这样的高级 RNN 变体,虽然可以处理较长的依赖关系,但在处理极长范围依赖关系时也显得不足。

更重要的是,RNNs(包括 LSTMs)按顺序处理输入,这意味着这些模型一次处理一个元素,按顺序处理,而不是同时查看整个序列。RNNs 沿着输入和输出序列的符号位置进行计算的事实阻止了并行训练,这使得训练速度变慢。这反过来又使得在大型数据集上训练模型成为不可能。

Transformers 的关键创新是自注意力机制,它擅长捕捉序列中的长期依赖关系。此外,由于模型中对输入的处理不是按顺序进行的,因此 Transformers 可以并行训练,这大大减少了训练时间。更重要的是,并行训练使得在大量数据上训练 Transformers 成为可能,这使得大型语言模型(LLMs)变得智能和知识渊博(基于它们处理和生成类似人类文本、理解上下文以及执行各种语言任务的能力)。这导致了 ChatGPT 等 LLMs 的兴起以及最近的 AI 繁荣。

1.3.1 注意力机制

注意力机制为序列中一个元素与序列中所有元素(包括该元素本身)的关系分配权重。权重越高,两个元素之间的关系越紧密。这些权重是在训练过程中从大量训练数据中学习得到的。因此,经过训练的 LLM,如 ChatGPT,可以找出句子中任意两个单词之间的关系,从而理解人类语言。

你可能会想:注意力机制是如何为序列中的元素分配分数以捕获长期依赖关系的?首先通过三个神经网络层处理输入以获得查询 Q、键 K 和值 V(我们将在第九章中详细解释)。使用查询、键和值来计算注意力的方法来自检索系统。例如,你可能会去公共图书馆查找一本书。你可以在图书馆的搜索引擎中输入,比如,“金融领域的机器学习”。在这种情况下,查询 Q 是“金融领域的机器学习”。键 K 是书名、书籍描述等。图书馆的检索系统将根据查询和键之间的相似性推荐一系列书籍(值 V)。自然地,标题或描述中包含“机器学习”或“金融”或两者都的书籍会出现在列表的顶部,而标题或描述中都不包含这两个短语的书籍会出现在列表的底部,因为这些书籍会被分配一个较低的匹配分数。

在第九章和第十章中,你将了解注意力机制的细节——更好的是,你将从头开始实现注意力机制以构建和训练一个 Transformer,以成功地将英语翻译成法语。

1.3.2 Transformer 架构

Transformer 首次在为机器语言翻译(例如,英语到德语或英语到法语)设计模型时被提出。图 1.6 是 Transformer 架构的示意图。左侧是编码器,右侧是解码器。在第九章和第十章中,你将学习从头开始构建 Transformer 以训练模型将英语翻译成法语,那时我们将更详细地解释图 1.6。

Transformer 中的编码器“学习”输入序列(例如,英语短语“你好吗?”)的含义,并将其转换为表示这种含义的向量,然后再将这些向量传递给解码器。解码器通过逐个预测序列中的单词,基于序列中的先前单词和编码器的输出来构建输出(例如,英语短语的法语翻译)。经过训练的模型可以将常见的英语短语翻译成法语。

变压器有三种类型:仅编码器 Transformer、仅解码器 Transformer 和编码器-解码器 Transformer。仅编码器 Transformer 没有解码器,能够将序列转换为用于各种下游任务(如情感分析、命名实体识别和文本生成)的抽象表示。例如,BERT 就是一个仅编码器 Transformer。仅解码器 Transformer 只有解码器但没有编码器,非常适合文本生成、语言建模和创意写作。GPT-2(ChatGPT 的前身)和 ChatGPT 都是仅解码器 Transformer。在第十一章中,你将学习从头开始创建 GPT-2,然后从 Hugging Face(一个托管和协作机器学习模型、数据集和应用的 AI 社区)中提取训练好的模型权重。你将把权重加载到你的 GPT-2 模型中,并开始生成连贯的文本。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH01_F06_Liu.png

图 1.6 Transformer 架构。Transformer 中的编码器(图示的左侧)学习输入序列(例如,英语短语“你好吗?”)的含义,并将其转换为捕获其含义的抽象表示,然后再传递给解码器(图示的右侧)。解码器通过逐个预测单词,基于序列中的先前单词和编码器提供的抽象表示来构建输出(例如,英语短语的法语翻译)。

对于复杂任务,如能够处理文本到图像生成或语音识别的多模态模型,需要编码器-解码器 Transformer。编码器-解码器 Transformer 结合了编码器和解码器的优点。编码器在处理和理解输入数据方面效率高,而解码器在生成输出方面表现卓越。这种组合使得模型能够有效地理解复杂的输入(如文本或语音)并生成复杂的输出(如图像或转录文本)。

1.3.3 多模态 Transformer 和预训练的 LLM

生成式 AI 最近的进展催生了各种多模态模型:这些模型不仅可以使用文本,还可以使用其他数据类型作为输入,例如音频和图像。文本到图像 Transformer 就是这样的一个例子。DALL-E 2、Imagen 和 Stable Diffusion 都是文本到图像模型,它们因其能够从文本提示中生成高分辨率图像的能力而受到媒体广泛关注。文本到图像 Transformer 结合了扩散模型的原则,涉及一系列转换来逐步增加数据的复杂性。因此,在讨论文本到图像 Transformer 之前,我们首先需要了解扩散模型。

想象一下,你想要通过使用基于扩散的模型生成高分辨率的鲜花图像。首先,你需要获取一组高质量的鲜花图像作为训练集。然后,你让模型逐渐向鲜花图像中添加噪声(所谓的扩散过程),直到它们变成完全随机的噪声。接着,训练模型从这些噪声图像中逐步去除噪声,以生成新的数据样本。扩散过程在图 1.7 中展示。左侧列包含四张原始的鲜花图像。随着我们向右移动,每一步都会向图像中添加一些噪声,直到右侧列,四张图像变成了纯随机噪声。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH01_F07_Liu.png

图 1.7 扩散模型向图像中添加越来越多的噪声,并学习重建它们。左侧列包含四张原始的鲜花图像。随着我们向右移动,每个时间步都会向图像中添加一些噪声,直到右侧列,四张图像变成了纯随机噪声。然后,我们使用这些图像来训练一个基于扩散的模型,逐步从噪声图像中去除噪声,以生成新的数据样本。

你可能会想知道:文本到图像的 Transformer 与扩散模型有何关联?文本到图像的 Transformer 接受一个文本提示作为输入,并生成与该文本描述相对应的图像。文本提示作为一种条件,模型使用一系列神经网络层将文本描述转换为图像。与扩散模型一样,文本到图像的 Transformer 使用具有多个层的分层架构,每个层逐步向生成的图像添加更多细节。迭代优化输出的核心概念在扩散模型和文本到图像的 Transformer 中是相似的,我们将在第十五章中解释这一点。

由于扩散模型能够提供稳定的训练并生成高质量的图像,它们现在变得越来越受欢迎,并且已经超越了其他生成模型,如 GANs 和变分自编码器。在第十五章中,你将首先学习使用牛津鲜花数据集训练一个简单的扩散模型。你还将学习多模态 Transformer 背后的基本思想,并编写一个 Python 程序,通过文本提示请求 OpenAI 的 DALL-E 2 生成图像。例如,当我输入“一个宇航员穿着太空服骑独角兽”作为提示时,DALL-E 2 生成了图 1.8 所示的图像。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH01_F08_Liu.png

图 1.8 通过文本提示“一个宇航员穿着太空服骑独角兽”生成的 DALL-E 2 图像

在第十六章中,你将学习如何访问预训练的大型语言模型(LLM),例如 ChatGPT、GPT4 和 DALL-E 2。这些模型在大量文本数据上进行了训练,并从数据中学习了通用知识。因此,它们可以执行各种下游任务,如文本生成、情感分析、问答和命名实体识别。由于预训练的 LLM 是在几个月前训练的,它们无法提供过去一两个月内的事件和发展的信息,更不用说实时信息,如天气状况、航班状态或股价。我们将使用 LangChain(一个用于构建 LLM 应用的 Python 库,提供提示管理、LLM 链式调用和输出解析的工具)将 LLM 与 Wolfram Alpha 和 Wikipedia API 链式调用,以创建一个无所不知的个人助理。

1.4 为什么需要从头开始构建生成模型?

本书的目标是向你展示如何从头开始构建和训练所有生成模型。这样,你将彻底理解这些模型的内部运作,并能更好地利用它们。从头开始创造是理解它的最佳方式。你将为 GANs 实现这一目标:所有模型,包括 DCGAN 和 CycleGAN,都是从头开始构建并使用公共领域精心整理的数据进行训练的。

对于 Transformers,你将从头开始构建和训练所有模型,除了 LLM。这个例外是由于训练某些 LLM 需要大量的数据和超级计算设施。然而,你将在这一方向上取得重大进展。具体来说,你将在第九章和第十章中逐行实现 2017 年那篇开创性的论文“Attention Is All You Need”,并以英语到法语的翻译为例(相同的 Transformer 可以训练其他数据集,如中文到英语或英语到德语的翻译)。你还将构建一个仅包含解码器的小型 Transformer,并使用欧内斯特·海明威的多部小说,包括《老人与海》进行训练。训练后的模型可以生成海明威风格的文本。ChatGPT 和 GPT-4 对于我们的目的来说太大太复杂,无法从头开始构建和训练,但你将一窥其前身 GPT-2,并学习如何从头开始构建它。你还将从 Hugging Face 提取训练好的权重,并将它们加载到你构建的 GPT-2 模型中,开始生成可以以人类写作方式通过的文字。

在这种意义上,本书采用了比大多数书籍更基础的方法。本书不是将生成 AI 模型视为黑盒,读者有机会揭开盖子,详细检查这些模型的内部运作机制。目标是让你对生成模型有更深入的理解。这反过来又可能帮助你构建更好、更负责任的生成 AI,以下是一些原因。

首先,对生成模型架构的深入了解有助于读者更好地实际应用这些模型。例如,在第五章中,你将学习如何选择生成图像中的特征,如性别特征和是否戴眼镜。通过从头构建条件 GAN,你理解生成图像的某些特征是由潜在空间中的随机噪声向量 Z 决定的。因此,你可以选择不同的 Z 值作为训练模型的输入,以生成所需的特征(如性别特征)。这种属性选择如果不了解模型的设计是难以实现的。

对于 Transformer,了解其架构(以及编码器和解码器的作用)使你能够创建和训练 Transformer 以生成你感兴趣的内容类型(例如,简·奥斯汀风格的小说或莫扎特风格的音乐)。这种理解也有助于你使用预训练的 LLM。例如,虽然从头开始训练具有 15 亿参数的 GPT-2 很困难,但你可以在模型中添加一个额外的层,并对其进行微调以完成其他下游任务,如文本分类、情感分析和问答。

其次,对生成式 AI 的深入了解有助于读者对 AI 的危险性进行无偏评估。虽然生成式 AI 的非凡能力在我们的日常生活和工作中带来了好处,但它也有可能造成巨大的危害。埃隆·马斯克甚至表示,“有一些可能性,它可能会出错并毁灭人类”(参见《The Hill》2023 年的文章,“Musk: There’s a Chance AI ‘Goes Wrong and Destroys Humanity’”,mng.bz/Aaxz)。越来越多的学术界和科技行业的人士担心 AI(尤其是生成式 AI)带来的危险。生成式 AI,尤其是 LLM,可能导致意想不到的后果,正如许多科技行业的先驱所警告的那样(例如,参见 Stuart Russell,2023 年的文章,“How to Stop Runaway AI”,mng.bz/ZVzP)。ChatGPT 发布仅仅五个月后,包括史蒂夫·沃兹尼亚克、特里斯坦·哈里斯、约书亚·本吉奥和山姆·奥特曼在内的许多科技行业专家和企业家签署了一封公开信,呼吁至少暂停六个月训练任何比 GPT-4 更强大的 AI 系统(参见《TechCrunch》的文章,“1,100+ Notable Signatories Just Signed an Open Letter Asking ‘All AI Labs to Immediately Pause for at Least 6 Months’”,mng.bz/RNEK)。对生成模型架构的深入了解有助于我们提供对 AI 的益处和潜在危险的深入和无偏评估。

摘要

  • 生成式 AI 是一种具有产生各种形式新内容的能力的技术,包括文本、图像、代码、音乐、音频和视频。

  • 判别性模型擅长于分配标签,而生成性模型则生成新的数据实例。

  • PyTorch 凭借其动态计算图和 GPU 训练的能力,非常适合深度学习和生成建模。

  • GANs(生成对抗网络)是一种由两个神经网络组成的生成建模方法:一个生成器和一个人工智能判别器。生成器的目标是创建逼真的数据样本,以最大化判别器认为这些样本是真实样本的概率。判别器的目标是正确识别出真实样本中的假样本。

  • 变换器是一种使用注意力机制来识别序列中元素之间长期依赖的深度神经网络。原始的变换器包括编码器和解码器。例如,当用于英语到法语翻译时,编码器将英语句子转换成抽象表示,然后传递给解码器。解码器根据编码器的输出和之前生成的单词,逐词生成法语翻译。

第二章:使用 PyTorch 进行深度学习

本章涵盖

  • PyTorch 张量和基本操作

  • 为 PyTorch 中的深度学习准备数据

  • 使用 PyTorch 构建和训练深度神经网络

  • 使用深度学习进行二进制和多类别分类

  • 创建验证集以决定训练停止点

在本书中,我们将使用深度神经网络生成各种内容,包括文本、图像、形状、音乐等。我假设你已经对机器学习(ML)有一个基础的了解,特别是人工神经网络。在本章中,我将回顾一些基本概念,如损失函数、激活函数、优化器和学习率,这些对于开发和管理深度神经网络至关重要。如果你在这些主题的理解上存在任何空白,我强烈建议你在继续本书的项目之前解决这些问题。附录 B 提供了所需的基本技能和概念的总结,包括人工神经网络的架构和训练。

注意:市面上有很多优秀的机器学习书籍供你选择。例如,包括《动手学习 Scikit-Learn、Keras 和 TensorFlow》(2019 年,O’Reilly)和《机器学习,动画》(2023 年,CRC 出版社)。这两本书都使用 TensorFlow 创建神经网络。如果你更喜欢使用 PyTorch 的书籍,我推荐《使用 PyTorch 的深度学习》(2020 年,Manning Publications)。

生成式 AI 模型经常面临二进制或多类别分类的任务。例如,在生成对抗网络(GANs)中,判别器扮演着二进制分类器的关键角色,其目的是区分生成器创建的假样本和训练集中的真实样本。同样,在文本生成模型的背景下,无论是循环神经网络还是 Transformer,其总体目标都是从大量可能性中预测下一个字符或单词(本质上是一个多类别分类任务)。

在本章中,你将学习如何使用 PyTorch 创建深度神经网络以执行二进制和多类别分类,以便你精通深度学习和分类任务。

具体来说,你将参与一个端到端的 PyTorch 深度学习项目,目标是将服装物品的灰度图像分类到不同的类别,如外套、包、运动鞋、衬衫等。目的是为你准备创建能够执行 PyTorch 中二进制和多类别分类任务的深度神经网络。这将使你为即将到来的章节做好准备,在这些章节中,你将使用 PyTorch 中的深度神经网络来创建各种生成模型。

为了训练生成式 AI 模型,我们利用各种数据格式,如原始文本、音频文件、图像像素和数字数组。在 PyTorch 中创建的深度神经网络不能直接以这些形式的数据作为输入。相反,我们必须首先将它们转换为神经网络理解和接受的形式。具体来说,你将把各种原始数据形式转换为 PyTorch 张量(用于表示和操作数据的基本数据结构),然后再将它们提供给生成式 AI 模型。因此,在本章中,你还将学习数据类型的基础知识、如何创建各种形式的 PyTorch 张量以及如何在深度学习中使用它们。

了解如何执行分类任务在我们的社会中有许多实际应用。分类在医疗保健中广泛用于诊断目的,例如确定患者是否患有特定疾病(例如,基于医学影像或测试结果,对特定癌症的阳性或阴性)。它们在许多商业任务中扮演着至关重要的角色(如股票推荐、信用卡欺诈检测等)。分类任务也是我们日常使用的许多系统和服务的核心,例如垃圾邮件检测和面部识别。

2.1 PyTorch 中的数据类型

本书将使用来自广泛来源和格式的数据集,深度学习的第一步是将输入转换为数字数组。

在本节中,你将了解 PyTorch 如何将不同格式的数据转换为称为 张量 的代数结构。张量可以表示为多维数字数组,类似于 NumPy 数组,但有几个关键区别,其中最重要的是能够进行 GPU 加速训练。根据其最终用途,存在不同类型的张量,你将学习如何创建不同类型的张量以及何时使用每种类型。我们将通过使用 46 位美国总统的高度作为示例来讨论 PyTorch 中的数据结构。

请参阅附录 A 中的说明,在计算机上创建虚拟环境并安装 PyTorch 和 Jupyter Notebook。在虚拟环境中打开 Jupyter Notebook 应用程序,并在新单元格中运行以下代码行:

!pip install matplotlib

此命令将在你的计算机上安装 Matplotlib 库,使你能够在 Python 中绘制图像。

2.1.1 创建 PyTorch 张量

在训练深度神经网络时,我们以数字数组的形式向模型提供输入。根据生成模型试图创建的内容,这些数字有不同的类型。例如,在生成图像时,输入是介于 0 和 255 之间的整数形式的原始像素,但我们将它们转换为介于 –1 和 1 之间的浮点数;在生成文本时,有一个类似于字典的“词汇表”,输入是一个整数序列,告诉你单词对应字典中的哪个条目。

注意:本章代码以及本书其他章节的代码可在本书的 GitHub 仓库中找到:github.com/markhliu/DGAI。

假设您想使用 PyTorch 计算美国 46 位总统的平均身高。我们首先收集 46 位美国总统的身高(以厘米为单位),并将它们存储在一个 Python 列表中:

heights = [189, 170, 189, 163, 183, 171, 185,
           168, 173, 183, 173, 173, 175, 178,
           183, 193, 178, 173, 174, 183, 183,
           180, 168, 180, 170, 178, 182, 180,
           183, 178, 182, 188, 175, 179, 183,
           193, 182, 183, 177, 185, 188, 188,
           182, 185, 191, 183]

这些数字按时间顺序排列:列表中的第一个值 189 表示美国第一任总统乔治·华盛顿身高 189 厘米。最后一个值显示乔·拜登的身高为 183 厘米。我们可以通过使用 PyTorch 中的 tensor() 方法将 Python 列表转换为 PyTorch 张量:

import torch
heights_tensor = torch.tensor(heights,      ①
           dtype=torch.float64)             ②

① 将 Python 列表转换为 PyTorch 张量

② 在 PyTorch 张量中指定数据类型

我们在 tensor() 方法中使用 dtype 参数指定数据类型。PyTorch 张量的默认数据类型是 float32,即 32 位浮点数。在前面的代码单元中,我们将数据类型转换为 float64,双精度浮点数。float64 提供比 float32 更精确的结果,但计算时间更长。精度和计算成本之间存在权衡。使用哪种数据类型取决于手头的任务。

表 2.1 列出了不同的数据类型以及相应的 PyTorch 张量类型。这些包括具有不同精度的整数和浮点数。整数也可以是有符号或无符号的。

表 2.1 PyTorch 中的数据和张量类型

PyTorch 张量类型tensor() 方法中的 dtype 参数数据类型
FloatTensortorch.float32 or torch.float32 位浮点数
HalfTensortorch.float16 or torch.half16 位浮点数
DoubleTensortorch.float64 or torch.double64 位浮点数
CharTensortorch.int88 位整数(有符号)
ByteTensortorch.uint88 位整数(无符号)
ShortTensortorch.int16 or torch.short16 位整数(有符号)
IntTensortorch.int32 or torch.int32 位整数(有符号)
LongTensortorch.int64 or torch.long64 位整数(有符号)

您可以通过两种方式之一创建具有特定数据类型的张量。第一种方式是使用表 2.1 的第一列中指定的 PyTorch 类。第二种方式是使用 torch.tensor() 方法,并通过 dtype 参数指定数据类型(该参数的值列在表 2.1 的第二列中)。例如,要将 Python 列表 [1, 2, 3] 转换为包含 32 位整数的 PyTorch 张量,您可以在以下列表中的两种方法中选择一种。

列表 2.1 指定张量类型的两种方法

t1=torch.IntTensor([1, 2, 3])    ①
t2=torch.tensor([1, 2, 3],
             dtype=torch.int)    ②
print(t1)
print(t2)

① 使用 torch.IntTensor() 指定张量类型

② 使用 dtype=torch.int 指定张量类型

这导致以下输出:

tensor([1, 2, 3], dtype=torch.int32)
tensor([1, 2, 3], dtype=torch.int32)

练习 2.1

使用两种不同的方法将 Python 列表[5, 8, 10]转换为包含 64 位浮点数的 PyTorch 张量。请参考表 2.1 的第三行来回答这个问题。

许多时候,你需要创建一个所有值都为 0 的 PyTorch 张量。例如,在生成对抗网络(GANs)中,我们创建一个全零张量作为伪造样本的标签,正如你在第三章中将会看到的。PyTorch 中的zeros()方法可以生成具有特定形状的全零张量。在 PyTorch 中,张量是一个 n 维数组,其形状是一个元组,表示其每个维度的尺寸。以下代码行生成一个具有两行三列的全零张量:

tensor1 = torch.zeros(2, 3)
print(tensor1)

输出结果如下

tensor([[0., 0., 0.],
        [0., 0., 0.]])

该张量的形状为(2,3),这意味着该张量是一个 2D 数组;第一维有两个元素,第二维有三个元素。在这里,我们没有指定数据类型,输出默认数据类型为float32*

不时地,你需要创建一个所有值都为 1 的 PyTorch 张量。例如,在 GANs 中,我们创建一个全 1 张量作为真实样本的标签。在这里,我们使用ones()方法创建一个所有值都为 1 的 3D 张量:

tensor2 = torch.ones(1,4,5)
print(tensor2)

输出结果如下

tensor([[[1., 1., 1., 1., 1.],
         [1., 1., 1., 1., 1.],
         [1., 1., 1., 1., 1.],
         [1., 1., 1., 1., 1.]]])

我们已经生成了一个 3D PyTorch 张量。张量的形状为(1,4,5)。

练习 2.2

创建一个所有值为 0 的 3D PyTorch 张量。张量的形状为(2,3,4)。

你也可以在张量构造函数中使用 NumPy 数组代替 Python 列表:

import numpy as np

nparr=np.array(range(10))
pt_tensor=torch.tensor(nparr, dtype=torch.int)
print(pt_tensor)

输出结果如下

tensor([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=torch.int32)

2.1.2 索引和切片 PyTorch 张量

我们使用方括号([ ])来索引和切片 PyTorch 张量,就像我们使用 Python 列表一样。索引和切片使我们能够对一个或多个张量元素进行操作,而不是对所有元素进行操作。为了继续我们关于 46 位美国总统身高的例子,如果我们想评估第三位总统托马斯·杰斐逊的身高,我们可以这样做:

height = heights_tensor[2]
print(height)

这导致输出结果如下

tensor(189., dtype=torch.float64)

输出结果显示,托马斯·杰斐逊的身高为 189 厘米。

我们可以使用负索引从张量的后端进行计数。例如,为了找到列表中倒数第二位总统唐纳德·特朗普的身高,我们使用索引-2:

height = heights_tensor[-2]
print(height)

输出结果如下

tensor(191., dtype=torch.float64)

输出结果显示,特朗普的身高为 191 厘米。

如果我们想知道heights_tensor张量中最近五位总统的身高怎么办?我们可以获取张量的一部分:

five_heights = heights_tensor[-5:]
print(five_heights)

冒号(:)用于分隔起始和结束索引。如果没有提供起始索引,则默认为 0;如果没有提供结束索引,则包括张量中的最后一个元素(正如我们在前面的代码单元中所做的那样)。负索引意味着从后向前计数。输出结果如下

tensor([188., 182., 185., 191., 183.], dtype=torch.float64)

结果显示,张量中的五位最近总统(克林顿、布什、奥巴马、特朗普和拜登)的身高分别为 188、182、185、191 和 183 厘米。

练习 2.3

使用切片从heights_tensor张量中获取前五位美国总统的身高。

2.1.3 PyTorch 张量形状

PyTorch 张量有一个名为shape的属性,它告诉我们张量的维度。了解 PyTorch 张量的形状非常重要,因为不匹配的形状会导致我们在操作它们时出错。例如,如果我们想找出张量heights_tensor的形状,我们可以这样做:

print(heights_tensor.shape)

输出如下

torch.Size([46])

这告诉我们heights_tensor是一个包含 46 个值的 1D 张量。

您还可以更改 PyTorch 张量的形状。要了解如何操作,让我们首先将高度从厘米转换为英尺。由于一英尺大约是 30.48 厘米,我们可以通过将张量除以 30.48 来完成此操作:

heights_in_feet = heights_tensor / 30.48
print(heights_in_feet)

这导致以下输出(为了节省空间,我省略了一些值;完整的输出在本书的 GitHub 仓库中):

tensor([6.2008, 5.5774, 6.2008, 5.3478, 6.0039, 5.6102, 6.0696, …
        6.0039], dtype=torch.float64)

新的张量heights_in_feet用于存储以英尺为单位的高度。例如,张量中的最后一个值显示乔·拜登身高为 6.0039 英尺。

我们可以使用 PyTorch 中的cat()方法连接两个张量:

heights_2_measures = torch.cat(
    [heights_tensor,heights_in_feet], dim=0)
print(heights_2_measures.shape)

dim参数在多种张量操作中用于指定要执行操作的维度。在前面的代码单元格中,dim=0表示我们沿着第一个维度连接两个张量。这导致以下输出:

torch.Size([92])

结果张量是 1D 的,包含 92 个值,其中一些值以厘米为单位,其他以英尺为单位。因此,我们需要将其重塑为两行 46 列,以便第一行表示厘米单位的高度,第二行表示英尺单位的高度:

heights_reshaped = heights_2_measures.reshape(2, 46)

新的张量heights_reshaped是 2D 的,形状为(2, 46)。我们可以使用方括号索引和切片多维张量。例如,为了打印出特朗普的身高(以英尺为单位),我们可以这样做:

print(heights_reshaped[1,-2])

这导致以下结果

tensor(6.2664, dtype=torch.float64)

命令heights_reshaped[1,-2]告诉 Python 查找第二行和倒数第二列的值,这返回了特朗普的身高,6.2664 英尺。

提示:引用张量内标量值所需的索引数量与张量的维度性相同。这就是为什么我们在 1D 张量heights_tensor中只使用一个索引来定位值,但在 2D 张量heights_reshaped中使用了两个索引来定位值。

练习 2.4

使用索引从张量heights_reshaped中获取乔·拜登的身高(以厘米为单位)。

2.1.4 PyTorch 张量上的数学运算

我们可以通过使用不同的方法如mean()、median()、sum()、max()等在 PyTorch 张量上执行数学运算。例如,为了找到 46 位总统的中位身高(以厘米为单位),我们可以这样做:

print(torch.median(heights_reshaped[0,:]))

代码片段heights_reshaped[0,:]返回张量heights_reshaped的第一行和所有列。前面的代码行返回第一行中的中位值,这导致以下输出

tensor(182., dtype=torch.float64)

这意味着美国总统的中位身高为 182 厘米。

要找到两行中的平均身高,我们可以在mean()方法中使用dim=1参数:

print(torch.mean(heights_reshaped,dim=1))

dim=1 参数表示通过折叠列(索引为 1 的维度),实际上是在索引为 0 的维度(行)上计算平均值。输出是

tensor([180.0652,   5.9077], dtype=torch.float64)

结果显示,两行中的平均值分别是 180.0652 厘米和 5.9077 英尺。

要找出最高的总统,我们可以这样做:

values, indices = torch.max(heights_reshaped, dim=1)
print(values)
print(indices)

输出是

tensor([193.0000,   6.3320], dtype=torch.float64)
tensor([15, 15])

torch.max()方法返回两个张量:一个包含最高总统身高的张量values(以厘米和英尺为单位),以及一个包含身高最高的总统索引的张量indices。结果显示,第 16 任总统(林肯)是最高的,身高为 193 厘米,或 6.332 英尺。

练习 2.5

使用torch.min()方法找出最矮的美国总统的索引和身高。

2.2 使用 PyTorch 的端到端深度学习项目

在接下来的几节中,你将通过 PyTorch 完成一个深度学习项目的示例,学习将服装物品的灰度图像分类为 10 种类型中的 1 种。在本节中,我们将首先提供一个涉及步骤的高级概述。然后,我们将讨论如何获取这个项目的训练数据以及如何预处理数据。

2.2.1 PyTorch 中的深度学习:高级概述

在这个项目中,我们的任务是创建并训练一个 PyTorch 深度神经网络,以对服装物品的灰度图像进行分类。图 2.1 提供了涉及步骤的示意图。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH02_F01_Liu.png

图 2.1 深度学习模型训练涉及的步骤

首先,我们将获取如图 2.1 左侧所示的灰度服装图像数据集。图像是原始像素,我们将它们转换为浮点数形式的 PyTorch 张量(步骤 1)。每张图像都附有标签。

然后,我们将在 PyTorch 中创建一个深度神经网络,如图 2.1 中心的所示。本书中的一些神经网络涉及卷积神经网络(CNNs)。对于这个简单的分类问题,我们目前只使用密集层。

我们将为多类别分类选择一个损失函数,交叉熵损失通常用于这项任务。交叉熵损失衡量预测概率分布与标签真实分布之间的差异。我们将在训练过程中使用 Adam 优化器(梯度下降算法的一种变体)来更新网络的权重。我们将学习率设置为 0.001。学习率控制模型权重在训练过程中相对于损失梯度的调整程度。

机器学习中的优化器

机器学习中的优化器是算法,根据梯度信息更新模型参数以最小化损失函数。随机梯度下降(SGD)是最基本的优化器,它使用基于损失梯度的简单更新。Adam 是最受欢迎的优化器,以其效率和即插即用的性能而闻名,因为它结合了自适应梯度算法(AdaGrad)和根均方传播(RMSProp)的优点。尽管它们有所不同,但所有优化器都旨在迭代调整参数以最小化损失函数,每个优化器都创建一条独特的优化路径以达到这一目标。

我们将把训练数据分成训练集和验证集。在机器学习中,我们通常使用验证集来提供模型的无偏评估,并选择最佳超参数,如学习率、训练 epoch 数等。验证集还可以用来避免模型过拟合,即模型在训练集中表现良好,但在未见过的数据上表现不佳。一个 epoch 是指所有训练数据被用来训练模型一次且仅一次。

在训练过程中,您将遍历训练数据。在正向传播过程中,您将图像通过网络以获得预测(步骤 2)并计算损失,通过比较预测标签与实际标签(步骤 3;见图 2.1 右侧)。然后,您将通过网络反向传播梯度以更新权重。这就是学习发生的地方(步骤 4),如图 2.1 底部所示。

您将使用验证集来确定何时停止训练。我们在验证集中计算损失。如果模型在固定数量的 epoch 后停止改进,我们认为模型已训练完成。然后,我们在测试集上评估训练好的模型,以评估其在将图像分类到不同标签方面的性能。

现在您已经对 PyTorch 中的深度学习有了高级概述,让我们深入到端到端项目吧!

2.2.2 数据预处理

在这个项目中,我们将使用 Fashion Modified National Institute of Standards and Technology (MNIST)数据集。在这个过程中,您将学习如何使用 Torchvision 库中的datasets和transforms包,以及 PyTorch 中的Dataloader包,这些包将帮助您在本书的其余部分。您将使用这些工具在本书中预处理数据。Torchvision 库提供了图像处理工具,包括流行的数据集、模型架构和深度学习应用中的常见图像转换。

我们首先导入所需的库并在transforms包中实例化一个Compose()类,以将原始图像转换为 PyTorch 张量。

列表 2.2 将原始图像数据转换为 PyTorch 张量

import torch
import torch.nn as nn
import torchvision
import torchvision.transforms as T

torch.manual_seed(42)
transform=T.Compose([             ①
    T.ToTensor(),                 ②
    T.Normalize([0.5],[0.5])])    ③

① 将多个转换组合在一起

② 将图像像素转换为 PyTorch 张量

③ 将值归一化到范围[–1, 1]

我们使用 PyTorch 中的 manual_seed() 方法来固定随机状态,以便结果可重现。Torchvision 中的 transforms 包可以帮助创建一系列转换以预处理图像。ToTensor() 类将图像数据(无论是 Python Imaging Library (PIL) 图像格式还是 NumPy 数组)转换为 PyTorch 张量。特别是,图像数据是介于 0 到 255 之间的整数,而 ToTensor() 类将它们转换为介于 0.0 和 1.0 范围内的浮点张量。

Normalize() 类使用 n 个通道的平均值和标准差对张量图像进行归一化。Fashion MNIST 数据是服装项目的灰度图像,因此只有一个颜色通道。在本书的后面部分,我们将处理具有三个不同颜色通道(红色、绿色和蓝色)的图像。在先前的代码单元中,Normalize([0.5],[0.5]) 表示从数据中减去 0.5,并将差值除以 0.5。结果图像数据范围从 –1 到 1。将输入数据归一化到 [–1, 1] 范围允许梯度下降在维度上保持更均匀的步长,这有助于在训练过程中更快地收敛。您将在本书中经常这样做。

注意:列表 2.2 中的代码仅定义了数据转换过程。它不执行实际的转换,这将在下一个代码单元中发生。

接下来,我们使用 Torchvision 中的 datasets 包将数据集下载到您的计算机上的一个文件夹,并执行转换:

train_set=torchvision.datasets.FashionMNIST(    ①
    root=".",                                   ②
    train=True,                                 ③
    download=True,                              ④
    transform=transform)                        ⑤
test_set=torchvision.datasets.FashionMNIST(root=".",
    train=False,download=True,transform=transform)

① 下载哪个数据集

② 数据保存的位置

③ 训练或测试数据集

④ 是否将数据下载到您的计算机上

⑤ 执行数据转换

您可以打印出训练集中的第一个样本:

print(train_set[0])

第一个样本包含一个具有 784 个值的张量和标签 9。784 个数字代表一个 28×28 的灰度图像(28 × 28 = 784),标签 9 表示这是一双短靴。您可能想知道:您如何知道标签 9 表示短靴?数据集中有 10 种不同的服装项目。数据集中的标签从 0 到 9 编号。您可以在网上搜索并找到 10 个类别的文本标签(例如,我在这里找到了文本标签 github.com/pranay414/Fashion-MNIST-Pytorch)。text_labels 列表包含与数值标签 0 到 9 对应的 10 个文本标签。例如,如果数据集中的项目具有数值标签 0,则相应的文本标签是“T 恤”。text_labels 列表定义如下:

text_labels=['t-shirt', 'trouser', 'pullover', 'dress', 'coat',
             'sandal', 'shirt', 'sneaker', 'bag', 'ankle boot']

我们可以绘制数据以可视化数据集中的服装项目。

列表 2.3 可视化服装项目

!pip install matplotlib
import matplotlib.pyplot as plt

plt.figure(dpi=300,figsize=(8,4))
for i in range(24):
    ax=plt.subplot(3, 8, i + 1)                 ①
    img=train_set[i][0]                         ②
    img=img/2+0.5                               ③
    img=img.reshape(28, 28)                     ④
    plt.imshow(img,
               cmap="binary")
    plt.axis('off')
    plt.title(text_labels[train_set[i][1]],     ⑤
        fontsize=8)
plt.show()

① 图像放置的位置

② 从训练数据中获取第 i 个图像

③ 将值从 [–1,1] 转换为 [0,1]

④ 将图像重塑为 28×28

⑤ 为每张图像添加文本标签

图 2.2 中的图表显示了 24 种服装项目,如大衣、套头衫、凉鞋等。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH02_F02_Liu.png

图 2.2 Fashion MNIST 数据集中服装项目的灰度图像。

在接下来的两节中,你将学习如何使用 PyTorch 创建深度神经网络,以执行二进制和多类别分类问题。

2.3 二进制分类

在本节中,我们将首先为训练创建数据批次。然后,我们使用 PyTorch 构建一个深度神经网络用于此目的,并使用数据训练模型。最后,我们将使用训练好的模型进行预测并测试预测的准确性。二进制和多类别分类涉及到的步骤相似,但有几个显著的例外,我将在后面强调。

2.3.1 创建批次

我们将创建一个只包含两种服装类型(T 恤和踝靴)的训练集和测试集。(在本章后面讨论多类别分类时,你还将学习如何创建验证集以确定何时停止训练。)以下代码单元实现了该目标:

binary_train_set=[x for x in train_set if x[1] in [0,9]]
binary_test_set=[x for x in test_set if x[1] in [0,9]]

我们只保留标签为 0 和 9 的样本,以创建一个具有平衡训练集的二进制分类问题。接下来,我们为训练深度神经网络创建批次。

列表 2.4 创建训练和测试的批次

batch_size=64
binary_train_loader=torch.utils.data.DataLoader(
    binary_train_set,                                ①
    batch_size=batch_size,                           ②
    shuffle=True)                                    ③
binary_test_loader=torch.utils.data.DataLoader(
    binary_test_set,                                 ④
    batch_size=batch_size,shuffle=True)

① 为二进制训练集创建批次

② 每个批次中的样本数量

③ 在批量处理时打乱观察值

④ 为二进制测试集创建批次

PyTorch utils 包中的 DataLoader 类帮助批量创建数据迭代器。我们将批大小设置为 64。在列表 2.4 中,我们创建了两个数据加载器:用于二进制分类的训练集和测试集。在创建批次时,我们随机打乱观察值,以避免原始数据集中的相关性:如果数据加载器中的标签分布均匀,则训练将更加稳定。

2.3.2 构建和训练二进制分类模型

我们首先创建一个二进制分类模型。然后,我们使用 T 恤和踝靴的图像来训练模型。一旦训练完成,我们将看看模型能否区分 T 恤和踝靴。我们使用 PyTorch 的 nn.Sequential 类(在后面的章节中,你还将学习如何使用 nn.Module 类来创建 PyTorch 神经网络)创建以下神经网络。

列表 2.5 创建二进制分类模型

import torch.nn as nn

device="cuda" if torch.cuda.is_available() else "cpu"  ①

binary_model=nn.Sequential(                            ②
    nn.Linear(28*28,256),                              ③
    nn.ReLU(),                                         ④
    nn.Linear(256,128),
    nn.ReLU(),
    nn.Linear(128,32),
    nn.ReLU(),
    nn.Linear(32,1),
    nn.Dropout(p=0.25),
    nn.Sigmoid()).to(device)                           ⑤

① PyTorch 自动检测是否有可用的 CUDA 启用 GPU。

② 在 PyTorch 中创建一个顺序神经网络

③ 线性层中的输入和输出神经元数量

④ 对层的输出应用 ReLU 激活函数

⑤ 应用 sigmoid 激活函数并将模型移动到可用的 GPU 上

PyTorch 中的Linear()类创建了对传入数据的线性变换。这实际上在神经网络中创建了一个密集层。输入形状是 784,因为我们稍后将 2D 图像展平成一个包含 28 × 28 = 784 个值的 1D 向量。我们将 2D 图像展平成一个 1D 张量,因为密集层只接受 1D 输入。在后面的章节中,你会看到当你使用卷积层时,不需要展平图像。网络中有三个隐藏层,分别包含 256、128 和 32 个神经元。256、128 和 32 这些数字的选择是相当随意的:将它们改为,比如说,300、200 和 50,不会影响训练过程。我们在三个隐藏层上应用了 ReLU 激活函数。ReLU 激活函数根据加权的总和决定是否激活神经元。这些函数向神经元的输出引入非线性,从而使网络能够学习输入和输出之间的非线性关系。ReLU 是除了极少数例外情况外你首选的激活函数,你将在后面的章节中遇到一些其他的激活函数。

模型的最后一层的输出包含一个单一值,我们使用 sigmoid 激活函数将数值压缩到[0, 1]的范围内,这样它可以被解释为该物体是踝靴的概率。通过互补概率,该物体是 T 恤。

在这里,我们设置学习率并定义优化器和损失函数:

lr=0.001
optimizer=torch.optim.Adam(binary_model.parameters(),lr=lr)
loss_fn=nn.BCELoss()

我们将学习率设置为 0.001。设置学习率是一个经验问题,答案来自于经验。它也可以通过使用验证集进行超参数调整来确定。PyTorch 中的大多数优化器使用默认的学习率 0.001。Adam 优化器是梯度下降算法的一种变体,用于确定在每一步训练中应调整多少模型参数。Adam 优化器首次由 Diederik Kingma 和 Jimmy Ba 于 2014 年提出。1 在传统的梯度下降算法中,只考虑当前迭代的梯度。相比之下,Adam 优化器还考虑了之前迭代的梯度。

我们使用nn.BCELoss(),这是二元交叉熵损失函数。损失函数衡量 ML 模型的表现。模型的训练涉及调整参数以最小化损失函数。二元交叉熵损失函数在 ML 中广泛使用,尤其是在二元分类问题中。它衡量的是输出为 0 到 1 之间概率值的分类模型的表现。当预测概率偏离实际标签时,交叉熵损失会增加。

我们按照以下列表训练我们刚刚创建的神经网络。

列表 2.6 训练二元分类模型

for i in range(50):                                    ①
    tloss=0
    for imgs,labels in binary_train_loader:            ②
        imgs=imgs.reshape(-1,28*28)                    ③
        imgs=imgs.to(device)
        labels=torch.FloatTensor(\
          [x if x==0 else 1 for x in labels])          ④
        labels=labels.reshape(-1,1).to(device)
        preds=binary_model(imgs)    
        loss=loss_fn(preds,labels)                     ⑤
        optimizer.zero_grad()
        loss.backward()                                ⑥
        optimizer.step()
        tloss+=loss.detach()
    tloss=tloss/n
    print(f"at epoch {i}, loss is {tloss}")

① 训练 50 个 epoch

② 遍历所有批次

③ 在将张量移动到 GPU 之前将图像展平

④ 将标签转换为 0 和 1

⑤ 计算损失

⑥ 反向传播

在 PyTorch 中训练深度学习模型时,loss.backward()计算损失相对于每个模型参数的梯度,从而实现反向传播,而optimizer.step()根据这些计算的梯度更新模型参数以最小化损失。为了简单起见,我们训练模型 50 个 epoch(一个 epoch 是指使用训练数据训练模型一次)。在下一节中,你将使用验证集和提前停止类来确定训练的 epoch 数。在二分类中,我们将目标标签标记为 0 和 1。由于我们只保留了标签为 0 和 9 的 T 恤和踝靴,我们在列表 2.6 中将它们转换为 0 和 1。因此,两种服装类别的标签分别为 0 和 1。

如果你使用 GPU 进行训练,这个过程可能需要几分钟。如果你使用 CPU 进行训练,则所需时间更长,但训练时间应该不到一小时。

2.3.3 测试二分类模型

训练好的二分类模型的预测结果是一个介于 0 和 1 之间的数字。我们将使用torch.where()方法将预测结果转换为 0 和 1:如果预测概率小于 0.5,我们将预测结果标记为 0;否则,我们将预测结果标记为 1。然后我们将这些预测结果与实际标签进行比较,以计算预测的准确率。在下面的列表中,我们使用训练好的模型对测试数据集进行预测。

列表 2.7 计算预测的准确率

import numpy as np
results=[]
for imgs,labels in binary_test_loader:                     ①
    imgs=imgs.reshape(-1,28*28).to(device)
    labels=(labels/9).reshape(-1,1).to(device)
    preds=binary_model(imgs)
    pred10=torch.where(preds>0.5,1,0)                      ②
    correct=(pred10==labels)                               ③
    results.append(correct.detach().cpu()\
      .numpy().mean())                                     ④
accuracy=np.array(results).mean()                          ⑤
print(f"the accuracy of the predictions is {accuracy}")

① 遍历测试集中的所有批次

② 使用训练好的模型进行预测

③ 将预测与标签进行比较

④ 在批次中计算准确率

⑤ 在测试集中计算准确率

我们遍历测试集中所有批次的数据。训练好的模型会输出一个概率,表示图像是否为踝靴。然后我们根据 0.5 的截止值,使用torch.where()方法将概率转换为 0 或 1。转换后,预测结果要么是 0(即 T 恤),要么是 1(踝靴)。我们将预测结果与实际标签进行比较,看模型正确预测的次数。结果显示,在测试集中预测的准确率为 87.84%。

2.4 多分类分类

在本节中,我们将使用 PyTorch 构建一个深度神经网络来将服装项目分类到 10 个类别之一。然后我们将使用 Fashion MNIST 数据集训练模型。最后,我们将使用训练好的模型进行预测,并查看其准确率。我们首先创建一个验证集并定义一个提前停止类,以便我们可以确定何时停止训练。

2.4.1 验证集和提前停止

当我们构建和训练一个深度神经网络时,有许多超参数可以选择(例如学习率和训练的 epoch 数)。这些超参数影响模型的性能。为了找到最佳超参数,我们可以创建一个验证集来测试不同超参数下模型的性能。

为了给您一个例子,我们将在多类别分类中创建一个验证集,以确定训练的最佳 epoch 数。我们之所以在验证集而不是在训练集中这样做,是为了避免过拟合,即模型在训练集中表现良好,但在样本外测试(即未见过的数据)中表现不佳。

在这里,我们将训练数据集的 60,000 个观测值分为一个训练集和一个验证集:

train_set,val_set=torch.utils.data.random_split(\
    train_set,[50000,10000])

原始训练集现在变成了两个集合:包含 50,000 个观测值的新训练集和包含剩余 10,000 个观测值的验证集。

我们使用 PyTorch utils包中的DataLoader类将训练集、验证集和测试集转换为三个数据迭代器,以批量形式:

train_loader=torch.utils.data.DataLoader(
    train_set,    
    batch_size=batch_size,   
    shuffle=True)   
val_loader=torch.utils.data.DataLoader(
    val_set,    
    batch_size=batch_size,   
    shuffle=True)
test_loader=torch.utils.data.DataLoader(
    test_set,    
    batch_size=batch_size,   
    shuffle=True)

接下来,我们定义一个EarlyStop()类并创建该类的实例。

列表 2.8 EarlyStop()类用于确定何时停止训练

class EarlyStop:
    def __init__(self, patience=10):         ①
        self.patience = patience
        self.steps = 0
        self.min_loss = float('inf')
    def stop(self, val_loss):                ②
        if val_loss < self.min_loss:         ③
            self.min_loss = val_loss
            self.steps = 0
        elif val_loss >= self.min_loss:      ④
            self.steps += 1
        if self.steps >= self.patience:
            return True
        else:
            return False
stopper=EarlyStop()

① 将 patience 的默认值设置为 10

② 定义了 stop()方法

③ 如果达到新的最小损失,则更新 min_loss 的值

④ 计算自上次最小损失以来的 epoch 数

EarlyStop()类用于确定验证集中的损失在最后patience=10个 epoch 中是否停止了改进。我们将patience参数的默认值设置为 10,但您在实例化类时可以选择不同的值。patience的值衡量了自上次模型达到最小损失以来您希望训练的 epoch 数。stop()方法记录最小损失和自最小损失以来的 epoch 数,并将该数字与patience的值进行比较。如果自最小损失以来的 epoch 数大于patience的值,则该方法返回True。

2.4.2 构建和训练一个多类别分类模型

Fashion MNIST 数据集包含 10 种不同的服装类别。因此,我们创建一个多类别分类模型来对它们进行分类。接下来,您将学习如何创建这样的模型并对其进行训练。您还将学习如何使用训练好的模型进行预测并评估预测的准确性。在下面的列表中,我们使用 PyTorch 创建多类别分类的神经网络。

列表 2.9 创建一个多类别分类模型

model=nn.Sequential(
    nn.Linear(28*28,256),
    nn.ReLU(),
    nn.Linear(256,128),
    nn.ReLU(),
    nn.Linear(128,64),
    nn.ReLU(),
    nn.Linear(64,10)                         ①
    ).to(device)                             ②

① 输出层有 10 个神经元。

② 输出层不应用 softmax 激活

与上一节中我们创建的二分类模型相比,这里做了一些修改。首先,输出现在有 10 个值,代表数据集中 10 种不同的服装类型。其次,我们将最后一隐藏层的神经元数量从 32 改为 64。创建深度神经网络的一个经验法则是从一层到下一层逐渐增加或减少神经元数量。由于输出神经元的数量从二分类中的 1 增加到多类别分类中的 10,我们将第二到最后一层的神经元数量从 32 改为 64 以匹配增加。然而,64 这个数字并没有什么特殊之处:如果你在第二到最后一层使用,比如说,100 个神经元,你将得到相似的结果。

我们将使用 PyTorch 的nn.CrossEntropyLoss()类作为我们的损失函数,该函数将nn.LogSoftmax()和nn.NLLLoss()合并到一个单独的类中。有关详细信息,请参阅此处文档:mng.bz/pxd2。特别是,文档中提到,“此标准计算输入 logits 和目标之间的交叉熵损失。”这解释了为什么我们在前面的列表中没有应用 softmax 激活。在本书的 GitHub 仓库中,我演示了如果我们使用nn.LogSoftmax()在模型中,并使用nn.NLLLoss()作为损失函数,我们将获得相同的结果。

因此,nn.CrossEntropyLoss()类将在对输出应用 softmax 激活函数之前,将 10 个数字压缩到[0, 1]的范围内进行对数运算。在二分类中,输出上首选的激活函数是 sigmoid,而在多类别分类中是 softmax。此外,softmax 激活后的 10 个数字加起来等于 1,这可以解释为对应于 10 种不同服装项目的概率。我们将使用与上一节二分类中相同的学习率和优化器。

lr=0.001
optimizer=torch.optim.Adam(model.parameters(),lr=lr)
loss_fn=nn.CrossEntropyLoss()

我们定义train_epoch()如下:

def train_epoch():
    tloss=0
    for n,(imgs,labels) in enumerate(train_loader):    
        imgs=imgs.reshape(-1,28*28).to(device)
        labels=labels.reshape(-1,).to(device)
        preds=model(imgs)    
        loss=loss_fn(preds,labels)
        optimizer.zero_grad()
        loss.backward()    
        optimizer.step()
        tloss+=loss.detach()
    return tloss/n

该函数用于训练模型一个 epoch。代码与我们之前在二分类中看到的是相似的,只是标签从 0 到 9,而不是两个数字(0 和 1)。

我们还定义了一个val_epoch()函数:

def val_epoch():
    vloss=0
    for n,(imgs,labels) in enumerate(val_loader):    
        imgs=imgs.reshape(-1,28*28).to(device)
        labels=labels.reshape(-1,).to(device)
        preds=model(imgs)    
        loss=loss_fn(preds,labels)    
        vloss+=loss.detach()
    return vloss/n

该函数使用模型对验证集中的图像进行预测,并计算数据批次平均损失。

我们现在训练多类别分类器:

for i in range(1,101):    
    tloss=train_epoch()
    vloss=val_epoch()
    print(f"at epoch {i}, tloss is {tloss}, vloss is {vloss}")
    if stopper.stop(vloss)==True:             
        break  

我们最多训练 100 个 epoch。在每个 epoch 中,我们首先使用训练集来训练模型。然后我们计算验证集中每个批次的平均损失。我们使用EarlyStop()类通过查看验证集中的损失来确定是否应该停止训练。如果损失在最后 10 个 epoch 中没有改善,则停止训练。经过 19 个 epoch 后,训练停止。

如果你使用 GPU 进行训练,训练大约需要 5 分钟,这比二分类中的训练过程要长,因为我们现在训练集中的观察结果更多(10 种服装而不是只有 2 种)。

模型的输出是一个包含 10 个数字的向量。我们使用torch.argmax()根据最高概率为每个观察结果分配一个标签。然后,我们将预测标签与实际标签进行比较。为了说明预测是如何工作的,让我们看看测试集中前五张图像的预测结果。

列表 2.10 在五张图像上测试训练好的模型

plt.figure(dpi=300,figsize=(5,1))
for i in range(5):                                          ①
    ax=plt.subplot(1,5, i + 1)
    img=test_set[i][0]    
    label=test_set[i][1]
    img=img/2+0.5    
    img=img.reshape(28, 28)    
    plt.imshow(img, cmap="binary")
    plt.axis('off')
    plt.title(text_labels[label]+f"; {label}", fontsize=8)
plt.show()
for i in range(5):
    img,label = test_set[i]                                 ②
    img=img.reshape(-1,28*28).to(device)
    pred=model(img)                                         ③
    index_pred=torch.argmax(pred,dim=1)                     ④
    idx=index_pred.item()
    print(f"the label is {label}; the prediction is {idx}") ⑤ 

① 在测试集中绘制前五张图像及其标签

② 获取测试集中的第 i 张图像和标签

③ 使用训练好的模型进行预测

④ 使用 torch.argmax()方法获取预测标签

⑤ 打印出实际标签和预测标签

我们以 1 × 5 的网格形式绘制测试集中的前五种服装。然后,我们使用训练好的模型对每种服装进行预测。预测结果是一个包含 10 个值的张量。torch.argmax()方法返回张量中最高概率的位置,我们将其用作预测标签。最后,我们打印出实际标签和预测标签以进行比较,看看预测是否正确。运行前面的代码列表后,你应该会看到图 2.3 中的图像。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH02_F03_Liu.png

图 2.3 测试数据集中的前五种服装及其相应的标签。每种服装都有一个文本标签和一个介于 0 到 9 之间的数字标签。

图 2.3 显示了测试集中的前五种服装分别是踝靴、套头衫、裤子、裤子和外套,分别对应数字标签 9、2、1、1 和 6。

运行列表 2.10 中的代码后的输出如下:

the label is 9; the prediction is 9
the label is 2; the prediction is 2
the label is 1; the prediction is 1
the label is 1; the prediction is 1
the label is 6; the prediction is 6

前面的输出显示,模型对所有五种服装都做出了正确的预测。

在 PyTorch 中固定随机状态

torch.manual_seed()方法固定随机状态,以便当你重新运行程序时结果相同。然而,即使你使用相同的随机种子,你也可能得到与本章中报告的不同结果。这是因为不同的硬件和不同的 PyTorch 版本在处理浮点运算时略有不同。例如,请参阅mng.bz/RNva中的解释。尽管如此,这种差异通常很小,因此无需惊慌。

接下来,我们计算在整个测试数据集上的预测准确率。

列表 2.11 测试训练好的多类别分类模型

results=[]

for imgs,labels in test_loader:                             ①
    imgs=imgs.reshape(-1,28*28).to(device)
    labels=(labels).reshape(-1,).to(device)
    preds=model(imgs)                                       ②
    pred10=torch.argmax(preds,dim=1)                        ③
    correct=(pred10==labels)                                ④
    results.append(correct.detach().cpu().numpy().mean())

accuracy=np.array(results).mean()                           ⑤
print(f"the accuracy of the predictions is {accuracy}") 

① 遍历测试集中的所有批次

② 使用训练好的模型进行预测

③ 将概率转换为预测标签

④ 将预测标签与实际标签进行比较

⑤ 计算测试集中的准确率

输出是

the accuracy of the predictions is 0.8819665605095541

我们遍历测试集中的所有服装项目,并使用训练好的模型进行预测。然后我们将预测结果与实际标签进行比较。在样本外测试中的准确率约为 88%。考虑到随机猜测的准确率约为 10%,88%的准确率相当高。这表明我们在 PyTorch 中构建并训练了两个成功的深度学习模型!你将在本书后面的内容中经常使用这些技能。例如,在第三章中,你将构建的判别网络本质上是一个二元分类模型,类似于本章中你创建的模型。

摘要

  • 在 PyTorch 中,我们使用张量来存储各种形式的输入数据,以便我们可以将它们输入到深度学习模型中。

  • 你可以对 PyTorch 张量进行索引和切片,重塑它们,并在它们上执行数学运算。

  • 深度学习是一种使用深度人工神经网络来学习输入和输出数据之间关系的机器学习方法。

  • ReLU 激活函数根据加权总和决定是否应该激活神经元。它为神经元的输出引入了非线性。

  • 损失函数衡量机器学习模型的性能。模型的训练涉及调整参数以最小化损失函数。

  • 二元分类是一种将观察结果分类到两个类别之一的机器学习模型。

  • 多类别分类是一种将观察结果分类到多个类别之一的机器学习模型。


^(1) 迪德里克·金卡马和吉米·巴,2014 年,“Adam:一种随机优化的方法。” arxiv.org/abs/1412.6980.

第三章:生成对抗网络:形状和数量生成

本章涵盖

  • 从头开始构建生成对抗网络中的生成网络和判别网络

  • 使用 GAN 生成数据点以形成形状(例如,指数增长曲线)

  • 生成所有都是 5 的倍数的整数序列

  • 训练、保存、加载和使用 GANs

  • 评估 GAN 性能和确定训练停止点

本书近一半的生成模型属于一种称为生成对抗网络(GANs)的类别。这种方法最初由 Ian Goodfellow 及其合著者在 2014 年提出。1 GANs 因其易于实现和多功能性而备受赞誉,使那些连深度学习基础知识都相当有限的人也能从头开始构建自己的模型。GAN 中的“对抗”一词指的是两个神经网络在零和博弈框架中相互竞争的事实。生成网络试图创建与真实样本不可区分的数据实例。相比之下,判别网络试图从真实样本中识别出生成的样本。这些多才多艺的模型可以生成各种内容格式,从几何形状和数字序列到高分辨率彩色图像,甚至逼真的音乐作品。

在本章中,我们将简要回顾 GANs 背后的理论。然后,我将向您展示如何将这一知识应用于 PyTorch。您将学习从头开始构建您的第一个 GAN,以便所有细节都变得不再神秘。为了使示例更具相关性,想象您将 1 美元存入年利率为 8%的储蓄账户。您想根据您投资了多少年找出账户中的余额。真实的关系是一个指数增长曲线。您将学习如何使用 GAN 生成数据样本——形成这种指数增长曲线的值对(x, y),其数学关系为 y = 1.08^x。掌握这项技能后,您将能够生成模拟任何形状的数据:正弦、余弦、二次等。

在本章的第二个项目中,您将学习如何使用 GAN 生成一系列都是 5 的倍数的数字。但您可以将模式更改为 2、3、7 或其他模式。在这个过程中,您将学习如何从头开始创建生成网络和判别网络。您将学习如何训练、保存和使用 GAN。此外,您还将学习如何通过可视化生成网络生成的样本或通过测量生成样本分布与真实数据分布之间的差异来评估 GAN 的性能。

想象一下,你需要数据来训练一个机器学习(ML)模型以预测一对值(x,y)之间的关系。然而,准备训练数据集既昂贵又耗时。在这种情况下,生成对抗网络(GANs)非常适合生成数据:虽然 x 和 y 的生成值通常符合数学关系,但生成的数据中也有噪声。当使用生成的数据来训练 ML 模型时,噪声可以有助于防止过拟合。

本章的主要目标不一定是为了生成具有最实用用途的新颖内容。相反,我的目标是教你如何从头开始训练和使用 GANs 创建各种格式的内 容。在这个过程中,你将获得对 GANs 内部运作的坚实基础。这个基础将使我们能够在后续章节中集中精力在其他更高级的 GANs 方面,例如生成高分辨率图像或听起来逼真的音乐(例如,卷积神经网络或如何将音乐表示为多维对象)。

3.1 训练 GANs 所涉及的步骤

在第一章中,你获得了 GANs 背后理论的概览。在本节中,我将总结训练 GANs 的步骤,特别是创建数据点以形成指数增长曲线的步骤。

让我们回到先前的例子:你计划投资一个年利率为 8% 的储蓄账户。你今天将 $1 存入账户,并想知道未来账户中会有多少钱。

未来你账户中的金额,y,取决于你投资储蓄账户的时间长短。让我们用 x 表示你投资年数,它可以是介于 0 到 50 之间的数字。例如,如果你投资 1 年,余额是 $1.08;如果你投资 2 年,余额是 1.08² = $1.17。为了概括,x 和 y 之间的关系是 y = 1.08^x。这个函数描绘了一个指数增长曲线。请注意,x 可以是整数,如 1 或 2,也可以是小数,如 1.14 或 2.35,公式仍然适用。

训练 GANs 生成符合特定数学关系的数据点,如前面的例子,是一个多步骤的过程。在你的情况下,你想要生成数据点(x,y),使得 y = 1.08^x。图 3.1 提供了 GANs 架构和生成指数增长曲线的步骤图。当你生成其他内容,如整数序列、图像或音乐时,你将遵循类似的步骤,正如你将在本章的第二项目中看到的那样,以及在本书中稍后其他 GAN 模型中。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH03_F01_Liu.png

图 3.1 训练 GAN 生成指数增长曲线的步骤以及 GAN 中的双网络架构。生成器从潜在空间(左上角)获取一个随机噪声向量 Z,以创建一个假样本并将其展示给判别器(中间)。判别器将样本分类为真实(来自训练集)或假(由生成器创建)。预测结果与真实值进行比较,判别器和生成器都会从预测结果中学习。经过多次训练迭代后,生成器学会创建与真实样本难以区分的形状。

在开始之前,我们需要获得一个训练数据集来训练 GAN。在我们的示例中,我们将使用数学关系 y = 1.08^x 生成一个(x, y)对的集合。我们使用储蓄账户的例子,以便数字更容易理解。本章中你学到的技术可以应用于其他形状:正弦、余弦、U 形等。你可以选择一个 x 值的范围(比如,0 到 50)并计算相应的 y 值。由于我们通常在深度学习中以数据批次的形式训练模型,因此训练数据集中的观测数通常设置为批次大小的倍数。一个真实样本位于图 3.1 的顶部,具有指数增长曲线的形状。

一旦准备好了训练集,你需要在 GAN 中创建两个网络:一个生成器和判别器。生成器位于图 3.1 的左下角,它以一个随机噪声向量 Z 作为输入并生成数据点(我们训练循环的第 1 步)。生成器使用的随机噪声向量 Z 来自潜在空间,这代表了 GAN 可以产生的可能输出范围,并且对于 GAN 生成多样化的数据样本的能力至关重要。在第五章中,我们将探索潜在空间以选择生成器创建的内容的属性。判别器位于图 3.1 的中心,它评估给定的数据点(x, y)是真实(来自训练数据集)还是假(由生成器创建);这是我们训练循环的第 2 步。

潜在空间的意义

GAN 中的潜在空间是一个概念空间,其中每个点都可以通过生成器转换成一个真实的数据实例。这个空间代表了 GAN 可以产生的可能输出范围,并且对于 GAN 生成多样化和复杂数据的能力至关重要。潜在空间只有在与生成模型结合使用时才具有其重要性。在这个背景下,可以在潜在空间中的点之间进行插值,以影响输出的属性,我们将在第五章中讨论这一点。

要知道如何调整模型参数,我们必须选择正确的损失函数。我们需要为生成器和判别器定义损失函数。损失函数鼓励生成器生成类似于训练数据集数据点的数据点,使判别器将它们分类为真实。损失函数鼓励判别器正确分类真实和生成数据点。

在训练循环的每一次迭代中,我们交替训练判别器和生成器。在每次训练迭代中,我们从训练数据集中采样一批真实的数据点(x, y)和一批由生成器生成的虚假数据点。当训练判别器时,我们比较判别模型(它是一个样本来自训练集的概率)的预测与真实值,真实值为 1(如果样本是真实的)和 0(如果样本是虚假的)(如图 3.1 右侧所示);这构成了训练循环中的第 3 步的一半。我们稍微调整判别器网络中的权重,以便在下一个迭代中,预测概率更接近真实值(这是我们的训练循环中第 4 步的一半)。

当训练生成器时,我们将虚假样本输入到判别模型中,并获得样本是真实的概率(第 3 步的另一半)。然后我们稍微调整生成器网络中的权重,以便在下一个迭代中,预测概率更接近 1(因为生成器想要创建样本来欺骗判别器,使其认为它们是真实的);这构成了第 4 步的另一半。我们重复这个过程多次迭代,使生成器网络创建更真实的数据点。

一个自然的问题是何时停止训练 GANs。为此,你通过生成一组合成数据点并将其与训练数据集中的真实数据点进行比较来评估 GAN 的性能。在大多数情况下,我们使用可视化技术来评估生成的数据与所需关系的一致性。然而,在我们的运行示例中,由于我们知道训练数据的分布,我们可以计算生成数据与真实数据分布之间的均方误差(MSE)。当在固定数量的训练轮次后,生成的样本停止提高其质量时,我们停止训练 GANs。

到这一点,模型被认为是训练好的。然后我们丢弃判别器,保留生成器。为了创建指数增长曲线,我们将随机噪声向量 Z 输入到训练好的生成器中,并获取(x, y)对以形成所需的形状。

3.2 准备训练数据

在本节中,你将创建训练数据集,以便你可以使用它来训练本章后面的 GAN 模型。具体来说,你将创建符合指数增长形状的数据点对(x, y)。你将将它们放入批次中,以便它们可以准备好输入到深度神经网络中。

注意:本章以及本书其他章节的代码可在本书的 GitHub 仓库中找到:github.com/markhliu/DGAI。

3.2.1 形成指数增长曲线的训练数据集

我们将创建一个包含许多数据对观察结果的数据集,即 (x, y),其中 x 在区间 [0, 50] 内均匀分布,y 根据公式 y = 1.08^x 与 x 相关,如下所示。

列表 3.1 创建训练数据以形成指数增长形状

import torch

torch.manual_seed(0)                                ①

observations = 2048

train_data = torch.zeros((observations, 2))         ②

train_data[:,0]=50*torch.rand(observations)         ③

train_data[:,1]=1.08**train_data[:,0]               ④

① 固定随机状态以确保结果可重现

② 创建一个具有 2,048 行和 2 列的张量

③ 生成 0 到 50 之间的 x 的值

④ 根据关系 y = 1.08^x 生成 y 的值

首先,我们使用 torch.rand() 方法创建 0 到 50 之间 2,048 个 x 的值。我们使用 PyTorch 中的 manual_seed() 方法固定随机状态,以确保所有结果都是可重现的。我们首先创建一个具有 2,048 行和 2 列的 PyTorch 张量 train_data。x 的值放置在张量 train_data 的第一列中。PyTorch 中的 rand() 方法生成介于 0.0 和 1.0 之间的随机值。通过将值乘以 50,得到的 x 的值介于 0.0 和 50.0 之间。然后,我们将 y = 1.08^x 的值填充到 train_data 的第二列中。

练习 3.1

将列表 3.1 修改为使用 torch.sin() 函数,使 x 和 y 之间的关系为 y = sin(x)。通过以下代码行设置 x 的值为 -5 到 5:train_data[:,0]=10*(torch.rand(observations)-0.5)。

我们使用 Matplotlib 库绘制 x 和 y 之间的关系。

列表 3.2 可视化 x 和 y 之间的关系

import matplotlib.pyplot as plt

fig=plt.figure(dpi=100,figsize=(8,6))
plt.plot(train_data[:,0],train_data[:,1],".",c="r")    ①
plt.xlabel("values of x",fontsize=15)
plt.ylabel("values of $y=1.08^x$",fontsize=15)         ②
plt.title("An exponential growth shape",fontsize=20)   ③
plt.show()

① 绘制 x 和 y 之间的关系

② 标记 y 轴

③ 为图表创建标题

运行列表 3.2 后,您将看到一个指数增长曲线形状,类似于图 3.1 中的顶部图表。

练习 3.2

根据练习 3.1 中的更改,修改列表 3.2 以根据 x 和 y = sin(x) 的关系绘制图表。确保您更改图表中的 y 轴标签和标题,以反映您所做的更改。

3.2.2 准备训练数据集

我们将把您刚刚创建的数据样本放入批次中,以便我们可以将它们输入到判别网络中。我们使用 PyTorch 中的 DataLoader() 类将可迭代对象包装在训练数据集周围,这样我们就可以在训练过程中轻松访问样本,如下所示:

from torch.utils.data import DataLoader

batch_size=128
train_loader=DataLoader(
    train_data,
    batch_size=batch_size,
    shuffle=True)

确保您选择观察总数和批量大小,以便所有批次中的样本数量相同。我们选择了 2,048 个观察结果和 128 的批量大小。因此,我们有 2,048/128 = 16 个批次。DataLoader() 中的 shuffle=True 参数在将观察结果分成批次之前随机打乱观察结果。

注意:打乱数据确保数据样本均匀分布,并且批次内的观察结果不相关,这反过来又稳定了训练。在这个特定示例中,打乱确保 x 的值在 0 到 50 之间随机分布,而不是聚集在某个范围内,比如 0 到 5 之间。

你可以通过使用next()和iter()方法来访问一批数据,如下所示:

batch0=next(iter(train_loader))
print(batch0)

你将看到 128 对数字(x, y),其中 x 的值在 0 到 50 之间随机分布。此外,每对中的 x 和 y 的值符合关系 y = 1.08^x。

3.3 创建 GANs

现在训练数据集已经准备好了,我们将创建一个判别网络和一个生成网络。判别网络是一个二元分类器,这与我们在第二章中创建和训练的服装物品二元分类器非常相似。在这里,判别网络的职责是将样本分类为真实或伪造。另一方面,生成网络试图创建与训练集中那些不可区分的数据点(x, y),以便判别器将它们分类为真实。

3.3.1 判别网络

我们使用 PyTorch 创建判别神经网络。我们将使用带有ReLU激活的全连接(密集)层。我们还将使用 dropout 层来防止过拟合。我们在 PyTorch 中创建一个顺序深度神经网络来表示判别器,如下所示。

列表 3.3 创建判别网络

import torch.nn as nn

device="cuda" if torch.cuda.is_available() else "cpu"    ①

D=nn.Sequential(
    nn.Linear(2,256),                                    ②
    nn.ReLU(),
    nn.Dropout(0.3),                                     ③
    nn.Linear(256,128),
    nn.ReLU(),
    nn.Dropout(0.3),
    nn.Linear(128,64),
    nn.ReLU(),
    nn.Dropout(0.3),
    nn.Linear(64,1),                                     ④
    nn.Sigmoid()).to(device)

① 自动检查 CUDA 启用 GPU 是否可用。

② 第一层的输入特征数是 2,与每个数据实例中的元素数量相匹配,每个数据实例有两个值,x 和 y。

③ dropout 层可以防止过拟合。

④ 最后层的输出特征数是 1,这样我们就可以将其压缩到 0 到 1 之间的值。

确保在第一层中,输入形状是 2,因为在我们这个示例中,每个数据实例中包含两个值:x 和 y。第一层的输入数量应始终与输入数据的大小相匹配。同时,确保最后一层的输出特征数是 1:判别网络的输出是一个单一值。我们使用 sigmoid 激活函数将输出压缩到[0, 1]的范围内,以便它可以被解释为样本是真实的概率,p。使用互补概率 1 – p,样本是伪造的。这与我们在第二章中做的非常相似,当时二元分类器试图识别一件服装物品是长靴还是 T 恤。

隐藏层分别包含 256、128 和 64 个神经元。这些数字并没有什么神奇之处,只要在合理的范围内,你可以轻松地更改它们,并得到相似的结果。如果隐藏层中的神经元数量过多,可能会导致模型过拟合;如果数量过少,可能会导致模型欠拟合。可以通过使用验证集和超参数调整来单独优化神经元数量。

Dropout 层随机地使应用到的层中的某些神经元失效(或“丢弃”)。这意味着这些神经元在训练过程中不参与正向或反向传播。当模型不仅学习到训练数据中的潜在模式,还学习到噪声和随机波动时,就会发生过拟合,导致在未见过的数据上表现不佳。Dropout 层是防止过拟合的有效方法.^(2)

3.3.2 生成器网络

生成器的任务是创建一对数字(x,y),以便它可以通过判别器的筛选。也就是说,生成器试图创建一对数字,以最大化判别器认为这些数字来自训练数据集(即,它们符合关系 y = 1.08^x)的概率。我们以下列表中的神经网络来表示生成器。

列表 3.4 创建生成器网络

G=nn.Sequential(
    nn.Linear(2,16),               ①
    nn.ReLU(),
    nn.Linear(16,32),
    nn.ReLU(),
    nn.Linear(32,2)).to(device)    ②

① 第一层的输入特征数量为 2,与来自潜在空间的随机噪声向量的维度相同。

② 最后层的输出特征数量为 2,与数据样本的维度相同,包含两个值(x,y)。

我们将来自二维潜在空间(z[1],z[2])的随机噪声向量输入到生成器中。然后,生成器根据潜在空间的输入生成一对值(x,y)。在这里,我们使用二维潜在空间,但将维度更改为其他数字,如 5 或 10,不会影响我们的结果。

3.3.3 损失函数、优化器和早停

由于判别器网络本质上执行的是二元分类任务(识别数据样本为真实或假),因此我们为判别器网络使用二元交叉熵损失函数,这是二元分类中首选的损失函数。判别器试图最大化二元分类的准确性:将真实样本识别为真实,将假样本识别为假。判别器网络中的权重根据损失函数相对于权重的梯度进行更新。

生成器试图最小化假样本被识别为假样本的概率。因此,我们也将使用二元交叉熵损失函数来训练生成器网络:生成器更新其网络权重,使得生成的样本在二元分类问题中被判别器分类为真实样本。

正如我们在第二章中所做的那样,我们使用 Adam 优化器作为梯度下降算法。我们将学习率设置为 0.0005。让我们通过使用 PyTorch 来实现这些步骤:

loss_fn=nn.BCELoss()
lr=0.0005
optimD=torch.optim.Adam(D.parameters(),lr=lr)
optimG=torch.optim.Adam(G.parameters(),lr=lr)

在我们进行实际训练之前,还有一个问题:我们应该训练多少个 epoch?我们如何知道模型已经很好地训练,以至于生成器可以创建可以模仿指数增长曲线形状的样本?如果你还记得,在第二章中,我们将训练集进一步分为训练集和验证集。然后我们使用验证集中的损失来确定参数是否收敛,以便我们可以停止训练。然而,GANs 的训练方法与传统监督学习模型(如你在第二章中看到的分类模型)不同。由于生成样本的质量在整个训练过程中都在提高,判别器的任务变得越来越困难(在某种程度上,GANs 中的判别器是在对一个移动目标进行预测)。判别器网络的损失并不是模型质量的良好指标。

测量 GANs 性能的一种常见方法是通过视觉检查。人类可以通过简单地查看它们来评估生成数据实例的质量和真实性。这是一种定性方法,但可以提供非常有价值的信息。但在我们的简单案例中,因为我们知道训练数据集的确切分布,我们将查看生成样本相对于训练集中样本的均方误差(MSE),并将其用作生成器性能的衡量标准。让我们用代码来实现这一点:

mse=nn.MSELoss()                              ①

def performance(fake_samples):
    real=1.08**fake_samples[:,0]              ②
    mseloss=mse(fake_samples[:,1],real)       ③
    return mseloss

① 使用 MSE 作为衡量性能的标准

② 找出真实分布

③ 将生成的分布与真实分布进行比较,并计算 MSE

如果生成器的性能在 1,000 个 epoch 内没有提高,我们将停止训练模型。因此,我们定义了一个早期停止类,就像我们在第二章中所做的那样,以决定何时停止训练模型。

列表 3.5 一个早期停止类,用于决定何时停止训练

class EarlyStop:
    def __init__(self, patience=1000):       ①
        self.patience = patience
        self.steps = 0
        self.min_gdif = float('inf')
    def stop(self, gdif):                    ②
        if gdif < self.min_gdif:             ③
            self.min_gdif = gdif
            self.steps = 0
        elif gdif >= self.min_gdif:
            self.steps += 1
        if self.steps >= self.patience:      ④
            return True
        else:
            return False

stopper=EarlyStop()

① 将耐心(patience)的默认值设置为 1000

② 定义 stop() 方法

③ 当达到生成分布和真实分布之间新的最小差异时,更新 min_gdif 的值。

④ 如果模型在 1,000 个 epoch 内停止改进,则停止训练

这样,我们就拥有了训练我们的 GANs 所需的所有组件,我们将在下一节中进行训练。

3.4 使用 GANs 进行形状生成训练和利用

现在我们有了训练数据和两个网络,我们将训练模型。之后,我们将丢弃判别器,并使用生成器生成数据点以形成指数增长曲线形状。

3.4.1 GANs 的训练

我们首先为真实样本和假样本分别创建标签。具体来说,我们将所有真实样本标记为 1,所有假样本标记为 0。在训练过程中,判别器将其自己的预测与标签进行比较,以获得反馈,以便它可以调整模型参数,在下一个迭代中做出更好的预测。

在这里,我们定义了两个张量,real_labels 和 fake_labels:

real_labels=torch.ones((batch_size,1))
real_labels=real_labels.to(device)

fake_labels=torch.zeros((batch_size,1))
fake_labels=fake_labels.to(device)

张量 real_labels 是一个形状为 (batch_size, 1) 的二维张量——即 128 行和 1 列。我们使用 128 行,因为我们将会向判别网络提供 128 个真实样本以获得 128 个预测。同样,张量 fake_labels 也是一个形状为 (batch_size, 1) 的二维张量。我们将向判别网络提供 128 个假样本以获得 128 个预测,并将它们与真实标签:128 个 0 进行比较。如果你的计算机具有启用 CUDA 的 GPU,我们将这两个张量移动到 GPU 上以实现快速训练。

为了训练 GANs,我们定义了一些函数,以便训练循环看起来更有组织。第一个函数 train_D_on_real() 使用一批真实样本来训练判别网络。

列表 3.6 定义 train_D_on_real() 函数

def train_D_on_real(real_samples):
    real_samples=real_samples.to(device)
    optimD.zero_grad()
    out_D=D(real_samples)                    ①
    loss_D=loss_fn(out_D,real_labels)        ②
    loss_D.backward()
    optimD.step()                            ③
    return loss_D

① 对真实样本进行预测

② 计算损失

③ 反向传播(即更新判别网络中的模型权重,以便在下一个迭代中做出更准确的预测)

函数 train_D_on_real() 首先将真实样本移动到 GPU 上,如果计算机具有启用 CUDA 的 GPU。判别网络 D 对样本批次进行预测。然后模型将判别器的预测 out_D 与真实标签 real_labels 进行比较,并相应地计算预测的损失。backward() 方法计算损失函数相对于模型参数的梯度。step() 方法调整模型参数(即反向传播)。zero_grad() 方法表示我们在反向传播之前显式地将梯度设置为 0。否则,在每次 backward() 调用中都会使用累积的梯度而不是增量梯度。

TIP 在训练每个数据批次时更新模型权重之前,我们调用 zero_grad() 方法。我们在反向传播之前显式地将梯度设置为 0,以使用增量梯度而不是在每次 backward() 调用中累积的梯度。

第二个函数 train_D_on_fake() 使用一批假样本来训练判别网络。

列表 3.7 定义 train_D_on_fake() 函数

def train_D_on_fake():
    noise=torch.randn((batch_size,2))
    noise=noise.to(device)
    fake_samples=G(noise)                    ①
    optimD.zero_grad()
    out_D=D(fake_samples)                    ②
    loss_D=loss_fn(out_D,fake_labels)        ③
    loss_D.backward()
    optimD.step()                            ④
    return loss_D

① 生成一批假样本

② 对假样本进行预测

③ 计算损失

④ 反向传播

函数train_D_on_fake()首先将来自潜在空间的一批随机噪声向量输入到生成器中,以获得一批假样本。然后,该函数将假样本展示给判别器以获取预测结果。该函数将判别器的预测结果out_D与真实标签fake_labels进行比较,并相应地计算预测的损失。最后,它根据损失函数相对于模型权重的梯度调整模型参数。

注意:我们互换使用术语权重和参数。严格来说,模型参数还包括偏置项,但我们使用术语模型权重来松散地包括模型偏置。同样,我们互换使用术语调整权重、调整参数和反向传播。

第三个函数train_G()使用一批假样本训练生成器网络。

列表 3.8 定义train_G()函数

def train_G(): 
    noise=torch.randn((batch_size,2))
    noise=noise.to(device)
    optimG.zero_grad()
    fake_samples=G(noise)                     ①
    out_G=D(fake_samples)                     ②
    loss_G=loss_fn(out_G,real_labels)         ③
    loss_G.backward()
    optimG.step()                             ④
    return loss_G, fake_samples 

① 创建一批假样本

② 向判别器展示假样本以获取预测结果

③ 根据 G 是否成功计算损失

④ 反向传播(即更新生成器网络中的权重,以便在下一迭代中生成的样本更加逼真)

为了训练生成器,我们首先将来自潜在空间的一批随机噪声向量输入到生成器中,以获得一批假样本。然后,我们将这些假样本展示给判别器网络以获取一批预测结果。我们比较判别器的预测结果与real_labels(一个全为 1 的张量)进行比较,并计算损失。使用全为 1 的张量而不是全为 0 的张量作为标签非常重要,因为生成器的目标是欺骗判别器,使其认为假样本是真实的。最后,我们根据损失函数相对于模型权重的梯度调整模型参数,以便在下一迭代中,生成器可以创建更加逼真的样本。

注意:在计算损失和评估生成器网络时,我们使用real_labels(一个全为 1 的张量)而不是fake_labels(一个全为 0 的张量),因为生成器希望判别器将假样本预测为真实样本。

最后,我们定义了一个函数test_epoch(),该函数定期打印出判别器和生成器的损失。此外,它绘制了生成器生成的数据点,并将它们与训练集中的数据点进行比较。test_epoch()函数的代码如下所示。

列表 3.9 定义test_epoch()函数

import os
os.makedirs("files", exist_ok=True)                           ①

def test_epoch(epoch,gloss,dloss,n,fake_samples):
    if epoch==0 or (epoch+1)%25==0:
        g=gloss.item()/n
        d=dloss.item()/n
        print(f"at epoch {epoch+1}, G loss: {g}, D loss {d}") ②
        fake=fake_samples.detach().cpu().numpy()
        plt.figure(dpi=200)
        plt.plot(fake[:,0],fake[:,1],"*",c="g",
            label="generated samples")                        ③
        plt.plot(train_data[:,0],train_data[:,1],".",c="r",
            alpha=0.1,label="real samples")                   ④
        plt.title(f"epoch {epoch+1}")
        plt.xlim(0,50)
        plt.ylim(0,50)
        plt.legend()
        plt.savefig(f"files/p{epoch+1}.png")
        plt.show()

① 创建一个文件夹来存放文件

② 定期打印出损失

③ 将生成的点以星号(*)的形式绘制出来

④ 将训练数据以点(.)的形式绘制出来

每过 25 个周期,函数会在该周期打印出生成器和判别器的平均损失。此外,它还会绘制生成器生成的一批假数据点(用星号表示)并与训练集中的数据点(用点表示)进行比较。该图被保存为图像保存在你的本地文件夹 /files/ 中。

现在我们已经准备好训练模型了。我们遍历训练数据集中的所有批次。对于每一批数据,我们首先使用真实样本来训练判别器。之后,生成器创建一批假样本,我们再次使用这些样本来训练判别器。最后,我们让生成器再次创建一批假样本,但这次,我们使用它们来训练生成器。我们训练模型直到满足早期停止条件,如下所示。

列表 3.10 训练生成指数增长曲线的 GAN

for epoch in range(10000):                                  ①
    gloss=0
    dloss=0
    for n, real_samples in enumerate(train_loader):         ②
        loss_D=train_D_on_real(real_samples)
        dloss+=loss_D
        loss_D=train_D_on_fake()
        dloss+=loss_D
        loss_G,fake_samples=train_G()
        gloss+=loss_G
    test_epoch(epoch,gloss,dloss,n,fake_samples)            ③
    gdif=performance(fake_samples).item()
    if stopper.stop(gdif)==True:                            ④
        break

① 开始训练循环

② 遍历训练数据集中的所有批次

③ 定期展示生成的样本

④ 确定是否停止训练

如果你使用 GPU 训练,训练可能需要几分钟。否则,可能需要 20 到 30 分钟,具体取决于你电脑的硬件配置。或者,你可以从本书的 GitHub 仓库下载训练好的模型:github.com/markhliu/DGAI。

经过 25 个训练周期后,生成的数据点围绕点 (0,0) 散布,并没有形成任何有意义的形状(一个周期是指所有训练数据被用于训练一次)。经过 200 个训练周期后,数据点开始形成指数增长曲线的形状,尽管许多点远离由训练集点形成的虚线曲线。经过 1,025 个训练周期后,生成的点与指数增长曲线非常吻合。图 3.2 提供了六个不同周期的输出子图。我们的 GAN 工作得非常好:生成器能够生成形成所需形状的数据点。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH03_F02_Liu.png

图 3.2 训练过程中不同阶段的生成形状(图中星号所示)与真实指数增长曲线形状(图中点所示)的比较子图。在第 25 个周期,生成的样本没有形成任何有意义的形状。在第 200 个周期,样本开始看起来像指数增长曲线的形状。在第 1,025 个周期,生成的样本与指数增长曲线非常吻合。

3.4.2 保存和使用训练好的生成器

既然 GAN 已经训练完成,我们将丢弃判别器网络,就像在 GAN 中我们通常所做的那样,并将训练好的生成器网络保存在本地文件夹中,如下所示:

import os
os.makedirs("files", exist_ok=True)
scripted = torch.jit.script(G) 
scripted.save('files/exponential.pt') 

torch.jit.script() 方法使用 TorchScript 编译器将函数或 nn.Module 类脚本化为 TorchScript 代码。我们使用此方法将训练好的生成器网络脚本化,并将其保存为文件 exponential.pt 在你的电脑上。

要使用生成器,我们甚至不需要定义模型。我们只需加载保存的文件,并使用它来生成数据点,如下所示:

new_G=torch.jit.load('files/exponential.pt',
                     map_location=device)
new_G.eval()

训练好的生成器现在已加载到你的设备上,这取决于你是否在电脑上安装了 CUDA 支持的 GPU,它可能是CPU或CUDA。torch.jit.load()中的map_location=device参数指定了加载生成器的位置。现在我们可以使用训练好的生成器生成一批数据点:

noise=torch.randn((batch_size,2)).to(device)
new_data=new_G(noise) 

在这里,我们首先从潜在空间中获取一批随机噪声向量。然后,我们将它们输入到生成器中以生成假数据。我们可以绘制生成的数据:

fig=plt.figure(dpi=100)
plt.plot(new_data.detach().cpu().numpy()[:,0],
  new_data.detach().cpu().numpy()[:,1],"*",c="g",
        label="generated samples")                    ①
plt.plot(train_data[:,0],train_data[:,1],".",c="r",
         alpha=0.1,label="real samples")              ②
plt.title("Inverted-U Shape Generated by GANs")
plt.xlim(0,50)
plt.ylim(0,50)
plt.legend()
plt.show()

① 以星号形式绘制生成的数据样本

② 以点形式绘制训练数据

你应该看到一个类似于图 3.2 中最后一个子图的图表:生成的数据样本非常接近指数增长曲线。

恭喜!你已经创建并训练了你的第一个 GAN。掌握了这项技能,你可以轻松地修改代码,使生成的数据匹配其他形状,如正弦、余弦、U 形等。

练习 3.3

修改第一个项目中的程序,使生成器生成在 x = –5 和 x = 5 之间形成正弦形状的数据样本。当你绘制数据样本时,将 y 的值设置为–1.2 到 1.2 之间。

3.5 使用模式生成数字

在这个第二个项目中,你将构建和训练 GANs 以生成 0 到 99 之间所有都是 5 的倍数的 10 个整数的序列。涉及的主要步骤与生成指数增长曲线的步骤类似,只是训练集不是具有两个值(x, y)的数据点。相反,训练数据集是 0 到 99 之间所有都是 5 的倍数的整数序列。

在本节中,你将首先学习将训练数据转换为神经网络可以理解的格式:独热变量。进一步,你将把独热变量转换回 0 到 99 之间的整数,这样人类就可以更容易理解。因此,你实际上是在将数据在人类可读格式和模型准备格式之间进行转换。之后,你将创建一个判别器和生成器,并训练 GANs。你还将使用早期停止来确定何时结束训练。然后,你将丢弃判别器,并使用训练好的生成器创建一个具有你想要的模式的整数序列。

3.5.1 什么是独热变量?

独热编码是机器学习和数据预处理中用于将分类数据表示为二进制向量的技术。分类数据由类别或标签组成,如颜色、动物类型或城市,它们本身不是数值。机器学习算法通常处理数值数据,因此将分类数据转换为数值格式是必要的。

想象一下,你正在处理一个分类特征——例如,房屋的颜色,它可以取“红色”、“绿色”和“蓝色”等值。使用 one-hot 编码,每个类别都表示为一个二进制向量。你将创建三个二进制列,每个类别一个。颜色“红色”被 one-hot 编码为[1, 0, 0],“绿色”为[0, 1, 0],而“蓝色”为[0, 0, 1]。这样做可以保留分类信息,而不在类别之间引入任何顺序关系。每个类别都被视为独立的。

在这里,我们定义了一个onehot_encoder()函数,用于将整数转换为 one-hot 变量:

import torch
def onehot_encoder(position,depth):
    onehot=torch.zeros((depth,))
    onehot[position]=1
    return onehot

该函数接受两个参数:第一个参数position是值被打开为 1 的索引,第二个参数depth是 one-hot 变量的长度。例如,如果我们打印出onehot_encoder(1,5)的值,它将看起来像这样:

print(onehot_encoder(1,5))

结果是

tensor([0., 1., 0., 0., 0.])

结果显示一个五值张量,第二个位置(其索引值为 1)被打开为 1,其余位置被关闭为 0。

现在你已经了解了 one-hot 编码的工作原理,你可以将 0 到 99 之间的任何整数转换为 one-hot 变量:

def int_to_onehot(number):
    onehot=onehot_encoder(number,100)
    return onehot

让我们使用该函数将数字 75 转换为 100 值张量:

onehot75=int_to_onehot(75)
print(onehot75)

输出是

tensor([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0.])

结果是一个 100 值张量,第 76 位(其索引值为 75)被打开为 1,其余位置被关闭为 0。

函数int_to_onehot()将整数转换为 one-hot 变量。从某种意义上说,该函数是将人类可读语言转换为模型准备语言。

接下来,我们希望将模型准备语言翻译回人类可读语言。假设我们有一个 one-hot 变量:我们如何将其转换为人类可理解的整数?以下函数onehot_to_int()实现了这一目标:

def onehot_to_int(onehot):
    num=torch.argmax(onehot)
    return num.item()

函数onehot_to_int()接受onehot参数,并根据哪个位置具有最高值将其转换为整数。

让我们测试一下函数,看看如果我们使用刚刚创建的onehot75张量作为输入会发生什么:

print(onehot_to_int(onehot75))

输出是

75

结果显示,该函数将 one-hot 变量转换为整数 75,这是正确答案。因此,我们知道这些函数已正确定义。

接下来,我们将构建和训练 GAN 以生成 5 的倍数。

3.5.2 使用 GAN 生成具有模式的数字

我们的目标是构建和训练一个模型,以便生成器可以生成一个由 10 个整数组成的序列,它们都是 5 的倍数。我们首先准备训练数据,然后将它们分批转换为模型准备好的数字。最后,我们使用训练好的生成器生成我们想要的模式。

为了简单起见,我们将生成 0 到 99 之间的 10 个整数序列。然后,我们将该序列转换为 10 个模型准备好的数字。

以下函数生成一个由 10 个整数组成的序列,它们都是 5 的倍数:

def gen_sequence():
    indices = torch.randint(0, 20, (10,))
    values = indices*5
    return values   

我们首先使用 PyTorch 中的randint()方法生成 0 到 19 之间的 10 个数字。然后我们将它们乘以 5 并转换为 PyTorch 张量。这创建了 10 个都是 5 的倍数的整数。

让我们尝试生成一个训练数据序列:

sequence=gen_sequence()
print(sequence)

输出如下

tensor([60, 95, 50, 55, 25, 40, 70,  5,  0, 55])

前面的输出中的所有值都是 5 的倍数。

接下来,我们将每个数字转换为独热变量,以便我们可以在以后将其输入到神经网络中:

import numpy as np

def gen_batch():
    sequence=gen_sequence()                            ①
    batch=[int_to_onehot(i).numpy() for i in sequence] ②
    batch=np.array(batch)
    return torch.tensor(batch)
batch=gen_batch()

① 创建一个由 10 个数字组成的序列,这些数字都是 5 的倍数

② 将每个整数转换为 100 值的独热变量

前面的函数gen_batch()创建了一组数据,以便我们可以将其输入到神经网络进行训练。

我们还定义了一个函数data_to_num(),用于将独热变量转换为一系列整数,以便人类可以理解输出:

def data_to_num(data):
    num=torch.argmax(data,dim=-1)                      ①
    return num
numbers=data_to_num(batch)                             ②

① 根据 100 值向量中的最大值将向量转换为整数

② 在一个示例上应用该函数

torch.argmax()函数中的dim=-1参数意味着我们试图找到最后一个维度(即索引)中最大值的位(即位置):即在 100 值的独热向量中,哪个位置具有最高值。

接下来,我们将创建两个神经网络:一个用于判别器 D,另一个用于生成器 G。我们将构建 GAN 来生成所需的数字模式。类似于本章前面所做的那样,我们创建了一个判别器网络,它是一个二元分类器,用于区分假样本和真实样本。我们还创建了一个生成器网络来生成一系列 10 个数字。以下是判别器神经网络:

from torch import nn
D=nn.Sequential(
    nn.Linear(100,1),
    nn.Sigmoid()).to(device)

由于我们将整数转换为 100 值的独热变量,我们在模型的第一层Linear中使用 100 作为输入大小。最后一层Linear只有一个输出特征,我们使用 sigmoid 激活函数将输出挤压到[0, 1]的范围内,以便它可以被解释为样本是真实的概率,p。使用互补概率 1 – p,样本是假的。

生成器的任务是创建一个数字序列,以便它们可以通过判别器 D。也就是说,G 试图创建一个数字序列,以最大化 D 认为这些数字来自训练数据集的概率。

我们创建了以下神经网络来表示生成器 G:

G=nn.Sequential(
    nn.Linear(100,100),
    nn.ReLU()).to(device)

我们将向生成器输入来自 100 维潜在空间的随机噪声向量。生成器随后根据输入创建一个包含 100 个值的张量。注意,在这里我们使用ReLU激活函数在最后一层,以确保输出为非负值。由于我们试图生成 100 个 0 或 1 的值,非负值在这里是合适的。

如同第一个项目一样,我们使用 Adam 优化器对判别器和生成器进行优化,学习率为 0.0005:

loss_fn=nn.BCELoss()
lr=0.0005
optimD=torch.optim.Adam(D.parameters(),lr=lr)
optimG=torch.optim.Adam(G.parameters(),lr=lr)

现在我们有了训练数据和两个网络,我们将训练模型。之后,我们将丢弃判别器并使用生成器生成一个 10 个整数的序列。

3.5.3 训练 GAN 生成具有模式的数字

本项目的训练过程与我们第一个项目中生成指数增长形状的项目非常相似。

我们定义了一个函数train_D_G(),它是我们为第一个项目定义的三个函数train_D_on_real()、train_D_on_fake()和train_G()的组合。函数train_D_G()在本书 GitHub 仓库的该章节 Jupyter Notebook 中:github.com/markhliu/DGAI。查看函数train_D_G(),你可以看到我们与第一个项目定义的三个函数相比做了哪些细微的修改。

我们使用与第一个项目定义相同的早期停止类,这样我们就可以知道何时停止训练。然而,当我们实例化类时,我们将patience参数修改为 800,如以下列表所示。

列表 3.11 训练 GAN 生成 5 的倍数

stopper=EarlyStop(800)                                  ①

mse=nn.MSELoss()
real_labels=torch.ones((10,1)).to(device)
fake_labels=torch.zeros((10,1)).to(device)
def distance(generated_data):                           ②
    nums=data_to_num(generated_data)
    remainders=nums%5
    ten_zeros=torch.zeros((10,1)).to(device)
    mseloss=mse(remainders,ten_zeros)
    return mseloss

for i in range(10000):
    gloss=0
    dloss=0
    generated_data=train_D_G(D,G,loss_fn,optimD,optimG) ③
    dis=distance(generated_data)
    if stopper.stop(dis)==True:
        break   
    if i % 50 == 0:
        print(data_to_num(generated_data))              ④

① 创建早期停止类的实例

② 定义一个 distance()函数来计算生成数字中的损失

③ 训练 GAN 进行一个 epoch 的训练

④ 每过 50 个 epoch 打印出生成的整数序列

我们还定义了一个distance()函数来衡量训练集和生成数据样本之间的差异:它计算每个生成的数字除以 5 的余数的均方误差。当所有生成的数字都是 5 的倍数时,该度量值为 0。

如果你运行前面的代码单元,你会看到以下输出:

tensor([14, 34, 19, 89, 44,  5, 58,  6, 41, 87], device='cuda:0')
… 
tensor([ 0, 80, 65,  0,  0, 10, 80, 75, 75, 75], device='cuda:0')
tensor([25, 30,  0,  0, 65, 20, 80, 20, 80, 20], device='cuda:0')
tensor([65, 95, 10, 65, 75, 20, 20, 20, 65, 75], device='cuda:0')

在每次迭代中,我们生成一批 10 个数字。我们首先使用真实样本训练判别器 D。然后,生成器创建一批假样本,我们再次使用它们来训练判别器 D。最后,我们让生成器再次创建一批假样本,但这次我们使用它们来训练生成器 G。如果生成器网络在达到最小损失后的 800 个 epoch 内停止改进,我们将停止训练。每过 50 个 epoch,我们打印出由生成器创建的 10 个数字序列,这样你就可以判断它们是否确实是 5 的倍数。

训练过程中的输出如图所示。在前几百个 epoch 中,生成器仍然产生不是 5 的倍数的数字。但在 900 个 epoch 之后,所有生成的数字都是 5 的倍数。使用 GPU 训练时,训练过程只需一分钟左右。如果你使用 CPU 训练,则不到 10 分钟。或者,你可以从书籍的 GitHub 仓库下载训练好的模型:github.com/markhliu/DGAI。

3.5.4 保存和使用训练好的模型

我们将丢弃判别器并将训练好的生成器保存在本地文件夹中:

import os
os.makedirs("files", exist_ok=True)
scripted = torch.jit.script(G) 
scripted.save('files/num_gen.pt') 

我们现在已经将生成器保存到了本地文件夹。要使用生成器,我们只需加载模型并使用它来生成一系列整数:

new_G=torch.jit.load('files/num_gen.pt',
                     map_location=device)             ①
new_G.eval()
noise=torch.randn((10,100)).to(device)                ②
new_data=new_G(noise)                                 ③
print(data_to_num(new_data))

① 加载保存的生成器

② 获取随机噪声向量

③ 将随机噪声向量输入到训练好的模型中以生成一系列整数

输出如下:

tensor([40, 25, 65, 25, 20, 25, 95, 10, 10, 65], device='cuda:0')

生成的数字都是 5 的倍数。

你可以轻松地更改代码以生成其他模式,例如奇数、偶数、3 的倍数等等。

练习 3.4

修改第二个项目的程序,使生成器生成一个由所有都是 3 的倍数的十个整数组成的序列。

现在你已经了解了 GANs 的工作原理,你将在后面的章节中能够将 GANs 的理念扩展到其他格式,包括高分辨率图像和听起来逼真的音乐。

摘要

  • GANs 由两个网络组成:一个用于区分伪造样本和真实样本的判别器,以及一个用于创建与训练集中样本不可区分的样本的生成器。

  • GANs 的步骤包括准备训练数据、创建判别器和生成器、训练模型并决定何时停止训练,最后,丢弃判别器并使用训练好的生成器来创建新的样本。

  • GANs 生成的内容取决于训练数据。当训练数据集包含形成指数增长曲线的数据对 (x, y) 时,生成的样本也是模仿这种形状的数据对。当训练数据集包含所有都是 5 的倍数的数字序列时,生成的样本也是包含 5 的倍数的数字序列。

  • GANs 是多才多艺的,能够生成许多不同格式的内 容。


^(1)  Goodfellow 等人,2014 年,“Generative Adversarial Nets.” arxiv.org/abs/1406.2661.

^(2)  尼提什·斯里瓦斯塔瓦(Nitish Srivastava)、杰弗里·辛顿(Geoffrey Hinton)、亚历克斯·克里泽夫斯基(Alex Krizhevsky)、伊利亚·苏茨克维(Ilya Sutskever)和鲁斯兰·萨拉胡丁诺夫(Ruslan Salakhutdinov),2014 年,“Dropout: A Simple Way to Prevent Neural Networks from Overfitting.” 《机器学习研究杂志》 15 (56): 1929−1958.

第二部分. 图像生成

第二部分深入探讨了图像生成。

在第四章中,你将学习如何构建和训练生成对抗网络以生成高分辨率的彩色图像。特别是,你将学习如何使用卷积神经网络来捕捉图像中的空间特征。你还将学习如何使用转置卷积层在图像中上采样并生成高分辨率特征图。在第五章中,你将学习两种在生成图像中选择特征的方法。在第六章中,你将学习构建和训练一个 CycleGAN,以在两个领域之间转换图像,例如黑发图像和金发图像或马图像和斑马图像。在第七章中,你将学习使用另一种生成模型:自编码器及其变体,变分自编码器来创建图像。

第四章:使用生成对抗网络进行图像生成

本章涵盖

  • 通过镜像判别网络中的步骤来设计生成器

  • 2D 卷积操作在图像上的工作原理

  • 2D 转置卷积操作如何在输出值之间插入间隔并生成更高分辨率的特征图

  • 构建和训练生成对抗网络以生成灰度和彩色图像

在第三章中,你已经成功生成了一个指数增长曲线和一系列都是 5 的倍数的整数。现在,既然你已经理解了生成对抗网络(GANs)的工作原理,你就可以应用相同的技能来生成许多其他形式的内容,例如高分辨率彩色图像和听起来逼真的音乐。然而,这说起来容易做起来难(你知道他们怎么说:魔鬼藏在细节里)。例如,我们究竟如何让生成器凭空创造出逼真的图像呢?这正是本章将要解决的问题。

生成器从零开始创建图像的常见方法是通过镜像判别网络中的步骤。在本章的第一个项目中,你的目标是创建如外套、衬衫、凉鞋等服装物品的灰度图像。你将学习在设计生成器网络时如何镜像判别网络中的层。在这个项目中,生成器和判别网络都只使用了密集层。密集层中的每个神经元都与前一层的每个神经元和后一层的每个神经元相连。因此,密集层也被称为全连接层。

在本章的第二个项目中,你的目标是创建动漫面孔的高分辨率彩色图像。就像在第一个项目中一样,生成器通过镜像判别网络中的步骤来生成图像。然而,在这个项目中,高分辨率彩色图像包含的像素比第一个项目中的低分辨率灰度图像要多得多。如果我们只使用密集层,模型的参数数量会急剧增加。这反过来又使得学习变得缓慢且无效。因此,我们转向卷积神经网络(CNNs)。在 CNNs 中,一个层中的每个神经元仅与输入的小区域相连。这种局部连接减少了参数数量,使得网络更加高效。与相同大小的全连接网络相比,CNNs 需要的参数更少,这导致了更快的训练时间和更低的计算成本。CNNs 通常在捕捉图像数据中的空间层次结构方面也更为有效,因为它们将图像视为多维对象,而不是一维向量。

为了让你为第二个项目做好准备,我们将向你展示卷积操作是如何工作的,以及它们如何对输入图像进行下采样并从中提取空间特征。你还将学习诸如滤波器大小、步长和零填充等概念,以及它们如何影响 CNN 中的下采样程度。虽然判别器网络使用卷积层,但生成器通过使用转置卷积层(也称为反卷积或上采样层)来镜像这些层。你将学习转置卷积层是如何用于上采样以生成高分辨率特征图的。

总结来说,你将在本章学习如何镜像判别器网络的步骤,从头开始创建图像。此外,你还将学习卷积层和转置卷积层的工作原理。在本章之后,你将使用卷积层和转置卷积层在本书的其他设置中创建高分辨率图像(例如,在训练 CycleGAN 将金发转换为黑发时进行特征转换,或在变分自编码器[VAE]中生成高分辨率人脸图像)。

4.1 使用 GAN 生成服装物品的灰度图像

我们在第一个项目中的目标是训练一个模型,生成如凉鞋、T 恤、外套和包等服装物品的灰度图像。

当你使用 GAN 生成图像时,你将始终从获取训练数据开始。然后,你将从头创建一个判别器网络。在创建生成器网络时,你将镜像判别器网络中的步骤。最后,你将训练 GAN 并使用训练好的模型进行图像生成。让我们通过一个简单的项目来看看这是如何工作的,该项目生成服装物品的灰度图像。

4.1.1 训练样本和判别器

准备训练数据所涉及的步骤与我们在第二章中做的类似,但有几点例外,我稍后会强调。为了节省时间,我将跳过你在第二章中已经看到的步骤,并指导你参考书籍的 GitHub 仓库。按照书籍 GitHub 仓库中该章节的 Jupyter Notebook 中的步骤进行操作(github.com/markhliu/DGAI),以便你创建一个带有批次的迭代器。

训练集中有 60,000 张图像。在第二章中,我们将训练集进一步分为训练集和验证集。我们使用验证集中的损失来确定参数是否收敛,以便我们可以停止训练。然而,与传统的监督学习模型(如第二章中看到的分类模型)相比,GANs 的训练采用不同的方法。由于生成的样本质量在整个训练过程中不断提高,判别器的任务变得越来越困难。判别器网络的损失并不是模型质量的良好指标。衡量 GANs 性能的通常方法是通过视觉检查来评估生成图像的质量和逼真度。我们可以将生成样本的质量与训练样本进行比较,并使用诸如 Inception Score 等方法来评估 GANs 的性能(例如,参见 Ali Borji 于 2018 年撰写的“GAN 评估措施的优缺点”,对各种 GAN 评估方法进行了调查;arxiv.org/abs/1802.03446)。然而,研究人员已经记录了这些措施的弱点(“关于 Inception Score 的笔记”,由 Shane Barratt 和 Rishi Sharma 于 2018 年撰写,表明在比较模型时,inception score 无法提供有用的指导;arxiv.org/abs/1801.01973)。在本章中,我们将定期进行视觉检查以检查生成样本的质量,并确定何时停止训练。

判别器网络是一个二元分类器,这与我们在第二章中讨论的服装项目的二元分类器相似。在这里,判别器的任务是分类样本为真实或假。

我们使用 PyTorch 创建以下判别器神经网络 D:

import torch
import torch.nn as nn

device="cuda" if torch.cuda.is_available() else "cpu"
D=nn.Sequential(
    nn.Linear(784, 1024),          ①
    nn.ReLU(),
    nn.Dropout(0.3),
    nn.Linear(1024, 512),
    nn.ReLU(),
    nn.Dropout(0.3),
    nn.Linear(512, 256),
    nn.ReLU(),
    nn.Dropout(0.3),
    nn.Linear(256, 1),    
    nn.Sigmoid()).to(device)       ②

① 第一个全连接层有 784 个输入和 1,024 个输出。

② 最后一个全连接层有 256 个输入和 1 个输出。

输入大小为 784,因为训练集中每个灰度图像的大小为 28 × 28 像素。由于密集层只接受 1D 输入,我们在将图像输入模型之前将其展平。输出层只有一个神经元:判别器 D 的输出是一个单一值。我们使用 sigmoid 激活函数将输出挤压到[0, 1]的范围内,以便它可以解释为样本是真实的概率,p。使用互补概率 1 – p,样本是假的。

练习 4.1

修改判别器 D,使其前三个层的输出数量分别为 1,000、500 和 200,而不是 1,024、512 和 256。确保一个层的输出数量与下一层的输入数量相匹配。

4.1.2 生成灰度图像的生成器

虽然创建判别器网络相对简单,但如何创建一个生成器以生成逼真的图像则是另一回事。一种常见的方法是镜像判别器网络中使用的层来创建生成器,如下面的列表所示。

列表 4.1 通过镜像判别器中的层来设计生成器

G=nn.Sequential(
    nn.Linear(100, 256),         ①
    nn.ReLU(),
    nn.Linear(256, 512),         ②
    nn.ReLU(),
    nn.Linear(512, 1024),        ③
    nn.ReLU(),
    nn.Linear(1024, 784),        ④
    nn.Tanh()).to(device)        ⑤

① 生成器的第一层与判别器的最后一层是对称的。

② 生成器的第二层与判别器的倒数第二层是对称的(输入和输出的位置已交换)。

③ 生成器的第三层与判别器的倒数第三层是对称的。

④ 生成器的最后一层与判别器的第一层是对称的。

⑤ 使用 Tanh()激活函数,使得输出值在-1 和 1 之间,与图像中的值相同

图 4.1 展示了 GAN 生成服装物品灰度图像的生成器和判别器网络的架构图。如图 4.1 右上角所示,来自训练集的平面灰度图像,包含 28 × 28 = 784 个像素,在判别器网络中依次通过四个密集层,输出是图像为真实的概率。为了创建图像,生成器使用相同的四个密集层,但顺序相反:它从潜在空间(图 4.1 左下角)获得一个 100 值的随机噪声向量,并将向量通过四个密集层。在每个层中,判别器中的输入和输出数量被反转,并用作生成器的输出和输入数量。最后,生成器得到一个 784 值的张量,可以被重塑为一个 28 × 28 的灰度图像(图 4.1 左上角)。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH04_F01_Liu.png

图 4.1 通过镜像判别器网络中的层来设计生成器网络以创建服装物品。图例的右侧显示了判别器网络,其中包含四个密集层。为了设计一个可以从无中生有地创造出服装物品的生成器,我们镜像了判别器网络中的层。具体来说,如图的左侧所示,生成器中有四个类似的密集层,但顺序相反:生成器的第一层镜像判别器的最后一层,生成器的第二层镜像判别器的倒数第二层,以此类推。此外,在顶部三个层中,判别器中的输入和输出数量被反转,并用作生成器的输出和输入数量。

图 4.1 的左侧是生成器网络,而右侧是判别器网络。如果你比较这两个网络,你会注意到生成器如何镜像判别器中使用的层。具体来说,生成器中有四个相似的密集层,但顺序相反:生成器中的第一层反映了判别器中的最后一层,生成器中的第二层反映了判别器中的倒数第二层,以此类推。生成器的输出数量为 784,经过Tanh()激活后的值介于-1 和 1 之间,这与判别器网络的输入相匹配。

练习 4.2

修改生成器 G,使其前三个层的输出数量分别为 1,000、500 和 200,而不是 1,024、512 和 256。确保修改后的生成器与修改后的判别器在练习 4.1 中使用的层相匹配。

如同我们在第三章中看到的 GAN 模型,损失函数是二元交叉熵损失,因为判别器 D 执行的是二元分类问题。我们将使用 Adam 优化器来优化判别器和生成器,学习率为 0.0001:

loss_fn=nn.BCELoss()
lr=0.0001
optimD=torch.optim.Adam(D.parameters(),lr=lr)
optimG=torch.optim.Adam(G.parameters(),lr=lr)  

接下来,我们将使用训练数据集中的服装物品图像来训练我们刚刚创建的 GANs。

4.1.3 训练 GANs 生成服装物品的图像

训练过程与我们第三章训练 GANs 生成指数增长曲线或生成所有都是 5 的倍数的数字序列时所做的类似。

与第三章不同,我们将仅依靠视觉检查来确定模型是否训练良好。为此,我们定义了一个 see_output() 函数,以定期可视化生成器创建的假图像。

注意:感兴趣的读者可以查看这个 GitHub 仓库,了解如何在 PyTorch 中实现 inception score 以评估 GANs:github.com/sbarratt/inception-score-pytorch。然而,该仓库不建议使用 inception score 来评估生成模型,因为它效果不佳。

列表 4.2 定义一个函数来可视化生成的服装物品

import matplotlib.pyplot as plt

def see_output():
    noise=torch.randn(32,100).to(device=device)
    fake_samples=G(noise).cpu().detach()                 ①
    plt.figure(dpi=100,figsize=(20,10))
    for i in range(32):
        ax=plt.subplot(4, 8, i + 1)                      ②
        img=(fake_samples[i]/2+0.5).reshape(28, 28)
        plt.imshow(img)                                  ③
        plt.xticks([])
        plt.yticks([])
    plt.show()

see_output()                                             ④

① 生成 32 张假图像

② 将它们绘制在 4 × 8 的网格中

③ 显示第 i 张图像

④ 在训练前调用 see_output() 函数来可视化生成的图像

如果你运行前面的代码单元,你会看到 32 张看起来像电视屏幕上的雪花静态图像,如图 4.2 所示。它们根本不像服装物品,因为我们还没有训练生成器。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH04_F02_Liu.png

图 4.2 GAN 模型在训练前生成服装物品的输出。由于模型尚未训练,生成的图像与训练集中的图像毫不相像。

要训练 GAN 模型,我们定义了几个函数:train_D_on_real()、train_D_on_fake() 和 train_G()。它们与第三章中定义的类似。请访问本书 GitHub 仓库中本章的 Jupyter Notebook,查看我们做了哪些细微的修改。

现在我们已准备好训练模型。我们遍历训练数据集中的所有批次。对于每个数据批次,我们首先使用真实样本训练判别器。之后,生成器创建一个假样本批次,我们再次使用它们来训练判别器。最后,我们让生成器再次创建一个假样本批次,但这次我们使用它们来训练生成器。我们训练模型 50 个周期,如下所示。

列表 4.3 训练 GAN 以生成服装项目

for i in range(50):    
    gloss=0
    dloss=0
    for n, (real_samples,_) in enumerate(train_loader):
        loss_D=train_D_on_real(real_samples)            ①
        dloss+=loss_D
        loss_D=train_D_on_fake()                        ②
        dloss+=loss_D
        loss_G=train_G()                                ③
        gloss+=loss_G
    gloss=gloss/n
    dloss=dloss/n    
    if i % 10 == 9:
        print(f"at epoch {i+1}, dloss: {dloss}, gloss {gloss}")
        see_output()                                    ④

① 使用真实样本训练判别器

② 使用假样本训练判别器

③ 训练生成器

④ 每 10 个周期后可视化生成的样本

如果你使用 GPU 训练,训练大约需要 10 分钟。否则,可能需要一个小时左右,具体取决于你电脑的硬件配置。或者你可以从我的网站上下载训练好的模型:gattonweb.uky.edu/faculty/lium/gai/fashion_gen.zip。下载后请解压。

在每 10 个周期的训练后,你可以可视化生成的服装项目,如图 4.3 所示。仅仅经过 10 个周期的训练,模型就能生成可以明显以真品身份出现的服装项目:你可以辨认出它们。图 4.3 第一行前三个项目显然是一件外套、一件连衣裙和一条裤子,例如。随着训练的进行,生成的图像质量越来越好。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH04_F03_Liu.png

图 4.3 经过 10 个周期训练的图像 GAN 模型生成的服装项目

正如我们在所有 GAN 中所做的那样,我们丢弃判别器并保存训练好的生成器以供以后生成样本:

scripted = torch.jit.script(G) 
scripted.save('files/fashion_gen.pt') 

我们现在已将生成器保存在本地文件夹中。要使用生成器,我们加载模型:

new_G=torch.jit.load('files/fashion_gen.pt',
                     map_location=device)
new_G.eval()

生成器现在已加载。我们可以用它来生成服装项目:

noise=torch.randn(32,100).to(device=device)
fake_samples=new_G(noise).cpu().detach()
for i in range(32):
    ax = plt.subplot(4, 8, i + 1)
    plt.imshow((fake_samples[i]/2+0.5).reshape(28, 28))
    plt.xticks([])
    plt.yticks([])
plt.subplots_adjust(hspace=-0.6)
plt.show() 

生成的服装项目如图 4.4 所示。如图所示,服装项目与训练集中的项目相当接近。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH04_F04_Liu.png

图 4.4 经过 50 个周期训练的图像 GAN 模型生成的服装项目

现在你已经学会了如何使用 GAN 创建灰度图像,你将在本章剩余部分学习如何使用深度卷积 GAN (DCGAN) 生成高分辨率彩色图像。

4.2 卷积层

要创建高分辨率彩色图像,我们需要比简单的全连接神经网络更复杂的技巧。具体来说,我们将使用卷积神经网络(CNNs),它们在处理具有网格状拓扑结构的数据(如图像)方面特别有效。它们在几个方面与全连接(密集)层不同。首先,在 CNNs 中,每一层的每个神经元仅连接到输入的小区域。这是基于这样的理解:在图像数据中,局部像素群更有可能相互关联。这种局部连接性减少了参数数量,使网络更高效。其次,CNNs 使用共享权重——相同的权重被用于输入的不同区域。这类似于在整个输入空间上滑动一个过滤器。这个过滤器检测特定的特征(例如,边缘或纹理),而不管它们在输入中的位置如何,从而导致了平移不变性的特性。

由于它们的结构,CNNs 在图像处理方面更高效。它们所需的参数比类似大小的全连接网络少,导致训练时间更快和计算成本更低。它们通常在捕获图像数据中的空间层次结构方面也更为有效。

卷积层和转置卷积层是 CNNs 中的两个基本构建块,常用于图像处理和计算机视觉任务。它们有不同的目的和特性:卷积层用于特征提取。它们将一组可学习的过滤器(也称为核)应用于输入数据,以检测不同空间尺度上的模式和特征。这些层对于捕获输入数据的层次表示至关重要。相比之下,转置卷积层用于上采样或生成高分辨率特征图。

在本节中,你将学习卷积操作的工作原理以及核大小、步长和零填充如何影响卷积操作。

4.2.1 卷积操作是如何工作的?

卷积层使用过滤器从输入数据中提取空间模式。一个卷积层能够自动检测大量模式并将它们与目标标签关联起来。因此,卷积层通常用于图像分类任务。

卷积操作涉及将一个过滤器应用于输入图像以生成一个特征图。这个过程包括使用过滤器与输入图像的逐元素乘法,并将结果相加。过滤器中的权重在过滤器在输入图像上移动以扫描不同区域时保持不变。图 4.5 展示了卷积操作的工作原理的数值示例。左侧列是输入图像,第二列是一个过滤器(一个 2×2 矩阵)。卷积操作(第三列)涉及将过滤器在输入图像上滑动,乘以相应的元素,并将它们相加(最后一列)。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH04_F05_Liu.png

图 4.5 卷积操作如何工作的数值示例,步长为 1,无填充

为了深入理解卷积操作的确切工作原理,让我们在 PyTorch 中并行实现卷积操作,以便您可以验证如图 4.5 所示的数字。首先,让我们创建一个 PyTorch 张量来表示图中的输入图像:

img = torch.Tensor([[1,1,1],
                    [0,1,2],
                    [8,7,6]]).reshape(1,1,3,3)    ①

① 图像形状中的四个值(1, 1, 3, 3)分别是批处理中的图像数量、颜色通道数量、图像高度和图像宽度。

图像被重塑,使其具有维度(1, 1, 3, 3),表示批处理中只有一个观察值,图像只有一个颜色通道。图像的高度和宽度都是 3 像素。

让我们通过在 PyTorch 中创建一个 2 × 2 滤波器,如图 4.5 的第二列所示,来表示它:

conv=nn.Conv2d(in_channels=1,
            out_channels=1,
            kernel_size=2, 
            stride=1)                             ①
sd=conv.state_dict()                              ②
print(sd)

① 初始化一个 2D 卷积层

② 从层中提取随机初始化的权重和偏置

2D 卷积层需要几个参数。in_channels参数是输入图像中的通道数。对于灰度图像,这个值是 1,对于彩色图像,这个值是 3,因为彩色图像有三个颜色通道(红色、绿色和蓝色[RGB])。out_channels是卷积层之后的通道数,可以根据你想从图像中提取多少特征来取任何数值。kernel_size参数控制内核的大小;例如,kernel_size=3表示滤波器的大小为 3 × 3,而kernel_size=4表示滤波器的大小为 4 × 4。我们将内核大小设置为 2,因此滤波器的大小为 2 × 2。

2D 卷积层也有几个可选参数。stride参数指定每次滤波器在输入图像上移动时向右或向下移动多少像素。stride参数的默认值为 1。更大的步长值会导致图像的更多下采样。padding参数表示在输入图像的四边添加多少行零,默认值为 0。bias参数表示是否将可学习的偏置作为参数添加,默认值为True。

前面的 2D 卷积层有一个输入通道,一个输出通道,内核大小为 2 × 2,步长为 1。当创建卷积层时,其中的权重和偏置是随机初始化的。您将看到以下输出作为此卷积层的权重和偏置:

OrderedDict([('weight', tensor([[[[ 0.3823,  0.4150],
          [-0.1171,  0.4593]]]])), ('bias', tensor([-0.1096]))])

为了使我们的例子更容易理解,我们将权重和偏置替换为整数:

weights={'weight':torch.tensor([[[[1,2],
   [3,4]]]]), 'bias':torch.tensor([0])}          ①
for k in sd:
    with torch.no_grad():
        sd[k].copy_(weights[k])                  ②
print(conv.state_dict())                         ③

① 手动选择权重和偏置

② 将卷积层中的权重和偏置替换为我们手动选择的数字

打印出卷积层中的新权重和偏置

由于我们不在卷积层中学习参数,使用 torch.no_grad() 来禁用梯度计算,这减少了内存消耗并加快了计算速度。现在卷积层具有我们选择的权重和偏置,它们也匹配图 4.5 中的数字。前面代码单元格的输出为:

OrderedDict([('weight', tensor([[[[1., 2.],
          [3., 4.]]]])), ('bias', tensor([0.]))])

如果我们将前面提到的 3 × 3 图像应用于卷积层,输出是什么?让我们来看看:

output = conv(img)
print(output)

输出为

tensor([[[[ 7., 14.],
          [54., 50.]]]], grad_fn=<ConvolutionBackward0>)

输出的形状为 (1, 1, 2, 2),其中包含四个值:7、14、54 和 50。这些数字与图 4.5 中的数字相匹配。

但卷积层究竟是如何通过滤波器生成这个输出的?我们将在下文中详细解释。

输入图像是一个 3 × 3 矩阵,滤波器是一个 2 × 2 矩阵。当滤波器在图像上扫描时,它首先覆盖图像左上角四个像素,这些像素的值为 [[1, 1], [0, 1]],如图 4.5 的第一行所示。滤波器的值为 [[1,2],[3,4]]。卷积操作找到两个张量(在这种情况下,一个张量是滤波器,另一个是被覆盖的区域)逐元素乘积的和。换句话说,卷积操作在每个四个单元格中执行逐元素乘法,然后将四个单元格中的值相加。因此,扫描左上角得到的输出是

1 × 1 × 1 × 2 + 0 × 3 + 1 × 4 = 7。

这解释了为什么输出左上角的值为 7。同样,当滤波器应用于图像的右上角时,覆盖区域为 [[1,1],[1,2]]。因此,输出为:

1 × 1 + 1 × 2 + 1 × 3 + 2 × 4 = 14。

这解释了为什么输出右上角的值为 14。

练习 4.3

当滤波器应用于图像右下角时,覆盖区域的值是什么?解释为什么输出右下角的值为 50。

4.2.2 步长和填充如何影响卷积操作?

步长和零填充是卷积操作中的两个重要概念。它们在确定输出特征图的维度以及滤波器与输入数据交互的方式中起着至关重要的作用。

步长指的是滤波器在输入图像上移动的像素数。当步长为 1 时,滤波器每次移动 1 个像素。较大的步长意味着滤波器在滑动图像时跳过了更多的像素。增加步长会减少输出特征图的空間维度。

零填充涉及在应用卷积操作之前在输入图像的边缘添加零层。零填充允许控制输出特征图的空間维度。没有填充,输出的维度将小于输入。通过添加填充,可以保留输入的维度。

让我们用一个例子来展示步长和填充是如何工作的。下面的代码单元重新定义了 2D 卷积层:

conv=nn.Conv2d(in_channels=1,
            out_channels=1,
            kernel_size=2, 
            stride=2,                           ①
            padding=1)                          ②
sd=conv.state_dict()
for k in sd:
    with torch.no_grad():
        sd[k].copy_(weights[k])
output = conv(img)
print(output)

① 将步长从 1 改为 2

② 将填充从 0 改为 1

输出是

tensor([[[[ 4.,  7.],
          [32., 50.]]]], grad_fn=<ConvolutionBackward0>)

padding=1参数在输入图像周围添加一行 0,因此填充后的图像现在的大小为 5 × 5 而不是 3 × 3。

当过滤器扫描填充后的图像时,它首先覆盖左上角,其值为[[0, 0], [0, 1]]。过滤器的值为[[1,2],[3,4]]。因此,扫描左上角的输出为:

0 × 1+0 × 2+0 × 3+1 × 4=4

这解释了为什么输出图像的左上角有一个值为 4。同样,当过滤器向下滑动两个像素到达图像的左下角时,覆盖的区域是[[0,0],[0,8]]。因此,输出如下:

0 × 1+0 × 2+0 × 3+8 × 4=32

这解释了为什么输出图像的左下角有一个值为 32。

4.3 转置卷积和批量归一化

转置卷积层也被称为反卷积或上采样层。它们用于上采样或生成高分辨率特征图。它们通常在生成模型如 GAN 和 VAE 中使用。

转置卷积层将过滤器应用于输入数据,但与标准卷积不同,它们通过在输出值之间插入间隔来增加空间维度,从而有效地“上采样”特征图。这个过程生成了更高分辨率的特征图。转置卷积层有助于提高空间分辨率,这在图像生成中很有用。

步长可以在转置卷积层中使用来控制上采样的量。步长的值越大,转置卷积层对输入数据的上采样就越多。

二维批量归一化是一种在神经网络中使用的技巧,特别是在 CNN 中,用于稳定和加速训练过程。它解决了包括饱和、消失梯度和梯度爆炸在内的几个问题,这些都是深度学习中常见的挑战。在本节中,你将查看一些示例,以便更深入地了解其工作原理。你将在下一节创建 GAN 时生成高分辨率彩色图像时使用它。

深度学习中的消失和梯度爆炸

消失梯度问题发生在深度神经网络中,当在反向传播过程中损失函数相对于网络参数的梯度变得极其小时。这导致参数更新非常缓慢,阻碍了学习过程,尤其是在网络的早期层。相反,梯度爆炸问题发生在这些梯度变得过大时,导致更新不稳定,并使模型参数振荡或发散到非常大的值。这两个问题都阻碍了深度神经网络的 有效训练。

4.3.1 转置卷积层是如何工作的?

与卷积层相反,转置卷积层通过使用核(即滤波器)在图像中上采样并填充间隙来生成特征并增加分辨率。在转置卷积层中,输出通常比输入大。因此,转置卷积层是生成高分辨率图像时的必要工具。为了向您展示 2D 转置卷积操作的确切工作方式,让我们用一个简单的例子和一张图来说明。假设你有一个非常小的 2 × 2 输入图像,如图 4.6 的左侧列所示。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH04_F06_Liu.png

图 4.6 转置卷积操作的一个数值示例

输入图像中的值为:

img = torch.Tensor([[1,0],
                    [2,3]]).reshape(1,1,2,2)

你想要上采样图像,使其具有更高的分辨率。你可以在 PyTorch 中创建一个 2D 转置卷积层:

transconv=nn.ConvTranspose2d(in_channels=1,
            out_channels=1,
            kernel_size=2, 
            stride=2)                            ①
sd=transconv.state_dict()
weights={'weight':torch.tensor([[[[2,3],
   [4,5]]]]), 'bias':torch.tensor([0])}
for k in sd:
    with torch.no_grad():
        sd[k].copy_(weights[k])                  ②

① 一个具有一个输入通道、一个输出通道、核大小为 2、步长为 2 的转置卷积层

② 用挑选的值替换转置卷积层中的权重和偏置

这个 2D 转置卷积层有一个输入通道,一个输出通道,核大小为 2 × 2,步长为 2。2 × 2 滤波器如图 4.6 的第二列所示。我们用我们挑选的整数替换了层中的随机初始化权重和偏置,这样便于跟踪计算。前述代码列表中的 state_dict() 方法返回深度神经网络中的参数。

当将转置卷积层应用于我们之前提到的 2 × 2 图像时,输出是什么?让我们来找出答案:

transoutput = transconv(img)
print(transoutput)

输出为

tensor([[[[ 2.,  3.,  0.,  0.],
          [ 4.,  5.,  0.,  0.],
          [ 4.,  6.,  6.,  9.],
          [ 8., 10., 12., 15.]]]], grad_fn=<ConvolutionBackward0>)

输出的形状为 (1, 1, 4, 4),这意味着我们将一个 2 × 2 的图像上采样到一个 4 × 4 的图像。转置卷积层是如何通过滤波器生成前面的输出的?我们将在下文中详细解释。

图像是一个 2 × 2 矩阵,滤波器也是一个 2 × 2 矩阵。当滤波器应用于图像时,图像中的每个元素与滤波器相乘,并传递到输出。图像左上角的值是 1,我们将其与滤波器中的值 [[2, 3], [4, 5]] 相乘,这导致输出矩阵 transoutput 左上角块中的四个值,值为 [[2, 3], [4, 5]],如图 4.6 右上角所示。同样,图像左下角的值是 2,我们将其与滤波器中的值 [[2, 3], [4, 5]] 相乘,这导致输出矩阵 transoutput 左下角块中的四个值,值为 [[4, 6], [8, 10]]。

练习 4.4

如果一个图像中包含值 [[10, 10], [15, 20]],在你对该图像应用 2D 转置卷积层 transconv 后,输出是什么?假设 transconv 中的值为 [[2, 3], [4, 5]]。假设核大小为 2,步长大小为 2。

4.3.2 批标准化

二维批量归一化是现代深度学习框架中的标准技术,并已成为有效训练深度神经网络的关键组件。你将在本书后面的内容中经常看到它。

在二维批量归一化中,通过调整和缩放通道中的值,使其具有 0 均值和 1 方差,对每个特征通道独立进行归一化。特征通道是指 CNN 中多维张量中的一个维度,用于表示输入数据的各个方面或特征。例如,它们可以表示颜色通道,如红色、绿色或蓝色。归一化确保了在训练过程中,网络深层输入的分布保持更稳定。这种稳定性源于归一化过程减少了内部协变量偏移,这是由于较低层权重的更新导致的网络激活分布的变化。它还有助于通过保持输入在适当的范围内,防止梯度变得太小(消失)或太大(爆炸)来解决问题。1

这是二维批量归一化工作原理的说明:对于每个特征通道,我们首先计算该通道内所有观察值的均值和方差。然后,我们使用之前获得的均值和方差(通过从每个观察值中减去均值,然后除以标准差)对每个特征通道的值进行归一化。这确保了归一化后每个通道的值具有 0 均值和 1 标准差,这有助于稳定和加速训练。它还有助于在反向传播过程中保持稳定的梯度,这进一步有助于训练深度神经网络。

让我们用一个具体的例子来说明二维批量归一化是如何工作的。

假设你有一个大小为 64 × 64 的三通道输入。你将输入通过一个有三个输出通道的 2D 卷积层,如下所示:

torch.manual_seed(42)                          ①
img = torch.rand(1,3,64,64)                    ②
conv = nn.Conv2d(in_channels=3,
            out_channels=3,
            kernel_size=3, 
            stride=1,
            padding=1)                         ③
out=conv(img)                                  ④
print(out.shape)

① 固定随机状态,以确保结果可重复

② 创建一个 3 通道输入

③ 创建一个二维卷积层

④ 将输入通过卷积层传递

上述代码单元格的输出是

torch.Size([1, 3, 64, 64])

我们创建了一个三通道输入,并通过一个有三个输出通道的 2D 卷积层传递。处理后的输入有三个通道,大小为 64 × 64 像素。

让我们看看三个输出通道中每个通道的像素均值和标准差:

for i in range(3):
    print(f"mean in channel {i} is", out[:,i,:,:].mean().item())
    print(f"std in channel {i} is", out[:,i,:,:].std().item())

输出是

mean in channel 0 is -0.3766776919364929
std in channel 0 is 0.17841289937496185
mean in channel 1 is -0.3910464942455292
std in channel 1 is 0.16061744093894958
mean in channel 2 is 0.39275866746902466
std in channel 2 is 0.18207983672618866

每个输出通道中像素的平均值不是 0;每个输出通道中像素的标准差不是 1。现在,我们执行二维批量归一化:

norm=nn.BatchNorm2d(3)
out2=norm(out)
print(out2.shape)
for i in range(3):
    print(f"mean in channel {i} is", out2[:,i,:,:].mean().item())
    print(f"std in channel {i} is", out2[:,i,:,:].std().item())

然后我们有以下输出:

torch.Size([1, 3, 64, 64])
mean in channel 0 is 6.984919309616089e-09
std in channel 0 is 0.9999650120735168
mean in channel 1 is -5.3085386753082275e-08
std in channel 1 is 0.9999282956123352
mean in channel 2 is 9.872019290924072e-08
std in channel 2 is 0.9999712705612183

每个输出通道的像素平均值现在实际上为 0(或一个非常接近 0 的小数);每个输出通道的像素标准差现在是一个接近 1 的数。这就是批量归一化的作用:它对每个特征通道中的观测值进行归一化,使得每个特征通道中的值具有 0 均值和单位标准差。

4.4 动漫面孔的彩色图像

在这个第二个项目中,你将学习如何创建高分辨率彩色图像。这个项目的训练步骤与第一个项目类似,不同之处在于训练数据是动漫面孔的彩色图像。此外,判别器和生成器神经网络更加复杂。我们将在两个网络中使用 2D 卷积和 2D 转置卷积层。

4.4.1 下载动漫面孔

你可以从 Kaggle mng.bz/1a9R下载训练数据,其中包含 63,632 张动漫面孔的彩色图像。你需要先设置一个免费的 Kaggle 账户进行登录。从 zip 文件中提取数据,并将它们放在你电脑上的一个文件夹中。例如,我将 zip 文件中的所有内容都放在了电脑上的/files/anime/文件夹中。因此,所有动漫面孔图像都在/files/anime/images/文件夹中。

定义路径名,以便以后可以在 Pytorch 中加载图像:

anime_path = r"files/anime"

根据你在电脑上保存图像的位置更改路径名。请注意,ImageFolder()类使用图像的目录名来识别图像所属的类别。因此,我们之前定义的anime_path中不包括最终的/images/目录。

接下来,我们使用 Torchvision datasets包中的ImageFolder()类来加载数据集:

from torchvision import transforms as T
from torchvision.datasets import ImageFolder

transform = T.Compose([T.Resize((64, 64)),               ①
    T.ToTensor(),                                        ②
    T.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])])      ③
train_data = ImageFolder(root=anime_path,
                         transform=transform)            ④

① 将图像大小调整为 64 × 64

② 将图像转换为 PyTorch 张量

③ 在所有三个颜色通道中将图像值归一化到[-1, 1]

④ 加载数据并转换图像

在从本地文件夹加载图像时,我们执行了三种不同的转换。首先,我们将所有图像的高度和宽度都调整为 64 像素。其次,我们使用ToTensor()类将图像转换为 PyTorch 张量,其值在[0, 1]范围内。最后,我们使用Normalize()类从值中减去 0.5,并将差值除以 0.5。因此,图像数据现在在-1 和 1 之间。

我们现在可以将训练数据放入批次中:

from torch.utils.data import DataLoader

batch_size = 128
train_loader = DataLoader(dataset=train_data, 
               batch_size=batch_size, shuffle=True)

训练数据集现在是批次的,批大小为 128。

4.4.2 PyTorch 中的通道优先彩色图像

PyTorch 在处理彩色图像时使用所谓的通道优先方法。这意味着 PyTorch 中图像的形状是(通道数,高度,宽度)。相比之下,在其他 Python 库(如 TensorFlow 或 Matplotlib)中,使用的是通道后方法:彩色图像的形状为(高度,宽度,通道数)。

让我们看看数据集中的一张示例图像,并打印出图像的形状:

image0, _ = train_data[0]
print(image0.shape)

输出为

torch.Size([3, 64, 64])

第一张图像的形状是 3 × 64 × 64。这意味着图像有三个颜色通道(RGB)。图像的高度和宽度都是 64 像素。

当我们在 Matplotlib 中绘制图像时,我们需要使用 PyTorch 中的 permute() 方法将它们转换为通道最后:

import matplotlib.pyplot as plt

plt.imshow(image0.permute(1,2,0)*0.5+0.5)
plt.show()

注意,我们需要将代表图像的 PyTorch 张量乘以 0.5,然后加上 0.5,以将值从范围 [–1, 1] 转换为范围 [0, 1]。运行前面的代码单元后,您将看到一个动漫人脸的绘图。

接下来,我们定义一个函数 plot_images() 来可视化四行八列的 32 张图像:

def plot_images(imgs):                              ①
    for i in range(32):
        ax = plt.subplot(4, 8, i + 1)               ②
        plt.imshow(imgs[i].permute(1,2,0)/2+0.5)
        plt.xticks([])
        plt.yticks([])
    plt.subplots_adjust(hspace=-0.6)
    plt.show()    

imgs, _ = next(iter(train_loader))                  ③
plot_images(imgs)                                   ④

① 定义一个函数来可视化 32 张图像

② 将它们放置在 4 × 8 的网格中

③ 获取一批图像

④ 调用函数来可视化图像

运行前面的代码单元后,您将看到一个 4 × 8 网格中的 32 张动漫人脸的绘图,如图 4.7 所示。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH04_F07_Liu.png

图 4.7 动漫人脸训练数据集的示例

4.5 深度卷积 GAN

在本节中,您将创建一个 DCGAN 模型,以便我们可以训练它生成动漫人脸图像。像往常一样,GAN 模型由判别器网络和生成器网络组成。然而,这些网络比我们之前看到的更复杂:在这些网络中,我们将使用卷积层、转置卷积层和批量归一化层。

我们将从判别器网络开始。之后,我将解释生成器网络如何镜像判别器网络中的层来生成逼真的彩色图像。然后,您将使用本章前面准备的数据训练模型,并使用训练好的模型生成动漫人脸图像的新颖图像。

4.5.1 构建 DCGAN

如我们之前看到的 GAN 模型一样,判别器是一个二元分类器,用于将样本分类为真实或伪造。然而,与迄今为止我们使用的网络不同,我们将使用卷积层和批量归一化。本项目中的高分辨率彩色图像参数太多,如果我们只使用密集层,就难以有效地训练模型。判别器神经网络的结构如下所示。

列表 4.4 DCGAN 中的判别器

import torch.nn as nn
import torch

device = "cuda" if torch.cuda.is_available() else "cpu"

D = nn.Sequential(
    nn.Conv2d(3, 64, 4, 2, 1, bias=False),           ①
    nn.LeakyReLU(0.2, inplace=True),                 ②
    nn.Conv2d(64, 128, 4, 2, 1, bias=False),
    nn.BatchNorm2d(128),                             ③
    nn.LeakyReLU(0.2, inplace=True),
    nn.Conv2d(128, 256, 4, 2, 1, bias=False),
    nn.BatchNorm2d(256),
    nn.LeakyReLU(0.2, inplace=True),
    nn.Conv2d(256, 512, 4, 2, 1, bias=False),
    nn.BatchNorm2d(512),
    nn.LeakyReLU(0.2, inplace=True),
    nn.Conv2d(512, 1, 4, 1, 0, bias=False),
    nn.Sigmoid(),
    nn.Flatten()).to(device)                         ④

① 将图像通过一个二维卷积层

② 在第一个卷积层的输出上应用 LeakyReLU 激活函数

③ 对第二个卷积层的输出执行二维批量归一化

④ 输出是一个介于 0 和 1 之间的单个值,可以解释为图像是真实的概率。

判别器网络的输入是一个具有三个颜色通道的彩色图像。第一个二维卷积层是 Conv2d(3, 64, 4, 2, 1, bias=False):这意味着输入有三个通道,输出有 64 个通道;核大小为 4;步长为 2;填充为 1。网络中的每个二维卷积层都接受一个图像并应用滤波器以提取空间特征。

从第二个 2D 卷积层开始,我们在输出上应用 2D 批量归一化(我在上一节中解释过)和 LeakyReLU 激活(我将在后面解释)。LeakyReLU 激活函数是 ReLU 的修改版。它允许输出在零以下的值具有斜率。具体来说,LeakyReLU 函数的定义如下:

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH04_F07_Liu_EQ01.png

其中β是介于 0 和 1 之间的常数。LeakyReLU 激活函数通常用于解决稀疏梯度问题(当大多数梯度变为零或接近零时)。训练 DCGANs 就是这样一种情况。当神经元的输入为负时,ReLU 的输出为零,神经元变得不活跃。LeakyReLU 对负输入返回一个小的负值,而不是零。这有助于保持神经元活跃和学习,保持更好的梯度流动,并导致模型参数更快地收敛。

在构建服装生成器的生成器时,我们将采用相同的方法。我们将使用 DCGAN 中判别器中使用的层来创建一个生成器,如下面的列表所示。

列表 4.5 在 DCGAN 中设计生成器

G=nn.Sequential(
    nn.ConvTranspose2d(100, 512, 4, 1, 0, bias=False),    ①
    nn.BatchNorm2d(512),
    nn.ReLU(inplace=True),
    nn.ConvTranspose2d(512, 256, 4, 2, 1, bias=False),    ②
    nn.BatchNorm2d(256),
    nn.ReLU(inplace=True),
    nn.ConvTranspose2d(256, 128, 4, 2, 1, bias=False),
    nn.BatchNorm2d(128),
    nn.ReLU(inplace=True),
    nn.ConvTranspose2d(128, 64, 4, 2, 1, bias=False),
    nn.BatchNorm2d(64),
    nn.ReLU(inplace=True),
    nn.ConvTranspose2d(64, 3, 4, 2, 1, bias=False),       ③
    nn.Tanh()).to(device)                                 ④

①生成器中的第一层是模仿判别器中的最后一层。

②生成器中的第二层与判别器中的倒数第二层对称(输入和输出的数量位置已交换)。

③生成器中的最后一层与判别器中的第一层对称。

④使用 Tanh()激活函数将输出层的值挤压到[–1, 1]的范围内,因为训练集中的图像值介于–1 和 1 之间

如图 4.8 所示,为了创建图像,生成器使用了五个 2D 转置卷积层:它们与判别器中的五个 2D 卷积层对称。例如,最后一层ConvTranspose2d(64, 3, 4, 2, 1, bias=False)是模仿判别器的第一层Conv2d(3, 64, 4, 2, 1, bias=False)。Conv2d中的*input*和*output*通道的数量被反转,并用作ConvTranspose2d中的*output*和*input*通道的数量。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH04_F08_Liu.png

图 4.8 在 DCGAN 中设计生成器网络以通过镜像判别器网络中的层来创建动漫面孔。图的右侧显示了判别器网络,其中包含五个 2D 卷积层。为了设计一个能够凭空创造出动漫面孔的生成器,我们镜像了判别器网络中的层。具体来说,如图的左侧所示,生成器有五个 2D 转置卷积层,与判别器中的 2D 卷积层对称。此外,在顶部四个层中,判别器中的input和output通道的数量被反转,并用作生成器中的output和input通道的数量。

第一层 2D 转置卷积层的输入通道数为 100。这是因为生成器从潜在空间(图 4.8 的左下角)获取一个 100 值的随机噪声向量并将其馈送到生成器。生成器中最后 2D 转置卷积层的输出通道数为 3,因为输出是一个具有三个颜色通道(RGB)的图像。我们对生成器的输出应用 Tanh 激活函数,将所有值挤压到范围[–1, 1],因为训练图像的所有值都在–1 和 1 之间。

如同往常,损失函数是二元交叉熵损失。判别器试图最大化二元分类的准确性:将真实样本识别为真实,将伪造样本识别为伪造。另一方面,生成器试图最小化伪造样本被识别为伪造的概率。

我们将使用 Adam 优化器来训练判别器和生成器,并将学习率设置为 0.0002:

loss_fn=nn.BCELoss()
lr = 0.0002
optimG = torch.optim.Adam(G.parameters(), 
                         lr = lr, betas=(0.5, 0.999))
optimD = torch.optim.Adam(D.parameters(), 
                         lr = lr, betas=(0.5, 0.999))

你在第二章中已经看到了 Adam 优化器,但这里我们选择了与默认值不同的 beta。在这里,我们选择了不同的 beta。Adam 优化器中的 beta 在稳定和加速训练过程方面起着至关重要的作用。它们通过控制对最近和过去梯度信息的重视程度(beta1)以及根据梯度信息的确定性调整学习率(beta2)来实现这一点。这些参数通常根据要解决的问题的具体特征进行微调。

4.5.2 训练和使用 DCGAN

DCGAN 的训练过程与我们之前在其他 GAN 模型中做过的类似,例如第三章和本章早些时候使用的模型。由于我们不知道动漫人脸图像的真实分布,我们将依靠可视化技术来确定何时训练完成。具体来说,我们定义了一个test_epoch()函数,用于在每次训练周期后可视化生成器创建的动漫人脸:

def test_epoch():
    noise=torch.randn(32,100,1,1).\
        to(device=device)                             ①
    fake_samples=G(noise).cpu().detach()              ②
    for i in range(32):                               ③
        ax = plt.subplot(4, 8, i + 1)
        img=(fake_samples.cpu().detach()[i]/2+0.5).\
            permute(1,2,0)
        plt.imshow(img)
        plt.xticks([])
        plt.yticks([])
    plt.subplots_adjust(hspace=-0.6)
    plt.show()
test_epoch()                                          ④

① 从潜在空间获取 32 个随机噪声向量

② 生成 32 张动漫人脸图像

③ 在 4 × 8 网格中绘制生成的图像

④ 在训练模型之前调用生成图像的函数

如果你运行前面的代码单元,你会看到 32 张看起来像电视屏幕上的雪花静态的图像。它们根本不像动漫人脸,因为我们还没有训练生成器。

我们定义了三个函数,train_D_on_real()、train_D_on_fake()和train_G(),类似于我们之前在本章中用于训练 GAN 以生成服装物品灰度图像的函数。请访问本书 GitHub 仓库中本章的 Jupyter Notebook,熟悉这些函数。它们使用真实图像训练判别器。然后,它们使用伪造图像训练判别器;最后,它们训练生成器。

接下来,我们训练模型 20 个周期:

for i in range(20):
    gloss=0
    dloss=0
    for n, (real_samples,_) in enumerate(train_loader):
        loss_D=train_D_on_real(real_samples)
        dloss+=loss_D
        loss_D=train_D_on_fake()
        dloss+=loss_D
        loss_G=train_G()
        gloss+=loss_G
    gloss=gloss/n
    dloss=dloss/n
    print(f"epoch {i+1}, dloss: {dloss}, gloss {gloss}")
    test_epoch()

如果您使用 GPU 进行训练,训练时间大约需要 20 分钟。否则,根据您电脑上的硬件配置,可能需要 2 到 3 小时。另外,您可以从我的网站上下载训练好的模型:gattonweb.uky.edu/faculty/lium/gai/anime_gen.zip。

在每个训练周期之后,您可以可视化生成的动漫人脸。经过仅一个周期的训练后,模型已经可以生成看起来像动漫人脸的彩色图像,如图 4.9 所示。随着训练的进行,生成的图像质量会越来越好。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH04_F09_Liu.png

图 4.9 DCGAN 经过一个周期训练后的生成图像

我们将丢弃判别器,并将训练好的生成器保存在本地文件夹中:

scripted = torch.jit.script(G) 
scripted.save('files/anime_gen.pt') 

要使用训练好的生成器,我们加载模型并使用它生成 32 张图像:

new_G=torch.jit.load('files/anime_gen.pt',
                     map_location=device)
new_G.eval()
noise=torch.randn(32,100,1,1).to(device)
fake_samples=new_G(noise).cpu().detach()
for i in range(32):
    ax = plt.subplot(4, 8, i + 1)
    img=(fake_samples.cpu().detach()[i]/2+0.5).permute(1,2,0)
    plt.imshow(img)
    plt.xticks([])
    plt.yticks([])
plt.subplots_adjust(hspace=-0.6)
plt.show() 

生成的动漫人脸图像如图 4.10 所示。生成的图像与图 4.7 中显示的训练集图像非常相似。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-genai-pt/img/CH04_F10_Liu.png

图 4.10 由训练好的 DCGAN 生成器生成的动漫人脸图像

您可能已经注意到生成的图像中的发色不同:有的是黑色,有的是红色,有的是金色。您可能会想:我们能否让生成器创建具有特定特征(如黑色头发或红色头发)的图像?答案是肯定的。您将在第五章中学习几种不同的方法来在 GANs 中选取生成图像的特征。

摘要

  • 要从无中生有地生成看起来逼真的图像,生成器会镜像判别器网络中使用的层。

  • 虽然仅使用全连接层就可以生成灰度图像是可行的,但要生成高分辨率彩色图像,我们需要使用卷积神经网络(CNNs)。

  • 二维卷积层用于特征提取。它们将一组可学习的滤波器(也称为核)应用于输入数据,以检测不同空间尺度上的模式和特征。这些层对于捕获输入数据的层次表示至关重要。

  • 二维转置卷积层(也称为反卷积或上采样层)用于上采样或生成高分辨率特征图。它们对输入数据应用一个滤波器。然而,与标准卷积不同,它们通过在输出值之间插入间隔来增加空间维度,从而有效地“上采样”特征图。这个过程生成了更高分辨率的特征图。

  • 二维批量归一化是一种在深度学习和神经网络中常用的技术,用于提高 CNNs 和其他处理 2D 数据(如图像)的模型训练和性能。它对每个特征通道的值进行归一化,使它们具有 0 的均值和 1 的标准差,这有助于稳定和加速训练。


^(1)  谢尔盖·约费,克里斯蒂安·塞格迪,2015 年,“批量归一化:通过减少内部协变量偏移来加速深度网络训练。” arxiv.org/abs/1502.03167.

PyTorch生成式AI实战:从零打造你的第一个创意模型 在生成式AI的世界里,代码是画笔,数据是颜料,想象力才是真正的画布!现在就开始你的创作之旅吧~:Midjourney生成概念设计图(2秒出4稿):支付宝刷脸支付(0.8秒完成验证)- PyTorch版最佳入门书。- 实战提升模型调优能力。- 获取最新预训练模型。- 免费GPU实验环境。 阅读详情

相关推荐

PyTorch生成式人工智能实战:从零打造创意引擎

专栏《PyTorch 生成式人工智能实战》深入探讨了生成式人工智能,这项技术通过其高效和快速的内容创作能力,正在重塑众多行业。具体来说,将学习如何使用生成式模型来创建各种形式的内容:数字、图像、文本和音频。此外,还将学习从零开始构建这些模型,以便深入理解生成式 AI 的内部工作原理,我们将使用 Python 和 PyTorch 来构建、训练和使用这些模型。

盼小辉丶的博客 3万+

生成式AI与非生成AI

生成式 AI 和非生成式 AI 是人工智能领域中的两种主要类型。它们的主要区别在于其生成输出的方式。生成式 AI 是能够生成新的数据或内容的 AI 模型。这些模型通过学习大量的训练数据来学会理解模式和关系,并能够根据这些学习生成新的数据。生成式 AI 的输出结果是全新的,是根据模型对输入数据的学习和理解生成的。常见的生成式 AI 模型包括语言模型(如 GPT)、视频生成模型(如 SORA)和音乐...

人机与认知实验室 1776

PyTorch生成式人工智能(2)——PyTorch基础

PyTorch 张量是多维数据的基本结构,支持灵活的形状变换、索引操作和数学计算。数据类型的选择(如精度权衡)和形状匹配是深度学习中的重要考量。通过张量操作,可高效实现数据预处理、特征整合及模型输入输出处理。

盼小辉丶的博客 1万+

深入浅出PyTorch:从零开始入门人工智能

PyTorch是由Facebook AI研究院开发的Python深度学习框架,是目前最流行的深度学习框架之一。它通过动态计算图的方式实现了神经网络的构建和优化,同时能够高效地利用GPU进行计算加速。PyTorch具有易用、灵活和高效等特点,被广泛应用于深度学习领域,如自然语言处理、计算机视觉、推荐系统等。以上就是关于PyTorch的一些基本知识、应用以及示范的介绍。PyTorch作为深度学习领域最流行的框架之一,具有易用、灵活和高效等特点,在深度学习领域拥有广泛的应用。

canshanyin的博客 1342

生成式人工智能 (Generative AI)

1. 背景介绍 1.1 人工智能的新纪元 人工智能 (AI) 经历了漫长的发展历程,从早期的符号主义到连接主义,再到如今的深度学习,每一次技术革新都为 AI 领域带来了巨大的进步。而近年来,一个新兴的领域——生成式人工智能 (Generative AI) 正逐渐走入大众视野,并被视为人工智能发展的新

AI天才研究院 2101

PyTorch生成式人工智能——生成式多模态人工智能

生成式人工智能 (Generative Artificial Intelligence, GenAI) 是一类能够生成输出的 AI 模型,这并不意味着其他模型不能生成输出(所有 AI 模型都会生成输出),生成式 AI 强调的是能够理解训练数据的潜在模式,并根据这些模式生成新数据的模型。在本节中,我们将详细解释 Stable Diffusion 在文本到图像生成中的工作原理,并展示其在实际图像生成中的应用。

盼小辉丶的博客 2691

配置你的AI学习框架,从sklearn谈到pytorch(1)

针对Window Nvidia显卡配置下的深度学习/机器学习环境配置简介

unrealnum_border的博客 1122

生成式AI:AI人工智能创新的关键元素

本文旨在全面介绍生成式AI技术,解释其工作原理、核心算法、应用场景和发展趋势。我们将从基础概念出发,逐步深入到技术实现细节,帮助读者理解这一正在改变世界的技术。文章将从生成式AI的基本概念开始,逐步深入到技术实现细节,包括核心算法、数学模型和实际应用案例。最后我们将探讨未来发展趋势和面临的挑战。生成式AI:能够创造新内容(如文本、图像、音乐等)的人工智能系统神经网络:模仿人脑神经元连接方式的计算模型Transformer架构:一种特别适合处理序列数据的神经网络结构预训练模型。

AI架构师小马 1161

【Pytorch】Pytorch深度学习实战教程:超分辨率重建AI与环境搭建

Pytorch深度学习实战教程:超分辨率重建AI与环境搭建

Cappuccino Blog 1700

PyTorch生成式人工智能——Hugging Face环境配置与应用详解

在本节中,我们将介绍如何在 PyTorch 和 TensorFlow 框架中使用 Hugging Face 模型库,我们将讨论如何获取不同的模型以及使用不同管道的步骤,例如,双向自回归 Transformer (BART)、BERT 和 TAPAS 等模型,同时介绍 GPT-2 (Generative Pretrained Transformer 2) 文本生成。需要注意的是,本节主要集中在如何准备环境以及如何使用预训练模型,而不是模型训练,模型训练将在后续学习中详细讨论。

盼小辉丶的博客 2251

PyTorch生成式人工智能(6)——生成模型(Generative Model)详解

本节介绍了人工智能的一个重要分支——生成模型,介绍了生成模型理论和应用的最新进展。我们从一个简单的示例开始,了解了生成模型最终关注的是对数据的潜在分布进行建模。通过总结生成模型框架,以理解生成模型的重要属性。然后,介绍了有助于理解生成模型的理论基础和关键概率概念,并概述了生成模型的分类方法。

盼小辉丶的博客 1万+

人工智能入门(一) Torch基本使用

本文目录 目录本文目录环境安装PyTorch数据类型操作pandas读写数据集矩阵操作自动求导概率论环境安装 PyTorch官网Jupyter NoteBook PyTorch数据类型操作 import torch x = torch.arange(12) # 使用arange创建一个行向量x tensor([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 1...

DXCyber409的博客 1492

实战指南:基于ELK与Grafana构建天融信防火墙日志可视化看板

本文详细介绍了如何基于ELK与Grafana构建天融信防火墙日志可视化看板,包括环境准备、日志收集与解析配置、Elasticsearch存储优化、Grafana看板设计等关键步骤。通过实战案例和优化技巧,帮助用户高效处理天融信防火墙日志,实现安全态势的可视化监控与分析。

weixin_34246529的博客 226

【LLM新书】使用PyTorch学习生成式人工智能,434页pdf

学习如何通过构建自己的模型来理解生成式人工智能,这些模型可以撰写连贯的文本、生成逼真的图像,甚至创作出栩栩如生的音乐。

DEVELOPERAA的博客 1079

从零开始:在CentOS服务器上部署R Shiny应用的完整指南

本文提供了一份在CentOS服务器上部署R Shiny应用的完整实战指南。内容涵盖从创建专用用户、安装R语言环境、配置Shiny Server,到最终部署应用并配置防火墙的全过程。针对部署过程中常见的权限、端口及依赖问题,给出了详细的解决方案,旨在帮助数据分析师和开发者将本地Shiny应用稳定、安全地发布至生产环境。

weixin_29079743的博客 135

手把手复现金蝶云星空V8.1文件上传漏洞(附完整POC与修复方案)

本文深度解析金蝶云星空V8.1文件上传漏洞,提供详细的漏洞复现步骤与完整POC,涵盖技术原理、测试环境搭建及验证流程。同时给出多维防护方案,包括官方补丁部署、临时缓解措施和长期安全加固建议,帮助企业有效防范任意文件上传风险。

weixin_30505225的博客 183

2025三掌柜赠书活动第二十三期 生成式人工智能(基于PyTorch实现)

当前,生成式人工智能(AI)技术飞速发展。在多模态和推理能力方面,OpenAI的o3推理模型在数学、编程及博士级科学问题上的表现已超越部分人类专家,其上下文窗口扩展至百万级token,代码生成效率提升20%。Meta提出“大型概念模型”,实现在语义层级进行抽象推理,显著提升逻辑严谨性。DeepSeek-R1等国产模型通过“慢思考”技术优化推理路径。与此同时,生成式AI技术推动了智能体(Agent)的实用化突破,促使智能体从理论迈向产业部署。那么本文就来详细聊聊。

全沾软贱开发攻城狮 1万+

聚焦112Gb/s SerDes芯片的AN/LT端口自协商和链路学习,评估验证高速链路的信号质量并分析调优(400/800G高速以太网互联接口,AI加速卡网络RDMA性能测试,交换背板接口性能评估)

在112G SerDes高速通道下的400G/800G高速以太网端口下,如何保障通信速率,验证SerDes的数据完整性和链路余量,自动协商(AN)和链路训练(LT)功能显得尤其重要,其在DAC、ACC、AEC铜缆连接或高速背板通信上,自动协商(AN)和链路训练(LT)的工作与否将决定高速链路传输质量的优劣。本文将系统的讨论介绍自动协商(AN)和链路训练(LT)的测试流程,其可以独立使用,也可以与teledynelecroy SierraNet M1288配合使用。

专注有线以太网性能测试--丹麦信雅纳网络(Xena Networks) 9404

PyTorch 生成式人工智能学习指南(四)

首先,让我们谈谈多轨音乐是如何工作的以及它是如何以数字形式表示的。在电子音乐制作中,“轨”通常指的是音乐的一个单独的层次或组成部分,例如鼓轨、贝斯轨或旋律轨。在古典音乐中,轨可能代表不同的声乐部分,如女高音、女低音、男高音和男低音。例如,我们在这章中使用的训练数据集,JSB Chorales 数据集,包含四个轨,对应四个声乐部分。在音乐制作中,每个轨都可以在数字音频工作站(DAW)中单独编辑和处理。这些轨由各种音乐元素组成,包括小节、步骤和音符。

龙哥盟 1323

自定义控件三部曲视图篇(六)——RecyclerView系列之三自定义LayoutManager

前言:把握生命里的每一分钟,全力以赴我们心中的梦,不经历风雨 怎么见彩虹,没有人能随随便便成功 在第一篇中已经讲过,LayoutManager主要用于布局其中的Item,在LayoutManager中能够对每个Item的大小,位置进行更改,将它放在我们想要的位置,在很多优秀的效果中,都是通过自定义LayoutManager来实现的,比如: 可以看到这些效果都非常棒,通过这一节的学习,大...

启舰 1万+

Python爬取腾讯视频m3u8加密流实战(附完整代码)

本文提供了一份详细的Python实战教程,指导如何通过抓包分析、模拟请求头与载荷,破解腾讯视频的m3u8加密流防盗链机制,并附上完整代码实现视频片段的下载与合并。教程涵盖从侦察到代码实现的完整流程,并包含参数动态化、多线程下载等高级技巧与避坑指南,旨在帮助开发者掌握流媒体下载的核心技术。

weixin_29090089的博客 1609

【LLM新书】使用PyTorch学习生成式人工智能,434页pdf,大模型入门到精通,收藏这篇就足够了!

学习如何通过构建自己的模型来理解生成式人工智能,这些模型可以撰写连贯的文本、生成逼真的图像,甚至创作出栩栩如生的音乐。

2401_82469710的博客 904

ZYNQ实战:如何用AXI-stream FIFO解决跨时钟域数据同步难题(附完整代码)

本文深入探讨了在ZYNQ项目中,如何利用AXI-Stream FIFO有效解决PS与PL之间的跨时钟域数据同步难题。文章详细分析了异步FIFO的架构设计、关键配置参数,并提供了PS端软件策略与PL端逻辑设计的完整代码示例,旨在帮助开发者构建稳定可靠的数据流通道,优化系统性能。

nginx7reverse的博客 754
上一篇: Scala 并发编程学习指南(四)
下一篇: PyTorch 生成式人工智能学习指南(二)
绝不原创的飞龙
绝不原创的飞龙 优质创作者: 人工智能、数据科学与机器学习技术领域 优质创作者: 人工智能、数据科学与机器学习技术领域
博客等级 码龄15年 4万+粉丝 2万+原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值