PyTorch实现经典网络之AlexNet
简介
本文是使用PyTorch来实现大神Alex等人在论文ImageNet Classification with Deep Convolutional Neural Networks中提出的深度卷积神经网络AlexNet。Alex等人首次在大规模图像数据集中使用了深层卷积神经网络结构,在2012年ILSVRC大赛中大放异彩并获得冠军,其准确率远超第二名,在当时引起了巨大的轰动。AlexNet可以说是具有历史意义的一个网络结构,在此之前,深度学习已经沉寂了很长时间,自2012年AlexNet诞生之后,随后的ImageNet冠军都是用卷积神经网络(CNN)来做的,并且层次越来越深,使得CNN成为在图像识别分类的核心算法模型,带来了深度学习的大爆发。
模型特点
AlexNet模型主要包含以下特点:
- 使用ReLU激活函数
- 重叠池化操作
- 提出Dropout防止过拟合
- 数据集增强
- 使用多GPU训练
- LRN局部归一化
1. 使用ReLU激活函数
传统的神经网络普遍使用了Sigmoid或者tanh等非线性函数作为激活函数,然而它们很容易出现梯度消失或者梯度饱和的情况。因此AlexNet使用了线性整流函数ReLU,函数定义为,图像如下:

相比于传统的神经网络激活函数,诸如逻辑函数(Logistic sigmoid)和tanh等双曲函数,ReLU函数在很大程度上避免了梯度消失和梯度爆炸的问题,大大简化了计算过程,提高了神经网络的训练速度。
原论文在CIFAR-10数据集上对比验证了分别使用ReLU和tanh激活函数使的4层卷积神经网络的训练错误率随着训练轮次变化的情况。如下图:

其中实线代表的是使用ReLU激活函数的CNN网络,虚线代表的是使用tanh激活函数的CNN网络。可以看到使用ReLU的CNN网络大概经过6轮训练就得到了25%的错误率,而使用tanh的CNN网络经过了约36轮训练才达到同样的错误率,整体速度快了6倍左右。
2. 重叠池化操作
在一般的池化操作中,池化区域的窗口大小与步长相同的。AlexNet使用的池化操作是可重叠的,意思是在池化操作的过程中,池化窗口每次移动的步长小于池化窗口长度。AlexNet的池化窗口大小为3x3,但是移动的步长为2,故每次池化操作会有重叠部分,重叠池化操作可以在一定程度上避免过拟合。原论文中写道,重叠池化操作的应用使得模型在top-1和top-5上的识别错误率分别降低了0.4%和0.3%。
但也有最直接的缺点:
- 增加了计算量
- 丰富特征的同时无可避免也带来了部分特征信息的冗余。
3. 提出Dropout防止过拟合
俗话说,三个臭皮匠赛过诸葛亮,结合多种不同模型的结果是一种有效的降低错误率的方式。但是对于大型神经网络结果来说,训练一个模型就需要花费数天时间,如果训练多个的话,成本就显得太高了。因此引入“Dropout”技术,它会以50%的概率将隐藏层的神经元的输出置为0,被置为0的神经元不再参与网络的前馈和反向传播。因此这相当于,对于每一个输入,神经网络都采用了不同的结构来学习,但是所有不同的网络结构都是共享参数的。这项技术减少了神经元的复杂适应性,即网络结构的不停变化,导致某个神经元无法依赖于其他特定的几个神经元而存在,这会让网络被迫学习更强大更具有鲁棒性的特性。
Dropout只需要两倍的训练时间即可实现模型组合的效果,这种方式可以有效地减少过拟合。Dropout技术示意图如下:

4. 数据集增强
人工扩充测试数据集是一个有效的方式来避免模型过拟合。AlexNet中使用了下面一些技巧来扩充数据集。
-
图像镜像
对于测试集中的图片,可以对其做一个镜像(水平翻转)操作得到一张新的图片,通过这种简单的操作,我们可以使用我的测试数据集扩大一倍。如下图所示:
图像镜像
-
随机裁剪
除此之外,我们还可以在原始图像中随机裁剪若干块得到新的图像,这些图像可以认为是由原始图像的一些简单偏移得到的。比如我们可以在原始图像的中间和四个角的位置裁剪出5张图片出来。
随机裁剪
作者认为如果不进行数据扩充的话,深层的神经网络将无法使用,因为它会将导致严重的过度拟合问题。
5. 使用多GPU训练
AlexNet当时使用了两块GTX580的GPU进行训练,在每个GPU中放置一半核(或神经元),将网络分布在两个GPU上进行并行计算,大大加快了AlexNet的训练速度。
作者采用双GPU的设计模式,并且规定GPU只能在特定的层进行通信交流。作者的原意应该是想利用双GPU分担一半 的计算量,事实上也做到了,但是这里的两个子网络合并起来并不能等价于一个大网络,除非每层都有作者所谓的交互。作者的实验数据表示,two-GPU方案会比只用one-GPU跑半个上面大小网络的方案,在准确度上提高了1.7%的top-1和1.2%的top-5。当然,one-GPU的半个网络和two-GPU网络结构是不一样的,two-GPU有指标上的提升也并不奇怪。
6. LRN局部归一化
ReLUs具有良好的特性,它不需要通过输入归一化来防止饱和。如果至少有一些训练实例为ReLU产生了正的输入,那么这个神经元就会学习。然而,我们还是发现下面的这种归一化方法有助于泛化。令表示第
个卷积核在
位置上的计算结果通过ReLU之后的输出,那么相应归一化的输出值
定义如下:

其中求和部分的代表的与当前位置
相邻的卷积核的数量,而
代表的是这一层所有的卷积核数目(通道数)。其中常数
都是超参数,它们的值由验证集决定。论文作者取
。
作者在CIFAR-10数据集上验证了这种方法的有效性,没有使用LRN的四层CNN结构取得了13%的错误率,而使用了LRN的网络取得了11%的错误率。
LRN其实有些许争议,目前还没有较为详细的理论证明其真的有效,因此这里并不进行详细描述,感兴趣的童鞋可以参考这里。
模型结构
AlexNet模型的整体框架如下:
其中红色框里面是5个卷积层,蓝色框内是3个全连接层。
整个模型分为上下两部分,分别运行在不同的GPU上,注意看第3个卷积核,它融合了第二层的所有卷积核的输出,即这里涉及到了跨GPU的操作。全连接层的神经元与上一层的神经元全部连接,第一和第二卷积层之后是响应归一化层,最大池化层在响应归一化层和第5卷积层之后。ReLU非线性层在每个卷积层和全连接层的输出上。

以第一层卷积为例解释一下计算过程。上图描述的AlexNet的输入是彩色图像,大小为227x227,包含R、G、B3个通道,故输入大小为224x224x3。第一层卷积层包含了96个卷积核,大小是11x11x3(因为输入的图像是3通道的),步长为4。故输入经过了第一层卷积操作之后的大小为(227-11)/4+1=55,卷积核的数量决定了通道的数量,故第一层卷积输出的总大小为55x55x96。
其他层以此类推,就不再赘述,大家可以根据上图计算一下。
这里补充一个卷积核输出计算公式:
- W 表示输入图片尺寸(W×W)
- F 表示卷积核的大小(F×F)
- S 表示步长
- P 表示填充的像素数(Padding)
- N代表输出大小(NxN)
实验结果
作者分别在ILSVRC-2010和ILSVRC-2012测试集上进行了对比实验。结果如下:

可以看到AlexNet比之前方法取得的最好结果还要提升不少。
CIFAR-10 数据集
由于ImageNet数据集过于庞大,对于个人学习而言其实完全没有必要。因此本文实现的AlexNet是针对CIFAR-10数据集的。CIFAR-10数据集是一个彩色图像数据集,它的图片大小为32x32。一共包含10个分类,每一类包含6000张图片,总共60000张图片。其中训练集包含图片50000张,测试集包含图片10000张。
数据集官网网址在这里。下面是该数据集中的一些示例:

代码实践
本文实现的AlexNet模型在结构上与论文网络结构相差不大,也包含5个卷积层和3个全连接层,但是输入、输出、以及卷积层相关参数不一致,并且去掉了LRN操作。
模型定义相关代码:
import torch
import torch.nn as nn
class AlexNet(nn.Module):
def __init__(self, config):
super(AlexNet, self).__init__()
self._config = config
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, stride=1, padding=2),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Conv2d(192, 384, kernel_size=3, stride=1, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(384, 256, kernel_size=3, stride=1, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
)
# 自适应层,将上一层的数据转换成6x6大小的,而无需手动计算
self.avgpool = nn.AdaptiveAvgPool2d((6, 6))
self.classifier = nn.Sequential(
nn.Dropout(),
nn.Linear(256 * 6 * 6, 4096),
nn.ReLU(inplace=True),
nn.Dropout(),
nn.Linear(4096, 1024),
nn.ReLU(inplace=True),
nn.Linear(1024, self._config['num_classes']),
)
def forward(self, x):
x = self.features(x)
x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.classifier(x)
return x
def saveModel(self):
torch.save(self.state_dict(), self._config['model_name'])
def loadModel(self, map_location):
state_dict = torch.load(self._config['model_name'], map_location=map_location)
self.load_state_dict(state_dict, strict=False)
模型训练和测试相关参数设置写成了config的形式,batch size设置为500,采用Adam优化器,学习率0.0001,迭代次数设置为100,相关代码如下:
import torch
from AlexNet.network import AlexNet
from AlexNet.trainer import Trainer
from AlexNet.dataloader import LoadCIFAR10
from AlexNet.dataloader import Construct_DataLoader
from torch.autograd import Variable
alexnet_config = \
{
'num_epoch': 5,
'batch_size': 500,
'lr': 1e-3,
'l2_regularization':1e-4,
'num_classes': 10,
'device_id': 2,
'use_cuda': False,
'model_name': '../TrainedModels/AlexNet.model'
}
if __name__ == "__main__":
####################################################################################
# AlexNet 模型
####################################################################################
train_dataset, test_dataset = LoadCIFAR10(True)
# define AlexNet model
alexNet = AlexNet(alexnet_config)
####################################################################################
# 模型训练阶段
####################################################################################
# # 实例化模型训练器
# trainer = Trainer(model=alexNet, config=alexnet_config)
# # 训练
# trainer.train(train_dataset)
# # 保存模型
# trainer.save()
####################################################################################
# 模型测试阶段
####################################################################################
alexNet.eval()
alexNet.loadModel(map_location=torch.device('cpu'))
if alexnet_config['use_cuda']:
alexNet = alexNet.cuda()
correct = 0
total = 0
for images, labels in Construct_DataLoader(test_dataset, alexnet_config['batch_size']):
images = Variable(images)
labels = Variable(labels)
if alexnet_config['use_cuda']:
images = images.cuda()
labels = labels.cuda()
y_pred = alexNet(images)
_, predicted = torch.max(y_pred.data, 1)
total += labels.size(0)
temp = (predicted == labels.data).sum()
correct += temp
print('Accuracy of the model on the test images: %.2f%%' % (100.0 * correct / total))
测试结果
经过100次迭代之后,在训练集上取得了98.45%的正确率,
但是在测试集上只得到了71.96%的准确率.

初步估计可能是由以下问题导致的:
- 参数设置不恰当导致模型过拟合
- 图像数据没有归一化
- 网络结构需要改善
完整代码见https://github.com/HeartbreakSurvivor/ClassicNetworks/tree/master/AlexNet。