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

128 阅读31分钟

PyTorch 现代计算机视觉(二)

三、使用 PyTorch 构建深度神经网络

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

为了不引入太多的复杂性和混乱,我们在前一章中只讨论了神经网络的基本方面。然而,在训练网络时,我们还需要调整更多的输入。通常,这些输入被称为超参数。与神经网络中的参数(在训练期间学习)相反,这些输入是由构建网络的人提供的超参数。改变每个超参数的不同方面可能会影响训练神经网络的精度或速度。此外,一些额外的技术(如缩放、批处理规范化和正则化)有助于提高神经网络的性能。我们将在本章中学习这些概念。

然而,在此之前,我们将了解图像是如何表示的——只有到那时,我们才会深入研究超参数的细节。在了解超参数的影响时,我们将局限于一个数据集——fashion mnist——以便我们可以比较各种超参数变化的影响。通过该数据集,我们还将了解训练和验证数据,以及拥有两个独立数据集的重要性。最后,我们将了解神经网络过度拟合的概念,然后了解某些超参数如何帮助我们避免过度拟合。

总之,在本章中,我们将讨论以下主题:

  • 代表一幅图像
  • 为什么要利用神经网络进行图像分析?
  • 为影像分类准备数据
  • 训练神经网络
  • 缩放数据集以提高模型准确性
  • 了解改变批量大小的影响
  • 了解改变损失优化器的影响
  • 理解改变学习速度的影响
  • 了解学习率退火的影响
  • 构建更深层次的神经网络
  • 了解批处理规范化的影响
  • 过度拟合的概念

我们开始吧!

代表一幅图像

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

一幅图像有个高度 x 宽度 x c 个像素,其中高度是像素的的数量,宽度是像素的的数量, c 是像素的通道的数量。 c 对于彩色图像是 3(图像的红、强度各一个通道),对于灰度图像是 1。包含 3 x 3 像素及其相应标量值的灰度图像示例如下所示:

同样,像素值为 0 意味着它是一片漆黑,而 255 意味着它是纯亮度的(也就是说,灰度是纯白,彩色图像的各个通道是纯红/绿/蓝)。

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

Python 可以将图像转换成结构化数组和标量,如下所示:

The following code is available as Inspecting_grayscale_images.ipynb in the Chapter03 folder of this book's GitHub repository - tinyurl.com/mcvp-packt

  1. 下载示例图像:
!wget https://www.dropbox.com/s/l98leemr7r5stnm/Hemanvi.jpeg

  1. 导入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')

前面一系列步骤的输出如下:

您可能已经注意到,前面的图像表示为 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')

这会产生以下输出:

自然地,用更少的像素来表示相同的图像会导致更模糊的输出。

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

这会产生以下输出:

当复制并粘贴到 Excel 中并按像素值进行颜色编码时,同一组像素值将如下所示:

正如我们之前提到的,标量值接近 255 的像素看起来更亮,而接近 0 的像素看起来更暗。

前面的步骤也适用于彩色图像,彩色图像被表示为三维向量。最亮的红色像素表示为(255,0,0)。类似地,三维矢量图像中的纯白像素表示为(255,255,255)。记住这一点,让我们为彩色图像创建一个结构化的像素值数组:

The following code is available as Inspecting_color_images.ipynb in the Chapter03 folder of this book's GitHub repository - tinyurl.com/mcvp-packt

  1. 下载彩色图像:
!wget https://www.dropbox.com/s/l98leemr7r5stnm/Hemanvi.jpeg
  1. 导入相关包并加载映像:
import cv2, matplotlib.pyplot as plt
%matplotlib inline
img = cv2.imread('Hemanvi.jpeg') 

  1. 裁剪图像:
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)

这会产生以下输出:

  1. 右下角的 3 x 3 像素阵列可以如下获得:
crop = img[-3:,-3:]

  1. 打印并绘制像素值:
print(crop)
plt.imshow(crop)

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

现在,我们可以将每个图像表示为标量数组(对于灰度图像)或数组数组(对于彩色图像),我们实际上已经将磁盘上的文件转换为结构化的数组格式,现在可以使用多种技术对其进行数学处理。将图像转换为结构化的数字数组(即将图像读入 Python 内存)使我们能够在图像(表示为数字数组)上执行数学运算。我们可以利用这种数据结构来执行各种任务,如分类、检测和分割,所有这些将在后面的章节中详细讨论。

现在我们已经了解了图像是如何表示的,让我们来理解利用神经网络进行图像分类的原因。

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

在传统的计算机视觉中,我们会在使用它们作为输入之前为每个图像创建一些特征。让我们基于下面的样本图像来看一些这样的特征,以便理解我们通过训练神经网络来避免的努力:

请注意,我们不会向您介绍如何获得这些特征,因为这里的目的是帮助您认识到为什么手动创建特征是次优的练习:

  • 直方图特征:对于一些任务,比如自动亮度或夜视,了解画面中的光照很重要;即亮或暗像素的比例。下图显示了示例图像的直方图。它描述了图像被很好地照亮,因为在 255:

  • 边缘和角点特征:对于图像分割等任务来说,找到每个人对应的像素集是很重要的,首先提取边缘是有意义的,因为人的边界只是边缘的集合。在诸如图像配准的其他任务中,检测关键标志是至关重要的。这些标志将是图像中所有角的子集。下图显示了我们的示例图像中可以找到的边缘和拐角:

  • 分色功能:在自动驾驶汽车的交通灯检测等任务中,系统理解交通灯上显示的是什么颜色是很重要的。下图(彩色效果最佳)显示了示例图像中的红色、绿色和蓝色像素:

  • 图像渐变 特征:更进一步,理解颜色在像素级别如何变化可能很重要。不同的纹理可以给我们不同的梯度,这意味着它们可以用作纹理检测器。事实上,找到梯度是边缘检测的先决条件。下图显示了示例图像的一部分的整体渐变以及渐变的 y 和 x 分量:

这些仅仅是这些特性中的一小部分。还有很多,很难全部涵盖。创建这些功能的主要缺点是,您需要成为图像和信号分析方面的专家,并且应该完全了解哪些功能最适合解决某个问题。即使两个约束条件都得到满足,也不能保证这样的专家能够找到正确的输入组合,即使他们找到了,也不能保证这样的组合能够在新的、未知的场景中工作。

由于这些缺点,社区在很大程度上转向了基于神经网络的模型。这些模型不仅能自动找到合适的功能,还能学习如何优化组合它们来完成工作。正如我们在第一章中已经了解的,神经网络同时充当特征提取器和分类器。

既然我们已经看了一些历史特征提取技术的例子和它们的缺点,让我们学习如何在图像上训练神经网络。

为图像分类准备数据

鉴于我们在本章中涉及多个场景,为了让我们看到一个场景相对于另一个场景的优势,我们将在本章中使用一个数据集——时尚 MNIST 数据集。让我们准备这个数据集:

The following code is available as Preparing_our_data.ipynb in the Chapter03 folder of this book's GitHub repository - tinyurl.com/mcvp-packt

  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来指定我们只想下载训练图像

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

  1. 检查我们正在处理的张量:
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}') 

上述代码的输出如下:

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

  1. 为所有 10 个可能的类别绘制 10 个图像的随机样本:
  • 导入相关的包以绘制图像网格,这样您也可以处理数组:
import matplotlib.pyplot as plt
%matplotlib inline
import numpy as np
  • 创建一个图,其中我们可以显示一个 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]

注意,在前面的代码中,我们获取第 0 个^(?? 索引作为np.where条件的输出,因为它的长度为 1。它包含目标值(tr_targets)等于label_class的所有索引的数组。)

  • 循环 10 次以填充给定行的列。此外,我们需要从先前获得的对应于给定类别的指数(label_x_rows)中选择一个随机值(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()

这会产生以下输出:

请注意,在前面的图像中,每一行都代表属于同一类的 10 个不同图像的样本。

既然我们已经学习了如何导入数据集,在下一节中,我们将学习如何使用 PyTorch 训练神经网络,以便它接收图像并预测该图像的类别。此外,我们还将了解各种超参数对预测准确性的影响。

训练神经网络

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

  1. 导入相关的包。
  2. 构建一个可以一次提取一个数据点的数据的数据集。
  3. 从数据集中包装数据加载器。
  4. 建立一个模型,然后定义损失函数和优化器。
  5. 定义两个函数来分别训练和验证一批数据。
  6. 定义一个计算数据准确性的函数。
  7. 根据每批数据在不断增加的时期内执行重量更新。

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

下面的代码可以在本书的 GitHub 库【tinyurl.com/mcvp-packt[…](tinyurl.com/mcvp-packt)

  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
  1. 构建一个获取数据集的类。记住,它是从一个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的数据集生成一个训练数据加载器trn_dl。这将针对批量随机采样 32 个数据点:
def get_data(): 
    train = FMNISTDataset(tr_images, tr_targets) 
    trn_dl = DataLoader(train, batch_size=32, shuffle=True)
    return trn_dl

在前面的代码行中,我们创建了一个名为trainFMNISTDataset类的对象,并调用了 DataLoader,以便它随机获取 32 个数据点来返回训练数据加载器;那就是,trn_dl

  1. 定义模型,以及损失函数和优化器:
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

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

注意,我们在神经网络中根本没有使用“softmax”。输出的范围是不受约束的,因为值可以具有无限的范围,而交叉熵损失通常期望输出为概率(每行总和应为 1)。这在这个设置中仍然有效,因为nn.CrossEntropyLoss实际上希望我们发送原始逻辑(即不受约束的值)。它在内部执行 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上提取batch_loss.item()来提取损失值作为一个标量。

  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与实际情况进行比较,以便我们可以检查每一行是否预测正确。最后,我们在将is_correct对象列表移动到 CPU 并转换成 numpy 数组后,返回该列表。

  1. 使用下列代码行来训练神经网络:
  • 初始化模型、损失、优化器和数据加载器:
trn_dl = get_data()
model, loss_fn, optimizer = get_model()
  • 在每个时期结束时调用包含精度和损失值的列表:
losses, accuracies = [], []
  • 定义纪元的数量:
for epoch in range(5):
    print(epoch)
  • 调用列表,该列表将包含与一个时期内的每个批次相对应的准确度和损失值:
    epoch_losses, epoch_accuracies = [], []
  • 通过迭代数据加载器创建批量训练数据:
    for ix, batch in enumerate(iter(trn_dl)):
        x, y = batch
  • 使用train_batch功能训练批次,并将训练结束时的损失值作为batch_loss存储在批次顶部。此外,将各批次的损失值存储在epoch_losses列表中:
        batch_loss = train_batch(x, y, model, optimizer, \
                                                    loss_fn)
        epoch_losses.append(batch_loss)
  • 我们存储一个时期内所有批次的平均损失值:
    epoch_loss = np.array(epoch_losses).mean()
  • 接下来,我们在所有批次的训练结束时计算预测的准确性:
    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)
  • 将每个时期结束时的损失和精度值存储在列表中:
    losses.append(epoch_loss)
    accuracies.append(epoch_accuracy)

可以使用以下代码来显示训练损失和准确度在增加的时期内的变化:

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()

上述代码的输出如下:

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

注意,由于我们没有保存torch.random_seed(0),当您执行提供的代码时,结果可能会有所不同。然而,你得到的结果也应该让你得到类似的结论。

现在,您已经对如何训练神经网络有了一个完整的了解,让我们来研究一些我们应该遵循的良好实践,以实现良好的模型性能,以及使用它们背后的原因。这可以通过微调各种超参数来实现,其中一些我们将在接下来的章节中讨论。

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

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

The following code is available as Scaling_the_dataset.ipynb in the Chapter03 folder of this book's GitHub repository - tinyurl.com/mcvp-packt

  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
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
  1. 修改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。

假设像素值的范围在 0 到 255 之间,将它们除以 255 将得到始终在 0 到 1 之间的值。

  1. 训练一个模型,就像我们在前面章节的步骤 4567 中所做的那样:
  • 获取数据:
def get_data(): 
    train = FMNISTDataset(tr_images, tr_targets) 
    trn_dl = DataLoader(train, batch_size=32, shuffle=True)
    return trn_dl
  • 定义模型:
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
  • 定义用于训练和验证一批数据的函数:
def train_batch(x, y, model, opt, loss_fn):
    model.train()
    # 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 memory for next batch of calculations
 optimizer.zero_grad()
 return batch_loss.item() @torch.no_grad()
def accuracy(x, y, model):
 model.eval()   
    # 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()
  • 在不断增加的时期内训练模型:
trn_dl = get_data()
model, loss_fn, optimizer = get_model()
losses, accuracies = [], []
for epoch in range(5):
    print(epoch)
    epoch_losses, epoch_accuracies = [], []
    for ix, batch in enumerate(iter(trn_dl)):
        x, y = batch
        batch_loss = train_batch(x, y, model, optimizer, 
                                        loss_fn)
        epoch_losses.append(batch_loss)
    epoch_loss = np.array(epoch_losses).mean()
    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)
    losses.append(epoch_loss)
    accuracies.append(epoch_accuracy)

训练损失和精度值的变化如下:

如我们所见,训练损失持续减少,训练准确度持续增加,从而将时期准确度增加到约 85%。

将前面的输出与输入数据未缩放的情况进行对比,在这种情况下,训练损失没有持续减少,并且在五个时期结束时训练数据集的准确性仅为 12%。

让我们深入探讨一下缩放在这里有所帮助的可能原因。

让我们以如何计算 sigmoid 值为例:

在下表中,我们根据前面的公式计算了 Sigmoid 列:

在左侧表格中,我们可以看到,当权重值大于 0.1 时,Sigmoid 值不会随着权重值的增加(变化)而变化。此外,当重量极小时,Sigmoid 值仅变化很小;改变 sigmoid 值的唯一方法是将重量改变到非常非常小的量。

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

其原因是大负值的指数(由权重值乘以大数值得到)非常接近于 0,而当权重值乘以比例输入时,指数值会发生变化,如右侧表格所示。

现在我们已经知道,除非权重值非常小,否则 Sigmoid 值不会发生显著变化,我们现在将了解如何影响权重值,使其趋向最佳值。

缩放输入数据集以使其包含更小范围的值通常有助于实现更高的模型精度。

接下来,我们将了解任何神经网络的其他主要超参数之一的影响:批量大小。

了解改变批量大小的影响

在上一节中,训练数据集中每批考虑 32 个数据点。这导致每个时期更大数量的权重更新,因为每个时期有 1,875 个权重更新(60,000/32 几乎等于 1,875,其中 60,000 是训练图像的数量)。

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

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

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

既然我们已经引入了验证数据,那么让我们重新运行在构建神经网络部分中提供的代码,使用额外的代码来生成验证数据,以及计算验证数据集的损失和准确度值。

The code for the Batch size of 32 and Batch size of 10,000 sections is available as Varying_batch_size.ipynb in the Chapter03 folder of this book's GitHub repository - tinyurl.com/mcvp-packt

32 件的批量

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

  1. 下载并导入训练图像和目标:
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)
tr_images = fmnist.data
tr_targets = fmnist.targets
  1. 与训练图像类似,我们必须通过指定train = False来下载和导入验证数据集,同时在我们的数据集中调用FashionMNIST方法:
val_fmnist =datasets.FashionMNIST(data_folder,download=True, \
                                                 train=False)
val_images = val_fmnist.data
val_targets = val_fmnist.targets
  1. 导入相关包并定义device:
import matplotlib.pyplot as plt
%matplotlib inline
import numpy as np
from torch.utils.data import Dataset, DataLoader
import torch
import torch.nn as nn
device = 'cuda' if torch.cuda.is_available() else 'cpu'
  1. 定义数据集类(FashionMNIST)、将用于对一批数据进行训练的函数(train_batch)、计算准确度(accuracy),然后定义模型架构、损失函数和优化器(get_model)。请注意,用于获取数据的函数将是唯一与我们在前面章节中看到的有所不同的函数(因为我们现在正在处理定型和验证数据集),因此我们将在下一步中构建它:
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)

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 = Adam(model.parameters(), lr=1e-2)
    return model, loss_fn, optimizer

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()

def accuracy(x, y, model):
 model.eval()
    # this is the same as @torch.no_grad 
    # at the top of function, only difference
    # being, grad is not computed in the with scope
 with torch.no_grad():
        prediction = model(x)
    max_values, argmaxes = prediction.max(-1)
    is_correct = argmaxes == y
    return is_correct.cpu().numpy().tolist()
  1. 定义一个将获取数据的函数;也就是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

在前面的代码中,除了我们之前看到的train对象之外,我们还创建了一个名为valFMNISTDataset类的对象。此外,用于验证的数据加载器(val_dl)的批次大小为len(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. 获取培训和验证数据加载器。此外,初始化模型、损失函数和优化器:
trn_dl, val_dl = get_data()
model, loss_fn, optimizer = get_model()
  1. 训练模型,如下所示:
  • 初始化包含训练和验证准确性以及递增时期的损失值的列表:
train_losses, train_accuracies = [], []
val_losses, val_accuracies = [], []
  • 遍历五个时期,并初始化包含给定时期内各批训练数据的准确度和损失的列表:
for epoch in range(5):
    print(epoch)
    train_epoch_losses, train_epoch_accuracies = [], []
  • 循环遍历一批训练数据,计算一个历元内的精度(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)
  • 计算一批验证数据内的损失值和准确度(因为验证数据的批量等于验证数据的长度):
    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)

注意,在前面的代码中,验证数据的丢失值是使用val_loss函数计算的,并存储在validation_loss变量中。此外,所有验证数据点的精度存储在val_is_correct列表中,而其平均值存储在val_epoch_accuracy变量中。

  • 最后,我们将训练和验证数据集的准确度和损失值附加到包含历元级聚合验证和准确度值的列表。我们这样做是为了在下一步中查看纪元级别的改进:
    train_losses.append(train_epoch_loss)
    train_accuracies.append(train_epoch_accuracy)
    val_losses.append(validation_loss)
    val_accuracies.append(val_epoch_accuracy)
  1. 直观显示训练和验证数据集中准确性和损失值随时间推移的提高情况:
epochs = np.arange(5)+1
import matplotlib.ticker as mtick
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
%matplotlib inline
plt.subplot(211)
plt.plot(epochs, train_losses, 'bo', label='Training loss')
plt.plot(epochs, val_losses, 'r', label='Validation loss')
plt.gca().xaxis.set_major_locator(mticker.MultipleLocator(1))
plt.title('Training and validation loss \
when batch size is 32')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.grid('off')
plt.show()
plt.subplot(212)
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 \
when batch size is 32')
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()

前面的代码给出了以下输出:

如您所见,当批次大小为 32 时,在五个时期结束时,训练和验证准确度约为 85%。接下来,当在get_data函数中训练数据加载器时,我们将改变batch_size参数,以查看它在五个时期结束时对准确性的影响。

10,000 件的批量

在本节中,我们将使用每批 10,000 个数据点,以便我们可以了解改变批量大小会产生什么影响。

请注意,除了步骤 5 中的代码之外,32 部分的批量中提供的代码在这里保持完全相同。这里,我们将在get_data函数中为训练和验证数据集指定数据加载器。我们鼓励您在执行代码时参考本书的 GitHub 资源库中的相应笔记本。

我们将修改get_data,使其在从训练数据集中获取训练数据加载器时具有 10,000 的批处理大小,如下所示:

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

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

在这里,我们可以看到准确性和损失值没有达到与前一个场景相同的级别,在前一个场景中,批量大小为 32,因为当批量大小为 32 (1875)时,时间权重更新的次数较少。在批量大小为 10,000 的情况下,每个时期有六个权重更新,因为每批有 10,000 个数据点,这意味着总的训练数据大小为 60,000。

到目前为止,我们已经了解了如何缩放数据集,以及改变批量大小对模型训练时间的影响,以实现一定的准确性。在下一节中,我们将了解在同一个数据集上改变丢失优化器的影响。

当您有少量的历元时,较低的批处理大小通常有助于实现最佳的准确性,但是它不应该太低而影响训练时间。

了解改变损失优化器的影响

到目前为止,我们一直在优化基于 Adam 优化器的损失。在本节中,我们将执行以下操作:

  • 修改优化器,使其成为一个随机梯度下降 ( 新币)优化器
  • 在数据加载器中提取数据时,恢复为 32 的批处理大小
  • 将时期数增加到 10(这样我们就可以在更长的时期内比较 SGD 和 Adam 的性能)

做出这些改变意味着 32 段的批量中只有一步会改变(因为在 32 段的批量中批量已经是 32);也就是说,我们将修改优化器,使其成为 SGD 优化器。

让我们修改 32 部分的批量的步骤 4* 中的get_model函数,以便修改 optimzier,这样我们就可以使用 SGD 优化器,如下所示:*

The following code is available as Varying_loss_optimizer.ipynb in the Chapter03 folder of this book's GitHub repository - tinyurl.com/mcvp-packt . Note that we are not providing all the steps for brevity and that only the steps where we're making a change from the code we went through in the Batch size of 32 section will be discussed in the following code. We encourage you to refer to the respective notebooks in this book's GitHub repository while executing the code.

  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 中的时期数,同时保持所有其他步骤(除了步骤 4步骤 8 )与它们在 32 部分的批量中相同。

  1. 增加我们将用于训练模型的纪元数量:
train_losses, train_accuracies = [], []
val_losses, val_accuracies = [], []
for epoch in range(10):
    train_epoch_losses, train_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)
        validation_loss = val_loss(x, y, model)
    val_epoch_accuracy = np.mean(val_is_correct)

    train_losses.append(train_epoch_loss)
    train_accuracies.append(train_epoch_accuracy)
    val_losses.append(validation_loss)
    val_accuracies.append(val_epoch_accuracy)

进行这些更改后,一旦我们按顺序执行 32 个部分的批量中的所有剩余步骤,训练和验证数据集的准确度和损失值在增加的时期内的变化将如下:

让我们在优化器为 Adam 的情况下,针对不断增加的时期的训练和验证损失以及准确性变化获取相同的输出。这就要求我们将步骤 4 中的优化器改为 Adam。

一旦进行了这种改变并且执行了代码,训练和验证数据集的准确度和损失值的变化如下:

如您所见,当我们使用 Adam 优化器时,准确率仍然非常接近 85%。但是,请注意,到目前为止,学习率为 0.01。

在下一节中,我们将了解学习率对验证数据集准确性的影响。

某些优化器比其他优化器更快地达到最佳精度。Adam 通常能更快地达到最佳精度。其他一些著名的优化器包括 Adagrad、Adadelta、AdamW、LBFGS 和 RMSprop。

理解改变学习速度的影响

到目前为止,我们在训练模型时一直使用 0.01 的学习率。在第一章人工神经网络基础中,我们了解到学习速率在获得最佳权重值方面起着关键作用。这里,当学习率小时,权重值逐渐向最佳值移动,而当学习率大时,权重值在非最佳值振荡。我们在第一章人工神经网络基础中处理了一个玩具数据集,所以我们将在这一部分处理一个现实场景。

为了理解变化的学习率的影响,我们将经历以下场景:

  • 规模数据集上的学习率更高(0.1)
  • 缩放数据集的学习率较低(0.00001)
  • 未缩放数据集的学习率较低(0.001)
  • 在非缩放数据集上的学习率更高(0.1)

总之,在本节中,我们将了解各种学习率值对缩放数据集和非缩放数据集的影响。

在本节中,我们将了解学习率对未缩放数据的影响,尽管我们已经确定缩放数据集是有帮助的。我们再次这样做是因为我们想让您直观地了解在模型能够适应数据的情况和模型不能适应数据的情况之间,权重的分布是如何变化的。

现在,让我们了解模型如何在缩放数据集上学习。

学习率对规模数据集的影响

在本节中,我们将根据以下内容对比训练和验证数据集的准确性:

  • 高学习率
  • 中等学习速度
  • 学习率低

以下三小节的代码可以在本书的 GitHub 资源库【tinyurl.com/mcvp-packt[… GitHub 资源库中相应的笔记本。**](tinyurl.com/mcvp-packt)

我们开始吧!

高学习率

在本节中,我们将采用以下策略:

  • 当我们使用 Adam 优化器时,我们需要执行的步骤将与 32 部分的批处理大小完全相同。
  • 当我们定义get_model函数时,唯一的变化是optimizer中的学习率。这里,我们将把学习率(lr)的值改为 0.1。

请注意,除了我们将在本节中对get_model函数进行的修改之外,所有代码都与 32 节中的批量相同。

要修改学习率,我们必须在optimizer的定义中进行更改,这可以在get_model函数中找到,如下所示:

def get_model():
    model = nn.Sequential(
                nn.Linear(28 * 28, 1000),
                nn.ReLU(),
                nn.Linear(1000, 10)
            ).to(device)

    loss_fn = nn.CrossEntropyLoss()
    optimizer = Adam(model.parameters(), lr=1e-1)
    return model, loss_fn, optimizer

注意,在前面的代码中,我们修改了优化器,使其学习率为 0.1 ( lr=1e-1)。

一旦我们执行了 GitHub 中提供的所有剩余步骤,对应于训练和验证数据集的准确度和损失值将如下所示:

请注意,验证数据集的准确度约为 25%(与我们在学习率为 0.01 时达到的约 85%的准确度相比)。

在下一节中,我们将了解学习率为中等(0.001)时验证数据集的准确性。

中等学习速度

在本节中,我们将通过修改get_model函数并从头开始重新训练模型,将优化器的学习率降低到 0.001。

get_model功能修改后的代码如下:

def get_model():
    model = nn.Sequential(
                nn.Linear(28 * 28, 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

注意,在前面的代码中,由于我们修改了lr参数值,学习率已经降低到一个很小的值。

一旦我们执行了 GitHub 中提供的所有剩余步骤,对应于训练和验证数据集的准确度和损失值将如下所示:

从前面的输出可以看出,当学习率(or)从 0.1 降低到 0.001 时,模型训练成功

在下一节中,我们将进一步降低学习率。

学习率低

在本节中,我们将通过修改get_model函数并从头开始重新训练模型,将优化器的学习率降低到 0.00001。此外,我们将运行模型更长的时期(100)。

我们将为get_model函数使用的修改后的代码如下:

def get_model():
    model = nn.Sequential(
                nn.Linear(28 * 28, 1000),
                nn.ReLU(),
                nn.Linear(1000, 10)
            ).to(device)

    loss_fn = nn.CrossEntropyLoss()
    optimizer = Adam(model.parameters(), lr=1e-5)
    return model, loss_fn, optimizer

注意,在前面的代码中,由于我们修改了lr参数值,学习率已经降低到一个非常小的值。

一旦我们执行了 GitHub 中提供的所有剩余步骤,对应于训练和验证数据集的准确度和损失值将如下所示:

从上图中,我们可以看到,与之前的场景相比,该模型的学习速度要慢得多(中等学习速度)。这里,与学习率为 0.001 时的八个时期相比,需要大约 100 个时期才能达到大约 89%的准确度。

此外,我们还应该注意到,当学习率与之前的场景相比较低时,训练和验证损失之间的差距要小得多(在之前的场景中,在时期 4 的末尾存在类似的差距)。其原因是当学习率低时,权重更新低得多,这意味着训练和验证损失之间的差距不会迅速扩大。

到目前为止,我们已经了解了学习率对训练和验证数据集准确性的影响。在下一节中,我们将了解对于不同的学习率值,权重值的分布如何在各层之间变化。

不同学习速率的跨层参数分布

在前面的章节中,我们了解到在高学习率(0.1)的情况下,模型无法被训练(模型训练不足)。然而,我们可以训练模型,以便当学习率为中等(0.001)或低(0.00001)时,它具有相当高的准确性。在这里,我们看到中等学习率能够快速地过度拟合,而低学习率需要更长的时间才能达到与中等学习率模型相当的精度。

在本节中,我们将了解参数分布如何成为模型过拟合和欠拟合的良好指标。

到目前为止,我们的模型中有四个参数组:

  • 将输入层连接到隐藏层的层中的权重
  • 隐藏层中的偏差
  • 将隐藏层连接到输出层的层中的权重
  • 输出层中的偏置

让我们使用下面的代码来看看这些参数的分布情况(我们将为每个模型执行下面的代码):

for ix, par in enumerate(model.parameters()):
    if(ix==0):
        plt.hist(par.cpu().detach().numpy().flatten())
        plt.title('Distribution of weights conencting \
                    input to hidden layer')
        plt.show()
    elif(ix ==1):
        plt.hist(par.cpu().detach().numpy().flatten())
        plt.title('Distribution of biases of hidden layer')
        plt.show()
    elif(ix==2):
        plt.hist(par.cpu().detach().numpy().flatten())
        plt.title('Distribution of weights conencting \
                    hidden to output layer')
        plt.show()
    elif(ix ==3):
        plt.hist(par.cpu().detach().numpy().flatten())
        plt.title('Distribution of biases of output layer')
        plt.show()

注意model.parameters将因我们绘制分布图的型号而异。上述代码在三种学习速率下的输出如下:

在这里,我们可以看到以下内容:

  • 当学习率高时,与中等和低学习率相比,参数具有大得多的分布。
  • 当参数分布较大时,会出现过度拟合。

到目前为止,我们已经研究了改变学习率对在规模数据集上训练的模型的影响。在下一节中,我们将了解改变学习率对在非缩放数据上训练的模型的影响。

请注意,尽管我们已经确定始终缩放输入值更好,但我们将继续确定在非缩放数据集上训练模型的影响。

改变学习率对未缩放数据集的影响

在这一节中,我们将通过在定义数据集的类中不执行除以 255 来恢复对数据集的操作。可以这样做:

The code for this section is available as Varying_learning_rate_on_non_scaled_data.ipynb in the Chapter03 folder of this book's GitHub repository - tinyurl.com/mcvp-packt . Note that we are not providing all the steps for brevity and that only the steps where there is a change from the code we went through in the Batch size of 32 section will be discussed in the following code. We encourage you to refer to the notebooks in this book's GitHub repository while executing the code.

class FMNISTDataset(Dataset):
    def __init__(self, x, y):
        x = x.float() # Note that the data is not scaled in this 
        # scenario
        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,这是我们在缩放数据集时执行的。

通过改变跨时期的准确度和损失值来改变学习率的结果如下:

正如我们所看到的,即使数据集是非缩放的,当学习率为 0.1 时,我们也无法训练出准确的模型。此外,当学习率为 0.001 时,精度不如前一部分中的精度高。

最后,当学习率非常小(0.00001)时,模型能够像在前面的部分中一样学习,但是这次过度拟合了训练数据。让我们通过研究跨层的参数分布来理解为什么会发生这种情况,如下所示:

这里,我们可以看到,当模型精度高时(当学习率为 0.00001 时),与学习率高时相比,权重具有小得多的范围(在这种情况下通常在-0.05 到 0.05 之间)。

因为学习率小,所以权重可以向小值调整。请注意,在非缩放数据集上学习率为 0.00001 的情况等同于在缩放数据集上学习率为 0.001 的情况。这是因为权重现在可以向非常小的值移动(因为梯度*学习率是非常小的值,假设学习率很小)。

既然我们已经确定了高学习率不可能在缩放数据集和非缩放数据集上产生最佳结果,那么在下一节中,我们将了解如何在模型开始过度拟合时自动降低学习率。

通常,0.001 的学习率是有效的。学习率非常低意味着需要很长时间来训练模型,而学习率很高会导致模型变得不稳定。

了解学习率退火的影响

到目前为止,我们已经初始化了一个学习率,在训练模型时,它在所有时期都保持不变。然而,最初,将权重快速更新到接近最优的情况是直观的。从那时起,它们应该非常缓慢地更新,因为最初减少的损失量很高,而在以后的时期减少的损失量很低。

这要求在开始时有一个高的学习率,然后随着模型达到接近最优的精度,逐渐降低学习率。这就需要我们了解什么时候必须降低学习率。

我们可以解决这个问题的一个潜在方法是通过持续监控验证损失,如果验证损失没有减少(比如说,在之前的 x 个时期),那么我们降低学习率。

PyTorch 为我们提供了一些工具,当验证损失在前一个“x”时期没有减少时,我们可以使用这些工具来降低学习率。在这里,我们可以使用lr_scheduler方法:

from torch import optim
scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer,
                                    factor=0.5,patience=0,
                                    threshold = 0.001, 
                                    verbose=True, 
                                    min_lr = 1e-5, 
                                    threshold_mode = 'abs')

在前面的代码中,我们指定,如果某个值在接下来的 n 个时期(这里 n 是 0)没有提高threshold(这里是 0.001),我们将把optimizer的学习率参数减少factor0.5。最后,我们指定学习率min_lr(假定它以 0.5 的因子减少)不能低于 1e-5,并且threshold_mode应该是绝对的,以确保越过最小阈值 0.001。

现在我们已经了解了调度程序,让我们在训练模型时应用它。

与前几节相似,所有代码与 32 节的批次大小相同,除了此处显示的粗体代码,该代码是为计算验证损失而添加的:

The code for this section is available as Learning_rate_annealing.ipynb in the Chapter03 folder of this book's GitHub repository - tinyurl.com/mcvp-packt . Note that we are not providing all the steps for brevity and that only the steps where there is a change from the code we went through in the Batch size of 32 section will be discussed in the following code. We encourage you to refer to the notebooks in this book's GitHub repository while executing the code.

from torch import optim
scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, 
 factor=0.5, patience=0, 
 threshold = 0.001, 
 verbose=True, 
 min_lr = 1e-5, 
 threshold_mode = 'abs')
train_losses, train_accuracies = [], []
val_losses, val_accuracies = [], []
for epoch in range(30):
    #print(epoch)
    train_epoch_losses, train_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)
        validation_loss = val_loss(x, y, model)
        scheduler.step(validation_loss)
    val_epoch_accuracy = np.mean(val_is_correct)

    train_losses.append(train_epoch_loss)
    train_accuracies.append(train_epoch_accuracy)
    val_losses.append(validation_loss)
    val_accuracies.append(val_epoch_accuracy)

在前面的代码中,我们指定只要验证损失在连续的时期内没有减少,就应该激活调度程序。在这些情况下,学习率降低到当前学习率的 0.5 倍。

在我们的模型上执行此操作的输出如下:

让我们了解随着时代的增加,训练和验证数据集的准确性和损失值的变化:

注意,每当验证损失在增加的时期内增加至少 0.001,学习率就减少一半。这发生在 5、8、11、12、13、15 和 16 世纪。

此外,我们没有任何巨大的过度拟合问题,即使我们训练了 100 个纪元的模型。这是因为学习率变得如此之小,以至于权重更新非常小,导致训练和验证精度之间的差距更小(与我们有 100 个时期而没有学习率退火的情况相比,训练精度接近 100%,而验证精度接近 89%)。

到目前为止,我们已经了解了各种超参数对模型准确性的影响。在下一节中,我们将了解神经网络的层数如何影响其准确性。

构建更深层次的神经网络

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

网络中有两层的模型可以按如下方式构建(注意,我们将第二个隐藏层中的单元数设置为 1000)。修改后的get_model函数(来自 32 部分的批量中的代码),有两个隐藏层,如下所示:

The following code is available as Impact_of_building_a_deeper_neural_network.ipynb in the Chapter03 folder of this book's GitHub repository - tinyurl.com/mcvp-packt . Note that we are not providing all the steps for brevity and that only the steps where there is a change from the code we went through in the Batch size of 32 section will be discussed in the following code. We encourage you to refer to the notebooks in this book's GitHub repository while executing the code.

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 部分中那样训练模型,训练和验证数据集的准确度和损失将如下:

在此,请注意以下几点:

  • 当没有隐藏层时,模型不能很好地学习。

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

到目前为止,在不同的部分,我们已经看到,当输入数据没有缩放(缩小到一个小范围)时,模型无法很好地训练。由于在获取隐藏层中的节点值时涉及矩阵乘法,非缩放数据(具有较高范围的数据)也可能出现在隐藏层中(特别是当我们有具有多个隐藏层的深度神经网络时)。在下一节中,我们将学习如何在中间层处理这种不可伸缩的数据。

了解批处理规范化的影响

之前,我们了解到当输入值较大时,当权重值显著变化时,Sigmoid 输出的变化不会产生太大影响。

现在,让我们考虑相反的场景,其中输入值非常小:

当输入值很小时,Sigmoid 输出会发生轻微变化,从而对权重值产生较大变化。

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

除了很小或很大的输入值之外,我们还可能会遇到这样的情况:隐藏层中某个节点的值可能会导致很小的数字或很大的数字,从而导致我们之前看到的连接隐藏层和下一层的权重的相同问题。

在这种情况下,批处理规范化可以解决问题,因为它可以对每个节点的值进行规范化,就像我们缩放输入值一样。

通常,一个批处理中的所有输入值按如下方式缩放:

通过从批次平均值中减去每个数据点,然后除以批次方差,我们已经将节点处批次的所有数据点标准化到固定范围。

虽然这被称为硬归一化,但通过引入γ和β参数,我们让网络识别最佳归一化参数。

为了理解批处理规范化过程的作用,我们来看一下训练和验证数据集的损失值和精度值,以及隐藏图层值在以下场景中的分布情况:

  • 非常小的输入值,没有批量标准化
  • 批量标准化的非常小的输入值

我们开始吧!

非常小的输入值,没有批量标准化

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

为了缩放输入数据集,使其具有非常低的值,我们将通过将输入值的范围从 0 减小到 0.0001 来更改通常在FMNISTDataset类中进行的缩放,如下所示:

The following code is available as Batch_normalization.ipynb in the Chapter03 folder of this book's GitHub repository - tinyurl.com/mcvp-packt . Note that we are not providing all the steps for brevity and that only the steps where there is a change from the code we went through in the Batch size of 32 section will be discussed in the following code. We encourage you to refer to the notebooks in this book's GitHub repository while executing the code.

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)),我们通过将输入像素值除以 10,000,缩小了输入像素值的范围。

接下来,我们必须重新定义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 ^个索引中,我们将修改函数,使其仅获取预测的第 0 ^个索引,如下所示:

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 ^个索引(因为第 0 ^个索引包含输出层的值)。

现在,当我们运行缩放 数据部分中提供的其余代码时,我们将看到训练和验证数据集中的准确度和损失值在增加的时期内的变化如下:

请注意,在前面的场景中,即使在 100 个时期之后,模型也没有训练好(在前面的部分中,模型在 10 个时期内在验证数据集上训练到大约 90%的准确度,而当前模型只有大约 85%的验证准确度)。

让我们通过研究隐藏值的分布以及参数分布,来了解当输入值的范围很小时,模型不能很好地训练的原因:

请注意,第一个分布表示隐藏层中的值的分布(我们可以看到这些值的范围非常小)。此外,假设输入层和隐藏层值的范围都很小,则权重必须变化很大(对于将输入连接到隐藏层的权重和将隐藏层连接到输出层的权重)。

既然我们已经了解了当输入值的范围很小时,网络的训练效果并不好,那么我们就来了解批处理规范化是如何帮助增加隐藏层中值的范围的。

批量标准化的非常小的输入值

在这一节中,我们将只对上一小节中的代码做一处修改;也就是说,我们将在定义模型架构的同时添加批处理规范化。

修改后的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

注意,在前面的代码中,我们声明了一个执行批处理规范化(nn.BatchNorm1d)的变量(batch_norm)。我们执行nn.BatchNorm1d(1000)的原因是因为每个图像的输出维度是 1000(即隐藏层的 1 维输出)。

此外,在forward方法中,我们在 ReLU 激活之前,通过批量标准化传递隐藏层值的输出。

训练和验证数据集的准确度和损失值在增加的时期内的变化如下:

在这里,我们可以看到模型的训练方式与输入值的范围不是很小时的训练方式非常相似。

让我们了解隐藏层值的分布和权重分布,如前一部分所示:

在这里,我们可以看到,当我们进行批量归一化时,隐藏层值具有较大的分布,而将隐藏层连接到输出层的权重具有较小的分布。模型学习的结果与前几节一样有效。

在训练深度神经网络时,批处理规范化非常有帮助。它帮助我们避免梯度变得如此之小,以至于权重几乎没有更新。

请注意,在前面的场景中,我们比根本没有批处理规范化时更快地获得了高验证准确性。这可能是标准化中间层的结果,从而减少了权重中发生饱和的机会。然而,过度拟合的问题仍有待解决。我们接下来会看这个。

过度拟合的概念

到目前为止,我们已经看到训练数据集的准确性通常超过 95%,而验证数据集的准确性大约为 89%。

本质上,这表明该模型不会对看不见的数据集进行太多的归纳,因为它可以从训练数据集中学习。这也表明模型正在学习训练数据集的所有可能的边缘情况;这些不能应用于验证数据集。

在训练数据集上具有高精度而在验证数据集上具有相当低的精度是指过拟合的情况。

用来减少过度拟合影响的一些典型策略如下:

  • 拒绝传统社会的人
  • 正规化

我们将在接下来的章节中探讨它们的影响。

增加辍学的影响

我们已经知道,每当计算loss.backward()时,都会发生权重更新。通常,我们在网络中有成千上万的参数和成千上万的数据点来训练我们的模型。这为我们提供了一种可能性,即虽然大多数参数有助于合理地训练模型,但某些参数可以针对训练图像进行微调,导致它们的值仅由训练数据集中的少数图像决定。这反过来导致训练数据具有高精度,但是验证数据集不能概括。

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

请注意,在预测过程中,不需要应用 dropout,因为这种机制只能应用于经过训练的模型。此外,权重将在预测(评估)期间自动缩减,以调整权重的大小(因为所有权重在预测时间期间都存在)。

通常,在训练和验证期间,各层会有不同的表现——就像你在辍学案例中看到的那样。因此,您必须使用两种方法之一提前指定模型的模式——model.train()让模型知道它处于训练模式,而model.eval()让它知道它处于评估模式。如果我们不这样做,我们可能会得到意想不到的结果。例如,在下图中,请注意模型(包含 dropout)如何在训练模式下对相同的输入给出不同的预测。但是,当同一个模型处于 eval 模式时,它将抑制 dropout 层并返回相同的输出:

定义架构时,Dropoutget_model函数中指定如下:

The following code is available as Impact_of_dropout.ipynb in the Chapter03 folder of this book's GitHub repository - tinyurl.com/mcvp-packt . Note that we are not providing all the steps for brevity and that only the steps where there is a change from the code we went through in the Batch size of 32 section will be discussed in the following code. We encourage you to refer to the notebooks in this book's GitHub repository while executing the code.

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是在线性激活之前指定的。这指定线性激活层中固定百分比的权重不会被更新。

一旦模型训练完成,如 32 个部分的批量,训练和验证数据集的损失和准确度值将如下:

请注意,在前面的方案中,定型数据集和验证数据集的精确度之间的差异没有我们在前面的方案中看到的那么大,因此导致了过度拟合较少的方案。

正规化的影响

除了训练精度远高于验证精度之外,过拟合的另一个特征是某些权重值将远高于其他权重值。高权重值可能是模型在训练数据上学习得非常好的症状(本质上,是对它所看到的内容的 rot 学习)。

虽然 dropout 是一种用于使权重值不会频繁更新的机制,但正则化是我们可以用于此目的的另一种机制。

正则化是一种技术,其中我们惩罚具有高权重值的模型。因此,这是一个双重目标函数——最小化训练数据和权重值的损失。在本节中,我们将了解两种类型的正则化:

  • L1 正则化
  • L2 正则化

The following code is available as Impact_of_regularization.ipynb in the Chapter03 folder of this book's GitHub repository - tinyurl.com/mcvp-packt . Note that we are not providing all the steps for brevity and that only the steps where there is a change from the code we went through in the Batch size of 32 section will be discussed in the following code. We encourage you to refer to the notebooks in this book's GitHub repository while executing the code.

我们开始吧!

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 个的批量部分中,训练和验证数据集在增加的时期内的损失和准确度值将如下:

在这里,我们可以看到,训练和验证数据集的准确性之间的差异没有 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 个的批量部分中,训练和验证数据集在增加的时期内的损失和准确度值将如下:

在这里,我们可以看到 L2 正则化也导致了验证和训练数据集的准确性和损失值彼此接近。

最后,让我们比较没有正则化和有 L1/ L2 正则化的权重值,以便我们可以验证我们的理解,即在记忆边缘情况的值时,某些权重变化很大。我们将通过遍历各层的权重分布来实现这一点,如下图所示:

这里,我们可以看到,与不执行正则化相比,当我们执行 L1/ L2 正则化时,参数的分布非常小。这潜在地减少了针对边缘情况更新权重的机会。

摘要

本章一开始,我们学习了图像是如何表现的。接下来,我们了解了扩展、学习率的值、我们选择的优化器以及批量大小如何帮助提高训练的准确性和速度。然后,我们了解了批处理规范化如何帮助提高训练速度,并解决隐藏层中非常小或非常大的值的问题。接下来,我们学习了如何安排学习速率来进一步提高准确性。然后,我们开始理解过度拟合的概念,并了解辍学和 L1 和 L2 正则化如何帮助我们避免过度拟合。

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

问题

  1. 如果输入数据集中的输入值没有缩放,会发生什么情况?
  2. 在训练神经网络时,如果背景是白色像素颜色,而内容是黑色像素颜色,会发生什么?
  3. 批量大小对模型的训练时间有什么影响,以及它在给定数量的时期内的准确性?
  4. 输入值范围对训练结束时的权重分布有什么影响?
  5. 批处理规范化如何帮助提高准确性?
  6. 我们如何知道一个模型是否过度拟合了训练数据?
  7. 正则化如何帮助避免过度拟合?
  8. L1 和 L2 正规化有何不同?
  9. 辍学如何有助于减少过度拟合?

第二部分:对象分类和检测

有了对神经网络 ( NN )基础知识的理解,在本节中,我们将发现构建在这些基础知识之上的更复杂的神经网络模块,以解决更复杂的视觉相关问题,包括目标检测、图像分类以及其他许多问题。

本节包括以下章节:

  • 第四章,介绍卷积神经网络
  • 第五章,用于图像分类的迁移学习
  • 第六章,图像分类实用方面
  • 第七章、目标检测基础知识
  • 第八章、高级物体探测
  • 第九章、图像分割
  • 第十章, 目标检测与分割的应用

四、卷积神经网络简介

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

本章将涵盖以下主题:

  • 传统深度神经网络的问题是
  • CNN 的构建模块
  • 实现 CNN
  • 使用深度细胞神经网络分类图像
  • 实现数据扩充
  • 可视化特征学习的结果
  • 构建用于分类真实世界图像的 CNN

我们开始吧!

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

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

让我们重新考虑一下我们在第三章、用 PyTorch 建立深度神经网络中在时尚-MNIST 数据集上建立的模型。我们将获取一个随机图像,并预测对应于该图像的类,如下所示:

The code for this section is available as Issues_with_image_translation.ipynb in the Chapter04 folder of this book's GitHub repository - tinyurl.com/mcvp-packt . Note that the entire code is available in GitHub and that only the additional code corresponding to the issue of image translation will be discussed here for brevity. We strongly encourage you to refer to the notebooks in this book's GitHub repository while executing the code.

  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]])

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

  1. 将图像通过训练过的模型(继续使用我们在第三章、批量 32* 部分训练过的模型,用 PyTorch* 构建深度神经网络)。
  • 预处理图像,使其经过我们在构建模型时执行的相同预处理步骤:
img = tr_images[ix]/255.
img = img.view(28*28)
img = img.to(device)
  • 提取与各种类别相关的概率:
np_output = model(img).cpu().detach().numpy()
np.exp(np_output)/np.sum(np.exp(np_output))

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

从前面的输出中,我们可以看到第 1 个 ^(st) 索引的概率最高,它属于Trouser类。

  1. 将图像平移(滚动/滑动)多次(一次一个像素),从向左平移 5 个像素到向右平移 5 个像素,并将预测存储在列表中。
  • 创建存储预测的列表:
preds = []
  • 创建一个循环,将图像从原始位置(位于图像中心)的-5 像素(向左 5 像素)平移(滚动)到+5 像素(向右 5 像素):
for px in range(-5,6):

在前面的代码中,我们将 6 指定为上限,尽管我们感兴趣的是平移到+5 像素,因为当(-5,6)是指定的范围时,范围的输出将是从-5 到+5。

  • 预处理图像,正如我们在步骤 2 中所做的:
    img = tr_images[ix]/255.
    img = img.view(28, 28)
  • for循环中将图像滚动一个等于px的值:
    img2 = np.roll(img, px, axis=1)

在前面的代码中,我们指定了axis=1,因为我们希望图像像素水平移动,而不是垂直移动。

  • 将滚动图像存储为张量对象,并注册到device:
    img3 = torch.Tensor(img2).view(28*28).to(device)
  • img3传递给训练好的模型,以预测翻译(滚动)图像的类别,并将其添加到存储各种翻译预测的列表中:
    np_output = model(img3).cpu().detach().numpy()
    preds.append(np.exp(np_output)/np.sum(np.exp(np_output)))
  1. 可视化所有平移的模型预测(-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')

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

图像的内容没有变化,因为我们只将图像向左平移了 5 个像素,向右平移了 5 个像素。然而,当平移超过 2 个像素时,图像的预测类别发生变化。这是因为当模型被训练时,所有训练和测试图像中的内容都在中心。这与前面的场景不同,在前面的场景中,我们使用偏离中心的翻译图像进行测试,从而导致错误预测的类。

既然我们已经了解了传统神经网络失败的场景,我们将了解CNN如何帮助解决这个问题。但在此之前,我们将了解 CNN 的组成部分。

CNN 的构建模块

CNN 是处理图像时最常用的架构。CNN 解决了我们在上一节中看到的深度神经网络的主要限制。除了图像分类之外,它们还有助于目标检测、图像分割、GANs 等等——基本上,在我们使用图像的任何地方。此外,有不同的方法来构建卷积神经网络,并且有多个预训练模型来利用 CNN 执行各种任务。从本章开始,我们将广泛使用 CNN。

在接下来的小节中,我们将了解 CNN 的基本构建模块,如下所示:

  • 回旋
  • 过滤
  • 步幅和衬垫
  • 联营

我们开始吧!

盘旋

卷积基本上是两个矩阵之间的乘法。正如你在前一章看到的,矩阵乘法是训练神经网络的一个关键因素。(我们在计算隐藏层值时执行矩阵乘法,这是输入值和将输入连接到隐藏层的权重值的矩阵乘法。同样,我们执行矩阵乘法来计算输出图层值。)

为了确保我们对卷积过程有一个坚实的理解,让我们来看看下面的例子。

让我们假设我们有两个矩阵可以用来执行卷积。

这是矩阵 A:

这是矩阵 B:

在执行卷积运算时,您在矩阵 A(较大的矩阵)上滑动矩阵 B(较小的矩阵)。此外,我们正在矩阵 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

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

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

过滤器

过滤器是在开始时随机初始化的权重矩阵。该模型在增加的时期内学习过滤器的最佳权重值。

过滤器的概念带给我们两个不同的方面:

  • 过滤器了解什么
  • 如何表示过滤器

一般来说,CNN 中的过滤器越多,模型可以了解的图像特征就越多。我们将在本章的可视化过滤器学习部分了解各种过滤器学习的内容。现在,我们将确保我们有一个中间的理解,即过滤器了解图像中存在的不同特征。例如,某个过滤器可能会学习猫的耳朵,并在与其卷积的图像部分包含猫的耳朵时提供高激活度(矩阵乘法值)。

在上一节中,我们了解到,当我们将一个大小为 2 x 2 的滤波器与一个大小为 4 x 4 的矩阵进行卷积时,我们得到的输出维度为 3 x 3。

然而,如果 10 个不同的滤波器乘以较大的矩阵(原始图像),结果是 10 组 3×3 输出矩阵。

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

此外,在我们处理有三个通道的彩色图像的情况下,与原始图像卷积的滤波器也将有三个通道,导致每个卷积只有一个标量输出。此外,如果滤波器与中间输出进行卷积,比如形状为 64 x 112 x 112,滤波器将有 64 个通道来获取标量输出。此外,如果有 512 个滤波器与在中间层获得的输出进行卷积,那么与 512 个滤波器进行卷积后的输出在形状上将是 512×111×111。

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

在上图中,我们可以看到输入图像与深度与输入图像相同的滤波器相乘(滤波器与输入图像进行卷积),卷积输出中的通道数量与滤波器数量相同。

步幅和衬垫

在前面的部分中,每个过滤器都在图像中大步前进——一次一列和一行(在图像结束时用尽所有可能的列之后)。这也导致输出尺寸比输入图像尺寸小 1 个像素——在高度和宽度方面都是如此。这会导致部分信息丢失,并可能影响我们将卷积运算的输出添加到原始图像的可能性(这称为残差加法,将在下一章详细讨论)。

在本节中,我们将了解步长和填充如何影响卷积的输出形状。

大步走

让我们利用在过滤器部分看到的同一个例子来理解 stride 的影响。此外,我们将在矩阵 a 上以步长 2 跨越矩阵 B。步长为 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

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

请注意,与跨距为 1 的场景(输出形状为 3 x 3)相比,前面的输出具有更低的维度,因为我们现在的跨距为 2。

填充

在前面的例子中,我们不能将过滤器最左边的元素乘以图像最右边的元素。如果我们要执行这样的矩阵乘法,我们将用零填充图像。这将确保我们可以使用过滤器对图像中的所有元素执行元素到元素的乘法。

让我们通过使用在卷积部分中使用的相同示例来理解填充。

一旦我们在矩阵 A 的顶部添加了填充,矩阵 A 的修订版将如下所示:

从前面的矩阵中,我们可以看到,我们已经用零填充了矩阵 A,并且与矩阵 B 的卷积不会导致输出维度小于输入维度。当我们在残差网络上工作时,这个方面很方便,我们必须将卷积的输出添加到原始图像中。

一旦我们完成了这些,我们就可以在卷积运算的输出之上执行激活。为此,我们可以使用在第三章、用 PyTorch 构建深度神经网络中看到的任何激活函数。

联营

池化将信息聚集在一个小块中。想象一个场景,其中卷积激活的输出如下:

此修补程序的最大池是 4。这里,我们已经考虑了这个元素池中的元素,并在所有存在的元素中取最大值。

同样,让我们了解一下更大矩阵的最大池:

在前面的情况下,如果池化跨度的长度为 2,则最大池化操作的计算如下,其中我们将输入图像除以跨度 2(即,我们将图像分成 2×2 个部分):

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

在实践中,不需要总是具有 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}

通过这样做,我们将看到展平层可以被视为等同于输入层(我们在第三章的中使用 PyTorch 将输入图像展平为 784 维输入)。一旦获得展平层(完全连接层)的值,我们可以通过隐藏层传递它,然后获得预测图像类别的输出。

CNN 的总体流程如下:

在前面的图像中,我们可以看到 CNN 模型的整体流程,其中我们通过多个过滤器将图像通过卷积,然后合并(在前面的情况中,重复卷积和合并过程两次),然后平坦化最终合并层的输出。这形成了前面图像的特征学习部分。

卷积和汇集的操作构成了特征学习部分,因为过滤器有助于从图像中提取相关特征,而汇集有助于聚合信息,从而减少展平层的节点数量。(如果我们直接展平输入图像(例如,大小为 300 x 300 像素),我们处理的是 90K 输入值。如果我们在一个隐藏层中有 90K 个输入像素值和 100K 个节点,我们会看到大约 90 亿个参数,这在计算方面是巨大的。)

卷积和池化有助于获取比原始图像更小的展平图层。

最后,分类的最后一部分类似于我们在第三章、在 PyTorch 中构建深度神经网络中分类图像的方式,在这里我们有一个隐藏层,然后获得输出层。

卷积和汇集如何帮助图像翻译

当我们执行池化时,我们可以将操作的输出视为一个区域的抽象(一小块)。这种现象会派上用场,尤其是在翻译图像的时候。

想象一个图像向左平移 1 个像素的场景。一旦我们在其上执行卷积、激活和合并,我们将减少图像的维度(由于合并),这意味着更少数量的像素存储了原始图像的大部分信息。此外,假定汇集存储区域(斑块)的信息,则汇集图像的像素内的信息不会变化,即使原始图像被平移 1 个单位。这是因为该区域的最大值可能会在合并的图像中被捕获。

卷积和汇集 cam 也帮助我们处理感受野。为了理解感受野,让我们想象一个场景,其中我们在形状为 100 x 100 的图像上执行两次卷积池操作。两个卷积池操作结束时的输出是 25 x 25 的形状(如果卷积操作是用填充完成的)。25 x 25 输出中的每个单元现在对应于原始图像的一个更大的 4 x 4 部分。因此,由于卷积和池化操作,结果图像中的每个单元对应于原始图像的一个小块。

现在我们已经了解了 CNN 的核心组件,让我们将它们全部应用到一个玩具示例中,以了解它们是如何协同工作的。

实现 CNN

CNN 是计算机视觉技术的基础之一,对你来说,深入了解它们是如何工作的非常重要。虽然我们已经知道 CNN 由卷积、汇集、展平以及最终的分类层组成,但在本节中,我们将了解在通过代码向前传递 CNN 的过程中发生的各种操作。

为了更好地理解这一点,首先,我们将使用 PyTorch 在一个玩具示例上构建一个 CNN 架构,然后通过用 Python 从头构建前馈传播来匹配输出。

使用 PyTorch 构建基于 CNN 的架构

CNN 架构将不同于我们在上一章中构建的神经网络架构,因为除了典型的普通深度神经网络之外,CNN 还包含以下内容:

  • 卷积运算
  • 联营业务
  • 展平层

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

The code for this section is available as CNN_working_details.ipynb in the Chapter04 folder of this book's GitHub repository - tinyurl.com/mcvp-packt

  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
  1. 然后,我们需要使用以下步骤创建数据集:
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. 定义模型架构:
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()),因为输出来自二进制类。我们还指定优化将使用 Adam 优化器完成,学习率为 0.001。

  1. 使用torch_summary包中可用的summary方法总结模型的架构,通过调用get_model函数获取我们的model、损失函数(loss_fn)和optimizer:
!pip install torch_summary
from torchsummary import summary
model, loss_fn, optimizer = get_model()
summary(model, X_train);

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

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

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

  • 线性图层有两个参数-一个权重和一个偏差-这意味着总共有 12 个参数(10 个来自卷积运算,两个来自线性图层)。
  1. 使用我们在第三章、使用 PyTorch 构建深度神经网络中使用的相同模型训练代码来训练模型,其中我们定义了将对批量数据进行训练的函数(train_batch)。然后,获取数据加载器,并对超过 2,000 个历元的批数据进行训练(我们只使用 2,000 个,因为这是一个小的玩具数据集),如下所示:
  • 定义将对批量数据进行训练的函数(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()
  • 通过使用TensorDataset方法指定数据集,然后使用DataLoader加载数据集,定义训练数据加载器:
trn_dl = DataLoader(TensorDataset(X_train, y_train))

注意,假设我们没有大量修改输入数据,我们将不会单独构建一个类,而是直接利用TensorDataset方法,它提供了一个对应于输入数据的对象。

  • 训练模型超过 2000 个时期:
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)

使用前面的代码,我们在玩具数据集上训练了 CNN 模型。

  1. 在第一个数据点的顶部执行向前传递:
model(X_train[:1])

前面代码的输出是0.1625

请注意,在执行前面的代码时,由于不同的随机权重初始化,您可能会有不同的输出值。但是,您应该能够将输出与下一节中得到的内容进行匹配。

在下一节中,我们将了解 CNN 中的前向传播是如何工作的,以便我们可以在第一个数据点上获得 0.1625 的值。

用 Python 向前传播输出

在我们继续之前,请注意本节只是为了帮助您清楚地了解 CNN 是如何工作的。在真实场景中,我们不需要执行以下步骤:

  1. 提取已定义架构的卷积和线性层的权重和偏差,如下所示:
  • 提取模型的各个层:
list(model.children())

这会产生以下输出:

  • 从模型的所有层中提取与weight属性相关联的层:
(cnn_w, cnn_b), (lin_w, lin_b) = [(layer.weight.data, \
                            layer.bias.data) for layer in \
                            list(model.children()) \
                                  if hasattr(layer,'weight')]

在前面的代码中,hasattr(layer,'weight')返回一个布尔值,而不管图层是否包含weight属性。

请注意,卷积(Conv2d)层和最后的Linear层是唯一包含参数的层,这就是为什么我们将它们分别保存为Conv2d层的cnn_wcnn_b以及Linear层的lin_wlin_b

cnn_w的形状是 1×1×3×3,因为我们已经初始化了一个滤波器,它有一个通道,尺寸为 3×3。cnn_b具有 1 的形状,因为它对应于一个过滤器。

  1. 为了对输入值执行cnn_w卷积运算,我们必须为 sumproduct ( sumprod)初始化一个零矩阵,其中高度为输入高度-滤波器高度+ 1 ,宽度为宽度-滤波器宽度+ 1 :
h_im, w_im = X_train.shape[2:]
h_conv, w_conv = cnn_w.shape[2:]
sumprod = torch.zeros((h_im - h_conv + 1, w_im - w_conv + 1))
  1. 现在,让我们通过在第一个输入上卷积滤波器(cnn_w)并在将滤波器形状从 1 x 1 x 3 x 3 形状整形为 3 x 3 形状后对滤波器偏置项(cnn_b)求和来填充sumprod:
for i in range(h_im - h_conv + 1):
    for j in range(w_im - w_conv + 1):
        img_subset = X_train[0, 0, i:(i+3), j:(j+3)]
        model_filter = cnn_w.reshape(3,3)
        val = torch.sum(img_subset*model_filter) + cnn_b
        sumprod[i,j] = val

在前面的代码中,img_subset存储了我们将与过滤器进行卷积的输入部分,因此我们将遍历可能的列,然后是行。

此外,假设输入的形状为 4 x 4,滤波器的形状为 3 x 3,则输出的形状为 2 x 2。

在这个阶段,sumprod的输出如下:

  1. 对输出执行 ReLU 操作,然后获取池的最大值(MaxPooling),如下所示:
  • ReLU 是在 Python 中的sumprod之上执行的,如下所示:
sumprod.clamp_min_(0)

请注意,在前面的代码中,我们将输出箝位到最小值 0(这就是 ReLU 激活的作用):

  • 池层的输出可以这样计算:
pooling_layer_output = torch.max(sumprod)

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

  1. 通过线性激活传递前面的输出:
intermediate_output_value = pooling_layer_output*lin_w+lin_b

该操作的输出如下:

  1. 通过sigmoid操作传递输出:
from torch.nn import functional as F # torch library 
# for numpy like functions
print(F.sigmoid(intermediate_output_value))

前面的代码给出了以下输出:

注意,我们执行sigmoid而不是softmax,因为损失函数是二元交叉熵,而不是像时尚-MNIST 数据集中那样的分类交叉熵。

前面的代码给出了我们使用 PyTorch 前馈方法获得的相同输出,从而加强了我们对 CNN 如何工作的理解。

现在我们已经了解了 CNN 是如何工作的,在下一节中,我们将把它应用到时尚 MNIST 数据集,并看看它在翻译图像上的表现。

使用深度细胞神经网络分类图像

到目前为止,我们已经看到传统的神经网络对翻译图像的预测是不正确的。这需要解决,因为在真实世界的场景中,需要应用各种增强,例如平移和旋转,这在训练阶段是看不到的。在本节中,我们将了解当图像转换发生在时尚 MNIST 数据集中的图像上时,CNN 如何解决不正确预测的问题。

时尚-MNIST 数据集的预处理部分与前一章相同,只是当我们对(.view)输入数据进行整形时,我们不是将输入数据展平为 28 x 28 = 784 维,而是将每个图像的输入数据整形为(1,28,28)的形状(记住,首先要指定通道,然后是它们的高度和宽度,单位为 PyTorch):

The code for this section is available as CNN_on_FashionMNIST.ipynb in the Chapter04 folder of this book's GitHub repository - tinyurl.com/mcvp-packt . Note that the entirety of the code is available in GitHub and that only the additional code corresponding to defining the model architecture is provided here for brevity. We strongly encourage you to refer to the notebooks in this book's GitHub repository while executing the code.

  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
  1. 时尚-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)

前面用粗体显示的代码行是我们对每个输入图像进行整形的地方(与我们在上一章中所做的不同),因为我们向 CNN 提供数据,该 CNN 期望每个输入具有批量大小 x 通道 x 高度 x 宽度的形状。

  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
  • 可以使用以下代码创建模型的摘要:
!pip install torch_summary
from torchsummary import summary
model, loss_fn, optimizer = get_model()
summary(model, torch.zeros(1,1,28,28));

这会产生以下输出:

为了巩固我们对 CNN 的理解,让我们来理解为什么在前面的输出中参数的数量是这样设置的:

  • 第 1 层:假设有 64 个内核大小为 3 的过滤器,我们有 64×3×3 的权重和 64×1 的偏差,总共有 640 个参数。
  • 第 4 层:假设有 128 个内核大小为 3 的过滤器,我们有 128 x 64 x3 x 3 的权重和 128 x 1 的偏差,总共有 73856 个参数。
  • 第 8 层:假设一个有 3200 个节点的层连接到另一个有 256 个节点的层,我们总共有 3,200 x 256 个权重+ 256 个偏差,总共有 819456 个参数。
  • 第 10 层:假设一个有 256 个节点的层连接到一个有 10 个节点的层,我们总共有 256 x 10 个权重和 10 个偏差,总共有 2570 个参数。

现在,我们训练模型,就像我们在前一章中训练它一样。完整的代码可以在本书的 GitHub 资源库-tinyurl.com/mcvp-packt中找到

训练完模型后,您会注意到训练和测试数据集的精度变化和损失如下:

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

现在,让我们翻译图像并预测翻译图像的类别:

  1. 将图像在-5 像素到+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)进行了整形,使其形状为(-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 个像素时,预测也是正确的,而在我们不使用 CNN 的情况下,当图像平移 4 个像素时,预测是不正确的。此外,当图像平移 5 个像素时,“裤子”的概率大大下降。

正如我们所看到的,虽然 CNN 有助于解决图像翻译的挑战,但它们并没有完全解决手头的问题。在下一节中,我们将学习如何通过利用数据增强和 CNN 来解决这种情况。

实现数据扩充

在前面的场景中,我们了解了 CNN 如何帮助预测图像在翻译时的类别。虽然这对于高达 5 个像素的转换很有效,但是超过 5 个像素的转换对于正确的类来说概率很低。在这一节中,我们将学习如何确保我们预测正确的类,即使图像被平移了相当大的量。

为了应对这一挑战,我们将通过将输入图像随机平移 10 个像素(向左和向右)并将它们传递给网络来训练神经网络。这样,相同的图像将在不同的通道中作为不同的图像来处理,因为它在每个通道中具有不同的平移量。

在我们利用增强来提高图像转换时模型的准确性之前,让我们了解一下可以在图像上进行的各种增强。

图像增强

到目前为止,我们已经了解了图像转换对模型预测准确性的影响。但是,在现实世界中,我们可能会遇到各种情况,例如:

  • 图像会轻微旋转
  • 图像被放大/缩小(缩放)
  • 图像中存在一定量的噪声
  • 图像亮度低
  • 图像已经翻转
  • 图像已被剪切(图像的一侧更加扭曲)

不考虑上述情况的神经网络不会提供准确的结果,就像在前面的部分中,我们有一个神经网络,它没有对经过大量翻译的图像进行显式训练。

在我们从给定图像创建更多图像的场景中,图像增强非常有用。每个创建的图像可以在旋转、平移、缩放、噪声和亮度方面有所不同。此外,这些参数中的每一个的变化程度也可以变化(例如,在给定迭代中特定图像的平移可以是+10 像素,而在不同的迭代中,它可以是-5 像素)。

imgaug包中的augmenters类具有执行这些扩充的有用工具。让我们来看看augmenters类中的各种工具,用于从给定图像生成增强图像。一些最著名的增强技术如下:

  • 仿射变换
  • 改变亮度
  • 添加噪声

注意 PyTorch 有一个方便的图像增强管道,形式为torchvision.transforms。然而,我们仍然选择引入一个不同的库,主要是因为imgaug包含了更多种类的选项,同时也是因为向新用户解释增强功能很容易。我们鼓励你将火炬视觉转换作为一个练习来研究,并重新创建所有的功能来加强你的理解。

仿射变换

仿射变换涉及图像的平移、旋转、缩放和剪切。它们可以使用augmenters类中的Affine方法在代码中执行。让我们通过下面的截图来看看Affine方法中的参数。这里,我们已经定义了Affine方法的所有参数:

Affine方法中的一些重要参数如下:

  • scale指定图像的缩放量
  • translate_percent以图像高度和宽度的百分比指定平移量
  • translate_px将平移量指定为绝对像素数
  • rotate指定要在图像上完成的旋转量
  • shear指定要在部分图像上完成的旋转量

在我们考虑其他参数之前,让我们先了解一下缩放、平移和旋转在哪里会派上用场。

The code for this section is available as Image_augmentation.ipynb in the Chapter04 folder of this book's GitHub repository - tinyurl.com/mcvp-packt

fashionMNIST的训练数据集中提取随机图像:

  1. 从时尚 MNIST 数据集下载图片:
from torchvision import datasets
import torch
data_folder = '/content/' # This can be any directory 
# you download FMNIST to
fmnist = datasets.FashionMNIST(data_folder, download=True, \
                               train=True)
  1. 从下载的数据集中获取图像:
tr_images = fmnist.data
tr_targets = fmnist.targets
  1. 让我们绘制第一幅图像:
import matplotlib.pyplot as plt
%matplotlib inline
plt.imshow(tr_images[0])

上述代码的输出如下:

在图像顶部执行缩放:

  1. 定义执行缩放的对象:
from imgaug import augmenters as iaa
aug = iaa.Affine(scale=2)
  1. 指定我们想要使用augment_image方法来放大图像,该方法在aug对象中可用,并绘制它:
plt.imshow(aug.augment_image(tr_images[0]))
plt.title('Scaled image')

上述代码的输出如下:

在前面的输出中,图像被放大了很多。由于图像的输出形状没有改变,这导致一些像素从原始图像中被剪切。

现在,让我们来看一个使用translate_px参数将图像平移了一定数量像素的场景:

aug = iaa.Affine(translate_px=10)
plt.imshow(aug.augment_image(tr_images[0]))
plt.title('Translated image by 10 pixels')

上述代码的输出如下:

在前面的输出中,x 轴和 y 轴都发生了 10 个像素的平移。

如果我们希望在一个轴上执行更多的平移,而在另一个轴上执行更少的平移,我们必须指定我们希望在每个轴上的平移量:

aug = iaa.Affine(translate_px={'x':10,'y':2})
plt.imshow(aug.augment_image(tr_images[0]))
plt.title('Translation of 10 pixels \nacross columns \
and 2 pixels over rows')

这里,我们提供了一个字典,它在translate_px参数中说明了 x 轴和 y 轴的平移量。

上述代码的输出如下:

前面的输出显示,与行相比,更多的转换发生在列之间。这也导致图像的某一部分被裁剪。

现在,让我们考虑旋转和剪切对图像增强的影响:

在前面的大多数输出中,我们可以看到某些像素在转换后的图像中被裁剪掉了。现在,让我们看看Affine方法中的其余参数如何帮助我们不因裁剪后增强而丢失信息。

fit_output是一个参数,可以帮助前面的场景。默认设置为False。然而,让我们看看当我们缩放、平移、旋转和剪切图像时,当我们将fit_output指定为True时,前面的输出是如何变化的:

plt.figure(figsize=(20,20))
plt.subplot(161)
plt.imshow(tr_images[0])
plt.title('Original image')
plt.subplot(162)
aug = iaa.Affine(scale=2, fit_output=True)
plt.imshow(aug.augment_image(tr_images[0]))
plt.title('Scaled image')
plt.subplot(163)
aug = iaa.Affine(translate_px={'x':10,'y':2}, fit_output=True)
plt.imshow(aug.augment_image(tr_images[0]))
plt.title('Translation of 10 pixels across \ncolumns and \
2 pixels over rows')
plt.subplot(164)
aug = iaa.Affine(rotate=30, fit_output=True)
plt.imshow(aug.augment_image(tr_images[0]))
plt.title('Rotation of image \nby 30 degrees')
plt.subplot(165)
aug = iaa.Affine(shear=30, fit_output=True)
plt.imshow(aug.augment_image(tr_images[0]))
plt.title('Shear of image \nby 30 degrees')

上述代码的输出如下:

在这里,我们可以看到原始图像没有被裁剪,并且增强图像的大小增加了,以说明增强图像没有被裁剪(在缩放图像的输出中或当图像旋转 30 度时)。此外,我们还可以看到,fit_output参数的激活否定了我们在 10 像素图像的翻译中所期望的翻译(这是一个已知的行为,如文档中所解释的)。

请注意,当增强图像的大小增加时(例如,当图像旋转时),我们需要弄清楚不属于原始图像的新像素应该如何填充。

cval参数解决了这个问题。它指定了当fit_outputTrue时创建的新像素的像素值。在前面的代码中,cval用默认值 0 填充,这导致黑色像素。让我们来了解一下当图像旋转时,将cval参数值更改为 255 会如何影响输出:

aug = iaa.Affine(rotate=30, fit_output=True, cval=255)
plt.imshow(aug.augment_image(tr_images[0]))
plt.title('Rotation of image by 30 degrees')

上述代码的输出如下:

在前面的图像中,新像素的像素值为 255,对应于白色。

此外,我们可以使用不同的模式来填充新创建的像素值。这些值用于mode参数,如下所示:

  • constant:具有恒定值的焊盘。
  • edge:填充数组的边缘值。
  • symmetric:沿阵列边缘镜像的矢量反射的焊盘。
  • reflect:沿每个轴的向量的第一个和最后一个值上镜像的向量的映射。
  • wrap:沿轴向量环绕的焊盘。

初始值用于填充结尾,而结束值用于填充开头。

cval设置为 0 并且我们改变mode参数时,我们收到的输出如下:

在这里,我们可以看到,对于我们当前基于时尚-MNIST 数据集的场景,使用constant模式进行数据扩充更可取。

到目前为止,我们已经指定了平移需要一定数量的像素。类似地,我们已经指定旋转角度应该是特定的度数。然而,在实践中,很难指定图像需要旋转的确切角度。因此,在下面的代码中,我们提供了图像旋转的范围。可以这样做:

plt.figure(figsize=(20,20))
plt.subplot(151)
aug = iaa.Affine(rotate=(-45,45), fit_output=True, cval=0, \
                 mode='constant')
plt.imshow(aug.augment_image(tr_images[0]), cmap='gray')
plt.subplot(152)
aug = iaa.Affine(rotate=(-45,45), fit_output=True, cval=0, \
                 mode='constant')
plt.imshow(aug.augment_image(tr_images[0]), cmap='gray')
plt.subplot(153)
aug = iaa.Affine(rotate=(-45,45), fit_output=True, cval=0, \
                 mode='constant')
plt.imshow(aug.augment_image(tr_images[0]), cmap='gray')
plt.subplot(154)
aug = iaa.Affine(rotate=(-45,45), fit_output=True, cval=0, \
                 mode='constant')
plt.imshow(aug.augment_image(tr_images[0]), cmap='gray')

上述代码的输出如下:

在前面的输出中,相同的图像在不同的迭代中以不同的方式旋转,因为我们根据旋转的上限和下限指定了可能的旋转角度范围。类似地,当我们翻译或分享图像时,我们可以随机化增强。

到目前为止,我们已经用不同的方式看了不同的图像。但是,图像的强度/亮度保持不变。接下来,我们将学习如何增加图像的亮度。

改变亮度

想象一个场景,背景和前景之间的差异不像我们到目前为止看到的那样明显。这意味着背景没有像素值 0,前景没有像素值 255。当图像中的照明条件不同时,通常会发生这种情况。

如果在模型定型时背景的像素值始终为 0,前景的像素值始终为 255,但我们预测的图像的背景像素值为 20,前景像素值为 220,则预测很可能不正确。

MultiplyLinearcontrast是两种不同的增强技术,可以用来解决这种情况。

Multiply方法将每个像素值乘以我们指定的值。到目前为止,我们考虑的图像的每个像素值乘以 0.5 的输出如下:

aug = iaa.Multiply(0.5)
plt.imshow(aug.augment_image(tr_images[0]), cmap='gray', \
           vmin = 0, vmax = 255)
plt.title('Pixels multiplied by 0.5')

上述代码的输出如下:

Linearcontrast根据以下公式调整每个像素值:

在上式中,当α等于 1 时,像素值保持不变。但是,当α小于 1 时,高像素值减少,低像素值增加。

让我们看看Linearcontrast对该图像输出的影响:

aug = iaa.LinearContrast(0.5)
plt.imshow(aug.augment_image(tr_images[0]), cmap='gray', \
           vmin = 0, vmax = 255)
plt.title('Pixel contrast by 0.5')

上述代码的输出如下:

在这里,我们可以看到背景变得更加明亮,而前景像素的强度降低。

接下来,我们将使用GaussianBlur方法模糊图像以模拟真实场景(图像可能会因运动而模糊):

aug = iaa.GaussianBlur(sigma=1)
plt.imshow(aug.augment_image(tr_images[0]), cmap='gray', \
           vmin = 0, vmax = 255)
plt.title('Gaussian blurring of image')

上述代码的输出如下:

在前面的图像中,我们可以看到图像相当模糊,随着sigma值的增加(默认值为 0 表示无模糊),图像变得更加模糊。

添加噪声

在现实世界中,由于糟糕的摄影条件,我们可能会遇到颗粒状图像。DropoutSaltAndPepper是两种有助于模拟粒状图像条件的突出方法。让我们来看看用这两种方法放大图像的输出:

plt.figure(figsize=(10,10))
plt.subplot(121)
aug = iaa.Dropout(p=0.2)
plt.imshow(aug.augment_image(tr_images[0]), cmap='gray', \
           vmin = 0, vmax = 255)
plt.title('Random 20% pixel dropout')
plt.subplot(122)
aug = iaa.SaltAndPepper(0.2)
plt.imshow(aug.augment_image(tr_images[0]), cmap='gray', \
           vmin = 0, vmax = 255)
plt.title('Random 20% salt and pepper noise')

上述代码的输出如下:

在这里,我们可以看到,Dropout方法随机丢弃了一定数量的像素(也就是说,它将它们转换为像素值为 0),而SaltAndPepper方法向我们的图像中随机添加了一些白色和黑色的像素。

执行一系列增强操作

到目前为止,我们已经看了各种增强,也进行了表演。然而,在现实世界的场景中,我们必须考虑尽可能多的扩充。在本节中,我们将了解执行扩充的顺序方式。

使用Sequential方法,我们可以使用所有必须执行的相关增强来构建增强方法。对于我们的例子,我们将只考虑rotateDropout来增强我们的形象。Sequential对象看起来如下:

seq = iaa.Sequential([
      iaa.Dropout(p=0.2),
      iaa.Affine(rotate=(-30,30))], random_order= True)

在前面的代码中,我们指定我们对两个增强感兴趣,并且还指定我们将使用random_order参数。扩增过程将在两者之间随机进行。

现在,让我们用这些放大图来绘制图像:

plt.imshow(seq.augment_image(tr_images[0]), cmap='gray', \
           vmin = 0, vmax = 255)
plt.title('Image augmented using a \nrandom order \
of the two augmentations')

上述代码的输出如下:

从前面的图像中,我们可以看到这两个放大是在原始图像的顶部执行的(您可以看到图像已经旋转,并且应用了 dropout)。

对一批图像执行数据扩充以及对 collate_fn 的需求

我们已经看到,在同一幅图像的不同迭代中执行不同的增强是更可取的。

如果我们有一个在__init__方法中定义的增强管道,我们将只需要在输入图像集上执行一次增强。这意味着我们在不同的迭代中不会有不同的扩充。

类似地,如果增强是在__getitem__方法中——这是理想的,因为我们想要对每幅图像执行不同的增强集——主要的瓶颈是对每幅图像执行一次增强。如果我们对一批图像进行增强,而不是一次对一幅图像进行增强,速度会快得多。让我们通过查看两个场景来详细理解这一点,在这两个场景中,我们将处理 32 幅图像:

  • 增加 32 幅图像,一次一幅
  • 一次性增加 32 幅图像

为了了解在这两种情况下扩充 32 幅图像所需的时间,让我们利用时尚 MNIST 数据集的训练图像中的前 32 幅图像:

The following code is available as Time_comparison_of_augmentation_scenario.ipynb in the Chapter04 folder of this book's GitHub repository - tinyurl.com/mcvp-packt

  1. 获取训练数据集中的前 32 幅图像:
from torchvision import datasets
import torch
data_folder = '/content/' 
fmnist = datasets.FashionMNIST(data_folder, download=True, \
                                                train=True)
tr_images = fmnist.data
tr_targets = fmnist.targets
  1. 指定要在图像上执行的增强:
from imgaug import augmenters as iaa
aug = iaa.Sequential([
              iaa.Affine(translate_px={'x':(-10,10)}, 
                                        mode='constant'),
            ])

接下来,我们需要理解如何在Dataset类中执行增强。有两种可能的方法来扩充数据:

  • 一次增加一批图像
  • 一次放大一批中的所有图像

让我们来了解一下执行前面两个场景所需的时间:

  • 场景 1: 扩充 32 幅图像,一次一幅:

使用augment_image方法计算一次放大一幅图像所需的时间:

%%time
for i in range(32):
    aug.augment_image(tr_images[i])

放大 32 幅图像需要大约 180 毫秒。

  • 场景 2: 一次性批量扩充 32 张图像:

使用augment_images方法计算一次增加 32 张图像所需的时间:

%%time
aug.augment_images(tr_images[:32])

对一批图像进行增强需要大约 8 毫秒。

最佳做法是在一批图像的基础上进行扩充,而不是一次扩充一个图像。另外,augment_images方法的输出是一个numpy数组。

然而,我们一直在做的传统的Dataset类在__getitem__方法中一次提供一个图像的索引。因此,我们需要学习如何使用一个新的功能——collate_fn——使我们能够对一批图像进行操作。

  1. 定义Dataset类,它将输入图像、它们的类和增强对象作为初始化器:
from torch.utils.data import Dataset, DataLoader
class FMNISTDataset(Dataset):
    def __init__(self, x, y, aug=None):
        self.x, self.y = x, y
        self.aug = aug
    def __getitem__(self, ix):
        x, y = self.x[ix], self.y[ix]
        return x, y
    def __len__(self): return len(self.x)
  • 定义collate_fn,将该批数据作为输入:
    def collate_fn(self, batch):
  • 将一批图像及其类别分成两个不同的变量:
        ims, classes = list(zip(*batch))
  • 指定如果提供了增强对象,则必须进行增强。如果我们需要对训练数据而不是验证数据进行扩充,这是很有用的:
        if self.aug: ims=self.aug.augment_images(images=ims)

在前面的代码中,我们利用了augment_images方法,这样我们就可以处理一批图像。

  • 通过将图像形状除以 255,创建图像的张量以及缩放数据:
        ims = torch.tensor(ims)[:,None,:,:].to(device)/255.
        classes = torch.tensor(classes).to(device)
        return ims, classes

一般来说,当我们必须执行繁重的计算时,我们利用collate_fn方法。这是因为一次对一批图像进行这样的计算比一次对一幅图像进行更快。

  1. 从现在开始,为了利用collate_fn方法,我们将在创建数据加载器时使用一个新的参数:
  • 首先,我们创建了train对象:
train = FMNISTDataset(tr_images, tr_targets, aug=aug)
  • 接下来,我们定义数据加载器,以及对象的collate_fn方法,如下所示:
trn_dl = DataLoader(train, batch_size=64, \
                    collate_fn=train.collate_fn,shuffle=True)
  1. 最后,我们训练模型,就像我们到目前为止一直在训练它一样。通过利用collate_fn方法,我们可以更快地训练模型。

现在,我们已经对我们可以使用的一些主要数据增强技术有了坚实的理解,包括像素转换和collate_fn,它允许我们增强一批图像,让我们了解如何将它们应用于一批数据以解决图像转换问题。

用于图像翻译的数据增强

现在,我们可以用增强的数据来训练模型了。让我们创建一些增强数据并训练模型:

The following code is available as Data_augmentation_with_CNN.ipynb in the Chapter04 folder of this book's GitHub repository - tinyurl.com/mcvp-packt

  1. 导入相关的包和数据集:
from torchvision import datasets
import torch
from torch.utils.data import Dataset, DataLoader
import torch
import torch.nn as nn
import matplotlib.pyplot as plt
%matplotlib inline
import numpy as np

device = 'cuda' if torch.cuda.is_available() else 'cpu'
data_folder = '/content/' # 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
val_fmnist=datasets.FashionMNIST(data_folder, download=True, \
                                        train=False)
val_images = val_fmnist.data
val_targets = val_fmnist.targets
  1. 创建一个类,该类可以对随机平移到-10 到+10 像素之间的任何位置(向左或向右)的图像执行数据扩充:
  • 定义数据扩充管道:
from imgaug import augmenters as iaa
aug = iaa.Sequential([
              iaa.Affine(translate_px={'x':(-10,10)}, 
                                        mode='constant'),
            ])
  • 定义Dataset类:
class FMNISTDataset(Dataset):
    def __init__(self, x, y, aug=None):
        self.x, self.y = x, y
        self.aug = aug
    def __getitem__(self, ix):
        x, y = self.x[ix], self.y[ix]
        return x, y
    def __len__(self): return len(self.x)
    def collate_fn(self, batch):
        'logic to modify a batch of images'
        ims, classes = list(zip(*batch))
        # transform a batch of images at once
        if self.aug: ims=self.aug.augment_images(images=ims) 
        ims = torch.tensor(ims)[:,None,:,:].to(device)/255.
        classes = torch.tensor(classes).to(device)
        return ims, classes

在前面的代码中,我们利用了collate_fn方法来指定我们想要对一批图像执行增强。

  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, 10)
            ).to(device)

    loss_fn = nn.CrossEntropyLoss()
    optimizer = Adam(model.parameters(), lr=1e-3)
    return model, loss_fn, optimizer
  1. 定义train_batch函数,以便对批量数据进行训练:
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()
  1. 定义get_data函数来获取训练和验证数据加载器:
def get_data(): 
    train = FMNISTDataset(tr_images, tr_targets, aug=aug)
    'notice the collate_fn argument'
    trn_dl = DataLoader(train, batch_size=64, \
                collate_fn=train.collate_fn, shuffle=True)
    val = FMNISTDataset(val_images, val_targets) 
    val_dl = DataLoader(val, batch_size=len(val_images), 
                collate_fn=val.collate_fn, shuffle=True)
    return trn_dl, val_dl
  1. 指定训练和验证数据加载器,并获取模型对象、损失函数和优化器:
trn_dl, val_dl = get_data()
model, loss_fn, optimizer = get_model()
  1. 5个时期内训练模型:
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)
  1. 像我们在上一节中所做的那样,在翻译的图像上测试模型:
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)))

现在,让我们绘制不同翻译的预测类的变化:

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')

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

现在,当我们预测图像的各种翻译时,我们将看到分类预测没有变化,从而确保通过在增强的翻译图像上训练我们的模型来处理图像翻译。

到目前为止,我们已经看到了用增强图像训练的 CNN 模型如何能够很好地预测翻译后的图像。在下一节中,我们将了解过滤器学习什么,这使得预测翻译的图像成为可能。

可视化特征学习的结果

到目前为止,我们已经了解了 CNN 如何帮助我们分类图像,即使图像中的对象已经被翻译。我们还了解到,过滤器在学习图像特征方面起着关键作用,这反过来有助于将图像分类到正确的类别中。然而,我们还没有提到过滤器学到了什么使它们变得强大。

在这一节中,我们将了解这些过滤器学到了什么,使 CNN 能够通过对包含 X 和 O 的图像的数据集进行分类来正确地分类图像。我们还将检查完全连接的层(展平层),以了解它们的激活看起来像什么。让我们来看看过滤器学到了什么:

The code for this section is available as Visualizing_the_features'_learning.ipynb in the Chapter04 folder of this book's GitHub repository - tinyurl.com/mcvp-packt.

  1. 下载数据集:
!wget https://www.dropbox.com/s/5jh4hpuk2gcxaaq/all.zip
!unzip all.zip

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

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

  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
  1. 定义一个获取数据的类。此外,请确保图像的大小已调整为 28 x 28 的形状,批次已用三个通道成形,并且因变量作为数值提取。我们将在下面的代码中一步一步地实现这一点:
  • 定义图像增强方法,该方法将图像的大小调整为 28 x 28 的形状:
tfm = iaa.Sequential(iaa.Resize(28))
  • 定义一个类,它将文件夹路径作为输入,并在__init__方法中遍历该路径中的文件:
class XO(Dataset):
    def __init__(self, folder):
        self.files = glob(folder)
  • 定义__len__方法,该方法返回要考虑的文件长度:
    def __len__(self): return len(self.files)
  • 定义__getitem__方法,我们用它来获取一个索引,返回该索引处的文件,读取该文件,然后对图像执行增强。这里我们没有使用collate_fn,因为这是一个小数据集,不会显著影响训练时间:
    def __getitem__(self, ix):
        f = self.files[ix]
        im = tfm.augment_image(cv2.imread(f)[:,:,0])
  • 假设每个图像的形状为 28 x 28,我们现在将在形状的开始处创建一个虚拟通道尺寸;也就是说,在图像的高度和宽度之前:
        im = im[None]
  • 现在,我们可以根据文件名中的字符 post '/'和 previous'@'来分配每个图像的类别:
        cl = f.split('/')[-1].split('@')[0] == 'x'
  • 最后,我们返回图像和相应的类:
        return torch.tensor(1 - im/255).to(device).float(), \
                       torch.tensor([cl]).float().to(device)
  1. 检查你得到的图像样本。在下面的代码中,我们通过从之前定义的类中获取数据来提取图像及其对应的类:
data = XO('/content/all/*')
  • 现在,我们可以从获得的数据集中绘制一个图像样本:
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()

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

  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));

这会产生以下输出:

  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]
  1. 定义一个DataLoader,其中输入是Dataset类:
trn_dl = DataLoader(XO('/content/all/*'), batch_size=32, \
                    drop_last=True)
  1. 初始化模型:
model, loss_fn, optimizer = get_model()
  1. 5个时期内训练模型:
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)
  1. 获取图像以检查过滤器对图像的了解:
im, c = trn_dl.dataset[2]
plt.imshow(im[0].cpu())
plt.show()

这会产生以下输出:

  1. 将图像传递给经过训练的模型,并获取第一层的输出。然后,将其存储在intermediate_output变量中:
first_layer = nn.Sequential(*list(model.children())[:1])
intermediate_output = first_layer(im[None])[0].detach()
  1. 绘制 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()

这会产生以下输出:

在前面的输出中,请注意,某些滤波器(如滤波器 0、4、6 和 7)学习网络中存在的边,而其他滤波器(如滤波器 54)学习反转图像。

  1. 传递多个图像并检查第四个滤波器在图像上的输出(我们使用第四个滤波器只是为了说明的目的;如果愿意,您可以选择不同的过滤器):
  • 从数据中提取多个图像:
x, y = next(iter(trn_dl))
x2 = x[y==0]
  • 重塑x2的形状,使其具有适合 CNN 模型的输入形状;即批量 x 通道 x 高度 x 宽度:
x2 = x2.view(-1,1,28,28)
  • 定义一个存储模型直到第一层的变量:
first_layer = nn.Sequential(*list(model.children())[:1])
  • 提取通过模型传递 O 图像(x2)直到第一层(first_layer)的输出,如前所述:
first_layer_output = first_layer(x2).detach()
  1. 绘制通过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()

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

请注意,给定滤镜(在这种情况下,第一层的第四个滤镜)的行为在图像之间保持一致。

  1. 现在,让我们创建另一个模型,该模型提取层,直到第二个卷积层(即,直到前面模型中定义的四个层),然后提取传递原始 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()

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

现在,让我们使用上图中第 34 个过滤器的输出作为例子。当我们让多个 O 图像通过过滤器 34 时,我们应该看到图像之间的类似激活。让我们测试一下,如下所示:

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()

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

注意,即使在这里,不同图像上第 34 个^(滤光器的激活是相似的,因为 O 的左半部分激活了滤光器。)

  1. 绘制完全连接层的激活,如下所示:
  • 首先,获取更大的图像样本:
custom_dl= DataLoader(XO('/content/all/*'),batch_size=2498, \
                       drop_last=True)
  • 接下来,仅从数据集中选择 O 图像,然后对它们进行整形,以便它们可以作为输入传递给我们的 CNN 模型:
x, y = next(iter(custom_dl))
x2 = x[y==0]
x2 = x2.view(len(x2),1,28,28)
  • 提取展平(完全连接)层,将前面的图像传递到模型中,直到它们到达展平层:
flatten_layer = nn.Sequential(*list(model.children())[:7])
flatten_layer_output = flatten_layer(x2).detach()
  • 绘制展平层:
plt.figure(figsize=(100,10))
plt.imshow(flatten_layer_output.cpu())

上述代码产生以下输出:

请注意,输出的形状是 1245 x 3200,因为我们的数据集中有 1,245 张 O 图像,展平层中的每张图像有 3,200 个维度。

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

请注意,该模型已经学会为完全连接的层带来一些结构,即使输入图像——虽然都属于同一类——在风格上有很大不同。

既然我们已经了解了 CNN 是如何工作的,以及过滤器是如何帮助这个过程的,我们将应用这一点,以便我们可以对猫和狗的图像进行分类。

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

到目前为止,我们已经了解了如何在时尚 MNIST 数据集上执行图像分类。在这一节中,我们将在一个更真实的场景中做同样的事情,任务是对包含猫或狗的图像进行分类。我们还将了解当我们改变可用于训练的图像数量时,数据集的准确性如何变化。

我们将在 Kaggle 中使用一个数据集:www.kaggle.com/tongpython/cat-and-dog

The code for this section is available as Cats_Vs_Dogs.ipynb in the Chapter04 folder of this book's GitHub repository - tinyurl.com/mcvp-packt Be sure to copy the URL from the notebook in GitHub to avoid any issue while reproducing the results

  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
  1. 下载数据集,如下所示:
  • 这里,我们必须下载在colab环境中可用的数据集。然而,首先我们必须上传我们的 Kaggle 认证文件:
!pip install -q kaggle
from google.colab import files
files.upload()

这一步你需要上传你的kaggle.json文件,可以从你的 Kaggle 账户获得。GitHub 上的相关笔记本中提供了如何获取kaggle.json文件的详细信息

  • 接下来,指定我们将移动到 Kaggle 文件夹,并将kaggle.json文件复制到其中:
!mkdir -p ~/.kaggle
!cp kaggle.json ~/.kaggle/
!ls ~/.kaggle
!chmod 600 /root/.kaggle/kaggle.json
  • 最后,下载猫狗数据集并解压:
!kaggle datasets download -d tongpython/cat-and-dog
!unzip cat-and-dog.zip
  1. 提供培训和测试数据集文件夹:
train_data_dir = '/content/training_set/training_set'
test_data_dir = '/content/test_set/test_set'
  1. 构建一个从前面的文件夹中获取数据的类。然后,根据图像对应的目录,为“狗”图像提供标签 1,为“猫”图像提供标签 0。此外,确保获取的图像已被规范化为 0 到 1 之间的比例,并对其进行置换,以便首先提供通道(因为 PyTorch 模型希望在图像的高度和宽度之前首先指定通道)。
  • 定义__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
  • 接下来,随机化文件路径,并基于对应于这些文件路径的文件夹创建目标变量:
        from random import shuffle, seed; seed(10); 
        shuffle(self.fpaths)
        self.targets=[fpath.split('/')[-1].startswith('dog') \
                      for fpath in self.fpaths] # dog=1 
  • 定义对应于self类的__len__方法:
    def __len__(self): return len(self.fpaths)
  • 定义__getitem__方法,我们用它从文件路径列表中指定一个随机的文件路径,读取图像,并调整所有图像的大小,使它们的大小为 224 x 224。假设我们的 CNN 期望首先为每个图像指定来自通道的输入,我们将permute调整大小的图像,以便在我们返回缩放的图像和相应的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)
  1. 检查随机图像:
data = cats_dogs(train_data_dir)
im, label = data[200]

我们需要把我们最后获得的图像传送到我们的频道。这是因为 matplotlib 希望在提供图像的高度和宽度后,图像具有指定的通道:

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

这会产生以下输出:

  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)
    )

在前面的代码中,我们将输入通道的数量(ni)、输出通道的数量(no)、滤波器的kernel_sizestride作为conv_layer函数的输入。

  • 定义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层一样。

  • 现在,我们必须调用get_model函数来获取模型、损失函数(loss_fn)和optimizer,然后使用从torchsummary包中导入的summary方法对模型进行汇总:
from torchsummary import summary
model, loss_fn, optimizer = get_model()
summary(model, torch.zeros(1,3, 224, 224));

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

  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()
  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()

请注意,前面用于精度计算的代码不同于时尚-MNIST 分类中的代码,因为当前模型(猫与狗的分类)是为二元分类构建的,而时尚-MNIST 模型是为多类分类构建的。

  • 定义验证损失计算函数:
@torch.no_grad()
def val_loss(x, y, model):
    prediction = model(x)
    val_loss = loss_fn(prediction, y)
    return val_loss.item()
  1. 针对5时段训练模型,并在每个时段结束时检查测试数据的准确性,正如我们在前面章节中所做的那样:
  • 定义模型并获取所需的数据加载器:
trn_dl, val_dl = get_data()
model, loss_fn, optimizer = get_model()
  • 在不断增加的时期内训练模型:
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)
  1. 绘制训练和验证准确度在增加的时期内的变化:
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()

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

注意,在5时期结束时的分类精度约为 86%。

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

到目前为止,我们所做的培训是基于大约 8K 个例子,其中 4K 的例子来自cat班,其余的来自dog班。在下一节中,我们将了解在测试数据集的分类准确性方面,减少训练样本数量对每个类有什么影响。

对用于训练的图像数量的影响

我们知道,一般来说,我们使用的训练样本越多,我们的分类准确度就越高。在本节中,我们将通过人为减少可用于训练的图像数量,然后在对测试数据集进行分类时测试模型的准确性,来了解使用不同数量的可用图像对训练准确性有何影响。

The code for this section is available as Cats_Vs_Dogs.ipynb in the Chapter04 folder of this book's GitHub repository - tinyurl.com/mcvp-packt . Given that the majority of the code that will be provided here is similar to what we have seen in the previous section, in text, we have only provided the modified code for brevity. The respective notebook in this book's GitHub repository will contain the full code.

这里,我们只希望训练数据集中每个类有 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 张)构建的模型的准确性将如下所示:

在这里,我们可以看到,因为我们在训练中有更少的图像示例,测试数据集的准确性大大降低;也就是下降到~66%。

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

我们将使用用于 500 个数据点训练示例的相同代码,但将改变可用图像的数量(分别为 2K、4K 和 8K 总数据点)。为简洁起见,我们将只查看在不同数量的可用于训练的图像上运行模型的输出。这会产生以下输出:

如您所见,可用的训练数据越多,模型对测试数据的准确性就越高。然而,在我们遇到的每个场景中,我们可能没有足够大的训练数据量。下一章将介绍迁移学习,将通过指导您使用各种技术来解决这个问题,即使是在少量的训练数据上,您也可以使用这些技术来获得高精度。

摘要

当与先前看到的已经被翻译的图像非常相似的新图像被输入到模型中时,传统的神经网络就失效了。卷积神经网络在解决这一缺点方面起着关键作用。这是通过 CNN 中的各种机制实现的,包括过滤器、步长和池。最初,我们构建了一个玩具示例来了解 CNN 是如何工作的。然后,我们了解了数据增强如何通过在原始图像上创建翻译增强来帮助提高模型的准确性。之后,我们了解了不同的过滤器在特征学习过程中学习什么,以便我们可以实现 CNN 来分类图像。

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

问题

  1. 为什么在使用传统神经网络时,对翻译图像的预测很低?

  2. 卷积是怎么做的?

  3. 如何确定过滤器中的最佳重量值?

  4. 卷积和汇集的结合如何帮助解决图像转换的问题?

  5. 更接近输入层的层中的过滤器学习什么?

  6. 池有哪些功能有助于构建模型?

  7. 为什么我们不能获取一个输入图像,将其展平(就像我们在时尚-MNIST 数据集上所做的那样),然后为真实世界的图像训练一个模型?

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

  9. 在什么场景下我们利用collate_fn进行数据加载?

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