【youcans动手学模型】AlexNet模型CIFAR10图像分类

欢迎关注『youcans动手学模型』系列
本专栏内容和资源同步到 GitHub/youcans



本文用 PyTorch 实现 AlexNet 网络模型,使用 CIFAR10 数据集训练模型,进行图像分类。


1. AlexNet 卷积神经网络模型

Alex Krizhevsky, Geoffrey Hinton(2018年获得图灵奖)等在 2012 年发表论文“ImageNet Classification with Deep Convolutional Neural Networks”,开启了深度学习研究热潮。本文是卷积神经网络的经典之作,在 2012 年 ImageNet 视觉识别挑战赛中以巨大优势获得冠军,该网络模型也被命名为 AlexNet 模型。

论文下载地址:下载1下载2

在这里插入图片描述


1.1 论文简介

本文训练了一个深度卷积神经网络,用于 ImageNet 数据集 120万张图像/1000 类别的图像分类。在测试数据上,实现了 37.5% 的 Top-1 错误率和17.0% 的 Top-5 错误率,这比以前的结果要好得多。改进模型在 ILSVRC-2012 竞赛中获得了 15.3% 的 Top-5 错误率,而亚军的成绩是 26.2%。

AlexNet 网络有 6000 万个参数和 65万个神经元,包括 5 个卷积层,其中一些卷积层之后是最大池化层,以及 3 个全连接层,最后是 1000 个输出的 softmax。为了使训练更快,使用了 ReLU 激活函数和 GPU 实现的高效卷积运算。为了减少过拟合,对全连接层采用了“dropout”的正则化方法,该方法非常有效。

利用 GPU 强大的并行计算能力,使用 CUDA 处理大量的矩阵计算,加速深度卷积网络的训练。使用了两块 GTX-580 GPU进行训练,GPU之间的通信只在网络的某些层进行,控制了通信的性能损耗。

在这里插入图片描述

本文的研究表明,使用监督学习方法训练大型深度卷积神经网络,在 ImageNet 数据集上取得了破记录的性能。

值得注意的是,减少卷积层将会使网络性能下降。例如,去除任何中间层都会导致网络的 Top-1 性能降低约 2%。因此,深度对于提高模型性能确实很重要。

我们使用更大的模型网络,训练时间也更长,结果也有所改善,但还有很多工作要做。我们希望在视频序列上使用非常大和非常深的卷积网络,其中的时间结构提供了很有用的信息。


1.2 AlexNet 的主要贡献

AlexNet 模型比 LeNet-5 模型更大更深,但并没有本质上的差别,总体结构都是用卷积层提取特征、全连接层作为分类器。

AlexNet 模型提出的改进技术主要是:

(1)使用 ReLU 激活函数, 可以显著提高训练速度,有效解决梯度消失问题。
(2)使用局部响应归一化(Local Response Normalization),降低了错误率。
(3)使用重叠池化(Overlapping Pooling),池化层卷积核的步长小于卷积核的大小,降低了错误率。
(4)使用多个 GPU 进行训练,降低了错误率,提高了训练速度。 在两块 GTX580/3GB 的 GPU 上的训练时间为 5~6天。
(5)使用数据增强(Data Augmentation),对图像进行裁剪、翻转、调节亮度来扩充数据集,用于防止过拟合。
(6)使用随机失活(Dropout)方法,让神经元以一定的概率失活(只与前一层的部分神经元连接), 用于简化网络、防止过拟合 。
(7)使用随机梯度下降法(SGD)作为参数优化算法。


1.3 AlexNet 网络

在这里插入图片描述

AlexNet 网络具有了 5个卷积层、2个池化层和 3个全连接层。

输入层:3×227×227 的三通道图像。

卷积层 C1:卷积 → ReLU → 最大池化

卷积:输入大小 227×227×3,96 个 11×11×3 的卷积核,不扩充边缘 padding=0,步长 stride=4,特征图大小为 (227-11+0×2+4)/4=55,输出大小为 55×55×96;
激活函数:ReLU;
池化:池化核大小 3×3,不扩充边缘 padding=0,步长 stride=2,特征图大小为 (55-3+0×2+2)/2=27,C1 层输出大小为 27×27×96(论文中分为两组,每组 27×27×48)。

卷积层 C2:卷积 → ReLU → 最大池化。

卷积:输入大小 27×27×96,256 个 5×5×96 的卷积核,扩充边缘 padding=2, 步长 stride=1,特征图大小为(27-5+2×2+1)/1=27,输出大小为 27×27×256;
激活函数:ReLU;
池化:池化核大小 3×3,不扩充边缘 padding=0,步长 stride=2,特征图大小为 (27-3+0+2)/2=13,C2 层输出大小为13×13×256(论文中分为两组,每组 13×13×128)。

卷积层 C3:卷积 → ReLU,没有进行池化

卷积:输入大小 13×13×256,384 个 3×3×256 的卷积核,扩充边缘 padding=1,步长 stride=1,特征图大小为(13-3+1×2+1)/1=13,输出大小为 13×13×384;
激活函数:ReLU;
C3 层输出为 13×13×384(论文中分为两组,每组 13×13×192)。

卷积层 C4:卷积 → ReLU,没有进行池化

卷积:输入大小 13×13×384,384 个 3×3×384 的卷积核, 扩充边缘 padding=1,步长 stride=1,特征图大小为 (13-3+1×2+1)/1=13,输出大小为 13×13×384;
激活函数:ReLU;
C4 层输出为 13×13×384(论文中分为两组,每组 13×13×192)。

卷积层 C5:卷积 → ReLU → 最大池化

卷积:输入大小 13×13×384,256 个 3×3×384 的卷积核,扩充边缘 padding=1,步长 stride=1,特征图大小为 (13-3+1×2+1)/1=13,输出大小为 13×13×256;
激活函数:ReLU;
池化:池化核大小 3×3,扩充边缘 padding=0,步长 stride=2,特征图大小为 (13-3+0×2+2)/2=6,C5 层输出大小为 6×6×256(论文中分为两组,每组 6×6×128)。

全连接层 FC6:全连接 → ReLU → Dropout

全连接:本层的全连接通过卷积实现,输入大小 6×6×256,4096 个 6×6×256 的卷积核,扩充边缘 padding=0, 步长 stride=1,特征图大小为 (6-6+0×2+1)/1=1,输出大小为 1×1×4096;
激活函数:ReLU;
Dropout:随机失活一些神经元节点,以防止过拟合,FC6 层输出为1×1×4096。

全连接层 FC7:全连接 → ReLU → Dropout

全连接:输入1×1×4096;
激活函数:ReLU;
Dropout:随机失活一些神经元节点,以防止过拟合,FC6 层输出为1×1×4096。

全连接层 FC8:全连接 → softmax

全连接:输入1×1×4096;
softmax:FC8 层输出为 1×1×1000(取决于数据集的类别数量)。


1.4 模型的运行结果

在这里插入图片描述


2. 在 PyTorch 中定义 AlexNet 模型类

2.1 按特征提取和分类器模块封装

PyTorch 通过 torch.nn 模块提供了高阶的 API,可以从头开始构建网络。

通过 Sequential 可以构建序列化的模块,使得网络模块的层次更加清晰,便于构造大型和复杂的网络模型。将卷积层 C1~C5 封装为特征提取模块,将全连接层 FC6~FC8 封装为分类器模块,定义 AlexNet 模型类。

import torch.nn as nn
import torch.nn.functional as F

# 定义 AlexNet 网络结构 1
class AlexNet1(nn.Module):
    def __init__(self, num_classes=10):  # 构造函数
        super(AlexNet1, self).__init__()
        self.features = nn.Sequential(  # 顺序容器
            nn.Conv2d(3, 48, kernel_size=11, stride=4, padding=2),  # [3,224,224] -> [48,55,55]
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),  # [48,55,55] -> [48,27,27]
            nn.Conv2d(48, 128, kernel_size=5, padding=2),  # [48,27,27] -> [128,27,27]
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),  # [128,27,27] -> [128,13,13]
            nn.Conv2d(128, 192, kernel_size=3, padding=1),  # [128,13,13] -> [192,13,13]
            nn.ReLU(inplace=True),
            nn.Conv2d(192, 192, kernel_size=3, padding=1),  # [192,13,13] -> [192,13,13]
            nn.ReLU(inplace=True),
            nn.Conv2d(192, 128, kernel_size=3, padding=1),  # [192,13,13] -> [128,13,13]
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),  # [128,13,13] -> [128,6,6]
        )
        self.classifier = nn.Sequential(
            nn.Dropout(p=0.5),
            nn.Linear(128*6*6, 2048),
            nn.ReLU(inplace=True),
            nn.Dropout(p=0.5),
            nn.Linear(2048, 2048),
            nn.ReLU(inplace=True),
            nn.Linear(2048, num_classes),
        )

    def forward(self, x):
        x = self.features(x)
        x = torch.flatten(x, start_dim=1)
        x = self.classifier(x)
        return x

注意论文中使用了两块 GPU 进行训练,而 AlexNet1 模型类只是其中一块 GPU 上的网络结构,相当于是简化版的 AlexNet 模型。

使用 print 可以输出 AlexNet1 模型的结构如下:

AlexNet(
  (features): Sequential(
    (0): Conv2d(3, 48, kernel_size=(11, 11), stride=(4, 4), padding=(2, 2))
    (1): ReLU(inplace=True)
    (2): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
    (3): Conv2d(48, 128, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))
    (4): ReLU(inplace=True)
    (5): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
    (6): Conv2d(128, 192, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (7): ReLU(inplace=True)
    (8): Conv2d(192, 192, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (9): ReLU(inplace=True)
    (10): Conv2d(192, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (11): ReLU(inplace=True)
    (12): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (classifier): Sequential(
    (0): Dropout(p=0.5, inplace=False)
    (1): Linear(in_features=4608, out_features=2048, bias=True)
    (2): ReLU(inplace=True)
    (3): Dropout(p=0.5, inplace=False)
    (4): Linear(in_features=2048, out_features=2048, bias=True)
    (5): ReLU(inplace=True)
    (6): Linear(in_features=2048, out_features=10, bias=True)
  )
)

2.2 按网络层逐层封装

通过 Sequential 可以构建序列化的模块,将 AlexNet 模型中的各网络层逐层进行封装,定义 AlexNet 模型类。

# 定义 AlexNet 网络结构 2
class AlexNet2(nn.Module):
    def __init__(self, num_classes=10):  # 构造函数
        super(AlexNet2, self).__init__()
        self.conv1 = nn.Sequential(
            nn.Conv2d(3, 48, kernel_size=11, stride=4, padding=2),  # [3,224,224] -> [48,55,55]
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2)  # [48,55,55] -> [48,27,27]
        )
        self.conv2 = nn.Sequential(
            nn.Conv2d(48, 128, kernel_size=5, padding=2),  # [48,27,27] -> [128,27,27]
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2)  # [128,27,27] -> [128,13,13]
        )
        self.conv3 = nn.Sequential(
            nn.Conv2d(128, 192, kernel_size=3, padding=1),  # [128,13,13] -> [192,13,13]
            nn.ReLU(inplace=True)
        )
        self.conv4 = nn.Sequential(
            nn.Conv2d(192, 192, kernel_size=3, padding=1),  # [192,13,13] -> [192,13,13]
            nn.ReLU(inplace=True)
        )
        self.conv5 = nn.Sequential(
            nn.Conv2d(192, 128, kernel_size=3, padding=1),  # [192,13,13] -> [128,13,13]
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),  # [128,13,13] -> [128,6,6]
        )
        # 全连接层
        self.classifier = nn.Sequential(
            nn.Dropout(p=0.5),
            nn.Linear(128 * 6 * 6, 2048),
            nn.ReLU(inplace=True),
            nn.Dropout(p=0.5),
            nn.Linear(2048, 2048),
            nn.ReLU(inplace=True),
            nn.Linear(2048, num_classes),
        )

    def forward(self, x):
        x = self.conv1(x)
        x = self.conv2(x)
        x = self.conv3(x)
        x = self.conv4(x)
        x = self.conv5(x)
        x = torch.flatten(x, start_dim=1)
        x = self.classifier(x)
        return x

使用 print 可以输出 AlexNet2 模型的结构如下:

AlexNet2(
  (conv1): Sequential(
    (0): Conv2d(3, 48, kernel_size=(11, 11), stride=(4, 4), padding=(2, 2))
    (1): ReLU(inplace=True)
    (2): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (conv2): Sequential(
    (0): Conv2d(48, 128, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))
    (1): ReLU(inplace=True)
    (2): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (conv3): Sequential(
    (0): Conv2d(128, 192, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (1): ReLU(inplace=True)
  )
  (conv4): Sequential(
    (0): Conv2d(192, 192, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (1): ReLU(inplace=True)
  )
  (conv5): Sequential(
    (0): Conv2d(192, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (1): ReLU(inplace=True)
    (2): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (classifier): Sequential(
    (0): Dropout(p=0.5, inplace=False)
    (1): Linear(in_features=4608, out_features=2048, bias=True)
    (2): ReLU(inplace=True)
    (3): Dropout(p=0.5, inplace=False)
    (4): Linear(in_features=2048, out_features=2048, bias=True)
    (5): ReLU(inplace=True)
    (6): Linear(in_features=2048, out_features=10, bias=True)
  )
)

2.3 从 torchvision.model 加载预定义模型

本节从 torchvision.models 加载 AlexNet 预定义模型,而不从 torchvision 加载预训练模型参数。

我们可以从头开始搭建各种网络模型,有利于学习和分析。但是自己搭建模型的效率低,容易出错。Torchvision 自带了很多经典的网络模型,可以直接加载这些预定义模型。我们可以只使用预定义的模型类来创建实例化模型对象(不加载预训练的模型参数)用于模型训练,也可以在实例化模型对象的同时加载预训练的模型参数,还可以基于预训练模型进行模型微调或迁移学习。

torchvision.models 提供了 alexnet模型类和预训练模型可以直接使用,原始代码可以参考:SOURCE CODE FOR TORCHVISION.MODELS.ALEXNET

class AlexNet(nn.Module):
    def __init__(self, num_classes: int = 1000, dropout: float = 0.5) -> None:
        super().__init__()
        _log_api_usage_once(self)
        self.features = nn.Sequential(
            nn.Conv2d(3, 64, kernel_size=11, stride=4, padding=2),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),
            nn.Conv2d(64, 192, kernel_size=5, padding=2),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),
            nn.Conv2d(192, 384, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(384, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),
        )
        self.avgpool = nn.AdaptiveAvgPool2d((6, 6))
        self.classifier = nn.Sequential(
            nn.Dropout(p=dropout),
            nn.Linear(256 * 6 * 6, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(p=dropout),
            nn.Linear(4096, 4096),
            nn.ReLU(inplace=True),
            nn.Linear(4096, num_classes),
        )

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        x = self.features(x)
        x = self.avgpool(x)
        x = torch.flatten(x, 1)
        x = self.classifier(x)
        return x

我们可以直接调用 torchvision.models.AlexNet 来实例化 AlexNet 模型类,使用 CIFAR10 数据集训练自己的 AlexNet 模型。

from torchvision import transforms, models

# (3) 从 torchvision.model 加载预定义模型 AlexNet (不加载模型权值)
model = models.AlexNet(num_classes=10)  # 实例化 AlexNet 模型类
model.to(device)  # 将网络分配到指定的device中
print(model)

使用 print 可以输出 torchvision 预定义的 AlexNet 模型的结构如下:

AlexNet(
  (features): Sequential(
    (0): Conv2d(3, 64, kernel_size=(11, 11), stride=(4, 4), padding=(2, 2))
    (1): ReLU(inplace=True)
    (2): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
    (3): Conv2d(64, 192, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))
    (4): ReLU(inplace=True)
    (5): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
    (6): Conv2d(192, 384, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (7): ReLU(inplace=True)
    (8): Conv2d(384, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (9): ReLU(inplace=True)
    (10): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (11): ReLU(inplace=True)
    (12): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (avgpool): AdaptiveAvgPool2d(output_size=(6, 6))
  (classifier): Sequential(
    (0): Dropout(p=0.5, inplace=False)
    (1): Linear(in_features=9216, out_features=4096, bias=True)
    (2): ReLU(inplace=True)
    (3): Dropout(p=0.5, inplace=False)
    (4): Linear(in_features=4096, out_features=4096, bias=True)
    (5): ReLU(inplace=True)
    (6): Linear(in_features=4096, out_features=10, bias=True)
  )
)

由此可见,Torchvision 预定义的 AlexNet 模型的结构,总体上是按特征提取模块和分类器模块进行封装的,与我们在 2.1 所建立的 AlexNet 模型结构非常相似,仅有细节处理上存在细微的差异。


使用 print 可以输出 torchvision 预定义的 AlexNet 模型的结构如下:

AlexNet(
  (features): Sequential(
    (0): Conv2d(3, 64, kernel_size=(11, 11), stride=(4, 4), padding=(2, 2))
    (1): ReLU(inplace=True)
    (2): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
    (3): Conv2d(64, 192, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))
    (4): ReLU(inplace=True)
    (5): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
    (6): Conv2d(192, 384, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (7): ReLU(inplace=True)
    (8): Conv2d(384, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (9): ReLU(inplace=True)
    (10): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (11): ReLU(inplace=True)
    (12): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (avgpool): AdaptiveAvgPool2d(output_size=(6, 6))
  (classifier): Sequential(
    (0): Dropout(p=0.5, inplace=False)
    (1): Linear(in_features=9216, out_features=4096, bias=True)
    (2): ReLU(inplace=True)
    (3): Dropout(p=0.5, inplace=False)
    (4): Linear(in_features=4096, out_features=4096, bias=True)
    (5): ReLU(inplace=True)
    (6): Linear(in_features=4096, out_features=10, bias=True)
  )
)

由此可见,Torchvision 预定义的 AlexNet 模型的结构,总体上是按特征提取模块和分类器模块进行封装的,与我们在 2.1 所建立的 AlexNet 模型结构非常相似,仅有细节处理上存在细微的差异。


3. 基于 AlexNet 模型的 CIFAR10 图像分类

3.1 PyTorch 建立神经网络模型的基本步骤

使用 PyTorch 建立、训练和使用神经网络模型的基本步骤如下。

  1. 准备数据集(Prepare dataset):加载数据集,对数据进行预处理。
  2. 建立模型(Design the model):实例化模型类,定义损失函数和优化器,确定模型结构和训练方法。
  3. 模型训练(Model trainning):使用训练数据集对模型进行训练,确定模型参数。
  4. 模型推理(Model inferring):使用训练好的模型进行推理,对输入数据预测输出结果。
  5. 模型保存与加载(Model saving/loading):保存训练好的模型,以便以后使用或部署。

以下按此步骤讲解 AlexNet 模型的例程。


3.2 加载 CIFAR10 数据集

通用数据集的样本结构均衡、信息高效,而且组织规范、易于处理。使用通用的数据集训练神经网络,不仅可以提高工作效率,而且便于评估模型性能。

PyTorch 提供了一些常用的图像数据集,预加载在 torchvision.datasets 类中。torchvision 模块实现神经网络所需的核心类和方法, torchvision.datasets 包含流行的数据集、模型架构和常用的图像转换方法。

CIFAR 数据集是一个经典的图像分类小型数据集,有 CIFAR10 和 CIFAR100 两个版本。CIFAR10 有 10 个类别,CIFAR100 有 100 个类别。CIFAR10 每张图像大小为 32*32,包括飞机、小汽车、鸟、猫、鹿、狗、青蛙、马、船、卡车 10 个类别。CIFAR10 共有 60000张图像,其中训练集 50000张,测试集 10000张。每个类别有 6000张图片,数据集平衡。

加载和使用 CIFAR 数据集的方法为:

torchvision.datasets.CIFAR10()
torchvision.datasets.CIFAR100()

CIFAR 数据集可以从官网下载:http://www.cs.toronto.edu/~kriz/cifar.html 后使用,也可以使用 datasets 类自动加载(如果本地路径没有该文件则自动下载)。

下载数据集时,使用预定义的 transform 方法进行数据预处理,包括调整图像尺寸、标准化处理,将数据格式转换为张量。标准化处理所使用 CIFAR10 数据集的均值和方差为 (0.49, 0.48, 0.45), (0.25, 0.24, 0.26)。

大型训练数据集不能一次性加载全部样本来训练,可以使用 Dataloader 类自动加载数据。Dataloader 是一个迭代器,基本功能是传入一个 Dataset 对象,根据参数 batch_size 生成一个 batch 的数据。

使用 DataLoader 类加载 CIFAR-10 数据集的例程如下。

    # (1) 将[0,1]的 PILImage 转换为[-1,1]的Tensor
    transform = transforms.Compose([  # Transform Compose of the image
        transforms.Resize([224,224]),  # 图像大小调整为 (w,h)=(224,224)
        transforms.ToTensor(),  # 将图像转换为张量 Tensor
        transforms.Normalize([0.485, 0.456, 0.406],
                             [0.229, 0.224, 0.225])])  # 标准化
    
    # (2) 加载 CIFAR10 数据集
    batchsize = 128
    # 加载 CIFAR10 数据集, 如果 root 路径加载失败, 则自动在线下载
    # 加载 CIFAR10 训练数据集, 50000 张训练图片
    train_set = torchvision.datasets.CIFAR10(root='../dataset', train=True,
                                            download=True, transform=transform)
    train_loader = torch.utils.data.DataLoader(train_set, batch_size=batchsize,
                                              shuffle=True, num_workers=2)
    # 加载 CIFAR10 测试数据集, 10000 张验证图片
    test_set = torchvision.datasets.CIFAR10(root='../dataset', train=False,
                                           download=True, transform=transform)
    test_loader = torch.utils.data.DataLoader(test_set, batch_size=1000,
                                             shuffle=False, num_workers=2)

3.3 建立 AlexNet 网络模型

建立一个 AlexNet 网络模型进行训练,包括三个步骤:

  • 实例化 AlexNet 模型对象;
  • 设置训练的损失函数;
  • 设置训练的优化器。

torch.nn.functional 模块提供了各种内置损失函数,本例使用交叉熵损失函数 CrossEntropyLoss。

torch.optim 模块提供了各种优化方法,本例使用 Adam 优化器。注意要将 model 的参数 model.parameters() 传给优化器对象,以便优化器扫描需要优化的参数。

    # (3) 构造 AlexNet 网络模型
    model = AlexNet(num_classes=10)  # 实例化 AlexNet 网络模型
    model.to(device)  # 将网络分配到指定的device中
    print(model)

    # 定义损失函数和优化器
    criterion = nn.CrossEntropyLoss()  # 定义损失函数 CrossEntropy
    optimizer = optim.Adam(model.parameters(), lr=0.001)  # 定义优化器 Adam

3.4 AlexNet 模型训练

PyTorch 模型训练的基本步骤是:

  1. 前馈计算模型的输出值;
  2. 计算损失函数值;
  3. 计算权重 weight 和偏差 bias 的梯度;
  4. 根据梯度值调整模型参数;
  5. 将梯度重置为 0(用于下一循环)。

在模型训练过程中,可以使用验证集数据评价训练过程中的模型精度,以便控制训练过程。模型验证就是用验证数据进行模型推理,前向计算得到模型输出,但不反向计算模型误差,因此需要设置 torch.no_grad()。

AlexNet 模型的训练时间较长,使用 GPU 设备可以有效地加速模型训练和模型推理。

使用 PyTorch 进行模型训练的例程如下。

    # (4) 训练 AlexNet 网络模型
    epoch_list = []  # 记录训练轮次
    loss_list = []  # 记录训练集的损失值
    accu_list = []  # 记录验证集的准确率
    num_epochs = 50  # 训练轮次
    for epoch in range(num_epochs):  # 训练轮次 epoch
        running_loss = 0.0  # 每个 epoch 的累加损失值清零
        for step, data in enumerate(train_loader, start=0):  # 迭代器加载数据
            inputs, labels = data  # inputs: [batch,3,224,224] labels: [batch]

            optimizer.zero_grad()  # 损失梯度清零
            outputs = model(inputs)  # 前向传播, [batch, 10]
            loss = criterion(outputs, labels)  # 计算损失函数
            loss.backward()  # 反向传播
            optimizer.step()  # 参数更新

            # 累加训练损失值
            running_loss += loss.item()
            if step%100==99:  # 每 100 个 step 打印一次训练信息
                print("epoch {}, step {}: loss = {:.4f}".format(epoch, step, loss.item()))

        # 计算每个轮次的验证集准确率
        with torch.no_grad():  # 验证过程, 不计算损失函数梯度
            outputs_valid = model(valid_images.to(device))  # 模型对验证集进行推理, [batch, 10]
        pred_labels = torch.max(outputs_valid, dim=1)[1]  # 预测类别, [batch]
        accuracy = torch.eq(pred_labels, valid_labels.to(device)).sum().item() / valid_size * 100  # 计算准确率
        print("Epoch {}: train loss={:.4f}, accuracy={:.2f}%".format(epoch, running_loss, accuracy))

        # 记录训练过程的统计数据
        epoch_list.append(epoch)  # 记录迭代次数
        loss_list.append(running_loss)  # 记录训练集的损失函数
        accu_list.append(accuracy)  # 记录验证集的准确率

程序运行结果如下:

Epoch 0: train loss=681.9708, accuracy=49.30%
Epoch 1: train loss=503.7996, accuracy=58.90%
Epoch 2: train loss=440.8897, accuracy=62.20%
Epoch 3: train loss=395.9854, accuracy=64.30%
Epoch 4: train loss=367.0464, accuracy=66.50%

Epoch 47: train loss=116.7378, accuracy=73.90%
Epoch 48: train loss=115.4059, accuracy=71.90%
Epoch 49: train loss=112.6429, accuracy=73.70%

经过 20 轮左右的训练,使用验证集中的 1000 张图片进行验证,模型准确率达到 70%左右。继续训练可以进一步降低训练损失函数值,经过 100轮左右的训练验证集的准确率达到 75%左右。

在这里插入图片描述


3.5 AlexNet 模型的保存与加载

模型训练好以后,将模型保存起来,以便下次使用。PyTorch 中模型保存主要有两种方式,一是保存模型权值,二是保存整个模型。本例使用 model.state_dict() 方法以字典形式返回模型权值,torch.save() 方法将权值字典序列化到磁盘,将模型保存为 .pth 文件。

    # (5) 保存 AlexNet 网络模型
    model_path = "../models/AlexNet_Cifar1.pth"
    torch.save(model.state_dict(), model_path)  # 保存模型权值

使用训练好的模型,首先要实例化模型类,然后调用 load_state_dict() 方法加载模型的权值参数。

    # (6) 加载 AlexNet 网络模型进行推理
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")  # 检测并指定设备
    # 加载 AlexNet 预训练模型
    model = AlexNet(num_classes=10)  # 实例化 AlexNet 网络模型
    model.to(device)  # 将网络分配到指定的device中
    model_path = "../models/AlexNet_Cifar1.pth"
    model.load_state_dict(torch.load(model_path))
    model.eval()  # 模型推理模式

需要特别注意的是:

(1)PyTorch 中的 .pth 文件只保存了模型的权值参数,而没有模型的结构信息,因此必须先实例化模型对象,再加载模型参数。

(2)模型对象必须与模型参数严格对应,才能正常使用。注意即使都是 LeNet5 模型,模型类的具体定义也可能有细微的区别。如果从一个来源获取模型类的定义,从另一个来源获取模型参数文件,就很容易造成模型结构与参数不能匹配。

(3)无论从 PyTorch 模型仓库加载的模型和参数,或从其它来源获取的预训练模型,或自己训练得到的模型,模型加载的方法都是相同的,也都要注意模型结构与参数的匹配问题。例如,本文 2.1 与 2.2 中的模型结构实际上完全相同,只是模块封装的方式不同,就会导致参数字典的格式存在差别,训练的模型参数 .pth 文件就不能混用。


3.6 模型检验

使用加载的 AlexNet 模型,输入新的图片进行模型推理,可以由模型输出结果确定输入图片所属的类别。

使用测试集数据进行模型推理,根据模型预测结果与图片标签进行比较,可以检验模型的准确率。模型验证集与模型检验集不能交叉使用,但为了简化例程在本程序中未做区分。

    # (7) 模型检验
    correct = 0
    total = 0
    for data in test_loader:  # 迭代器加载测试数据集
        imgs, labels = data  # torch.Size([batch,3,224,224]) torch.Size([batch])
        # print(imgs.shape, labels.shape)
        outputs = model(imgs.to(device))  # 正向传播, 模型推理, [batch, 10]
        labels_pred = torch.max(outputs, dim=1)[1]  # 模型预测的类别 [batch]
        # _, labels_pred = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += torch.eq(labels_pred, labels.to(device)).sum().item()
    accuracy = 100. * correct / total
    print("Test samples: {}".format(total))
    print("Test accuracy={:.2f}%".format(accuracy))

使用测试集进行模型推理,测试模型准确率为 77.72%。

Test samples: 10000
Test accuracy=77.72%


3.7 模型推理

使用加载的 AlexNet 模型,输入新的图片进行模型推理,可以由模型输出结果确定输入图片所属的类别。

从测试集中提取几张图片,或者读取图像文件,进行模型推理,获得图片的分类类别。在提取图片或读取文件时,要注意对图片格式和图片大小进行适当的转换。

    # (8) 提取测试集图片进行模型推理
    batch = 8  # 批次大小
    data_set = torchvision.datasets.CIFAR10(root='../dataset', train=False,
                                           download=False, transform=None)
    plt.figure(figsize=(9, 6))
    for i in range(batch):
        imgPIL = data_set[i][0]  # 提取 PIL 图片
        label = data_set[i][1]  # 提取 图片标签
        # 预处理/模型推理/后处理
        imgTrans = transform(imgPIL)  # 预处理变换, torch.Size([3, 224, 224])
        imgBatch = torch.unsqueeze(imgTrans, 0)  # 转为批处理,torch.Size([batch=1, 3, 224, 224])
        outputs = model(imgBatch.to(device))  # 模型推理, 返回 [batch=1, 10]
        indexes = torch.max(outputs, dim=1)[1]  # 注意 [batch=1], device = 'device
        index = indexes[0].item()  # 预测类别,整数
        # 绘制第 i 张图片
        imgNP = np.array(imgPIL)  # PIL -> Numpy
        out_text = "label:{}/model:{}".format(classes[label], classes[index])
        plt.subplot(2, 4 ,i+1)
        plt.imshow(imgNP)
        plt.title(out_text)
        plt.axis('off')
    plt.tight_layout()
    plt.show()

结果如下。

在这里插入图片描述

    # (9) 读取图像文件进行模型推理
    from PIL import Image
    filePath = "../images/img_plane_01.jpg"  # 数据文件的地址和文件名
    imgPIL = Image.open(filePath)  # PIL 读取图像文件, <class 'PIL.Image.Image'>

    # 预处理/模型推理/后处理
    imgTrans = transform["test"](imgPIL)  # 预处理变换, torch.Size([3, 224, 224])
    imgBatch = torch.unsqueeze(imgTrans, 0)  # 转为批处理,torch.Size([batch=1, 3, 224, 224])
    outputs = model(imgBatch.to(device))  # 模型推理, 返回 [batch=1, 10]
    indexes = torch.max(outputs, dim=1)[1]  # 注意 [batch=1], device = 'device
    percentages = nn.functional.softmax(outputs, dim=1)[0] * 100
    index = indexes[0].item()  # 预测类别,整数
    percent = percentages[index].item()  # 预测类别的概率,浮点数

    # 绘制第 i 张图片
    imgNP = np.array(imgPIL)  # PIL -> Numpy
    out_text = "Prediction:{}, {}, {:.2f}%".format(index, classes[index], percent)
    print(out_text)
    plt.imshow(imgNP)
    plt.title(out_text)
    plt.axis('off')
    plt.tight_layout()
    plt.show()

结果如下。

在这里插入图片描述


4. AlexNet 模型对 CIFAR10 进行图像分类的完整例程

本文的完整例程如下。

# Beginner_AlexNet_CIFAR_1.py
# AlexNet model for beginner with PyTorch
# 经典模型: AlexNet 模型 CIFAR10 图像分类
# Copyright: [email protected]
# Crated: Huang Shan, 2023/05/15

# _*_coding:utf-8_*_
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
from matplotlib import pyplot as plt
import numpy as np


# 定义 AlexNet 网络结构 2
class AlexNet2(nn.Module):
    def __init__(self, num_classes=10):  # 构造函数
        super(AlexNet2, self).__init__()
        self.conv1 = nn.Sequential(
            nn.Conv2d(3, 48, kernel_size=11, stride=4, padding=2),  # [3,224,224] -> [48,55,55]
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2)  # [48,55,55] -> [48,27,27]
        )
        self.conv2 = nn.Sequential(
            nn.Conv2d(48, 128, kernel_size=5, padding=2),  # [48,27,27] -> [128,27,27]
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2)  # [128,27,27] -> [128,13,13]
        )
        self.conv3 = nn.Sequential(
            nn.Conv2d(128, 192, kernel_size=3, padding=1),  # [128,13,13] -> [192,13,13]
            nn.ReLU(inplace=True)
        )
        self.conv4 = nn.Sequential(
            nn.Conv2d(192, 192, kernel_size=3, padding=1),  # [192,13,13] -> [192,13,13]
            nn.ReLU(inplace=True)
        )
        self.conv5 = nn.Sequential(
            nn.Conv2d(192, 128, kernel_size=3, padding=1),  # [192,13,13] -> [128,13,13]
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),  # [128,13,13] -> [128,6,6]
        )
        # 全连接层
        self.classifier = nn.Sequential(
            nn.Dropout(p=0.5),
            nn.Linear(128 * 6 * 6, 2048),
            nn.ReLU(inplace=True),
            nn.Dropout(p=0.5),
            nn.Linear(2048, 2048),
            nn.ReLU(inplace=True),
            nn.Linear(2048, num_classes),
        )

    def forward(self, x):
        x = self.conv1(x)
        x = self.conv2(x)
        x = self.conv3(x)
        x = self.conv4(x)
        x = self.conv5(x)
        x = torch.flatten(x, start_dim=1)
        x = self.classifier(x)
        return x


if __name__ == '__main__':
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    print(device)

    # (1) 将[0,1]的 PILImage 转换为[-1,1]的Tensor
    transform = transforms.Compose([  # Transform Compose of the image
        transforms.Resize([224, 224]),  # 图像大小调整为 (w,h)=(224,224)
        transforms.ToTensor(),  # 将图像转换为张量 Tensor
        transforms.Normalize([0.485, 0.456, 0.406],
                             [0.229, 0.224, 0.225])])  # 标准化

    # (2) 加载 CIFAR10 数据集
    batchsize = 128
    # 加载 CIFAR10 数据集, 如果 root 路径加载失败, 则自动在线下载
    # 加载 CIFAR10 训练数据集, 50000 张训练图片
    train_set = torchvision.datasets.CIFAR10(root='../dataset', train=True,
                                            download=True, transform=transform)
    train_loader = torch.utils.data.DataLoader(train_set, batch_size=batchsize,
                                              shuffle=True, num_workers=2)
    # 加载 CIFAR10 测试数据集, 10000 张验证图片
    test_set = torchvision.datasets.CIFAR10(root='../dataset', train=False,
                                           download=True, transform=transform)
    test_loader = torch.utils.data.DataLoader(test_set, batch_size=1000,
                                             shuffle=False, num_workers=2)
    # 创建生成器,用 next 获取一个批次的数据
    valid_data_iter = iter(test_loader)  # _SingleProcessDataLoaderIter 对象
    valid_images, valid_labels = next(valid_data_iter)  # images: [batch,3,224,224], labels: [batch]
    valid_size = valid_labels.size(0)  # 验证数据集大小,batch

    # 定义类别名称,CIFAR10 数据集的 10个类别
    classes = ('plane', 'car', 'bird', 'cat', 'deer',
               'dog', 'frog', 'horse', 'ship', 'truck')

    # (3) 构造 AlexNet 网络模型
    model = AlexNet2(num_classes=10)  # 实例化 AlexNet 网络模型
    model.to(device)  # 将网络分配到指定的device中
    # print(model)

    # 定义损失函数和优化器
    criterion = nn.CrossEntropyLoss()  # 定义损失函数 CrossEntropy
    optimizer = optim.Adam(model.parameters(), lr=0.001)  # 定义优化器 Adam

    # (4) 训练 AlexNet 网络模型
    epoch_list = []  # 记录训练轮次
    loss_list = []  # 记录训练集的损失值
    accu_list = []  # 记录验证集的准确率
    num_epochs = 50  # 训练轮次
    for epoch in range(num_epochs):  # 训练轮次 epoch
        running_loss = 0.0  # 每个轮次的累加损失值清零
        for step, data in enumerate(train_loader, start=0):  # 迭代器加载数据
            inputs, labels = data  # inputs: [batch,3,224,224] labels: [batch]

            optimizer.zero_grad()  # 损失梯度清零
            outputs = model(inputs.to(device))  # 正向传播
            loss = criterion(outputs, labels.to(device))  # 计算损失函数
            loss.backward()  # 反向传播
            optimizer.step()  # 参数更新

            # 累加训练损失值
            running_loss += loss.item()
            # if step%100==99:  # 每 100 个 step 打印一次训练信息
            #     print("epoch {}, step {}: loss = {:.4f}".format(epoch, step, loss.item()))

        # 计算每个轮次的验证集准确率
        with torch.no_grad():  # 验证过程, 不计算损失函数梯度
            outputs_valid = model(valid_images.to(device))  # 模型对验证集进行推理, [batch, 10]
        pred_labels = torch.max(outputs_valid, dim=1)[1]  # 预测类别, [batch]
        accuracy = torch.eq(pred_labels, valid_labels.to(device)).sum().item() / valid_size * 100  # 计算准确率
        print("Epoch {}: train loss={:.4f}, accuracy={:.2f}%".format(epoch, running_loss, accuracy))

        # 记录训练过程的统计数据
        epoch_list.append(epoch)  # 记录迭代次数
        loss_list.append(running_loss)  # 记录训练集的损失函数
        accu_list.append(accuracy)  # 记录验证集的准确率

    # 训练结果可视化
    plt.figure(figsize=(11, 5))
    plt.suptitle("AlexNet-5 Model in CIFAR10")
    plt.subplot(121), plt.title("Train loss")
    plt.plot(epoch_list, loss_list)
    plt.xlabel('epoch'), plt.ylabel('loss')
    plt.subplot(122), plt.title("Valid accuracy")
    plt.plot(epoch_list, accu_list)
    plt.xlabel('epoch'), plt.ylabel('accuracy')
    plt.show()

    # (5) 保存 AlexNet 网络模型
    model_path = "../models/AlexNet_Cifar2.pth"
    torch.save(model.state_dict(), model_path)  # 保存模型权值

    # # 以下模型加载和模型推理,可以是另一个独立的程序
    # # (6) 加载 AlexNet 网络模型进行推理
    # device = torch.device("cuda" if torch.cuda.is_available() else "cpu")  # 检测并指定设备
    # # device = 'cpu'  # 用户指定设备,'cpu' 或 'cuda'
    # # 加载 AlexNet 预训练模型
    # model = AlexNet2(num_classes=10)  # 实例化 AlexNet 网络模型
    # model.to(device)  # 将网络分配到指定的device中
    # model_path = "../models/AlexNet_Cifar2.pth"
    # model.load_state_dict(torch.load(model_path))
    # model.eval()  # 模型推理模式
    # 
    # # (7) 模型检验
    # correct = 0
    # total = 0
    # for data in test_loader:  # 迭代器加载测试数据集
    #     imgs, labels = data  # torch.Size([batch,3,224,224]) torch.Size([batch])
    #     # print(imgs.shape, labels.shape)
    #     outputs = model(imgs.to(device))  # 正向传播, 模型推理, [batch, 10]
    #     labels_pred = torch.max(outputs, dim=1)[1]  # 模型预测的类别 [batch]
    #     # _, labels_pred = torch.max(outputs.data, 1)
    #     total += labels.size(0)
    #     correct += torch.eq(labels_pred, labels.to(device)).sum().item()
    # accuracy = 100. * correct / total
    # print("Test samples: {}".format(total))
    # print("Test accuracy={:.2f}%".format(accuracy))
    #
    # # (8) 提取测试集图片进行模型推理
    # batch = 8  # 批次大小
    # data_set = torchvision.datasets.CIFAR10(root='../dataset', train=False,
    #                                        download=False, transform=None)
    # plt.figure(figsize=(9, 6))
    # for i in range(batch):
    #     imgPIL = data_set[i][0]  # 提取 PIL 图片
    #     label = data_set[i][1]  # 提取 图片标签
    #     # 预处理/模型推理/后处理
    #     imgTrans = transform(imgPIL)  # 预处理变换, torch.Size([3, 224, 224])
    #     imgBatch = torch.unsqueeze(imgTrans, 0)  # 转为批处理,torch.Size([batch=1, 3, 224, 224])
    #     outputs = model(imgBatch.to(device))  # 模型推理, 返回 [batch=1, 10]
    #     indexes = torch.max(outputs, dim=1)[1]  # 注意 [batch=1], device = 'device
    #     index = indexes[0].item()  # 预测类别,整数
    #     # 绘制第 i 张图片
    #     imgNP = np.array(imgPIL)  # PIL -> Numpy
    #     out_text = "label:{}/model:{}".format(classes[label], classes[index])
    #     plt.subplot(2, 4, i+1)
    #     plt.imshow(imgNP)
    #     plt.title(out_text)
    #     plt.axis('off')
    # plt.tight_layout()
    # plt.show()


参考文献:

  1. Alex Krizhevsky, Ilya Sutskever, Geoffrey Hinton, ImageNet Classification with Deep Convolutional Neural Networks, 2012 (https://doi.org/10.1145/3065386)
  2. https://pytorch.org/tutorials/beginner/blitz/cifar10_tutorial.html

【本节完】


版权声明:
欢迎关注『youcans动手学模型』系列
转发请注明原文链接:
【youcans动手学模型】AlexNet模型CIFAR10图像分类
Copyright 2023 youcans, XUPT
Crated:2023-05-18


猜你喜欢

转载自blog.csdn.net/youcans/article/details/130753748