VGGNet由牛津大学的视觉几何组(Visual Geometry Group)提出,并在2014年举办的ILSVRC(ImageNet 2014比赛)中获得了定位任务第1名和分类任务第2名的好成绩,(GoogleNet 是2014 年的分类任务第1 名)。虽然VGGNet在性能上不及GoogleNet,但因为VGG结构简单,应用性强,所以很多技术人员都喜欢使用基于VGG 的网络。

VGG 最大的特点就是通过比较彻底地采用 3x3 尺寸的卷积核来堆叠神经网络,这样也加深整个神经网络的深度。这两个重要的改变对于人们重新定义卷积神经网络模型架构也有不小的帮助,至少证明使用更小的卷积核并且增加卷积神经网络的深度,可以更有效地提升模型的性能。

VGG 选择的是在 AlexNet 的基础上加深它的层数,但是它有个很显著的特征就是持续性的添加 3x3 的卷积核。而AlexNet 有 5 层卷积层,从下面的网络结构图我们可以看出来,VGG 就是针对这 5 层卷积层进行改造,共进行了 6 种配置。

vgg16使用imagenet权重 vgg imagenet_VGG

这六种配置的参数量:

vgg16使用imagenet权重 vgg imagenet_网络结构_02

这六种配置的效果展示图:

vgg16使用imagenet权重 vgg imagenet_网络结构_03


从上面的效果图中,我们发现VGG19是最好的。但是,VGG-19 的参数比 VGG-16 的参数多了好多。由于VGG-19需要消耗更大的资源,因此实际中VGG-16使用得更多。而且VGG-16网络结构十分简单,并且很适合迁移学习,因此至今VGG-16仍在广泛使用,下面我们主要来讨论一下VGG16的网络结构,也就是图中的类型D。(VGG19即上面的E类型)

VGG16相比AlexNet的一个改进是采用连续的3x3的卷积核代替AlexNet中的较大卷积核(11x11,7x7,5x5),如下图中所示,共有13个卷积层,3个全连接层。其全部采用3*3卷积核,步长统一为1,Padding统一为1,和2*2最大池化核,步长为2,Padding统一为0

vgg16使用imagenet权重 vgg imagenet_vgg16使用imagenet权重_04


那么,就让我们一层一层来分析吧!

(1)INPUT层:VGG16卷积神经网络默认的输入数据必须是维度为224×224×3的图像,和 AlexNet一样,其输入图像的高度和宽度均为224,而且拥有的色彩通道是R、G、B这三个。
(2)CONV3-64:使用的卷积核为(3*3*3)*64(卷积核大小为3*3,输入通道为3,输出通道为64),步长为1,Padding为1。通过套用卷积通用公式,可以得到最后输出的特征图的高度和宽度均为224,即vgg16使用imagenet权重 vgg imagenet_深度学习_05 ,最后输出的特征图的维度为224×224×64。
(3)CONV3-64:使用的卷积核为(3*3*64)*64,步长为1,Padding为1。通过套用卷积通用公式,可以得到最后输出的特征图的高度和宽度均为224,即vgg16使用imagenet权重 vgg imagenet_深度学习_05 ,最后输出的特征图的维度为224×224×64。
(4)Max pool:池化核大小为2×2,步长为2。通过套用池化通用公式,可以得到最后输出的特征图的高度和宽度均为112,即 vgg16使用imagenet权重 vgg imagenet_vgg16使用imagenet权重_07 ,最后得到的输出的特征图的维度为112×112×64。
(5)CONV3-128:使用的卷积核为(3*3*64)*128,步长为1,Padding为1。通过套用卷积通用公式,可以得到最后输出的特征图的高度和宽度均为112,即vgg16使用imagenet权重 vgg imagenet_VGG_08 ,最后输出的特征图的维度为112×112×128。
(6)CONV3-128:使用的卷积核为(3*3*128)*128,步长为1,Padding为1。通过套用卷积通用公式,可以得到最后输出的特征图的高度和宽度均为112,即vgg16使用imagenet权重 vgg imagenet_VGG_08 ,最后输出的特征图的维度为112×112×128。
(7)Max pool:池化核大小为2×2,步长为2。通过套用池化通用公式,可以得到最后输出的特征图的高度和宽度均为56,即 vgg16使用imagenet权重 vgg imagenet_vgg16使用imagenet权重_10 ,最后得到的输出的特征图的维度为56×56×128。
(8)CONV3-256:使用的卷积核为(3*3*128)*256,步长为1,Padding为1。通过套用卷积通用公式,可以得到最后输出的特征图的高度和宽度均为56,即vgg16使用imagenet权重 vgg imagenet_深度学习_11 ,最后输出的特征图的维度为56×56×256。
(9)CONV3-256:使用的卷积核为(3*3*256)*256,步长为1,Padding为1。通过套用卷积通用公式,可以得到最后输出的特征图的高度和宽度均为56,即vgg16使用imagenet权重 vgg imagenet_VGG_12 ,最后输出的特征图的维度为56×56×256。
(10)CONV3-256:经过(3*3*256)*256卷积核,生成featuremap为56*56*256
(11)Max pool: 经过(2*2)maxpool,生成featuremap为28*28*256
(12)CONV3-512:经过(3*3*256)*512卷积核,生成featuremap为28*28*512
(13)CONV3-512:经过(3*3*512)*512卷积核,生成featuremap为28*28*512
(14)CONV3-512:经过(3*3*512)*512卷积核,生成featuremap为28*28*512
(15)Max pool:经过(2*2)maxpool,生成featuremap为14*14*512
(16)CONV3-512:经过(3*3*512)*512卷积核,生成featuremap为14*14*512
(17)CONV3-512:经过(3*3*512)*512卷积核,生成featuremap为14*14*512
(18)CONV3-512:经过(3*3*512)*512卷积核,生成featuremap为14*14*512
(19)Max pool:经过2*2卷积,生成featuremap为7*7*512
(20)FC-4096:输入为7*7*512,和AlexNet模型一样,都需要对输入特征图进行扁平化处理以得到1×25088的数据,输出数据的维度要求是1×4096,所以需要一个维度为25088×4096的矩阵完成输入数据和输出数据的全连接,最后得到输出数据的维度为1×4096。
(21)FC-4096:输入数据的维度为1×4096,输出数据的维度要求是1×4096,所以需要一个维度为4096×4096的矩阵完成输入数据和输出数据的全连接,最后得到输出数据的维度为1×4096。
(22)FC-1000:输入数据的维度为1×4096,输出数据的维度要求是1×1000,所以需要一个维度为4096×1000的矩阵完成输入数据和输出数据的全连接,最后得到输入数据的维度为1×1000。

上面加粗的层即带有可训练参数的层,共16 weight layers。

VGG16的参数一共是多少呢,现在来计算吧!

vgg16使用imagenet权重 vgg imagenet_深度学习_13


VGG16(即上图D)总参数量是138M,具体如下:

第1层:1792 = 3*3*3*64+64 第2层:36928 = 3*3*64*64+64

第3层:73856 = 3*3*64*128+128

第4层:147584 = 3*3*128*128+128

第5层:295168 = 3*3*128*256+256

第6层:590080 = 3*3*256*256+256

第7层:590080 = 3*3*256*256+256

第8层:1180160 = 3*3*256*512+512

第9层:2359808 = 3*3*512*512+512

第10层:2359808 = 3*3*512*512+512

第11层:2359808 = 3*3*512*512+512

第12层:2359808 = 3*3*512*512+512

第13层:2359808 = 3*3*512*512+512

第14层:102764544 = 7*7*512*4096+4096

第15层:16781312 = 4096*4096+4096

第16层:4097000 = 4096*1000+1000

总计:138357544个 (138M)

总结:

  1. VGG16相比AlexNet的一个改进是采用连续的3x3的卷积核代替AlexNet中的较大卷积核(11x11,7x7,5x5)
  2. 加深结构都使用ReLU激活函数:提升非线性变化的能力
  3. VGG16 全部采用3*3卷积核,步长统一为1,Padding统一为1,和2*2最大池化核,步长为2,Padding统一为0
  4. VGG19比VGG16的区别在于多了3个卷积层,其它完全一样
  5. VGG16基本是AlexNet(AlexNet是8层,包括5个卷积层和3个全连接层)的加强版,深度上是其2倍,参数量大小也是两倍多。

我们已经知道,VGG16相比AlexNet的一个改进是采用连续的3x3的卷积核代替AlexNet中的较大卷积核(11x11,7x7,5x5),现在,来思考几个问题吧。

Thinking1:使用3x3卷积核替代7x7卷积核的好处?

  • 2 个 3x3 的卷积核叠加,它们的感受野等同于 1 个 5x5 的卷积核,3 个叠加后,它们的感受野等同于 1 个 7x7 的效果。用2个3x3的卷积核代替原来的 5x5卷积核如下图所示:
  • 由于感受野相同,3个3x3的卷积,使用了3个非线性激活函数,增加了非线性表达能力,从而可以提供更复杂的模式学习。
  • 使用3x3卷积核可以减少参数,假设现在有 3 层 3x3 卷积核堆叠的卷积层,输出和输出通道数都是C,那么它的参数总数是 3x(3x3xCxC)=27xCxC 。同样和它感受野大小一样的一个卷积层,卷积核是 7x7 的尺寸,假如输出和输出通道数都是C,那么它的参数总数就是 7x7xCxC=49xCxC。而且通过上述方法网络层数还加深了。三层3x3的卷积核堆叠参数量比一层7x7的卷积核参数链还要少。
  • 总的来说,使用3x3卷积核堆叠的形式,既增加了网络层数又减少了参数量。

Thinking2:多少个3x3的卷积核可以替代原来11x11的卷积核?

(11-1)/2=5,故5个3x3的卷积核可以替代原来11x11的卷积核,即n-11+1=n+(-3+1)*5

Thinking3:VGG的C网络结构使用了1x1卷积核,1x1卷积核的主要好处?

  • 使用多个1x1卷积核,在保持feature map 尺寸不变(即不损失分辨率)的前提下,可以大幅增加非线性表达能力,把网络做得很deep。
  • 进行卷积核通道数的降维和升维。
  • 1x1卷积相当于线性变换,非线性激活函数起到非线性作用。
  • 总结就是:1x1 卷积核的好处是不改变感受野的情况下,进行升维和降维,同时也加深了网络的深度。

VGG16和VGG19都在pytorch封装好了,如下所示:

torchvision.models.vgg16(pretrained=False)
torchvision.models.vgg19(pretrained=False)

代码实现:

import torch
import torch.nn as nn
# 带BN层的vgg16
class VGG16_bn(torch.nn.Module):

    def __init__(self, num_classes):
        super(VGG16_bn, self).__init__()
        
        self.block_1 = nn.Sequential(
                nn.Conv2d(in_channels=3,
                          out_channels=64,
                          kernel_size=3,
                          stride=1,
                          padding=1),
                nn.BatchNorm2d(64),
                nn.ReLU(inplace=True),
            
                nn.Conv2d(in_channels=64,
                          out_channels=64,
                          kernel_size=3,
                          stride=1,
                          padding=1),
                nn.BatchNorm2d(64),
                nn.ReLU(inplace=True),
            
                nn.MaxPool2d(kernel_size=2,
                             stride=2)
        )
        
        self.block_2 = nn.Sequential(
                nn.Conv2d(in_channels=64,
                          out_channels=128,
                          kernel_size=3,
                          stride=1,
                          padding=1),
                nn.BatchNorm2d(128),
                nn.ReLU(inplace=True),
            
                nn.Conv2d(in_channels=128,
                          out_channels=128,
                          kernel_size=3,
                          stride=1,
                          padding=1),
                nn.BatchNorm2d(128),
                nn.ReLU(inplace=True),
            
                nn.MaxPool2d(kernel_size=2,
                             stride=2)
        )
        
        self.block_3 = nn.Sequential(
                nn.Conv2d(in_channels=128,
                          out_channels=256,
                          kernel_size=3,
                          stride=1,
                          padding=1),
                nn.BatchNorm2d(256),
                nn.ReLU(inplace=True),
            
                nn.Conv2d(in_channels=256,
                          out_channels=256,
                          kernel_size=3,
                          stride=1,
                          padding=1),
                nn.BatchNorm2d(256),
                nn.ReLU(inplace=True),
            
                nn.Conv2d(in_channels=256,
                          out_channels=256,
                          kernel_size=3,
                          stride=1,
                          padding=1),
                nn.BatchNorm2d(256),
                nn.ReLU(inplace=True),
            
                nn.MaxPool2d(kernel_size=2,
                             stride=2)
        )
        
          
        self.block_4 = nn.Sequential(
                nn.Conv2d(in_channels=256,
                          out_channels=512,
                          kernel_size=3,
                          stride=1,
                          padding=1),
                nn.BatchNorm2d(512),
                nn.ReLU(inplace=True),
            
                nn.Conv2d(in_channels=512,
                          out_channels=512,
                          kernel_size=3,
                          stride=1,
                          padding=1),
                nn.BatchNorm2d(512),
                nn.ReLU(inplace=True),
            
                nn.Conv2d(in_channels=512,
                          out_channels=512,
                          kernel_size=3,
                          stride=1,
                          padding=1),
                nn.BatchNorm2d(512),
                nn.ReLU(inplace=True),

                nn.MaxPool2d(kernel_size=2,
                             stride=2)
        )
        
        self.block_5 = nn.Sequential(
                nn.Conv2d(in_channels=512,
                          out_channels=512,
                          kernel_size=(3, 3),
                          stride=(1, 1),
                          padding=1),
                nn.BatchNorm2d(512),
                nn.ReLU(inplace=True),
            
                nn.Conv2d(in_channels=512,
                          out_channels=512,
                          kernel_size=(3, 3),
                          stride=(1, 1),
                          padding=1),
                nn.BatchNorm2d(512),
                nn.ReLU(inplace=True),
            
                nn.Conv2d(in_channels=512,
                          out_channels=512,
                          kernel_size=(3, 3),
                          stride=(1, 1),
                          padding=1),
                nn.BatchNorm2d(512),
                nn.ReLU(inplace=True),

                nn.MaxPool2d(kernel_size=(2, 2),
                             stride=(2, 2))
        )
        
        #自适应平均池化,见https://www.zhihu.com/question/282046628
        self.avgpool = nn.AdaptiveAvgPool2d((7, 7))
        
        self.classifier = nn.Sequential(
                nn.Linear(512 * 7 * 7, 4096),
                nn.ReLU(True),
                nn.Dropout(),
                nn.Linear(4096, 4096),
                nn.ReLU(True),
                nn.Dropout(),
                nn.Linear(4096, num_classes),
        )
        
         
        # 初始化权重
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                # VGG采用了Kaiming initialization
                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.BatchNorm2d):
                nn.init.constant_(m.weight, 1)
                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)            
    
        
    def forward(self, x):

        x = self.block_1(x)
        x = self.block_2(x)
        x = self.block_3(x)
        x = self.block_4(x)
        x = self.block_5(x) 
        x = self.avgpool(x)
        # 拉平     
        x = torch.flatten(x, 1)      
        x = self.classifier(x)
        # 一般不用softmax
        # x = F.softmax(x, dim=1)
        return x
# 拿一个数据测试下吧!
if __name__ == "__main__":
    a=torch.randn(3,224,224)
    a=a.unsqueeze(0)
    print(a.size())
    net = VGG16_bn(10)
    x=net(a)
    print(x.size())
"""
# 输出
torch.Size([1, 3, 224, 224])
torch.Size([1, 10])
"""

【参考文档】

  1. 【卷积神经网络结构专题】经典网络结构之VGG(附代码实现)
  2. 【模型解读】从LeNet到VGG,看卷积+池化串联的网络结构
  3. 【深度学习之pytorch计算机视觉】-唐进民著