人工智能

PyTorch实现经典网络之AlexNet

2020-12-10  本文已影响0人  HaloZhang

简介

本文是使用PyTorch来实现大神Alex等人在论文ImageNet Classification with Deep Convolutional Neural Networks中提出的深度卷积神经网络AlexNet。Alex等人首次在大规模图像数据集中使用了深层卷积神经网络结构,在2012年ILSVRC大赛中大放异彩并获得冠军,其准确率远超第二名,在当时引起了巨大的轰动。AlexNet可以说是具有历史意义的一个网络结构,在此之前,深度学习已经沉寂了很长时间,自2012年AlexNet诞生之后,随后的ImageNet冠军都是用卷积神经网络(CNN)来做的,并且层次越来越深,使得CNN成为在图像识别分类的核心算法模型,带来了深度学习的大爆发。

模型特点

AlexNet模型主要包含以下特点:

  1. 使用ReLU激活函数
  2. 重叠池化操作
  3. 提出Dropout防止过拟合
  4. 数据集增强
  5. 使用多GPU训练
  6. LRN局部归一化

1. 使用ReLU激活函数

传统的神经网络普遍使用了Sigmoid或者tanh等非线性函数作为激活函数,然而它们很容易出现梯度消失或者梯度饱和的情况。因此AlexNet使用了线性整流函数ReLU,函数定义为f(x) = max(0,x),图像如下:

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技术示意图如下:


Dropout示意图

4. 数据集增强

人工扩充测试数据集是一个有效的方式来避免模型过拟合。AlexNet中使用了下面一些技巧来扩充数据集。

作者认为如果不进行数据扩充的话,深层的神经网络将无法使用,因为它会将导致严重的过度拟合问题。

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产生了正的输入,那么这个神经元就会学习。然而,我们还是发现下面的这种归一化方法有助于泛化。令a_{x,y}^i表示第i个卷积核在(x,y)位置上的计算结果通过ReLU之后的输出,那么相应归一化的输出值b_{x,y}^i定义如下:

其中求和部分的n代表的与当前位置(x,y)相邻的卷积核的数量,而N代表的是这一层所有的卷积核数目(通道数)。其中常数k,n,\alpha , \beta都是超参数,它们的值由验证集决定。论文作者取k=2,n=5,\alpha = 10^{-4}, \beta = 0.75
作者在CIFAR-10数据集上验证了这种方法的有效性,没有使用LRN的四层CNN结构取得了13%的错误率,而使用了LRN的网络取得了11%的错误率。
LRN其实有些许争议,目前还没有较为详细的理论证明其真的有效,因此这里并不进行详细描述,感兴趣的童鞋可以参考这里

模型结构

AlexNet模型的整体框架如下: AlexNet模型

其中红色框里面是5个卷积层,蓝色框内是3个全连接层。
整个模型分为上下两部分,分别运行在不同的GPU上,注意看第3个卷积核,它融合了第二层的所有卷积核的输出,即这里涉及到了跨GPU的操作。全连接层的神经元与上一层的神经元全部连接,第一和第二卷积层之后是响应归一化层,最大池化层在响应归一化层和第5卷积层之后。ReLU非线性层在每个卷积层和全连接层的输出上。

上面的图看着不是很清楚,下面的图详细标注了每一层的卷积核参数、以及输出特征大小的计算过程。下图跟原论文中的框架图有一些差异,不过并不影响读者理解AlexNet结构。 来自https://www.learnopencv.com/understanding-alexnet

以第一层卷积为例解释一下计算过程。上图描述的AlexNet的输入是彩色图像,大小为227x227,包含R、G、B3个通道,故输入大小为224x224x3。第一层卷积层包含了96个卷积核,大小是11x11x3(因为输入的图像是3通道的),步长为4。故输入经过了第一层卷积操作之后的大小为(227-11)/4+1=55,卷积核的数量决定了通道的数量,故第一层卷积输出的总大小为55x55x96。
其他层以此类推,就不再赘述,大家可以根据上图计算一下。

这里补充一个卷积核输出计算公式:
N=(W−F+2P)/S+1

  • W 表示输入图片尺寸(W×W)
  • F 表示卷积核的大小(F×F)
  • S 表示步长
  • P 表示填充的像素数(Padding)
  • N代表输出大小(NxN)

实验结果

作者分别在ILSVRC-2010和ILSVRC-2012测试集上进行了对比实验。结果如下: ILSVRC-2010测试集结果
ILSVRC-2012测试集结果

可以看到AlexNet比之前方法取得的最好结果还要提升不少。

CIFAR-10 数据集

由于ImageNet数据集过于庞大,对于个人学习而言其实完全没有必要。因此本文实现的AlexNet是针对CIFAR-10数据集的。CIFAR-10数据集是一个彩色图像数据集,它的图片大小为32x32。一共包含10个分类,每一类包含6000张图片,总共60000张图片。其中训练集包含图片50000张,测试集包含图片10000张。
数据集官网网址在这里。下面是该数据集中的一些示例:

CIFAR-10数据集

代码实践

本文实现的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

参考

上一篇 下一篇

猜你喜欢

热点阅读