PyTorch 人工智能研讨会(二)
四、卷积神经网络
概述
本章介绍了训练卷积神经网络(CNN)的过程-也就是说,发生在不同层的计算通常可以在 CNN 架构中找到,其目的可在训练过程中发现。 您将学习如何通过对模型应用数据扩充和批量规范化来改善计算机视觉模型的表现。 到本章末,您将能够使用 CNN 通过 PyTorch 解决图像分类问题。 这将是在计算机视觉领域实现其他解决方案的起点。
简介
在上一章中,解释了最传统的神经网络架构并将其应用于现实生活中的数据问题。 在本章中,我们将探讨 CNN 的不同概念,这些概念主要用于解决计算机视觉问题(即图像处理)。
即使当今所有神经网络领域都很流行,但 CNN 可能是所有神经网络架构中最流行的。 这主要是因为,尽管它们在许多领域中都可以使用,但是它们尤其擅长处理图像,并且技术的进步已使大量图像的收集和存储成为可能,从而可以解决当今使用图像处理各种挑战。 图像作为输入数据。
从图像分类到物体检测,CNN 被用于诊断癌症患者和检测系统中的欺诈行为,以及用于构建将彻底改变未来的深思熟虑的自动驾驶汽车。
本章将重点介绍在处理图像时 CNN 优于其他架构的原因,并更详细地说明其架构的组成部分。 它将涵盖用于构建 CNN 来解决图像分类数据问题的主要编码结构。
此外,我们将探讨数据扩充和批量规范化的概念,这些概念将用于改善模型的表现。 本章的最终目标是比较三种不同方法的结果,以便使用 CNN 解决图像分类问题。
注意
构建 CNN
CNN 是处理图像数据问题时的理想架构。 但是,由于它们通常用于图像分类任务,因此即使它们的功能扩展到图像处理领域的其他领域,它们也常常未被充分利用。 本章不仅将说明 CNN 为何如此擅长理解图像的原因,还将识别可以解决的不同任务,并提供一些实际应用的示例。
此外,本章还将探讨 CNN 的不同构建块及其在 PyTorch 中的应用,以最终构建一个使用 PyTorch 的数据集进行图像分类来解决数据问题的模型。
为什么将 CNN 用于图像处理?
图像是像素矩阵,那么为什么不将矩阵展平为向量并使用传统的神经网络架构对其进行处理呢? 答案是,即使使用最简单的图像,也存在一些像素相关性会改变图像的含义。 例如,猫眼,汽车轮胎甚至物体边缘的表示都是由以某种方式布置的几个像素构成的。 如果我们将图像展平,则这些依赖关系将丢失,传统模型的准确率也会丢失:
图 4.1:展平矩阵的表示
CNN 能够捕获图像的空间相关性,因为它根据过滤器的大小将它们作为矩阵进行处理并一次分析整个图像块。 例如,使用大小为3 x 3的过滤器的卷积层将一次分析 9 个像素,直到它覆盖了整个图像。
图像的每个块都具有一组参数(权重和偏差),这些参数将取决于该组像素与整个图像的相关性,具体取决于手边的过滤器。 这意味着垂直边缘过滤器将为包含垂直边缘的图像块分配更大的权重。 据此,通过减少参数数量并通过分块分析图像,CNN 可以呈现更好的图像表示形式。
作为输入的图像
如前所述,CNN 的典型输入是矩阵形式的图像。 矩阵的每个值代表图像中的一个像素,其中的数字由颜色的强度决定,其范围为 0 到 255。
在灰度图像中,白色像素用数字 255 表示,黑色像素用数字 0 表示。灰色像素是介于两者之间的任何数字,具体取决于颜色的强度。 灰色越浅,数字越接近 255。
彩色图像通常使用 RGB 系统表示,该系统将每种颜色表示为红色,绿色和蓝色的组合。 在这里,每个像素将具有三个尺寸,每种颜色一个。 每个维度中的值的范围从 0 到 255。这里,颜色越深,数字越接近 255。
根据上一段,给定图像的矩阵是三维的。 这里,第一维是指图像的高度(以像素数为单位),第二维是指图像的宽度(以像素数为单位),而第三维是指通道,是指图像的配色方案。
彩色图像的通道数为三个(RGB 系统中每种颜色一个通道)。 另一方面,灰度图像只有一个通道:
图 4.2:图像的矩阵表示–左侧是彩色图像; 右边是灰度图像
与文本数据相比,馈入 CNN 的图像不需要太多预处理。 图像通常按原样提供,最常见的更改如下:
- 标准化像素值来加快学习过程并提高性能
- 缩小图像(即减小其宽度和长度)以加快学习过程
归一化输入的最简单方法是取每个像素的值并将其除以 255,这样我们最终得到的值介于 0 到 1 之间。但是,使用了不同的方法来归一化图像,例如均值中心技术 。 在大多数情况下,选择一个或另一个的决定是一个优先事项。 但是,在使用预训练模型时,强烈建议您使用与第一次训练模型相同的技术。 该信息通常在预训练模型的文档中可用。
CNN 的应用
尽管 CNN 主要用于解决计算机视觉问题,但重要的是要提及它们解决其他学习问题的能力,主要是在分析数据序列方面。 例如,已知 CNN 在文本,音频和视频的序列上表现良好,有时与其他网络架构结合使用,或者通过将序列转换为可以由 CNN 处理的图像。 使用带有数据序列的 CNN 可以解决的一些特定数据问题包括文本的机器翻译,自然语言处理和视频帧标记等。
CNN 可以执行不同的任务,这些任务适用于所有监督学习问题。 但是,本章将重点介绍计算机视觉。 以下是对每个任务的简要说明,以及每个任务的真实示例。
分类
这是计算机视觉中最常见的任务。 主要思想是将图像的一般内容分类为一组类别,称为标签。
例如,分类可以确定图像是狗,猫还是任何其他动物的。 通过输出图像属于每个类别的概率来完成此分类,如下图所示:
图 4.3:分类任务
定位
定位的主要目的是生成一个边界框,以描述对象在图像中的位置。 输出由一个类标签和一个边界框组成。
此任务可用于传感器中,以确定对象在屏幕的左侧还是右侧:
图 4.4:定位任务
检测
该任务包括对图像中的所有对象执行对象定位。 输出由多个边界框以及多个类标签(每个框一个)组成。
此任务用于自动驾驶汽车的构造,目的是能够找到交通标志,道路,其他汽车,行人和其他与确保安全驾驶体验有关的其他物体:
图 4.5:检测任务
分割
这里的任务是输出图像中存在的每个对象的类标签和轮廓。 这主要用于标记图像的重要对象以进行进一步分析。
例如,该任务可用于严格划定对应于患者肺部图像中肿瘤的区域。 下图描述了如何概述感兴趣的对象并为其分配标签:
图 4.6:分割任务
从本节开始,本章将重点介绍训练模型以使用 PyTorch 的图像数据集之一进行图像分类。
CNN 的基础
深度卷积网络是一种将图像作为输入并通过一系列卷积层和过滤器,池化层和全连接层(FC)的网络,以最终应用 softmax 激活函数,该函数将图像分类为类标签。 与 ANN 一样,这种分类形式是通过为图像赋予每个类别标签一个介于 0 和 1 之间的值来计算属于每个类别标签的图像的概率来执行的。 具有较高概率的类别标签被选择为该图像的最终预测。
以下是对每个层的详细说明,以及如何在 PyTorch 中定义此类层的代码示例。
卷积层
这是从图像中提取特征的第一步。 目的是通过学习图像一小部分的特征来保持附近像素之间的关系。
数学运算在该层中进行,其中给出两个输入(图像和过滤器),并获得一个输出。 如前所述,该操作包括对过滤器和与过滤器大小相同的图像部分进行卷积。 对图像的所有子部分重复此操作。
注意
再次访问 “第 2 章”,“神经网络 CNN 的简介”部分,以提醒您在输入和过滤器之间执行的确切计算。
所得矩阵的形状取决于输入的形状,其中包含大小为[hxwxc的图像矩阵) 大小(fhxfwxc)的大小将根据以下公式输出:
图 4.7:卷积层的输出高度,宽度和深度
此处,h表示输入图像的高度,w表示宽度,c表示深度(也称为通道),fh和fw是用户关于过滤器尺寸设置的值。
下图以矩阵形式描述了此尺寸转换,其中左侧的矩阵表示彩色图像,中间的矩阵表示将应用于图像所有通道的单个过滤器,而矩阵将应用于矩阵。 右边是图像和过滤器计算的输出:
图 4.8:输入,过滤器和输出的尺寸
重要的是要提到,在单个卷积层中,可以将多个过滤器应用于相同的图像,所有过滤器都具有相同的形状。 考虑到这一点,将两个过滤器应用于其输入的卷积层的输出形状就其深度而言等于两个,如下图所示:
图 4.9:带两个过滤器的卷积层
这些过滤器中的每一个将执行不同的操作,以发现图像的不同特征。 例如,在具有两个过滤器的单个卷积层中,这些操作可以是垂直边缘检测和水平边缘检测。 随着网络在层数方面的增长,过滤器将执行更复杂的操作,这些操作将利用先前检测到的特征(例如,通过使用边缘检测器的输入来检测人的轮廓)。
过滤器通常每层增加。 这意味着,尽管第一卷积层具有 8 个过滤器,但通常会创建第二个卷积层,使其具有两倍的数量(16),第三层使它具有两倍的数量(32),依此类推 。
但是,必须指出的是,在 PyTorch 中,就像在许多其他框架中一样,您应该仅定义要使用的过滤器数量,而不是过滤器的类型(例如,垂直边缘检测器)。 每种过滤器配置(用于检测特定特征的所包含的数量)都是系统变量的一部分。
将向卷积层主题介绍两个附加概念,如下所示。
填充
顾名思义,填充参数将图像填充为零。 这意味着它将在图像的每一侧添加其他像素(用零填充)。
下图显示了一个示例,该示例已在每侧用一个图像填充:
图 4.10:用一个图填充一个输入图像的图形表示
一旦输入矩阵通过过滤器,就可以用来保持其形状。 这是因为,尤其是在前几层中,目标应该是从原始输入中保留尽可能多的信息,以便从中提取最多的特征。
为了更好地理解填充的概念,请考虑以下情形。
将3 x 3过滤器应用于形状为32 x 32 x 3的彩色图像将得到形状为30 x 30 x 1的矩阵。这意味着下一层的输入已缩小。 但是,通过向输入图像添加 1 的填充,输入的形状将更改为34 x 34 x 3,这将导致使用相同过滤器的输出为32 x 32 x 1。
使用填充时,以下公式可用于计算输出宽度:
图 4.11:使用填充应用卷积层后的输出宽度
在此,W表示输入矩阵的宽度,F表示过滤器的宽度,P表示填充。 可以使用相同的公式来计算输出的高度。
要获得形状与输入相等的输出矩阵,请使用以下公式计算填充的值(考虑到我们将在下一节中定义的步幅等于 1):
图 4.12:填充数字以获得形状与输入相等的输出矩阵
请记住,输出通道(深度)的数量将始终等于已应用于输入的过滤器的数量。
步幅
此参数是指过滤器将在水平和垂直方向上在输入矩阵上移动的像素数。 到目前为止,我们已经看到过滤器通过图像的左上角,然后向右移一个像素,依此类推,直到它垂直和水平通过图像的所有部分为止。 此示例是步幅等于 1 的卷积层之一,这是此参数的默认配置。
当步幅等于 2 时,移位将改为两个像素,如下图所示:
图 4.13:跨度为 2 的卷积层的图形表示
可以看出,初始操作发生在左上角。 然后,通过向右移动两个像素,第二个计算将在右上角进行。 接下来,计算向下移动两个像素以在左下角执行计算,最后,通过再次向右移动两个像素,最终的计算将在右下角进行。
注意
“图 4.12”中的数字是构成的,不是实际的计算。 重点应放在方框上,这些方框说明步幅等于 2 时的移动过程。
使用步幅时,以下方程式可用于计算输出宽度:
图 4.14:使用步幅的卷积层的输出宽度
在此,W表示输入矩阵的宽度,F表示过滤器的宽度,S表示步幅。 可以使用相同的公式来计算输出的高度。
引入这些参数后,用于计算从卷积层得出的矩阵的输出形状(宽度和高度)的最终方程式如下:
图 4.15:使用填充和跨度的卷积层后的输出宽度
每当值是浮点型时,都应四舍五入。 这基本上意味着输入的某些区域将被忽略,并且不会从中提取任何特征。
最后,一旦输入已通过所有过滤器,则将输出馈送到激活函数以破坏线性,这类似于传统神经网络的过程。 尽管在此步骤中可以应用多种激活函数,但是首选函数是 ReLU 函数,因为它在 CNN 中显示了出色的效果。 我们在此处获得的输出将成为后续层(通常是池化层)的输入。
练习 4.01:计算卷积层的输出形状
使用给定的方程式,请考虑以下情形并计算输出矩阵的形状:
注意
此练习不需要编码,而是由基于我们前面提到的概念的计算组成。
-
计算从卷积层派生的矩阵的输出形状,该卷积层的输入形状为
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,10 个形状为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 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 中编码卷积层非常简单。 使用自定义模块,只需要创建网络类。 该类应包含定义网络架构(即网络层)的__init__方法和定义要对信息进行计算的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__()
self.conv1 = nn.Conv2d(3, 18, 3, 1, 1)
def forward(self, x):
x = F.relu(self.conv1(x))
return x
在定义卷积层时,从左到右传递的参数是指输入通道,输出通道(过滤器数量),核大小(过滤器大小),步幅和填充。
前面的示例由一个卷积层组成,该卷积层具有三个输入通道,18 个过滤器,每个过滤器的大小为 3,步幅和填充等于 1。
等效于上一个示例的另一种有效方法是将自定义模块的语法与顺序容器的使用结合起来,如以下代码片段所示:
import torch.nn as nn
class CNN_network(nn.Module):
def __init__(self):
super(CNN_network, self).__init__()
self.conv1 = nn.Sequential(nn.Conv2d(3, 18, 3, 1, 1), \
nn.ReLU())
def forward(self, x):
x = self.conv1(x)
return x
在这里,层的定义发生在顺序容器内部。 通常,一个容器包括卷积层,激活函数和池化层。 一组新的层包含在其下面的不同容器中。
在前面的示例中,在顺序容器内定义了卷积层和激活层。 因此,在forward方法中,不需要卷积层的输出通过激活函数,因为已经使用容器对其进行了处理。
池化层
按照惯例,池化层是特征选择步骤的最后部分,这就是为什么池化层通常可以在卷积层之后找到的原因。 正如我们在前几章中所解释的那样,其思想是从图像的各个子部分中提取最相关的信息。 池化层的大小通常为 2,步幅等于其大小。
池化层通常将输入的高度和宽度减小一半。 考虑到要使卷积层找到图像中的所有特征,必须使用多个过滤器,并且此操作的输出可能变得太大,这意味着要考虑许多参数,这一点很重要。 池化层旨在通过保留最相关的特征来减少网络中的参数数量。 从图像的各个子部分中选择相关特征,可以通过获取最大数量或平均该区域中的数量来进行。
对于图像分类任务,最常见的是在平均池化层上使用最大池化层。 这是因为前者在保留最相关特征的任务中表现出更好的效果,而后者已被证明在诸如平滑图像等任务中表现更好。
要计算输出矩阵的形状,请使用以下公式:
图 4.16:合并层后的输出矩阵宽度
此处,W表示输入的宽度,F表示过滤器的尺寸,S表示步幅。 可以使用相同的公式来计算输出高度。
输入的通道或深度保持不变,因为合并层将对图像的所有通道执行相同的操作。 这意味着池化层的结果仅会影响宽度和长度的输入。
练习 4.02:计算一组卷积和池化层的输出形状
以下练习将结合卷积层和池化层。 目的是确定经过一组层后的输出矩阵的大小。
注意
此练习不需要编码,而是由基于我们前面提到的概念的计算组成。
考虑以下几组层,并在所有转换结束时指定输出层的形状,并考虑256 x 256 x 3的输入图像:
-
卷积层,具有 16 个大小为 3 的过滤器,步幅和填充为 1。
-
池化层还具有大小为 2 的过滤器以及大小为 2 的步幅。
-
卷积层,具有八个大小为 7 的过滤器,跨度为 1,填充为 3。
-
池化层,其过滤器的大小为 2,步幅也为 2。
经过每一层后,矩阵的输出大小如下:
-
在第一个卷积层之后:
output_width/height = ((256 – 3) + 2 * 1)/1 + 1 = 256 output_channels = 16 filters were applied output_matrix_size = 256 x 256 x 16 -
在第一个池化层之后:
output_width/height = (256 – 2) / 2 + 1 = 128 output_channels = 16 as pooling does not affect the number of channels output_matrix_size = 128 x 128 x 16 -
在第二个卷积层之后:
output_width/height = ((128 – 7) + 2 =* 3)/1 + 1 = 128 output_channels = 8 filters were applied output_matrix_size = 128 x 128 x 8 -
在第二个池化层之后:
output_width/height = (128 – 2) / 2 + 1 = 64 output_channels = 8 as pooling does not affect the number of channels 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
可以看出,在__init__方法中将池化层(MaxPool2d)添加到网络架构中。 在这里,进入最大池化层的参数从左到右分别是过滤器(2)和步幅(2)的大小。 接下来,更新了forward方法,以使信息通过新的合并层。
同样,这里显示了一种同样有效的方法,其中使用了自定义模块和顺序容器:
import torch.nn as nn
class CNN_network(nn.Module):
def __init__(self):
super(CNN_network, self).__init__()
self.conv1 = nn.Sequential(nn.Conv2d(3, 18, 3, 1, 1),\
nn.ReLU(),\
nn.MaxPool2d(2, 2))
def forward(self, x):
x = self.conv1(x)
return x
正如我们之前提到的,池化层还包含在激活函数下方与卷积层相同的容器中。 在新的顺序容器中,下面将定义一组后续层(卷积,激活和池化)。
同样,forward方法不再需要单独调用每个层; 而是通过容器传递信息,该容器既包含层又包含激活函数。
全连接层
在输入经过一组卷积和池化层之后,在网络架构的末尾定义一个或多个 FC 层。 来自第一 FC 层之前的层的输出数据从矩阵展平为向量,可以将其馈送到 FC 层(与传统神经网络的隐藏层相同)。
这些 FC 层的主要目的是考虑先前层检测到的所有特征,以便对图像进行分类。
除非是最后一层,否则不同的 FC 层会通过一个激活函数(通常是 ReLU 函数)传递,除非它是最后一层,否则它将使用 softmax 函数来输出属于每个类标签的输入的概率。
第一 FC 层的输入大小对应于前一层的平坦输出矩阵的大小。 输出大小是由用户定义的,同样,与 ANN 一样,设置此数字没有确切的科学依据。 最后一个 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
使用与上一节相同的编码示例,在__init__方法内部,将两个 FC 层添加到网络。 接下来,在forward函数内部,使用view()函数将池化层的输出展平。 然后,它通过第一 FC 层,该层应用激活函数。 最后,数据连同其激活函数一起通过最终的 FC 层。
同样,使用与之前相同的编码示例,可以使用自定义模块和顺序容器将 FC 层添加到我们的模型中,如下所示:
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
可以看出,顺序容器保持不变,并在__init__方法内在下面添加了两个 FC 层。 接下来,forward函数将信息传递通过整个容器,然后将输出平坦化以通过 FC 层。
一旦定义了网络的架构,就可以按照与 ANN 相同的方式来处理训练网络的以下步骤。
旁注–从 PyTorch 下载数据集
要从 PyTorch 加载数据集,请使用以下代码。 除了下载数据集之外,以下代码还显示了如何通过批量加载而不是一次加载图像来使用数据加载器来节省资源:
from torchvision import datasets
import torchvision.transforms as transforms
transform = \
transforms.Compose([transforms.ToTensor(), \
transforms.Normalize((0.5, 0.5, 0.5), \
(0.5, 0.5, 0.5))])
transforms变量用于定义要在数据集上执行的一组转换。 在这种情况下,数据集将被转换为张量并在其所有维度上进行规范化。
train_data = datasets.MNIST(root='data', train=True,\
download=True, transform=transform)
test_data = datasets.MNIST(root='data', train=False,\
download=True, transform=transform)
在前面的代码中,要下载的数据集是 MNIST。 这是一个流行的数据集,其中包含从零到九的手写灰度数字图像。 PyTorch 数据集提供训练和测试集。
从前面的代码片段可以看出,要下载数据集,有必要定义数据的根,默认情况下应将其定义为data。 接下来,定义您是要下载训练还是测试数据集。 我们将download参数设置为True。 最后,我们使用之前定义的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)
在前面的代码段中,PyTorch 的SubsetRandomSampler()函数用于通过随机采样索引将原始训练集分为训练集和验证集。 在接下来的步骤中将使用它来生成将在每次迭代中馈送到模型中的批量:
batch_size = 20
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)
DataLoader()函数用于为每组数据批量加载图像。 首先,将包含集合的变量作为参数传递,然后定义批量大小。 最后,我们在上一步中创建的采样器用于确保随机创建每次迭代中使用的批量,这有助于提高模型的表现。 此函数的结果变量(train_loader,dev_loader和test_loader)将分别包含特征部分和目标的值。
注意
问题越复杂,网络越深,训练模型所需的时间就越长。 考虑到这一点,本章中的活动可能比上一章中的活动花费更长的时间。
活动 4.01:针对图像分类问题构建 CNN
在此活动中,将在来自 PyTorch 的图像数据集上训练 CNN(也就是说,框架提供了数据集)。 要使用的数据集是 CIFAR10,其中包含总共 60,000 张车辆和动物的图像。 有 10 个不同的类标签(例如“飞机”,“鸟”,“汽车”,“猫”等)。 训练集包含 50,000 张图像,而测试集包含剩余的 10,000 张图像。
注意
要进一步探索该数据集,请访问以下 URL。
让我们看一下我们的场景。 您在一家人工智能公司工作,该公司根据客户需求开发定制模型。 您的团队当前正在创建一个模型,该模型可以区分车辆的图像和动物的图像,更具体地说,可以识别不同种类的动物和不同类型的车辆。 他们为您提供了包含 60,000 张图像的数据集以构建模型。
注意
本章中的活动可能需要很长时间才能在常规计算机(CPU)上进行训练。 为了在 GPU 上运行代码,本书的 GitHub 存储库中的每个活动都有一个等效文件。
-
导入所需的库。
-
设置要对数据执行的转换,这将是数据到张量的转换以及像素值的归一化。
-
设置批量为 100 张图像,并从 CIFAR10 数据集下载训练和测试数据。
-
使用 20% 的验证大小,定义将用于将数据集分为这两组的训练和验证采样器。
-
使用
DataLoader()函数定义用于每组数据的批量。 -
定义您的网络架构。 使用以下信息来这样做:
Conv1:卷积层,将彩色图像作为输入,并将其通过大小为 3 的 10 个过滤器。应将 padding 和 stride 都设置为 1。
Conv2:一个卷积层,它将输入数据通过大小为 3 的 20 个过滤器传递。填充和跨距都应设置为 1。
Conv3:一个卷积层,它将输入数据通过大小为 3 的 40 个过滤器传递。填充和跨距都应设置为 1。
在每个卷积层之后使用 ReLU 激活函数。
每个卷积层之后的池化层,过滤器大小和跨度为 2。
展平图像后,滤除项设置为 20%。
Linear1:一个全连接层,接收上一层的展平矩阵作为输入,并生成 100 个单元的输出。 为此层使用 ReLU 激活函数。 此处,丢弃期限设置为 20%。
Linear2:一个全连接层,可生成 10 个输出,每个类标签一个。 将
log_softmax激活函数用于输出层。 -
定义训练模型所需的所有参数。 将周期数设置为 50。
-
训练您的网络,并确保保存训练和验证集的损失和准确率值。
-
绘制两组的损失和准确率。
-
在测试集上检查模型的准确率--它应该在 72% 左右。
注意
有关此活动的解决方案,请参见第 262 页。
由于数据在每个周期都经过重新排序,因此结果将无法完全重现。 但是,您应该能够获得与本书所获得的结果相似的结果。
这段代码可能需要一些时间才能运行,这就是为什么在本书的 GitHub 存储库中提供了等效的 GPU 版本解决方案的原因。
数据扩充
学习如何有效地编码神经网络是开发表现良好的解决方案所涉及的步骤之一。 此外,要开发出色的深度学习解决方案,至关重要的是找到一个感兴趣的领域,我们可以在其中提供解决当前挑战的解决方案。 但是一旦完成所有这些操作,我们通常会面临相同的问题:通过自收集或通过从互联网和其他可用来源下载来获得适当大小的数据集以从模型中获得良好的表现。
您可能会想到,即使现在可以收集和存储大量数据,但由于与之相关的成本,这并不是一件容易的事。 因此,在大多数情况下,我们只能处理包含数万个条目的数据集,而在引用图像时甚至更少。
在开发计算机视觉问题的解决方案时,这成为一个相关问题,主要是由于两个原因:
-
数据集越大,结果越好,并且更大的数据集对于获得足够好的模型至关重要。 考虑到训练模型是调整一堆参数的问题,以便能够映射输入和输出之间的关系,这是正确的。 这是通过最小化损失函数以使预测值尽可能接近基本事实来实现的。 在此,模型越复杂,所需的参数就越多。
考虑到这一点,有必要向模型提供大量示例,以便能够找到这样的模式,其中训练示例的数量应与要调整的参数的数量成比例。
-
计算机视觉问题中的最大挑战之一是使模型在图像的多种变体上都能表现良好。 这意味着图像不需要按照特定的对齐方式进行送入,也不必具有设定的质量,而是可以以其原始格式(包括不同的位置,角度,光照和其他变形)进行送入。 因此,有必要找到一种以这种变化提供模型的方法。
因此,设计了数据扩充技术。 简而言之,这是通过稍微修改现有示例来增加训练示例数量的一种措施。 例如,您可以复制当前可用的实例,并向这些重复项添加一些噪音以确保它们不完全相同。
在计算机视觉问题中,这意味着通过更改现有图像来增加训练数据集中的图像数量,这可以通过稍微更改当前图像以创建略有不同的重复版本来完成。
对图像的这些较小调整可以采取以下形式:轻微旋转,更改对象在帧中的位置,水平或垂直翻转,不同的配色方案以及变形等。 由于 CNN 将这些图像中的每一个视为不同图像,因此该技术行之有效。
例如,下图显示了一条狗的三幅图像,它们在人眼中是具有某些变化的同一幅图像,而在神经网络中却是完全不同的:
图 4.17:增强图像
能够识别图像中的物体而不管变化的 CNN 具有不变性。 实际上,CNN 对于每种类型的变化都可以是不变的。
PyTorch 数据扩充
使用Torchvision包在 PyTorch 中执行数据扩充非常容易。 该包除了包含流行的数据集和模型架构之外,还包含可以在数据集上执行的常见图像转换函数。
注意
在本节中,将提到其中一些图像转换。 要获取所有可能的转换的完整列表,请访问这里。
与在上一活动中使用的过程一样,将数据集归一化并将其转换为张量,执行数据扩充需要我们定义所需的转换,然后将其应用于数据集,如以下代码片段所示:
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)
在这里,使用HorizontalFlip函数,将要下载的数据将进行水平翻转(考虑用户设置的概率值,并确定将进行此变换的图像的百分比)。 通过使用RandomGrayscale函数,图像将被转换为灰度(还考虑了概率)。 然后,将数据转换为张量并进行规范化。
考虑到模型是在迭代过程中进行训练的,其中多次输入训练数据,因此这些转换可确保第二次遍历数据集不会将完全相同的图像馈入模型。
此外,可以为不同的集合设置不同的变换。 这很有用,因为数据扩充的目的是增加训练示例的数量,但是用于测试模型的图像应基本保持不变。 但是,应该调整测试集的大小,以便将大小相等的图像输入模型。
这可以通过以下方式完成:
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"]
如我们所见,定义了一个字典,其中包含用于训练和测试集的一组转换。 然后,可以调用字典以将转换相应地应用于每个集合。
活动 4.02:图像数据扩充
在此活动中,数据扩充将引入到我们在上一个活动中创建的模型中,以测试其准确率是否可以提高。 让我们看一下以下情况。
您创建的模型不错,但是其准确率达不到期望的水平。 您被要求考虑一种可以改善模型表现的方法。 请按照以下步骤完成此活动:
-
复制上一个活动中的笔记本。
-
修改
transform变量的定义,使其除了将数据归一化并转换为张量外,还包括以下转换:对于训练/验证集,
RandomHorizontalFlip函数的概率为 50%(0.5),RandomGrayscale函数的概率为 10%(0.1)。对于测试集,请勿添加任何其他转换。
-
训练模型 100 个周期。由此得到的训练集和验证集的损失和准确率图应与这里所示的相似。
图 4.18:结果图显示集合的损失
图 4.19:结果图显示了集合的准确率
注意
由于每个周期的数据打乱,结果将无法精确再现。 但是,您应该能够得到类似的结果。
-
计算测试集上所得模型的准确率。
预期输出:模型在测试集上的表现应为 75% 左右。
注意
此活动的解决方案可以在第 272 页上找到。
这段代码可能需要一些时间才能运行,这就是为什么在本书的 GitHub 存储库中提供了等效的 GPU 版本解决方案的原因。
批量归一化
通常会标准化输入层,以尝试加快学习速度,并通过将所有特征重新缩放为相同比例来提高性能。 因此,问题是,如果模型受益于输入层的归一化,为什么不对所有隐藏层的输出进行归一化以进一步提高训练速度呢?
批量归一化顾名思义,是对隐藏层的输出进行归一化,以便减小每个层的方差,也称为协方差平移。 协方差偏移的这种减小是有用的,因为它使模型也可以在遵循与用于训练它的图像不同的分布的图像上很好地工作。
例如,以检测动物是否为猫为目的的网络为例。 当仅使用黑猫的图像训练网络时,批量归一化可以通过对数据进行归一化来帮助网络对不同颜色的猫的新图像进行分类,以便黑猫和彩色猫的图像都遵循相似的分布。 下图表示了这样的问题:
图 4.20:猫分类器-即使仅使用黑猫训练后,模型也可以识别有色猫
此外,批量归一化为模型的训练过程带来了以下好处,最终可以帮助您获得表现更好的模型:
-
它允许设置更高的学习率,因为批量归一化有助于确保任何输出都不是太高或太低。 更高的学习率等同于更快的学习时间。
-
它有助于减少过拟合,因为它具有正则化效应。这使得可以将丢弃概率设置为一个较低的值,这意味着在每个前向通道中忽略的信息较少。
注意
我们不应该主要依靠批量归一化来处理过拟合。
如前几节所述,通过减去批量平均值并除以批量标准差,可以对隐藏层的输出进行归一化。
此外,通常在卷积层以及 FC 层(不包括输出层)上执行批归一化。
PyTorch 批量归一化
在 PyTorch 中,考虑到有两种不同的类型,添加批归一化就像向网络架构添加新层一样简单,如下所述:
BatchNorm1d:此层用于在二维或三维输入上实现批量标准化。 它从上一层接收输出节点的数量作为参数。 这通常在 FC 层上使用。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
如我们所见,批量规范化层的初始定义与__init__方法内的其他任何层类似。 接下来,在forward方法内的激活函数之后,将每个批量归一化层应用于其对应层的输出。
活动 4.03:实现批量归一化
对于此活动,我们将在前一个活动的架构上实现批量归一化,以查看是否有可能进一步提高测试集上模型的表现。 让我们看一下以下情况。
您在表现方面所做的最后改进使您的队友印象深刻,现在他们对您的期望更高。 他们要求您最后一次尝试改进模型,以使准确率提高到 80%。 请按照以下步骤完成此活动:
-
复制上一个活动中的笔记本。
-
将批量归一化添加到每个卷积层以及第一个 FC 层。
-
训练模型 100 个周期。由此得到的训练集和验证集的损失和准确率图应该与这里的图相似。
图 4.21:结果图显示了集合的损失
图 4.22:结果图显示了集合的损失
-
计算结果模型在测试集上的准确率--它应该是 78% 左右。
注意
此活动的解决方案可以在第 274 页上找到。
由于在每个周期对数据进行混洗,结果将无法完全重现。 但是,您应该能够获得与本书所获得的结果相似的结果。
这段代码可能需要一些时间才能运行,这就是为什么在本书的 GitHub 存储库中提供了等效的 GPU 版本解决方案的原因。
总结
本章重点介绍 CNN,它由一种在计算机视觉问题上表现出色的神经网络架构组成。 我们首先说明了为何广泛使用 CNN 来处理图像数据集的主要原因,并介绍了可以通过使用它们解决的不同任务。
本章通过解释卷积层,池化层以及最后的 FC 层的性质,说明了网络架构的不同构建块。 在每个部分中,都包括对每个层目的的解释,以及可用于有效编码 PyTorch 中的架构的代码段。
这导致引入了图像分类问题,该问题着重于对车辆和动物的图像进行分类。 这个问题的目的是将 CNN 的不同构建块付诸实践,以解决图像分类数据问题。
接下来,数据扩充被引入为一种工具,它通过增加训练示例的数量来提高网络表现,而无需收集更多图像。 该技术专注于创建现有图像的变体,以创建要馈送到模型的“新”图像。
通过实现数据扩充,本章的第二个活动旨在解决相同的图像分类问题,目的是比较结果。
最后,本章介绍了批量规范化的概念。 这包括标准化每个隐藏层的输出,以加快学习速度。 在解释了在 PyTorch 中应用批量归一化的过程之后,本章的最后一个活动旨在使用批量归一化解决相同的图像分类问题。
既然 CNN 的概念已经很清楚并且已经应用于解决计算机视觉问题,那么在下一章中,我们将探索 CNN 更加复杂的应用来创建图像,而不仅仅是对其进行分类。
五、样式迁移
概述
本章介绍了使用预训练的模型来创建或利用表现良好的算法而不必收集大量数据的过程。 在本章中,您将学习如何从 PyTorch 加载预训练的模型以创建样式迁移模型。 到本章末,您将能够通过使用预训练的模型来执行样式迁移。
简介
上一章介绍了传统 CNN 的不同构建块,以及一些改进其表现并减少训练时间的技术。 此处说明的架构虽然很典型,但并不是一成不变的,并且出现了许多 CNN 架构来解决不同的数据问题,这在计算机视觉领域最为普遍。
这些架构的配置和学习任务各不相同。 如今,由牛津机器人学院的 Karen Simonyan 和 Andrew Zisserman 创建的视觉几何组(VGG)架构是一种非常流行的方法。 它是为对象识别而开发的,并由于网络所依赖的大量参数而达到了最先进的表现。 它在数据科学家中受欢迎的主要原因之一是训练后的模型的参数(权重和偏差)的可用性,这使研究人员无需训练即可使用它,并且模型具有出色的表现。
在本章中,我们将使用这种经过预训练的模型来解决计算机视觉问题,该问题由于专门用于共享图像的社交媒体渠道的普及而特别著名。 它包括执行样式迁移,以便使用一个图像的样式(颜色和纹理)以及另一个图像的内容(形状和对象)创建新图像。
每天对常规图像应用过滤器以提高其质量和吸引力,同时在社交媒体个人资料上发布时,每天执行数百万次此任务。 尽管这看起来很简单,但本章将解释这些图像编辑应用幕后发生的魔术。
注意
样式迁移
简而言之,样式迁移包括修改图像的样式,同时又保留其内容。 一个示例是拍摄动物的图像,然后将样式迁移为类似莫奈的绘画,如下图所示:
图 5.1:样式迁移输入和输出–本章最后练习的结果
注意
可以在 GitHub 上找到此图像。
根据前面的图像,模型有两个输入:内容图像和样式图像。 内容是指图像的对象,而样式是指颜色和纹理。 结果,模型的输出应该是包含内容图像中的对象和样式图像的艺术外观的图像。
它如何工作?
与解决上一章中说明的传统计算机视觉问题相反,样式迁移需要一组不同的步骤才能有效地将两个图像作为输入并创建一个新图像作为输出。
以下是解决样式迁移问题时要遵循的步骤的简要说明:
-
馈入输入:内容和样式图像都将馈入模型,并且它们必须具有相同的形状。 这里的常见做法是调整样式图像的大小,以使其与内容图像具有相同的形状。
-
加载模型:牛津大学的 VGG 创建了一个模型架构,该模型在样式迁移问题上表现出色,被称为 VGG 网络。 他们还将模型的参数提供给任何人,以便可以缩短或跳过模型的训练过程(这就是预训练模型的目的)。
注意
VGG 网络有不同的版本,并且都使用不同数量的层。 为了区分不同的版本,术语是这样的,即首字母缩写处的破折号和数字代表该特定架构的层数。 在本章中,我们将使用网络的 19 层版本,即 VGG-19。
因此,可以使用 PyTorch 的模型子包加载预训练的模型,以执行样式迁移任务,而无需训练大量图像的网络。
-
确定层的函数:鉴于手头有两项主要任务(识别图像的内容并区分另一幅图像的样式),不同的层将具有不同的函数来提取不同的特征。 对于样式图像,重点应该放在颜色和纹理上,对于内容图像,重点应该放在边缘和形状上。 在此步骤中,将不同的层分为不同的任务。
-
定义优化问题:与其他任何监督问题一样,有必要定义一个损失函数,该函数将负责测量输出和输入之间的差异。 与其他受监督的问题不同,样式迁移的任务要求您定义三个不同的损失函数,在训练过程中应将所有这些损失最小化。 这里介绍了三种损失函数:
内容损失:这仅在考虑与内容相关的特征的情况下测量内容图像与输出之间的距离。
样式损失:这仅在考虑与样式相关的特征时测量样式图像与输出之间的距离。
总损失:这结合了内容损失和样式损失。 两种损失都具有与之相关的权重,该权重用于确定它们参与总损失的计算。
-
参数更新:此步骤使用梯度来更新模型的不同参数。
使用 VGG-19 网络架构实现样式迁移
VGG-19 是由 19 层组成的 CNN。 它使用 ImageNet 数据库中的数百万张图像进行了训练。 该网络能够将图像分类为 1,000 种不同的类别标签,其中包括大量的动物和不同的工具。
注意
要浏览 ImageNet 数据库,请访问以下 URL。
考虑到其深度,该网络能够从各种图像中识别出复杂的特征,这使其特别适用于样式迁移问题,在这些问题中,特征提取对于不同阶段和不同目的至关重要。
本节将重点介绍如何使用预训练的 VGG-19 模型执行样式迁移。 本章的最终目标将是拍摄动物或风景的图像(作为内容图像)以及来自知名艺术家的一幅画(作为样式图像)以创建常规对象的新图像。 具有艺术风格。
但是,在进行此过程之前,以下是导入的列表以及其使用的简要说明:
-
NumPy:这将用于转换要显示的图像。
-
Torch:
torch.nn和torch.optim:它们将实现神经网络并定义优化算法。 -
PIL.Image:将按照以下代码片段加载图像:image = Image.open(image_name) image = transformation(image).unsqueeze(0)可以看出,第一步包括打开图像(此处,
image_name应该替换为图像的路径)。 接下来,可以将先前定义的任何变换应用于图像。注意
为了提醒您如何定义图像的变换,请重新访问“第 4 章”,“卷积神经网络”。
unsqueeze()函数用于根据将图像馈送到 VGG-19 模型的要求向图像添加额外的尺寸。 -
matplotlib.pyplot:这将显示图像。 -
torchvision.transforms和torchvision.models:将图像转换为张量并加载预训练的模型。
输入–加载和显示
执行样式迁移的第一步包括加载内容和样式图像。 在此步骤中,将处理基本的预处理,其中图像必须具有相同的大小(最好是用于训练预训练模型的图像的大小),该大小也将等于输出图像的大小。 此外,图像会转换为 PyTorch 张量,并且可以根据需要进行归一化。
最好始终显示已加载的图像,以确保它们符合要求。 考虑到此时图像已被转换为张量并进行了规范化,应该克隆张量,并且需要执行一组新的转换,以便我们可以使用 Matplotlib 显示它们。 这意味着张量应转换回 Python 图像库(PIL)图像,并且必须还原规范化过程,如以下示例所示:
image = tensor.clone()
image = image.squeeze(0)
img_display = \
transforms.Compose([transforms.Normalize((-0.5/0.25, \
-0.5/0.25, -0.5/0.25), \
(1/0.25, 1/0.25, \
1/0.25)), \
transforms.ToPILImage()])
首先,张量被克隆,附加维被删除。 接下来,定义转换。
要了解恢复归一化的过程,请考虑使用所有尺寸的均值 0.5 和标准差 0.25 进行归一化的图像。 恢复标准化的方法是使用平均值的负值除以标准差作为平均值(-0.5 除以 0.25)。 新的标准差应等于一除以标准差(1 除以 0.25)。 定义用于加载和显示图像的函数可以帮助节省时间,并确保对内容图像和样式图像执行相同的过程。 在下面的练习中将扩展此过程。
注意
本章的所有练习都应在同一笔记本中进行编码,合并后的代码将一起执行样式迁移任务。
练习 5.01:加载和显示图像
这是执行样式迁移的四个步骤中的第一步。 本练习的目的是加载并显示将在以后的练习中使用的图像(内容和样式图像)。 请按照以下步骤完成此练习:
注意
对于本章中的练习和活动,您将需要安装 Python 3.7,Jupyter 6.0,Matplotlib 3.1,NumPy 1.17,Pillow 6.2 和 PyTorch 1.3+(最好是 PyTorch 1.4,有或没有 CUDA)(如“前言”)。
在本书的 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如果有可用的 GPU,请定义一个名为
device的变量,该变量等于cuda,该变量将用于为您的计算机的 GPU 分配一些变量:device = "cuda" device -
设置用于两个图像的图像尺寸。 另外,设置要在图像上执行的转换,这应该包括调整图像的大小,将它们转换为张量以及对其进行规范化:
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-19 模型的图像相同的大小。 归一化也使用与归一化训练图像相同的值进行。
注意
使用规范化图像训练 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 -
调用该函数以加载内容和样式图像。 将狗图像用作内容图像,将 Matisse 图像用作样式图像,这两者都可以在本书的 GitHub 存储库中找到:
content_img = image_loader("images/dog.jpg") style_img = image_loader("images/matisse.jpg")如果您的计算机有可用的 GPU,请改用以下代码段以达到相同的结果:
content_img = image_loader("images/dog.jpg").to(device) style_img = image_loader("images/matisse.jpg").to前面的代码片段将保存图像的变量分配给 GPU,以便使用这些变量的所有操作都由 GPU 处理。
-
调用该函数以加载内容和样式图像。 将狗图像用作内容图像,将 Matisse 图像用作样式图像,这两者都可以在本书的 GitHub 存储库中找到:...
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如果您的计算机具有可用的 GPU,请改用以下等效代码段:
def tensor2image(tensor): image = tensor.to('cpu').clone() image = image.squeeze(0) image = unloader(image) return image前面的代码片段将图像分配回 CPU,以便我们可以绘制它们。
-
调用两个图像的函数并绘制结果:
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:样式图像
注意
要访问此特定部分的源代码,请参考这里。
您也可以通过这里在线运行此示例。 您必须执行整个笔记本才能获得所需的结果。
要访问此源代码的 GPU 版本,请参考这里。 此版本的源代码无法作为在线交互示例使用,需要通过 GPU 设置在本地运行。
这样,您已经成功加载并显示了用于样式迁移的内容和样式图像。
载入模型
像在许多其他框架中一样,PyTorch 有一个子包,其中包含不同的模型,这些模型先前已经过训练并可供公众使用。 考虑到从头开始训练神经网络非常耗时,这一点很重要。 从预先训练的模型开始可以帮助减少训练时间。 这意味着可以加载经过预训练的模型,以便我们可以使用它们的最终参数(应该是使损失函数最小的参数),而无需经过迭代过程。
如前所述,用于执行样式迁移任务的架构是 19 层 VGG 网络的架构,也称为 VGG-19。 在torchvision的model子包下提供了预训练的模型。 在 PyTorch 中保存的模型分为两部分:
-
vgg19.features:这包括网络的所有卷积和池化层以及参数。 这些层负责从图像中提取特征。 有些层专门用于样式特征(例如颜色),而另一些层则专门用于内容特征(例如边缘)。 -
vgg19.classifier:这是指位于网络末端的线性层(也称为全连接层),包括其参数。 这些层是将图像分类为标签类别之一的层,例如,识别图像中的动物类型。注意
要探索 PyTorch 中可用的其他预训练模型,请访问这里。
根据前面的信息,仅应加载模型的特征部分,以便提取内容和样式图像的必要特征。 加载模型包括调用model子包,然后调用模型名称,并确保将pretrained参数设置为True(为了加载先前训练过程中的参数),并按照以下代码片段仅加载特征层:
model = models.vgg19(pretrained=True).features
考虑到那些将有助于检测所需特征的参数,每层中的参数(权重和偏差)应保持不变。 这可以通过定义模型不需要为任何这些层计算梯度来实现,如下所示:
for param in model.parameters():
param.requires_grad_(False)
在这里,对于先前加载的模型的每个参数,为了避免计算梯度, require_grad_方法设置为False,因为目标是利用预先训练的参数,而不更新它们。
练习 5.02:在 PyTorch 中加载预先训练的模型
使用与上一练习相同的笔记本,本练习旨在加载预先训练的模型,该模型将在后续练习中使用我们先前加载的图像执行样式迁移任务。
-
打开上一个练习中的笔记本。
-
从 PyTorch 加载 VGG-19 预训练模型:
model = models.vgg19(pretrained=True).feature如前所述,选择模型的特征部分。 这将使您能够访问模型的所有卷积和池化层,这些层将用于在本章的后续练习中提取特征。
-
通过先前加载的模型的参数执行
for循环。 设置每个参数,使其不需要进行梯度计算:for param in model.parameters(): param.requires_grad_(False)通过将梯度计算设置为
False,我们确保在训练过程中不计算梯度。如果您的计算机具有可用的 GPU,则将以下代码段添加到前面的代码段中,以便将模型分配给 GPU:
model.to(device)注意
要访问此特定部分的源代码,请参考这里。
您也可以通过这里在线运行此示例。 您必须执行整个笔记本才能获得所需的结果。
要访问此源代码的 GPU 版本,请参考这里。 此版本的源代码无法作为在线交互示例使用,需要通过 GPU 设置在本地运行。
这样,您已经成功加载了预训练的模型。
提取特征
正如我们前面提到的,VGG-19 网络包含 19 个不同的层,包括卷积,池化和全连接层。 卷积层在每个池化层之前先进入栈,其中五个是整个架构中的栈数。
在样式迁移领域,已经有不同的论文确定了对于识别内容和样式图像上的相关特征至关重要的那些层。 因此,常规上接受的是,每个栈的第一卷积层都能够提取样式特征,而仅第四栈的第二卷积层应用于提取内容特征。
从现在开始,我们将提取样式特征的层称为conv1_1,conv2_1,conv3_1,conv4_1和conv5_1,而负责提取内容特征的层将被称为conv4_2。
注意
这意味着样式图像的特征是从五个不同的层获得的,而内容图像的特征是仅从一层获取的。 每个层的输出都用于将输出图像与输入图像进行比较,目的是修改目标图像的参数,使其与内容图像的内容和样式图像的样式相似, 可以通过优化三个不同的损失函数来实现(将在本章中进一步说明)。
要提取每个层的特征,可以使用以下代码片段:
layers = {'0': 'conv1_1', '5': 'conv2_1', '10': 'conv3_1', \
'19': 'conv4_1', '21': 'conv4_2', '28': 'conv5_1'}
features = {}
x = image
for index, layer in model._modules.items():
x = layer(image)
if index in layers:
features[layers[index]] = x
在前面的代码段中,layers是一个字典,将所有相关层的位置(在网络中)映射到将用于识别它们的名称以及model._modules包含保存网络各层的字典。
通过使用循环遍历不同层,我们将图像传递到不同层,并保存感兴趣的层的输出(我们先前创建的layers词典中的层) 进入features词典。 输出字典由包含层名称的键和包含该层的输出特征的值组成。
为了确定目标图像是否包含与内容图像相同的内容,我们需要检查两个图像中是否存在某些特征。 但是,为了检查目标图像和样式图像的样式表示,必须检查相关性而不是两个图像的特征是否严格存在。 这是因为两个图像的样式特征都不精确,而是近似的。
语法矩阵用于检查这些相关性。 它包括创建一个矩阵,该矩阵查看给定层中不同样式特征的相关性。 这是通过将卷积层的向量化输出乘以相同的转置向量化输出来完成的,如下图所示:
图 5.4:克矩阵的计算
在上图中,A表示具有4x4尺寸(高度和宽度)的输入样式图像,而B表示将图像通过具有五个过滤器的卷积层后的输出。 最后,C表示语法矩阵的计算,其中左侧的图像表示B的向量化版本,右侧的图像是其转置版本。 从向量化输出的乘法中,创建一个5x5 Gram 矩阵,其值表示沿不同通道(过滤器)的样式特征方面的相似性(相关性)。
这些相关性可以用于确定与图像的样式表示相关的那些特征,然后可以将其用于更改目标图像。 考虑到在五个不同的层中获得了样式特征,可以安全地假定网络能够从样式图像中检测大小特征,并考虑到必须为每个层创建一个 gram 矩阵。
练习 5.03:设置特征提取过程
使用上一练习的网络架构和本章第一练习的图像,我们将创建几个函数,这些函数能够从输入图像中提取特征并为样式特征创建语法矩阵。
-
打开上一个练习中的笔记本。
-
打印我们在上一个练习中加载的模型的架构。 这将帮助我们识别相关层,以便我们可以执行样式迁移任务:
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(): x = layer(x) if index in layers: features[layers[index]] = x return features输出应该是字典,键是层的名称,值是该层的输出特征。
-
在我们在本章第一个练习中加载的内容和样式图片上调用
features_extractor函数。
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对于每一层,都将获得样式特征矩阵的形状以对其进行向量化。 接下来,通过将向量化输出乘以其转置版本来创建 gram 矩阵。
-
创建一个初始目标图像。该图像将在以后与内容和风格图像进行比较,并进行更改,直到达到所需的相似度。
target_img = content_img.clone().requires_grad_(True)优良作法是将初始目标图像创建为内容图像的副本。 此外,考虑到我们希望能够在迭代过程中对其进行修改,直到内容与内容图像的内容和样式与样式图像的相似为止,将其设置为需要梯度计算至关重要。
同样,如果您的计算机具有可用的 GPU,请确保同时使用以下代码段将目标图像分配给 GPU:
target_img = content_img.clone().requires_grad_(True).to(device) -
使用我们在本章第一个练习中创建的
tensor2image函数,绘制目标图像,它看起来应该和内容图像一样。plt.figure() plt.imshow(tensor2image(target_img)) plt.title("Target Image") plt.show()输出图像如下:
图 5.5:目标图像
注意
要访问此特定部分的源代码,请参考这里。
您也可以通过这里在线运行此示例。 您必须执行整个笔记本才能获得所需的结果。
要访问此源代码的 GPU 版本,请参考这里。 此版本的源代码无法作为在线交互示例使用,需要通过 GPU 设置在本地运行。
这样,您就成功完成了特征提取并计算了语法矩阵来执行样式迁移。
优化算法,损失和参数更新
尽管使用参数不变的预训练网络执行样式迁移,但是创建目标图像包含一个迭代过程,在此过程中,通过仅更新与目标图像有关的参数来计算并最小化三个不同的损失函数。
为了实现目标图像的创建,需要计算两个不同的损失函数(内容损失和样式损失),然后将它们放在一起以计算总损失函数,该函数将被优化以得到合适的目标图像。 但是,考虑到在内容和样式方面实现的测量精度差异很大,下面是对内容和样式损失函数的计算的说明,以及如何计算总损失的说明。
内容损失
它由一个函数组成,该函数基于给定层获得的特征映射来计算内容图像和目标图像之间的距离。 在 VGG-19 网络的情况下,仅根据conv4_2层的输出来计算内容损失。
内容损失函数背后的主要思想是最小化内容图像和目标图像之间的距离,以便后者在内容方面与前者高度相似。
内容损失可以计算为相关层的内容和目标图像的特征映射之间的均方差(conv4_2),可以使用以下公式实现:
图 5.6:内容损失函数
样式损失
与内容损失类似,样式损失是一项函数,可通过计算均方差来衡量样式特征(例如颜色和纹理)方面的样式与目标图像之间的距离。
与内容损失的情况相反,它不比较从不同层派生的特征映射,而是比较根据样式和目标图像的特征映射计算出的语法矩阵。
必须使用循环为所有相关层(在本例中为五层)计算样式损失。 这将导致损失函数考虑来自两个图像的简单和复杂样式表示。
此外,优良作法是将这些层中每个层的样式表示权衡在 0 到 1 之间,以便比提取非常复杂特征的层更多地强调提取更大和更简单特征的层。 这是通过为较早的层(conv1_1和conv2_1)赋予更高的权重来实现的,这些层从样式图像中提取了更多的通用特征。
对于每个相关层,可以使用以下方程式来计算样式损失:
图 5.7:样式损失计算
总损失
最后,总损失函数由内容损失和样式损失共同组成。 通过更新目标图像的参数,在创建目标图像的迭代过程中将其值最小化。
同样,建议您为内容和样式损失分配权重,以确定它们是否参与最终输出。 这有助于确定目标图像样式化的程度,同时使内容仍然可见。 优良作法是将内容损失的权重设置为 1,而样式损失的权重必须更高,才能达到您的喜好比例。
分配给内容损失的权重通常称为alpha,而赋予样式损失的权重称为beta。
计算总损失的最终公式如下:
图 5.8:总损失计算
一旦确定了损失的权重,就可以设置迭代步骤的数量以及优化算法了,这只会影响目标图像。 这意味着,在每个迭代步骤中,将计算所有三个损失,以便我们可以使用梯度来优化与目标图像相关的参数,直到损失函数最小化并获得具有所需外观的目标图像为止。
与以前的神经网络的优化一样,以下是每次迭代中可以观察到的步骤:
- 从目标图像获取内容和样式方面的特征。 在初始迭代中,此图像将是内容图像的精确副本。
- 计算内容损失。 这是通过比较内容和目标图像的内容特征映射来完成的。
- 计算所有相关层的平均样式损失。 这是通过比较样式图像和目标图像所有层的 gram 矩阵来实现的。
- 计算总损失。
- 计算总损失函数相对于目标图像参数(权重和偏差)的偏导数。
- 重复此过程,直到达到所需的迭代次数。
最终输出将是内容与内容图像相似且样式与样式图像相似的图像。
练习 5.04:创建目标图像
在本章的最后练习中,您将执行样式迁移的任务。 该练习包括对负责执行不同迭代的部分进行编码,同时优化损失函数,以便获得理想的目标图像。 为此,至关重要的是利用我们在本章前面的练习中编程的代码位:
注意
在 GPU 上运行此代码时,需要进行一些更改。 请访问本书的 GitHub 存储库以修改此代码的 GPU 版本。
-
打开上一个练习中的笔记本。
-
定义一个字典,包含负责提取风格特征的每个层的权重。
style_weights = {'conv1_1': 1., 'conv2_1': 0.8, 'conv3_1': 0.6, \ 'conv4_1': 0.4, 'conv5_1': 0.2}确保使用与上一个练习中的层相同的名称作为键。
-
定义与内容和风格损失相关的权重。
alpha = 1 beta = 1e5 -
定义迭代步数,以及优化算法。如果我们想看到当时已经创建的图像的图,也可以设置迭代次数。
print_statement = 200 optimizer = torch.optim.Adam([target_img], lr=0.001) iterations = 2000该优化算法要更新的参数应该是目标图像的参数。
注意
如本练习中的示例所示,运行 2,000 次迭代将花费相当长的时间,具体取决于您的资源。 但是,为了获得出色的目标图像,可能需要更多的迭代。
为了使您理解每次迭代中目标图像发生的变化,可以进行几次迭代,但是建议您尝试更长的训练时间。
-
定义
for循环,在这个循环中,将计算所有三个损失函数,并执行优化过程。for i in range(1, iterations+1): # Extract features for all relevant layers target_features = features_extractor(target_img, model, \ relevant_layers) # Calculate the content loss content_loss = torch.mean((target_features['conv4_2'] \ - content_features['conv4_2'])**2) # Loop through all style layers style_losses = 0 for layer in style_weights: # Create gram matrix for that layer 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] # Calculate style loss for that layer style_loss = style_weights[layer] \ * torch.mean((target_gram - style_gram)**2) #Calculate style loss for all layers style_losses += style_loss / (d1 * d2 * d3) # Calculate the total loss total_loss = alpha * content_loss + beta * style_losses # Perform back propagation optimizer.zero_grad() total_loss.backward() optimizer.step() # Print target image if i % print_statement == 0 or i == 1: print('Total loss: ', total_loss.item()) plt.imshow(tensor2image(target_img)) plt.show() -
绘制内容和目标图像来比较结果。这可以通过使用
tensor2image函数来实现,我们在之前的练习中创建了这个函数,以便将张量转换为可以使用matplotlib打印的 PIL 图像。fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(20, 10)) ax1.imshow(tensor2image(content_img)) ax2.imshow(tensor2image(target_img)) ax3.imshow(tensor2image(style_img)) plt.show()最终图像应类似于以下内容:
图 5.9:内容,样式和目标图像之间的比较
注意
要查看高质量的彩色图像,请访问本书的 GitHub 存储库。
这样,您就成功地执行了样式迁移。
注意
要访问此特定部分的源代码,请参考这里。
本部分当前没有在线交互示例,需要在本地运行。
要访问此源代码的 GPU 版本,请参考这里。 此版本的源代码无法作为在线交互示例使用,需要通过 GPU 设置在本地运行。
练习 5.01:执行样式迁移
在此活动中,我们将进行样式迁移。 为此,我们将编写本章所学的所有概念。 让我们看一下以下情况。
您需要更改一些图像,以使其具有艺术气息,为实现此目的,您决定创建一些代码,该代码使用预先训练的神经网络执行样式迁移。 请按照以下步骤完成此活动:
-
导入所需的库。
-
指定要对输入图像执行的转换。 确保将它们调整为相同的大小,将它们转换为张量,然后对其进行规范化。
-
定义图像加载器函数。 这应该打开图像并对其进行转换。 调用图像加载器函数加载两个输入图像。
-
为了能够显示图像,请定义一组新的转换,以恢复图像的规范化并将张量转换为 PIL 图像。
-
创建一个函数(
tensor2image),该函数能够对张量执行先前的转换。 调用两个图像的函数并绘制结果。 -
加载 VGG-19 模型。
-
创建一个字典,将相关层(键)的索引映射到名称(值)。 然后,创建一个函数以提取相关层的特征映射。 使用它们提取两个输入图像的特征。
-
计算样式特征的克矩阵。 另外,创建初始目标图像。
-
设置不同样式层的权重以及内容和样式损失的权重。
-
运行模型 500 次迭代。在开始训练模型之前,定义 Adam 优化算法,以 0.001 作为学习率。
注意
根据您的资源,训练过程可能需要几个小时。 因此,要获得出色的结果,建议您进行数千次迭代训练。 如果您希望查看训练过程的进度,则添加打印语句是一种很好的做法。
本章中显示的结果是通过运行大约 5,000 次迭代而获得的,如果不使用 GPU,则将花费很长时间(在本书的 GitHub 存储库中也找到了使用 GPU 的活动的解决方案)。 但是,仅看到一些小的更改,就可以按照本活动的建议运行它几百次迭代就足够了(500)。
-
绘制内容、风格、目标图片,比较结果。
5,000 次迭代后的输出应如下所示:
图 5.10:绘制内容和目标图像
注意
有关此活动的解决方案,请参见第 277 页。
要查看“图 5.10”的高质量彩色图像,请访问这里。
总结
本章介绍了样式迁移,这是当今很流行的任务,可以使用 CNN 来执行。 它包括同时获取内容图像和样式图像作为输入,并返回新创建的图像作为输出,以保留其中一个图像的内容和另一个图像的样式。 它通常用于通过将随机的常规图像与伟大艺术家的绘画相结合来赋予图像艺术外观。
尽管使用 CNN 进行样式迁移,但是按常规训练网络并不能实现创建目标图像的过程。 本章介绍了如何使用经过预训练的网络来考虑一些相关层的输出,这些层尤其擅长识别某些特征。
本章介绍了开发能够执行样式迁移任务的代码所需的每个步骤,其中第一步包括加载和显示输入。 正如我们前面提到的,模型有两个输入(内容和样式图像)。 每个图像都将经历一系列转换,目的是将图像调整为相等大小,将它们转换为张量,并对它们进行规范化,以使它们可以被网络正确处理。
接下来,加载预训练的模型。 如本章所述,VGG-19 是解决此类任务的最常用架构之一。 它由 19 个层组成,包括卷积层,池化层和全连接层,其中对于所讨论的任务,仅使用某些卷积层。 考虑到 PyTorch 提供了一个包含多个预训练网络架构的子包,加载预训练模型的过程非常简单。
一旦加载了网络,在检测某些对于样式迁移至关重要的特征时,网络的某些层将被识别为表现卓越的提供商。 尽管五个不同的层都具有提取与图像样式相关的特征(例如颜色和纹理)的能力,但是只有一层可以非常出色地提取内容特征(例如边缘和形状)。 因此,至关重要的是定义将用于从输入图像中提取信息以创建所需目标图像的那些层。
最后,是时候对该迭代过程进行编码了,以用于创建具有所需特征的目标图像。 为此,计算了三种不同的损失。 有一个用于比较内容图像和目标图像之间的内容差异(内容损失),另一个用于比较样式图像和目标图像之间的风格差异(样式损失), 通过计算克矩阵实现。 最后,有一个结合了内容损失和样式损失(总损失)。
通过最小化总损失的值来创建目标图像,这可以通过更新与目标图像有关的参数来完成。 尽管可以使用预先训练的网络,但获得理想目标图像的过程可能需要进行数千次迭代和相当多的时间。
在下一章中,将说明不同的网络架构,以便使用文本数据序列解决数据问题。 RNN 是保存内存的神经网络架构,允许它们处理序列数据。 它们通常用于解决与人类语言理解有关的问题。
六、使用 RNN 分析数据序列
概述
本章扩展了循环神经网络的概念。 您将了解循环神经网络(RNN)的学习过程以及它们如何存储内存。 本章将介绍长短期记忆(LSTM)网络架构,该架构使用短期和长期存储器来解决数据序列中的数据问题。 在本章的最后,您将牢固地掌握 RNN 以及如何解决自然语言处理(NLP)数据问题。
简介
在前面的章节中,介绍了不同的网络架构-从可以同时解决分类和回归问题的传统 ANN 到主要用于通过执行对象分类,定位,检测和分段任务来解决计算机视觉问题的 CNN 。
在最后一章中,我们将探讨 RNN 的概念并解决序列数据问题。 这些网络架构能够保存上下文至关重要的序列数据,这要归功于它们能够保存来自先前预测的信息(称为内存)。 这意味着,例如,当逐个单词分析句子时,RNN 在处理最后一个单词时可以保留有关该单词第一个单词的信息。
本章将探讨 LSTM 网络架构,它是一种 RNN,可以同时存储长期和短期内存,并且对于处理长数据序列(例如视频剪辑)特别有用。
本章还将探讨 NLP 的概念。 NLP 指的是计算机与人类语言的交互,由于虚拟助手的兴起,如今这已成为热门话题,虚拟助手可以提供定制的客户服务。 本章将使用 NLP 进行情感分析,其中包括分析句子后面的含义。 这有助于根据客户评论了解客户对产品或服务的看法。
注意
循环神经网络
就像人类不会每秒重新设置思想一样,旨在理解人类语言的神经网络也不应这样做。 这意味着,为了理解段落中甚至整个书中的每个单词,您或模型需要理解之前的单词,这可以帮助给可能具有不同含义的单词提供上下文。
到目前为止,我们已经讨论了传统的神经网络无法执行此类任务,因此创建了 RNN 的概念和网络架构。 正如我们之前简要解释的那样,这些网络架构包含不同节点之间的环路。 这使信息可以在模型中保留更长的时间。 因此,来自模型的输出既成为预测又是存储器,当下一行已排序的文本通过模型时将使用该存储器。
这个概念可以追溯到 1980 年代,尽管它只是在最近才变得流行,这要归功于技术的进步,这种进步导致机器的计算能力提高了,并允许数据的收集以及对概念的发展。 1990 年代的 LSTM RNN,增加了其应用范围。 由于 RNN 具有存储内部存储器的能力,因此它们是最有前途的网络架构之一,这使它们能够有效地处理数据序列并解决各种数据问题。
RNN 的应用
尽管我们已经很清楚地表明 RNN 最适合于数据序列,例如文本,音频片段和视频,但仍然有必要解释 RNN 在现实生活中的不同应用。
这是通过使用 RNN 可以执行的不同任务的一些简要说明:
-
NLP。这指的是机器代表人类语言的能力。这可能是深度学习中探索最多的领域之一,也无疑是利用 RNN 时首选的数据问题。其思路是以文本作为输入数据来训练网络,如诗词和书籍等,目的是创建一个能够生成此类文本的模型。
NLP 通常用于创建聊天机器人(虚拟助手)。 通过从以前的人类对话中学习,NLP 模型能够帮助一个人解决常见问题或疑问。 因此,他们的句子表达能力仅限于他们在训练过程中所学到的内容,这意味着他们只能回答所学的内容。
当您尝试通过在线聊天系统与银行联系时,您可能会遇到这种情况,在这种情况下,通常您会在查询超出常规范围的那一刻将您转到人工运算符那里。 现实生活中聊天机器人的另一个常见示例是通过 Facebook Messenger 进行查询的餐馆:
图 6.1:Facebook 的 Messenger 聊天机器人
-
语音识别。与 NLP 类似,语音识别试图理解和表示人类语言。然而,这里的区别在于,前者(NLP)是以文本的形式进行训练并产生输出,而后者(语音识别)则使用音频片段。随着这一领域的发展,以及大公司的兴趣,这些模型能够理解不同的语言,甚至不同的口音和发音。
语音识别设备的一个流行示例是 Alexa –来自亚马逊的语音激活虚拟助手模型:
图 6.2:亚马逊的 Alexa
-
机器翻译。这是指机器有效翻译人类语言的能力。据此,输入是源语言(如西班牙语),输出是目标语言(如英语)。NLP 与机器翻译的主要区别在于,在后者中,输出是在将整个输入输入输入到模型后建立的。
随着全球化的兴起和当今休闲旅行的普及,人们需要使用多种语言。 因此,出现了能够在不同语言之间进行翻译的设备。 该领域的最新成果之一是 Google 的 Pixel Buds,它可以实时执行翻译:
图 6.3:Google 的像素芽
-
时间序列预测。RNN 的一个不太流行的应用是根据历史数据预测未来的数据点序列。由于 RNN 能够保留内部记忆,使时间序列分析能够考虑过去的不同时间段来进行未来的预测或一系列预测,因此 RNN 特别擅长这项任务。
这通常用于预测未来的收入或需求,这有助于公司为不同的情况做好准备。 下图显示了每月销售额的预测:
图 6.4:每月销售额的预测(数量)
例如,如果通过预测对几种保健产品的需求,确定对一种产品的需求增加而对另一种产品的需求减少,则公司可以决定生产更多的该产品,以及更少的其他产品。
- 图像识别:结合 CNN,RNN 可以为图像提供标题或描述。 通过这些模型组合,您可以检测图像中的所有对象,从而确定图像的主要成分。 输出可以是图像中存在的对象的一组标签,图像的描述或图像中相关对象的标题,如下图所示:
图 6.5:使用 RNN 进行图像识别
RNN 如何工作?
简而言之,RNN 接受输入(x)并返回输出(y)。 在此,输出不仅受输入影响,而且还受过去输入的输入的整个历史影响。 输入的这种历史记录通常称为模型的内部状态或内存,它们是遵循顺序并相互关联的数据序列,例如时间序列,它是一系列数据点(例如,销售) )(按月列出)。
注意
请记住,RNN 的一般结构可能会有所变化,具体取决于当前的问题。 例如,它们可以是一对多类型,也可以是多对一类型,正如我们在“第 2 章”,“神经网络构建模块”中提到的那样。
要了解 RNN 的概念,了解 RNN 与传统神经网络之间的区别非常重要。 传统的神经网络通常被称为前馈神经网络,因为信息仅沿一个方向移动(即,从输入到输出),而没有两次通过节点执行预测。 这些网络对过去所馈送的内容没有任何记忆,这就是为什么它们不善于预测序列中接下来将发生什么。
另一方面,在 RNN 中,信息在循环中循环,因此每次预测都通过考虑先前预测的输入和内存来进行。 它通过复制每个预测的输出并将其传递回网络以进行后续预测来工作。 这样,RNN 就有两个输入-现在值和过去信息:
图 6.6:网络的图形表示,其中 A 显示前馈神经网络,B 显示 RNN
注意
传统 RNN 的内部记忆只是短期的。 但是,我们将探索一种能够在以后存储长期和短期内存的架构。
通过使用来自先前预测的信息,网络将按照一系列有序数据进行训练,从而可以预测下一步。 这是通过将当前信息和上一步的输出合并为一个操作来实现的。 在下图中可以看到。 此操作的输出将成为预测,以及后续预测的输入的一部分:
图 6.7:每个预测的 RNN 计算
如您所见,节点内部发生的操作是任何其他神经网络的操作。 最初,数据通过线性函数传递。 权重和偏差是在训练过程中要更新的参数。 接下来,使用激活函数来破坏该输出的线性度。 在这种情况下,这就是 tanh 函数,因为多项研究表明,对于大多数排序数据问题,它可以获得更好的结果:
图 6.8:传统 RNN 的数学计算
此处,M[t-1]是指从先前的预测得出的内存,W和b是权重和偏差,而E[t]表示当前事件。
考虑到这一学习过程,让我们考虑一下过去两年中产品的销售数据。 RNN 能够预测下个月的销售量,因为通过存储最近几个月的信息,RNN 可以检查销售量是增加还是减少。
使用“图 6.7”,可以通过获取上个月的销售额(即当前事件)和短期记忆(代表最近一个月的数据)来处理后面几个月的预测并将其合并。 此操作的输出将包含下个月的预测以及最近几个月的一些相关信息,这些信息将反过来成为后续预测的新短期记忆。
此外,重要的是要提到一些 RNN 架构,例如 LSTM 网络,也将能够考虑 2 年前甚至更早的数据(因为它存储了长期内存)。 这将使网络知道特定月份内的减少是否可能继续减少或开始增加。 稍后,我们将更详细地探讨该主题。
输入和目标序列数据
考虑到目标是预测序列中的下一个元素,因此目标矩阵通常是与输入数据相同的信息,而目标则领先一步。
这意味着输入变量应包含序列的所有数据点(最后一个值除外),而目标变量应包含序列的所有数据点,但第一个值除外(即,第一个值) 目标变量应该是输入变量的第二个,依此类推,如下图所示:
图 6.9:序列数据问题的输入变量和目标变量
练习 6.01:为序列数据问题创建输入和目标变量
在本练习中,您将使用虚拟数据集学习如何创建可用于解决排序数据问题的输入和目标变量。 请按照以下步骤完成此练习:
注意
对于本章中的练习和活动,您需要在本地计算机上安装 Python 3.7,Jupyter 6.0,Matplotlib 3.1,NumPy 1.17,Pandas 0.25 和 PyTorch 1.3+(最好是 PyTorch 1.4)。
-
导入以下库:
import pandas as pd import numpy as np import torch -
创建一个
10×5大小的 PandasDataFrame,里面充满了从 0 到 100 的随机数。命名五列如下:["Week1", "Week2", "Week3", "Week4", "Week5"]。确保将随机种子设置为
0,以便能够重现本书中显示的结果:np.random.seed(0) data = pd.DataFrame(np.random.randint(0,100,size=(10, 5)), columns=['Week1','Week2','Week3',\ 'Week4','Week5']) data注意
提醒一下,在 Jupyter 笔记本中,无需打印函数即可打印变量的值。 在其他编程平台上,可能需要使用打印函数。
结果数据帧如下:
图 6.10:创建的
DataFrame -
创建一个输入变量和一个目标变量,考虑到输入变量应该包含所有实例的所有值,除了最后一列数据。目标变量应包含所有实例的所有值,但第一列数据除外。
inputs = data.iloc[:,:-1] targets = inputs.shift(-1, axis="columns", \ fill_value=data.iloc[:,-1:]) -
打印输入变量以验证其内容,如下图所示。
inputs输入变量应如下所示:
图 6.11:输入变量
-
使用下面的代码打印出目标变量。
targets运行前面的代码将显示以下输出:
图 6.12:目标变量
注意
要访问此特定部分的源代码,请参考这里。
您也可以通过这里在线运行此示例。 您必须执行整个笔记本才能获得所需的结果。
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函数中,输入通过循环层,而这些层的输出被展平,以便可以通过全连接层。 值得一提的是,信息与隐藏状态(内存)一起通过 RNN 层传递。
此外,这种网络的训练可以按以下方式处理:
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()
对于每个周期,隐藏状态都初始化为None。 这是因为,在每个周期,网络都会尝试将输入映射到目标(给定一组参数时)。 该映射应该与数据集中之前的运行没有任何偏差(隐藏状态)地发生。
接下来,for循环遍历不同批量的数据。 在此循环内,将进行预测并保存隐藏状态,该状态将用作下一批的输入。
最后,计算损失函数,该函数用于更新网络参数。 然后,该过程再次开始,直到达到所需的周期数。
活动 6.01:使用简单 RNN 的时间序列预测
对于此活动,您将使用简单的 RNN 解决时间序列问题。 让我们考虑以下情况:您的公司希望能够提前预测所有产品的需求。 这是因为生产每种产品都需要花费相当长的时间,并且该过程花费大量金钱。 因此,除非产品很可能被出售,否则他们不希望在生产上花费金钱和时间。 为了预测未来需求,他们为您提供了一个数据集,其中包含去年销售中所有产品的每周需求(在销售交易中)。 请按照以下步骤完成此活动:
注意
可以在这个页面上找到包含将在此活动中使用的数据集的 CSV 文件。 也可以从这里在线获取。
数据集和相关分析首次在此处发布:Tan S.C., Lau J.P.S. (2014) Time Series Clustering: A Superior Alternative for Market Basket Analysis. In: Herawan T., Deris M., Abawajy J. (eds) Proceedings of the First International Conference on Advanced Data and Information Engineering (DaEng-2013). Lecture Notes in Electrical Engineering, vol 285. Springer, Singapore.
-
导入所需的库。
-
加载数据集并对其进行切片,以使其包含所有行,但仅包含索引 1 至 52 的列。
-
从整个数据集中绘制五个随机选择的产品的每周销售交易图。 进行随机采样时,请使用
0的随机种子,以获得与当前活动相同的结果。 -
创建
inputs和targets变量,这些变量将被输入到网络中以创建模型。这些变量应具有相同的形状,并转换为 PyTorch 张量。input变量应包含除上周外所有周所有产品的数据,因为模型的目的是预测最后一周。target变量应比input变量领先一步; 也就是说,target变量的第一个值应该是输入变量的第二个,依此类推,直到target变量的最后一个值(应该是最后一个input变量之外的一周)。 -
创建一个包含网络架构的类; 请注意,全连接层的输出大小应为 1。
-
实例化包含模型的类函数。 输入输入大小,每个循环层中的神经元数(10)和循环层数(1)。
-
定义损失函数,优化算法和训练网络的周期数。 为此,请使用均方误差损失函数,Adam 优化器和 10,000 个周期。
-
使用
for循环通过遍历所有周期来执行训练过程。 在每个周期,都必须进行预测,以及随后的损失函数计算和网络参数优化。 然后,保存每个周期的损失。 -
绘制所有周期的损失。
-
使用散点图,显示在训练过程的最后一个周期中获得的预测值与真实情况值(即上周的销售交易)的对比。
注意
此活动的解决方案可以在第 284 页上找到。
长短期记忆网络
如前所述,RNN 仅存储短期内存。 在处理较长的数据序列时,这是一个问题,在这种情况下,网络将信息从较早的步骤传输到最终步骤时会遇到麻烦。
例如,以著名的诗人埃德加·艾伦·坡(Edgar Alan Poe)所写的诗《乌鸦》(The Raven)为例,诗长超过 1000 个字。 试图使用传统的 RNN 来处理它,以创建相似的相关诗歌为目标,将导致该模型在前几段中遗漏了关键信息。 反过来,这可能导致输出与诗歌的初始主题无关。 例如,它可以忽略事件是在晚上发生的,因此使新诗不是很吓人。
之所以无法保持长期记忆,是因为传统的 RNN 遭受称为梯度消失的问题。 当用于更新网络参数以最小化损失函数的梯度变得非常小,从而不再对网络的学习过程有所帮助时,就会发生这种情况。 这通常发生在网络的前几层,使网络忘记了一段时间。
因此,开发了 LSTM 网络。 LSTM 网络可以像存储计算机一样存储内部存储器,因此可以长时间记住信息。 也就是说,它们根据需要读取,写入和删除信息,这是通过使用门来实现的。
这些门根据分配给每个信息位的重要性,帮助网络决定保留哪些信息以及从内存中删除哪些信息(是否打开门)。 这非常有用,因为它不仅可以存储更多信息(作为长期记忆),而且还有助于丢弃可能改变预测结果的无用信息,例如句子中的文章。
LSTM 网络的应用
除了我们先前解释的应用之外,LSTM 网络存储长期信息的能力还使数据科学家能够处理复杂的数据问题,这些问题利用大数据序列作为输入,下面将对其中的一些问题进行说明:
- 文本生成:生成任何文本,例如您在此处阅读的文本,都可以转换为 LSTM 网络的任务。 通过基于所有先前字母选择每个字母来工作。 使用大型文本(例如著名书籍的文本)来训练执行此任务的网络。 这是因为最终模型将创建一个文本,该文本类似于受其训练的文本的书写样式。 例如,以诗训练的模型将具有与您在与邻居交谈中期望的叙事不同的叙事。
- 音乐生成:就像可以将文本序列输入到网络中以生成相似的新文本一样,也可以将一系列音符输入网络以生成新的音符序列。 跟踪先前的音符将有助于获得和谐的旋律,而不仅仅是一系列随机的音符。 例如,用披头士乐队的一首流行歌曲来馈送音频文件,将产生一系列音符,这些音符类似于乐队的和声。
- 手写体的生成和识别:这里,每个字母也是所有先前字母的乘积,这又将导致一组具有含义的手写字母。 同样,LSTM 网络也可以用于识别手写文本,其中一个字母的预测将取决于所有先前预测的字母。 例如,在考虑先前的字母以及整个段落时,识别难看的手写字母会更容易,因为这有助于根据上下文缩小预测范围。
LSTM 网络如何工作?
到目前为止,已经明确了 LSTM 网络与传统 RNN 的区别在于它们具有长期记忆的能力。 但是,必须指出的是,随着时间的流逝,非常古老的信息不太可能影响下一个输出。 考虑到这一点,LSTM 网络还具有考虑数据位与底层上下文之间距离的能力,以便也可以决定忘记一些不再相关的信息。
那么,LSTM 网络如何决定何时记住和何时忘记? 与传统的 RNN 不同,LSTM 网络在每个节点中仅执行一次计算,而 LSTM 网络执行四种不同的计算,从而允许网络的不同输入(即当前事件,短期记忆和长期记忆项)之间进行交互,以得出结果。
要了解 LSTM 网络背后的过程,请考虑用于管理网络中信息的四个门,如下图所示:
图 6.13:LSTM 网络门
上图中每个门的功能可以解释如下:
- 学习门:短期记忆(也称为隐藏状态)和当前事件都进入学习门,在此分析信息,并忽略所有不需要的信息。 在数学上,这是通过使用线性函数和激活函数(tanh)将短期记忆和当前事件结合在一起来实现的。 它的输出乘以忽略因子,从而删除所有不相关的信息。 为了计算忽略因子,通过线性函数传递短期记忆和当前事件。 然后,将它们通过 Sigmoid 激活函数压缩在一起:
图 6.14:学习门中发生的数学计算
此处, STM[t-1]是指从先前的预测得出的短期记忆,W和b是权重和偏差,并且E[t]表示当前事件。
- 遗忘门:长期存储(也称为单元状态)进入遗忘门,其中删除了一些信息。 这是通过将长期记忆和遗忘因子相乘来实现的。 要计算遗忘因子,请将短期记忆和当前事件通过线性函数和激活函数(sigmoid)传递:
图 6.15:遗忘门中发生的数学计算
此处, STM[t-1]是指从先前的预测中得出的短期记忆, LTM[t-1]是从先前的预测中得出的长期记忆 ,W和b是权重和偏差,E[t]是指当前事件。
- 记忆门:在记忆门中未被遗忘的长期记忆和从学习门保留的信息将被组合在记忆门中,这将成为新的长期记忆。 从数学上讲,这是通过对学习和遗忘门的输出求和来实现的:
图 6.16:记忆门中发生的数学计算
此处,L[t]表示来自学习门的输出,而F[t]表示来自遗忘门的输出。
-
使用门。这也称为输出门。在这里,来自学习门和遗忘门的信息被合并到使用门中。该门利用所有相关信息进行预测,也成为新的短期记忆。
这可以通过三个步骤实现。 首先,它将线性函数和激活函数(tanh)应用于遗忘门的输出。 其次,它将线性函数和激活函数(Sigmoid)应用于短期记忆和当前事件。 第三,它将前面步骤的输出相乘。 第三步的输出将是新的短期记忆和当前步的预测:
图 6.17:使用门中发生的数学计算
此处, STM[t-1]是指从先前的预测得出的短期记忆,W和b是权重和偏差,并且E[t]表示当前事件。
注意
尽管使用不同的激活函数和数学运算符似乎是任意的,但这样做是因为已被证明可以处理大多数处理大型数据序列的数据问题。
对于模型执行的每个预测,都完成了前面的过程。 例如,对于构建用于创建文学作品的模型,将对模型将产生的每个单个字母执行学习,遗忘,记忆和使用信息的过程,如下图所示:
图 6.18:整个时间的 LSTM 网络过程
PyTorch 中的 LSTM 网络
在 PyTorch 中定义 LSTM 网络架构的过程与到目前为止我们讨论过的任何其他神经网络的过程相似。 但是,重要的是要注意,当处理与数字序列不同的数据序列时,需要进行一些预处理才能为网络提供它可以理解和处理的数据。
考虑到这一点,我们需要解释训练模型的一般步骤,以便能够将文本数据作为输入并检索新的文本数据。 值得一提的是,并非严格要求此处说明的所有步骤,而是作为一个整体,它们为使用 LSTM 与文本数据结合在一起提供了干净且可重用的代码。
预处理输入数据
第一步是将文本文件加载到代码中。 这些数据将经过一系列转换,以便正确地输入到模型中。 这是必需的,因为神经网络执行一系列数学计算才能得出输出,这意味着所有输入都必须是数字。 另外,将数据批量而不是一次全部馈入模型也是一个好习惯,因为这有助于减少训练时间,尤其是对于长数据集。 这些转换将在以下小节中进行说明。
数字标签
首先,从输入数据中获得未重复字符的列表。 这些字符中的每一个都分配有一个数字。 然后,考虑到必须始终用相同的数字表示相同的字母,通过将每个字符替换为指定的数字来对输入数据进行编码。 例如,给定以下字符和数字映射,单词hello将被编码为 12334:
图 6.19:字符和数字的映射
可以通过以下代码片段实现上述输出:
text = "this is a test text!"
chars = list(set(text))
indexer = {char: index for (index, char) \
in enumerate(chars)}
indexed_data = []
for c in text:
indexed_data.append(indexer[c])
代码的第二行创建一个包含文本字母(即文本序列中的字母和字符)的列表。 接下来,使用每个字母或字符作为键并使用与其关联的数字表示作为值来创建字典。 最后,通过对文本执行for循环,可以将每个字母或字符替换为其数字表示形式,从而将文本转换为数字矩阵。
生成批量
对于 RNN,使用两个变量创建批量:每个批量的序列数和每个序列的长度。 这些值用于将数据划分为矩阵,这将有助于加快计算速度。
使用 24 个整数的数据集,每批的序列数设置为 2,序列长度等于 4,除法如下:
图 6.20:RNN 的批量生成
如上图所示,创建了三个批量–每个批量包含两个长度为 4 的序列。
对于x和y,应完成此批生成过程,其中前者是网络的输入,后者是目标。 据此,网络的思想是找到一种方法来映射x和y之间的关系,考虑到y将领先一步。x。
x的批量是按照上图中说明的方法创建的。 然后,将创建y的批量,以使其与x的长度相同。 这是因为y的第一个元素将是x的第二个元素,依此类推,直到y的最后一个元素(将是第一个元素)x的数量):
注意
您可以使用多种不同的方法来填充y的最后一个元素,这里提到的是最常用的一种方法。 方法的选择通常是优先考虑的问题,尽管某些数据问题可能从某种方法中受益而不是其他方法。
图 6.21:x和y批量的表示
批量的生成可以通过以下代码片段实现:
x = np.array(indexed_data).reshape((2,-1))
for b in range(0, x.shape[1], 5):
batch = x[:,b:b+5]
print(batch)
首先,将数字矩阵划分为多个序列(根据需要)。 接下来,通过for循环,可以将已排序的数据划分为指定长度的批量。 通过打印batch变量,可以观察到结果。
注意
尽管生成批量被视为预处理数据的一部分,但通常会在训练过程的for循环内部对其进行编程。
单热编码
将所有字符转换为数字不足以将其输入模型。 这是因为这种近似会给您的模型带来一些偏差,因为转换为更高数值的字符将被视为更重要。 为避免这种情况,优良作法是将不同批量编码为一热矩阵。 这包括创建一个具有零和一的三维矩阵,其中零表示不存在事件,而一个表示存在事件。 矩阵的最终形状应为one hot = [number of sequences, sequence length, number of characters]。
这意味着,对于批量中的每个元素,它将创建一个长度等于整个文本中字符总数的值序列。 对于每个字符,它将放置一个零,但该位置存在一个零(它将放置一个)。
注意
您可以在这个页面中找到有关单热编码的更多信息。
这可以通过以下代码片段实现:
batch = np.array([[2 4 7 6 5]
[2 1 6 2 5]])
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))
首先,将二维批量展平。 接下来,创建一个矩阵,并用零填充。 当我们需要在给定位置表示正确的字符时,用零代替零。 最后,展平的尺寸再次扩大。
练习 6.02:预处理输入数据并创建单热矩阵
在本练习中,您将预处理文本片段,然后将其转换为单热矩阵。 请按照以下步骤完成此练习:
-
导入 NumPy。
import numpy as np -
创建一个名为
text的变量,其中将包含文本样本"Hello World!"。text = "Hello World!" -
通过将每个字母映射到一个数字来创建一个字典。
chars = list(set(text)) indexer = {char: index for (index, char) \ in enumerate(chars)} print(indexer)运行前面的代码将得到以下输出:
{'d': 0, 'o': 1, 'H': 2, ' ': 3, 'e': 4, 'W': 5, '!': 6, 'l': 7, 'r': 8} -
用我们在上一步中定义的数字对你的文本样本进行编码。
encoded = [] for c in text: encoded.append(indexer[c]) -
将编码变量转换为 NumPy 数组,并对其进行重塑,使句子被分成两个大小相同的序列。
encoded = np.array(encoded).reshape(2,-1) encoded运行前面的代码将得到以下输出:
array([[2, 4, 7, 7, 1, 3], [5, 1, 8, 7, 0, 6]]) -
定义一个函数,接收一个数字数组,并创建一个单热矩阵。
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 -
通过之前定义的函数将编码数组转换为单热矩阵。
one_hot = index2onehot(encoded) one_hot输出应如下所示:
图 6.22:示例文本的单热表示
注意
要访问此特定部分的源代码,请参考这里。
您也可以通过这里在线运行此示例。 您必须执行整个笔记本才能获得所需的结果。
您已成功将一些示例文本转换为单热矩阵。
构建架构
与其他神经网络类似,可以在一行代码中轻松定义 LSTM 层。 但是,包含网络架构的类必须包含一个函数,该函数允许初始化隐藏状态和单元状态(即网络的两个内存)。 LSTM 网络架构的示例如下:
```py
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 层数
与任何其他网络一样,forward函数定义了在forward传递中通过层移动数据的方式。
最后,定义一个函数以在每个周期将隐藏状态和单元状态初始化为零。 这是通过next(self.parameters()).data.new()实现的,该方法获取模型的第一个参数,并在括号内创建具有指定尺寸的相同类型的新张量, 然后用零填充。 将hidden和cell状态都作为元组输入模型。
训练模型
一旦定义了损失函数和优化算法,就该训练模型了。 这是通过采用与其他神经网络架构非常相似的方法来实现的,如以下代码片段所示:
# Step 1: for through epochs
for e in range(1, epochs+1):
# Step 2: Memory initialized
states = model.init_states(n_seq)
# Step 3: for loop to split data in batches.
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]
"""
Step 4: input data is converted to one-hot matrix.
Inputs and targets are converted to tensors.
"""
x_onehot = torch.Tensor(index2onehot(x_batch))
y = torch.Tensor(y_batch).view(n_seq * seq_length)
"""
Step 5: get a prediction and perform the
backward propagation
"""
pred, states = model(x_onehot, states)
loss = loss_function(pred, y.long())
optimizer.zero_grad()
loss.backward(retain_graph=True)
optimizer.step()
如前面的代码所示,步骤如下:
- 为了获得更好的模型,有必要遍历数据多次,因此需要设置多个周期。
- 在每个周期,必须初始化隐藏状态和单元状态。 这是通过调用该类中先前创建的函数来实现的。
- 使用循环
for将数据分批馈入模型。if语句用于确定它是否是最后一批,以便在句末添加句点,以表示句点。 - 输入数据被转换为一热矩阵。 输入和目标都将转换为 PyTorch 张量。
- 通过对一批数据调用模型来获得网络的输出。 然后,计算损失函数,并优化参数。
执行预测
优良作法是为训练后的模型提供前几个字符,以便执行具有某种目的的预测(例如,以单词on time on time开头的段落)。 这些初始字符应在不执行任何预测的情况下馈送到模型中,但目的是生成内存。 接下来,将前一个字符和存储器馈入网络,并通过 softmax 函数传递输出,以便计算每个字符成为序列中下一个字符的概率。 最后,从概率较高的角色中随机选择一个。
这可以通过以下代码片段实现:
# Step 1
starter = "This is the starter text"
states = None
# Step 2
for ch in starter:
x = np.array([[indexer[ch]]])
x = index2onehot(x)
x = torch.Tensor(x)
pred, states = model(x, states)
# Step 3
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())
# Step 4
starter += chars[index]
print(starter)
在上一个代码段中,将执行以下步骤:
- 起始句子已定义。
for循环将开始句子的每个字符输入模型,以便在进行预测之前更新模型的内存。- 只要循环和循环用于执行新字符的预测,只要字符数不超过 50,并且直到新字符是一个句点即可。
- 每个新字符都添加到起始句子中,以形成新的文本序列。
活动 6.02:使用 LSTM 网络的文本生成
注意
可以在互联网上免费访问此活动中将使用的文本数据,尽管您也可以在本书的 GitHub 存储库中找到它。 在本章的简介中提到了存储库的 URL。
在本活动中,我们将使用《爱丽丝梦游仙境》 一书训练 LSTM 网络,以便我们可以将一个起始语句输入模型并使其完成该语句。 请考虑以下情形:您喜欢使生活更轻松的事物,并决定建立一个模型来帮助您在编写电子邮件时完成句子。 为此,您已决定使用一本流行的儿童读物来训练网络。 请按照以下步骤完成此活动:
注意
值得一提的是,尽管本次活动中的网络经过了足够的迭代训练以显示出令人满意的结果,但并未对其进行训练和配置以实现最佳表现。 鼓励您使用它来提高性能。
-
导入所需的库。
-
打开并将爱丽丝梦游仙境中的文本读到笔记本中。 打印前 50 个字符和文本文件总长度的摘录。
-
创建一个变量,该变量包含数据集中未重复字符的列表。 然后,创建一个字典,将每个字符映射到一个整数,其中字符将是键,而整数将是值。
-
将数据集中的每个字母编码为成对的整数。 打印数据集的前 50 个编码字符和编码版本的总长度。
-
创建一个接受批量并将其编码为单热点矩阵的函数。
-
创建一个定义网络架构的类。 该类应包含一个用于初始化 LSTM 层状态的附加函数。
-
请确定要从数据集中创建的批量数量,请记住每个批量应包含 100 个序列,每个批量的长度应为 50。接下来,将编码数据拆分为 100 个序列。
-
使用 256 作为隐藏单元数(总共两个循环层)实例化模型。
-
定义损失函数和优化算法。使用 Adam 优化器和交叉熵损失。训练网络 20 个周期。
注意
根据您的资源,训练过程将花费很长时间,这就是为什么建议仅运行 20 个周期的原因。 但是,本书的 GitHub 存储库中提供了可以在 GPU 上运行的代码的等效版本。 这将使您运行更多的周期并获得出色的表现。
-
在每一个周期,数据必须被划分为 50 个序列长度的批次。这意味着每个周期将有 100 个序列,每个序列的长度为 50。
注意
为输入和目标创建了批量 ,其中后者是前者的副本,但领先一步。
-
绘制随时间推移的损失进度。
-
将下面的句子启动器输入到训练好的模型中,并完成这个句子:
"So she was considering in her own mind "。注意
有关此活动的解决方案,请参见第 290 页。
最后一句话会有所不同,因为在选择每个字符时会有一个随机因素。 但是,它应该看起来像这样:
So she was considering in her own mind us on," said she whad se the sire.前面的句子没有意义,因为网络没有经过足够的时间训练(损失函数可能仍然是最小化的),并且可以一次选择每个字符,而无需长期存储以前创建的单词(尽管如此),我们可以看到,仅 20 个周期后,网络就已经能够形成一些有意义的单词。
自然语言处理
计算机擅长分析标准化数据,例如财务记录或表中存储的数据库。 实际上,它们具有比人类更好的能力,因为它们能够一次分析数百个变量。 另一方面,人类擅长分析非结构化数据(例如语言),除非计算机掌握了一套规则以帮助他们理解,否则计算机就不会擅长这样做。
考虑到这一点,对于人类语言而言,计算机面临的最大挑战是,即使在一台非常大的数据集上经过很长时间的训练之后,计算机可以擅长分析人类语言,但它们仍然无法理解句子背后的真实含义,因为它们既不直观也不能够在两行之间阅读。
这意味着,尽管人类能够理解说“他昨晚着火了,真是个好游戏!”的句子。 指某种运动的运动员的表现,计算机会从字面意义上理解它-意味着它将把它解释为昨晚确实起火的人。
NLP 是人工智能(AI)的子字段,它通过使计算机能够理解人类语言来工作。 尽管在某些情况下人类可能总是会做得更好,但是 NLP 的主要目标是通过使计算机理解人类语言来使计算机与人类更接近。
这个想法是创建专注于特定领域的模型,例如机器翻译和文本摘要。 任务的这种专业化有助于计算机开发一种模型,该模型能够解决现实生活中的数据问题,而无需一次处理所有复杂的人类语言。
情感分析是人类语言理解的这些领域之一(近来非常流行)。
情感分析
一般而言,情感分析包括理解输入文本背后的情感。 考虑到随着社交媒体平台的普及,公司每天收到的消息和评论的数量呈指数增长,因此它变得越来越受欢迎。 这使得手动修改和实时响应每条消息的任务变得不可能,这可能会损害公司的形象。
情感分析的重点是在忽略细节的同时提取句子的基本组成部分。 这有助于解决两个主要需求:
- 确定客户最关心的产品或服务的关键方面。
- 提取这些方面背后的感受,以确定哪些因素引起正面和负面反应,并相应地进行处理:
图 6.23:一个推文示例
从前面的屏幕截图中获取文本,执行情感分析的模型可能会获得以下信息:
AI作为推文的对象- 从中得到的
happy的感觉 decade作为对象情感的时间范围
如您所见,情感分析的概念对于任何具有在线业务的公司而言都是关键的,因为它能够以惊人的速度对需要立即关注的评论做出快速响应,并且其准确率类似于人类。 。
作为情感分析的示例用例,某些公司可能选择对每天收到的大量消息进行情感分析,以便优先处理对包含抱怨或负面情感的消息的响应。 这不仅有助于减轻特定客户的负面情感; 它还将帮助公司快速纠正错误并与客户建立信任关系。
下一部分将详细说明执行 NLP 进行情感分析的过程。 我们将解释单词嵌入的概念以及在 PyTorch 中开发此类模型所能执行的不同步骤,这将是本章最后活动的目标。
PyTorch 中的情感分析
建立一个在 PyTorch 中执行情感分析的模型与我们迄今为止使用 RNN 所看到的非常相似。 不同之处在于,在这种情况下,将逐字处理文本数据。 本节将提供构建这种模型所需的步骤。
预处理输入数据
与其他任何数据问题一样,您需要将数据加载到代码中,同时要记住对不同的数据类型使用不同的方法。 除了将整个单词集转换为小写字母之外,数据还经过一些基本的转换,可让您将数据输入网络。 最常见的转换如下:
-
消除标点符号。在为 NLP 目的逐字处理文本数据时,删除任何标点符号。这样做是为了避免把同一个词当作两个独立的词,因为其中一个词后面有句号、逗号或任何其他特殊字符。一旦实现了这一点,就可以定义一个包含输入文本词汇的列表(也就是整个词集)。
这可以通过使用
string模块的punctuation预初始化的字符串来完成,该字符串提供了可用于在文本序列中标识它们的标点符号列表,例如以下代码段:test = pd.Series(['Hey! This is example #1.', \ 'Hey! This is example #2.', \ 'Hey! This is example #3.']) for i in punctuation: test = test.str.replace(i,"") -
带编号的标签:类似于前面介绍的映射字符的过程,词汇表中的每个单词都映射到一个整数,该整数将用于替换输入文本的单词,以便将它们输入网络 :
图 6.24:单词和数字的映射
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 层的输入尺寸。 其余的架构将保持与以前相同。
训练模型
最后,在定义了损失函数和优化算法之后,训练模型的过程与任何其他神经网络相同。 根据研究的需要和目的,可以将数据分为不同的组。 接下来,您必须设置周期数和将数据分成批量的方法。 通常在处理每批数据时保留网络内存,但随后在每个周期将其初始化为零。 通过对一批数据调用模型来获得网络的输出。 然后,计算损失函数,并优化参数。
活动 6.03:用于情感分析的 NLP
您将用于此活动的数据集称为情感标记句子数据集,可从 UC Irvine 机器学习存储库中获得。
注意
数据集和相关分析首次在此处发布:使用深度特征从组到单个标签,Kotzias 等,KDD 2015
在此活动中,将使用 LSTM 网络分析一组评论,以确定其背后的观点。 让我们考虑以下情况:您在一家互联网提供商的公共关系部门工作,并且审查您在公司的社交媒体资料中遇到的每个查询的过程都将花费很长时间。 最大的问题是与服务有问题的客户相比,没有服务的客户缺乏耐心,因此您需要确定响应的优先级,以便首先解决他们。 当您在业余时间喜欢编程时,您决定尝试构建一个神经网络,该网络能够确定消息是消极还是肯定。 请按照以下步骤完成此活动:
注意
值得一提的是,此活动中的数据并未分为不同的数据集,以使模型可以进行微调和测试。 这是因为此活动的主要重点是实现创建能够执行情感分析的模型的过程。
-
导入所需的库。
-
加载包含来自亚马逊的 1,000 条产品评论的数据集,并与标签 0(对于负面评论)或 1(对于正面评论)配对。 将数据分成两个变量-一个包含评论,另一个包含标签。
-
从评论中删除标点符号。
-
创建一个包含整个评论集的词汇表的变量。 此外,创建一个字典,将每个单词映射到一个整数,其中单词将作为键,而整数将是值。
-
通过将评论中的每个单词替换为其成对的整数来对评论数据进行编码。
-
创建一个包含网络架构的类。确保你包含一个嵌入层。
注意
由于在训练过程中不会批量输入数据,因此无需返回
forward函数中的状态。 但是,这并不意味着该模型将没有内存,而是因为该内存不依赖于下一个审阅,因此该内存用于单独处理每个审阅。 -
使用 3 个 LSTM 层的 64 个嵌入尺寸和 128 个神经元实例化模型。
-
定义损失函数,优化算法以及要训练的周期数。 例如,您可以将二进制交叉熵损失用作损失函数,Adam 优化器,并训练 10 个周期。
-
创建一个
for循环,该循环将经历不同的周期并分别经历每个审核。 对于每次审核,都要进行预测,计算损失函数并更新网络参数。 此外,根据该训练数据计算网络的准确率。 -
随时间绘制损失和准确率。
最终的精度图将如下所示:
图 6.25:显示准确率得分进度的图
注意
可以在第 298 页上找到此活动的解决方案。
总结
在本章中,我们讨论了 RNN。 开发这种类型的神经网络是为了解决与序列数据有关的问题。 这意味着单个实例不包含所有相关信息,因为这取决于先前实例中的信息。
有几种适合这种类型描述的应用。 例如,如果没有文本其余部分的上下文,则文本(或语音)的特定部分可能意义不大。 但是,即使 NLP 在 RNN 中得到了最多的研究,在其他应用中,文本的上下文也很重要,例如预测,视频处理或音乐相关的问题。
RNN 的工作方式非常聪明。 网络不仅输出结果,而且还输出一个或多个通常称为内存的值。 该内存值用作将来预测的输入。
当处理涉及非常大序列的数据问题时,传统的 RNN 会出现一个称为梯度消失问题的问题。 在这里梯度变得非常小,因此它们不再对网络的学习过程有所贡献,而这种学习过程通常发生在网络的较早层中,从而导致网络无法长期存储。
为了解决这个问题,开发了 LSTM 网络。 这种网络架构能够存储两种类型的内存,因此得名。 此外,在此网络中进行的数学计算允许它仅通过存储过去的相关信息来忘记信息。
最后,解释了一个非常流行的 NLP 任务:情感分析。 在此任务中,重要的是要理解文本提取背后的情感。 对于机器而言,这是一个非常困难的问题,因为人们可以使用许多不同的单词和表达形式(例如讽刺)来描述事件背后的情感。 但是,由于社交媒体使用量的增加,这导致需要更快地处理文本数据,这个问题在投入大量时间和金钱来创建多个近似值以解决该问题的大公司中变得非常普遍,如图所示。 本章的最后活动。
既然您已经遍历了本书的所有章节,您将对不同的深度神经网络架构有广泛的了解,这些架构可用于使用 PyTorch 解决各种数据问题。 本书中说明的架构也可以用于解决其他数据问题。
七、附录
1.深度学习和 PyTorch 简介
活动 1.01:创建单层神经网络
解决方案
-
导入所需的库,包括 pandas,用于导入 CSV 文件。
import pandas as pd import torch import torch.nn as nn import matplotlib.pyplot as plt -
读取包含数据集的 CSV 文件。
data = pd.read_csv("SomervilleHappinessSurvey2015.csv") -
将输入特征与目标分开。注意,目标位于 CSV 文件的第一列。将值转换为张量,确保值转换为浮点数。
x = torch.tensor(data.iloc[:,1:].values).float() y = torch.tensor(data.iloc[:,:1].values).float() -
定义模型的架构,并将其存储在一个名为
model的变量中。记住要创建一个单层模型。model = nn.Sequential(nn.Linear(6, 1), nn.Sigmoid()) -
定义要使用的损失函数。使用 MSE 损失函数。
loss_function = torch.nn.MSELoss() -
定义你模型的优化器。使用亚当优化器和学习率
0.01。optimizer = torch.optim.Adam(model.parameters(), lr=0.01) -
运行优化 100 次迭代。每迭代 10 次,打印并保存损失值。
losses = [] for i in range(100): y_pred = model(x) loss = loss_function(y_pred, y) losses.append(loss.item()) optimizer.zero_grad() loss.backward() optimizer.step() if i%10 == 0: print(loss.item())最终损失应约为
0.24。 -
做一个线图来显示每个迭代步骤的损失值。
plt.plot(range(0,100), losses) plt.show()结果图应如下所示:
图 1.4:整个训练过程中的损失函数
这意味着训练过程能够使损失函数最小化,这意味着结果模型将可能能够绘制出市民对城市服务的满意度与他们对行政管理是否满意之间的关系。
注意
要访问此特定部分的源代码,请参考这里。
您也可以通过这里在线运行此示例。 您必须执行整个笔记本才能获得所需的结果。
2.神经网络的构建块
活动 2.01:执行数据准备
解决方案
-
导入所需的库。
import pandas as pd -
使用 pandas,加载
.csv文件。data = pd.read_csv("YearPredictionMSD.csv", nrows=50000) data.head()注意
为避免内存限制,在读取文本文件时,请使用
nrows自变量,以读取整个数据集的较小部分。 在前面的示例中,我们正在读取前 50,000 行。输出如下:
图 2.33:
YearPredictionMSD.csv -
核实数据集中是否存在任何定性数据。
cols = data.columns num_cols = data._get_numeric_data().columns list(set(cols) - set(num_cols))输出应为空列表,这意味着没有定性特征。
-
检查是否有缺失值。
如果在先前用于此目的的代码行中添加一个附加的
sum()函数,则将获得整个数据集中的缺失值之和,而无需按列进行区分:data.isnull().sum().sum()输出应为
0,这意味着所有特征均不包含缺失值。 -
检查是否有异常值。
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)输出字典应显示所有特征均不包含代表超过 5% 数据的离群值。
-
将特征从目标数据中分离出来。
X = data.iloc[:, 1:] Y = data.iloc[:, 0] -
使用标准化方法对特征数据进行重新缩放。
X = (X - X.mean())/X.std() X.head()输出如下:
图 2.34:重新缩放的特征数据
-
将数据分成三组:训练、验证和测试。使用你喜欢的方法。
from sklearn.model_selection import train_test_split X_shuffle = X.sample(frac=1, random_state=0) Y_shuffle = Y.sample(frac=1, random_state=0) 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) -
打印所得形状如下。
print(x_train.shape, y_train.shape) print(x_dev.shape, y_dev.shape) print(x_test.shape, y_test.shape)输出应如下所示:
(30000, 90) (30000, ) (10000, 90) (10000, ) (10000, 90) (10000, )注意
要访问此特定部分的源代码,请参考这里。
您也可以通过这里在线运行此示例。 您必须执行整个笔记本才能获得所需的结果。
活动 2.02:为回归问题开发深度学习解决方案
解决方案
-
导入所需的库。
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循环来训练网络,迭代步数为 3000 步。for i in range(3000): y_pred = model(x_train).squeeze() loss = loss_function(y_pred, y_train) optimizer.zero_grad() loss.backward() optimizer.step() if i%250 == 0: print(i, loss.item()) -
通过对测试集的第一个实例进行预测,并与真实情况进行比较来测试你的模型。
pred = model(x_test[0]) print("Ground truth:", y_test[0].item(), \ "Prediction:", pred.item())您的输出应类似于以下内容:
Ground truth: 1995.0 Prediction: 1998.0279541015625注意
要访问此特定部分的源代码,请参考这里。
您也可以通过这里在线运行此示例。 您必须执行整个笔记本才能获得所需的结果。
3.使用 DNN 的分类问题
活动 3.01:构建人工神经网络
解:
-
导入以下库:
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) -
读取之前准备好的数据集,该数据集应该命名为
dccc_prepared.csv。data = pd.read_csv("dccc_prepared.csv") data.head()输出应如下所示:
图 3.14:
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)您可以使用以下代码打印每个集合的最终形状:
print("Training sets:",X_train.shape, y_train.shape) print("Validation sets:",X_dev.shape, y_dev.shape) print("Testing sets:",X_test.shape, y_test.shape)每个集合的最终形状如下所示:
Training sets: (28036, 22) (28036,) Validation sets: (9346, 22) (9346,) Testing sets: (9346, 22) (9346,) -
将验证集和测试集转换为张量,记住特征矩阵应该是
float类型,而目标矩阵不应该。训练集暂不转换,因为它们将进行进一步的转换。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) 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)) -
绘出两组的损失。
fig = plt.figure(figsize=(15, 5)) plt.plot(train_losses, label='Training loss') plt.plot(dev_losses, label='Validation loss') plt.legend(frameon=False, fontsize=15) plt.show()考虑到打乱训练数据可能会得出略有不同的结果,结果图应与此处显示的图相似,尽管有所不同。
图 3.15:显示训练和验证损失的图
-
绘制两组的精度。
fig = plt.figure(figsize=(15, 5)) plt.plot(train_acc, label="Training accuracy") plt.plot(dev_acc, label="Validation accuracy") plt.legend(frameon=False, fontsize=15) plt.show()这是从此代码段派生的图:
图 3.16:显示集合精度的图
注意
要访问此特定部分的源代码,请参考这里。
您也可以通过这里在线运行此示例。 您必须执行整个笔记本才能获得所需的结果。
练习 3.02:提高模型的表现
解:
-
导入你在上一个活动中使用的相同的库。
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 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) 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接下来,定义训练过程的不同参数。 这包括损失函数,优化算法,批量大小和周期数,如以下代码所示:
model = Classifier(X_train.shape[1]) criterion = nn.NLLLoss() optimizer = optim.Adam(model.parameters(), lr=0.001) epochs = 4000 batch_size = 128最后,按照以下代码片段处理训练过程:
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 with torch.no_grad(): model.eval() 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) model.train() 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))注意
可以在以前共享的 GitHub 存储库中找到此活动随附的 Jupyter 笔记本。 在那里,您会发现对模型进行微调的各种尝试及其结果。 表现最佳的模型可以在笔记本电脑的末尾找到。
-
绘制两组数据的损失和准确率。
注意
请记住,此处显示的结果与您的结果不完全匹配。 这主要是由于训练网络时使用了打乱函数。
使用以下代码绘制损失:
fig = plt.figure(figsize=(15, 5)) plt.plot(x_axis,train_losses, label='Training loss') plt.plot(x_axis, dev_losses, label='Validation loss') plt.legend(frameon=False , fontsize=15) plt.show()运行前面的代码将显示以下图:
图 3.17:显示集合损失的图
使用以下代码来绘制精度:
fig = plt.figure(figsize=(15, 5)) plt.plot(x_axis, train_acc, label="Training accuracy") plt.plot(x_axis, dev_acc, label="Validation accuracy") plt.legend(frameon=False , fontsize=15) plt.show()运行前面的代码将显示以下图:
图 3.18:显示集合精度的图
-
使用表现最好的模型,对测试集(在微调过程中不应该使用)进行预测。通过计算模型在该集上的准确率,将预测结果与基本事实进行比较。
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) print(acc_test)通过模型架构和此处定义的参数获得的精度应为 80% 左右。
注意
要访问此特定部分的源代码,请参考这里。
本部分当前没有在线交互示例,需要在本地运行。
活动 3.03:使用模型
解决方案
-
打开用于上一个活动的 Jupyter 笔记本。
-
复制包含最佳表现模型架构的类,并将其保存在 Python 文件中。确保导入了 PyTorch 所需的库和模块,并将其命名为
final_model.py。将其命名为final_model.py。该文件应如下所示:
图 3.19:
final_model.py的屏幕截图 -
在 Jupyter 笔记本中,保存表现最好的模型。请务必保存与输入单元相关的信息,以及模型的参数。将其命名为
checkpoint.pth。checkpoint = {"input": X_train.shape[1], \ "state_dict": model.state_dict()} torch.save(checkpoint, "checkpoint.pth") -
打开一个新的 Jupyter 笔记本。
-
导入 PyTorch,以及我们在“步骤 2”中创建的 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) -
通过输入“步骤 7”的相同张量到模型的跟踪脚本中进行预测。
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(是)。 -
打开一个新的 Jupyter 笔记本,并导入所需的库来使用 Flask 创建一个 API,以及加载保存的模型的库。
import flask from flask import request import torch import final_model -
初始化 Flask 应用。
app = flask.Flask(__name__) app.config["DEBUG"] = True -
定义一个函数,加载保存的模型,然后实例化模型。
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") -
定义 API 的路由为
/prediction,并将方法设置为POST。然后,定义接收POST数据的函数,并将其反馈给模型进行预测。@app.route('/prediction', methods=['POST']) def prediction(): body = request.get_json() example = torch.tensor(body['data']).float() pred = model(example) pred = torch.exp(pred) _, top_class_test = pred.topk(1, dim=1) top_class_test = top_class_test.numpy() return {"status":"ok", "result":int(top_class_test[0][0])} -
运行 Flask 应用。
app.run(debug=True, use_reloader=False)使用为 API 开发而创建的平台 Postman,可以测试 API。 要向 Postman 提交成功的请求,标头的
Content-Type应当等于application/json。 结果输出应如下所示:
图 3.20:应用运行后的屏幕截图
注意
要访问此特定部分的源代码,请参考这里。
本部分当前没有在线交互示例,需要在本地运行。
4.卷积神经网络
活动 4.01:针对图像分类问题构建 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)前面的代码将下载可通过 PyTorch 的
Torchvision包获得的训练和测试数据集。 根据上一步中定义的转换对数据集进行转换。 -
使用 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)为了将训练集分为两组(训练和验证),为每个组定义了一个索引列表,然后可以使用
SubsetRandomSampler函数对其进行随机采样。 -
使用
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)PyTorch 的
DataLoader函数用于创建批量,这些批量将在开发过程的训练,验证和测试阶段馈送到模型中。 -
定义你的网络架构。使用以下信息进行定义。
Conv1:卷积层,将彩色图像作为输入,并将其通过大小为 3 的 10 个过滤器。应将填充和跨步都设置为 1。
Conv2:一个卷积层,它将输入数据通过大小为 3 的 20 个过滤器传递。填充和跨距都应设置为 1。
Conv3:一个卷积层,它将输入数据通过大小为 3 的 40 个过滤器传递。填充和跨距都应设置为 1。
在每个卷积层之后使用 ReLU 激活函数。
在每个卷积层之后使用池化层,过滤器大小和步幅为 2。
展平图像后,使用掉落项设置为 20%。
Linear1:一个全连接层,接收上一层的展平矩阵作为输入,并生成 100 个单元的输出。 为此层使用 ReLU 激活函数。 此处的丢弃期限设置为 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前面的代码段包含一个定义了网络架构的类(
__init__方法),以及在信息正向传播过程中所遵循的步骤(forward方法)。 -
定义训练模型所需的所有参数。设置周期数为
50。model = CNN() loss_function = nn.NLLLoss() optimizer = optim.Adam(model.parameters(), lr=0.001) epochs = 50我们为此练习选择的优化器是 Adam。 同样,负对数似然率用作损失函数,如本书前一章所述。
如果您的计算机具有可用的 GPU,则应按以下步骤完成模型的实例化:
model = CNN().to("cuda") -
训练你的网络,并确保保存训练集和验证集的损失和准确率的值。
train_losses, dev_losses, train_acc, dev_acc= [], [], [], [] x_axis = [] # For loop through the epochs for e in range(1, epochs+1): losses = 0 acc = 0 iterations = 0 model.train() """ For loop through the batches (created using the train loader) """ for data, target in train_loader: iterations += 1 # Forward and backward pass of the training data 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 # Validation of model for given epoch if e%5 == 0 or e == 1: x_axis.append(e) with torch.no_grad(): model.eval() """ For loop through the batches of the validation set """ 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) # Losses and accuracy are appended to be printed 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))如果您的计算机具有可用的 GPU,则对前面的代码进行一些修改,如下所示:
train_losses, dev_losses, train_acc, dev_acc= [], [], [], [] x_axis = [] # For loop through the epochs for e in range(1, epochs+1): losses = 0 acc = 0 iterations = 0 model.train() """ For loop through the batches (created using the train loader) """ for data, target in train_loader: iterations += 1 # Forward and backward pass of the training data pred = model(data.to("cuda")) loss = loss_function(pred, target.to("cuda")) 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.to("cpu"), \ top_class.to("cpu")) dev_losss = 0 dev_accs = 0 iter_2 = 0 # Validation of model for given epoch if e%5 == 0 or e == 1: x_axis.append(e) with torch.no_grad(): model.eval() """ For loop through the batches of the validation set """ for data_dev, target_dev in dev_loader: iter_2 += 1 dev_pred = model(data_dev.to("cuda")) dev_loss = loss_function(dev_pred, \ target_dev.to("cuda")) 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.to("cpu"), \ dev_top_class.to("cpu")) # Losses and accuracy are appended to be printed 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.23:结果图显示了集合的损失
要绘制精度,请使用以下代码:
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.24:结果图显示了集合的准确率
可以看出,在第 15 个周期之后,过拟合开始影响模型。
-
在测试集上检查模型的准确率。
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)使用我们之前创建的数据加载器,可以对测试集数据进行图像分类,以估计模型在看不见数据上的准确率。
如果您的计算机具有可用的 GPU,则对前面的代码进行一些修改,如下所示:
model.eval() iter_3 = 0 acc_test = 0 for data_test, target_test in test_loader: iter_3 += 1 test_pred = model(data_test.to("cuda")) test_pred = torch.exp(test_pred) top_p, top_class_test = test_pred.topk(1, dim=1) acc_test += accuracy_score(target_test .to("cpu"), \ top_class_test .to("cpu")) print(acc_test/iter_3)测试集的准确率与其他两组所达到的准确率非常相似,这意味着该模型能够对看不见的数据表现出同样出色的表现。 它应该在 72% 左右。
注意
要访问此特定部分的源代码,请参考这里。
本部分当前没有在线交互示例,需要在本地运行。
要访问此源代码的 GPU 版本,请参考这里。 此版本的源代码无法作为在线交互示例使用,需要通过 GPU 设置在本地运行。
活动 4.02:实现数据扩充
解决方案
-
复制之前活动中的笔记本。
为了完成此活动,按照以下步骤,除了修改
tranforms值之外,不会更改任何代码。 -
修改
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 个周期。
如果您的计算机具有可用的 GPU,请确保使用代码的 GPU 版本来训练模型。
在训练和验证集上得出的损失和准确率图应与此处显示的图相似:
图 4.25:结果图显示了集合的损失
图 4.26:结果图显示了集合的准确率
通过添加数据扩充,可以改善模型的表现,并减少发生的过拟合。
-
计算所得模型在测试集上的精度。
该模型在测试设备上的表现提高了约 75%。
注意
要访问此特定部分的源代码,请参考这里。
本部分当前没有在线交互示例,需要在本地运行。
要访问此源代码的 GPU 版本,请参考这里。 此版本的源代码无法作为在线交互示例使用,需要通过 GPU 设置在本地运行。
活动 4.03:实现批量标准化
解决方案
-
复制之前活动中的笔记本。
要完成此活动,按照以下步骤,除了在网络架构中添加一些层之外,不会更改任何代码。
-
将批量归一化添加到每个卷积层,以及第一个全连接层。
网络的最终架构应如下:
class CNN(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 个周期。
如果您的计算机具有可用的 GPU,请确保使用代码的 GPU 版本来训练模型。 训练和验证集的损失和准确率的结果图应类似于此处所示:
图 4.27:结果图显示集合的损失
图 4.28:结果图显示集合的损失
尽管过拟合再次引入了模型,但是我们可以看到两组的表现都有所提高。
注意
尽管本章未对此进行探讨,但理想的步骤是为网络架构添加丢弃以减少高方差。 随意尝试一下,看看您是否能够进一步提高性能。
-
计算所得模型在测试集上的精度。
该模型在测试设备上的表现已提高了约 78%。
注意
要访问此特定部分的源代码,请参考这里。
本部分当前没有在线交互示例,需要在本地运行。
要访问此源代码的 GPU 版本,请参考这里。 此版本的源代码无法作为在线交互示例使用,需要通过 GPU 设置在本地运行。
5.样式迁移
活动 5.01:执行样式迁移
解决方案
-
导入所需的库。
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如果您的计算机具有可用的 GPU,请确保定义一个名为
device的变量,该变量将有助于为 GPU 分配一些变量,如下所示:device = "cuda" -
指定要对输入图像进行的变换。请确保将它们调整为相同的大小,将它们转换为张力,并将它们标准化。
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")如果您的计算机有可用的 GPU,请改用以下代码段:
def image_loader(image_name): image = Image.open(image_name) image = loader(image).unsqueeze(0) return image content_img = image_loader("images/landscape.jpg").to(device) style_img = image_loader("images/monet.jpg").to(device) -
为了能够显示图像,设置变换以恢复图像的归一化,并将张量转换为
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()]) -
创建一个函数(
tensor2image),它能够在张量上执行前面的变换。对两幅图像调用该函数并绘制结果。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()如果您的计算机有可用的 GPU,请改用以下代码段:
def tensor2image(tensor): image = tensor.to("cpu").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(): x = layer(x) if index in layers: 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 矩阵。同时,创建初始目标图像。
以下代码段为用于提取样式特征的每个层创建了 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)如果您的计算机有可用的 GPU,请改用以下代码段:
target_img = content_img.clone().requires_grad_(True).to(device) -
设置不同样式层的权重,以及内容和样式损失的权重。
style_weights = {'conv1_1': 1., 'conv2_1': 0.8, \ 'conv3_1': 0.6, 'conv4_1': 0.4, \ 'conv5_1': 0.2} alpha = 1 beta = 1e5 -
运行模型 500 次迭代。在开始训练模型之前,定义 Adam 优化算法,以
0.001作为学习率。注意
为了获得本书所示的最终目标图像,该代码运行了 5,000 次迭代,而没有 GPU 则需要很长时间才能运行。 但是,要欣赏输出图像中开始发生的更改,尽管鼓励您测试不同的训练时间,但只需运行 500 次迭代就足够了。
print_statement = 500 optimizer = torch.optim.Adam([target_img], lr=0.001) iterations = 5000 for i in range(1, iterations+1): # Extract features for all relevant layers target_features = features_extractor(target_img, model, \ relevant_layers) # Calculate the content loss content_loss = torch.mean((target_features['conv4_2'] \ - content_features['conv4_2'])**2) # Loop through all style layers style_losses = 0 for layer in style_weights: # Create gram matrix for that layer 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] # Calculate style loss for that layer style_loss = style_weights[layer] * \ torch.mean((target_gram - \ style_gram)**2) #Calculate style loss for all layers style_losses += style_loss / (d1 * d2 * d3) # Calculate the total loss total_loss = alpha * content_loss + beta * style_losses # Perform back propagation optimizer.zero_grad() total_loss.backward() optimizer.step() # Print the target image if i % print_statement == 0 or i == 1: print('Total loss: ', total_loss.item()) plt.imshow(tensor2image(target_img)) plt.show() -
绘制内容、风格、目标的图片,比较结果。
fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(15, 5)) ax1.imshow(tensor2image(content_img)) ax2.imshow(tensor2image(target_img)) ax3.imshow(tensor2image(style_img)) plt.show()从此代码段派生的图应类似于此处显示的图:
图 5.11:输出图
注意
要查看高质量彩色图像,请访问本书的 GitHub 存储库,网址为 packt.live/2KcORcw。
要访问此特定部分的源代码,请参考这里。
本部分当前没有在线交互示例,需要在本地运行。
要访问此源代码的 GPU 版本,请参考这里。 此版本的源代码无法作为在线交互示例使用,需要通过 GPU 设置在本地运行。
6.使用 RNN 分析数据序列
活动 6.01:使用简单 RNN 的时间序列预测
解决方案
-
导入所需的库,具体如下:
import pandas as pd import matplotlib.pyplot as plt import torch from torch import nn, optim -
加载数据集,然后对其进行切片,使其包含所有的行,但只包含索引 1 到 52 的列。
data = pd.read_csv("Sales_Transactions_Dataset_Weekly.csv") data = data.iloc[:,1:53] data.head()输出如下:
图 6.26:显示索引 1 到 52 列的数据集
-
绘制从整个数据集中随机选择的五种产品的每周销售交易情况。在进行随机采样时,使用随机种子
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.27:输出图
-
创建
input和target变量,这些变量将被输入到网络中以创建模型。这些变量应具有相同的形状,并转换为 PyTorch 张量。input变量应包含除上周外的所有星期的所有产品数据,因为模型的目的是预测最后一周。target变量应比input变量领先一步; 也就是说,target变量的第一个值应该是输入变量中的第二个,依此类推,直到target变量的最后一个值(应该被留在input变量之外):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与之前的活动一样,该类包含
__init__方法以及网络架构,以及forward方法,该方法确定信息在各层之间的流动。 -
实例化包含模型的类函数。输入输入大小、每个循环层的神经元数量(
10)和循环层数量(1)。model = RNN(data_train.shape[1], 10, 1) model运行前面的代码将显示以下输出:
RNN( (rnn): RNN(51, 10, batch_first=True) (output): Linear(in_features=10, out_features=1, bias=True) ) -
定义一个损失函数,一个优化算法,以及训练网络的周期数。使用 MSE 损失函数、Adam 优化器和 10,000 个周期来完成这一任务。
loss_function = nn.MSELoss() optimizer = optim.Adam(model.parameters(), lr=0.001) epochs = 10000 -
使用
for循环来执行训练过程,经历所有的周期。在每个周期中,必须进行预测,以及随后的损失函数计算和网络参数的优化。保存每个周期的损失。注意
考虑到没有批量用于遍历数据集,
hidden量实际上并未在批量之间传递(而是在处理序列的每个元素时使用隐藏状态),但是为了清楚起见,它留在这里。losses = [] for i in range(1, epochs+1): hidden = None pred, hidden = model(inputs, hidden) target = targets[:,-1].unsqueeze(1) 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])输出应如下所示:
epoch: 1000 ... Loss function: 58.48879623413086 epoch: 2000 ... Loss function: 24.934917449951172 epoch: 3000 ... Loss function: 13.247632026672363 epoch: 4000 ... Loss function: 9.884735107421875 epoch: 5000 ... Loss function: 8.778228759765625 epoch: 6000 ... Loss function: 8.025042533874512 epoch: 7000 ... Loss function: 7.622503757476807 epoch: 8000 ... Loss function: 7.4796295166015625 epoch: 9000 ... Loss function: 7.351718902587891 epoch: 10000 ... Loss function: 7.311776161193848 -
将所有周期的损失绘制如下:
x_range = range(len(losses)) plt.plot(x_range, losses) plt.xlabel("epochs") plt.ylabel("Loss function") plt.show()结果图应如下所示:
图 6.28:显示所有周期的损失的图
-
使用散点图,显示在训练过程的最后一个周期中获得的预测值与真实情况值(即上周的销售交易)的对比。
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.29:显示预测的散点图
注意
要访问此特定部分的源代码,请参考这里。
您也可以通过这里在线运行此示例。 您必须执行整个笔记本才能获得所需的结果。
活动 6.02:使用 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 -
打开并将《爱丽丝梦游仙境》中的文字读入笔记本。打印前 50 个字符的摘要和文本文件的总长度。
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)} The output should look as follows:输出应如下所示:
Extract: ALICE was beginning to get very tired of sitting b Length: 145178 -
将数据集的每个字母编码为其配对的整数。打印前 50 个编码字符和数据集编码版本的总长度。
indexed_data = [] for c in data: indexed_data.append(indexer[c]) print("Indexed extract: ", indexed_data[:50]) print("Length: ", len(indexed_data))输出如下:
Indexed extract: [51, 52, 29, 38, 28, 25, 11, 59, 39, 25, 16, 53, 2, 1, 26, 26, 1, 26, 2, 25, 56, 60, 25, 2, 53, 56, 25, 23, 53, 7, 45, 25, 56, 1, 7, 53, 13, 25, 60, 14, 25, 39, 1, 56, 56, 1, 26, 2, 25, 16] Length: 145178 -
创建一个函数,接收一个批量,并将其编码为单热矩阵。
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此函数采用二维矩阵并将其展平。 接下来,它创建一个平坦矩阵的形状和包含字母的字典长度的零填充矩阵(在“步骤 3”中创建)。 接下来,它用一个字符填充对应于批量中每个字符的字母。 最后,它对矩阵进行整形以使其为三维。
-
创建一个定义网络架构的类。这个类应该包含一个额外的函数,用于初始化 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 This class contains an __init__ method where the此类包含
__init__方法(其中定义了网络的架构),forward方法(用于确定通过层的数据流)以及init_state用零初始化隐藏状态和单元状态的方法。 -
确定要从数据集中创建的批次数量,记住每个批次应该包含 100 个序列,每个序列的长度为 50 个。接下来,将编码后的数据分成 100 个序列。
# Number of sequences per batch n_seq = 100 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作为共两个循环层的隐藏单元数来实例化你的模型。model = LSTM(len(chars), 256, 2) model运行前面的代码将显示以下输出:
LSTM( (lstm): LSTM(70, 256, num_layers=2, batch_first=True) (output): Linear(in_features=256, out_features=70, bias=True) )如果您的计算机有可用的 GPU,请确保使用以下代码片段将模型分配给 GPU:
model = LSTM(len(chars), 256, 2).to("cuda") -
定义损失函数和优化算法。使用 Adam 优化器和交叉熵损失来完成。训练网络
20周期。loss_function = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=0.001) epochs = 20如果您的机器有可用的 GPU,请尝试运行
500周期的训练过程:epochs = 500 -
在每个周期,数据必须被划分为序列长度为 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%2 == 0: print("epoch: ", e, "... Loss function: ", losses[-1])输出应如下所示:
epoch: 2 ... Loss function: 3.1667490992052802 epoch: 4 ... Loss function: 3.1473221943296235 epoch: 6 ... Loss function: 2.897721455014985 epoch: 8 ... Loss function: 2.567064647016854 epoch: 10 ... Loss function: 2.4197753791151375 epoch: 12 ... Loss function: 2.314083896834275 epoch: 14 ... Loss function: 2.2241266349266313 epoch: 16 ... Loss function: 2.1459227183769487 epoch: 18 ... Loss function: 2.0731402758894295 epoch: 20 ... Loss function: 2.0148646708192497如果您的计算机具有可用的 GPU,则用于训练网络的等效代码段如下所示:
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))\ .to("cuda") y = torch.Tensor(y_batch).view(n_seq * \ seq_length).to("cuda") 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%50 == 0: print("epoch: ", e, "... Loss function: ", \ losses[-1])将训练过程运行 500 个周期的结果如下:
epoch: 50 ... Loss function: 1.5207843986050835 epoch: 100 ... Loss function: 1.006190665836992 epoch: 150 ... Loss function: 0.5197970939093622 epoch: 200 ... Loss function: 0.24446514968214364 epoch: 250 ... Loss function: 0.0640328845073437 epoch: 300 ... Loss function: 0.007852113484565553 epoch: 350 ... Loss function: 0.003644719101681278 epoch: 400 ... Loss function: 0.006955199634078248 epoch: 450 ... Loss function: 0.0030021724242973945 epoch: 500 ... Loss function: 0.0034294885518992768可以看出,通过将训练过程运行更多的时间段,损失函数将达到较低的值。
-
绘制损失随时间推移的进展情况。
x_range = range(len(losses)) plt.plot(x_range, losses) plt.xlabel("epochs") plt.ylabel("Loss function") plt.show()该图表应如下所示:
图 6.30:显示损失函数进度的图表
如我们所见,在 20 个周期之后,损失函数仍然可以减少,这就是为什么强烈建议训练更多周期以便从模型中获得良好结果的原因。
-
将下面的句子
starter输入到训练好的模型中,让它来完成这个句子:"So she was considering in her own mind "。starter = "So she was considering in her own mind " states = None如果您的计算机具有可用的 GPU,则将模型分配回 CPU 以执行预测:
model = model.to("cpu")首先,
for循环的将种子输入模型,以便可以生成内存。 接下来,执行预测,如以下代码片段所示: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 < 100: 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)注意
要访问此特定部分的源代码,请参考这里。
本部分当前没有在线交互示例,需要在本地运行。
要访问此源代码的 GPU 版本,请参考这里。 此版本的源代码无法作为在线交互示例使用,需要通过 GPU 设置在本地运行。
活动 6.03:用于情感分析的 NLP
解决方案
-
导入所需的库。
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).unsqueeze(0) return out返回
该类包含用于定义网络架构的
__init__方法和用于确定数据流经不同层的方式的forward方法。 -
使用 64 个嵌入维度和 128 个神经元为三个 LSTM 层实例化模型。
model = LSTM(len(vocabulary), 64, 128, 3) model运行前面的代码将显示以下输出:
LSTM( (embedding): Embedding(1905, 64) (lstm): LSTM(64, 128, num_layers=3, batch_first=True) (output): Linear(in_features=128, out_features=1, bias=True) ) -
定义损失函数,优化算法,以及训练的周期数。例如,您可以使用二进制交叉熵损失作为损失函数,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.31:显示损失函数进度的图
以下代码用于绘制准确率得分:
x_range = range(len(acc)) plt.plot(x_range, acc) plt.xlabel("epochs") plt.ylabel("Accuracy score") plt.show()该图应如下所示:
图 6.32:显示准确率得分进度的图
注意
要访问此特定部分的源代码,请参考这里。
本部分当前没有在线交互示例,需要在本地运行。