一、网络结构

  VGG16网络的基本结构如下:
在这里插入图片描述
一个VGG_block的组成有如下特点:

  • 带填充以保持分辨率的卷积层:指对输入特征图卷积操作时会带有填充,使得只改变通道数而不改变图像高、宽。
  • 非线性激活函数ReLU:卷积操作后将特征图输入激活函数,提供使之具有非线性性。
  • 最大池化层:使用最大池化函数,不改变图像通道数,但会缩小图像尺寸。

可归纳为:
卷积层(+relu激活函数)
卷积层(+relu激活函数)
最大池化层
卷积层(+relu激活函数)
卷积层(+relu激活函数)
最大池化层
卷积层(+relu激活函数)
卷积层(+relu激活函数)
卷积层(+relu激活函数)
最大池化层
卷积层(+relu激活函数)
卷积层(+relu激活函数)
卷积层(+relu激活函数)
最大池化层
卷积层(+relu激活函数)
卷积层(+relu激活函数)
卷积层(+relu激活函数)
最大池化层
全连接层(+relu激活函数)
全连接层(+relu激活函数)
全连接层

【第一层:卷积层】
  前置知识:若输入图像大小为NxN,卷积核大小为FxF,若不填充而直接进行卷积操作,则输出图像大小为:
( N − F + 1 ) x ( N − F + 1 ) (N-F+1)x(N-F+1) (NF+1)x(NF+1)
而若在原始图像周围填充P个像素,此时图像大小为(N+2P)x(N+2P),则卷积后输出图像大小为:
( N + 2 P − F + 1 ) x ( N + 2 P − F + 1 ) (N+2P-F+1)x(N+2P-F+1) (N+2PF+1)x(N+2PF+1)
  输入图像大小为 ( 224 , 224 , 3 ) (224,224,3) (224,224,3),使用了64个3x3大小的卷积核进行卷积,若不进行填充(padding),则输出图像大小应为 ( 222 , 222 , 64 ) (222,222,64) (222,222,64)。而图中给出输出图像大小为 ( 224 , 224 , 64 ) (224,224,64) (224,224,64),说明进行了填充操作,且padding=1。这就使得输出图像与原始图像大小一样,保证了图像大小的一致性。VGG16网络第一层代码:

		self.conv1 = nn.Conv2d(3, 64, kernel_size=3, padding=1)
        self.relu1 = nn.ReLU(inplace=True)#激活函数不会改变数据维度

【第二层:卷积层】
  第二层中,输入图像大小为 ( 224 , 224 , 64 ) (224,224,64) (224,224,64),输出图像大小为 ( 224 , 224 , 64 ) (224,224,64) (224,224,64),同样需要填充padding=1。VGG网络第二层代码为:

		self.conv2 = nn.Conv2d(64, 64, kernel_size=3, padding=1)
        self.relu2 = nn.ReLU(inplace=True)

【池化层】
  VGG网络的池化层采用最大池化操作,即,通过在每个池化窗口中选择像素大小最大值来减小特征图的尺寸。最大池化层通常用于减少特征图的空间维度,从而降低模型的计算量,同时保留重要的特征。VGG16池化核大小为2x2,若步长为1,即以1为间隔平移。则输出数据尺寸应为:22422464->(224-2+1)(224-2+1)64也就是(22322364)。而若步长为2,则刚好可使输出图像大小减半,即为 ( 112 , 112 , 64 ) (112,112,64) (112,112,64),代码:

        self.max_pooling1 = nn.MaxPool2d(kernel_size=2, stride=2)

  总结每一次操作后数据尺度的变化:

输入尺寸输出尺寸
卷积层1 ( 224 , 224 , 3 ) (224,224,3) (224,224,3) ( 224 , 224 , 64 ) (224,224,64) (224,224,64)
卷积层2 ( 224 , 224 , 64 ) (224,224,64) (224,224,64) ( 224 , 224 , 64 ) (224,224,64) (224,224,64)
池化层 ( 224 , 224 , 64 ) (224,224,64) (224,224,64) ( 112 , 112 , 64 ) (112,112,64) (112,112,64)
卷积层3 ( 112 , 112 , 64 ) (112,112,64) (112,112,64) ( 112 , 112 , 128 ) (112,112,128) (112,112,128)
卷积层4 ( 112 , 112 , 128 ) (112,112,128) (112,112,128) ( 112 , 112 , 128 ) (112,112,128) (112,112,128)
池化层 ( 112 , 112 , 128 ) (112,112,128) (112,112,128) ( 56 , 56 , 128 ) (56,56,128) (56,56,128)
卷积层5 ( 56 , 56 , 128 ) (56,56,128) (56,56,128) ( 56 , 56 , 256 ) (56,56,256) (56,56,256)
卷积层6 ( 56 , 56 , 256 ) (56,56,256) (56,56,256) ( 56 , 56 , 256 ) (56,56,256) (56,56,256)
卷积层7 ( 56 , 56 , 256 ) (56,56,256) (56,56,256) ( 56 , 56 , 256 ) (56,56,256) (56,56,256)
池化层 ( 56 , 56 , 256 ) (56,56,256) (56,56,256) ( 28 , 28 , 256 ) (28,28,256) (28,28,256)
卷积层8 ( 28 , 28 , 256 ) (28,28,256) (28,28,256) ( 28 , 28 , 512 ) (28,28,512) (28,28,512)
卷积层9 ( 28 , 28 , 256 ) (28,28,256) (28,28,256) ( 28 , 28 , 512 ) (28,28,512) (28,28,512)
卷积层10 ( 28 , 28 , 256 ) (28,28,256) (28,28,256) ( 28 , 28 , 512 ) (28,28,512) (28,28,512)
池化层 ( 28 , 28 , 512 ) (28,28,512) (28,28,512) ( 14 , 14 , 512 ) (14,14,512) (14,14,512)
卷积层11 ( 14 , 14 , 512 ) (14,14,512) (14,14,512) ( 14 , 14 , 512 ) (14,14,512) (14,14,512)
卷积层12 ( 14 , 14 , 512 ) (14,14,512) (14,14,512) ( 14 , 14 , 512 ) (14,14,512) (14,14,512)
卷积层13 ( 14 , 14 , 512 ) (14,14,512) (14,14,512) ( 14 , 14 , 512 ) (14,14,512) (14,14,512)
池化层 ( 14 , 14 , 512 ) (14,14,512) (14,14,512) ( 7 , 7 , 512 ) (7,7,512) (7,7,512)
全连接层1 ( 7 , 7 , 512 ) (7,7,512) (7,7,512) 4096 4096 4096
全连接层2 4096 4096 4096 4096 4096 4096
全连接层3 4096 4096 4096 1000 1000 1000

二、代码实现

  代码实现:

import torch
import torch.nn as nn
import numpy as np
# 定义VGG16网络类
class VGG16(nn.Module):
    def __init__(self):
        super(VGG16, self).__init__()
        # 卷积层部分
        self.conv1 = nn.Conv2d(3, 64, kernel_size=3, padding=1)
        self.relu1 = nn.ReLU(inplace=True)
        self.conv2 = nn.Conv2d(64, 64, kernel_size=3, padding=1)
        self.relu2 = nn.ReLU(inplace=True)
        self.max_pooling1 = nn.MaxPool2d(kernel_size=2, stride=2)

        self.conv3 = nn.Conv2d(64, 128, kernel_size=3, padding=1)
        self.relu3 = nn.ReLU(inplace=True)
        self.conv4 = nn.Conv2d(128, 128, kernel_size=3, padding=1)
        self.relu4 = nn.ReLU(inplace=True)
        self.max_pooling2 = nn.MaxPool2d(kernel_size=2, stride=2)

        self.conv5 = nn.Conv2d(128, 256, kernel_size=3, padding=1)
        self.relu5 = nn.ReLU(inplace=True)
        self.conv6 = nn.Conv2d(256, 256, kernel_size=3, padding=1)
        self.relu6 = nn.ReLU(inplace=True)
        self.conv7 = nn.Conv2d(256, 256, kernel_size=3, padding=1)
        self.relu7 = nn.ReLU(inplace=True)
        self.max_pooling3 = nn.MaxPool2d(kernel_size=2, stride=2)

        self.conv8 = nn.Conv2d(256, 512, kernel_size=3, padding=1)
        self.relu8 = nn.ReLU(inplace=True)
        self.conv9 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
        self.relu9 = nn.ReLU(inplace=True)
        self.conv10 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
        self.relu10 = nn.ReLU(inplace=True)
        self.max_pooling4 = nn.MaxPool2d(kernel_size=2, stride=2)

        self.conv11 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
        self.relu11 = nn.ReLU(inplace=True)
        self.conv12 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
        self.relu12 = nn.ReLU(inplace=True)
        self.conv13 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
        self.relu13 = nn.ReLU(inplace=True)
        self.max_pooling5 = nn.MaxPool2d(kernel_size=2, stride=2)

        # 全连接层部分
        self.fc1 = nn.Linear(512 * 7 * 7, 4096)
        self.relu14 = nn.ReLU(inplace=True)
        self.fc2 = nn.Linear(4096, 4096)
        self.relu15 = nn.ReLU(inplace=True)
        self.dropout = nn.Dropout()#正则化,防止过拟合
        self.fc3 = nn.Linear(4096, 1000)

    # 前向传播函数
    def forward(self, x):
        x = self.conv1(x)
        x = self.relu1(x)
        x = self.conv2(x)
        x = self.relu2(x)
        x = self.max_pooling1(x)

        x = self.conv3(x)
        x = self.relu3(x)
        x = self.conv4(x)
        x = self.relu4(x)
        x = self.max_pooling2(x)

        x = self.conv5(x)
        x = self.relu5(x)
        x = self.conv6(x)
        x = self.relu6(x)
        x = self.conv7(x)
        x = self.relu7(x)
        x = self.max_pooling3(x)

        x = self.conv8(x)
        x = self.relu8(x)
        x = self.conv9(x)
        x = self.relu9(x)
        x = self.conv10(x)
        x = self.relu10(x)
        x = self.max_pooling4(x)

        x = self.conv11(x)
        x = self.relu11(x)
        x = self.conv12(x)
        x = self.relu12(x)
        x = self.conv13(x)
        x = self.relu13(x)
        x = self.max_pooling5(x)
        print(x.shape)

        x = x.view(-1, 512*7*7)
        print(x.shape)
        x = self.fc1(x)
        x = self.relu14(x)
        x = self.fc2(x)
        x = self.relu15(x)
        x = self.fc3(x)
        return x
  • 卷积层:kernel_size=3, padding=1可使图片大小不会改变。
  • 最大池化层:kernel_size=2, stride=2可使得图片宽高减半。
  • 全连接层:设输入张量形状为 ( B , C , H , W ) (B,C,H,W) (B,C,H,W),其中 B B B表示批量大小、 C C C表示通道数、 H H H表示高度、 W W W表示宽度。执行x = x.view(-1, 512*7*7)可将 ( B , 7 , 7 , 512 ) (B,7,7,512) (B,7,7,512)的数据张量展平为 ( B , 7 ∗ 7 ∗ 512 ) (B,7*7*512) (B,77512),即保持批次大小不变,将特征图数据展平为一维,之后再执行x = self.fc1(x),即将数据输入全连接层参与运算。

  也可使用torch.nn.Sequential()简化网络的写法,并且将类别数作为参数传入网络模型中:

import torch
import torch.nn as nn
import numpy as np
# 定义VGG16网络类
class VGG16(nn.Module):
    def __init__(self, num_classes=1000):
        super(VGG16, self).__init__()
        # 卷积层部分
        self.features = nn.Sequential(
            nn.Conv2d(3, 64, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(64, 64, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),

            nn.Conv2d(64, 128, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(128, 128, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),

            nn.Conv2d(128, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),

            nn.Conv2d(256, 512, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),

            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
        )
        # 全连接层部分
        self.classifier = nn.Sequential(
            nn.Linear(512 * 7 * 7, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(),
            nn.Linear(4096, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(),
            nn.Linear(4096, num_classes),
        )

    # 前向传播函数
    def forward(self, x):
        x = self.features(x)
        x = torch.flatten(x, 1)
        x = self.classifier(x)
        return x

三、模型总结

  VGG是为ImageNet分类挑战训练的,这是一个带有1000个类的对象识别问题,最后的全连接层(4096x1000)为每个输入图像输出一个长度为1000的向量,softmax层将这个长度为1000的向量转换为1000个类。
  从网络结构中可看出,VGG均全部使用3×3大小、步长为1的小卷积核,3×3卷积核同时也是最小的能够表示上下左右中心的尺寸。3x3卷积核卷积过程如下:
在这里插入图片描述
假设输入图像尺寸为假输入为5×5,使用2次3×3卷积后最终得到1×1的特征图,这和直接使用一个5×5卷积核得到1×1的特征图是一样的。也就是说2次3×3卷积可以代替一次5×5卷积,并且,2次3×3卷积的参数更少(2×3×3=18<5×5=25),而且会经过两次激活函数进行非线性变换,学习能力会更好。同样的3次3×3卷积可以替代一次7×7的卷积等等。除此之外,步长为1可以不会丢失信息,网络深度增加可以提高网络性能。
  在网络结构中还使用了Dropout,这是一种提高深度学习泛化能力的方法,它将连接到网络中某一百分比节点的权重设置为0。VGG16在两个dropout层中将百分比设为0.5。

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐