论文解读与复现:Alexnet-ImageNet Classification with Deep Convolutional Neural Networks

ImageNet Classification with Deep Convolutional Neural Networks

今天要进行细读和复现的论文是Alexnet,在谷歌学术上12万的引用量的文章,也是深度学习的开山之作
 

摘要:论文训练了一个大型的深度卷积神经网络,将ImageNet LSVRC-2010比赛中的120万张高分辨率图像分类为1000个不同的类别。在测试数据上,实现了37.5%的top-1错误率和17.0%的top-5错误率。这个神经网络具有6000万个参数和65万个神经元,由五个卷积层组成,其中一些后面跟着最大池化层,还有三个全连接层,最后是一个包含1000个类别的softmax层。为了加快训练速度,论文使用了非饱和神经元和卷积操作的高效GPU实现。为了减少全连接层的过拟合,采用了“dropout”的正则化方法。Alexnet在ILSVRC-2012比赛中提交了该模型的一个变体,并在测试中获得了15.3%的top-5错误率,而第二名的错误率为26.2%。

INTRODUCTION:目标识别的方法主要使用机器学习方法。为了提高它们的性能,我们可以收集更大的数据集,学习更强大的模型,并使用更好的防止过拟合的技术。直到最近,带有标签的图像数据集相对较小,数量在几万张图像左右(例如NORB、Caltech-101/256和CIFAR-10/100)。但是要从数百万张图像中学习数千个对象,就需要一个具有大学习能力的模型。卷积神经网络(CNNs)是这类模型之一。它们的容量可以通过改变它们的深度和广度来控制,而且它们还对图像的性质(即统计的静止性和像素依赖性的局部性)进行了强有力且大部分正确的假设。因此,与具有相似大小层的标准前馈神经网络相比,CNN具有更少的连接和参数,因此更容易训练,而它们的理论最佳性能可能只稍逊于最佳。

数据集:ImageNet是一个包含超过1500万标记的高分辨率图像的数据集,涵盖了大约22,000个类别。这些图像是从网络上收集的,并由人类标记者使用亚马逊的Mechanical Turk众包工具进行标记。从2010年开始,作为Pascal Visual Object Challenge的一部分,每年都会举行一项名为ImageNet Large-Scale Visual Recognition Challenge(ILSVRC)的竞赛。ILSVRC使用ImageNet的一个子集,每个子集中包含大约1000个类别的1000张图像。总共有大约120万张训练图像,5万张验证图像和15万张测试图像。

ILSVRC-2010是唯一提供测试集标签的ILSVRC版本,因此也是论文使用的数据集。同时论文也参加了ILSVRC-2012竞赛,在本论文中也进行训练和比较。在ImageNet上,通常报告两个错误率:top-1和top-5,其中top-5错误率是模型认为最有可能的五个标签中不包含正确标签的测试图像的比例。

ImageNet包含可变分辨率的图像,而系统需要恒定的输入维度。因此,需要将图像降采样到固定分辨率256×256。对于给定的矩形图像,我们首先将图像缩放,使较短的一侧长度为256,然后从结果图像中裁剪出中心的256×256补丁。但是Alexnet没有进行图像预处理,除了从每个像素中减去训练集上的均值活动,直接原始RGB像素值上训练网络。

网络架构:包含八个可学习层,其中包括五个卷积层和三个全连接层。如下图


(这里看着貌似很复杂,不要慌,在论文复现的时候容我再对这个结构进行分析和重构)

标准的神经元激活函数通常使用 Tanh函数或Sigmoid函数。在使用梯度下降进行训练时,这些饱和非线性比非饱和非线性Relu函数要慢得多。根据Nair和Hinton的方法,我们将具有这种非线性的神经元称为修正线性单元(Rectified Linear Units, ReLUs)。使用ReLUs的深度卷积神经网络训练速度比使用tanh单元的等效网络快几倍。

在论文中Alexnet的训练是使用了两个RTX580进行训练,单个GTX 580 GPU只有3GB的内存,这限制了可以在其上训练的网络的最大大小。事实证明,120万个训练样本足以训练网络,而这些网络太大,无法放入一个GPU中。因此,我们将网络扩展到两个GPU上。当前的GPU非常适合跨GPU并行化,因为它们能够直接读取和写入彼此的内存,而无需经过主机内存。论文采用的并行化方案基本上将每个GPU的卷积核(或神经元)一分为二,加上一个额外的技巧:GPU只在某些层之间进行通信。

ReLUs具有一个特性,即它们不需要输入归一化来防止它们饱和。如果至少一些训练样本对ReLU产生正输入,那么该神经元将进行学习。然而,我们仍然发现以下本地归一化方案有助于泛化。响应归一化活性x, y由以下表达式给出:

 其中求和是在相同空间位置上的n个“相邻”卷积核图上进行的,N是该层中的总卷积核数。卷积核图的排序当然是任意的,并且是在训练开始之前确定的。这种响应归一化实施了一种受到真实神经元中发现的侧抑制启发的形式,为使用不同卷积核计算的神经元输出之间的大活动创建竞争。常数 \alpha \beta n k是通过验证集确定的超参数;论文使用了k=2,n=5,α=10−4,β=0.75。

在CNN中,池化层总结了同一卷积核图中相邻神经元组的输出。传统上,由相邻池化单元总结的邻域不重叠。更准确地说,可以将池化层看作由相距s像素的池化单元组成的网格,每个单元总结以该池化单元的位置为中心的大小为z × z的邻域。如果设置s = z,则获得在CNN中常用的传统本地池化。如果设置s < z,则获得重叠池化。论文发现s = 2和z = 3与非重叠方案s = 2、z = 2相比,这个方案将top-1和top-5错误率分别降低了0.4%和0.3%,其产生的输出具有相同的维度。通常在训练过程中观察到,具有重叠池化的模型在一定程度上更难过拟合。 这一点其实涉及到卷积神经网络的先验知识的保留,重叠池化可以使输出特征更具有连续性。而非重叠方案,就像把特征图切成一块块,在每一块中取特定的数值,块与块之间缺少特征保留。

 Alexnet的整体结构

如图所示,网络包含八个带有权重的层;前五个是卷积层,剩下的三个是全连接层。最后一个全连接层的输出被馈送到一个1000路softmax,产生了对1000个类标签的分布。我们的网络最大化多项式逻辑回归目标,这等效于最大化在预测分布下正确标签的对数概率在所有训练样本上的平均。

第二、四和五个卷积层的卷积核仅连接到前一层同一GPU上的卷积核图(参见图2)。第三个卷积层的卷积核与第二层的所有卷积核图相连。全连接层中的神经元与前一层的所有神经元相连。响应规范化层跟随第一和第二个卷积层。如第4.4节所述,最大池化层跟随响应规范化层以及第五个卷积层。ReLU非线性被应用于每个卷积和全连接层的输出。

第一个卷积层使用大小为11 × 11 × 3的96个卷积核对224 × 224 × 3的输入图像进行滤波,步长为4像素(这是相邻神经元在卷积核图中感受野中心之间的距离)。第二个卷积层以第一个卷积层的(响应规范化和池化后的)输出为输入,并使用大小为5 × 5 × 48的256个卷积核进行滤波。第三、四和五个卷积层直接相互连接,没有任何池化或规范化层。第三个卷积层有384个大小为3 × 3 × 256的卷积核,连接到第二个卷积层的(规范化、池化后的)输出。第四个卷积层有384个大小为3 × 3 × 192的卷积核,第五个卷积层有256个大小为3 × 3 × 192的卷积核。全连接层每个有4096个神经元。

论文的训练超参数,论文中设置的batch size为128,采用SGD作为优化策略,动量选择为0.9,并且权重衰减为0.0005

到论文的后面,就卖一下最终的结果,Alexnet的提出,是具有跨时代意义的,是使用CNN与其他机器学习的方法进行比较,并且CNN均领先近80个点。

下图为三种方法在ILSVRC-2010上的结果对比

接下来就是论文复现的部分,这里说明一下,我采用的事CIFAR-10这个数据集,论文中也提到CIFAR-10的训练精度,而且CIFAR-10比较小,适合新手上手操作的数据集。

CIFAR-10的训练精度图,其中实线采用的是ReLu作为激活函数,虚线用的是Tanh

代码复现部分

其实在目前的实际使用中,Alexnet是不需要使用分卡跑,因为现在的显卡已经很强大了,哪怕使用CPU跑Alexnet模型、CIFAR10数据集,也仅需要几十分钟。下面是简化版之后的Alexnet模型框架 ,大框表示输入尺寸,小框表示卷积核大小,通过五次下采用和展平变成一维向量用于最后的分类,具体的参数参考下面的网络结构代码

网络结构代码如下: 

import torch.nn as nn
import torch


class AlexNet(nn.Module):
    def __init__(self, num_classes=1000, init_weights=False):
        super(AlexNet, self).__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3, 48, kernel_size=11, stride=4, padding=2),  # input[3, 224, 224]  output[48, 55, 55]
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),                  # output[48, 27, 27]
            nn.Conv2d(48, 128, kernel_size=5, padding=2),           # output[128, 27, 27]
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),                  # output[128, 13, 13]
            nn.Conv2d(128, 192, kernel_size=3, padding=1),          # output[192, 13, 13]
            nn.ReLU(inplace=True),
            nn.Conv2d(192, 192, kernel_size=3, padding=1),          # output[192, 13, 13]
            nn.ReLU(inplace=True),
            nn.Conv2d(192, 128, kernel_size=3, padding=1),          # output[128, 13, 13]
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),                  # output[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),
        )
        if init_weights:
            self._initialize_weights()

    def forward(self, x):
        x = self.features(x)
        x = torch.flatten(x, start_dim=1)# 把二维向量展平为一维向量,用于最后的分类
        x = self.classifier(x)
        return x

    def _initialize_weights(self):# 权重初始化
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
                if m.bias is not None:
                    nn.init.constant_(m.bias, 0)
            elif isinstance(m, nn.Linear):
                nn.init.normal_(m.weight, 0, 0.01)
                nn.init.constant_(m.bias, 0)

使用CIFAR10数据集,完整代码如下:

import numpy as np
import torch
import torchvision
import argparse
from torch import nn
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
from torchvision import transforms
from torchvision.transforms import InterpolationMode
from alexnet import AlexNet
import ssl

# 全局取消证书验证,否则在下载数据集时可能会出现证书验证问题报错
ssl._create_default_https_context = ssl._create_unverified_context

device= torch.device('cuda' if torch.cuda.is_available() else 'cpu')# 查看cuda是否能用
print(device)

def train(args):
    # 超参数设置,方便管理
    num_epochs = args.max_epoch
    batch_size = args.batch_size
    learning_rate = args.lr
    image_size = args.image_size
    momentum = args.momentum
    # 设置数据集的格式
    transform = transforms.Compose([transforms.Resize((image_size, image_size),
                                    interpolation=InterpolationMode.BICUBIC),
                                    transforms.ToTensor(),
                                    transforms.Normalize(mean=[0.4914, 0.4822, 0.4465], std=[0.247, 0.2435, 0.2616])
                                    ])
    # 数据加载
    # 如果没有这个数据集的话会自动下载
    train_data = torchvision.datasets.CIFAR10(root="dataset",download=True,transform=transform,train=True)
    test_data = torchvision.datasets.CIFAR10(root="dataset",download=True,transform=transform,train=False)

    train_dataloader = DataLoader(train_data, batch_size=batch_size)
    test_dataloader = DataLoader(test_data, batch_size=batch_size)
    print('Dataload is Ready')
    # 添加tensorboard路径
    writer = SummaryWriter(log_dir=args.SummerWriter_log)
    # 模型加载
    model = AlexNet(num_classes=10).to(device)
    # 参数量估计
    total = sum([param.nelement() for param in model.parameters()])
    print("Number of parameters: %.2fM" % (total / 1e6))
    # Loss and optimizer
    # 选择交叉熵作为损失函数
    criterion = nn.CrossEntropyLoss()
    # 选择SGD为优化器
    optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate, momentum=momentum)
    total_train_step = 0#记录训练次数
    total_test_step=0#记录测试次数


    # 开始训练
    for epoch in range(num_epochs):
        print("---------------第{}轮训练开始-------------".format(epoch + 1))
        for i, (images, labels) in enumerate(train_dataloader):
            images = images.to(device)
            labels = labels.to(device)

            # Forward pass
            outputs = model(images)
            loss = criterion(outputs, labels)

            # Backward and optimize
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            total_train_step = total_train_step + 1

            if (i + 1) % args.print_map_epoch == 0:# 100次显示一次loss
                print("Epoch [{}/{}], Step [{}] Loss: {:.4f}"
                          .format(epoch + 1, num_epochs, total_train_step, loss.item()))
            writer.add_scalar("train_loss", loss.item(), total_train_step)

        # Test the model
        model.eval()
        total_test_loss = 0
        total_accuary = 0  # 正确率
        with torch.no_grad():
            correct = 0
            total = 0
            for images, labels in test_dataloader:
                images = images.to(device)
                labels = labels.to(device)
                outputs = model(images)
                loss = criterion(outputs, labels)
                _, predicted = torch.max(outputs.data, 1)
                total += labels.size(0)
                correct += (predicted == labels).sum().item()
                total_test_loss += loss
                total_accuary += correct
            print('Accuracy of the model on the test images: {} %'.format(100 * correct / total))
            writer.add_scalar("test_loss",total_test_loss,total_test_step)
            writer.add_scalar("test_accuary", correct / total, total_test_step)
            total_test_step += 1
    # Save the model checkpoint
    torch.save(model, 'weights.pth')



if __name__ == "__main__":
    parser = argparse.ArgumentParser()

    '''------------------------------------------调  节  部  分------------------------------------------------------'''
    parser.add_argument('--max_epoch', type=int, default=40, help='total epoch')
    parser.add_argument('--device_num', type=str, default='cpu', help='select GPU or cpu')
    parser.add_argument('--image_size', type=int, default=224, help='if crop img, img will be resized to the size')
    parser.add_argument('--batch_size', type=int, default=32, help='batch size, recommended 16')
    parser.add_argument('--lr', type=float, default=0.001, help='learning rate')
    parser.add_argument('--momentum', type=float, default=0.90, help='choice a float in 0-1')
    parser.add_argument('--print_map_epoch', type=int, default=100, help='')
    parser.add_argument('--SummerWriter_log', type=str, default='Alexnet', help='')


    args = parser.parse_args()

    train(args)






数据分析:

用tensorboard打开图表可以观察到,训练10个epoch,19511次,训练集的loss达到0.2左右

 测试集的精准度如下图,最高的准确率约73%,由于我这里设置的batch size比较小,就导致需要在第八个epoch准确率才到达73%

 由于我设置的学习率太大,导致随机梯度下降时已经越过最优点,loss开始上升,模型变得不可控后续如果更精准的数据的话可以减少学习率重复进行训练,可以达到一个不错的值,下面是测试集的loss,可以发现在第6个epoch的loss已经最小了,由于越过最优点,就导致loss后面不断上升

 

  • 25
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值