神经网络模型AlexNet与论文学习

AlexNet


AlexNet最早是由 Alex Krizhevsky 等人于2012年提出的,论文标题为 《Imagenet classification with deep convolutional neural networks》

一、论文学习

1、论文简介

本篇本文提出了采用了关键技术,例如 R e L U ReLU ReLU激活函数, D r o p o u t Dropout Dropout等技术,同时使用了双GPU进行模型的训练,这些技术在当下的深度学习模型中也经常使用,因此这篇文章具有非常高的学习价值

2、论文重点
(1)ReLU激活函数

一般的模型训练使用的是饱和非线性激活函数,例如 f ( x ) = t a n h ( x ) = ( 1 + e − x ) − 1 f(x)=tanh(x)=(1+e^{-x})^{-1} f(x)=tanh(x)=(1+ex)1,本文使用的是 R e L U ReLU ReLU激活函数,即: f ( x ) = max ⁡ ( 0 , x ) f(x)=\max(0,x) f(x)=max(0,x),是一种不饱和的激活函数,主要有以下三个优点:

  • 使网络训练更快
  • 防止梯度消失(弥散)
  • 使网络具有稀疏性

在这里插入图片描述
上图为论文中的插图,从上图可以看出,使用 R e L U ReLU ReLU激活函数(实线)比 t a n h tanh tanh激活函数训练速度更快

(2)多GPU训练

本文采用了双GPU对模型进行并行训练,提高了模型训练的速度。双GPU或多GPU训练都会涉及到不同GPU数据的交流问题,本文中的这个交流问题在介绍AlexNet结构时着重讲解

(3)Local Response Normaliza(LRN)
  • LRN翻译为局部响应标准化,有助于 A l e x N e t AlexNet AlexNet泛化能力的提升,是收到了真实神经元侧抑制现象(lateral inhibition)启发而提出的一种方法,所谓的侧抑制,是一种生物学上的概念,指的是细胞分化变为不同时,它会对周围细胞产生抑制信号,阻止它们向相同方向分化,最终表现为细胞命运的不同

  • L R N LRN LRN公式:
    b x , y i = a x , y i / ( k + α ∑ j = max ⁡ ( 0 , i − n / 2 ) min ⁡ ( N − 1 , i + n / 2 ) ( a x , y j ) 2 ) β b_{x, y}^{i}=a_{x, y}^{i} /\left(k+\alpha \sum_{j=\max (0, i-n / 2)}^{\min (N-1, i+n / 2)}\left(a_{x, y}^{j}\right)^{2}\right)^{\beta} bx,yi=ax,yi/k+αj=max(0,in/2)min(N1,i+n/2)(ax,yj)2β

  • 公式参数介绍:

  • a i a^i ai:代表第 i i i个神经元的激活值

  • b i b^i bi:代表第 i i i个神经元通过LRN操作后的激活值

  • k k k:超参数,由原型中的bias指定

  • α \alpha α:超参数,由原型中的alpha指定

  • β \beta β:超参数,由原型中的beta指定

  • n / 2 n/2 n/2:超参数,由原型中的deepth_radius指定

  • x , y x,y x,y:像素的位置,公式中用不到

  • i , j i,j i,j:代表通道 channel

  • 示意图:

在这里插入图片描述

从上图中可以看出, x , y x,y x,y分别代表的是 widthheight ,而 i , j i,j i,j代表的是 channel 的索引,这里假设的 channel 索引为0到 N − 1 N-1 N1,因此可以看出公式中的求和符号 Σ \Sigma Σ的上下限求最大和最小的目的就是为了防止索引超过 channel 的范围,而 n / 2 n/2 n/2表示只有在距离第 i i i个神经元 n / 2 n/2 n/2范围内的神经元才会对第 i i i个神经元有抑制作用

  • 局限性:在本文的数据集上,通过 L R N LRN LRN方法,使得模型准确率提高了1%,但是2014年的一篇文章证明了 L R N LRN LRN并不是一种通用的方法,是无效的,而且现阶段有更好的正则化方法,例如 Batch Normalization 等,因此 LRN 方法逐渐被淘汰
(4)Overlapping Pooling

如果定义 z z zkernel size 的大小, s s s为步长大小( stride ),则通常情况下是 s = z s=z s=z,这种情况下是没有重叠的,但是如果 s < z s<z s<z,就会出现重叠的情况,我们称这种情况为 Overlapping Pooling ,论文中是采用了 s = 2 , z = 3 s=2,z=3 s=2,z=3的池化层,提升了模型精度。

(5)数据增强方法(Data Augmentation)

数据增强能够有效地避免过拟合,论文中主要针对图片的位置色彩进行数据增强,文章通过这两个方面的数据增强,能够从一张图片得到了2048张图片。

  • 方法一:针对位置
  • 训练阶段:
    1. 图片统一缩放至 256 × 256 256\times256 256×256
    2. 随机位置裁剪出 224 × 224 224\times224 224×224区域
    3. 随机进行水平翻转
  • 测试阶段:
    1. 图片统一缩放至 256 × 256 256\times256 256×256
    2. 裁剪出5个 224 × 224 224\times224 224×224区域
    3. 均进行水平翻转,共得到10张 224 × 224 224\times224 224×224图片
  • 方法二:针对色彩
  • 通过PCA方法修改RGB通道的像素值,实现颜色扰动,但是这种方法效果有限

几点说明:

  • 从上面对位置的操作可以得到为什么能够从一张图片得到了2048张图片,由于裁剪过程是随机选择,因此有 ( 256 − 244 ) 2 = 1024 (256 - 244)^2 =1024 (256244)2=1024种可能,同时又进行水平翻转,因此再乘2,得到2048
  • 训练阶段的裁剪是随机裁剪,并且论文中提到,是需要先将短边裁剪到224像素,然后再在长边中心裁剪得到 224 × 224 224\times224 224×224;在测试阶段的裁剪并不是随机的,而是在左上、左下、右上、右下以及中心这五个位置进行裁剪,然后翻转得到10张图片,并且这10张图片都要输入到模型中得到的概率值再取平均
  • 对色彩的处理提升效果并不明显,并且涉及到矩阵的分解(PCA),因此现阶段很少用到这种方法对图像色彩进行扰动
(6)Dropout

Dropout技术是一个非常实用的减轻过拟合现象的一种技术,在现阶段的深度学习中也是一种非常常用的技术

在这里插入图片描述

如上图所示,左边为不使用Dropout的情况,下一层的某一神经元与上一层的每个神经元都保持连接,而右边是使用了Dropout的情况,可以看出下一层的某一神经元只与上一层的某些神经元保持连接,具体与哪些神经元连接并不是固定不变的,这就体现出了随机的效果,通常会设置 dropout probability 来实现**“随机”**,一般设置为0.5,这里需要注意的是,Dropout是用于训练过程中,因此在测试过程中,为了保证数据尺度的一致性,我们必须对测试过程中的神经元输出值乘以 dropout probability ,这一点是非常关键的

3、AlexNet结构
  • 论文中提到AlexNet包含有八个带权值的层,其中有5个是卷基层,有3个是全连接层,这仅仅是带权值的层,如果加上不带权值的层,例如池化层,就不止8个层了
  • 在5个卷积层中,第2、4、5个卷积层均只与在同一GPU上的前一层进行连接,而 第3层是对双GPU上的所有前一层的神经元都进行了连接,完成了不同GPU上信息的交流
  • LRN的位置在第一和第二个卷积层中间,而池化层的位置在第1、2、5卷积层后面,ReLU用于所有的卷积层和全连接层

在这里插入图片描述

  • 从上图中可以看出,整个网络的结构分为上下两层,表示两个GPU上的训练(注意,上下两层应该是完全一样的,但是原文的图片上层是不完整的,理论上应该是与下层结构完全一致),输入的是一个 224 × 224 × 3 = 150528 224\times224\times3 = 150528 224×224×3=150528维的向量,输出的是一个1000维的向量,代表的是1000类的分类结果
  • 从上图中可以看出,图中并没有包含到ReLU、LRN等操作,只有卷积层和全连接层,每一层后的后续操作如下:
  • C o n v 1   →   R e L U   →   P o o l   →   L R N Conv1\ \rightarrow \ ReLU \ \rightarrow \ Pool \ \rightarrow \ LRN Conv1  ReLU  Pool  LRN
  • C o n v 2   →   R e L U   →   P o o l   →   L R N Conv2\ \rightarrow \ ReLU \ \rightarrow \ Pool \ \rightarrow \ LRN Conv2  ReLU  Pool  LRN
  • C o n v 3   → R e L U Conv3\ \rightarrow ReLU Conv3 ReLU
  • C o n v 4   →   R e L U Conv4\ \rightarrow \ ReLU Conv4  ReLU
  • C o n v 5   →   R e L U   →   P o o l Conv5\ \rightarrow \ ReLU \ \rightarrow \ Pool Conv5  ReLU  Pool

在这里插入图片描述

  • AlexNet中,每个阶段特征图的变化情况如下图所示,这里与论文不同的是输入的像素为 227 × 227 227\times227 227×227,其实在早期的AlexNet中,就是 227 × 227 227\times227 227×227的输入,这对于结果是没有任何影响的,因为根据卷积输出特征图大小公式: F o = ⌊ F i n − k s + 2 p s ⌋ + 1 F_{o}=\left \lfloor \frac{F_{\mathrm{in}}-k_{s}+2 p}{s}\right \rfloor+1 Fo=sFinks+2p+1,如果是 227 × 227 227\times227 227×227,则 p a d d i n g = 0 padding = 0 padding=0 p = 0 p=0 p=0,因此输出的特征图大小为 ⌊ 227 − 11 4 ⌋ + 1 = 55 \lfloor \frac{227-11}{4} \rfloor+1 = 55 422711+1=55,如果是 224 × 224 224\times224 224×224,则需要加入 p a d d i n g padding padding,通常 p a d d i n g = 2 padding=2 padding=2,因此输出的特征图大小为 ⌊ 224 − 11 + 2 t i m e s 2 4 ⌋ + 1 = 55 \lfloor \frac{224-11+2\\times 2}{4} \rfloor+1 = 55 422411+2times2+1=55,因此两种情况下卷积后得到的特征图大小相同,都为 55 × 55 55\times55 55×55,而通道数都是96,这是因为卷积核的个数为96个,因此通道数为96,后面的特征图大小的计算都可以通过计算得到。而论文中提到了AlexNet结构一共有六千万的参数,这里的计算方式是将每一层的参数个数计算出来再相加,而对某一层的参数个数的计算公式为: c o u n t = F i × ( K s × K s ) × K n + K n count = F_{i} \times\left(K_{\mathrm{s}} \times K_{\mathrm{s}}\right) \times K_{n}+K_{n} count=Fi×(Ks×Ks)×Kn+Kn,其中 F i F_{i} Fi为通道数, K s K_s Ks为卷积核的大小, K n K_n Kn为卷积核的个数,最后加上的 K n K_n Kn称之为偏置( b i a s bias bias),以第一层为例,第一层的参数个数为 3 × ( 11 × 11 ) × 96 + 96 = 34944 3\times(11\times11)\times96+96 = 34944 3×(11×11)×96+96=34944,每一层的参数个数计算如下图所示:

在这里插入图片描述

从上图中可以看出,FC1这个全连接层的参数个数占到了总参数个数的一半以上,因此到了后期的神经网络模型,FC层使用率大大下降,因为全连接层会占据大量的内存

4、实验分析
(1)卷积可视化

在这里插入图片描述
论文将第一个卷积层后的特征图进行了可视化,一共是2个GPU上的96个卷积核,前3行为第一个GPU学习到的特征,主要学习到了图片的纹路等结构特征,而后3行为第二个GPU学习到的特征,主要学习到了图片色彩方面的特征,可以看出,两个GPU上的学习到的特征并不相同

(2)高级特征的相似性

论文实验发现,ALexNet提取到的高级特征之间具有很强的相关性,即相似图片的第二个全连接层输出特征向量的欧式距离相近,如下图所示,如果两张图片的第二个全连接层输出特征向量的欧式距离很近,则这两张图片应该是非常相似的,此外,论文中提到这种相似的图片在没有输入到模型时,计算两张图片的欧式距离并不是非常小的,这就启发我们可以使用 4096 4096 4096的特征向量进行比较相似性,这就可以用来做图像的检索图像的编码图像的聚类,这样可以大大的减小复杂度。

在这里插入图片描述

3、论文总结与启发
(1)关键点
  • 大量带标签数据——ImageNet
  • 高性能计算资源——GPU
  • 合理算法模型——深度卷积神经网络
(2)创新点
  • 采用ReLu加快大型神经网络训练
  • 采用LRN提升大型网络泛化能力
  • 采用Overlapping Pooling提升指标
  • 采用随机裁剪翻转及色彩扰动增加数据多样性
  • 采用Drpout减轻过拟合
(3)启发点
  • 深度与宽度可决定网络能力
  • 更强大GPU及更多数据可进一步提高模型性能
  • 图片缩放细节,对短边先缩放
  • ReLU不需要对输入进行标准化来防止饱和现象,即说明sigmoid/tanh激活函数有必要对输入进行标准化

二、代码实现

代码实现采用猫狗数据集,使用Pytorch实现,主要有下面5个部分

1、构建DataLoader

分别构建训练集和测试集对应的DataLoader

train_loader = DataLoader(dataset=train_data, batch_size=BATCH_SIZE, shuffle=True)
valid_loader = DataLoader(dataset=valid_data, batch_size=4)

这里需要构建一个Dataset类——CatDogDataset,并使用下面两行代码构建MyDataset实例:

train_data = CatDogDataset(data_dir=data_dir, mode="train", transform=train_transform)
valid_data = CatDogDataset(data_dir=data_dir, mode="valid", transform=valid_transform)

其中CatDogDataset类代码实现如下:

class CatDogDataset(Dataset):
    def __init__(self, data_dir, mode="train", split_n=0.9, rng_seed=620, transform=None):
        """
        猫狗分类任务的Dataset
        :param data_dir: str, 数据集所在路径
        :param transform: torch.transform,数据预处理
        """
        self.mode = mode
        self.data_dir = data_dir
        self.rng_seed = rng_seed
        self.split_n = split_n
        self.data_info = self._get_img_info()  # data_info存储所有图片路径和标签,在DataLoader中通过index读取样本
        self.transform = transform

    def __getitem__(self, index):
        path_img, label = self.data_info[index]
        img = Image.open(path_img).convert('RGB')     # 0~255

        if self.transform is not None:
            img = self.transform(img)   # 在这里做transform,转为tensor等等

        return img, label

    def __len__(self):
        if len(self.data_info) == 0:
            raise Exception("\ndata_dir:{} is a empty dir! Please checkout your path to images!".format(self.data_dir))
        return len(self.data_info)

    def _get_img_info(self):

        img_names = os.listdir(self.data_dir)
        img_names = list(filter(lambda x: x.endswith('.jpg'), img_names))

        random.seed(self.rng_seed)
        random.shuffle(img_names)

        img_labels = [0 if n.startswith('cat') else 1 for n in img_names]

        split_idx = int(len(img_labels) * self.split_n)  # 25000* 0.9 = 22500
        # split_idx = int(100 * self.split_n)
        if self.mode == "train":
            img_set = img_names[:split_idx]     # 数据集90%训练
            # img_set = img_names[:22500]     #  hard code 数据集90%训练
            label_set = img_labels[:split_idx]
        elif self.mode == "valid":
            img_set = img_names[split_idx:]
            label_set = img_labels[split_idx:]
        else:
            raise Exception("self.mode 无法识别,仅支持(train, valid)")

        path_img_set = [os.path.join(self.data_dir, n) for n in img_set]
        data_info = [(n, l) for n, l in zip(path_img_set, label_set)]

        return data_info

在构建自定义的Dataset时,需要重写__getitem____len__这两个方法,__len__主要是记录数据的大小,而__getitem__是通过图片索引寻找图片,这里需要主要的是__getitem__是会不停的被调用,因此不能在__getitem__中进行过多的操作,否则会导致训练过程非常缓慢,对于图片信息的获取就不建议放到__getitem__中,这里是将图片信息的获取过程单独写成一个函数_get_img_info,通过这个函数得到所有图片的信息并存储到一个列表中,这样在__getitem__中就只需要传入这个列表对对列表中的元素就行索引,得到图片的信息,避免了重复操作,加快了代码的运行速度

2、构建模型

Pytorch中已经实现了 A l e x N e t AlexNet AlexNet,因此直接调用即可,在调用后需要加载预训练参数以加快模型训练速度:

alexnet_model = get_model(path_state_dict, False)

def get_model(path_state_dict, vis_model=False):
    """
    创建模型,加载参数
    :param path_state_dict:
    :return:
    """
    model = models.alexnet()
    pretrained_state_dict = torch.load(path_state_dict)
    model.load_state_dict(pretrained_state_dict)

    if vis_model:
        from torchsummary import summary
        summary(model, input_size=(3, 224, 224), device="cpu")

    model.to(device)
    return model

另外, A l e x N e t AlexNet AlexNet是一个1000类的分类网络,而我们使用的数据集是二分类任务,因此需要替换最后的输出层:

num_ftrs = alexnet_model.classifier._modules["6"].in_features
alexnet_model.classifier._modules["6"] = nn.Linear(num_ftrs, num_classes) # num_classes = 2
3、构建损失函数

由于是二分类问题,因此直接采用交叉熵损失函数即可

criterion = nn.CrossEntropyLoss()
4、构建优化器

优化器选择传统的随机梯度下降即可:

optimizer = optim.SGD(alexnet_model.parameters(), lr=LR, momentum=0.9)

当然,也可以选择冻结卷积层:

fc_params_id = list(map(id, alexnet_model.classifier.parameters()))  # 返回的是parameters的 内存地址
base_params = filter(lambda p: id(p) not in fc_params_id, alexnet_model.parameters())
optimizer = optim.SGD([
    {'params': base_params, 'lr': LR * 0.1},  # 0
    {'params': alexnet_model.classifier.parameters(), 'lr': LR}], momentum=0.9)
5、迭代训练
for epoch in range(start_epoch + 1, MAX_EPOCH):

    loss_mean = 0.
    correct = 0.
    total = 0.

    alexnet_model.train()
    for i, data in enumerate(train_loader):

        # forward
        inputs, labels = data
        inputs, labels = inputs.to(device), labels.to(device)
        outputs = alexnet_model(inputs)

        # backward
        optimizer.zero_grad()
        loss = criterion(outputs, labels)
        loss.backward()

        # update weights
        optimizer.step()

        # 统计分类情况
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).squeeze().cpu().sum().numpy()

        # 打印训练信息
        loss_mean += loss.item()
        train_curve.append(loss.item())
        if (i+1) % log_interval == 0:
            loss_mean = loss_mean / log_interval
            print("Training:Epoch[{:0>3}/{:0>3}] Iteration[{:0>3}/{:0>3}] Loss: {:.4f} Acc:{:.2%}".format(
                epoch, MAX_EPOCH, i+1, len(train_loader), loss_mean, correct / total))
            loss_mean = 0.

    scheduler.step()  # 更新学习率

    # validate the model
    if (epoch+1) % val_interval == 0:

        correct_val = 0.
        total_val = 0.
        loss_val = 0.
        alexnet_model.eval()
        with torch.no_grad():
            for j, data in enumerate(valid_loader):
                inputs, labels = data
                inputs, labels = inputs.to(device), labels.to(device)

                bs, ncrops, c, h, w = inputs.size()     # [4, 10, 3, 224, 224
                outputs = alexnet_model(inputs.view(-1, c, h, w))
                outputs_avg = outputs.view(bs, ncrops, -1).mean(1)

                loss = criterion(outputs_avg, labels)

                _, predicted = torch.max(outputs_avg.data, 1)
                total_val += labels.size(0)
                correct_val += (predicted == labels).squeeze().cpu().sum().numpy()

                loss_val += loss.item()

            loss_val_mean = loss_val/len(valid_loader)
            valid_curve.append(loss_val_mean)
            print("Valid:\t Epoch[{:0>3}/{:0>3}] Iteration[{:0>3}/{:0>3}] Loss: {:.4f} Acc:{:.2%}".format(
                epoch, MAX_EPOCH, j+1, len(valid_loader), loss_val_mean, correct_val / total_val))
        alexnet_model.train()

最后的训练损失曲线如下图所示:

在这里插入图片描述

从上图可以看出训练效果非常好,在测试集上的准确率能够达到97.52%,如果多训练几个Epoch,准确率会进一步提高


完整代码见我的Github

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值