通俗学AI(3):VGG套路得人心

目录

前言

网络结构与配置

“积小成多”

VGG小套路

训练细节

实验结果讨论

Pytorch实现


前言

        今天我们要来讨论的论文是《ImageNet Classification with Deep Convolutional Neural Networks》,其作者提出的AlexNet网络是ISLVRC 2012竞赛的冠军网络。这篇论文里有很多关于减少过拟合的做法,可以说是对深度卷积网络应用于图像分类任务的重大探索。


网络结构与配置

        老规矩,先把VGGNet的结构图摆上来。其实VGGNet的配置有六种,深度和其他配置也逐渐升级,可以看下表,A的配置是最拉的,而E的配置是最豪华的。

 图片来自论文

        而我们平时使用VGGNet时,一般都是使用D这个配置,也就是16层这个配置,下面是它的网络结构图。

 图片来自网络


 

“积小成多”

        我们可以看到,VGGNet使用的卷积核都是3*3,不像AlexNet那些,起手就是11*11。那是因为这篇论文的作者使用了一些技巧。在讲这个技巧前,我们先讲讲感受野的概念,感受野其实就是卷积后,输入图上多少个格子映射为输出图上的一个格子,如下图。

  图片来自网络

        最下面的图是5*5的尺寸,在经过3*3卷积核,步长为1的卷积后,输出了中间那张图,尺寸为3*3。也就是说原图的3*3映射为输出图上的1*1,所以感受野为3*3。而第二张图经过3*3卷积核卷积后,输出最顶上那张图,感受野也是3*3。

        假如我们把第一张图看成输入图,最顶上那张图看作输出图,那么输入图上的5*5映射为了输出图上的1*1。所以说此时的感受野为5*5。可以看出,使用连续两层3*3的卷积,其感受野就为5*5

        而VGGNet就是使用了这种方法。连续使用两层的感受野是5*5,连续使用三层就是7*7。

 图片来自论文

 

  图片来自论文

        有的人可能要问,如果我要5*5的感受野,直接给个5*5的卷积核不就搞定了吗,为啥还要叠两层这么麻烦?

        第一个原因就是这样叠几层可以多用几次激活函数。注意到上面的结构图,每个卷积层后面都跟着一个激活函数。如果我们叠加两层用3*3卷积核的卷积层,那么我们会使用两次激活函数。而如果我们只是使用一层5*5卷积核的卷积层,虽然感受野一样,但是只使用了一次激活函数。作者在文中提到,使用多次激活函数可以让决策函数更具区分性

        第二个原因是这样做可以减少参数。假设我们叠两层3*3卷积核的卷积层,那么参数就是2*3*3=18,我们使用有同样感受野的单层5*5卷积核的卷积层,它的参数为5*5=25(这里假设通道一致,因此不加入计算,如果忘记如何计算可以看这篇)。很明显叠两层的参数比直接一层的参数要少。


VGG小套路

        有我们上面提到的积小成多的方法,我们就可以疯狂叠这个组合了。值得注意的是,在两层卷积之后,我们的图像尺寸并没有发生变化,因为作者加了padding。我们可以简单计算一下:(224-3+2)/1+1=224。可以看成尺寸并没有变化。整张图的尺寸变化都是在池化层里。因此我们可以总结出VGGNet网络结构的一个套路。

        首先图像输入,叠多个3*3卷积核的卷积层(每个卷积层后记得加激活函数ReLU),然后使用最大池化层缩小一半的尺寸(因为池化的核是2*2,步长是2),然后重复叠卷积和最大池化这个过程,当图像尺寸缩小到一定的程度,就可以把它扁平化,输入一个全连接网络了。全连接网络连接个三层,最后一层的神经元的数量设定为分类的数量便可以了。

        但是这个套路并不是没有限制的,首先作者提到,当叠到19层后,网络的误差就饱和了,不能再下降了。除此之外,卷积核为3*3的卷积层也不可以叠太多,因为这样叠而尺寸不发生变化是由于我们加了padding,叠到后面,真正有效的数据都集中在中间一小部分,而周围都是我们加的padding,是无效信息,所以我们像VGGNet叠个两次或者三次就好了。


训练细节

        这篇论文中关于训练的细节也挺有意思,我们也拿出来说说。首先作者在论文里提到,虽然它的网络更深,参数也更多,但是这个网络的收敛时间却比AlexNet少。为什么呢?其一是因为更大深度和更小的卷积核尺寸施加了隐式正则化,使网络需要更少的时间收敛。其二是因为使用了预初始化

        我们重点看第二个原因。作者在论文中说:“网络权重的初始化非常重要,因为由于深度网络中梯度的不稳定性,不好的初始化可能会阻碍学习。”那么他是如何进行网络的初始化的呢

        由缓至急徐徐进

 图片来自论文

 

  图片来自论文

        首先作者先训练个A配置的网络(请看上面的配置表),因为网络A的深度比较浅,可以通过随机初始化进行训练。训练完A配置后,再利用A的权重给其他配置的网络进行初始化。你可能会发现,其他配置有的层A没有啊,那怎么办?问题不大,A没有的层,我们就对它进行随机初始化。

        当然这只是一个办法,后来作者发现其他人设计了一个初始化的方法,可以不用这样做预训练。


另外一个训练的细节是训练用的图像

        首先我们要先设定一个尺寸S,这个S的设定有两个方法。第一个方法是直接设定为256或者384。第二个方法是在[256,512]这个区间里随机选一个。

        得到这个尺寸S后,我们把图像的最短边缩小为S,长边跟着缩同样的幅度。比如说S是256,我有一张图片它是640*512,首先把短边512缩成256,也就是除二,因此长边640也要除二,变成320。所以最后是640*512变为320*256,我们叫这个是各向同性的缩放。放缩后,我们再从中间随机裁剪出224*224的图像来,再给它做一个随机水平翻转,随机RGB色移。


实验结果讨论

 图片来自论文

        讨论一下结果。通过这个表格,我们可以看出,同一个网络配置之下,使用方法二确定S(作者称这个方法是尺寸抖动)来训练的结果错误率会更低

        同时我们也看到,使用了LRN的错误率会更高,这也是为啥上一篇我提到有论文认为LRN没鬼用。

        同时我们又看到,随着深度提高,错误率会越低,说明性能有所提升。同时还有个小细节,D的配置不使用方法二确定S时结果比E配置还好,所以这也是为什么平时使用D配置更多。

        注意:论文里还提到了多种测试方法,上图的结果为其中一种测试方法,但其实其他方法得到趋势和这个一样,所以就只拿这个出来讲。


Pytorch实现

说了这么多,我们照旧用Pytorch来实现一下VGGNet的D配置。

# 引入
import torch.nn as nn
import torch


class VGGNet(nn.Module):
    def __init__(self):
        super(VGGNet, self).__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1),  # input[3, 224, 224]  output[64, 224, 224]
            nn.ReLU(inplace=True),
            nn.Conv2d(64, 64, kernel_size=3, stride=1, padding=1),  # output[64, 224, 224]
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),                  # output[64, 112, 112]
            
            nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1),  # output[128, 112, 112]
            nn.ReLU(inplace=True),
            nn.Conv2d(128, 128, kernel_size=3, stride=1, padding=1),  # output[128, 112, 112]
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),                  # output[128, 56, 56]
            
            nn.Conv2d(128, 256, kernel_size=3, stride=1, padding=1),  # output[256, 56, 56]
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),  # output[256, 56, 56]
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),  # output[256, 56, 56]
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),                  # output[256, 28, 28]
            
            nn.Conv2d(256, 512, kernel_size=3, stride=1, padding=1),  # output[512, 28, 28]
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),  # output[512, 28, 28]
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),  # output[512, 28, 28]
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),                  # output[512, 14, 14]
            
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),  # output[512, 14, 14]
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),  # output[512, 14, 14]
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),  # output[512, 14, 14]
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),                  # output[512, 7, 7]
        )
        self.classifier = nn.Sequential(
            nn.Dropout(p=0.5),
            nn.Linear(512*7*7, 4096),
            nn.ReLU(True),
            nn.Dropout(p=0.5),
            nn.Linear(4096, 4096),
            nn.ReLU(True),        
            nn.Linear(4096, 1000)
        )

    def forward(self, x):
        x = self.features(x)
        x = torch.flatten(x, start_dim=1)
        x = self.classifier(x)
        return x

作者:公|众|号【荣仙翁】

内容同步在其中,想看更多内容可以关注我哦。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值