原文:
zh.annas-archive.org/md5/355d709877e6e04dc1540c8ccd0b447d
译者:飞龙
前言
人工智能 (AI) 已经到来,并成为推动日常应用的强大力量。 就像火的发现/发明,轮子,石油,电力和电子技术一样,AI 正在以我们难以想象的方式重塑我们的世界。 AI 历来是一个计算机科学的小众学科,只有少数实验室提供。 但由于出色理论的爆炸,计算能力的增加和数据的可用性,这一领域在 21 世纪初开始呈指数增长,并显示出不会很快放缓的迹象。
AI 一再证明,只要给定合适的算法和足够的数据量,它可以几乎不需要人为干预地学习任务,并产生可以与人类判断相匹敌甚至超越的结果。 无论您是刚开始学习或是驾驭大型组织的资深人士,都有充分的理由了解 AI 的工作原理。 神经网络 (NNs) 是适应广泛应用的最灵活的 AI 算法之一,包括结构化数据,文本和视觉领域。
本书从 NN 的基础知识开始,涵盖了超过 40 个使用PyTorch的计算机视觉应用。 通过掌握这些应用,您将能够为各种领域(如汽车,安全,金融后勤,医疗保健等)的各种用例构建 NN,具备必要的技能不仅能实施最先进的解决方案,还能创新并开发解决更多现实世界挑战的新应用。
最终,本书旨在成为学术学习和实际应用之间的桥梁,使您能够自信前行,并在职业生涯中做出重要贡献。
本书适合的读者
本书适合于刚接触 PyTorch 和中级机器学习实践者,他们希望通过深度学习和 PyTorch 熟悉 CV 技术。 刚开始接触 NN 的人士也会发现本书有用。 您只需具备 Python 编程语言和机器学习的基础知识即可开始本书。
本书内容涵盖了什么
第一章,人工神经网络基础,为您详细介绍了 NN 的工作原理。 您将首先学习与 NN 相关的关键术语。 接下来,您将了解构建块的工作细节,并在玩具数据集上从头构建 NN。 在本章结束时,您将对 NN 的工作原理感到自信。
第二章,PyTorch 基础,介绍了如何使用 PyTorch。 您将了解创建和操作张量对象的方法,然后学习使用 PyTorch 构建神经网络模型的不同方法。 您将继续使用玩具数据集,以便了解与 PyTorch 的具体工作方式。
第三章,使用 PyTorch 构建深度神经网络,结合前几章的内容,深入理解各种神经网络超参数对模型准确性的影响。通过本章的学习,你将自信地在真实数据集上操作神经网络。
第四章,引入卷积神经网络,详细介绍了使用传统神经网络的挑战,以及卷积神经网络(CNNs)如何克服传统神经网络的各种局限性的原因。你将深入了解 CNN 的工作细节和其中的各个组件。接下来,你将学习在处理图像时的最佳实践。在本章中,你将开始处理真实世界的图像,并学习 CNN 如何帮助进行图像分类的复杂性。
第五章,图像分类的迁移学习,让你在实际中解决图像分类问题。你将学习多种迁移学习架构,了解它们如何显著提高图像分类的准确性。接下来,你将利用迁移学习实现面部关键点检测和年龄性别估计的使用案例。
第六章,图像分类的实际方面,深入探讨了在构建和部署图像分类模型时需要注意的实际方面。你将实际看到如何利用数据增强和批归一化在真实数据上带来优势。此外,你将学习类激活图如何帮助解释 CNN 模型为什么会预测某个结果的原因。通过本章的学习,你将能够自信地解决大多数图像分类问题,并在自定义数据集上利用前三章讨论的模型。
第七章,物体检测基础,为物体检测奠定了基础,你将了解用于构建物体检测模型的各种技术。接下来,通过一个使用案例,你将学习基于区域提议的物体检测技术,实现一个定位图像中卡车和公共汽车的模型。
第八章,高级物体检测,让你了解了基于区域提议的架构的局限性。接下来,你将学习更先进的架构,如 YOLO 和 SSD,它们解决了基于区域提议的架构的问题。你将在同一数据集(卡车与公共汽车检测)上实现所有这些架构,以便对比每种架构的工作原理。
第九章,图像分割,在前几章的基础上建立,并将帮助你构建能准确定位各种类别对象及其实例在图像中位置的模型。你将在道路图像和普通家居图像上实现使用案例。本章结束时,你将能够自信地解决任何图像分类和对象检测/分割问题,并通过使用 PyTorch 构建模型来解决这些问题。
第十章,目标检测和分割的应用,总结了我们在所有前几章中学到的内容,并开始在几行代码中实现目标检测和分割,并实现模型来执行人群计数和图像着色。接下来,你将学习在真实数据集上进行 3D 目标检测。最后,你将学习如何在视频上执行动作识别。
第十一章,自编码器和图像处理,为修改图像打下基础。你将从学习各种自编码器开始,这些自编码器有助于压缩图像并生成新颖图像。接下来,你将学习对抗攻击,这些攻击能欺骗模型,在实施神经风格转换之前。最后,你将实现一个自编码器来生成深度伪造图像。
第十二章,使用 GAN 生成图像,首先深入介绍了 GAN 的工作原理。接下来,你将实现虚假面部图像生成,并使用 GAN 生成感兴趣的图像。
第十三章,高级 GAN 用于图像操作,将图像操作推向了新的高度。你将使用 GAN 来将对象从一类转换到另一类,从草图生成图像,并操作自定义图像,以便按特定风格生成图像。本章结束时,你将能够自信地使用自编码器和 GAN 的组合进行图像操作。
第十四章,结合计算机视觉和强化学习,首先介绍了强化学习(RL)的术语和为状态分配价值的方式。当你学习深度 Q 学习时,你将了解到如何将 RL 和神经网络结合起来。利用这些知识,你将实现一个代理来玩乒乓球游戏,以及一个实现自动驾驶汽车的代理。
第十五章,结合计算机视觉和 NLP 技术,详细介绍了变压器的工作原理,你将利用它实现诸如图像分类、手写识别、护照图像中的键-值提取以及最后的图像视觉问答等应用。在这个过程中,你将学习多种自定义/利用变压器架构的方法。
Chapter 16, Foundation Models in Computer Vision, starts by strengthening your understanding of combining image and text using CLIP model. Next, you will discuss the Segment Anything Model (SAM), which helps with a variety of tasks – segmentation, recognition, and tracking without any training. Finally, you will understand the working details of diffusion models before you learn the importance of prompt engineering and the impact of bigger pre-trained models like SDXL.
Chapter 17, Applications of Stable Diffusion, extends what you learned in the previous chapters by walking you through how a variety of Stable Diffusion applications (image in-painting, ControlNet, DepthNet, SDXL Turbo, and text-to-video) are trained and then walking you through leveraging different models to perform different tasks.
Chapter 18, Moving a Model to Production, describes the best practices for moving a model to production. You will first learn about deploying a model on a local server before moving it to the AWS public cloud. Next, you will learn about the impact of half-precision on latency, and finally, you will learn about leveraging vector stores (for instance, FAISS) and identifying data drift once a model is moved to production.
随着领域的发展,我们将定期向 GitHub 存储库添加有价值的补充内容。请检查每个章节目录中的supplementary_sections
文件夹以获取新的有用内容。
要充分利用本书
Software/hardware covered in the book | OS requirements |
---|---|
Minimum 128 GB storageMinimum 8 GB RAMIntel i5 processor or betterNVIDIA 8+ GB graphics card – GTX1070 or betterMinimum 50 Mbps internet speed | Windows, Linux, and macOS |
Python 3.6 and above | Windows, Linux, and macOS |
PyTorch 2.1 | Windows, Linux, and macOS |
Google Colab (can run in any browser) | Windows, Linux, and macOS |
请注意,本书中的几乎所有代码都可以通过点击 GitHub 上每个笔记本章节的Open Colab按钮在 Google Colab 中运行。
如果您使用本书的数字版本,我们建议您自行输入代码或通过 GitHub 存储库访问代码(链接在下一部分提供)。这样做可以帮助您避免与复制粘贴代码相关的潜在错误。
下载示例代码文件
本书的代码包托管在 GitHub 上,链接为:github.com/PacktPublishing/Modern-Computer-Vision-with-PyTorch-2E
。我们还提供了来自丰富书籍和视频目录中的其他代码包。请查看!
下载彩色图像
我们还提供了一份包含本书中使用的屏幕截图/图表的彩色图像的 PDF 文件。您可以在此处下载:packt.link/gbp/9781803231334
。
使用约定
本书采用了许多文本约定。
CodeInText
:表示文本中的代码词语、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。以下是一个例子:“我们正在创建FMNISTDataset
类的val
对象,除了我们之前看到的train
对象。”
代码块设置如下:
# Crop image
img = img[50:250,40:240]
# Convert image to grayscale
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Show image
plt.imshow(img_gray, cmap='gray')
当我们希望引起您对代码块特定部分的注意时,相关行或项将以粗体显示:
**def****accuracy****(****x, y, model****):**
**model.****eval****()** # <- let's wait till we get to dropout section
# get the prediction matrix for a tensor of `x` images
**prediction = model(x)**
# compute if the location of maximum in each row coincides
# with ground truth
**max_values, argmaxes = prediction.****max****(-****1****)**
**is_correct = argmaxes == y**
**return** **is_correct.cpu().numpy().tolist()**
任何命令行输入或输出均如下所示:
$ python3 -m venv fastapi-venv
$ source fastapi-env/bin/activate
粗体:表示一个新术语,一个重要词或您在屏幕上看到的单词。例如,菜单或对话框中的词语会出现在文本中,如此处所示。以下是一个例子:“我们将使用梯度下降(在前馈传递后)逐个批次进行,直到我们在一个训练轮次内用尽所有数据点。”
警告或重要说明如下所示。
提示和技巧如下所示。
与我们联系
我们始终欢迎读者的反馈。
一般反馈:电子邮件 feedback@packtpub.com
,并在您的消息主题中提及书名。如果您对本书的任何方面有疑问,请发送电子邮件至 questions@packtpub.com
联系我们。
勘误:尽管我们尽了最大努力确保内容的准确性,但错误难免会发生。如果您在本书中发现错误,我们将不胜感激您向我们报告。请访问 www.packtpub.com/submit-errata
,选择您的书籍,点击勘误提交表单链接,并输入详细信息。
盗版:如果您在互联网上发现我们作品的任何形式的非法副本,请向我们提供位置地址或网站名称将不胜感激。请通过 copyright@packtpub.com
提供链接至该材料的链接。
如果您有兴趣成为作者:如果您在某个专题上有专业知识,并且有兴趣撰写或为书籍做贡献,请访问 authors.packtpub.com
。
分享您的想法
一旦您阅读完Modern Computer Vision with PyTorch, Second Edition,我们很想听听您的想法!请点击此处直接访问亚马逊评论页面,与我们分享您的反馈。
您的评论对我们和技术社区都非常重要,将帮助我们确保提供优质内容。
下载本书的免费 PDF 副本
感谢购买本书!
您喜欢随时随地阅读,但无法随身携带印刷书籍吗?
您购买的电子书与您选择的设备不兼容吗?
别担心,现在每本 Packt 书籍都可以免费获取不受 DRM 限制的 PDF 版本。
无论何时何地,任何设备上都能阅读。直接从您喜爱的技术书籍中搜索、复制和粘贴代码到您的应用程序中。
福利不止于此,您还可以在每天的电子邮箱中获取独家折扣、新闻简报和精彩的免费内容。
按照以下简单步骤获取这些福利:
- 扫描 QR 码或访问以下链接:
packt.link/free-ebook/9781803231334
-
提交您的购买证明。
-
这就是!我们将免费的 PDF 和其他福利直接发送到您的电子邮箱。
第一部分:计算机视觉深度学习基础
在本节中,我们将学习神经网络的基本构建模块,以及每个模块在成功训练神经网络中的作用。在这部分内容中,我们将首先简要介绍神经网络理论,然后转向使用 PyTorch 库构建和训练神经网络。
本节包括以下章节:
-
第一章, 人工神经网络基础
-
第二章, PyTorch 基础
-
第三章, 使用 PyTorch 构建深度神经网络
第一章:人工神经网络基础知识
人工神经网络(ANN)是一种监督学习算法,松散地受人类大脑功能的启发。类似于人类大脑中神经元的连接和激活方式,神经网络接受输入并通过函数传递,导致某些后续神经元被激活,并因此产生输出。
有几种标准的 ANN 架构。通用逼近定理表明,我们总是能找到足够大的神经网络架构和合适的权重集合,可以精确预测任何给定输入的输出。这意味着对于给定的数据集/任务,我们可以创建一个架构,并不断调整其权重,直到 ANN 预测我们想要的结果。调整权重直到 ANN 学习到给定任务称为训练神经网络。在解决各种相关任务中,ANN 如何通过在大型数据集上训练和定制架构来获得重要性。
计算机视觉中的一个突出任务是识别图像中存在的对象类别。ImageNet(www.image-net.org/challenges/LSVRC/index.php
)是一个比赛,旨在识别图像中存在的对象类别。多年来分类错误率的减少如下所示:
图 1.1:ImageNet 比赛中的分类错误率(来源:www.researchgate.net/publication/331789962_Basics_of_Supervised_Deep_Learning
)
2012 年,神经网络(AlexNet)赢得了 ImageNet 比赛。正如您从前面的图表中可以看到的那样,从 2011 年到 2012 年,错误率显著减少,通过利用神经网络。从那时起,随着更深层和复杂的神经网络,分类错误率继续减少,并超过了人类水平的表现。
神经网络不仅在图像分类(以及对象检测和分割等相关任务)中达到了人类水平的性能,而且还启用了一整套全新的用例。生成 AI(GenAI)利用神经网络以多种方式生成内容:
-
从输入文本生成图像
-
从输入图像和文本生成新的定制图像
-
利用多个输入模态(图像、文本和音频)生成新内容
-
从文本/图像输入生成视频
这为我们学习和实现神经网络在我们的定制任务中的应用提供了坚实的动机。
在本章中,我们将在一个简单的数据集上创建一个非常简单的架构,并主要关注人工神经网络的各种构建模块(前馈、反向传播和学习率)如何调整权重,以使网络能够从给定的输入中学习预测期望的输出。我们首先将从数学上学习神经网络是什么,然后从头开始构建一个,以建立坚实的基础。然后,我们将详细了解每个负责训练神经网络的组件,并编写相应的代码。总体而言,我们将涵盖以下主题:
-
比较人工智能和传统机器学习
-
学习关于人工神经网络构建模块
-
实现前向传播
-
实现反向传播
-
将前向传播和反向传播结合起来实现
-
理解学习率的影响
-
总结神经网络的训练过程
所有本章节中的代码片段都可以在 Github 仓库的 Chapter01
文件夹中找到,链接为 bit.ly/mcvp-2e
。
我们强烈建议您通过每个笔记本中的在 Colab 中打开按钮执行代码。
比较人工智能和传统机器学习
传统上,系统通过程序员编写的复杂算法来实现智能化。例如,假设您有兴趣识别一张照片是否包含狗。在传统的机器学习(ML)设置中,机器学习从业者或专业主题专家首先确定需要从图像中提取的特征。然后,他们提取这些特征,并通过一个精心编写的算法将给定的特征解析出来,告诉我们图像是否是一只狗。下图说明了这个概念:
图 1.2:传统机器学习用于分类的工作流程
看下面的样本:
图 1.3:生成规则的示例图像
从前述图像中,一个简单的规则可能是,如果一幅图像中有三个黑色圆圈呈三角形排列,那么它可以被分类为狗。然而,这个规则在这张欺骗性的松饼特写图像面前就会失败:
图 1.4:简单规则可能失败的图像
当然,当显示一个除了狗脸特写之外的图像时,这个规则也会失败。因此,为了准确分类图像,我们需要创建的手动规则数量可能是指数级的,特别是随着图像变得更加复杂。因此,传统方法在非常受限制的环境中效果良好(比如拍护照照片,所有尺寸都在毫米内限制),在无约束环境中效果不佳,因为每幅图像都有很大变化。
我们可以将相同的思路扩展到任何领域,比如文本或结构化数据。过去,如果有人对编程以解决现实任务感兴趣,那么了解输入数据的一切并尽可能多地编写规则以涵盖每种情况就变得必要了。这是一件繁琐的事情,并且不能保证所有新情况都会遵循这些规则。
然而,通过利用 ANN,我们可以在一步中完成这一操作。
神经网络提供了独特的好处,即在单一步骤中结合特征提取(手动调整)并将这些特征用于分类/回归,只需少量手动特征工程。这两个子任务只需标记数据(例如哪些图片是狗,哪些不是狗)和一个神经网络架构。它不需要人类提出规则来分类图像,这减少了传统技术对程序员的大部分负担。
注意,主要要求是我们为需要解决方案的任务提供大量示例。例如,在前面的情况下,我们需要为模型提供多个狗和非狗图片,以便它学习特征。神经网络在分类任务中的高层视图如下所示:
图 1.5:基于神经网络的分类方法
现在我们已经对神经网络比传统计算机视觉方法表现更好的基本原因有了非常高层次的概述,让我们在本章的各个部分深入了解神经网络如何工作。
了解 ANN 构建模块
人工神经网络(ANN)是一组张量(权重)和数学操作,以松散复制人脑功能的方式排列。它可以被视为接受一个或多个张量作为输入并预测一个或多个张量作为输出的数学函数。连接这些输入到输出的操作排列被称为神经网络的架构 - 我们可以根据手头任务定制它,即基于问题是否包含结构化(表格)或非结构化(图像、文本和音频)数据(这是输入和输出张量列表)。
人工神经网络由以下组成:
-
输入层:这些层将独立变量作为输入。
-
隐藏(中间)层:这些层连接输入和输出层,同时在输入数据之上执行变换。此外,隐藏层包含节点(在下图中表示为单元/圆圈),用于将它们的输入值修改为更高维/低维的值。通过使用各种激活函数修改中间层节点的值来实现更复杂表示的功能。
-
输出层:这生成了输入变量通过网络传递时预期的值。
考虑到这一点,神经网络的典型结构如下:
图 1.6:神经网络结构
输出层中的节点(在上图中表示为圆圈)的数量取决于手头的任务以及我们是否试图预测连续变量或分类变量。如果输出是连续变量,则输出层有一个节点。如果输出是具有m个可能类别的分类变量,则输出层有m个节点。让我们放大一个节点/神经元并看看发生了什么。一个神经元的输入转换如下:
图 1.7:神经元的输入转换
在上述图表中,x[1],x[2], …, x[n]是输入变量,w[0]是偏置项(类似于线性/逻辑回归中的偏差)。
注意,w[1],w[2], …, w[n]是分配给每个输入变量的权重,w[0]是偏置项。输出值a的计算如下:
正如您所看到的,它是权重和输入对的乘积之和,后跟额外的函数f(偏置项 + 乘积之和)。函数f是激活函数,用于在这些乘积之和之上应用非线性。在接下来的部分中,将详细介绍激活函数。此外,通过具有多个隐藏层,可以实现更多的非线性。
在高层次上,神经网络是一组节点,每个节点具有称为权重的可调浮点值,并且节点作为图形互连以按照网络结构返回输出。网络由三个主要部分组成:输入层、隐藏层(们)和输出层。请注意,可以有更多的隐藏层数 (n),术语深度学习指的是更多的隐藏层数。通常,在神经网络需要理解像图像识别这样复杂的事物时,需要更多的隐藏层。
考虑到神经网络的架构,让我们了解一下前馈传播,它有助于估计网络架构存在的误差(损失)的量。
实现前馈传播
为了建立对前馈传播如何工作的坚实基础理解,我们将通过训练神经网络的玩具示例来进行介绍,其中神经网络的输入为(1, 1),对应的(期望的)输出为 0。在此,我们将根据这对单一输入输出找到神经网络的最优权重。
在实际项目中,将有数千个数据点用于训练人工神经网络。
本例中的神经网络架构包含一个包含三个节点的隐藏层,如下所示:
图 1.8:带有 1 个隐藏层的示例神经网络架构
前述图表中的每个箭头都包含一个浮点值(权重),可调整。有 9 个浮点数(6 个对应于输入节点与隐藏层节点之间的连接的权重,3 个对应于隐藏层与输出层之间的连接的权重),我们需要找到这些值,使得当输入为(1,1)时,输出尽可能接近(0)。这就是我们所说的训练神经网络。出于简化目的,我们尚未引入偏置值,但基本逻辑保持不变。
在接下来的部分,我们将了解前述网络的以下内容:
-
计算隐藏层的值
-
执行非线性激活
-
估计输出层的值
-
计算对应于期望值的损失值
计算隐藏层单元值
现在,我们将为所有连接分配权重。在第一步中,我们随机分配所有连接的权重。一般来说,在训练开始之前,神经网络会用随机权重进行初始化。再次强调,为了简化起见,在介绍前向传播和反向传播的过程中,我们不包括偏置值。但是在后续章节中实现从头开始的前向传播和反向传播时会有。
让我们从在 0 到 1 之间随机初始化的初始权重开始。
重要提示
神经网络训练过程中的最终权重不需要处于特定数值范围内。
网络中提供的权重和值的正式表示如下图所示(左半部分),网络中提供的随机初始化权重如右半部分所示。
图 1.9:(左)神经网络的正式表示(右)神经网络的随机权重初始化
在下一步中,我们执行输入与权重的乘法,以计算隐藏层中隐藏单元的值。在激活之前,隐藏层单元的值如下获得:
此处计算的隐藏层单元值(在激活之前)也显示在下图中:
图 1.10:激活前的隐藏层单元值
现在,我们将通过非线性激活函数传递隐藏层的值。
重要提示
如果我们在隐藏层中不应用非线性激活函数,无论存在多少隐藏层,神经网络都会变成从输入到输出的巨大线性连接。
应用激活函数
激活函数有助于建模输入和输出之间的复杂关系。一些经常使用的激活函数如下计算(其中 x 是输入):
对于各个先前激活的可视化,用于各种输入值的图形如下:
图 1.11:不同输入值的不同激活函数的输出
以我们的例子为例,让我们对三个隐藏层的总和应用 sigmoid(逻辑)激活函数 S(x)。通过这样做,我们在 sigmoid 激活后得到以下数值:
现在我们已经获得了激活后的隐藏层数值,在接下来的部分中,我们将获得输出层数值。
计算输出层的数值
到目前为止,我们已经计算出了应用 sigmoid 激活后的最终隐藏层数值。使用激活后的隐藏层数值和权重值(在第一次迭代中随机初始化),我们将计算网络的输出值:
图 1.12:在隐藏单元值上应用 Sigmoid 激活
我们执行隐藏层值和权重值的乘积之和以计算输出值。另一个提醒:我们排除了需要在每个单元(节点)添加的偏差项,仅简化我们对前向传播和反向传播工作细节的理解,将在编写前向传播和反向传播时包含它:
因为我们从一组随机权重开始,输出节点的值与目标非常不同。在这种情况下,差异为 1.235(请记住,目标是 0)。接下来,让我们计算与当前网络状态相关的损失值。
计算损失值
损失值(也称为成本函数)是我们在神经网络中优化的值。为了理解如何计算损失值,让我们看看两种情况:
-
连续变量预测
-
分类变量预测
计算连续变量预测期间的损失
通常,在变量连续时,损失值计算为实际值和预测之间差的平方的平均值 —— 也就是说,我们通过改变与神经网络相关的权重值来最小化均方误差。均方误差值计算如下:
在上述方程中, 是实际输出。
是由神经网络计算的预测
(其权重存储在形式为
的形式中),其输入为
,m 是数据集中的行数。
关键要点是对于每组唯一的权重,神经网络将预测不同的损失,我们需要找到使损失为零(或在现实场景中尽可能接近零)的黄金权重组合。
在我们的例子中,假设我们正在预测的结果是连续的。在这种情况下,损失函数值是均方误差,计算方法如下:
现在我们已经计算出连续变量的损失值,接下来学习如何计算分类变量的损失值。
在预测分类变量时计算损失
当要预测的变量是离散的(即变量中仅有少数类别时),通常使用分类交叉熵损失函数。当要预测的变量具有其中两个不同值时,损失函数为二元交叉熵。
二元交叉熵的计算方法如下,其中y是输出的实际值,p是输出的预测值,m是数据点的总数:
分类交叉熵的计算方法如下,其中y是输出的实际值,p是输出的预测值,m是数据点的总数,C是类别的总数:
一种简单的可视化交叉熵损失的方法是查看预测矩阵本身。假设您在图像识别问题中预测五类——狗、猫、老鼠、牛和母鸡。神经网络的最后一层必须有五个神经元,采用 softmax 激活(关于 softmax 的更多信息请见下一节)。这样,它将被迫为每个数据点的每个类别预测一个概率。假设有五幅图像,并且预测的概率如下所示(每行中突出显示的单元格对应于目标类):
图 1.13:交叉熵损失计算
注意每一行的总和为 1。在第一行中,当目标为Dog且预测概率为0.88时,相应的损失为0.128(这是0.88的负对数)。类似地,计算其他损失。正如您所见,当正确类别的概率高时,损失值较低。正如您所知,概率在 0 到 1 之间变化。因此,最小可能的损失可以是 0(当概率为 1 时),而最大损失可以是无穷大(当概率为 0 时)。
在数据集中,最终的损失是所有行中所有个体损失的平均值。
现在我们已经对计算均方误差损失和交叉熵损失有了扎实的理解,让我们回到我们的示例中。假设我们的输出是一个连续变量,我们将在后面的章节中学习如何使用反向传播来最小化损失值。我们将更新之前随机初始化的权重值 来最小化损失 (
)。但在此之前,让我们先用 Python 中的 NumPy 数组编写前向传播代码,以加深对其工作原理的理解。
编写代码中的前向传播
编写前向传播代码的高级策略如下:
-
在每个神经元执行求和乘积。
-
计算激活。
-
在每个神经元重复前两个步骤直到输出层。
-
通过将预测值与实际输出进行比较来计算损失。
前向传播函数接受输入数据、当前神经网络权重和输出数据作为输入,并返回当前网络状态的损失作为输出。
用于计算所有数据点上均方误差损失值的前向传播函数在 GitHub 代码库的 Chapter01
文件夹中的 Feed_forward_propagation.ipynb
文件中,链接地址为 bit.ly/mcvp-2e
。
我们强烈建议您点击每个笔记本中的 在 Colab 中打开 按钮来执行代码笔记本。示例如下:
图 1.14: GitHub 笔记本中的“在 Colab 中打开”按钮
一旦点击 在 Colab 中打开,您将能够轻松执行所有代码,并应能够复制本书中显示的结果。
为了使这个练习更加现实,我们将使每个节点都有关联的偏置。因此,权重数组将包含连接不同节点的权重以及隐藏/输出层节点的偏置。现在可以执行代码了,让我们继续编写前向传播代码:
-
将输入变量值 (
inputs
)、权重 (weights
,如果这是第一次迭代则为随机初始化) 和提供的数据集中的实际输出 (outputs
) 作为feed_forward
函数的参数:import numpy as np def feed_forward(inputs, outputs, weights):
-
通过执行输入和连接输入层到隐藏层的权重值 (
weights[0]
) 的矩阵乘法 (np.dot
) 并添加与隐藏层节点相关的偏置项 (weights[1]
) 来计算隐藏层的值:pre_hidden = np.dot(inputs,weights[0])+ weights[1]
-
在前一步骤中获得的隐藏层值
pre_hidden
上应用 Sigmoid 激活函数:hidden = 1/(1+np.exp(-pre_hidden))
-
通过执行隐藏层激活值 (
hidden
) 和连接隐藏层到输出层的权重 (weights[2]
) 的矩阵乘法 (np.dot
),并将输出与与输出层节点相关的偏置weights[3]
相加来计算输出层的值:pred_out = np.dot(hidden, weights[2]) + weights[3]
-
计算数据集上的均方误差值,并返回均方误差的均值:
mean_squared_error = np.mean(np.square(pred_out - outputs)) return mean_squared_error
现在,通过网络进行前向传播,我们能够得到均方误差值。
在我们学习反向传播之前,让我们学习一些前馈网络的组成部分——激活函数和损失值计算——通过在 NumPy 中实现它们,以便我们深入了解它们的工作原理。
代码中的激活函数
虽然我们在前面的代码中对隐藏层值应用了 sigmoid 激活,让我们看看其他常用的激活函数:
-
Tanh:值(隐藏层单元值)的 tanh 激活计算如下:
def tanh(x): return (np.exp(x)-np.exp(-x))/(np.exp(x)+np.exp(-x))
-
ReLU:值(隐藏层单元值)的修正线性单元(ReLU)计算如下:
def relu(x): return np.where(x>0,x,0)
-
线性:值的线性激活是值本身。这也称为“恒等激活”或“无激活”,很少使用。表示如下:
def linear(x): return x
-
Softmax:与其他激活函数不同,softmax 是在一组值之上执行的。这通常用于确定在给定场景中输入属于m个可能输出类别中的一个的概率。假设我们试图将一幅数字的图像分类为可能的 10 个类别(从 0 到 9)中的一个。
-
在这种情况下,有 10 个输出值,其中每个输出值应代表输入图像属于 10 个类别之一的概率。
-
Softmax 激活用于为输出中的每个类别提供概率值,计算如下:
-
def softmax(x): return np.exp(x)/np.sum(np.exp(x))
注意,对输入x
的两个操作——np.exp
将使所有值变为正数,而通过np.sum(np.exp(x))
对所有这些指数进行的除法将强制所有值位于 0 和 1 之间。这个范围与事件概率重合。这就是我们所说的返回概率向量的含义。
现在我们已经学习了各种激活函数,接下来我们将学习不同的损失函数。
代码中的损失函数
在神经网络训练过程中,通过更新权重值来最小化损失值(在代码中)。定义适当的损失函数是构建工作和可靠的神经网络模型的关键。通常在构建神经网络时使用的损失函数如下:
- 均方误差:均方误差是输出的实际值和预测值之间的平方差。我们对误差取平方,因为误差可以是正或负(当预测值大于实际值或反之时)。平方确保正负误差不会互相抵消。我们计算平均值的平方误差,以便在数据集大小不同时比较两个不同数据集上的误差。
预测输出值数组 (p
) 和实际输出值数组 (y
) 之间的均方误差计算如下:
def mse(p, y):
return np.mean(np.square(p - y))
当试图预测具有连续性质的值时,通常使用均方误差。
- 均绝对误差: 均绝对误差的工作方式与均方误差非常相似。均绝对误差通过计算所有数据点的实际值和预测值之间的绝对差的平均值,确保正负误差不会互相抵消。
预测输出值数组 (p
) 和实际输出值数组 (y
) 之间的均绝对误差实现如下:
def mae(p, y):
return np.mean(np.abs(p-y))
类似于均方误差,均绝对误差通常用于连续变量。
- 二元交叉熵:交叉熵是衡量两个不同分布之间差异的指标:实际分布和预测分布。二元交叉熵适用于二进制输出数据,与我们讨论过的前两种损失函数(适用于连续变量预测)不同。
预测值数组 (p
) 和实际值数组 (y
) 之间的二元交叉熵实现如下:
def binary_cross_entropy(p, y):
return -np.mean((y*np.log(p)+(1-y)*np.log(1-p)))
注意,当预测值远离实际值时,二元交叉熵损失值很高,而当预测值和实际值接近时,损失值很低。
- 分类交叉熵:预测值数组 (
p
) 和实际值数组 (y
) 之间的分类交叉熵实现如下:
def categorical_cross_entropy(p, y):
return -np.mean(np.log(p[np.arange(len(y)),y]))
到目前为止,我们已经学习了前向传播及其组成部分,如权重初始化、与节点相关的偏差以及激活和损失函数。在接下来的部分中,我们将学习反向传播,这是一种调整权重的技术,使得它们导致的损失尽可能小。
实施反向传播
在前向传播中,我们将输入层连接到隐藏层,然后连接到输出层。在第一次迭代中,我们随机初始化权重,然后计算由这些权重值导致的损失。在反向传播中,我们采取相反的方法。我们从前向传播中获得的损失值开始,并以这样的方式更新网络的权重,以使损失值尽可能小。
通过执行以下步骤减少损失值:
-
通过逐个地改变神经网络中的每个权重来更新权重。
-
当改变权重值时 (
),测量损失的变化 (
)。
-
更新权重为
,其中 k 是一个正值,是称为学习率的超参数。
注意,对特定权重的更新与通过改变它稍微减少的损失量成比例。直观地说,如果改变一个权重减少了大量损失值,那么我们可以大幅度更新该权重。然而,如果通过改变权重减少的损失较小,那么我们只更新它一小部分。
如果在整个数据集上(我们已经进行了前向传播和反向传播)执行了前面的步骤n次,这实质上就是训练n个epochs。
由于典型神经网络包含成千上万的权重,改变每个权重的值并检查损失是否增加或减少并不是最优的做法。前述列表中的核心步骤是测量在改变权重时损失的变化。正如你在微积分中学习的那样,衡量这一点与计算损失关于权重的梯度相同。在下一节中,关于反向传播的链式法则将更多地利用微积分中的偏导数来计算损失关于权重的梯度。但在这一节之前,我们将通过稍微改变一次更新一个权重的方式来从头实现梯度下降。然而,在实施反向传播之前,让我们了解神经网络的一个额外细节:学习率。
直观地说,学习率有助于建立对算法的信任。例如,在决定权重更新的大小时,我们可能不会一次性改变权重值很大的量,而是更慢地更新它。
这导致我们的模型获得稳定性;我们将看看学习率如何在理解学习率影响部分中帮助稳定性。
我们更新权重以减少错误的整个过程被称为梯度下降。随机梯度下降是如何在前述场景中最小化错误的。如前所述,梯度表示差异(即当权重值稍微改变时损失值的差异),下降意味着减少。另一种解释是,梯度表示斜率(损失下降的方向),下降意味着向更低的损失移动。随机代表基于随机样本的选择,根据这些样本作出决策。
除了随机梯度下降外,还有许多类似的优化器帮助最小化损失值;不同的优化器将在下一章讨论。
在接下来的两节中,我们将学习如何在 Python 中从头开始编写反向传播,并简要讨论使用链式法则进行反向传播的工作原理。
代码中的梯度下降
梯度下降在 Python 中的实现如下:
以下代码在本书的 GitHub 存储库的Chapter01
文件夹中作为Gradient_descent.ipynb
提供 - bit.ly/mcvp-2e
。
-
定义前馈网络并计算均方误差损失值,就像我们在代码中的前向传播部分所做的那样:
from copy import deepcopy import numpy as np def feed_forward(inputs, outputs, weights): pre_hidden = np.dot(inputs,weights[0])+ weights[1] hidden = 1/(1+np.exp(-pre_hidden)) pred_out = np.dot(hidden, weights[2]) + weights[3] mean_squared_error = np.mean(np.square(pred_out - outputs)) retur mean_squared_error
-
逐个将每个权重和偏置值增加一个非常小的量(0.0001),并计算每个权重和偏置更新的总平方误差损失值。
- 在以下代码中,我们正在创建一个名为
update_weights
的函数,该函数执行梯度下降过程以更新权重。函数的输入是网络的输入变量inputs
、期望输出outputs
、权重(在训练模型开始时随机初始化)以及模型的学习率lr
(关于学习率的更多信息将在后面的部分讨论):
def update_weights(inputs, outputs, weights, lr):
- 确保深度复制权重列表。由于权重将在后续步骤中被操作,
deepcopy
确保我们可以使用多个副本的权重而不影响原始权重值。我们将创建三个原始权重集的副本作为函数的输入 -original_weights
、temp_weights
和updated_weights
:
original_weights = deepcopy(weights) temp_weights = deepcopy(weights) updated_weights = deepcopy(weights)
- 通过将输入、输出和原始权重通过前馈函数传递,计算损失值(original_loss):
original_loss = feed_forward(inputs, outputs, original_weights)
- 我们将遍历网络的所有层:
for i, layer in enumerate(original_weights):
- 在我们的神经网络中有四个参数列表 - 两个用于连接输入到隐藏层的权重和偏置参数列表,另外两个用于连接隐藏层到输出层的权重和偏置参数列表。现在,我们遍历所有的单个参数,并且由于每个列表具有不同的形状,我们利用
np.ndenumerate
来遍历给定列表中的每个参数:
for index, weight in np.ndenumerate(layer):
- 现在我们将原始权重集存储在
temp_weights
中。我们选择第 i 层中存在的权重的索引,并增加一个小的值。最后,我们使用神经网络的新权重集计算新的损失:
temp_weights = deepcopy(weights) temp_weights[i][index] += 0.0001 _loss_plus = feed_forward(inputs, outputs, temp_weights)
-
在前述代码的第一行中,我们将
temp_weights
重置为原始权重集,因为在每次迭代中,当参数在给定 epoch 内以微小量更新时,我们会更新不同的参数以计算损失。 -
我们计算由于权重改变而产生的梯度(损失值的变化):
grad = (_loss_plus - original_loss)/(0.0001)
- 在以下代码中,我们正在创建一个名为
这个通过微小量更新参数然后计算梯度的过程相当于微分的过程。
-
最后,我们更新对应第 i 层和
index
的参数。更新后的权重值将按照梯度值的比例减少。此外,我们不完全通过减去梯度值来快速减少权重值,而是通过使用学习率lr
逐步建立信任(有关学习率的更多信息,请参阅理解学习率影响部分): -
updated_weights[i][index] -= grad*lr
-
一旦跨所有层和层内索引的参数值更新完成,我们返回更新后的权重值 -
updated_weights
: -
return updated_weights, original_loss
神经网络中的另一个参数是用于计算损失值的批量大小。
在前面的场景中,我们考虑了所有数据点来计算损失(均方误差)值。然而,在实际操作中,当我们有成千上万(或在某些情况下,百万级)的数据点时,计算损失值时更多数据点的增量贡献会遵循边际收益递减法则,因此我们会使用远小于总数据点数的批量大小。我们将应用梯度下降(在前馈传播之后)每次使用一个批次,直到我们在一轮训练的一个时期内耗尽所有数据点。在构建模型时考虑的典型批次大小介于 32 和 1,024 之间。通常是 2 的幂,并且对于非常大的模型,根据情况,批量大小可以小于 32。
使用链式法则实现反向传播
到目前为止,我们通过更新权重值一小部分来计算与权重相关的损失的梯度,然后计算在原始情景(权重未更改时)和在更新权重后的前向传播损失之间的差异。以这种方式更新权重值的一个缺点是,当网络很大(有更多权重需要更新)时,需要大量计算来计算损失值(实际上,需要两次计算 - 一次是权重值未更改时,再次是权重值稍微更新后)。这导致更多的计算,因此需要更多的资源和时间。在本节中,我们将学习如何利用链式法则,它不需要我们手动计算损失值即可得出损失对权重值的梯度。
在第一次迭代中(我们随机初始化权重时),输出的预测值为 1.235。为了得到理论公式,让我们将权重和隐藏层值及隐藏层激活表示为w、h和a,如下所示:
图 1.15:泛化权重初始化过程
请注意,在前面的图表中,我们已经将左图的每个组件值概括在右图中。
为了简化理解,在本节中,我们将了解如何使用链规则计算损失值相对于仅 w[11] 的梯度。这种学习方法可以扩展到神经网络的所有权重和偏差。我们鼓励您练习并应用链规则计算到其余的权重和偏差值。此外,为了我们学习的目的简化,我们将仅处理一个数据点,其中输入为 {1,1},预期输出为 {0}。
本书的 GitHub 存储库中Chapter01
文件夹中的chain_rule.ipynb
笔记本包含使用链规则计算网络中所有参数的权重和偏差变化的梯度的方法,网址为bit.ly/mcvp-2e
。
鉴于我们正在计算损失值相对于 w[11] 的梯度,请让我们通过以下图表理解所有需要包含的中间组件(在以下图表中,未连接输出到 w[11] 的组件已被标为灰色):
图 1.16:突出显示需要计算损失梯度的值(h[11], a[11], ŷ)
从上述图表中,我们可以看到 w[11]通过突出显示的路径对损失值有贡献 – ,
, 和
。让我们详细说明
,
, 和
是如何分别获得的。
网络的损失值表示如下:
预测输出值 计算如下:
隐藏层激活值(sigmoid 激活)计算如下:
隐藏层值计算如下:
现在我们已经制定了所有方程,请计算损失值 (C) 相对于权重 的变化的影响。
这就是所谓的链规则。本质上,我们正在执行一系列不同的微分以获取我们感兴趣的微分值。
请注意,在上述方程中,我们已经建立了一系列偏微分方程,使我们能够逐个计算这四个组件的偏导数,并最终计算出损失值相对于权重值 的导数。
上述方程中的各个偏导数计算如下:
- 损失值对预测输出值
的偏导数如下所示:
- 预测输出值
对隐藏层激活值
的偏导数如下所示:
- 隐藏层激活值
对隐藏层激活前数值
的偏导数如下所示:
请注意,上述方程来自于 Sigmoid 函数的导数 如下所示:
- 隐藏层激活前数值对权重值
的偏导数如下所示:
在各个偏导数计算完毕后,损失值相对于 的梯度通过用前面步骤中计算的相应值替换每个偏导数项来计算,如下所示:
从上述公式可以看出,我们现在能够计算在权重值略微变化时对损失值的影响(即损失相对于权重的梯度),而无需通过重新计算前向传播来进行蛮力计算。
接下来,我们将按以下方式更新权重值:
两种方法的工作版本:1)使用链式法则识别梯度,然后更新权重;2)学习权重微小变化对损失值影响的更新权重值,在本书的 GitHub 仓库的 Chapter01
文件夹的笔记本 Chain_rule.ipynb
中提供相同的更新权重值,链接为 bit.ly/mcvp-2e
。
在梯度下降中,我们按顺序执行权重更新过程(逐个权重)。通过利用链式法则,我们学到了另一种计算权重值微小变化对损失值影响的替代方法,同时有机会并行计算。
因为我们在所有层上更新参数,整个参数更新过程可以并行化。此外,考虑到在实际场景中可能存在数百万个层参数,将每个参数在 GPU 的不同核心上进行计算,更新权重的时间比逐个权重循环的速度更快。
现在我们对反向传播有了坚实的理解,不仅从直觉上,还通过利用链式法则,让我们学习一下如何使前向传播和反向传播共同工作,以获得最佳的权重值。
将前向传播和反向传播结合起来
在本节中,我们将在与代码中的前向传播部分中使用的玩具数据集相同的简单神经网络上建立一个隐藏层,并利用我们在前一节中定义的update_weights
函数执行反向传播以获得最佳的权重和偏差值。
请注意,我们没有利用链式法则,只是为了让您对前向和反向传播的基础有坚实的理解。从下一章开始,您将不会以这种方式执行神经网络训练。
我们按以下方式定义模型:
-
输入连接到具有三个单元/节点的隐藏层。
-
隐藏层连接到具有一个输出层中的单元。
以下代码在本书的 GitHub 存储库Chapter01
文件夹中作为Back_propagation.ipynb
提供 – bit.ly/mcvp-2e
。
我们将按以下方式创建网络:
-
导入相关包并定义数据集:
from copy import deepcopy import numpy as np x = np.array([[1,1]]) y = np.array([[0]])
-
随机初始化权重和偏差值。
隐藏层中有三个单元,并且每个输入节点连接到隐藏层单元中的每一个。因此,总共有六个权重值和三个偏差值 – 每个隐藏单元对应一个偏差和两个权重(两个权重来自两个输入节点)。此外,最终层有一个单元连接到隐藏层的三个单元。因此,总共有三个权重和一个偏差决定输出层的值。随机初始化的权重如下:
W = [ np.array([[-0.0053, 0.3793], [-0.5820, -0.5204], [-0.2723, 0.1896]], dtype=np.float32).T, np.array([-0.0140, 0.5607, -0.0628], dtype=np.float32), np.array([[ 0.1528,-0.1745,-0.1135]],dtype=np.float32).T, np.array([-0.5516], dtype=np.float32) ]
在前面的代码中,第一个参数数组对应于连接输入层到隐藏层的 2 x 3 权重矩阵。第二个参数数组代表与隐藏层每个节点相关联的偏差值。第三个参数数组对应于连接隐藏层到输出层的 3 x 1 权重矩阵,最后一个参数数组表示与输出层相关联的偏差。
-
将神经网络通过 100 次前向传播和反向传播的时期 – 函数已经学习并在前面的章节中定义为
feed_forward
和update_weights
函数:- 定义
feed_forward
函数:
def feed_forward(inputs, outputs, weights): pre_hidden = np.dot(inputs,weights[0])+ weights[1] hidden = 1/(1+np.exp(-pre_hidden)) pred_out = np.dot(hidden, weights[2]) + weights[3] mean_squared_error = np.mean(np.square(pred_out - outputs)) return mean_squared_error
- 定义
update_weights
函数(我们将在下一节详细了解学习率lr):
def update_weights(inputs, outputs, weights, lr): original_weights = deepcopy(weights) temp_weights = deepcopy(weights) updated_weights = deepcopy(weights) original_loss = feed_forward(inputs, outputs, original_weights) for i, layer in enumerate(original_weights): for index, weight in np.ndenumerate(layer): temp_weights = deepcopy(weights) temp_weights[i][index] += 0.0001 _loss_plus = feed_forward(inputs, outputs, temp_weights) grad = (_loss_plus - original_loss)/(0.0001) updated_weights[i][index] -= grad*lr return updated_weights, original_loss
- 在 100 个时期内更新权重并获取损失值和更新后的权重值:
losses = [] for epoch in range(100): W, loss = update_weights(x,y,W,0.01) losses.append(loss)
- 定义
-
绘制损失值图表:
import matplotlib.pyplot as plt %matplotlib inline plt.plot(losses) plt.title('Loss over increasing number of epochs') plt.xlabel('Epochs') plt.ylabel('Loss value')
上述代码生成了以下图表:
图 1.17:随着 epochs 增加,损失值的变化情况
正如你所看到的,损失从约 0.33 开始稳定下降到约 0.0001。这表明权重根据输入输出数据进行调整,当给定输入时,我们可以预期它预测与损失函数中进行比较的输出。输出权重如下:
[array([[ 0.01424004, -0.5907864 , -0.27549535], [ 0.39883757, -0.52918637, 0.18640439]], dtype=float32), array([ 0.00554004, 0.5519136 , -0.06599568], dtype=float32), array([[ 0.3475135 ], [-0.05529078], [ 0.03760847]], dtype=float32), array([-0.22443289], dtype=float32)]
在理解核心 PyTorch 概念之后,可以在 GitHub 仓库的
Chapter02
文件夹中的Auto_gradient_of_tensors.ipynb
文件中演示相同权重的 PyTorch 版本的相同代码。重新访问本节,验证无论网络是用 NumPy 还是 PyTorch 编写,输入和输出都是相同的。在 NumPy 数组中从头开始构建网络,虽然不是最佳选择,但在本章中是为了让您对神经网络的工作细节有坚实的基础。
-
一旦我们有了更新的权重,通过网络传递输入来进行预测,并计算输出值:
pre_hidden = np.dot(x,W[0]) + W[1] hidden = 1/(1+np.exp(-pre_hidden)) pred_out = np.dot(hidden, W[2]) + W[3] # -0.017
上述代码的输出是 -0.017
的值,这个值非常接近预期的输出值 0. 随着 epochs 的增加,pred_out
的值甚至更接近 0。
到目前为止,我们已经学习了前向传播和反向传播。在这里定义的 update_weights
函数的关键部分是学习率,我们将在下一节中学习它。
理解学习率的影响
为了理解学习率如何影响模型的训练,让我们考虑一个非常简单的情况,我们尝试拟合以下方程(请注意,以下方程不同于我们到目前为止正在处理的玩具数据集):
注意,y 是输出,x 是输入。通过一组输入和预期输出值,我们将尝试使用不同的学习率拟合方程,以了解学习率的影响:
以下代码可在本书 GitHub 仓库的 Chapter01
文件夹中的 Learning_rate.ipynb
中找到 – bit.ly/mcvp-2e
。
-
指定输入和输出数据集如下:
x = [[1],[2],[3],[4]] y = [[3],[6],[9],[12]]
-
定义
feed_forward
函数。此外,在这个例子中,我们将修改网络,使其没有隐藏层,并且架构如下:注意,在上述函数中,我们正在估计参数 w 和 b:
from copy import deepcopy import numpy as np def feed_forward(inputs, outputs, weights): pred_out = np.dot(inputs,weights[0])+ weights[1] mean_squared_error = np.mean(np.square(pred_out - outputs)) return mean_squared_error
-
定义
update_weights
函数,就像我们在 梯度下降的代码 部分中定义的那样:def update_weights(inputs, outputs, weights, lr): original_weights = deepcopy(weights) org_loss = feed_forward(inputs, outputs,original_weights) updated_weights = deepcopy(weights) for i, layer in enumerate(original_weights): for index, weight in np.ndenumerate(layer): temp_weights = deepcopy(weights) temp_weights[i][index] += 0.0001 _loss_plus = feed_forward(inputs, outputs, temp_weights) grad = (_loss_plus - org_loss)/(0.0001) updated_weights[i][index] -= grad*lr return updated_weights
-
初始化权重和偏置值为随机值:
W = [np.array([[0]], dtype=np.float32), np.array([[0]], dtype=np.float32)]
注意,权重和偏置值随机初始化为 0 值。此外,输入权重值的形状为 1 x 1,因为输入中每个数据点的形状为 1 x 1,而偏置值的形状为 1 x 1(因为输出中只有一个节点,每个输出只有一个值)。
-
让我们利用学习率为 0.01 的
update_weights
函数,循环执行 1,000 次迭代,并检查权重值(W
)随 epoch 增加的变化情况:weight_value = [] for epx in range(1000): W = update_weights(x,y,W,0.01) weight_value.append(W[0][0][0])
请注意,在上述代码中,我们使用学习率为 0.01,并重复update_weights
函数以在每个 epoch 结束时获取修改后的权重。此外,在每个 epoch 中,我们将最近更新的权重作为输入,以获取下一个 epoch 中的更新后的权重。
-
绘制每个 epoch 结束时的权重参数值:
import matplotlib.pyplot as plt %matplotlib inline epochs = range(1, 1001) plt.plot(epochs,weight_value) plt.title('Weight value over increasing \ epochs when learning rate is 0.01') plt.xlabel('Epochs') plt.ylabel('Weight value')
上述代码导致权重值随 epoch 增加而变化如下:
图 1.18:学习率为 0.01 时随着 epoch 增加的权重值
请注意,在上述输出中,权重值逐渐向右方向增加,然后饱和在约为 3 的最佳值。
要了解学习率对到达最佳权重值的影响,让我们了解在学习率为 0.1 和学习率为 1 时,当增加 epoch 时权重值如何变化。
当我们修改相应的学习率值在步骤 5中并执行步骤 6时,获得以下图表:
图 1.19:(左)学习率为 0.1 时随着 epoch 增加的权重值(右)学习率为 1 时随着 epoch 增加的权重值
注意,当学习率非常小时(0.01),权重值缓慢移动(在较多的 epoch 中)向最佳值靠近。然而,稍高的学习率(0.1)时,权重值起初振荡,然后迅速饱和(在较少的 epoch 中)到达最佳值。最后,当学习率很高(1)时,权重值急剧上升到非常高的值,并且无法达到最佳值。
当学习率较低时,权重值没有大幅波动的原因在于我们通过*梯度 学习率限制了权重更新的幅度,从本质上讲,当学习率较小时,权重更新量较小。然而,当学习率较高时,权重更新量较大,在权重更新量较小时,损失的变化很小,以至于权重无法达到最佳值。
为了更深入地理解梯度值、学习速率和权重值之间的相互作用,让我们仅运行update_weights
函数 10 个周期。此外,我们将打印以下数值以了解它们随着周期增加的变化:
-
每个周期开始时的权重值
-
权重更新前的损失
-
当权重稍作调整时的损失
-
梯度值
我们修改update_weights
函数,打印如下前述数值:
def update_weights(inputs, outputs, weights, lr):
original_weights = deepcopy(weights)
org_loss = feed_forward(inputs, outputs, original_weights)
updated_weights = deepcopy(weights)
for i, layer in enumerate(original_weights):
for index, weight in np.ndenumerate(layer):
temp_weights = deepcopy(weights)
temp_weights[i][index] += 0.0001
_loss_plus = feed_forward(inputs, outputs, temp_weights)
grad = (_loss_plus - org_loss)/(0.0001)
updated_weights[i][index] -= grad*lr
**if****(i %** **2** **==** **0****):**
**print****(****'weight value:'****, \**
**np.****round****(original_weights[i][index],****2****), \**
**'original loss:'****, np.****round****(org_loss,****2****), \**
**'loss_plus:'****, np.****round****(_loss_plus,****2****), \**
**'gradient:'****, np.****round****(grad,****2****), \**
**'updated_weights:'****, \**
**np.****round****(updated_weights[i][index],****2****))**
return updated_weights
在前述代码中加粗显示的行是我们从前一节中修改update_weights
函数的地方,首先我们通过检查 (i % 2 == 0
) 来确认当前是否在处理权重参数,因为另一个参数对应于偏差值,然后我们打印原始权重值 (original_weights[i][index]
)、损失值 (org_loss
)、更新后的损失值 (_loss_plus
)、梯度 (grad
) 和更新后的权重值 (updated_weights
)。
现在让我们了解在我们考虑的三种不同学习速率下,随着周期增加,前述数值如何变化。
学习速率为 0.01 的情景
我们将使用以下代码检查数值:
W = [np.array([[0]], dtype=np.float32),
np.array([[0]], dtype=np.float32)]
weight_value = []
for epx in range(10):
W = update_weights(x,y,W,0.01)
weight_value.append(W[0][0][0])
import matplotlib.pyplot as plt
%matplotlib inline
plt.figure(figsize=(15,5))
plt.subplot(121)
epochs = np.arange(1,11)
plt.plot(epochs, weight_value)
plt.title('Weight value over increasing epochs \n when learning rate is 0.01')
plt.xlabel('Epochs')
plt.ylabel('Weight value')
plt.subplot(122)
plt.plot(epochs, loss_value)
plt.title('Loss value over increasing epochs \n when learning rate is 0.01')
plt.xlabel('Epochs')
plt.ylabel('Loss value')
前述代码的输出如下:
图 1.20:当学习速率为 0.01 时,随着周期增加权重和损失值的变化
注意,当学习速率为 0.01 时,损失值下降缓慢,权重值向最优值更新速度也慢。现在让我们了解学习速率为 0.1 时的前述变化。
学习速率为 0.1
代码与学习速率为 0.01 的情景保持一致;然而,在这种情况下,学习速率参数将为 0.1。更改学习速率参数值后运行相同代码的输出如下:
图 1.21:当学习速率为 0.1 时,随着周期增加权重和损失值的变化
让我们对比学习速率为 0.01 和 0.1 的情景——两者之间的主要区别如下:
当学习速率为 0.01 时,与学习速率为 0.1 相比,权重更新速度较慢(在第一个周期中,学习速率为 0.01 时从 0 更新到 0.45,而学习速率为 0.1 时更新到 4.5)。更新速度较慢的原因是学习速率较低,因为权重是通过梯度乘以学习速率进行更新。
除了权重更新幅度,我们还应注意权重更新的方向。当权重值小于最优值时,梯度为负;当权重值大于最优值时,梯度为正。这种现象有助于权重值向正确方向更新。
最后,我们将以学习速率为 1 进行对比。
学习速率为 1
代码与学习率为 0.01 的情况下保持不变;但在这种情况下,学习率参数为 1。更改学习率参数后运行相同代码的输出如下:
图 1.22:在学习率为 1 时,随着周期的增加,权重和损失值的变化
从上图中我们可以看到,权重已偏向一个非常高的值(例如在第一个周期结束时,权重值为 45,在后续周期中进一步偏向一个非常大的值)。此外,权重值已移动到一个非常大的数值,因此权重值的微小变化几乎不会导致梯度变化,从而使得权重困在这个高值上。
注意
一般来说,采用较低的学习率效果更好。这样一来,模型学习得更慢,但会朝着最优值调整权重。典型的学习率参数值范围在 0.0001 到 0.01 之间。
现在我们已经了解了神经网络的构建模块——前向传播、反向传播和学习率——在接下来的部分,我们将总结如何将这三者结合起来训练神经网络的高级概述。
总结神经网络的训练过程
训练神经网络是通过重复两个关键步骤——前向传播和反向传播——来找到神经网络架构的最优权重的过程,给定一个学习率。
在前向传播中,我们将一组权重应用于输入数据,通过定义的隐藏层传递,对隐藏层输出执行定义的非线性激活,然后通过将隐藏层节点值与另一组权重相乘连接到输出层,以估算输出值。最后,我们计算与给定权重集合对应的整体损失。对于第一次前向传播,权重值是随机初始化的。
在反向传播中,我们通过调整权重以减少总体损失的方向来减少损失值(误差)。此外,权重更新的幅度是梯度乘以学习率。
反向传播和前向传播的过程重复进行,直到尽可能地减少损失。这意味着,在训练结束时,神经网络已经调整了其权重 以预测我们希望其预测的输出。在上述示例中,训练后,更新的网络在输入 {1,1} 时将预测输出值为 0,因为它被训练成这样。
总结
在这一章中,我们了解到需要一个单一网络来同时执行特征提取和分类,然后学习了人工神经网络的架构和各个组件。接下来,我们学习了如何连接网络的各个层,并实现了前馈传播来计算网络当前权重对应的损失值。然后,我们实现了反向传播来优化权重以最小化损失值,并学习了学习率在实现网络的最优权重中的作用。此外,我们从头开始使用 NumPy 实现了网络的所有组件——前馈传播、激活函数、损失函数、链式法则和梯度下降来更新权重,以便在接下来的章节中建立坚实的基础。
现在我们了解了神经网络的工作原理,接下来将在下一章中使用 PyTorch 实现一个神经网络,并深入探讨可以在神经网络中调整的各种其他组件(超参数)。
问题
-
神经网络中的各种层有哪些?
-
前馈传播的输出是什么?
-
连续依赖变量的损失函数与二元依赖变量或分类依赖变量的损失函数有何不同?
-
什么是随机梯度下降?
-
反向传播练习的作用是什么?
-
在反向传播期间如何更新跨层所有权重?
-
在训练神经网络的每个 epoch 中使用了哪些函数?
-
为什么在 GPU 上训练网络比在 CPU 上训练更快?
-
在训练神经网络时学习率的影响是什么?
-
学习率参数的典型值是多少?
在 Discord 上了解更多信息
加入我们社区的 Discord 空间,与作者和其他读者进行讨论:
第二章:PyTorch 基础知识
在上一章中,我们学习了神经网络的基本构建模块,并且在 Python 中从头开始实现了前向传播和反向传播。
在本章中,我们将深入探讨使用 PyTorch 构建神经网络的基础知识,在后续章节中,当我们学习图像分析的各种用例时,将多次利用这些知识。我们将首先学习 PyTorch 工作的核心数据类型——张量对象。然后,我们将深入探讨可以在张量对象上执行的各种操作,以及在构建神经网络模型时如何利用它们,这是在一个玩具数据集上(在我们逐步查看更现实的数据集之前,从下一章开始)加强我们的理解。这将使我们能够理解如何使用 PyTorch 构建神经网络模型以映射输入和输出值。最后,我们将学习如何实现自定义损失函数,以便根据我们解决的用例定制它们。
具体而言,本章将涵盖以下主题:
-
安装 PyTorch
-
PyTorch 张量
-
使用 PyTorch 构建神经网络
-
使用顺序方法构建神经网络
-
保存和加载 PyTorch 模型
本书的 GitHub 仓库的 Chapter02
文件夹中提供了本章中的所有代码的参考:bit.ly/mcvp-2e
。
安装 PyTorch
PyTorch 提供了多个功能,有助于构建神经网络——使用高级方法抽象各种组件,并且还提供了利用 GPU 加速训练神经网络的张量对象。
在安装 PyTorch 之前,我们首先需要确保已安装 Python。
接下来,我们将安装 PyTorch,这相当简单:
- 访问
pytorch.org/
网站上的 QUICK START LOCALLY 部分,并选择您的操作系统 (Your OS),Conda 作为 Package,Python 作为 Language,以及 CPU 作为 Compute Platform。如果您有 CUDA 库,可以选择适当的版本:
图 2.1:安装 PyTorch
这将提示您在终端中运行命令,例如 conda install pytorch torchvision torchaudio pytorch-cuda=12.1 -c pytorch -c nvidia
。
- 在命令提示符/终端中运行命令,让 Anaconda 安装 PyTorch 和必要的依赖项。
如果您拥有 NVIDIA 显卡作为硬件组件,强烈建议安装 CUDA 驱动程序,这将大幅加速深度学习训练。有关如何安装 CUDA 驱动程序的说明,请参阅本书的 GitHub 仓库中的 附录。安装完毕后,您可以选择 12.1 作为 CUDA 版本,并使用该命令安装 PyTorch。
-
您可以在命令提示符/终端中执行
python
,然后输入以下内容以验证 PyTorch 确实已安装:>>> import torch >>> print(torch.__version__)
本书中的所有代码都可以在 Google Colab 中执行:
colab.research.google.com/
。Python 和 PyTorch 在 Google Colab 中默认可用。我们强烈建议您在 Colab 中执行所有代码 – 这还包括免费访问 GPU!感谢 Google 提供了这样一个出色的资源!
因此,我们已经成功安装了 PyTorch。现在我们将在 Python 中执行一些基本的张量操作,帮助您掌握相关内容。
PyTorch 张量
张量是 PyTorch 的基本数据类型。张量是类似于 NumPy 的 ndarray 的多维矩阵:
-
标量可以表示为零维张量。
-
向量可以表示为一维张量。
-
二维矩阵可以表示为二维张量。
-
多维矩阵可以表示为多维张量。
在图表中,张量如下所示:
图 2.2:张量表示
例如,我们可以将彩色图像视为像素值的三维张量,因为彩色图像由height x width x 3
个像素组成 – 其中三个通道对应 RGB 通道。类似地,灰度图像可以被视为二维张量,因为它由height x width
个像素组成。
通过本节末尾,我们将学习张量为何有用,如何初始化它们,以及如何在张量之上执行各种操作。这将为我们后面在本章中利用张量构建神经网络模型打下基础。
初始化张量
张量在多种情况下都很有用。除了将它们作为图像的基本数据结构使用外,它们还在连接神经网络不同层的权重时发挥了更显著的作用。在本节中,我们将练习初始化张量对象的不同方法:
本书的Chapter02
文件夹中的Initializing_a_tensor.ipynb
文件中可以找到以下代码:bit.ly/mcvp-2e
。
-
导入 PyTorch 并通过在列表上调用
torch.tensor
来初始化一个张量:import torch x = torch.tensor([[1,2]]) y = torch.tensor([[1],[2]])
-
接下来,访问张量对象的形状和数据类型:
print(x.shape) # torch.Size([1,2]) # one entity of two items print(y.shape) # torch.Size([2,1]) # two entities of one item each print(x.dtype) # torch.int64
张量中所有元素的数据类型都是相同的。这意味着如果一个张量包含不同数据类型的数据(例如布尔值、整数和浮点数),整个张量将被强制转换为最通用的数据类型:
x = torch.tensor([False, 1, 2.0]) print(x) # tensor([0., 1., 2.])
正如您在前面代码的输出中看到的那样,布尔值
False
和整数1
被转换为浮点数。或者,与 NumPy 类似,我们可以使用内置函数初始化张量对象。请注意,我们现在描绘的张量和神经网络权重之间的类比现在变得明显 - 我们正在初始化张量以表示神经网络的权重初始化。
-
生成一个由三行四列填充为 0 的张量对象:
torch.zeros((3, 4))
-
生成一个由三行四列填充为 1 的张量对象:
torch.ones((3, 4))
-
生成三行四列的数值,其取值范围在 0 到 10 之间(包括低值但不包括高值):
torch.randint(low=0, high=10, size=(3,4))
-
生成三行四列的随机数,取值范围在 0 到 1 之间:
torch.rand(3, 4)
-
生成遵循正态分布的数值,有三行四列:
torch.randn((3,4))
-
最后,我们可以直接使用
torch.tensor(<numpy-array>)
将 NumPy 数组转换为 Torch 张量:x = np.array([[10,20,30],[2,3,4]]) y = torch.tensor(x) print(type(x), type(y)) # <class 'numpy.ndarray'> <class 'torch.Tensor'>
现在我们已经了解了如何初始化张量对象,接下来我们将学习如何在其上执行各种矩阵操作。
张量的操作
与 NumPy 类似,您可以对张量对象执行各种基本操作。神经网络操作的类比包括将输入与权重进行矩阵乘法、加上偏差项以及在需要时重新塑造输入或权重值。每个操作以及其他操作如下执行:
可以在本书的 GitHub 代码库的Chapter02
文件夹中的Operations_on_tensors.ipynb
文件中找到以下代码:bit.ly/mcvp-2e
。
-
将
x
中所有元素乘以10
可以通过以下代码执行:import torch x = torch.tensor([[1,2,3,4], [5,6,7,8]]) print(x * 10) # tensor([[10, 20, 30, 40], # [50, 60, 70, 80]])
-
将
x
中的元素加10
并将结果张量存储在y
中的操作可以通过以下代码实现:x = torch.tensor([[1,2,3,4], [5,6,7,8]]) y = x.add(10) print(y) # tensor([[11, 12, 13, 14], # [15, 16, 17, 18]])
-
可以通过以下代码执行张量的重新塑形:
y = torch.tensor([2, 3, 1, 0]) # y.shape == (4) y = y.view(4,1) # y.shape == (4, 1)
-
另一种重新塑形张量的方法是使用
squeeze
方法,我们提供要移除的轴索引。请注意,仅当要移除的轴在该维度中只有一个项目时才适用:x = torch.randn(10,1,10) z1 = torch.squeeze(x, 1) # similar to np.squeeze() # The same operation can be directly performed on # x by calling squeeze and the dimension to squeeze out z2 = x.squeeze(1) assert torch.all(z1 == z2) # all the elements in both tensors are equal print('Squeeze:\n', x.shape, z1.shape) # Squeeze: torch.Size([10, 1, 10]) torch.Size([10, 10])
-
squeeze
的相反操作是unsqueeze
,意味着我们向矩阵中添加一个维度,可以通过以下代码执行:x = torch.randn(10,10) print(x.shape) # torch.size(10,10) z1 = x.unsqueeze(0) print(z1.shape) # torch.size(1,10,10) # The same can be achieved using [None] indexing # Adding None will auto create a fake dim # at the specified axis x = torch.randn(10,10) z2, z3, z4 = x[None], x[:,None], x[:,:,None] print(z2.shape, z3.shape, z4.shape) # torch.Size([1, 10, 10]) # torch.Size([10, 1, 10]) # torch.Size([10, 10, 1])
在索引中使用None
是一种添加维度的巧妙方式,如所示,将经常在本书中用于创建假通道/批量维度。
-
可以通过以下代码执行两个不同张量的矩阵乘法:
x = torch.tensor([[1,2,3,4], [5,6,7,8]]) print(torch.matmul(x, y)) # tensor([[11], # [35]])
-
或者,也可以使用
@
运算符执行矩阵乘法:print(x@y) # tensor([[11], # [35]])
-
与 NumPy 中的
concatenate
类似,我们可以使用cat
方法对张量进行连接:import torch x = torch.randn(10,10,10) z = torch.cat([x,x], axis=0) # np.concatenate() print('Cat axis 0:', x.shape, z.shape) # Cat axis 0: torch.Size([10, 10, 10]) # torch.Size([20, 10, 10]) z = torch.cat([x,x], axis=1) # np.concatenate() print('Cat axis 1:', x.shape, z.shape) # Cat axis 1: torch.Size([10, 10, 10]) # torch.Size([10, 20, 10])
-
通过以下代码可以提取张量中的最大值:
x = torch.arange(25).reshape(5,5) print('Max:', x.shape, x.max()) # Max: torch.Size([5, 5]) tensor(24)
-
我们可以提取最大值以及最大值所在的行索引:
x.max(dim=0) # torch.return_types.max(values=tensor([20, 21, 22, 23, 24]), # indices=tensor([4, 4, 4, 4, 4]))
请注意,在上述输出中,我们获取维度
0
(即张量的行)上的最大值。因此,所有行的最大值是第 4 个索引处的值,因此indices
输出也全为四。此外,.max
返回最大值及其位置(argmax
)。类似地,当获取跨列的最大值时,输出如下:
m, argm = x.max(dim=1) print('Max in axis 1:\n', m, argm) # Max in axis 1: tensor([ 4, 9, 14, 19, 24]) # tensor([4, 4, 4, 4, 4])
min
操作与max
完全相同,但在适用时返回最小值和最小位置。 -
重新排列张量对象的维度:
x = torch.randn(10,20,30) z = x.permute(2,0,1) # np.permute() print('Permute dimensions:', x.shape, z.shape) # Permute dimensions: torch.Size([10, 20, 30]) # torch.Size([30, 10, 20])
请注意,当我们在原始张量上执行置换时,张量的形状会发生变化。
永远不要重新形状(即在tensor.view
上使用),以交换维度。即使 Torch 不会报错,这也是错误的,并且会在训练期间产生意想不到的结果。如果需要交换维度,请始终使用 permute。
由于在本书中涵盖所有可用操作很困难,重要的是要知道您几乎可以用与 NumPy 几乎相同的语法在 PyTorch 中执行几乎所有 NumPy 操作。标准的数学运算,如abs
、add
、argsort
、ceil
、floor
、sin
、cos
、tan
、cumsum
、cumprod
、diag
、eig
、exp
、log
、log2
、log10
、mean
、median
、mode
、resize
、round
、sigmoid
、softmax
、square
、sqrt
、svd
和transpose
等等,可以直接在具有或不具有适用轴的任何张量上调用。您可以随时运行dir(torch.Tensor)
来查看 Torch 张量的所有可能方法,并运行help(torch.Tensor.<method>)
以查看该方法的官方帮助和文档。
接下来,我们将学习如何利用张量在数据上执行梯度计算,这是神经网络中反向传播的关键方面。
张量对象的自动梯度
正如我们在前一章中看到的,微分和计算梯度在更新神经网络的权重中起着至关重要的作用。 PyTorch 的张量对象具有内置功能来计算梯度。
您可以在本书 GitHub 存储库的Chapter02
文件夹中的Auto_gradient_of_tensors.ipynb
文件中找到以下代码:bit.ly/mcvp-2e
。
在本节中,我们将了解如何使用 PyTorch 计算张量对象的梯度:
-
定义一个张量对象,并指定需要计算梯度:
import torch x = torch.tensor([[2., -1.], [1., 1.]], requires_grad=True) print(x)
在上述代码中,requires_grad
参数指定要为张量对象计算梯度。
-
接下来,定义计算输出的方式,这在本例中是所有输入平方和的总和:
这在代码中用以下行表示:
out = x.pow(2).sum()
我们知道前述函数的梯度是2x*。让我们使用 PyTorch 提供的内置函数来验证这一点。
-
可通过调用
backward()
方法计算变量的梯度。在我们的情况下,我们计算梯度 - 输出out
对输入x
的微小变化的影响 - 如下所示:out.backward()
-
现在,我们可以获取相对于
x
的out
的梯度如下:x.grad
这将产生以下输出:
# tensor([[4., -2.], # [2., 2.]])
注意,先前获得的梯度与直观梯度值(即 x 的两倍)匹配。
作为练习,在 第一章 的 Chain rule.ipynb
中尝试重新创建此场景,并使用 PyTorch 计算梯度。在进行前向传播后进行单次更新后,验证更新的权重是否与我们在笔记本中计算的相匹配。
到目前为止,我们已经学习了在张量对象的顶部初始化、操作和计算梯度,这些构成神经网络的基本构建模块。除了计算自动梯度外,还可以使用 NumPy 数组进行初始化和操作数据。这要求我们理解为什么在构建神经网络时应使用张量对象而不是 NumPy 数组的原因,我们将在下一节中详细介绍。
PyTorch 张量相对于 NumPy 的 ndarray 的优势
在前一章中,我们看到在计算最优权重值时,我们通过微小变化每个权重,并理解其对减少总体损失值的影响。请注意,基于一个权重更新的损失计算不会影响同一迭代中其他权重更新的损失计算。因此,如果每个权重更新由不同的核并行进行而不是顺序更新权重,则可以优化此过程。在这种情况下,GPU 非常有用,因为它在核心数量上比 CPU 多得多(一般情况下,CPU 可能有 <=64 核)。
与 NumPy 相比,Torch 张量对象被优化以在 GPU 上工作。为了进一步理解这一点,让我们进行一个小实验,在这个实验中,我们在一个场景中使用 NumPy 数组进行矩阵乘法操作,而在另一个场景中使用张量对象,并比较执行矩阵乘法所需的时间:
可在本书 GitHub 存储库的 Chapter02
文件夹中的 Numpy_Vs_Torch_object_computation_speed_comparison.ipynb
文件中找到以下代码:bit.ly/mcvp-2e
。
-
生成两个不同的
torch
对象:import torch x = torch.rand(1, 6400) y = torch.rand(6400, 5000)
-
定义我们将在 步骤 1 中创建的张量对象存储到的设备:
device = 'cuda' if torch.cuda.is_available() else 'cpu'
请注意,如果您没有 GPU 设备,设备将是 cpu
(此外,当使用 CPU 时,您不会注意到执行所需的时间差异很大)。
-
注册在 步骤 1 中创建的张量对象到设备上(注册张量对象意味着将信息存储在设备中):
x, y = x.to(device), y.to(device)
-
在 Torch 对象上执行矩阵乘法,并计时以便我们可以将其与在 NumPy 数组上执行矩阵乘法的速度进行比较:
%timeit z=(x@y) # It takes 0.515 milli seconds on an average to # perform matrix multiplication
-
在
cpu
上执行相同张量的矩阵乘法:x, y = x.cpu(), y.cpu() %timeit z=(x@y) # It takes 9 milli seconds on an average to # perform matrix multiplication
-
执行相同的矩阵乘法,这次在 NumPy 数组上进行:
import numpy as np x = np.random.random((1, 6400)) y = np.random.random((6400, 5000)) %timeit z = np.matmul(x,y) # It takes 19 milli seconds on an average to # perform matrix multiplication
您将注意到,在 GPU 上执行的 Torch 对象的矩阵乘法比在 CPU 上快约 18 倍,并且比在 NumPy 数组上执行的矩阵乘法快约 40 倍。通常情况下,使用 Torch 张量在 CPU 上进行的matmul
仍然比 NumPy 快。请注意,只有当您拥有 GPU 设备时,才能注意到这种速度增加。如果您在 CPU 设备上工作,您将不会注意到这种显著的速度增加。这就是为什么如果您没有 GPU,我们建议使用 Google Colab 笔记本,因为该服务提供免费的 GPU。
现在我们已经学会了如何在神经网络的各个组件/操作中利用张量对象,并且了解了如何使用 GPU 加速计算,接下来我们将学习将所有这些放在一起使用 PyTorch 构建神经网络。
使用 PyTorch 构建神经网络
在前一章中,我们学习了如何从头开始构建神经网络,其中神经网络的组件如下:
-
隐藏层的数量
-
隐藏层中的单元数
-
各层执行的激活函数
-
我们试图优化的损失函数
-
与神经网络相关联的学习率
-
用于构建神经网络的数据批量大小
-
正向传播和反向传播的时期数量
然而,所有这些都是使用 Python 中的 NumPy 数组从头构建的。在本节中,我们将学习如何在玩具数据集上使用 PyTorch 来实现所有这些。请注意,我们将利用迄今为止关于初始化张量对象、在其上执行各种操作以及计算梯度值以在构建神经网络时更新权重的学习。
为了学会直观地执行各种操作,我们将在本章节的玩具数据集上构建一个神经网络。但从下一章开始,我们将处理更现实的问题和数据集。
我们将解决一个简单的添加两个数字的玩具问题,以便理解使用 PyTorch 实现神经网络的方法,其中我们初始化数据集如下:
在本书的 GitHub 存储库的Chapter02
文件夹中的Building_a_neural_network_using_PyTorch_on_a_toy_dataset.ipynb
文件中可以找到以下代码,网址为bit.ly/mcvp-2e
。
-
定义输入(
x
)和输出(y
)的值:import torch x = [[1,2],[3,4],[5,6],[7,8]] y = [[3],[7],[11],[15]]
请注意,在前面的输入和输出变量初始化中,输入和输出都是一个列表的列表,其中输入列表中的值的和是输出列表中的值。
-
将输入列表转换为张量对象:
X = torch.tensor(x).float() Y = torch.tensor(y).float()
正如您所见,我们已将张量对象转换为浮点对象。将张量对象作为浮点数或长整数是一个好习惯,因为它们最终将乘以小数值(权重)。
此外,我们将输入(
X
)和输出(Y
)数据点注册到设备 - 如果您有 GPU,则为cuda
,如果您没有 GPU,则为cpu
:device = 'cuda' if torch.cuda.is_available() else 'cpu' X = X.to(device) Y = Y.to(device)
-
定义神经网络架构:
torch.nn
模块包含有助于构建神经网络模型的函数:
import torch.nn as nn
- 我们将创建一个类(
MyNeuralNet
),该类可以组合我们的神经网络架构。在创建模型架构时,强制性继承nn.Module
作为所有神经网络模块的基类:
class MyNeuralNet(nn.Module):
- 在类内部,我们使用
__init__
方法初始化了神经网络的所有组件。我们应该调用super().__init__()
以确保该类继承自nn.Module
:
def __init__(self): super().__init__()
-
在上述代码中,通过指定
super().__init__()
,我们现在能够利用为nn.Module
编写的所有预构建功能。将在init
方法中初始化的组件将在MyNeuralNet
类的不同方法中使用。 -
定义神经网络中的层:
self.input_to_hidden_layer = nn.Linear(2,8) self.hidden_layer_activation = nn.ReLU() self.hidden_to_output_layer = nn.Linear(8,1)
-
在前面的代码行中,我们指定了神经网络的所有层 - 一个线性层(
self.input_to_hidden_layer
),然后是 ReLU 激活(self.hidden_layer_activation
),最后是线性层(self.hidden_to_output_layer
)。目前,层数和激活的选择是任意的。在下一章中,我们将更详细地学习层中单位数量和层激活的影响。 -
接下来,让我们通过打印
nn.Linear
方法的输出来理解上述代码中的函数在做什么:
**# NOTE - This line of code is not a part of model building,** **# this is used only for illustration of Linear method** print(nn.Linear(2, 7)) Linear(in_features=2, out_features=7, bias=True)
-
在前面的代码中,线性方法接受两个值作为输入并输出七个值,并且还有一个与之相关的偏差参数。此外,
nn.ReLU()
调用了 ReLU 激活函数,然后可以在其他方法中使用。 -
以下是一些其他常用的激活函数:
-
Sigmoid
-
Softmax
-
Tanh
- 现在我们已经定义了神经网络的组件,让我们在定义网络的前向传播时将这些组件连接在一起:
def forward(self, x): x = self.input_to_hidden_layer(x) x = self.hidden_layer_activation(x) x = self.hidden_to_output_layer(x) return x
使用forward
作为函数名称是强制性的,因为 PyTorch 已将此函数保留为执行前向传播的方法。使用其他任何名称将引发错误。
-
到目前为止,我们已经建立了模型架构;让我们在下一步中检查随机初始化的权重值。
-
您可以通过执行以下步骤访问每个组件的初始权重:
- 创建
MyNeuralNet
类对象的实例,并将其注册到device
:
mynet = MyNeuralNet().to(device)
- 可以通过指定以下内容访问每层的权重和偏差:
# NOTE - This line of code is not a part of model building, # this is used only for illustration of # how to obtain parameters of a given layer mynet.input_to_hidden_layer.weight
-
上述代码的输出如下所示:
图 2.3:输入层与隐藏层之间连接的权重值
- 创建
由于神经网络每次以随机值初始化,因此输出的值将与前值不同。如果希望在多次迭代执行代码时保持相同值,需要在创建类对象实例之前使用 Torch 的manual_seed
方法指定种子,如torch.manual_seed(0)
。
-
所有神经网络的参数可以通过以下代码获取:
-
# NOTE - This line of code is not a part of model building, # this is used only for illustration of # how to obtain parameters of all layers in a model mynet.parameters()
-
上述代码返回一个生成器对象。
-
最后,通过以下方式循环生成器以获取参数:
-
# NOTE - This line of code is not a part of model building, # this is used only for illustration of how to # obtain parameters of all layers in a model # by looping through the generator object for par in mynet.parameters(): print(par)
-
上述代码将导致以下输出:
图 2.4:权重和偏置值
当在__init__
方法中定义任何nn
层时,模型已将这些张量注册为特殊对象,必要时用于跟踪前向和反向传播。它将自动创建相应的张量并同时注册它们。您还可以使用nn.Parameter(<tensor>)
函数手动注册这些参数。因此,以下代码等效于我们之前定义的神经网络类。
-
使用
nn.Parameter
函数定义模型的另一种方式如下: -
# for illustration only class MyNeuralNet(nn.Module): def __init__(self): super().__init__() self.input_to_hidden_layer = nn.Parameter(torch.rand(2,8)) self.hidden_layer_activation = nn.ReLU() self.hidden_to_output_layer = nn.Parameter(torch.rand(8,1)) def forward(self, x): x = x @ self.input_to_hidden_layer x = self.hidden_layer_activation(x) x = x @ self.hidden_to_output_layer return x
-
定义我们优化的损失函数。考虑到我们在预测连续输出,我们将优化均方误差:
loss_func = nn.MSELoss()
其他显著的损失函数如下:
-
CrossEntropyLoss
(用于多项分类) -
BCELoss
(用于二元分类的二元交叉熵损失) -
通过将输入值传递给
neuralnet
对象,然后计算给定输入的MSELoss
,可以计算神经网络的损失值:
-
_Y = mynet(X) loss_value = loss_func(_Y,Y) print(loss_value) # tensor(91.5550, grad_fn=<MseLossBackward>) # Note that loss value can differ in your instance # due to a different random weight initialization
在上述代码中,mynet(X)
计算输入通过神经网络时的输出值。此外,loss_func
函数计算神经网络预测(_Y
)和实际值(Y
)之间的MSELoss
值。
作为惯例,在本书中,我们将使用_<variable>
来关联与真实值<variable>
对应的预测。在此<variable>
之上是Y
。
还要注意,在计算损失时,我们始终先发送预测值,然后是真实值。这是 PyTorch 的惯例。
现在我们已定义了损失函数,接下来我们将定义优化器,以尝试减少损失值。优化器的输入将是与神经网络相对应的参数(权重和偏置)以及在更新权重时的学习率。
对于这个示例,我们将考虑随机梯度下降(在下一章中将会更多地讨论不同的优化器和学习率的影响)。
-
从
torch.optim
模块中导入SGD
方法,然后将神经网络对象(mynet
)和学习率(lr
)作为参数传递给SGD
方法:from torch.optim import SGD opt = SGD(mynet.parameters(), lr = 0.001)
-
将一个 epoch 中要完成的所有步骤一起执行:
-
计算给定输入和输出对应的损失值。
-
计算每个参数对应的梯度。
-
根据学习率和每个参数的梯度更新参数值。
-
更新权重后,请确保在计算下一个 epoch 中的梯度之前刷新在上一步中计算的梯度:
**# NOTE - This line of code is not a part of model building,** **# this is used only for illustration of how we perform** opt.zero_grad() # flush the previous epoch's gradients loss_value = loss_func(mynet(X),Y) # compute loss loss_value.backward() # perform backpropagation opt.step() # update the weights according to the #gradients computed
- 使用
for
循环重复前述步骤的次数等于 epoch 的数量。在以下示例中,我们执行了总共 50 个 epoch 的权重更新过程。此外,我们将每个 epoch 中的损失值存储在列表loss_history
中:
loss_history = [] for _ in range(50): opt.zero_grad() loss_value = loss_func(mynet(X),Y) loss_value.backward() opt.step() loss_history.append(loss_value.item())
- 让我们绘制损失随着 epoch 增加的变化(正如我们在前一章中看到的,我们以使整体损失值随 epoch 增加而减少的方式更新权重):
import matplotlib.pyplot as plt %matplotlib inline plt.plot(loss_history) plt.title('Loss variation over increasing epochs') plt.xlabel('epochs') plt.ylabel('loss value')
上述代码导致以下图表:
-
图 2.5:随着 epoch 增加的损失变化
请注意,正如预期的那样,损失值随着 epoch 的增加而减少。
到目前为止,在本节中,我们通过基于输入数据集中提供的所有数据点计算损失来更新神经网络的权重。在接下来的部分,我们将了解每次权重更新时仅使用部分输入数据点的优势。
数据集、数据加载器和批大小
神经网络中一个尚未考虑的超参数是批大小(batch size)。批大小指的是用于计算损失值或更新权重的数据点数量。
这个特别的超参数在有数百万数据点的情况下特别有用,在这种情况下,使用所有数据点来更新权重的一个实例并不是最佳选择,因为内存无法容纳如此多的信息。此外,样本可能足够代表数据。批大小有助于确保我们获取足够代表性的多个数据样本,但不一定是总数据的 100%代表性。
在本节中,我们将提出一种方法来指定在计算权重梯度和更新权重时要考虑的批大小,这些权重进而用于计算更新后的损失值:
您可以在本书 GitHub 存储库的 Chapter02
文件夹中的 Specifying_batch_size_while_training_a_model.ipynb
文件中找到以下代码:bit.ly/mcvp-2e
。
-
导入帮助加载数据和处理数据集的方法:
from torch.utils.data import Dataset, DataLoader import torch import torch.nn as nn
-
导入数据,将其转换为浮点数,并将其注册到设备上:
- 提供要处理的数据点:
x = [[1,2],[3,4],[5,6],[7,8]] y = [[3],[7],[11],[15]]
- 将数据转换为浮点数:
X = torch.tensor(x).float() Y = torch.tensor(y).float()
- 将数据注册到设备上 - 鉴于我们使用的是 GPU,我们指定设备为
'cuda'
。如果您使用 CPU,请将设备指定为'cpu'
:
device = 'cuda' if torch.cuda.is_available() else 'cpu' X = X.to(device) Y = Y.to(device)
-
实例化数据集的类 -
MyDataset
:class MyDataset(Dataset):
在MyDataset
类内部,我们存储信息以逐个获取数据点,以便将一批数据点捆绑在一起(使用DataLoader
),然后通过一次前向传播和一次反向传播来更新权重:
-
定义一个
__init__
方法,该方法接受输入和输出对,并将它们转换为 Torch 浮点对象:def __init__(self,x,y): self.x = torch.tensor(x).float() self.y = torch.tensor(y).float()
-
指定输入数据集的长度(
__len__
),以便类知道输入数据集中存在的数据点数量:def __len__(self): return len(self.x)
-
最后,使用
__getitem__
方法获取特定的行:def __getitem__(self, ix): return self.x[ix], self.y[ix]
在前面的代码中,ix
指的是要从数据集中获取的行的索引,该索引将是 0 到数据集长度之间的整数。
-
创建已定义类的实例:
ds = MyDataset(X, Y)
-
将之前定义的数据集实例通过
DataLoader
传递,以获取原始输入和输出张量对象中的batch_size
数量的数据点:dl = DataLoader(ds, batch_size=2, shuffle=True)
此外,在前面的代码中,我们还指定从原始输入数据集(
ds
)中获取两个数据点的随机样本(通过设置shuffle=True
)以及指定批次大小为2
。要从
dl
中获取批次数据,我们需要遍历它:# NOTE - This line of code is not a part of model building, # this is used only for illustration of # how to print the input and output batches of data for x,y in dl: print(x,y)
这将导致以下输出:
tensor([[1., 2.], [3., 4.]]) tensor([[3.], [7.]]) tensor([[5., 6.], [7., 8.]]) tensor([[1.], [15.]])
在前面的代码中,由于原始数据集中有四个数据点,而指定的批次大小为
2
,因此导致了两组输入输出对。 -
现在,我们定义在前一节中定义的神经网络类:
class MyNeuralNet(nn.Module): def __init__(self): super().__init__() self.input_to_hidden_layer = nn.Linear(2,8) self.hidden_layer_activation = nn.ReLU() self.hidden_to_output_layer = nn.Linear(8,1) def forward(self, x): x = self.input_to_hidden_layer(x) x = self.hidden_layer_activation(x) x = self.hidden_to_output_layer(x) return x
-
接下来,我们定义模型对象(
mynet
),损失函数(loss_func
)和优化器(opt
),如前一节所定义的:mynet = MyNeuralNet().to(device) loss_func = nn.MSELoss() from torch.optim import SGD opt = SGD(mynet.parameters(), lr = 0.001)
-
最后,遍历数据点批次以最小化损失值,就像我们在前一节中的步骤 6中所做的一样:
import time loss_history = [] start = time.time() for _ in range(50): for data in dl: x, y = data opt.zero_grad() loss_value = loss_func(mynet(x),y) loss_value.backward() opt.step() loss_history.append(loss_value.item()) end = time.time() print(end - start)
尽管前面的代码看起来与我们在前一节中讨论过的代码非常相似,但是在每个 epoch 中,我们的权重更新次数是前一节中权重更新次数的两倍。本节中的批次大小为2
,而前一节中的批次大小为4
(总数据点数)。
对新数据点进行预测:
在前一节中,我们学习了如何在已知数据点上拟合模型。在本节中,我们将学习如何利用前一节中训练的mynet
模型中定义的前向方法来预测未见数据点。我们将继续使用前一节中构建的代码:
-
创建我们想要测试模型的数据点:
val_x = [[10,11]]
注意,新数据集(val_x
)也将是一个列表的列表,因为输入数据集是一个列表的列表。
-
将新数据点转换为浮点张量对象并注册到设备上:
val_x = torch.tensor(val_x).float().to(device)
-
将张量对象通过已训练的神经网络
mynet
传递,就像它是一个 Python 函数一样。这与通过建立的模型进行前向传播是相同的:mynet(val_x) # 20.99
上述代码返回与输入数据点相关联的预测输出值。
到目前为止,我们已经能够训练我们的神经网络将输入映射到输出,通过执行反向传播来更新权重值以最小化损失值(使用预定义的损失函数计算)。
在接下来的部分,我们将学习如何构建我们自己的自定义损失函数,而不是使用预定义的损失函数。
实现自定义损失函数
在某些情况下,我们可能需要实现一个自定义损失函数,该函数根据我们解决的问题进行自定义,特别是涉及**目标检测/生成对抗网络(GANs)**的复杂用例。PyTorch 为我们提供了编写自定义函数来构建自定义损失函数的功能。
在本节中,我们将实现一个自定义损失函数,其功能与nn.Module
中预先构建的MSELoss
函数相同:
以下代码可以在本书的 GitHub 存储库的Chapter02
文件夹中的Implementing_custom_loss_function.ipynb
文件中找到,网址为bit.ly/mcvp-2e
。
-
导入数据,构建数据集和
DataLoader
,并定义一个神经网络,与上一节相同:x = [[1,2],[3,4],[5,6],[7,8]] y = [[3],[7],[11],[15]] import torch X = torch.tensor(x).float() Y = torch.tensor(y).float() device = 'cuda' if torch.cuda.is_available() else 'cpu' X = X.to(device) Y = Y.to(device) import torch.nn as nn from torch.utils.data import Dataset, DataLoader class MyDataset(Dataset): def __init__(self,x,y): self.x = torch.tensor(x).float() self.y = torch.tensor(y).float() def __len__(self): return len(self.x) def __getitem__(self, ix): return self.x[ix], self.y[ix] ds = MyDataset(X, Y) dl = DataLoader(ds, batch_size=2, shuffle=True) class MyNeuralNet(nn.Module): def __init__(self): super().__init__() self.input_to_hidden_layer = nn.Linear(2,8) self.hidden_layer_activation = nn.ReLU() self.hidden_to_output_layer = nn.Linear(8,1) def forward(self, x): x = self.input_to_hidden_layer(x) x = self.hidden_layer_activation(x) x = self.hidden_to_output_layer(x) return x mynet = MyNeuralNet().to(device)
-
通过取两个张量对象作为输入,获取它们的差异,对它们进行平方处理,然后返回两者之间平方差的均值来定义自定义损失函数:
def my_mean_squared_error(_y, y): loss = (_y-y)**2 loss = loss.mean() return loss
-
对于在上一节中相同的输入和输出组合,使用
nn.MSELoss
来获取均方误差损失,如下所示:loss_func = nn.MSELoss() loss_value = loss_func(mynet(X),Y) print(loss_value) # 92.7534
-
类似地,当我们在步骤 2中定义的函数中使用时,损失值的输出如下:
my_mean_squared_error(mynet(X),Y) # 92.7534
注意结果匹配。我们使用了内置的MSELoss
函数,并将其结果与我们构建的自定义函数进行了比较。我们可以根据我们解决的问题定义我们选择的自定义函数。
到目前为止,我们已经学会了计算最后一层的输出。迄今为止,中间层的值一直是一个黑盒子。在接下来的部分,我们将学习如何获取神经网络的中间层值。
获取中间层的值
在某些情况下,获取神经网络的中间层值非常有帮助(在我们讨论风格转移和迁移学习用例时将会详细介绍第四章和第五章)。
PyTorch 提供了两种方式来获取神经网络的中间值:
以下代码可以在本书的 GitHub 存储库的Chapter02
文件夹中的Fetching_values_of_intermediate_layers.ipynb
文件中找到,网址为bit.ly/mcvp-2e
。
-
一种方法是直接调用层,就像它们是函数一样。可以按如下方式完成:
input_to_hidden = mynet.input_to_hidden_layer(X) hidden_activation = mynet.hidden_layer_activation(input_to_hidden) print(hidden_activation)
请注意,在调用
hidden_layer_activation
层之前,我们必须先调用input_to_hidden_layer
激活,因为input_to_hidden_layer
的输出是hidden_layer_activation
层的输入。 -
另一种方法是通过指定我们在
forward
方法中想要查看的层来实现。让我们查看本章中我们一直在使用的模型的激活后的隐藏层值。
虽然以下代码与我们在上一节中看到的代码相同,但我们确保
forward
方法不仅返回输出,还返回激活后的隐藏层值(hidden2
):class NeuralNet(nn.Module): def __init__(self): super().__init__() self.input_to_hidden_layer = nn.Linear(2,8) self.hidden_layer_activation = nn.ReLU() self.hidden_to_output_layer = nn.Linear(8,1) def forward(self, x): hidden1 = self.input_to_hidden_layer(x) hidden2 = self.hidden_layer_activation(hidden1) output = self.hidden_to_output_layer(hidden2) return output, hidden2
现在,我们可以通过指定以下内容来访问隐藏层值:
mynet = NeuralNet().to(device) mynet(X)[1]
请注意,mynet
的第 0 个索引输出是我们定义的网络前向传播的最终输出,而第一个索引输出是激活后的隐藏层值。
使用序列方法构建神经网络
到目前为止,我们已经学习了如何使用神经网络类来实现神经网络,其中我们手动构建了每一层。然而,除非我们构建的是一个复杂的网络,否则构建神经网络架构的步骤是直接的,我们只需指定层和层堆叠的顺序。让我们继续学习一种简化定义神经网络架构的方法,使用Sequential
类。
我们将执行与前几节相同的步骤,不同之处在于用于手动定义神经网络架构的类将替换为Sequential
类,以创建神经网络架构。让我们为在本章中已经处理过的相同玩具数据编写网络代码:
以下代码在本书 GitHub 存储库的Chapter02
文件夹中的Sequential_method_to_build_a_neural_network.ipynb
中提供:bit.ly/mcvp-2e
。
-
定义玩具数据集:
x = [[1,2],[3,4],[5,6],[7,8]] y = [[3],[7],[11],[15]]
-
导入相关包并定义我们将在其上工作的设备:
import torch import torch.nn as nn import numpy as np from torch.utils.data import Dataset, DataLoader device = 'cuda' if torch.cuda.is_available() else 'cpu'
-
现在,我们定义数据集类(
MyDataset
):class MyDataset(Dataset): def __init__(self, x, y): self.x = torch.tensor(x).float().to(device) self.y = torch.tensor(y).float().to(device) def __getitem__(self, ix): return self.x[ix], self.y[ix] def __len__(self): return len(self.x)
-
定义数据集(
ds
)和数据加载器(dl
)对象:ds = MyDataset(x, y) dl = DataLoader(ds, batch_size=2, shuffle=True)
-
使用
nn
包中可用的Sequential
方法定义模型架构:model = nn.Sequential( nn.Linear(2, 8), nn.ReLU(), nn.Linear(8, 1) ).to(device)
请注意,在前述代码中,我们定义了与前几节中相同的网络架构,但我们的定义方式有所不同。nn.Linear
接受二维输入,并为每个数据点输出八维输出。此外,nn.ReLU
在八维输出上执行 ReLU 激活,最后,八维输入通过最终的nn.Linear
层得到一维输出(在我们的情况下,这是两个输入相加的输出)。
-
打印我们在步骤 5中定义的模型摘要:
- 安装并导入使我们能够打印模型摘要的包:
!pip install torch_summary from torchsummary import summary
- 打印模型的摘要,该摘要期望模型的名称和模型的输入大小:
summary(model, torch.zeros(1,2))
上述代码给出了以下输出:
图 2.6:模型架构总结
第一层的输出形状是(-1, 8),其中 -1 表示可以有与批处理大小一样多的数据点,8 表示每个数据点我们有一个八维输出,从而导致形状为(批处理大小 x 8)的输出。对于接下来的两层,解释是类似的。
-
接下来,我们定义损失函数(
loss_func
)和优化器(opt
),并训练模型,就像我们在上一节中所做的那样。在这种情况下,我们不需要定义一个模型对象;在这种情况下,网络没有在类内定义:loss_func = nn.MSELoss() from torch.optim import SGD opt = SGD(model.parameters(), lr = 0.001) import time loss_history = [] start = time.time() for _ in range(50): for ix, iy in dl: opt.zero_grad() loss_value = loss_func(model(ix),iy) loss_value.backward() opt.step() loss_history.append(loss_value.item()) end = time.time() print(end - start)
-
现在我们已经训练好了模型,我们可以在我们现在定义的验证数据集上预测值:
- 定义验证数据集:
val = [[8,9],[10,11],[1.5,2.5]]
- 预测通过模型传递验证列表的输出(请注意,预期值是列表中每个列表的两个输入的总和)。根据数据集类的定义,我们首先将列表的列表转换为浮点数,然后将其转换为张量对象并注册到设备:
model(torch.tensor(val).float().to(device)) # tensor([[16.9051], [20.8352], [ 4.0773]], # device='cuda:0', grad_fn=<AddmmBackward>)
上述代码的输出(如上面的注释所述)接近预期的输出(即输入值的总和)。
现在我们已经学会了利用顺序方法来定义和训练模型,让我们学习一下如何保存和加载模型以进行推理。
保存和加载 PyTorch 模型
在处理神经网络模型时的一个重要方面是在训练后保存并重新加载模型。想象一下,您必须从已经训练好的模型进行推理的场景。您将加载训练好的模型,而不是再次训练它。
下面的代码可以在本书 GitHub 仓库的 Chapter02
文件夹中的 save_and_load_pytorch_model.ipynb
文件中找到,链接为 bit.ly/mcvp-2e
。
在讨论相关命令来完成这一点之前,以我们的案例为例,让我们了解完全定义神经网络的重要组件。我们需要以下内容:
-
每个张量(参数)都有一个唯一的名称(键)
-
将逻辑连接到网络中的每个张量中的一个或另一个
-
每个张量的值(权重/偏差值)
在定义期间的 __init__
阶段处理第一个点,而在 forward
方法定义期间处理第二个点。在 __init__
阶段,默认情况下,在张量中随机初始化值。但是我们想要的是加载训练模型时学到的一组特定权重(或值),并将每个值与特定名称关联起来。这是通过调用以下特殊方法获得的。
使用 state_dict
model.state_dict()
命令是理解如何保存和加载 PyTorch 模型工作原理的根本。model.state_dict()
中的字典对应于参数名称(键)和模型对应的值(权重和偏置值)。state
指的是模型的当前快照(快照是每个张量的值集)。
它返回一个键和值的字典(OrderedDict
):
图 2.7:带有权重和偏置值的状态字典
键是模型层的名称,值对应于这些层的权重。
保存
运行torch.save(model.state_dict(), 'mymodel.pth')
会将该模型以 Python 序列化格式保存在名为mymodel.pth
的磁盘上。一个好的做法是在调用torch.save
之前将模型转移到 CPU,因为这样会将张量保存为 CPU 张量而不是 CUDA 张量。这将有助于在加载模型到任何计算机上时,无论其是否具有 CUDA 功能。
我们使用以下代码保存模型:
torch.save(model.to('cpu').state_dict(), 'mymodel.pth')
加载
加载模型需要我们首先使用随机权重初始化模型,然后从state_dict
中加载权重:
-
使用与训练时使用的相同命令创建一个空模型:
model = nn.Sequential( nn.Linear(2, 8), nn.ReLU(), nn.Linear(8, 1) ).to(device)
-
从磁盘加载模型并反序列化以创建一个
orderedDict
值:state_dict = torch.load('mymodel.pth')
-
将
state_dict
加载到model
中,注册到device
,并进行预测:model.load_state_dict(state_dict) # <All keys matched successfully> model.to(device) model(torch.tensor(val).float().to(device))
如果所有权重名称都存在于模型中,则会收到所有键匹配的消息。这意味着我们可以在世界上的任何计算机上从磁盘加载我们的模型,以便所有目的。接下来,我们可以将模型注册到设备,并对新数据点进行推断。
或者,我们可以使用torch.save(model, '<path>')
保存模型,并使用torch.load('<path>')
加载模型。尽管这看起来步骤更少更方便,但不建议,且在神经网络版本/Python 版本更改时更不灵活,容易出错。虽然torch.save(model.state_dict())
仅保存权重(即张量字典),torch.save(model)
将同时保存 Python 类。这在加载时可能会导致 PyTorch/Python 版本不一致的问题。
总结
在本章中,我们学习了 PyTorch 的基础构建模块——张量对象——以及在其上执行各种操作。我们进一步构建了一个玩具数据集上的神经网络,首先构建了一个初始化前馈架构的类,通过指定批处理大小从数据集中获取数据点,并定义了损失函数和优化器,通过多个时期进行循环。最后,我们还学习了定义自定义损失函数以优化选择的指标,并利用顺序方法简化网络架构定义的过程。所有这些步骤构成了构建神经网络的基础,将在后续章节中多次应用于各种用例中。
通过掌握使用 PyTorch 构建神经网络的各种组件的知识,我们将继续进入下一章节,在该章节中,我们将学习处理图像数据集上神经网络超参数的各种实际方面。
问题
-
在训练期间为什么要将整数输入转换为浮点数值?
-
用于重新塑形张量对象的方法有哪些?
-
使用张量对象比使用 NumPy 数组计算更快的原因是什么?
-
在神经网络类中,初始化魔术函数包括哪些内容?
-
在执行反向传播之前为什么要执行零梯度操作?
-
哪些魔术函数构成数据集类?
-
如何在新数据点上进行预测?
-
如何获取神经网络中间层的值?
-
Sequential
方法如何帮助简化神经网络架构的定义? -
在更新
loss_history
时,我们附加loss.item()
而不是loss
。这样做有什么作用,为什么附加loss.item()
而不只是loss
很有用? -
使用
torch.save(model.state_dict())
的优势是什么?
在 Discord 上了解更多
加入我们社区的 Discord 空间,与作者和其他读者进行讨论: