Python-深度学习教程-二-

103 阅读1小时+

Python 深度学习教程(二)

原文:Deep Learning with Python

协议:CC BY-NC-SA 4.0

三、前馈神经网络

前馈神经网络是深度学习中最早的实现。这些网络被称为前馈网络,因为网络中的信息只沿一个方向(向前)移动,即从输入节点(单元)向输出单元移动。在本章中,我们将涵盖一些围绕前馈神经网络的关键概念,这些神经网络是深度学习中各种主题的基础。我们将从研究神经网络的结构开始,然后研究它们是如何被训练和用于预测的。我们还将简要了解在不同设置中应该使用的损失函数、在神经元中使用的激活函数,以及可以用于训练的不同类型的优化器。最后,我们将使用 PyTorch 将这些较小的组件缝合到一个成熟的前馈神经网络中。

让我们开始吧。

什么是神经网络?

在抽象层次上,神经网络可以被认为是一个函数

{f}_{\theta }:x\to y

它接受一个输入xRn并产生一个输出yRm,其行为由θRp参数化。因此,例如, f θ 可以简单地表示为y=fθ(x)=θ**x

图 3-1 显示了一个神经元(或神经网络内的一个单元)的架构。

img/478491_2_En_3_Fig1_HTML.jpg

图 3-1

前馈网络中的一个单元

单位

一个单元(也称为节点神经元)是神经网络的基本构建模块,参见图 3-1 和图 3-2 。

一个单元/节点/神经元是一个函数,它将一个向量xRn作为输入,并产生一个标量。一个单元由权重向量wRn和由 b 表示的偏差项来参数化。

单位的输出可以描述为

f\left({\sum}_{i=1}^n{x}_i\cdotp {w}_i+b\right)

其中 f : RR 称为激活函数

虽然可以使用各种各样的激活函数,但正如我们将在本章后面看到的,通常使用非线性函数。

图 3-2 显示了该装置的详细情况。

img/478491_2_En_3_Fig2_HTML.jpg

图 3-2

神经网络中的单元

神经网络的整体结构

使用该单元作为基本构建块来构建神经网络。这些单元被组织成层,每层包含一个或多个单元。最后一层被称为输出层。输出层之前的所有层被称为隐藏层。第一层,通常称为第 0 ,是输入层。每一层通过权重连接到下一个连续层,权重以迭代的方式被训练/更新。

一层中的单元数量被称为该层的宽度。每层的宽度不必相同,但是尺寸应该一致,我们将在本章后面看到。

层数被称为网络的深度。这就是“深度”(如“深度学习”)概念的来源。

每一层都将前一层产生的输出作为输入,除了第一层消耗输入。最后一层的输出是网络的输出,是基于输入生成的预测。

如前所述,神经网络可以看作是一个函数fθ:xy,它以xRn作为输入,产生yRm我们现在可以更精确地了解 θ 。它只是网络中所有单元的所有权重的集合。

设计神经网络包括定义网络的整体结构,包括层数(深度)和这些层的宽度。图 3-3 显示了一个神经网络的整体结构。

img/478491_2_En_3_Fig3_HTML.jpg

图 3-3

神经网络的结构

以向量形式表达神经网络

让我们更详细地看看神经网络的层及其维度(参见图 3-3 )。如果我们假设输入的维度是xRn并且第一层有 p 1 个单元,那么每个单元都有wRn个权重与之相关联。也就是说,与第一层相关联的权重是形式为{w}_1\in {R}^{n\times {p}_1}的矩阵。虽然图 3-3 中没有显示,但是每个p1 单元也有一个与之相关的偏置项。

第一层产生输出{o}_1\in {R}^{p_1},其中{o}_i=f\left({\sum}_{k=1}^n{x}_k\cdotp {w}_k+{b}_i\right)。注意,索引 k 对应于每个输入/权重(从 1… n ),索引 i 对应于第一层中的单元(从 1。。p1)。

现在让我们看看第一层的矢量化符号输出。通过矢量化符号,我们简单地表示线性代数运算,例如向量矩阵乘法和对产生向量的向量的激活函数的计算(而不是标量到标量)。第一层的输出可以表示为f(x**w1+b1)。

这里,我们将输入xRn视为维数为 1 × n ,将权重矩阵 w 1 视为维数为n×p1,将偏差项视为维数为 1×p的向量那么请注意,x**w1+b产生一个维数为 1 × p 1 的向量,函数 f 简单变换向量的每个元素产生{o}_1\in {R}^{p_1}

{o}_1\in {R}^{p_1}{o}_2\in {R}^{p_2}的第二层遵循类似的过程。这可以用矢量化的形式写成f(o1w2+b2)。我们也可以将整个计算以矢量化的形式写到第 2 层,如f(f(x**w1+b1)w2+b2。图 3-4 显示了一个矢量形式的神经网络。

img/478491_2_En_3_Fig4_HTML.jpg

图 3-4

向量形式的神经网络

评估神经网络的输出

现在我们已经了解了神经网络的结构,让我们看看如何根据标记数据评估神经网络的输出。参见图 3-5 。

对于单个数据点,我们可以计算神经网络的输出,我们将其表示为\hat{y}。现在我们需要计算我们的神经网络\hat{y}的预测与 y 相比有多好。这里出现了损失函数的概念。

损失函数测量\hat{y}y 之间的差异,我们用 l 表示。许多损失函数适用于手头的任务,比如二元分类、多类分类或我们将在本章后面讨论的回归(通常使用最大似然法导出,这是一种概率框架,旨在增加找到最佳解释数据的概率分布的可能性)。

损失函数通常计算多个数据点而不是单个数据点上的\hat{y}y 之间的差异。图 3-5 展示了\hat{y}y 不一致的计算流程。

img/478491_2_En_3_Fig5_HTML.jpg

图 3-5

损失/成本函数和成本/损失的计算

训练神经网络

现在让我们看看神经网络是如何训练的。图 3-6 说明了训练一个神经网络。

假设与前面相同的符号,我们用 θ 表示网络所有层的所有权重和偏差项的集合。让我们假设 θ 已经用随机值初始化。我们用 f NN 表示代表神经网络的整体函数。

如前所述,我们可以取单个数据点,并将神经网络的输出计算为\hat{y}。我们还可以利用损失函数l\left(\hat{y},y\right)\hbox{---}计算出与实际产量 y 的不一致,即l(fNN(xθy )。

现在让我们计算这个损失函数的梯度,并用𝛻l(fnn(xθy )来表示。

我们现在可以使用最速下降法更新 θθs=θs1αl(fNN(xθ ), 请注意,我们可以对我们的训练集中的不同数据点反复采取许多这样的步骤,直到我们对l(fNN(xθ ), y )有一个合理的好值。

Note

现在,我们将远离损失函数𝛻l(fnn(xθy )的梯度的计算。这些可以很容易地用自动微分法(在本书的其他地方讨论过)来生成(甚至对于任意复杂的损失函数),而不需要手动推导。

img/478491_2_En_3_Fig6_HTML.jpg

图 3-6

训练神经网络

使用最大似然法驱动成本函数

如前所述,成本函数(也称为损失函数)有助于用量化指标确定预测和实际目标之间的差异。基于特定的用例以及目标变量的性质,有几种方法来定义损失函数。损失函数是通过利用一个框架(比如,最大似然法)得出的,在这个框架中,我们最大化或最小化一组感兴趣的结果的参数。使用损失函数计算不一致的量化值。因此,它为模型的训练框架提供了一种估计不一致程度的切实可行的方法,从而更新权重参数,以便减少不一致,从而提高模型性能。

我们现在将研究如何使用最大似然法导出各种损失函数。具体来说,我们将看到深度学习中常用的损失函数——如二进制交叉熵、交叉熵(用于非二进制结果)和平方误差——如何使用最大似然原理推导出来。

二元交叉熵

二进制交叉熵对数损失,衡量分类模型的性能,其中结果是二进制的,并以 0 到 1 之间的概率值的形式表示。随着模型性能降低,测井损失值增加,产生的预测值偏离期望值。理想模型的二进制交叉熵值为 0。

让我们考虑一个简单的例子来理解二元交叉熵的概念,并获得最大似然的基本直觉。我们有一些数据,由 D = {( x 1y 1 ), x 2y 2 ),…(xny n

让我们假设我们已经生成了一个模型,在给定 x 的情况下预测 y 的概率。我们用 f ( xθ 来表示这个模型,其中 θ 表示模型的参数。最大似然背后的思想是找到一个最大化P(D|θ)的 θ 。假设一个伯努利分布,并给定每个例子{( x 1y 1 ),( x 2y 2 ),…(xny

我们可以对两边进行对数运算得出如下:

\mathit{\log}\ P\left(\theta \right)=\mathit{\log}\ {\prod}_{i=1}^nf{\left({x}_i,\theta \right)}^{y_i}\cdotp {\left(1-f\left({x}_i,\theta \right)\right)}^{\left(1-{y}_i\right)}

从而简化为以下表达式:

\mathit{\log}\ P\left(\theta \right)={\sum}_{i=1}^n{y}_i\mathit{\log}\ f\left({x}_i,\theta \right)+\left(1-{y}_i\right)\mathit{\log}\ \left(1-f\left({x}_i,\theta \right)\right)

我们不是最大化 RHS,而是最小化它的负值,如下:

P\left(\theta \right)=-{\sum}_{i=1}^n{y}_i\mathit{\log}\ f\left({x}_i,\theta \right)+\left(1-{y}_i\right)\mathit{\log}\ \left(1-f\left({x}_i,\theta \right)\right)

这就引出了下面这个二元交叉熵函数:

-{\sum}_{i=1}^n{y}_i\mathit{\log}\ f\left({x}_i,\theta \right)+\left(1-{y}_i\right)\mathit{\log}\ \left(1-f\left({x}_i,\theta \right)\right)

因此,最大似然的思想使我们能够导出二元交叉熵函数,该函数可以在二元分类的上下文中用作损失函数。

交叉熵

基于二进制交叉熵的思想,现在让我们考虑导出交叉熵损失函数以用于多分类的上下文中。我们假设 y ∈ {0,1,.. k },其中{0,1,.. k }是类。我们还将 n 1n2nk表示为每个 k 类的观察计数。观察{\sum}_{i=1}^k{n}_i=n。同样,在这种情况下,让我们假设我们已经以某种方式生成了一个模型,该模型在给定 x 的情况下预测了 y 的概率。我们用 f ( xθ 来表示这个模型,其中 θ 表示模型的参数。让我们再次使用最大似然背后的思想,即找到一个使P(D|θ最大化的 θ 。假设是多项式分布,并给定每个例子{( * x * 1y 1 ),( x 2y 2 ),…(xn

*我们可以对两边进行对数运算得出如下:

\mathit{\log}\ P\left(\theta \right)=\mathit{\log}\ n!-\kern0.5em \mathit{\log}\ {n}_1!\cdotp {n}_2!\cdots {n}_k!+\mathit{\log}\ {\prod}_{i=1}^nf{\left({x}_i,\theta \right)}^{y_i}

这可以简化为:

\mathit{\log}\ P\left(\theta \right)=\mathit{\log}\ n!-\kern0.5em \mathit{\log}\ {n}_1!\cdotp {n}_2!\cdots {n}_k!+{\sum}_{i=1}^n{y}_i\mathit{\log}\ f\left({x}_i,\theta \right)

术语日志 n !还有 log n 1n2!⋯n??k!不被 θ 参数化,并且可以被安全地忽略,因为我们试图找到最大化P(D|θ)的 θ 。由此,我们有了以下:

\mathit{\log}\ P\left(\theta \right)={\sum}_{i=1}^n{y}_i\mathit{\log}\ f\left({x}_i,\theta \right)

和以前一样,我们不是最大化 RHS,而是最小化它的负值,如下:

P\left(\theta \right)=-{\sum}_{i=1}^n{y}_i\mathit{\log}\ f\left({x}_i,\theta \right)

这就导致了下面的二元交叉熵函数:

-{\sum}_{i=1}^n{y}_i\mathit{\log}\ f\left({x}_i,\theta \right)

因此,最大似然的思想使我们能够导出交叉熵函数,它可以在多分类的情况下用作损失函数。

平方误差

现在让我们讨论使用最大似然法推导回归中使用的平方误差。让我们假设 yR 。与前面的情况不同,我们假设我们有一个预测概率的模型,我们将假设我们有一个预测 y 值的模型。为了应用最大似然思想,我们假设实际的 y 和预测的\hat{y}之间的差具有零均值和方差为σ2 的高斯分布。然后,可以显示最小化

{\sum}_{i=1}^n{\left(y-\hat{y}\ \right)}²

导致 P ( θ )最小化。

损失函数概述

我们现在总结关于损失函数的三个要点,以及给定手头问题的特定损失函数的适当性。

  1. The binary cross-entropy given by the expression

    -{\sum}_{i=1}^n{y}_i logf\left({x}_i,\theta \right)+\left(1-{y}_i\right)\mathit{\log}\left(1-f\left({x}_i,\theta \right)\right)

是二元分类的推荐损失函数。当设计神经网络来预测结果的概率时,通常应该使用这种损失函数。在这种情况下,输出层具有单个单元,该单元具有合适的 sigmoid 作为激活函数。

  1. The cross-entropy function given by the expression

    -{\sum}_{i=1}^n{y}_i logf\left({x}_i,\theta \right)

是多分类的推荐损失函数。这个损失函数通常应该与设计用来预测每一类结果的概率的神经网络一起使用。在这种情况下,输出图层具有 softmax 单位(每个类一个)。

  1. The squared loss function given by

    {\sum}_{i=1}^n{\left(y-\hat{y}\ \right)}²

应该用于回归问题。在这种情况下,输出图层只有一个单元。

几个其他损失函数可以用于分类和回归;涵盖详尽无遗的清单超出了本章的范围。一些值得注意的损失函数是 Huber 损失(回归)和铰链损失(分类)。

激活功能的类型

我们现在来看看一些常用于神经网络的激活函数。

让我们从列举激活函数的几个感兴趣的属性开始。

  • 理论上,当激活函数是非线性的时,两层神经网络可以逼近任何函数(给定隐藏层中足够数量的单元)。因此,我们总是使用非线性激活函数来解决深度学习领域中的问题。

  • 一个连续可微的函数允许计算梯度,并使用基于梯度的方法(优化器)来寻找使数据损失函数最小化的参数。如果一个函数不是连续可微的,基于梯度的方法在网络的训练中将不会取得进展。

  • 使用基于梯度的方法,我们可以从值域有限的函数中获得稳定的性能(相对于无穷大)。

  • 平滑函数是优选的(经验证据),单层的单片函数导致凸误差表面。(这通常不是关于深度学习的考虑因素。)

  • 此外,我们更倾向于期望激活函数关于原点对称,并且在原点()附近表现得像恒等函数。

至此,让我们简单看看激活函数中值得注意的选项。

线性单位

线性单元是将输入转换为 y = w 的最简单单元。 x + b 。顾名思义,该单位没有非线性行为,通常用于生成条件高斯分布的平均值。

线性单元使基于梯度的学习成为一项相当简单的任务(图 3-7 )。

img/478491_2_En_3_Fig7_HTML.jpg

图 3-7

神经网络中的线性单元

乙状结肠激活

sigmoid 激活将输入转换如下:

y=\frac{1}{1+{e}^{-\left( wx+b\right)}}.

底层激活函数(图 3-8 )由

f(x)=\frac{1}{1+{e}^{-x}}.

给出

Sigmoid 单元可在输出图层中与二进制交叉熵一起用于二进制分类问题。该单元的输出可以在以 x 为条件的输出 y 上模拟伯努利分布。

img/478491_2_En_3_Fig8_HTML.jpg

图 3-8

Sigmoid 函数

Softmax 激活

softmax 图层通常仅在输出图层中与交叉熵损失函数一起用于多分类任务。参见图 3-9 。softmax 层对前一层的输出进行归一化,使其总和为 1。通常,前一层的单元对输入属于特定类别的可能性的非标准化分数进行建模。softmax 层对此进行了标准化,以便输出表示每个类的概率。

img/478491_2_En_3_Fig9_HTML.jpg

图 3-9

Softmax 层

整流器线性单元

与线性变换结合使用的整流线性单元(ReLU)将输入变换为

f(x)=\mathit{\max}\left(0, wx+b\right)

底层激活函数为f(x)=max(0, x )。最近,ReLU 更常用作隐藏单元。结果表明,ReLUs 导致较大且一致的梯度,这有助于基于梯度的学习(图 3-10 )。虽然 ReLU 看起来像一个线性单元,但它有一个导数函数,因此可以计算损耗的梯度。最近,ReLU 已经成为隐藏网络激活的最流行的选择。在大多数情况下,一个 ReLU 可以是一个默认的选择,它会在一个合适的时间内产生想要的结果。

img/478491_2_En_3_Fig10_HTML.jpg

图 3-10

整流器线性单元

然而,ReLU 也有一些缺点。当输入接近零时,函数的梯度变为零,因此停留在训练步骤中,训练没有进展。这就是俗称的将死的 ReLU 问题

双曲正切

双曲正切单元对输入(与线性变换结合使用)进行如下变换:

y=\mathit{\tanh}\left( wx+b\right).

底层激活函数(图 3-11 )由

f(x)=\mathit{\tanh}(x).

给出

双曲正切单位也常用作隐藏单位。

图 3-11 仅涵盖了深度学习激活功能中的少数可用选项。

img/478491_2_En_3_Fig11_HTML.jpg

图 3-11

双曲正切激活函数

在特定的设置或使用案例中,还有更多的方法可用于定制收益。著名的例子包括泄漏 ReLU、参数 ReLU 和 Swish。探索附加激活功能的良好起点是 https://pytorch.org/docs/stable/nn.html#non-linear-activations-weighted-sum-nonlinearity

反向传播

深度学习最基本的构建模块是反向传播,是误差反向传播的缩写,是一种用于在监督学习中训练神经网络的算法。虽然反向传播是在 20 世纪 70 年代发明的,但它在几年后的 1989 年由 Rumelhart、Hinton 和 Williams 在他们的论文“通过反向传播误差学习表征”中得到推广

之前,我们研究了衡量预测产出和实际产出之间差异的损失函数。网络的权重首先被随机初始化。为了让网络学习(训练),下一个逻辑步骤是调整权重,使得不一致最小(理想情况下为零)。这就是我们与反向传播接口的地方,反向传播是一种直观的算法,能够使用链式法则计算损失相对于权重的梯度。

在前向传递中,网络计算给定输入样本的预测,损失函数测量实际目标值和网络预测值之间的差异。反向传播计算相对于权重和偏差的损失梯度,从而为我们提供权重的微小变化如何影响总体损失的公平的总体情况。然后,我们需要迭代地更新权重,并以小的增量(在梯度的相反方向)达到局部最小值。这个过程叫做梯度下降——即将损失函数降低到最小。因此,网络学习(对权重的迭代和增量更新)能够以最小的不一致正确预测给定输入样本的模式。

对于神经网络,在梯度下降中有几个变量来更新权重。下一节将探讨其中的一些。在下一章中,我们将简要地看一下自动微分,它以编程的方式实现了反向传播的思想。

梯度下降变体

梯度下降技术主要有三种变体。每种方法的不同之处在于用于计算损失梯度的数据量。根据使用的数据量,我们在参数更新的准确性和执行更新所需的时间之间进行权衡。下面,我们讨论在训练深度学习网络中使用的三种不同的变体,稍后(在下面的部分中)我们研究几个流行的梯度下降优化算法。

批量梯度下降

最初的梯度下降被称为批量梯度下降 (BGD)技术。该名称源自用于计算梯度的数据量,在本例中为整个批次。BGD 技术本质上利用整个可用数据集来计算成本函数相对于参数(权重)的梯度。这导致了固有的缓慢,并且在大多数情况下,这是一个不可行的选择,因为我们可能会耗尽内存来加载整个批处理。在大多数常见的场景中,我们大多倾向于避免 BGD 方法,争论小数据集(这在深度学习中是一种罕见的现象)。

随机梯度下降

为了克服来自 BGD 的问题,我们有随机梯度下降(SGD)。使用 SGD,我们计算梯度并更新数据集中每个样本的权重。这一过程大大减少了深度学习硬件中的内存使用,并更快地获得结果。但是,更新的频率远远高于预期。随着更频繁地更新权重,成本函数波动很大。

然而,当目标是将更新收敛到精确的最小值时,SGD 会导致更大的问题。考虑到更新的频繁程度,过早更新的可能性非常高。为了克服这些权衡,我们可能需要在一段时间内缓慢降低学习速率,以帮助网络收敛到局部或全局最小值。

小批量梯度下降

小批量梯度下降 (MBGD)结合了 SGD 和 BGD 的优点。MBGD 不是使用整个数据集(批次)或仅来自数据集的单个样本来计算成本函数相对于参数的梯度,而是利用更小的批次,该批次大于 1 但小于整个数据集。常见的批量有 16/32/64/…1024 等。建议使用 2 的幂范围内的一个数(但不是必需的),因为从计算的角度来看它最合适。

使用 MBGD,更新频率比 SGD 低,但比 BGD 高,并且利用小批量而不是单个样本或整个数据集。这样,方差在更大程度上减小,并且我们在速度上实现了更好的折衷。

基于梯度的优化技术

在接下来的部分,我们将简要讨论深度学习中常用的几种流行的优化技术。每种技术中使用的数学细节超出了本书的范围。

动量梯度下降

我们之前讨论的 SGD 和 BGD 之间的问题用 MBGD 解决了。但是,即使使用了 MBGD,更新的方向仍然会发生变化(虽然比使用 SGD 时要小,但比使用 MGD 时要大)。具有动量的梯度下降利用过去的梯度来计算梯度的指数加权平均值,以进一步平滑参数更新。

图 3-12 说明了更新过程。

img/478491_2_En_3_Fig12_HTML.jpg

图 3-12

动量梯度下降

更新过程可以使用以下公式来简化。首先,我们计算过去梯度的指数加权平均值为 ν t ,其中νt=γνt—1+ηθj(θ)和θ=θ-

*这里的 γ 是一个取值在 0 到 1 之间的超参数。接下来,我们在权重更新中使用这个指数加权平均值,而不是直接使用梯度。

通过利用梯度的指数加权平均值,而不是直接使用梯度,增量步长更平滑和更快,从而克服了围绕最小值振荡的问题。

RMSprop

RMSprop 是 Geoffry Hinton 在 Coursera 的在线课程“机器学习的神经网络”的第 6 讲中提出的一种未公开的优化算法。在核心处,RMSprop 计算每个权重的平方梯度的移动平均值,并将梯度除以均方的平方根。这个复杂的过程应该有助于解码名字均方根 prop 。在这里利用指数平均有助于给予最近的更新比不太最近的更新更多的偏好。

RMSprop 可以表示如下:

对于θ中的每个权重 w,我们有

{\nu}_t=\beta\ {\nu}_{t-1}+\left(1-\beta \right)\ast {g}_t²

\Delta  {\mathrm{w}}_t=-\frac{\eta }{\sqrt{\nu_t+\in }\ }*g??t

{w}_{t+1}={w}_t+\Delta  {\mathrm{w}}_t

更新权重

其中η–是定义初始学习率的超参数,gt是θ中参数/权重 w 在时间 t 的梯度。我们将∈加到分母上,以避免被零除的情况。

圣经》和《古兰经》传统中)亚当(人类第一人的名字

Adam 是自适应矩估计的简化名称,是深度学习优化器最近最受欢迎的选择。简单地说,Adam 结合了 RMSprop 和带动量的随机梯度下降的优点。从 RMSprop 中,它借用了使用平方梯度来缩放学习速率的思想,并且当与具有动量的 SGD 相比时,它采用梯度的移动平均值的思想,而不是直接使用梯度。

这里,对于θ中的每个权重 w,我们有

{\nu}_t={\beta}_1\ {\nu}_{t-1}+\left(1-\kern0.5em {\beta}_1\right)\ast {g}_t

还有

{s}_t={\beta}_2\ {s}_{t-1}+\left(1-\kern0.5em {\beta}_2\right)\ast {g}_t²

然后用来计算

\Delta  {\mathrm{w}}_t=-\eta \frac{\nu_t}{\sqrt{s_t+\in }\ }*g??t

最后,权重更新为

{w}_{t+1}={w}_t+\Delta  {\mathrm{w}}_t

前面三种类型的优化算法只是深度学习中不同类型用例的可用选项中的一小部分。我们肯定没有涵盖这些主题中每一个的详细深度和数学,所以强烈建议读者更详细地探索前面的优化技术和其他技术。阿达格拉德和阿达德尔塔是热门和强烈推荐的选择。

PyTorch 的实际实现

到目前为止,我们已经提供了前馈神经网络的基本主题的简要概述。我们现在将使用 PyTorch 实现一个简单的网络。引入第一个网络所需的所有构建模块的想法使得 PyTorch 中的懒惰学习(在必要时学习构造)过程更加有效。

清单 3-1 为这个练习导入了必要的 Python 包。

#Import required libraries
import torch as tch
import torch.nn as nn

import numpy as np

from sklearn.datasets import make_blobs
from matplotlib import pyplot

Listing 3-1Importing the Necessary Python Packages

我们将需要 Torch 及其神经网络模块,以及 NumPy、matplotlib(用于可视化)和 sklearn(用于创建虚拟数据集)。虽然有一百万种方法可以创建虚拟数据集,但我们将利用 sklearn 中提供的一个简单函数。

Note

在本书中,我们使用了几个与机器学习相关的流行 Python 包。这些包中的大多数都是随 Anaconda 发行版一起安装的。如果需要的话,将特别调用其他包。

接下来,让我们为神经网络创建一个虚拟数据集。清单 3-2 展示了为练习创建一个玩具(假人)数据集。

samples = 5000

#Let's divide the toy dataset into training (80%) and rest for validation.
train_split = int(samples*0.8)

#Create a dummy classification dataset
X, y = make_blobs(n_samples=samples, centers=2, n_features=64, cluster_std=10, random_state=2020)
y = y.reshape(-1,1)

#Convert the numpy datasets to Torch Tensors
X,y = tch.from_numpy(X),tch.from_numpy(y)
X,y =X.float(),y.float()

#Split the datasets inot train and test(validation)
X_train, x_test = X[:train_split], X[train_split:]
Y_train, y_test = y[:train_split], y[train_split:]

#Print shapes of each dataset
print("X_train.shape:",X_train.shape)
print("x_test.shape:",x_test.shape)
print("Y_train.shape:",Y_train.shape)
print("y_test.shape:",y_test.shape)
print("X.dtype",X.dtype)
print("y.dtype",y.dtype)

Output[]
X_train.shape: torch.Size([4000, 64])
x_test.shape: torch.Size([1000, 64])
Y_train.shape: torch.Size([4000, 1])
y_test.shape: torch.Size([1000, 1])
X.dtype torch.float32
y.dtype torch.float32

Listing 3-2Creating a Toy Dataset

玩具数据集有 5000 个样本,每个样本有 32 个特征,分为 80%训练和 20%测试。让我们创建一个使用 PyTorch 的 NN 模块定义神经网络的类。清单 3-3 定义了用于本练习的神经网络的创建。

#Define a neural network with 3 hidden layers and 1 output layer
#Hidden Layers will have 64,256 and 1024 neurons
#Output layers will have 1 neuron

class NeuralNetwork(nn.Module):

    def __init__(self):
        super().__init__()
        tch.manual_seed(2020)
        self.fc1 = nn.Linear(64, 256)
        self.relu1 = nn.ReLU()
        self.fc2 = nn.Linear(256, 1024)
        self.relu2 = nn.ReLU()
        self.out = nn.Linear(1024, 1)
        self.final = nn.Sigmoid()

    def forward(self, x):
        op = self.fc1(x)
        op = self.relu1(op)
        op = self.fc2(op)
        op = self.relu2(op)
        op = self.out(op)
        y = self.final(op)
        return y

Listing 3-3Defining a Feed Forward Neural Network

torch.nn模块提供了定义和训练神经网络的基本方法。它包含创建各种类型、大小和复杂性的神经网络的所有必要构件。我们将通过继承这个模块为我们的神经网络创建一个类,并创建一个初始化方法和一个向前传递方法。

__init__方法创建网络的不同部分,并在我们每次用这个类创建一个对象时为我们准备好。本质上,我们使用初始化方法来创建隐藏层、输出层和每个层的激活。nn.Linear(64,256)函数创建一个具有 64 个输入特征和 256 个输出特征的图层。下一层自然会有 256 个输入特征,依此类推。当连接到一个层时,nn.ReLU()nn.Sigmoid()功能增加了激活功能。在初始化函数中创建的每个单独的组件都在forward()方法中连接。

forward方法中,我们连接神经网络的各个组件。第一个隐藏层fc1接受输入数据,并为下一层产生 256 个输出。fc1层被传递给relu1激活层,然后激活层将激活的输出传递给下一层fc2,后者重复相同的过程,以创建最终的输出层,该层具有 sigmoid 激活函数(因为我们的玩具数据集是为二进制分类而制作的)。

在创建一个类为NeuralNetwork的对象并调用forward方法时,我们从网络中获得输出,这些输出是通过将输入矩阵与一个随机初始化的权重矩阵相乘来计算的,该权重矩阵通过一个激活函数传递,并对隐藏层的数量进行重复,直到最终的输出层。起初,网络显然会产生垃圾输出——即预测(这对我们的分类问题没有任何价值,至少现在没有)。

为了对我们给定的问题进行更准确的预测,我们需要训练网络,即反向传播损失并更新损失函数的权重。幸运的是,PyTorch 以一种非常容易使用和直观的方式提供了这些基本的构建模块。清单 3-4 说明了定义神经网络的损失、优化器和训练循环。

#Define function for training a network
def train_network(model,optimizer,loss_function \
                  ,num_epochs,batch_size,X_train,Y_train):
    #Explicitly start model training
    model.train()

    loss_across_epochs = []
    for epoch in range(num_epochs):
        train_loss= 0.0

        for i in range(0,X_train.shape[0],batch_size):

            #Extract train batch from X and Y
            input_data = X_train[i:min(X_train.shape[0],i+batch_size)]
            labels = Y_train[i:min(X_train.shape[0],i+batch_size)]

            #set the gradients to zero before starting to do backpropragation
            optimizer.zero_grad()

            #Forward pass
            output_data  = model(input_data)

            #Caculate loss
            loss = loss_function(output_data, labels)

            #Backpropogate
            loss.backward()

            #Update weights
            optimizer.step()

            train_loss += loss.item() * batch_size

        print("Epoch: {} - Loss:{:.4f}".format(epoch+1,train_loss ))
        loss_across_epochs.extend([train_loss])

    #Predict
    y_test_pred = model(x_test)
    a =np.where(y_test_pred>0.5,1,0)
    return(loss_across_epochs)
###------------END OF FUNCTION--------------

#Create an object of the Neural Network class
model = NeuralNetwork()

#Define loss function
loss_function = nn.BCELoss()  #Binary Crosss Entropy Loss

#Define Optimizer
adam_optimizer = tch.optim.Adam(model.parameters(),lr= 0.001)

#Define epochs and batch size
num_epochs = 10
batch_size=16

#Calling the function for training and pass model, optimizer, loss and related paramters
adam_loss = train_network(model,adam_optimizer \
                             ,loss_function,num_epochs,batch_size,X_train,Y_train)

Listing 3-4Defining the Loss, Optimizer, and Training Function for the Neural Network

在我们进入清单 3-4 的细节之前,让我们看看我们利用 PyTorch 现成的构建模块定义的各个组件。我们需要定义一个损失函数来衡量我们的预测和实际标签之间的差异。PyTorch 提供了不同结果的损失函数的综合列表。这些损失函数在torch.nn.*下可用。例子有MSELoss(均方误差损失)CrossEntropyLoss(用于多类分类)BCELoss(二元交叉熵损失),用于二元分类。对于我们的用例,我们将利用二元交叉熵损失。

这被定义为loss_function = torch.nn.BCELoss()

接下来,我们为我们的网络定义一个优化器。在本章的前面,我们探讨了 SGD、Adam 和 RMSProp 优化器。Pytorch 提供了一个全面的优化器列表,可用于构建各种类型的神经网络。所有优化器都组织在torch.optim.*下(例如,torch.optim.SGD,用于 SGD 优化器)。对于我们的用例,我们使用 Adam 优化器(大多数用例最推荐的优化器)。在定义优化器时,我们还需要定义在反向传播过程中需要计算梯度的参数。对于神经网络,该列表将是前馈网络中的所有权重。通过在优化器的定义中使用model.parameters(),我们可以很容易地向优化器表示模型权重的完整列表。然后,我们可以为所选的优化器另外定义超参数。默认情况下,PyTorch 为所有必需的超参数提供了相当好的值。然而,我们可以进一步覆盖它们,为我们的用例定制优化器。

adam_optimizer = tch.optim.Adam(model.parameters(),lr= 0.001)

最后,我们需要定义批量大小和训练模型所需的历元数。批量指小批量更新中一个批次内的样本数量。覆盖所有样本的所有批次的一次向前和向后通过被称为一个时期。最后,我们将所有这些构造传递给我们的函数来训练我们的模型。让我们详细看看函数中的构造。

在我们的训练函数中,我们定义了一个结构,用所提供的优化器、损失函数、模型对象和训练数据来训练我们的网络。首先,我们用model.train()初始化我们的训练模式模型。将模型对象明确设置为训练模式是必要的;在利用模型进行评估时,这也是必不可少的——即,使用model.eval()显式地将模型设置为评估模式。这确保了模型知道期望何时更新参数以及何时不更新参数。在前面的例子中,我们没有添加评估循环,因为它是一个很小的玩具数据集。然而,在后面的大型数据集示例中,我们将使用单独的函数进行评估。

我们将小批量训练网络。for循环按照我们定义的大小将训练数据分成几批。使用以下代码为一个批次提取训练数据以及相应的标签:

input_data = X_train[i:min(X_train.shape[0],i+batch_size)]
labels = Y_train[i:min(X_train.shape[0],i+batch_size)]

然后,在使用optimizer.zero_grad()开始反向传播之前,我们需要将梯度设置为零。错过这个步骤将会在后续的反向过程中累积梯度,并导致不期望的效果。这种行为是 PyTorch 设计的。然后,我们使用output_data = model(input_data)计算向前传球。向前传递是在我们的类定义中执行forward()函数。它连接我们为网络定义的不同层,最终输出每个样本的预测。一旦我们有了预测,我们就可以使用损失函数计算它与实际标签的偏差,即loss = loss_function(output_data, labels)

为了反向传播我们的损失,PyTorch 提供了一个内置的模块来计算损失相对于权重的梯度。我们简单地调用loss.backward()方法,整个反向传播就完成了。第四章“深度学习中的自动微分”更详细地探讨了 PyTorch 中负责反向传播的自动签名模块。一旦计算出梯度,就该更新我们的模型权重了。这在步骤optimizer.step()中完成。优化器步骤知道需要用梯度更新的参数,因为我们在定义优化器时提供了这些参数。调用optimizer.step()函数更新网络的权重,自动考虑优化器中定义的超参数——在我们的例子中是学习率。

我们对整个训练样本分批重复这个过程。训练过程针对多个时期重复进行,并且随着每次迭代,我们期望损失减少并且权重对齐,以便实现更好的预测准确性。

清单 3-5 使用不同的优化器来说明前面的神经网络的训练过程。由于网络是为玩具数据集训练的,我们将在每个时期后为不同的优化器绘制总损失,而不是绘制验证准确性。我们可以研究图 3-13 中展示的每个优化变量的输出,即跨时段的损失。

#Define loss function
loss_function = nn.BCELoss()  #Binary Crosss Entropy Loss
num_epochs = 10
batch_size=16

#Define a model object from the class defined earlier
model = NeuralNetwork()

#Train network using RMSProp optimizer
rmsprp_optimizer = tch.optim.RMSprop(model.parameters()
, lr=0.01, alpha=0.9
, eps=1e-08, weight_decay=0.1
, momentum=0.1, centered=True)
print("RMSProp...")
rmsprop_loss = train_network(model,rmsprp_optimizer,loss_function
,num_epochs,batch_size,X_train,Y_train)

#Train network using Adam optimizer

model = NeuralNetwork()
adam_optimizer = tch.optim.Adam(model.parameters(),lr= 0.001)
print("Adam...")
adam_loss = train_network(model,adam_optimizer,loss_function
,num_epochs,batch_size,X_train,Y_train)

#Train network using SGD optimizer

model = NeuralNetwork()
sgd_optimizer = tch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
print("SGD...")
sgd_loss = train_network(model,sgd_optimizer,loss_function
,num_epochs,batch_size,X_train,Y_train)

#Plot the losses for each optimizer across epochs
import matplotlib.pyplot as plt
%matplotlib inline

epochs = range(0,10)

ax = plt.subplot(111)
ax.plot(adam_loss,label="ADAM")
ax.plot(sgd_loss,label="SGD")
ax.plot(rmsprop_loss,label="RMSProp")
ax.legend()
plt.xlabel("Epochs")
plt.ylabel("Overall Loss")
plt.title("Loss across epochs for different optimizers")
plt.show()

Output[]
RMSProp...
Epoch: 1 - Loss:5794.6734
Epoch: 2 - Loss:1680.3092
Epoch: 3 - Loss:1169.5457
Epoch: 4 - Loss:1518.7088
Epoch: 5 - Loss:1727.5753
Epoch: 6 - Loss:661.7122
Epoch: 7 - Loss:532.6023
Epoch: 8 - Loss:2613.1597
Epoch: 9 - Loss:283.5713
Epoch: 10 - Loss:1058.1581

Adam...

Epoch: 1 - Loss:106.7566
Epoch: 2 - Loss:11.5689
Epoch: 3 - Loss:7.8169
Epoch: 4 - Loss:0.2327
Epoch: 5 - Loss:0.0313
Epoch: 6 - Loss:0.0034
Epoch: 7 - Loss:0.0019
Epoch: 8 - Loss:0.0012
Epoch: 9 - Loss:0.0009
Epoch: 10 - Loss:0.0007

SGD...

Epoch: 1 - Loss:801.0526
Epoch: 2 - Loss:131.7263
Epoch: 3 - Loss:296.2784
Epoch: 4 - Loss:240.0572
Epoch: 5 - Loss:248.2811
Epoch: 6 - Loss:248.2784
Epoch: 7 - Loss:248.2759
Epoch: 8 - Loss:248.2733
Epoch: 9 - Loss:248.2708
Epoch: 10 - Loss:248.2684

Listing 3-5Training Model with Various Optimizers

img/478491_2_En_3_Fig13_HTML.jpg

图 3-13

网络跨时段的分布损耗

摘要

本章关于前馈神经网络的内容将作为本书其余部分的概念基础。我们讨论的关键概念是神经网络的整体结构、输入、隐藏和输出层,以及成本函数及其基于最大似然原则的基础。我们还探索了 PyTorch 作为实际实现神经网络的方法。在最后一个练习中,我们在一个玩具数据集上用各种优化器对网络进行了训练,以研究损失是如何随着时代的推移而减少的。

下一章,我们将探讨深度学习中的自动微分。**

四、深度学习中的自动微分

在第三章中探讨随机梯度下降时,我们将损失函数𝛻xl(x)的梯度计算视为黑箱。在这一章中,我们打开了黑盒,涵盖了自动微分的理论和实践,以及探索 PyTorch 的亲笔签名的模块,实现了同样的功能。自动微分是一种成熟的方法,它可以轻松有效地计算任意复杂损失函数的梯度。当涉及到最小化感兴趣的损失函数时,这是至关重要的;构建任何深度学习模型的核心都是一个优化问题,总是使用随机梯度下降来解决,这反过来需要计算梯度。

自动微分不同于数值微分和符号微分。我们首先对这两者进行足够的介绍,以便区分变得清晰。为了便于说明,假设我们感兴趣的函数是 f : RR ,并且我们想要找到 f 的导数,表示为f(x)。

数值微分

数值微分的基本形式来自导数/梯度的定义。它用于估计数学函数的导数。y 相对于 x 的导数更具体地定义了 y 相对于 x 的变化率。一个简单的方法是通过线 x,f(x)和 x+h,f(x+h)计算函数的斜率。

所以,鉴于

{f}^{\prime }(x)=\frac{df}{dx}=\frac{f\left(x+\varDelta x\right)-f(x)}{\varDelta x}

我们可以用向前差分法计算出 f(x)为

{f}^{\prime }(x)={D}_{+}(h)=\frac{f\left(x+h\right)-f(x)}{h}

h 设置一个适当小的值。同样,我们可以用向后差分法计算f(x)为

{f}^{\prime }(x)=D\_(h)=\frac{f(x)-f\left(x-h\right)}{h}

同样,通过为 h 设置一个适当小的值。

一种更对称的形式是中心差分法,它将f计算为

{f}^{\prime }(x)={D}_0(h)=\frac{f\left(x+h\right)-f\left(x-h\right)}{2h}

外推法是一种使用已知值来预测超出预期的现有已知范围的值的过程。 Richardson 外推法是一种技术,有助于实现仅使用几个数值系列来估计高阶积分。

{f}^{\prime }(x)=\frac{4{D}_0(h)-{D}_0(2h)}{3}

前向和后向差分的逼近误差依次为 h ,即O(h)—而中心差分和 Richardson 外推的逼近误差分别为 O ( h 2 )和O(h4)。

数值微分的关键问题是计算成本,它随着损失函数中参数的数量、截断误差和舍入误差而增加。截断误差是我们在计算f(x)时由于 h 不为零而产生的不准确性。舍入误差是使用浮点数和浮点运算所固有的(与使用无限精度数相反,后者的成本高得惊人)。

因此,在构建深度学习模型时,数值微分不是计算梯度的可行方法。数值微分派上用场的唯一地方是快速检查梯度计算是否正确。当您已经手动计算梯度或使用新的/未知的自动微分库时,强烈建议您这样做。理想情况下,这种检查应该在启动 SGD 之前作为自动检查/断言进行。

Note

数值微分在一个名为 Scipy 的 Python 包中实现。我们在这里不涉及它,因为它与深度学习没有直接关系。

符号微分法

符号微分的基本形式是应用于损失函数以得到导数/梯度的一组符号重写规则。考虑两个这样简单的规则

\frac{d\ }{dx}\left(f(x)+g(x)\right)=\frac{d}{dx}\ f(x)+\frac{d}{dx}\ g(x)

还有

\frac{d}{dx}\ {x}^n=n{x}^{\left(n-1\right)}

给定一个函数,如f(x)= 2x3+x2,我们可以依次应用符号书写规则,首先到达

{f}^{\prime }(x)=\frac{d}{dx}\ \left(2{x}³\right)+\frac{d}{dx}\ \left({x}²\right)

通过应用第一重写规则,和

{f}^{\prime }(x)=6{x}²+2x

运用第二条规则。

因此,当我们手动推导梯度时,符号微分是自动化的。当然,这种规则的数量可以很大,并且可以利用更复杂的算法来使这种符号重写更有效。然而,在本质上,符号微分只是一套符号重写规则的应用。符号微分的主要优点是它为导数/梯度生成一个清晰的数学表达式,可以被理解和分析。

符号微分的关键问题是,它仅限于已经定义的符号微分规则,这可能导致我们在试图最小化复杂的损失函数时遇到障碍。例如,当损失函数涉及 if-else 子句或 for/while 循环时。从某种意义上说,符号微分是在微分一个(封闭形式的)数学表达式;它不区分给定的计算过程。

符号微分的另一个问题是,在某些情况下,符号重写规则的天真应用会导致符号项的爆炸(表达式膨胀),并使该过程在计算上不可行。通常,需要大量的计算工作来简化这样的表达式,并产生导数的封闭形式的表达式。

Note

符号微分是在一个名为 SymPy 的 Python 包中实现的。我们在这里不涉及它,因为它与深度学习没有直接关系。

自动微分基础

自动微分背后的第一个关键直觉是,所有感兴趣的函数(我们打算微分的)都可以表示为初等函数的组合,对于这些初等函数,相应的导函数是已知的。因此,复合函数可以用导数的链式法则来求导。这种直觉也是符号分化的基础。

自动微分背后的第二个关键直觉是,我们可以简单地评估它们(对于一组特定的输入值),从而解决表达式膨胀的问题,而不是存储和操纵原始函数的导数的中间符号形式。因为正在评估中间符号形式,所以我们没有简化表达式的负担。注意,这阻止了我们得到导数的封闭形式的数学表达式,就像符号微分给我们的那样;我们通过自动微分得到的是对一组给定值的导数的评估。

自动微分背后的第三个关键直觉是,因为我们正在计算原始形式的导数,我们可以处理任意的计算过程,而不仅仅是封闭形式的数学表达式。也就是说,我们的函数可以包含 if-else 语句、for 循环,甚至递归。自动微分处理任何计算过程的方式是将过程的单次评估(对于给定的一组输入)视为输入变量上初等函数评估的有限列表,以产生一个或多个输出变量。尽管可能有控制流语句(if-else 语句、for 循环等。),最终,有一个特定的函数求值列表,它将给定的输入转换为输出。这种列表/评估轨迹被称为文格特列表

为了理解自动微分对于深度学习用例是如何具体工作的,让我们以一个简单的函数为例,我们将使用链式规则手动计算它,并查看实现它的 PyTorch 等价物。

在深度学习网络中,使用计算图来表示整个流程,计算图是一种有向图,其中节点表示数学运算。这提供了一个容易评估数学表达式。计算图可以被翻译成数据结构,以便使用计算机编程语言有计划地解决问题,从而使得解决更大的问题更加直观。

我们将使用一个相对较小且易于计算的函数来完成我们的示例。

假设 f(x,y,z) = (x + y)*z,我们有三个变量的值,x=1,y =-2,z =3。

我们可以用计算图来表示这个函数,如图 4-1 所示。

img/478491_2_En_4_Fig1_HTML.jpg

图 4-1

计算图

除了输入变量(x、y 和 z),我们还会看到变量 a ,它是存储(x + y)的计算值的中间变量,以及变量 f ,它存储(x + y)z 的最终值,即 a*z

在正向传递中,我们将替换这些值,并得出最终值,如下所示

x = 1,y =-2,z= 3

然后,

(x + y )z = (1 - 2)3 = -3

因此,

f = -3

我们可以使用图 4-2 中所示的计算图对此进行可视化。

img/478491_2_En_4_Fig2_HTML.jpg

图 4-2

带有计算值的计算图

现在,通过自动微分,我们想要找到相对于输入变量(x,y 和 z)的 f 的梯度,输入变量表示为\frac{\partial f}{\partial x}\frac{\partial f}{\partial y}\frac{\partial f}{\partial z}

在前馈网络中,本质上,我们找到损失函数相对于权重的梯度。为了解决这个问题,我们可以使用链式法则。

让我们找出上面方程的偏导数。

我们知道 a = (x + y),z = a * x,因而 f = az。

因此,

\frac{\partial f}{\partial z}=\frac{\partial\ (az)}{\partial z}=a=(x+y)=(1–2)=-1

还有

\frac{\partial f}{\partial a}=\frac{\partial\ (az)}{\partial a}=z

如果再进一步,我们可以求出 a 关于 x 和 y 的偏导数。

\frac{\partial a}{\partial x}=\frac{\partial\ \left(x+y\right)}{\partial x}=1\frac{\partial a}{\partial y}=\frac{\partial\ \left(x+y\right)}{\partial y}=1

现在,到了我们的最终目标,找到 f 相对于 x、y 和 z 的梯度。我们已经计算了相对于 z 的所需梯度。对于 x 和 y,我们可以利用之前在链式法则中计算的值作为

\frac{\partial f}{\partial x}=\frac{\partial f}{\partial a}\frac{\partial a}{\partial x}=z\ast 1=3

\frac{\partial f}{\partial y}=\frac{\partial f}{\partial a}\frac{\partial a}{\partial y}=z\ast 1=3

我们现在已经计算了所有需要的值。

\frac{\partial f}{\partial x}=3,\kern0.75em \frac{\partial f}{\partial y} = 3 和\frac{\partial f}{\partial z} = -1

本质上,网络会推断 x 和 y 对结果有正面影响,而 z 对结果有负面影响(图 4-3 )。该信息对于减少损失是有用的,并且递增地更新网络的权重以达到最小值。

img/478491_2_En_4_Fig3_HTML.jpg

图 4-3

含有偏导数的计算图

实现自动微分

现在让我们考虑在 PyTorch 中如何实现自动微分。前面的例子非常简单;当我们在纸上探索大型函数(即深度学习函数)的方法时,事情会变得非常复杂。在大多数常见网络中,涉及的参数数量非常多,使得手动编程梯度计算成为一项艰巨的任务。

PyTorch 提供了亲笔签名的包装,从本质上简化了我们的整个过程。回想一下我们在第三章中为玩具神经网络利用的loss.backward()函数。网络计算相对于权重的损失的所有必要梯度。让我们进一步探讨这个问题。

什么是亲笔签名?

PyTorch 中的自动签名包为 tensors 上的所有操作提供了自动区分。它在反向传播过程中为我们的神经网络执行必要的计算。当调用backward()函数时,模块自动计算所有反向传播梯度。我们也可以通过变量的grad属性来访问单独的渐变。

自动签名模块为实现任意标量值函数的自动微分提供了现成的工具(函数/类)。为了能够计算变量的梯度,我们只需要将关键字requires_grad的值设置为True

让我们复制我们用来手动实现自动微分的同一个例子,但是使用 PyTorch(清单 4-1 )。

#Import required libraries
import torch

#Define ensors
x = torch.Tensor([1])
y = torch.Tensor([-2])
z= torch.Tensor([3])

print("Default value for requires_grad for x:",x.requires_grad)

#Set the keyword requires_grad as True (default is False)
x.requires_grad=True
y.requires_grad=True
z.requires_grad=True

print("Updated  value for requires_grad for x:",x.requires_grad)

#Compute a
a = x + y

#Finally define the function f
f = z * a

print("Final value for Function f = ",f)

#Compute gradients

f.backward()

#Print the gradient value
print("Gradient value for x:",x.grad)
print("Gradient value for y:",y.grad)
print("Gradient value for z:",z.grad)

Output[]
Default value for requires_grad for x: False

Updated value for requires_grad for x: True

Final value for Function f = tensor([-3.], grad_fn=<MulBackward0>)
Gradient value for x: tensor([3.])
Gradient value for y: tensor([3.])
Gradient value for z: tensor([-1.])

Listing 4-1Implementing Automatic Differentition (Autograd) in PyTorch

这里的梯度值与我们之前手动计算的值完全匹配。

在前面的例子中,我们首先创建了一个张量,然后将关键字requires_grad指定为True。我们也可以把这个和我们的定义结合起来。

x = torch.autograd.Variable(torch.Tensor([1]),requires_grad=True)

当我们在 PyTorch 中定义一个网络时,很多细节都被考虑到了。当我们定义一个网络层时,用nn.Linear(64, 256)(参考章节 3 的例子),PyTorch 用必要的值创建权重和偏差张量(设置requires_gradTrue)。输入张量不需要梯度;因此,在我们的例子中,我们从不设置它们,而是使用默认值(例如,False)。

摘要

本章讲述了自动微分的基础知识。反向传播是用于训练深度神经网络的自动微分的特例。在现代深度学习文献中,自动微分类似于反向传播,因为它是一个更广义的术语。本章的关键要点是,自动微分能够计算任意复杂损失函数的梯度,是深度学习的关键使能技术之一。你应该理解自动微分的概念,以及它与符号微分和数值微分的区别。

在下一章中,我们将更详细地研究一些与深度学习相关的其他主题,包括性能指标和模型评估,分析过拟合和欠拟合,正则化和超参数调整。最后,我们将把我们迄今为止所涉及的关于深度学习的所有基础知识结合到一个实际例子中,该例子为真实世界的数据集实现了前馈神经网络。

五、训练深度学习模型

到目前为止,我们已经利用玩具数据集来提供深度学习模型的最早实现的概述。在这一章中,我们将围绕深度学习探索几个额外的重要主题,并在一个实际例子中实现它们。我们将深入研究模型性能的细节,并研究过拟合和欠拟合、超参数调整和正则化的细节。最后,我们将结合我们目前所讨论的内容和一个真实的数据集来展示一个使用 PyTorch 的实际例子。

性能指标

在第三章中,当我们设计我们的玩具神经网络时,我们定义了损失函数来衡量预测和实际标签之间的差异。让我们用更有意义的方式来探讨这个话题。基于目标变量的类型(连续或离散),我们将需要不同类型的性能指标。接下来的部分将讨论每个类别中的指标。

分类指标

模型开发过程通常从制定清晰的问题定义开始。这基本上包括定义模型的输入和输出,以及这样一个模型能够交付的影响(有用性)。这种问题定义的一个例子是将产品图像分类成产品类别——这种模型的输入是产品图像,输出是产品类别。这种模型可能有助于在电子商务或在线市场环境中对产品进行自动分类。

定义了问题定义之后,下一个任务是定义性能指标。性能指标的主要目的是告诉我们我们的模型做得有多好。一个简单的性能度量可以是准确性(或者,等价地,误差),它简单地度量了预期输出和模型产生的输出之间的不一致。然而,准确性可能是一个很差的性能指标。两个主要原因是阶级不平衡和不平等的错误分类成本。我们用一个例子来看一下阶层失衡问题。作为我们之前产品分类例子中问题的子问题,考虑区分手机及其配件的情况。移动电话类别的示例数量比移动电话配件的类别少得多。例如,如果 95%的例子是移动电话配件,5%是移动电话,则通过预测多数类可以简单地获得 95%的准确度。因此,在这个例子中,准确性是一个很差的度量选择。

现在让我们通过考虑一个与产品分类问题相关的例子来理解不相等的错误分类成本的问题。考虑将不含过敏原的食品(不含八大过敏原——即牛奶、鸡蛋、鱼、甲壳类贝类、坚果、花生、小麦和大豆)与其他食品(不含过敏原)进行分类的错误。从购买者的角度以及商业的角度来看,与将不含过敏原的产品归类为不含过敏原的产品相比,将不含过敏原的产品归类为不含过敏原的产品的错误明显更多。精度没有捕捉到这一点,因此在这种情况下将是一个糟糕的选择。

另一组度量标准是精度和召回率,它们分别测量预测类中正确恢复的预测的比例,以及报告的预测类的比例(见图 5-1 )。总的来说,精确度和召回率对于类别不平衡是鲁棒的。

img/478491_2_En_5_Fig1_HTML.jpg

图 5-1

精确度和召回率

精确度和召回率通常使用 PR 曲线来可视化,该曲线在 Y 轴上绘制精确度,在 X 轴上绘制召回率(参见图 5-2 )。通过改变分数的决策阈值或模型产生的概率,可以获得不同的精度和召回值,例如,0 表示 A 类,1 表示 B 类,较高的值在一侧表示特定的类。该曲线可用于通过改变阈值来折衷召回的精确度。

img/478491_2_En_5_Fig2_HTML.jpg

图 5-2

PR 曲线

定义为\frac{2 pr}{p+r}F 值,其中 p 表示精度 r 表示召回,可以用来概括 PR 曲线。

接收器工作特性(ROC) 曲线在类别不平衡和错误分类成本不等的情况下是有用的。在这种背景下,例子被认为属于两类:积极和消极。

真阳性率测量真阳性相对于实际阳性的比例,真阴性率测量真阴性相对于实际阴性的比例(见图 5-3 )。ROC 曲线在 X 轴上绘制真阳性率,在 Y 轴上绘制假阳性率(见图 5-4 )。曲线下的面积(AUC)用于概括 ROC 曲线。

在许多情况下,标准的度量标准,如准确度、精确度、召回率等。不允许我们真实地捕捉手边业务用例的模型性能。在这种情况下,需要制定适合业务用例的度量标准,记住问题的性质、类别不平衡和错误分类的成本。例如,在我们运行的产品分类示例中,我们可以选择不使用低置信度的预测,而是手动对它们进行分类。手动分类的例子是有成本的,在电子商务网站上错误的类别中显示错误的产品也有不同的成本。对流行产品进行错误分类的成本也不同于(通常更高)对很少购买的产品进行错误分类的成本。在这种情况下,我们可以选择只使用模型中的高可信度预测。要使用的度量的一个可能的选择是错误分类的例子的数量(具有高置信度)和覆盖范围(被高置信度覆盖的例子的数量)。人们也可以通过对两者进行加权平均来考虑这种设置中的误分类成本。(可以基于错误分类成本来选择适当的权重。)

在行业环境中,指标定义是建模过程的关键步骤。从业者应该深入分析业务领域,理解错误分类成本和数据,理解类分布,并相应地设计性能度量。定义不当的度量标准会导致项目走向错误的道路。

img/478491_2_En_5_Fig4_HTML.jpg

图 5-4

受试者工作特征曲线

img/478491_2_En_5_Fig3_HTML.jpg

图 5-3

真阳性和假阳性率

回归度量

与分类指标相比,回归的性能指标相当简单。可以普遍应用于大多数用例的最常见指标是均方误差(MSE)。根据用例,可以使用一些其他指标来获得更有利的结果。考虑预测给定商店的月销售额的问题,其中商店几个月的销售额可能在50005,000 到50,000 之间。

以下部分探讨了一些流行的选择。

均方误差

我们已经在第三章“前馈神经网络”中探讨了均方误差(MSE)顾名思义,MSE 是实际值和预测值的平方差的平均值。最终结果是一个正数,因为我们取了分歧的平方。本质上,平方运算是有价值的,因为较大的差异会受到更多的惩罚。在您不希望模型更严重地惩罚较大差异的用例中,MSE 不是理想的选择。给定模型的 MSE 越低,该模型的性能越好。

数学上,我们可以将 MSE 定义为

MSE=\frac{1}{n}{\sum}_{i=0}^n{\left({y}_i-{\hat{y}}_i\right)}²

RMSE=\sqrt{\frac{\sum_{i=0}^n{\left({y}_i-{\hat{y}}_i\right)}²}{n}}

绝对平均误差

平均绝对误差 (MAE)计算预测值和目标值之间的绝对差值的平均值。对于回归用例,结果总是积极的,是比 MSE 更容易解释的性能度量。模型的 MAE 越低,性能越好。

数学上,我们可以将 MAE 定义为

MAE=\frac{1}{n}{\sum}_{i=0}^n\left|{y}_i-{\hat{y}}_i\right|

平均绝对百分比误差

平均绝对百分比误差 (MAPE)是 MAE 的百分比当量。鉴于它的相对性质,它是迄今为止最容易解释的回归性能指标。模型的 MAPE 越低,模型的性能越好。

数学上,我们可以把 MAPE 定义为

MAPE=\frac{1}{n}{\sum}_{i=0}^n\frac{\left|{y}_i-{\hat{y}}_i\right|}{y_i}

虽然具有高度的可解释性,但 MAPE 在处理小的差异时会感到痛苦。微小偏差的百分比差异通常会导致较大的 MAPE,从而导致误导性结果。例如,假设我们预测给定商店的销售天数,目标值的范围是 0 到 60。当实际值为 2 且预测值为 6 时,MAPE 为 400%,而当实际值为 10 且预测值为 12 时,MAPE 为 20%。

数据采购

数据获取是根据一个问题陈述,为建立模型而收集数据的过程。数据获取可能涉及从生产系统收集旧的(已经生成的)数据,从生产系统收集实时数据,并且在许多情况下,收集由人工操作员标记的数据(通过众包或内部运营团队)。在我们运行的产品分类示例中,产品标题、图片、描述等。将需要从公司目录中收集,标记的数据可以使用众包生成。我们可能还想收集点击数据和销售额来确定受欢迎的产品。(在这些情况下,错误分类的代价很高。)

数据获取通常与定义问题陈述和成功度量的过程一起发生。从业者必须在数据获取过程中扮演积极的角色。通常,在行业环境中,数据采集是一个相当耗时且痛苦的过程。数据采集中的细微错误可能会在后期破坏项目。

为培训、验证和测试拆分数据

一旦获得了用于构建模型的数据,就需要将它分成用于训练、参数调整和上线测试的数据。从概念上讲,现有数据将用于三个不同的目的。第一个目的是训练模型,也就是说,模型将尝试拟合这些数据。第二个目的是确定模型是否过度拟合数据;这个数据集被称为验证集。这些数据不会用于训练,但会推动超参数调整、正则化技术等方面的决策。(我们将在本章后面更详细地讨论这些主题。)数据的第三个目的是确定模型是否真的好到足以投入生产/上线(称为测试集)。

要内化的第一个关键概念是,数据不能为了这三个目的而共享;每个目的都需要数据的不同部分。如果数据的某一部分已用于训练模型,则不能用于调整模型的超参数或用作最终的性能关口(生产/上线)。同样,如果数据的某一部分已用于调整参数,则它不能作为生产/上线的测试数据。因此,从业者需要将数据分成三个部分:培训、参数调整和上线。虽然训练数据应该不同于用于参数调整的数据的想法是直观的,但拥有不同的上线设置背后的推理却不是。内化的关键点是,如果模型已经看到了数据,或者建模者已经看到了数据,那么这些数据已经从根本上驱动了围绕模型的一些决策,并且如果我们需要测试真正盲测,则这些数据不能用于最终的上线测试。真正的盲目意味着从不看数据(和标签)或从不使用它来做出任何建立模型的决定。不能通过查看上线测试集的结果来进一步调整模型。

要内在化的第二个关键点是,三个集合(训练、超参数调整和上线测试)中的每一个都必须是底层数据群体的真实代表。分割数据集时应考虑到这一点。例如,示例在各个类中的分布应该与基础总体相同。如果数据不是真实的表示(也就是说,如果数据在任何方面都有偏差),那么一旦模型投入生产,模型的性能就无法实现。

要内化的第三个关键点是,对于这三个目的中的任何一个,更多的数据总是更好的。因为数据集不能重叠,并且整个数据集是有限的,所以从业者需要仔细选择用于每个目的的数据部分。培训、验证和测试之间 50/25/25 或 60/20/20 的分割是合理的选择。

建立差错率的可实现极限

定义了问题和性能指标,获取了数据并将其分为培训、参数调整和上线测试集,下一步是建立可实现的错误率限制。从概念上讲,这是在给定无限数据供应的情况下人们希望达到的错误率,被称为贝叶斯错误。在人工智能任务中建立错误率的限制通常是通过类似代理的人工标记或适合业务用例的主题变化来完成的。变化可以包括使用该主题的专家、一组人或一组专家来标记数据。建立这个限制是很有价值的,值得花费人力/专家的帮助。首先,它建立了可能达到的最佳结果,在某些情况下,可能不足以满足业务用例(在这种情况下,需要重新考虑问题的表述)。第二,它告诉我们当前的模型离可实现的最佳结果有多远。

用标准选择建立基线

开始建模过程的最佳位置是具有架构和算法的标准选择(基于文献或部分经验)的基线模型——例如,对图像使用卷积神经网络(CNN ),对序列使用长短期记忆(LSTM)网络。(这两个主题将在接下来的章节中讨论。)使用校正线性单元(ReLUs)作为激活单元和批量随机梯度下降(SGD)也是很好的选择。基本上,基线模型建立了一个稻草人,基于对缺点的分析进行改进。

构建自动化的端到端管道

确定基线模型后,建立端到端的全自动管道至关重要,这包括在训练集上训练模型,在参数调整集上进行预测,以及在两个集上计算指标。自动化是非常重要的,因为它使从业者能够通过调整模型架构和超参数来快速迭代新模型。

可视化流程编排

在构建端到端管道时,加入流程编排来可视化激活直方图、梯度、训练和验证集的指标等也是一个好主意。在调试意外行为时,模型训练、权重和性能的可见性非常有用。关键点是首先要为可见性构建自动化和流程编排。这样以后会节省很多时间和精力。

过拟合和欠拟合分析

模型改进的迭代周期的理想目标是开发一个模型,在该模型中,训练集和验证集的性能几乎等于已建立的性能限制(贝叶斯误差的代理)。图 5-5 说明了模型改进过程的最终目的。然而,在迭代开发新模型时,从业者将会遇到欠拟合和过拟合。欠拟合发生在模型在训练集和验证集上的性能几乎相等,但性能低于期望水平的时候。这是一个开发不良的模型的结果,其中的参数没有适当地捕获训练数据中的模式。另一方面,过拟合发生在模型在验证集上的性能显著低于其在训练集上的性能时。这是一个模型的直接结果,该模型已经学习了太多复杂的模式,这些模式在理想情况下应该被视为噪声。这种模型(将数据中的噪声作为有效模式)在训练(可见)数据上表现最佳,但在不可见数据上表现不佳。欠拟合和过拟合并不相互排斥。在模型拟合不足的情况下,我们更正式地将这种情况定义为具有高偏差的模型。类似地,当一个已经从噪声中学习了几个复杂模式的模型在看不见的数据上提供高度不一致的性能时,我们说该模型具有高方差。理想情况下,我们需要一个低偏差和低方差的模型。

检测模型是过拟合还是欠拟合是训练新模型后的第一步。在欠拟合的情况下,关键步骤是增加模型的有效容量,这通常通过修改架构(增加层、宽度等)来完成。在过度拟合的情况下,关键的步骤是正则化方法(本章后面会讲到)或增加数据集的大小。一个重要的可视化是学习曲线,它在 Y 轴上绘制性能指标,在 x 轴上绘制模型可用的训练数据。这对于确定投资获取更多标签数据是否有意义非常有用。

img/478491_2_En_5_Fig5_HTML.jpg

图 5-5

过度拟合和欠拟合

超参数调谐

调整模型的超参数(例如学习率或动量)可以通过网格搜索(其中网格是在一小组值上定义的)或通过随机搜索(其中超参数的值是从用户定义的分布中随机抽取的)手动完成。

在网格搜索中,从业者必须为网络中的每个超参数创建一个潜在值的小子集(因为计算资源是有限的)。训练过程基本上循环通过每个可能的组合,并且具有最佳性能的超参数组合是最终选择。使用网格搜索,有可能不具有超参数的最佳可能组合,因为如果大量选择被添加到网格中,排列被限制到所提供的网格或者在计算上非常昂贵。

随机搜索通常更适合超参数调整。通过随机搜索,模型获得超参数最佳组合的可能性较高,但组合数量相对较少(尽管不能保证)。

调整超参数通常是迭代的和实验性的。

模型容量

让我们简单回顾一下模型容量、过拟合和欠拟合的概念。我们将使用之前拟合回归模型的例子(参见第一章)。

我们有格式为 D = {( x 1y 1 ),( x 2y 2 ),…(xny n 我们的任务是生成一个计算程序,实现函数f*:xy。 我们用看不见的数据的均方根误差(RMSE)来衡量这个任务的性能,如下:*

E\left(f,D,U\right)={\left(\frac{\sum_{\left({x}_i,{y}_i\right)\in U}\ {\left({y}_i-f\left({x}_i\right)\right)}²\ }{\mid U\mid}\right)}^{\frac{1}{2}}.

给定一个形式为 D = {( x 1y 1 ),( x 2y 2 ),…(xny n 我们使用最小二乘模型,其形式为 y = βx ,其中 β 是使{\left\Vert X\beta -y\right\Vert}_2²最小化的向量。 这里, X 是一个矩阵,其中每一行是一个 xβ 的值可以用封闭形式β*=(XTX)—1XTy导出。*

我们可以把 x 变换成一个值的向量[ x 0x 1x 2 ]。也就是说,如果 x = 2,就转化为【1,2,4】。在这个变换之后,我们可以使用前面的公式生成最小二乘模型 β 。在引擎盖下,我们用一个二阶多项式(次数= 2)方程来逼近给定的数据,最小二乘算法只是简单地曲线拟合或生成每个x0、x1、x2 的系数。

同样,我们可以用最小二乘算法生成另一个模型,但是我们将把 x 变换为[ x 0x 1x 2x 3x 4x 5 也就是说,我们用次数= 8 的多项式来逼近给定的数据。通过增加多项式的次数,我们可以拟合任意数据。很容易看出,如果我们有 n 个数据点,一个次数为 n 的多项式可以完美地拟合这些数据。也很容易看出,这样的模型只是简单地记忆数据。我们可以使用这个例子来开发模型容量、过度拟合和欠拟合的透视图。我们用来拟合数据的多项式的次数基本上代表了模型的能力。度数越大,模型的容量越高。

让我们假设数据是使用带有一些噪声的 5 次多项式生成的。另外,请注意,在拟合数据时,我们对生成数据的过程一无所知。我们必须制作一个最符合数据的模型。本质上,我们不知道有多少数据是模式,有多少数据是噪声

在这样的数据集上,如果我们使用具有足够高容量的模型(多项式的次数大于 5,在最坏的情况下等于数据点的数量),当在训练数据上评估时,我们可以获得完美的模型;然而,这个模型在看不见的数据上表现很差,因为它本质上符合噪声。这太合身了。如果我们使用低容量(小于 5)的模型,它将既不适合训练数据也不适合看不见的数据。这是不合适的。

正则化模型

从前面的例子中可以很容易地看出,在拟合模型时,一个中心问题是准确地获得模型的容量,以便既不过度拟合也不欠拟合数据。规则化可以简单地看作是对模型(或其训练过程)的任何修改,旨在通过系统地限制模型的能力来改善未知数据的误差(以训练数据的误差为代价)。这种系统地限制或调节模型能力的过程是由未用于训练的一部分标记数据来引导的。这些数据通常被称为验证集

在我们运行的例子中,最小二乘法的正则化版本采用的形式是 y = βx ,其中 β 是使![Xβy22+λβ22{\left\Vert X\beta -y\right\Vert}_2²+\lambda {\left\Vert \beta \right\Vert}_2²最小化的向量, λ 是控制复杂度的用户定义参数。这里,通过引入术语\lambda {\left\Vert \beta \right\Vert}_2²,我们惩罚了具有额外容量的模型。要了解为什么会出现这种情况,请考虑使用 10 次多项式拟合最小二乘模型,但向量 β 中的值有 8 个零和 2 个非零值。与此相反,考虑向量 β 中的所有值都不为零的情况。出于所有实际目的,前一个模型是一个 degree = 2 并且具有较低值\lambda {\left\Vert \beta \right\Vert}_2²的模型。 λ 项允许我们平衡训练数据的准确性和模型的复杂性。 λ 的较低值意味着型号容量较低。

一个自然的问题是,为什么我们不简单地使用验证集作为指导,并增加前面例子中多项式的次数。既然多项式的次数代表了模型的容量,为什么我们不能用它来调整模型的容量呢?为什么我们需要在模型中引入变化({\left\Vert X\beta -y\right\Vert}_2²+\lambda {\left\Vert \beta \right\Vert}_2²而不是之前的{\left\Vert X\beta -y\right\Vert}_2²)。答案是我们想要系统地限制模型的容量,我们需要一个细粒度的控制。通过改变模型的程度来改变模型容量是非常粗粒度的、离散的旋钮,而改变 λ 是非常细粒度的。

提前停止

深度学习中最简单的正则化技术之一就是提前停止。给定一个训练集和一个验证集以及一个有足够容量的网络,我们观察到随着训练步数的增加,首先训练集和验证集的误差都减小,然后训练集的误差继续减小,而验证的误差增加(见图 5-6 )。

早期停止的关键思想是跟踪在验证集上给出最佳性能的模型参数/权重,然后在这个验证集上迄今的最佳性能在预定义数量的训练步骤上没有改善之后停止训练。

img/478491_2_En_5_Fig6_HTML.jpg

图 5-6

提前停止

通过限制模型的参数/权重值,早期停止起到了正则化的作用(见图 5-7 )。提前停止限制 w 到起始值附近的一个邻域内(在w0 附近)。所以,如果我们停在 w s 处,ws+1的值是不可能的。这实质上限制了模型的容量。

早期停止是非侵入性的,因为它不需要对模型做任何改变。它也很便宜,因为它只需要存储模型的参数(这是迄今为止验证集上最好的)。它也可以很容易地与其他正则化技术相结合。

img/478491_2_En_5_Fig7_HTML.jpg

图 5-7

提前停止限制 w

标准惩罚

范数惩罚是深度学习(以及一般的机器学习)中一种常见的正则化形式。这个想法仅仅是在神经网络的损失函数中增加一项 r ( θ )(参见第三章),其中 r 通常表示 L 1 范数或者 L 2 范数,而 θ 表示网络的参数/权重。这样,正则化的损失函数就变成了l(fNN(xθ ),y+α**r(θ,而不仅仅是 l ( f )注意, α 项是正则化参数。

Note

一般来说,一个 L p 定额定义为‖xp=(σI|xI|p)1/p据此, L 1 定额定义为‖x1=(σI|xI|11/1I同理, L 2 定额定义为‖x2=(σI||I|2)**

让我们更深入地研究正则化损失函数l(fNN(xθ ),y)+α**r(θ)。应注意以下几点:

  1. 由于我们试图最小化总损失函数l(fNN(xθy)+α**r(θ),我们试图减少l(f)

  2. 接下来对于两组参数,θa和θb,如果l(fNN(xθ ay θ b ), y ),那么优化算法就会选择 θ a 如果r(θa<r(θ

  3. 因此,正则项的作用是将优化导向降低 r ( θ )的 θ 方向。

  4. 很容易看出,当 r 对应于L1 正则化时,较低的 r ( θ )值将导致更稀疏的 θ ,从而降低有效容量。

  5. 很容易看出,当 r 对应于L2 正则化时, r ( θ )的较低值将导致 θ 更接近于 0,从而降低有效容量(见图 5-8 )。

  6. α 项用来控制我们对l(fNN(xθ ), y )对 r ( θ )的重视程度。较高的值 α 意味着更加重视正则化。

必须注意,范数惩罚应用于权重向量,而不是偏差项。背后的原因是,任何正则化都是过度拟合和欠拟合之间的权衡,正则化偏差项会由于太多的欠拟合而导致糟糕的权衡。在训练深度学习网络时,不同的层可以使用不同的值 α ,并且通过使用验证集作为指导的实验来确定合适的值 α

img/478491_2_En_5_Fig8_HTML.jpg

图 5-8

L 2 范数导致θ更接近于零。θ a 由于正则化而被优化算法选取;没有它,θ b 将被选取

拒绝传统社会的人

Dropout 本质上是模型集合/平均的计算廉价替代方案。让我们首先考虑模型集合/平均的关键概念。虽然具有足够容量的单个模型可能会过度拟合,但如果我们对多个模型(根据数据子集、不同权重初始化或不同超参数进行训练)的预测进行平均或多数投票,我们就可以解决过度拟合问题。模型集成/平均是一种非常有用的正则化形式,可以帮助我们处理过拟合问题。然而,考虑到我们必须训练多个模型并对多个模型进行预测(然后通过投票或平均将它们组合起来),这在计算上是相当昂贵的。对于具有多层的深度学习模型,这种计算开销特别高。辍学提供了一个廉价的选择。

dropout 的关键思想是在以概率 p 训练网络时随机丢弃单元及其连接,然后在预测时将学习到的权重乘以 p (见图 5-9 )。让我们用数学表达式的形式来精确地表达这个想法。一个标准的神经网络层可以表示为y=f(w**x+b,其中 y 为输出, x 为输入, f 为激活函数, wb 分别为权向量和偏置项。训练时的一个漏层可以表示为y=f(w(xr)+b,其中 r ~ 伯努利 ( p ),符号⨀表示两个向量的逐点相乘(如果a= 在预测时,漏层可以表示为y=f(pwx)+b)。

很容易看出,dropout 层在训练的同时,实际上训练了多个网络,至于每一个不同的 r ,我们都有一个不同的网络。很容易看出,在预测时间,我们对多个网络进行平均,如y=f(pwx)+b)。

在使用批量随机梯度进行辍学训练时,在整个批次中使用单一值 r 。在相关文献中, p 的推荐值对于输入单元为 0.8,对于隐藏单元为 0.5。发现对丢失有用的范数正则化是最大范数正则化,其中 w 被约束为‖w2<c,其中 c 是用户定义的参数。

img/478491_2_En_5_Fig9_HTML.jpg

图 5-9

拒绝传统社会的人

PyTorch 中的实际实现

现在,我们将通过一个实际的例子来探讨我们到目前为止已经讨论过的主题。出于本练习的目的,我们将使用托管在 https://www.kaggle.com/janiobachmann/bank-marketing-dataset 的银行电话营销数据集。原始数据集来自 UCI 机器学习知识库,由[Moro et al .,2014]提供。与原始数据集相比,Kaggle 上托管的子集是一个平衡的数据集(正样本和负样本的数量相似),并且使练习的目的更加容易。

到目前为止,我们已经探索了使用 Python 制作的玩具数据集,因此我们几乎没有探索在建立深度学习模型之前必不可少的数据处理和数据工程的想法。这适用于所有形式的数据可视化——表格、图像、文本、音频/视频/语音等。在本练习中,我们将了解一些基本的数据处理步骤。尽管大量的数据处理超出了本书的范围,但本文的目的是让您了解现实生活用例可能需要的处理类型。

让我们开始吧。在下载前述数据集之前,您首先需要在 www.kaggle.com 注册并创建一个帐户。在清单 5-1 中,我们为我们的练习导入了基本的 Python 包。

#Import required libraries
import torch.nn as nn
import torch as tch
import numpy as np, pandas as pd
from sklearn.metrics import confusion_matrix, accuracy_score
from sklearn.metrics import precision_score, recall_score,roc_curve, auc, roc_auc_score
from sklearn.model_selection import train_test_split
from sklearn.utils import shuffle
import matplotlib.pyplot as plt

Listing 5-1Importing the Required Libraries

Sklearn 是 Python 中的一个机器学习库,它提供了一个全面的算法、度量、数据处理工具和其他实用函数的列表。我们使用 sklearn 中的指标模块来获得方便的函数,这些函数有助于计算模型性能——精度、召回率、准确度等指标。类似地,Pandas 是一个很棒的 Python 包,它提供了处理、操作和探索表格数据帧的综合方法。在我们的练习中,我们将使用 Pandas 来读取和探索数据集,并利用 Pandas 中的一些功能来定制数据集以满足我们在 PyTorch 中的需求。清单 5-2 展示了使用 Pandas 将数据加载到内存中。

#Load data into memory using pandas
df = pd.read_csv("/Users/Downloads/dataset.csv")
print("DF Shape:",df.shape)
df.head()

Out[]
DF Shape: (11162, 17)

Listing 5-2Loading Data into Memory

img/478491_2_En_5_Figa_HTML.jpg

在 Jupyter 笔记本上使用 Pandas 提供了一种迭代探索数据的优雅方式。前面的输出是df.head()命令的结果,它打印数据集的前五行;df.shape命令将数据集的形状表示为[rows x columns]。

在这个数据集中,我们获得了银行电话营销活动的详细信息。该数据集捕获目标客户的详细信息、关于之前和当前营销电话的一些详细信息,以及成功结果存款。客户属性包括年龄工作、婚姻状况(婚姻)、学历、是否有拖欠付款(违约)、当前银行余额(余额)、住房贷款和个人贷款指标*。活动属性包括联系类型(联系)、联系时间(日/月)和持续时间(持续时间)、代理执行的联系次数(活动)、前一次联系的天数(p 天)、前一次联系次数(前一次)和前一次结果( poutcome )。*

有关数据集中属性的详细说明,请访问 https://archive.ics.uci.edu/ml/datasets/Bank+Marketin g

我们的目标是建立一个深度学习模型,对给定客户和活动组合的结果(存款)进行正确分类。让我们首先看看目标列在数据集中的分布。清单 5-3 展示了探索目标值的分布。

print("Distribution of Target Values in Dataset -")
df.deposit.value_counts()

Out[]:
Distribution of Target Values in Dataset -
no     5873
yes    5289
Name: deposit, dtype: int64

Listing 5-3Distributing the Target Values

我们可以看到,在我们的数据集中,yesno之间有大致相似的分布。清单 5-4 探究了数据集中空值的分布。

#Check if we have 'na' values within the dataset
df.isna().sum()

Out[]:
age          0
job          0
marital      0
education    0
default      0
balance      0
housing      0
loan         0
contact      0
day          0
month        0
duration     0
campaign     0
pdays        0
previous     0
poutcome     0
deposit      0
dtype: int64

Listing 5-4Distributing the NA (Null) Values in the Dataset

数据集没有任何 NA 值或缺失值。在大多数现实生活的数据集中,这可能不成立。研究人员和数据工程师花费大量时间处理缺失值或异常值。以下是您应该独立试验的附加检查:

  • 检查异常值。

    • 确定处理数据中异常值的策略。
      • 平均输入。

      • 用模式输入。

      • 用中位数输入。

      • 使用其他先进技术(基于聚类的回归插补技术来处理值)。

  • 检查缺少的值。

    • 确定处理缺失价值的策略。

    • 删除记录(如果缺失记录的数量< = 3%)。

    • 用类似于离群值的方法估算记录。

接下来,让我们探索数据集中不同的数据类型。深度学习模型只理解数字。更具体地说,PyTorch 只处理 32 位浮点数。我们需要将数据集转换成适合 PyTorch 使用的形式。清单 5-5 探究了不同数据类型的分布。

#Check the distinct datatypes within the dataset
df.dtypes.value_counts()
Out[]:

int64     11
object     6
dtype: int64

Listing 5-5Distributing the Distinct Datatypes

我们有六个基于 object (string)数据类型的列,我们需要在构建模型之前将它们转换成数字标志。我们将把分类列转换成独热编码形式,其中每个类别值都表示为一个二进制标志。但是,在此之前,让我们手动将具有 yes/no 二进制类别的列转换为一个列,并利用一个基于 Pandas 的函数来自动转换剩余的分类列集。清单 5-6 演示了从数据集中提取分类列。

#Extract categorical columns from dataset
categorical_columns = df.select_dtypes(include="object").columns
print("Categorical cols:",list(categorical_columns))

#For each categorical column if values in (Yes/No) convert into a 1/0 Flag
for col in categorical_columns:
    if df[col].nunique() == 2:
        df[col] = np.where(df[col]=="yes",1,0)

df.head()

Listing 5-6Extracting Categorical Columns from the Dataset

img/478491_2_En_5_Figb_HTML.jpg

我们可以看到,我们的目标列存款和少数其他列,包括装载默认住房,已经被转换为二进制标志(手动)。对于具有非二进制分类值的剩余列集,我们可以利用 Pandas get_dummies函数来自动处理它们。清单 5-7 对数据集中的分类变量进行一次性编码。

#For the remaining cateogrical variables;
#create one-hot encoded version of the dataset
new_df = pd.get_dummies(df)

#Define target and predictors for the model
target = "deposit"
predictors = set(new_df.columns) - set([target])
print("new_df.shape:",new_df.shape)
new_df[predictors].head()

Out[]:

new_df.shape: (11162, 49)

Listing 5-7One-Hot Encoding for the Remaining Non-Binary Categorical Variables

img/478491_2_En_5_Figc_HTML.jpg

我们现在已经定义了一个包含所有独立预测值列名的预测值列表,以及一个包含我们的 y(即存款列名)的目标。

Pandas 中的get_dummies函数将new_df数据帧中的所有分类列作为一个热编码形式进行处理。清单 5-7 的上述输出将列的视图限制为前几个;我们可以看到联系人现在转化为联系人 _ 未知联系人 _ 蜂窝等。数据集现在只有数字列。

最后,在设计我们的神经网络之前,我们需要将所有列转换为 float32 数据类型,并拆分为训练和验证数据集,然后转换为 PyTorch 张量。清单 5-8 为训练和验证准备数据集。

#Convert all datatypes within pandas dataframe to Float32
#(Compatibility with PyTorch tensors)
new_df = new_df.astype(np.float32)

#Split dataset into Train/Test [80:20]
X_train,x_test, Y_train,y_test = train_test_split(new_df[predictors],new_df[target],test_size= 0.2)

#Convert Pandas dataframe, first to numpy and then to Torch Tensors
X_train = tch.from_numpy(X_train.values)
x_test  = tch.from_numpy(x_test.values)
Y_train = tch.from_numpy(Y_train.values).reshape(-1,1)
y_test  = tch.from_numpy(y_test.values).reshape(-1,1)

#Print the dataset size to verify
print("X_train.shape:",X_train.shape)
print("x_test.shape:",x_test.shape)
print("Y_train.shape:",Y_train.shape)
print("y_test.shape:",y_test.shape)

Out[]:
X_train.shape: torch.Size([8929, 48])
x_test.shape: torch.Size([2233, 48])
Y_train.shape: torch.Size([8929, 1])
y_test.shape: torch.Size([2233, 1])

Listing 5-8Preparing the Dataset for Training and Validation

我们现在已经为我们的深度学习实验准备好了数据集。在设计我们的网络之前,让我们先准备一些可以在实验中重复使用的基本构件。清单 5-9 展示了在 PyTorch 中训练模型的样板代码。

Note

在本书的练习中,我们总是将数据集分为 80%的训练和 20%的验证(与前面讨论的将其分为训练、验证和测试相反)。在真实的生产实验中,我们建议读者拥有一个单独的测试数据集,可以在投入生产系统之前完成所需的检查。

#Define function to train the network
def train_network(model,optimizer,loss_function,num_epochs,batch_size,X_train,Y_train,lambda_L1=0.0):
    loss_across_epochs = []

    for epoch in range(num_epochs):
        train_loss= 0.0

        #Explicitly start model training
        model.train()

        for i in range(0,X_train.shape[0],batch_size):

            #Extract train batch from X and Y
            input_data = X_train[i:min(X_train.shape[0],i+batch_size)]
            labels = Y_train[i:min(X_train.shape[0],i+batch_size)]

            #set the gradients to zero before starting to do backpropragation
            optimizer.zero_grad()

            #Forward pass
            output_data  = model(input_data)

            #Caculate loss
            loss = loss_function(output_data, labels)
            L1_loss = 0

            #Compute L1 penalty to be added with loss
            for p in model.parameters():
                L1_loss = L1_loss + p.abs().sum()

            #Add L1 penalty to loss
            loss = loss + lambda_L1 * L1_loss

            #Backpropogate
            loss.backward()

            #Update weights
            optimizer.step()

            train_loss += loss.item() * input_data.size(0)

        loss_across_epochs.append(train_loss/X_train.size(0))
        if epoch%500 == 0:
            print("Epoch: {} - Loss:{:.4f}".format(epoch,train_loss/X_train.size(0) ))

    return(loss_across_epochs)

Listing 5-9Defining the Function to Train the Model

前面的函数在定义数量的时期内分批循环,并训练我们的神经网络。你已经熟悉这个功能了(参见第三章);当我们使用 L1 正则化时,该函数唯一新增加的是 L1 罚函数的计算。lambda_L1变量是一个超参数,我们可以调整它来控制 L1 正则化的效果。

现在,让我们定义一个函数,该函数可用于绘制各时期的损失、训练和验证数据集的 ROC 曲线,以及评估模型的重要指标。因为这是一个分类用例,我们将使用之前从 sklearn 导入的函数来计算准确度、精确度和召回率。清单 5-10 展示了评估模型的样板代码。

#Define function for evaluating NN
def evaluate_model(model,x_test,y_test,X_train,Y_train,loss_list):

    model.eval() #Explicitly set to evaluate mode

    #Predict on Train and Validation Datasets
    y_test_prob = model(x_test)
    y_test_pred =np.where(y_test_prob>0.5,1,0)
    Y_train_prob = model(X_train)
    Y_train_pred =np.where(Y_train_prob>0.5,1,0)

    #Compute Training and Validation Metrics
    print("\n Model Performance -")
    print("Training Accuracy-",round(accuracy_score(Y_train,Y_train_pred),3))
    print("Training Precision-",round(precision_score(Y_train,Y_train_pred),3))
    print("Training Recall-",round(recall_score(Y_train,Y_train_pred),3))
    print("Training ROCAUC", round(roc_auc_score(Y_train
                                   ,Y_train_prob.detach().numpy()),3))

    print("Validation Accuracy-",round(accuracy_score(y_test,y_test_pred),3))
    print("Validation Precision-",round(precision_score(y_test,y_test_pred),3))
    print("Validation Recall-",round(recall_score(y_test,y_test_pred),3))
    print("Validation ROCAUC", round(roc_auc_score(y_test
                                     ,y_test_prob.detach().numpy()),3))
    print("\n")

    #Plot the Loss curve and ROC Curve

    plt.figure(figsize=(20,5))
    plt.subplot(1, 2, 1)
    plt.plot(loss_list)
    plt.title('Loss across epochs')
    plt.ylabel('Loss')
    plt.xlabel('Epochs')

    plt.subplot(1, 2, 2)

    #Validation
    fpr_v, tpr_v, _ = roc_curve(y_test, y_test_prob.detach().numpy())
    roc_auc_v = auc(fpr_v, tpr_v)

    #Training
    fpr_t, tpr_t, _ = roc_curve(Y_train, Y_train_prob.detach().numpy())
    roc_auc_t = auc(fpr_t, tpr_t)

    plt.title('Receiver Operating Characteristic:Validation')
    plt.plot(fpr_v, tpr_v, 'b', label = 'Validation AUC = %0.2f' % roc_auc_v)
    plt.plot(fpr_t, tpr_t, 'r', label = 'Training AUC = %0.2f' % roc_auc_t)
    plt.legend(loc = 'lower right')
    plt.plot([0, 1], [0, 1],'r--')
    plt.xlim([0, 1])
    plt.ylim([0, 1])
    plt.ylabel('True Positive Rate')
    plt.xlabel('False Positive Rate')

    plt.show()

Listing 5-10Defining the Function to Evaluate the Model Performance

最后,在所有必要的构建模块就绪后,是时候定义我们的神经网络并利用前面的帮助器功能来训练和评估深度学习模型了。我们将从没有正则化器的普通神经网络开始;稍后,我们将通过添加 L1、L2 和辍学生来研究效果,并选择最佳者进行预测。清单 5-11 定义了我们神经网络的结构。

#Define Neural Network

class NeuralNetwork(nn.Module):

    def __init__(self):
        super().__init__()
        tch.manual_seed(2020)
        self.fc1 = nn.Linear(48, 96)
        self.fc2 = nn.Linear(96, 192)
        self.fc3 = nn.Linear(192, 384)
        self.out = nn.Linear(384, 1)
        self.relu = nn.ReLU()
        self.final = nn.Sigmoid()

    def forward(self, x):
        op = self.fc1(x)
        op = self.relu(op)
        op = self.fc2(op)
        op = self.relu(op)
        op = self.fc3(op)
        op = self.relu(op)
        op = self.out(op)
        y = self.final(op)
        return y

#Define training variables

num_epochs = 500
batch_size= 128
loss_function = nn.BCELoss()  #Binary Crosss Entropy Loss

#Hyperparameters
weight_decay=0.0 #set to 0; no L2 Regularizer; passed into the Optimizer
lambda_L1=0.0    #Set to 0; no L1 reg; manually added in loss (train_network)

#Create a model instance
model = NeuralNetwork()

#Define optimizer
adam_optimizer = tch.optim.Adam(model.parameters(), lr= 0.001,weight_decay=weight_decay)

#Train model
adam_loss = train_network(model,adam_optimizer,loss_function
                                    ,num_epochs,batch_size,X_train,Y_train,lambda_
                                         L1=0.0)

#Evaluate model
evaluate_model(model,x_test,y_test,X_train,Y_train,adam_loss)

Out[]:

Epoch: 0 - Loss:1.7305
Epoch: 100 - Loss:0.3219
Epoch: 200 - Loss:0.2470
Epoch: 300 - Loss:0.1910
Epoch: 400 - Loss:0.1431

Model Performance -
Training Accuracy- 0.922
Training Precision- 0.89
Training Recall- 0.957
Training ROCAUC 0.981

Validation Accuracy- 0.801
Validation Precision- 0.757
Validation Recall- 0.827
Validation ROCAUC 0.869

Listing 5-11Defining the Structure of the Neural Network

img/478491_2_En_5_Figd_HTML.jpg

我们将历元数定义为 500,批量大小定义为 128,同时保留weight_decay=0lambda_L1=0.0(这基本上消除了 L1 和 L2 正则化子的影响;我们将很快试验这些值)。正如在第三章中,我们为我们的网络使用了带有BCELoss()的 Adam 优化器。我们的网络有三个隐藏层,分别有 96、192 和 384 个神经元。我们可以在神经网络架构中使用不同大小的单元。

如果我们仔细看看训练和验证数据集之间的结果,我们可以看到一个巨大的差距。有助于捕捉这种差异的单一指标是 ROC AUC(曲线下面积);我们的 AUC 为 98%,而培训和验证的 AUC 为 87%。这个差距是巨大的。本质上,我们面临着过度拟合的问题。为了克服过度拟合,我们需要添加正则化子,这将增加模型损失的惩罚,提示模型学习更简单的模式。理想情况下,我们希望在培训和验证之间看到相似的结果。

先说 L1 正则化。我们在train_network()函数中添加了一小段代码,用于计算参数绝对值的总和,并添加到乘以 Lambda(超参数)后计算的损失中。为了启用 L1 正则化,我们需要向lambda_L1变量传递一个非零值。清单 5-12 展示了网络的 L1 正则化。

#L1 Regularization
num_epochs = 500
batch_size= 128

weight_decay=0.0   #Set to 0; no L2 reg
lambda_L1 = 0.0001 #Enables L1 Regularization

model = NeuralNetwork()
loss_function = nn.BCELoss()  #Binary Crosss Entropy Loss

adam_optimizer = tch.optim.Adam(model.parameters(),lr= 0.001 ,weight_decay=weight_decay)

#Define hyperparater for L1 Regularization

#Train network
adam_loss = train_network(model,adam_optimizer,loss_function,num_epochs,batch_size,X_train,Y_train,lambda_L1=lambda_L1)

#Evaluate model
evaluate_model(model,x_test,y_test,X_train,Y_train,adam_loss)

Out[]:

Epoch: 0   - Loss:2.0634
Epoch: 100 – Loss:0.4042
Epoch: 200 – Loss:0.3852
Epoch: 300 – Loss:0.3668
Epoch: 400 – Loss:0.3616

Model Performance –
Training Accuracy- 0.84
Training Precision- 0.77
Training Recall- 0.949
Training ROCAUC 0.93

Validation Accuracy- 0.813
Validation Precision- 0.732
Validation Recall- 0.928
Validation ROCAUC 0.894

Listing 5-12L1 Regularization

img/478491_2_En_5_Fige_HTML.jpg

同样,让我们试试 L2 正则化。默认情况下,PyTorch 提供了一种直接通过优化器中的参数启用 L2 正则化的方法。在 Adam 优化中,我们可以使用weight_decay变量来添加它。

清单 5-13 展示了网络的 L2 正则化。

#L2 Regularization
num_epochs = 500
batch_size= 128
weight_decay=0.001 #Enables L2 Regularization
lambda_L1 = 0.00    #Set to 0; no L1 reg

model = NeuralNetwork()
loss_function = nn.BCELoss()  #Binary Crosss Entropy Loss

adam_optimizer = tch.optim.Adam(model.parameters(),lr= 0.001,weight_decay=weight_decay)

#Train Network
adam_loss = train_network(model,adam_optimizer,loss_function,num_epochs,batch_size,X_train,Y_train,lambda_L1=lambda_L1)

#Evaluate model
evaluate_model(model,x_test,y_test,X_train,Y_train,adam_loss)

Out[]:

Epoch: 0 – Loss:1.8140
Epoch: 100 – Loss:0.3927
Epoch: 200 – Loss:0.3658
Epoch: 300 – Loss:0.3604
Epoch: 400 – Loss:0.3414

Model Performance –
Training Accuracy- 0.862
Training Precision- 0.822
Training Recall- 0.909
Training ROCAUC 0.935

Validation Accuracy- 0.82
Validation Precision- 0.77
Validation Recall- 0.861
Validation ROCAUC 0.9

Listing 5-13L2 Regularization

img/478491_2_En_5_Figf_HTML.jpg

与 L1 类似,我们看到 L2 的结果比没有正规化要好一些。差距缩小,验证 AUC 增加了一小部分。

随着 L1 和 L2 正则化(分别),我们看到训练和验证性能之间的差距减少,以及减少过度拟合。我们现在对我们的用例有了有利的结果。在最终确定结果之前,让我们添加辍学层。清单 5-14 增加了一个丢弃层,在学习过程中随机丢弃 10%的输入神经元。我们将下降层添加到输入层和隐藏层。

#Define Network with Dropout Layers
class NeuralNetwork(nn.Module):
    #Adding dropout layers within Neural Network to reduce overfitting
    def __init__(self):
        super().__init__()
        tch.manual_seed(2020)
        self.fc1 = nn.Linear(48, 96)
        self.fc2 = nn.Linear(96, 192)
        self.fc3 = nn.Linear(192, 384)
        self.relu = nn.ReLU()
        self.out = nn.Linear(384, 1)
        self.final = nn.Sigmoid()
        self.drop = nn.Dropout(0.1)  #Dropout Layer

    def forward(self, x):
        op = self.drop(x)  #Dropout for input layer
        op = self.fc1(op)
        op = self.relu(op)
        op = self.drop(op) #Dropout for hidden layer 1
        op = self.fc2(op)
        op = self.relu(op)
        op = self.drop(op) #Dropout for hidden layer 2
        op = self.fc3(op)
        op = self.relu(op)
        op = self.drop(op) #Dropout for hidden layer 3
        op = self.out(op)
        y = self.final(op)
        return y

num_epochs = 500
batch_size= 128

weight_decay=0.0 #Set to 0; no L2 reg
lambda_L1 = 0.0  #Set to 0; no L1 reg

model = NeuralNetwork()
loss_function = nn.BCELoss()  #Binary Crosss Entropy Loss

adam_optimizer = tch.optim.Adam(model.parameters(),lr= 0.001
,weight_decay=weight_decay)
#Train model

adam_loss = train_network(model,adam_optimizer,loss_function,num_epochs
,batch_size,X_train,Y_train
,lambda_L1= lambda_L1)

#Evaluate model
evaluate_model(model,x_test,y_test,X_train,Y_train,adam_loss)

Out[]:

Epoch: 0 - Loss:1.9511
Epoch: 100 - Loss:0.4087
Epoch: 200 - Loss:0.3961
Epoch: 300 - Loss:0.3798
Epoch: 400 - Loss:0.3789

Model Performance -
Training Accuracy  - 0.816
Training Precision - 0.766
Training Recall    - 0.885
Training ROCAUC    - 0.899

Validation Accuracy  - 0.802
Validation Precision - 0.74
Validation Recall    - 0.867
Validation ROCAUC    - 0.882

Listing 5-14Dropout Regularization

img/478491_2_En_5_Figg_HTML.jpg

培训和验证绩效之间的差距已经缩小;我们可以在两个数据集上看到相似的性能。

最后,让我们结合所有三种类型的正则化,并研究对模型性能的影响。清单 5-15 展示了 L1、L2 和辍学正规化。

#Create a network with Dropout layer
class NeuralNetwork(nn.Module):
    def __init__(self):
        super().__init__()
        tch.manual_seed(2020)
        self.fc1 = nn.Linear(48, 96)
        self.fc2 = nn.Linear(96, 192)
        self.fc3 = nn.Linear(192, 384)
        self.relu = nn.ReLU()
        self.out = nn.Linear(384, 1)
        self.final = nn.Sigmoid()
        self.drop = nn.Dropout(0.1)  #Dropout Layer

    def forward(self, x):
        op = self.drop(x)  #Dropout for input layer
        op = self.fc1(op)
        op = self.relu(op)
        op = self.drop(op) #Dropout for hidden layer 1
        op = self.fc2(op)
        op = self.relu(op)
        op = self.drop(op) #Dropout for hidden layer 2
        op = self.fc3(op)
        op = self.relu(op)
        op = self.drop(op) #Dropout for hidden layer 3
        op = self.out(op)
        y = self.final(op)
        return y

num_epochs = 500

batch_size= 128

lambda_L1    = 0.0001  #Enabled L1
weight_decay =0.001    #Enabled L2

model = NeuralNetwork()
loss_function = nn.BCELoss()

adam_optimizer = tch.optim.Adam(model.parameters(),lr= 0.001 ,weight_decay=weight_decay)

adam_loss = train_network(model,adam_optimizer,loss_function ,num_epochs,batch_size,X_train,Y_train,lambda_L1=lambda_L1)

evaluate_model(model,x_test,y_test,X_train,Y_train,adam_loss)

Epoch: 0 - Loss:2.2951
Epoch: 100 - Loss:0.4887
Epoch: 200 - Loss:0.4865
Epoch: 300 - Loss:0.4617
Epoch: 400 - Loss:0.4647

Model Performance -
Training Accuracy- 0.794
Training Precision- 0.764
Training Recall- 0.826
Training ROCAUC 0.873

Validation Accuracy- 0.807
Validation Precision- 0.758
Validation Recall- 0.843
Validation ROCAUC 0.884

Listing 5-15L1, L2, and Dropout Regularization

img/478491_2_En_5_Figh_HTML.jpg

总的来说,我们在上述三个场景中看到了相似的性能。在一个理想的实验中,没有定义的基准,我们可以使用它来选择哪种正则化类型会更好。我们需要试验不同类型的正则化以及超参数的不同值:lambda 正则化和超参数值(0.0001,0.001,0.005,0.01),dropout layer 值(0.05,0.1,0.2,0.3 等)。).有了所有实验的结果,我们将更加了解哪种类型的正则化最适合数据。

解读深度学习的业务成果

结果还算不错。我们看到培训和验证性能之间的差距很小。(参考 ROC 图中红色和蓝色线之间的间隙。)

总的来说,我们在验证数据集上有 80%的准确率,精确度为 76%,召回率为 84%。这些结果非常令人鼓舞。在对营销活动结果做出的 10 个“是”的预测中,我们的正确率为 7.6 倍,同时涵盖了 84%会积极响应该活动的所有客户。

让我们花点时间来更好地理解这些结果。我们从一个大约有 50-50%正面和负面结果的数据集开始。考虑到业务问题,这将转化为(考虑到营销团队的努力)在锁定 50%的客户方面的巨大努力损失,并产生负面结果。假设我们总共有 100 个客户(因此,50 个正面结果和 50 个负面结果)。针对每个客户,我们有 100 个工作单位(针对 100 次呼叫),最后我们有 50 次成功存款。

然而,凭借大约 76%的准确率和 84%的召回率,我们有了一个经过筛选的客户列表,可以轻松地锁定这些客户。

因此,我们现在的目标不是所有的 100 个客户,而是我们预测正确的客户,这也包括误报。如果我们总共有 50 个正面结果,那么前面的模型具有 84%的召回率和 76%的精确度,我们将预测(x * 0.84)/0.76(x = 50)。因此,我们总共有约 55 个阳性预测,其中 12 个为假阳性,43 个为真阳性(每 100 个预测)。

与前面的场景相比,对于 100 次尝试,我们有 50 次成功存款。在深度学习模型中,对于 55 次尝试(结果预测为 1),我们有 43 次成功存款。

尽管在活动中损失了七笔正存款,但我们已经大大减少了达到几乎相同的成功标准所需的工作量。这些指标可以根据业务需求进一步调整,以适应更有利的结果。

Note

我们还没有涵盖类似的(详细的)回归用例。鼓励读者独立试验回归用例,其中目标变量是连续的。尽管损失函数的选择、输出层的激活和性能度量需要基于用例,但是问题的方法和公式保持不变。我们推荐尝试的一个样本回归数据集是桑坦德集团的价值预测挑战( https://www.kaggle.com/c/santander-value-prediction-challenge/ )。损失函数的一个好选择是 RMSE;输出层的激活将是线性的;性能度量选择可以是 RMSE 或 MSE。

摘要

本章讲述了模型训练的过程。我们还描述了一些关键步骤和分析,为了改进模型,应该系统地执行这些步骤和分析。我们还讨论了深度学习中常用的正则化技术,即规范惩罚和辍学。在文献中还发现了其他一些必须提及的高级/特定领域技术。到目前为止,我们已经使用一个玩具数据集和一个实际数据集,以及两者的结合和一个业务用例,介绍了前馈神经网络和深度学习的所有基本内容。您现在应该对制定用例、定义基准模型的相关度量、评估模型性能以及评估业务可行性有了更加直观的理解。在下一章中,我们将探索深度学习中最重要的主题之一——卷积神经网络——并拥抱计算机视觉领域。

六、卷积神经网络

卷积神经网络(CNN)本质上是一种采用卷积运算(而不是全连接层)作为其一层的神经网络。CNN 是一项令人难以置信的成功技术,它已经被应用于这样的问题,即在要进行预测的输入数据中具有已知的网格状拓扑,如时间序列(一维网格)或图像(二维网格)。CNN 将深度学习引入现代,解决了计算机视觉数字时代最关键的计算问题之一。随着 CNN 的普及,深度学习的研究热潮一直持续到今天。

本章简要介绍了 CNN 的核心概念,并探索了 PyTorch 中的一个简单示例来研究它们的实际实现。我们还将探索迁移学习,其中我们将利用之前训练过的网络作为我们的用例。

让我们从基础开始。

卷积运算

我们先来看看一维的卷积运算。给定一个输入 I ( t )和一个内核 K ( a ),卷积运算由

s(t)={\sum}_aI(a)\cdotp K\left(t-a\right)

给出

给定卷积运算的交换性,该运算的等价形式如下:

s(t)={\sum}_aI\left(t-a\right)\cdotp K(a)

此外,可以替换负号(翻转)来获得互相关,如下:

s(t)={\sum}_aI\left(t+a\right)\cdotp K(a)

深度学习文献和软件实现互换使用术语卷积互相关。运算的本质是,与输入相比,核是一组更短的数据点,当输入与核相似时,卷积运算的输出更高。图 6-1 和图 6-2 说明了这一关键思想。我们采用任意输入和任意核,并执行卷积运算。当内核与输入的特定部分相似时,获得最高值。

img/478491_2_En_6_Fig2_HTML.png

图 6-2

卷积运算—一维

img/478491_2_En_6_Fig1_HTML.png

图 6-1

卷积运算的简单概述

应注意以下几点:

  1. 输入是任意的大量数据点。

  2. 内核是一组在数量上小于输入的数据点。

  3. 从某种意义上说,卷积运算将内核滑过输入,并计算内核与输入部分的相似程度。

  4. 卷积运算在核与输入的一部分最相似的地方产生最高值。

卷积运算可以扩展到二维。给定一个输入 I ( mn )和一个内核 K ( ab ),卷积运算由

s(t)=\sum \limits_a\sum \limits_bI\left(a,b\right)\cdotp K\left(m-a,n-b\right)

给出

给定卷积运算的交换性,该运算的等价形式如下:

s(t)=\sum \limits_a\sum \limits_bI\left(m-a,n-b\right)\cdotp K\left(a,b\right)

此外,可以替换负号(翻转)来获得互相关,给出如下:

s(t)=\sum \limits_a\sum \limits_bI\left(m+a,n+b\right)\cdotp K\left(a,b\right)

图 6-3 以二维图示了卷积运算。请注意,这只是将卷积的思想扩展到二维。

img/478491_2_En_6_Fig3_HTML.png

图 6-3

卷积运算—二维

在介绍了卷积运算之后,我们现在可以更深入地研究 CNN 的关键组成部分,其中使用了卷积层而不是全连接层,这涉及到矩阵乘法。

一个全连通层可以描述为y=f(x**w),其中 x 为输入向量, y 为输出向量, w 为一组权重, f 为激活函数。相应地,一个卷积层可以描述为y=f(s(xw),其中 s 表示输入和权值之间的卷积运算。

现在让我们对比一下全连接层和卷积层。图 6-4 示意性地示出了全连接层,图 6-5 示意性地示出了卷积层。图 6-6 说明了卷积层中的参数共享以及全连接层中的参数共享缺失。应注意以下几点:

img/478491_2_En_6_Fig6_HTML.png

图 6-6

参数共享权重

img/478491_2_En_6_Fig5_HTML.png

图 6-5

卷积层中的稀疏相互作用

img/478491_2_En_6_Fig4_HTML.png

图 6-4

全连接层中的密集相互作用

  • 对于相同数量的输入和输出,全连接层比卷积层有更多的连接和相应的权重。

  • 与全连接层相比,卷积层中产生输出的输入之间的交互较少。这被称为稀疏相互作用

  • 假设内核比输入小得多,并且内核在输入上滑动,则参数/权重在卷积层上共享。因此,卷积层中的唯一参数/权重要少得多。

联营业务

现在让我们来看看合并运算,它几乎总是与卷积一起用于 CNN。池化操作背后的思想是,如果事实上已经发现了特征的确切位置,那么它就不是问题。它只是提供了平移不变性。例如,假设任务是学习识别照片中的人脸。还假设照片中的人脸是倾斜的(通常如此),我们有一个卷积层来检测眼睛。我们想从照片中眼睛的方向提取出它们的位置。汇集操作实现了这一点,并且是 CNN 的重要组成部分。

图 6-7 说明了二维输入的汇集操作。应注意以下几点:

img/478491_2_En_6_Fig7_HTML.png

图 6-7

汇集或二次抽样

  • 函数 f 通常是最大值运算(导致最大池化),但是也可以使用其他变型,例如平均值或L2 范数。

  • 对于二维输入,这是一个矩形部分。

  • 与输入相比,汇集产生的输出在维度上要小得多。

卷积检测器池构建模块

现在让我们来看看卷积检测器池模块,它可以被看作是 CNN 的一个构建模块,并看看我们前面介绍的所有操作是如何协同工作的。参见图 6-8 和图 6-9 。需要注意以下几点。

img/478491_2_En_6_Fig9_HTML.png

图 6-9

给出多个特征图的多个过滤器/内核

img/478491_2_En_6_Fig8_HTML.png

图 6-8

卷积,然后是检测器阶段和合并

  • 检测器级只是一个非线性激活函数。

  • 卷积、检测器和池操作按顺序应用,以将输入转换为输出。输出被称为特征图

  • 输出通常会传递到其他层(卷积层或全连接层)。

  • 多个卷积检测器池模块可以并行应用,消耗相同的输入并产生多个输出或特征图。

如果图像输入由三个通道组成,则对每个通道进行单独的卷积运算,然后在卷积后将输出相加(参见图 6-10 )。

img/478491_2_En_6_Fig10_HTML.png

图 6-10

多通道卷积

在介绍了 CNN 的所有组成元素之后,我们现在可以完整地看一个 CNN 的例子(见图 6-11 )。CNN 由两级卷积检测器池模块组成,每级都有多个滤波器/内核产生多个特征图。在这两个阶段之后,我们有一个产生输出的完全连接的层。一般而言,CNN 可能具有多级卷积检测器池模块(采用多个滤波器),通常后跟一个全连接层。

img/478491_2_En_6_Fig11_HTML.png

图 6-11

完整的 CNN 架构

除了这些基本结构,我们还将探讨一些与卷积层相关的其他主题。

进展

步幅可以定义为过滤器/内核移动的量。当讨论滤波器在输入图像上的滑动时,我们假设该移动只是在预期方向上的一个单位。然而,我们可以用我们选择的一些数字来控制滑动(尽管通常使用一个)。基于用例,我们可以选择一个更合适的数字。更大的步幅通常有助于减少计算、概括特征学习等。

填料

我们还看到,与输入图像的大小相比,应用卷积减小了特征图的大小。在应用大于 1x1 的过滤器并避免边界处的信息丢失之后,零填充是控制维度收缩的通用方法。

批量标准化

批量标准化是一种技术,通过标准化每个小批量的层输入来帮助训练非常深的神经网络。标准化输入有助于稳定学习过程,从而大大减少训练深度网络所需的训练次数。批量标准化层被添加在卷积层之后,并且通常是卷积运算的标准块的一部分。也就是说,卷积层、批量标准化层、激活和最大池操作在同一序列中的组合被定义为一个卷积单元。我们通常在 CNN 中添加几个这样的单元。

过滤器

过滤器类似于内核。在最近的实现(包括 PyTorch)和学术界,术语过滤器内核更常见。一般来说,对于卷积运算,我们使用大小为 3×3 和 5×5 的滤波器。早期的实现也支持 7×7 滤波器。

过滤深度

滤镜深度通常指输入图像中颜色通道数对应的深度。对于后面层中的过滤器,深度对应于前面层中过滤器的数量。对于具有三个颜色通道(即 R、G 和 B)的常规图像,我们使用深度为 3 的滤镜。

过滤器数量

过滤器充当特征提取器;因此,在网络的每个卷积块中有几个滤波器是很常见的。一个示例排列是一个卷积块,具有 32 个大小为 3×3(深度为 3)的滤波器,接着是激活/批量归一化和池化块,接着是另一个具有 64 个滤波器的块(现在深度为 32),依此类推。

总结 CNN 的主要经验

到目前为止,我们已经讨论了 CNN 背后的关键组成概念:卷积运算和池运算,以及它们如何结合使用。现在让我们后退一步,用这些构件来内化 CNN 背后的思想。

  • 首先要考虑的是 CNN 的容量。用卷积运算代替神经网络的至少一个完全连接的层的 CNN 比完全连接的网络具有更小的容量。也就是说,存在全连接网络能够模拟 CNN 不能模拟的数据集。因此,要注意的第一点是,CNN 通过限制容量并因此使训练有效来实现更多。

  • 要考虑的第二个想法是,学习驱动卷积运算的滤波器在某种意义上是表示学习。例如,已学习的过滤器可以学习检测边缘、形状等。这里要考虑的要点是,我们不是手动描述要从输入数据中提取的特征;相反,我们描述的是一个学会设计特性/表现的架构。

  • 要考虑的第三个想法是池操作引入的位置不变性。池操作将特征的位置与其被检测到的事实分开。检测直线的过滤器可能会在图像的任何部分检测到该特征,但池操作会选择检测到该特征的事实(最大池)。

  • 第四个想法是等级。一个 CNN 可以有多个卷积层和汇集层堆叠在一起,然后是一个完全连接的网络。这允许 CNN 建立一个概念层次,其中更抽象的概念基于更简单的概念(参见第一章)。

  • 最后一个想法是在一系列卷积层和汇集层的末端存在一个全连接层。一系列卷积层和汇集层生成特征,标准神经网络学习最终的分类/回归函数。将 CNN 的这一方面与传统的机器学习区分开来是很重要的。在传统的机器学习中,专家会手工设计特征,并将其输入神经网络。在 CNN 中,这些特征/表示是从数据中学习的。

使用 PyTorch 实现基本的 CNN

现代深度学习框架负责我们开发 CNN 所需的大量操作和构造。让我们用一个简单的例子来说明 PyTorch 如何用于定义、训练和评估 CNN。

我们将从 MNIST 的一个例子开始,那里有一组手写数字图像。我们的任务是将给定的图像分类为 0 到 9 之间的数字。

#Note

计算机视觉任务是非常计算密集型的,通常需要高端硬件来训练和评估大型鲁棒网络。我们探索的 MNIST 例子是一个微型数据集,读者应该很容易在商用硬件上重现。对于本章中更深入的例子,我们推荐一个免费的、基于网络的、支持 GPU 的计算实例,比如 Kaggle 或 Google Colab。这两个版本都提供了一个标准计算实例,具有大约 16GB RAM 和 16GB GPU 内存,每月配额。出于实验目的,这些都是很好的资源。对于更深入的实验,读者需要探索云(AWS/GCP/Azure)或定制硬件上的深度学习实例。

首先,从 https://www.kaggle.com/c/digit-recognizer/data 下载数据集。

我们将只使用提供了标签的训练数据集。训练数据集将进一步分为训练和验证。现在我们已经准备好了数据,让我们通过导入所需的包来开始实现(清单 6-1 )。

#pytorch utility imports
import torch
from torch.utils.data import DataLoader, TensorDataset

#neural net imports
import torch.nn as nn, torch.nn.functional as F, torch.optim as optim
from torch.autograd import Variable

#import external libraries
import pandas as pd,numpy as np,matplotlib.pyplot as plt, os
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix, accuracy_score
%matplotlib inline

#Set device to GPU or CPU based on availability

if torch.cuda.is_available():
    device = torch.device('cuda')
else:
    device = torch.device('cpu')

Listing 6-1Importing the Required Packages

我们现在将使用 Pandas 加载数据集(类似于第五章)并分离标签和像素值。请注意,大多数图像数据集都是以简单的图像格式存储的。jpeg 或者。png)放在一个适合 PyTorch 的简单文件夹结构中。然而,为了简化这个例子,我们使用一个数据集,其中像素值作为横截面数据存储在一个. csv 文件中。然后,我们将数据集分为训练和测试,并绘制几个样本。在下一个示例中,我们将使用存储在传统文件夹结构中的数据集。

在本例中,我们将使用由 PyTorch 提供的包装器 TensorDataset 将标签和张量组合成一个统一的数据集。清单 6-2 演示了将数据集加载到内存中。

img/478491_2_En_6_Figa_HTML.jpg

input_folder_path = "/input/data/MNIST/"

#The CSV contains a flat file of images,
#i.e. each 28*28 image is flattened into a row of 784 colums
#(1 column represents a pixel value)
#For CNN, we would need to reshape this to our desired shape

train_df = pd.read_csv(input_folder_path+"train.csv")

#First column is the target/label
train_labels = train_df['label'].values

#Pixels values start from the 2nd column
train_images = (train_df.iloc[:,1:].values).astype('float32')

#Training and Validation Split
train_images, val_images, train_labels, val_labels =
                                         train_test_split(
                                             train_images
                                             ,train_labels
                                             ,random_state=2020
                                             ,test_size=0.2)
#Here we reshape the flat row into [#images,#Channels,#Width,#Height]
#Given this a simple grayscale image, we will have just 1 channel
train_images = train_images.reshape(train_images.shape[0],1,28, 28)
val_images = val_images.reshape(val_images.shape[0],1,28, 28)

#Also, let's plot few samples
for i in range(0, 6):
    plt.subplot(160 + (i+1))
    plt.imshow(train_images[i].reshape(28,28), cmap=plt.get_cmap('gray'))
    plt.title(train_labels[i])

Listing 6-2Loading the Dataset into Memory

接下来,我们将归一化像素值,并将数据集转换为 PyTorch 张量用于训练(清单 6-3 )。

#Covert Train Images from pandas/numpy to tensor and normalize the values
train_images_tensor = torch.tensor(train_images)/255.0
train_images_tensor = train_images_tensor.view(-1,1,28,28)
train_labels_tensor = torch.tensor(train_labels)

#Create a train TensorDataset
train_tensor = TensorDataset(train_images_tensor, train_labels_tensor)

#Covert Validation Images from pandas/numpy to tensor and normalize the values
val_images_tensor = torch.tensor(val_images)/255.0
val_images_tensor = val_images_tensor.view(-1,1,28,28)
val_labels_tensor = torch.tensor(val_labels)

#Create a Validation TensorDataset
val_tensor = TensorDataset(val_images_tensor, val_labels_tensor)

print("Train Labels Shape:",train_labels_tensor.shape)
print("Train Images Shape:",train_images_tensor.shape)
print("Validation Labels Shape:",val_labels_tensor.shape)
print("Validation Images Shape:",val_images_tensor.shape)

#Load Train and Validation TensorDatasets into the data generator for Training
train_loader = DataLoader(train_tensor, batch_size=64
                          , num_workers=2, shuffle=True)
val_loader = DataLoader(val_tensor, batch_size=64, num_workers=2, shuffle=True)

Output[]
Train Labels Shape: torch.Size([33600])
Train Images Shape: torch.Size([33600, 1, 28, 28])
Validation Labels Shape: torch.Size([8400])
Validation Images Shape: torch.Size([8400, 1, 28, 28])

Listing 6-3Normalizing the Data and Preparing the Training/Validation Datasets

准备好训练和验证数据集后,让我们定义网络的下一个重要方面。这包括 CNN 本身、用于训练的功能以及评估和做出预测。这些结构中的大多数都是从我们之前在第五章中的例子中借来的。我们将在这里处理一些新的代码结构。

在我们的 CNN 中,我们需要定义一个卷积单元,如前所述。每个单元组合了一个卷积层,随后是批量标准化(可选)、激活和最大池层。要考虑的一个重要方面是每个卷积单元后的结果图像的大小。

在本例中,我们的原始图像大小为 28×28。当我们通过第一个卷积单元时,图像大小会根据我们定义的内核大小缩小。假设我们已经使用“padding=1”向输入添加了一个填充单元,卷积后原始大小保持不变。然而,使用最大池操作,大小减少了一半(正如我们所希望的)。因此,最初为 28×28 的合成图像将被转换为大小为 14×14×16 的张量(其中 16 是我们定义的过滤器数量)。对于每一个额外的卷积单元,我们将看到数量减少了一半(作为最大池操作的结果)。

因此,在三个连续的卷积单元之后,最终大小将是 7(即,28 -> 14 -> 7)。

全连接层 fc1 的输入节点为 7×7×32(其中 32 是前一个卷积单元中的内核数)。转发功能将这些卷积单元与完全连接的层顺序连接。最后一层将有 10 个输出节点,因为我们在这里有多类分类问题:即将一个数字分类为 0,1,2,3,… 9。最后一层中的 softmax 函数为我们的多类用例将输出裁剪成一组简洁的概率分数。

在清单 6-4 中,我们定义了 CNN 的结构和助手函数来评估模型的性能并生成预测。

#Define conv-net
class ConvNet(nn.Module):
    def __init__(self, num_classes=10):
        super(ConvNet, self).__init__()
        #First unit of convolution
        self.conv_unit_1 = nn.Sequential(
            nn.Conv2d(1, 16, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(16),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2))

        #Second unit of convolution

        self.conv_unit_2 = nn.Sequential(
            nn.Conv2d(16, 32, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(32),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2))

        #Fully connected layers
        self.fc1 = nn.Linear(7*7*32, 128)
        self.fc2 = nn.Linear(128, 10)

    #Connect the units
    def forward(self, x):
        out = self.conv_unit_1(x)
        out = self.conv_unit_2(out)
        out = out.view(out.size(0), -1)
        out = self.fc1(out)
        out = self.fc2(out)
        out = F.log_softmax(out,dim=1)
        return out

#Define Functions for Model Evaluation and generating Predictions
def make_predictions(data_loader):
    #Explcitly set the model to eval mode
    model.eval()
    test_preds = torch.LongTensor()
    actual = torch.LongTensor()

    for data, target in data_loader:

        if torch.cuda.is_available():
            data = data.cuda()
        output = model(data)

        #Predict output/Take the index of the output with max value
        preds = output.cpu().data.max(1, keepdim=True)[1]

        #Combine tensors from each batch
        test_preds = torch.cat((test_preds, preds), dim=0)
        actual  = torch.cat((actual,target),dim=0)

    return actual,test_preds

#Evalute model

def evaluate(data_loader):
    model.eval()
    loss = 0
    correct = 0

    for data, target in data_loader:
        if torch.cuda.is_available():
            data = data.cuda()
            target = target.cuda()
        output = model(data)
        loss += F.cross_entropy(output, target, size_average=False).data.item()
        predicted = output.data.max(1, keepdim=True)[1]
        correct += (target.reshape(-1,1) == predicted.reshape(-1,1)).float().sum()

    loss /= len(data_loader.dataset)

    print('\nAverage Val Loss: {:.4f}, Val Accuracy: {}/{} ({:.3f}%)\n'.format(
        loss, correct, len(data_loader.dataset),
        100\. * correct / len(data_loader.dataset)))

Listing 6-4Defining the CNN and the Helper Functions

有了重要的构造,我们现在可以创建模型的实例,并定义我们的标准函数和优化器,如清单 6-5 所示。

img/478491_2_En_6_Figb_HTML.jpg

#Create Model  instance
model = ConvNet(10).to(device)

#Define Loss and optimizer
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
print(model)
Output[]

Listing 6-5Creating a Model Instance and Defining the Loss Function and Optimizer

清单 6-6 展示了为定义数量的时期训练 CNN 模型——在本例中是五个时期。

num_epochs = 5

# Train the model
total_step = len(train_loader)
for epoch in range(num_epochs):
    for i, (images, labels) in enumerate(train_loader):
        images = images.to(device)
        labels = labels.to(device)

        # Forward pass
        outputs = model(images)
        loss = criterion(outputs, labels)

        # Backward and optimize
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

    #After each epoch print Train loss and validation loss + accuracy
    print ('Epoch [{}/{}], Loss: {:.4f}' .format(epoch+1, num_epochs, loss.item()))
    evaluate(val_loader)

Output[]
Epoch [1/5], Loss: 0.0564
Average Val Loss: 0.0700, Val Accuracy: 8196.0/8400 (97.571%)

Epoch [2/5], Loss: 0.0096
Average Val Loss: 0.0481, Val Accuracy: 8279.0/8400 (98.560%)

Epoch [3/5], Loss: 0.0088
Average Val Loss: 0.0474, Val Accuracy: 8273.0/8400 (98.488%)

Epoch [4/5], Loss: 0.0362

Average Val Loss: 0.0520, Val Accuracy: 8243.0/8400 (98.131%)

Epoch [5/5], Loss: 0.0013
Average Val Loss: 0.0458, Val Accuracy: 8277.0/8400 (98.536%)

Listing 6-6Training a CNN Model

我们可以看到,该模型在验证数据集上取得了相当积极的结果。以 98.5%的准确度(在五个时期内),我们可以断定我们的模型具有良好的性能。

让我们对验证数据集进行预测,并可视化混淆矩阵(参见清单 6-7 )。

img/478491_2_En_6_Figc_HTML.jpg

#Make Predictions on Validation Dataset

actual, predicted = make_predictions(val_loader)
actual,predicted = np.array(actual).reshape(-1,1)
                            ,np.array(predicted).reshape(-1,1)

print("Validation Accuracy-",round(accuracy_score(actual,predicted),4)*100)
print("\n Confusion Matrix\n",confusion_matrix(actual,predicted))

Output[]

Listing 6-7Making Predictions

在 PyTorch 中实现更大的 CNN

这是我们 CNN 的第一个样本。给定小数据集,我们可以在我们的个人计算机(商用硬件)上轻松地训练我们的网络,并且仍然可以获得令人满意的结果。让我们探索一个类似的例子,但是有更复杂的图像。一个很好的例子就是猫和狗的数据集。这里,我们的目标是根据给定的图像将数据集分类为猫或狗。

该数据集最初由微软研究院发布,后来在 https://www.kaggle.com/c/dogs-vs-cats/data 通过 Kaggle 提供。

数据集被托管为一个简单的文件夹,文件名代表标签,因此我们可能必须在使用它之前重新组织数据集。

PyTorch 通过 ImageFolder 和 DataLoader 为图像提供了简洁的抽象。PyTorch 希望数据存储在以下文件夹结构中:

Root/label_1/*
Root/label_2/*
Root/label_N/*

对于我们的用例,这将是以下内容:

/input/train/cats/*
/input/train/dogs/*
/input/test/cats/*
/input/test/dogs/*

为了简化过程,我们在 https://www.kaggle.com/jojomoolayil/catsvsdogs 提供了一个有组织的结构,带有适合 PyTorch 实验的图像。

我们建议在这个实验中使用带 GPU 加速器的 Kaggle 笔记本。右侧栏上的设置显示了训练数据文件夹结构,以及加速器(参见图 6-12 )。我们已经打开了互联网选项,并将加速器设置为 GPU。

img/478491_2_En_6_Fig12_HTML.jpg

图 6-12

Kaggle 笔记本中的环境设置

让我们从所需包的新导入开始。清单 6-8 展示了如何为这个练习导入包。

# Import required libraries
import torch
import torchvision.transforms as transforms
import torchvision.datasets as datasets
import torchvision.models as models
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from PIL import Image
import matplotlib.pyplot as plt
import glob,os
import matplotlib.image as mpimg

new_path = "/kaggle/input/catsvsdogs/"

Listing 6-8Importing the Packages for This Exercise

确保您已经打开了互联网选项,并选择了加速器作为 GPU。我们使用清单 6-9 中的命令确认 GPU 可用。

#Check if GPU is available
if torch.cuda.is_available():
    device = torch.device('cuda')
else:
    device = torch.device('cpu')
print("Device:",device)

Output[]
Device: cuda

Listing 6-9Enabling the GPU (If Available) in the Kernel

请注意,建议只使用 GPU,而不是命令。然而,对于计算机视觉实验来说,使用 CPU 会慢得多。

我们现在可以探索一组随机的猫和狗的图像。清单 6-10 从训练数据集中随机绘制样本图像。

img/478491_2_En_6_Figd_HTML.jpg

%matplotlib inline
images = []
#Collect Cat images
for img_path in glob.glob(os.path.join(new_path,"train","cat","*.jpg"))[:5]:
    images.append(mpimg.imread(img_path))

#Collect Dog images
for img_path in glob.glob(os.path.join(new_path,"train","dog","*.jpg"))[:5]:
    images.append(mpimg.imread(img_path))

#Plot a grid of cats and Dogs

plt.figure(figsize=(20,10))
columns = 5
for i, image in enumerate(images):
    plt.subplot(len(images) / columns + 1, columns, i + 1)
    plt.imshow(image)

Listing 6-10Plotting Sample Images from the Training Dataset

对于计算机视觉实验,我们总是会对原始数据集应用许多变换。这样做的一个核心原因是,实验中使用的大多数图像大小不同。此外,有时我们可能需要通过扩充现有样本来添加更多的训练样本。一些例子包括用随机旋转增加更多的训练样本、从中心裁剪图像、跨轴翻转、标准化像素值等。PyTorch 提供了一个方便的功能来组合几个这样的转换,并在训练和验证样本上编排它们。在清单 6-11 中,我们编写了一个transformations对象,它将顺序地将所有图像调整为 255×255,从中心向 224×224 裁剪它们,将它们转换为张量,并归一化它们的像素值。

#Compose sequence of transformations for image
transformations = transforms.Compose([
    transforms.Resize(255),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

# Load in each dataset and apply transformations using
# the torchvision.datasets as datasets library
train_set = datasets.ImageFolder(os.path.join(new_path,"train")
                                 , transform = transformations)
val_set = datasets.ImageFolder(os.path.join(new_path,"test")
                               , transform = transformations)

# Put into a Dataloader using torch library
train_loader = torch.utils.data.DataLoader(train_set
                                 , batch_size=32, shuffle=True)
val_loader = torch.utils.data.DataLoader(val_set, batch_size =32, shuffle=True)

Listing 6-11Transforming the Data and Creating the Training and Validation Sets

请注意,train_loaderval_loader是为我们的训练循环处理和创建带有标签的小批量图像的对象。在创建小批量图像之前,transformations对象会确保所有图像都得到适当的放大。

接下来,清单 6-12 定义了我们的 CNN。

#Define Convolutional network
class ConvNet(nn.Module):
    def __init__(self, num_classes=2):
        super(ConvNet, self).__init__()
        #First unit of convolution
        self.conv_unit_1 = nn.Sequential(
            nn.Conv2d(3, 16, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)) #112

        #Second unit of convolution
        self.conv_unit_2 = nn.Sequential(
            nn.Conv2d(16, 32, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)) #56

        #Third unit of convolution
        self.conv_unit_3 = nn.Sequential(
            nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)) #28

        #Fourth unit of convolution
        self.conv_unit_4 = nn.Sequential(
            nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)) #14

        #Fully connected layers
        self.fc1 = nn.Linear(14*14*128, 128)
        self.fc2 = nn.Linear(128, 1)
        self.final = nn.Sigmoid()

    def forward(self, x):
        out = self.conv_unit_1(x)
        out = self.conv_unit_2(out)
        out = self.conv_unit_3(out)
        out = self.conv_unit_4(out)

        #Reshape the output
        out = out.view(out.size(0),-1)
        out = self.fc1(out)
        out = self.fc2(out)
        out  = self.final(out)

        return(out)

Listing 6-12Defining the CNN

类似于 MNIST 的例子,全连接层需要输入维度的数量,这将基于卷积单元而不同。因为我们在原始样本中应用了四个卷积单元,所以图像的大小会缩小,为([原始]224->[第一]112->[第二]56->[第三]28->[第四] 14。因此,全连接层将具有 14×14×128 个输入维度,其中 128 是前一单元中的内核数。

清单 6-13 定义了一个评估我们新网络的函数。

def evaluate(model,data_loader):
    loss = []
    correct = 0
    with torch.no_grad():
            for images, labels in data_loader:
                images = images.to(device)
                labels = labels.to(device)

                model.eval()

                output = model(images)

                predicted = output > 0.5
                correct += (labels.reshape(-1,1) == predicted.reshape(-1,1)).float().sum()

                #Clear memory
                del([images,labels])
                if device == "cuda":
                    torch.cuda.empty_cache()

    print('\nVal Accuracy: {}/{} ({:.3f}%)\n'.format(
        correct, len(data_loader.dataset),
        100\. * correct / len(data_loader.dataset)))

Listing 6-13Defining the Evaluation Function

有了这些,让我们定义并创建一个模型实例,并为 10 个时期训练我们的网络。清单 6-14 演示了定义损失函数和优化器,创建模型实例,以及为定义数量的时期进行训练。

num_epochs = 10
loss_function = nn.BCELoss()  #Binary Crosss Entropy Loss
model = ConvNet()
model.cuda()
adam_optimizer = torch.optim.Adam(model.parameters(), lr= 0.001)

# Train the model
total_step = len(train_loader)
print("Total Batches:",total_step)

for epoch in range(num_epochs):
    model.train()
    train_loss = 0
    for i, (images, labels) in enumerate(train_loader):
        images = images.to(device)
        labels = labels.to(device)

        # Forward pass

        outputs = model(images)
        loss = loss_function(outputs.float(), labels.float().view(-1,1))

        # Backward and optimize
        adam_optimizer.zero_grad()
        loss.backward()
        adam_optimizer.step()
        train_loss += loss.item()* labels.size(0)

        #After each epoch print Train loss and validation loss + accuracy
    print ('Epoch [{}/{}], Loss: {:.4f}' .format(epoch+1, num_epochs, loss.item()))
    #Evaluate model after each training epoch
    evaluate(model,val_loader)

Output[]
Total Batches: 625

Epoch [1/10], Loss: 0.6990
Val Accuracy: 3768.0/5000 (75.360%)

Epoch [2/10], Loss: 0.4914
Val Accuracy: 3885.0/5000 (77.700%)

Epoch [3/10], Loss: 0.2088
Val Accuracy: 4141.0/5000 (82.820%)

Epoch [4/10], Loss: 0.2832
Val Accuracy: 4219.0/5000 (84.380%)

Epoch [5/10], Loss: 0.1797

Val Accuracy: 4271.0/5000 (85.420%)

Epoch [6/10], Loss: 0.3226
Val Accuracy: 4248.0/5000 (84.960%)

Epoch [7/10], Loss: 0.2027
Val Accuracy: 4250.0/5000 (85.000%)

Epoch [8/10], Loss: 0.2660
Val Accuracy: 4137.0/5000 (82.740%)

Epoch [9/10], Loss: 0.1867
Val Accuracy: 4286.0/5000 (85.720%)

Epoch [10/10], Loss: 0.1286
Val Accuracy: 4271.0/5000 (85.420%)

Listing 6-14Defining the Loss Function and Optimizer, Creating the Model Instance, and Training for a Defined Number of Epochs

10 个纪元后,性能大致为 85%。几个时代之后,性能肯定会提高;然而,训练这样一个网络所需的时间是昂贵的。我们可能想知道的一个问题是,是否有更快、更容易的替代方法来加速这一过程。事实证明,迁移学习对我们的资源是可用的。关于 CNN 的惊人消息是,一旦一个层被训练,它基本上可以被重新用于另一个任务。对于大多数计算机视觉任务来说,较低级别的特征(例如曲线、边和圆)和几个较高级别的特征总是共同的或相似的。然而,我们可能需要重新训练最后几层,以便专门为我们的用例定制网络。尽管如此,在训练大型网络时,这还是带来了巨大的缓解。

今天,我们有大量经过预训练的网络,这些网络在一个大的数据集语料库上训练了几个小时,几乎代表了我们遇到的最常见的对象。在 PyTorch 下,这些网络中有许多都是现成的。我们可以直接利用它们,而不是从头开始训练我们自己的网络。

欲了解更多关于预训练模型列表的信息,请访问 https://pytorch.org/docs/stable/torchvision/models.html

对于我们的用例,让我们使用 VGGNet。清单 6-15 展示了下载和利用 VGGNet 进行迁移学习。

#Download the model (pretrained)
from torchvision import models
new_model = models.vgg16(pretrained=True)

# Freeze model weights
for param in new_model.parameters():
    param.requires_grad = False

print(new_model.classifier)
Output[]

Sequential(
  (0): Linear(in_features=25088, out_features=4096, bias=True)
  (1): ReLU(inplace=True)
  (2): Dropout(p=0.5, inplace=False)
  (3): Linear(in_features=4096, out_features=4096, bias=True)
  (4): ReLU(inplace=True)
  (5): Dropout(p=0.5, inplace=False)
  (6): Linear(in_features=4096, out_features=1000, bias=True)
)

Listing 6-15Downloading and Initializing the Pretrained Model

预训练网络有六层。最初的网络用于对 1000 个不同的物体进行分类;因此,最后一层有 1000 个输出连接。然而,我们的用例是一个简单的二元分类练习;因此,我们需要替换最后一层来适应我们的用例。清单 6-16 用一个定制层替换预训练网络中的最后一层,该定制层输出一个带有 sigmoid 激活的单个单元。

#Define our custom model last layer
new_model.classifier[6] = nn.Sequential(
                      nn.Linear(new_model.classifier[6].in_features, 256),
                      nn.ReLU(),
                      nn.Dropout(0.4),
                      nn.Linear(256, 1),
                      nn.Sigmoid())

# Find total parameters and trainable parameters
total_params = sum(p.numel() for p in new_model.parameters())
print(f'{total_params:,} total parameters.')
total_trainable_params = sum(
    p.numel() for p in new_model.parameters() if p.requires_grad)
print(f'{total_trainable_params:,} training parameters.')

Output[]
135,309,633 total parameters.
1,049,089 training parameters.

Listing 6-16Replacing the Last Layer with Our Custom Layer

在这里,我们利用了 VGG 预训练模型的现有层,并在最后添加了一个新的全连接层,以针对我们的二进制用例定制网络结构。除了我们添加的层之外,所有层的权重都被冻结,也就是说,除了最后一个完全连接的层之外,模型权重在训练过程中不会更新。

现在让我们为数据集训练 10 个时期的新模型。所有组件都与前面的示例相似。清单 6-17 展示了为我们的用例训练预训练网络。

#Define epochs, optimizer and loss function
num_epochs = 10
loss_function = nn.BCELoss()  #Binary Crosss Entropy Loss
new_model.cuda()
adam_optimizer = torch.optim.Adam(new_model.parameters(), lr= 0.001)

# Train the model
total_step = len(train_loader)
print("Total Batches:",total_step)

for epoch in range(num_epochs):
    new_model.train()
    train_loss = 0
    for i, (images, labels) in enumerate(train_loader):
        images = images.to(device)
        labels = labels.to(device)

        # Forward pass
        outputs = new_model(images)
        loss = loss_function(outputs.float(), labels.float().view(-1,1))

        # Backward and optimize
        adam_optimizer.zero_grad()
        loss.backward()
        adam_optimizer.step()
        train_loss += loss.item()* labels.size(0)

    #After each epoch print Train loss and validation loss + accuracy
    print ('Epoch [{}/{}], Loss: {:.4f}' .format(epoch+1, num_epochs, loss.item()))

    #After each epoch evaluate model
    evaluate(new_model,val_loader)

Output[]
Total Batches: 625

Epoch [1/10], Loss: 0.0140
Val Accuracy: 4933.0/5000 (98.660%)

Epoch [2/10], Loss: 0.0411
Val Accuracy: 4931.0/5000 (98.620%)

Epoch [3/10], Loss: 0.0054
Val Accuracy: 4933.0/5000 (98.660%)

Epoch [4/10], Loss: 0.0017
Val Accuracy: 4937.0/5000 (98.740%)

Epoch [5/10], Loss: 0.0285
Val Accuracy: 4935.0/5000 (98.700%)

Epoch [6/10], Loss: 0.0070
Val Accuracy: 4935.0/5000 (98.700%)

Epoch [7/10], Loss: 0.0310
Val Accuracy: 4940.0/5000 (98.800%)

Epoch [8/10], Loss: 0.0091
Val Accuracy: 4922.0/5000 (98.440%)

Epoch [9/10], Loss: 0.0116

Val Accuracy: 4937.0/5000 (98.740%)

Epoch [10/10], Loss: 0.0442
Val Accuracy: 4930.0/5000 (98.600%)

Listing 6-17Training the Pretrained Model for the Defined Use Case

通过仅仅 10 个时期,我们可以看到我们的预训练模型在验证数据集上给出了大约 98%的准确度。与我们的原始模型(从头开始训练)相比,性能改进是显著的。

CNN 经验法则

对于计算机视觉任务,我们可以描绘一些规则,这些规则可以作为大多数实验的良好起点。

  • 任何给定的计算机视觉任务的起点都应该利用预先训练的网络。从头开始训练网络总是可能的,但是当结果已经可用时,巨大的计算努力将是徒劳的任务。

  • 在模型性能达不到您的基准的情况下,尝试使用其他几个预训练的网络,而不是一个。PyTorch 提供了几种现成的预训练模型。

  • 当您的图像分类任务包括一组非常多样化的图像时,预训练的网络可能不会为您提供最佳性能。在这种情况下,建议逐步解冻更多顶层。这个想法是试验什么级别的特性表示对您的用例有意义。在最坏的情况下,您可能需要从头开始训练整个网络。然而,在大多数情况下,通过预训练网络中的几层或更多层,您很可能能够节省计算工作量。

  • 使用辍学总是一个好主意。

  • 对于大多数用例,ReLUs 可以被盲目地用作事实上的激活函数。

  • 要获得相当可接受的性能,请确保每个类有 6,000 个或更多的训练样本。越多越好。

  • 批量大小应该是 GPU 或 CPU 能够处理的最大值。优化批量大小有助于加快训练过程。

  • 总是推荐使用 GPU。对于大多数常见用例,GPU 性能几乎是 50 倍或更高。获得基于 GPU 的实例的成本已经显著下降。所有主要的云参与者都提供现成的深度学习映像或虚拟机,可以通过合适的计算和 GPU 按需供应。整个繁重的任务(即安装所需的依赖项、包和驱动程序,以及配置深度学习、Python 框架、工作区等。)一点就抽象出来了。成本也下降了,以提供一个负担得起的手段来训练一些实验。今天,你可以以每小时 1 美元的价格为大多数研究项目配备功能强大的 GPU。

  • 许多资源都是免费的。Google Colab 和 Kaggle 提供了开始尝试深度学习的绝佳场所。

摘要

本章讲述了 CNN 的基础知识。关键的要点是卷积运算、汇集运算、它们是如何结合使用的,以及特性是如何通过学习而不是手工设计的。CNN 是深度学习最成功的应用,体现了学习特征/表示而不是手工设计它们的思想。本章中的练习使用一个相当简单的数据集和一个中等大小的数据集从零开始训练来探索 CNN。我们还利用了预训练的网络,并看到了由此带来的性能提升。

在下一章中,我们将探讨循环神经网络,它广泛应用于自然语言处理和语音识别领域。