动手学深度学习 5.6 深度卷积神经网络(AlexNet)

前言

从零开始学习ai文章系列计划是个人在《动手学深度学习》和《磨菇书》两本书的学习中的个人笔记,文章也会以课本中的章节分开,即每个章节一片笔记。我会尽量的把主要内容以及遇到的难点进行记录与解决,如果哪里有错误的欢迎指正。或者不清晰的可以直接查看原文部分。

《动手学深度学习》原文(课本):https://tangshusen.me/Dive-into-DL-PyTorch/#/

《动手学深度学习》代码:https://github.com/ShusenTang/Dive-into-DL-PyTorch

(由于有时候公式太多,可能会直接贴图片)

LeNet可以在早期的小数据集上取得好的成绩,但是在更大的真实数据集上的表现并不尽如人意。使用较干净的数据集和较有效的特征甚至比机器学习模型的选择对图像分类结果的影响更大。

AlexNet跟LeNet结构类似,但使用了更多的卷积层更大的参数空间来拟合大规模数据集ImageNet。它是浅层神经网络和深度神经网络的分界线。虽然看上去AlexNet的实现比LeNet的实现也就多了几行代码而已,但这个观念上的转变和真正优秀实验结果的产生令学术界付出了很多年。

1. 学习特征表示

在相当长的时间里,特征都是基于各式各样手工设计的函数从数据中提取的。事实上,不少研究者通过提出新的特征提取函数不断改进图像分类结果。这一度为计算机视觉的发展做出了重要贡献。

另一些研究者则持异议。他们认为特征本身也应该由学习得来。他们还相信,为了表征足够复杂的输入,特征本身应该分级表示。持这一想法的研究者相信,多层神经网络可能可以学得数据的多级表征,并逐级表示越来越抽象的概念或模式。

2. AlexNet

2012年,AlexNet横空出世。 AlexNet使用了8层卷积神经网络,并以很大的优势赢得了ImageNet 2012图像识别挑战赛。它首次证明了学习到的特征可以超越手工设计的特征,从而一举打破计算机视觉研究的前状。

图中虽然分了两层,但只要看下面完整的一层就行了

AlexNet与LeNet的设计理念非常相似,但也有显著的区别。

第一,与相对较小的LeNet相比,AlexNet包含8层变换,其中有5层卷积2层全连接隐藏层,以及1个全连接输出层。下面我们来详细描述这些层的设计。AlexNet第一层中的卷积窗口形状是11×11。因为ImageNet中绝大多数图像的高和宽均比MNIST图像的高和宽大10倍以上,ImageNet图像的物体占用更多的像素,第二层中的卷积窗口形状减小到5×5,之后全采用3×3。此外,第一、第二和第五个卷积层之后都使用了窗口形状为3×3、步幅为2的最大池化层。紧接着最后一个卷积层的是两个输出个数为4096的全连接层。这两个巨大的全连接层带来将近1 GB的模型参数。

第二,AlexNet将sigmoid激活函数改成了更加简单的ReLU激活函数。一方面,ReLU激活函数的计算更简单,例如它并没有sigmoid激活函数中的求幂运算。另一方面,ReLU激活函数在不同的参数初始化方法下使模型更容易训练。

第三,AlexNet通过丢弃法(参见3.13节)来控制全连接层的模型复杂度。而LeNet并没有使用丢弃法。“模型复杂度”可以理解为模型的拟合能力。一个太复杂的模型容易“记住”训练数据,导致在新数据上表现差,即过拟合。因此这里的意思是通过丢弃法解决过拟合问题。

第四,AlexNet引入了大量的图像增广,如翻转、裁剪和颜色变化,从而进一步扩大数据集来缓解过拟合。

下面我们实现稍微简化过的AlexNet。

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
class AlexNet(nn.Module):
def __init__(self):
super(AlexNet, self).__init__()
self.conv = nn.Sequential(
nn.Conv2d(1, 96, 11, 4), # in_channels, out_channels, kernel_size, stride, padding
nn.ReLU(),
nn.MaxPool2d(3, 2), # kernel_size, stride

# 减小卷积窗口,使用填充为2来使得输入与输出的高和宽一致,且增大输出通道数
nn.Conv2d(96, 256, 5, 1, 2),
nn.ReLU(),
nn.MaxPool2d(3, 2),

# 连续3个卷积层,且使用更小的卷积窗口。除了最后的卷积层外,进一步增大了输出通道数。
# 前两个卷积层后不使用池化层来减小输入的高和宽
nn.Conv2d(256, 384, 3, 1, 1),
nn.ReLU(),
nn.Conv2d(384, 384, 3, 1, 1),
nn.ReLU(),
nn.Conv2d(384, 256, 3, 1, 1),
nn.ReLU(),
nn.MaxPool2d(3, 2)
)

# 这里全连接层的输出个数比LeNet中的大数倍。使用丢弃层来缓解过拟合
self.fc = nn.Sequential(
nn.Linear(256*5*5, 4096),
nn.ReLU(),
nn.Dropout(0.5),

nn.Linear(4096, 4096),
nn.ReLU(),
nn.Dropout(0.5),

# 输出层。由于这里使用Fashion-MNIST,所以用类别数为10,而非论文中的1000
nn.Linear(4096, 10),
)

def forward(self, img):
feature = self.conv(img)
output = self.fc(feature.view(img.shape[0], -1))
return output

可以先通过以下代码确定每层输入输出是否有误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def forward(self, x):

print("Input:", x.shape)
for i, layer in enumerate(self.conv):
x = layer(x)
print(f"After conv layer {i}: {x.shape}")

x = torch.flatten(x, 1)

print("After flatten:", x.shape)
for i, layer in enumerate(self.fc):
x = layer(x)
print(f"After fc layer {i}: {x.shape}")
return x

填入测试数据查看结果

1
2
3
net = AlexNet()
input_tensor = torch.randn(1, 1, 224, 224) # 例如用224x224输入
output = net(input_tensor)

下一节VGG网络中,我们会使用更加方便的工具进行网络层参数的审查

3. 读取数据

虽然论文中AlexNet使用ImageNet数据集,但因为ImageNet数据集训练时间较长,我们仍用前面的Fashion-MNIST数据集来演示AlexNet。

读取数据的时候我们额外做了一步将图像高和宽扩大到AlexNet使用的图像高和宽224。这个可以通过torchvision.transforms.Resize实例来实现。torchvision.transforms.Resize 在放大图像时,会使用插值方式来填充像素,从而生成目标大小的图像。

也就是说,我们在ToTensor前使用Resize,然后使用Compose实例来将这两个变换串联以方便调用。

4. 训练

这时候我们可以开始训练AlexNet了。相对于LeNet,由于图片尺寸变大了而且模型变大了,所以需要更大的显存,也需要更长的训练时间了。

1
2
3
4
5
lr, num_epochs = 0.001, 5
optimizer = torch.optim.Adam(net.parameters(), lr=lr)

if __name__ == '__main__':
d2l.train_ch5(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs)

与LeNet相比提升了很多,当然训练所需时间也多了很多

5.小结

LeNet是仅有2层卷积的网络,而AlexNet则可以理解为更多卷积层的LeNet网络。