1 前言

经过前面两篇文章的介绍,我们已经了解了LeNet5和AlexNet网络模型。但是总体上来说两者的网络结构几乎并没有太大的差别,仅仅,同时网络的深度以及参数的规模也没有太大的变化。在接下来的这篇文章中,我们将会看到卷积网络中的第三个经典模型VGG。在这篇文章中,作者对卷积网络卷积深度的设计进行了一个探索,并且通过尝试逐步加深网络的深度来提高模型的整体性能。这使得VGG在当年的ILSVRC任务中取得了TOP1的成绩。

2 VGG网络

VGG网络产生于2014年的Visual Geometry Group 实验室,而这三个单词的首字母也代表了VGG的含义。VGG网络总体上一共有五种网络架构,但是从本质上来说这五种网络架构都是一样的,仅仅只是在卷积的深度上有所差别。下面,就让我们一步步的来探索VGG的网络结构。公众号后台回复“论文”即可获取论文下载链接!

2.1 网络结构

如图1所示,一共有六列,其中第二列是在第一例的基础上加入了LRN标准化操作[2]。同时,需要注意的是,例如图中A结构列出的"11 weight layers"的含义是其包含有11个参数层,即8个卷积层和3个全连接层,而没有包含池化层和非线性变换层。通常,这也是一种默认的叫法,说网络有多少层的时候只计算有多少层含有可训练的参数。

在整个网络的训练过中,VGG固定输入网络图片的大小为 224 × 224 224\times224 224×224的RGB图像,并且在输入网络之前仅仅只是做了去均值化的处理,即在训练集中每个像素值都会减去整体像素的一个平均值。接着,预处理完成的图片将会被喂入到一些列仅仅只由窗口大小为 3 × 3 3\times3 3×3的卷积核堆叠而成的卷积网络中。但是从图1中的模型C可以看出,其还使用了窗口大小为 1 1 1的卷积。这是因为作者认为, 1 × 1 1\times1 1×1的卷积既可以增加模型的非线性拟合能力,同时还不会改变卷积层的可视野。


VGG: Very deep convolutional networks for large-scale image recognition_卷积

图 1. VGG网络架构图

在五种网络架构中,所有卷积时的步长都被设置成了固定的 1 1 1;并且为了使得卷积后特征图的大小同输入时保持一直,网络在每次卷积之前均做了对应的填充处理。在池化方面,五种网络模型均使用了五次最大池化操作,其窗口大小均为 2 × 2 2\times2 2×2,移动步长均为 2 2 2

在完成一系列的卷积处理后,VGG会将卷积得到的特征图再喂入到全连接网络中:其中前两个全连接层均包含有 4096 4096 4096个神经元;而最后一个全连接层神经元的个数则是对应的分类数 1000 1000 1000,紧接着再是一个Softmax的分类层。对于所有的五种网络结构来说,这部分都采用了相同的配置。最后,在VGG中,所有的隐藏层(所有卷积层和前两个全连接层)都进行了ReLU非线性变换。

从图1所示的网络结构可以看出,在整个过程中作者都仅仅只使用了 3 × 3 3\times3 3×3大小的卷积核,而摒弃了诸如 5 × 5 5\times5 5×5或者是 7 × 7 7\times7 7×7这类更大卷积核。因为作者研究发现,连续两次(中间没有pooling)使用窗口为 3 3 3的卷积核卷积后的可视野(receptive field)等同于一次窗口大小为 5 5 5的卷积过程;而连续三次(中间没有pooling)使用 3 × 3 3\times3 3×3卷积,其效果等价于一次窗口大小为 7 7 7的卷积过程。尽管两种方式都能获得同样的可视野,但作者依旧采用了前者。


VGG: Very deep convolutional networks for large-scale image recognition_2d_02

图 2. 不同窗口大小卷积对比图

如图2所示,左右两边均是大小为 5 × 5 5\times5 5×5的输入,左边通过连续两次 3 × 3 3\times3 3×3大小的卷积核进行卷积后能够实现 5 × 5 5\times5 5×5的可视野;而右边仅用一次 5 × 5 5\times5 5×5大小的卷积核进行卷积后同样也能够实现 5 × 5 5\times5 5×5的可视野。那这样做的好处是什么呢?以窗口大小为 7 7 7和连续三个窗口大小为 3 3 3的卷积过程为例,作者认为:

第一,连续三次卷积的同时也进行非线性变化得到的模型,比仅仅只进行一次卷积和非线性变换得到的模型要更具有泛化能力,尽管两者能够获得同样大小的可视野;而这也可以看作是对 7 × 7 7\times7 7×7的卷积核施加了一次正则化的结果。

第二,假设卷积时输入输出的通道数均为 C C C,则一次 7 × 7 7\times7 7×7的卷积需要的参数量为 7 2 C 2 = 49 C 2 7^2C^2=49C^2 72C2=49C2,而三次 3 × 3 3\times3 3×3的卷积需要的参数量为 ( 3 2 C 2 ) 3 = 27 C 2 (3^2C^2)3=27C^2 (32C2)3=27C2,前者比后者多了 81 % 81\% 81%的参数量。

2.2 参数设置

在整个网络的训练过程中,作者选择的是基于动量的随机梯度下降算法,设置了​​batch_size=256​​​,​​momentum=9.9​​。对于VGG中的前两个全连接层,作者还对其施加了 L 2 L_2 L2正则化( λ = 5 ⋅ 1 0 − 4 \lambda=5\cdot10^{-4} λ=5104)和随机丢弃( p = 0.5 p=0.5 p=0.5)。对于网络的学习率,作者将其初始化为 0.02 0.02 0.02,并且当其在验证集上的准确率没有发生变化时,就将其再缩小十倍。最后,对于参数的初始化等方法文中描述得略显复杂,我们在这里就不再细说,后面我们将采用更为优秀的kaiming初始化方法进行初始化。下面,我们就来看看该如何一步一步的实现这些网络。

3 VGG实现

从图1可以看出,VGG有多种不同类型的网络配置。如果是按照之前的实现思路,那么 就得写多份代码,但显然这里面是由很多重复工作的。那能不能写一个通用的函数,然后只需要传入对应的配置参数就能够实现对应的网络结构呢?

3.1 通用模块

如下代码所示就是A、B、D和E四种网络结构的配置参数,其中’M’表示该层为最大池化层,而其它的数字则表示对应的卷积核个数。至于网络结构C我们这里就不进行示例,有兴趣的可以自己进行修改。

config={
'A':[64,'M',128,'M',256,256,'M',512,512,'M',512,512,'M'],
'B':[64,64,'M',128,128,'M',256,256,'M',512,512,'M',512,512,'M'],
'D':[64,64,'M',128,128,'M',256,256,256,'M',512,512,512,'M',512,512,512,'M'],
'E'[64,64,'M',128,128,'M',256,256,256,256,'M',512,512,512,512,'M',512,512,512,512,'M'],
}#M表示maxpool

再列完这些网络的参数配置后,下面我们就来实现这样一个函数:

def make_layers(cfg):
layers = []
in_channels = 3
for v in cfg:
if v == 'M':
layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
else:
conv2d = nn.Conv2d(in_channels, v, kernel_size=3, padding=1)
layers += [conv2d, nn.ReLU(inplace=True)]
in_channels = v
return nn.Sequential(*layers) # *号的作用解包这个list

如上代码所示,​​cfg​​​传递进来的就是上面​​config​​​中对应的某个网络配置,然后再根据配置实现对应的卷积操作。其中倒数第三行代码中​​inplace=True​​​的意思是在原地进行修改,那为什么需要在原地进行修改呢?这是由于​​nn.ReLU​​​层并没有额外的参数,所以我们可以将​​ReLU​​​后的结果直接赋值到输入的变量中,从而避免了新的内存开销。但是其它层并不建议用​​inplace=True​​​这一设置,因为这样的话上层的输出结果就会被本层的输出结果给覆盖掉,从而后续无法在引用上层的输出。最后一行代码是将这个列表“解开”,然后将里面的层放入到​​nn.Sequential​​中。

这样,我们就完成了对于全连接之前所有网络结构的前向传播过程。下面我们以结构B为了,通过​​make_layers​​来输出示例:

net = make_layers(config['B'])
print(net)

(0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): ReLU(inplace=True)
(2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(3): ReLU(inplace=True)
(4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(5): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(6): ReLU(inplace=True)
(7): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(8): ReLU(inplace=True)
(9): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(10): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(11): ReLU(inplace=True)
(12): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(13): ReLU(inplace=True)
(14): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(15): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(16): ReLU(inplace=True)
(17): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(18): ReLU(inplace=True)
(19): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(20): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(21): ReLU(inplace=True)
(22): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(23): ReLU(inplace=True)
(24): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
)

从输出结果可以看到,这里列出了网络结构B中,卷积部分每一层的详细参数配置

3.2 前向传播

在上面的通用模块中,我们仅仅只是实现了不同配置下的卷积网络,而后面的三个全连接层还没有进行实现。下面我们就来对这部分代码进行实现。

class VGG(nn.Module):
def __init__(self, features, num_classes=1000, init_weights=True):
super(VGG, self).__init__()
self.features = features
self.avgpool = nn.AdaptiveAvgPool2d((7, 7))
self.classifier = nn.Sequential(
nn
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),
)
if init_weights:
self._initialize_weights()

def forward(self, x):
x = self.features(x)
x = self.avgpool(x)
x = torch.flatten(x, 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)

在上面的代码中,可以看到类​​VGG​​​在进行初始化时传入了​​feature​​​这么一个参数,而这个参数恰好就是函数​​make_layer​​​的返回值,即一个​​nn.Sequential()​​​,并且在我们将其赋值给了​​self.features​​​。接着,我们定义了​​nn.AdaptiveAvgPool2d()​​​,以及后续的三个全连接层。这里​​AdaptiveAvgPool2d​​​的作用是将前面输入的结果强制改变成指定的形状。例如在上面的代码中,不管上一层输出特征图的长和宽是多少,都将被自适应的改变成​​[7,7]​​的形状。

除此之外,在这里我们还额外的实现了一个​​_initialize_weights()​​方法。这是因为在利用梯度下降算法优化参数的过程中,对于初始时的选择是非常重要的,一个好的初始化参数在少数几次的迭代过程中就可能到达全局最优解,而一个糟糕的初始化参数往往可能使得目标函数发散。在​​_initialize_weights()​​​中,我们依据不同的网络层选择了不同的初始化方法。在这里,我们也第一次的见到了多用于卷积层初始化的​​kaiming_normal_()​​方法。关于这个初始化方法的原理,等到后续有机会我们再进行介绍。通过这一步,我们也就知晓了在Pytorch中如何自定义的设置每个网络层的参数初始化方法。

最后,我们在​​forward()​​方法中调用了各个部分的特征提取模块,完成了VGG网络的整个通用的前向传播过程。因此,后续我们要使用某个特定的VGG模型时,就能够以如下方式进行调用(这里以VGG19为例):

def vgg_19(num_classes=1000, init_weights=True):
cnn_features = make_layers(config['E'])
model = VGG(cnn_features, num_classes=num_classes, init_weights=init_weights)
return model

3.3 训练网络

在定义完成一系列的前向传播过程后,我们就可以来实现模型的训练部分了。这部分代码整体上和AlexNet中代码一样,因此就不再赘述,完整代码可以参见[1]。但这里需要注意的是,由于我们使用到的数据集依旧是Fashion MNIST,而它只有一个通道,因此我们需要将前面​​make_layer()​​​函数中的​​in_channels​​​设置为​​1​​。最后,我们就能够以如下方式进行模型的训练:

if __name__ == '__main__':
model = vgg_19(num_classes=10)
vgg19 = MyModel(model=model, batch_size=128, epochs=5, learning_rate=0.001)
vgg19.train()

3.4 训练结果

Epochs[1/5]---batch 0---acc on test 0.8686
Epochs[2/5]---batch 0---acc on test 0.8919
Epochs[3/5]---batch 0---acc on test 0.8994
Epochs[4/5]---batch 0---acc on test 0.9033
Epochs[5/5]---batch 0---acc on test 0.9054

从上面的结果可以看出,其相比于AlexNet网络模型,在同等条件下提升的准确率并不明显。其原因可能在于VGG19的参数量相比于AlexNet要大很多,而这里仅仅只是训练了5个Epochs,显然模型的参数并没有得到很好的训练。因此兴趣的朋友可以同时将AlexNet和VGG19都训练50个Epochs再来进行对比一下。

到此为止,对于VGG网络模型的原理以及如何通过Pytorch来进行实现就算是介绍完了。这里顺便提一句,以上的整个代码实现笔者都是直接取自于Pytorch的官方实现,并且我们还可以直接通过下面这一行代码来完成对于VGG模型的调用:

from torchvision.models import vgg19

当然,Pytorch官方实现的模型还包括了AlexNet,ResNet和GoogleNet等,同时还能直接使用对应已经训练好的预训练模型。

4 总结

在这篇文章中,笔者首先VGG网络的原理以及VGG中各个模型的细节之处;然后笔者以Pytorch官方实现的VGG模型代码为例,详细介绍了如何一步一步的来实现VGG中的各个网络模型,包括对于不同网络模块的拼接、不同网络层参数的自定义初始化方法等;最后我们还将VGG19在Fashion MNIST上的测试准确率同AlexNet模型进行了对比。在下一篇的文章中,我们将开始学习卷积网络中的第四个经典模型NIN。