《PyTorch计算机视觉实战》:第三章

发布于:2024-06-29 ⋅ 阅读:(14) ⋅ 点赞:(0)

在本章中,我们将学习如何使用神经网络进行图像分类。

从本质上讲,我们将学习如何实现对图像的表示,以及如何通过对超参数的调整来理解这些超参数对神经网络的影响。

为了避免太多的复杂性和混乱,我们在前一章中只讨论了神经网络的一些基本方面。然而,在训练网络时还需要调整更多的输入。通常将这些输入称为超参数

与神经网络中的参数(在训练中习得)相比,这些输入是由构建该网络的人提供的超参数。每个超参数的更改都可能会影响神经网络训练的准确度或速度。此外,诸如缩放、批归一化和正则化等一些额外的技术,也有助于提高神经网络的性能。

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

表示图像

数字图像文件(扩展名通常为“ JPEG”或“ PNG”)是由像素数组组成的。像素是构成图像的最小元素。在灰度图像中,每个像素是取值在 0 和 255 之间的标量(单个)值—0 表示黑,255 表示白,中间的值表示灰色(像素值越小,像素灰度越暗)。另一方面,彩色图像中的像素是一个三维向量,向量中的每个分量分别对应于红色、绿色和蓝色通道中的标量值

一幅图像包含 height × width × c 的像素,其中 height 表示像素的数, width 表示像素的数,c 表示通道数。c 为 3 表示彩色图像(每个通道分别表示图像的红、绿、蓝的强度),c 为 1 表示灰度图像。一个包含 3×3 像素及其对应标量值的灰度图像示例如图 3-1 所示。

在这里插入图片描述

同样,像素值为 0 表示该像素是纯黑的,255 则表示该像素是纯亮度的(灰度图像表示该像素是纯白色的,彩色图像则表示各通道中的纯红、纯绿或纯蓝)。

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

首先使用下列代码将图像转换为灰度图像,并绘制出该图像:

cv2 这个工具包是由 opencv-python 这个包所提供的。

# 导入 cv2(用于从磁盘读取图像)和 matplotlib(用于绘制加载的图像)库,
# 并将下载的图像读取到 Python 环境中:
import cv2, matplotlib.pyplot as plt


img = cv2.imread('Hemanvi.jpeg')
# 输出:(516, 300, 3)
print(img.shape)
# 上述代码使用 cv2.imread 方法读取图像。这将把图像转换为像素值数组。

# 在图像的第 50 ~ 250 行和第 40 ~ 240 列之间进行裁剪。
img = img[50:250, 40:240]

# 最后,使用下列代码将图像转换为灰度图像,并绘制出该图像:
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
plt.imshow(img_gray, cmap='gray')
plt.show()

# 将图像转换为一个 25×25 的数组,并绘制出来:
img_gray_small = cv2.resize(img_gray, (25, 25))
plt.imshow(img_gray_small, cmap='gray')
plt.show()

# 显然,使用较少的像素表示相同的图像会导致较为模糊的输出。


# 可以查看像素值
print(img_gray_small)

关于 cvtColor 函数的解释:

在这里插入图片描述

效果:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

下面将为彩色图像创建一个结构化的像素值数组:

import cv2, matplotlib.pyplot as plt

img = cv2.imread('./Hemanvi.jpeg')

img = img[50:250, 40:240, :]
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
"""
注意,上述代码使用 cv2.cvtColor 方法对通道进行了重新排序。
这样做是因为使用 cv2 导入图像时,通道顺序是蓝色先,绿色次之,最后是红色,
而我们通常习惯于观看基于 RGB 通道的图像,其通道顺序是红色、绿色,然后是蓝色。
"""

plt.imshow(img)
plt.show()
# 输出:(200, 200, 3)
print(img.shape)

# 可以如下得到图片的右下角的 3×3 像素数组:
crop = img[-3:, -3:]
# 输出并绘制像素值:
print(crop)
plt.imshow(crop)
plt.show()

运行效果:

在这里插入图片描述

在这里插入图片描述

书中有一副图很形象:

在这里插入图片描述
上图的意思是:第一个像素对应的 RGB 值 分别为:242、149、141,然后第二个像素对应的 RGB 值分别为:249、161、151,剩下的依次类推。这些 RGB 值共同组合在一起就显示出了运行效果中的图像。

这个图肯定不能按列看,如果是按列的话,以第一个块为例,其值为242、240、239,如果为255、255、255的话就是全黑,而242、240、239的组合就算不是全黑也应该是贼黑了,但是明显效果图中并非全黑,所以肯定是按行看。

另外上图中展示的平面图,从空间上看实际其应该为一个立体的方图:

在这里插入图片描述

从上图可以看出,每一个像素的颜色都由 RGB 三个颜色的值共同确定,这也是为什么我们打印 img 时其输出的结果是一个三维数组(一维表示长度、一维表示宽度、一维表示 RGB 通道数)。

现在可以将每个图像表示为标量数组(在灰度图像的情况下)或数组的数组(在彩色图像的情况下),既然已经将磁盘上的图像文件转换为结构化的数组格式,那么就可以用多种技术对图像进行数学处理。也就是说,将图像转换为结构化的数字数组(即将图像读入Python 内存)后,就可以在图像(以数字数组表示)上执行数学运算。可以利用这个数据结构来执行各种任务,如分类、检测和分割,我们将在后续章节中详细讨论这些内容。

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

传统计算机视觉会为每幅图像创建一些特征,然后使用这些特征作为输入。

如:
直方图特征:对于一些任务,如自动亮度调节或夜视系统,了解图片中的光照分布
(即像素中亮或暗的部分)是很重要的。

边角特征:对于图像分割这样的任务,找到每个人对应的像素集很重要,首先提取
边缘是有意义的,因为一个人的边界只是边缘的集合。

等等等等…

创建这些特征的主要缺点是,你需要是图像和信号分析方面的专家,应该充分了解什么特征最适合解决什么问题。即使满足了这两个限制条件,也不能保证这样的专家就能够找到正确的特征输入组合,即使他们找到了,也不能保证这样的组合将在新的未见过场景中能够正确发挥作用。由于这些缺点,计算机视觉社区已经在很大程度上转向了基于神经网络的模型。这些模型不仅会自动找到正确的特征,而且还会学习如何优化组合这些特征来完成工作

现在已经看了一些已有特征提取技术示例和它们的缺点,下面将学习如何在图像数据集上训练神经网络。

为图像分类准备数据

准备数据代码如下:

# 首先下载数据集并将其导入相关的包。
# torchvision 包包含多种数据集,其中一个是本章中讨论的 FashionMNIST 数据集:
from torchvision import datasets
import torch
import cv2

# 指定所要存储数据集的文件夹
data_folder = '~/data/FMNIST'
# 指定需要的数据集的行为:下载到哪里,是否需要下载,下载的是训练集还是测试集
fmnist = datasets.FashionMNIST(data_folder, download=True, train=True)

# 接下来,必须将从 fmnist.data 中获取的图像存储为 tr_images,
# 并将从 fmnist.targets 中获得的标签(目标)数据存储为 tr_targets:
tr_images = fmnist.data
tr_targets = fmnist.targets

# 检查正在处理的张量:
unique_values = tr_targets.unique()
print(f'tr_images & tr_targets: \n\t '
      f'X - {tr_images.shape}\n\t '
      f'Y - {tr_targets.shape}\n\t '
      f'Y - Unique values : {unique_values}')
print(f'TASK:\n\t{len(unique_values)} class Classification')
print(f'UNIQUE CLASSES:\n\t{fmnist.classes}')

上面的操作得到如下输出:

在这里插入图片描述

可以看到有 6 万张 28×28 大小的图片,所有的图片有 10 个可能的类别。注意,tr_targets 包含每个类的标签,fmnist.classes 则提供了与 tr_targets 中每个标签对应的名称。

书中更完整的代码:

# 首先下载数据集并将其导入相关的包。
# torchvision 包包含多种数据集,其中一个是本章中讨论的 FashionMNIST 数据集:
from torchvision import datasets
import torch
import matplotlib.pyplot as plt
import numpy as np
import cv2

# 指定所要存储数据集的文件夹
data_folder = '~/data/FMNIST'
# 指定需要的数据集的行为:下载到哪里,是否需要下载,下载的是训练集还是测试集
fmnist = datasets.FashionMNIST(data_folder, download=True, train=True)

# 接下来,必须将从 fmnist.data 中获取的图像存储为 tr_images,
# 并将从 fmnist.targets 中获得的标签(目标)数据存储为 tr_targets:
tr_images = fmnist.data
tr_targets = fmnist.targets

# 检查正在处理的张量:
unique_values = tr_targets.unique()
print(f'tr_images & tr_targets: \n\t '
      f'X - {tr_images.shape}\n\t '
      f'Y - {tr_targets.shape}\n\t '
      f'Y - Unique values : {unique_values}')
print(f'TASK:\n\t{len(unique_values)} class Classification')
print(f'UNIQUE CLASSES:\n\t{fmnist.classes}')

# 为所有 10 个可能的类别分别给出 10 个随机图像样本
# 导入相关的包来绘制一个图像网格,这样你也可以进行数组处理:
"""
创建一个包含多个子图的图像网格,用于可视化标签类别在训练数据集中的分布.
创建一个图,用于显示一个 10×10 的网格,网格的每一行对应于一个类别,每一
列表示分属于每个类别的示例图像。循环遍历唯一的类号(label_class),并获
取对应于给定类号的行索引(label_x_rows):
"""
R, C = len(tr_targets.unique()), 10
"""
plt.subplots(R, C, figsize=(10,10)):plt.subplots() 是 Matplotlib 中用于创建包含子图的图形对象的函数。
      R, C:指定子图网格的行数和列数,即前面计算得到的行数和每行子图的数量。
      figsize=(10,10):指定图形的尺寸,单位是英寸,这里是宽度和高度都为10英寸。
fig, ax:
      fig 是整个图形对象,而 ax 是一个包含所有子图对象的二维数组或矩阵。
      ax 的形状为 (R, C),其中 R 行 C 列,每个元素代表一个子图对象。
"""
fig, ax = plt.subplots(R, C, figsize=(10, 10))
# enumerate() 函数用于在循环中同时获取索引和元素。
# plot_row:当前行的子图对象数组。因为 ax 是一个二维数组,所以 plot_row 是一个一维数组,包含当前行的所有子图对象。
for label_class, plot_row in enumerate(ax):
      """
      np.where(tr_targets == label_class)[0]:NumPy 的 where() 函数用于根据条件获取元素的索引。
      tr_targets == label_class:创建一个布尔数组,指示 tr_targets 中哪些位置的元素等于当前的 label_class。
      [0]:取出满足条件的元素的索引数组,这里假设所有类别标签都是顺序编码的整数,所以只取索引数组的第一个维度。
      label_x_rows:包含所有标签为 label_class 的样本在 tr_targets 中的索引数组。
      """
      label_x_rows = np.where(tr_targets == label_class)[0]
      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()
plt.show()
# 每一行代表一个包含 10 个不同图像的样本,它们都属于同一个类别。

运行结果如图:

在这里插入图片描述

每一行代表一个包含 10 个不同图像的样本,它们都属于同一个类别。

训练神经网络

在这里插入图片描述

from torch.utils.data import Dataset, DataLoader
import torch
import torch.nn as nn
from torch.optim import SGD
import numpy as np
import matplotlib.pyplot as plt
from torchvision import datasets

# ------------------------- 训练神经网络之前的准备工作 ---------------------------------

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

data_folder = '~/data/FMNIST'

fmnist = datasets.FashionMNIST(data_folder, download=True, train=True)

tr_images = fmnist.data
tr_targets = fmnist.targets


# 构建一个获取数据集的类。
# 记住,它派生自 Dataset 类,需要三个神奇的函数—__init__、__getitem__ 和 __len__
class FMNISTDataset(Dataset):
    # x 和 y 是构造函数的参数,分别表示输入的特征数据和对应的标签数据。
    def __init__(self, x, y):
        # x.float() 将输入数据 x 转换为浮点数类型,通常是为了适应神经网络的输入要求。
        x = x.float()
        # x.view(-1, 28*28) 对 x 进行形状重塑,
        # 将每个图像扁平化为一个长度为 784 的向量(28x28=784),其中 -1 表示自动推断该维度的大小。
        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)


# 创建一个函数,从名为 FMNISTDataset 的数据集生成一个训练数据加载器 trn_dl。
# 对于批大小 32,这将随机抽样 32 个数据点:
def get_data():
    train = FMNISTDataset(tr_images, tr_targets)
    trn_dl = DataLoader(train, batch_size=32, shuffle=True)
    return trn_dl


# 定义一个模型,以及损失函数和优化器:
def get_model():
    """
    该模型包含一个隐藏层,隐藏层具有 1000 个神经元。
    输出层包含 10 个神经元,因为有 10 个可能的类别。
    此外,这里调用了 CrossEntropyLoss 函数,因为输出值可能属于这 10 个类别中的任何一个。
    最后,在这个练习中需要注意的一个关键点是将学习率 lr 初始化为值 0.01,而不是默认值 0.001,以查看模型将如何进行学习。
    """
    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()
    prediction = model(x)
    batch_loss = loss_fn(prediction, y)
    batch_loss.backward()
    opt.step()
    opt.zero_grad()
    return batch_loss.item()


"""
上述代码在前向传播中通过模型传递这批图像数据。
它还计算在批数据上的损失,然后通过反向传播方式计算梯度并实现权重更新。
最后,它会刷新梯度的内存,这样就不会影响下一次的梯度计算。

现在已经完成了这一步,可以通过在 batch_loss 之上获取 batch_loss.item()以标量的形式提取损失值。
"""

# 构建用于计算给定数据集准确度的函数:
"""
由于不需要更新权重,我们可以选择不计算梯度。
这段代码使用了 @torch.no_grad() 装饰器来禁用在函数内部的梯度计算。
    @torch.no_grad() 是 PyTorch 提供的一个装饰器,用于在函数执行期间临时禁用梯度计算。
    当我们不需要在函数内部计算梯度(例如在评估模型或进行推断时),可以使用这个装饰器来提高运行效率和节省内存。
"""


@torch.no_grad()
# accuracy 函数用于计算模型在给定输入 x 和标签 y 下的准确率。
def accuracy(x, y, model):
    # model.eval() 将模型设置为评估模式,
    # 这通常会影响某些层(如 dropout 和 batch normalization),使其在推断时表现更稳定。
    model.eval()
    # prediction = model(x) 使用模型对输入 x 进行预测,得到预测结果 prediction。
    prediction = model(x)
    # print("-----------------------------------------------")
    # print(prediction.shape) 输出:torch.Size([32, 10])
    # print("-----------------------------------------------")
    """
    prediction:
        prediction 是模型给出的预测结果。
        通常情况下,它是一个张量(tensor),形状可能为 (batch_size, num_classes),
        其中 batch_size 是批次大小,num_classes 是类别数(对于分类任务)
    .max(-1) 方法:
        .max(-1) 是 PyTorch 张量的方法,用于沿着指定维度(这里是 -1,通常是最后一个维度)找到最大值。
        对于二维张量(即 (batch_size, num_classes)),.max(-1) 将返回两个张量:
            max_values:包含每个样本中最大值的张量,形状为 (batch_size,)。每个元素是对应样本的最大预测概率值。
            argmaxes:包含每个样本中最大值的索引的张量,形状也为 (batch_size,)。每个元素是对应样本中预测值最大的类别索引。
    max_values 和 argmaxes 变量:
        max_values 是存储了每个样本预测的最大概率值的变量。
        argmaxes 是存储了每个样本预测的最大概率值对应的类别索引的变量。
    """
    max_values, argmaxes = prediction.max(-1)
    # print("-------------------")
    # print("max_values: {}, argmaxes: {}".format(max_values, argmaxes))
    """
    输出:
    max_values: tensor([ 8.7139,  0.0789,  0.0789,  0.0789,  0.0789,  0.0789,  0.0789,  0.0789,
         0.0789,  0.0789,  0.0789,  0.0789,  0.6622,  0.0789,  0.0789,  0.0789,
         0.0789,  0.0789,  0.0789,  0.0789,  0.0789,  0.7531, 12.8652,  0.0789,
         0.0789,  0.0789,  0.0789,  0.0789,  0.0789,  0.0789,  0.0789,  0.0789]), 
    argmaxes: tensor([5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 7, 4, 4, 4, 4, 4, 4, 4, 4, 7, 7, 4,
         4, 4, 4, 4, 4, 4, 4, 4])
    """
    # print("-------------------")
    # argmaxes == y 比较预测的类别索引 argmaxes 是否与真实标签 y 相等,得到一个布尔值张量
    is_correct = argmaxes == y
    # is_correct.cpu().numpy().tolist() 将布尔张量转换为 NumPy 数组,
    # 然后再转换为 Python 列表,以便后续处理或返回结果。
    return is_correct.cpu().numpy().tolist()


# ------------------------- 使用以下代码行训练神经网络 ---------------------------------

# 初始化模型、损失、优化器和 DataLoader:
trn_dl = get_data()
model, loss_fn, optimizer = get_model()

# 在每轮结束时调用包含准确度和损失值的列表:
losses, accuracies = [], []

# 定义轮数:
for epoch in range(5):
    print(epoch)
    # 调用列表,它将包含一轮中每个批处理对应的准确度和损失值:
    epoch_losses, epoch_accuracies = [], []
    # 通过迭代使用 DataLoader 来创建一批训练数据:
    # iter 是 Python 中的另一个内置函数,用于生成一个迭代器对象。enumerate则迭代这个迭代器对象
    for ix, batch in enumerate(iter(trn_dl)):
        x, y = batch
        # print("-----------------------")
        # print(x.shape) x 是一批32张图片数据
        # print(y.shape) y 则是这一批32张图片各自所对应的真实的类别标签
        """
        输出:
        torch.Size([32, 784])
        torch.Size([32])
        """
        # print("-----------------------")
        # 使用 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()
plt.show()

运行结果如下:

在这里插入图片描述

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

注意,因为没有保留 torch.random_seed(0),所以在执行这里的代码时,结果可能会有所不同。然而,你得到的结果应该会让你得出类似的结论。

现在,你已经对如何训练神经网络有了一个完整的了解,还需要学习一些应该遵循的实践经验以实现良好的模型性能,以及使用这些实践经验的原因。这可以通过对各种超参数的微调实践来实现,下面就讨论其中的一些超参数

在这里插入图片描述

缩放数据集以提升模型准确度

缩放数据集是确保变量被限制在某个有限范围内的过程

在本节中,我们通过对每个输入值除以数据集中可能的最大值将自变量的值限制为 0 和 1 之间的值。这个最大值是255,对应于白色像素。

class FMNISTDataset(Dataset):
    # x 和 y 是构造函数的参数,分别表示输入的特征数据和对应的标签数据。
    def __init__(self, x, y):
        # 通过对每个输入值除以数据集中可能的最大值,将自变量的值限制为0和1之间的值
        # x.float() 将输入数据 x 转换为浮点数类型,通常是为了适应神经网络的输入要求。
        x = x.float() / 255
        
        # x.view(-1, 28*28) 对 x 进行形状重塑,
        # 将每个图像扁平化为一个长度为 784 的向量(28x28=784),其中 -1 表示自动推断该维度的大小。
        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 之间。

此时的效果如下:

在这里插入图片描述

可以看到,训练损失在持续减少,训练准确度在持续增加,目前已经增加到 85%左右。

缩放输入数据集,使其被限制在比较小的取值范围,通常有助于获得更好的模型准确度

理解不同批大小的影响

在上一节中,每批训练数据集包含 32 个数据点。这导致了每轮的权重更新的数量较大,因为每轮有 1875 个权重更新(60 000÷32 近似等于 1875,其中 60 000 是训练图像的数量)

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

在本节中,将讨论和比较以下内容:
❍ 当训练批大小为 32 时,训练和验证数据的损失值和准确度;
❍ 当训练批大小为 10 000 时,训练和验证数据的损失值和准确度。

当轮数较少时,较小的批大小通常有助于达到最佳准确度,但它不应低到影响训练时间的程度

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

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

理解不同学习率的影响

到目前为止,在训练模型时一直使用 0.01 的学习率。在第 1 章,我们了解到学习率在获得最优权重值方面起着关键作用。这里,当学习率较小时,权重值逐渐向最优值移动,而当学习率较大时,权重值则可能出现振荡

此外,还应该注意到学习率较小时,训练损失和验证损失之间的差距比前一种情形(这里类似的差距存在于第 4 轮结束时)小得多。原因是学习率较小时,权重更新要慢得多,这就意味着训练损失和验证损失之间的差距不会很快扩大

在前文中,我们了解到在学习率较大(0.1)时,模型无法被训练(模型欠拟合)。在学习率适中(0.001)或学习率较小(0.000 01)时,则可以训练出一个准确度良好的模型。在这里,我们看到学习率适中能够快速产生过拟合,学习率较小则需要更长的时间才能达到与学习率适中模型相当的准确度。

目前确定了在缩放数据集和非缩放数据集上拥有较大学习率都不太可能产生最好的结果,下一节将介绍如何在模型开始出现过拟合时自动降低学习率

在这里插入图片描述

理解不同学习率衰减的影响

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

这就要求在一开始具有较大的学习率,然后随着模型达到接近最佳准确度,逐渐降低学习率。这就要求了解什么时候必须降低学习率。

解决这个问题的一种潜在方法是持续监测验证损失,如果验证损失(在前面的 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')

在这里插入图片描述

现在已经了解了调节器,下面就在模型训练时使用这个调节器。

与前面几节相似,除了这里显示的粗体代码,其余的所有代码都与 3.6.1 节的相同,这些粗体代码是为了计算验证损失而添加的:

在这里插入图片描述
在这里插入图片描述

在上述代码中,我们规定只要验证损失没有在连续的轮中减少,就应该激活调节器。此时,学习率会将当前的学习率减半。

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

构建更深的神经网络

到目前为止,我们的神经网络架构只有一个隐藏层。

在本节中,我们将对比具有两个隐藏层和没有隐藏层(没有隐藏层是逻辑回归)的模型的性能。

目前我们已经从不同的角度看到,当输入数据没有被缩放(缩小到一个小范围)时,模型就不能得到很好的训练。非缩放数据(范围更大的数据)也有可能出现在隐藏层中(特别是对于具有多个隐藏层的深度神经网络),因为在获取隐藏层中节点的值时通常会涉及矩阵乘法。在下一节中,我们将学习如何在中间层中处理此类未缩放数据。

理解不同批归一化的影响

首先,输入值既不能非常小也不能非常大。

关于很小或很大的输入值,也可能遇到下面这种情形:某个隐藏层的节点可能会产生一个很小的数值或一个很大的数值,此时将隐藏层权重连接到下一层就会导致之前遇到的同样的问题

在这种情况下,可以使用批归一化方法解决这个问题,因为这种方法对每个节点上的值进行了归一化处理,就像对输入值进行缩放处理一样

在这里插入图片描述

在训练深度神经网络时,批归一化很有帮助。它能够帮助避免梯度变得太小,以至于不能更新权重

过拟合的概念

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

从本质上说,这表明该模型在未见过的数据集上泛化程度不高,因为它只是从训练数据集学习。这也说明该模型正在学习训练数据集中所有可能的边缘情况,这些情况并不适用于验证数据集。

在这里插入图片描述

减少过拟合的一些典型策略如下:

❍ dropout;
❍ 正则化。

下面将介绍这些策略。

添加 dropout 的影响

dropout 是一种随机选择特定百分比的激活并将其降至 0 的机制

在下一次迭代中,随机选择另一组隐藏单元并将其关闭。这样,神经网络就不会对边缘情形进行优化,因为该网络没有那么多机会通过调整权重来记忆边缘情形(假设权重不会在每次迭代中都得到更新)。

注意,预测过程中不需要使用 dropout,因为该机制只能应用于已训练的模型

在模型训练和模型验证过程中,层的行为通常会有所不同 —— 就像你在 dropout 的例子中看到的那样。出于这个原因,你必须使用以下两种方法之一预先指定模型的模式:model.train() 为训练模式,model.eval() 为评估模式。如果不这样做,就有可能得到意想不到的结果。例如,在图 3-34 中,请注意该模型(包含 dropout)如何在训练模式下对相同的输入给出不同的预测。然而,当相同的模型处于 eval 模式时,将会抑制 dropout层并返回相同的输出。

在定义模型架构的时候,在 get_model 函数中指定 dropout 的相关代码如下:

def get_model():
 	model = nn.Sequential(
 		nn.Dropout(0.25),
 		nn.Linear(2828, 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 是在线性激活之前确定的。这使得线性激活层中固定百分比的权重不会被更新。

正则化的影响

除了模型的训练准确度远高于验证准确度之外,过拟合的另一个特征是某些权重值会远远高于其他权重值。高权重值可能是模型在训练数据上学习得很好的表现(本质上却是对已知数据的一种比较糟糕的学习)。

dropout 是一种用于使权重值不会频繁更新的机制,正则化则是另一种用于此目的的机制

正则化是一种基于对模型中高权重值进行惩罚的技术。因此,它是一种具有双重优化目标的目标函数,即最小化训练数据的损失和权重值。在本小节中,将学习两种类型的正则化方法:

❍ L1 正则化;
❍ L2 正则化。

在这里插入图片描述

在这里插入图片描述

现在已经了解了使用深度神经网络进行的图像分类,以及帮助训练模型的各种超参数。

在下一章中,我们将了解使用本章所介绍的方法面临的一些问题,以及如何使用卷积神经网络解决这些问题

第四章:卷积神经网络

在本章中,我们首先考察传统深度神经网络面临的问题,通过一个简单示例介绍卷积神经网络(Convolutional Neural Network,CNN)的内部工作原理,然后了解卷积神经网络的主要超参数,包括步长、池化和滤波器。接下来,我们将利用 CNN 以及各种数据增强技术来解决传统深度神经网络准确度较低的问题。之后,我们将考察使用 CNN 进行特征学习获得的输出结果。最后,我们将把所学内容整合在一起来解决一个具体用例:通过说明图像中包含的是狗还是猫的方式实现对图像的分类。

传统神经网络面临的问题

在这里插入图片描述

在了解传统神经网络失败的情形之后,我们将学习 CNN 如何有助于解决这个问题。但是在开始之前,我们首先学习 CNN 的若干构建模块

CNN 的构建模块

CNN 是图像处理领域最具突出应用的一种神经网络架构。CNN 解决了前一节中传统深度神经网络的主要限制。除了用于图像分类,CNN 还可以用于完成目标检测、图像分割、GAN 等任务。无论我们在什么情况下使用图像,CNN 基本上都能派上用场。可以使用多种不同的方法构建卷积神经网络,有多种预训练模型利用 CNN 执行各种任务。

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

❍卷积;
❍滤波器;
❍步长和填充;
❍池化。

卷积

卷积基本上就是两个矩阵的乘法运算。正如你在前文看到的那样,矩阵乘法是神经网络训练的一个关键运算(在计算隐藏层值的时候,我们执行的是输入值和连接到隐藏层的权重值之间的矩阵乘法运算。类似地,模型通过执行矩阵乘法来计算输出层的值)。

在这里插入图片描述
在这里插入图片描述

较小的矩阵一般也称为卷积核

滤波器(卷积核)

滤波器是一个权重矩阵,需要在开始的时候对其进行随机初始化。网络模型通过不断增加的轮数学习滤波器的最优权重值

在这里插入图片描述

一般来说,CNN 模型的滤波器越多,该模型可以学到的图像特征就越多。

滤波器用于学习图像中存在的不同特征。例如,某个特定的滤波器可能学习到猫的耳朵特征,如果与该滤波器进行卷积运算的图像中包含猫耳朵的部分,那么就会提供高激活(矩阵乘法值)。

在前文中,我们学到将一个大小为 2 × 2 的滤波器与一个大小为 4 × 4 的矩阵进行卷积时,得到的输出是 3 × 3 维的。

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

在这里插入图片描述

此外,对于包含三个通道的彩色图像这种情况,与图像进行卷积运算的滤波器也将有三个通道,使得每个卷积只有一个标量输出

在这里插入图片描述

从图 4-4 可以看出,输入图像应乘以与输入数据相同深度的滤波器,卷积运算输出的通道数量与滤波器的数量相同

步长和填充

在上一小节中,每个滤波器都在图像上移动,每次移动一列或一行。无论是在高度还是宽度方面,这都会导致输出尺寸总是比输入图像尺寸小 1 个像素。这可能会导致部分信息的丢失,并影响将卷积运算的输出加到原始图像上的可能性(这通常称为残差相加,将在
下一章详细讨论)。

在本小节中,我们将学习使用步长和填充来影响卷积输出的形状。

步长

在这里插入图片描述
在这里插入图片描述

填充

在这里插入图片描述

池化

在这里插入图片描述
在这里插入图片描述

在实践中,最大池化最被常用。

整合各个构建模块

目前,我们学习了卷积、滤波器和池化,以及它们在图像降维方面的作用。在将已学到的这三个部分放在一起之前,我们先学习 CNN 的另一个关键组成部分,即扁平层(全连接层)

在这里插入图片描述
在这里插入图片描述

CNN模型的整体流程:通过多个滤波器对图像进行卷积计算,然后进行池化处理,最后对池化层的输出进行扁平化处理。这就是图像的特征学习部分

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

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

卷积和池化的图像平移不变性原理

当我们执行池化运算时,可以把该运算的输出看作某个区域(某个小块)的一种抽象。这很有用,尤其是在处理图像平移的时候。

实现 CNN

CNN 是计算机视觉技术的一个基础模块,理解它的工作原理非常重要。虽然我们已经知道 CNN 是由卷积、池化、扁平化和最后的分类层组成,但在本节中,我们将了解 CNN在前向传播过程中完成各种运算的代码。

为了深入理解这一点,首先使用 PyTorch 在一个简单示例上构建 CNN 架构,然后基于Python 从头构建用于产生模型输出的前向传播计算过程。

使用 PyTorch 构建基于 CNN 的架构

import torch
from torch import nn
from torch.utils.data import TensorDataset, Dataset, DataLoader
from torch.optim import SGD, Adam
from torchvision import datasets
from torchsummary import summary
import numpy as np
import matplotlib.pyplot as plt

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

# 使用以下步骤创建数据集
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()
# 缩放输入数据集,通过将输入数据除以最大输入值 8,将数据取值范围限制在 -1 到 +1 之间
X_train /= 8
y_train = torch.tensor([0, 1]).to(device).float()


# 因为输入数据集有两个数据点,每个点的形状是 4×4,有 1 个通道,所以它的形状是(2,1,4,4)。

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

# 定义模型架构:
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 个通道(即有一个大小为 3×3 的滤波器)。
然后,我们使用 nn.MaxPool2d方法实现最大池化,并使用 ReLU(使用 nn.Relu())激活函数。
此后,对数据进行扁平化处理并连接到最后一层,其中每个数据点有一个输出。
因为这里的输出结果是二元分类,所以损失函数是二元交叉熵损失函数(nn.BCELoss())。
我们还指定使用 Adam 优化器进行优化,并且学习率为 0.001。
"""

model, loss_fn, optimizer = get_model()
summary(model, X_train)


运行结果如下:

在这里插入图片描述

我们指定卷积内核(kernel_size)的大小为 3,out_channels的数量为 1(本质上,滤波器的数量为 1),其中初始(输入)通道的数量为 1。因此,对于每个输入图像,将形状为 3×3 的滤波器与形状为 1×4×4 的滤波器进行卷积,可以得到形状为 1×2×2 的输出。因为学习的是 9 个权重参数(3×3)和卷积核的一个偏置,故有 10个参数。关于 MaxPool2d、ReLU 和 Flatten 层,它们没有参数,因为这些层是在卷积层的输出上执行的运算,不涉及权重或偏置。

线性层有两个参数:一个权重和一个偏置,这意味着总共有 12 个参数(10 个来自卷积运算,2 个来自线性层)。

使用深度 CNN 分类图像

目前,我们已经发现传统神经网络对平移图像的预测是错误的。需要解决这个问题,因为在现实场景中,需要使用各种数据增强技术,例如,在前面训练阶段没有使用的图像平移和旋转技术。在本节中,我们将了解 CNN 如何解决在 Fashion-MNIST 数据集上对平移图像的错误预测问题。

Fashion-MNIST 数据集的预处理部分仍然与前一章中的相同,除了重塑(.view) 输入数据时,没有将输入数据扁平化为 28×28=784 维,而是将图像的每个输入数据形状重塑为(1,28,28)(记住,首先在 PyTorch 中指定通道,然后加上它们的高度和宽度):

from torchvision import datasets
from torch.utils.data import Dataset, DataLoader
import torch
import torch.nn as nn
from torch.optim import SGD, Adam
import numpy as np
import matplotlib.pyplot as plt

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

data_folder = '~/data/FMNIST'
fmnist = datasets.FashionMNIST(data_folder, download=True, train=True)

tr_images = fmnist.data
tr_targets = fmnist.targets


class FMNISTDataset(Dataset):
    def __init__(self, x, y):
        x = x.float() / 255
        # -1:它的作用是根据剩余的元素数量来自动确定 batch size 的大小。
        #    换句话说,如果 x 的总元素数量是可被 1 * 28 * 28 整除的,PyTorch 将自动推断出合适的 batch size。
        # 1: 这是输出张量中的通道数(channels)。在这里,它将创建一个具有 1 个通道的张量,表示灰色图像
        #    灰度图像通常被表示为单通道图像,因为每个像素只需要一个数值来表示其灰度级别(从黑到白的程度)。
        #    这个灰度级别通常是一个介于0到255之间的整数,其中0表示黑色,255表示白色,中间的数字表示不同灰度的阶段。
        #    因此,对于灰度图像来说,每个像素只有一个值来描述其亮度,因此只需要一个通道。
        x = x.view(-1, 1, 28, 28)
        # 上一行代码是对每个输入图像进行重构的地方(与前一章中所做的不同),因为
        # 我们向 CNN 提供数据,希望每个输入具有 批大小 × 通道 × 高度 × 宽度 的形状。
        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)


def get_data():
    train = FMNISTDataset(tr_images, tr_targets)
    trn_dl = DataLoader(train, batch_size=32, shuffle=True)
    return trn_dl


# CNN 模型架构的定义如下:
def get_model():
    model = nn.Sequential(
        """
        下面这行代码参数的解释:
        1:输入通道数。在这里,表示输入的图像或特征图的通道数。因为前面提到的灰度图像只有一个通道,所以这里是1。
        64:输出通道数。这是卷积操作后得到的特征图的通道数。这个数字决定了卷积核的数量,也就是说,卷积操作会使用64个
            不同的卷积核对输入进行卷积,每个卷积核产生一个特征图。
        kernel_size=3: 卷积核的大小。在这里,卷积核是一个 3x3 的正方形矩阵。这意味着每个卷积核的尺寸是 3x3。
        """,
        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


"""
为了巩固对 CNN 的理解,下面讨论一下在前面的输出中,参数的数量为什么是这样设置的:
    ❍ 第1层:假设有64个滤波器,核大小为3,则有64×3×3个权重和64×1个偏置项,
总共有 640 个参数。
    ❍ 第 4 层:假设有 128 个滤波器,核大小为 3,则有 128×64×3×3 个权重和 128×1
个偏置项,总共有 73 856 个参数。
    ❍ 第 8 层:假设一个有 3 200 个节点的层连接到另一个有 256 个节点的层,则总共有
3 200×256 个权重和 256 个偏置项,总共有 819 456 个参数。
    ❍ 第 10 层:假设一个有 256 个节点的层连接到一个有 10 个节点的层,则总共有
256×10 个权重和 10 个偏置项,总共有 2 570 个参数。
"""


实现数据增强

在前面的场景中,我们了解了 CNN 如何有助于预测平移图像的类别。虽然这种方法对于 5 个像素的平移量很有效,但是超过 5 个像素的平移很有可能导致错误的类别预测。在本节中,我们将学习在图像大幅度平移的情况下,如何确保正确的类别预测。

在利用数据增强技术提高模型预测准确度之前,先了解一下可以在图像上使用的各种增强技术。

图像增强

在这里插入图片描述

imgaug 包中的 augmenters 类具有执行这些扩展功能的实用程序。下面来学习augmenters 类中提供的各种实用工具,它们用于从给定的图像生成增强图像。其中一些较为突出的增强技术如下:仿射变换、改变亮度、添加噪声

在这里插入图片描述

仿射变换

仿射变换包括图像的平移、旋转、缩放和剪切。

改变亮度

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

如果模型训练时训练图像的背景中一直有一个像素值 0,前景的像素值总是 255,而被预测图像背景的像素值为 20、前景像素值为 220,那么此时的预测结果可能是不正确的。

添加噪声

在现实世界中,我们可能会因为糟糕的摄影条件而遇到图像噪声。Dropout 和SaltAndPepper 是两个重要的方法,有助于模拟图像噪声。

在这里插入图片描述