PyTorch-应用深度学习指南-全-
PyTorch 应用深度学习指南(全)
原文:
zh.annas-archive.org/md5/92179b2ab6cdecd2d1f691b24d96e09f
译者:飞龙
前言
关于
本节简要介绍了作者、本书的内容覆盖范围、开始所需的技术技能以及完成所有包含的活动和练习所需的硬件和软件要求。
关于本书
机器学习正迅速成为解决数据问题的首选方式,这要归功于大量的数学算法,这些算法可以发现我们看不见的模式。
应用深度学习与 PyTorch 将带领您深入了解深度学习及其算法和应用。本书从帮助您浏览深度学习和 PyTorch 的基础开始。一旦您熟悉了 PyTorch 语法并能够构建单层神经网络,您将逐步学习通过配置和训练卷积神经网络(CNN)来解决更复杂的数据问题。随着章节的推进,您将发现如何通过实现递归神经网络(RNN)来解决自然语言处理问题。
在本书结束时,您将能够应用您在学习过程中积累的技能和信心,使用 PyTorch 构建深度学习解决方案,解决您的业务数据问题。
关于作者
海雅特·萨莱 毕业于商业管理专业后,发现数据分析对理解和解决现实生活问题的重要性。此后,作为一名自学者,她不仅为全球多家公司担任机器学习自由职业者,还创立了一家旨在优化日常流程的人工智能公司。她还撰写了由 Packt Publishing 出版的《机器学习基础》。
目标
-
检测多种数据问题,可以应用深度学习解决方案
-
学习 PyTorch 语法并用其构建单层神经网络
-
构建一个深度神经网络以解决分类问题
-
开发风格迁移模型
-
实施数据增强并重新训练您的模型
-
使用递归神经网络构建文本处理系统
受众
应用深度学习与 PyTorch 适用于希望使用深度学习技术处理数据的数据科学家、数据分析师和开发人员。任何希望探索并实施 PyTorch 高级算法的人都会发现本书有用。具备 Python 的基本知识和机器学习基础是必需的。然而,了解 NumPy 和 pandas 将是有益但不是必要的。
方法
应用深度学习与 PyTorch 采用实际操作的方式,每章节都有一个完整的实例,从数据获取到结果解释全过程演示。考虑到所涉及概念的复杂性,各章节包含多个图形表示以促进学习。
硬件要求
为了最佳学习体验,我们建议使用以下硬件配置:
-
处理器:Intel Core i3 或同等级别
-
内存:4 GB RAM
-
存储空间:35 GB 可用空间
软件需求
您还需要提前安装以下软件:
-
操作系统:Windows 7 SP1 64 位、Windows 8.1 64 位或 Windows 10 64 位、Ubuntu Linux 或 OS X 的最新版本
-
浏览器:Google Chrome/Mozilla Firefox 最新版本
-
Notepad++/Sublime Text 作为 IDE(可选,因为您可以使用浏览器中的 Jupyter 笔记本练习所有内容)
-
已安装 Python 3.4+(最新版本为 Python 3.7)(来自
python.org
) -
需要的 Python 库(Jupyter、Numpy、Pandas、Matplotlib、BeautifulSoup4 等)
约定
文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下:"我们在这里使用 requires_grad
参数告诉 PyTorch 计算该张量的梯度。"
代码块如下所示:
a = torch.tensor([5.0, 3.0], requires_grad=True)
b = torch.tensor([1.0, 4.0])
ab = ((a + b) ** 2).sum()
ab.backward()
新术语和重要单词显示为粗体。屏幕上看到的单词,例如菜单或对话框中的内容,以如下形式出现在文本中:"要下载将使用的数据集,请访问 archive.ics.uci.edu/ml/datasets/default+of+credit+card+clients
,然后点击 .xls
文件。"
安装代码包
将课程的代码包复制到您本地计算机上的一个文件夹,以便在学习本书时轻松访问。确切的位置取决于您操作系统的限制和个人偏好。
在本书的 GitHub 仓库中(github.com/TrainingByPackt/Applied-Deep-Learning-with-PyTorch
),您可以找到一个 requirements.txt
文件,其中包含本书不同活动和练习所需的所有库和模块列表及其版本。
其他资源
本书的代码包也托管在 GitHub 上,链接为 github.com/TrainingByPackt/Applied-Deep-Learning-with-PyTorch
。
我们还有其他代码包,来自我们丰富的书籍和视频目录,可在 github.com/PacktPublishing/
查看!
第一章:深度学习和 PyTorch 简介
学习目标
在本章结束时,您将能够:
-
解释深度学习是什么,其重要性以及它如何适用于 AI 和 ML
-
确定可以使用深度学习解决的数据问题类型
-
通过理解该库的优缺点,区分 PyTorch 与其他机器学习库
-
使用 PyTorch 创建单层神经网络
在本章中,我们将探讨深度学习与人工智能以及机器学习的共鸣。通过对 PyTorch 的介绍,我们将探索基本的编程练习,以应用 PyTorch 语法上的知识。
介绍
深度学习是机器学习的一个子集,专注于使用深度神经网络来解决复杂的数据问题。由于软件和硬件的进步允许收集和处理大量数据(我们谈论的是数百万甚至数十亿条记录),因此深度学习如今变得越来越流行,考虑到神经网络目前是唯一能够通过向模型提供更多数据来达到更高准确度水平的算法。
有了这个想法,对于更快的处理时间的需求是不可避免的。PyTorch 诞生于 2017 年,其主要特点在于利用 GPU 的力量来运行使用张量的数据。这使得算法能够以非常高的速度运行,并且同时为其用户提供了灵活性和标准的语法,以获得许多数据问题的最佳结果。
本书专注于使用 PyTorch 揭示神经网络的神秘,以消除隐含在神经网络架构复杂性周围的一些恐惧。
考虑到这一点,本章专注于介绍深度学习和 PyTorch 的主题。在这里,您将学习深度学习是什么,它如何适用于机器学习和人工智能的世界,它在一般条件下的工作原理,以及一些当前最流行的应用。此外,您还将了解 PyTorch 的工作原理,其主要模块和特征,以及对其用户提出的主要优缺点。
理解深度学习
要理解深度学习是什么,以及为什么它如今变得如此流行,首先定义人工智能和机器学习的概念是很重要的,以及深度学习如何融入这个世界。
图 1.1: 人工智能、机器学习和深度学习的图表
如前所示,人工智能(AI)是一个涵盖机器学习和深度学习的通用类别。它指的是机器展示的任何智能,最终导致解决问题。这些技术包括遵循一组规则或逻辑,或从先前的数据中学习,等等。考虑到这一点,人工智能解决方案可能具有或不具有学习能力以实现最优解。
具有学习能力的人工智能解决方案属于机器学习的子集。简单来说,机器学习只是实现人工智能的一种方式,它由能够在没有明确编程的情况下学习的算法组成。这意味着算法能够解析数据、从中学习,并据此做出决策(预测)。这种机器学习方法称为“监督学习”,基本上意味着算法同时接收输入数据和目标值(期望的输出)。
另一种机器学习方法称为“无监督学习”,与前述方法相比,只输入数据,没有与输出相关的任何关系。这里算法的目标是理解手头的数据以寻找相似之处。
最后,深度学习是机器学习的一个子集,使用多层神经网络(大型神经网络),灵感来自于人类大脑的生物结构,在一个层中的神经元接收一些输入数据,处理它,并将输出发送到下一层。这些神经网络可以由数千个互连的节点(神经元)组成,大多数以不同的层次组织,其中一个节点连接到前一层中的几个节点,接收其输入数据,同时连接到下一层中的几个节点,将经过处理的输出数据发送给它们。
神经网络的结构和功能将在本书的后续部分进一步解释。
深度学习为什么重要?为什么变得流行?
总体而言,深度学习之所以流行是因为准确性问题。深度学习在非常复杂的数据问题上实现了比以往任何时候都更高的准确性水平。这种出色表现的能力已经达到了机器可以胜过人类的水平,这不仅使模型能够优化流程,还能提高其质量。由于这一点,在对安全至关重要的革命性领域,如自动驾驶汽车,准确性的进步是显著的。
尽管神经网络理论上几十年前就存在,但它们最近变得流行有两个主要原因:
-
神经网络需要大量标记数据才能达到最优解,并且实际上利用这些数据。这意味着为了算法能够创建出优秀的模型,需要拥有数十万条记录(对于某些数据问题甚至需要数百万条),其中包含特征和目标值。
注意
标记数据指的是包含一组特征(描述一个实例的特征)和目标值(要实现的值)的数据。
图 1.2:深度学习在数据量方面与其他算法的性能比较
这种现象现在得以实现,得益于软件方面的进步,允许收集如此详细的数据,同时硬件方面的进步则允许对其进行处理。
-
神经网络需要大量的计算能力来处理这些数据,正如前面提到的。这是至关重要的,否则传统网络的训练时间将需要数周(甚至更长时间),考虑到实现最佳模型的过程是基于试错的,需要尽可能高效地运行训练过程。
通过使用 GPU,可以将神经网络的训练时间从几周缩短到几小时。
注意
此外,为了加速深度学习以利用大量的训练数据并构建最先进的模型,主要的云计算提供商(如 AWS、Microsoft Azure 和 Google)正在开发 FPGA(现场可编程门阵列)和 TPU(张量处理单元)。
深度学习的应用
深度学习正在彻底改变我们所知的技术,因为基于其应用的许多发展目前正在影响我们的生活。此外,据认为在接下来的 5 到 10 年内,许多处理过程的方式将发生根本性变化。
此外,深度学习可以应用于广泛的情况,从医疗和安全用途到更琐碎的任务,如给黑白图像上色或实时翻译文本。
以下是目前正在开发或正在使用的一些深度学习应用场景:
- 自动驾驶车辆:谷歌等多家公司正在开发部分或完全自动驾驶的车辆,这些车辆通过使用数字传感器来识别周围的物体学习驾驶。
图 1.3:Google 的自动驾驶汽车
-
医学诊断:深度学习正在重新定义这一行业,通过提高诊断脑部和乳腺癌等终末疾病的准确性。这是通过对新患者的图像进行分类来完成的,基于先前患者的标记图像,这些图像表明患者是否患有癌症。
-
语音助手: 这可能是当今最流行的应用之一,因为不同的语音激活智能助手大量普及,例如苹果的 Siri、Google Home 和亚马逊的 Alexa。
图 1.4: 亚马逊的 Alexa 智能助手
- 自动文本生成: 这涉及基于输入的句子生成新的文本。在电子邮件撰写中,这被广泛应用,其中电子邮件提供商根据迄今为止写入的文本向用户建议接下来的几个词。
图 1.5: Gmail 的文本生成功能
-
广告: 这里的主要思想是通过针对正确的受众或创建更有效的广告等方法来增加广告投资的回报率。
-
价格预测: 对于初学者来说,这是通过使用机器学习算法可以实现的典型示例。价格预测包括基于实际数据训练模型,包括在房地产领域中,物业特征及其最终价格,以便仅基于物业特征预测未来条目的价格。
PyTorch 简介
图 1.6: PyTorch 图书馆标志
PyTorch 是一个开源库,主要由 Facebook 的人工智能研究小组开发,作为 Torch 的 Python 版本,于 2017 年 1 月首次向公众发布。它利用图形处理单元(GPU)的强大能力来加速张量的计算,从而加快复杂模型的训练时间。
该库具有 C++ 后端,与 Torch 深度学习框架结合,比起许多带有多个深度学习功能的本地 Python 库,可以实现更快的计算。另一方面,其前端是 Python,这一点帮助了它的流行,因为新手数据科学家可以轻松构建非常复杂的神经网络。而且,由于与 Python 的集成,可以将 PyTorch 与其他流行的 Python 包一起使用。
尽管这个库相对较新,但由于使用了来自该领域许多专家的反馈,它迅速获得了广泛的流行,这使它成为了为用户而创建的库。在下一节中讨论了使用它的许多优缺点。
优势
如今有几个库可用于开发深度学习解决方案,那么为什么选择 PyTorch?因为 PyTorch 是一个动态库,允许用户以非常灵活的方式开发可以适应每个特定数据问题的复杂架构。
因此,它已被大量研究人员和人工智能开发人员采纳,这使得它成为机器学习领域求职必备。
这里显示了需要强调的关键方面:
-
易用性:关于 API,PyTorch 具有简单的界面,使得开发和运行模型非常容易。许多早期采用者认为它比其他库(如 TensorFlow)更直观。它具有 Pythonic 风格,这意味着它与 Python 集成,在许多开发者看来,即使对于许多开发者来说,这个库还是新的,但是也很直观,易于使用。这种集成还允许使用许多 Python 包,如 NumPy 和 SciPy,以扩展其功能。
-
速度:PyTorch 利用 GPU 进行加速张量计算。这使得该库训练速度比其他深度学习库更快。当需要测试不同的近似值以获得最佳模型时,速度是至关重要的。此外,即使其他库也可能有使用 GPU 加速计算的选项,PyTorch 只需输入几行简单的代码就可以完成此操作。
注意
下面的 URL 包含了对不同深度学习框架的速度基准测试(考虑到在处理大量训练数据时,训练时间的差异显而易见):
-
便利性:PyTorch 非常灵活。它使用动态计算图,允许您在运行时更改网络。此外,它在构建架构时提供了极大的灵活性,因为很容易对传统架构进行调整。
-
命令式:PyTorch 还是命令式的。每行代码都是单独执行的,允许您实时跟踪模型,以及以更方便的方式调试模型。
-
预训练模型:最后,它包含许多预训练模型,非常易于使用,是解决某些数据问题的绝佳起点。
缺点
虽然优点很多,但仍然有一些需要考虑的缺点,这里进行了解释:
-
社区小:与 TensorFlow 等其他库相比,这个库的适配者社区非常小。然而,尽管只有两年的时间向公众开放,PyTorch 如今已经是实施深度学习解决方案的第三大流行库,并且其社区日益壮大。
-
文档不完善:考虑到该库的新颖性,文档不如 TensorFlow 等更成熟的库完整。然而,随着库的功能和能力的增加,文档正在不断扩展。此外,随着社区的持续增长,互联网上将会有更多的信息可用。
-
不适用于生产环境:尽管有关该库的许多投诉集中在其无法用于生产的能力上,但在版本 1.0 发布后,该库包含了生产能力,可以导出最终模型并在生产环境中使用。
什么是张量?
与 NumPy 类似,PyTorch 使用张量来表示数据,这些张量是类似于矩阵的 n 维结构,如 图 1.7 所示,不同之处在于张量可以在 GPU 上运行,这有助于加速数值计算。此外,值得一提的是,对于张量来说,维度被称为秩。
![图 1.7:不同维度张量的视觉表示
![图 1.7:不同维度张量的视觉表示
图 1.7:不同维度张量的视觉表示
与矩阵相反,张量是包含在结构中可以与其他数学实体交互的数学实体。当一个张量转换另一个张量时,前者也携带自己的转换。
这意味着张量不仅仅是数据结构,而是容器,当提供一些数据时,它们可以与其他张量进行多线性映射。
练习 1:使用 PyTorch 创建不同秩的张量
注意
所有练习和活动将主要在 Jupyter 笔记本中开发。建议为不同的作业保留单独的笔记本,除非另有建议。
在此练习中,我们将使用 PyTorch 库创建一秩、二秩和三秩的张量。
注意
对于本章中的练习和活动,您需要安装 Python 3.6、Jupyter、Matplotlib 和 PyTorch 1.0。
注意
要克隆包含本书中所有练习和活动的存储库,请在导航到所需路径后,在您的 CMD 或终端中使用以下命令:
git clone https://github.com/TrainingByPackt/Applied-Deep-Learning-with-PyTorch.git
-
打开 Jupyter 笔记本以实现此练习。
打开您的 CMD 或终端,导航至所需路径,并使用以下命令打开 Jupyter 笔记本:
jupyter notebook
。注意
此命令可能会根据您的操作系统及其配置而变化。
-
导入名为
torch
的 PyTorch 库:import torch
-
创建以下秩的张量:1、2 和 3。
使用介于 0 和 1 之间的值填充您的张量。可以根据您的需求定义张量的大小,只要创建正确的秩即可:
tensor_1 = torch.tensor([0.1,1,0.9,0.7,0.3]) tensor_2 = torch.tensor([[0,0.2,0.4,0.6],[1,0.8,0.6,0.4]]) tensor_3 = torch.tensor([[[0.3,0.6],[1,0]], [[0.3,0.6],[0,1]]])
当使用启用 GPU 的机器时,请使用以下脚本创建张量:
tensor_1 = torch.cuda.tensor([0.1,1,0.9,0.7,0.3]) tensor_2 = torch.cuda.tensor([[0,0.2,0.4,0.6],[1,0.8,0.6,0.4]]) tensor_3 = torch.cuda.tensor([[[0.3,0.6],[1,0]], [[0.3,0.6],[0,1]]])
-
使用
shape
属性打印每个张量的形状,就像您在 NumPy 数组中所做的那样:print(tensor_1.shape) print(tensor_2.shape) print(tensor_3.shape)
每个张量的最终形状应如下:
torch.Size([5]) torch.Size([2, 4]) torch.Size([2, 2, 2])
恭喜!您已成功创建不同秩的张量。
PyTorch 的关键元素
像任何其他库一样,PyTorch 有各种模块、库和包,用于开发不同的功能。在本节中,将解释构建深度神经网络所使用的三个最常用元素,并提供一个语法的简单示例。
PyTorch autograd 库
autograd
库包含一种称为自动微分的技术。它的目的是数值计算函数的导数。这对我们将在下一章节学习的反向传播概念至关重要,这是在训练神经网络时执行的操作。
注意
后续章节将详细解释神经网络,包括训练模型所采取的不同步骤。
要计算梯度,只需调用backward()
函数,如下所示:
a = torch.tensor([5.0, 3.0], requires_grad=True)
b = torch.tensor([1.0, 4.0])
ab = ((a + b) ** 2).sum()
ab.backward()
在上述代码中,创建了两个张量。我们在这里使用requires_grad
参数告诉 PyTorch 计算该张量的梯度。然而,在构建您的神经网络时,此参数是不需要的。
接下来,使用两个张量的值定义了一个函数。最后,使用backward()
函数计算了梯度。
PyTorch nn 模块
autograd
库本身可以用来构建简单的神经网络,考虑到更复杂的部分(梯度计算)已经处理好。然而,这种方法可能会有些麻烦,因此引入了 nn 模块。
nn 模块是一个完整的 PyTorch 模块,用于创建和训练神经网络,通过使用不同的元素,可以进行非常简单和非常复杂的开发。例如,Sequential()
容器允许轻松创建按预定义模块(或层)序列排列的网络架构,无需太多的知识。
注意
可以在后续章节进一步解释可以用于每种神经网络架构的不同层。
此外,该模块还具备定义损失函数以评估模型的能力,以及许多更高级的功能,本书将对其进行讨论。
将神经网络架构建立为一系列预定义模块的过程可以在几行代码中完成,如下所示:
import torch.nn as nn
model = nn.Sequential(nn.Linear(input_units, hidden_units), nn.ReLU(), nn.Linear(hidden_units, output_units), nn.Sigmoid())
loss_funct = torch.nn.MSELoss()
首先,导入模块。接下来,定义模型架构。input_units
指的是输入数据包含的特征数,hidden_units
指的是隐藏层节点数,output_units
指的是输出层节点数。
可以看出,网络的架构包含一个隐藏层,具有 ReLU 激活函数,以及一个具有 sigmoid 激活函数的输出层。
最后,将损失函数定义为均方误差(MSE)。
注意
为了创建不遵循现有模块序列的模型,使用了自定义的 nn 模块。我们将在本书的后续部分介绍这些内容。
PyTorch 优化包
使用optim
包定义优化器,该优化器将用于更新每次迭代中的参数(在接下来的章节中将进一步解释),使用autograd
模块计算的梯度。在这里,可以从可用的优化算法中选择,例如 Adam、随机梯度下降(SGD)和 RMSprop 等。
要设置要使用的优化器,以下代码行足以:
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
在这里,model.parameters()
参数指的是先前创建的模型的权重和偏差,而lr
指的是学习率,设置为0.01
。
接下来,显示运行 100 次迭代的优化过程,正如您所看到的,使用了 nn 模块创建的模型和由autograd
库计算的梯度:
for i in range(100):
# Call to to the model to perform a prediction
y_pred = model(x)
# Calculation of loss function based on y_pred and y
loss = loss_func(y_pred, y)
# Zero the gradients so that previous ones don't accumulate
optimizer.zero_grad()
# Calculate the gradients of the loss function
loss.backward()
# Call to the optimizer to perform an update of the parameters
optimizer.step()
每次迭代中,调用模型以获取预测值(y_pred
)。将该预测值和地面真值(y
)馈送到损失函数中,以确定模型逼近地面真值的能力。
然后,将梯度归零,并使用backward()
函数计算损失函数的梯度。
最后,调用step()
函数来基于优化算法和先前计算的梯度更新权重和偏差。
活动 1:创建单层神经网络
对于此活动,我们将创建一个单层神经网络,这将是我们未来活动中创建深度神经网络的起点。让我们看看以下情景。
您正在申请一家主要技术公司的工作,并且已经通过了所有筛选面试。招聘过程的下一步是在面试中实时展示您的编程机器学习技能。他们要求您使用 PyTorch 构建一个单层神经网络:
-
导入所需的库。
-
创建随机值的虚拟输入数据(
x
)和只包含 0 和 1 的虚拟目标数据(y
)。将数据存储在张量中。张量x
的大小应为(100,5),而y
的大小应为(100,1)。注意
PyTorch 张量可以像 NumPy 数组一样操作。使用
torch.randn(number_instances, number_features)
创建x
。使用torch.randint(low=0, high, size)
创建y
。请注意,randint
是上限独占的。确保将y
张量转换为FloatTensor
类型,因为这是 nn 模块处理的默认类型。为此,请使用.type(torch.FloatTensor)
。 -
定义模型的架构并将其存储在名为
model
的变量中。记得创建单层模型。 -
定义要使用的损失函数。
使用均方误差损失函数。
-
定义模型的优化器。
使用 Adam 优化器。
-
运行 100 次迭代的优化。在每次迭代中,打印并保存损失值。
注意
使用以下代码行将每次迭代步骤的损失值附加到在 for 循环之外预先创建的列表(losses)中:
losses.append(loss.item())
-
打印最终权重和偏置值的数值。应该有五个权重值(每个输入数据的特征一个)和一个偏置值:
model.state_dict()
-
制作一条线图显示每次迭代步骤的损失值。
注意
此活动的解决方案可以在第 186 页找到。
总结
在过去几年中,人工智能这个词变得越来越流行。我们在电影中看到它,也在现实生活中看到它,它基本上指的是机器展示的任何形式的智能,目的是优化人类的任务。人工智能的一个子类专注于那些能够从数据中学习的算法,称为机器学习。
深度学习是机器学习的一个子集,灵感来源于人类大脑的生物结构。它使用深度神经网络通过大量数据解决复杂的数据问题。尽管理论几十年前就已经发展,但最近由于硬件和软件的进步,这些理论得以应用,使得可以收集和处理数百万条数据。
随着深度学习解决方案的普及,已经开发了许多深度学习库。其中,最近的一个是 PyTorch。PyTorch 使用 C++后端来加速计算,同时拥有 Python 前端,使得库易于使用。
它使用张量来存储数据,这些张量是类似于 n 级矩阵的结构,可以在 GPU 上运行以加快处理速度。它提供了三个主要元素,对于以较少的工作量创建复杂的神经网络架构非常有用。
autograd
库可以计算函数的导数,这些导数被用作优化模型的权重和偏差的梯度。此外,nn
模块帮助您轻松地将模型的架构定义为一系列预定义模块,并确定用于测量模型的损失函数。最后,optim
包用于选择优化算法以更新先前计算的梯度。
第二章:神经网络的基本构建模块
学习目标
通过本章结束时,您将能够:
-
识别神经网络的优缺点
-
区分神经网络解剖学中的不同组成部分
-
识别最流行的神经网络架构并了解它们主要用于什么
-
使用技术准备数据输入到神经网络中
-
使用简单的架构解决回归问题
-
通过解决高偏差或高方差来提高模型的性能
在本章中,我们将探讨神经网络的基本构建模块。我们将探索不同的架构来解决各种任务。最后,我们将学习如何使用 PyTorch 构建神经网络。
简介
尽管神经网络理论几十年前就已经发展,概念起源于感知机,但是近年来已经创建了不同的架构来解决不同的数据问题。这主要是由于现实数据问题中可以找到的不同数据格式,如文本、音频和图像。本章的目的是介绍神经网络的主题及其主要优缺点,以便更好地理解何时以及如何使用它们。接下来,本章将详细解释最流行的神经网络架构的构建模块:人工神经网络(ANNs)、卷积神经网络(CNNs)和循环神经网络(RNNs)。
随后,通过解决实际的回归问题来解释构建有效模型的过程。这包括准备输入到神经网络中的数据(也称为数据预处理)、定义要使用的神经网络架构,最后评估模型的性能,以确定如何改进以实现最佳解决方案。
前述过程将使用前一章学习的神经网络架构之一来完成,考虑到每个数据问题的解决方案应使用最适合该数据类型的架构。其他架构将在后续章节中用于解决涉及使用图像和文本序列作为输入数据的更复杂的数据问题。
注意
作为提醒,包含本章中使用的所有代码的 GitHub 存储库可以在以下链接找到:github.com/TrainingByPackt/Applied-Deep-Learning-with-PyTorch
简介神经网络
几十年前发展起来的神经网络需要从训练数据中学习,而不是按照一套规则编程来解决特定任务。学习过程可以遵循以下方法之一:
-
监督学习:这是学习的最简单形式,因为它包括一个带标签的数据集,神经网络需要找到解释特征与目标之间关系的模式。学习过程中的迭代旨在最小化预测值与真实值之间的差异。一个例子是基于叶子属性对植物进行分类。
-
无监督学习:与前述方法相比,无监督学习包括使用未标记数据(即没有目标值)训练模型。其目的是更好地理解输入数据,通常网络会接收输入数据,对其进行编码,然后从编码版本中重建内容,理想情况下保留相关信息。例如,给定一段文字,神经网络可以映射单词以输出实际关键的单词,这些单词可以用作描述段落的标签。
-
强化学习:这种方法论包括利用手头的数据进行学习,其主要目标是在长期内最大化奖励函数。因此,决策不是基于即时奖励,而是基于整个学习过程中的累积奖励,例如将资源分配给不同任务,以期减少减缓整体性能的瓶颈。
注
从提到的学习方法中,最常用的是监督学习,这也是后续章节中主要使用的学习方法。这意味着大多数练习、活动和示例将使用带标签的数据集作为输入数据。
神经网络是什么?
简而言之,神经网络是一种机器学习算法,其模型建立在人脑解剖学基础上,利用数学方程从训练数据的观察中学习模式。
然而,要真正理解神经网络通常遵循的训练过程背后的逻辑,首先理解感知器的概念非常重要。
感知器在 20 世纪 50 年代由 Frank Rosenblatt 开发,是一种人工神经元,类似于人脑中的神经元,接收多个输入并产生二进制输出,成为后续神经元的输入。它们是神经网络的基本构建块(就像神经元是人脑的构建块一样)。
图 2.1:感知器示意图
这里,X1、X2、X3 和 X4 表示感知器接收的不同输入,可能有任意数量的输入。灰色圆圈表示感知器,在这里处理输入以得出结果。
罗森布拉特还引入了权重(w1, w2, …, wn)的概念,这些是表达每个输入重要性的数字。输出可以是 0 或 1,这取决于输入的加权和是高于还是低于给定的阈值,可以作为感知器的参数设置,如下所示:
图 2.2:感知器输出方程
练习 2:执行感知器的计算
下面的练习不需要任何编程;相反,它包含简单的计算,以帮助你理解感知器的概念。要执行这些计算,请考虑以下情景。
下周五你的城里有个音乐节,但你病了,正考虑是否去参加(其中 0 表示你会去,1 表示你不会去)。为了做出决定,你决定考虑三个因素:
-
天气会好吗?(X1)
-
你有人可以一起去吗?(X2)
-
你喜欢这里的音乐吗?(X3)
针对上述因素,如果问题的答案是是,则我们将使用 1,如果答案是否,则使用 0。另外,由于你病得很厉害,天气因素相关性很高,你决定给这个因素的权重是其他两个因素的两倍。因此,因素的权重为 4(w1)、2(w2)和 2(w3)。现在,考虑一个阈值为 5:
- 下周五天气不好,但你有人可以一起去音乐节,并且喜欢那里的音乐:
图 2.3:感知器的输出
考虑到输出低于阈值,最终结果将等于 1,意味着你不应该去音乐节,以避免病情加重的风险。
恭喜!你成功地执行了感知器的计算。
多层感知器
考虑到前述情况,多层网络的概念包括多个感知器(也称为节点或神经元)堆叠在一起的网络,如此处所示:
图 2.4:多层感知器的图示
注
神经网络中用于指代层次的符号如下:第一层也称为输入层,最后一层也称为输出层,中间的所有层称为隐藏层。
在这里,再次使用一组输入来训练模型,但不是将输入馈送到单个感知器,而是馈送到第一层中的所有感知器(神经元)。接下来,从该层获得的输出被用作下一层感知器的输入,依此类推,直到达到最终层,负责输出结果。
需要指出的是,感知器的第一层通过加权输入来处理简单的决策过程,而后续层可以根据前一层的输出处理更复杂和抽象的决策,因此深度神经网络(使用许多层的网络)在处理复杂数据问题时表现出色。
与传统的感知器不同,神经网络已经演变为能够在输出层具有一个或多个节点,以便将结果呈现为二进制或多类别。
神经网络的学习过程
一般来说,神经网络是多个神经元的连接,其中每个神经元计算一个线性函数以及一个激活函数,以便根据一些输入得出输出。此输出与权重相关联,代表其重要性水平,以供后续层次的计算使用。
此外,这些计算是在整个网络架构中进行的,达到最终输出。此输出用于确定网络性能与真实情况的比较,然后用于调整网络的不同参数,以重新开始计算过程。
考虑到这一点,神经网络的训练过程可以被视为一个迭代过程,通过网络的各层前进和后退,以达到一个最优结果,如下图所示,并将详细解释:
图 2.5:神经网络学习过程的图示
前向传播
这是通过网络架构从左到右进行的过程,同时使用输入数据进行计算,以得出可以与真实情况进行比较的预测。这意味着网络中的每个神经元都会根据其关联的权重和偏置转换输入数据(初始数据或来自前一层的数据),并将输出发送到下一层,直到达到最终层并进行预测。
每个神经元中进行的计算包括一个线性函数,该函数将输入数据乘以一些权重再加上偏置,然后通过激活函数传递。激活函数的主要目的是打破模型的线性性,这在考虑到大多数使用神经网络解决的现实生活数据问题不是线性定义,而是由复杂函数定义时非常关键。相关公式可以在这里找到:
图 2.6:每个神经元执行的计算
此处,如前所述,X 指输入数据,W 是确定输入数据重要性水平的权重,b 是偏置值,sigma()表示应用于线性函数的激活函数。
激活函数的作用在于向模型引入非线性。现今常用的激活函数如下:
- Sigmoid:它呈 S 形,基本上将值转换为介于 0 和 1 之间的简单概率,sigmoid 函数得到的大多数输出将接近 0 和 1 的极端值:
图 2.7:Sigmoid 激活函数
图 2.8:Sigmoid 激活函数的图形表示
- Softmax:与 sigmoid 函数类似,它计算一个事件在 n 个事件中的概率分布,这意味着其输出不是二元的。简单来说,该函数计算输出属于目标类别之一的概率,相对于其他类别:
图 2.9:Softmax 激活函数
考虑到其输出为概率,这种激活函数通常出现在分类网络的输出层中。
- Tanh:该函数表示双曲正弦和双曲余弦之间的关系,其结果在 -1 到 1 之间。该激活函数的主要优势在于能更轻松地处理负值:
图 2.10:tanh 激活函数。
图 2.11:tanh 激活函数的图形表示
- 修正线性单元函数(ReLU):基本上激活一个节点,条件是线性函数的输出大于 0,否则其输出将为 0。如果线性函数的输出大于 0,则该激活函数的结果将是其接收的原始数字:
图 2.12:ReLU 激活函数
传统上,该激活函数用于所有隐藏层:
图 2.13:ReLU 激活函数的图形表示
损失函数的计算
一旦前向传播完成,训练过程的下一步是通过比较预测结果与真实值来计算损失函数,以估计模型的误差。考虑到这一点,理想的值应为 0,这意味着两个值之间没有偏差。
这意味着在训练过程的每个迭代中,目标是通过改变参数(权重和偏置)来最小化损失函数,在前向传播期间执行计算。
再次强调,有多种损失函数可供选择。然而,用于回归和分类任务的最常用的损失函数在此有解释:
- 均方误差(MSE):广泛用于衡量回归模型性能,MSE 函数计算实际值和预测值之间距离的平方和:
图 2.14: 均方误差损失函数
这里,n 是样本数, 是实际值,而
是预测值。
- 交叉熵/多类交叉熵:这个函数通常用于二元或多类分类模型。它衡量两个概率分布之间的差异;一个较大的损失函数将代表较大的差异。因此,这里的目标是尽量减少损失函数:
图 2.15: 交叉熵损失函数
再次强调,n 代表样本数。 和
分别是实际值和预测值。
反向传播(反向传递)
训练过程的最后一步是沿着网络架构从右到左计算损失函数对每一层权重和偏置的偏导数,以便更新这些参数(权重和偏置),以便在下一次迭代步骤中减少损失函数。
此外,优化算法的最终目标是找到损失函数达到可能的最小值的全局极小值,如下图所示:
注意
提醒一下,局部极小值指的是函数定义域中的最小值。另一方面,全局极小值指的是整个函数定义域的最小值。
图 2.16: 通过迭代步骤优化损失函数。二维空间。
这里,最左边的点(A)是损失函数的初始值,在任何优化之前。最右边最底部的点(B)是经过多次迭代步骤后损失函数被最小化的值。从一个点到另一个点的过程称为步骤。
然而,重要的是要提到,损失函数并不总是像前面的那样平滑,这可能在优化过程中引入达到局部极小值的风险。
这个过程也称为优化,有不同的算法以不同的方法达到相同的目标。接下来将解释最常用的优化算法。
梯度下降
梯度下降是数据科学家中最广泛使用的优化算法,也是许多其他优化算法的基础。在计算每个神经元的梯度后,权重和偏置会朝梯度的相反方向进行更新,更新步骤的大小由学习率控制(用于控制每次优化中的步骤大小),如下方程式所示。
在训练过程中,学习率至关重要,因为它可以防止权重和偏置的更新过度/不足,这可能会阻止模型达到收敛或延迟训练过程。
在梯度下降算法中,权重和偏置的优化如下所示:
图 2.17:梯度下降算法中参数的优化
在这里,α 表示学习率,dw/db 表示给定神经元中权重或偏置的梯度。这两个值的乘积从权重或偏置的原始值中减去,以惩罚那些导致计算大损失函数的较高值。
对梯度下降算法的改进称为随机梯度下降,它基本上遵循相同的过程,但不同之处在于它以随机批次方式获取输入数据,而不是一次性获取整个数据块,这可以提高训练时间同时达到卓越的性能。此外,这种方法允许使用更大的数据集,因为通过使用数据集的小批次作为输入,我们不再受到计算资源的限制。
优势和劣势
下面是神经网络的优势和劣势的解释。
优势
过去几年中,神经网络因四个主要原因变得越来越受欢迎:
-
数据:神经网络以其利用大量数据的能力而广为人知,多亏了硬件和软件的进步,现在可以回忆和存储大量数据库。这使得神经网络在输入更多数据时展现出了其真正的潜力。
-
复杂数据问题:正如前面所述,神经网络非常适合解决其他机器学习算法无法解决的复杂数据问题。这主要是因为它们能够处理大规模数据集并揭示复杂的模式。
-
计算能力:技术的进步也提升了当今可用的计算能力,这对于训练使用数百万数据的神经网络模型至关重要。
-
学术研究:由于前述三点,关于这一主题的大量学术研究可在互联网上找到,这不仅促进了每天新研究的涌现,还有助于保持算法及硬件/软件需求的最新性。
缺点
只因为使用神经网络有很多优点,并不意味着每个数据问题都应该用这种方式解决。这是一个常见的错误。没有一种算法适用于所有数据问题,选择算法应该依赖于可用的资源以及数据问题本身。
尽管神经网络被认为能够胜任几乎所有的机器学习算法,但重要的是也要考虑它们的缺点,以权衡对数据问题最为重要的因素。
-
黑盒子:这是神经网络最常见的缺点之一。它基本上意味着神经网络如何以及为什么会得出特定的输出是未知的。例如,当神经网络错误地将一张猫的图片预测为狗时,无法知道错误的原因是什么。
-
数据需求:为了达到最佳结果,神经网络需要大量的数据,这既是优点也是缺点。神经网络需要比传统机器学习算法更多的数据,这可能是某些数据问题中选择它们与其他算法之间的主要原因。当任务是有监督学习时,即数据需要被标记,这一问题变得尤为突出。
-
训练时间:与前述的缺点相关联,对大量数据的需求也使得训练过程比传统机器学习算法更长,而在某些情况下这是不可选的。通过使用 GPU 可以缩短训练时间,因为 GPU 能加速计算过程。
-
计算成本高昂:再次强调,神经网络的训练过程是计算成本高昂的。虽然一个神经网络可能需要数周才能收敛,其他机器学习算法则可能只需几小时或几分钟即可完成训练。所需的计算资源取决于手头数据的量以及网络的复杂性;更深层次的神经网络需要更长的训练时间。
注意
神经网络的架构种类繁多。本章将解释其中三种最常用的架构,并在后续章节中介绍它们的实际实现。然而,如果你希望了解其他架构,请访问
www.asimovinstitute.org/neural-network-zoo/
。
人工神经网络简介
人工神经网络(ANNs),也称为多层感知器,是多个感知器的集合,如前所述。这里重要的是提到,感知器之间的连接发生在层之间,其中一个层可以拥有任意多的感知器,并且它们都与前后层的所有其他感知器相连接。
网络可以有一个或多个层。具有超过四层的网络被认为是深度神经网络,并且通常用于解决复杂和抽象的数据问题。
ANN 通常由三个主要元素组成,在前面已经详细解释过,也可以在图 2.18中看到:
-
输入层:这是网络的第一层,传统上位于网络图表的最左侧。它在执行任何计算之前接收输入数据,并完成第一组计算,在这里最通用的模式被揭示。
对于监督学习问题,输入数据由特征和目标值的一对组成。网络的任务是揭示输入和输出之间的相关性或依赖关系。
-
隐藏层:接下来,可以找到隐藏层。神经网络可以拥有尽可能多的隐藏层。层数越多,它可以处理的数据问题越复杂,但训练时间也会更长。也有一些神经网络架构完全不包含隐藏层,这在单层网络中是适用的情况。
在每一层中,基于从前一层接收到的输入信息进行计算,输出一个预测结果,成为后续层的输入。
-
输出层:这是网络的最后一层,位于网络图表的最右侧。它在所有神经元处理数据后接收数据,以生成和显示最终预测。
输出层可以有一个或多个神经元。前者是指解决方案是二进制形式的模型,即 0 或 1 的形式。另一方面,后者包括输出实例属于每个可能类标签(目标)的概率,这意味着该层将具有与类标签数量相同的神经元。
图 2.18:具有两个隐藏层的神经网络架构
卷积神经网络介绍
卷积神经网络(CNNs)主要用于计算机视觉领域,在这个领域,近几十年来,机器已经达到甚至超过人类能力的准确性水平,因此它们变得越来越受欢迎。
受人类大脑启发,CNN 搜索创建模型,利用不同组的神经元识别图像的不同方面。这些组应该能够相互通信,以便共同形成整体图像。
考虑到这一点,CNN 架构中的层分割它们的识别任务。第一层专注于简单的模式,网络末端的层使用这些信息揭示更复杂的模式。
例如,在识别图片中的人脸时,前几层专注于查找将一个特征与另一个分开的边缘。接下来的层强调人脸的某些特征,如鼻子。最后,最后几层使用这些信息将整个人脸拼合在一起。
通过使用滤波器或核心来激活一组神经元以在遇到特定特征时激活的想法,这是通过使用卷积神经网络结构的主要构建块之一来实现的。然而,它们并不是结构中唯一存在的元素,这就是为什么将提供对 CNN 的所有组成部分的简要解释:
注
当使用 CNN 时可能听说过的填充和步幅的概念,将在本书的后续章节中进行解释。
-
卷积层:在这些层中,图像(表示为像素矩阵)与滤波器之间进行卷积计算。这种计算产生特征图作为输出,最终作为下一层的输入。
计算取图像矩阵的一个子部分,并执行值的乘法。然后,乘积的总和被设定为该图像部分的输出,如下图所示:
图 2.19:图像与滤波器之间的卷积操作
这里,左侧的矩阵是输入数据,中间的矩阵是滤波器,右侧的矩阵是计算的输出。可以在这里看到通过红框突出显示的值进行的计算:
图 2.20:图像第一部分的卷积
这种卷积乘法对图像的所有子部分进行。图 2.21 展示了相同示例的另一个卷积步骤:
图 2.21:卷积操作的进一步步骤
卷积层的一个重要概念是,它们是不变的,每个滤波器都有一个特定的功能,在训练过程中不会变化。例如,负责检测耳朵的滤波器在整个训练过程中只专注于这个功能。
此外,卷积神经网络通常会有多个卷积层,每个层都会根据所使用的滤波器专注于识别图像的特定特征。此外,需要指出的是,在两个卷积层之间通常有一个池化层。
-
池化层:尽管卷积层能够从图像中提取相关特征,但当分析复杂的几何形状时,其结果可能会变得庞大,这会使得训练过程在计算能力方面变得不可能。因此,池化层的发明就显得尤为重要。
这些层不仅达到了减少卷积层输出的目标,而且实现了去除特征中存在的噪声,从而最终提高了模型的准确性。
可应用两种主要类型的池化层,并且它们的理念在于检测在图像中表达出更强烈影响的区域,从而可以忽略其他区域:
- 最大池化:此操作包括取矩阵中给定大小的子段,并将该子段中的最大数作为最大池化操作的输出。
图 2.22:最大池化操作
在上述图中,通过使用 3x3 的最大池化滤波器,得到了右侧的结果。在这里,黄色区域(左上角)的最大值为 4,而橙色区域(右上角)的最大值为 5。
- 平均池化:类似地,平均池化操作会取矩阵的子段并输出符合规则的数字,这种情况下是该子段中所有数字的平均值。
图 2.23:平均池化操作
在这里,使用 3x3 的滤波器,我们得到了黄色区域(左上角)所有数字的平均值为 8.6,而橙色区域(右上角)的平均值为 9.6。
-
全连接层:最后,考虑到如果网络只能检测一组特征而无法将其分类到类别标签中,那么网络将毫无用处,因此在 CNN 的末端使用全连接层,将前一层(称为特征图)检测到的特征输出,并输出这些特征组属于类别标签的概率,这被用于进行最终预测。
像人工神经网络一样,全连接层利用感知器根据给定的输入计算输出。此外,需要提到的是,卷积神经网络通常在架构末端有多个全连接层。
结合所有这些概念,可以得到卷积神经网络的传统结构,其中每种类型可以有任意多层,每个卷积层可以有任意多个滤波器(每个用于特定任务),池化层应具有相同数量的滤波器,如下图所示:
图 2.24:卷积神经网络结构图
递归神经网络简介
前述神经网络的主要限制在于它们仅通过考虑当前事件(正在处理的输入)来学习,而不考虑先前或随后的事件,这在我们人类的思考方式中是不方便的。例如,在阅读一本书时,通过考虑上一段落或更多的上下文,你可以更好地理解每个句子。
因此,考虑到神经网络的优化过程通常由人类完成,设计能够考虑输入和输出序列的网络变得至关重要,因此递归神经网络(RNNs)应运而生。它们是一种强大的神经网络类型,通过使用内部记忆解决复杂的数据问题。
简而言之,这些网络包含了其中的循环,允许信息在其内存中保留更长时间,即使正在处理后续信息。这意味着 RNN 中的感知器不仅将输出传递给下一个感知器,还会向自身传递一些信息,这对于分析下一个信息片段是有用的。这种记忆保持能力使得它们在预测接下来会发生什么方面非常准确。
递归神经网络的学习过程,类似于其他网络,试图映射输入(x)和输出(y)之间的关系,不同之处在于这些模型还考虑了全部或部分先前输入的历史。
RNNs 允许处理数据序列,可以是输入序列、输出序列,甚至同时处理两者,如下图所示:
图 2.25:递归神经网络处理的数据序列
在这里,每个框都是一个矩阵,箭头表示发生的函数。底部框是输入,顶部框是输出,中间框表示该点的 RNN 状态,保存了网络的记忆。
从左到右,以下是前述图表的解释:
-
典型的模型不需要 RNN 解决。它具有固定的输入和固定的输出。例如,这可以是图像分类。
-
这个模型接受一个输入并产生一系列的输出。例如,一个接收图像作为输入并输出图像标题的模型。
-
与上述相反,这个模型接受一系列的输入并生成一个单一的输出。这种类型的架构可以在情感分析问题中看到,其中输入是要分析的句子,输出是句子背后的预测情感。
-
最后两个模型接受一系列的输入并返回一系列的输出,不同之处在于第一个模型首先分析整个输入集,然后生成输出集。例如,在语言翻译中,需要先完全理解一种语言中的整个句子,然后再进行实际翻译。另一方面,第二个多对多模型同时分析输入并同时生成输出。例如,标记视频每一帧时。
数据准备
在收集数据之后,任何深度学习模型的开发的第一步,应该是数据的准备。这对于正确理解手头的数据并能够正确界定项目范围至关重要。
许多数据科学家未能这样做,导致模型性能不佳,甚至模型无用,因为它们根本不解决数据问题。
准备数据的过程可以分为三个主要任务:1)理解数据并处理任何潜在问题,2)重新缩放特征以确保不会由于错误引入偏差,以及 3)拆分数据以能够准确地衡量性能。所有这三个任务将在下一节中进一步解释。
注意
所有先前解释的任务在应用任何机器学习算法时基本相同,因为它们涉及到预先准备数据所需的技术。
处理混乱数据
这项任务主要包括执行探索性数据分析(EDA)以理解可用的数据,并检测可能影响模型开发的潜在问题。
EDA 过程非常有用,因为它帮助开发人员发现对行动计划定义至关重要的信息。这些信息在这里解释:
-
数据量:这既涉及实例的数量,也涉及特征的数量。前者对于确定是否有必要甚至可能使用神经网络或深度神经网络解决数据问题至关重要,考虑到这类模型需要大量数据才能达到高精度水平。而后者则有助于确定是否在开发之前采用某些特征选择方法,以减少特征数量、简化模型并消除任何冗余信息。
-
目标特征:对于监督模型,数据需要被标记。考虑到这一点,选择目标特征(建立模型时希望达到的目标)非常重要,以评估特征是否存在许多缺失或异常值。此外,这有助于确定开发的目标,该目标应与可用数据保持一致。
-
噪声数据/异常值:噪声数据指的是明显不正确的数值,例如年龄为 200 岁的人。另一方面,异常值指的是虽然可能是正确的数值,但离均值很远,例如 10 岁的大学生。
检测异常值并没有确切的科学方法,但有些方法学是被普遍接受的。假设一个正态分布的数据集,其中最流行的方法之一是将任何偏离所有数值均值约 3-6 个标准偏差的值定义为异常值,无论是正方向还是负方向。
识别异常值的一个同样有效的方法是选择处于 99 分位和 1 分位的数值。
处理此类数值非常重要,特别是当它们代表特征数据的 5%以上时,因为不处理可能会引入模型偏差。处理这些数值的方法与任何其他机器学习算法一样,要么删除异常值,要么使用均值或回归插补技术赋予新值。
-
缺失值:与前述情况类似,数据集中存在许多缺失值可能会引入模型偏差,考虑到不同模型会对这些值做出不同的假设。同样,当缺失值占特征值的 5%以上时,应通过删除或替换它们的方式进行处理,同样可以使用均值或回归插补技术。
-
定性特征:最后,检查数据集是否包含定性数据也是一个关键步骤,因为移除或编码数据可能会导致更准确的模型。
此外,在许多研究开发中,会在同一数据上测试多个算法,以确定哪一个表现更好,而其中一些算法不能容忍使用定性数据,因此转换或编码它们以能够将所有算法用同样的数据进行输入显得尤为重要。
练习 3:处理混乱数据
注意
本章所有的练习将使用 UC Irvine 机器学习库中的Appliances energy prediction Dataset
进行,可以通过以下 URL 下载,使用Data Folder
超链接:archive.ics.uci.edu/ml/datasets/Appliances+energy+prediction
在这个练习中,我们将使用 Python 的一个受欢迎的包来探索手头的数据,并学习如何检测缺失值、异常值和定性值:
注意
对于本章中的练习和活动,您需要安装 Python 3.6、Jupyter、NumPy 和 Pandas(至少版本 0.21)。
-
打开 Jupyter 笔记本以实施这个练习。
打开您的 cmd 或终端,导航到所需路径,并使用以下命令打开 Jupyter 笔记本:
jupyter notebook
-
导入 pandas 库:
import pandas as pd
-
使用 pandas 读取先前从 UC Irvine 机器学习库站点下载的包含数据集的 CSV 文件。
然后,删除名为
date
的列,因为我们不打算在接下来的练习中考虑它。最后,打印 DataFrame 的头部:
data = pd.read_csv("energydata_complete.csv") data = data.drop(columns=["date"]) data.head()
-
检查数据集中的分类特征:
cols = data.columns num_cols = data._get_numeric_data().columns list(set(cols) - set(num_cols))
结果列表为空,这表明没有分类特征需要处理。
-
使用 Python 的
isnull()
和sum()
函数来查找数据集每一列中是否有缺失值:data.isnull().sum()
此命令计算每列中的空值数量。对于正在使用的数据集,不应该有任何缺失值。
-
使用三个标准差作为测量值,以检测数据集中所有特征的异常值:
outliers = {} for i in range(data.shape[1]): min_t = data[data.columns[i]].mean() – (3 * data[data.columns[i[[.std()) max_t = data[data.columns[i]].mean() + (3 * data[data.columns[i[[.std()) count = 0 for j in data[data.columns[i]]: if j < min_t or j > max_t: count += 1 percentage = count / data.shape[0] outliers[data.columns[i]] = "%.3f" % percentage
结果字典显示了数据集中所有特征的列表,以及异常值的百分比。从这些结果可以得出结论,由于异常值比例低于 5%,因此无需处理。
恭喜!您已成功探索了数据集并处理了潜在问题。
数据重缩放
尽管数据不需要重新缩放以供算法训练,但这是提高模型准确性的重要步骤。基本上是因为每个特征具有不同的比例可能会导致模型认为某个特征比其他特征更重要,因为它具有更高的数值。
举例来说,考虑两个特征:一个测量一个人有几个孩子,另一个说明这个人的年龄。尽管年龄特征可能有更高的数值,但在推荐学校的研究中,孩子数量特征可能更重要。
考虑到这一点,如果所有特征被等比例缩放,模型实际上可以根据目标特征的重要性给予更高的权重,而不是它们具有的数值。此外,它还可以通过消除模型学习数据的不变性来加速训练过程。
数据科学家中有两种主要的重新缩放方法,虽然没有选择其中一种的规则,但重要的是要强调它们应该单独使用(一种或另一种)。
可以在这里找到这两种方法的简要说明:
- 归一化:这包括重新调整值,使得所有特征的所有值都在 0 到 1 之间,使用以下方程:
图 2.26:数据归一化
- 标准化: 相反,这种缩放方法通过以下方程将所有值转换为均值为 0、标准差为 1。
图 2.27: 数据标准化
练习 4: 数据重新缩放
在本练习中,我们将对上一个练习中的数据进行重新缩放:
注意
使用与上一个练习中相同的 Jupyter 笔记本。
-
将特征与目标分开。这样做是为了只对特征数据进行重新缩放:
X = data.iloc[:, 1:] Y = data.iloc[:, 0]
-
使用归一化方法对特征数据进行重新缩放。显示结果 DataFrame 的前几行以验证结果:
X = (X - X.min()) / (X.max() - X.min()) X.head()
恭喜!您已成功地对数据集进行了重新缩放。
数据分割
将数据集分为三个子集的目的是,使模型可以适当地进行训练、微调和测量,而不引入偏差。以下是每个集合的解释:
-
训练集: 如其名称所示,此集合被馈送到神经网络进行训练。对于监督学习来说,它包括特征和目标值。考虑到神经网络需要大量数据进行训练,因此这通常是三个集合中最大的集合。
-
验证集(开发集): 主要用于衡量模型的性能,以便调整超参数以提高性能。这个微调过程是为了找到能够获得最佳结果的超参数配置。
尽管模型没有在这些数据上进行训练,但它间接影响了这些数据,这就是为什么不应该在这些数据上进行最终的性能评估,因为它可能会产生偏差。
-
测试集: 这个集合对模型没有影响,因此用于对未见数据进行最终评估,这成为衡量模型在未来数据集上表现的指导。
没有关于将数据分成所提到的三个集合的完美比例的实际科学,考虑到每个数据问题都不同,并且开发深度学习解决方案通常需要试错方法。尽管如此,众所周知,较大的数据集(数十万和数百万个实例)的分割比例应为 98%/1%/1%,因为对于训练集来说,尽可能使用尽可能多的数据至关重要。对于较小的数据集,传统的分割比例是 60%/20%/20%。
练习 5: 分割数据集
在本练习中,我们将从上一个练习中的数据集中分割出三个子集。为了学习目的,我们将探索两种不同的方法。首先,将使用索引来分割数据集。接下来,将使用 scikit-learn 的train_test_split()
函数来实现相同的目的,使用这两种方法都可以达到相同的结果:
注意
使用与上一个练习中相同的 Jupyter 笔记本。
-
打印数据集的形状,以确定要使用的拆分比例。
X.shape
此操作的输出应为
(19735, 28)
。这意味着可以使用 60%/20%/20%的拆分比例用于训练、验证和测试集。 -
获取用作训练集和验证集底限的值。这将用于使用索引拆分数据集:
train_end = int(len(X) * 0.6) dev_end = int(len(X) * 0.8)
-
对数据集进行洗牌:
X_shuffle = X.sample(frac=1) Y_shuffle = Y.sample(frac=1)
-
使用索引将洗牌后的数据集拆分为三个集合,分别用于特征和目标数据:
x_train = X_shuffle.iloc[:train_end,:] y_train = Y_shuffle.iloc[:train_end] x_dev = X_shuffle.iloc[train_end:dev_end,:] y_dev = Y_shuffle.iloc[train_end:dev_end] x_test = X_shuffle.iloc[dev_end:,:] y_test = Y_shuffle.iloc[dev_end:]
-
打印所有三个集合的形状:
print(x_train.shape, y_train.shape) print(x_dev.shape, y_dev.shape) print(x_test.shape, y_test.shape)
前面操作的结果应如下所示:
(11841, 27) (11841, ) (3947, 27) (3947, ) (3947, 27) (3947, )
-
从 scikit-learn 的
model_selection
模块导入train_test_split()
函数:from sklearn.model_selection import train_test_split
-
拆分洗牌后的数据集:
x_new, x_test_2, y_new, y_test_2 = train_test_split(X_shuffle, Y_shuffle, test_size=0.2, random_state=0) dev_per = x_test_2.shape[0]/x_new.shape[0] x_train_2, x_dev_2, y_train_2, y_dev_2 = train_test_split(x_new, y_new, test_size=dev_per, random_state=0)
第一行代码执行初始拆分。该函数接受两个要拆分的数据集(X 和 Y)、
test_size
(测试集中包含的实例百分比)以及random_state
(确保结果可重现)。该代码的结果是将每个数据集(X 和 Y)分为两个子集。为了创建一个额外的集合(验证集),我们将执行第二次拆分。前面代码的第二行负责确定用于第二次拆分的
test_size
,以便测试集和验证集具有相同的形状。最后,代码的最后一行执行第二次拆分,使用先前计算的值作为
test_size
。 -
打印所有三个集合的形状:
print(x_train_2.shape, y_train_2.shape) print(x_dev_2.shape, y_dev_2.shape) print(x_test_2.shape, y_test_2.shape)
前面操作的结果应如下所示:
(11841, 27) (11841, ) (3947, 27) (3947, ) (3947, 27) (3947, )
可以看到,两种方法得到的结果集具有相同的形状。使用其中一种方法还是另一种方法是一种个人偏好。
恭喜!您已成功将数据集分割为三个子集。
活动 2:执行数据准备
对于接下来的活动,我们将准备一个包含多个属性的歌曲列表数据集,这些属性有助于确定歌曲发布的年份。这一数据准备步骤对本章节内后续活动至关重要。让我们看看以下场景。
您在音乐唱片公司工作,他们想要揭示区分不同时间段唱片的细节,因此他们已经整理了一个包含数据的数据集,该数据集包含了 515,345 条记录,发布年份从 1922 年到 2011 年不等。他们委托您准备数据集,以便输入神经网络使用。
注意
要下载数据集,请访问以下 UC Irvine Machine Learning Repository 的网址:archive.ics.uci.edu/ml/datasets/YearPredictionMSD
-
导入所需的库。
-
使用 pandas,加载文本文件。由于先前下载的文本文件与 CSV 文件的格式相同,因此可以使用
read_csv()
函数进行读取。确保将头部参数设置为None
。 -
验证数据集中是否存在任何定性数据。
-
检查缺失值。
如果您在先前用于此目的的代码行中添加额外的
sum()
函数,您将得到整个数据集中缺失值的总和,而不是按列区分。 -
检查异常值。
-
将特征与目标数据分开。
-
使用标准化方法重新调整数据。
-
将数据分成三组:训练集、验证集和测试集。使用您喜欢的方法。
注意
正解可以在第 188 页找到。
构建深度神经网络
从一般角度来看,构建神经网络可以通过非常简单的方式实现,使用像 scikit-learn 这样的库(不适用于深度学习),它会为您完成所有数学运算,但灵活性不高;或者通过非常复杂的方式实现,从头编写训练过程的每一个步骤,或者使用更强大的框架,它可以在同一个地方允许两者的近似。如前所述,它有一个 nn 模块,专门用于使用顺序容器轻松预定义简单架构的实现,同时允许创建引入灵活性的自定义模块,以构建非常复杂的架构过程。
另一方面,PyTorch 是考虑了领域内许多开发者的输入构建的,具有允许在同一地方进行两者近似的优势。如前所述,它有一个 nn 模块,专门用于使用顺序容器轻松预定义简单架构的实现,同时允许创建引入灵活性的自定义模块,以构建非常复杂的架构过程。
在本节中,我们将进一步讨论使用顺序容器开发深度神经网络的使用,以揭开它们的复杂性。然而,在本书的后续章节中,我们将继续探讨更复杂和抽象的应用,这些应用也可以通过极少的努力实现。
正如前面提到的,顺序容器是一个模块,用于包含按顺序跟随的模块序列。它包含的每个模块都会对给定的输入应用一些计算,以得出结果。
可以在顺序容器内使用的一些最受欢迎的模块(层)以开发常规分类模型在这里进行解释:
注意
用于其他类型架构(如卷积神经网络和循环神经网络)的模块将在接下来的章节中进行解释。
-
True
默认情况下)作为参数。 -
False
默认情况下。Tanh:将逐元素 tanh 函数应用于包含输入数据的张量。它不接受任何参数。
Sigmoid:将之前解释过的 sigmoid 函数应用于包含输入数据的张量。它不接受任何参数。
Softmax:将 softmax 函数应用于包含输入数据的 n 维张量。输出经过重新缩放,使得张量元素位于介于零到一之间的范围内,并且总和为一。它接受作为参数应计算 softmax 函数的维度。
-
False
默认情况下。这种技术通常用于处理过拟合模型,稍后将进一步解释。 -
归一化层:有不同的方法可以用来在顺序容器中添加归一化层。其中一些方法包括 BatchNorm1d、BatchNorm2d 和 BatchNorm3d。其背后的想法是对前一层的输出进行归一化,最终在较短的训练时间内达到类似的准确性水平。
练习 6:使用 PyTorch 构建深度神经网络
在这个练习中,我们将使用 PyTorch 库定义一个四层深度神经网络的架构,然后使用之前准备好的数据集进行训练:
注:
使用与之前练习中使用的相同的 Jupyter 笔记本。
-
从 PyTorch 库中导入所需的库,称为
torch
,以及来自 PyTorch 的nn
模块:import torch import torch.nn as nn
注:
尽管不同的包和库根据实际学习目的在需要时被导入,但是始终将它们导入到代码的开头是一个良好的实践。
-
从之前的练习中分离出特征列和目标列,对于每个创建的数据集。另外,将最终的数据框转换为张量:
x_train = torch.tensor(x_train.values).float() y_train = torch.tensor(y_train.values).float() x_dev = torch.tensor(x_dev.values).float() y_dev = torch.tensor(y_dev.values).float() x_test = torch.tensor(x_test.values).float() y_test = torch.tensor(y_test.values).float()
-
使用
sequential()
容器定义网络架构。确保创建一个四层网络。对于前三层使用 ReLU 激活函数,并且考虑到我们处理的是回归问题,最后一层不使用激活函数。
每层的单元数应为:100、50、25 和 1:
model = nn.Sequential(nn.Linear(x_train.shape[1], 100), nn.ReLU(), nn.Linear(100, 50), nn.ReLU(), nn.Linear(50, 25), nn.ReLU(), nn.Linear(25, 1))
-
将损失函数定义为均方误差:
loss_function = torch.nn.MSELoss()
-
将优化器算法定义为 Adam 优化器:
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
-
使用
for
循环在训练数据上进行 100 次迭代步骤训练网络:for i in range(100): y_pred = model(x_train) loss = loss_function(y_pred, y_train) print(i, loss.item()) optimizer.zero_grad() loss.backward() optimizer.step()
-
为了测试模型,对测试集的第一个实例进行预测,并将其与基准值(目标值)进行比较:
pred = model(x_test[0]) print(y_test[0], pred)
从这个结果可以看出,模型表现不佳,因为目标值与预测值差异很大。在本书的后续部分,您将学习如何提高模型的性能。
祝贺!您已成功创建并训练了一个深度神经网络,以解决回归问题。
活动 3:为回归问题开发深度学习解决方案
在下一个活动中,我们将创建并训练一个四层隐藏层神经网络,以解决之前活动中提到的回归问题。让我们看一下以下的场景:
您继续在音乐唱片公司工作,看到您在准备数据集方面做得很出色后,他们信任您来定义网络的架构和代码,并使用准备好的数据集进行训练:
注:
使用与之前活动中使用的相同的 Jupyter 笔记本。
-
导入所需的库。
-
从之前活动中分离特征和目标,针对所有三组数据。将数据框转换为张量。
-
定义网络的架构。可以尝试不同的层数和每层的单元数组合。
-
定义损失函数和优化算法。
-
使用
for
循环训练网络进行 100 次迭代。 -
通过在测试集的第一个实例上进行预测并将其与真实值进行比较来测试您的模型。
您的输出应该与以下类似:
图 2.28:活动的输出
注意
这项活动的解决方案可以在第 190 页找到。
概要
几十年前,由 Frank Rosenblatt 开发的理论孕育了神经网络。它始于感知器的定义,这是一个受人类神经元启发的单元,它接收数据作为输入并对其进行转换。它包括为输入数据分配权重以进行计算,以便最终结果要么是一种结果,要么是另一种,取决于结果。
神经网络最广为人知的形式是由一系列感知器组成的,这些感知器堆叠在一起形成层,其中一列感知器(层)的输出是下一列的输入。
根据这一点,解释了神经网络的典型学习过程。在这个主题上,有三个主要过程需要考虑:前向传播、损失函数的计算和反向传播。
这个过程的最终目标是通过更新伴随着神经网络每个输入值的权重和偏差来最小化损失函数。这通过一个迭代过程实现,可能需要几分钟、几小时,甚至几周,具体取决于数据问题的性质。
三种类型的神经网络的主要架构也进行了讨论:人工神经网络、卷积神经网络和循环神经网络。第一种用于解决传统分类问题,第二种因其解决计算机视觉问题(图像分类)的能力而广受欢迎,而最后一种能够处理序列数据,对于诸如语言翻译之类的任务非常有用。
第三章:使用 DNN 解决分类问题
学习目标
在本章结束时,您将能够:
-
解释深度学习在银行业的应用
-
区分为回归任务和分类任务构建神经网络
-
应用 PyTorch 中的自定义模块概念来解决问题
-
使用深度神经网络解决分类问题
-
处理欠拟合或过拟合的模型
-
部署 PyTorch 模型
在本章中,我们将专注于使用 DNN 解决简单的分类任务,以巩固我们对深度神经网络的知识。
引言
虽然深度神经网络(DNNs)可以用来解决回归问题,正如前一章所见,但它们更常用于解决分类任务,目标是从一系列选项中预测结果。
利用这些模型的一个领域是银行业。这主要是由于他们需要基于人口统计数据预测未来行为,以及确保长期盈利的主要目标。在银行业的一些应用包括评估贷款申请、信用卡批准、预测股市价格和通过分析行为检测欺诈。
本章将专注于使用深度人工神经网络解决银行业的分类问题,遵循到达有效模型所需的所有步骤:数据探索、数据准备、架构定义和模型训练、模型微调、错误分析,最后是最终模型的部署。
注意
提醒一下,在本章中使用的所有代码的 GitHub 仓库可以在以下网址找到:github.com/TrainingByPackt/Applied-Deep-Learning-with-PyTorch
。
问题定义
定义问题和构建模型或提高准确性同等重要。为什么呢?因为即使您可能使用了最强大的算法,并使用了最先进的方法来改进其结果,如果您解决了错误的问题或使用了错误的数据,这一切可能都是无用的。
此外,学会深入思考理解什么可以做和不能做,以及如何完成可以做的事情至关重要。特别是考虑到当我们学习应用机器学习或深度学习算法时,问题总是清楚呈现的,除了模型训练和性能改进外,不需要进一步的分析;另一方面,在现实生活中,问题通常令人困惑,数据也常常混乱。
因此,在本节中,您将学习如何根据组织需求和手头数据的最佳实践来定义问题。
为此,需要完成以下事项:
-
理解问题的“什么”、“为什么”和“如何”。
-
分析手头的数据以确定我们模型的一些关键参数,例如要执行的学习任务类型、必要的准备工作和性能指标的定义。
-
进行数据准备以减少向模型引入偏见的概率。
银行业中的深度学习
类似于健康领域,银行和金融机构每天都处理大量信息,这些信息需要做出关键决策,这些决策不仅影响到他们自己组织的未来,还影响到信任他们的数百万个人的未来。
每一秒都在做出这些决策,在 1990 年代,银行业人士过去依赖于基本上利用人类专家知识来编码基于规则的程序的专家系统。毫不奇怪,这些程序表现不佳,因为它们要求所有信息或可能的情景在前期都进行编程,这使得它们对处理不确定性和高度变化的市场效率低下。
随着技术的进步以及获取客户数据的能力的提高,银行业已经引领了向更专业的系统过渡,这些系统利用统计模型来帮助做出此类决策。此外,由于银行需要同时考虑自身的盈利能力和客户的盈利能力,它们被认为是那些不断跟上技术进步以日益提高效率和准确性的行业之一。
如今,与医疗市场一样,银行和金融行业正在推动神经网络市场。这主要是因为神经网络能够利用大量先前数据来预测未来行为中的不确定性。这是人类专家知识基础系统无法实现的,考虑到人类大脑无法分析如此大量的数据。
以下简要介绍了银行和金融服务领域中使用深度学习的一些领域:
-
贷款申请评估:银行根据不同因素向客户发放贷款,包括人口统计信息、信用历史等。他们在此过程中的主要目标是最小化客户违约贷款的数量(最小化失败率),从而最大化通过发放贷款获得的回报。
神经网络被用来帮助决定是否批准贷款。通常使用以前未能按时还款的贷款申请人以及按时还款的贷款申请人的数据来训练它们。一旦建立了模型,想法就是将新申请人的数据输入到模型中,以便预测他们是否会还款,考虑到模型的重点是减少误判(即模型预测会违约贷款的客户,但实际上他们没有)。
行业已知神经网络的失败率低于依赖人类专业知识的传统方法。
-
欺诈检测: 对于银行和金融服务提供商来说,欺诈检测至关重要,现在比以往任何时候都更加重要,考虑到技术的进步,尽管使我们的生活更轻松,但也使我们在网上银行平台上面临更大的财务风险。
在这个领域使用神经网络,具体来说是卷积神经网络(CNN),用于字符和图像识别,以检测图像中隐藏的抽象模式,以确定用户是否被替代。
-
.xls
文件。
探索数据集
在接下来的章节中,我们将专注于使用信用卡客户违约(DCCC)数据集解决与信用卡付款相关的分类任务,该数据集已经从 UC Irvine Repository 网站上下载。
本节的主要目的是清楚地说明数据问题的什么、为什么和如何,这将有助于确定研究目的和评估指标。此外,在本节中,我们将详细分析手头的数据,以便识别准备数据时需要的一些步骤(例如,将定性特征转换为它们的数值表示)。
首先,让我们定义什么、为什么和如何。考虑到这一点,应该确保识别组织的真实需求:
什么: 构建一个能够确定客户是否会在即将到期的付款中违约的模型。
为什么: 能够预见下个月将收到的付款金额(以货币形式)。这将帮助公司确定该月的支出策略,此外还允许他们为每个客户定义应采取的行动,既确保那些将支付账单的客户未来的付款,又提高那些将违约客户的支付概率。
如何: 使用包含客户人口统计信息、信用历史和之前账单声明的历史数据来训练模型。在对输入数据进行训练后,该模型应能够确定客户是否有可能在下一个付款中违约。
考虑到这一点,似乎目标特征应该是一个说明客户是否会违约下一个付款的特征,这意味着学习任务是一个分类任务,因此损失函数应该能够测量这种类型学习的差异(例如,交叉熵函数,如前一章所述)。
一旦问题定义清楚,就需要确定最终模型的优先级。这意味着确定所有输出类是否同等重要。例如,一个测量肺部肿块是否恶性的模型应主要集中在最小化假阴性
(模型预测为没有恶性肿块的患者,但实际上是恶性的肿块)。另一方面,一个用于识别手写字符的模型不应专注于一个特定字符,而应最大化在识别所有字符方面的性能。
考虑到这一点,以及为什么声明中的解释,信用卡客户违约
数据集的模型优先级应该是最大化模型的整体性能,而不优先考虑任何类标签。这主要是因为为什么声明宣称,研究的主要目的应该更好地了解银行将收到的款项,并对可能违约付款的客户执行某些操作,以及对不会违约的客户执行不同的操作。
根据此,本案例研究中要使用的性能指标是准确度,其侧重点是最大化正确分类的实例。这指的是任何类标签的正确分类实例与总实例数之间的比率。
下表包含数据集中每个特征的简要解释,这可以帮助确定它们对研究目的的相关性,并确定需要执行的一些准备任务。
图 3.1:来自 DCCC 数据集的特征描述
图 3.2:来自 DCCC 数据集的特征描述,继续
综合考虑这些信息,可以得出结论,在 25 个特征(包括目标特征)中,有 2 个需要从数据集中移除,因为它们被认为与研究目的无关。请记住,对于本研究无关的特征可能在其他研究中是相关的。例如,关于私密卫生产品的研究可能认为性别特征是相关的。
此外,所有特征都是定量的,这意味着除了重新缩放它们之外,无需转换它们的值。目标特征也已转换为其数值表示,其中下次付款违约的客户表示为 1,而未违约的付款客户表示为 0。
数据准备
虽然在这方面有一些良好的实践,但在准备数据集以开发深度学习解决方案时,没有固定的步骤集,大多数情况下,需要采取的步骤将取决于手头的数据、要使用的算法以及研究的其他特性。
尽管如此,在开始训练模型之前,有一些必须遵循的关键方面作为良好的实践。其中大部分您已经从前一章中了解到,将针对所讨论的数据集进行修订,另外还要对目标特征的类别不平衡进行修订:
注意
在本节中将处理准备 DCCC 数据集的过程,并附上简要说明。随时打开 Jupyter 笔记本,复制这个过程,考虑到这将是后续活动的起点。
-
使用
skiprows
参数移除 Excel 文件的第一行,该行不相关,因为它包含第二组标题。根据给定的代码行,得出以下结果:
图 3.3:DCCC 数据集的头部
数据集的形状是 30,000 行和 25 列,可以使用以下代码行获取:
print("rows:",data.shape[0]," columns:", data.shape[1])
-
删除不相关的特征:通过对每个特征的分析,确定了两个特征与研究目的无关,因此应将其从数据集中移除。
data_clean = data.drop(columns=["ID", "SEX"]) data_clean.head()
最终的数据集应包含 23 列,而不是原来的 25 列:
图 3.4:删除不相关特征后的 DCCC 数据集的头部
-
检查缺失值:接下来是检查数据集是否存在缺失值,并计算它们在每个特征中所占的百分比,可以使用以下代码行完成:
total = data_clean.isnull().sum() percent = (data_clean.isnull().sum()/ data_clean.isnull().count()*100) pd.concat([total, percent], axis=1, keys=['Total', 'Percent']).transpose()
第一行对数据集的每个特征的缺失值进行求和。接下来,计算每个特征中缺失值在所有值中的参与度。最后,将之前计算的两个值连接起来,以表格形式显示结果。结果显示在图 3.5中:
。
图 3.5:DCCC 数据集中缺失值的计数
从这些结果可以看出,数据集中没有缺失任何值,因此在这里不需要进一步的步骤。
-
BILL_AMT1
和BILL_AMT4
,每个占总实例的 2.3%。这意味着考虑到它们的参与度太低,并且不太可能对最终模型产生影响,因此不需要进一步操作。
检查类别不平衡:当目标特征中的类标签表示不均匀时,就会发生类别不平衡;例如,一个包含 90%未来未违约客户与 10%违约客户的数据集被认为是不平衡的。
处理类别不平衡的几种方法,其中一些在这里解释:
收集更多数据:尽管这并非总是可行的途径,但可能有助于平衡类别,或者允许删除过度表示类别而不严重减少数据集。
更改性能指标:某些指标,如准确性,不适合用于衡量不平衡数据集的性能。因此,建议使用精确度或召回率等指标来衡量分类问题的性能。
对数据集进行重新采样:这包括改变数据集以平衡各类别。可以通过两种不同的方式实现:1)添加欠表示类别的副本(称为过采样),或者,2)删除过度表示类别的实例(称为欠采样)。
可以通过简单地计算目标特征中每个类别的出现次数来检测类别不平衡,如下所示:
target = data_clean["default payment next month"] yes = target[target == 1].count() no = target[target == 0].count() print("yes %: " + str(yes/len(target)*100) + " - no %: " + str(no/len(target)*100))
从前述代码中可以得出结论,违约支付客户的数量占数据集的 22.12%。这些结果也可以使用以下代码行显示在图中:
import matplotlib.pyplot as plt fig, ax = plt.subplots() plt.bar("yes", yes) plt.bar("no", no) ax.set_yticks([yes,no]) plt.show()
这导致以下图表:
图 3.6:目标特征的类别计数
为了解决这个问题,并考虑到没有更多数据可以添加,并且性能指标实际上是准确性,需要进行数据重新采样。
以下是执行数据集过采样的代码片段,随机创建欠表示类别的重复行:
data_yes = data_clean[data_clean["default payment next month"] == 1]
data_no = data_clean[data_clean["default payment next month"] == 0]
over_sampling = data_yes.sample(no, replace=True, random_state = 0)
data_resampled = pd.concat([data_no, over_sampling], axis=0)
首先,我们将每个类标签的数据分别放入独立的 DataFrame 中。接下来,我们使用 pandas 的sample()
函数构建一个包含与过度表示的类别 DataFrame 相同数量的重复实例的新 DataFrame。
最后,使用concat()
函数将过度表示类别的 DataFrame 和相同大小的新 DataFrame 连接起来。
通过计算整个数据集中每个类别的参与度,结果应显示出均衡的类别。此外,到目前为止数据集的最终形状应为(46728,23)。
-
从目标中分离特征:我们将数据集分割成特征矩阵和目标矩阵,以避免重新调整目标值:
X = data_clean.drop(columns=["default payment next month"]) y = data_clean["default payment next month"]
-
重新调整数据:最后,我们重新调整特征矩阵的值,以避免向模型引入偏差:
X = (X - X.min())/(X.max() - X.min()) X.head()
前面几行代码的结果显示在图 3.7中:
图 3.7:归一化后的特征矩阵
注意事项
注意,婚姻和教育都是序数特征,意味着它们遵循一定的顺序或层次;在选择重新缩放方法时,请确保保持顺序。
为了方便后续活动使用准备好的数据集,特征(X
)和目标(y
)矩阵将连接成一个 pandas DataFrame,并保存到 CSV 文件中,使用以下代码:
final_data = pd.concat([X, y], axis=1)
final_data.to_csv("dccc_prepared.csv", index=False)
完成所有这些步骤后,DCCC 数据集已准备就绪(保存在新的 CSV 文件中),以用于训练模型,这将在接下来的章节中进行解释。
模型构建
一旦问题被定义,并且探索和准备手头的数据,就是定义模型的时候了。在进行先前分析之后,应处理网络架构、层类型、损失函数等的定义。这主要是因为在机器学习中没有“一刀切”的方法,尤其在深度学习中更是如此。
与分类任务不同,回归任务需要不同的方法,聚类、计算机视觉或机器翻译也是如此。因此,在接下来的章节中,您将找到解决分类任务的模型构建的关键特征,以及如何得到“好”的架构以及如何何时使用 PyTorch 中的自定义模块的解释。
用于分类任务的人工神经网络(ANN)
如前一章中的活动所示,用于回归任务的神经网络使用输出作为连续值,这就是为什么输出函数没有激活函数,只有一个输出节点(实际值)的原因,例如基于房屋特征和社区特征来预测房价的模型。
考虑到这一点,性能的测量应通过计算地面真实值与预测值之间的差异来完成,就像计算 125.3(预测值)与 126.38(地面真实值)之间的距离一样。如前所述,有许多方法可以衡量这种差异,其中均方误差(MSE)或其变体均方根误差(RMSE)是最常用的度量标准。
相反,分类任务的输出是某组输入特征属于每个输出标签或类别的概率,这是通过使用 Sigmoid(用于二分类)或 Softmax(用于多类分类)激活函数来完成的。此外,对于二分类任务,输出层应包含一个(用于 Sigmoid)或两个(用于 Softmax)输出节点,而对于多类分类任务,输出节点应等于类标签的数量。
能够计算属于每个输出类的可能性的这种能力,再加上argmax
函数,将检索具有更高概率的类作为最终预测。
注
在 Python 中,argmax
是一个函数,能够返回沿着轴的最大值的索引。
考虑到这一点,模型的性能应该是实例是否已被分类到正确的类别标签的问题,而不是与测量两个值之间的距离有关的任何事情,因此训练神经网络用于分类问题的使用不同的损失函数(交叉熵是最常用的),以及使用不同的性能指标,如准确率、精确率和召回率。
一个良好的架构
首先,正如本书中所解释的那样,重要的是理解手头的数据问题,以确定神经网络的一般拓扑结构。再次强调,普通的分类问题不需要与计算机视觉问题相同的网络架构。
一旦定义了这一点,并考虑到在确定隐藏层数量、其类型或每个层中单元数量方面没有正确答案,最好的方法是从一个初始架构开始,然后可以改进以增加性能。
为什么这么重要?因为有大量参数需要调整时,有时很难承诺并开始。这是不幸的,因为在训练神经网络时,有几种方法可以确定一旦训练和测试了初始架构后需要改进的内容。实际上,将数据集分成三个子集的整个目的是允许用一个集合训练数据集,用另一个集合测量和微调模型,并最终用一个之前未使用过的最终子集测量最终模型的性能。
考虑到所有这些,将解释以下一套惯例和经验法则,以帮助决策过程,定义人工神经网络的初始架构:
-
输入层:这很简单 - 只有一个输入层,其单元数取决于训练数据的形状。具体来说,输入层中的单元数应该等于输入数据包含的特征数。
-
隐藏层:隐藏层的数量可以不同。人工神经网络可以有一个、多个或者没有隐藏层。要选择合适的数量,重要的是考虑以下几点:
数据问题越简单,需要的隐藏层就越少。请记住,可以线性可分的数据问题应该只有一个隐藏层。另一方面,随着深度学习的进步,现在可以使用许多隐藏层(没有限制)来解决非常复杂的数据问题。
要开始的隐藏单元数应该在输入层单元数和输出层单元数之间。
-
输出层:同样,任何人工神经网络只有一个输出层。它包含的单元数取决于要开发的学习任务以及数据问题。如前所述,对于回归任务,只会有一个单元,即预测值。另一方面,对于分类问题,单元数应等于可用的类标签数,考虑到模型的输出应该是一组特征属于每个类标签的概率。
-
其他参数:传统上,应该将其他参数保留为网络的第一个配置的默认值。这主要是因为在考虑可能表现同样良好或更差但需要更多资源的更复杂近似方法之前,测试数据问题上最简单的模型是一种良好的实践。
一旦定义了初始架构,就是训练和评估模型性能的时候了,以便进行进一步的分析,这很可能会导致网络架构或其他参数值的更改,例如学习率的更改或添加正则化项。
PyTorch 自定义模块
PyTorch 的开发团队创建了自定义模块,以允许用户更进一步地灵活性。与前几章探讨的 Sequential
容器相反,只要希望构建更复杂的模型架构或者希望在每一层的计算中具有更多控制权,就应该使用自定义模块。
尽管如此,这并不意味着自定义模块方法只能在这种情况下使用。相反,一旦学会同时使用两种方法,选择在较不复杂的架构中使用哪种方法就成为一种偏好问题。
例如,以下代码片段展示了使用 Sequential
容器定义的两层神经网络:
import torch.nn as nn
model = nn.Sequential(nn.Linear(D_i, D_h),
nn.ReLU(),
nn.Linear(D_h, D_o),
nn.Softmax())
这里,D_i
指的是输入维度(输入数据中的特征),D_h
指的是隐藏层的节点数(隐藏维度),D_o
指的是输出维度。
使用自定义模块,可以构建一个等效的网络架构,如下所示:
import torch
from torch import nn, optim
import torch.nn.functional as F
class Classifier(nn.Module):
def __init__(self, input_size):
super().__init__()
self.hidden_1 = nn.Linear(input_size, 100)
self.hidden_2 = nn.Linear(100, 100)
self.hidden_3 = nn.Linear(100, 50)
self.hidden_4 = nn.Linear(50,50)
self.output = nn.Linear(50, 2)
self.dropout = nn.Dropout(p=0.1)
#self.dropout_2 = nn.Dropout(p=0.1)
def forward(self, x):
z = self.dropout(F.relu(self.hidden_1(x)))
z = self.dropout(F.relu(self.hidden_2(z)))
z = self.dropout(F.relu(self.hidden_3(z)))
z = self.dropout(F.relu(self.hidden_4(z)))
out = F.log_softmax(self.output(z), dim=1)
return out
需要提及的是,交叉熵损失函数要求网络输出是原始的(在通过 softmax 激活函数获得概率之前),这就是为什么在没有激活函数的情况下找到用于分类问题的神经网络架构是常见的。此外,在采用这种方法后,为了得到预测结果,需要在训练后将 softmax 激活函数应用于网络输出。
处理此限制的另一种方法是在输出层使用log_softmax
激活函数。接下来,损失函数被定义为负对数似然损失(nn.NLLLoss
)。最后,可以通过从网络输出中取指数来获取属于每个类标签的实际概率。这是本章活动中将要使用的方法。
一旦模型架构被定义,接下来的步骤将是编写负责在训练数据上训练模型并测量其在训练和验证集上性能的部分代码。
这里,将给出按步骤编码我们讨论过的内容的说明:
model = Classifier()
criterion = nn.NLLLoss()
optimizer = optim.Adam(model.parameters(), lr=0.005)
epochs = 10
batch_size = 100
如可见,第一步是定义在网络训练期间将使用的所有变量。
注意
提醒一下,“epochs
”指的是整个数据集通过网络结构前后传递的次数。“batch_size
”是单个批次(数据集的一个片段)中的训练样本数。最后,“iterations”指的是完成一个 epoch 所需的批次数。
接下来,首先使用一个for
循环遍历之前定义的 epoch 次数。随后,使用一个新的for
循环遍历总数据集的每个批次,直到一个 epoch 完成。在这个循环内,发生以下计算:
-
模型在训练集的一个批次上进行训练。这里得到一个预测。
-
通过比较上一步的预测和训练集(地面真实)的标签来计算损失。
-
梯度被归零,并且针对当前步骤再次计算。
-
根据梯度更新网络的参数。
-
在训练数据上计算模型的准确率如下:
-
获取模型预测的指数,以获得给定数据属于每个类标签的概率。
-
使用
topk()
方法获取具有较高概率的类标签。 -
使用 scikit-learn 的指标部分计算准确率、精确率或召回率。您还可以探索其他性能指标。
-
-
关闭梯度计算,以验证当前模型在验证数据上的表现,具体操作如下:
-
模型对验证集中的数据进行预测。
-
通过比较前一个预测和验证集标签来计算损失函数。
-
要计算模型在验证集上的准确率,使用与在训练数据上进行相同计算的步骤:
train_losses, dev_losses, train_acc, dev_acc= [], [], [], [] for e in range(epochs): X, y = shuffle(X_train, y_train) running_loss = 0 running_acc = 0 iterations = 0 for i in range(0, len(X), batch_size): iterations += 1 b = i + batch_size X_batch = torch.tensor(X.iloc[i:b,:].values).float() y_batch = torch.tensor(y.iloc[i:b].values) pred = model(X_batch) loss = criterion(pred, y_batch) optimizer.zero_grad() loss.backward() optimizer.step() running_loss += loss.item() ps = torch.exp(pred) top_p, top_class = ps.topk(1, dim=1) running_acc += accuracy_score(y_batch, top_class) dev_loss = 0 acc = 0 with torch.no_grad(): pred_dev = model(X_dev_torch) dev_loss = criterion(pred_dev, y_dev_torch) ps_dev = torch.exp(pred_dev) top_p, top_class_dev = ps_dev.topk(1, dim=1) acc = accuracy_score(y_dev_torch, top_class_dev) train_losses.append(running_loss/iterations) dev_losses.append(dev_loss) train_acc.append(running_acc/iterations) dev_acc.append(acc) print("Epoch: {}/{}.. ".format(e+1, epochs), "Training Loss: {:.3f}.. ".format(running_loss/iterations), "Validation Loss: {:.3f}.. ".format(dev_loss), "Training Accuracy: {:.3f}.. ".format(running_acc/iterations), "Validation Accuracy: {:.3f}".format(acc))
-
上述代码片段将打印出训练集和验证集数据的损失和准确率。
活动 4:构建人工神经网络
为了此活动,使用先前准备好的数据集,我们将构建一个能够确定客户是否会违约下一个付款的四层模型。为此,我们将使用自定义模块的方法。
让我们看看以下场景:您在一家专门为全球各地的银行提供机器/深度学习解决方案的数据科学精品公司工作。他们最近接受了一个银行的项目,希望预测下个月不会收到的付款。探索性数据分析团队已经为您准备好了数据集,并要求您构建模型并计算模型的准确性:
-
导入以下库。
import pandas as pd import numpy as np from sklearn.model_selection import train_test_split from sklearn.utils import shuffle from sklearn.metrics import accuracy_score import torch from torch import nn, optim import torch.nn.functional as F import matplotlib.pyplot as plt
注意
即使使用了种子,由于每个 epoch 之前训练集都会被洗牌,因此这些活动的确切结果也无法再现。
-
读取先前准备好的数据集,该数据集应命名为
dccc_prepared.csv
。 -
将特征和目标分开。
-
使用 scikit-learn 的
train_test_split
函数,将数据集分割为训练集、验证集和测试集。使用 60/20/20%的分割比例。将random_state
设置为 0。 -
将验证集和测试集转换为张量,考虑到特征矩阵应为 float 类型,而目标矩阵则不应为 float 类型。目前保持训练集未转换,因为它们将经历进一步的转换。
-
构建一个自定义模块类来定义网络的各层。包括一个 forward 函数,指定将应用于每个层输出的激活函数。在所有层中使用 ReLU,除了输出层,其中应使用
log_softmax
。 -
定义训练模型所需的所有变量。将 epoch 数设置为 50,批量大小设置为 128。使用学习率 0.001。
-
使用训练集数据训练网络。使用验证集来衡量性能。因此,在每个 epoch 中保存训练集和验证集的损失和准确性。
注意
训练过程可能需要几分钟,具体取决于您的资源。添加打印语句是查看训练进程的良好实践。
-
绘制两个数据集的损失。
-
绘制两个数据集的准确性。
注意
此活动的解决方案可以在第 192 页找到。
处理欠拟合或过拟合模型
建立深度学习解决方案不仅仅是定义架构然后使用输入数据训练模型的问题;相反,大多数人认为那只是简单部分。创建高科技模型的艺术在于实现超过人类性能的高准确性水平。鉴于此,本节将介绍错误分析的主题,该主题通常用于诊断已训练模型,以发现哪些操作更有可能对模型的性能产生积极影响。
错误分析。
误差分析顾名思义是对训练和验证数据集的错误率进行初步分析。然后使用这个分析来确定改进模型性能的最佳方案。
为了执行误差分析,需要确定贝叶斯误差,也称为不可约误差,这是可达到的最小误差。几十年前,贝叶斯误差等同于人类误差,这意味着那时专家可以达到的最低误差水平。
如今,随着技术和算法的改进,估计这个值变得越来越困难,因为机器能够超越人类的表现,但我们无法衡量它们相比于人类能做得更好多少,因为我们只能理解到我们的能力所及。
通常将贝叶斯误差初步设置为人类误差,以执行误差分析。然而,这种限制并不是一成不变的,研究人员知道,超越人类表现也应该是一个最终目标。
执行误差分析的过程如下所示:
-
计算选择的指标来衡量模型的表现。这个度量应该在训练集和验证集上都计算。
-
使用这个度量,通过从 1 中减去先前计算的性能指标来计算每个集合的错误率。例如,使用以下方程式:
图 3.8:计算模型在训练集上错误率的方程式
-
从训练集误差(A)中减去贝叶斯误差。保存这个差值,将用于进一步分析。
-
从验证集错误(B)中减去训练集错误,并保存差值的值。
-
取步骤 3 和 4 中计算的差异,并使用以下一组规则:
-
如果步骤 3 中计算的差异高于其他差异,则模型欠拟合,也称为高偏差。
-
-
如果步骤 4 中计算的差异高于其他差异,则模型过拟合,也称为高方差:
图 3.9:展示如何执行误差分析的图表
我们解释的规则并不表明模型只能遭受提到的问题之一,而是高差异的那个问题对模型性能影响更大,修复它将更大程度地提高性能。
让我们解释如何处理每个问题:
-
高偏差:欠拟合模型,或者受到高偏差影响的模型,是一种不能理解训练数据的模型,因此,它不能揭示模式并且不能与其他数据集泛化。这意味着该模型在任何数据集上的表现都不佳。
为了减少影响模型的偏差,建议定义更大/更深的网络(增加隐藏层)或增加训练迭代次数。通过增加层数和增加训练时间,网络有更多资源来发现描述训练数据的模式。
-
高方差:一个过拟合的模型或者受到高方差影响的模型,是指模型在泛化训练数据时出现困难;它过于深入学习训练数据的细节,包括异常值。这意味着模型在训练数据上表现过好,但在其他数据集上表现不佳。
通常可以通过向训练集添加更多数据或在损失函数中添加正则化项来处理这种情况。第一种方法旨在强制网络泛化到数据,而不是理解少量示例的细节。另一种方法则通过惩罚具有更高权重的输入来忽略异常值,并平等考虑所有值。
考虑到这一点,处理影响模型的一个条件可能会导致另一个条件的出现或增加。例如,一个受到高偏差影响的模型,在处理后可能会改善其在训练数据上的表现,但在验证数据上却没有改善,这意味着模型开始受到高方差的影响,并需要采取另一组补救措施。
一旦对模型进行了诊断并采取了必要的措施来提高性能,应选择最佳模型进行最终测试。每个模型都应用于对测试集的预测(这是唯一不会影响模型构建的数据集)。
考虑到这一点,可以选择在测试数据上表现最佳的模型作为最终模型。这主要是因为在测试数据上的表现作为模型在未来未见数据集上性能的指标,这是最终目标。
练习 7:执行误差分析
使用前一活动计算的准确度指标,在本活动中我们将执行错误分析,帮助我们确定下一活动中需要执行的行动:
注意
此活动无需编码,而是要分析前一活动的结果。
-
假设贝叶斯错误率为 0.15,执行错误分析并诊断模型:
Bayes error (BE) = 0.15 Training set error (TSE) = 1 – 0.715 = 0.285 Validation set error (VSE) = 1 – 0.706 = 0.294
作为两个集合准确性的值(0.715 和 0.706),它们是在前一活动的最后迭代中获得的。
Bias = TSE – BE = 0.135 Variance = VSE – TSE = 0.009
根据此,模型存在高偏差问题,意味着模型欠拟合。
-
确定要遵循以提高模型准确性的行动方案。
为了提高模型的性能,可以采取以下两种行动方案:增加迭代次数并增加隐藏层和/或单元数。
根据此,可以进行一系列测试,以达到最佳结果。
恭喜!您已成功进行了错误分析。
活动 5:改善模型性能
对于接下来的活动,我们将实施在练习中定义的操作,以减少影响模型性能的高偏差。让我们看一下以下情景:在将模型交付给您的队友后,他们对您的工作以及您组织代码的方式印象深刻(做得好!),但他们要求您尝试将性能提高到 80%,考虑到这是他们向客户承诺的。
注意
使用不同的笔记本进行此活动。在那里,您将再次加载数据集,并执行与上一个活动类似的步骤,不同之处在于训练过程将多次进行,以训练不同的架构和训练时间。
-
导入与上一个活动相同的库。
-
加载数据并将特征与目标分离。然后,将数据分成三个子集(训练、验证和测试),使用 60:20:20 的分割比例。最后,将验证和测试集转换为 PyTorch 张量,就像您在上一个活动中所做的那样。
-
考虑到模型正在遭受高偏差,重点应放在增加 epochs 的数量或通过添加额外的层或单位来增加网络的大小。目标应该是将测试集上的准确性近似到 80%。
注意
考虑到选择先进行哪项测试没有正确的方法,因此要有创造性和分析能力。如果模型架构的更改减少或消除了高偏差,但引入了高方差,则考虑保留这些更改,同时添加一些措施来对抗高方差。
-
绘制两组数据的损失和准确率。
-
使用表现最佳的模型,对测试集进行预测(在微调过程中不应使用)。通过计算模型在此集合上的准确性,将预测与实际情况进行比较。
注意
可在第 196 页找到此活动的解决方案。
部署您的模型
到目前为止,已经讨论并实践了用于常规回归和分类问题构建出色深度学习模型的关键概念和技巧。在现实生活中,模型不仅仅是为了学习目的而构建的。相反,当为除研究目的以外的目的训练模型时,主要思想是能够在未来重复使用它们,以对新数据执行预测,即使该模型未经训练,也应该表现出类似的良好性能。
在小型组织中,序列化和反序列化模型的能力就足够了。然而,当模型需要被大型企业、用户使用或用于改变重要且大型任务时,将模型转换为能在大多数生产环境中使用的格式,如 API、网站、在线和离线应用等,会是更好的做法。
根据此文,本节中我们将学习如何保存和加载模型,以及如何使用 PyTorch 的最新功能将您的模型转换为高度通用的 C++ 应用程序。
保存和加载您的模型
您可能想象,每次使用模型都重新训练是非常不实际的,尤其是考虑到大多数深度学习模型可能需要相当长的时间来训练(根据您的资源)。
相反,PyTorch 中的模型可以被训练、保存和重新加载,以进行进一步的训练或推断。这可以通过保存每个 PyTorch 模型层的参数(权重和偏置)到 state_dict
字典中来实现。
这里提供了关于如何保存和加载训练过的模型的逐步指南:
-
最初,模型的检查点仅包括模型的参数。然而,在加载模型时,不仅需要这些信息,还可能需要保存其他信息,例如输入单元的数量,这取决于分类器接受的参数。因此,第一步是定义要保存的信息:
checkpoint = {"input": X_train.shape[1], "state_dict": model.state_dict()}
当加载模型时,将保存输入层中的单位数到检查点中将非常有用。
-
使用您选择的文本编辑器创建一个 Python 文件,导入 PyTorch 库,并包含创建模型网络架构的类。这样做是为了能够方便地将模型加载到不同于训练模型所用的工作表中。
-
使用 PyTorch 的
save()
函数保存模型:torch.save(checkpoint, "checkpoint.pth")
第一个参数是先前创建的字典,第二个参数是要使用的文件名。
-
要加载模型,让我们创建一个执行三个主要操作的函数:
def load_model_checkpoint(path): checkpoint = torch.load(path) model = final_model.Classifier(checkpoint["input"], checkpoint["output"], checkpoint["hidden"]) model.load_state_dict(checkpoint["state_dict"]) return model model = load_model_checkpoint("checkpoint.pth")
函数接收保存的模型文件路径作为输入。首先加载检查点,接着使用保存在 Python 文件中的网络架构初始化模型。这里,
final_model
是 Python 文件的名称,应该已经被导入到新的工作表中,而Classifier()
是保存在该文件中的类的名称。此模型将具有随机初始化的参数。最后,将检查点中的参数加载到模型中。调用时,该函数返回经过训练的模型,现在可以用于进一步训练或进行推断。
PyTorch 用于 C++ 生产环境
根据框架名称,PyTorch 的主要接口是 Python 编程语言。这主要是因为许多用户偏爱这种编程语言,因为它的动态性和用于开发机器学习解决方案的易用性。
然而,在某些情况下,Python 的特性变得不利。这正是为生产环境开发的场景,其他编程语言被证明更有用的情况。例如,C++广泛用于机器/深度学习解决方案的生产目的。
鉴于此,PyTorch 最近提出了一个简单的方法,允许用户享受两个世界的好处。虽然他们继续以 Python 方式编程,但现在有可能将模型序列化为可以在 C++中加载和执行的表示形式,不依赖于 Python。
将 PyTorch 模型转换为 Torch Script 是通过 PyTorch 的 JIT(即时编译)模块完成的。通过将你的模型以及示例输入传递给torch.jit.trace()
函数来实现,如下所示:
traced_script = torch.jit.trace(model, example)
这将返回一个脚本模块,可以像常规的 PyTorch 模块一样使用,如下所示:
prediction = traced_script(input)
上述操作将返回通过模型运行输入数据得到的输出。
活动 6:利用你的模型
对于这个活动,保存在前一活动中创建的模型。此外,保存的模型将加载到一个新的笔记本中供使用。我们将把模型转换为一个序列化的表示形式,可以在 C++上执行。让我们看一下以下的情景:哇!大家都对你改进模型的承诺以及最终版本感到非常满意,因此他们要求你保存模型,并将其转换为可以用于客户端构建在线应用程序的格式。
注意
这个活动将使用两个 Jupyter 笔记本。首先,我们将使用与前一活动相同的笔记本保存最终模型。接下来,我们将打开一个新的笔记本,用于加载保存的模型。
-
打开你用于前一活动的 Jupyter 笔记本。
-
保存一个包含定义你的最佳性能模块架构的类的 Python 文件。确保导入 PyTorch 所需的库和模块。命名为
final_model.py
。 -
保存表现最佳的模型。确保保存每层单元的信息以及模型的参数。命名为
checkpoint.pth
。 -
打开一个新的 Jupyter 笔记本。
-
导入 PyTorch,以及之前创建的 Python 文件。
-
创建一个加载模型的函数。
-
通过将以下张量输入到你的模型中进行预测。
torch.tensor([[0.0606, 0.5000, 0.3333, 0.4828, 0.4000, 0.4000, 0.4000, 0.4000, 0.4000, 0.4000, 0.1651, 0.0869, 0.0980, 0.1825, 0.1054, 0.2807, 0.0016, 0.0000, 0.0033, 0.0027, 0.0031, 0.0021]]).float()
-
使用 JIT 模块转换模型。
-
通过输入相同的张量到你的模型的追踪脚本中执行预测。
注意
此活动的解决方案可在 202 页找到。
总结
在前几章涵盖了大部分理论知识之后,本章通过一个真实案例研究来巩固我们的知识。其想法是通过实践和动手操作来鼓励学习。
本章首先解释了深度学习在需要精确度的广泛行业中的影响。推动深度学习增长的主要行业之一是银行和金融,这些算法在诸如评估贷款申请、欺诈检测以及评估过去决策以预见未来行为等领域中得到应用,主要是由于这些算法在这些方面能够超越人类的表现。
本章使用了一个来自亚洲银行的真实数据集,目标是预测客户是否会违约。本章从解决方案的开发开始,通过解释定义数据问题的什么、为什么和如何的重要性,以及分析手头的数据来最大程度地利用它。
一旦数据根据问题定义准备好,本章探讨了定义“好”架构的想法。在这个主题中,即使有几个经验法则可以考虑,主要的要点是不要过度思考,构建一个初始架构以获得一些可以用于执行错误分析以改进模型性能的结果。
错误分析的概念包括分析模型在训练集和验证集上的错误率,以确定模型是否在更大比例上受到高偏差或高方差的影响。然后利用模型的这一诊断来修改模型的架构和一些学习参数,从而提高性能。
最后,本章探讨了两种利用表现最佳模型的主要方法。第一种方法是保存模型,然后将其重新加载到任何编码平台以继续训练或执行推断。另一种方法主要用于将模型投入生产,并通过使用 PyTorch 的 JIT 模块来实现,该模块创建了一个可以在 C++上运行的模型的序列化表示。
下一章中,我们将专注于使用深度神经网络解决简单的分类任务。
第四章:卷积神经网络
学习目标
在本章结束时,您将能够:
-
解释卷积神经网络(CNN)的训练过程
-
执行数据增强
-
对 CNN 应用批归一化
-
使用 CNN 解决图像分类问题
在本章中,您将被介绍 CNN。您将学习卷积、池化、填充和步幅等概念。
引言
尽管当前神经网络领域都很受欢迎,但 CNN 可能是所有神经网络架构中最受欢迎的。这主要是因为尽管它们在许多领域都适用,但它们在处理图像时特别出色,技术的进步使得收集大量图像来解决当今各种挑战成为可能。
从图像分类到对象检测,CNN 被用于诊断癌症患者、检测系统中的欺诈行为,以及构建深思熟虑的自动驾驶车辆,将彻底改变未来。
本章将重点解释卷积神经网络(CNN)在处理图像时优于其他架构的原因,以及更详细地解释它们的架构构建模块。它将涵盖构建 CNN 解决图像分类数据问题的主要编码结构。
此外,本章将探讨数据增强和批归一化的概念,这些将用于改善模型的性能。本章的最终目标是比较使用 CNN 解决图像分类问题的三种不同方法的结果。
注意
作为提醒,本章中使用的所有代码的 GitHub 仓库可以在github.com/TrainingByPackt/Applied-Deep-Learning-with-PyTorch
找到。
构建 CNN
众所周知,处理图像数据问题时 CNN 是首选。然而,它们通常被低估,因为它们通常只被认为适用于图像分类,而事实上它们在处理图像方面的能力扩展到了更多领域。本章不仅将解释 CNN 在理解图像方面的优势,还将识别可以处理的不同任务,并给出一些现实生活中应用的示例。
此外,本章将探索 CNN 的不同构建模块及其在 PyTorch 中的应用,最终构建一个使用 PyTorch 图像分类数据集解决数据问题的模型。
为什么选择 CNN?
图像是像素矩阵,因此你可能会问,为什么我们不将矩阵展平成向量,然后使用传统的神经网络架构进行处理呢?答案是,即使是最简单的图像,也存在一些像素依赖关系会改变图像的含义。例如,猫眼的表现、车轮的轮胎,甚至是物体的边缘都是由几个以特定方式布置的像素构成的。展平图像会导致这些依赖关系丢失,传统模型的准确性也会因此丧失:
图 4.1:展平矩阵的表示
另一方面,CNN 能够捕捉图像的空间依赖关系,因为它将图像处理为矩阵并一次性分析整个图像的区块,这取决于滤波器的大小。例如,使用大小为 3x3 的卷积层将一次性分析 9 个像素,直到覆盖整个图像。
图像的每个区块都被赋予一组参数(权重和偏差),这些参数将根据手头的滤波器确定该像素区块与整个图像的相关性。这意味着垂直边缘滤波器将赋予包含垂直边缘的图像区块更大的权重。因此,通过减少参数数量并分析图像的区块,CNN 能够更好地呈现图像的表现。
输入
正如前面提到的,CNN 的典型输入是以矩阵形式表示的图像。矩阵中的每个值代表图像中的一个像素,其数值由颜色的强度确定,取值范围从 0 到 255。
在灰度图像中,白色像素由数字 255 表示,黑色像素由数字 0 表示。灰色像素是介于两者之间的任意数字,取决于颜色的强度;灰色越浅,数字越接近 255。
彩色图像通常使用 RGB 系统表示,其中每种颜色表示为红、绿和蓝的组合。每个像素将具有三个维度,每个颜色一个维度。每个维度的值范围从 0 到 255。颜色越浓,数字越接近 255。
根据前面的段落,给定图像的矩阵是三维的,其中第一维度表示图像的高度(以像素数表示),第二维度表示图像的宽度(以像素数表示),第三维度称为通道,表示图像的颜色方案。
彩色图像的通道数为三(RGB 系统中每种颜色一个通道)。而灰度图像只有一个通道:
图 4.2:图像的矩阵表示。左边是彩色图像,右边是灰度图像。
与文本数据不同,输入到 CNN 中的图像不需要太多预处理。 图像通常按原样输入,唯一的变化是将值标准化以加快学习过程并提高性能,并且作为良好实践,可以将图像缩小,考虑到 CNN 模型通常是使用较小的图像构建的,这也有助于加快学习过程。
规范化输入的最简单方法是取每个像素的值并将其除以 255,最终得到在 0 到 1 之间的值范围。 然而,有不同的规范化图像的方法,例如均值中心化技术。 在选择使用其中一种方法时,通常是个人偏好的问题; 但是,当使用预训练模型时,强烈建议使用第一次训练模型时使用的相同技术,这些信息始终包含在预训练模型的文档中。
CNN 的应用
尽管 CNN 主要用于计算机视觉问题,但重要的是提到它们解决其他学习问题的能力,主要是关于分析数据序列。 例如,CNN 已知在文本、音频和视频序列上表现良好,有时结合其他网络架构使用,或通过将序列转换为图像以供 CNN 处理。 可以使用 CNN 处理数据序列的一些特定问题包括文本的机器翻译、自然语言处理和视频帧标记等。
此外,CNN 可以执行适用于所有监督学习问题的不同任务。 然而,从现在开始,本章将集中在计算机视觉上。 以下是每个任务的简要解释,以及每个任务的一个现实示例:
分类:这是计算机视觉中最常见的任务。 主要思想是将图像的一般内容分类为一组标签。
例如,分类可以确定图像是狗、猫还是任何其他动物。 此分类通过输出图像属于每个类的概率来完成,如下图所示:
图 4.3: 分类任务
定位:主要目的是生成描述图像中物体位置的边界框。 输出包括一个类标签和一个边界框。
它可以在传感器中使用,以确定对象是在屏幕的左侧还是右侧:
图 4.4: 定位任务
检测:此任务包括在图像中对所有对象执行对象定位。 输出包括多个边界框以及多个类标签(每个框一个)。
它被用于自动驾驶汽车的建设中,旨在能够定位交通标志、道路、其他车辆、行人和任何可能影响安全驾驶的对象:
图 4.5:检测任务
分割:这里的任务是输出每个对象的类别标签和轮廓。主要用于标记图像中的重要对象,以进行进一步分析。
例如,它可以严格地限定在患者整个肺部图像中对应肿瘤的区域:
图 4.6:分割任务
从这一节开始,本章将重点讲述如何训练模型来执行图像分类,使用 PyTorch 的图像数据集之一。
CNN 的基本组成部分
如前所述,深度卷积网络将图像作为输入,经过一系列卷积层、池化层和全连接层,最终应用 softmax
激活函数对图像进行分类。与人工神经网络一样,分类通过计算图像属于每个类别的概率来进行,给每个类别标签赋予介于零和一之间的值。概率较高的类别标签被选择为该图像的最终预测。
下面详细解释了每个发现的层,以及如何在 PyTorch 中定义这些层的编码示例:
卷积层
这是从图像中提取特征的第一步。其目标是通过学习图像的小部分来保持附近像素之间的关系。
在这一层进行数学运算,输入两个(图像和滤波器),得到一个输出。如前所述,该操作包括对滤波器和与滤波器大小相同的图像部分进行卷积。这个操作对图像的所有子部分都会重复进行。
注
回顾第二章,神经网络的基本组成部分,标题为卷积神经网络介绍,以便回忆输入与滤波器之间的确切计算。
结果矩阵的形状取决于输入的形状,其中图像矩阵的大小为 (h x w x c),滤波器的大小为 (fh x fw x c),将根据以下方程输出矩阵:
方程式 4.7:卷积层的输出高度、宽度和深度
在这里,h 是输入图像的高度,w 是宽度,c 是深度(也称为通道数),fh 和 fw 是用户根据滤波器大小设定的值。
图 4.8:输入、滤波器和输出的尺寸
需要强调的是,在单个卷积层中,可以对同一图像应用多个相同形状的滤波器。考虑到这一点,对于将两个滤波器应用于其输入的卷积层而言,其输出形状的深度是两个,正如以下图所示:
图 4.9:具有两个滤波器的卷积层
这些滤波器中的每一个将执行不同的操作,以便从图像中发现不同的特征。例如,在具有两个滤波器的单个卷积层中,操作可能包括垂直边缘检测和水平边缘检测。此外,随着网络在层数上的增长,滤波器将执行更复杂的操作,利用先前检测到的特征,例如使用边缘检测器的输入来检测人物轮廓的操作。
此外,滤波器通常会在每一层中增加。这意味着,虽然第一个卷积层有八个滤波器,但通常会创建第二个卷积层,其滤波器数量是前者的两倍(16),依此类推。
然而,需要提到的是,在 PyTorch 中,如同许多其他框架一样,你只需定义要使用的滤波器数量,而无需指定滤波器的类型(例如,垂直边缘检测器)。每个滤波器配置(用于检测特定特征的数字)是系统变量的一部分。
关于卷积层,还有两个额外的概念需要介绍,将在接下来进行解释:
填充:
填充功能正如其名称所示,用零填充图像。这意味着在图像的每一侧添加额外的像素,并用零填充。
下图显示了一个图像示例,其每一侧都添加了一个填充像素:
图 4.10:图像输入在一侧填充一个图形表示
这被用来在通过滤波器后保持输入矩阵的形状。这是因为,特别是在前几层,目标应该是尽可能保留原始输入中的信息,以便从中提取最多的特征。
为了更好地理解填充的概念,请考虑以下情景:
对于形状为 32 x 32 x 3 的彩色图像应用一个 3 x 3 的滤波器将得到一个形状为 30 x 30 x 1 的矩阵。这意味着输入到下一层的图像尺寸已经减小。另一方面,通过在输入图像上添加填充值为 1,输入的形状则变为 34 x 34 x 3,使用同样的滤波器将得到一个输出尺寸为 32 x 32 x 1 的矩阵。
当使用填充时,可以使用以下方程式计算输出宽度:
图 4.11:使用填充后卷积层的输出宽度
这里,W 是输入矩阵的宽度,F 是滤波器的宽度,P 是填充。同样的方程可以适应计算输出的高度。
要获得与输入矩阵相同形状的输出矩阵,请使用以下方程来计算填充值(考虑步幅等于一):
图 4.12:获得与输入矩阵相同大小的输出矩阵所需的填充数
请记住,输出通道(深度)的数量始终等于应用于输入的滤波器数。
步幅:
此参数是指滤波器在输入矩阵上水平和垂直移动的像素数。正如我们迄今所见,滤波器从图像的左上角通过,然后向右移动一个像素,依此类推,直到垂直和水平地通过图像的所有部分。这个例子是步幅为一的卷积层,默认配置为此参数。
当步幅为二时,移动将为两个像素,如下图所示:
图 4.13:步幅为二的卷积层的图形表示
可以看到,初始操作发生在左上角,然后向右移动两个像素,第二次计算发生在右上角。接下来,计算向下移动两个像素,以在左下角执行计算,最后再次向右移动两个像素,最终计算发生在右下角。
注意
图 4.13 中的数字是虚构的,不是实际计算。重点应放在解释步幅为二时的移动过程的方框上。
当使用步幅时,可以使用以下方程来计算输出宽度:
方程 4.14:使用步幅进行卷积层后的输出宽度
这里,W 是输入矩阵的宽度,F 是滤波器的宽度,S 是步幅。同样的方程可以适应计算输出的高度。
一旦引入这些参数,计算来自卷积层的矩阵输出形状(宽度和高度)的最终方程如下:
方程 4.15:使用填充和步幅后的卷积层的输出宽度
每当数值为浮点数时,应向下取整。这基本上意味着输入的某些区域被忽略,并且不会从中提取任何特征。
最终,一旦输入通过所有的过滤器,输出将被送入激活函数中,以打破线性,类似于传统神经网络的过程。虽然在这一步骤中有几种激活函数可供使用,但首选的是 ReLU 函数,因为它在 CNN 中表现出色。在此处获得的输出成为下一层的输入,通常是一个池化层。
练习 8:计算卷积层的输出形状
考虑给定的方程式,考虑以下情景并计算输出矩阵的形状。
注释
这个练习不需要编码,而是由先前提到的概念练习组成。
-
输入形状为 64 x 64 x 3。形状为 3 x 3 x 3 的过滤器:
Output height = 64 -3 + 1 = 62 Output width = 64 - 3 + 1 = 62 Output depth = 1
-
输入形状为 32 x 32 x 3。5 个形状为 5 x 5 x 3 的过滤器。填充为 2:
Output height = 32 - 5 + (2 * 2) + 1 = 32 Output width = 32-5 + (2 * 2) + 1 = 32 Output depth = 10
-
输入形状为 128 x 128 x 1。5 个形状为 5 x 5 x 1 的过滤器。步长为 3:
Output height = (128 - 5)/ 3 + 1 = 42 Output width = (128 - 5)/ 3 + 1 = 42 Output depth = 5
-
输入形状为 64 x 64 x 1。形状为 8 x 8 x 1 的过滤器。填充为 3,步长为 3:
Output height = ((64 - 8 + (2 * 3)) / 3) +1 = 21.6 ≈ 21 Output width = ((64 - 8 + (2 * 3)) / 3) +1 = 21.6 ≈ 21 Output depth = 1
恭喜!您成功地计算出源自卷积层的矩阵输出形状。
在 PyTorch 中编写卷积层非常简单。使用自定义模块,只需创建network
类,其中包含网络的各层,以及一个forward
函数,定义通过先前定义的不同层传递信息的步骤,如下面的代码片段所示:
import torch.nn as nn
import torch.nn.functional as F
class CNN_network(nn.Module):
def __init__(self):
super(CNN_network, self).__init__()
# input channels = 3, output channels = 18,
# filter size = 3, stride = 1 and padding = 1
self.conv1 = nn.Conv2d(3, 18, 3, 1, 1)
def forward(self, x):
x = F.relu(self.conv1(x))
return x
在定义卷积层时,从左到右传递的参数是指输入通道数,输出通道数(过滤器数量),核大小(滤波器大小),步长和填充。
考虑到这一点,前述的例子由具有三个输入通道,18 个大小为 3 的过滤器组成,步长和填充均为 1 的卷积层组成。
另一种有效的方法,相当于前面的例子,包括来自自定义模块的语法组合和使用Sequential
容器,如下面的代码片段所示:
import torch.nn as nn
class CNN_network(nn.Module):
def __init__(self):
super(CNN_network, self).__init__()
self.conv1 = nn.Sequential(nn.Conv2d(1, 16, 5, 1, 2,), nn.ReLU())
def forward(self, x):
x = self.conv1(x)
return x
在这里,层的定义发生在Sequential
容器内。通常,一个容器会包含一个卷积层,一个激活函数和一个池化层。在下面的不同容器中将包含一组新的层。
池化层
按照惯例,池化层是特征选择步骤的最后部分,这就是为什么池化层大多数情况下可以在卷积层之后找到的原因。正如在前面的章节中解释的那样,其思想是从图像的子部分中提取最相关的信息。池化层的大小通常为 2,步长等于其大小。
根据前文,池化层通常通过减半输入的高度和宽度来实现。这很重要,考虑到为了让卷积层在图像中找到所有特征,需要使用多个滤波器,并且该操作的输出可能会变得过大,这意味着有很多参数需要考虑。池化层旨在通过保留最相关的特征来减少网络中的参数数量。在图像的子区域中选择相关特征的方法可以是获取最大数量或对该区域中的数字进行平均。
对于图像分类任务,最常用的是最大池化层,而不是平均池化层。这是因为前者在保留最相关特征的任务中表现更好,而后者在平滑图像等任务中被证明更有效。
可以使用以下方程式来计算输出矩阵的形状:
方程式 4.16:池化层后的输出矩阵宽度
在这里,W 指的是输入的宽度,F 指的是滤波器的大小,S 指的是步长。可以将相同的方程式用于计算输出的高度
输入的通道或深度保持不变,因为池化层将在图像的所有通道上执行相同的操作。这意味着池化层的结果仅影响输入的宽度和长度。
练习 9:计算一组卷积和池化层的输出形状
下面的练习将结合卷积层和池化层。目标是确定经过一组层后的输出矩阵的大小。
注意
这个活动不需要编码,而是基于之前提到的概念进行的实践练习。
考虑以下一组层,并指定在所有变换结束时输出层的形状:
-
输入图像大小为 256 x 256 x 3。
-
具有 16 个大小为三的滤波器、步长和填充均为一的卷积层。
-
一个池化层,滤波器大小为二,步长大小也为二。
-
具有八个大小为七、步长为一、填充为三的滤波器的卷积层。
-
一个池化层,滤波器大小为二,步长也为二。
下面展示了通过每个层后经过的矩阵的输出大小:
# After the first convolutional layer output_matrix_size = 256 x 256 x 16 # After the first pooling layer output_matrix_size = 128 x 128 x 16 # After the second convolutional layer output_matrix_size = 128 x 128 x 8 # After the second pooling layer output_matrix_size = 64 x 64 x 8
恭喜!您已成功计算出通过一系列卷积和池化层导出的矩阵的输出形状。
使用与之前相同的编码示例,定义池化层的 PyTorch 方式如下代码片段所示:
import torch.nn as nn import torch.nn.functional as F class CNN_network(nn.Module): def __init__(self): super(CNN_network, self).__init__() self.conv1 = nn.Conv2d(3, 18, 3, 1, 1) self.pool1 = nn.MaxPool2d(2, 2) def forward(self, x): x = F.relu(self.conv1(x)) x = self.pool1(x) return x
在这里,进入最大池化层的参数从左到右依次是滤波器的大小和步长。
再次展示了一个同样有效的方法,使用自定义模块和
Sequential
容器:import torch.nn as nn class CNN_network(nn.Module): def __init__(self): super(CNN_network, self).__init__() self.conv1 = nn.Sequential(nn.Conv2d(1, 16, 5, 1, 2,), nn.ReLU(), nn.MaxPool2d(2, 2)) def forward(self, x): x = self.conv1(x) return x
如前所述,池化层也包含在与卷积层和激活函数相同的容器中。后续的一组层(卷积、激活和池化)将在新的Sequential
容器中定义。
全连接层
全连接(FC)层或层在网络架构末端定义,在输入通过一组卷积和池化层之后。从前一个全连接层之前的输出数据被从矩阵扁平化为向量,可以被馈送到全连接层(与传统神经网络中的隐藏层相同)。
这些 FC 层的主要目的是考虑前面层次检测到的所有特征,以便对图像进行分类。
不同的 FC 层通过激活函数传递,通常是 ReLU,除非是最后一层,该层将使用 softmax 函数输出输入属于每个类标签的概率。
第一个全连接层的输入大小对应于前一层的扁平化输出矩阵的大小。输出大小由用户定义,与 ANNs 一样,设置这个数字并没有确切的科学依据。最后一个 FC 层的输出大小应等于类标签的数量。
要在 PyTorch 中定义一组 FC 层,请考虑以下代码片段:
import torch.nn as nn
import torch.nn.functional as F
class CNN_network(nn.Module):
def __init__(self):
super(CNN_network, self).__init__()
self.conv1 = nn.Conv2d(3, 18, 3, 1, 1)
self.pool1 = nn.MaxPool2d(2, 2)
self.linear1 = nn.Linear(32*32*16, 64)
self.linear2 = nn.Linear(64, 10)
def forward(self, x):
x = F.relu(self.conv1(x))
x = self.pool1(x)
x = x.view(-1, 32 * 32 *16)
x = F.relu(self.linear1(x))
x = F.log_softmax(self.linear2(x), dim=1)
return x
在这里,向网络添加了两个全连接层。接下来,在前向函数内部,使用view()
函数将池化层的输出扁平化。然后,它通过第一个 FC 层,该层应用激活函数。最后,数据通过最后一个 FC 层及其激活函数传递。
使用自定义模块和Sequential
容器定义全连接层的代码如下所示:
import torch.nn as nn
class CNN_network(nn.Module):
def __init__(self):
super(CNN_network, self).__init__()
self.conv1 = nn.Sequential(nn.Conv2d(1, 16, 5, 1, 2,), nn.ReLU(),
nn.MaxPool2d(2, 2))
self.linear1 = nn.Linear(32*32*16, 64)
self.linear2 = nn.Linear(64, 10)
def forward(self, x):
x = self.conv1(x)
x = x.view(-1, 32 * 32 *16)
x = F.relu(self.linear1(x))
x = F.log_softmax(self.linear2(x), dim=1)
return x
一旦网络架构被定义,定义不同参数(包括损失函数和优化算法)以及训练过程的后续步骤可以像 ANNs 一样处理。
旁注 - 从 PyTorch 下载数据集
要从 PyTorch 加载数据集,请使用以下代码。除了下载数据集外,它还展示了如何使用数据加载器按批次加载图像以节省资源:
from torchvision import datasets
import torchvision.transforms as transforms
batch_size = 20
transform = transforms.Compose([transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
train_data = datasets.MNIST(root='data', train=True,
download=True, transform=transform)
test_data = datasets.MNIST(root='data', train=False,
download=True, transform=transform)
dev_size = 0.2
idx = list(range(len(train_data)))
np.random.shuffle(idx)
split_size = int(np.floor(dev_size * len(train_data)))
train_idx, dev_idx = idx[split_size:], idx[:split_size]
train_sampler = SubsetRandomSampler(train_idx)
dev_sampler = SubsetRandomSampler(dev_idx)
train_loader = torch.utils.data.DataLoader(train_data, batch_size=batch_size, sampler=train_sampler)
dev_loader = torch.utils.data.DataLoader(train_data, batch_size=batch_size, sampler=dev_sampler)
test_loader = torch.utils.data.DataLoader(test_data, batch_size=batch_size)
在上述代码中,要下载的数据集是 MNIST。这是一个流行的数据集,包含从零到九的手写灰度数字的图像。在下载数据集之前定义的transform
变量负责对数据集执行一些转换。在这种情况下,数据集将被转换为张量并在所有维度上进行归一化。
使用 PyTorch 中的SubsetRandomSampler()
函数,通过随机采样索引将原始训练集分为训练集和验证集。此外,DataLoader()
函数负责按批次加载图像。该函数的结果变量(train_loader
、dev_loader
和test_loader
)将分别包含特征和目标的值。
活动 7:为图像分类问题构建 CNN
注意
问题越复杂,网络越深,模型训练时间越长。考虑到这一点,本章的活动可能比之前的章节需要更长的时间。
在下一个活动中,将在 PyTorch 的图像数据集上训练 CNN。让我们看一下以下情景:
您在一家人工智能公司工作,该公司为客户的需求开发定制模型。您的团队目前正在创建一个能够区分车辆和动物的模型,更具体地说,是一个能够区分不同动物和不同类型车辆的模型。他们为您提供了包含 60,000 张图像的数据集,并希望您构建这样一个模型。
注意
对于本章中的活动,您需要准备 Python 3.6、Jupyter、NumPy 和 Matplotlib。
-
导入以下库:
import numpy as np import torch from torch import nn, optim import torch.nn.functional as F from torchvision import datasets import torchvision.transforms as transforms from torch.utils.data.sampler import SubsetRandomSampler from sklearn.metrics import accuracy_score import matplotlib.pyplot as plt
注意
将使用的数据集是 PyTorch 中的 CIFAR10,其中包含总共 60,000 张车辆和动物的图像。有 10 个不同的类标签。训练集包含 50,000 张图像,测试集包含剩余的 10,000 张。
-
设置对数据执行的转换,即将数据转换为张量并对像素值进行归一化。
-
设置批量大小为 100 张图像,并从 CIFAR10 数据集下载训练和测试数据。
-
使用 20%的验证大小,定义用于将数据集分为这两组的训练和验证采样器。
-
使用
DataLoader()
函数定义每组数据集使用的批次。 -
定义您的网络架构。使用以下信息来完成:
-
Conv1:一个卷积层,以彩色图像作为输入,并通过 10 个大小为 3 的滤波器进行处理。填充和步幅均设置为 1。
-
Conv2:一个卷积层,通过 20 个大小为 3 的滤波器处理输入数据。填充和步幅均设置为 1。
-
Conv3:一个卷积层,通过 40 个大小为三的滤波器处理输入数据。填充和步幅均设置为 1。
-
在每个卷积层后使用 ReLU 激活函数。
-
每个卷积层后使用池化层,滤波器大小和步幅均为 2。
-
在展平图像后设置 20%的丢失率。
-
Linear1:接收来自上一层的展平矩阵作为输入,并生成 100 个单元的输出。对于这一层,使用 ReLU 激活函数。此处的丢弃项设置为 20%。
-
Linear2:生成 10 个输出的全连接层,每个类标签一个。对输出层使用
log_softmax
激活函数。
-
-
定义训练模型所需的所有参数。训练 50 个 epoch。
-
训练您的网络,并确保保存训练和验证集的损失和准确性值。
-
绘制两个集的损失和准确性。
注意
由于每个 epoch 中数据的重新排序,结果将不完全可再现。但是,您应该能够得到类似的结果。
-
检查测试集上的模型准确性。
注意
可在第 204 页找到此活动的解决方案。
数据增强
学习如何有效编写神经网络是开发最先进解决方案的步骤之一。此外,要开发出优秀的深度学习解决方案,还必须找到一个可以提供解决当前挑战的领域(顺便说一句,这并不是一件容易的事情)。但是,一旦所有这些都完成了,我们通常会面临同样的问题:获取一个足够大的数据集以使我们的模型性能良好,无论是通过自我收集还是来自互联网和其他可用来源。
正如您可能想象的那样,即使现在可以收集和存储大量数据,由于相关的成本,这并不是一件容易的任务。因此,大多数情况下,我们都在处理包含数万条记录的数据集,而在涉及图像时,这个数字甚至更少。
在开发计算机视觉问题的解决方案时,这变成一个相关问题,主要有两个原因:
-
数据集越大,结果越好,较大的数据集对于获得足够好的模型至关重要。这是真实的,考虑到训练模型是调整一堆参数的过程,使其能够尽可能接近地图输入和输出之间的关系,并通过最小化损失函数来预测值。在这里,模型越复杂,需要的参数就越多。
考虑到这一点,有必要向模型提供足够数量的示例,以便它能够找到这些模式,其中训练示例的数量应与要调整的参数数量成比例。
-
此外,计算机视觉问题中最大的挑战之一是使您的模型在图像的多种变化上表现良好。这意味着图像不需要按特定对齐方式或具有固定质量进行馈送,而是可以以其原始格式进行馈送,包括不同的位置、角度、光照和其他失真。因此,有必要找到一种方法来将这些变化输入模型。
因此,设计了数据增强技术。简单来说,它是通过轻微修改现有示例来增加训练示例的数量。例如,可以复制当前可用的实例,并对这些副本添加一些噪声,以确保它们并非完全相同。
在计算机视觉问题中,这意味着通过改变现有图像来增加训练数据集中的图像数量,可以通过微调当前图像来创建略有不同的重复版本。
对这些图像的微小调整可以是轻微的旋转、物体在帧内位置的变化、水平或垂直翻转、不同的色彩方案和扭曲等形式。这种技术有效,因为卷积神经网络将认为每个这样的图像是不同的图像。
例如,以下图示展示了一只狗的三张图像,对于人眼来说它们是带有某些变化的同一图像,但对于神经网络来说它们是完全不同的:
图 4.17: 增强图像
能够独立于任何形式的变化识别图像中对象的 CNN 被认为具有不变性属性。事实上,CNN 可以对每种类型的变化都具有不变性。
使用 PyTorch 进行数据增强
使用 torchvision
包在 PyTorch 中执行数据增强非常简单。该包除了包含流行的数据集和模型架构外,还包含常用的图像转换函数来处理数据集。
注意
在本节中,将提及一些这些图像转换。要获取所有可能的转换列表,请访问 pytorch.org/docs/stable/torchvision/transforms.html
.
与前一个活动中用于归一化和将数据集转换为张量的过程相似,执行数据增强需要我们首先定义所需的转换,然后将其应用于数据集,如下面的代码片段所示:
transform = transforms.Compose([
transforms.HorizontalFlip(probability_goes_here),
transforms.RandomGrayscale(probability_goes_here),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
train_data = datasets.CIFAR10('data', train=True, download=True, transform=transform)
test_data = datasets.CIFAR10('data', train=False, download=True, transform=transform)
在这里,将要下载的数据将经历水平翻转(考虑到定义图像是否将被翻转的概率值),并将被转换为灰度图像(同样考虑到概率)。然后,数据被转换为张量并进行归一化。
考虑到模型是通过迭代过程进行训练的,其中训练数据被多次输入,这些转换确保第二次通过数据集时不会向模型提供完全相同的图像。
此外,重要的是提到可以为不同的数据集设置不同的转换。这是有用的,因为数据增强的目的是增加训练示例的数量,但用于测试模型的图像应该保持大部分不变。尽管如此,测试集应调整大小,以便将等尺寸的图像输入模型。
可以如下所示完成:
transform = {
"train": transforms.Compose([
transforms.RandomHorizontalFlip(probability_goes_here),
transforms.RandomGrayscale(probability_goes_here),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]),
"test": transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)),
transforms.Resize(size_goes_here)])}
train_data = datasets.CIFAR10('data', train=True, download=True, transform=transform["train"])
test_data = datasets.CIFAR10('data', train=False, download=True, transform=transform["test"])
如图所示,定义了一个包含训练和测试集各自转换的字典。然后,根据需要调用它们来应用转换。
活动 8:实施数据增强
在下一个活动中,将引入数据增强到先前活动创建的模型中,以测试是否可以提高其准确性。让我们看看以下情景:
您创建的模型很好,但准确率尚未令任何人印象深刻。他们要求您考虑一种能够改进模型性能的方法论。
-
复制上一个活动中的笔记本。
-
将
transform
变量的定义修改,除了将数据标准化并转换为张量外,还包括以下转换:-
对于训练/验证集,使用概率为 50%(0.5)的
RandomHorizontalFlip
函数和概率为 10%(0.1)的RandomGrayscale
函数。 -
对于测试集,不添加任何其他转换。
-
-
将模型训练 100 个 epochs。
注意
由于每个 epoch 中数据的洗牌,结果将不会完全可复制。然而,您应该能够得到类似的结果。
-
计算在测试集上得到的模型准确性。
注意
此活动的解决方案可以在第 209 页找到。
批量标准化
通常在尝试加快学习速度的同时,对输入层进行标准化以及通过重新缩放所有特征到相同尺度来提高性能是很常见的。因此,问题在于,如果模型从输入层的标准化中获益,为什么不试图通过标准化所有隐藏层的输出来进一步提高训练速度呢?
批量标准化,顾名思义,标准化隐藏层的输出,以减少每层的方差,这也被称为协方差偏移。减少协方差偏移对于使模型能够在遵循与训练图像不同分布的图像上表现良好也是有用的。
以检测动物是否为猫为目的的网络为例。当网络仅使用黑猫图像进行训练时,批量标准化可以帮助网络通过标准化数据,使黑色和彩色猫图像都遵循相似的分布,从而对不同颜色的猫的新图像进行分类。这类问题如下图所示:
图 4.18:猫分类器。即使仅使用黑猫进行训练,该模型也能识别有色猫。
此外,除了上述内容,批量归一化还为训练模型的过程引入以下好处,最终帮助您得到表现更好的模型:
-
它允许设置更高的学习率,因为批量归一化有助于确保输出不会过高或过低。更高的学习率等同于更快的学习速度。
-
它有助于减少过拟合,因为它具有正则化效果。这使得可以将丢弃概率设置为较低的值,这意味着每次前向传递中会忽略较少的信息。
注意
提醒一下,我们不应过于依赖批量归一化来处理过拟合问题。
如前面各层所述,对隐藏层输出进行归一化是通过减去批量均值并除以批量标准差完成的。
此外,重要的是提到,批量归一化通常应用于卷积层以及全连接层(不包括输出层)。
使用 PyTorch 进行批量归一化
在 PyTorch 中,添加批量归一化就像是向网络架构添加新层一样简单,考虑到这里所解释的两种不同类型:
BatchNorm1d:此层用于对二维或三维输入实施批量归一化。它接收前一层的输出节点数作为参数。这在全连接层上通常使用。
BatchNorm2d:这对四维输入应用批量归一化。同样,它接收前一层的输出节点数作为参数。它通常用于卷积层,因此它接收的参数应等于前一层的通道数。
根据这个,CNN 中批量归一化的实现如下:
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
self.conv1 = nn.Conv2d(3, 16, 3, 1, 1)
self.norm1 = nn.BatchNorm2d(16)
self.pool = nn.MaxPool2d(2, 2)
self.linear1 = nn.Linear(16 * 16 * 16, 100)
self.norm2 = nn.BatchNorm1d(100)
self.linear2 = nn.Linear(100, 10)
def forward(self, x):
x = self.pool(self.norm1(F.relu(self.conv1(x))))
x = x.view(-1, 16 * 16 * 16)
x = self.norm2(F.relu(self.linear1(x)))
x = F.log_softmax(self.linear2(x), dim=1)
return x
可见,批量归一化层的初始化方式与任何其他层类似。接下来,在激活函数后应用于其相应层的输出。
活动 9:实现批量归一化
对于接下来的活动,我们将在上一个活动的架构上实现批量归一化,以查看是否可能进一步改善模型在测试集上的性能。让我们看看以下情景:
哇!您以最后的性能改进给队友留下了深刻印象,现在他们期待您更多。他们要求您最后再试一次改进模型,以便将准确率提高到 80%:
-
复制来自上一个活动的笔记本。
-
将批归一化添加到每个卷积层以及第一个全连接层。
-
训练模型进行 100 个 epochs。
注意
由于每个 epoch 中数据的洗牌,结果将无法完全复现。不过,你应该能够得到相似的结果。
-
计算模型在测试集上的准确率。
注意
本活动的解决方案可在第 211 页找到。
总结
上一章集中讨论了 CNN(卷积神经网络),这是一种在计算机视觉问题上表现出色的神经网络架构。它首先解释了为什么 CNN 在处理图像数据集时被广泛使用的主要原因,并介绍了可以通过它们解决的不同任务的概述。
此外,本章解释了网络架构的不同构建模块,从解释卷积层的性质开始,然后转向池化层,最后解释全连接层。在每个部分中,都包括了每个层的目的解释,以及有效编写 PyTorch 架构所需的代码片段。
这导致引入一个图像分类问题,使用之前解释过的构建模块来解决。
接下来,数据增强被引入为一个工具,通过增加训练示例的数量来提高网络性能,而无需收集更多图像。该技术专注于对现有图像进行一些变化,以创建“新”图像供模型使用。
通过实施数据增强,本章的第二个活动旨在解决同一图像分类问题,并旨在比较结果。
最后,本章解释了批归一化的概念。它包括对每个隐藏层的输出进行归一化,以加快学习速度。在解释如何在 PyTorch 中应用批归一化的过程后,本章的最后一个活动再次旨在使用批归一化解决同一图像分类问题。
第五章:风格转移
学习目标
到本章末尾,您将能够:
-
从 PyTorch 加载预训练模型
-
提取图像的风格
-
获取图像的内容
-
创建一个新图像,使用一张图像的风格和另一张图像的内容
在本章中,您将学习如何将艺术风格从一张图片转移到另一张图片。这样,您就能够将日常图片转变为艺术杰作。
介绍
上一章详细解释了传统卷积神经网络(CNNs)的不同构建模块,以及一些技术,以提高性能并减少训练时间。尽管那里解释的架构是典型的,但并非一成不变,相反,已经出现了大量用于解决不同数据问题的 CNN 架构,更常见的是在计算机视觉领域。
这些架构因配置和学习任务而异。如今非常流行的一种架构是由牛津视觉几何组(Visual Geometry Group)创建的 VGG 架构。它是为了对象识别而开发的,通过依赖大量参数,实现了最先进的性能。其在数据科学家中的流行原因之一是训练模型的参数(权重和偏差)的可用性,这使得研究人员可以在不进行训练的情况下使用它,同时模型的性能也非常出色。
在本章中,我们将使用这个预训练模型来解决一个计算机视觉问题,这个问题因社交媒体频道的普及而变得特别有名,专门用于分享图像。它包括执行风格转移,以改善图像的外观,使其具有另一张图像的风格(颜色和纹理)。
在每天应用滤镜来提高社交媒体上常规图像质量和吸引力的过程中,进行了数百万次前述任务。尽管在使用时似乎是一个简单的任务,但本章将解释在这些图像编辑应用程序的幕后发生的魔法。
注意
作为提醒,包含本章所有代码的 GitHub 存储库可以在github.com/TrainingByPackt/Applied-Deep-Learning-with-PyTorch
找到。
风格转移
简单来说,风格转移包括修改图像的风格,同时保留其内容。例如,将动物图像的风格转换为梵高风格的绘画,如下图所示:
图 5.1:风格转移的输入和输出。本章最终练习的结果。
根据前述图示,预训练模型有两个输入:内容图像和样式图像。内容指的是物体,而样式指的是颜色和纹理。因此,模型的输出应该是包含内容图像中物体和样式图像艺术外观的图像。
工作原理是怎样的?
与解决传统计算机视觉问题不同(如前一章所述),风格转移需要按不同步骤有效地将两个图像作为输入并创建新图像作为输出。
以下是解决风格转移问题时遵循的步骤的简要解释:
-
输入数据的提供:内容图像和样式图像都需要输入模型,并且它们的形状必须相同。在这里的常见做法是将样式图像调整为与内容图像相同的形状。
-
加载模型:牛津大学视觉几何组创建了一个在风格转移问题上表现出色的模型架构,称为 VGG 网络。此外,他们还提供了模型的参数,以便任何人可以缩短或跳过模型的训练过程。
注
VGG 网络有不同版本,使用不同数量的层。为了区分不同的版本,其命名方式会在缩写后加上一个破折号和数字,代表该特定架构的层数。本章将使用网络的 19 层版本,即被称为 VGG-19 的版本。
因此,利用 PyTorch 的预训练模型子包,可以加载预训练模型,以执行风格转移任务,无需使用大量图像训练网络。
-
确定层的功能:鉴于有两个主要任务(识别图像内容和区分另一个图像的样式),不同的层将有不同的功能来提取不同的特征;对于样式图像,重点应放在颜色和纹理上;而对于内容图像,则应关注边缘和形式。在这一步骤中,不同的层被分配到不同的任务中。
-
定义优化问题:与任何其他监督问题一样,需要定义一个损失函数,它负责衡量输出和输入之间的差异。与其他监督问题不同的是,风格转移问题需要最小化三种不同的损失函数:
内容损失:仅考虑与内容相关的特征,衡量内容图像和输出之间的距离。
风格损失:仅考虑与样式相关的特征,衡量样式图像和输出之间的距离。
总损失:这结合了内容损失和风格损失。内容损失和风格损失都有一个相关的权重,用于确定它们在计算总损失中的贡献。
-
参数更新:此步骤使用梯度来更新网络的不同参数。
使用 VGG-19 网络架构实现风格迁移的实施
VGG-19 是一个包含 19 层的卷积神经网络。它使用来自 ImageNet 数据库的数百万图像进行训练。该网络能够将图像分类为 1000 个不同的类标签,包括大量的动物和各种工具。
注意
若要探索 ImageNet 数据库,请使用以下网址:www.image-net.org/
。
考虑到其深度,该网络能够从各种图像中识别复杂的特征,这使其特别适合风格迁移问题,其中在不同阶段和不同目的下的特征提取至关重要。
下一节将专注于解释如何使用预训练的 VGG-19 模型进行风格迁移的过程。本章的最终目的是将动物或风景图像(作为内容图像)和知名艺术家的绘画图像(作为风格图像)合成新的带有艺术风格的常规物体图像。
然而,在深入到过程之前,以下是导入的解释及其用途的简要解释:
-
NumPy:将用于将图像转换为显示的格式。
-
torch, torch.nn 和 torch.optim:这些将实现神经网络,并定义优化算法。
-
PIL.Image:这将加载图像。
-
matplotlib.pyplot:这将显示图像。
-
torchvision.transforms 和 torchvision.models:这些将把图像转换为张量并加载预训练模型。
输入:加载和显示
执行风格迁移的第一步包括加载内容图像和风格图像。在此步骤中,处理基本的预处理,其中图像必须是相同大小的(最好是用于训练预训练模型的图像大小),这也将是输出图像的大小。此外,图像被转换为 PyTorch 张量,并且可以根据需要进行归一化。
此外,始终将加载的图像显示出来是一个好习惯,以确保它们如预期一样。考虑到图像已经转换为张量并在此时进行了归一化,应克隆张量,并进行新一组转换,以便使用 Matplotlib 显示它们。
定义函数来加载和显示图像可以节省时间,并确保内容图像和风格图像上的处理过程相同。此过程将在后续练习中展开。
注意
本章的所有练习都要在同一个笔记本中编写,因为它们将一起执行风格转移任务。
练习 10:加载和显示图像
这是进行风格转移的四个步骤中的第一步。本章的目标是加载和显示图像(内容和风格),这些图像将在后续练习中使用。
注意
在 GitHub 仓库(本章开头分享的链接)中,您可以找到将在本章中不同练习和活动中使用的不同图像。
-
导入所有进行风格转移所需的包:
import numpy as np import torch from torch import nn, optim from PIL import Image import matplotlib.pyplot as plt from torchvision import transforms, models
-
设置用于两幅图像的图像大小。此外,设置应在图像上执行的变换,其中包括调整图像大小、将其转换为张量并进行归一化:
imsize = 224 loader = transforms.Compose([ transforms.Resize(imsize), transforms.ToTensor(), transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225))])
注意
VGG 网络是使用归一化图像训练的,其中每个通道分别具有均值 0.485、0.456 和 0.406,标准差为 0.229、0.224 和 0.225。
-
定义一个函数,该函数将接收图像路径作为输入,并使用 PIL 打开图像。接下来,它应该对图像应用变换:
def image_loader(image_name): image = Image.open(image_name) image = loader(image).unsqueeze(0) return image
-
调用函数以加载内容图像和风格图像。将狗图像作为内容,将马蒂斯图像作为风格,这两者都可在 GitHub 仓库中找到:
content_img = image_loader("images/dog.jpg") style_img = image_loader("images/matisse.jpg")
-
要显示图像,将它们转换回 PIL 图像并恢复归一化过程。将这些变换定义在一个变量中:
unloader = transforms.Compose([ transforms.Normalize((-0.485/0.229, -0.456/0.224, -0.406/0.225), (1/0.229, 1/0.224, 1/0.225)), transforms.ToPILImage()])
要恢复归一化,需要使用与用于数据归一化的均值相反的均值,除以先前用于数据归一化的标准差。此外,新的标准差应等于归一化数据之前使用的标准差的倒数。
-
创建一个函数,克隆张量,压缩它,并最终对张量应用变换:
def tensor2image(tensor): image = tensor.clone() image = image.squeeze(0) image = unloader(image) return image
-
对两幅图像调用函数并绘制结果:
plt.figure() plt.imshow(tensor2image(content_img)) plt.title("Content Image") plt.show() plt.figure() plt.imshow(tensor2image(style_img)) plt.title("Style Image") plt.show()
结果图像应如下所示:
图 5.2:内容图像
图 5.3:风格图像
恭喜!您已成功加载并显示用于风格转移的内容和风格图像。
加载模型
与许多其他框架一样,PyTorch 拥有一个子包,其中包含之前训练过的不同模型,并已公开供使用。这一点很重要,因为从头开始训练神经网络非常耗时,而使用预训练模型可以帮助减少这些训练时间。这意味着可以加载预训练模型以使用它们的最终参数(应为最小化损失函数的参数),而无需经历迭代过程。
如前所述,用于执行风格转移任务的架构是 VGG 网络的 19 层,也被称为 VGG-19。预训练模型位于 torchvision
的模型子包下。在 PyTorch 中保存的模型被分成两部分,如下所述和解释的那样:
-
vgg19.features:这包括网络的所有卷积和池化层以及其参数。这些层负责从图像中提取特征,其中一些层专门处理风格特征,如颜色,而其他层专门处理内容特征,如边缘。
-
vgg19.classifier:这指的是网络末端的线性层(也称为全连接层),包括它们的参数。这些层负责将图像分类为标签类别之一。
注意
要了解 PyTorch 中提供的其他预训练模型,请访问
pytorch.org/docs/stable/torchvision/models.html
。
根据前述信息,应仅加载模型的特征部分,以便提取内容和风格图像的必要特征。加载模型包括调用模型的子包,后跟模型的名称,确保预训练参数设置为 True
,并且仅加载特征层。
此外,应保持每层的参数不变,考虑到这些参数将有助于检测所需的特征。可以通过定义模型不需要计算这些层的任何梯度来实现这一点。
练习 11:在 PyTorch 中加载预训练模型
使用与前一练习中相同的笔记本,本练习旨在加载预训练模型,该模型将在随后的练习中使用,以执行使用先前加载的图像执行风格转移任务:
-
打开之前练习中的笔记本。
-
加载来自 PyTorch 的 VGG-19 预训练模型:
model = models.vgg19(pretrained=True).features
根据先前解释的内容选择模型的特征部分。这将允许访问模型的所有卷积和池化层,这些层将用于在本章后续练习中执行特征提取。
-
通过之前加载的模型的参数进行
for
循环。将每个参数设置为不需要计算梯度:for param in model.parameters(): param.requires_grad_(False)
通过将梯度计算设置为
False
,我们确保在创建目标图像的过程中不需要对梯度进行计算。
恭喜!您已成功加载了预训练模型。
提取特征
正如前面提到的,VGG-19 网络包含 19 层不同的层,包括卷积层、池化层和全连接层。每个池化层之前都有卷积层堆叠,整个架构中有五个堆叠。
在风格转移领域,已经有不同的论文确定了那些识别内容和风格图像中相关特征的关键层。根据这一点,通常认为每个堆栈的第一个卷积层能够提取风格特征,而只有第四个堆栈的第二个卷积层应用于提取内容特征。从现在开始,我们将称提取风格特征的层为 conv1_1
、conv2_1
、conv3_1
、conv4_1
和 conv5_1
,而负责提取内容特征的层将称为 conv4_2
。
注意
本章的指导文件可以通过以下网址访问:www.cv-foundation.org/openaccess/content_cvpr_2016/papers/Gatys_Image_Style_Transfer_CVPR_2016_paper.pdf
。
这意味着风格图像应通过五个不同的层,而内容图像只需通过一个层。每个层的输出用于比较输出图像与输入图像,其目标是修改目标图像的参数,使其类似于内容图像的内容和风格图像的风格,这可以通过优化三个不同的损失函数来实现(这将在本章中进一步解释)。
要确定目标图像是否包含与内容图像相同的内容,我们需要检查两者中是否存在某些特征。然而,要检查目标图像和风格图像的风格表示,需要检查它们之间的相关性,而不是严格的特征存在。这是因为两者的风格特征不会完全相同,而是近似。
为了实现这一点,引入了格拉姆矩阵。它由创建一个矩阵组成,该矩阵查看给定层中不同风格特征的相关性。这是通过将卷积层的向量化输出与相同的转置向量化输出相乘来完成的,如下图所示:
图 5.4:格拉姆矩阵的计算
在上图中,A 表示输入的风格图像,具有四乘四的尺寸(高度和宽度),B 表示通过五个滤波器的卷积层后的输出。最后,C 表示格拉姆矩阵的计算,其中左侧的图像代表 B 的向量化版本,右侧的图像是其转置版本。通过向量化输出的乘积,创建了一个五乘五的格拉姆矩阵,其值指示了不同通道(滤波器)中风格特征的相似性(相关性)。
这些相关性可以用来确定对图像的风格表示而言重要的特征,随后可用于修改目标图像。考虑到风格特征是从五个不同的层获取的,可以安全地假设网络能够检测到风格图像的小和大特征,因为每个层都必须创建一个 Gram 矩阵。
练习 12:设置特征提取过程
使用前一练习中的网络架构和本章第一次练习中的图像,我们将创建一对函数,能够从输入图像中提取特征并为风格特征创建 Gram 矩阵:
-
打开前一练习中的笔记本。
-
打印在前一练习中加载的模型的架构。这将有助于识别执行风格迁移任务所需的相关层:
print(model)
-
创建一个将相关层的索引(键)映射到名称(值)的字典。这将简化未来调用相关层的过程:
relevant_layers = {'0': 'conv1_1', '5': 'conv2_1', '10': 'conv3_1', '19': 'conv4_1', '21': 'conv4_2', '28': 'conv5_1'}
要创建字典,我们使用从上一步的输出,显示网络中每一层的输出。在那里,可以观察到第一个堆栈的第一层标记为
0
,而第二个堆栈的第一层标记为5
,依此类推。 -
创建一个函数,从输入图像中提取相关特征(仅从相关层提取的特征)。命名为
features_extractor
,确保它以图像、模型和先前创建的字典作为输入:def features_extractor(x, model, layers): features = {} for index, layer in model._modules.items(): if index in layers: x = layer(x) features[layers[index]] = x return features
model._modules
包含一个字典,其中存储了网络的每一层。通过对不同层进行for
循环,我们将图像通过感兴趣的层(之前创建的layers
字典内的层)并将输出保存到features
字典中。输出字典包含键,其中包含层的名称,值包含该层的输出特征。
-
在本章第一次练习中加载的内容和风格图像上调用
features_extractor
函数:content_features = features_extractor(content_img, model, relevant_layers) style_features = features_extractor(style_img, model, relevant_layers)
-
对风格特征执行 Gram 矩阵计算。考虑到风格特征来自不同的层,因此应创建不同的 Gram 矩阵,每层的输出各一个:
style_grams = {} for i in style_features: layer = style_features[i] _, d1, d2, d3 = layer.shape features = layer.view(d1, d2 * d3) gram = torch.mm(features, features.t()) style_grams[i] = gram
-
创建一个初始目标图像。稍后将与内容图像和风格图像进行比较,并在达到所需相似度之前进行更改:
target_img = content_img.clone().requires_grad_(True)
将初始目标图像创建为内容图像的副本是一种良好的做法。此外,设置为需要计算梯度是至关重要的,因为我们希望能够在迭代过程中修改它,直到内容与内容图像相似,风格与风格图像相似。
-
使用本章第一个练习期间创建的
tensor2image
函数,绘制目标图像,该图像应与内容图像相同:plt.figure() plt.imshow(tensor2image(target_img)) plt.title("Target Image") plt.show()
输出图像如下:
图 5.5:目标图像
恭喜!您已成功执行特征提取并计算格拉姆矩阵,以执行样式转移任务。
优化算法、损失和参数更新
尽管样式转移是使用预训练网络执行的,其中参数保持不变,但创建目标图像涉及一个迭代过程,其中通过仅更新与目标图像相关的参数来计算并最小化三种不同的损失函数。
为了实现这一目标,计算了两种不同的损失函数(内容损失和样式损失),然后将它们结合在一起计算出总损失函数,以优化得到一个合适的目标图像。然而,考虑到以内容和样式为度量精确度是非常不同的,以下是对计算内容和样式损失函数以及描述如何计算总损失的说明:
内容损失
这包括一个函数,根据给定层获得的特征映射计算内容图像和目标图像之间的距离。在 VGG-19 网络的情况下,仅基于conv4_2
层的输出计算内容损失。
内容损失函数的主要思想是最小化内容图像和目标图像之间的距离,使得后者在内容上高度类似于前者。
内容损失可以通过以下方程计算,即内容和目标图像在相关层(conv4_2
)的特征映射之间的均方差差异来实现:
图 5.6:内容损失函数
样式损失
与内容损失类似,样式损失是一个函数,通过计算样式特征(例如颜色和纹理)的均方差差异来衡量样式和目标图像之间的距离。
与内容损失相反,样式损失不是比较来自不同层的特征映射,而是比较基于样式和目标图像的特征映射计算得到的格拉姆矩阵。
需要提到的是,样式损失必须使用for
循环来计算所有相关层(在本例中为五层)。这将导致一个损失函数,考虑了来自两幅图像的简单和复杂样式表示。
此外,将这些层的样式表示加权在 0 到 1 之间是一个很好的做法,以便更强调从样式图像中提取较大和更简单特征的层。通过给予更早的层(conv1_1
和 conv2_1
)更高的权重,从而实现这一点,这些层从样式图像中提取更通用的特征。
鉴于此,可以使用以下方程来计算每个相关层的样式损失:
图 5.7:样式损失计算
总损失
最后,总损失函数由内容损失和样式损失的组合构成。在创建目标图像的迭代过程中,通过更新目标图像的参数来最小化其值。
同样,建议分配内容和样式损失的权重,以确定它们在最终输出中的参与程度。这有助于确定目标图像的风格化程度,同时仍然保持内容的可见性。考虑到这一点,将内容损失的权重设置为 1 是一个很好的做法,而样式损失的权重必须更高,以实现您喜欢的比例。
被分配给内容损失的权重通常被称为α,而被分配给样式损失的权重则被称为β。
计算总损失的最终方程可以如下所示:
图 5.8:总损失计算
一旦确定了损失的权重,就是设置迭代步数和优化算法的时候了,这只会影响目标图像。这意味着,在每个迭代步中,将计算这三个损失,然后利用梯度来优化与目标图像相关的参数,直到最小化损失函数并实现具有所需外观的目标函数。
与以前的神经网络优化类似,每次迭代中遵循以下步骤:
-
从目标图像获取内容和样式的特征。在初始迭代中,此图像将是内容图像的精确副本。
-
计算内容损失。这是通过比较内容和目标图像的内容特征图来完成的。
-
计算所有相关层的平均样式损失。这是通过比较样式和目标图像的所有层的格拉姆矩阵来实现的。
-
计算总损失。
-
计算目标图像参数(权重和偏置)的总损失函数的偏导数。
-
直到达到所需的迭代次数为止重复此过程。
最终输出将是一个内容类似于内容图像且风格类似于样式图像的图像。
练习 13:创建目标图像
在本章的最后一个练习中,将实现风格转移任务。本练习包括编写负责在优化损失函数的同时执行不同迭代的部分的代码,以达到理想的目标图像。为此,关键是利用本章之前编程的代码片段:
-
打开上一个练习中的笔记本。
-
定义一个包含每个负责提取风格特征层的权重的字典:
style_weights = {'conv1_1': 1., 'conv2_1': 0.8, 'conv3_1': 0.6, 'conv4_1': 0.4, 'conv5_1': 0.2}
确保使用与前一章节中给出的层相同的名称作为键。
-
定义与内容损失和风格损失相关联的权重:
alpha = 1 beta = 1e6
-
定义迭代步骤的数量以及优化算法。我们也可以设置在特定迭代之后要看到创建图像的情况。
print_statement = 500 optimizer = torch.optim.Adam([target_img], lr=0.001) iterations = 2000
优化算法应更新目标图像的参数。
注意
如本练习中的示例所示,运行 2,000 次迭代将需要相当长的时间,这取决于您的资源。然而,要达到风格转移的卓越结果,通常需要更多的迭代(大约 6,000 次)。
为了欣赏从迭代到迭代发生在目标图像上的变化,几次迭代就足够了,但建议您尝试更长时间的训练。
-
定义一个
for
循环,在其中计算所有三个损失函数,并执行优化:for i in range(1, iterations+1): target_features = features_extractor(target_img, model, relevant_ layers) content_loss = torch.mean((target_features['conv4_2'] - content_ features['conv4_2'])**2) style_losses = 0 for layer in style_weights: target_feature = target_features[layer] _, d1, d2, d3 = target_feature.shape target_reshaped = target_feature.view(d1, d2 * d3) target_gram = torch.mm(target_reshaped, target_reshaped.t()) style_gram = style_grams[layer] style_loss = style_weights[layer] * torch.mean((target_gram - style_gram)**2) style_losses += style_loss / (d1 * d2 * d3) total_loss = alpha * content_loss + beta * style_loss optimizer.zero_grad() total_loss.backward() optimizer.step() if i % print_statement == 0 or i == 1: print('Total loss: ', total_loss.item()) plt.imshow(tensor2image(target_img)) plt.show()
-
绘制内容和目标图像以比较结果:
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(20, 10)) ax1.imshow(tensor2image(content_img)) ax2.imshow(tensor2image(target_img)) plt.show()
最终的图像应该看起来类似于以下的图例:
图 5.9:内容和目标图像的比较
恭喜!你成功地完成了风格转移任务。
活动 10:执行风格转移
在这个活动中,我们将执行风格转移任务。为此,我们将编写本章学到的所有概念。让我们看看以下情况:
你是一个环球旅行者,决定创建一个记录你旅行的博客。然而,你还热衷艺术,并希望所有的图片看起来都像莫奈的画作一样具有艺术感。为了实现这一目标,你决定创建一个使用预训练神经网络执行风格转移任务的代码:
-
导入所需的库。
-
指定要对输入图像执行的转换。确保将它们调整为相同的大小,转换为张量,并进行归一化。
-
定义一个图像加载函数。它应该打开并加载图像。调用图像加载函数以加载两个输入图像。
-
为了能够显示这些图片,设置转换以恢复图片的归一化,并将张量转换为 PIL 图像。
-
创建一个能够在张量上执行先前转换的函数。为两个图像调用该函数并绘制结果。
-
加载 VGG-19 模型。
-
创建一个将相关层的索引(键)映射到名称(值)的字典。然后,创建一个函数来提取相关层的特征映射。使用它们来提取两个输入图像的特征。
-
计算风格特征的 Gram 矩阵。同时,创建初始目标图像。
-
设置不同风格层的权重,以及内容和风格损失的权重。
-
运行 500 次迭代的模型。在开始训练模型之前,定义 Adam 优化算法,并使用 0.001 作为学习率。
注意
根据您的资源情况,训练过程可能需要数小时,考虑到为了获得出色的结果,建议进行数千次迭代的训练。添加打印语句是查看训练过程进展的良好实践。
根据先前的信息,本章的结果是通过运行大约 30,000 次迭代实现的,如果没有 GPU,运行时间会很长(此配置可以在 GitHub 的存储库中找到)。然而,为了看到一些细微的变化,仅需运行几百次迭代即可,正如本活动中推荐的那样(500 次)。
-
绘制内容图像和目标图像以比较结果。
注意
本活动的解决方案可以在第 214 页找到。
摘要
本章介绍了风格转换,这是当今流行的任务之一,可以使用 CNN 来解决。它包括将内容和风格图像作为输入,并返回一个新创建的图像作为输出,该图像保留了一个图像的内容和另一个图像的风格。通常用于通过将随机常规图像与伟大艺术家的绘画相结合来赋予图像艺术感。
尽管使用 CNN 解决了风格转换问题,但创建新图像的过程并不是通过传统训练网络来实现的。本章详细解释了如何通过使用预训练网络,考虑到一些特别擅长识别特定特征的相关层的输出。
本章解释了开发能够执行风格转换任务的代码的每个步骤,其中第一步是加载和显示输入。如前所述,模型有两个输入(内容和风格图像)。每个图像都要经历一系列转换步骤,目的是将图像调整为相同大小,转换为张量,并进行归一化,以便网络正确处理它们。
接下来,加载预训练模型。如本章所述,VGG-19 是解决此类任务中最常用的体系结构之一。它由 19 层组成,包括卷积、池化和全连接层,对于所讨论的任务,仅使用其中的一些卷积层。加载预训练模型的过程相当简单,因为 PyTorch 提供了一个子包,其中包含几种预训练的网络体系结构。
此外,一旦加载网络,就解释了如何确定网络的某些层被标识为过度表现者来检测对样式转移至关重要的某些特征。虽然有五个不同的层能够提取与图像样式相关的特征,比如颜色和纹理,但其中一个层在提取边缘和形状等内容特征方面表现异常出色。因此,定义这些相关层是至关重要的,这些层将用于从输入图像中提取信息,以创建所需的目标图像。
最后,是编写迭代过程的时候,该过程能够创建具有所需特征的目标图像。为此,计算了三种不同的损失。一种是比较内容图像与目标图像在内容方面的差异(内容损失),另一种是比较样式图像与目标图像在样式方面的差异,这是通过计算格拉姆矩阵来实现的(样式损失)。最后一种是结合了这两种损失的总损失。
然后,通过减少总损失值的方法实现了目标图像,这可以通过更新与目标图像相关的参数来完成。尽管使用了预训练网络,但到达理想的目标图像可能需要数千次迭代和相当长的时间。
第六章:使用 RNN 分析数据序列
学习目标
在本章结束时,您将能够:
-
解释递归神经网络(RNNs)的概念
-
构建一个简单的 RNN 架构来解决预测数据问题
-
使用长短期记忆(LSTM)架构工作,并使用 LSTM 网络生成文本
-
使用长期和短期记忆解决数据问题
-
使用 RNN 解决自然语言处理(NLP)问题
在本章中,您将学习如何使用 RNN 解决 NLP 问题所需的技能。
简介
在前面的章节中,解释了不同的网络架构——从传统的人工神经网络(ANNs),可以解决分类和回归问题,到卷积神经网络(CNNs),主要用于通过执行目标分类、定位、检测和分割的任务来解决计算机视觉问题。
在本章的最后,我们将探讨递归神经网络(RNNs)的概念,并解决序列数据问题。这些网络架构能够处理序列数据,其中上下文至关重要,这得益于它们能够保持来自先前预测的信息,这称为记忆。这意味着例如在分析句子时,逐词处理时,当处理最后一个词时,RNNs 有能力保持来自句子第一个词的信息。
此外,本章还将探索长短期记忆(LSTM)网络架构,这是一种能够同时保持长期和短期记忆的 RNN 类型,特别适用于长序列数据,如视频剪辑。
最后,本章还将探讨自然语言处理(NLP)的概念。NLP 指的是计算机与人类语言的交互,这是一个当前流行的话题,得益于提供定制客户服务的虚拟助手的兴起。尽管如此,本章将使用 NLP 来进行情感分析,其目的是分析句子背后的含义。这对于理解客户对产品或服务的情感态度非常有用,基于客户的评价。
注意
作为提醒,本章节使用的所有代码都可以在github.com/TrainingByPackt/Applied-Deep-Learning-with-PyTorch
找到。
递归神经网络
就像人类不会每秒都重新思考一样,旨在理解人类语言的神经网络也不应该这样做。这意味着为了理解段落甚至整本书中的每个单词,您或模型需要理解先前的单词,这有助于给那些可能有不同含义的单词提供上下文。
传统神经网络,正如我们迄今所讨论的,无法执行这些任务——因此产生了 RNN 的概念和网络架构。如前所述,这些网络架构在不同节点之间包含循环。这使得信息能够在模型中保留更长时间。因此,模型的输出既是预测,也是记忆,当通过模型传递下一个序列文本片段时将使用该记忆。
这个概念可以追溯到 20 世纪 80 年代,尽管近年来才因技术进步而受到欢迎,这些技术进步提高了机器的计算能力,并允许数据的收集,还有 1990 年代 LSTM RNN 概念的发展,增加了它们的行动范围。由于能够存储内部记忆,RNN 是最有前途的网络架构之一,这使它们能够高效处理数据序列并解决各种数据问题。
RNN 的应用
尽管我们已经非常清楚 RNN 最适合处理数据序列,比如文本、音频片段和视频,但仍有必要解释 RNN 在现实问题中的不同应用,以理解它们为什么每天都在日益增长的流行度。
这里简要解释了通过使用 RNN 可以执行的不同任务:
-
自然语言处理(NLP):这指的是机器代表人类语言的能力。如今,这可能是深度学习中最受关注的领域之一,无疑也是在利用 RNN 时首选的数据问题。其思想是使用文本作为输入数据来训练网络,比如诗歌和书籍等,目的是创建一个能够生成这些文本的模型。
NLP 通常用于创建聊天机器人(虚拟助手)。通过学习以前的人类对话,NLP 模型能够帮助人解决常见问题或查询。您可能在尝试通过在线聊天系统联系银行时经历过这种情况,在这种情况下,一般会在查询超出常规范围时转接到人工操作员。
图 6.1:Facebook 的 Messenger 聊天机器人
-
语音识别:类似于自然语言处理(NLP),语音识别试图理解和表达人类语言。然而,这里的区别在于前者(NLP)是经过训练并以文本形式输出结果,而后者(语音识别)则使用音频片段。随着该领域的发展以及大公司的兴趣,这些模型能够理解不同的语言,甚至不同的口音和发音。
语音识别设备的一个流行例子是 Alexa - 亚马逊的语音激活虚拟助理模型:
图 6.2:亚马逊的 Alexa
-
机器翻译:这指的是机器有效地翻译人类语言的能力。根据这一原理,输入是源语言(例如西班牙语),输出是目标语言(例如英语)。自然语言处理与机器翻译的主要区别在于,后者的输出是在将整个输入馈送到模型之后构建的。
随着全球化的兴起和休闲旅行的流行,现代人需要访问多种语言。因此,涌现了能够在不同语言之间进行翻译的设备的大量使用。其中最新的创新之一是 Google 推出的 Pixel Buds,可以实时进行翻译:
图 6.3:Google Pixel Buds
-
时间序列预测:RNN 的一个较少被使用的应用是基于历史数据预测未来数据点序列。由于 RNN 具有保持内部记忆的能力,因此特别擅长这项任务,使得时间序列分析能够考虑过去不同时间步中的数据来进行未来的预测或一系列预测。
这经常用于预测未来的收入或需求,帮助公司为不同的情况做好准备:
图 6.4:每月销量(数量)的预测
例如,通过预测多种健康产品的需求,确定其中一种产品将增加而另一种将减少,公司可以决定生产更多这种产品而减少其他产品的生产量。
- 图像识别:结合 CNN,RNN 可以给图像加上标题或描述。这种模型组合使得您能够检测图像中的所有物体,并因此确定图像的主要构成。输出可以是图像中存在的对象的一组标签,图像的描述,或者是图像中相关对象的标题,如下图所示:
图 6.5:使用 RNN 进行图像识别
RNN 如何工作?
简而言之,RNN 接收一个输入(x)并返回一个输出(y)。在这里,输出不仅受输入影响,还受过去输入的整个历史影响。这些输入的历史通常称为模型的内部状态或记忆,这些是按顺序排列并相互关联的数据序列,例如时间序列,即按顺序列出的数据点(例如销售),这些数据点相互关联。
注意
请记住,RNN 的一般结构可能会根据具体问题而变化。例如,它们可以是一对多类型或多对一类型,如第二章中所述,神经网络的基本构建块。
为了更好地理解 RNN 的概念,重要的是解释 RNN 与传统神经网络之间的区别。传统神经网络通常被称为前馈神经网络,因为信息只沿着一个方向移动,即从输入到输出,不会经过节点两次进行预测。这些网络对过去输入的记忆没有任何信息,这也是它们无法预测序列中接下来发生什么的原因。
另一方面,在循环神经网络中,信息通过循环来循环使用,以便每个预测都考虑输入和先前预测的记忆。它通过复制每个预测的输出,并将其传递回网络进行下一个预测。这样,循环神经网络有两个输入:当前值和过去的信息:
图 6.6:网络的图形表示,其中 A 显示了前馈神经网络,B 显示了 RNN
注意
传统 RNN 的内部记忆仅限于短期。然而,我们将在后面探讨一种能够存储长期和短期记忆的架构。
通过使用先前预测的信息,网络使用一系列有序数据进行训练,从而预测下一个步骤。这是通过将当前信息与前一步骤的输出合并为单个操作来实现的(如图 6.7所示)。这个操作的输出将成为预测结果,同时也是后续预测的一部分输入:
图 6.7:每个预测的 RNN 计算
如你所见,节点内部的操作与任何其他神经网络相同;最初,数据通过线性函数传递。权重和偏差是训练过程中要更新的参数。接下来,使用激活函数打破这个输出的线性性质。在这种情况下,使用的是tanh
函数,因为多项研究表明它对大多数数据问题能够达到更好的结果:
图 6.8:传统 RNN 的数学计算
在这里,Mt-1 指的是从先前预测导出的记忆,W 和 b 是权重和偏差,而 E 则指当前事件。
考虑一个产品过去两年的销售数据。RNNs 能够预测下个月的销售情况,因为它们通过存储过去几个月的信息,可以检查销售是增加还是减少。
使用 Figure 6.7,可以通过使用上个月的销售数据(即当前事件)和短期记忆(这是过去几个月数据的表示)进行下个月的预测。这个操作的输出将包含下个月的预测以及过去几个月的相关信息,这些信息反过来将成为后续预测的新的短期记忆。
此外,还需提到一些 RNN 架构,如 LSTM 网络,也能考虑两年甚至更早的数据(因为它存储了长期记忆),这将帮助网络了解某个月份的减少趋势是否可能继续减少或开始增加。我们稍后会更详细地探讨这个话题。
PyTorch 中的 RNN
在 PyTorch 中,就像任何其他层一样,递归层在一行代码中定义。然后会在网络的前向函数中调用,如下面的代码所示:
class RNN(nn.Module):
def __init__(self, input_size, hidden_size, num_layers):
super().__init__()
self.hidden_size = hidden_size
self.rnn = nn.RNN(input_size, hidden_size, num_layers,
batch_first=True)
self.output = nn.Linear(hidden_size, 1)
def forward(self, x, hidden):
out, hidden = self.rnn(x, hidden)
out = out.view(-1, self.hidden_size)
out = self.output(out)
return out, hidden
在此,递归层必须定义为接受输入中预期特征的数量 (input_size
);由用户定义的隐藏状态中的特征数量 (hidden_size
);以及递归层数量 (num_layers
)。
注意
与任何其他神经网络类似,隐藏大小指的是该层中的节点(神经元)数量。
batch_first
参数设置为 True
,以定义输入和输出张量为批处理、序列和特征的形式。
在 forward
函数中,输入通过递归层并展开,以通过完全连接的层传递。
此类网络的训练可以如下处理:
for i in range(1, epochs+1):
hidden = None
for inputs, targets in batches:
pred, hidden = model(inputs, hidden)
loss = loss_function(pred, targets)
optimizer.zero_grad()
loss.backward()
optimizer.step()
对于每个 epoch,隐藏状态被初始化为 none
。这是因为在每个 epoch 中,网络将尝试将输入映射到目标(在给定一组参数的情况下)。这种映射应该在不受来自先前数据集运行的偏置(隐藏状态)的影响下进行。
接下来,通过 for
循环遍历不同的数据批次。在此循环内,进行预测,并保存隐藏状态以供下一个批次使用。
最后,计算损失函数,用于更新网络的参数。然后,这个过程会再次开始,直到达到期望的 epoch 数量。
活动 11:使用简单的 RNN 进行时间序列预测
对于下面的活动,将使用一个简单的 RNN 来解决时间序列问题。考虑以下情景:您的公司希望能够提前预测所有产品的需求。这是因为每个产品的生产需要相当长的时间,并且程序成本高昂。因此,他们不希望在产品可能被销售之前花费时间和金钱。为了预测这一点,他们提供了一个数据集,其中包含去年销售的所有产品的每周需求(销售交易量):
注意
包含用于下面活动的数据集的 CSV 文件可以在本书的 GitHub 仓库中找到。该仓库的 URL 在本章的介绍中提到。它也可以在线访问:archive.ics.uci.edu/ml/datasets/Sales_Transactions_Dataset_Weekly.
-
首先,导入所需的库。
-
然后,将
seed
设置为0
,以在本书中重现结果,使用以下代码行:torch.manual_seed(0)
-
加载数据集并对其进行切片,以包含所有行但只包含从索引 1 到 52 的列。
-
绘制来自整个数据集的五种随机选择产品的每周销售交易。在进行随机抽样时,请使用随机种子
0
,以获得与当前活动中相同的结果。 -
创建将输入到网络以创建模型的
inputs
和targets
变量。这些变量应该具有相同的形状,并转换为 PyTorch 张量。inputs
变量应该包含所有产品在所有周的数据,除了最后一周,因为模型的想法是预测这最后一周。targets
变量应该比inputs
变量提前一步 - 也就是说,targets
变量的第一个值应该是inputs
变量的第二个值,依此类推,直到targets
变量的最后一个值(应该是在inputs
变量之外剩下的最后一周)。 -
创建一个包含网络架构的类;请注意全连接层的输出大小应为 1。
-
初始化包含模型的类函数。输入大小、每个递归层中的神经元数(10)和递归层的数量(1)。
-
定义损失函数、优化算法和训练网络的 epochs 数量;使用均方误差损失函数、Adam 优化器和 10,000 个 epochs。
-
使用
for
循环执行训练过程,遍历所有 epochs。在每个 epoch 中,必须进行预测,同时计算损失函数并优化网络参数。然后,保存每个 epoch 的损失。 -
绘制所有 epochs 的损失。
-
使用散点图显示在训练过程的最后一个时期获得的预测结果与地面真实值(即上周销售交易)的对比。
注意
此活动的解决方案可在第 219 页找到。
长短期记忆网络(LSTM)
如前所述,RNN 只存储短期记忆。在处理长序列数据时会出现问题,网络将难以将早期步骤的信息传递到最终步骤。
例如,以诗人埃德加·爱伦·坡创作的诗《乌鸦》为例,全文超过 1000 字。试图使用传统的 RNN 处理它,目的是创建一个相关的后续诗歌,将导致模型忽略第一段落中的关键信息。这反过来可能导致输出与诗歌的初始主题无关。例如,它可能会忽略事件发生在夜晚,从而使新诗歌不够可怕。
这种无法保持长期记忆的问题是因为传统的 RNN 遇到了称为梯度消失的问题。当梯度变得极小以至于不再对网络的学习过程有贡献时,用于更新网络参数以最小化损失函数的梯度在网络的早期层次通常会出现这种情况,导致网络忘记了一段时间前看到的信息。
因此,LSTM 网络被开发出来。LSTM 网络能够像计算机一样在长时间内记住信息,通过使用门控的方式来读取、写入和删除信息。
这些门有助于网络决定保留哪些信息以及删除哪些信息(是否打开门),根据它分配给每个信息位的重要性。这非常有用,因为它不仅允许存储更多信息(作为长期记忆),而且还有助于丢弃可能改变预测结果的无用信息,例如句子中的冠词。
应用
除了先前解释的应用外,LSTM 网络存储长期信息的能力使数据科学家能够解决复杂的数据问题,这些问题利用大量数据序列作为输入,下面将进一步解释其中一些:
-
文本生成:生成任何文本,比如你正在阅读的文本,可以转换为 LSTM 网络的任务。这通过基于所有先前的字母选择每个字母来实现。执行此任务的网络使用大文本进行训练,例如著名书籍的文本。这是因为最终模型将创建与训练文本写作风格相似的文本。例如,经过诗歌训练的模型将具有与与邻居交谈不同的叙述。
-
音乐生成:就像文本序列可以输入到网络中以生成类似的新文本一样,音符序列也可以输入到网络中以生成新的音乐音符序列。跟踪先前的音符将有助于实现和谐的旋律,而不仅仅是一系列随机的音乐音符。例如,输入来自 The Beatles 的一首流行歌曲的音频文件将产生一系列音乐音符,这些音符类似于该组合的和声。
-
手写生成和识别:在这里,每个字母也是所有前一个字母的产物,这将导致一组有意义的手写字母。同样,LSTM 网络也可以用于识别手写文本,其中一个字母的预测将依赖于先前预测的所有字母。
LSTM 网络如何工作?
到目前为止,已经明确了 LSTM 网络与传统 RNN 的区别在于它们具有长期记忆的能力。然而,值得一提的是,随着时间的推移,非常旧的信息不太可能影响下一个输出。考虑到这一点,LSTM 网络还具有考虑数据位之间距离和底层上下文的能力,以便还可以决定遗忘一些不再相关的信息。
那么,LSTM 网络如何决定何时记住何时遗忘?与传统的 RNN 不同,传统的 RNN 在每个节点只执行一个计算,而 LSTM 网络执行四种不同的计算,允许网络的不同输入之间的交互(即当前事件、短期记忆和长期记忆)得出结果。
要理解 LSTM 网络背后的过程,让我们考虑用于管理网络中信息的四个门,这些门在下图中表示:
图 6.9:LSTM 网络门
图 6.9 中每个门的功能可以解释如下:
tanh
). 这个输出乘以一个忽略因子,去除任何不相关的信息。要计算忽略因子,将短期记忆和当前事件通过线性函数传递。然后,它们通过sigmoid
激活函数挤压在一起:
图 6.10: 学习门中发生的数学计算
在这里,STM 指的是从先前预测中得出的短期记忆,W 和 b 是权重和偏置,E 指当前事件。
sigmoid
):
图 6.11: 忘记门中发生的数学计算
在这里,STM 指的是从先前预测中得出的短期记忆,LSM 是从先前预测中得出的长期记忆,W 和 b 是权重和偏置,E 指当前事件。
- 记忆门:在忘记门中未被遗忘的长期记忆和从学习门中保留的信息在记忆门中合并在一起,成为新的长期记忆。从数学上讲,这通过将来自学习门和忘记门的输出相加来实现:
图 6.12: 记忆门中发生的数学计算
在这里,L 指的是来自学习门的输出,而 F 是来自忘记门的输出。
tanh
)对忘记门的输出执行线性和激活函数(sigmoid
)。其次,它对短期记忆和当前事件的输出进行线性和激活函数(sigmoid
)运算。第三,它将前述步骤的输出相乘。第三步的输出将成为新的短期记忆和当前步骤的预测:
图 6.13: 使用门中发生的数学计算
在这里,STM 指的是从先前预测中得出的短期记忆,W 和 b 是权重和偏置,E 指当前事件。
注意
尽管使用不同的激活函数和数学运算符似乎是随意的,但之所以这样做是因为它已被证明适用于处理大量数据序列的大多数数据问题。
模型执行的每一个预测都会进行上述过程。例如,对于一个用于创建文学作品的模型,学习、遗忘、记忆和使用信息的过程将针对每个将由模型生成的字母执行,如下图所示:
图 6.14: LSTM 网络随时间的过程
PyTorch 中的 LSTM 网络
在 PyTorch 中定义 LSTM 网络架构的过程与我们迄今讨论的任何其他神经网络类似。然而,重要的是要注意,当处理与数字不同的数据序列时,需要进行一些预处理,以便将数据馈送到网络中进行理解和处理。
考虑到这一点,将会对训练模型的一般步骤进行解释,以便能够将文本数据作为输入并检索到新的文本数据。重要的是要提到,并非所有在此处解释的步骤都是严格必需的,但作为一组,它们使得使用 LSTM 处理文本数据的代码简洁且可重复使用:
预处理输入数据
第一步是将文本文件加载到代码中。此数据将经过一系列转换,以便正确地馈送到模型中。这是必要的,因为神经网络执行一系列数学计算以产生输出,这意味着所有输入必须是数值型的。此外,将数据以批次形式馈送到模型中也是一个好习惯,而不是一次性全部馈送,因为这有助于减少训练时间,特别是对于长数据集。这些转换过程如下所述:
编号标签
首先,从输入数据中获取一个无重复字符的列表。每个字符都被分配一个数字。然后,通过将每个字符替换为分配的数字来对输入数据进行编码。例如,单词"hello"将根据以下字符和数字的映射被编码为 123344:
图 6.15:字符和数字的映射
生成批次
对于 RNNs,批次是使用两个变量创建的。首先是每个批次中的序列数,其次是每个序列的长度。这些值用于将数据分割成矩阵,有助于加快计算速度。
使用一个包含 24 个整数的数据集,每批次的序列数设置为 2,序列长度为 4,划分过程如下:
图 6.16:用于 RNN 的批次生成
如图 6.16所示,创建了 3 个批次,每个批次包含 2 个长度为 4 的序列。
这个批次生成过程应该对x
和y
分别进行,前者是网络的输入,后者代表目标。根据这一点,网络的思想是找到一种方法来映射x
和y
之间的关系,考虑到y
将比x
提前 1 步。
x
的批次是按照前述图表(图 6.16)中解释的方法创建的。然后,y
的批次将与x
的长度相同。这是因为y
的第一个元素将是x
的第二个元素,依此类推,直到y
的最后一个元素(它将是x
的第一个元素):
注意
有多种不同的方法可以用来填充y
的最后一个元素,这里提到的方法是最常用的方法。选择方法通常是偏好的问题,尽管某些数据问题可能更适合某种方法而不是其他方法。
图 6.17:X 和 Y 的批次表示
注意
尽管生成批次被认为是数据预处理的一部分,但通常在训练过程的for
循环内编程。
单热编码
将所有字符转换为数字并不足以将它们馈送到模型中。这是因为此近似会为您的模型引入一些偏差,因为转换为较高数值的字符将被视为更重要。为了避免这种情况,最好的做法是将不同批次编码为单热矩阵。这包括创建一个由零和一组成的三维矩阵,其中零表示事件的缺失,而一表示事件的存在。请记住,矩阵的最终形状应如下所示:
方程式 6.18:单热矩阵维度
这意味着对于批次中的每个位置,它将创建一个长度等于整个文本中字符总数的值序列。对于每个字符,它将放置一个零,除了在该位置存在的字符(在该位置将放置一个一)。
注意
您可以在hackernoon.com/what-is-one-hot-encoding-why-and-when-do-you-have-to-use-it-e3c6186d008f
找到更多关于单热编码的信息。
构建架构
与其他神经网络类似,LSTM 层可以在一行代码中轻松定义。然而,网络架构的类现在必须包含一个函数,允许初始化隐藏状态和细胞状态的特征(即网络的两个记忆)。以下是 LSTM 网络架构的示例:
class LSTM(nn.Module):
def __init__(self, char_length, hidden_size, n_layers):
super().__init__()
self.hidden_size = hidden_size
self.n_layers = n_layers
self.lstm = nn.LSTM(char_length, hidden_size, n_layers, batch_first=True)
self.output = nn.Linear(hidden_size, char_length)
def forward(self, x, states):
out, states = self.lstm(x, states)
out = out.contiguous().view(-1, self.hidden_size)
out = self.output(out)
return out, states
def init_states(self, batch_size):
hidden = next(self.parameters()).data.new(self.n_layers, batch_size, self.hidden_size).zero_()
cell = next(self.parameters()).data.new(self.n_layers, batch_size, self.hidden_size).zero_()
states = (hidden, cell)
return states
注意
再次,当输入和输出张量以批次、序列和特征的形式存在时,batch_first
参数被设置为True
。否则,无需定义它,因为其默认值为False
。
正如所示,LSTM 层在一行中定义,其参数包括输入数据中的特征数(即非重复字符的数量)、隐藏维度(神经元数)和 LSTM 层的数量。
前向函数与任何其他网络一样,定义了数据在前向传递过程中在网络中的移动方式。
最后,定义一个函数来在每个 epoch 中将隐藏状态和单元状态初始化为零。这通过next(self.parameters()).data.new()
来实现,它获取模型的第一个参数,并创建一个相同类型的新张量,其内部括号中指定的维度被填充为零。隐藏状态和单元状态被作为元组输入到模型中。
训练模型
一旦损失函数和优化算法被定义,就可以开始训练模型了。这通过遵循与其他神经网络架构类似的方法来实现,如下面的代码片段所示:
for e in range(1, epochs+1):
states = model.init_states(n_seq)
for b in range(0, x.shape[1], seq_length):
x_batch = x[:,b:b+seq_length]
if b == x.shape[1] - seq_length:
y_batch = x[:,b+1:b+seq_length]
y_batch = np.hstack((y_batch, indexer["."] * np.ones((y_batch.shape[0],1))))
else:
y_batch = x[:,b+1:b+seq_length+1]
x_onehot = torch.Tensor(index2onehot(x_batch))
y = torch.Tensor(y_batch).view(n_seq * seq_length)
pred, states = model(x_onehot, states)
loss = loss_function(pred, y.long())
optimizer.zero_grad()
loss.backward(retain_graph=True)
optimizer.step()
如前面的代码所示,遵循以下步骤:
-
需要多次通过数据以获得更好的模型;因此,需要设置一个 epoch 数。
-
每个 epoch 中,必须初始化隐藏状态和单元状态。这通过调用在类中之前创建的函数来实现。
-
数据以批次输入到模型中;需要考虑将输入数据编码为一个独热矩阵。
-
通过调用模型在一批数据上的输出,然后计算损失函数,最后优化参数来获取网络的输出。
进行预测
在训练模型之前,提供前几个字符给训练模型是一个好的实践,以便进行具有一定目的的预测。这个初始字符应该在不进行任何预测的情况下输入到模型中,但目的是生成一个记忆。接下来,每个新字符是通过将前一个字符和记忆输入到网络中来创建的。然后,模型的输出通过softmax
函数传递,以获取新字符成为每个可能字符的概率。最后,从具有较高概率的字符中随机选择一个。
活动 12:使用 LSTM 网络进行文本生成
注意
用于接下来的活动的文本数据可以在互联网上免费获取,尽管您也可以在本书的 GitHub 存储库中找到它。存储库的 URL 在本章的介绍中有提及。
对于以下活动,我们将使用《爱丽丝梦游仙境》训练一个 LSTM 网络,然后能够向模型提供一个起始句子并让它完成句子。让我们考虑以下情景:你喜欢能让生活更轻松的事物,并决定建立一个模型,帮助你在写电子邮件时完成句子。为此,你已经决定使用一本流行的儿童书籍来训练一个网络:
注意
值得一提的是,虽然本活动中的网络经过了足够的迭代以显示出不错的结果,但它并未经过训练和配置以达到最佳性能。鼓励您进行调整以改善性能。
-
导入所需的库。
-
打开并读取《爱丽丝梦游仙境》的文本到笔记本中。打印前 100 个字符的摘录和文本文件的总长度。
-
创建一个包含数据集中不重复字符的列表变量。然后,创建一个字典,将每个字符映射到一个整数,其中字符将是键,整数将是值。
-
将数据集中的每个字母编码为它们配对的整数。打印前 100 个编码字符和编码版本的总长度。
-
创建一个函数,接受一个批次并将其编码为一个独热矩阵。
-
创建定义网络架构的类。该类应包含一个额外的函数,用于初始化 LSTM 层的状态。
-
确定要从数据集中创建的批次数,记住每个批次应包含 100 个序列,每个序列长度为 50。接下来,将编码数据拆分为 100 个序列。
-
使用 256 作为两个递归层的隐藏单元数来初始化您的模型。
-
定义损失函数和优化算法。使用 Adam 优化器和交叉熵损失。
-
训练网络 20 个时期,记住每个时期数据必须分成具有 50 个序列长度的批次。这意味着每个时期将有 100 个序列,每个长度为 50。
注意
请牢记,批次不仅适用于输入和目标,其中后者是前者的副本,但向前推进一步。
-
绘制损失函数随时间的进展。
-
使用以下句子作为训练模型的开头,并完成句子:"So she was considering in her own mind "
注意
本活动的解决方案可以在第 223 页找到。
自然语言处理(NLP)
计算机擅长分析标准化数据,例如财务记录或存储在表格中的数据库。事实上,它们比人类更擅长这样做,因为它们能够同时分析数百个变量。另一方面,人类擅长分析非结构化数据,例如语言,这是计算机在没有一套规则的情况下理解得不太好的事情。
有鉴于此,计算机在处理人类语言方面最大的挑战是,即使计算机在非常长时间内在非常大的数据集上经过训练后能够很好地分析人类语言,它们仍然无法理解句子背后的真实含义,因为它们既不直观,也无法读懂行间之义。
这意味着,虽然人类能理解这样一句话:“昨晚他火力全开。多么精彩的比赛!”指的是某种体育运动中某位运动员的表现,但计算机会按照字面意义理解它,即将其解释为昨晚某人确实着火了。
自然语言处理(NLP)是人工智能(AI)的一个子领域,通过使计算机能够理解人类语言来运作。虽然可能总是人类在这项任务上更胜一筹,但 NLP 的主要目标是使计算机在理解人类语言方面更接近人类。
思路是创建专注于理解人类语言特定领域的模型,如机器翻译和文本摘要。这种任务的专业化有助于计算机开发出能够解决现实数据问题的模型,而无需一次处理所有人类语言的复杂性。
当今非常流行的人类语言理解领域之一是情感分析。
情感分析
总体而言,情感分析包括理解输入文本背后的情感。随着社交媒体平台的兴起,每天公司接收到的消息和评论数量呈指数级增长,情感分析因此变得越来越受欢迎。这使得实时手动检查和回复每条消息的任务变得不可能,这可能对公司形象造成损害。
情感分析专注于提取句子的关键组成部分,同时忽略细节。这有助于解决两个主要需求:
-
辨认顾客最关心的产品或服务的关键方面。
-
提取每个方面背后的情感,以确定哪些方面引起了积极和消极反应,并因此能够相应地进行转化:
图 6.19:一条推特的示例
从上图可见,进行情感分析的模型可能会获取以下信息:
"Debates" 作为推文的主要话题。
"Sad" 表示从中产生的情感。
"America" 作为该话题情感的主要地点。
正如您所见,情感分析的概念对于任何具有在线存在的公司都可能至关重要,因为它将能够对那些需要立即关注的评论作出令人惊讶的快速反应,并且具有与人类相似的精度。
作为情感分析的示例用途,一些公司可能选择对他们每天接收的大量消息执行情感分析,以便为那些包含投诉或负面情绪的消息优先进行响应。这不仅有助于缓解特定客户的负面情绪;还有助于公司迅速改进他们的错误并与客户建立信任关系。
关于情感分析的自然语言处理(NLP)过程将在接下来的部分中进一步解释。我们将解释词嵌入的概念以及您可以执行的不同步骤来在 PyTorch 中开发这样一个模型,这将是本章最后活动的目标。
在 PyTorch 中的情感分析
在 PyTorch 中构建情感分析模型与我们迄今为止看到的 RNNs 非常相似。不同之处在于,这次文本数据将逐词进行处理。下面列出了构建这样一个模型所需的步骤。
预处理输入数据
与任何其他数据问题一样,首先将数据加载到代码中,记住不同数据类型使用不同的方法。除了将整套单词转换为小写之外,数据还经历了一些基本的转换,这将允许您将数据馈送到网络中。最常见的转换如下所示:
-
消除标点符号:在处理文本数据时,逐词进行自然语言处理时,去除任何标点符号是一个良好的实践。这是为了避免将同一个单词视为两个不同的单词,因为其中一个后面跟着句点、逗号或任何其他特殊字符。一旦实现了这一点,就可以定义一个包含词汇表(即输入文本中存在的所有单词集合)的列表。
-
数字标签:与先前解释的字符映射过程类似,词汇表中的每个单词都映射到一个整数,该整数将用于替换输入文本中的单词以供输入到网络中:
Equation 6.20: 单词和数字的映射
与执行独热编码不同,PyTorch 允许您在包含网络架构的类内部定义一行代码,该代码可以嵌入单词,这将在接下来解释。
构建架构
再次强调,定义网络架构的过程与我们迄今为止所学的相似。然而,正如前面提到的,网络还应包括一个嵌入层,该层将接收已转换为数值表示的输入数据,并为每个词分配一个相关度。也就是说,在训练过程中,将更新这些值,直到最相关的词被赋予更高的权重。
接下来,显示了一个架构示例:
class LSTM(nn.Module):
def __init__(self, vocab_size, embed_dim, hidden_size, n_layers):
super().__init__()
self.hidden_size = hidden_size
self.embedding = nn.Embedding(vocab_size, embed_dim)
self.lstm = nn.LSTM(embed_dim, hidden_size, n_layers
self.output = nn.Linear(hidden_size, 1)
def forward(self, x, states):
out = self.embedding(x)
out, states = self.lstm(out, states)
out = out.contiguous().view(-1, self.hidden_size)
out = self.output(out)
return out, states
如您所见,嵌入层将以整个词汇表的长度和由用户设置的嵌入维度作为参数。这个嵌入维度将是 LSTM 层的输入大小,其余的架构将与之前保持一致。
训练模型
最后,在定义损失函数和优化算法之后,训练模型的过程与其他神经网络相同。数据可能根据研究的需求和目的分为不同的集合。为了将数据分成批次,定义了一些时期和方法。网络的内存通常在数据的批次之间保持不变,但在每个时期都会初始化为零。通过对数据批次调用模型来获得网络的输出,然后计算损失函数并优化参数。
活动 13:进行情感分析的自然语言处理
注意
包含以下活动将使用的数据集的文本文件可以在本书的 GitHub 仓库中找到。仓库的 URL 在本章的介绍中提到。它也可以在archive.ics.uci.edu/ml/datasets/Sentiment+Labelled+Sentences
上找到。
在以下活动中,将使用 LSTM 网络分析一组评论以确定它们背后的情感。假设以下情景:您在互联网提供商的公共关系部门工作,并且审查公司社交媒体个人资料上每个查询的过程需要相当长的时间。最大的问题是那些对服务有问题的客户比没有问题的客户更加缺乏耐心,因此您需要优先处理他们的回应。由于您在空闲时间喜欢编程,您决定尝试构建一个能够确定消息是负面还是正面的神经网络:
注意
需要提到的是,本活动中的数据并未分为不同的数据集,这些数据集允许对模型进行微调和测试。这是因为活动的主要重点是展示创建一个能够执行情感分析的模型的过程。
-
导入所需的库。
-
加载包含 1,000 条亚马逊产品评论及其标签(0 表示负面评论,1 表示正面评论)的数据集。将数据分成两个变量:一个包含评论,另一个包含标签。
-
从评论中删除标点符号。
-
创建一个包含整个评论集词汇的变量。另外,创建一个将每个单词映射到整数的字典,其中单词是键,整数是值。
-
通过用其对应的整数替换评论中的每个单词来对评论数据进行编码。
-
创建一个包含网络架构的类。确保包含嵌入层。
注意
在训练过程中,数据不会以批次方式提供,因此在前向函数中没有必要返回状态。然而,这并不意味着模型没有记忆,而是记忆用于处理每个评论,因为一个评论不依赖于下一个评论。
-
使用 64 个嵌入维度和 128 个神经元初始化模型,以及 3 层 LSTM。
-
定义损失函数、优化算法和训练的 epoch 数。例如,可以使用二元交叉熵损失作为损失函数,Adam 优化器,并训练 10 个 epoch。
-
创建一个
for
循环,遍历不同的 epoch 和每个单独的评论。对于每个评论,进行预测,计算损失函数,并更新网络的参数。另外,计算训练数据的准确率。 -
绘制损失函数和准确率随时间的进展情况。
注意
此活动的解决方案可以在第 228 页找到。
概要
在本章中,讨论了 RNN。这种类型的神经网络是为了解决序列数据问题而开发的。这意味着单个实例并不包含所有相关信息,因为它依赖于前面实例的信息。
有几个应用程序符合此类描述。例如,文本(或语音)的特定部分如果没有其余文本的上下文可能意义不大。然而,尽管自然语言处理主要探索了 RNN,但在其他应用中文本的上下文仍然很重要,比如预测、视频处理或音乐相关问题。
RNN 的工作方式非常聪明;网络不仅输出结果,还输出一个或多个通常称为记忆的值。这个记忆值被用作未来预测的输入。
还存在不同类型的 RNN 配置,这些配置基于架构的输入和输出。例如,在一对多配置中,多个示例(如单词)可能导致单个最终输出(例如评论是否粗鲁)。在一对多配置中,多个输入将导致多个输出,如语言翻译问题中,输入单词和输出单词不同。
在处理涉及非常大序列的数据问题时,传统的循环神经网络(RNNs)存在一个称为梯度消失的问题,其中梯度变得极小,以至于不再对网络的学习过程做出贡献,通常发生在网络的较早层,导致网络无法具有长期记忆。
为了解决这个问题,开发了 LSTM 网络。这种网络架构能够存储两种类型的记忆,因此得名。此外,在这种网络中发生的数学计算也使其能够遗忘信息——只存储过去的相关信息。
最后,解释了一个非常流行的自然语言处理问题:情感分析。在这个问题中,重要的是理解文本提取背后的情感。对于机器来说,这是一个非常困难的问题,考虑到人类可以使用许多不同的词语和表达形式(例如讽刺)来描述事件背后的情感。然而,由于社交媒体使用的增加,这导致了对文本数据更快处理的需求增加,这个问题因此在大公司中变得非常流行,它们投入了大量时间和资金来创建几种近似解决方案,正如本章的最后一部分所展示的。
附录
关于
这一部分包括帮助学生执行书中活动的步骤。它包括详细的步骤,学生需要执行这些步骤以实现活动的目标。
第一章:深度学习和 PyTorch 简介
活动 1:创建单层神经网络
解决方案:
-
导入所需库:
import torch import torch.nn as nn import matplotlib.pyplot as plt
-
创建随机值的虚拟输入数据 (
x
) 和仅包含 0 和 1 的虚拟目标数据 (y
)。将数据存储在 PyTorch 张量中。张量x
应该大小为 (100,5),而y
的大小应为 (100,1):x = torch.randn(100,5) y = torch.randint(0, 2, (100, 1)).type(torch.FloatTensor)
-
定义模型的架构并将其存储在名为
model
的变量中。记得创建一个单层模型:model = nn.Sequential(nn.Linear(5, 1), nn.Sigmoid())
定义要使用的损失函数。使用均方误差损失函数:
loss_function = torch.nn.MSELoss()
定义模型的优化器。使用 Adam 优化器和学习率为 0.01:
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
-
运行 100 次优化迭代。在每次迭代中,打印并保存损失值:
losses = [] for i in range(100): y_pred = model(x) loss = loss_function(y_pred, y) print(loss.item()) losses.append(loss.item()) optimizer.zero_grad() loss.backward() optimizer.step()
最终损失应约为 0.238。
-
打印最终权重和偏置的值。应该有五个权重(每个输入数据特征一个)和一个偏置值:
model.state_dict()
-
制作线图以显示每次迭代步骤的损失值:
plt.plot(range(0,100), losses) plt.show()
结果图应该如下所示:
图 1.8:训练过程中的损失函数
第二章:神经网络的构建模块
活动 2:执行数据准备
解决方案:
-
导入所需库:
import pandas as pd
-
使用 pandas 加载文本文件。考虑到之前下载的文本文件与 CSV 文件的格式相同,可以使用
read_csv()
函数读取它。确保将 header 参数更改为None
:data = pd.read_csv("YearPredictionMSD.txt", header=None, nrows=50000) data.head()
注意
为避免内存限制,在读取文本文件时使用
nrows
参数以读取整个数据集的较小部分。在上述示例中,我们读取了前 50,000 行。 -
验证数据集中是否存在任何定性数据。
data.iloc[0,:]
-
检查缺失值。
如果在之前用于此目的的代码行中添加额外的
sum()
函数,则将获得整个数据集中缺失值的总和,而不区分列:data.isnull().sum().sum()
-
检查异常值:
outliers = {} for i in range(data.shape[1]): min_t = data[data.columns[i]].mean() - ( 3 * data[data.columns[i]].std()) max_t = data[data.columns[i]].mean() + ( 3 * data[data.columns[i]].std()) count = 0 for j in data[data.columns[i]]: if j < min_t or j > max_t: count += 1 percentage = count/data.shape[0] outliers[data.columns[i]] = "%.3f" % percentage print(outliers)
-
将特征与目标数据分开:
X = data.iloc[:, 1:] Y = data.iloc[:, 0]
-
使用标准化方法对特征数据进行重新缩放:
X = (X - X.mean())/X.std() X.head()
-
将数据分割为三组:训练集、验证集和测试集。使用您偏好的方法:
from sklearn.model_selection import train_test_split X_shuffle = X.sample(frac=1) Y_shuffle = Y.sample(frac=1) x_new, x_test, y_new, y_test = train_test_split(X_shuffle, Y_shuffle, test_size=0.2, random_state=0) dev_per = x_test.shape[0]/x_new.shape[0] x_train, x_dev, y_train, y_dev = train_test_split(x_new, y_new, test_size=dev_per, random_state=0)
结果的形状应该如下所示:
(30000, 90) (30000, )
(10000, 90) (10000, )
(10000, 90) (10000, )
活动 3:执行数据准备
解决方案:
-
导入所需库:
import torch import torch.nn as nn
-
将先前活动中创建的所有三组数据的特征与目标分离。将 DataFrame 转换为张量:
x_train = torch.tensor(x_train.values).float() y_train = torch.tensor(y_train.values).float() x_dev = torch.tensor(x_dev.values).float() y_dev = torch.tensor(y_dev.values).float() x_test = torch.tensor(x_test.values).float() y_test = torch.tensor(y_test.values).float()
-
定义网络的架构。可以尝试不同的层数和每层单元的组合:
model = nn.Sequential(nn.Linear(x_train.shape[1], 10), nn.ReLU(), nn.Linear(10, 7), nn.ReLU(), nn.Linear(7, 5), nn.ReLU(), nn.Linear(5, 1))
-
定义损失函数和优化器算法:
loss_function = torch.nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
-
使用
for
循环训练网络进行 100 次迭代步骤:for i in range(100): y_pred = model(x_train) loss = loss_function(y_pred, y_train) print(i, loss.item()) optimizer.zero_grad() loss.backward() optimizer.step()
-
通过对测试集的第一个实例进行预测并将其与实际值进行比较来测试您的模型:
pred = model(x_test[0]) print(y_test[0], pred)
您的输出应类似于此:
图 2.29:活动输出
第三章:使用深度神经网络的分类问题
活动 4:构建人工神经网络
解决方案:
-
导入以下库:
import pandas as pd import numpy as np from sklearn.model_selection import train_test_split from sklearn.utils import shuffle from sklearn.metrics import accuracy_score import torch from torch import nn, optim import torch.nn.functional as F import matplotlib.pyplot as plt
-
读取之前准备好的数据集,该数据集应命名为
dccc_prepared.csv
:data = pd.read_csv("dccc_prepared.csv")
-
将特征从目标分离开:
X = data.iloc[:,:-1] y = data["default payment next month"]
-
使用 scikit-learn 的
train_test_split
函数将数据集分割为训练、验证和测试集。使用 60/20/20%的分割比例。将random_state
设置为 0:X_new, X_test, y_new, y_test = train_test_split(X, y, test_size=0.2, random_state=0) dev_per = X_test.shape[0]/X_new.shape[0] X_train, X_dev, y_train, y_dev = train_test_split(X_new, y_new, test_size=dev_per, random_state=0)
各个集合的最终形状如下所示:
Training sets: (28036, 22) (28036,) Validation sets: (9346, 22) (9346,) Testing sets: (9346, 22) (9346,)
-
将验证和测试集转换为张量,考虑到特征矩阵应为浮点类型,而目标矩阵不应为浮点类型。
暂时保持训练集未转换,因为它们将经历进一步的转换。
X_dev_torch = torch.tensor(X_dev.values).float() y_dev_torch = torch.tensor(y_dev.values) X_test_torch = torch.tensor(X_test.values).float() y_test_torch = torch.tensor(y_test.values)
-
构建自定义模块类来定义网络的层。包括一个前向函数,该函数指定将应用于每个层输出的激活函数。对于所有层使用 ReLU,输出层使用
log_softmax
:class Classifier(nn.Module): def __init__(self, input_size): super().__init__() self.hidden_1 = nn.Linear(input_size, 10) self.hidden_2 = nn.Linear(10, 10) self.hidden_3 = nn.Linear(10, 10) self.output = nn.Linear(10, 2) def forward(self, x): z = F.relu(self.hidden_1(x)) z = F.relu(self.hidden_2(z)) z = F.relu(self.hidden_3(z)) out = F.log_softmax(self.output(z), dim=1) return out
-
定义训练模型所需的所有变量。将训练周期设置为 50,批量大小设置为 128。使用学习率为 0.001:
model = Classifier(X_train.shape[1]) criterion = nn.NLLLoss() optimizer = optim.Adam(model.parameters(), lr=0.001) epochs = 50 batch_size = 128
-
使用训练集数据训练网络。使用验证集来衡量性能。在每个周期中保存训练和验证集的损失和准确率:
train_losses, dev_losses, train_acc, dev_acc= [], [], [], [] for e in range(epochs): X_, y_ = shuffle(X_train, y_train) running_loss = 0 running_acc = 0 iterations = 0 for i in range(0, len(X_), batch_size): iterations += 1 b = i + batch_size X_batch = torch.tensor(X_.iloc[i:b,:].values).float() y_batch = torch.tensor(y_.iloc[i:b].values) log_ps = model(X_batch) loss = criterion(log_ps, y_batch) optimizer.zero_grad() loss.backward() optimizer.step() running_loss += loss.item() ps = torch.exp(log_ps) top_p, top_class = ps.topk(1, dim=1) running_acc += accuracy_score(y_batch, top_class) dev_loss = 0 acc = 0 with torch.no_grad(): log_dev = model(X_dev_torch) dev_loss = criterion(log_dev, y_dev_torch) ps_dev = torch.exp(log_dev) top_p, top_class_dev = ps_dev.topk(1, dim=1) acc = accuracy_score(y_dev_torch, top_class_dev) train_losses.append(running_loss/iterations) dev_losses.append(dev_loss) train_acc.append(running_acc/iterations) dev_acc.append(acc) print("Epoch: {}/{}.. ".format(e+1, epochs), "Training Loss: {:.3f}.. ".format(running_loss/iterations), "Validation Loss: {:.3f}.. ".format(dev_loss), "Training Accuracy: {:.3f}.. ".format(running_acc/ iterations), "Validation Accuracy: {:.3f}".format(acc))
-
绘制两组数据的损失:
plt.plot(train_losses, label='Training loss') plt.plot(dev_losses, label='Validation loss') plt.legend(frameon=False) plt.show()
生成的图表应该与此处类似,尽管由于训练数据的洗牌可能会导致略有不同的结果。
图 3.10:显示训练和验证损失的图表
-
绘制两组数据的准确率:
plt.plot(train_acc, label="Training accuracy") plt.plot(dev_acc, label="Validation accuracy") plt.legend(frameon=False) plt.show()
这是从此代码片段生成的图表:
图 3.11:显示集合准确度的图表
活动 5:提升模型性能
解决方案:
-
导入与上一个活动中相同的库:
import pandas as pd import numpy as np from sklearn.model_selection import train_test_split from sklearn.utils import shuffle from sklearn.metrics import accuracy_score import torch from torch import nn, optim import torch.nn.functional as F import matplotlib.pyplot as plt torch.manual_seed(0)
-
加载数据并将特征与目标分离。然后,使用 60:20:20 的比例将数据分割为三个子集(训练、验证和测试),最后,像在上一个活动中一样将验证和测试集转换为 PyTorch 张量:
data = pd.read_csv("dccc_prepared.csv") X = data.iloc[:,:-1] y = data["default payment next month"] X_new, X_test, y_new, y_test = train_test_split(X, y, test_size=0.2, random_state=0) dev_per = X_test.shape[0]/X_new.shape[0] X_train, X_dev, y_train, y_dev = train_test_split(X_new, y_new, test_size=dev_per, random_state=0) X_dev_torch = torch.tensor(X_dev.values).float() y_dev_torch = torch.tensor(y_dev.values) X_test_torch = torch.tensor(X_test.values).float() y_test_torch = torch.tensor(y_test.values)
-
考虑到模型存在高偏差问题,重点应该是增加训练周期或者通过增加额外的层或单元来扩展网络的规模。
目标应该是将验证集的准确率近似到 80%。
下面的代码片段来自经过多次微调后表现最佳的模型:
# class defining model's architecture and operations between layers class Classifier(nn.Module): def __init__(self, input_size): super().__init__() self.hidden_1 = nn.Linear(input_size, 100) self.hidden_2 = nn.Linear(100, 100) self.hidden_3 = nn.Linear(100, 50) self.hidden_4 = nn.Linear(50,50) self.output = nn.Linear(50, 2) self.dropout = nn.Dropout(p=0.1) #self.dropout_2 = nn.Dropout(p=0.1) def forward(self, x): z = self.dropout(F.relu(self.hidden_1(x))) z = self.dropout(F.relu(self.hidden_2(z))) z = self.dropout(F.relu(self.hidden_3(z))) z = self.dropout(F.relu(self.hidden_4(z))) out = F.log_softmax(self.output(z), dim=1) return out # parameters definition model = Classifier(X_train.shape[1]) criterion = nn.NLLLoss() optimizer = optim.Adam(model.parameters(), lr=0.001) epochs = 3000 batch_size = 128 # training process train_losses, dev_losses, train_acc, dev_acc= [], [], [], [] x_axis = [] for e in range(1, epochs + 1): X_, y_ = shuffle(X_train, y_train) running_loss = 0 running_acc = 0 iterations = 0 for i in range(0, len(X_), batch_size): iterations += 1 b = i + batch_size X_batch = torch.tensor(X_.iloc[i:b,:].values).float() y_batch = torch.tensor(y_.iloc[i:b].values) log_ps = model(X_batch) loss = criterion(log_ps, y_batch) optimizer.zero_grad() loss.backward() optimizer.step() running_loss += loss.item() ps = torch.exp(log_ps) top_p, top_class = ps.topk(1, dim=1) running_acc += accuracy_score(y_batch, top_class) dev_loss = 0 acc = 0 # Turn off gradients for validation, saves memory and computations with torch.no_grad(): log_dev = model(X_dev_torch) dev_loss = criterion(log_dev, y_dev_torch) ps_dev = torch.exp(log_dev) top_p, top_class_dev = ps_dev.topk(1, dim=1) acc = accuracy_score(y_dev_torch, top_class_dev) if e%50 == 0 or e == 1: x_axis.append(e) train_losses.append(running_loss/iterations) dev_losses.append(dev_loss) train_acc.append(running_acc/iterations) dev_acc.append(acc) print("Epoch: {}/{}.. ".format(e, epochs), "Training Loss: {:.3f}.. ".format(running_loss/ iterations), "Validation Loss: {:.3f}.. ".format(dev_loss), "Training Accuracy: {:.3f}.. ".format(running_acc/ iterations), "Validation Accuracy: {:.3f}".format(acc))
注意
与此活动相关的 Jupyter 笔记本可以在之前分享的 GitHub 存储库中找到。在那里,您将找到微调模型的不同尝试及其结果。表现最佳的模型位于笔记本的末尾。
-
绘制两组数据的损失和准确性图表:
注意
plt.plot(x_axis,train_losses, label='Training loss') plt.plot(x_axis, dev_losses, label='Validation loss') plt.legend(frameon=False) plt.show()
Figure 3.12: 显示损失的图表
plt.plot(x_axis, train_acc, label="Training accuracy") plt.plot(x_axis, dev_acc, label="Validation accuracy") plt.legend(frameon=False) plt.show()
Figure 3.13: 显示准确性的图表
-
使用表现最佳的模型,在测试集上进行预测(在微调过程中不应使用)。通过计算模型在此集合上的准确性,将预测与实际情况进行比较:
model.eval() test_pred = model(X_test_torch) test_pred = torch.exp(test_pred) top_p, top_class_test = test_pred.topk(1, dim=1) acc_test = accuracy_score(y_test_torch, top_class_test)
通过上述模型架构和定义的参数,获得的准确率应约为 80%。
活动 6:使用您的模型
解决方案:
-
打开您用于上一个活动的 Jupyter 笔记本。
-
保存一个 Python 文件,其中包含定义表现最佳模块架构的类。确保导入 PyTorch 所需的库和模块。将其命名为
final_model.py
。文件应如下所示:
Figure 3.14: final_model.py 的屏幕截图
-
保存表现最佳的模型。确保保存输入单元的信息以及模型的参数。将其命名为
checkpoint.pth
:checkpoint = {"input": X_train.shape[1], "state_dict": model.state_dict()} torch.save(checkpoint, "checkpoint.pth")
-
打开一个新的 Jupyter 笔记本。
-
导入 PyTorch 以及先前创建的 Python 文件:
import torch import final_model
-
创建一个加载模型的函数:
def load_model_checkpoint(path): checkpoint = torch.load(path) model = final_model.Classifier(checkpoint["input"]) model.load_state_dict(checkpoint["state_dict"]) return model model = load_model_checkpoint("checkpoint.pth")
-
将以下张量输入到模型中进行预测:
example = torch.tensor([[0.0606, 0.5000, 0.3333, 0.4828, 0.4000, 0.4000, 0.4000, 0.4000, 0.4000, 0.4000, 0.1651, 0.0869, 0.0980, 0.1825, 0.1054, 0.2807, 0.0016, 0.0000, 0.0033, 0.0027, 0.0031, 0.0021]]).float() pred = model(example) pred = torch.exp(pred) top_p, top_class_test = pred.topk(1, dim=1)
通过打印
top_class_test
,我们获得模型的预测结果,这里等于 1(是)。 -
使用 JIT 模块转换模型:
traced_script = torch.jit.trace(model, example, check_trace=False)
-
通过将以下信息输入到模型的跟踪脚本中进行预测:
prediction = traced_script(example) prediction = torch.exp(prediction) top_p_2, top_class_test_2 = prediction.topk(1, dim=1)
通过打印
top_class_test_2
,我们从模型的跟踪脚本表示中获取预测结果,再次等于 1(是)。
第四章:卷积神经网络
活动 7:为图像分类问题构建 CNN
解决方案:
-
导入以下库:
import numpy as np import torch from torch import nn, optim import torch.nn.functional as F from torchvision import datasets import torchvision.transforms as transforms from torch.utils.data.sampler import SubsetRandomSampler from sklearn.metrics import accuracy_score import matplotlib.pyplot as plt
-
设置在数据上执行的转换,即将数据转换为张量并标准化像素值:
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
-
设置一个批次大小为 100 张图像,并从
CIFAR10
数据集下载训练和测试数据:batch_size = 100 train_data = datasets.CIFAR10('data', train=True, download=True, transform=transform) test_data = datasets.CIFAR10('data', train=False, download=True, transform=transform)
-
使用 20%的验证集大小,定义训练和验证采样器,用于将数据集分成这两组:
dev_size = 0.2 idx = list(range(len(train_data))) np.random.shuffle(idx) split_size = int(np.floor(dev_size * len(train_data))) train_idx, dev_idx = idx[split_size:], idx[:split_size] train_sampler = SubsetRandomSampler(train_idx) dev_sampler = SubsetRandomSampler(dev_idx)
-
使用
DataLoader()
函数定义每组数据的批处理:train_loader = torch.utils.data.DataLoader(train_data, batch_size=batch_size, sampler=train_sampler) dev_loader = torch.utils.data.DataLoader(train_data, batch_size=batch_size, sampler=dev_sampler) test_loader = torch.utils.data.DataLoader(test_data, batch_size=batch_size)
-
定义网络的架构。使用以下信息来完成这一步骤:
-
Conv1:一个卷积层,以彩色图像作为输入,并通过大小为 3 的 10 个滤波器进行处理。填充和步长都应设置为 1。
-
Conv2:一个卷积层,将输入数据通过大小为 3 的 20 个滤波器进行处理。填充和步长都应设置为 1。
-
Conv3:一个卷积层,将输入数据通过大小为 3 的 40 个滤波器进行处理。填充和步长都应设置为 1。
-
在每个卷积层后使用 ReLU 激活函数。
-
每个卷积层后都有一个池化层,滤波器大小和步长均为 2。
-
在展平图像后设置的 20% 的 dropout 项。
-
Linear1:一个完全连接的层,接收前一层展平矩阵作为输入,并生成 100 个单元的输出。此层使用 ReLU 激活函数。这里的 dropout 项设置为 20%。
-
Linear2:一个完全连接的层,生成 10 个输出,每个类标签一个。输出层使用
log_softmax
激活函数:class CNN(nn.Module): def __init__(self): super(CNN, self).__init__() self.conv1 = nn.Conv2d(3, 10, 3, 1, 1) self.conv2 = nn.Conv2d(10, 20, 3, 1, 1) self.conv3 = nn.Conv2d(20, 40, 3, 1, 1) self.pool = nn.MaxPool2d(2, 2) self.linear1 = nn.Linear(40 * 4 * 4, 100) self.linear2 = nn.Linear(100, 10) self.dropout = nn.Dropout(0.2) def forward(self, x): x = self.pool(F.relu(self.conv1(x))) x = self.pool(F.relu(self.conv2(x))) x = self.pool(F.relu(self.conv3(x))) x = x.view(-1, 40 * 4 * 4) x = self.dropout(x) x = F.relu(self.linear1(x)) x = self.dropout(x) x = F.log_softmax(self.linear2(x), dim=1) return x
-
-
定义训练模型所需的所有参数。将其训练 100 个 epochs:
model = CNN() loss_function = nn.NLLLoss() optimizer = optim.Adam(model.parameters(), lr=0.001) epochs = 50
-
训练你的网络,并确保保存训练和验证集的损失和准确性值:
train_losses, dev_losses, train_acc, dev_acc= [], [], [], [] x_axis = [] for e in range(1, epochs+1): losses = 0 acc = 0 iterations = 0 model.train() for data, target in train_loader: iterations += 1 pred = model(data) loss = loss_function(pred, target) optimizer.zero_grad() loss.backward() optimizer.step() losses += loss.item() p = torch.exp(pred) top_p, top_class = p.topk(1, dim=1) acc += accuracy_score(target, top_class) dev_losss = 0 dev_accs = 0 iter_2 = 0 if e%5 == 0 or e == 1: x_axis.append(e) with torch.no_grad(): model.eval() for data_dev, target_dev in dev_loader: iter_2 += 1 dev_pred = model(data_dev) dev_loss = loss_function(dev_pred, target_dev) dev_losss += dev_loss.item() dev_p = torch.exp(dev_pred) top_p, dev_top_class = dev_p.topk(1, dim=1) dev_accs += accuracy_score(target_dev, dev_top_class) train_losses.append(losses/iterations) dev_losses.append(dev_losss/iter_2) train_acc.append(acc/iterations) dev_acc.append(dev_accs/iter_2) print("Epoch: {}/{}.. ".format(e, epochs), "Training Loss: {:.3f}.. ".format(losses/iterations), "Validation Loss: {:.3f}.. ".format(dev_losss/iter_2), "Training Accuracy: {:.3f}.. ".format(acc/iterations), "Validation Accuracy: {:.3f}".format(dev_accs/iter_2))
-
绘制两组集合的损失和准确性:
plt.plot(x_axis,train_losses, label='Training loss') plt.plot(x_axis, dev_losses, label='Validation loss') plt.legend(frameon=False) plt.show()
结果图应该类似于这样:
图 4.19:显示集合损失的结果图
plt.plot(x_axis, train_acc, label="Training accuracy") plt.plot(x_axis, dev_acc, label="Validation accuracy") plt.legend(frameon=False) plt.show()
准确性应该类似于下一个图表:
图 4.20:显示集合准确性的结果图
可以看出,在第十五个 epoch 后,过拟合开始影响模型。
-
检查模型在测试集上的准确性:
model.eval() iter_3 = 0 acc_test = 0 for data_test, target_test in test_loader: iter_3 += 1 test_pred = model(data_test) test_pred = torch.exp(test_pred) top_p, top_class_test = test_pred.topk(1, dim=1) acc_test += accuracy_score(target_test, top_class_test) print(acc_test/iter_3)
测试集上的准确性非常接近于其他两个集合的准确性,这意味着模型在未见数据上同样表现出色。应该约为 72%。
活动 8:实施数据增强
解决方案:
-
复制前一个活动中的笔记本。
要解决此活动,除了在下一步中提到的变量定义外,不会更改任何代码。
-
更改
transform
变量的定义,除了将数据标准化并转换为张量外,还包括以下转换:-
对于训练/验证集,使用
RandomHorizontalFlip
函数,概率为 50%(0.5),以及RandomGrayscale
函数,概率为 10%(0.1)。 -
对于测试集,不添加任何其他转换:
transform = { "train": transforms.Compose([ transforms.RandomHorizontalFlip(0.5), transforms.RandomGrayscale(0.1), transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]), "test": transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])}
-
-
将模型训练 100 个 epochs。
在训练和验证集上的损失和准确性的结果图应与以下显示的图表类似:
图 4.21:显示集合损失的结果图
图 4.22:显示集合准确性的结果图
通过增加数据增强,可以改善模型的性能,并减少过拟合现象。
-
计算模型在测试集上的准确率。
模型在测试集上的性能提升到了约 76%。
活动 9:实现批量归一化
解决方案:
-
复制上一个活动的笔记本。
-
在每个卷积层以及第一个全连接层中添加批量归一化。
网络的最终架构应如下所示:
class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.conv1 = nn.Conv2d(3, 10, 3, 1, 1) self.norm1 = nn.BatchNorm2d(10) self.conv2 = nn.Conv2d(10, 20, 3, 1, 1) self.norm2 = nn.BatchNorm2d(20) self.conv3 = nn.Conv2d(20, 40, 3, 1, 1) self.norm3 = nn.BatchNorm2d(40) self.pool = nn.MaxPool2d(2, 2) self.linear1 = nn.Linear(40 * 4 * 4, 100) self.norm4 = nn.BatchNorm1d(100) self.linear2 = nn.Linear(100, 10) self.dropout = nn.Dropout(0.2) def forward(self, x): x = self.pool(self.norm1(F.relu(self.conv1(x)))) x = self.pool(self.norm2(F.relu(self.conv2(x)))) x = self.pool(self.norm3(F.relu(self.conv3(x)))) x = x.view(-1, 40 * 4 * 4) x = self.dropout(x) x = self.norm4(F.relu(self.linear1(x))) x = self.dropout(x) x = F.log_softmax(self.linear2(x), dim=1) return x
-
将模型训练 100 个 epochs。
在训练和验证集上显示的损失和准确率的结果图应与接下来显示的图表类似:
图 4.23:显示损失的结果图
图 4.24:显示损失的结果图
尽管模型再次出现过拟合问题,但可以看到两组数据的性能都有所提升。
注意
虽然本章未探讨此项内容,但理想的步骤将是在网络架构中添加 dropout,以减少高方差。随时尝试,看看能否进一步提升性能。
-
计算模型在测试集上的准确率。
模型在测试集上的准确率应该在 78%左右。
第五章:风格转移
活动 10:执行风格转移
解决方案:
注意
为了能够在许多迭代(30,000 次)中运行此活动,使用了 GPU。根据这一点,可以在 GitHub 的存储库中找到适用于 GPU 的代码副本。
-
导入所需的库:
import numpy as np import torch from torch import nn, optim from PIL import Image import matplotlib.pyplot as plt from torchvision import transforms, models
-
指定对输入图像执行的转换。确保将它们调整为相同大小,转换为张量并进行归一化:
imsize = 224 loader = transforms.Compose([ transforms.Resize(imsize), transforms.ToTensor(), transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225))])
-
定义一个图像加载函数。它应打开并加载图像。调用图像加载函数加载两幅输入图像:
def image_loader(image_name): image = Image.open(image_name) image = loader(image).unsqueeze(0) return image content_img = image_loader("images/landscape.jpg") style_img = image_loader("images/monet.jpg")
-
为了能够显示图像,设置转换以恢复图像的归一化,并将张量转换为 PIL 图像:
unloader = transforms.Compose([ transforms.Normalize((-0.485/0.229, -0.456/0.224, -0.406/0.225), (1/0.229, 1/0.224, 1/0.225)), transforms.ToPILImage()])
-
创建一个能够对张量执行前述变换的函数。为两幅图像调用该函数并绘制结果:
def tensor2image(tensor): image = tensor.clone() image = image.squeeze(0) image = unloader(image) return image plt.figure() plt.imshow(tensor2image(content_img)) plt.title("Content Image") plt.show() plt.figure() plt.imshow(tensor2image(style_img)) plt.title("Style Image") plt.show()
-
加载 VGG-19 模型:
model = models.vgg19(pretrained=True).features for param in model.parameters(): param.requires_grad_(False)
-
创建一个将相关层的索引(键)映射到名称(值)的字典。然后,创建一个函数来提取相关层的特征图。使用它们来提取两个输入图像的特征:
relevant_layers = {'0': 'conv1_1', '5': 'conv2_1', '10': 'conv3_1', '19': 'conv4_1', '21': 'conv4_2', '28': 'conv5_1'} def features_extractor(x, model, layers): features = {} for index, layer in model._modules.items(): if index in layers: x = layer(x) features[layers[index]] = x return features content_features = features_extractor(content_img, model, relevant_layers) style_features = features_extractor(style_img, model, relevant_layers)
-
计算样式特征的 Gram 矩阵。同时,创建初始目标图像:
style_grams = {} for i in style_features: layer = style_features[i] _, d1, d2, d3 = layer.shape features = layer.view(d1, d2 * d3) gram = torch.mm(features, features.t()) style_grams[i] = gram target_img = content_img.clone().requires_grad_(True)
-
设置不同风格层的权重,以及内容和风格损失的权重:
style_weights = {'conv1_1': 1., 'conv2_1': 0.8, 'conv3_1': 0.6, 'conv4_1': 0.4, 'conv5_1': 0.2} alpha = 1 beta = 1e6
-
运行 500 次迭代的模型。在开始训练模型之前,定义 Adam 优化算法,并使用 0.001 作为学习率。
注意
for i in range(1, iterations+1): target_features = features_extractor(target_img, model, relevant_layers) content_loss = torch.mean((target_features['conv4_2'] - content_features['conv4_2'])**2) style_losses = 0 for layer in style_weights: target_feature = target_features[layer] _, d1, d2, d3 = target_feature.shape target_reshaped = target_feature.view(d1, d2 * d3) target_gram = torch.mm(target_reshaped, target_reshaped.t()) style_gram = style_grams[layer] style_loss = style_weights[layer] * torch.mean((target_gram - style_gram)**2) style_losses += style_loss / (d1 * d2 * d3) total_loss = alpha * content_loss + beta * style_loss optimizer.zero_grad() total_loss.backward() optimizer.step() if i % print_statement == 0 or i == 1: print('Total loss: ', total_loss.item()) plt.imshow(tensor2image(target_img)) plt.show()
-
绘制内容和目标图像以比较结果:
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(20, 10)) ax1.imshow(tensor2image(content_img)) ax2.imshow(tensor2image(target_img)) plt.show()
以下是从此代码片段衍生的图:
图 5.10:内容和目标图像的绘图
第六章:使用 RNN 分析数据序列
活动 11:使用简单 RNN 进行时间序列预测
解决方案:
-
导入所需的库,如下所示:
import pandas as pd import matplotlib.pyplot as plt import torch from torch import nn, optim
-
将种子设置为 0 以在本书中重现结果,使用以下代码行:
torch.manual_seed(10)
-
载入数据集,然后将其切片,使其包含所有行但只有从索引 1 到 52 的列:
data = pd.read_csv("Sales_Transactions_Dataset_Weekly.csv") data = data.iloc[:,1:53] data.head()
-
绘制整个数据集中五种随机选择产品每周的销售交易。在进行随机抽样时使用随机种子 0,以获得与当前活动相同的结果:
plot_data = data.sample(5, random_state=0) x = range(1,53) plt.figure(figsize=(10,5)) for i,row in plot_data.iterrows(): plt.plot(x,row) plt.legend(plot_data.index) plt.xlabel("Weeks") plt.ylabel("Sales transactions per product") plt.show()
-
结果图应如下所示:
图 6.21:输出的绘图
-
创建将馈送到网络以创建模型的
inputs
和targets
变量。这些变量应具有相同的形状,并转换为 PyTorch 张量。 -
inputs
变量应包含所有产品所有周的数据,除了最后一周 — 因为模型的想法是预测这最后一周。 -
targets
变量应比inputs
变量超前一步 — 即targets
变量的第一个值应为inputs
变量的第二个值,依此类推,直到targets
变量的最后一个值(即inputs
变量之外的最后一周):data_train = data.iloc[:,:-1] inputs = torch.Tensor(data_train.values).unsqueeze(1) targets = data_train.shift(-1, axis="columns", fill_value=data.iloc[:,-1]).astype(dtype = "float32") targets = torch.Tensor(targets.values)
-
创建包含网络架构的类;请注意完全连接层的输出大小应为 1:
class RNN(nn.Module): def __init__(self, input_size, hidden_size, num_layers): super().__init__() self.hidden_size = hidden_size self.rnn = nn.RNN(input_size, hidden_size, num_layers, batch_first=True) self.output = nn.Linear(hidden_size, 1) def forward(self, x, hidden): out, hidden = self.rnn(x, hidden) out = out.view(-1, self.hidden_size) out = self.output(out) return out, hidden
-
初始化包含模型的
class
函数;然后,输入大小、每个循环层中的神经元数(10)和循环层数(1):model = RNN(data_train.shape[1], 10, 1)
-
定义损失函数、优化算法和要训练网络的时期数;例如,可以使用均方误差损失函数、Adam 优化器和 10,000 个时期:
loss_function = nn.MSELoss() optimizer = optim.Adam(model.parameters(), lr=0.001) epochs = 10000
-
使用
for
循环通过所有时期执行训练过程。在每个时期中,必须进行预测,并随后计算损失函数并优化网络参数。保存每个时期的损失:注意
losses = [] for i in range(1, epochs+1): hidden = None pred, hidden = model(inputs, hidden) loss = loss_function(targets, pred) optimizer.zero_grad() loss.backward() optimizer.step() losses.append(loss.item()) if i%1000 == 0: print("epoch: ", i, "=... Loss function: ", losses[-1])
-
绘制所有时期的损失如下:
x_range = range(len(losses)) plt.plot(x_range, losses) plt.xlabel("epochs") plt.ylabel("Loss function") plt.show()
-
结果图应如下所示:
图 6.22:显示所有时期损失的绘图
-
使用散点图,显示在训练过程的最后一个时期获得的预测值与基准真值(即上周的销售交易):
x_range = range(len(data)) target = data.iloc[:,-1].values.reshape(len(data),1) plt.figure(figsize=(15,5)) plt.scatter(x_range[:20], target[:20]) plt.scatter(x_range[:20], pred.detach().numpy()[:20]) plt.legend(["Ground truth", "Prediction"]) plt.xlabel("Product") plt.ylabel("Sales Transactions") plt.xticks(range(0, 20)) plt.show()
-
最终的图应如下所示:
图 6.23:显示预测的散点图
活动 12:使用 LSTM 网络生成文本
解决方案:
-
导入所需的库如下:
import math import numpy as np import matplotlib.pyplot as plt import torch from torch import nn, optim import torch.nn.functional as F
-
打开并读取《爱丽丝梦游仙境》中的文本到笔记本中。打印前 100 个字符的摘录和文本文件的总长度:
with open('alice.txt', 'r', encoding='latin1') as f: data = f.read() print("Extract: ", data[:50]) print("Length: ", len(data))
-
创建一个包含数据集中不重复字符的列表变量。接着,创建一个字典,将每个字符映射为一个整数,其中字符作为键,整数作为值:
chars = list(set(data)) indexer = {char: index for (index, char) in enumerate(chars)}
-
将数据集中的每个字母编码为它们的配对整数。打印前 100 个编码字符和您的数据集编码版本的总长度:
indexed_data = [] for c in data: indexed_data.append(indexer[c]) print("Indexed extract: ", indexed_data[:50]) print("Length: ", len(indexed_data))
-
创建一个函数,接收一个批次并将其编码为一个独热矩阵:
def index2onehot(batch): batch_flatten = batch.flatten() onehot_flat = np.zeros((batch.shape[0] * batch.shape[1],len(indexer))) onehot_flat[range(len(batch_flatten)), batch_flatten] = 1 onehot = onehot_flat.reshape((batch.shape[0], batch.shape[1], -1)) return onehot
-
创建定义网络架构的类。该类应包含一个额外的函数,初始化 LSTM 层的状态:
class LSTM(nn.Module): def __init__(self, char_length, hidden_size, n_layers): super().__init__() self.hidden_size = hidden_size self.n_layers = n_layers self.lstm = nn.LSTM(char_length, hidden_size, n_layers, batch_first=True) self.output = nn.Linear(hidden_size, char_length) def forward(self, x, states): out, states = self.lstm(x, states) out = out.contiguous().view(-1, self.hidden_size) out = self.output(out) return out, states def init_states(self, batch_size): hidden = next(self.parameters()).data.new( self.n_layers, batch_size, self.hidden_size).zero_() cell = next(self.parameters()).data.new(self.n_layers, batch_size, self.hidden_size). zero_() states = (hidden, cell) return states
-
确定要从数据集中创建的批次数量,每个批次应包含 100 个序列,每个序列长度为 50。然后,将编码数据拆分为 100 个序列:
n_seq = 100 ## Number of sequences per batch seq_length = 50 n_batches = math.floor(len(indexed_data) / n_seq / seq_length) total_length = n_seq * seq_length * n_batches x = indexed_data[:total_length] x = np.array(x).reshape((n_seq,-1))
-
初始化您的模型,使用 256 作为总共 2 个循环层的隐藏单元数:
model = LSTM(len(chars), 256, 2)
-
定义损失函数和优化算法。使用 Adam 优化器和交叉熵损失:
loss_function = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=0.001) epochs = 20
-
训练网络 20 个时期,每个时期的数据需分为序列长度为 50 的批次。这意味着每个时期将有 100 个批次,每个批次有一个长度为 50 的序列:
losses = [] for e in range(1, epochs+1): states = model.init_states(n_seq) batch_loss = [] for b in range(0, x.shape[1], seq_length): x_batch = x[:,b:b+seq_length] if b == x.shape[1] - seq_length: y_batch = x[:,b+1:b+seq_length] y_batch = np.hstack((y_batch, indexer["."] * np.ones((y_batch.shape[0],1)))) else: y_batch = x[:,b+1:b+seq_length+1] x_onehot = torch.Tensor(index2onehot(x_batch)) y = torch.Tensor(y_batch).view(n_seq * seq_length) pred, states = model(x_onehot, states) loss = loss_function(pred, y.long()) optimizer.zero_grad() loss.backward(retain_graph=True) optimizer.step() batch_loss.append(loss.item()) losses.append(np.mean(batch_loss)) if e%1 == 0: print("epoch: ", e, "... Loss function: ", losses[-1])
-
绘制随时间推移的
loss
函数进展:x_range = range(len(losses)) plt.plot(x_range, losses) plt.xlabel("epochs") plt.ylabel("Loss function") plt.show()
-
图表应如下所示:![图 6.24:显示损失函数进展的图表
图 6.24:显示损失函数进展的图表
-
将以下句子开头输入训练好的模型,并完成该句子:
"她正在自己的心里考虑"
starter = "So she was considering in her own mind " states = None for ch in starter: x = np.array([[indexer[ch]]]) x = index2onehot(x) x = torch.Tensor(x) pred, states = model(x, states) counter = 0 while starter[-1] != "." and counter < 50: counter += 1 x = np.array([[indexer[starter[-1]]]]) x = index2onehot(x) x = torch.Tensor(x) pred, states = model(x, states) pred = F.softmax(pred, dim=1) p, top = pred.topk(10) p = p.detach().numpy()[0] top = top.numpy()[0] index = np.random.choice(top, p=p/p.sum()) starter += chars[index] print(starter)
-
最终句子会因为在选择每个字符时存在随机因素而有所不同,但它应该看起来像这样:
So she was considering in her own mind of would the cace to she tount ang to ges seokn.
-
前一句并没有意义,因为网络一次选择一个字符,没有长期记忆以前创建的单词。然而,我们可以看到在仅仅 20 个时期后,网络已经能够形成一些有意义的单词。
活动 13:进行情感分析的自然语言处理
解决方案:
-
导入所需的库:
import pandas as pd import numpy as np import matplotlib.pyplot as plt from string import punctuation from sklearn.metrics import accuracy_score import torch from torch import nn, optim import torch.nn.functional as F
-
载入包含 1,000 条亚马逊产品评论及其标签(0 表示负面评论,1 表示正面评论)的数据集。将数据分为两个变量 – 一个包含评论,另一个包含标签:
data = pd.read_csv("amazon_cells_labelled.txt", sep="\t", header=None) reviews = data.iloc[:,0].str.lower() sentiment = data.iloc[:,1].values
-
移除评论中的标点符号:
for i in punctuation: reviews = reviews.str.replace(i,"")
-
创建一个包含整个评论集的词汇表的变量。此外,创建一个字典,将每个单词映射为一个整数,其中单词作为键,整数作为值:
words = ' '.join(reviews) words = words.split() vocabulary = set(words) indexer = {word: index for (index, word) in enumerate(vocabulary)}
-
通过用每个单词在评论中的配对整数替换来对评论数据进行编码:
indexed_reviews = [] for review in reviews: indexed_reviews.append([indexer[word] for word in review.split()])
-
创建一个包含网络架构的类。确保包含嵌入层:
class LSTM(nn.Module): def __init__(self, vocab_size, embed_dim, hidden_size, n_layers): super().__init__() self.hidden_size = hidden_size self.embedding = nn.Embedding(vocab_size, embed_dim) self.lstm = nn.LSTM(embed_dim, hidden_size, n_layers, batch_first=True) self.output = nn.Linear(hidden_size, 1) def forward(self, x): out = self.embedding(x) out, _ = self.lstm(out) out = out.contiguous().view(-1, self.hidden_size) out = self.output(out) out = out[-1,0] out = torch.sigmoid(out) return out
-
使用 64 个嵌入维度和 128 个神经元的 3 个 LSTM 层来初始化模型:
model = LSTM(len(vocabulary), 64, 128, 3)
-
定义损失函数、优化算法和训练的时期数。例如,您可以使用二元交叉熵损失作为损失函数,Adam 优化器,并训练 10 个时期:
loss_function = nn.BCELoss() optimizer = optim.Adam(model.parameters(), lr=0.001) epochs = 10
-
创建一个
for
循环,遍历不同的时期和每个单独的评论。对于每个评论,执行预测,计算损失函数,并更新网络的参数。另外,计算网络在该训练数据上的准确率:losses = [] acc = [] for e in range(1, epochs+1): single_loss = [] preds = [] targets = [] for i, r in enumerate(indexed_reviews): if len(r) <= 1: continue x = torch.Tensor([r]).long() y = torch.Tensor([sentiment[i]]) pred = model(x) loss = loss_function(pred, y) optimizer.zero_grad() loss.backward() optimizer.step() final_pred = np.round(pred.detach().numpy()) preds.append(final_pred) targets.append(y) single_loss.append(loss.item()) losses.append(np.mean(single_loss)) accuracy = accuracy_score(targets,preds) acc.append(accuracy) if e%1 == 0: print("Epoch: ", e, "... Loss function: ", losses[-1], "... Accuracy: ", acc[-1])
-
绘制损失函数和准确率随时间的进展:
x_range = range(len(losses)) plt.plot(x_range, losses) plt.xlabel("epochs") plt.ylabel("Loss function") plt.show()
-
输出图应如下所示:
图 6.25:显示损失函数进展的图表
x_range = range(len(acc)) plt.plot(x_range, acc) plt.xlabel("epochs") plt.ylabel("Accuracy score") plt.show()
-
图应如下所示:
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析