正则化秘籍(三)

原文:annas-archive.org/md5/b0a4e1a7c9576619c74e69137644debd

译者:飞龙

协议:CC BY-NC-SA 4.0

第六章:深度学习提示

深度学习 是基于神经网络的机器学习特定领域。深度学习被认为在处理非结构化数据(如文本、音频和图像)时特别强大,但对于时间序列和结构化数据也能发挥作用。在本章中,我们将回顾深度学习的基础知识,从感知机到神经网络的训练。我们将提供训练神经网络的三大主要用例的配方:回归、二分类和多类分类。

在本章中,我们将覆盖以下配方:

  • 训练感知机

  • 训练一个回归神经网络

  • 训练一个二分类神经网络

  • 训练一个多类分类神经网络

技术要求

在本章中,你将训练一个感知机以及多个神经网络。为此,需要以下库:

  • NumPy

  • pandas

  • scikit-learn

  • PyTorch

  • torchvision

训练感知机

感知机可以说是深度学习的基石。即便在生产系统中没有直接使用感知机,理解其原理对于构建深度学习的坚实基础是非常有帮助的。

在本配方中,我们将回顾感知机的基本概念,然后使用 scikit-learn 在鸢尾花数据集上训练一个感知机。

入门

感知机是一种最早提出用于模拟生物神经元的机器学习方法。它最早在 1940 年代提出,并在 1950 年代得到了实现。

从高层次来看,神经元可以被描述为一种接收输入信号并在输入信号的和超过某个阈值时发出信号的细胞。这正是感知机的工作原理;你只需做以下操作:

  • 用特征替换输入信号

  • 对这些特征应用加权和,并对其应用激活函数

  • 用预测值替代输出信号

更正式地说,假设有 n 个输入特征 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/B19629_06_01.pngn 个权重 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/formula_06_002.png,则感知机的输出 ŷ 如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/formula_06_003.jpg

其中 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/formula_06_004.png 是偏置,g 是激活函数;从历史上看,激活函数通常是步进函数,它对正输入值返回 1,其他情况返回 0。因此,最终,对于 n 个输入特征,感知机由 n+1 个参数组成:每个特征一个参数,再加上偏置。

提示

步进函数也被称为赫维赛德函数,并广泛应用于其他领域,如物理学。

感知机的前向计算总结在图 6.1中。如你所见,给定一组特征 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/formula_06_005.png 和权重 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/formula_06_006.png,前向计算只是加权和,再应用激活函数。

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/B19629_06_01.jpg

图 6.1 – 感知机的数学表示:从输入特征到输出,通过权重和激活函数

在实践中,安装此配方所需的唯一工具是 scikit-learn。可以通过 pip install scikit-learn 命令安装。

如何实现…

我们将再次使用 Iris 数据集,因为感知机在复杂分类任务中表现并不好:

  1. 从 scikit-learn 中导入所需的模块:

    • load_iris:一个加载数据集的函数

    • train_test_split:一个用于拆分数据的函数

    • StandardScaler:一个可以重新缩放数据的类

    • Perceptron:包含感知机实现的类:

      from sklearn.datasets import load_iris
      
      from sklearn.model_selection import train_test_split
      
      from sklearn.preprocessing import StandardScaler
      
      from sklearn.linear_model import Perceptron
      
  2. 加载 Iris 数据集:

    # Load the Iris dataset
    
    X, y = load_iris(return_X_y=True)
    
  3. 使用 train_test_split 函数将数据拆分为训练集和测试集,并将 random state 设置为 0 以确保可重复性:

    # Split the data
    
    X_train, X_test, y_train, y_test = train_test_split(
    
        X, y, random_state=0)
    
  4. 由于这里所有的特征都是定量的,我们只需使用标准缩放器对所有特征进行重新缩放:

    # Rescale the data
    
    scaler = StandardScaler()
    
    X_train = scaler.fit_transform(X_train)
    
    X_test = scaler.transform(X_test)
    
  5. 使用默认参数实例化模型,并通过 .fit() 方法在训练集上进行拟合:

    perc = Perceptron()perc.fit(X_train, y_train)
    
  6. 使用 LinearRegression 类的 .score() 方法在训练集和测试集上评估模型,并提供准确率得分:

    # Print the R2-score on train and test
    
    print('R2-score on train set:',
    
        perc.score(X_train, y_train))
    
    print('R2-score on test set:',
    
        perc.score(X_test, y_test))
    

这里是输出:

R2-score on train set: 0.9285714285714286
R2-score on test set: 0.8421052631578947
  1. 出于好奇,我们可以查看 .coef_ 中的权重和 .intercept_ 中的偏置。

    print('weights:', perc.coef_)
    
    print('bias:', perc.intercept_)
    

这里是输出:

weights: [[-0.49201984  2.77164495 -3.07208498 -2.51124259]
  [ 0.41482008 -1.94508614  3.2852582  -2.60994774]
  [-0.32320969  0.48524348  5.73376173  4.93525738]] bias: [-2\. -3\. -6.]

重要提示

共有三组四个权重和一个偏置,因为 scikit-learn 自动处理 One-vs-Rest 多类分类,所以我们为每个类别使用一个感知机。

还有更多…

感知机不仅仅是一个机器学习模型。它可以用来模拟逻辑门:OR、AND、NOR、NAND 和 XOR。让我们来看看。

我们可以通过以下代码轻松实现感知机的前向传播:

import numpy as np
class LogicalGatePerceptron:
    def __init__(self, weights: np.array, bias: float):
        self.weights = weights
        self.bias = bias
    def forward(self, X: np.array) -> int:
        return (np.dot(
            X, self.weights) + self.bias > 0).astype(int)

这段代码没有考虑许多边界情况,但在这里仅用于解释和演示简单概念。

AND 门具有以下真值表中定义的输入和期望输出:

输入 1输入 2输出
000
010
100
111

表 6.1 – AND 门真值表

让我们使用一个具有两个特征(输入 1 和输入 2)和四个样本的数组 X 来重现这个数据,并使用一个包含期望输出的数组 y

# Define X and y
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
y = [0, 0, 0, 1]

我们现在可以找到一组权重和偏置,使感知机能够作为 AND 门工作,并检查结果以验证它是否正常工作:

gate = LogicalGatePerceptron(np.array([1, 1]), -1)
y_pred = gate.forward(X)
print('Error:', (y - y_pred).sum())

这里是输出:

Error: 0

以相同的逻辑,感知机可以创建大多数基本的逻辑门:

  • AND 门:权重 [1, 1] 和偏置 -1

  • OR 门:权重 [1, 1] 和偏置 0

  • NOR 门:权重 [-1, -1] 和偏置 1

  • NAND 门:权重 [-1, -1] 和偏置 2

  • XOR 门:这需要两个感知机

提示

你可以通过试错法猜测权重和偏置,但你也可以使用逻辑门的真值表来做出合理的猜测,甚至可以通过解方程组来求解。

这意味着使用感知机可以计算任何逻辑函数。

另见

scikit-learn 实现的官方文档:scikit-learn.org/stable/modules/generated/sklearn.linear_model.Perceptron.xhtml

回归任务中训练神经网络

感知器并不是一个强大且常用的机器学习模型,但将多个感知器结合在神经网络中使用,可以成为一个强大的机器学习模型。在本教程中,我们将回顾一个简单的神经网络,有时称为多层感知器基础神经网络。然后,我们将使用广泛应用于深度学习的框架 PyTorch,在加利福尼亚住房数据集上进行回归任务的训练。

开始使用

让我们先回顾一下什么是神经网络,以及如何从输入特征开始进行神经网络的前向传播。

神经网络可以分为三部分:

  • 输入层,包含输入特征

  • 隐层,可以有任意数量的层和单元

  • 输出层,由神经网络的预期输出定义

在隐层和输出层中,我们将每个单元(或神经元)视为一个感知器,具有其自己的权重和偏差。

这三部分在图 6.2中得到了很好的表示。

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/B19629_06_02.jpg

图 6.2 – 神经网络的典型表示:左边是输入层,中间是隐层,右边是输出层

我们将记录输入特征 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/formula_06_007.png,第l层单位i的激活值,以及 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/formula_06_008.png,第l层单位i的权重。

提示

如果神经网络中至少有一个隐层,我们认为它涉及深度学习。

训练回归任务中的神经网络与训练线性回归没有太大区别。它由相同的组成部分构成:

  • 前向传播,从输入特征和权重到预测结果

  • 一个需要最小化的损失函数

  • 更新权重的算法

让我们来看看这些组成部分。

前向传播

前向传播用于从输入特征中计算并输出结果。它必须从左到右计算,从输入层(输入特征)到输出层(输出预测)。每个单元都是感知器,第一隐层的计算相对简单:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/formula_06_009.jpghttps://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/formula_06_010.jpghttps://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/formula_06_011.jpghttps://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/formula_06_012.jpg

其中 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/formula_06_013.png 是偏差项,https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/formula_06_014.png 是第 1 层的激活函数。

现在,如果我们想计算第二个隐藏层的激活值 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/formula_06_015.png,我们将使用完全相同的公式,但输入将是第一隐藏层的激活值 (https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/formula_06_016.png),而不是输入特征:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/formula_06_017.jpghttps://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/formula_06_018.jpghttps://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/formula_06_019.jpghttps://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/formula_06_020.jpg

提示

你可以轻松地推广到任意数量的隐藏层和每层任意数量的单元——原理保持不变。

最后,输出层的计算方式完全相同,只不过在这种情况下我们只有一个输出神经元:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/formula_06_021.png

有一点值得强调:激活函数也是依赖于层的,这意味着每一层可以有不同的激活函数。对于输出层来说,这一点尤为重要,因为它需要根据任务和预期输出使用特定的输出函数。

对于回归任务,常用的激活函数是线性激活函数,这样神经网络的输出值可以是任意数字。

提示

激活函数在神经网络中起着决定性作用:它增加了非线性。如果我们对隐藏层仅使用线性激活函数,无论层数多少,这相当于没有隐藏层。

损失函数

在回归任务中,损失函数可以与线性回归中的相同:均方误差。在我们的例子中,如果我们认为预测 ŷ 是输出值 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/formula_06_022.png,那么损失 L 只是如下:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/formula_06_023.jpg

假设 j 是样本索引。

更新权重

更新权重是通过尝试最小化损失函数来完成的。这与线性回归几乎相同。难点在于,与线性回归不同,我们有多个层的单元,其中每个单元都有权重和偏置,都需要更新。这就是所谓的反向传播,它允许逐层更新,从最右侧到最左侧(遵循 图 6.2 中的约定)。

反向传播的细节,尽管有用且有趣,但超出了本书的范围。

同样,正如在逻辑回归中有多个算法优化权重(在 scikit-learnLogisticRegression 中的 solver 参数),训练神经网络也有多种算法。这些算法通常被称为优化器。其中最常用的有随机梯度下降SGD)和自适应动量Adam)。

PyTorch

PyTorch 是一个广泛使用的深度学习框架,使我们能够轻松地训练和重用深度学习模型。

它非常容易使用,并且可以通过以下命令轻松安装:

pip install torch

对于这个食谱,我们还需要 scikit-learnmatplotlib,可以使用 pip install scikit-learn matplotlib 安装它们。

如何实现…

在这个示例中,我们将构建并训练一个神经网络来处理加利福尼亚住房数据集:

  1. 首先,我们需要导入所需的模块。在这些导入中,有一些来自我们在本书中已经使用过的 scikit-learn:

    • fetch_california_housing用于加载数据集。

    • train_test_split用于将数据分割为训练集和测试集。

    • StandardScaler用于重新缩放定量数据。

    • r2_score用于评估模型。

  2. 为了显示目的,我们还导入了 matplotlib:

    from sklearn.datasets import fetch_california_housing
    
    from sklearn.model_selection import train_test_split
    
    from sklearn.preprocessing import StandardScaler
    
    from sklearn.metrics import r2_score
    
    import matplotlib.pyplot as plt
    
  3. 我们还需要从 torch 导入一些模块:

    • torch本身提供了一些库中较低层级的函数。

    • torch.nn包含许多用于构建神经网络的有用类。

    • torch.nn.functional用于一些有用的函数。

    • DatasetDataLoader用于处理数据操作:

      import torch
      
      import torch.nn as nn
      
      import torch.nn.functional as F
      
      from torch.utils.data import Dataset, DataLoader
      
  4. 我们需要使用fetch_california_housing函数加载数据,并返回特征和标签:

    X, y = fetch_california_housing(return_X_y=True)
    
  5. 然后我们可以使用train_test_split函数将数据分割为训练集和测试集。我们设置测试集大小为 20%,并为可重复性指定随机种子:

    X_train, X_test, y_train, y_test = train_test_split(
    
        X.astype(np.float32), y.astype(np.float32),
    
           test_size=0.2, random_state=0)
    
  6. 现在我们可以使用标准缩放器对数据进行重新缩放:

    scaler = StandardScaler()
    
    X_train = scaler.fit_transform(X_train)
    
    X_test = scaler.transform(X_test)
    

重要提示

请注意,我们将Xy变量转换为 float32 类型的变量。这是为了防止后续在 PyTorch 中处理 float64 变量时出现问题。

  1. 对于 PyTorch,我们需要创建数据集类。这里并不复杂;这个类只需要以下内容才能正常工作:

    • 它必须继承自Dataset类(前面导入过的)。

    • 它必须有一个构造函数(__init__方法),处理(并可选地准备)数据。

    • 它必须有一个__len__方法,以便能够获取样本的数量。

    • 它必须有一个__getitem__方法,以便获取给定索引的Xy

让我们为加利福尼亚数据集实现这个,并将我们的类命名为CaliforniaDataset

class CaliforniaDataset(Dataset):
    def __init__(self, X: np.array, y: np.array):
        self.X = torch.from_numpy(X)
        self.y = torch.from_numpy(y)
    def __len__(self) -> int:
        return len(self.X)
    def __getitem__(self, idx: int) -> tuple[torch.Tensor]:
        return self.X[idx], self.y[idx]

如果我们分解这个类,我们会看到以下函数:

  • init构造函数简单地将Xy转换为 torch 张量,使用torch.from_numpy函数,并将结果存储为类的属性。

  • len方法只是返回X属性的长度;它同样适用于使用y属性的长度。

  • getitem方法简单地返回一个元组,其中包含给定索引idxXy张量。

这相当简单,然后会让pytorch知道数据是什么,数据集中有多少个样本,以及样本i是什么。为此,我们需要实例化一个DataLoader类。

小贴士

重新缩放也可以在这个CaliforniaDataset类中计算,以及任何预处理。

  1. 现在,我们实例化CaliforniaDataset对象用于训练集和测试集。然后,我们使用导入的DataLoader类实例化相关的加载器:

    # Instantiate datasets
    
    training_data = CaliforniaDataset(X_train, y_train)
    
    test_data = CaliforniaDataset(X_test, y_test)
    
    # Instantiate data loaders
    
    train_dataloader = DataLoader(training_data,
    
        batch_size=64, shuffle=True)
    
    test_dataloader = DataLoader(test_data, batch_size=64,
    
        shuffle=True)
    

数据加载器实例有几个可用的选项。在这里,我们指定以下内容:

  • batch_size:训练的批次大小。它可能对最终结果产生影响。

  • shuffle:确定是否在每个 epoch 时打乱数据。

  1. 我们最终可以创建神经网络模型类。对于这个类,我们只需要填充两个方法:

    • 构造函数,包含所有有用的内容,如参数和属性

    • forward方法计算前向传播:

      class Net(nn.Module):
      
          def __init__(self, input_shape: int,
      
              hidden_units: int = 24):
      
                  super(Net, self).__init__()
      
                  self.hidden_units = hidden_units
      
                  self.fc1 = nn.Linear(input_shape,
      
                      self.hidden_units)
      
                  self.fc2 = nn.Linear(self.hidden_units,
      
                      self.hidden_units)
      
                  self.output = nn.Linear(self.hidden_units,
      
                      1)
      
          def forward(self,
      
              x: torch.Tensor) -> torch.Tensor:
      
                  x = self.fc1(x)
      
                  x = F.relu(x)
      
                  x = self.fc2(x)
      
                  x = F.relu(x)
      
                  output = self.output(x)
      
                  return output
      

如果我们细分一下,我们设计了一个类,它接受两个输入参数:

  • input_shape是神经网络的输入形状——这基本上是数据集中特征的数量

  • hidden_units是隐藏层中单元的数量,默认为 24

神经网络本身包括以下内容:

  • 两个隐藏层,每个隐藏层有hidden_units个单元,激活函数为 ReLU

  • 一个输出层,只有一个单元,因为我们只需要预测一个值

重要提示

关于 ReLU 和其他激活函数的更多内容将在下一个这里有 更多小节中给出。

  1. 我们现在可以实例化一个神经网络,并在随机数据(形状符合预期)上进行测试,以检查forward方法是否正常工作:

    # Instantiate the network
    
    net = Net(X_train.shape[1])
    
    # Generate one random sample of 8 features
    
    random_data = torch.rand((1, X_train.shape[1]))
    
    # Compute the forward
    
    propagationprint(net(random_data))
    

我们将得到这个输出:

tensor([[-0.0003]], grad_fn=<AddmmBackward0>)

正如我们所看到的,前向传播在随机数据上的计算工作得很好,按预期返回了一个单一的值。这个步骤中的任何错误都意味着我们做错了什么。

  1. 在能够在数据上训练神经网络之前,我们需要定义损失函数和优化器。幸运的是,均方误差已经实现,并作为nn.MSELoss()提供。有许多优化器可以选择;我们在这里选择了 Adam,但也可以测试其他优化器:

    criterion = nn.MSELoss()
    
    optimizer = torch.optim.Adam(net.parameters(), lr=0.001)
    

重要提示

优化器需要将网络参数作为输入传递给其构造函数。

  1. 最后,我们可以使用以下代码训练神经网络 10 个 epoch:

    losses = []
    
    # Loop over the dataset multiple times
    
    for epoch in range(10):
    
        # Reset the loss for this epoch
    
        running_loss = 0.
    
        For I, data in enumerate(train_dataloader, 0):
    
            # Get the inputs per batch: data is a list of [inputs, labels]
    
            inputs, labels = data
    
            # Zero the parameter gradients
    
            optimizer.zero_grad()
    
            # Forward propagate + backward + optimize
    
            outputs = net(inputs)
    
            # Unsqueeze for dimension matching
    
            labels = labels.unsqueeze(1)
    
            # Compute the loss
    
            Loss = criterion(outputs, labels)
    
            # Backpropagate and update the weights
    
            loss.backward()
    
            optimizer.step()
    
            # Add this loss to the running loss
    
            running_loss += loss.item()
    
         # Compute the loss for this epoch and add it to the list
    
        epoch_loss = running_loss / len(
    
            train_dataloader)
    
        losses.append(epoch_loss)
    
        # Print the epoch and training loss
    
        print(f'[epoch {epoch + 1}] loss: {
    
            epoch_loss:.3f}')print('Finished Training')
    

希望注释是自解释的。基本上,有两个嵌套循环:

  • 一个外部循环遍历所有的 epoch:即模型在整个数据集上训练的次数

  • 一个内循环遍历样本:每个步骤中,使用batch_size样本的批次来训练模型

在内循环的每一步中,我们有以下主要步骤:

  • 获取一批数据:包括特征和标签

  • 对数据进行前向传播并获取输出预测

  • 计算损失:预测值与标签之间的均方误差

  • 使用反向传播更新网络的权重

在每个步骤结束时,我们打印损失值,希望它随着每个 epoch 的进行而减少。

  1. 我们可以将损失作为 epoch 的函数进行绘制。这非常直观,并且让我们能够确保网络在学习,如果损失在减少的话:

    plt.plot(losses)
    
    plt.xlabel('epoch')
    
    plt.ylabel('loss (MSE)')plt.show()
    

这是结果图:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/B19629_06_03.jpg

图 6.3 – MSE 损失与 epoch 的关系

重要提示

我们也可以在这一步追踪测试集上的损失,并显示出来,以获取更多信息。为了避免信息过载,我们将在下一个食谱中进行这个操作。

  1. 最后,我们可以在训练集和测试集上评估模型。正如本书前面在回归任务中所做的那样,我们将使用 R2-score。其他相关指标也可以使用:

    # Compute the predictions with the trained neural
    
    Network
    
    y_train_pred = net(torch.tensor((
    
        X_train))).detach().numpy()
    
    y_test_pred = net(torch.tensor((
    
        X_test))).detach().numpy()
    
    # Compute the R2-score
    
    print('R2-score on training set:',
    
        r2_score(y_train, y_train_pred))
    
    print('R2-score on test set:',
    
        r2_score(y_test, y_test_pred))
    

这是输出结果:

R2-score on training set: 0.7542622050620708 R2-score on test set: 0.7401526252651656

正如我们在这里看到的,我们在训练集上得到了合理的 R2-score 值 0.74,存在轻微的过拟合。

还有更多内容…

在本节中,我们提到了激活函数,但并没有真正解释它们是什么或为什么需要它们。

简而言之,激活函数添加了非线性因素,使模型能够学习更复杂的模式。事实上,如果我们有一个没有激活函数的神经网络,无论层数或单元数多少,整个模型都等同于一个线性模型(例如线性回归)。这一点在 图 6.4 中得到了总结。

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/B19629_06_04.jpg

图 6.4 – 左侧的神经网络没有激活函数,只能学习线性可分的决策函数;右侧的神经网络有激活函数,可以学习复杂的决策函数

有许多可用的激活函数,但最常见的隐藏层激活函数包括 sigmoid、ReLU 和 tanh。

Sigmoid

Sigmoid 函数与逻辑回归中使用的函数相同:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/formula_06_024.jpg

这个函数的值范围从 0 到 1,当 x = 0 时输出 0.5。

tanh

tanh 或双曲正切函数的值范围从 -1 到 1,当 x 为 0 时值为 0:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/formula_06_025.jpg

ReLU

ReLU修正线性单元 函数对于任何负输入值返回 0,对于任何正输入值 x 返回 x。与 sigmoid 和 tanh 不同,它不会出现平台效应,因此能够避免梯度消失问题。其公式如下:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/formula_06_026.jpg

可视化

我们可以使用以下代码将这三个激活函数(sigmoid、tanh 和 ReLU)一起可视化,从而更直观地理解它们:

import numpy as np
import matplotlib.pyplot as plt
x = np.arange(-2, 2, 0.02)
sigmoid = 1./(1+np.exp(-x))
tanh = (np.exp(x)-np.exp(-x))/(np.exp(x)+np.exp(-x))
relu = np.max([np.zeros(len(x)), x], axis=0)
plt.plot(x, sigmoid)
plt.plot(x, tanh)
plt.plot(x, relu)plt.grid()
plt.xlabel('x')
plt.ylabel('activation')
plt.legend(['sigmoid', 'tanh', 'relu'])
plt.show()

运行之前的代码时,你会得到这个输出,计算的是这些函数在 [-2, 2] 范围内的输出值:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/B19629_06_05.jpg

图 6.5 – Sigmoid、tanh 和 ReLU 激活函数在 [-2, 2] 输入范围内的结果图

想了解更多关于 PyTorch 中可用的激活函数,请查看以下链接:pytorch.org/docs/stable/nn.xhtml#non-linear-activations-weighted-sum-nonlinearity.

另见

这里有几个 PyTorch 教程的链接,它们对于熟悉 PyTorch 和深入理解其工作原理非常有帮助:

以下链接是一个关于深度学习的优秀网站,适合那些希望更好理解神经网络、梯度下降和反向传播的人:neuralnetworksanddeeplearning.com/.

训练一个用于二分类的神经网络

在这个食谱中,我们将训练第一个用于乳腺癌数据集的二分类任务神经网络。我们还将了解学习率和优化器对优化过程的影响,以及如何通过测试集评估模型。

准备工作

正如我们在这个食谱中所看到的,训练一个用于二分类的神经网络与训练一个回归神经网络并没有太大不同。主要有两个变化需要进行:

  • 输出层的激活函数

  • 损失函数

在之前的回归任务食谱中,输出层没有激活函数。实际上,对于回归任务,可以期望预测值取任意值。

对于二分类任务,我们期望输出是一个概率值,也就是介于 0 和 1 之间的值,就像逻辑回归一样。这就是为什么在做二分类时,输出层的激活函数通常是 sigmoid 函数。最终的预测结果将与逻辑回归的预测结果类似:一个数值,我们可以应用一个阈值(例如 0.5),超过该阈值时我们认为预测为类别 1。

由于标签是 0 和 1,而预测值是介于 0 和 1 之间的值,因此均方误差不再适用于训练此类模型。因此,就像逻辑回归一样,我们将使用二元交叉熵损失函数。

这个食谱所需的库是 matplotlib、scikit-learn 和 PyTorch,可以通过 pip install matplotlib scikit-learn torch 来安装。

如何做到这一点…

我们将训练一个具有两个隐藏层的简单神经网络,用于乳腺癌数据集上的二分类任务。尽管这个数据集不太适合深度学习,因为它是一个小型数据集,但它使我们能够轻松理解训练二分类神经网络的所有步骤:

  1. 我们从 scikit-learn 导入了以下所需的库:

    • load_breast_cancer 用于加载数据集

    • train_test_split 用于将数据拆分为训练集和测试集

    • 用于重新缩放定量数据的 StandardScaler

    • 用于评估模型的 accuracy_score

我们还需要 matplotlib 来进行显示,并且需要从 torch 中引入以下内容:

  • torch 本身

  • 包含构建神经网络所需类的 torch.nn

  • 用于激活函数(如 ReLU)的 torch.nn.functional

  • 处理数据的 DatasetDataLoader

    from sklearn.datasets import load_breast_cancer
    
    from sklearn.model_selection import train_test_split
    
    from sklearn.preprocessing import StandardScaler
    
    from sklearn.metrics import accuracy_score
    
    import matplotlib.pyplot as plt
    
    import torchimport torch.nn as nn
    
    import torch.nn.functional as F
    
    from torch.utils.data import Dataset, DataLoader
    
  1. 使用 load_breast_cancer 函数加载特征和标签:

    X, y = load_breast_cancer(return_X_y=True)
    
  2. 将数据拆分为训练集和测试集,指定随机状态以确保结果可复现。同时将特征和标签转换为 float32,以便与 PyTorch 后续操作兼容:

    X_train, X_test, y_train, y_test = train_test_split(
    
        X.astype(np.float32), y.astype(np.float32),
    
        test_size=0.2, random_state=0)
    
  3. 创建处理数据的 Dataset 类。请注意,在这个食谱中,我们将数据重新缩放集成到此步骤中,不像在之前的食谱中那样:

    class BreastCancerDataset(Dataset):
    
        def __init__(self, X: np.array, y: np.array,
    
            x_scaler: StandardScaler = None):
    
                if x_scaler is None:
    
                    self.x_scaler = StandardScaler()
    
                    X = self.x_scaler.fit_transform(X)
    
                else:
    
                    self.x_scaler = x_scaler
    
                    X = self.x_scaler.transform(X)
    
                self.X = torch.from_numpy(X)
    
                self.y = torch.from_numpy(y)
    
        def __len__(self) -> int:
    
            return len(self.X)
    
        def __getitem__(self, idx: int) -> tuple[torch.Tensor]:
    
            return self.X[idx], self.y[idx]
    

重要提示

将缩放器包含在类中有利有弊,其中一个优点是可以正确处理训练集和测试集之间的数据泄露。

  1. 实例化训练集和测试集及其加载器。请注意,训练数据集没有提供缩放器,而测试数据集则提供了训练集的缩放器,以确保所有数据以相同方式处理,避免数据泄露:

    training_data = BreastCancerDataset(X_train, y_train)
    
    test_data = BreastCancerDataset(X_test, y_test,
    
        training_data.x_scaler)
    
    train_dataloader = DataLoader(training_data,
    
        batch_size=64, shuffle=True)
    
    test_dataloader = DataLoader(test_data, batch_size=64,
    
        shuffle=True)
    
  2. 构建神经网络。在这里,我们构建一个具有两个隐藏层的神经网络。在 forward 方法中,torch.sigmoid() 函数被应用于输出层,确保返回的值在 0 和 1 之间。实例化模型所需的唯一参数是输入形状,这里仅为特征的数量:

    class Net(nn.Module):
    
        def __init__(self, input_shape: int,
    
            hidden_units: int = 24):
    
                super(Net, self).__init__()
    
                    self.hidden_units = hidden_units
    
                    self.fc1 = nn.Linear(input_shape,
    
                        self.hidden_units)
    
                    self.fc2 = nn.Linear(
    
                        self.hidden_units,
    
                        self.hidden_units)
    
                    self.output = nn.Linear(
    
                        self.hidden_units, 1)
    
        def forward(self, x: torch.Tensor) -> torch.Tensor:
    
            x = self.fc1(x)
    
            x = F.relu(x)
    
            x = self.fc2(x)
    
            x = F.relu(x)
    
            output = torch.sigmoid(self.output(x))
    
            return output
    
  3. 现在我们可以使用正确的输入形状实例化模型,并检查给定随机张量上的前向传播是否正常工作:

    # Instantiate the network
    
    net = Net(X_train.shape[1])
    
    # Generate one random sample
    
    random_data = torch.rand((1, X_train.shape[1]))
    
    # Compute the forward propagation
    
    print(net(random_data))
    

运行上述代码后,我们得到以下输出:

tensor([[0.4487]], grad_fn=<SigmoidBackward0>)
  1. 定义损失函数和优化器。如前所述,我们将使用二元交叉熵损失,PyTorch 中提供的 nn.BCELoss() 实现此功能。选择的优化器是 Adam,但也可以测试其他优化器:

    criterion = nn.BCELoss()
    
    optimizer = torch.optim.Adam(net.parameters(),
    
        lr=0.001)
    

重要提示

有关优化器的更多解释将在下一节 There’s more 中提供。

  1. 现在我们可以训练神经网络 50 个 epoch。我们还会在每个 epoch 计算训练集和测试集的损失,以便之后绘制它们。为此,我们需要切换模型的模式:

    • 在训练训练集之前,使用 model.train() 切换到训练模式

    • 在评估测试集之前,使用 model.eval() 切换到 eval 模式

      train_losses = []
      
      test_losses = []
      
      # Loop over the dataset 50 times
      
      for epoch in range(50):
      
          ## Train the model on the training set
      
          running_train_loss = 0.
      
          # Switch to train mode
      
          net.train()
      
          # Loop over the batches in train set
      
          for i, data in enumerate(train_dataloader, 0):
      
              # Get the inputs: data is a list of [inputs, labels]
      
              inputs, labels = data
      
              # Zero the parameter gradients
      
              optimizer.zero_grad()
      
              # Forward + backward + optimize
      
              outputs = net(inputs)
      
              loss = criterion(outputs, labels.unsqueeze(1))
      
              loss.backward()
      
              optimizer.step()
      
              # Add current loss to running loss
      
              running_train_loss += loss.item()
      
          # Once epoch is over, compute and store the epoch loss
      
          train_epoch_loss = running_train_loss / len(
      
              train_dataloader)
      
          train_losses.append(train_epoch_loss)
      
          ## Evaluate the model on the test set
      
          running_test_loss = 0.
      
          # Switch to eval model
      
          net.eval()
      
          with torch.no_grad():
      
              # Loop over the batches in test set
      
              for i, data in enumerate(test_dataloader, 0):
      
                  # Get the inputs
      
                  inputs, labels = data
      
                  # Compute forward propagation
      
                  outputs = net(inputs)
      
                  # Compute loss
      
                  loss = criterion(outputs,
      
                      labels.unsqueeze(1))
      
                  # Add to running loss
      
                  running_test_loss += loss.item()
      
                  # Compute and store the epoch loss
      
                  test_epoch_loss = running_test_loss / len(
      
                      test_dataloader)
      
                  test_losses.append(test_epoch_loss)
      
          # Print stats
      
          print(f'[epoch {epoch + 1}] Training loss: {
      
              train_epoch_loss:.3f} | Test loss: {
      
                  test_epoch_loss:.3f}')
      
          print('Finished Training')
      

重要提示

请注意在评估部分使用 with torch.no_grad()。这行代码允许我们禁用自动求导引擎,从而加速处理。

  1. 现在,我们将训练集和测试集的损失作为 epoch 的函数绘制,使用上一步骤中计算的两个列表:

    plt.plot(train_losses, label='train')
    
    plt.plot(test_losses, label='test')
    
    plt.xlabel('epoch')plt.ylabel('loss (BCE)')
    
    plt.legend()
    
    plt.show()
    

这是输出结果:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/B19629_06_06.jpg

图 6.6 – 训练集和测试集的 MSE 损失

正如我们所看到的,两个损失都在减少。一开始,训练损失和测试损失几乎相等,但在 10 个 epoch 后,训练损失继续减少,而测试损失没有变化,意味着模型在训练集上发生了过拟合。

  1. 可以使用训练集和测试集的准确度分数来评估模型,通过 scikit-learn 的accuracy_score函数。计算预测结果时需要更多步骤,因为我们必须执行以下操作才能获得实际的类别预测:

    • 使用用于训练的标准化器对数据进行重新缩放,该标准化器可在training_data.x_scaler属性中找到

    • 使用torch.tensor()将 NumPy 数据转换为 torch 张量

    • 将前向传播应用到模型上

    • 使用.detach().numpy()将输出的 torch 张量转换回 NumPy

    • 使用> 0.5应用阈值,将概率预测(介于 0 和 1 之间)转换为类别预测

      # Compute the predictions with the trained neural network
      
      y_train_pred = net(torch.tensor((
      
          training_data.x_scaler.transform(
      
              X_train)))).detach().numpy() > 0.5
      
      y_test_pred = net(torch.tensor((
      
          training_data.x_scaler.transform(
      
              X_test)))).detach().numpy() > 0.5
      
      # Compute the accuracy score
      
      print('Accuracy on training set:', accuracy_score(
      
          y_train, y_train_pred))
      
      print('Accuracy on test set:', accuracy_score(y_test,
      
          y_test_pred))
      

这是前面代码的输出:

Accuracy on training set: 0.9912087912087912 Accuracy on test set: 0.9649122807017544

我们在训练集上获得了 99%的准确率,在测试集上获得了 96%的准确率,这证明了模型确实存在过拟合现象,正如从训练和测试损失随 epoch 变化的曲线所预期的那样。

还有更多…

正如我们在这里看到的,损失随着时间的推移而减少,意味着模型实际上在学习。

重要提示

即使有时损失出现一些波动,只要总体趋势保持良好,也不必担心。

有两个重要的概念可能会影响这些结果,它们在某种程度上是相互关联的:学习率和优化器。与逻辑回归或线性回归类似,优化器的目标是找到提供最低可能损失值的参数。因此,这是一个最小化问题,可以如图 6.7所示表示:我们寻求找到一组参数,能给出最低的损失值。

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/B19629_06_07.jpg

图 6.7 – 损失 L 作为参数 w 的函数的表示。红色交叉点是最优点,而蓝色交叉点是一个随机的任意权重集合

让我们看看学习率如何影响学习曲线。

学习率

在 PyTorch 中,通过实例化优化器时设置学习率,例如使用lr=0.001参数。可以说,学习率的值主要有四种情况,正如在图 6.8中所展示的,从较低的学习率到非常高的学习率。

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/B19629_06_08.jpg

图 6.8 – 学习率的四个主要类别:学习率过低、合适的学习率、学习率过高、学习率非常高(损失发散)

就损失而言,学习率的变化可以从图 6**.9中直观感受到,图中展示了多个 epoch 中权重和损失的变化过程。

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/B19629_06_09.jpg

图 6.9 – 学习率四种情况的可视化解释:a) 低学习率,b) 合适的学习率,c) 高学习率,d) 非常高的学习率

图 6*.9* 可以通过以下内容进一步解释:

  • 低学习率 (a):损失会随着 epoch 的进行而逐渐减少,但速度太慢,可能需要非常长的时间才能收敛,也可能使模型陷入局部最小值。

  • 合适的学习率 (b):损失将稳定下降,直到接近全局最小值。

  • 略大的学习率 ©:损失一开始会急剧下降,但可能很快跳过全局最小值,无法再到达它。

  • 非常高的学习率 (d):损失将迅速发散,学习步伐过大。

调整学习率有时有助于产生最佳结果。几种技术,如所谓的学习率衰减,会随着时间推移逐渐降低学习率,以期更加准确地捕捉到全局最小值。

优化器

除了可能最著名的随机梯度下降和 Adam 优化器,深度学习中还有许多强大且有用的优化器。在不深入讨论这些优化器的细节的前提下,让我们简要了解它们的工作原理和它们之间的差异,如图 6**.10所总结:

  • 随机梯度下降 只是根据每个批次的损失计算梯度,没有进一步的复杂处理。这意味着,有时一个批次的优化方向可能几乎与另一个批次相反。

  • Adam 使用动量,这意味着对于每个批次,除了使用该批次的梯度外,还会使用之前计算过的梯度的动量。这使得 Adam 能够保持一个更加一致的方向,并且有望更快地收敛。

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/B19629_06_10.jpg

图 6.10 – 向全局最小值训练的可视化表现,左边是随机梯度下降,右边是 Adam 保持之前步骤的动量

优化过程可以用一个简单的比喻来概括。这就像在雾霾中爬山,试图向下走(到达全局最小值)。你可以通过随机梯度下降或 Adam 来进行下降:

  • 使用随机梯度下降时,你观察周围的情况,选择向下坡度最陡的方向,并沿该方向迈出一步。然后再重复一次。

  • 使用 Adam,你做的和随机梯度下降一样,但速度更快。你迅速环顾四周,看到朝下坡度最陡的方向,然后试图朝那个方向迈出一步,同时保持之前步骤的惯性,因为你是在跑步。然后再重复这一过程。

请注意,在这个类比中,步长就是学习率。

另见

PyTorch 上可用的优化器列表:pytorch.org/docs/stable/optim.xhtml#algorithms

训练一个多类别分类神经网络

在这个食谱中,我们将关注另一个非常常见的任务:使用神经网络进行多类别分类,在本例中使用 PyTorch。我们将处理一个深度学习中的经典数据集:MNIST 手写数字识别。该数据集包含 28x28 像素的小灰度图像,描绘的是 0 到 9 之间的手写数字,因此有 10 个类别。

准备工作

在经典机器学习中,多类别分类通常不会原生处理。例如,在使用 scikit-learn 训练一个三分类任务(例如,Iris 数据集)时,scikit-learn 将自动训练三个模型,采用一对其余法(one-versus-the-rest)。

在深度学习中,模型可以原生处理超过两个类别。为了实现这一点,与二分类相比,只需进行少量的更改:

  • 输出层的单元数与类别数相同:这样,每个单元将负责预测一个类别的概率

  • 输出层的激活函数是 softmax 函数,该函数使得所有单元的和等于 1,从而允许我们将其视为概率

  • 损失函数是交叉熵损失,考虑多个类别,而不是二元交叉熵

在我们的案例中,我们还需要对代码进行一些其他特定于数据本身的更改。由于输入现在是图像,因此需要进行一些转换:

  • 图像是一个二维(或三维,如果是 RGB 彩色图像)数组,必须将其展平为一维

  • 数据必须标准化,就像定量数据的重缩放一样

为了做到这一点,我们将需要以下库:torch、torchvision(用于数据集加载和图像转换)和 matplotlib(用于可视化)。可以通过pip install torch torchvision matplotlib来安装。

如何做……

在这个食谱中,我们将重用本章之前相同的模式:训练一个有两个隐藏层的神经网络。但有一些事情会有所不同:

  • 输入数据是来自 MNIST 手写数字数据集的灰度图像,因此它是一个需要展平的二维数组

  • 输出层将不只有一个单元,而是为数据集的十个类别准备十个单元;损失函数也会相应改变

  • 我们不仅会在训练循环中计算训练和测试损失,还会计算准确率

现在让我们看看如何在实践中做到这一点:

  1. 导入所需的库。和以前的食谱一样,我们导入了几个有用的 torch 模块和函数:

    • torch本身

    • torch.nn包含构建神经网络所需的类

    • torch.nn.functional用于激活函数,例如 ReLU

    • DataLoader用于处理数据

我们还需要从torchvision导入一些内容:

  • MNIST用于加载数据集

  • transforms用于转换数据集,包括重新缩放和展平数据:

    import torch
    
    import torch.nn as nn
    
    import torch.nn.functional as F
    
    from torch.utils.data import DataLoader
    
    from torchvision.datasets import MNIST
    
    import torchvision.transforms as transforms
    
    import matplotlib.pyplot as plt
    
  1. 实例化转换。我们使用Compose类,可以将两个或更多的转换组合起来。这里,我们组合了三个转换:

    • transforms.ToTensor():将输入图像转换为torch.Tensor格式。

    • transforms.Normalize():使用均值和标准差对图像进行归一化。它将减去均值(即 0.1307),然后除以标准差(即 0.3081),对每个像素值进行处理。

    • transforms.Lambda(torch.flatten):将 2D 张量展平为 1D 张量:

      transform = transforms.Compose([transforms.ToTensor(),
      
          transforms.Normalize((0.1307), (0.3081)),
      
          transforms.Lambda(torch.flatten)])
      

重要提示

图像通常会用均值和标准差为 0.5 进行归一化。我们使用前面代码块中使用的特定值进行归一化,因为数据集是基于特定图像创建的,但 0.5 也能正常工作。有关解释,请查看本食谱中的另见小节。

  1. 加载训练集和测试集,以及训练数据加载器和测试数据加载器。通过使用MNIST类,我们分别使用train参数为TrueFalse来获取训练集和测试集。在加载数据时,我们也直接应用之前定义的转换。然后,我们以批大小 64 实例化数据加载器:

    trainset = MNIST('./data', train=True, download=True,
    
        transform=transform)
    
    train_dataloader = DataLoader(trainset, batch_size=64,
    
        shuffle=True)
    
    testset = MNIST('./data', train=False, download=True,
    
        transform=transform)
    
    test_dataloader = DataLoader(testset, batch_size=64,
    
        shuffle=True)
    
  2. 定义神经网络。这里我们默认定义了一个具有 2 个隐藏层、每层 24 个单元的神经网络。输出层有 10 个单元,表示数据的 10 个类别(我们的是 0 到 9 之间的数字)。请注意,Softmax 函数应用于输出层,使得 10 个单元的和严格等于 1:

    class Net(nn.Module):
    
        def __init__(self, input_shape: int,
    
            hidden_units: int = 24):
    
                super(Net, self).__init__()
    
                self.hidden_units = hidden_units
    
                self.fc1 = nn.Linear(input_shape,
    
                    self.hidden_units)
    
                self.fc2 = nn.Linear(
    
                    self.hidden_units,
    
                    self.hidden_units)
    
                self.output = nn.Linear(
    
                    self.hidden_units, 10)
    
        def forward(self,
    
            x: torch.Tensor) -> torch.Tensor:
    
                x = self.fc1(x)
    
                x = F.relu(x)
    
                x = self.fc2(x)
    
                x = F.relu(x)
    
                output = torch.softmax(self.output(x),
    
                    dim=1)
    
                return output
    
  3. 现在,我们可以用正确的输入形状 784(28x28 像素)来实例化模型,并检查前向传播在给定的随机张量上是否正常工作:

    # Instantiate the model
    
    net = Net(784)
    
    # Generate randomly one random 28x28 image as a 784 values tensor
    
    random_data = torch.rand((1, 784))
    
    result = net(random_data)
    
    print('Resulting output tensor:', result)
    
    print('Sum of the output tensor:', result.sum())
    

这是输出结果:

Resulting output tensor: tensor([[0.0918, 0.0960, 0.0924, 0.0945, 0.0931, 0.0745, 0.1081, 0.1166, 0.1238,              0.1092]], grad_fn=<SoftmaxBackward0>) Sum of the output tensor: tensor(1.0000, grad_fn=<SumBackward0>)

提示

请注意,输出是一个包含 10 个值的张量,其和为 1。

  1. 定义损失函数为交叉熵损失,在 PyTorch 中可以通过nn.CrossEntropyLoss()获得,并将优化器定义为 Adam:

    criterion = nn.CrossEntropyLoss()
    
    optimizer = torch.optim.Adam(net.parameters(),
    
        lr=0.001)
    
  2. 在训练之前,我们实现了一个epoch_step辅助函数,适用于训练集和测试集,允许我们遍历所有数据,计算损失和准确度,并在训练集上训练模型:

    def epoch_step(net, dataloader, training_set: bool):
    
        running_loss = 0.
    
        Correct = 0.
    
        For i, data in enumerate(dataloader, 0):
    
            # Get the inputs: data is a list of [inputs, labels]
    
            inputs, labels = data
    
            if training_set:
    
                # Zero the parameter gradients
    
                optimizer.zero_grad()
    
                # Forward + backward + optimize
    
                outputs = net(inputs)
    
                loss = criterion(outputs, labels)
    
                if training_set:
    
                    loss.backward()
    
                    optimizer.step()
    
                # Add correct predictions for this batch
    
                correct += (outputs.argmax(
    
                    dim=1) == labels).float().sum()
    
                # Compute loss for this batch
    
                running_loss += loss.item()
    
        return running_loss, correct
    
  3. 现在,我们可以在 20 个 epoch 上训练神经网络。在每个 epoch 中,我们还会计算以下内容:

    • 训练集和测试集的损失

    • 训练集和测试集的准确率

和前一个食谱一样,在训练之前,模型会通过model.train()切换到训练模式,而在评估测试集之前,它会通过model.eval()切换到评估模式:

# Create empty lists to store the losses and accuracies
train_losses = []
test_losses = []
train_accuracy = []
test_accuracy = []
# Loop over the dataset 20 times for 20 epochs
for epoch in range(20):
    ## Train the model on the training set
    net.train()
    running_train_loss, correct = epoch_step(net,
        dataloader=train_dataloader,training_set=True)
    # Compute and store loss and accuracy for this epoch
    train_epoch_loss = running_train_loss / len(
        train_dataloader)
    train_losses.append(train_epoch_loss)
    train_epoch_accuracy = correct / len(trainset)
     rain_accuracy.append(train_epoch_accuracy)
    ## Evaluate the model on the test set
    net.eval()
    with torch.no_grad():
        running_test_loss, correct = epoch_step(net,
            dataloader=test_dataloader,training_set=False)
        test_epoch_loss = running_test_loss / len(
            test_dataloader)
        test_losses.append(test_epoch_loss)
        test_epoch_accuracy = correct / len(testset)
        test_accuracy.append(test_epoch_accuracy)
    # Print stats
    print(f'[epoch {epoch + 1}] Training: loss={train_epoch_loss:.3f} accuracy={train_epoch_accuracy:.3f} |\
\t Test: loss={test_epoch_loss:.3f} accuracy={test_epoch_accuracy:.3f}')
print('Finished Training')
  1. 我们可以绘制损失随时代变化的图像,因为我们已经存储了每个时代的这些值,并且可以同时查看训练集和测试集的变化:

    plt.plot(train_losses, label='train')
    
    plt.plot(test_losses, label='test')
    
    plt.xlabel('epoch')plt.ylabel('loss (CE)')
    
    plt.legend()plt.show()
    

这是输出:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/B19629_06_11.jpg

图 6.11 – 交叉熵损失随时代变化的结果,适用于训练集和测试集

由于在经过 20 个时代后,训练集测试集的损失似乎仍在不断改进,从性能角度来看,继续训练更多的时代可能会很有趣。

  1. 同样,也可以通过显示相应结果,使用准确度分数进行相同操作:

    plt.plot(train_accuracy, label='train')
    
    plt.plot(test_accuracy, label='test')
    
    plt.xlabel('epoch')
    
    plt.ylabel('Accuracy')plt.legend()plt.show()
    

以下是结果:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/B19629_06_12.jpg

图 6.12 – 训练集和测试集准确度随时代变化的结果

最终,训练集的准确度大约为 97%,测试集的准确度为 96%。

一旦模型训练完成,当然可以将其存储起来,以便在新数据上直接使用。有几种保存模型的方法:

  • 必须首先实例化net类。

  • 保存整个模型:这会保存权重和架构,意味着只需要加载文件。

  • 以 torchscript 格式保存:这会使用更高效的表示方式保存整个模型。此方法更适合于大规模的部署和推断。

现在,我们只需保存state字典,重新加载它,然后对图像进行推断:

# Save the model's state dict
torch.save(net.state_dict(), 'path_to_model.pt')
# Instantiate a new model
new_model = Net(784)
# Load the model's weights
new_model.load_state_dict(torch.load('path_to_model.pt'))

现在,可以使用已加载的训练好模型对给定图像进行推断:

plt.figure(figsize=(12, 8))
for i in range(6):
    plt.subplot(3, 3, i+1)
    # Compute the predicted number
    pred = new_model(
        testset[i][0].unsqueeze(0)).argmax(axis=1)
    # Display the image and predicted number as title
    plt.imshow(testset[i][0].detach().numpy().reshape(
        28, 28), cmap='gray_r')
    plt.title(f'Prediction: {pred.detach().numpy()}')
    plt.axis('off')

这是我们得到的结果:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/B19629_06_13.jpg

图 6.13 – 使用六个输入图像及其来自训练模型的预测结果

正如预期的那样,加载的模型能够正确预测大多数图像上的正确数字。

还有更多内容……

深度学习通常用于需要大量计算资源的任务。在这种情况下,使用 GPU 往往是必须的。当然,PyTorch 允许我们在 GPU 上训练和推断模型。只需要几个步骤:声明一个设备变量并将模型和数据移到该设备。让我们快速了解一下如何实现这一过程。

选择设备

声明一个设备变量为 GPU 可以通过以下 Python 代码完成:

device = torch.device(
    "cuda" if torch.cuda.is_available() else "cpu") print(device)

这一行实例化了一个torch.device对象,如果 CUDA 可用,它将包含"cuda",否则包含"cpu"。事实上,如果未安装 CUDA,或者硬件上没有 GPU,将使用 CPU(这是默认行为)。

如果 GPU 已被正确检测,print(device)的输出将是"cuda"。否则,输出将是"cpu"

将模型和数据移到 GPU

一旦设备正确设置为 GPU,模型和数据都必须移动到 GPU 内存中。为此,只需要在模型和数据上调用.to(device)方法。例如,我们在此食谱中使用的训练和评估代码将变成以下内容:

train_losses = []
test_losses = []
train_accuracy = []
test_accuracy = []
# Move the model to the GPU
net = net.to(device)
for epoch in range(20):
    running_train_loss = 0.
    correct = 0.
    net.train()
    for i, data in enumerate(train_dataloader, 0):
        inputs, labels = data
        # Move the data to the device
        inputs = inputs.to(device)
        labels = labels.to(device)
    running_test_loss = 0.
    correct = 0.
    net.eval()
    with torch.no_grad():
        for i, data in enumerate(test_dataloader, 0):
            inputs, labels = data
            # Move the data to the device
            inputs = inputs.to(device)
            labels = labels.to(device)
print('Finished Training')

一开始,模型通过net = net.to(device)将其一次性移动到 GPU 设备。

在每次训练和评估的迭代循环中,输入和标签张量都通过tensor = tensor.to(device)移动到设备上。

提示

数据可以在加载时完全加载到 GPU 中,或者在训练过程中一次加载一个批次。然而,由于只有较小的数据集可以完全加载到 GPU 内存中,因此我们在此并未展示此解决方案。

另请参见

第七章:深度学习正则化

在本章中,我们将介绍几种技巧和方法来正则化神经网络。我们将重用 L2 正则化技术,就像在处理线性模型时一样。但本书中还有其他尚未介绍的技术,比如早停法和 dropout,这些将在本章中进行讲解。

在本章中,我们将介绍以下食谱:

  • 使用 L2 正则化来正则化神经网络

  • 使用早停法正则化神经网络

  • 使用网络架构进行正则化

  • 使用 dropout 进行正则化

技术要求

在本章中,我们将训练神经网络来处理各种任务。这将要求我们使用以下库:

  • NumPy

  • Scikit-learn

  • Matplotlib

  • PyTorch

  • torchvision

使用 L2 正则化来正则化神经网络

就像线性模型一样,无论是线性回归还是逻辑回归,神经网络都有权重。因此,就像线性模型一样,可以对这些权重使用 L2 惩罚来正则化神经网络。在这个食谱中,我们将在 MNIST 手写数字数据集上应用 L2 惩罚来正则化神经网络。

提醒一下,当我们在第六章中训练神经网络时,经过 20 个周期后出现了轻微的过拟合,训练集的准确率为 97%,测试集的准确率为 95%。让我们通过在本食谱中添加 L2 正则化来减少这种过拟合。

准备工作

就像线性模型一样,L2 正则化只是向损失函数中添加一个新的 L2 项。给定权重 W=w1,w2,…,添加到损失函数中的项为 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/Formula_07_001.png。这个新增的项对损失函数的影响是,权重会受到更多约束,并且必须保持接近零以保持损失函数较小。因此,它为模型添加了偏差,并帮助进行正则化。

注意

这里的权重表示法进行了简化。实际上,每个单元 i、每个特征 j 和每一层 l 都有权重 https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/Formula_07_002.png。但最终,L2 项仍然是所有权重平方的总和。

对于这个食谱,只需要三种库:

  • matplotlib 用于绘制图表

  • pytorch 用于深度学习

  • torchvision 用于 MNIST 数据集

可以使用 pip install matplotlib torch torchvision 安装这些库。

如何做…

在这个食谱中,我们重新使用了上一章中训练多类分类模型时的相同代码,数据集仍然是 MNIST。唯一的区别将在于 第 6 步 ——如果需要,可以直接跳到这一步。

输入数据是 MNIST 手写数据集:28x28 像素的灰度图像。因此,在能够训练自定义神经网络之前,数据需要进行重新缩放和展平处理:

  1. 导入所需的库。像以前的食谱一样,我们导入了几个有用的 torch 模块和函数:

    • torch

    • torch.nn 包含构建神经网络所需的类

    • torch.nn.functional用于激活函数,如 ReLU:

    • 用于处理数据的DataLoader

我们还从torchvision中导入了一些模块:

  • MNIST用于加载数据集:

  • 用于数据集转换的transforms——既包括缩放也包括扁平化数据:

    import torch
    
    import torch.nn as nn
    
    import torch.nn.functional as F
    
    from torch.utils.data import DataLoader
    
    from torchvision.datasets import MNIST
    
    import torchvision.transforms as transforms
    
    import matplotlib.pyplot as plt
    
  1. 实例化转换。此处使用Compose类来组合三个转换:

    • transforms.ToTensor():将输入图像转换为torch.Tensor格式

    • transforms.Normalize(): 使用均值和标准差对图像进行归一化处理。会先减去均值(即0.1307),然后将每个像素值除以标准差(即0.3081)。

    • transforms.Lambda(torch.flatten):将 2D 张量展平为 1D 张量:

以下是代码:

transform = transforms.Compose([transforms.ToTensor(),
    transforms.Normalize((0.1307), (0.3081)),
    transforms.Lambda(torch.flatten)])

注意:

图像通常使用均值和标准差为 0.5 进行归一化。我们使用这些特定的值进行归一化,因为数据集是由特定的图像构成的,但 0.5 也可以很好地工作。

  1. 加载训练集和测试集,以及训练和测试数据加载器。使用MNIST类,我们分别通过train=Truetrain=False参数获取训练集和测试集。在加载数据时,我们还直接应用之前定义的转换。然后,使用批量大小为64实例化数据加载器:

    trainset = MNIST('./data', train=True, download=True,
    
        transform=transform)
    
    train_dataloader = DataLoader(trainset, batch_size=64,
    
        shuffle=True)
    
    testset = MNIST('./data', train=False, download=True,
    
        transform=transform)
    
    test_dataloader = DataLoader(testset, batch_size=64,
    
        shuffle=True)
    
  2. 定义神经网络。这里默认定义了一个包含 2 个隐藏层(每层 24 个神经元)的神经网络。输出层包含 10 个神经元,因为有 10 个类别(数字从 0 到 9)。最后,对输出层应用softmax函数,使得 10 个单元的和严格等于1

    class Net(nn.Module):
    
        def __init__(self, input_shape: int,
    
        hidden_units: int = 24):
    
            super(Net, self).__init__()
    
            self.hidden_units = hidden_units
    
            self.fc1 = nn.Linear(input_shape,
    
                self.hidden_units)
    
            self.fc2 = nn.Linear(self.hidden_units,
    
                self.hidden_units)
    
            self.output = nn.Linear(self.hidden_units, 10)
    
        def forward(self,
    
            x: torch.Tensor) -> torch.Tensor:
    
                x = self.fc1(x)
    
                x = F.relu(x)
    
                x = self.fc2(x)
    
                x = F.relu(x)
    
                output = torch.softmax(self.output(x), dim=1)
    
                return output
    
  3. 为了检查代码,我们实例化模型,并使用正确的输入形状784(28x28 像素),确保在给定的随机张量上正向传播正常工作:

    # Instantiate the model
    
    net = Net(784)
    
    # Generate randomly one random 28x28 image as a 784 values tensor
    
    random_data = torch.rand((1, 784))
    
    result = net(random_data)
    
    print('Resulting output tensor:', result)
    
    print('Sum of the output tensor:', result.sum())
    

代码输出将类似如下(只有总和必须等于 1;其他数字可能不同):

Resulting output tensor: tensor([[0.0882, 0.1141, 0.0846, 0.0874, 0.1124, 0.0912, 0.1103, 0.0972, 0.1097,
         0.1048]], grad_fn=<SoftmaxBackward0>)
Sum of the output tensor: tensor(1.0000, grad_fn=<SumBackward0>)
  1. 定义损失函数为交叉熵损失,使用pytorch中的nn.CrossEntropyLoss(),并将优化器定义为Adam。在这里,我们给Adam优化器设置另一个参数:weight_decay=0.001。该参数是 L2 惩罚的强度。默认情况下,weight_decay0,表示没有 L2 惩罚。较高的值意味着更强的正则化,就像在 scikit-learn 中的线性模型一样:

    criterion = nn.CrossEntropyLoss()
    
    optimizer = torch.optim.Adam(net.parameters(),
    
        lr=0.001, weight_decay=0.001)
    
  2. 实例化epoch_step辅助函数,用于计算正向传播和反向传播(仅限于训练集),以及损失和准确率:

    def epoch_step(net, dataloader, training_set: bool):
    
        running_loss = 0.
    
        correct = 0.
    
        for i, data in enumerate(dataloader, 0):
    
            # Get the inputs: data is a list of [inputs, labels]
    
            inputs, labels = data
    
            if training_set:
    
                # Zero the parameter gradients
    
                optimizer.zero_grad()
    
            # Forward + backward + optimize
    
            outputs = net(inputs)
    
            loss = criterion(outputs, labels)
    
            if training_set:
    
                loss.backward()
    
                optimizer.step()
    
            # Add correct predictions for this batch
    
            correct += (outputs.argmax(
    
                dim=1) == labels).float().sum()
    
            # Compute loss for this batch
    
            running_loss += loss.item()
    
        return running_loss, correct
    
  3. 我们最终可以在 20 个 epoch 上训练神经网络,并计算每个 epoch 的损失和准确率。

由于我们在训练集上进行训练,并在测试集上进行评估,模型在训练前会切换到train模式(model.train()),而在评估测试集之前,则切换到eval模式(model.eval()):

# Create empty lists to store the losses and accuracies
train_losses = []
test_losses = []
train_accuracy = []
test_accuracy = []
# Loop over the dataset 20 times for 20 epochs
for epoch in range(20):
    ## Train the model on the training set
    running_train_loss, correct = epoch_step(net,
        dataloader=train_dataloader,
        training_set=True)
    # Compute and store loss and accuracy for this epoch
    train_epoch_loss = running_train_loss / len(
        train_dataloader)
    train_losses.append(train_epoch_loss)
    train_epoch_accuracy = correct / len(trainset)
    train_accuracy.append(train_epoch_accuracy)
    ## Evaluate the model on the test set
    #running_test_loss = 0.
    #correct = 0.
    net.eval()
    with torch.no_grad():
        running_test_loss, correct = epoch_step(net,
            dataloader=test_dataloader,
            training_set=False)
        test_epoch_loss = running_test_loss / len(
            test_dataloader)
        test_losses.append(test_epoch_loss)
        test_epoch_accuracy = correct / len(testset)
        test_accuracy.append(test_epoch_accuracy)
    # Print stats
    print(f'[epoch {epoch + 1}] Training: loss={
        train_epoch_loss:.3f}accuracy={
            train_epoch_accuracy:.3f} |\
            \t Test: loss={test_epoch_loss:.3f}
            accuracy={test_epoch_accuracy:.3f}')
print('Finished Training')

在最后一个 epoch,输出应该如下所示:

[epoch 20] Training: loss=1.505 accuracy=0.964 |   Test: loss=1.505 accuracy=0.962
Finished Training
  1. 为了可视化,我们可以绘制训练集和测试集的损失随 epoch 的变化:

    plt.plot(train_losses, label='train')
    
    plt.plot(test_losses, label='test')
    
    plt.xlabel('epoch')
    
    plt.ylabel('loss (CE)')
    
    plt.legend()
    
    plt.show()
    

这里是它的图示:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/B19629_07_01.jpg

图 7.1 – 交叉熵损失随 epoch 的变化;来自前面的代码输出

我们可以注意到,训练集和测试集的损失几乎相同,没有明显的偏离。而在没有 L2 惩罚的前几次尝试中,损失相差较大,这意味着我们有效地对模型进行了正则化。

  1. 显示相关结果,我们也可以用准确率来表示:

    plt.plot(train_accuracy, label='train')
    
    plt.plot(test_accuracy, label='test')
    
    plt.xlabel('epoch')
    
    plt.ylabel('Accuracy')
    
    plt.legend()
    
    plt.show()
    

这里是它的图示:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/B19629_07_02.jpg

图 7.2 – 准确率随 epoch 的变化;来自前面的代码输出

最终,训练集和测试集的准确率都约为 96%,且没有明显的过拟合。

还有更多内容…

即使 L2 正则化是正则化线性模型(如线性回归和逻辑回归)中非常常见的技术,它通常不是深度学习中的首选方法。其他方法,如早停或丢弃法,通常更受青睐。

另外,在这个示例中,我们只提到了训练集和测试集。但为了正确优化weight_decay超参数,需要使用验证集;否则,结果会产生偏差。为了简洁起见,我们简化了这个示例,只用了两个数据集。

注意

一般来说,在深度学习中,任何其他的超参数优化,如层数、单元数、激活函数等,必须针对验证集进行优化,而不仅仅是测试集。

另见

通过模型的优化器调整 L2 惩罚,而不是直接在损失函数中进行调整,可能看起来有些奇怪,实际上也是如此。

当然,也可以手动添加 L2 惩罚,但这可能不是最优选择。请查看这个 PyTorch 讨论帖,了解更多关于这个设计选择的信息,以及如何添加 L1 惩罚的示例:discuss.pytorch.org/t/simple-l2-regularization/139

使用早停法对神经网络进行正则化

早停是深度学习中常用的一种方法,用于防止模型过拟合。这个概念简单而有效:如果模型由于过长的训练周期而发生过拟合,我们就提前终止训练,以避免过拟合。我们可以在乳腺癌数据集上使用这一技术。

准备开始

在一个完美的世界里,是不需要正则化的。这意味着在训练集和验证集中,无论经过多少个 epoch,损失几乎完全相等,如图 7.3所示。

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/B19629_07_03.jpg

图 7.3 – 训练和验证损失随着周期数增加但无过拟合的示例

但现实中并非总是如此完美。在实践中,可能会发生神经网络在每个周期中逐渐学习到更多关于训练集数据分布的信息,这可能会牺牲对新数据的泛化能力。这个情况在图 7.4中有所展示。

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/B19629_07_04.jpg

图 7.4 – 训练和验证损失随着周期数增加而过拟合的示例

当遇到这种情况时,一个自然的解决方案是,在模型的验证损失停止下降时暂停训练。一旦模型的验证损失停止下降,继续训练额外的周期可能会导致模型更擅长记忆训练数据,而不是提升其在新数据上的预测准确性。这个技术叫做早停法,它可以防止模型过拟合。

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/B19629_07_05.jpg

图 7.5 – 一旦验证损失停止下降,我们就可以停止学习并认为模型已经完全训练好;这就是早停法。

由于这个示例将应用于乳腺癌数据集,因此必须安装scikit-learn,以及用于模型的torch和可视化的matplotlib。可以通过pip install sklearn torch matplotlib安装这些库。

如何实现…

在本示例中,我们将首先在乳腺癌数据集上训练一个神经网络,并可视化随着周期数增加,过拟合效应的加剧。然后,我们将实现早停法来进行正则化。

正常训练

由于乳腺癌数据集相对较小,我们将只考虑训练集和验证集,而不是将数据集拆分为训练集、验证集和测试集:

  1. scikit-learnmatplotlibtorch导入所需的库:

    • 使用load_breast_cancer加载数据集

    • 使用train_test_split将数据拆分为训练集和验证集

    • 使用StandardScaler对定量数据进行重新缩放

    • 使用accuracy_score评估模型

    • 使用matplotlib进行显示

    • 使用torch本身

    • 使用torch.nn包含构建神经网络所需的类

    • 使用torch.nn.functional实现激活函数,如 ReLU

    • 使用DatasetDataLoader处理数据

下面是实现代码:

import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import accuracy_score
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader
  1. 使用load_breast_cancer函数加载特征和标签:

    X, y = load_breast_cancer(return_X_y=True)
    
  2. 将数据分为训练集和验证集,指定随机种子以确保可重复性,并将特征和标签转换为float32,以便后续与 PyTorch 兼容:

    X_train, X_val, y_train, y_val = train_test_split(
    
        X.astype(np.float32), y.astype(np.float32),
    
        test_size=0.2, random_state=0)
    
  3. 创建Dataset类来处理数据。我们简单地重用了上一章实现的类:

    class BreastCancerDataset(Dataset):
    
        def __init__(self, X: np.array, y: np.array,
    
            x_scaler: StandardScaler = None):
    
                if x_scaler is None:
    
                    self.x_scaler = StandardScaler()
    
                    X = self.x_scaler.fit_transform(X)
    
                else:
    
                    self.x_scaler = x_scaler
    
                    X = self.x_scaler.transform(X)
    
                self.X = torch.from_numpy(X)
    
                self.y = torch.from_numpy(y)
    
        def __len__(self) -> int:
    
            return len(self.X)
    
        def __getitem__(self, idx: int) -> tuple[torch.Tensor]:
    
            return self.X[idx], self.y[idx]
    
  4. 为 PyTorch 实例化训练集和验证集及其数据加载器。注意,在实例化验证集时,我们提供了训练数据集的缩放器,确保两个数据集使用的缩放器是基于训练集拟合的:

    training_data = BreastCancerDataset(X_train, y_train)
    
    val_data = BreastCancerDataset(X_val, y_val,
    
        training_data.x_scaler)
    
    train_dataloader = DataLoader(training_data,
    
        batch_size=64, shuffle=True)
    
    val_dataloader = DataLoader(val_data, batch_size=64,
    
        shuffle=True)
    
  5. 定义神经网络架构——2 个隐藏层,每个隐藏层有 36 个单元,输出层有 1 个单元,并使用 sigmoid 激活函数,因为这是一个二分类任务:

    class Net(nn.Module):
    
        def __init__(self, input_shape: int,
    
            hidden_units: int = 36):
    
                super(Net, self).__init__()
    
                self.hidden_units = hidden_units
    
                self.fc1 = nn.Linear(input_shape,
    
                    self.hidden_units)
    
                self.fc2 = nn.Linear(self.hidden_units,
    
                    self.hidden_units)
    
                self.output = nn.Linear(self.hidden_units,
    
                    1)
    
        def forward(self, x: torch.Tensor) ->
    
            torch.Tensor:
    
                x = self.fc1(x)
    
                x = F.relu(x)
    
                x = self.fc2(x)
    
                x = F.relu(x)
    
                output = torch.sigmoid(self.output(x))
    
                return output
    
  6. 用期望的输入形状(特征数量)实例化模型。我们还可以选择检查给定随机张量的前向传播是否正常工作:

    # Instantiate the model
    
    net = Net(X_train.shape[1])
    
    # Generate randomly one random 28x28 image as a 784 values tensor
    
    random_data = torch.rand((1, X_train.shape[1]))
    
    result = net(random_data)
    
    print('Resulting output tensor:', result)
    

该代码的输出如下(具体值可能会有所变化,但由于最后一层使用的是 sigmoid 激活函数,所以输出值会在 0 和 1 之间):

Resulting output tensor: tensor([[0.5674]], grad_fn=<SigmoidBackward0>)
  1. 将损失函数定义为二分类交叉熵损失函数,因为这是一个二分类任务。同时实例化优化器:

    criterion = nn.BCELoss()
    
    optimizer = torch.optim.Adam(net.parameters(),
    
        lr=0.001)
    
  2. 实现一个辅助函数epoch_step,该函数计算前向传播、反向传播(对于训练集)、损失和准确率,适用于一个训练周期:

    def epoch_step(net, dataloader, training_set: bool):
    
        running_loss = 0.
    
        correct = 0.
    
        for i, data in enumerate(dataloader, 0):
    
            # Get the inputs: data is a list of [inputs, labels]
    
            inputs, labels = data
    
            labels = labels.unsqueeze(1)
    
            if training_set:
    
                # Zero the parameter gradients
    
                optimizer.zero_grad()
    
            # Forward + backward + optimize
    
            outputs = net(inputs)
    
            loss = criterion(outputs, labels)
    
            if training_set:
    
                loss.backward()
    
                optimizer.step()
    
            # Add correct predictions for this batch
    
            correct += ((
    
                outputs > 0.5) == labels).float().sum()
    
            # Compute loss for this batch
    
            running_loss += loss.item()
    
        return running_loss, correct
    
  3. 现在让我们实现train_model函数,以便训练一个模型,无论是否使用耐心。该函数存储每个训练周期的信息,然后返回以下结果:

    • 训练集的损失和准确率

    • 验证集的损失和准确率

下面是模型的代码:

def train_model(net, train_dataloader, val_dataloader, criterion, optimizer, epochs, patience=None):
    # Create empty lists to store the losses and accuracies
    train_losses = []
    val_losses = []
    train_accuracy = []
    val_accuracy = []
    best_val_loss = np.inf
    best_val_loss_epoch = 0
    # Loop over the dataset 20 times for 20 epochs
    for epoch in range(500):
        ## If the best epoch was more than the patience, just stop training
        if patience is not None and epoch - best_val_loss_epoch > patience:
            break
        ## Train the model on the training set
        net.train()
        running_train_loss, correct = epoch_step(net,
            dataloader=train_dataloader,
            training_set=True)
        # Compute and store loss and accuracy for this epoch
        train_epoch_loss = running_train_loss / len(
            train_dataloader)
        train_losses.append(train_epoch_loss)
        train_epoch_accuracy = correct / len(training_data)
        train_accuracy.append(train_epoch_accuracy)
        ## Evaluate the model on the val set
        net.eval()
        with torch.no_grad():
            running_val_loss, correct = epoch_step(
                net, dataloader=val_dataloader,
                training_set=False)
            val_epoch_loss = running_val_loss / len(
                val_dataloader)
            val_losses.append(val_epoch_loss)
            val_epoch_accuracy = correct / len(val_data)
            val_accuracy.append(val_epoch_accuracy)
            # If the loss is better than the current best, update it
            if best_val_loss >= val_epoch_loss:
                best_val_loss = val_epoch_loss
                best_val_loss_epoch = epoch + 1
        # Print stats
        print(f'[epoch {epoch + 1}] Training: loss={
            train_epoch_loss:.3f} accuracy={
            train_epoch_accuracy:.3f} |\
                \t Valid: loss={val_epoch_loss:.3f}
                accuracy={val_epoch_accuracy:.3f}')
    return train_losses, val_losses, train_accuracy,
        val_accuracy

现在让我们在 500 个周期上训练神经网络,重用之前实现的train_model函数。以下是代码:

train_losses, val_losses, train_accuracy,
    val_accuracy = train_model(
        net, train_dataloader, val_dataloader,
        criterion, optimizer, epochs=500
)

在 500 个周期后,代码输出将类似于以下内容:

[epoch 500] Training: loss=0.000 accuracy=1.000 |   Validation: loss=0.099 accuracy=0.965
  1. 现在我们可以绘制训练集和验证集的损失图,作为训练周期的函数,并可视化随着周期数增加而加剧的过拟合效应:

    plt.plot(train_losses, label='train')
    
    plt.plot(val_losses, label='valid')
    
    plt.xlabel('epoch')
    
    plt.ylabel('loss (CE)')
    
    plt.legend()
    
    plt.show()
    

以下是该损失的图表:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/B19629_07_06.jpg

图 7.6 – 交叉熵损失作为训练周期的函数。(尽管有几个波动,训练损失仍然在持续下降)

确实,我们看到训练损失总体上持续下降,甚至达到了零的值。另一方面,验证损失开始下降并在第 100 个周期左右达到最小值,然后在接下来的周期中缓慢增加。

我们可以通过多种方式实现早停来避免这种情况:

  • 在第一次训练后,我们可以重新训练模型,最多 100 个周期(或任何识别的最佳验证损失),希望能够得到相同的结果。这将是浪费 CPU 时间。

  • 我们可以在每个周期保存模型,然后在之后挑选最佳模型。这个方法有时会被实现,但可能会浪费存储空间,特别是对于大型模型。

  • 我们可以在验证损失没有改善时,自动停止训练,这个停止条件通常称为“耐心”。

现在让我们实现后者的解决方案。

注意

使用耐心也有风险:耐心太小可能会让模型陷入局部最小值,而耐心太大可能会错过真正的最优 epoch,导致停止得太晚。

使用耐心和早停的训练

现在让我们使用早停重新训练一个模型。我们首先实例化一个新的模型,以避免训练已经训练过的模型:

  1. 实例化一个新的模型以及一个新的优化器。如果你使用的是相同的笔记本内核,就不需要测试它,也不需要重新实例化损失函数。如果你想单独运行这段代码,步骤 1步骤 8必须重复使用:

    # Instantiate a fresh model
    
    net = Net(X_train.shape[1])
    
    optimizer = torch.optim.Adam(net.parameters(), lr=0.001)
    
  2. 我们现在使用30的耐心来训练这个模型。在连续 30 个 epoch 内,如果val损失没有改善,训练将会停止:

    train_losses, val_losses, train_accuracy,
    
        val_accuracy = train_model(
    
            net, train_dataloader, val_dataloader,
    
            criterion, optimizer, patience=30, epochs=500
    
    )
    

代码输出将类似以下内容(在达到早停之前的总 epoch 数量可能会有所不同):

[epoch 134] Training: loss=0.004 accuracy=1.000 |   Valid: loss=0.108 accuracy=0.982

训练在大约 100 个 epoch 后停止(结果可能会有所不同,因为默认情况下结果是非确定性的),验证准确率大约为 98%,远远超过我们在 500 个 epoch 后得到的 96%。

  1. 让我们再次绘制训练和验证损失,作为 epoch 数量的函数:

    plt.plot(train_losses, label='train')
    
    plt.plot(val_losses, label='validation')
    
    plt.xlabel('epoch')
    
    plt.ylabel('loss (BCE)')
    
    plt.legend()
    
    plt.show()
    

这里是它的图表:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/B19629_07_07.jpg

图 7.7 – 交叉熵损失与 epoch 的关系

如我们所见,验证损失已经发生过拟合,但没有时间增长太多,从而避免了进一步的过拟合。

还有更多…

如本例中前面所述,为了进行正确的评估,需要在单独的测试集上计算准确率(或任何选定的评估指标)。实际上,基于验证集停止训练并在同一数据集上评估模型是一种偏向的方法,可能会人为提高评估结果。

使用网络架构进行正则化

在本例中,我们将探讨一种不太常见但有时仍然有用的正则化方法:调整神经网络架构。在回顾为何使用此方法以及何时使用后,我们将其应用于加利福尼亚住房数据集,这是一个回归任务。

准备中

有时,最好的正则化方法不是使用任何花哨的技术,而是常识。在许多情况下,使用的神经网络可能对于输入任务和数据集来说过于庞大。一个简单的经验法则是快速查看网络中的参数数量(例如,权重和偏置),并将其与数据点的数量进行比较:如果比率大于 1(即参数多于数据点),则有严重过拟合的风险。

注意

如果使用迁移学习,这条经验法则不再适用,因为网络已经在一个假定足够大的数据集上进行了训练。

如果我们退后一步,回到线性模型,比如线性回归,大家都知道,特征之间高度相关会降低模型的性能。神经网络也是如此:过多的自由参数并不会提高性能。因此,根据任务的不同,并不总是需要几十层的网络;只需几层就足以获得最佳性能并避免过拟合。让我们通过加利福尼亚数据集在实践中验证这一点。

为此,需要使用的库有 scikit-learn、Matplotlib 和 PyTorch。可以通过pip install sklearn matplotlib torch来安装它们。

如何实现…

这将是一个两步的流程:首先,我们将训练一个较大的模型(相对于数据集来说),以揭示网络对过拟合的影响。然后,我们将在相同数据上训练另一个更适配的模型,期望解决过拟合问题。

训练一个大型模型

以下是训练模型的步骤:

  1. 首先需要导入以下库:

    • 使用fetch_california_housing来加载数据集

    • 使用train_test_split将数据划分为训练集和测试集

    • 使用StandardScaler来重新缩放特征

    • 使用r2_score来评估模型的最终表现

    • 使用matplotlib来显示损失

    • torch本身提供一些库中低级功能的实现

    • 使用torch.nn,它提供了许多用于构建神经网络的实用类

    • 使用torch.nn.functional来实现一些有用的函数

    • 使用DatasetDataLoader来处理数据操作

以下是这些import语句的代码:

import numpy as np
from sklearn.datasets
import fetch_california_housing
from sklearn.model_selection
import train_test_split
from sklearn.preprocessing
import StandardScaler
from sklearn.metrics
import r2_score
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader
  1. 使用fetch_california_housing函数加载数据:

    X, y = fetch_california_housing(return_X_y=True)
    
  2. 使用train_test_split函数以 80%/20%的比例将数据划分为训练集和测试集。设置一个随机种子以保证可复现性。对于pytorch,数据会被转换成float32类型的变量:

    X_train, X_test, y_train, y_test = train_test_split(
    
        X.astype(np.float32), y.astype(np.float32),
    
        test_size=0.2, random_state=0)
    
  3. 使用标准化缩放器对数据进行重新缩放:

    scaler = StandardScaler()
    
    X_train = scaler.fit_transform(X_train)
    
    X_test = scaler.transform(X_test)
    
  4. 创建CaliforniaDataset类,用于处理数据。这里唯一的变换是将numpy数组转换为torch张量:

    class CaliforniaDataset(Dataset):
    
        def __init__(self, X: np.array, y: np.array):
    
            self.X = torch.from_numpy(X)
    
            self.y = torch.from_numpy(y)
    
        def __len__(self) -> int:
    
            return len(self.X)
    
        def __getitem__(self, idx: int) ->
    
            tuple[torch.Tensor]: return self.X[idx], self.y[idx]
    
  5. 实例化训练集和测试集的数据集以及数据加载器。这里定义了一个批处理大小为64,但可以根据需要进行调整:

    # Instantiate datasets
    
    training_data = CaliforniaDataset(X_train, y_train)
    
    test_data = CaliforniaDataset(X_test, y_test)
    
    # Instantiate data loaders
    
    train_dataloader = DataLoader(training_data,
    
        batch_size=64, shuffle=True)
    
    test_dataloader = DataLoader(test_data, batch_size=64,
    
        shuffle=True)
    
  6. 创建神经网络架构。考虑到数据集的规模,我们故意创建一个较大的模型——包含 5 个隐藏层,每个层有 128 个单元:

    class Net(nn.Module):
    
        def __init__(self, input_shape: int,
    
            hidden_units: int = 128):
    
                super(Net, self).__init__()
    
                self.hidden_units = hidden_units
    
                self.fc1 = nn.Linear(input_shape,
    
                    self.hidden_units)
    
                self.fc2 = nn.Linear(self.hidden_units,
    
                    self.hidden_units)
    
                self.fc3 = nn.Linear(self.hidden_units,
    
                    self.hidden_units)
    
                self.fc4 = nn.Linear(self.hidden_units,
    
                    self.hidden_units)
    
                self.fc5 = nn.Linear(self.hidden_units,
    
                    self.hidden_units)
    
                self.output = nn.Linear(self.hidden_units, 1)
    
        def forward(self, x: torch.Tensor) -> torch.Tensor:
    
            x = self.fc1(x)
    
            x = F.relu(x)
    
            x = self.fc2(x)
    
            x = F.relu(x)
    
            x = self.fc3(x)
    
            x = F.relu(x)
    
            x = self.fc4(x)
    
            x = F.relu(x)
    
            x = self.fc5(x)
    
            x = F.relu(x)
    
            output = self.output(x)
    
            return output
    
  7. 使用给定的输入形状(特征数量)实例化模型。可选地,我们可以使用预期形状的输入张量来检查网络是否正确创建(这里指的是特征数量):

    # Instantiate the network
    
    net = Net(X_train.shape[1])
    
    # Generate one random sample of 8 features
    
    random_data = torch.rand((1, X_train.shape[1]))
    
    # Compute the forward propagation
    
    print(net(random_data))
    
    tensor([[0.0674]], grad_fn=<AddmmBackward0>)
    
  8. 将损失函数实例化为均方误差损失(MSE),因为这是一个回归任务,并定义优化器为Adam,学习率为0.001

    criterion = nn.MSELoss()
    
    optimizer = torch.optim.Adam(net.parameters(), lr=0.001)
    
  9. 最后,使用train_model函数训练神经网络 500 个纪元。这个函数的实现与之前的类似,可以在 GitHub 仓库中找到。再次提醒,我们故意选择了一个较大的纪元数;否则,过拟合可能会通过提前停止来得到补偿。我们还会存储每个纪元的训练和测试损失,用于可视化和信息展示:

    train_losses, test_losses = train_model(net,
    
        train_dataloader, test_dataloader, criterion,
    
        optimizer, 500)
    

经过 500 个纪元后,最终的输出曲线将如下所示:

[epoch 500] Training: loss=0.013 | Test: loss=0.312
Finished Training
  1. 将训练集和测试集的损失绘制为与纪元相关的函数:

    plt.plot(train_losses, label='train')
    
    plt.plot(test_losses, label='test')
    
    plt.xlabel('epoch')
    
    plt.ylabel('loss (MSE)')
    
    plt.legend()
    
    plt.show()
    

这是其绘图:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/B19629_07_08.jpg

图 7.8 – 平均平方误差损失与纪元的关系(注意训练集和测试集的损失明显分离)

我们可以注意到,训练损失持续下降,而测试损失很快达到一个平台期,然后再次上升。这是过拟合的明显信号。让我们通过计算 R2 分数来确认是否存在过拟合。

  1. 最后,让我们使用 R2 分数评估模型在训练集和测试集上的表现:

    # Compute the predictions with the trained neural network
    
    y_train_pred = net(
    
        torch.tensor((X_train))).detach().numpy()
    
    y_test_pred = net(
    
        torch.tensor((X_test))).detach().numpy()
    
    # Compute the R2-score
    
    print('R2-score on training set:', r2_score(y_train,
    
         y_train_pred))
    
    print('R2-score on test set:', r2_score(y_test,
    
        y_test_pred))
    

这段代码将输出如下类似的值:

R2-score on training set: 0.9922777453770203
R2-score on test set: 0.7610035849523354

如预期所示,我们确实遇到了明显的过拟合,在训练集上几乎达到了完美的 R2 分数,而在测试集上的 R2 分数约为 0.76。

注意

这可能看起来像是一个夸张的例子,但选择一个对于任务和数据集来说过于庞大的架构其实是相当容易的。

使用更小的网络进行正则化

现在让我们训练一个更合理的模型,看看这如何影响过拟合,即使是使用相同数量的纪元。目标不仅是减少过拟合,还要在测试集上获得更好的表现。

如果你使用的是相同的内核,则不需要重新执行第一步。否则,步骤 16 必须重新执行:

  1. 定义神经网络。这次我们只有两个包含 16 个单元的隐藏层,因此这个网络比之前的要小得多:

    class Net(nn.Module):
    
        def __init__(self, input_shape: int,
    
            hidden_units: int = 16):
    
                super(Net, self).__init__()
    
            self.hidden_units = hidden_units
    
            self.fc1 = nn.Linear(input_shape,
    
                self.hidden_units)
    
            self.fc2 = nn.Linear(self.hidden_units,
    
                self.hidden_units)
    
            self.output = nn.Linear(self.hidden_units, 1)
    
        def forward(self, x: torch.Tensor) -> torch.Tensor:
    
            x = self.fc1(x)
    
            x = F.relu(x)
    
            x = self.fc2(x)
    
            x = F.relu(x)
    
            output = self.output(x)
    
            return output
    
  2. 使用预期数量的输入特征和优化器实例化网络:

    # Instantiate the network
    
    net = Net(X_train.shape[1])
    
    optimizer = torch.optim.Adam(net.parameters(),
    
        lr=0.001)
    
  3. 训练神经网络 500 个纪元,以便我们可以将结果与之前的结果进行比较。我们将重新使用之前在本配方中使用的train_model函数:

    train_losses, test_losses = train_model(net,
    
        train_dataloader, test_dataloader, criterion,
    
        optimizer, 500)
    
    [epoch 500] Training: loss=0.248 | Test: loss=0.273
    
    Finished Training
    
  4. 将损失绘制为与纪元相关的函数,分别针对训练集和测试集:

    plt.plot(train_losses, label='train')
    
    plt.plot(test_losses, label='test')
    
    plt.xlabel('epoch')
    
    plt.ylabel('loss (MSE)')
    
    plt.legend()
    
    plt.show()
    

这是其绘图:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/B19629_07_09.jpg

图 7.9 – 平均平方误差损失与纪元的关系(注意训练集和测试集几乎重叠)

如我们所见,即使经过许多纪元,这次也没有明显的过拟合:无论纪元数量如何(除了少数噪声波动),训练集和测试集的损失保持接近,尽管随着时间推移,似乎会出现少量过拟合。

  1. 让我们再次使用 R2 分数评估模型在训练集和测试集上的表现:

    # Compute the predictions with the trained neural network
    
    y_train_pred = net(
    
        torch.tensor((X_train))).detach().numpy()
    
    y_test_pred = net(
    
        torch.tensor((X_test))).detach().numpy()
    
    # Compute the R2-score
    
    print('R2-score on training set:', r2_score(y_train,
    
        y_train_pred))
    
    print('R2-score on test set:', r2_score(y_test,
    
        y_test_pred))
    

以下是此代码的典型输出:

R2-score on training set: 0.8161885562733123
R2-score on test set: 0.7906037325658601

虽然训练集上的 R2 分数从 0.99 降到了 0.81,但测试集上的得分从 0.76 提高到了 0.79,有效地提高了模型的性能。

即使这是一个相当极端的例子,整体思路仍然是成立的。

注意

在这种情况下,提前停止(early stopping)也可能效果很好。这两种技术(提前停止和缩小网络)并不是相互排斥的,实际上可以很好地协同工作。

还有更多…

模型的复杂度可以通过参数数量来计算。即使这不是一个直接的度量,它仍然是一个良好的指示器。

例如,本食谱中使用的第一个神经网络,具有 10 个隐藏层和 128 个单元,共有 67,329 个可训练参数。另一方面,第二个神经网络,只有 2 个隐藏层和 16 个单元,仅有 433 个可训练参数。

全连接神经网络的参数数量是基于单元数量和层数的:不过,单元数和层数并不直接决定参数的数量。

要计算 torch 网络中可训练参数的数量,我们可以使用以下代码片段:

sum(p.numel() for p in net.parameters() if p.requires_grad)

为了更好地理解,让我们再看三个神经网络的例子,这三个网络的神经元数量相同,但层数不同。假设它们都具有 10 个输入特征和 1 个单元输出层:

  • 一个具有 1 个隐藏层和 100 个单元的神经网络:1,201 个参数

  • 一个具有 2 个隐藏层和 50 个单元的神经网络:3,151 个参数

  • 一个具有 10 个隐藏层和 10 个单元的神经网络:1,111 个参数

因此,在层数和每层单元数之间存在权衡,以便在给定神经元数量的情况下构建最复杂的神经网络。

使用丢弃法进行正则化

一种广泛使用的正则化方法是丢弃法(dropout)。丢弃法就是在训练阶段随机将一些神经元的激活值设置为零。让我们首先回顾一下它是如何工作的,然后将其应用于多类分类任务——sklearn数字数据集,这是 MNIST 数据集的一个较旧且较小的版本。

准备就绪

丢弃法是深度学习中广泛采用的正则化方法,因其简单有效而受到青睐。这种技术易于理解,但能够产生强大的效果。

原理很简单——在训练过程中,我们随机忽略一些单元,将它们的激活值设置为零,正如图 7.10中所示:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/B19629_07_10.jpg

图 7.10 – 左侧是一个标准的神经网络及其连接,右侧是同一个神经网络应用丢弃法后,训练时平均有 50%的神经元被忽略

但是,dropout 添加了一个超参数:dropout 概率。对于 0% 的概率,没有 dropout;对于 50% 的概率,约 50% 的神经元将被随机选择忽略;对于 100% 的概率,嗯,那就没有东西可学了。被忽略的神经元并不总是相同的:对于每个新的批量大小,都会随机选择一组新的单元进行忽略。

注意

剩余的激活值会被缩放,以保持每个单元的一致全局输入。实际上,对于 1/2 的 dropout 概率,所有未被忽略的神经元都会被缩放 2 倍(即它们的激活值乘以 2)。

当然,在评估或对新数据进行推理时,dropout 会被停用,导致所有神经元都被激活。

但是这样做的意义何在呢?为什么随机忽略一些神经元会有帮助呢?一个正式的解释超出了本书的范围,但至少我们可以提供一些直观的理解。这个想法是避免给神经网络提供过多信息而导致混淆。作为人类,信息过多有时比有帮助更多:有时,信息更少反而可以帮助你做出更好的决策,避免被信息淹没。这就是 dropout 的思想:不是一次性给网络所有信息,而是通过在短时间内随机关闭一些神经元,以较少的信息来温和训练网络。希望这能最终帮助网络做出更好的决策。

在本教程中,将使用 scikit-learn 的 digits 数据集,该数据集实际上是 光学手写数字识别 数据集的一个链接。这些图像的一个小子集展示在 图 7.11 中。

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/B19629_07_11.jpg

图 7.11 – 数据集中的一组图像及其标签:每个图像由 8x8 像素组成

每张图像都是一个 8x8 像素的手写数字图像。因此,数据集由 10 个类别组成,每个类别代表一个数字。

要运行本教程中的代码,所需的库是 sklearnmatplotlibtorch。可以通过 pip install sklearnmatplotlibtorch 安装这些库。

如何做……

本教程将包括两个步骤:

  1. 首先,我们将训练一个没有 dropout 的神经网络,使用一个相对较大的模型,考虑到数据的特点。

  2. 然后,我们将使用 dropout 训练相同的神经网络,希望能提高模型的性能。

我们将使用相同的数据、相同的批量大小和相同的训练轮次,以便比较结果。

没有 dropout

下面是没有 dropout 的正则化步骤:

  1. 必须加载以下导入:

    • 使用 sklearn 中的 load_digits 加载数据集

    • 使用 sklearn 中的 train_test_split 来拆分数据集

    • torchtorch.nntorch.nn.functional 用于神经网络

    • DatasetDataLoader 来自 torch,用于在 torch 中加载数据集

    • matplotlib用于可视化损失

这是import语句的代码:

import numpy as np
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader
import matplotlib.pyplot as plt
  1. 加载数据。数据集包含 1,797 个样本,图像已经展平为 64 个值,范围从 0 到 16,对应 8x8 像素:

    X, y = load_digits(return_X_y=True)
    
  2. 将数据拆分为训练集和测试集,80%的数据用于训练集,20%的数据用于测试集。特征值转换为float32,标签值转换为int64,以避免后续torch错误:

    X_train, X_test, y_train, y_test = train_test_split(
    
        X.astype(np.float32), y.astype(np.int64),
    
        test_size=0.2, random_state=0)
    
  3. 为 PyTorch 创建DigitsDataset类。除了将特征转换为torch张量外,唯一的转换操作是将值除以 255,以使特征的范围落在[0, 1]之间:

    class DigitsDataset(Dataset):
    
        def __init__(self, X: np.array, y: np.array):
    
            self.X = torch.from_numpy(X/255)
    
            self.y = torch.from_numpy(y)
    
        def __len__(self) -> int:
    
            return len(self.X)
    
        def __getitem__(self, idx: int) -> tuple[torch.Tensor]:
    
            return self.X[idx], self.y[idx]
    
  4. 为训练集和测试集实例化数据集,并使用批次大小64实例化数据加载器:

    # Instantiate datasets
    
    training_data = DigitsDataset(X_train, y_train)
    
    test_data = DigitsDataset(X_test, y_test)
    
    # Instantiate data loaders
    
    train_dataloader = DataLoader(training_data,
    
        batch_size=64, shuffle=True)
    
    test_dataloader = DataLoader(test_data, batch_size=64,
    
        shuffle=True)
    
  5. 定义神经网络架构——这里有 3 个隐藏层,每层有 128 个单元(默认为 128),并且对所有隐藏层应用了 25%的丢弃概率:

    class Net(nn.Module):
    
        def __init__(self, input_shape: int,
    
            hidden_units: int = 128,
    
            dropout: float = 0.25):
    
                super(Net, self).__init__()
    
                self.hidden_units = hidden_units
    
                self.fc1 = nn.Linear(input_shape,
    
                    self.hidden_units)
    
                self.fc2 = nn.Linear(self.hidden_units,
    
                    self.hidden_units)
    
                self.fc3 = nn.Linear(self.hidden_units,
    
                    self.hidden_units)
    
                self.dropout = nn.Dropout(p=dropout)
    
                self.output = nn.Linear(self.hidden_units, 10)
    
        def forward(self, x: torch.Tensor) -> torch.Tensor:
    
            x = self.fc1(x)
    
            x = F.relu(x)
    
            x = self.dropout(x)
    
            x = self.fc2(x)
    
            x = F.relu(x)
    
            x = self.dropout(x)
    
            x = self.fc3(x)
    
            x = F.relu(x)
    
            x = self.dropout(x)
    
            output = torch.softmax(self.output(x), dim=1)
    
            return output
    

这里,丢弃操作分两步添加:

  • 在构造函数中实例化一个nn.Dropout(p=dropout)类,传入丢弃概率

  • 在每个隐藏层的激活函数之后应用丢弃层(在构造函数中定义),x = self.dropout(x)

注意

对于 ReLU 激活函数来说,将丢弃层设置在激活函数之前或之后不会改变输出。但对于其他激活函数,如 sigmoid,这会产生不同的结果。

  1. 使用正确的输入形状64(8x8 像素)实例化模型,并且由于我们希望先检查没有丢弃的结果,所以设置丢弃率为0。检查前向传播是否在给定的随机张量上正常工作:

    # Instantiate the model
    
    net = Net(X_train.shape[1], dropout=0)
    
    # Generate randomly one random 28x28 image as a 784 values tensor
    
    random_data = torch.rand((1, 64))
    
    result = net(random_data)
    
    print('Resulting output tensor:', result)
    
    print('Sum of the output tensor:', result.sum())
    

这段代码的输出应该如下所示:

Resulting output tensor: tensor([[0.0964, 0.0908, 0.1043, 0.1083, 0.0927, 0.1047, 0.0949, 0.0991, 0.1012,
         0.1076]], grad_fn=<SoftmaxBackward0>)
Sum of the output tensor: tensor(1., grad_fn=<SumBackward0>)
  1. 将损失函数定义为交叉熵损失,并将优化器设置为Adam

    criterion = nn.CrossEntropyLoss()
    
    optimizer = torch.optim.Adam(net.parameters(), lr=0.001)
    
  2. 使用 GitHub 仓库中可用的train_model函数,在 500 个周期内训练神经网络。每个周期,我们都存储并计算训练集和测试集的损失和精度:

    train_losses, test_losses, train_accuracy,
    
        test_accuracy = train_model(
    
            net, train_dataloader, test_dataloader,
    
            criterion, optimizer, epochs=500
    
    )
    

在 500 个周期后,你应该得到如下输出:

[epoch 500] Training: loss=1.475 accuracy=0.985 |       Test: loss=1.513 accuracy=0.947
Finished Training
  1. 绘制训练集和测试集的交叉熵损失与周期数的关系:

    plt.plot(train_losses, label='train')
    
    plt.plot(test_losses, label='test')
    
    plt.xlabel('epoch')
    
    plt.ylabel('loss (CE)')
    
    plt.legend()
    
    plt.show()
    

这里是它的图示:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/B19629_07_12.jpg

图 7.12 – 交叉熵损失作为周期的函数(注意训练集和测试集之间的轻微偏差)

  1. 绘制精度图将展示等效结果:

    plt.plot(train_accuracy, label='train')
    
    plt.plot(test_accuracy, label='test')
    
    plt.xlabel('epoch')
    
    plt.ylabel('Accuracy')
    
    plt.legend()
    
    plt.show()
    

这里是它的图示:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/B19629_07_13.jpg

图 7.13 – 精度作为周期的函数;我们再次可以看到过拟合现象

最终精度在训练集上大约为 98%,而在测试集上仅为 95%左右,显示出过拟合现象。现在我们尝试添加丢弃层来减少过拟合。

使用丢弃层

在这部分,我们将简单地从步骤 7开始,但使用丢弃层,并与之前的结果进行比较:

  1. 64作为输入共享,25%的 dropout 概率实例化模型。25%的概率意味着在训练过程中,在每一层隐藏层中,大约会有 32 个神经元被随机忽略。重新实例化一个新的优化器,依然使用Adam

    # Instantiate the model
    
    net = Net(X_train.shape[1], dropout=0.25)
    
    optimizer = torch.optim.Adam(net.parameters(), lr=0.001)
    
  2. 再次训练神经网络 500 个周期,同时记录训练和测试的损失值及准确度:

    train_losses, test_losses, train_accuracy, test_accuracy = train_model(
    
        net, train_dataloader, test_dataloader, criterion,
    
            optimizer, epochs=500
    
    )
    
    [epoch 500] Training: loss=1.472 accuracy=0.990 |       Test: loss=1.488 accuracy=0.975
    
    Finished Training
    
  3. 再次绘制训练和测试损失随周期变化的图表:

    plt.plot(train_losses, label='train')
    
    plt.plot(test_losses, label='test')
    
    plt.xlabel('epoch')
    
    plt.ylabel('loss (CE)')
    
    plt.legend()
    
    plt.show()
    

这是它的图表:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/B19629_07_14.jpg

图 7.14 – 交叉熵损失随周期变化,得益于 dropout 减少了发散

我们在这里观察到与之前不同的行为。训练和测试损失似乎不会随着周期的增加而相差太多。在最初的 100 个周期中,测试损失略低于训练损失,但之后训练损失进一步减少,表明模型轻微过拟合。

  1. 最后,绘制训练和测试准确度随周期变化的图表:

    plt.plot(train_accuracy, label='train')
    
    plt.plot(test_accuracy, label='test')
    
    plt.xlabel('epoch')
    
    plt.ylabel('Accuracy')
    
    plt.legend()
    
    plt.show()
    

这是它的图表:

https://github.com/OpenDocCN/freelearn-dl-pt5-zh/raw/master/docs/rgl-cb/img/B19629_07_15.jpg

图 7.15 – 准确度随周期变化(得益于 dropout,过拟合大幅度减少)

我们的训练准确度达到了 99%,相比之前的 98%有所提升。更有趣的是,测试准确度也从之前的 95%上升到 97%,有效地实现了正则化并减少了过拟合。

还有更多…

尽管 dropout 并非万无一失,但它已被证明是一种有效的正则化技术,尤其是在对小数据集进行大规模网络训练时。有关更多内容,可以参考 Hinton 等人发表的论文《通过防止特征检测器的共适应来改进神经网络》。这篇论文可以在arxiv上找到:arxiv.org/abs/1207.0580

另见

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值