PyTorch-现代计算机视觉-一-

73 阅读1小时+

PyTorch 现代计算机视觉(一)

零、前言

人工智能 ( AI )就在这里,已经成为一股强大的力量,正在推动一些日常使用的现代应用。就像火、轮子、石油、电和电子的发现/发明一样,人工智能正在以我们只能幻想的方式重塑我们的世界。人工智能在历史上一直是一个利基计算机科学学科,由少数实验室提供。但由于优秀理论的爆炸、计算能力的提高和数据的可用性,该领域自 2000 年代以来开始呈指数增长,并且没有任何迹象表明会很快放缓。 人工智能一次又一次地证明,只要有正确的算法和足够多的数据,它可以在有限的人工干预下自行学习任务,并产生与人类判断相媲美的结果,有时甚至超过人类判断。无论你是一个正在学习诀窍的菜鸟,还是一个驱动大型组织的老手,都有充分的理由了解人工智能是如何工作的。神经网络是人工智能算法中最灵活的一类,已经适应了包括结构化数据、文本和视觉领域在内的广泛应用。

这本书从神经网络的基础开始,涵盖了计算机视觉的 50 多个应用。首先,您将使用 NumPy、PyTorch 从头开始构建一个神经网络 ( NN ),然后学习调整 NN 的超参数的最佳实践。随着我们的进展,你将学习 CNN,重点是图像分类的迁移学习。您还将了解构建神经网络模型时需要注意的实际问题。

接下来,您将了解多目标检测、分割,并使用 R-CNN 系列、SSD、YOLO、U-Net、Mask-RCNN 架构实现它们。然后,您将学习使用 Detectron2 框架来简化构建用于目标检测和人体姿态估计的神经网络的过程。最后,您将实现三维目标检测。

随后,您将学习自编码器和 GANs,重点是图像处理和生成。在这里,您将实现 VAE,DCGAN,CGAN,Pix2Pix,CycleGan,StyleGAN2,SRGAN,Style-Transfer 来处理各种任务的图像。

然后,您将学习在使用变形器执行 OCR、图像字幕、目标检测时结合 NLP 和 CV 技术。接下来,您将学习结合 RL 和 CV 技术来实现一个自动驾驶汽车代理。最后,您将完成将一个神经网络模型移植到产品中,并使用 OpenCV 库学习传统的 CV 技术。

这本书是给谁的

这本书是为 PyTorch 新手和中级机器学习从业者编写的,他们希望通过使用深度学习和 PyTorch 精通 CV 技术。那些刚刚开始使用 NNs 的人也会发现这本书很有用。Python 编程语言和机器学习的基础知识是你开始阅读这本书所需要的。

这本书涵盖的内容

第一章、人工神经网络基础,给你一个神经网络如何工作的完整细节。您将从学习与神经网络相关的关键术语开始。接下来,您将了解构建模块的工作细节,并在玩具数据集上从头开始构建神经网络。到本章结束时,你将对神经网络的工作方式有信心。

第二章, PyTorch 基础,向您介绍如何使用 PyTorch。在了解使用 PyTorch 构建神经网络模型的不同方法之前,您将了解创建和操作张量对象的方法。您仍将使用玩具数据集,以便理解使用 PyTorch 的细节。

第三章,用 PyTorch 构建深度神经网络,结合了前面章节已经涉及的所有内容,了解各种神经网络超参数对模型精度的影响。在本章结束时,你将对在真实数据集上使用神经网络充满信心。

第四章,介绍卷积神经网络,详细介绍使用普通神经网络的挑战,您将了解卷积神经网络克服传统神经网络各种局限性的原因。您将深入了解 CNN 的工作细节,并了解其中的各种组件。接下来,您将学习处理图像的最佳实践。在这一章中,你将开始处理真实世界的图像,并学习 CNN 如何帮助图像分类的复杂性。

第五章、图像分类的迁移学习,让你接触到解决现实世界中的图像分类问题。您将了解多迁移学习架构,并了解它如何帮助显著提高图像分类的准确性。接下来,您将利用迁移学习来实现面部关键点检测和年龄、性别估计的用例。

第六章、影像分类的实际方面,提供了在构建和部署影像分类模型时需要注意的实际方面的见解。您将实际看到在真实世界数据上利用数据扩充和批量规范化的优势。此外,您将了解到类激活图如何帮助解释 CNN 模型预测某种结果的原因。本章结束时,您可以自信地解决大多数影像分类问题,并在您的自定义数据集上利用前 3 章中讨论的模型。

第七章,*,*目标检测的基础知识为目标检测打下基础,在这里你将学习用于建立目标检测模型的各种技术。接下来,您将通过一个用例了解基于区域提议的目标检测技术,在这个用例中,您将实现一个模型来定位图像中的卡车和公共汽车。

第八章、高级目标检测,向您展示基于区域提议的架构的局限性。然后,您将了解更高级的架构的工作细节,这些架构解决了基于区域提议的架构的问题。您将在同一个数据集上实现所有架构(卡车与公共汽车检测),这样您就可以对比每个架构的工作原理。

第九章、图像分割,建立在前面章节的学习基础上,将帮助您建立模型,精确定位图像中各类物体以及物体实例的位置。您将在道路图像和普通家庭图像上实现用例。在本章结束时,你将自信地处理任何图像分类、目标检测/分割问题,并通过使用 PyTorch 建立模型来解决它。

第十章、目标检测和分割的应用,总结了前面所有章节的学习内容,您将在几行代码中实现目标检测、分割,实现模型以执行人群计数和图像着色。最后,您还将了解如何在真实数据集上进行 3D 目标检测。

第十一章,自编码器和图像处理,,为修改图像奠定基础。首先,您将了解各种有助于压缩图像和生成新颖图像的自编码器。接下来,您将了解在实现神经类型转移之前欺骗模型的对抗性攻击。最后,您将实现一个自编码器来生成深度假图像。

第十二章,使用 GANs 生成图像,首先让你深入了解 GANs 的工作原理。接下来,您将实现假面部图像生成,以及使用 GANs 生成感兴趣的图像。

第十三章、高级 GANs 操控图像,将图像操控提升到一个新的高度。您将实现 GANs 来将对象从一个类转换到另一个类,从草图生成图像,并操作自定义图像,以便我们可以生成特定样式的图像。在本章结束时,你可以自信地使用自编码器和 GANs 的组合来执行图像操作。

第十四章,用最少的数据点进行训练,为你学习利用其他技术与计算机视觉技术相结合奠定基础。您还将了解如何根据最小训练数据点和零训练数据点分类图像。

第十五章,结合计算机视觉和自然语言处理技术,为您提供各种自然语言处理技术的工作细节,如文字嵌入、LSTM、变形器,您将使用这些技术实现图像字幕、OCR 和变形器目标检测等应用。

第十六章,结合计算机视觉和强化学习,首先让你接触到强化学习的术语,以及给一个状态赋值的方法。当你了解深度 Q 学习时,你会明白 RL 和神经网络是如何结合在一起的。通过学习,您将实现一个代理来玩 Pong 游戏,还将实现一个代理来实现一辆自动驾驶汽车。

第十七章,将模型投入生产,描述了将模型投入生产的最佳实践。在将模型转移到 AWS 公共云之前,您将首先了解如何在本地服务器上部署模型。

第十八章,使用 OpenCV 实用程序进行图像分析,详细介绍了创建 5 个有趣应用程序的各种 OpenCV 实用程序。通过本章,您将了解有助于深度学习的实用程序,以及在内存或推理速度受到相当大限制的情况下可以替代深度学习的实用程序。

从这本书中获得最大收益

| 书中涵盖的软件/硬件 | 操作系统要求 | | 最低 128 GB 存储 最低 8 GB 内存 英特尔 i5 处理器或更好的处理器 英伟达 8+ GB 显卡–gtx 1070 或更好的显卡 最低 50 Mbps 互联网速度 | Windows、Linux 和 macOS | | Python 3.6 及以上版本 | Windows、Linux 和 macOS | | PyTorch 1.7 | Windows、Linux 和 macOS | | Google Colab(可以在任何浏览器中运行) | Windows、Linux 和 macOS |

请注意,书中几乎所有的代码都可以使用 Google Colab 运行,只需在 GitHub 的每个章节笔记本上点击打开 Colab 按钮即可。

如果你使用的是这本书的数字版本,我们建议你自己输入代码或者通过 GitHub 库获取代码(链接见下一节)。这样做将帮助您避免任何与复制和粘贴代码相关的潜在错误。

下载示例代码文件

你可以从 GitHub 的 https://GitHub . com/packt publishing/Modern-Computer-Vision-with-py torch 下载本书的示例代码文件。如果代码有更新,它将在现有的 GitHub 库中更新。

我们在也有丰富的书籍和视频目录中的其他代码包。看看他们!

下载彩色图像

我们还提供了一个 PDF 文件,其中有本书中使用的截图/图表的彩色图像。可以在这里下载:static . packt-cdn . com/downloads/9781839213472 _ color images . pdf

使用的惯例

本书通篇使用了许多文本约定。

CodeInText:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、伪 URL、用户输入和 Twitter 句柄。下面是一个例子:“除了我们之前看到的train对象,我们正在创建一个名为valFMNISTDataset类的对象。”

代码块设置如下:

# 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()

Bold :表示一个新术语、一个重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词出现在文本中,如下所示。下面是一个例子:“我们将在一个 时间使用一个批次应用梯度下降(在前馈通过之后),直到我们在一个训练时期内用尽所有数据点为止。”

警告或重要提示如下所示。

提示和技巧是这样出现的。

取得联系

我们随时欢迎读者的反馈。

总体反馈:如果您对这本书的任何方面有疑问,请在邮件主题中提及书名,并在customercare@packtpub.com发送电子邮件给我们。

勘误表:虽然我们已经尽力确保内容的准确性,但错误还是会发生。如果你在这本书里发现了一个错误,请告诉我们,我们将不胜感激。请访问 www.packtpub.com/support/err…

盗版:如果您在互联网上遇到我们作品的任何形式的非法拷贝,如果您能提供我们的地址或网站名称,我们将不胜感激。请通过copyright@packt.com联系我们,并提供材料链接。

如果你有兴趣成为一名作家:如果有你擅长的主题,并且你有兴趣写书或投稿,请访问 authors.packtpub.com。

复习

请留下评论。一旦你阅读并使用了这本书,为什么不在你购买它的网站上留下评论呢?潜在的读者可以看到并使用您不带偏见的意见来做出购买决定,我们 Packt 可以了解您对我们产品的看法,我们的作者可以看到您对他们的书的反馈。谢谢大家!

更多关于 Packt 的信息,请访问packt.com

第一部分:面向计算机视觉的深度学习基础

在本节中,我们将学习神经网络的基本构建模块是什么,以及每个模块的作用是什么,以便成功地训练网络。在这一部分中,我们将首先简要介绍神经网络的理论,然后继续使用 PyTorch 库构建和训练神经网络。

本节包括以下章节:

  • 第一章,人工神经网络基础
  • 第二章、 PyTorch 基本面
  • 第三章,用 PyTorch 构建深度神经网络

一、人工神经网络基础

一个人工神经网络 ( )是一个监督学习算法,它是由人脑功能的方式松散地启发而来的。类似于人类大脑中神经元的连接和激活方式,神经网络接受输入并通过一个函数传递它,导致某些后续神经元被激活,从而产生输出。

有几种标准的人工神经网络结构。通用近似定理说,我们总是可以找到一个足够大的神经网络架构,它具有正确的权重集,可以准确地预测任何给定输入的任何输出。这意味着,对于给定的数据集/任务,我们可以创建一个架构,并不断调整其权重,直到人工神经网络预测出我们希望它预测的内容。调整权重直到发生这种情况称为训练神经网络。在大型数据集和定制架构上的成功训练是人工神经网络在解决各种相关任务中获得突出地位的原因。

计算机视觉中的一个突出任务是识别图像中存在的对象的类别。ImageNet 是一项旨在识别图像中存在的对象类别的竞赛。历年来分类错误率的降低情况如下:

2012 年,一个神经网络(AlexNet)被用于竞赛的获胜解决方案。从上图中可以看出,通过利用神经网络,从 2011 年到 2012 年,错误有了相当大的减少。从那时起,随着时间的推移,随着更深更复杂的神经网络的出现,分类错误不断减少,并击败了人类水平的表现。这给了我们一个坚实的动力去学习和实现神经网络来完成我们的定制任务,只要适用。

在本章中,我们将在一个简单的数据集上创建一个非常简单的架构,并主要关注人工神经网络的各种构建模块(前馈、反向传播、学习速率)如何帮助调整权重,以便网络学习从给定的输入预测预期的输出。我们将首先从数学上了解什么是神经网络,然后从头开始构建一个神经网络,以便有一个坚实的基础。然后,我们将了解负责训练神经网络的每个组件,并对它们进行编码。总体而言,我们将涵盖以下主题:

  • 人工智能和传统机器学习的比较
  • 了解人工神经网络构建块
  • 实现前馈传播
  • 实现反向传播
  • 将前馈传播和反向传播放在一起
  • 了解学习速度的影响
  • 总结神经网络的训练过程

人工智能和传统机器学习的比较

传统上,系统是通过使用程序员编写的复杂算法来实现智能化的。

例如,假设您对识别照片中是否包含狗感兴趣。在传统的机器学习 ( ML )设置中,ML 从业者或主题专家首先识别需要从图像中提取的特征。然后,他们提取这些特征,并通过一个精心编写的算法来解读给定的特征,以判断图像是否是一只狗。下图说明了同样的想法:

取以下样本:

根据前面的图像,一个简单的规则可能是,如果一个图像包含三个排成三角形的黑色圆圈,它可以被归类为一只狗。然而,这条规则对这个欺骗性的松饼特写无效:

当然,这个规则也不适用于除了狗的脸部特写以外的任何图像。因此,自然地,我们需要为多种类型的精确分类创建的手动规则的数量可能是指数级的,尤其是当图像变得更加复杂时。因此,传统方法在非常受限的环境中工作得很好(比如,拍摄护照照片,其中所有维度都被限制在毫米以内),而在无约束的环境中工作得很差,在无约束的环境中,每个图像变化很大。

我们可以将同样的思路扩展到任何领域,比如文本或结构化数据。在过去,如果有人对编程解决现实世界的任务感兴趣,他们就有必要了解关于输入数据的一切,并编写尽可能多的规则来覆盖每个场景。这是乏味的,并且不能保证所有新的场景都遵循所述规则。

然而,通过利用人工神经网络,我们可以一步完成。

神经网络提供了将特征提取(手动调整)相结合的独特优势,并在单次操作中使用这些特征进行分类/回归,几乎不需要手动特征工程。这两个子任务都只需要标签化的数据(比如哪些图片是狗,哪些图片不是狗)和神经网络架构。它不需要人类想出规则来分类图像,这消除了传统技术强加给程序员的大部分负担。

请注意,主要要求是我们为需要解决方案的任务提供大量的示例。例如,在前面的例子中,我们需要向模型提供很多很多的非狗的图片,以便它学习特征。如何利用神经网络完成分类任务的高级视图如下:

现在,我们已经对神经网络性能优于传统计算机视觉方法的根本原因有了一个非常高层次的概述,让我们在本章的各个部分更深入地了解神经网络是如何工作的。

了解人工神经网络构建模块

人工神经网络是张量(权重)和数学运算的集合,以松散地复制人脑功能的方式排列。它可以被看作是一个数学函数,接受一个或多个张量作为输入,并预测一个或多个张量作为输出。将这些输入连接到输出的操作安排被称为神经网络的架构——我们可以根据手头的任务进行定制,即根据问题是否包含结构化(表格)或非结构化(图像、文本、音频)数据(即输入和输出张量的列表)。

人工神经网络由以下部分组成:

  • 输入层:这些层以自变量为输入。
  • 隐藏(中间)层:这些层连接输入和输出层,同时在输入数据之上执行转换。此外,隐藏层包含节点(下图中的单位/圆),以将其输入值修改为更高/更低维度的值。通过使用修改中间层节点值的各种激活函数来实现实现更复杂表示的功能。
  • 输出层:包含输入变量预期产生的值。

考虑到这一点,神经网络的典型结构如下:

输出层中的节点(上图中的圆圈)的数量取决于手头的任务以及我们试图预测的是连续变量还是分类变量。如果输出是连续变量,则输出有一个节点。如果输出是具有 m 个可能类别的分类,则在输出层中将有 m 个节点。让我们放大其中一个节点/神经元,看看发生了什么。神经元按如下方式转换其输入:

在上图中,x[1T5, x [2] ,...、 x [n] 为输入变量, w [0] 为偏差项(类似于我们在线性/逻辑回归中有偏差的方式)。]

注意,w[1T5, w [2] ,..., w [n] 是赋予每个输入变量的权重, w [0] 是偏差项。输出值 a 计算如下:]

如您所见,它是权重和输入对的乘积之和,后跟一个附加函数 f (偏差项+乘积之和)。函数 f 是激活函数,用于在乘积的总和上应用非线性。关于激活函数的更多细节将在下一节前馈传播中提供。此外,通过具有一个以上的隐藏层,堆叠大量的神经元,可以实现更高的非线性。

在高层次上,神经网络是节点的集合,其中每个节点都具有可调整的浮点值,并且这些节点以图形的形式互连,以网络架构所规定的格式返回输出。网络由三个主要部分组成:输入层、隐藏层和输出层。请注意,您可以拥有更高数量(n)的个隐藏层,术语深度学习指的是更大数量的隐藏层。通常,当神经网络必须理解一些复杂的东西(如图像识别)时,需要更多的隐藏层。

了解了神经网络的架构后,在下一节中,我们将了解前馈传播,它有助于估计网络架构的误差(损失)量。

实现前馈传播

为了建立对前馈传播如何工作的强有力的基础理解,我们将通过一个训练神经网络的玩具示例,其中神经网络的输入是(1,1)并且对应的(预期的)输出是 0。这里,我们将基于这一单个输入-输出对找到神经网络的最佳权重。但是,您应该注意到,在现实中,将会有成千上万的数据点用于训练人工神经网络。

本例中的神经网络架构包含一个隐藏层,其中有三个节点,如下所示:

上图中的每个箭头恰好包含一个可调整的浮点值( weight )。我们需要找到 9 个(第一个隐层 6 个,第二个隐层 3 个)浮点,这样当输入为(1,1)时,输出尽可能接近(0)。这就是我们所说的训练神经网络。为了简单起见,我们还没有引入偏差值——底层逻辑保持不变。

在随后的章节中,我们将了解前面网络的以下内容:

  • 计算隐藏层值
  • 执行非线性激活
  • 估计输出图层值
  • 计算对应于期望值的损失值

计算隐藏层单元值

我们现在将为所有连接分配权重。第一步,我们在所有连接中随机分配权重。通常,神经网络在训练开始前用随机权重初始化。同样,为了简单起见,在介绍主题时,我们将而不是在学习前馈传播和反向传播时包括偏差值。但是我们将从头开始实现前馈传播和反向传播。

让我们从在 0 和 1 之间随机初始化的初始权重开始,但是注意,神经网络的训练过程之后的最终权重不需要在一组特定的值之间。下图(左半部分)提供了网络中权重和值的正式表示,右半部分提供了网络中随机初始化的权重。

在下一步中,我们将输入与权重相乘,以计算隐藏层中隐藏单元的值。

激活前隐藏层的单位值如下获得:

此处计算的隐藏层的单位值(激活前)也显示在下图中:

现在,我们将通过非线性激活传递隐藏层值。请注意,如果我们不在隐藏层中应用非线性激活函数,则无论存在多少隐藏层,神经网络都将成为从输入到输出的巨大线性连接。

应用激活功能

激活函数有助于对输入和输出之间的复杂关系进行建模。

一些常用的激活函数计算如下(其中 x 为输入):

各种输入值的每个先前激活的可视化如下:

对于我们的示例,让我们使用 sigmoid(逻辑)函数进行激活。

通过将 sigmoid(逻辑)激活 S(x) 应用于三个隐藏层,我们在 sigmoid 激活后得到以下值:

现在我们已经获得了激活后的隐藏层值,在下一节中,我们将获得输出层值。

计算输出层值

到目前为止,我们已经计算了应用 sigmoid 激活后的最终隐藏层值。使用激活后的隐藏层值和权重值(在第一次迭代中随机初始化),我们将计算网络的输出值:

我们执行隐藏层值和权重值的乘积之和来计算输出值。另一个提醒:我们排除了需要在每个单元(节点)添加的偏置项,只是为了简化我们对前馈传播和反向传播的工作细节的理解,并将在编码前馈传播和反向传播时包括它:

因为我们从一组随机的权重开始,所以输出节点的值与目标非常不同。这种情况下,差的是 1.235 (记住,目标是 0)。在下一节中,我们将学习如何计算网络当前状态下的损耗值。

计算损失值

损失值(也称为成本函数)是我们在神经网络中优化的值。为了理解损失值是如何计算的,我们来看两种情况:

  • 分类变量预测
  • 连续变量预测

连续变量预测时计算损失

通常,当变量是连续的时,损失值被计算为实际值和预测值之差的平方的平均值,也就是说,我们试图通过改变与神经网络相关联的权重值来最小化均方误差。均方误差值计算如下:

在上式中,是实际输出。是神经网络(其权重以的形式存储)计算出的预测,其输入为m 为数据集中的行数。

关键是,对于每一组独特的权重,神经网络会预测不同的损失,我们需要找到损失为零的黄金权重组(或者,在现实情况下,尽可能接近零)。

在我们的例子中,让我们假设我们预测的结果是连续的。在这种情况下,损失函数值是均方误差,计算方法如下:

现在我们已经了解了如何计算连续变量的损失值,在下一节中,我们将了解如何计算分类变量的损失值。

在分类变量预测期间计算损失

当要预测的变量是离散的(即变量中只有几个类别)时,我们通常使用分类交叉熵损失函数。当要预测的变量中有两个不同的值时,损失函数是二元交叉熵。

二进制交叉熵的计算如下:

y 是输出的实际值, p 是输出的预测值, m 是数据点的总数。

分类交叉熵的计算如下:

y 为输出的实际值, p 为输出的预测值, m 为数据点总数, C 为总类数。

可视化交叉熵损失的一个简单方法是查看预测矩阵本身。假设你在一个图像识别问题中预测五个类别——狗、猫、鼠、牛和母鸡。神经网络在激活 softmax 的最后一层必须有五个神经元(在下一节中有更多关于 softmax 的内容)。因此,它将被迫预测每个类别、每个数据点的概率。假设有五幅图像,预测概率如下所示(每行中突出显示的单元格对应于目标类):

请注意,每一行的总和为 1。第一行,当目标为,预测概率为 0.88 时,对应的损失为 0.128 (是 0.88 的对数的负数)。类似地,计算其他损失。如您所见,当正确类别的概率较高时,损失值较小。如你所知,概率范围在 0 和 1 之间。因此,最小可能损失可以是 0(当概率为 1 时),最大损失可以是无穷大(当概率为 0 时)。

数据集中的最终损失是所有行中所有单个损失的平均值。

现在我们已经对计算均方误差损失和交叉熵损失有了坚实的理解,让我们回到我们的玩具例子。假设我们的输出是一个连续变量,我们将在后面的部分学习如何使用反向传播来最小化损失值。我们将更新权重值(之前随机初始化的)以最小化损失()。但是,在此之前,让我们首先使用 NumPy 数组在 Python 中编写前馈传播代码,以巩固我们对其工作细节的理解。

代码中的前馈传播

编码前馈传播的高级策略如下:

  1. 在每个神经元上执行和积。
  2. 计算激活。
  3. 在每个神经元上重复前两步,直到输出层。
  4. 通过比较预测值和实际输出值来计算损耗。

这将是一个函数,它将输入数据、当前神经网络权重和输出数据作为函数的输入,并返回当前网络状态的丢失。

计算所有数据点的均方误差损失值的前馈函数如下:

The following code is available as Feed_forward_propagation.ipynb in the Chapter01 folder of this book's GitHub repository - tinyurl.com/mcvp-packt

我们强烈建议您通过点击每个笔记本中的在 Colab 中打开按钮来执行代码笔记本。屏幕截图示例如下:

一旦你点击 Colab 中的打开(在前面的截图中突出显示),你将能够毫不费力地执行所有代码,并且应该能够复制本书中显示的结果。

有了执行代码的方法,让我们继续编写前馈传播代码:

  1. 将输入变量值(inputs)、weights(如果是第一次迭代,则随机初始化)和提供的数据集中的实际outputs作为feed_forward函数的参数:
import numpy as np
def feed_forward(inputs, outputs, weights):        

为了让这个练习更真实一点,我们将偏差与每个节点关联起来。因此,权重数组将不仅包含连接不同节点的权重,还包含与隐藏/输出层中的节点相关联的偏差。

  1. 通过执行inputs的矩阵乘法(np.dot)和将输入层连接到隐藏层的权重值(weights[0])计算隐藏层值,并添加与隐藏层节点相关的偏差项(weights[1]):
    pre_hidden = np.dot(inputs,weights[0])+ weights[1]
  1. 将 sigmoid 激活函数应用于上一步获得的隐藏层值之上-pre_hidden:
    hidden = 1/(1+np.exp(-pre_hidden))
  1. 通过执行隐藏层激活值(hidden)和将隐藏层连接到输出层的权重(weights[2])的矩阵乘法(np.dot),以及将输出与输出层中的节点相关联的偏差求和来计算输出层值-weights[3]:
    pred_out = np.dot(hidden, weights[2]) + weights[3]
  1. 计算整个数据集的均方误差值,并返回均方误差:
    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))

注意,在输入xnp.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))

与均方误差类似,平均绝对误差通常用于连续变量。此外,一般而言,当要预测的输出具有小于 1 的值时,优选地将平均绝对误差作为损失函数,因为当预期输出小于 1 时,均方误差将显著降低损失的幅度(1 和-1 之间的数的平方是更小的数)。

  • 二元交叉熵:交叉熵是两种不同分布之间差异的度量:实际的和预测的。二进制交叉熵应用于二进制输出数据,不同于我们讨论的前两个损失函数(在连续变量预测期间应用)。

预测值数组(p)和实际值数组(y)之间的二进制交叉熵实现如下:

def binary_cross_entropy(p, y):      
    return -np.mean(np.sum((y*np.log(p)+(1-y)*np.log(1-p))))

注意,当预测值远离实际值时,二进制交叉熵损失具有高值,当预测值和实际值接近时,具有低值。

  • 分类交叉熵:预测值数组(p)和实际值数组(y)之间的分类交叉熵实现如下:
def categorical_cross_entropy(p, y):         
    return -np.mean(np.sum(y*np.log(p)))

到目前为止,我们已经了解了前馈传播,以及构成前馈传播的各种组件,如权重初始化、与节点相关的偏差、激活和损失函数。在下一节中,我们将学习反向传播,这是一种调整权重的技术,使权重的损失尽可能小。

实现反向传播

在前馈传播中,我们将输入层连接到隐藏层,然后隐藏层连接到输出层。在第一次迭代中,我们随机初始化权重,然后计算这些权重值导致的损失。在反向传播中,我们采用相反的方法。我们从前馈传播中获得的损失值开始,并以尽可能最小化损失值的方式更新网络的权重。

当我们执行以下步骤时,损失值会降低:

  1. 少量改变神经网络中的每个权重——一次一个。
  2. 当重量值发生变化()时,测量损失的变化()。
  3. 通过更新权重(其中 k 为正值,是一个被称为学习率的超参数)。

请注意,对特定权重的更新与通过少量更改而减少的损失量成比例。直观地说,如果改变一个权重可以大幅度减少损失,那么我们可以大幅度更新权重。但是,如果通过改变权重减少的损失很小,那么我们只对其进行少量更新。

如果前面的步骤在整个数据集上执行 n 次(其中我们已经完成了前馈传播和反向传播),它基本上导致了对n时期的训练。

由于典型的神经网络包含数千/数百万(如果不是数十亿)个权重,因此改变每个权重的值,并检查损失是增加还是减少并不是最佳的。前面列表中的核心步骤是当重量改变时“损失变化”的测量。你可能学过微积分,测量这个和计算重量损失的梯度是一样的。在下一节,关于反向传播的链式法则,会有更多关于利用微积分的偏导数来计算重量损失的梯度。

在本节中,我们将从头开始实现梯度下降,一次更新一个权重,每次更新一个小的量,如本节开始时所详述的。然而,在实现反向传播之前,让我们了解神经网络的一个额外的细节:学习速率。

直观上,学习率有助于建立对算法的信任。例如,当决定权重更新的幅度时,我们可能不会一次改变很大的权重值,而是更慢地更新它。

这导致在我们的模型中获得稳定性;我们将在了解学习率的影响一节中了解学习率如何帮助稳定性。

我们更新权重以减少误差的整个过程被称为梯度下降

随机梯度下降是在前面的场景中误差最小化的方式。如前所述,梯度代表差异(当权重值少量更新时损失值的差异),而下降表示减少。随机代表随机样本的选择,基于此做出决策。

除了随机梯度下降,许多其他类似的优化器有助于最小化损失值;不同的优化器将在下一章讨论。

在接下来的两节中,我们将学习用 Python 从头开始编写反向传播的直觉代码,还将简要讨论反向传播如何使用链规则工作。

代码中的梯度下降

梯度下降在 Python 中实现如下:

The following code is available as Gradient_descent.ipynb in the Chapter01 folder of this book's GitHub repository - tinyurl.com/mcvp-packt

  1. 定义前馈网络并计算均方误差损失值,如我们在代码中的前馈传播部分所做的:
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)) 
    return mean_squared_error
  1. 将每个权重和偏移值增加一个非常小的量(0.0001),并为每个权重和偏移更新一次计算一个总的平方误差损失值。
  • 在下面的代码中,我们创建了一个名为update_weights的函数,它执行梯度下降过程来更新权重。该函数的输入是网络的输入变量—inputs、预期的outputsweights(在训练模型开始时随机初始化),以及模型的学习速率—lr(在后面的部分中详细介绍学习速率):
def update_weights(inputs, outputs, weights, lr):
  • 确保你的重量清单。由于权重将在后面的步骤中被操作,deepcopy确保我们可以在不干扰实际权重的情况下使用权重的多个副本。我们将创建原始权重集的三个副本,它们作为输入传递给函数——original_weightstemp_weightsupdated_weights:
original_weights = deepcopy(weights)
temp_weights = deepcopy(weights)
updated_weights = deepcopy(weights)           
  • 通过feed_forward函数传递inputsoutputsoriginal_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重置为原始权重集,因为在每次迭代中,当参数在给定时段内少量更新时,我们会更新不同的参数来计算损失。

  • 我们计算由于重量变化引起的梯度(损失值的变化):
grad = (_loss_plus - original_loss)/(0.0001)

这个以非常小的量更新一个参数然后计算梯度的过程相当于微分的过程。

  • 最后,我们更新出现在相应的第 i ^层和updated_weightsindex中的参数。更新的权重值将与梯度值成比例地减少。此外,我们引入了一种通过使用学习率来慢慢建立信任的机制,而不是完全减少一个等于梯度值的值—lr(在了解学习率的影响部分中有更多关于学习率的信息):
updated_weights[i][index] -= grad*lr
  • 一旦所有层的参数值和层内的指数被更新,我们返回更新的权重值-updated_weights:
return updated_weights, original_loss

神经网络中的另一个参数是在计算损失值时考虑的批量

在前面的场景中,我们考虑了所有数据点来计算损失(均方误差)值。然而,在实践中,当我们有数千个(在某些情况下,数百万个)数据点时,在计算损失值时,更多数据点的增量贡献将遵循收益递减规律,因此我们将使用与我们拥有的数据点总数相比小得多的批量。我们将一次使用一个批次来应用梯度下降(在前馈传播之后),直到我们在训练的一个时期内用尽中的所有数据点。

构建模型时考虑的典型批量大小在 32 到 1,024 之间。

在本节中,我们学习了当权重值发生少量变化时,根据损失值的变化来更新权重值。在下一节中,我们将了解如何更新权重,而不需要一次计算一个梯度。

使用链式法则实现反向传播

到目前为止,我们已经通过少量更新权重,然后计算原始场景(当权重不变时)中的前馈损失和更新权重后的前馈损失之间的差异,计算了关于权重的损失梯度。以这种方式更新权重值的一个缺点是,当网络很大时,需要大量的计算来计算损失值(实际上,计算要进行两次——一次是权重值不变,另一次是权重值少量更新)。这导致更多的计算,因此需要更多的资源和时间。在本节中,我们将了解如何利用链式法则,该法则不要求我们手动计算损失值来得出与重量值相关的损失梯度。

在第一次迭代中(我们随机初始化权重),输出的预测值是 1.235。

为了得到理论公式,让我们将权重和隐藏层值以及隐藏层激活分别表示为 w 、 *h、*和 a ,如下所示:

请注意,在前面的图表中,我们已经将左图中的每个组件值归纳到右图中。

为了便于理解,在本节中,我们将了解如何使用链式法则来计算仅关于 w [11] 的损失值的梯度。同样的学习可以扩展到神经网络的所有权重和偏差。我们鼓励您练习并将链式法则计算应用于其余的权重和偏差值。

本书 GitHub 资源库的Chapter01文件夹中的chain_rule.ipynb笔记本包含了使用链式法则计算网络中所有参数的权重和偏差变化的梯度的方法。

此外,为了便于学习,我们将只处理一个数据点,其中输入为{1,1},预期输出为{0}。

假设我们正在用 w [11] 计算损耗值的梯度,让我们通过下图了解计算梯度时要包括的所有中间元件(不将输出连接到 w [11] 的元件在下图中显示为灰色):

从上图中,我们可以看到 w [11] 通过突出显示的路径—贡献了损失值。

接下来,我们来阐述一下是如何分别获得的。

网络的损耗值表示如下:

预测输出值计算如下:

隐藏层激活值(sigmoid 激活)计算如下:

隐藏层值计算如下:

既然我们已经制定了所有的方程,让我们计算损失值( C )的变化相对于重量的变化的影响如下:

这被称为链式法则。本质上,我们正在执行一个微分链,以获取我们感兴趣的微分。

请注意,在前面的等式中,我们已经建立了一系列偏微分方程,现在我们能够对四个分量中的每一个单独进行偏微分,并最终计算损失值相对于重量值的导数。

上述等式中的各个偏导数计算如下:

  • 损失值相对于预测输出值的偏导数如下:

  • 预测输出值相对于隐藏层激活值的偏导数如下:

  • 隐藏层激活值相对于激活前隐藏层值的偏导数如下:

注意,前面的等式来自于 sigmoid 函数的导数是的事实。

  • 激活前的隐藏层值相对于权重值的偏导数如下:

这样,损失值相对于的梯度通过将每个偏微分项替换为之前步骤中计算的相应值来计算,如下所示:

从前面的公式中,我们可以看到,我们现在能够计算重量值的微小变化(损失相对于重量的梯度)对损失值的影响,而无需通过再次计算前馈传播来强行进行。

接下来,我们将继续更新权重值,如下所示:

这两种方法的工作版本,1)使用链式法则识别梯度,然后更新权重,以及 2)通过了解权重值的微小变化对损失值的影响来更新权重值,从而使更新后的权重值具有相同的值,在本书的 GitHub 资源库-【tinyurl.com/mcvp-packt】?? 的Chapter01文件夹中的笔记本Chain_rule.ipynb中提供

在梯度下降中,我们顺序执行权重更新过程(一次一个权重)。通过利用链式法则,我们了解到有一种替代方法可以计算重量的少量变化对损失值的影响,但是有机会进行并行计算。

Because we are updating parameters across all layers, the whole process of updating parameters can be parallelized. Further, given that in a realistic scenario, there can exist millions of parameters across layers, performing the calculation for each parameter on a different core of GPU results in the time taken to update weights is a much faster exercise than looping through each weight, one at a time.

现在,我们已经从直觉的角度和利用链式法则对反向传播有了一个坚实的了解,在下一节中,我们将了解前馈和反向传播如何协同工作以达到最佳权重值。

将前馈传播和反向传播放在一起

在本节中,我们将构建一个简单的神经网络,它具有一个隐藏层,将输入连接到我们在代码部分的前馈传播中处理的相同玩具数据集的输出,并且还利用我们在上一节中定义的update_weights函数来执行反向传播,以获得最佳权重和偏差值。

我们将模型定义如下:

  1. 输入连接到具有三个单元/节点的隐藏层。
  2. 隐藏层连接到输出,输出层中有一个单元。

The following code is available as Back_propagation.ipynb in the Chapter01 folder of this book's GitHub repository - tinyurl.com/mcvp-packt

我们将创建如下网络:

  1. 导入相关包并定义数据集:
from copy import deepcopy
import numpy as np 
x = np.array([[1,1]])
y = np.array([[0]])
  1. 随机初始化权重和偏差值。

隐藏层中有三个单元,每个输入节点连接到每个隐藏层单元。因此,总共有六个权重值和三个偏置值-一个偏置和两个权重(两个权重来自两个输入节点)对应于每个隐藏单元。此外,最后一层有一个单元连接到隐藏层的三个单元。因此,总共三个权重和一个偏差决定了输出层的值。随机初始化的权重如下:

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 权重矩阵,最后一个参数数组表示与输出层相关联的偏差。

  1. 运行神经网络通过 100 个前馈传播和反向传播时期——其功能已经在前面章节中学习并定义为feed_forwardupdate_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功能:
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)
  1. 绘制损失值:
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')

上述代码生成了以下图形:

你可以看到,损失开始在 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)]

GitHub 笔记本(Auto_gradient_of_tensors.ipynb)中演示了相同代码相同权重的 PyTorch 版本。理解下一章 PyTorch 的核心概念后,再来看这一节。自己验证一下,不管网络是用 NumPy 还是 PyTorch 写的,输入和输出确实是一样的。使用 NumPy 数组从零开始构建一个网络,虽然不是最优的,但在这一章中会帮助你对神经网络的工作细节有一个坚实的基础。

  1. 一旦我们有了更新的权重,通过将输入传递到网络来对输入进行预测,并计算输出值:
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。随着我们训练更多的纪元,pred_out值变得更加接近 0。

到目前为止,我们已经了解了前馈传播和反向传播。我们在这里定义的update_weights函数的关键部分是学习率——我们将在下一节中学习。

了解学习速度的影响

为了理解学习率如何影响模型的训练,让我们考虑一个非常简单的情况,其中我们尝试拟合以下方程(注意,以下方程不同于我们迄今为止一直在处理的玩具数据集):

注意, y 是输出, x 是输入。有了一组输入值和预期输出值,我们将尝试用不同的学习率来拟合方程,以了解学习率的影响。

The following code is available as Learning_rate.ipynb in the Chapter01 folder of this book's GitHub repository - tinyurl.com/mcvp-packt

  1. 我们指定输入和输出数据集如下:
x = [[1],[2],[3],[4]]
y = [[3],[6],[9],[12]]
  1. 定义feed_forward功能。此外,在这种情况下,我们将修改网络,使我们没有隐藏层,架构如下:

注意,在前面的函数中,我们正在估计参数 wb :

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
  1. 定义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
  1. 将权重和偏差值初始化为随机值:
W = [np.array([[0]], dtype=np.float32), 
     np.array([[0]], dtype=np.float32)]

注意,权重和偏移值被随机初始化为值 0。此外,输入权重值的形状是 1×1,因为输入中每个数据点的形状是 1×1,偏移值的形状是 1×1(因为输出中只有一个节点,每个输出有一个值)。

  1. 让我们利用学习率为 0.01 的update_weights函数,循环 1,000 次迭代,并检查权重值(W)在增加的时期内如何变化:
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函数在每个时期结束时获取修改后的权重。此外,在每个时期,我们给出最近更新的权重作为输入,以在下一个时期获取更新的权重。

  1. 在每个时期结束时绘制权重参数的值:
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')

前面的代码会导致权重值在增加的时期内发生变化,如下所示:

请注意,在前面的输出中,权重值逐渐向右增加,然后在最佳值~3 处饱和。

为了理解学习率的值对达到最佳权重值的影响,让我们理解当学习率为 0.1 和学习率为 1 时,权重值如何随着时期的增加而变化。

当我们在步骤 5 中修改相应的学习率值并执行步骤 6 时,会获得以下图表(生成以下图表的代码与我们之前学习的代码相同,只是学习率值有所变化,可在 GitHub 中的相关笔记本中找到):

请注意,当学习率非常小(0.01)时,权重值缓慢地(经过更多的时期)向最佳值移动。然而,在稍高的学习率(0.1)下,权重值最初振荡,然后迅速饱和(在更少的时期内)到最优值。最后,当学习率很高(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])
print(W)
import matplotlib.pyplot as plt
%matplotlib inline
epochs = range(1, 11)
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')

上述代码会产生以下输出:

注意,当学习率为 0.01 时,损失值缓慢下降,并且权重值也朝着最佳值缓慢更新。现在让我们来了解一下当学习率为 0.1 时,上述情况是如何变化的。

  • **0.1 的学习率:**代码与学习率为 0.01 的场景中的代码保持相同,但是,在该场景中,学习率参数将为 0.1。使用更改的学习率参数值运行相同代码的输出如下:

让我们对比一下 0.01 和 0.1 的学习率场景,两者之间的主要区别如下:

当学习率为 0.01 时,与 0.1 的学习率相比,权重更新得更慢(当学习率为 0.01 时,从第一时段中的 0 到 0.45,当学习率为 0.1 时,到 4.5)。更新较慢的原因是学习速率较低,因为权重是通过梯度乘以学习速率来更新的。

除了权重更新幅度之外,我们还应该注意权重更新的方向:

当权重值小于最优值时,梯度为负,当权重值大于最优值时,梯度为正。这种现象有助于在正确的方向上更新权重值。

最后,我们将学习率 1:

  • 1的学习率:代码保持与 0.01 的学习率场景中的代码相同,但是,在该场景中,学习率参数将为 1。使用更改后的学习率参数运行相同代码的输出如下:

从前面的图中,我们可以看到权重已经偏离到非常高的值(如在第一个时期结束时,权重值为 45,在后面的时期中进一步偏离到非常大的值)。除此之外,权重值移动到非常大的量,因此权重值的小变化几乎不会导致梯度的变化,因此权重停留在该高值。

一般来说,学习率低比较好。这样,模型能够缓慢学习,但会将权重调整到最佳值。典型的学习率参数值范围在 0.0001 和 0.01 之间。

现在,我们已经了解了神经网络的构建模块——前馈传播、反向传播和学习速率,在下一节中,我们将总结如何将这三者结合起来训练神经网络的高级概述。

总结神经网络的训练过程

训练神经网络是通过以给定的学习速率重复前向传播和反向传播这两个关键步骤来得出神经网络架构的最佳权重的过程。

在前向传播中,我们将一组权重应用于输入数据,使其通过定义的隐藏层,对隐藏层的输出执行定义的非线性激活,然后通过将隐藏层节点值乘以另一组权重来估计输出值,从而将隐藏层连接到输出层。然后,我们最终计算对应于给定权重集的总损失。对于第一次前向传播,权重值被随机初始化。

在反向传播中,我们通过在减少总损失的方向上调整权重来减少损失值(误差)。此外,权重更新的幅度是梯度乘以学习速率。

前馈传播和反向传播的过程重复进行,直到我们实现尽可能小的损失。这意味着,在训练结束时,神经网络已经调整了它的权重,以便它预测我们希望它预测的输出。在前面的玩具示例中,经过训练后,当 {1,1} 被输入时,更新后的网络将预测 0 值作为输出,因为它被训练以实现该值。

摘要

在本章中,在我们了解人工神经网络的架构和各种组件之前,我们了解了对单个网络的需求,该网络可以在单个镜头中执行特征提取和分类。接下来,我们学习了如何在实现前馈传播之前连接网络的各层,以计算与网络当前权重相对应的损耗值。接下来,我们实现了反向传播,以了解优化权重来最小化损失值的方法。此外,我们还了解了学习速率如何在实现网络的最佳权重方面发挥作用。此外,我们实现了网络的所有组件——前馈传播、激活函数、损失函数、链式法则和梯度下降,以从头开始更新 NumPy 中的权重,从而为我们在接下来的章节中构建打下坚实的基础。

既然我们已经了解了神经网络的工作原理,我们将在下一章使用 PyTorch 实现一个神经网络,并在第三章深入研究神经网络中可以调整的各种其他组件(超参数)。

问题

  1. 神经网络中的各层是什么?
  2. 前馈传播的输出是什么?
  3. 连续因变量的损失函数与二元因变量以及分类因变量的损失函数有何不同?
  4. 什么是随机梯度下降?
  5. 反向传播练习做什么?
  6. 在反向传播期间,跨层的所有权重的权重更新是如何发生的?
  7. 在训练神经网络的每个时期内,神经网络的哪些功能发生?
  8. 为什么在 GPU 上训练网络比在 CPU 上训练更快?
  9. 学习率如何影响神经网络的训练?
  10. 学习率参数的典型值是多少?

二、PyTorch 基础

在前一章中,我们学习了神经网络的基本构建模块,并且用 Python 从头开始实现了正向和反向传播。

在本章中,我们将深入探讨使用 PyTorch 构建神经网络的基础,在后续章节中,当我们了解图像分析中的各种用例时,我们将多次利用 py torch。我们将从 PyTorch 研究的核心数据类型——张量对象开始。然后,我们将深入研究可以在张量对象上执行的各种操作,以及在玩具数据集上构建神经网络模型时如何利用它们(以便我们在从下一章开始逐步查看更现实的数据集之前加强理解)。这将允许我们直观地了解如何使用 PyTorch 构建神经网络模型来映射输入和输出值。最后,我们将学习实现定制损失函数,这样我们就可以基于我们正在解决的用例进行定制。

具体而言,本章将涵盖以下主题:

  • 安装 PyTorch
  • PyTorch tensors
  • 使用 PyTorch 构建神经网络
  • 使用顺序方法建立神经网络
  • 保存和加载 PyTorch 模型

安装 PyTorch

PyTorch 提供了多种功能来帮助构建神经网络——使用高级方法抽象各种组件,并为我们提供张量对象,利用 GPU 更快地训练神经网络。

在安装 PyTorch 之前,我们首先需要安装 Python,如下:

  1. 为了安装 Python,我们将使用anaconda.com/distributio…平台来获取安装程序,该安装程序将为我们自动安装 Python 以及重要的深度学习专用库:

选择最新 Python 版本 3.xx (3.7,截至撰写本书时)的图形安装程序,并让它下载。

  1. 使用下载的安装程序进行安装:

在安装过程中选择 Add Anaconda to my PATH 环境变量选项,因为这将使我们在命令提示符/终端中键入python时调用 Anaconda 版本的 Python 变得容易。

接下来,我们将安装 PyTorch,这同样简单。

  1. 访问pytorch.org/网站上的本地快速入门部分,选择您的操作系统(您的 OS),对于软件包选择 Conda,对于语言选择 Python,对于 CUDA 选择 None。如果你有 CUDA 库,你可以选择合适的版本:

这将提示您在终端中运行一个命令,比如conda install pytorch torchvision cpuonly -c pytorch

  1. 在命令提示符/终端中运行命令,让 Anaconda 安装 PyTorch 和必要的依赖项。

如果你拥有一个 NVIDIA 显卡作为硬件组件,强烈建议安装 CUDA 驱动,它可以将深度学习训练加速几个数量级。有关如何安装 CUDA 驱动程序的说明,请参考附录。一旦你安装了它们,你可以选择 10.1 作为 CUDA 版本,并使用这个命令来安装 PyTorch。

  1. 您可以在命令提示符/终端中执行python,然后键入以下命令来验证 PyTorch 确实已安装:
>>> import torch
>>> print(torch.__version__)
# '1.7.0'

本书中的所有代码都可以在 Google Colab-colab.research.google.com/中执行。Python 和 PyTorch 在 Google Colab 中默认可用。我们强烈建议您在 Colab 上执行所有代码——包括免费访问 GPU!感谢谷歌提供如此优秀的资源!

所以,我们已经成功安装了 Python 和 PyTorch。我们现在将在 Python 中执行一些基本的张量运算来帮助你掌握它。

PyTorch tensors

张量是 PyTorch 的基本数据类型。张量是一种多维矩阵,类似于 NumPy 的 ndarrays:

  • 标量可以表示为零维张量。
  • 向量可以表示为一维张量。
  • 二维矩阵可以表示为二维张量。
  • 多维矩阵可以表示为多维张量。

从图像上看,张量如下:

例如,我们可以将彩色图像视为像素值的三维张量,因为彩色图像由height x width x 3像素组成——其中三个通道对应于 RGB 通道。类似地,灰度图像可以被认为是二维张量,因为它由height x width个像素组成。

在本节结束时,我们将学习张量为什么有用,如何初始化它们,以及在张量上执行各种操作。这将作为我们在下一节研究利用张量构建神经网络模型时的基础。

初始化张量

张量在很多方面都很有用。除了用作图像的基本数据结构之外,张量的一个更突出的用途是用于初始化连接神经网络不同层的权重。

在本节中,我们将练习初始化张量对象的不同方法:

下面的代码可以在本书的 GitHub 库【tinyurl.com/mcvp-packt[…](tinyurl.com/mcvp-packt)

  1. 导入 PyTorch 并通过调用列表上的torch.tensor初始化张量:
import torch
x = torch.tensor([[1,2]])
y = torch.tensor([[1],[2]])
  1. 接下来,访问张量对象的形状和数据类型:
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,我们可以使用内置函数初始化张量对象。请注意,我们在神经网络的张量和权重之间绘制的相似之处现在暴露出来了——我们正在初始化张量,以便它们代表神经网络的权重初始化。

  1. 生成一个张量对象,该对象有三行四列,用零填充:
torch.zeros((3, 4))
  1. 生成一个张量对象,该对象有三行四列,用 1 填充:
torch.ones((3, 4))
  1. 生成三行四列介于 0 和 10 之间的值(包括低值,但不包括高值):
torch.randint(low=0, high=10, size=(3,4))
  1. 用三行四列生成 0 到 1 之间的随机数:
torch.rand(3, 4)
  1. 生成符合三行四列正态分布的数字:
torch.randn((3,4))

  1. 最后,我们可以使用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 类似,可以对张量对象执行各种基本操作。与神经网络操作类似的是输入与权重的矩阵乘法、偏置项的添加以及在需要时对输入或权重值进行整形。这些操作和附加操作的完成方式如下:

The following code is available as Operations_on_tensors.ipynb in the Chapter02 folder of this book's GitHub repository.

  • 可以使用以下代码将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]])
  • 10添加到x中的元素,并将结果张量存储到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输出也是全 4。此外,.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完全相同,但在适用的情况下返回最小值和 arg-minimum。

  • 置换张量对象的维度:
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 on)来交换维度。尽管 Torch 不会抛出错误,但这是错误的,会在训练过程中产生无法预料的结果。如果需要交换尺寸,请始终使用置换。

因为很难涵盖本书中所有可用的操作,所以知道您可以使用与 NumPy 几乎相同的语法在 PyTorch 中执行几乎所有的 NumPy 操作是很重要的。标准的数学运算,如absaddargsortceilfloorsincostancumsumcumproddiageigexploglog2log10meanmedianmoderesizeroundsigmoidsoftmaxsquaresqrtsvd你可以随时运行dir(torch.Tensor)来查看 Torch 张量的所有可能方法,运行help(torch.Tensor.<method>)来查看该方法的官方帮助和文档。

接下来,我们将了解如何利用张量在数据之上执行梯度计算,这是在神经网络中执行反向传播的一个关键方面。

张量对象的自动渐变

正如我们在前一章中看到的,微分和计算梯度在更新神经网络的权重中起着关键作用。PyTorch 的张量对象带有计算梯度的内置功能。

在本节中,我们将了解如何使用 PyTorch 计算张量对象的梯度:

The following code is available as Auto_gradient_of_tensors.ipynb in the Chapter02 folder of this book's GitHub repository - tinyurl.com/mcvp-packt

  1. 定义一个张量对象,并指定它需要计算梯度:
import torch
x = torch.tensor([[2., -1.], [1., 1.]], requires_grad=True)
print(x)

在前面的代码中,requires_grad参数指定要为张量对象计算渐变。

  1. 接下来,定义计算输出的方法,在本例中,输出是所有输入的平方和:

这在代码中用下面一行表示:

out = x.pow(2).sum()

我们知道前一个函数的梯度是 2x* 。让我们使用 PyTorch 提供的内置函数来验证这一点。

  1. 可以通过对值调用backward()方法来计算值的梯度。在我们的例子中,我们计算梯度–对于x(输入)的微小变化out(输出)的变化–如下:
out.backward()
  1. 我们现在可以获得out相对于x的梯度,如下所示:
x.grad

这会产生以下输出:

请注意,之前获得的梯度与直观的梯度值相匹配(是 x 值的两倍)。

As an exercise, try recreating the scenario in Chain rule.ipynb in Chapter 1, Artificial Neural Network Fundamentals, with PyTorch. Compute the gradients after making a forward pass and make a single update. Verify that the updated weights match what we calculated in the notebook.

到目前为止,我们已经了解了如何在张量对象上初始化、操作和计算梯度——它们共同构成了神经网络的基本构件。除了计算自动渐变,初始化和操作数据也可以使用 NumPy 数组。这要求我们理解为什么在构建神经网络时应该使用张量对象而不是 NumPy 数组——这将在下一节中讨论。

PyTorch 的张量优于 NumPy 的 ndarrays

在前一章中,我们看到,在计算最佳权重值时,我们会对每个权重进行少量调整,并了解其对降低整体损失值的影响。注意,基于一个权重的权重更新的损失计算不影响同一迭代中其他权重的权重更新的损失计算。因此,如果每个权重更新由不同的核心并行进行,而不是顺序更新权重,则可以优化该过程。在这种情况下,GPU 很方便,因为与 CPU(一般情况下,CPU 可能不超过 64 个内核)相比,它由数千个内核组成。

与 NumPy 相比,Torch 张量对象经过优化,可与 GPU 配合使用。为了进一步理解这一点,让我们进行一个小实验,其中我们在一个场景中使用 NumPy 数组执行矩阵乘法操作,在另一个场景中使用 tensor 对象执行矩阵乘法操作,并比较在两个场景中执行矩阵乘法所花费的时间:

The following code is available as Numpy_Vs_Torch_object_computation_speed_comparison.ipynb in the Chapter02 folder of this book's GitHub repository - tinyurl.com/mcvp-packt

  1. 生成两个不同的torch对象:
import torch
x = torch.rand(1, 6400)
y = torch.rand(6400, 5000)
  1. 定义我们将存储在步骤 1 中创建的张量对象的设备:
device = 'cuda' if torch.cuda.is_available() else 'cpu'

请注意,如果您没有 GPU 设备,该设备将是cpu(此外,您不会注意到使用 CPU 时执行时间的巨大差异)。

  1. 用设备注册在步骤 1 中创建的张量对象。注册张量对象意味着在设备中存储信息:
x, y = x.to(device), y.to(device)
  1. 对 Torch 对象执行矩阵乘法,并计时,以便我们可以比较在 NumPy 数组上执行矩阵乘法的情况下的速度:
%timeit z=(x@y)
# It takes 0.515 milli seconds on an average to 
# perform matrix multiplication
  1. cpu进行相同张量的矩阵乘法:
x, y = x.cpu(), y.cpu()
%timeit z=(x@y)
# It takes 9 milli seconds on an average to 
# perform matrix multiplication
  1. 执行相同的矩阵乘法,这次是在 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 上对 Torch 对象执行的矩阵乘法快大约 18 倍,比在 NumPy 数组上执行的矩阵乘法快大约 40 倍。总的来说,matmul在 CPU 上用 Torch tensors 还是比 NumPy 快。请注意,只有当您有 GPU 设备时,您才会注意到这种加速。如果您正在使用 CPU 设备,您不会注意到速度的显著提高。这就是为什么如果你没有自己的 GPU,我们建议使用谷歌 Colab 笔记本电脑,因为该服务提供免费的 GPU。

现在,我们已经了解了如何在神经网络的各个单独组件/操作中利用张量对象,以及如何使用 GPU 来加速计算,在下一节中,我们将了解如何使用 PyTorch 将所有这些放在一起构建神经网络。

使用 PyTorch 构建神经网络

在前一章中,我们学习了如何从头开始构建神经网络,其中神经网络的组件如下:

  • 隐藏层的数量
  • 隐藏层中的单元数
  • 在不同层执行的激活功能
  • 我们试图优化的损失函数
  • 与神经网络相关联的学习速率
  • 用于构建神经网络的批量数据
  • 正向和反向传播的次数

然而,对于所有这些,我们使用 Python 中的 NumPy 数组从头开始构建它们。在本节中,我们将学习在玩具数据集上使用 PyTorch 实现所有这些。请注意,在使用 PyTorch 构建神经网络时,我们将利用到目前为止在初始化张量对象、对其执行各种操作以及计算梯度值来更新权重方面的学习。

请注意,在本章中,为了获得执行各种操作的直觉,我们将在玩具数据集上构建一个神经网络。从下一章开始,我们将处理解决更现实的问题和数据集。

为了理解使用 PyTorch 实现神经网络,我们要解决的玩具问题是两个数的简单相加,其中我们按如下方式初始化数据集:

The following code is available as Building_a_neural_network_using_PyTorch_on_a_toy_dataset.ipynb in the Chapter02 folder of this book's GitHub repository - tinyurl.com/mcvp-packt

  1. 定义输入(x)和输出(y)值:
import torch
x = [[1,2],[3,4],[5,6],[7,8]]
y = [[3],[7],[11],[15]]

请注意,在前面的输入和输出变量初始化中,输入和输出是一个列表列表,其中输入列表中的值之和就是输出列表中的值。

  1. 将输入列表转换为张量对象:
X = torch.tensor(x).float()
Y = torch.tensor(y).float()

请注意,在前面的代码中,我们已经将张量对象转换为浮点对象。将张量对象作为浮点数或长整型是一个很好的实践,因为它们无论如何都会乘以十进制值(权重)。

此外,我们将输入(X)和输出(Y)数据点注册到设备——cuda(如果您有 GPU)和cpu(如果您没有 GPU ):

device = 'cuda' if torch.cuda.is_available() else 'cpu'
X = X.to(device)
Y = Y.to(device)
  1. 定义神经网络架构:
  • 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 激活,然后可以在其他方法中使用。

其他一些常用的激活功能如下:

  • 乙状结肠的
  • Softmax
  • 双曲正切

现在我们已经定义了神经网络的组件,让我们在定义网络的正向传播时将组件连接在一起:

    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 已经将该函数保留为执行正向传播的方法。在它的位置上使用任何其他名称都会引发错误。

到目前为止,我们已经构建了模型架构;让我们在下一步检查随机初始化的权重值。

  1. 您可以通过执行以下步骤来访问每个组件的初始重量:
  • 创建我们之前定义的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

上述代码的输出如下:

输出中的值将与前面的不同,因为神经网络每次都用随机值进行初始化。如果您希望它们在执行相同代码的多次迭代中保持不变,那么您需要在创建类对象实例之前使用 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)

上述代码会产生以下输出:

该模型已将这些张量注册为特殊对象,这些对象是跟踪向前和向后传播所必需的。在__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
  1. 定义我们优化的损失函数。假设我们预测的是连续输出,我们将针对均方误差进行优化:
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 大会。

现在我们已经定义了损失函数,我们将定义试图减少损失值的优化器。优化器的输入将是对应于神经网络的参数(权重和偏差)以及更新权重时的学习率。

对于这种情况,我们将考虑随机梯度下降(更多关于不同的优化器和学习率的影响在下一章)。

  1. torch.optim模块导入SGD方法,然后将神经网络对象(mynet)和学习率(lr)作为参数传递给SGD方法:
from torch.optim import SGD
opt = SGD(mynet.parameters(), lr = 0.001)
  1. 一起执行一个时期内要完成的所有步骤:
  • 计算对应于给定输入和输出的损耗值。
  • 计算每个参数对应的梯度。
  • 基于每个参数的学习速率和梯度更新权重。
  • 更新权重后,请确保在下一个时段计算梯度之前,刷新上一步中计算的梯度:
# 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 back-propagation
opt.step() # update the weights according to the gradients computed
  • 使用for循环,重复上述步骤,重复次数与历元数相同。在下面的例子中,我们对总共 50 个时期执行权重更新过程。此外,我们将每个时期的损失值存储在列表中—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)
  • 让我们绘制损失随增加的时期的变化(正如我们在上一章中看到的,我们以总损失值随增加的时期减少的方式更新权重):
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')

上述代码会产生以下图形:

请注意,正如预期的那样,损失值随着时期的增加而降低。

到目前为止,在本节中,我们已经通过基于输入数据集中提供的所有数据点计算损失来更新神经网络的权重。在下一节中,我们将了解每次权重更新仅使用输入数据点样本的优势。

数据集、数据加载器和批处理大小

神经网络中我们还没有考虑的一个超参数是批量大小。批量是指计算损失值或更新权重时考虑的数据点数量。

这种超参数在有数百万个数据点的情况下特别有用,将所有这些数据点用于一次权重更新并不是最佳选择,因为内存无法容纳如此多的信息。此外,样本可以充分代表数据。批量大小有助于获取足够有代表性的多个数据样本,但不一定是全部数据的 100%代表。

在本节中,我们将提出一种方法来指定计算权重梯度时要考虑的批量大小,以更新权重,进而用于计算更新的损失值:

The following code is available as Specifying_batch_size_while_training_a_model.ipynb in the Chapter02 folder of this book's GitHub repository - tinyurl.com/mcvp-packt

  1. 导入有助于加载数据和处理数据集的方法:
from torch.utils.data import Dataset, DataLoader
import torch
import torch.nn as nn
  1. 导入数据,将数据转换为浮点数,并将它们注册到设备:
  • 提供要处理的数据点:
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)
  1. 实例化数据集的一个类-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指的是要从数据集中提取的行的索引。

  1. 创建已定义类的实例:
ds = MyDataset(X, Y)
  1. 通过DataLoader传递先前定义的数据集实例,以从原始输入和输出张量对象中获取batch_size个数据点:
dl = DataLoader(ds, batch_size=2, shuffle=True)

此外,在前面的代码中,我们还指定从原始输入数据集(ds)中获取两个数据点(通过提及batch_size=2)的随机样本(通过提及shuffle=True)。

  • 为了从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)

这会产生以下输出:

注意,前面的代码产生了两组输入输出对,因为原始数据集中总共有四个数据点,而指定的批处理大小是2

  1. 现在,我们按照上一节中的定义来定义神经网络类:
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
  1. 接下来,我们还定义了模型对象(mynet)、损失函数(loss_func)和优化器(opt),如前一节所定义的:
mynet = MyNeuralNet().to(device)
loss_func = nn.MSELoss()
from torch.optim import SGD
opt = SGD(mynet.parameters(), lr = 0.001)
  1. 最后,循环遍历数据点批次,以最小化损失值,就像我们在上一节的步骤 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)
end = time.time()
print(end - start)

请注意,虽然前面的代码似乎与我们在上一节中经历的代码非常相似,但与上一节中更新权重的次数相比,我们在每个时期执行的权重更新次数是 2 倍,因为本节中的批量大小是2,而上一节中的批量大小是4(数据点的总数)。

现在我们已经训练了一个模型,在下一节中,我们将学习对一组新的数据点进行预测。

预测新的数据点

在上一节中,我们学习了如何在已知数据点上拟合模型。在本节中,我们将学习如何利用前一节中已训练的mynet模型中定义的向前方法来预测看不见的数据点。我们将继续上一节中构建的代码:

  1. 创建我们想要测试模型的数据点:
val_x = [[10,11]]

注意,新数据集(val_x)也将是一个列表列表,因为输入数据集是一个列表列表。

  1. 将新数据点转换为张量浮点对象,并注册到设备:
val_x = torch.tensor(val_x).float().to(device)
  1. 将张量对象通过训练好的神经网络-mynet-就像它是一个 Python 函数一样。这与通过构建的模型执行正向传播是一样的:
mynet(val_x)
# 20.99

前面的代码返回与输入数据点相关联的预测输出值。

到目前为止,我们已经能够训练我们的神经网络来映射输入与输出,其中我们通过执行反向传播来更新权重值,以最小化损失值(使用预定义的损失函数来计算)。

在下一节中,我们将学习如何构建我们自己的自定义损失函数,而不是使用预定义的损失函数。

实现自定义损失函数

在某些情况下,我们可能必须实现一个针对我们正在解决的问题定制的损失函数——特别是在涉及目标检测/ 生成性广告网络 ( GANs )的复杂用例中。PyTorch 为我们提供了通过编写自己的函数来构建自定义损失函数的功能。

在本节中,我们将实现一个定制的损失函数,它与nn.Module中预构建的MSELoss函数做相同的工作:

The following code is available as Implementing_custom_loss_function.ipynb in the Chapter02 folder of this book's GitHub repository - tinyurl.com/mcvp-packt

  1. 导入数据,构建数据集和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()
import torch.nn as nn
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)
  1. 通过将两个张量对象作为输入来定义自定义损失函数,取它们的差,对它们求平方,并返回两者之间的平方差的平均值:
def my_mean_squared_error(_y, y):
    loss = (_y-y)**2
    loss = loss.mean()
    return loss
  1. 对于上一节中的相同输入和输出组合,nn.MSELoss用于获取均方误差损失,如下所示:
loss_func = nn.MSELoss()
loss_value = loss_func(mynet(X),Y)
print(loss_value)
# 92.7534
  1. 同样,当我们使用在步骤 2 中定义的函数时,损失值的输出如下:
my_mean_squared_error(mynet(X),Y)
# 92.7534

请注意结果是匹配的。我们使用了内置的MSELoss函数,并将其结果与我们构建的自定义函数进行了比较。

我们可以根据我们要解决的问题定义一个自定义函数。

到目前为止,我们已经了解了如何计算最后一层的输出。到目前为止,中间层值一直是一个黑箱。在下一节中,我们将学习获取神经网络的中间层值。

获取中间层的值

在某些情况下,获取神经网络的中间层值是有帮助的(当我们在后面的章节中讨论风格迁移和转移学习用例时,会有更多关于这方面的内容)。

PyTorch 提供了以两种方式获取神经网络中间值的功能:

The following code is available as Fetching_values_of_intermediate_layers.ipynb in the Chapter02 folder of this book's GitHub repository - tinyurl.com/mcvp-packt

  • 一种方法是直接调用层,就像它们是函数一样。这可以通过以下方式完成:
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类所替代之外,我们将执行与前面章节相同的步骤。

让我们为本章中讨论过的相同玩具数据编写网络代码:

The following code is available as Sequential_method_to_build_a_neural_network.ipynb in the Chapter02 folder of this book's GitHub repository - tinyurl.com/mcvp-packt

  1. 定义玩具数据集:
x = [[1,2],[3,4],[5,6],[7,8]]
y = [[3],[7],[11],[15]]
  1. 导入相关的包并定义我们将要使用的设备:
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'
  1. 现在,我们定义数据集类(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)
  1. 定义数据集(ds)和数据加载器(dl)对象:
ds = MyDataset(x, y)
dl = DataLoader(ds, batch_size=2, shuffle=True)
  1. 使用nn包中可用的Sequential方法定义模型架构:
model = nn.Sequential(
            nn.Linear(2, 8),
            nn.ReLU(),
            nn.Linear(8, 1)
        ).to(device)

请注意,在前面的代码中,我们定义了与前面几节中定义的相同的网络架构,但是定义不同。nn.Linear接受二维输入,并给出每个数据点的八维输出。此外,nn.ReLU在八维输出的顶部执行 ReLU 激活,最后,八维输入使用最后的nn.Linear层给出一维输出(在我们的例子中是两个输入相加的输出)。

  1. 打印我们在步骤 5 中定义的模型摘要:
  • 安装并导入使我们能够打印模型摘要的包:
!pip install torch_summary
from torchsummary import summary
  • 打印模型摘要,包括模型名称和模型的输入尺寸:
summary(model, torch.zeros(1,2))

上述代码给出了以下输出:

请注意,第一层的输出形状是(-1,8),其中-1 表示可以有与批大小一样多的数据点,8 表示对于每个数据点,我们有一个八维输出,结果得到形状批大小 x 8 的输出。对下两层的解释是相似的。

  1. 接下来,我们定义损失函数(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)
end = time.time()
print(end - start)
  1. 现在我们已经训练了模型,我们可以预测我们现在定义的验证数据集的值:
  • 定义验证数据集:
val = [[8,9],[10,11],[1.5,2.5]]
  • 预测通过模型传递验证列表的输出(注意,期望值是列表列表中每个列表的两个输入的总和)。如 dataset 类中所定义的,在将列表转换为张量对象并将它们注册到设备后,我们首先将列表转换为浮点数:
model(torch.tensor(val).float().to(device))
# tensor([[16.9051], [20.8352], [ 4.0773]], 
# device='cuda:0', grad_fn=<AddmmBackward>)

请注意,前面代码的输出(如注释所示)接近预期值(即输入值的总和)。

现在,我们已经了解了如何利用顺序方法来定义和训练模型,在下一节中,我们将了解如何保存和加载模型以进行推理。

保存和加载 PyTorch 模型

处理神经网络模型的一个重要方面是在训练后保存和加载模型。想象一个场景,你必须从一个已经训练好的模型中做出推论。您将加载已训练的模型,而不是再次训练它。

下面的代码可以在本书的 GitHub 库【tinyurl.com/mcvp-packt[…](tinyurl.com/mcvp-packt)

在浏览相关命令之前,以前面的例子为例,让我们了解一下完整定义一个神经网络的所有重要组件。我们需要以下内容:

  • 每个张量(参数)的唯一名称(键)
  • 连接网络中每个张量的逻辑
  • 每个张量的值(权重/偏差值)

第一点在定义的__init__阶段处理,第二点在forward方法定义阶段处理。默认情况下,张量中的值在__init__阶段被随机初始化。但我们想要的是加载一组在训练模型时学习到的特定的权重(或值),并将每个值与一个特定的名称相关联。这是您通过调用一个特殊的方法获得的,将在下面的部分中描述。

国家声明

model.state_dict()命令是理解如何保存和加载 PyTorch 模型的基础。model.state_dict()中的字典对应模型对应的参数名(键)和数值(权重和偏差值)。state指模型的当前快照(其中快照是每个张量上的值的集合)。

它返回一个键和值的字典(OrderedDict):

键是模型层的名称,值对应于这些层的权重。

节约

运行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加载权重:

  1. 使用培训时首先使用的命令创建一个空模型:
model = nn.Sequential(
            nn.Linear(2, 8),
            nn.ReLU(),
            nn.Linear(8, 1)
        ).to(device)
  1. 从磁盘中加载模型并将其解序列化,以创建一个orderedDict值:
state_dict = torch.load('mymodel.pth')
  1. state_dict加载到model上,注册到device,并进行预测:
model.load_state_dict(state_dict)
# <All keys matched successfully>
model.to(device)
model(torch.tensor(val).float().to(device))

如果所有的权重名称都出现在模型中,那么您将得到一条消息,说明所有的键都匹配。这意味着我们能够在世界上的任何机器上,出于任何目的,从磁盘加载我们的模型。

接下来,我们可以将模型注册到设备,并对新的数据点执行推理,正如我们在上一节中所了解的那样。

摘要

在这一章中,我们学习了 PyTorch 张量对象的构建模块以及在它们之上执行各种操作。我们进一步在玩具数据集上构建神经网络,首先构建一个初始化前馈架构的类,通过指定批量大小从数据集中获取数据点,并定义损失函数和优化器,循环多个时期。最后,我们还了解了如何定义自定义损失函数来优化选择指标,以及如何利用顺序方法来简化定义网络架构的过程。

所有前面的步骤构成了构建神经网络的基础,我们将在后续章节中构建的各种用例中多次利用它。

了解了使用 PyTorch 构建神经网络的各种组件后,我们将进入下一章,在这一章中,我们将了解处理影像数据集上神经网络的超参数的各种实际方面。

问题

  1. 训练时为什么要把整数输入转换成浮点值?
  2. 重塑张量物体的各种方法有哪些?
  3. 为什么张量对象比 NumPy 数组的计算速度更快?
  4. 神经网络类中的 init 神奇函数是由什么构成的?
  5. 为什么我们在执行反向传播之前执行零梯度?
  6. 数据集类由哪些神奇的函数构成?
  7. 我们如何对新的数据点进行预测?
  8. 我们如何获取神经网络的中间层值?
  9. 顺序方法如何有助于简化神经网络架构的定义?