PyTorch-现代计算机视觉第二版-二-

74 阅读31分钟

PyTorch 现代计算机视觉第二版(二)

原文:zh.annas-archive.org/md5/355d709877e6e04dc1540c8ccd0b447d

译者:飞龙

协议:CC BY-NC-SA 4.0

第三章:使用 PyTorch 构建深度神经网络

在前一章中,我们学习了如何使用 PyTorch 编写神经网络。我们还了解了神经网络中存在的各种超参数,如批量大小、学习率和损失优化器。在本章中,我们将转变方向,学习如何使用神经网络进行图像分类。基本上,我们将学习如何表示图像并调整神经网络的超参数以理解它们的影响。

为了不引入过多复杂性和混乱,我们仅在上一章中涵盖了神经网络的基本方面。但是,在训练网络时,我们调整的输入还有很多。通常,这些输入称为超参数。与神经网络中的参数(在训练过程中学习的)相反,超参数是由构建网络的人提供的。更改每个超参数的不同方面可能会影响训练神经网络的准确性或速度。此外,一些额外的技术,如缩放、批量归一化和正则化,有助于提高神经网络的性能。我们将在本章中学习这些概念。

但在此之前,我们将学习图像的表示方法:只有这样,我们才能深入探讨超参数的细节。在学习超参数影响时,我们将限制自己使用一个数据集:Fashion MNIST(有关数据集的详细信息可以在 github.com/zalandoresearch/fashion-mnist 找到),以便我们可以比较不同超参数变化对准确性的影响。通过这个数据集,我们还将介绍训练和验证数据的概念,以及为什么有两个单独的数据集是重要的。最后,我们将学习神经网络过拟合的概念,然后了解某些超参数如何帮助我们避免过拟合。

总之,在本章中,我们将涵盖以下主题:

  • 表示图像

  • 为何利用神经网络进行图像分析?

  • 为图像分类准备数据

  • 训练神经网络

  • 缩放数据集以提高模型准确性

  • 理解批量大小变化的影响

  • 理解损失优化器变化的影响

  • 理解学习率变化的影响

  • 构建更深层次的神经网络

  • 理解批量归一化的影响

  • 过拟合的概念

让我们开始吧!

本章中的所有代码可以在本书 GitHub 仓库的 Chapter03 文件夹中查阅,链接为 bit.ly/mcvp-2e

我们在 GitHub 仓库的相关代码中已经覆盖了学习率变化的影响。

表示图像

数字图像文件(通常与扩展名“JPEG”或“PNG”相关联)由像素数组组成。 像素是图像的最小构成元素。 在灰度图像中,每个像素是介于0255之间的标量(单一)值:0 代表黑色,255 代表白色,介于两者之间的是灰色(像素值越小,像素越暗)。 另一方面,彩色图像中的像素是三维向量,对应于其红、绿和蓝通道中的标量值。

一个图像有 height x width x c 个像素,其中 height 是像素的行数width 是像素的列数c通道数。 对于彩色图像,c3(分别对应图像的红色、绿色和蓝色强度的一个通道),而对于灰度图像,c1。 这里展示了一个包含 3 x 3 像素及其对应标量值的灰度图像示例:

包含文本、障子、填字游戏的图片

图 3.1: 图像表示

再次强调,像素值为 0 意味着它是纯黑色,而 255 表示纯亮度(即灰度图像的纯白色和彩色图像中相应通道的纯红/绿/蓝色)。

将图像转换为结构化数组和标量

Python 可以将图像转换为结构化数组和标量,具体如下:

可在 GitHub 的 Chapter03 文件夹中找到 Inspecting_grayscale_images.ipynb 文件中的以下代码:bit.ly/mcvp-2e

  1. 下载一个样本图像或上传您自己的自定义图像:

    !wget https://www.dropbox.com/s/l98lee/Hemanvi.jpeg 
    
  2. 导入 cv2(从磁盘读取图像)和 matplotlib(绘制加载的图像)库,并将下载的图像读入 Python 环境:

    %matplotlib inline
    import cv2, matplotlib.pyplot as plt
    img = cv2.imread('Hemanvi.jpeg') 
    

在前述代码行中,我们利用 cv2.imread 方法读取图像。 这将图像转换为像素值数组。

  1. 我们将裁剪图像,从第 50 行到第 250 行,以及从第 40 列到第 240 列。 最后,我们将使用以下代码将图像转换为灰度并绘制它:

    # Crop image
    img = img[50:250,40:240]
    # Convert image to grayscale
    img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    # Show image
    plt.imshow(img_gray, cmap='gray') 
    

上述步骤的输出如下所示:

包含日历的图片

图 3.2: 裁剪后的图像

您可能已经注意到,前述图像被表示为一个 200 x 200 的像素数组。 现在,让我们减少用于表示图像的像素数量,以便我们可以在图像上叠加像素值(与在 200 x 200 数组上可视化像素值相比,在 25 x 25 数组上更难实现此目标)。

  1. 将图像转换为一个 25 x 25 的数组并绘制它:

    img_gray_small = cv2.resize(img_gray,(25,25))
    plt.imshow(img_gray_small, cmap='gray') 
    

这将产生以下输出:

图表

图 3.3: 调整大小后的图像

自然地,使用较少像素来表示相同图像会产生模糊的输出。

  1. 让我们检查像素值。请注意,由于空间限制,以下输出只粘贴了前四行像素值:

    print(img_gray_small) 
    

这导致以下输出:

计算机屏幕截图的说明(低置信度自动生成)

图 3.4:输入图像的像素值

将相同的像素值集合复制并粘贴到 MS Excel 中,并按像素值进行颜色编码,效果如下:

文本的说明(自动生成)

图 3.5:图像对应的像素值

正如我们之前提到的,像素的标量值接近 255 的显示更浅,接近 0 的则显示更暗。

为彩色图像创建一个结构化数组

前面的步骤同样适用于彩色图像,其表示为三维向量。最亮的红色像素表示为(255,0,0)。同样,三维向量图像中的纯白色像素表示为(255,255,255)。有了这些基础知识,让我们为彩色图像创建一个结构化的像素值数组:

可以在 GitHub 上的 Chapter03 文件夹中的 Inspecting_color_images.ipynb 文件中找到以下代码:bit.ly/mcvp-2e

  1. 下载一幅彩色图像:

    !wget https://www.dropbox.com/s/l98lee/Hemanvi.jpeg 
    
  2. 导入相关包并加载图像:

    import cv2, matplotlib.pyplot as plt
    %matplotlib inline
    img = cv2.imread('Hemanvi.jpeg') 
    
  3. 裁剪图像:

    img = img[50:250,40:240,:]
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) 
    

请注意,在上述代码中,我们使用了 cv2.cvtcolor 方法重新排序了通道。我们这样做是因为当使用 cv2 导入图像时,通道的顺序是蓝色首先,然后是绿色,最后是红色;通常,我们习惯于查看 RGB 通道的图像,其中顺序是红色、绿色,最后是蓝色。

  1. 绘制获得的图像:

    plt.imshow(img)
    print(img.shape)
    # (200,200,3) 
    

这导致以下输出(请注意,如果您正在阅读印刷版的书籍,并且尚未下载彩色图像包,则以下图像将以灰度显示):

图表的说明(自动生成)

图 3.6:RGB 格式的图像

  1. 可以按以下步骤获取右下角的 3 x 3 像素阵列:

    crop = img[-3:,-3:] 
    
  2. 打印并绘制像素值:

    print(crop)
    plt.imshow(crop) 
    

前述代码的输出如下:

图表的说明(自动生成)

图 3.7:图像一个区域的 RGB 值

现在我们已经学会如何将图像(即计算机上的文件)表示为张量,我们现在可以学习各种数学运算和技术,利用这些张量执行任务,如图像分类、目标检测、图像分割等,本书中的许多任务。

但首先,让我们了解为何人工神经网络ANNs)在图像分析中很有用。

为何要利用神经网络进行图像分析?

在传统的计算机视觉中,我们会在使用图像作为输入之前为每个图像创建一些特征。让我们看一些基于以下示例图像的这些特征,以便体会通过训练神经网络节省的努力:

包含文本的图片的描述 自动生成

图 3.8:从图像中生成的一部分特征

注意,我们不会详细介绍如何获取这些特征,因为这里的意图是帮助您意识到手动创建特征是一种次优的练习。但是,您可以在docs.opencv.org/4.x/d7/da8/tutorial_table_of_content_imgproc.html了解各种特征提取方法:

  • 直方图特征:对于某些任务,如自动亮度或夜视,理解图片中的照明情况是很重要的:即明亮或黑暗像素的比例。

  • 边缘和角落特征:对于诸如图像分割的任务,重要的是找到与每个人对应的像素集合,因此首先提取边缘是有意义的,因为人的边界只是边缘的集合。在其他任务中,如图像配准,检测关键地标是至关重要的。这些地标将是图像中所有角落的子集。

  • 颜色分离特征:在自动驾驶汽车的任务中,如交通灯检测,系统理解交通灯显示的颜色是非常重要的。

  • 图像梯度特征:进一步探讨颜色分离特征,理解像素级别的颜色变化可能很重要。不同的纹理可以给我们不同的梯度,这意味着它们可以用作纹理检测器。事实上,找到梯度是边缘检测的先决条件。

这些只是少数这类特征中的一部分。还有许多其他特征,涵盖它们是困难的。创建这些特征的主要缺点是,您需要成为图像和信号分析的专家,并且应该充分理解哪些特征最适合解决问题。即使两个条件都满足,也不能保证这样的专家能够找到正确的输入组合,即使找到了,也不能保证这样的组合在新的未见场景中能够有效工作。

由于这些缺点,社区主要转向基于神经网络的模型。这些模型不仅可以自动找到正确的特征,还可以学习如何最优地组合它们来完成工作。正如我们在第一章中已经看到的,神经网络既是特征提取器,也是分类器。

现在我们已经看过一些历史特征提取技术及其缺点的示例,让我们学习如何在图像上训练神经网络。

准备我们的数据用于图像分类

鉴于本章涵盖多种场景,为了看到一个场景比另一个场景的优势,我们将在本章中专注于一个数据集:Fashion MNIST 数据集,其中包含 10 种不同类别的服装图片(衬衫、裤子、鞋子等)。让我们准备这个数据集:

下面的代码可以在 GitHub 上的 Chapter03 文件夹中的 Preparing_our_data.ipynb 文件中找到,网址为 bit.ly/mcvp-2e

  1. 开始下载数据集并导入相关包。torchvision 包含各种数据集,其中之一是 FashionMNIST 数据集,在本章中我们将对其进行处理:

    from torchvision import datasets
    import torch
    data_folder = '~/data/FMNIST' # This can be any directory
    # you want to download FMNIST to
    fmnist = datasets.FashionMNIST(data_folder, download=True, train=True) 
    

在上述代码中,我们指定了要存储下载数据集的文件夹(data_folder)。然后,我们从 datasets.FashionMNIST 获取 fmnist 数据并将其存储在 data_folder 中。此外,我们指定只下载训练图像,通过指定 train = True

  1. 接下来,我们必须将 fmnist.data 中可用的图像存储为 tr_images,将 fmnist.targets 中可用的标签(目标)存储为 tr_targets

    tr_images = fmnist.data
    tr_targets = fmnist.targets 
    
  2. 检查我们正在处理的张量:

    unique_values = tr_targets.unique()
    print(f'tr_images & tr_targets:\n\tX -{tr_images.shape}\n\tY \
    -{tr_targets.shape}\n\tY-Unique Values : {unique_values}')
    print(f'TASK:\n\t{len(unique_values)} class Classification')
    print(f'UNIQUE CLASSES:\n\t{fmnist.classes}') 
    

上述代码的输出如下:

Chart  Description automatically generated with low confidence

图 3.9:输入和输出形状以及唯一类别

在这里,我们可以看到有 60,000 张图片,每张大小为 28 x 28,并且在所有图片中有 10 种可能的类别。请注意,tr_targets 包含每个类别的数值,而 fmnist.classes 给出与 tr_targets 中每个数值对应的名称。

  1. 为所有 10 种可能的类别绘制 10 张随机样本的图片:

    1. 导入相关包以绘制图像网格,以便也可以处理数组:
    import matplotlib.pyplot as plt
    %matplotlib inline
    import numpy as np 
    
    1. 创建一个图表,我们可以展示一个 10 x 10 的网格,其中每行网格对应一个类别,每列呈现属于该行类别的示例图片。循环遍历唯一的类别号码(label_class),并获取对应给定类别号码的行索引(label_x_rows):
    R, C = len(tr_targets.unique()), 10
    fig, ax = plt.subplots(R, C, figsize=(10,10))
    for label_class, plot_row in enumerate(ax):
        label_x_rows = np.where(tr_targets == label_class)[0] 
    

    请注意,在上述代码中,我们将 np.where 条件的第 0 个索引作为输出提取出来,因为其长度为 1。它包含所有目标值(tr_targets)等于 label_class 的索引数组。

    1. 循环 10 次以填充给定行的列。此外,我们需要从之前获取的与给定类别对应的索引中选择一个随机值(ix)并绘制它们:
     for plot_cell in plot_row:
            plot_cell.grid(False); plot_cell.axis('off')
            ix = np.random.choice(label_x_rows)
            x, y = tr_images[ix], tr_targets[ix]
            plot_cell.imshow(x, cmap='gray')
    plt.tight_layout() 
    

这导致以下输出:

图 3.10:Fashion MNIST 样本图片

请注意,在上述图片中,每一行代表同一类别的 10 张不同图片的样本。

训练神经网络

要训练神经网络,我们必须执行以下步骤:

  1. 导入相关包

  2. 建立一个可以逐个数据点获取数据的数据集

  3. 从数据集中包装 DataLoader

  4. 建立一个模型,然后定义损失函数和优化器

  5. 分别定义两个函数来训练和验证一批数据

  6. 定义一个计算数据准确率的函数

  7. 根据每个数据批次进行权重更新,逐渐增加 epochs

在接下来的代码行中,我们将执行以下每个步骤:

以上代码可以在 GitHub 的Chapter03文件夹中的Steps_to_build_a_neural_network_on_FashionMNIST.ipynb文件中找到,位于bit.ly/mcvp-2e

  1. 导入相关的包和fmnist数据集:

    from torch.utils.data import Dataset, DataLoader
    import torch
    import torch.nn as nn
    import numpy as np
    import matplotlib.pyplot as plt
    %matplotlib inline
    device = "cuda" if torch.cuda.is_available() else "cpu"
    from torchvision import datasets
    data_folder = '~/data/FMNIST' # This can be any directory you
    # want to download FMNIST to
    fmnist = datasets.FashionMNIST(data_folder, download=True, train=True)
    tr_images = fmnist.data
    tr_targets = fmnist.targets 
    
  2. 建立一个获取数据集的类。请记住,它是从Dataset类派生的,并需要定义三个魔法函数__init____getitem____len__

    class FMNISTDataset(Dataset):
        def __init__(self, x, y):
            x = x.float()
            x = x.view(-1,28*28)
            self.x, self.y = x, y
        def __getitem__(self, ix):
            x, y = self.x[ix], self.y[ix]
            return x.to(device), y.to(device)
        def __len__(self):
            return len(self.x) 
    

注意,在__init__方法中,我们将输入转换为浮点数,并将每个图像展平为 28*28 = 784 个数值(其中每个数值对应一个像素值)。在__len__方法中,我们还指定数据点的数量;这里是x的长度。__getitem__方法包含了当我们请求第ix个数据点时应该返回什么的逻辑(ix将是一个介于0__len__之间的整数)。

  1. 创建一个函数,从名为FMNISTDataset的数据集生成一个训练 DataLoader,称为trn_dl。这将随机抽样 32 个数据点作为批量大小:

    **def****get_data****():**
        train = FMNISTDataset(tr_images, tr_targets)
        trn_dl = DataLoader(train, batch_size=32, shuffle=True)
        return trn_dl 
    
  2. 定义一个模型,以及损失函数和优化器:

    from torch.optim import SGD
    **def****get_model****():**
        model = nn.Sequential(
                    nn.Linear(28 * 28, 1000),
                    nn.ReLU(),
                    nn.Linear(1000, 10)
                ).to(device)
        loss_fn = nn.CrossEntropyLoss()
        optimizer = SGD(model.parameters(), lr=1e-2)
        return model, loss_fn, optimizer 
    

该模型是一个含有 1,000 个神经元的隐藏层网络。输出是一个 10 个神经元的层,因为有 10 个可能的类。此外,我们调用CrossEntropyLoss函数,因为输出可以属于每个图像的 10 个类中的任意一个。最后,本练习中需要注意的关键方面是,我们已将学习率lr初始化为0.01,而不是默认值0.001,以查看模型在此练习中的学习情况。

注意,我们在神经网络中根本不使用“softmax”。输出的范围是不受限制的,即值可以具有无限的范围,而交叉熵损失通常期望输出为概率(每行应该总和为1)。在这种情况下,输出中的无约束值仍然可以工作,因为nn.CrossEntropyLoss实际上期望我们发送原始 logits(即无约束值)。它在内部执行 softmax。

  1. 定义一个函数,将数据集训练到一批图像上:

    **def****train_batch****(****x, y, model, opt, loss_fn****):**
        **model.train()** # <- let's hold on to this until we reach
        # dropout section
        # call your model like any python function on your batch
        # of inputs
        **prediction = model(x)**
        # compute loss
        **batch_loss = loss_fn(prediction, y)**
        # based on the forward pass in `model(x)` compute all the
        # gradients of 'model.parameters()'
        **batch_loss.backward()**
        # apply new-weights = f(old-weights, old-weight-gradients)
        # where "f" is the optimizer
        **optimizer.step()**
        # Flush gradients memory for next batch of calculations
     **optimizer.zero_grad()**
    **return** **batch_loss.item()** 
    

前面的代码在前向传播中将图像批量传递给模型。它还计算批次上的损失,然后通过反向传播传递权重并更新它们。最后,它清除梯度的内存,以免影响下一次传播中的梯度计算方式。

现在我们完成了这些步骤,我们可以通过获取batch_loss.item()上的batch_loss来提取损失值作为标量。

  1. 构建一个计算给定数据集准确率的函数:

    # since there's no need for updating weights,
    # we might as well not compute the gradients.
    # Using this '@' decorator on top of functions
    # will disable gradient computation in the entire function
    **@torch.no_grad()**
    **def****accuracy****(****x, y, model****):**
        **model.****eval****()** # <- let's wait till we get to dropout
        # section
        # get the prediction matrix for a tensor of `x` images
        **prediction = model(x)**
        # compute if the location of maximum in each row
        # coincides with ground truth
        **max_values, argmaxes = prediction.****max****(-****1****)**
        **is_correct = argmaxes == y**
        **return** **is_correct.cpu().numpy().tolist()** 
    

在前述代码中,我们明确提到通过使用@torch.no_grad()并计算prediction值,通过模型进行前向传播,我们不需要计算梯度。接下来,我们调用prediction.max(-1)来识别每行对应的argmax索引。然后,我们通过argmaxes == y来将我们的argmaxes与基准真实值进行比较,以便检查每行是否被正确预测。最后,我们在将其转移到 CPU 并转换为 NumPy 数组后返回is_correct对象列表。

  1. 使用以下代码行训练神经网络:

    1. 初始化模型、损失、优化器和 DataLoaders:
    trn_dl = get_data()
    model, loss_fn, optimizer = get_model() 
    
    1. 初始化将在每个 epoch 结束时包含准确率和损失值的列表:
    losses, accuracies = [], [] 
    
    1. 定义 epoch 的数量:
    for epoch in range(5):
        print(epoch) 
    
    1. 初始化将包含每个 epoch 内每个批次的准确率和损失值的列表:
     epoch_losses, epoch_accuracies = [], [] 
    
    1. 通过遍历 DataLoader 来创建训练数据的批次:
     for ix, batch in enumerate(iter(trn_dl)):
            x, y = batch 
    
    1. 使用train_batch函数训练批次,并将训练结束时的损失值存储在batch_loss的顶部。此外,将跨批次的损失值存储在epoch_losses列表中:
     batch_loss = train_batch(x, y, model,optimizer, loss_fn)
            epoch_losses.append(batch_loss) 
    
    1. 我们存储每个 epoch 内所有批次的平均损失值:
     epoch_loss = np.array(epoch_losses).mean() 
    
    1. 接下来,在所有批次训练结束时计算预测的准确率:
     for ix, batch in enumerate(iter(trn_dl)):
            x, y = batch
            is_correct = accuracy(x, y, model)
            epoch_accuracies.extend(is_correct)
        epoch_accuracy = np.mean(epoch_accuracies) 
    
    1. 在列表中存储每个 epoch 结束时的损失和准确率值:
     losses.append(epoch_loss)
        accuracies.append(epoch_accuracy) 
    
    1. 可以使用以下代码显示随着 epoch 增加的训练损失和准确率的变化。
    epochs = np.arange(5)+1
    plt.figure(figsize=(20,5))
    plt.subplot(121)
    plt.title('Loss value over increasing epochs')
    plt.plot(epochs, losses, label='Training Loss')
    plt.legend()
    plt.subplot(122)
    plt.title('Accuracy value over increasing epochs')
    plt.plot(epochs, accuracies, label='Training Accuracy')
    plt.gca().set_yticklabels(['{:.0f}%'.format(x*100) \
                              for x in \ plt.gca().get_yticks()])
    plt.legend() 
    

    前述代码的输出如下:

    计算机截图的描述

    图 3.11:随着 epoch 增加的训练损失和准确率值

我们的训练准确率在五个 epoch 结束时为 12%。请注意,随着 epoch 数量的增加,损失值并没有显著减少。换句话说,无论我们等待多长时间,模型都不太可能提供高准确率(比如超过 80%)。这要求我们了解所使用的各种超参数如何影响神经网络的准确性。

请注意,由于我们在代码开始时没有指定torch.random_seed(0),因此当您执行所提供的代码时,结果可能会有所不同。但是,您得到的结果应该让您得出类似的结论。

现在,您已经全面了解了如何训练神经网络,让我们研究一些应遵循的良好实践以实现良好的模型性能以及使用它们的原因。可以通过微调各种超参数来实现这一目标,其中一些将在接下来的部分中进行介绍。

缩放数据集以提高模型准确性

缩放数据集是确保变量限制在有限范围内的过程。在本节中,我们将通过将每个输入值除以数据集中可能的最大值 255 来将独立变量的值限制在 0 到 1 之间。这对应于白色像素的值255

为简洁起见,我们仅在接下来的代码中提供了修改后的代码(来自前一节)。完整代码可在 GitHub 上的Chapter03文件夹中的Scaling_the_dataset.ipynb文件中找到:bit.ly/mcvp-2e

  1. 获取数据集以及训练图像和目标,就像我们在前一节中所做的那样。

  2. 修改FMNISTDataset,该数据集获取数据,使得输入图像除以 255(像素的最大强度/值):

    class FMNISTDataset(Dataset):
        def __init__(self, x, y):
            **x = x.****float****()/****255**
            x = x.view(-1,28*28)
            self.x, self.y = x, y
        def __getitem__(self, ix):
            x, y = self.x[ix], self.y[ix]
            return x.to(device), y.to(device)
        def __len__(self):
            return len(self.x) 
    

请注意,与上一节相比,我们唯一更改的是将输入数据除以最大可能像素值:255

鉴于像素值范围在0255之间,将它们除以 255 将导致值始终在01之间。

  1. 训练一个模型,就像我们在前一节的步骤 4、5、6 和 7 中所做的那样。训练损失和准确率值的变化如下:

计算机屏幕截图 自动生成的描述

图 3.12:在经过扩展的数据集上随着周期的增加的训练损失和准确率数值

如图所示,训练损失持续降低,训练准确率持续增加,达到约 85%的准确率。与未对输入数据进行缩放的情况相比,训练损失未能持续降低,而五个周期结束时训练数据集的准确率仅为 12%。

让我们深入探讨为什么在这里缩放有帮助的可能原因。我们将以 Sigmoid 值计算的示例为例:

在下表中,我们根据前述公式计算了Sigmoid列。

图 3.13:不同输入和权重值的 Sigmoid 值

在左表中,我们可以看到当权重值大于 0.1 时,Sigmoid 值不随权重值的增加(变化)而变化。此外,当权重非常小时,Sigmoid 值只有少量变化;改变 Sigmoid 值的唯一方法是通过非常小的数值改变权重。

然而,当输入值很小时,右表中的 Sigmoid 值变化很大。

这是因为大的负值的指数(由于将权重值乘以一个大数得出)非常接近于 0,而指数值在权重乘以扩展输入时变化,正如右表所示。

现在我们了解到,除非权重值非常小,否则 sigmoid 值不会有很大变化,我们将学习如何将权重值影响向最优值。

缩放输入数据集,使其包含一个范围更小的值通常有助于实现更好的模型准确性。

接下来,我们将学习神经网络的另一个重要超参数之一的影响:批大小

理解不同批大小的影响

在之前的章节中,训练数据集中每批有 32 个数据点。这导致每个 epoch 有更多的权重更新次数,因为每个 epoch 有 1,875 次权重更新(60,000/32 接近于 1,875,其中 60,000 是训练图像的数量)。

此外,我们没有考虑模型在未见数据集(验证数据集)上的性能。我们将在本节中探讨这一点。

在本节中,我们将比较以下内容:

  • 当训练批大小为 32 时,训练和验证数据的损失值和准确率。

  • 当训练批大小为 10,000 时,训练和验证数据的损失值和准确率。

现在我们已经介绍了验证数据,让我们重新运行构建神经网络部分中提供的代码,增加额外的代码以生成验证数据,并计算验证数据集的损失值和准确率。

为了简洁起见,我们只提供了修改后的代码(来自上一节)在接下来的部分。完整的代码可以在 GitHub 存储库中的Chapter03文件夹中的Varying_batch_size.ipynb文件中找到,链接为bit.ly/mcvp-2e

批大小为 32

鉴于我们已经在前一节中使用批大小为 32 构建了一个模型,现在我们将详细说明用于处理验证数据集的附加代码。我们将跳过训练模型的详细过程,因为这已经在构建神经网络部分中涵盖了。让我们开始吧:

  1. 下载并导入训练图像和目标。

  2. 与训练图像类似,我们必须通过在调用数据集中的FashionMNIST方法时指定train = False来下载和导入验证数据集:

    val_fmnist =datasets.FashionMNIST(data_folder,download=True, train=False)
    val_images = val_fmnist.data
    val_targets = val_fmnist.targets 
    
  3. 导入相关包并定义device

  4. 定义数据集类(FashionMNIST)和将用于批处理数据的函数(train_batch)、计算准确率的函数(accuracy),然后定义模型架构、损失函数和优化器(get_model)。

  5. 定义一个函数,将获取数据,即get_data。该函数将返回批大小为32的训练数据和与验证数据长度相同的验证数据集(我们将不使用验证数据来训练模型;我们只使用它来了解模型在未见数据上的准确性):

    def get_data():
        train = FMNISTDataset(tr_images, tr_targets)
        trn_dl = DataLoader(train, **batch_size=****32**, shuffle=True)
        val = FMNISTDataset(val_images, val_targets)
        val_dl = DataLoader(val, batch_size=len(val_images),
                                                shuffle=False)
        return trn_dl, val_dl 
    

在上述代码中,我们创建了 FMNISTDataset 类的 val 对象,除了之前看到的 train 对象。此外,验证数据的 DataLoader (val_dl) 使用了 val_images 的长度作为批大小,而 trn_dl 的批大小为 32。这是因为训练数据用于训练模型,而我们获取验证数据的准确率和损失指标。在本节和下节中,我们尝试理解基于模型训练时间和准确性变化 batch_size 的影响。

  1. 定义一个函数来计算验证数据的损失值:即 val_loss。请注意,我们单独计算这个,因为在训练模型时计算了训练数据的损失值:

    @torch.no_grad()
    def val_loss(x, y, model):
        model.eval()
        prediction = model(x)
        val_loss = loss_fn(prediction, y)
        return val_loss.item() 
    

如您所见,我们应用 torch.no_grad 因为我们不训练模型,只获取预测。此外,我们通过损失函数 (loss_fn) 传递我们的 prediction 并返回损失值 (val_loss.item())。

  1. 获取训练和验证 DataLoader。同时,初始化模型、损失函数和优化器:

    trn_dl, val_dl = get_data()
    model, loss_fn, optimizer = get_model() 
    
  2. 训练模型如下:

    1. 初始化包含训练和验证准确率以及损失值的列表,随着 epoch 增加逐步记录它们的变化:
    train_losses, train_accuracies = [], []
    val_losses, val_accuracies = [], [] 
    
    1. 在五个 epoch 中循环,并初始化包含给定 epoch 内训练数据批次的准确率和损失的列表:
    for epoch in range(5):
        print(epoch)
        train_epoch_losses, train_epoch_accuracies = [], [] 
    
    1. 在一个 epoch 内,循环遍历训练数据的批次,并计算准确率 (train_epoch_accuracy) 和损失值 (train_epoch_loss):
     for ix, batch in enumerate(iter(trn_dl)):
            x, y = batch
            batch_loss = train_batch(x, y, model,optimizer, loss_fn)
            train_epoch_losses.append(batch_loss)
        train_epoch_loss = np.array(train_epoch_losses).mean()
        for ix, batch in enumerate(iter(trn_dl)):
            x, y = batch
            is_correct = accuracy(x, y, model)
            train_epoch_accuracies.extend(is_correct)
        train_epoch_accuracy = np.mean(train_epoch_accuracies) 
    
    1. 计算验证数据一批次内的损失值和准确率(因为验证数据的批大小等于验证数据的长度):
     for ix, batch in enumerate(iter(val_dl)):
            x, y = batch
            val_is_correct = accuracy(x, y, model)
            validation_loss = val_loss(x, y, model)
        val_epoch_accuracy = np.mean(val_is_correct) 
    
    1. 请注意,在上述代码中,验证数据的损失值是使用 val_loss 函数计算的,并存储在 validation_loss 变量中。此外,所有验证数据点的准确率存储在 val_is_correct 列表中,而其平均值存储在 val_epoch_accuracy 变量中。

    2. 最后,我们将训练和验证数据集的准确率和损失值附加到包含 epoch 级别聚合验证和准确率值的列表中。我们这样做是为了在下一步中查看 epoch 级别的改进:

     train_losses.append(train_epoch_loss)
        train_accuracies.append(train_epoch_accuracy)
        val_losses.append(validation_loss)
        val_accuracies.append(val_epoch_accuracy) 
    
  3. 可视化训练和验证数据集在增加的 epoch 中准确率和损失值的改善:

图表,线图 自动生成的描述

图 3.14: 随着批大小为 32 增加的情况下的训练和验证损失及准确率

如您所见,当批大小为 32 时,训练和验证准确率在五个 epoch 结束时达到约 85%。接下来,我们将在 get_data 函数中训练 DataLoader 时变化 batch_size 参数,以查看其对五个 epoch 结束时准确率的影响。

批大小为 10,000

在本节中,我们将每批使用 10,000 个数据点,以便了解变化 batch_size 的影响。

请注意,“批量大小为 32”的部分提供的代码在此处保持完全相同,除了“步骤 5”中的代码,我们将指定批量大小为 10,000。在执行代码时,请参考此书的 GitHub 仓库中提供的相应笔记本。

通过在“步骤 5”中仅进行这一必要更改,并在执行所有步骤直到“步骤 9”后,当批量大小为 10,000 时,随着增加时期的训练和验证准确率和损失的变化如下:

图表,折线图 描述自动生成

图 3.15:使用批量大小为 10,000 的增加时期的训练和验证损失和准确率

在这里,我们可以看到准确率和损失值未达到前一个场景的相同水平,其中批量大小为 32,因为每个时期更新权重的次数较少(6 次),而批量大小为 32 为 1,875 次。

当你只有少量时期时,较低的批量大小通常有助于实现最佳精度,但不应过低以至于影响训练时间。

到目前为止,我们已经学习了如何缩放数据集,以及在模型训练时间内通过改变批量大小来达到特定精度的影响。在接下来的部分中,我们将学习如何在相同数据集上改变损失优化器的影响。

理解不同损失优化器的影响

到目前为止,我们已经基于 Adam 优化器优化了损失。损失优化器有助于确定最优权重值以最小化总体损失。有多种损失优化器(不同的更新权重值以最小化损失值的方式)会影响模型的整体损失和准确率。在本节中,我们将执行以下操作:

  1. 修改优化器,使其成为随机梯度下降SGD)优化器

  2. 在 DataLoader 中获取数据时恢复为批量大小为 32

  3. 将时期数增加到 10(这样我们可以比较 SGD 和 Adam 在更长时期内的表现)

这些更改意味着只有“批量大小为 32”的部分中的一个步骤会改变(因为该部分的批量大小已经为 32);也就是说,我们将修改优化器,使其成为 SGD 优化器。

让我们修改“批量大小为 32”的“步骤 4”中的get_model函数,以修改优化器,使其使用 SGD 优化器,如下所示:

完整的代码可以在 GitHub 上的Chapter03文件夹中的Varying_loss_optimizer.ipynb文件中找到,链接为bit.ly/mcvp-2e。为了简洁起见,我们不会详细说明“批量大小为 32”的每一个步骤;相反,在接下来的代码中,我们将仅讨论引入更改的步骤。在执行代码时,建议您参考 GitHub 上的相应笔记本。

  1. 修改优化器,使其在get_model函数中使用 SGD 优化器,同时确保其他所有内容保持不变:

    **from** **torch.optim** **import** **SGD, Adam**
    def get_model():
        model = nn.Sequential(
                    nn.Linear(28 * 28, 1000),
                    nn.ReLU(),
                    nn.Linear(1000, 10)
                ).to(device)
        loss_fn = nn.CrossEntropyLoss()
        optimizer = **SGD**(model.parameters(), lr=1e-2)
        return model, loss_fn, optimizer 
    

现在,在步骤 8 中增加 epoch 数量,同时保持除步骤 4步骤 8 外的所有其他步骤与批大小为 32节相同。

  1. 增加我们将用于训练模型的 epoch 数量。

在进行这些更改后,一旦按顺序执行批大小为 32节中的所有其余步骤,使用 SGD 和 Adam 优化器单独训练时,随着 epoch 增加,训练和验证数据集的准确性和损失值变化如下:

图表,折线图  自动生成的描述

图 3.16:使用 SGD 优化器随着 epoch 增加的训练和验证损失及准确率

图 3.17:使用 Adam 优化器随着 epoch 增加的训练和验证损失及准确率

正如您所看到的,当我们使用 Adam 优化器时,准确率仍非常接近 85%。

注意

某些优化器比其他优化器更快地达到最佳准确率。其他显著的优化器包括 Adagrad、Adadelta、AdamW、LBFGS 和 RMSprop。

到目前为止,在训练我们的模型时,我们使用了学习率为 0.01,并在训练过程中的所有 epoch 中保持不变。在第一章人工神经网络基础中,我们学到学习率在达到最佳权重值时发挥关键作用。在这里,当学习率较小时,权重值逐渐向最优值移动,而当学习率较大时,权重值在非最优值处振荡。

然而,最初权重迅速更新到接近最优状态是直观的。从那时起,它们应该被非常缓慢地更新,因为最初减少的损失量很高,而后续 epoch 中减少的损失量则很低。

这要求初始时具有较高的学习率,并随后逐渐降低,直到模型达到接近最优准确率。这要求我们理解何时必须降低学习率(随时间的学习率退火)。请参阅 GitHub 上Chapter03文件夹中的Learning_rate_annealing.ipynb文件,以了解学习率退火的影响。

要了解学习率变化的影响,以下几种情况将有所帮助:

  • 在经过缩放的数据集上使用较高的学习率(0.1)

  • 在经过缩放的数据集上使用较低的学习率(0.00001)

  • 在未经缩放的数据集上使用较低的学习率(0.00001)

这三种情况不会在本章中讨论;但是,您可以在 GitHub 存储库的Chapter03文件夹中的Varying_learning_rate_on_scaled_data.ipynb文件和Varying_learning_rate_on_non_scaled_data.ipynb文件中获取它们的全部代码,网址为bit.ly/mcvp-2e

在下一节中,我们将学习神经网络中层数如何影响其准确性。

构建更深的神经网络

到目前为止,我们的神经网络架构仅有一个隐藏层。在本节中,我们将对比存在两个隐藏层和没有隐藏层(没有隐藏层时为逻辑回归)的模型性能。

可以构建如下的网络内两个层的模型(请注意,我们将第二个隐藏层的单元数设置为 1,000)。修改后的get_model函数(来自批量大小为 32部分中的代码),其中包含两个隐藏层,如下所示:

完整的代码可以在 GitHub 上的Chapter03文件夹中的Impact_of_building_a_deeper_neural_network.ipynb文件中找到,网址为bit.ly/mcvp-2e。为了简洁起见,我们不会详细描述批量大小为 32部分的每一步。在执行代码时,请参考 GitHub 上的笔记本。

def get_model():
    model = nn.Sequential(
                nn.Linear(28 * 28, 1000),
                nn.ReLU(),
 **nn.Linear(****1000****,** **1000****),**
 **nn.ReLU(),**
                nn.Linear(1000, 10)
            ).to(device)
    loss_fn = nn.CrossEntropyLoss()
    optimizer = Adam(model.parameters(), lr=1e-3)
    return model, loss_fn, optimizer 

同样地,没有隐藏层的get_model函数如下所示:

def get_model():
    model = nn.Sequential(
                **nn.Linear(****28** ***** **28****,** **10****)**
            ).to(device)
    loss_fn = nn.CrossEntropyLoss()
    optimizer = Adam(model.parameters(), lr=1e-3)
    return model, loss_fn, optimizer 

在上述函数中,请注意,我们直接将输入连接到输出层。

当我们像在批量大小为 32部分那样训练模型时,训练和验证数据集上的准确率和损失如下:

自动生成的带有数字说明的数字表格

图 3.18:随着不同隐藏层数量的增加,训练和验证损失以及准确率的变化

在这里,请注意以下内容:

  • 当没有隐藏层时,模型无法学习得很好。

  • 与仅有一个隐藏层的模型相比,当存在两个隐藏层时,模型的过拟合程度更大(验证损失比仅有一个隐藏层的模型更高)。

到目前为止,在不同的部分中,我们发现当输入数据未经过缩放时(未缩小到一个小范围),模型的训练效果并不好。非缩放的数据(具有较大范围的数据)也可能出现在隐藏层中(特别是在具有多个隐藏层的深度神经网络中),这是因为涉及到节点值获取的矩阵乘法。让我们在下一节中了解批量归一化如何帮助处理非缩放数据。

理解批量归一化的影响

我们之前学到,当输入值较大时,当权重值发生显著变化时,Sigmoid 输出的变化不会有太大的差异。

现在,让我们考虑相反的情况,即输入值非常小的情况:

自动生成的带有数字说明的数字表格

图 3.19:不同输入和权重值的 Sigmoid 值

当输入值非常小时,Sigmoid 输出会稍微变化,需要对权重值进行大的改变才能达到最优结果。

另外,在缩放输入数据部分中,我们看到大输入值对训练精度有负面影响。这表明,我们既不能有非常小的输入值,也不能有非常大的输入值。

除了输入中的非常小或非常大的值之外,我们还可能遇到一个情况,即隐藏层中某个节点的值可能会导致一个非常小的数字或一个非常大的数字,这会导致与连接隐藏层到下一层的权重之前看到的问题相同。在这种情况下,批次归一化就派上用场了,因为它像我们缩放输入值时那样归一化每个节点的值。通常,批次中的所有输入值都按以下方式缩放:

通过从批次均值减去每个数据点,然后将其除以批次方差,我们将节点上批次的所有数据点归一化到一个固定范围。虽然这被称为硬归一化,但通过引入γ和β参数,我们让网络确定最佳归一化参数。

为了理解批次归一化过程如何帮助,让我们看看在训练和验证数据集上的损失和精度值,以及以下场景中隐藏层值的分布:

  1. 没有批次归一化的非常小的输入值

  2. 带有批次归一化的非常小的输入值

让我们开始吧!

没有批次归一化的非常小的输入值

到目前为止,当我们必须缩放输入数据时,我们将其缩放为介于 0 和 1 之间的值。在本节中,我们将进一步将其缩放为介于 0 和 0.0001 之间的值,以便我们能够理解缩放数据的影响。正如我们在本节开头所看到的,即使权重值变化很大,小输入值也不能改变 sigmoid 值。

为了缩放输入数据集,使其具有非常低的值,我们将通过以下方式更改我们通常在FMNISTDataset类中执行的缩放,即通过将输入值的范围从0减少到0.0001

完整的代码可以在 GitHub 上的Chapter03文件夹中的Batch_normalization.ipynb文件中找到。为简洁起见,我们不会详细说明批次大小为 32部分的每一个步骤。在执行代码时,请参考 GitHub 上的笔记本。

class FMNISTDataset(Dataset):
    def __init__(self, x, y):
        **x = x.****float****()/(****255*********10000****)** **# Done only for us to**
**# understand the impact of Batch normalization**
        x = x.view(-1,28*28)
        self.x, self.y = x, y
    def __getitem__(self, ix):
        x, y = self.x[ix], self.y[ix]
        return x.to(device), y.to(device)
    def __len__(self):
        return len(self.x) 

请注意,在我们突出显示的代码行中(x = x.float()/(255*10000)),我们通过将其除以 10000 来减少输入像素值的范围。

接下来,我们必须重新定义get_model函数,以便我们可以获取模型的预测以及隐藏层的值。我们可以通过指定一个神经网络类来做到这一点,如下所示:

def get_model():
    class neuralnet(nn.Module):
        def __init__(self):
            super().__init__()
            self.input_to_hidden_layer = nn.Linear(784,1000)
            self.hidden_layer_activation = nn.ReLU()
            self.hidden_to_output_layer = nn.Linear(1000,10)
        def forward(self, x):
            x = self.input_to_hidden_layer(x)
            x1 = self.hidden_layer_activation(x)
            x2= self.hidden_to_output_layer(x1)
            return x2, x1
    model = neuralnet().to(device)
    loss_fn = nn.CrossEntropyLoss()
    optimizer = Adam(model.parameters(), lr=1e-3)
    return model, loss_fn, optimizer 

在上述代码中,我们定义了neuralnet类,该类返回输出层值(x2)和隐藏层的激活值(x1)。请注意,网络的架构没有改变。

给定 get_model 函数现在返回两个输出,我们需要修改 train_batchval_loss 函数,这些函数进行预测时,通过模型传递输入。在这里,我们只会获取输出层的数值,而不是隐藏层的数值。考虑到从模型返回的内容中输出层的数值位于第 0^(th) 索引中,我们将修改这些函数,使它们只获取预测结果的第 0^(th) 索引,如下所示:

def train_batch(x, y, model, opt, loss_fn):
    model.train()
    **prediction = model(x)[****0****]**
    batch_loss = loss_fn(prediction, y)
    batch_loss.backward()
    optimizer.step()
    optimizer.zero_grad()
    return batch_loss.item()
def accuracy(x, y, model):
    model.eval()
    with torch.no_grad():
        **prediction = model(x)[****0****]**
    max_values, argmaxes = prediction.max(-1)
    is_correct = argmaxes == y
    return is_correct.cpu().numpy().tolist() 

请注意,上述代码的突出部分是我们确保只获取模型输出的第 0^(th) 索引处的位置(因为第 0^(th) 索引包含输出层的数值)。

现在,当我们运行 Scaling the data 部分提供的其余代码时,我们将看到随着 epochs 增加,训练和验证数据集中准确率和损失值的变化如下:

图表  自动生成的描述

图 3.20:当网络使用非常小的输入值训练时的训练和验证损失与准确率

注意,在上述情景中,即使经过 100 个 epochs,模型训练效果仍然不佳(在之前的章节中,模型在验证数据集上的准确率在 10 个 epochs 内就能达到约 90%,而当前模型的验证准确率仅为约 85%)。

让我们通过探索隐藏值的分布以及参数分布,来理解当输入值具有非常小的范围时,为何模型训练效果不佳:

一个带有数字的蓝色矩形对象  自动生成的描述

图 3.21:当网络使用非常小的输入值训练时的权重和隐藏层节点数值的分布

请注意,第一个分布指示了隐藏层数值的分布(我们可以看到这些数值具有非常小的范围)。此外,考虑到输入和隐藏层数值都非常小,权重必须经过大量变化(包括连接输入到隐藏层的权重和连接隐藏层到输出层的权重)。

现在我们知道,当输入值具有非常小的范围时,网络训练效果不佳,让我们来了解批标准化如何帮助增加隐藏层中数值的范围。

使用批标准化的非常小的输入值

在本节中,我们将只对前一小节的代码进行一处更改;也就是在定义模型架构时添加批标准化。

修改后的 get_model 函数如下所示:

def get_model():
    class neuralnet(nn.Module):
        def __init__(self):
            super().__init__()
            self.input_to_hidden_layer = nn.Linear(784,1000)
            **self.batch_norm = nn.BatchNorm1d(****1000****)**
            self.hidden_layer_activation = nn.ReLU()
            self.hidden_to_output_layer = nn.Linear(1000,10)
        def forward(self, x):
            x = self.input_to_hidden_layer(x)
            **x0 = self.batch_norm(x)**
 **x1 = self.hidden_layer_activation(x0)**
            x2= self.hidden_to_output_layer(x1)
            return x2, x1
    model = neuralnet().to(device)
    loss_fn = nn.CrossEntropyLoss()
    optimizer = Adam(model.parameters(), lr=1e-3)
    return model, loss_fn, optimizer 

在前面的代码中,我们声明了一个变量(batch_norm),它执行批量归一化(nn.BatchNorm1d)。我们执行nn.BatchNorm1d(1000)的原因是因为每个图像的输出维度为 1,000(即隐藏层的一维输出)。此外,在forward方法中,我们将隐藏层的输出值通过批量归一化传递,然后通过 ReLU 激活。

随着训练和验证数据集在增加的 epochs 中准确性和损失值的变化如下:

图 3.22:当网络使用非常小的输入值和批量归一化进行训练时的训练和验证损失

在这里,我们可以看到该模型的训练方式与输入值范围不太小时的训练方式非常相似。现在,让我们了解隐藏层值的分布和权重分布,正如在前一节中看到的:

图表,直方图 描述自动生成

图 3.23:当网络使用非常小的输入值和批量归一化进行训练时,权重和隐藏层节点值的分布

我们可以看到,当我们使用批量归一化时,隐藏层的值具有更广泛的分布,连接隐藏层与输出层的权重具有更小的分布。这导致模型学习效果与前面的部分一样有效。

在训练深度神经网络时,批量归一化大大有助于我们。它帮助我们避免梯度变得太小,以至于权重几乎不会更新。

到目前为止,我们已经看到了训练损失和准确率比验证准确率和损失要好得多的情况:这表明模型在训练数据上过拟合,但在验证数据集上泛化能力不强。接下来我们将看看如何解决过拟合问题。

过拟合的概念

到目前为止,我们看到训练数据集的准确率通常超过 95%,而验证数据集的准确率约为 89%。基本上,这表明模型在未见数据上的泛化能力不强,因为它可以从训练数据集中学习。这还表明,模型学习了训练数据集的所有可能边界情况,这些情况不能应用于验证数据集。

在训练数据集上有很高的准确率,而在验证数据集上有相对较低的准确率,指的是过拟合的情况。

一些常见的策略用于减少过拟合的影响,包括dropout和正则化。接下来我们将看看它们对训练和验证损失的影响。

添加 dropout 的影响

我们已经学到,每当计算 loss.backward() 时,权重更新就会发生。通常情况下,我们会在网络内有数十万个参数,并且有数千个数据点用于训练我们的模型。这给我们可能性,即使大多数参数有助于合理地训练模型,某些参数可能会被微调以训练图像,导致它们的值仅由训练数据集中的少数图像决定。这反过来导致训练数据在高准确性上表现良好,但在验证数据集上并非必然如此。

Dropout 是一种机制,它随机选择指定百分比的节点激活并将其减少为 0。在下一次迭代中,另一组随机的隐藏单元将被关闭。这样,神经网络不会针对边缘情况进行优化,因为网络在每次迭代中都没有那么多机会来调整权重以记住边缘情况(鉴于权重不是每次迭代都会更新)。

请记住,在预测过程中,不需要应用 dropout,因为这种机制只能在训练模型时应用。

通常,在训练和验证过程中,层的行为可能会有所不同,就像在 dropout 的情况下所见。因此,您必须在模型前端使用以下两种方法之一指定模式:model.train() 用于让模型知道它处于训练模式,以及 model.eval() 用于让模型知道它处于评估模式。如果我们不这样做,可能会得到意想不到的结果。例如,在下图中,请注意模型(其中包含 dropout)在训练模式下对相同输入给出不同预测的情况。

然而,当同一模型处于 eval 模式时,它将抑制 dropout 层并返回相同输出:

图形用户界面,文本,应用 自动生成的描述

图 3.24:model.eval()对输出值的影响

在定义架构时,在 get_model 函数中如下指定 Dropout

完整代码可以在 GitHub 上的 Impact_of_dropout.ipynb 文件中的 Chapter03 文件夹中找到,网址为 bit.ly/mcvp-2e。为简洁起见,我们在执行代码时不会详细介绍每个步骤。在执行代码时,请参考 GitHub 上的笔记本。

def get_model():
    model = nn.Sequential(
                **nn.Dropout(****0.25****),**
                nn.Linear(28 * 28, 1000),
                nn.ReLU(),
                **nn.Dropout(****0.25****),**
                nn.Linear(1000, 10)
            ).to(device)
    loss_fn = nn.CrossEntropyLoss()
    optimizer = Adam(model.parameters(), lr=1e-3)
    return model, loss_fn, optimizer 

请注意,在前述代码中,Dropout 在线性激活之前指定。这指定了线性激活层中一定比例的权重不会被更新。

一旦模型训练完成,就像 Batch size of 32 部分一样,训练集和验证集的损失和准确性值如下:

图表,折线图 自动生成的描述

图 3.25:使用 dropout 的训练和验证损失及准确度

在这种情况下,训练集和验证集的准确性之间的差异不像前一情景中那么大,因此导致了一个具有较少过拟合的情况。

正则化的影响

除了训练准确性远高于验证准确性之外,过拟合的另一个特征是某些权重值远高于其他权重值。大权重值可以是模型在训练数据上学习得非常好的迹象(基本上是根据所见的内容进行死记硬背学习)。

Dropout 是一种机制,用于使权重值的更新频率降低,而正则化是另一种我们可以用于此目的的机制。

正则化是一种技术,通过惩罚模型具有大权重值来实现。因此,它是一种目标函数,旨在最小化训练数据的损失以及权重值。在本节中,我们将学习两种类型的正则化:L1 正则化和 L2 正则化。

完整的代码可以在 GitHub 的Chapter03文件夹中的Impact_of_regularization.ipynb文件中找到,链接为bit.ly/mcvp-2e。为了简洁起见,我们不会详细说明来自批量大小为 32部分的每一步。在执行代码时,请参考 GitHub 上的笔记本。

让我们开始吧!

L1 正则化

L1 正则化计算如下:

前面公式的第一部分是我们到目前为止用于优化的分类交叉熵损失,而第二部分是指模型权重值的绝对和。请注意,L1 正则化通过将权重的绝对值的和纳入损失值计算中来确保对高绝对权重值进行惩罚。

指的是我们与正则化(权重最小化)损失相关联的权重。

在训练模型时实施 L1 正则化,如下所示:

def train_batch(x, y, model, opt, loss_fn):
    model.train()
    prediction = model(x)
    **l1_regularization =** **0**
    for param in model.parameters():
        **l1_regularization += torch.norm(param,****1****)**
    **batch_loss = loss_fn(prediction, y)+****0.0001*****l1_regularization**
    batch_loss.backward()
    optimizer.step()
    optimizer.zero_grad()
    return batch_loss.item() 

你可以看到,我们通过初始化l1_regularization在所有层上强制实施了权重和偏差的正则化。

torch.norm(param,1)提供了跨层权重和偏差值的绝对值。此外,我们还有一个与层间参数的绝对值的和相关联的非常小的权重(0.0001)。

一旦我们执行剩余代码,如批量大小为 32部分所示,随着增加的 epochs,训练和验证数据集的损失和准确率值将如下:

图表 自动生成的描述

图 3.26:使用 L1 正则化的训练和验证损失以及准确率

如您所见,训练集和验证集的准确性之间的差异与没有 L1 正则化时相比并不高。

L2 正则化

L2 正则化计算如下:

前述公式的第一部分指的是获取的分类交叉熵损失,而第二部分指的是模型权重值的平方和。与 L1 正则化类似,通过将权重的平方和纳入损失值计算中,我们惩罚了较大的权重值。

指的是我们与正则化(权重最小化)损失相关联的权重值。

在训练模型时实施 L2 正则化,具体如下:

def train_batch(x, y, model, opt, loss_fn):
    model.train()
    prediction = model(x)
    **l2_regularization =** **0**
    for param in model.parameters():
        **l2_regularization += torch.norm(param,****2****)**
    **batch_loss = loss_fn(prediction, y) +** **0.01*****l2_regularization**
    batch_loss.backward()
    optimizer.step()
    optimizer.zero_grad()
    return batch_loss.item() 

在前述代码中,正则化参数, (0.01),略高于 L1 正则化,因为权重通常在 -1 和 1 之间,平方后会变得更小。将它们乘以更小的数字,正如我们在 L1 正则化中所做的那样,会导致我们在整体损失计算中对正则化的权重非常小。执行剩余代码后,就像 批大小为 32 部分一样,随着 epoch 的增加,训练和验证数据集的损失和准确率值如下:

训练和验证的图形化描述

图 3.27:使用 L2 正则化的训练和验证损失以及准确率

我们可以看到,L2 正则化还导致了验证和训练数据集的准确率和损失值更加接近。

总结

我们从学习图像表示开始了解本章内容。然后,我们学习了如何通过缩放、学习率的值、优化器的选择以及批大小来提高训练的准确性和速度。接着,我们了解了批归一化如何提高训练速度并解决隐藏层中的非常小或非常大的值问题。然后,我们学习了如何通过调整学习率来进一步提高准确性。最后,我们深入了解了过拟合的概念,并学习了如何通过 dropout 以及 L1 和 L2 正则化来避免过拟合。

现在我们已经学习了使用深度神经网络进行图像分类以及有助于训练模型的各种超参数,在下一章中,我们将学习本章所学内容的失败情况以及如何使用卷积神经网络解决这些问题。

问题

  1. 如果输入数据集中的输入值未经过缩放,会发生什么?

  2. 当训练神经网络时,如果背景是白色像素而内容是黑色像素,可能会发生什么?

  3. 批大小对模型训练时间和内存的影响是什么?

  4. 输入值范围对训练结束时权重分布的影响是什么?

  5. 批归一化如何帮助提高准确率?

  6. 为什么权重在 dropout 层的训练和评估过程中表现不同?

  7. 我们如何知道模型是否在训练数据上过拟合了?

  8. 正则化如何帮助避免过拟合?

  9. L1 和 L2 正则化有何不同?

  10. 丢弃法如何帮助减少过拟合?

了解更多关于 Discord

加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

packt.link/modcv

第二部分:目标分类和检测

在掌握了神经网络 (NN) 基础的理解之后,在本节中,我们将探索建立在这些基础之上的更复杂的神经网络模块,以解决更复杂的与视觉相关的问题,包括目标检测、图像分类和分割以及许多其他问题。

本节包括以下章节:

  • 第四章, 卷积神经网络简介

  • 第五章, 图像分类的迁移学习

  • 第六章, 图像分类的实际方面

  • 第七章, 目标检测基础

  • 第八章, 高级目标检测

  • 第九章, 图像分割

  • 第十章, 目标检测和分割的应用

第四章:引入卷积神经网络

到目前为止,我们学习了如何构建深度神经网络以及调整它们的各种超参数所产生的影响。在本章中,我们将了解传统深度神经网络无法解决的问题。然后,我们将通过一个玩具例子了解卷积神经网络CNNs)的内部工作原理,然后理解它们的一些主要超参数,包括步幅、池化和滤波器。接下来,我们将利用 CNN 以及各种数据增强技术来解决传统深度神经网络精度不高的问题。然后,我们将了解 CNN 中特征学习过程的结果是什么样子。最后,我们将把学到的东西结合起来解决一个用例:我们将通过声明图像中包含狗还是猫来分类图像。通过这样做,我们将能够理解预测准确性如何随着用于训练的数据量的变化而变化。

到本章末尾,您将对 CNN 有深入的理解,CNN 是多个模型架构的基础,用于各种任务。

本章将涵盖以下主题:

  • 传统深度神经网络的问题

  • CNN 的构建模块

  • 实现 CNN

  • 使用深度 CNN 分类图像

  • 实现数据增强

  • 可视化特征学习的结果

  • 为了对真实世界图像进行分类构建 CNN

让我们开始吧!

本章中的所有代码都可在此书的 GitHub 存储库的Chapter04文件夹中找到:bit.ly/mcvp-2e

传统深度神经网络的问题

在深入研究 CNN 之前,让我们看看在使用传统深度神经网络时面临的主要问题。

让我们重新考虑我们在第三章中在 Fashion-MNIST 数据集上构建的模型。我们将获取一个随机图像并预测与该图像对应的类别,如下所示:

以下代码可以在 GitHub 上Chapter04文件夹中的Issues_with_image_translation.ipynb文件中找到:bit.ly/mcvp-2e。这里仅讨论与图像翻译问题对应的附加代码,以保持简洁。

  1. 获取可用训练图像中的随机图像:

    # Note that you should run the code in
    # Batch size of 32 section in Chapter 3
    # before running the following code
    import matplotlib.pyplot as plt
    %matplotlib inline
    # ix = np.random.randint(len(tr_images))
    ix = 24300
    plt.imshow(tr_images[ix], cmap='gray')
    plt.title(fmnist.classes[tr_targets[ix]]) 
    

前面的代码将产生以下输出:

图 4.1:与索引 24300 相对应的图像

  1. 将图像通过训练好的模型传递(继续使用我们在第三章批量大小为 32节中训练的模型):

    1. 预处理图像,使其经过与构建模型时执行的相同预处理步骤:
    img = tr_images[ix]/255.
    img = img.view(28*28)
    img = img.to(device) 
    
    1. 提取与各种类别相关联的概率:
    np_output = model(img).cpu().detach().numpy()
    np.exp(np_output)/np.sum(np.exp(np_output)) 
    
  2. 前面的代码将产生以下输出,我们可以看到最高概率是第一个索引,即裤子类:

    图 4.2:不同类别的概率

  3. 将图像多次(每次移动一个像素)从向左 5 像素到向右 5 像素进行翻译,并将预测结果存储在列表中:

    1. 创建一个存储预测的列表:
    preds = [] 
    
    1. 创建一个循环,将图像从原始位置的-5 像素(向左 5 像素)滚动到+5 像素(向右 5 像素):
    for px in range(-5,6): 
    
    1. 在前述代码中,我们指定 6 作为上限,尽管我们只关注将图像翻译到+5 像素,因为范围的输出将为-5 到+5,而(-5,6)是指定范围的输出。

    2. 预处理图像,就像我们在步骤 2中所做的那样:

     img = tr_images[ix]/255.
        img = img.view(28, 28) 
    
    1. for循环中以px的值滚动图像:
     img2 = np.roll(img, px, axis=1) 
    
    1. 在这里,我们指定axis=1,因为我们希望图像像素在水平方向移动而不是垂直方向。

    2. 将滚动后的图像存储为张量对象并注册到device

     img3 = torch.Tensor(img2).view(28*28).to(device) 
    
    1. img3通过训练模型以预测滚动图像的类,并将其附加到存储各种翻译预测的列表中:
     np_output = model(img3).cpu().detach().numpy()
        preds.append(np.exp(np_output)/np.sum(np.exp(np_output))) 
    
  4. 可视化模型对所有翻译(-5 像素到+5 像素)的预测:

    import seaborn as sns
    fig, ax = plt.subplots(1,1, figsize=(12,10))
    plt.title('Probability of each class \
    for various translations')
    sns.heatmap(np.array(preds), annot=True, ax=ax, fmt='.2f',
    xticklabels=fmnist.classes,yticklabels=[str(i)+ \
    str(' pixels') for i in range(-5,6)], cmap='gray') 
    

前面的代码导致以下输出:

图 4.3:不同翻译每个类的概率

由于我们只将图像从左移动 5 像素到右移动 5 像素,因此图像内容没有变化。但是,当翻译超过 2 像素时,图像的预测类别会发生变化。这是因为在模型训练时,所有训练和测试图像的内容都在中心。这与前一情景不同,我们测试的是偏离中心的翻译图像(偏移 5 像素),导致预测类别不正确。

现在我们已经了解了传统神经网络失败的场景,接下来我们将学习 CNN 如何帮助解决这个问题。但在此之前,让我们首先看一下 CNN 的基本组成部分。

CNN 的基本构建块

CNN 在处理图像时是最突出的架构之一。它们解决了深度神经网络的主要局限性,就像我们在前一节中看到的那样。除了图像分类外,它们还有助于目标检测、图像分割、生成对抗网络等各种任务 - 实质上是我们使用图像的任何地方。此外,有多种构建 CNN 的方式,也有多个预训练模型利用 CNN 执行各种任务。从本章开始,我们将广泛使用 CNN。

在接下来的小节中,我们将理解 CNN 的基本构建块,它们如下:

  • 卷积

  • 过滤器

  • 步幅和填充

  • 池化

让我们开始吧!

卷积

卷积基本上是两个矩阵之间的乘法。正如您在前一章节中看到的,矩阵乘法是训练神经网络的关键组成部分。(我们在计算隐藏层值时执行矩阵乘法——这是输入值和连接输入到隐藏层的权重值之间的矩阵乘法。类似地,我们执行矩阵乘法来计算输出层值。)

为了确保我们对卷积过程有牢固的理解,让我们通过一个例子来进行说明。假设我们有两个矩阵用于执行卷积。

这里是矩阵 A:

图 4.4:矩阵 A

这里是矩阵 B:

图 4.5:矩阵 B

在执行卷积操作时,我们将矩阵 B(较小的矩阵)滑动到矩阵 A(较大的矩阵)上。此外,我们在矩阵 A 和矩阵 B 之间执行元素对元素的乘法,如下所示:

  1. 乘以较大矩阵的{1,2,5,6}部分与较小矩阵的{1,2,3,4}部分:

11 + 22 + 53 + 64 = 44

  1. 乘以较大矩阵的{2,3,6,7}部分与较小矩阵的{1,2,3,4}部分:

21 + 32 + 63 + 74 = 54

  1. 乘以较大矩阵的{3,4,7,8}部分与较小矩阵的{1,2,3,4}部分:

31 + 42 + 73 + 84 = 64

  1. 乘以较大矩阵的{5,6,9,10}部分与较小矩阵的{1,2,3,4}部分:

51 + 62 + 93 + 104 = 84

  1. 乘以较大矩阵的{6,7,10,11}部分与较小矩阵的{1,2,3,4}部分:

61 + 72 + 103 + 114 = 94

  1. 乘以较大矩阵的{7,8,11,12}部分与较小矩阵的{1,2,3,4}部分:

71 + 82 + 113 + 124 = 104

  1. 乘以较大矩阵的{9,10,13,14}部分与较小矩阵的{1,2,3,4}部分:

91 + 102 + 133 + 144 = 124

  1. 乘以较大矩阵的{10,11,14,15}部分与较小矩阵的{1,2,3,4}部分:

101 + 112 + 143 + 154 = 134

  1. 乘以较大矩阵的{11,12,15,16}部分与较小矩阵的{1,2,3,4}部分:

111 + 122 + 153 + 164 = 144

执行上述操作的结果如下所示:

图 4.6:卷积操作的输出

较小的矩阵通常被称为滤波器或内核,而较大的矩阵是原始图像。

滤波器

一个滤波器是一组权重的矩阵,初始时以随机方式初始化。模型随着时间的推移学习到滤波器的最优权重值。滤波器的概念带来了两个不同的方面:

  • 滤波器学习的内容

  • 如何表示滤波器

通常情况下,CNN 中的滤波器越多,模型能够学习图像的特征也就越多。在本章的可视化特征学习结果部分中,我们将学习各种滤波器学习的内容。目前,我们先掌握这样一个中间理解,即滤波器学习图像中不同特征的情况。例如,某个滤波器可能学习猫的耳朵,并在其卷积的图像部分包含猫耳朵时提供高激活(矩阵乘法值)。

在前一节中,当我们将大小为 2 x 2 的滤波器与大小为 4 x 4 的矩阵进行卷积时,我们得到的输出尺寸为 3 x 3。然而,如果有 10 个不同的滤波器与更大的矩阵(原始图像)相乘,则结果将是 10 组 3 x 3 的输出矩阵。

在上述情况下,一个 4 x 4 的图像与大小为 2 x 2 的 10 个滤波器进行卷积,得到 3 x 3 x 10 的输出值。基本上,当图像被多个滤波器卷积时,输出的通道数等于图像被卷积的滤波器数目。

此外,在处理彩色图像的情况下,图像有三个通道,与原始图像进行卷积的滤波器也将有三个通道,导致每次卷积得到单个标量输出。另外,如果滤波器与中间输出(例如形状为 64 x 112 x 112)进行卷积,滤波器将具有 64 个通道以获取标量输出。此外,如果有 512 个滤波器与中间层获得的输出进行卷积,使用 512 个滤波器的卷积输出将具有形状为 512 x 112 x 112。

为了进一步巩固我们对滤波器输出的理解,让我们看看以下的图示:

图 4.7: 多个滤波器进行卷积操作的输出

在前述图中,我们可以看到输入图像被与其深度相同的滤波器乘以,并且卷积输出的通道数与卷积的滤波器数目相同。

步幅和填充

在前一节中,每个滤波器跨越图像时,一次跨越一列和一行(在图像末尾穷尽所有可能的列之后)。这也导致输出尺寸在高度和宽度上比输入图像尺寸少 1 个像素。这会导致信息的部分丢失,并且如果卷积的输出和原始图像不具有相同的形状,则可能限制我们将卷积操作的输出添加到原始图像中的可能性。这被称为残差添加,并将在下一章节中详细讨论。目前,让我们学习步幅和填充如何影响卷积操作的输出形状。

步幅

让我们通过利用“滤波器”部分中看到的同一示例来理解步幅的影响。我们将矩阵 B 以步幅 2 移动到矩阵 A 上。因此,步幅为 2 的卷积输出如下所示:

  1. 大矩阵的{1,2,5,6}与小矩阵的{1,2,3,4}相乘:

11 + 22 + 53 + 64 = 44

  1. 大矩阵的{3,4,7,8}与小矩阵的{1,2,3,4}相乘:

31 + 42 + 73 + 84 = 64

  1. 大矩阵的{9,10,13,14}与小矩阵的{1,2,3,4}相乘:

91 + 102 + 133 + 144 = 124

  1. 大矩阵的{11,12,15,16}与小矩阵的{1,2,3,4}相乘:

111 + 122 + 153 + 164 = 144

执行上述操作的结果如下:

图 4.8:步幅卷积输出

由于我们现在步幅为 2,请注意前述输出与步幅为 1 时相比具有较低的维度(其中输出形状为 3 x 3)。

填充

在前述情况下,我们不能将过滤器的最左侧元素与图像的最右侧元素相乘。如果我们执行这样的矩阵乘法,我们将在图像上加入零填充。这将确保我们可以对图像中所有元素与滤波器中的元素进行逐元素乘法。

让我们通过使用“卷积”部分中使用的同一示例来理解填充。一旦在矩阵 A 上添加填充,修订后的矩阵 A 将如下所示:

图 4.9:矩阵 A 上的填充

正如你所见,我们已经用零填充了矩阵 A,并且与矩阵 B 的卷积不会导致输出维度小于输入维度。在我们处理残差网络时,这一点非常有用,因为我们必须将卷积的输出添加到原始图像中。

一旦完成这一步,我们可以对卷积操作的输出执行激活。对于这一步,我们可以使用第三章中看到的任何激活函数。

池化

池化在一个小区块中聚合信息。想象一种情况,卷积激活的输出如下所示:

图 4.10:卷积操作的输出

此区域的最大池化为 4,因为这是区块中数值的最大值。让我们理解更大矩阵的最大池化:

图 4.11:卷积操作的输出

在前述情况下,如果池化步幅长度为 2,则最大池化操作计算如下,我们将输入图像通过步幅 2 进行分割(即,我们将图像分割成 2 x 2 的分区):

图 4.12:突出显示的步幅卷积输出

对于矩阵的四个子部分,元素池中的最大值如下:

图 4.13:最大池化值

在实践中,并非总是需要步幅为 2;这里仅用于举例说明。池化的其他变体包括求和池化和平均池化。然而,在实践中,最大池化更常用。

请注意,通过执行卷积和池化操作后,原始矩阵的大小从 4 x 4 减小为 2 x 2。在现实情况下,如果原始图像的形状为 200 x 200 并且滤波器的形状为 3 x 3,则卷积操作的输出将是 198 x 198。随后,步长为 2 的池化操作的输出将是 99 x 99。这样,通过利用池化,我们保留了更重要的特征同时减少了输入的维度。

将它们整合在一起

到目前为止,我们已经了解了卷积、滤波器、步幅、填充和池化,以及它们在减少图像维度中的作用。现在,我们将了解 CNN 的另一个关键组成部分——平坦化层(全连接层)——然后将我们学到的三个部分整合在一起。

要理解平坦化过程,我们将使用前一节中池化层的输出,并对其进行平坦化处理。平坦化池化层的输出是 {6, 8, 14, 16}。

通过这样做,我们会看到平坦化层可以等同于输入层(在第三章中我们将输入图像压平为 784 维输入)。一旦获取了平坦化层(全连接层)的值,我们可以将其传递到隐藏层然后获取用于预测图像类别的输出。

CNN 的整体流程如下:

图 4.14:CNN 工作流程

我们可以看到 CNN 模型的整体流程,我们通过多个滤波器将图像通过卷积传递,然后通过池化(在前述情况下,重复两次卷积和池化过程),最后将最终池化层的输出进行平坦化处理。这形成了前述图像的特征学习部分,其中我们将图像转换为较低维度(平坦化输出),同时恢复所需的信息。

卷积和池化操作构成了特征学习部分,滤波器帮助从图像中提取相关特征,池化则有助于聚合信息,从而减少在平坦化层的节点数量。

如果直接将输入图像(例如尺寸为 300 x 300 像素)进行平坦化处理,我们处理的是 90K 个输入值。如果输入有 90K 个像素值并且隐藏层有 100K 个节点,我们需要处理约 9 亿个参数,这在计算上是巨大的。

卷积和池化有助于获取一个比原始图像更小的平坦化层表示。

最后,分类的最后部分类似于我们在第三章中对图像进行分类的方式,那里有一个隐藏层然后获得输出层。

卷积和池化在图像翻译中的帮助

当我们执行池化操作时,可以将操作的输出视为一个区域(一个小补丁)的抽象。特别是在图像被翻译时,这种现象非常方便。

想象一种情景,图像向左平移了 1 个像素。一旦我们对其执行卷积、激活和池化,我们将减少图像的维度(由于池化),这意味着较少数量的像素存储了来自原始图像的大部分信息。此外,由于池化存储了区域(补丁)的信息,即使原始图像平移了 1 个单位,池化图像中一个像素的信息也不会变化。这是因为该区域的最大值很可能已被捕获在池化图像中。

卷积和池化还可以帮助我们实现感受野。要理解感受野,让我们想象一个场景,我们在一个形状为 100 x 100 的图像上进行两次卷积 + 池化操作。如果卷积操作进行了填充,那么在两次卷积池化操作结束时的输出形状将为 25 x 25。25 x 25 输出中的每个单元格现在对应于原始图像中一个较大的 4 x 4 部分。因此,由于卷积和池化操作,结果图像中的每个单元格都包含原始图像的一个补丁内的关键信息。

现在我们已经了解了 CNN 的核心组件,让我们通过一个玩具示例将它们应用起来,以理解它们如何一起工作。

实施 CNN

CNN 是计算机视觉技术的基础组成部分之一,对于您深入理解它们的工作原理非常重要。虽然我们已经知道 CNN 包括卷积、池化、展平,然后是最终的分类层,但在本节中,我们将通过代码了解 CNN 前向传播期间发生的各种操作。

要对此有一个坚实的理解,首先,我们将使用 PyTorch 在一个玩具示例上构建一个 CNN 架构,然后通过 Python 从头开始构建前向传播以匹配输出。CNN 架构将与我们在上一章中构建的神经网络架构有所不同,因为 CNN 除了典型的香草深度神经网络外,还包括以下内容:

  • 卷积操作

  • 池化操作

  • 展平层

在以下代码中,我们将在玩具数据集上构建一个 CNN 模型,如下所示:

可在 GitHub 的Chapter04文件夹中找到CNN_working_details.ipynb文件中的以下代码:bit.ly/mcvp-2e

  1. 首先,我们需要导入相关的库:

    import torch
    from torch import nn
    from torch.utils.data import TensorDataset, Dataset, DataLoader
    from torch.optim import SGD, Adam
    device = 'cuda' if torch.cuda.is_available() else 'cpu'
    from torchvision import datasets
    import numpy as np
    import matplotlib.pyplot as plt
    %matplotlib inline 
    
  2. 接下来,我们需要按照以下步骤创建数据集:

    X_train = torch.tensor([[[[1,2,3,4],[2,3,4,5],
                              [5,6,7,8],[1,3,4,5]]],
                            [[[-1,2,3,-4],[2,-3,4,5],
                [-5,6,-7,8],[-1,-3,-4,-5]]]]).to(device).float()
    X_train /= 8
    y_train = torch.tensor([0,1]).to(device).float() 
    

注意,PyTorch 期望输入的形状为N x C x H x W,其中N是图像的数量(批量大小),C是通道数,H是高度,W是图像的宽度。

在这里,我们将输入数据集进行缩放,使其范围在-1 到+1 之间,通过将输入数据除以最大输入值即 8。输入数据集的形状为(2,1,4,4),因为有两个数据点,每个数据点的形状为 4 x 4,并且有 1 个通道。

  1. 定义模型架构:

    def get_model():
        model = nn.Sequential(
                    nn.Conv2d(1, 1, kernel_size=3),
                    nn.MaxPool2d(2),
                    nn.ReLU(),
                    nn.Flatten(),
                    nn.Linear(1, 1),
                    nn.Sigmoid(),
                ).to(device)
        loss_fn = nn.BCELoss()
        optimizer = Adam(model.parameters(), lr=1e-3)
        return model, loss_fn, optimizer 
    

注意,在上述模型中,我们指定输入中有 1 个通道,并且我们使用nn.Conv2d方法在卷积后提取 1 个通道的输出(即,我们有 1 个大小为 3 x 3 的滤波器)。

然后,我们使用nn.MaxPool2d进行最大池化和 ReLU 激活(使用nn.Relu()),然后扁平化并连接到最终层,每个数据点有一个输出。

此外,请注意,损失函数是二元交叉熵损失(nn.BCELoss()),因为输出来自二元类别。我们还指定优化将使用学习率为 0.001 的 Adam 优化器进行。

  1. 使用torch_summary包中的summary方法对我们的model、损失函数(loss_fn)和optimizer进行获取后,总结模型的架构:

    !pip install torch_summary
    from torchsummary import summary
    model, loss_fn, optimizer = get_model()
    summary(model, X_train); 
    

上述代码产生以下输出:

图 4.15:模型架构摘要

让我们理解每一层包含多少参数的原因。Conv2d类的参数如下:

图 4.16:Conv2d 中的参数说明

在前面的例子中,我们指定卷积核的大小(kernel_size)为 3,并且out_channels的数量为 1(本质上,滤波器的数量为 1),其中初始(输入)通道的数量为 1。因此,对于每个输入图像,我们在一个形状为 1 x 4 x 4 的图像上卷积一个形状为 3 x 3 的滤波器,得到一个形状为 1 x 2 x 2 的输出。有 10 个参数,因为我们正在学习九个权重参数(3 x 3)和卷积核的一个偏置。对于MaxPool2d、ReLU 和 flatten 层,没有参数,因为这些是在卷积层输出之上执行的操作;不涉及权重或偏置。

线性层有两个参数 - 一个权重和一个偏置 - 这意味着总共有 12 个参数(来自卷积操作的 10 个和线性层的两个)。

  1. 使用我们在第三章中使用的相同模型训练代码来训练模型,定义将对数据批次进行训练的函数(train_batch)。然后,获取DataLoader并在 2,000 个 epochs 中对数据批次进行训练(我们只使用 2,000 个是因为这是一个小型玩具数据集),如下所示:

    1. 定义将对数据批次进行训练的函数(train_batch):
    def train_batch(x, y, model, opt, loss_fn):
        model.train()
        prediction = model(x)
        batch_loss = loss_fn(prediction.squeeze(0), y)
        batch_loss.backward()
        optimizer.step()
        optimizer.zero_grad()
        return batch_loss.item() 
    
    1. 使用 TensorDataset 方法指定数据集来定义训练 DataLoader,然后使用 DataLoader 加载它:
    trn_dl = DataLoader(TensorDataset(X_train, y_train)) 
    
    1. 鉴于我们没有对输入数据进行大量修改,我们不会单独构建一个类,而是直接利用 TensorDataset 方法,该方法提供了与输入数据对应的对象。

    2. 在 2,000 个 epochs 上训练模型:

    for epoch in range(2000):
        for ix, batch in enumerate(iter(trn_dl)):
            x, y = batch
            batch_loss = train_batch(x, y, model,optimizer, loss_fn) 
    
  2. 使用上述代码,我们已经在我们的玩具数据集上训练了 CNN 模型。

  3. 对第一个数据点执行前向传播:

    model(X_train[:1]) 
    

上述代码的输出是 0.1625

请注意,由于执行上述代码时随机权重初始化可能不同,因此您可能会有不同的输出值。

使用 GitHub 仓库中的 CNN from scratch in Python.pdf 文件,我们可以学习如何从头开始构建 CNN 中的前向传播,并在第一个数据点上复制输出 0.1625。

在接下来的部分中,我们将把这个应用到 Fashion-MNIST 数据集,并看看它在翻译后的图像上表现如何。

使用深度 CNN 对图像进行分类

到目前为止,我们已经看到传统神经网络对翻译图像预测不正确。这需要解决,因为在实际情况中,会需要应用各种增强技术,如翻译和旋转,这些在训练阶段没有看到。在本节中,我们将了解 CNN 如何解决在 Fashion-MNIST 数据集的图像发生翻译时预测不正确的问题。

Fashion-MNIST 数据集的预处理部分与上一章节相同,除了当我们对输入数据进行重塑(.view)时,我们不再将输入展平为 28 x 28 = 784 维度,而是将每个图像重塑为形状为 (1,28,28)(请记住,首先需要指定通道,然后是它们的高度和宽度,在 PyTorch 中):

可在 GitHub 的 Chapter04 文件夹中的 CNN_on_FashionMNIST.ipynb 文件中找到以下代码:bit.ly/mcvp-2e.

  1. 导入必要的包:

    from torchvision import datasets
    from torch.utils.data import Dataset, DataLoader
    import torch
    import torch.nn as nn
    device = "cuda" if torch.cuda.is_available() else "cpu"
    import numpy as np
    import matplotlib.pyplot as plt
    %matplotlib inline
    data_folder = '~/data/FMNIST' # This can be any directory you
    # want to download FMNIST to
    fmnist = datasets.FashionMNIST(data_folder,download=True, train=True)
    tr_images = fmnist.data
    tr_targets = fmnist.targets 
    
  2. Fashion-MNIST 数据集类定义如下。请记住,Dataset 对象将总是需要我们定义的 __init____getitem____len__ 方法:

    class FMNISTDataset(Dataset):
        def __init__(self, x, y):
            x = x.float()/255
            **x = x.view(-****1****,****1****,****28****,****28****)**
            self.x, self.y = x, y
        def __getitem__(self, ix):
            x, y = self.x[ix], self.y[ix]
            return x.to(device), y.to(device)
        def __len__(self):
            return len(self.x) 
    

粗体的代码行是我们重塑每个输入图像的地方(与前一章节所做的不同),因为我们正在为期望每个输入具有批大小 x 通道 x 高度 x 宽度形状的 CNN 提供数据。

  1. CNN 模型架构定义如下:

    from torch.optim import SGD, Adam
    def get_model():
        model = nn.Sequential(
                    nn.Conv2d(1, 64, kernel_size=3),
                    nn.MaxPool2d(2),
                    nn.ReLU(),
                    nn.Conv2d(64, 128, kernel_size=3),
                    nn.MaxPool2d(2),
                    nn.ReLU(),
                    nn.Flatten(),
                    nn.Linear(3200, 256),
                    nn.ReLU(),
                    nn.Linear(256, 10)
                ).to(device)
        loss_fn = nn.CrossEntropyLoss()
        optimizer = Adam(model.parameters(), lr=1e-3)
        return model, loss_fn, optimizer 
    
  2. 可以使用以下代码创建模型总结:

    from torchsummary import summary
    model, loss_fn, optimizer = get_model()
    summary(model, torch.zeros(1,1,28,28)); 
    

这导致以下输出:

图 4.17:模型架构总结

为了加深我们对 CNN 的理解,让我们了解在前面的输出中参数数量被设定为什么样的原因:

  • 第一层:考虑到有 64 个大小为 3 的滤波器,我们有 64 x 3 x 3 个权重和 64 个偏置,总共 640 个参数。

  • 第四层:考虑到有 128 个大小为 3 的滤波器,我们有 128 x 64 x 3 x 3 个权重和 128 个偏置,总共 73,856 个参数。

  • 第八层:考虑到一个具有 3,200 个节点的层连接到另一个具有 256 个节点的层,我们有 3,200 x 256 个权重和 256 个偏置,总共 819,456 个参数。

  • 第十层:考虑到一个具有 256 个节点的层连接到一个具有 10 个节点的层,我们有 256 x 10 个权重和 10 个偏置,总共 2570 个参数。

现在,我们像在前一章中训练模型一样训练模型。

完整的代码可以在本书的 GitHub 存储库中找到:https://bit.ly/mcvp-2e

一旦模型训练完成,您会注意到在训练集和测试集上的准确性和损失的变化如下:

图 4.18:随着时代的推移训练和验证损失及准确率

在上述场景中,请注意,验证数据集在前五个时期内的准确率约为 92%,这已经优于我们在上一章节中通过各种技术所见到的准确率,即使没有额外的正则化。

让我们翻译这张图片并预测翻译后图片的类别:

  1. 将图像向左或向右平移 5 个像素并预测其类别:

    preds = []
    ix = 24300
    for px in range(-5,6):
        img = tr_images[ix]/255.
        img = img.view(28, 28)
        img2 = np.roll(img, px, axis=1)
        plt.imshow(img2)
        plt.show()
        img3 = torch.Tensor(img2).view(-1,1,28,28).to(device)
        np_output = model(img3).cpu().detach().numpy()
        preds.append(np.exp(np_output)/np.sum(np.exp(np_output))) 
    

在上述代码中,我们对图像(img3)进行了 reshape,使其形状为(-1,1,28,28),这样我们可以将图像传递给 CNN 模型。

  1. 绘制各种翻译下类别的概率:

    import seaborn as sns
    fig, ax = plt.subplots(1,1, figsize=(12,10))
    plt.title('Probability of each class for \
    various translations')
    sns.heatmap(np.array(preds).reshape(11,10), annot=True,
                ax=ax,fmt='.2f', xticklabels=fmnist.classes,
                yticklabels=[str(i)+str(' pixels') \
                for i in range(-5,6)], cmap='gray') 
    

上述代码的输出如下:

图 4.19:各种翻译下各类别的概率

在这种情况下,请注意,即使将图像向右或向左平移了 4 个像素,预测仍然是正确的,而在没有使用 CNN 的情况下,图像向右或向左平移 4 个像素时预测是错误的。此外,当图像向右或向左平移 5 个像素时,Trouser类别的概率显著下降。

正如我们所看到的,虽然 CNN 有助于解决图像平移的挑战,但并不能完全解决问题。我们将学习如何通过结合数据增强和 CNN 来解决这样的场景。

鉴于可以利用不同的数据增强技术,我们在 GitHub 存储库的Chapter04文件夹中的implementing data augmentation.pdf文件中提供了详尽的数据增强信息。

可视化特征学习的结果

到目前为止,我们已经了解到 CNN 如何帮助我们分类图像,即使图像中的对象已被转换。我们还了解到滤波器在学习图像特征方面起着关键作用,这反过来有助于将图像正确分类。但是,我们尚未提到使滤波器强大的是什么。在本节中,我们将学习有关滤波器学习的内容,这使得 CNN 能够正确分类图像,并通过对包含 X 和 O 图像的数据集进行分类来理解完全连接层(展平层)的激活状态。

让我们看看滤波器学到了什么:

下面的代码可以在 GitHub 上的 Chapter04 文件夹中的 Visualizing_the_features'_learning.ipynb 文件中找到,链接为 bit.ly/mcvp-2e

  1. 下载数据集:

    !wget https://www.dropbox.com/s/5jh4hpuk2gcxaaq/all.zip
    !unzip all.zip 
    

注意文件夹中的图像命名如下:

图 4.20:图像的命名约定

图像的类别可以从图像名称中获取,其中图像名称的第一个字符指定图像所属的类别。

  1. 导入所需模块:

    import torch
    from torch import nn
    from torch.utils.data import TensorDataset,Dataset,DataLoader
    from torch.optim import SGD, Adam
    device = 'cuda' if torch.cuda.is_available() else 'cpu'
    from torchvision import datasets
    import numpy as np, cv2
    import matplotlib.pyplot as plt
    %matplotlib inline
    from glob import glob
    from imgaug import augmenters as iaa 
    
  2. 定义一个获取数据的类。还要确保图像已调整为 28 x 28 的形状,批次已用三个通道形状,并且因变量已作为数值值获取。我们将在以下代码中逐步完成这些操作:

    1. 定义图像增强方法,将图像调整为 28 x 28 的形状:
    tfm = iaa.Sequential(iaa.Resize(28)) 
    
    1. 定义一个类,它接受文件夹路径作为输入,并在 __init__ 方法中循环处理该路径中的文件:
    class XO(Dataset):
        def __init__(self, folder):
            self.files = glob(folder) 
    
    1. 定义 __len__ 方法,它返回要考虑的文件的长度:
     def __len__(self): return len(self.files) 
    
    1. 定义 __getitem__ 方法,我们用它来获取返回该索引处的文件,读取文件,然后对图像进行增强。我们在这里没有使用 collate_fn,因为这是一个小数据集,不会显著影响训练时间:
     def __getitem__(self, ix):
            f = self.files[ix]
            im = tfm.augment_image(cv2.imread(f)[:,:,0]) 
    
    1. 鉴于每个图像的形状为 28 x 28,我们现在将在形状的开头创建一个虚拟通道维度,即在图像的高度和宽度之前:
     im = im[None] 
    
    1. 现在,我们可以根据文件名中 '/' 后和 '@' 前的字符确定每个图像的类别:
     cl = f.split('/')[-1].split('@')[0] == 'x' 
    
    1. 最后,我们返回图像及其对应的类别:
     return torch.tensor(1 - im/255).to(device).float(),
                          torch.tensor([cl]).float().to(device) 
    
  3. 检查您获取的图像样本。在以下代码中,我们通过从之前定义的类中获取数据来提取图像及其对应的类别:

    data = XO('/content/all/*') 
    

现在,我们可以绘制我们获取的数据集的图像样本:

  1. R, C = 7,7
    fig, ax = plt.subplots(R, C, figsize=(5,5))
    for label_class, plot_row in enumerate(ax):
        for plot_cell in plot_row:
            plot_cell.grid(False); plot_cell.axis('off')
            ix = np.random.choice(1000)
            im, label = data[ix]
            print()
            plot_cell.imshow(im[0].cpu(), cmap='gray')
    plt.tight_layout() 
    

上述代码的结果如下输出:

图 4.21:样本图像

  1. 定义模型架构、损失函数和优化器:

    from torch.optim import SGD, Adam
    def get_model():
        model = nn.Sequential(
                    nn.Conv2d(1, 64, kernel_size=3),
                    nn.MaxPool2d(2),
                    nn.ReLU(),
                    nn.Conv2d(64, 128, kernel_size=3),
                    nn.MaxPool2d(2),
                    nn.ReLU(),
                    nn.Flatten(),
                    nn.Linear(3200, 256),
                    nn.ReLU(),
                    nn.Linear(256, 1),
                    nn.Sigmoid()
                ).to(device)
        loss_fn = nn.BCELoss()
        optimizer = Adam(model.parameters(), lr=1e-3)
        return model, loss_fn, optimizer 
    

    注意,损失函数是二元交叉熵损失 (nn.BCELoss()),因为提供的输出来自二进制类。可以通过以下方式获取上述模型的摘要:

    !pip install torch_summary
    from torchsummary import summary
    model, loss_fn, optimizer = get_model()
    summary(model, torch.zeros(1,1,28,28)); 
    

这导致以下输出:

图 4.22:模型架构摘要

  1. 定义一个用于批量训练的函数,该函数接受图像和它们的类别作为输入,并在对给定数据批次执行反向传播后返回它们的损失值和准确度:

    def train_batch(x, y, model, opt, loss_fn):
        model.train()
        prediction = model(x)
        is_correct = (prediction > 0.5) == y
        batch_loss = loss_fn(prediction, y)
        batch_loss.backward()
        optimizer.step()
        optimizer.zero_grad()
        return batch_loss.item(), is_correct[0] 
    
  2. 定义一个 DataLoader,其中输入是 Dataset 类:

    trn_dl = DataLoader(XO('/content/all/*'),batch_size=32, drop_last=True) 
    
  3. 初始化模型:

    model, loss_fn, optimizer = get_model() 
    
  4. 5 个 epoch 上训练模型:

    for epoch in range(5):
        for ix, batch in enumerate(iter(trn_dl)):
            x, y = batch
            batch_loss = train_batch(x, y, model,optimizer, loss_fn) 
    
  5. 获取图像以查看滤波器对图像的学习效果:

    im, c = trn_dl.dataset[2]
    plt.imshow(im[0].cpu())
    plt.show() 
    

这导致以下输出:

图 4.23:示例图像

  1. 通过训练好的模型传递图像并获取第一层的输出。然后,将其存储在 intermediate_output 变量中:

    first_layer = nn.Sequential(*list(model.children())[:1])
    intermediate_output = first_layer(im[None])[0].detach() 
    
  2. 绘制 64 个滤波器的输出。intermediate_output 中的每个通道是每个滤波器的卷积输出:

    fig, ax = plt.subplots(8, 8, figsize=(10,10))
    for ix, axis in enumerate(ax.flat):
        axis.set_title('Filter: '+str(ix))
        axis.imshow(intermediate_output[ix].cpu())
    plt.tight_layout()
    plt.show() 
    

这导致以下输出:

图 4.24:64 个滤波器的激活

注意,某些滤波器(如 0、4、6 和 7 号滤波器)学习了网络中存在的边缘,而另一些滤波器(如第 54 号滤波器)学习了反转图像的技巧。

  1. 传递多个 O 图像并检查第四个滤波器在这些图像上的输出(我们仅用第四个滤波器作为示例用途;如果您希望,可以选择不同的滤波器)。

    1. 从数据中获取多个 O 图像:
    x, y = next(iter(trn_dl))
    x2 = x[y==0] 
    
    1. 重塑 x2,使其具有适合 CNN 模型的正确输入形状,即批处理大小 x 通道数 x 高度 x 宽度:
    x2 = x2.view(-1,1,28,28) 
    
    1. 定义一个变量,该变量存储了第一层的模型:
    first_layer = nn.Sequential(*list(model.children())[:1]) 
    
    1. 提取通过模型直到第一层(first_layer)的 O 图像的输出,如前所述:
    first_layer_output = first_layer(x2).detach() 
    
  2. 绘制将多个图像通过 first_layer 模型的输出:

    n = 4
    fig, ax = plt.subplots(n, n, figsize=(10,10))
    for ix, axis in enumerate(ax.flat):
        axis.imshow(first_layer_output[ix,4,:,:].cpu())
        axis.set_title(str(ix))
    plt.tight_layout()
    plt.show() 
    

上述代码的结果如下所示:

图 4.25:当多个 O 图像通过时第四个滤波器的激活

注意,在给定滤波器的行为(在本例中是第一层的第四个滤波器)在图像间保持一致。

  1. 现在,让我们创建另一个模型,该模型提取直到第二个卷积层(即前述模型中定义的四个层),然后提取通过原始 O 图像的输出。然后,我们将绘制第二层滤波器与输入 O 图像卷积的输出:

    second_layer = nn.Sequential(*list(model.children())[:4])
    second_intermediate_output=second_layer(im[None])[0].detach() 
    

    绘制将滤波器与相应图像卷积的输出:

    fig, ax = plt.subplots(11, 11, figsize=(10,10))
    for ix, axis in enumerate(ax.flat):
        axis.imshow(second_intermediate_output[ix].cpu())
        axis.set_title(str(ix))
    plt.tight_layout()
    plt.show() 
    

    上述代码的结果如下所示:

    图 4.26:第二个卷积层中 128 个滤波器的激活

    现在,让我们以前述图像中第 34 个滤波器的输出为例。当我们通过第 34 个滤波器传递多个 O 图像时,我们应该看到图像间的激活类似。让我们测试一下,如下所示:

    second_layer = nn.Sequential(*list(model.children())[:4])
    second_intermediate_output = second_layer(x2).detach()
    fig, ax = plt.subplots(4, 4, figsize=(10,10))
    for ix, axis in enumerate(ax.flat):
        axis.imshow(second_intermediate_output[ix,34,:,:].cpu())
        axis.set_title(str(ix))
    plt.tight_layout()
    plt.show() 
    

上述代码的结果如下所示:

图 4.27:当多个 O 图像通过时第 34 个滤波器的激活

注意,即使在这里,不同图像上第 34 个滤波器的激活也是相似的,即 O 的左半部分在激活滤波器时是相同的。

  1. 绘制全连接层的激活如下:

    1. 首先,获取更大的图像样本:
    custom_dl= DataLoader(XO('/content/all/*'),batch_size=2498, drop_last=True) 
    
    1. 接下来,从数据集中选择仅包含O图像,并将它们重塑,以便可以作为输入传递给我们的 CNN 模型:
    x, y = next(iter(custom_dl))
    x2 = x[y==0]
    x2 = x2.view(len(x2),1,28,28) 
    
    1. 获取扁平(全连接)层,并将之前的图像通过模型传递到扁平层:
    flatten_layer = nn.Sequential(*list(model.children())[:7])
    flatten_layer_output = flatten_layer(x2).detach() 
    
    1. 绘制扁平层:
    plt.figure(figsize=(100,10))
    plt.imshow(flatten_layer_output.cpu()) 
    

上述代码的输出如下所示:

图 4.28:全连接层的激活

注意输出的形状为 1,245 x 3,200,因为我们的数据集中有 1,245 个O图像,并且每个图像在扁平化层中有 3,200 个维度。

还有趣的是,当输入为O时,完全连接层的某些值会被突出显示(在这里,我们可以看到白色线条,每个点代表大于零的激活值)。

注意,尽管输入图像风格差异很大,但模型已经学会将一些结构带入完全连接的层。

现在我们已经学会了 CNN 的工作原理以及滤波器如何在这个过程中起作用,我们将应用这些知识来分类包含猫和狗图像的数据集。

构建用于分类真实世界图像的 CNN

到目前为止,我们已经学习了如何在 Fashion-MNIST 数据集上执行图像分类。在本节中,我们将为更真实的场景执行相同的操作,任务是对包含猫或狗的图像进行分类。我们还将学习当我们改变用于训练的图像数量时,数据集的准确性如何变化。

我们将在 Kaggle 上的数据集上工作,网址为www.kaggle.com/tongpython/cat-and-dog

您可以在 GitHub 上的Cats_Vs_Dogs.ipynb文件中找到以下代码,位于Chapter04文件夹中,网址为bit.ly/mcvp-2e。请务必从 GitHub 笔记本中复制 URL,以避免在重现结果时出现任何问题。

  1. 导入必要的包:

    import torchvision
    import torch.nn as nn
    import torch
    import torch.nn.functional as F
    from torchvision import transforms,models,datasets
    from PIL import Image
    from torch import optim
    device = 'cuda' if torch.cuda.is_available() else 'cpu'
    import cv2, glob, numpy as np, pandas as pd
    import matplotlib.pyplot as plt
    %matplotlib inline
    from glob import glob
    !pip install torch_summary 
    
  2. 下载数据集如下:

    1. 我们必须下载在colab环境中可用的数据集。但首先,我们必须上传我们的 Kaggle 认证文件:
    !pip install -q aggle
    from google.colab import files
    files.upload() 
    

为了进行这一步骤,您将需要上传您的kaggle.json文件,可以从您的 Kaggle 帐户中获取。有关如何获取kaggle.json文件的详细信息,请参见 GitHub 上关联笔记本中提供的相关信息,网址为bit.ly/mcvp-2e

  1. 接下来,指定我们要移动到 Kaggle 文件夹,并将kaggle.json文件复制到其中:

  2. !mkdir -p ~/.kaggle
    !cp kaggle.json ~/.kaggle/
    !ls ~/.kaggle
    !chmod 600 /root/.kaggle/kaggle.json 
    
  3. 最后,下载猫和狗的数据集并解压缩它:

  4. !kaggle datasets download -d tongpython/cat-and-dog
    !unzip cat-and-dog.zip 
    
  5. 提供训练和测试数据集文件夹:

    train_data_dir = '/content/training_set/training_set'
    test_data_dir = '/content/test_set/test_set' 
    
  6. 构建一个从前述文件夹获取数据的类。然后,根据图像对应的目录,为狗图像提供标签 1,猫图像提供标签 0。此外,确保获取的图像已被归一化为介于 0 和 1 之间的比例,并重新排列它以便先提供通道(因为 PyTorch 模型期望在提供图像的高度和宽度之前首先指定通道) - 如下所示进行:

    1. 定义 __init__ 方法,该方法接受一个文件夹作为输入,并将与 catsdogs 文件夹中的图像对应的文件路径(图像路径)存储在单独的对象中,在连接文件路径成为单个列表后:
    from torch.utils.data import DataLoader, Dataset
    class cats_dogs(Dataset):
        def __init__(self, folder):
            cats = glob(folder+'/cats/*.jpg')
            dogs = glob(folder+'/dogs/*.jpg')
            self.fpaths = cats + dogs 
    
    1. 接下来,随机化文件路径并根据这些文件路径对应的文件夹创建目标变量:
     from random import shuffle, seed; seed(10);
            shuffle(self.fpaths)
            self.targets=[fpath.split('/')[-1].startswith('dog') \
                          for fpath in self.fpaths] # dog=1 
    
    1. 定义 __len__ 方法,该方法对应于 self 类:
     def __len__(self): return len(self.fpaths) 
    
    1. 定义 __getitem__ 方法,我们用该方法从文件路径列表中指定一个随机文件路径,读取图像,并调整所有图像的大小为 224 x 224。考虑到我们的 CNN 需要从通道中获取每个图像的输入,我们将重新排列调整大小后的图像,以便在返回缩放后的图像和相应的 target 值之前,首先提供通道:
     def __getitem__(self, ix):
            f = self.fpaths[ix]
            target = self.targets[ix]
            im = (cv2.imread(f)[:,:,::-1])
            im = cv2.resize(im, (224,224))
            return torch.tensor(im/255).permute(2,0,1).to(device).float(),\
                   torch.tensor([target]).float().to(device) 
    
  7. 检查一个随机图像:

    data = cats_dogs(train_data_dir)
    im, label = data[200] 
    

我们需要将获取的图像重新排列以使通道排列在最后。这是因为 matplotlib 期望图像的通道在提供图像的高度和宽度之后指定:

  1. plt.imshow(im.permute(1,2,0).cpu())
    print(label) 
    

这将导致以下输出:

图 4.29:示例狗图像

  1. 定义模型、损失函数和优化器,如下所示:

    1. 首先,我们必须定义 conv_layer 函数,在其中按顺序执行卷积、ReLU 激活、批归一化和最大池化。此方法将在我们定义的最终模型中重复使用:
    def conv_layer(ni,no,kernel_size,stride=1):
        return nn.Sequential(
            nn.Conv2d(ni, no, kernel_size, stride),
            nn.ReLU(),
            nn.BatchNorm2d(no),
            nn.MaxPool2d(2)
        ) 
    
    1. 在上述代码中,我们将输入通道数 (ni)、输出通道数 (no)、kernel_sizestride 作为 conv_layer 函数的输入。

    2. 定义 get_model 函数,该函数执行多次卷积和池化操作(通过调用 conv_layer 方法),将输出展平,并在连接到输出层之前连接一个隐藏层:

    def get_model():
        model = nn.Sequential(
                  conv_layer(3, 64, 3),
                  conv_layer(64, 512, 3),
                  conv_layer(512, 512, 3),
                  conv_layer(512, 512, 3),
                  conv_layer(512, 512, 3),
                  conv_layer(512, 512, 3),
                  nn.Flatten(),
                  nn.Linear(512, 1),
                  nn.Sigmoid(),
                ).to(device)
        loss_fn = nn.BCELoss()
        optimizer=torch.optim.Adam(model.parameters(), lr= 1e-3)
        return model, loss_fn, optimizer 
    

您可以在 nn.Sequential 内部链式调用 nn.Sequential,深度可以随意。在上述代码中,我们使用 conv_layer 就像它是任何其他 nn.Module 层一样。

  1. 现在,我们必须调用 get_model 函数来获取模型、损失函数 (loss_fn) 和优化器,并使用我们从 torchsummary 包中导入的 summary 方法对模型进行总结:

  2. from torchsummary import summary
    model, loss_fn, optimizer = get_model()
    summary(model, torch.zeros(1,3, 224, 224)); 
    

上述代码将产生以下输出:

图 4.30:模型架构摘要

  1. 创建 get_data 函数,该函数创建 cats_dogs 类的对象,并为训练和验证文件夹分别创建 batch_size 为 32 的 DataLoader

    def get_data():
        train = cats_dogs(train_data_dir)
        trn_dl = DataLoader(train, batch_size=32, shuffle=True,
                                              drop_last = True)
        val = cats_dogs(test_data_dir)
        val_dl = DataLoader(val,batch_size=32, shuffle=True, drop_last = True)
        return trn_dl, val_dl 
    

在前述代码中,我们通过指定drop_last = True来忽略最后一个数据批次。我们这样做是因为最后一个批次的大小可能与其他批次不同。

  1. 定义将在数据批次上训练模型的函数,就像我们在前面的章节中所做的一样:

    def train_batch(x, y, model, opt, loss_fn):
        model.train()
        prediction = model(x)
        batch_loss = loss_fn(prediction, y)
        batch_loss.backward()
        optimizer.step()
        optimizer.zero_grad()
        return batch_loss.item() 
    
  2. 定义用于计算准确性和验证损失的函数,就像我们在前面的章节中所做的一样:

    1. 定义accuracy函数:
    @torch.no_grad()
    def accuracy(x, y, model):
        prediction = model(x)
        is_correct = (prediction > 0.5) == y
        return is_correct.cpu().numpy().tolist() 
    
    1. 请注意,用于精度计算的前述代码与 Fashion-MNIST 分类中的代码不同,因为当前模型(猫与狗分类)是为二元分类构建的,而 Fashion-MNIST 模型是为多类分类构建的。

    2. 定义验证损失计算函数:

    @torch.no_grad()
    def val_loss(x, y, model):
        prediction = model(x)
        val_loss = loss_fn(prediction, y)
        return val_loss.item() 
    
  3. 在 5 个 epoch 上训练模型,并在每个 epoch 结束时检查测试数据的准确性,就像我们在前面的章节中所做的一样:

    1. 定义模型并获取所需的 DataLoaders:
    trn_dl, val_dl = get_data()
    model, loss_fn, optimizer = get_model() 
    
    1. 在增加的 epoch 上训练模型:
    train_losses, train_accuracies = [], []
    val_losses, val_accuracies = [], []
    for epoch in range(5):
        train_epoch_losses, train_epoch_accuracies = [], []
        val_epoch_accuracies = []
        for ix, batch in enumerate(iter(trn_dl)):
            x, y = batch
            batch_loss = train_batch(x, y, model,optimizer, loss_fn)
            train_epoch_losses.append(batch_loss)
        train_epoch_loss = np.array(train_epoch_losses).mean()
        for ix, batch in enumerate(iter(trn_dl)):
            x, y = batch
            is_correct = accuracy(x, y, model)
            train_epoch_accuracies.extend(is_correct)
        train_epoch_accuracy = np.mean(train_epoch_accuracies)
        for ix, batch in enumerate(iter(val_dl)):
            x, y = batch
            val_is_correct = accuracy(x, y, model)
            val_epoch_accuracies.extend(val_is_correct)
        val_epoch_accuracy = np.mean(val_epoch_accuracies)
        train_losses.append(train_epoch_loss)
        train_accuracies.append(train_epoch_accuracy)
        val_accuracies.append(val_epoch_accuracy) 
    
  4. 绘制随着 epoch 增加,训练和验证准确性的变化:

    epochs = np.arange(5)+1
    import matplotlib.ticker as mtick
    import matplotlib.pyplot as plt
    import matplotlib.ticker as mticker
    %matplotlib inline
    plt.plot(epochs, train_accuracies, 'bo',
             label='Training accuracy')
    plt.plot(epochs, val_accuracies, 'r',
             label='Validation accuracy')
    plt.gca().xaxis.set_major_locator(mticker.MultipleLocator(1))
    plt.title('Training and validation accuracy \
    with 4K data points used for training')
    plt.xlabel('Epochs')
    plt.ylabel('Accuracy')
    plt.gca().set_yticklabels(['{:.0f}%'.format(x*100) \
                               for x in plt.gca().get_yticks()])
    plt.legend()
    plt.grid('off')
    plt.show() 
    

前述代码的输出如下:

图 4.31:随着 epoch 增加,训练和验证准确性的变化

请注意,在 5 个 epoch 结束时的分类准确性约为 86%。

正如我们在前一章中讨论的,批量归一化对提高分类准确性有很大影响——通过训练模型而不使用批量归一化来自行验证这一点。此外,如果您使用较少的参数,模型也可以在没有批量归一化的情况下进行训练。您可以通过减少层数、增加步幅、增加池化或将图像调整为低于 224 x 224 的数字来实现这一点。

到目前为止,我们的训练基于约 8K 个示例,其中 4K 个示例来自cat类,其余来自dog类。在接下来的章节中,我们将了解在分类测试数据集的分类准确性中,训练示例数量减少对每个类别的影响。

对训练图像数量的影响

我们知道,通常使用的训练示例越多,我们的分类准确性就越好。在本节中,我们将通过人为减少可用于训练的图像数量,然后在分类测试数据集时测试模型的准确性,来了解使用不同数量的可用图像对训练准确性的影响。

可在 GitHub 上的Chapter04文件夹中的Cats_Vs_Dogs.ipynb文件中找到以下代码,网址为bit.ly/mcvp-2e。鉴于这里提供的大部分代码与我们在前一节中看到的类似,我们仅为简洁起见提供了修改后的代码。相应的 GitHub 笔记本将包含完整的代码。

在这里,我们只希望在训练数据集中有每类 500 个数据点。我们可以通过在 __init__ 方法中限制文件数到每个文件夹中的前 500 个图像路径,并确保其余保持与上一节相同的方式来实现这一点:

 def __init__(self, folder):
        cats = glob(folder+'/cats/*.jpg')
        dogs = glob(folder+'/dogs/*.jpg')
        self.fpaths = cats[:500] + dogs[:500]
        from random import shuffle, seed; seed(10);
            shuffle(self.fpaths)
        self.targets = [fpath.split('/')[-1].startswith('dog') \
                        for fpath in self.fpaths] 

在上述代码中,与我们在前一节中执行的初始化唯一区别在于 self.paths,我们现在将考虑的文件路径数量限制为每个文件夹中的前 500 个。

现在,一旦我们执行其余代码,就像在前一节中所做的那样,构建在 1,000 张图像(每类 500 张)上的模型在测试数据集上的准确率如下:

图 4.32:使用 1K 数据点的训练和验证准确率

可以看到,由于训练中图像样本较少,在测试数据集上模型的准确率显著降低,即下降至约 66%。

现在,让我们看看训练数据点数量如何影响测试数据集的准确性,通过改变用于训练模型的可用训练示例的数量(我们为每种情况构建一个模型)。

我们将使用与 1K(每类 500 个)数据点训练示例相同的代码,但会改变可用图像的数量(分别为 2K、4K 和 8K 总数据点)。为简洁起见,我们只关注在不同训练图像数量下运行模型的输出。结果如下:

图 4.33:使用不同数据点数量的训练和验证准确率

正如您所见,可用的训练数据越多,模型在测试数据上的准确率就越高。然而,在我们遇到的每种情况下,我们可能没有足够大量的训练数据。下一章将涵盖迁移学习,通过引导您了解各种技术,即使在少量训练数据的情况下也能获得高准确率。

总结

当将与先前看到的已被翻译的类似的新图像作为模型的输入时,传统神经网络会失败。CNN 在解决此缺陷中发挥了关键作用。这是通过 CNN 中存在的各种机制实现的,包括滤波器、步幅和池化。最初,我们建立了一个玩具示例来学习 CNN 的工作原理。然后,我们学习了数据增强如何通过在原始图像上创建翻译增强来增加模型的准确性。之后,我们了解了不同滤波器在特征学习过程中学到的内容,以便我们能够实现一个用于图像分类的 CNN。

最后,我们看到不同数量的训练数据对测试数据准确性的影响。在这里,我们看到可用的训练数据越多,测试数据的准确性就越高。在下一章中,我们将学习如何利用各种迁移学习技术来提高测试数据集的准确性,即使我们只有少量的训练数据。

问题

  1. 在使用传统神经网络时,为什么在第一章节中对翻译图像的预测结果较低?

  2. 卷积是如何进行的?

  3. 如何确定滤波器中的最优权重值?

  4. 卷积和池化的组合如何帮助解决图像翻译的问题?

  5. 靠近输入层的卷积滤波器学习什么?

  6. 池化在构建模型时有哪些功能性?

  7. 为什么我们不能像在 Fashion-MNIST 数据集上那样,对输入图像进行展平,然后为真实世界的图像训练模型?

  8. 数据增强如何帮助改进图像翻译?

  9. 在什么情况下我们利用collate_fn来处理数据加载器?

  10. 改变训练数据点的数量对验证数据集的分类准确率有什么影响?

了解更多关于 Discord 的内容

加入我们社区的 Discord 空间,与作者和其他读者讨论:

packt.link/modcv