PyTorch 应用深度学习指南(二)
原文:
zh.annas-archive.org/md5/92179b2ab6cdecd2d1f691b24d96e09f译者:飞龙
第四章:卷积神经网络
学习目标
在本章结束时,您将能够:
-
解释卷积神经网络(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 featuresmodel._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() -
图应如下所示: