欢迎关注『youcans动手学模型』系列
本专栏内容和资源同步到 GitHub/youcans
【youcans动手学模型】AlexNet模型CIFAR10图像分类
- 1. AlexNet 卷积神经网络模型
- 1.1 论文简介
- 1.2 AlexNet 的主要贡献
- 1.3 AlexNet 网络
- 1.4 模型的运行结果
- 2. 在 PyTorch 中定义 AlexNet 模型类
- 2.1 按特征提取和分类器模块封装
- 2.2 按网络层逐层封装
- 2.3 从 torchvision.model 加载预定义模型
- 3. 基于 AlexNet 模型的 CIFAR10 图像分类
- 3.1 PyTorch 建立神经网络模型的基本步骤
- 3.2 加载 CIFAR10 数据集
- 3.3 建立 AlexNet 网络模型
- 3.4 AlexNet 模型训练
- 3.5 AlexNet 模型的保存与加载
- 3.6 模型检验
- 3.7 模型推理
- 4. AlexNet 模型对 CIFAR10 进行图像分类的完整例程
本文用 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 建立、训练和使用神经网络模型的基本步骤如下。
- 准备数据集(Prepare dataset):加载数据集,对数据进行预处理。
- 建立模型(Design the model):实例化模型类,定义损失函数和优化器,确定模型结构和训练方法。
- 模型训练(Model trainning):使用训练数据集对模型进行训练,确定模型参数。
- 模型推理(Model inferring):使用训练好的模型进行推理,对输入数据预测输出结果。
- 模型保存与加载(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 模型训练的基本步骤是:
- 前馈计算模型的输出值;
- 计算损失函数值;
- 计算权重 weight 和偏差 bias 的梯度;
- 根据梯度值调整模型参数;
- 将梯度重置为 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: youcans@qq.com
# 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()
参考文献:
- Alex Krizhevsky, Ilya Sutskever, Geoffrey Hinton, ImageNet Classification with Deep Convolutional Neural Networks, 2012 (https://doi.org/10.1145/3065386)
- https://pytorch.org/tutorials/beginner/blitz/cifar10_tutorial.html
【本节完】
版权声明:
欢迎关注『youcans动手学模型』系列
转发请注明原文链接:
【youcans动手学模型】AlexNet模型CIFAR10图像分类
Copyright 2023 youcans, XUPT
Crated:2023-05-18