【CV with Pytorch】第 2 章 :图像分类

上一章讨论了计算机视觉中的几个重要概念。还讨论了计算机视觉领域的一些最佳实践,因此是时候将它们付诸实践了。本章为计算机视觉领域的多种应用定下了基调。我们首先对如何开始使用 Torch 组件构建模型、定义损失函数和训练进行基本说明。

需要通过名称来识别的对象涉及分类过程。我们在涉及分类要求的数据科学的各个方面都遇到过问题。它可以很简单,比如将手机上的图像分类为山还是海,或者是鸟还是狗。分类是最基本但最强大的概念之一。让我们看看计算机视觉模型是如何建立分类的:

  1. 检测边缘

  1. 检测梯度

  1. 识别纹理

  1. 识别模式

  1. 物体的组成部分

模型需要将名称与图像中的特定对象相关联。它通过遵循结构化的知识提取机制,然后为决策过程重新生成输入来做到这一点。

要涵盖的主题

  1. 资料准备方法

  1. 数据扩充技术

  1. 使用批量归一化和丢弃

  1. 比较激活函数

  1. 设置模型及其变体

  1. 训练过程

  1. 运行推理并比较模型结果

定义问题

我们将检查肺部的 X 射线图像,并在计算机视觉建模技术的帮助下将它们分类为患有肺炎或正常。由于这是一个医疗保健问题,因此最好让模型过度预测。我们需要以最高形式的准确度进行预测,并且如果可能的话应该有接近 100% 的召回率,以及高精度得分。我们需要确保诊断出任何可能的感染病例,而不是因为边缘小而将受感染的肺部错误分类为健康。Softmax logits 通常可用于确定预测而不是 softmax 函数决定类别。这是一个基于数据经验和模型行为的关键决策。

掩盖这些图像分类问题的一个主要问题是正确注释数据的可用性。卷积神经网络的图像分类有助于完成多个下游任务。在某些数据上训练的模型可用于微调其他类似数据并用于预测目的。有多个开源图像存储库,但对于大多数工业用途而言,它们为我们提供了一个起点。我们还必须使用特定于任务的图像。

方法概述

我们将使用卷积神经网络来解决分类问题。我们将尝试适应流程的变化并检查更高的准确性和稳定的结果。第1章中学习的概念将在该过程中广泛使用。这完全是一个实验,我们只会为需要迭代的方法设置基线标准。

这种方法包括以下步骤:

  1. 从源头下载数据并将其放置在根目录中。

  1. 检查数据完整性、可配置信息,例如图像的形状、大小和分布。

  1. 初始化用于训练和测试的数据加载器功能。

  1. 定义模型架构并验证它。

  1. 定义训练和测试的功能。

  1. 为训练和其他训练信息定义优化器,例如正则化器、时期、批次等。

  1. 训练和检查损失和准确性模式,以了解架构和模型训练过程的稳定性。

  1. 决定改进或更改的多个阶段,选择哪个阶段进行进一步调整或生产。

图2-1显示了此方法的图形概述,可以将其用于解决方案。

图 2-1 图像分类管道

创建图像分类管道

可以有多种方法来处理一个简单的分类问题。由于我们正在使用可以从空间模式中计算出特征的深度学习模型,因此深入研究网络会有所帮助。我们还需要研究其他策略,如学习率调制和正则化技术来帮助模型。让我们看看在考虑问题的复杂性时可以应用的策略:

  1. 我们可以根据可用数据量和问题的复杂性来检查数据可用性。然后我们可以决定是否必须过度采样。

  1. 我们获得的验证数据和图像数据类型有助于数据增强策略。

  1. 我们需要检查图像大小和图像大小内的对象,以便更好地理解模型架构。

  1. 我们需要围绕模型需要运行的生产基础架构以及我们需要的延迟类型制定策略。

  1. 模型策略也会要求我们弄清楚准确率,是需要更高的召回率还是我们需要更高的准确率。

  1. 在构建模型时,还需要考虑基础设施的培训时间和成本。

我们将尝试四种渐进的方法来处理手头的图像分类问题。我们将逐步增加解决方案的复杂性,以查看各个过程的影响。我们来看第一个策略。

第一基本模型

数据

此用例在肺炎肺部和正常肺部的 X 射线图像数据集上创建图像分类器。我们将下载数据集并将其放在我们的本地目录中,Python 编译器可以访问该目录。如果你使用的是Google Colab,你可以使用 Google Drive 作为存储,可以挂载到 Colab 上。

在这个问题集中,我们使用开源数据,可以在https://www.kaggle.com/tolgadincer/labeled-chest-xray-images找到。

数据集分为测试和训练文件夹,每个文件夹进一步分为正常和肺炎类别。

  • NORMAL类别中的训练样本数为 1349。

  • PNEUMONIA类别中的训练样本数为 3883。

  • NORMAL类别的测试样本数为 234。

  • PNEUMONIA类别中的测试样本数量为 390。

让我们看一下NORMAL文件夹中的示例图像,以检查图像的质量和定位。图2-2显示了来自NORMAL train 文件夹的随机 2283x2120图像。由于这是从 mpimg 生成的,因此在 Jupyter notebook 中显示时的颜色不同。您还可以使用另一个命令cv2.imshow()代替它来显示图像。

图 2-2 来自正常肺部训练数据的示例图像

现在让我们看一下PNEUMONIA train 文件夹中的示例图像。图2-3显示了此类的 776x1224 图像。

图 2-3 感染肺部的样本图像

让我们从基本的import开始编码。这些是整个流程工作所必需的。GPU 更适合更快的训练过程,但 CPU 也应该可以。

我们需要安装具有 CUDA 支持的 PyTorch,以防我们在训练过程中使用本地 CUDA 内核。我们需要小心所有放在CUDA中处理的对象和所有放在CPU中处理的对象。除非特别指定,否则不支持跨不同处理器类型的数据混合。

我们需要为这些分类问题导入一些自定义库。让我们按顺序招募他们。

import os
import numpy as np
import cv2
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
%matplotlib inline
from PIL import Image
from IPython.display import display
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
import torch.nn.functional as F
from torchvision import datasets, transforms, models
from torch.optim.lr_scheduler import StepLR
from torchsummary import summary
from tqdm import tqdm

导入所有必需的库后,我们可以开始从目录链接数据。我们首先将文件解压缩到文件夹中。如果我们在这个过程中使用Google Colab,我们可以使用以下命令将 Google Drive 挂载到 Colab 并使用存储在那里的数据。

from google.colab import drive
drive.mount('/content/gdrive')
!unzip <zipped file location>

这会将数据带到 Colab 位置,因此模型可以轻松使用它。在此之后,我们设置数据目录的数据路径,而不考虑我们将使用的系统。

data_path = '/content/chest_xray'

数据探索

我们现在将探索并检查数据的完整性。我们必须分配可以在模型中使用的训练和测试文件夹。在图像分类中,没有特定的图像注释。如果图像按文件夹分隔,我们可以使用文件夹名称作为类名称。可以有另一种变体,我们可以看到一个文件夹中的所有图像,然后指定哪个图像路径属于哪个类。

class_name = ['NORMAL','PNEUMONIA']
def get_list_files(dirName):
    '''
    input - 目录位置
    output - 列出目录中的文件
    '''
    files_list = os.listdir(dirName)
    return files_list
files_list_normal_train = get_list_files(data_path+'/train/'+class_name[0])
files_list_pneu_train = get_list_files(data_path+'/train/'+class_name[1])
files_list_normal_test = get_list_files(data_path+'/test/'+class_name[0])
files_list_pneu_test = get_list_files(data_path+'/test/'+class_name[1])

我们将类名硬编码为NORMAL和PNEUMONIA,因为文件夹是以这种方式排列的。

print("Number of train samples in Normal category {}".format(len(files_list_normal_train)))
print("Number of train samples in Pneumonia category {}".format(len(files_list_pneu_train)))
print("Number of test samples in Normal category {}".format(len(files_list_normal_test)))
print("Number of test samples in Pneumonia category {}".format(len(files_list_pneu_test)))

输出:

Number of train samples in Normal category 1349

Number of train samples in Pneumonia category 3883

Number of test samples in Normal category 234

Number of test samples in Pneumonia category 390

现在我们已经提取了图像并找到了路径,让我们看看如何查看NORMAL和PNEUMONIA 文件夹中的示例图像。

rand_img_no = np.random.randint(0,len(files_list_normal_train))
img = data_path + '/train/NORMAL/'+ files_list_normal_train[rand_img_no]
print(plt.imread(img).shape)
#display(Image.open(img,'r'),)
img = mpimg.imread(img)
imgplot = plt.imshow(img)
plt.show()

此处的输出是图2-2中所示的图像。

img = data_path + '/train/PNEUMONIA/'+ files_list_pneu_train[np.random.randint(0,len(files_list_pneu_train))]
print(plt.imread(img).shape)
img = mpimg.imread(img)
imgplot = plt.imshow(img)
plt.show()

这种情况下的输出是如图2-3所示的图像。

数据加载器

由于我们已经探索了数据,现在让我们为训练目的设置数据加载器。在这个变体中,我们不会使用增强来帮助训练正则化。我们将调整大小并将图像裁剪为统一的 224x224 大小。图像的这个起点不是一成不变的。如果你愿意,你可以使用不同的尺寸。

除了图像的大小和裁剪之外,我们还将着眼于将图像转换为 PyTorch 框架的张量。我们将尝试使用均值和标准差值对图像进行归一化。如果我们考虑每个图像三个通道,那么我们需要为一个通道提供三个值。我们需要均值和标准差的一种组合。

这是代码:

train_transform = transforms.Compose([
    transforms.Resize(224),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406],
                          [0.229, 0.224, 0.225])
test_transform = transforms.Compose([
    transforms.Resize(224),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406],
                          [0.229, 0.224, 0.225])])
train_data = datasets.ImageFolder(os.path.join(data_path, 'train'), transform= train_transform)
test_data = datasets.ImageFolder(os.path.join(data_path, 'test'), transform= test_transform)
train_loader = DataLoader(train_data,
                          batch_size= 16, shuffle= True, pin_memory= True)
test_loader = DataLoader(test_data,
                         batch_size= 1, shuffle= False, pin_memory= True)
class_names = train_data.classes
print(class_names)
print(f'Number of train images: {len(train_data)}')
print(f'Number of test images: {len(test_data)}')

输出:

['NORMAL', 'PNEUMONIA']

Training images available: 5232

Testing images available: 624

我们正在使用 PyTorch 的默认数据加载器。我们将创建两组数据加载器,一组用于训练数据集,另一组用于测试集。批大小在每种情况下都是可变的,具体取决于系统的 GPU 和 RAM。我们可以打乱训练数据,因为不需要特定的顺序。在测试数据的情况下,需要关闭shuffle。

如果有人需要将先前加载到 CPU 中的数据集传输到 GPU,则 pin 内存参数会有所帮助。启用引脚存储器后,该过程会更快。

我们正在使用数据加载器来转换数据中的功能,然后在训练功能中使用它们。image文件夹一般用在图片按照文件夹中类名排列的时候。

定义模型

我们将使用卷积块定义我们的模型架构,并使用 ReLU 作为激活层。基线模型将有 12 个卷积块,包括一个用于设置输入的卷积块和一个用于输出的卷积块。前三个卷积块有一个最大池化函数,通过过滤信息从图像的高维度下降到低维度。

模型定义如下:

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        # Input Block
        self.convblock1 = nn.Sequential(
            nn.Conv2d(in_channels=3, out_channels=8, kernel_size=(3, 3),
                      padding=0, bias=False),
            nn.ReLU(),
            #nn.BatchNorm2d(4)
        )
        self.pool11 = nn.MaxPool2d(2, 2)
        # CONVOLUTION BLOCK
        self.convblock2 = nn.Sequential(
            nn.Conv2d(in_channels=8, out_channels=16, kernel_size=(3, 3),
                      padding=0, bias=False),
            nn.ReLU(),
            #nn.BatchNorm2d(16)
        )
        # TRANSITION BLOCK
        self.pool22 = nn.MaxPool2d(2, 2)
        self.convblock3 = nn.Sequential(
            nn.Conv2d(in_channels=16, out_channels=10, kernel_size=(1, 1), padding=0, bias=False),
            #nn.BatchNorm2d(10),
            nn.ReLU()
        )
        self.pool33 = nn.MaxPool2d(2, 2)
        # CONVOLUTION BLOCK
        self.convblock4 = nn.Sequential(
            nn.Conv2d(in_channels=10, out_channels=10, kernel_size=(3, 3), padding=0, bias=False),
            nn.ReLU(),
            #nn.BatchNorm2d(10)
        )
        self.convblock5 = nn.Sequential(
            nn.Conv2d(in_channels=10, out_channels=32, kernel_size=(1, 1), padding=0, bias=False),
            #nn.BatchNorm2d(32),
            nn.ReLU(),
        )
        self.convblock6 = nn.Sequential(
            nn.Conv2d(in_channels=32, out_channels=10, kernel_size=(1, 1), padding=0, bias=False),
            nn.ReLU(),
            #nn.BatchNorm2d(10),
        )
        self.convblock7 = nn.Sequential(
            nn.Conv2d(in_channels=10, out_channels=10, kernel_size=(3, 3), padding=0, bias=False),
            nn.ReLU(),
            #nn.BatchNorm2d(10)
        )
        self.convblock8 = nn.Sequential(
            nn.Conv2d(in_channels=10, out_channels=32, kernel_size=(1, 1), padding=0, bias=False),
            #nn.BatchNorm2d(32),
            nn.ReLU()
        )
        self.convblock9 = nn.Sequential(
            nn.Conv2d(in_channels=32, out_channels=10, kernel_size=(1, 1), padding=0, bias=False),
            nn.ReLU(),
            #nn.BatchNorm2d(10),
        )
        self.convblock10 = nn.Sequential(
            nn.Conv2d(in_channels=10, out_channels=14, kernel_size=(3, 3), padding=0, bias=False),
            nn.ReLU(),
            #nn.BatchNorm2d(14),
        )
        self.convblock11 = nn.Sequential(
            nn.Conv2d(in_channels=14, out_channels=16, kernel_size=(3, 3), padding=0, bias=False),
            nn.ReLU(),
            #nn.BatchNorm2d(16),
        )
        # OUTPUT BLOCK
        self.gap = nn.Sequential(
            nn.AvgPool2d(kernel_size=4)
        )
        self.convblockout = nn.Sequential(
              nn.Conv2d(in_channels=16, out_channels=2, kernel_size=(4, 4), padding=0, bias=False),
        )
    def forward(self, x):
        x = self.convblock1(x)
        x = self.pool11(x)
        x = self.convblock2(x)
        x = self.pool22(x)
        x = self.convblock3(x)
        x = self.pool33(x)
        x = self.convblock4(x)
        x = self.convblock5(x)
        x = self.convblock6(x)
        x = self.convblock7(x)
        x = self.convblock8(x)
        x = self.convblock9(x)
        x = self.convblock10(x)
        x = self.convblock11(x)
        x = self.gap(x)
        x = self.convblockout(x)
        x = x.view(-1, 2)
        return F.log_softmax(x, dim=-1)

在这种方法中,我们正在创建一个Net类,它使用 Python超级 功能启用多重继承选项。我们从输入卷积块开始;输入通道的数量设置为 3,输出通道设置为 8。这些参数可以调整,但应该与体系结构和核心可用性保持一致。我们使用具有 3x3 卷积的内核,因为如前所述,它是最有效的卷积方式之一。在几个块中,我们还可以看到一个 1x1,它通过提出所有特征图的组合来帮助减少 z 方向上的特征图。

下面是模型的详细解释:

  1. 该模型的输入块接收三通道 224x224 输入,并使用 3x3 卷积生成 222x222 和八个通道。接下来是 ReLU 激活层。我们没有为这个模型架构使用填充。

  1. 输入后,我们调用最大池化函数将特征图大小减小到 111x111。

  1. 池化函数作用于特征图后,我们将特征图进行 3x3 卷积,从 8 个通道生成 16 个通道,并将特征图维度减小到109x109

  1. 一旦我们使用卷积块获得 16 个通道,我们再次使用最大池化函数将特征图维度变为 54x54。

  1. 然后我们使用一个转换块(在网络中第一次)将通道数从 16 减少到 10,然后是另一个最大池化函数。

  1. 一旦我们使用完最大池并且特征图维度现在是 27x27,我们使用 3x3 内核进行卷积并创建相同数量的特征图。

  1. 第五个和第六个卷积块是过渡层,其中我们将层数从 10 增加到 32,然后再增加到 10。像往常一样没有填充。

  1. 第七个卷积块用于 3x3 卷积运算,但通道大小再次保持不变。

  1. 我们在第八和第九个卷积块中有类似的操作。使用转换卷积运算,我们将通道从 10 移动到 32,然后再次回到 10。

  1. 我们添加了一个 3x3 卷积块,排在第十位。我们将特征图的数量从 10 个增加到 14 个。

  1. 该架构的倒数第二个构建块使用 3x3 内核将通道数从 14 增加到 16。

  1. 在输出块中,我们使用平均池从 19x19 引入二四单元。这可以用于二进制分类。在我们的平均池之后,我们使用与特征图维度相同的内核大小的卷积块将其带入单个输出单元。

  1. 最后,我们使用对数softmax函数来生成输出。它是一个缩放输出,我们使用argmax 函数来确定每个批次元素的类别。

出于此架构设计的目的,我们将添加的偏差设置为 false。这意味着没有偏差被添加到网络中形成的所有神经组件的计算中。然而,我们可以尝试偏见。在大多数地方,只要数据居中和规范化,它应该不会对网络产生太大影响。

让我们看看模型的签名作为摘要功能的输出。如果 GPU 可供处理,我们还可以将模型放入 GPU 中。

use_cuda = torch.cuda.is_available()
device = torch.device("cuda" if use_cuda else "cpu")
print("Available processor {}".format(device))
model = Net().to(device)
summary(model, input_size=(3, 224, 224))
Available processor cuda
---------------------------------------------------------------
        Layer (type)               Output Shape         Param #
===============================================================
            Conv2d-1          [-1, 8, 222, 222]             216
              ReLU-2          [-1, 8, 222, 222]               0
         MaxPool2d-3          [-1, 8, 111, 111]               0
            Conv2d-4         [-1, 16, 109, 109]           1,152
              ReLU-5         [-1, 16, 109, 109]               0
         MaxPool2d-6           [-1, 16, 54, 54]               0
            Conv2d-7           [-1, 10, 54, 54]             160
              ReLU-8           [-1, 10, 54, 54]               0
         MaxPool2d-9           [-1, 10, 27, 27]               0
           Conv2d-10           [-1, 10, 25, 25]             900
             ReLU-11           [-1, 10, 25, 25]               0
           Conv2d-12           [-1, 32, 25, 25]             320
             ReLU-13           [-1, 32, 25, 25]               0
           Conv2d-14           [-1, 10, 25, 25]             320
             ReLU-15           [-1, 10, 25, 25]               0
           Conv2d-16           [-1, 10, 23, 23]             900
             ReLU-17           [-1, 10, 23, 23]               0
           Conv2d-18           [-1, 32, 23, 23]             320
             ReLU-19           [-1, 32, 23, 23]               0
           Conv2d-20           [-1, 10, 23, 23]             320
             ReLU-21           [-1, 10, 23, 23]               0
           Conv2d-22           [-1, 14, 21, 21]           1,260
             ReLU-23           [-1, 14, 21, 21]               0
           Conv2d-24           [-1, 16, 19, 19]           2,016
             ReLU-25           [-1, 16, 19, 19]               0
        AvgPool2d-26             [-1, 16, 4, 4]               0
           Conv2d-27              [-1, 2, 1, 1]             512
===============================================================
Total params: 8,396
Trainable params: 8,396
Non-trainable params: 0
---------------------------------------------------------------
Input size (MB): 0.57
Forward/backward pass size (MB): 11.63
Params size (MB): 0.03
Estimated Total Size (MB): 12.23
---------------------------------------------------------------

这是从模型设计创建的模型的摘要。我们通过给出模型期望的输入维度并在过程中验证它们来计算工作流。

我们需要注意模型的可训练和不可训练参数,这将是我们训练以及进入生产基础设施的权重的一个因素。模型大小为 11.63MB。

训练过程

在定义了模型和数据加载器之后,我们开始进行训练。培训过程将包括以下重要过程:

  1. 初始化模型工作流程的梯度。

  1. 给定模型的当前权重,从当前模型或前向传递中获取预测。最初,使用 Xavier 或 He 初始化从分布中随机分配权重。(对于 ReLU 激活网络,使用 He,而对于 sigmoid,使用 Xavier。)

  1. 前向传播完成后,将计算损失,衡量预测与实际值之间的差距。

  1. 然后我们计算给定累积损失的反向传播。

  1. 反向传播损失计算完成后,我们将进入优化器步骤,该步骤将使用学习率和其他参数来刷新和更新模型的权重。

下面是为训练和测试准备数据的代码:

train_losses = []
test_losses = []
train_acc = []
test_acc = []
def train(model, device, train_loader, optimizer, epoch):
    model.train()
    pbar = tqdm(train_loader)
    correct = 0
    processed = 0
    for batch_idx, (data, target) in enumerate(pbar):
        # 获取数据
        data, target = data.to(device), target.to(device)
        # 梯度的初始化
        optimizer.zero_grad()
        #  在 PyTorch 中,梯度是通过反向传播累积的,即使在 RNN 中使用的梯度通常不在 CNN 中使用
        # 或具体要求
        ## 数据预测
        y_pred = model(data)
        # 根据预测计算损失
        loss = F.nll_loss(y_pred, target)
        train_losses.append(loss)
        # 反向传播
        loss.backward()
        optimizer.step()
        # 获取对应于最大值的对数概率的索引
        pred = y_pred.argmax(dim=1, keepdim=True)
        correct += pred.eq(target.view_as(pred)).sum().item()
        processed += len(data)
        pbar.set_description(desc= f'Loss={loss.item()} Batch_id={batch_idx} Accuracy={100*correct/processed:0.2f}')
        train_acc.append(100*correct/processed)
def test(model, device, test_loader):
    model.eval()
    test_loss = 0
    correct = 0
    with torch.no_grad():
        for data, target in test_loader:
            data, target = data.to(device), target.to(device)
            output = model(data)
            test_loss += F.nll_loss(output, target, reduction='sum').item()
            pred = output.argmax(dim=1, keepdim=True)
            correct += pred.eq(target.view_as(pred)).sum().item()
    test_loss /= len(test_loader.dataset)
    test_losses.append(test_loss)
    print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.2f}%)\n'.format(
            test_loss, correct, len(test_loader.dataset),
            100. * correct / len(test_loader.dataset)))
    test_acc.append(100. * correct / len(test_loader.dataset))

该代码块实质上创建了两个可用于训练目的的函数,并根据它在测试数据上的工作效率来评估模型。代码块还在训练过程中从训练和测试数据中创建两组准确度和两组损失。这有助于确定模型的行为方式并衡量其稳健性。

让我们通过代码块并解密步骤:

1.train 函数设置训练模型。

2.如果模型在 GPU 中,我们将数据放在 GPU 中,如果模型在 CPU 中,我们将数据放在 CPU 中。初始化设备可确保数据和模型在训练期间位于同一设备上。

3.每次新批次进入时,我们都将梯度设置为 0,因为 PyTorch 默认情况下会尝试累积梯度,这对卷积神经网络不利。梯度累积过程可用于基于时间的模型和架构。

4.我们使用数据加载器生成批量图像并将它们传递给模型进行训练。

5.我们从前向传递中计算预测并将其放入变量中。完成后,我们将计算由于模型预测而造成的损失。

6.计算出的损失有助于反向传播,并帮助优化器根据最陡上升/下降的方向更新模型的权重。

7.预测类是根据对数softmax函数计算的,方法是取索引的最大值并相应地计算值。

8.为了计算测试精度和损失,我们执行相同的过程,但对模型进行评估。

9.我们在计算测试样本的损失时不更新权重。

现在我们已经计算了损失计算、反向传播和权重的函数,我们可以启动优化器和调度器开始训练。下面是训练过程的代码:

model =  Net().to(device)
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
scheduler = StepLR(optimizer, step_size=6, gamma=0.5)
EPOCHS = 15
for epoch in range(EPOCHS):
    print("EPOCH:", epoch)
    train(model, device, train_loader, optimizer, epoch)
    scheduler.step()
    print('current Learning Rate: ', optimizer.state_dict()["param_groups"][0]["lr"])
    test(model, device, test_loader)

我们使用具有动量的随机梯度优化器来适应模型。我们还使用调度来定期更改优化器的学习率。这可以间接帮助加快收敛速度。epoch 的数量还取决于我们想要如何训练模型,以及计算时间是否适合我们的目的。我们将在停止训练过程之前查看损失函数的饱和度。

查看图2-4中显示的输出片段。

图 2-4 输出外观的快照

代码块的输出将生成训练信息,例如训练和测试损失,并显示准确性。我们注意到最高的准确度,最终只想在那个时候保存模型权重。

这种方法可能不会给我们最好的结果,但它已经建立了我们将用来逐步获得更好准确性的工作流程。在这个模型中,我们在测试数据集中得到的准确度非常低,只有 38%。让我们分析测试和训练数据的损失模式来找出问题所在。

产生损失可视化的代码片段如下:

train_losses1 = [float(i.cpu().detach().numpy()) for i in train_losses]
train_acc1 = [i for i in train_accuracies]
test_losses1 = [i for i in test_losses]
test_acc1 = [i for i in test_accuracies]
fig, axs = plt.subplots(2,2,figsize=(16,10))
axs[0, 0].plot(train_losses1,color='green')
axs[0, 0].set_title("Training Loss")
axs[1, 0].plot(train_acc1,color=’green’)
axs[1, 0].set_title("Training Accuracy")
axs[0, 1].plot(test_losses1)
axs[0, 1].set_title("Test Loss")
axs[1, 1].plot(test_acc1)
axs[1, 1].set_title("Test Accuracy")

输出如图2-5所示。

图 2-5 训练后的预期输出

从图2-5中,我们可以看到,尽管测试准确度随着时间的推移而增加,并且损失不断减少,但仍未达到理想状态。训练损失表明该模型在实时情况下非常不稳定。是时候重新考虑这种方法并在此工作流程之上构建了。

模型的第二个变体

让我们从扩充数据开始,看看准确性是否有变化。有多个增强过程;我们应该选择最符合我们业务需求的。我们需要小心,因为太多的增强会对优化产生影响。

让我们尝试一些基本的增强方法,例如:

  • 使用颜色抖动来扩充训练数据。

  • 随机翻转训练数据。

  • 随机旋转训练样本。

我们不会遍历整个代码示例,因为我们保持大部分工作流程不变,只更改选定的部分。

图 2-6 图像分类流水线通过增强更新

到目前为止,我们已经实现了图2-6中绿色的块。在这个变体中,我们专注于以蓝色突出显示的块(思考增强),我们有意将其分开。这将帮助我们衡量增强技术的影响。

让我们看一下扩充代码块:

train_transform = transforms.Compose([
    transforms.Resize(224),
    transforms.CenterCrop(224),
    transforms.ColorJitter(brightness=0.10, contrast=0.1, saturation=0.10, hue=0.1),
    transforms.RandomHorizontalFlip(),
    transforms.RandomRotation(10),
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406],
                          [0.229, 0.224, 0.225])
])
test_transform = transforms.Compose([
    transforms.Resize(224),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406],
                          [0.229, 0.224, 0.225])
])

扩充过程包含在组合函数本身中,因此我们不需要更改代码的数据加载器部分。我们将重用组合函数中的确切代码过程,并在迭代中运行它。

通过数据扩充,我们在第五个 epoch 达到了 80% 的准确率,但随后在训练过程中准确率下降了。这显示了训练过程的不稳定性。损失有一个高峰。训练精度图也显示了这些波动。这是由于数据的增加和模型无法有效地找出变化。

图 2-7 增强训练管道的输出

2-7显示这些高精度下降到 42% 左右。当我们将此工作流的准确性与早期版本的结果进行比较时,我们发现工作流在数据增强的情况下效果更好。

饱和度的准确性仍然很低,并且模型行为在波动的情况下不能被认为是稳定的。我们需要做进一步的改变,寻找更好更稳定的模式。

模型的第三种变化

在本节中,我们考虑已建立的工作流程并对其进行修改。该模型架构运行有 11 个卷积块和三个最大池化层。层中的分布变化可能存在变化,也称为内部协变量偏移。我们现在可以尝试在网络架构中应用批量归一化。

批量归一化,如前一章所述,调制层内传递输入的分布。分布的变化对其之前的所有层都有级联效应。

我们将尝试在每个层定义之后以及块中的所有通道中使用批量归一化。如果我们有一个具有 16 个输出通道的卷积块,这意味着我们需要对所有 16 个通道进行批量归一化。至此我们已经完成了计算机视觉分类器模型的所有功能,如图2-8所示。我们将在这个工作流程的顶部添加批量归一化,并保留上一次迭代的所有内容。图中的绿色块已经完成;我们将合并蓝色块更改(批量归一化)。

图 2-8 具有批量归一化的图像分类管道

让我们看看这个模型的代码块:

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        # Input Block
        self.convblock1 = nn.Sequential(
            nn.Conv2d(in_channels=3, out_channels=8, kernel_size=(3, 3),
                      padding=0, bias=False),
            nn.ReLU(),
            nn.BatchNorm2d(8)
        )
        self.pool11 = nn.MaxPool2d(2, 2)
        # CONVOLUTION BLOCK 1
        self.convblock2 = nn.Sequential(
            nn.Conv2d(in_channels=8, out_channels=16, kernel_size=(3, 3),
                      padding=0, bias=False),
            nn.ReLU(),
            nn.BatchNorm2d(16)
        )
        self.pool22 = nn.MaxPool2d(2, 2)
        self.convblock3 = nn.Sequential(
            nn.Conv2d(in_channels=16, out_channels=10, kernel_size=(1, 1), padding=0, bias=False),
            nn.ReLU(),
            nn.BatchNorm2d(10),
        )
        self.pool33 = nn.MaxPool2d(2, 2)
        # CONVOLUTION BLOCK 2
        self.convblock4 = nn.Sequential(
            nn.Conv2d(in_channels=10, out_channels=10, kernel_size=(3, 3), padding=0, bias=False),
            nn.ReLU(),
            nn.BatchNorm2d(10)
        )
        self.convblock5 = nn.Sequential(
            nn.Conv2d(in_channels=10, out_channels=32, kernel_size=(1, 1), padding=0, bias=False),
            nn.ReLU(),
            nn.BatchNorm2d(32),
        )
        self.convblock6 = nn.Sequential(
            nn.Conv2d(in_channels=32, out_channels=10, kernel_size=(1, 1), padding=0, bias=False),
            nn.ReLU(),
            nn.BatchNorm2d(10),
        )
        self.convblock7 = nn.Sequential(
            nn.Conv2d(in_channels=10, out_channels=10, kernel_size=(3, 3), padding=0, bias=False),
            nn.ReLU(),
            nn.BatchNorm2d(10)
        )
        self.convblock8 = nn.Sequential(
            nn.Conv2d(in_channels=10, out_channels=32, kernel_size=(1, 1), padding=0, bias=False),
            nn.ReLU(),
            nn.BatchNorm2d(32)
        )
        self.convblock9 = nn.Sequential(
            nn.Conv2d(in_channels=32, out_channels=10, kernel_size=(1, 1), padding=0, bias=False),
            nn.ReLU(),
            nn.BatchNorm2d(10)
        )
        self.convblock10 = nn.Sequential(
            nn.Conv2d(in_channels=10, out_channels=14, kernel_size=(3, 3), padding=0, bias=False),
            nn.ReLU(),
            nn.BatchNorm2d(14)
        )
        self.convblock11 = nn.Sequential(
            nn.Conv2d(in_channels=14, out_channels=16, kernel_size=(3, 3), padding=0, bias=False),
            nn.ReLU(),
            nn.BatchNorm2d(16)
        )
        # OUTPUT BLOCK
        self.gap = nn.Sequential(
            nn.AvgPool2d(kernel_size=4)
        )
        self.convblockout = nn.Sequential(
              nn.Conv2d(in_channels=16, out_channels=2, kernel_size=(4, 4), padding=0, bias=False),
        )
    def forward(self, x):
        x = self.convblock1(x)
        x = self.pool11(x)
        x = self.convblock2(x)
        x = self.pool22(x)
        x = self.convblock3(x)
        x = self.pool33(x)
        x = self.convblock4(x)
        x = self.convblock5(x)
        x = self.convblock6(x)
        x = self.convblock7(x)
        x = self.convblock8(x)
        x = self.convblock9(x)
        x = self.convblock10(x)
        x = self.convblock11(x)
        x = self.gap(x)
        x = self.convblockout(x)
        x = x.view(-1, 2)
        return F.log_softmax(x, dim=-1)

一旦我们将批量归一化添加到模型块中,准确度就会上升。测试准确率在第 10 个 epoch 达到最大值,接近 90%。之后,准确率保持在 85% 左右,直到第 15 个纪元。这是能够理解类之间差异的巨大改进。

还有一件事要检查。对于相同的处理器,我们预计每个 epoch 的时间消耗会更高,但考虑到它移交的准确性增加量,它不应该大到足以造成麻烦。让我们看一下 torch.summary 函数所描述的模型定义。

---------------------------------------------------------------
        Layer (type)               Output Shape         Param #
===============================================================
            Conv2d-1          [-1, 8, 222, 222]             216
              ReLU-2          [-1, 8, 222, 222]               0
       BatchNorm2d-3          [-1, 8, 222, 222]              16
         MaxPool2d-4          [-1, 8, 111, 111]               0
            Conv2d-5         [-1, 16, 109, 109]           1,152
              ReLU-6         [-1, 16, 109, 109]               0
       BatchNorm2d-7         [-1, 16, 109, 109]              32
         MaxPool2d-8           [-1, 16, 54, 54]               0
            Conv2d-9           [-1, 10, 54, 54]             160
             ReLU-10           [-1, 10, 54, 54]               0
      BatchNorm2d-11           [-1, 10, 54, 54]              20
        MaxPool2d-12           [-1, 10, 27, 27]               0
           Conv2d-13           [-1, 10, 25, 25]             900
             ReLU-14           [-1, 10, 25, 25]               0
      BatchNorm2d-15           [-1, 10, 25, 25]              20
           Conv2d-16           [-1, 32, 25, 25]             320
             ReLU-17           [-1, 32, 25, 25]               0
      BatchNorm2d-18           [-1, 32, 25, 25]              64
           Conv2d-19           [-1, 10, 25, 25]             320
             ReLU-20           [-1, 10, 25, 25]               0
      BatchNorm2d-21           [-1, 10, 25, 25]              20
           Conv2d-22           [-1, 10, 23, 23]             900
             ReLU-23           [-1, 10, 23, 23]               0
      BatchNorm2d-24           [-1, 10, 23, 23]              20
           Conv2d-25           [-1, 32, 23, 23]             320
             ReLU-26           [-1, 32, 23, 23]               0
      BatchNorm2d-27           [-1, 32, 23, 23]              64
           Conv2d-28           [-1, 10, 23, 23]             320
             ReLU-29           [-1, 10, 23, 23]               0
      BatchNorm2d-30           [-1, 10, 23, 23]              20
           Conv2d-31           [-1, 14, 21, 21]           1,260
             ReLU-32           [-1, 14, 21, 21]               0
      BatchNorm2d-33           [-1, 14, 21, 21]              28
           Conv2d-34           [-1, 16, 19, 19]           2,016
             ReLU-35           [-1, 16, 19, 19]               0
      BatchNorm2d-36           [-1, 16, 19, 19]              32
        AvgPool2d-37             [-1, 16, 4, 4]               0
           Conv2d-38              [-1, 2, 1, 1]             512
===============================================================
Total params: 8,732
Trainable params: 8,732
Non-trainable params: 0
---------------------------------------------------------------
Input size (MB): 0.57
Forward/backward pass size (MB): 16.86
Params size (MB): 0.03
Estimated Total Size (MB): 17.46
---------------------------------------------------------------

我们可以看到,添加批量归一化后,模型大小从 12.23MB 增加到 17.46MB。可训练参数的数量也从 8396 增加到 8732。从我们对批量归一化概念的理解,我们可以看到每个通道将增加两个可训练参数。模型的最后一层不应有任何批量归一化或丢失。

图 2-9 批量归一化流水线的输出

2-9显示了模型训练过程的准确性和损失。我们可以从训练损失图中看出,与我们未应用批量归一化的版本相比,波动已显着减少。测试的损失稳定在 0.35 到 0.60 之间。这种损失在一定程度上似乎是稳定的,但我们可以尝试另一种方法来进一步稳定它。

让我们转向我们方法的最后一个补充并检查改进。

模型的第四个变体

我们现在将尝试应用正则化并查看测试和训练数据集之间的损失差异。我们从基础模型开始,然后向训练集添加扩充。扩充后,我们尝试在更改之上运行批量归一化并获得了良好的结果。对于这个变体,我们使用与第三个变体相同的工作流程并将正则化器附加到它。

2-10显示了工作流,其中已完成的任务为绿色,待处理任务(正则化)为蓝色。

图 2-10 具有正则化的图像分类管道

在计算完损失后,正则化器的代码块需要放在训练函数中。

loss = F.nll_loss(y_pred, target)
        l1 = 0
        for p in model.parameters():
            l1=l1+p.abs().sum()
        loss = loss+lambda1*l1

代码块描述了我们如何将正则化器参数附加到训练函数并进行训练。让我们分析一下这种方法的结果。

2-10显示了训练损失和测试损失的模式。如果我们将其与图2-11进行比较,我们会发现损失波动较小。该模式在测试损失的第四个时期有一个主要的偏转,但除此之外,它是稳定的。我们可以根据需要更改正则化强度并进行试验。

图 2-11 使用正则化的分类管道输出

概括

我们通过定义基本模型和对数据运行迭代来开始本章。我们介绍了一些基本的增强技术,这些技术有助于创建与测试将来可能带来的类似分布。这有助于构建和训练稳健的模型。我们探索了归一化和正则化以提高模型的准确性和稳定性。

在下一章中,我们将研究基于本章所学概念的对象检测框架。图像分类网络构成了各种对象检测网络的基础。

  • 7
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 10
    评论
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Sonhhxg_柒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值