HyFan

奔赴山海,保持热爱

0%

图像分类及经典CNN实现

实验目的和要求

实现MINIST手写数字图像识别分类任务。

要求使用多种CNN模型:
必选LetNet,AlexNet,ResNet
自己再选择至少一个架构实现。

实验环境

Python3.8
VsCode

下载MINIST手写数字图像数据以及数据处理

下载地址:MINIST

读取数据文件

下载的文件格式为IDX:

通过导入idx2numpy库,用于将IDX文件格式转化为Numpy数组。

分别从上面四个文件读取训练集图像和标签、测试集图像和标签,并将他们存储在 NumPy 数组中以供进一步处理或分析。

数据处理

首先需要将图像数据的像素值除以 255.0 来归一化它们。此操作将像素值缩放到 0 和 1 之间,以确保数值稳定性。

接着将图像数据从二维数组(28x28 像素)重塑为包含 784 个特征的一维数组。

同时对标签数据执行单热编码。单热编码是一种用于将分类变量表示为二进制向量的技术。在这种情况下,标签值是表示数字类别的 0 到 9 之间的整数。使用np.eye(10)函数生成一个大小为 10x10 的单位矩阵,其中每一行对应一个唯一的类标签。通过使用标签值对该矩阵进行索引,我们获得了相应的单热编码向量。

划分验证集

按要求自行划分验证集:

使用train_test_split函数将训练数据分成两部分:实际训练集 (train_images和train_labels) 和验证集 (val_images和val_labels)。设置参数为0.2,即原始训练数据的 20%将用于验证,而其余 80%将用于训练。

创建训练数据集的子集

数据集比较大,特征数量很多,导致较为复杂的模型在训练时间上很长,于是当使用较复杂一点的模型,我通过选择原始训练数据的一部分进行训练来缩短时间。

因此我创建一个采样器SequentialSampler(train_idx)选择原始训练数据前百分之十的样本进行训练。

CNN模型

LetNet

LetNet是深度学习领域的先驱模型之一,在计算机视觉任务的进步中发挥了重要作用,特别是在手写数字识别方面,主要设计用于识别图像中的手写数字,特别是来自 MNIST 数据集。

LetNet网络共有7层,包含卷积层、池化层和全连接层。

  • 输入层:LeNet 将灰度图像作为输入,大小为 32x32 像素。

  • 卷积层:LeNet 从两个卷积层开始,每个卷积层后跟一个 sigmoid 激活函数。这些层将一组可学习的过滤器应用于输入图像,执行卷积操作以提取边缘和角落等特征。

  • 池化层:这些层减少了特征图的空间维度,同时保留了重要信息。池化方式统一为平均池化。

  • 全连接层:在卷积层和池化层之后,LeNet 包括两个全连接层。这些层类似于传统的神经网络层,旨在对提取的特征进行分类。第一个全连接层有 120 个节点,第二个全连接层有 84 个节点。两层都使用 sigmoid 激活函数。

  • 输出层:最后一层是一个全连接层,有 10 个节点,代表 10 个可能的类(数字 0 到 9)。输出层使用 softmax 激活函数来生成类的概率分布。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class LeNet(nn.Module):
def __init__(self):
super(LeNet, self).__init__()
self.conv1 = nn.Conv2d(1, 6, 5)
self.pool1 = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(6, 16, 5)
self.pool2 = nn.MaxPool2d(2, 2)
self.fc1 = nn.Linear(16 * 4 * 4, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)

def forward(self, x):
x = self.pool1(F.relu(self.conv1(x)))
x = self.pool2(F.relu(self.conv2(x)))
x = x.view(-1, 16 * 4 * 4)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x

AlexNet

AlexNet 是一种深度卷积神经网络 (CNN) 架构,由 Alex Krizhevsky、Ilya Sutskever 和 Geoffrey Hinton 开发。它在 2012 年赢得了 ImageNet 大规模视觉识别挑战赛 (ILSVRC),显着推进了计算机视觉领域。

AlexNet 包含数百万个标记图像。它由多个卷积层和全连接层组成,采用各种技术来提高性能。以下是其关键组件的概述:

  • 输入层:AlexNet 将 RGB 图像作为输入,大小为 224x224 像素。
  • 卷积层:第一个卷积层应用 96 个大小为 11x11 的卷积核,步幅为 4。随后的卷积层使用更小的卷积核尺寸 (3x3) 并具有更小的步幅。卷积核的数量从 96 个增加到 256 个。
  • 最大池化层:在卷积层之间,AlexNet 利用最大池化层来减少空间维度并捕获最显着的特征。池化层的卷积核大小为 3x3,步幅为 2。
  • 全连接层:在卷积层之后,有三个全连接层。第一个全连接层由 4,096 个神经元组成,然后是 ReLU 激活函数和 dropout 正则化以减轻过拟合。接下来的两个全连接层分别有 4,096 和 1,0 个神经元。最后一层使用 softmax 激活来生成 1,0 个类别的概率分布。
  • Dropout:为了防止过拟合,AlexNet 在第一层和第二层全连接层之后应用了 dropout 正则化。Dropout 在训练期间随机将一部分神经元设置为零,迫使网络学习更稳健的表示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class AlexNet(nn.Module):
def __init__(self, dropout):
super(AlexNet, self).__init__()
self.net = nn.Sequential(
nn.Upsample(scale_factor=8, mode='bilinear'),
nn.Conv2d(in_channels=1, out_channels=96, kernel_size=11, stride=4, padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Conv2d(in_channels=96, out_channels=256, kernel_size=5, padding=2),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Conv2d(in_channels=256, out_channels=384, kernel_size=3, padding=1),
nn.ReLU(),
nn.Conv2d(in_channels=384, out_channels=384, kernel_size=3, padding=1),
nn.ReLU(),
nn.Conv2d(in_channels=384, out_channels=256, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Flatten(),
nn.Linear(in_features=6400, out_features=4096),
nn.ReLU(),
nn.Dropout(p=dropout),
nn.Linear(in_features=4096, out_features=4096),
nn.ReLU(),
nn.Dropout(p=dropout),
nn.Linear(in_features=4096, out_features=10)
)

def forward(self, x):
return self.net(x)

ResNet

ResNet是现代计算机视觉模型中一个里程碑式的网络结构,由Kaiming He等人于2016年提出。它旨在通过利用残差连接来解决非常深的神经网络中发生的梯度消失问题。

ResNet 通过引入允许梯度更容易地在网络中流动的跳跃连接来解决训练深度神经网络时会出现梯度消失问题,使网络难以有效学习,阻碍性能的进一步提升的问题。

ResNet中基本构建块是残差块(Residue Block),其基本结构如下图所示。可以看到,相比普通的卷积块,残差块通过加入一个直接连接输入和输出的快速通道来将输入的特征直接叠加到输出特征之上,从而使得输入特征得以保留。

ResNet网络结构主要由残差块和全连接部分组成。

在网络开始的部分采用了和AlexNet相同的 (7 \times 7) 卷积核,并加入了批归一化(Batch Normalization)这一操作,使得网络的特征尺度放缩到同一水平,从而使得网络更容易训练。随后的网络结构由8个残差块组成,其中第一组残差块包含了两个同尺寸连接的普通残差块,之后的三组残差块均有一个半尺寸连接的残差块和一个普通残差块组成。

ResNet 没有在网络末端使用全连接层,而是使用全局平均池化,这减少了模型中的参数数量并有助于防止过度拟合。

对于这个实验,我们只需要实现最少层数的ResNet就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class ResBlock(nn.Module):
def __init__(self, in_channels, out_channels, stride, res_conv=False):
super(ResBlock, self).__init__()
self.net = nn.Sequential(
nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=3, padding=1, stride=stride),
nn.BatchNorm2d(out_channels),
nn.ReLU(),
nn.Conv2d(in_channels=out_channels, out_channels=out_channels, kernel_size=3, padding=1),
nn.BatchNorm2d(out_channels)
)
if res_conv:
self.res_conv = nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=1, stride=stride)
else:
self.res_conv = None
self.relu = nn.ReLU()

def forward(self, x):
y = self.net(x)
if self.res_conv:
x = self.res_conv(x)
y = y + x
return self.relu(y)

class ResNet(nn.Module):
def __init__(self):
super(ResNet, self).__init__()
self.net = nn.Sequential(
nn.Upsample(scale_factor=8, mode='bilinear'),
nn.Conv2d(in_channels=1, out_channels=64, kernel_size=7, stride=2, padding=3),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
)
self.net.add_module('res_1', nn.Sequential(
ResBlock(in_channels=64, out_channels=64, stride=1),
ResBlock(in_channels=64, out_channels=64, stride=1)
))
self.net.add_module('res_2', nn.Sequential(
ResBlock(in_channels=64, out_channels=128, stride=2, res_conv=True),
ResBlock(in_channels=128, out_channels=128, stride=1)
))
self.net.add_module('res_3', nn.Sequential(
ResBlock(in_channels=128, out_channels=256, stride=2, res_conv=True),
ResBlock(in_channels=256, out_channels=256, stride=1)
))
self.net.add_module('res_4', nn.Sequential(
ResBlock(in_channels=256, out_channels=512, stride=2, res_conv=True),
ResBlock(in_channels=512, out_channels=512, stride=1)
))
self.net.add_module('output', nn.Sequential(
nn.AdaptiveAvgPool2d((1, 1)),
nn.Flatten(),
nn.Linear(in_features=512, out_features=10)
))

def forward(self, x):
return self.net(x)

VGGNet

VGGNet由 Karen Simonyan 和 Andrew Zisserman 于 2014 年推出。由于其在图像分类任务中的简单性和有效性而获得了极大的关注和普及。
该网络主要包括卷积层和最大池化层。卷积层负责从输入图像中学习空间层次结构,而最大池化层有助于减少空间维度。

VGGNet架构具有统一的配置,更易于理解和实现。卷积层使用小的 3x3 过滤器,步长为 1,填充为 1,这有助于保持空间分辨率。最大池化层有一个 2x2 的窗口,步幅为 2,导致空间维度减半。

VGGNet 的更深层使其能够从图像中捕获更多抽象特征。然而,这种深度架构的缺点是它的大量参数,这使得它的计算量大且内存密集。原始的 VGGNet 模型有大约 1.38 亿个参数。

VGGNet中最为核心的元素即为可变深度的VGG块,VGG块的基本结构是确定的, 只需要指定卷积层的个数以及输入输出的通道数即可。对于每个VGG块的第一个卷积层,我们将输入的通道数匹配为输出的通道数;对于后面的若干卷积层,我们只需要保持输出通道输和输入通道数一致。

这次实验,我们只需实现层数最少的11层VGGNet。与AlexNet类似,在输入核心网络结构前,需要将输入数据放缩到 (224*224) 大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class VGGBlock(nn.Module):
def __init__(self, conv_num, in_channels, out_channels):
super(VGGBlock, self).__init__()
self.net = nn.Sequential()
for i in range(conv_num):
self.net.add_module(
"conv_{0}".format(i), nn.Sequential(
nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=3, padding=1),
nn.ReLU()
)
)
in_channels = out_channels
self.net.add_module("pool", nn.MaxPool2d(kernel_size=2, stride=2))

def forward(self, x):
return self.net(x)

class VGGNet(nn.Module):
def __init__(self, dropout):
super(VGGNet, self).__init__()
self.net = nn.Sequential(
nn.Upsample(scale_factor=8, mode='bilinear'),
VGGBlock(1, 1, 64),
VGGBlock(1, 64, 128),
VGGBlock(2, 128, 256),
VGGBlock(2, 256, 512),
VGGBlock(2, 512, 512),
nn.Flatten(),
nn.Linear(in_features=512 * 7 * 7, out_features=4096),
nn.ReLU(),
nn.Dropout(p=dropout),
nn.Linear(in_features=4096, out_features=4096),
nn.ReLU(),
nn.Dropout(p=dropout),
nn.Linear(in_features=4096, out_features=10)
)

def forward(self, x):
return self.net(x)


实验总结分析

代码实现过程遇到的问题及解决办法

因为读取数据文件后将数据转化成数组,对于后面的深度学习模型训练不能很好的兼容和格式匹配,因此需要将数据转换为张量,。张量是用于在这些框架中存储和操作数据的基本数据结构。通过将数据转换为张量,确保与框架的兼容性并实现与其他深度学习操作的无缝集成,从而简化数据处理和预处理任务。

  • 因此,
    我用两个张量torch.tensor(train_images,dtype=torch.float).view(-1, 1, 28, 28)torch.tensor(np.argmax(train_labels, axis=1), dtype=torch.long)创建torch.utils.data.TensorDataset的训练数据集。因为MNIST数据集中的原始图像尺寸为 (1$\times$$28$$\times$28),所以需要将train_images转化并重塑为维度(-1, 1, 28, 28)。

AlexNet和VGGNet框架是对于图像尺寸为(3 * 224 * 224)的,而MINIST数据集原始图像尺寸为 (1 * 28 * 28)的,这一输入在后几层卷积层中甚至无法完成一次卷积操作。

  • 因此首先需要将MINIST数据集放缩为(224 * 224)大小。

AlexNet的网络结构有着46764746个可训练参数,这一参数量几乎是LeNet的758倍,VGGNet可训练参数的规模甚至达到了超过1亿的级别,相比较AlexNet不到5000万的参数量多了整整一倍。对于如此大量的网络参数,往往需要借助GPU来进行训练和推理,但是因为没有GPU的条件,如果全部数据进行训练需要耗费很长很长时间。

  • 因此我创建一个采样器SequentialSampler(train_idx)选择原始训练数据前百分之十的样本进行训练。

实验结果对比分析

训练模型过程我统一使用交叉熵损失作为训练时的损失函数,为了比较各网络结构对分类结果的影响,我通过命令行使用了完全相同的超参数:
--model lenet --lr 0.001 --dropout 0.0
--model alexnet --lr 0.001 --dropout 0.0
--model resnet --lr 0.001 --dropout 0.0
--model vggnet --lr 0.001 --dropout 0.0

  • LetNet:

  • AlexNet:

  • ResNet:

  • VGGNet:


    可以看到,虽然LeNet的准确率最高,但是AlexNet和ResNet都是训练的部分训练数据而LeNet是全部训练数据。所以总的来说,针对MINISIT数据集,LeNet模型效果已经很好了,ResNet和AlexNet的效果相比,ResNet更好上一点,但是这两类模型在MINIST数据集上训练时间比LeNet要长很多很多,所以对于本次实验所用的这种特征较为简单的数据图像,我更倾向于使用LeNet模型。

影响模型性能的原因

  • 学习率:
    学习率主要是控制模型的学习进度或是速度。高学习率会使损失函数变化加快,但是往往得不到最优解,即在最优解附近来回震荡且震荡幅度较大。但是,使用低学习率可以确保我们不会错过任何局部极小值,同样也意味着我们将花费更长的时间来进行收敛,特别是在被困在高原区域的情况下。
    在对AlexNet和ResNet模型训练我分别设置了0.01和0.001的学习率,得到了差别很大的结果,0.001的结果在上面可以看到,而0.01的结果只有仅仅 12% 的准确率。因此学习率是影响模型性能的原因之一。
  • Batchsize:
    根据随机梯度下降算法原理: n(批量大小)也是影响模型性能收敛的重要参数,影响模型的泛化性能。
  • 优化算法:
    优化算法的选择,例如随机梯度下降 (SGD) 或其变体,如 Adam 或 RMSprop,会影响模型收敛和找到最优解的速度。不同的优化算法具有不同的收敛行为,会影响训练速度和最终性能。
    在本次实验中,我通过分别使用SGD、Adam优化器对各个模型进行训练,结果在不同优化器下,模型训练得到的结果也是大有差异。