参考文档:https://mp.weixin.qq.com/s/BAvS5ETwuv09pjBrqm3zWQ
通过前面的学习,现在我们可以利用已经会了的网络层来搭建自己想要的神经网络。今天的笔记从两方面去进行,首先是简单介绍 vgg,并利用前面学到的网络层来搭建出 vgg 的实现;另外一方面是参考 GitHub 上一个 vgg 实现的仓库,给大家介绍一个实现多层网络,多类网络时,如何进行简化代码的方法。
1、利用自带网络层来实现vgg
首先 vgg 网络来源于 ICLR 2015 的一篇 paper:https://arxiv.org/abs/1409.1556。其中的亮点在于提高网络深度,并用更小维度的卷积核来实现和大卷积核同样的感受野,但是大大减少了卷积的参数量,同时更多的深度意味着更多的激活函数,网络对于非线性的拟合就可以做的更好。
虽然我们的目标不是学习 cv,但是练习卷积相关的神经网络,并理解卷积网络的一些背后工作,最好的途径还是通过 cv 相关的一些 paper。所以这也是我选择 vgg 来练习的一个原因。
这张表提供了 vgg 的常见参数,我们常说的 vgg16 和 vgg19,就是 16 层的 vgg 网络和 19 层的 vgg 网络,这里的 vgg16 可以分为 C 和 D 两类,区别在于 C 选择了一些尺寸为
1
∗
1
1*1
1∗1 的卷积核,而 D 在对应的位置依然使用
3
∗
3
3*3
3∗3 的卷积核。
接下来我们选择以 D 为例,实现一个 vgg16 的网络结构。具体的实现就是照着 paper 中的网络参数,依次往下搭:
class VGG(nn.Module):
def __init__(self):
super(VGG, self).__init__()
self.conv1=nn.Sequential(
*[nn.Conv2d(3,64,3,1,1),nn.BatchNorm2d(64),nn.ReLU()]
)
# after convolution layer 1, the shape is 64x32x32
self.conv2=nn.Sequential(
nn.Conv2d(64,64,3,1,1),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(2)
)
# after convolution layer 2, the shape is 64x16x16
self.conv3=nn.Sequential(
nn.Conv2d(64,128,3,1,1),
nn.BatchNorm2d(128),
nn.ReLU()
)
# after convolution layer 3, the shape is 128x16x16
self.conv4=nn.Sequential(
nn.Conv2d(128,128,3,1,1),
nn.BatchNorm2d(128),
nn.ReLU(),
nn.MaxPool2d(2)
)
# after convolution layer 4, the shape is 128x8x8
self.conv5=nn.Sequential(
nn.Conv2d(128,256,3,1,1),
nn.BatchNorm2d(256),
nn.ReLU()
)
# after convolution layer 5, the shape is 256x8x8
self.conv6=nn.Sequential(
nn.Conv2d(256,256,3,1,1),
nn.BatchNorm2d(256),
nn.ReLU()
)
# after convolution layer 6, the shape is 256x8x8
self.conv7=nn.Sequential(
nn.Conv2d(256,256,3,1,1),
nn.BatchNorm2d(256),
nn.ReLU(),
nn.MaxPool2d(2)
)
# after convolution layer 7, the shape is 256x4x4
self.conv8=nn.Sequential(
nn.Conv2d(256,512,3,1,1),
nn.BatchNorm2d(512),
nn.ReLU()
)
# after convolution layer 8, the shape is 512x4x4
self.conv9=nn.Sequential(
nn.Conv2d(512,512,3,1,1),
nn.BatchNorm2d(512),
nn.ReLU()
)
# after convolution layer 9, the shape is 512x4x4
self.conv10=nn.Sequential(
nn.Conv2d(512,512,3,1,1),
nn.BatchNorm2d(512),
nn.ReLU(),
nn.MaxPool2d(2)
)
# after convolution layer 10, the shape is 512x2x2
self.conv11=nn.Sequential(
nn.Conv2d(512,512,3,1,1),
nn.BatchNorm2d(512),
nn.ReLU()
)
# after convolution layer 11, the shape is 512x2x2
self.conv12=nn.Sequential(
nn.Conv2d(512,512,3,1,1),
nn.BatchNorm2d(512),
nn.ReLU()
)
# after convolution layer 12, the shape is 512x2x2
self.conv13=nn.Sequential(
nn.Conv2d(512,512,3,1,1),
nn.BatchNorm2d(512),
nn.ReLU(),
nn.MaxPool2d(2)
)
# after convolution layer 13, the shape is 512x1x1
self.out=nn.Sequential(
nn.Dropout(),
nn.Linear(512,512),
nn.ReLU(),
nn.Dropout(),
nn.Linear(512,512),
nn.ReLU(),
nn.Linear(512,10)
)
def forward(self,x):
x=self.conv1(x)
x=self.conv2(x)
x=self.conv3(x)
x=self.conv4(x)
x=self.conv5(x)
x=self.conv6(x)
x=self.conv7(x)
x=self.conv8(x)
x=self.conv9(x)
x=self.conv10(x)
x=self.conv11(x)
x=self.conv12(x)
x=self.conv13(x)
x=x.view(x.size(0),-1)
output=self.out(x)
return output
有几个小点提一下:
- 我们选择在 cifar10 上进行测试,图片的尺寸都是 3232。而论文原文中选择的输入尺寸是 224224,所以我们最后计算出来的全连接层的尺寸会不一致,全连接层的定义看下面代码。但是网络的结构都是一致的。
- 我们选择了 BN 层作为正则化项。
- 以输入的 32*32 的图片为例,我们在每一层结束后,用注释介绍了当前层输出的数据形状。
self.out=nn.Sequential(
nn.Dropout(),
nn.Linear(512,512),
nn.ReLU(),
nn.Dropout(),
nn.Linear(512,512),
nn.ReLU(),
nn.Linear(512,10)
)
上面这个就是最后的三个全连接层,我们可以根据前面的表看到,如果长宽是 224 的图片输入后,最后的全连接层是 4096,而我们选择了长宽为 32 的图片,所以最后全连接层的宽度是 512。
接下来,我们展示一下按照之前我们的实现方法,一个网络如何连接一层层连接起来呢?
def forward(self,x):
x=self.conv1(x)
x=self.conv2(x)
x=self.conv3(x)
x=self.conv4(x)
x=self.conv5(x)
x=self.conv6(x)
x=self.conv7(x)
x=self.conv8(x)
x=self.conv9(x)
x=self.conv10(x)
x=self.conv11(x)
x=self.conv12(x)
x=self.conv13(x)
x=x.view(x.size(0),-1)
output=self.out(x)
return output
至此我们的代码核心部分就完成了,其余训练部分的操作和之前的笔记内容一样,这一段代码就可以跑起来了。那你是不是发现这样子写有点太麻烦了,长长的一大串,丑就罢了,还浪费写的时间?哈哈,我也发现了,所以要给你介绍下面的这个 trick,如何完成这类工作的简化呢?
2、简化过深神经网络层的写法
首先我们提一个 python 当中的基础语法,动态参数的使用。我们知道动态参数可以当函数不确定输入的时候使用,将所有实参以一个 tuple 传进来,形参就可以分别调用。在函数接收参数时可以使用动态参数,同样的输入的时候,也可以动态输入。我们将一个 list 前面加上 * 号传给函数,相当于将 list 中的每个元素都作为一个参数输入到函数中。
现在我们可以回头看看我们前面定义的 Sequential() 函数了,在它当中将所有的网络层进行了定义。打开 Sequential() 函数的源码,我们可以看到注释 docs 中写了,会按照输入的顺序来依次构建网络。所以我们可以构建一个 list,将所有的网络层都放进去。
以我们前面的代码为例,修改一下第一层的 Sequential() 函数,并不影响代码的执行:
class VGG(nn.Module):
def __init__(self):
super(VGG, self).__init__()
self.conv1 = nn.Sequential(
# nn.Conv2d(3, 64, 3, 1, 1),
# nn.BatchNorm2d(64),
# nn.ReLU()
*[nn.Conv2d(3, 64, 3, 1, 1), nn.BatchNorm2d(64), nn.ReLU()]
)
# after convolution layer 1, the shape is 64x32x32.
可以看到我们将原本的一个卷积层,BN 层,和一个激活层一起放到一个 list 中,然后以动态参数的形式传递给 Sequential(),代码的执行结果和原来一样。
那么,我们是不是就可以将前面的所有层都这样子写下来呢?当然可以,不但可以将所有层都这样子写下来,还可以将所有层都写进一个 list 中:
class VGG(nn.Module):
def __init__(self, features):
super(VGG, self).__init__()
self.features = features
self.out = nn.Sequential(
nn.Dropout(),
nn.Linear(512, 512),
nn.ReLU(True),
nn.Dropout(),
nn.Linear(512, 512),
nn.ReLU(True),
nn.Linear(512, 10),
)
def forward(self, x):
x = self.features(x)
x = x.view(x.size(0), -1)
output = self.out(x)
return output
现在我们把原始的代码简化到这个程度,可以看到多了一个 self.features,少了所有的卷积层,其余的保持不变。
那么现在的问题似乎变得简单了,我们输入一个变量 feature,是一个由多个网络层组成的 list,比如找个样子就可以了:
[Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)), ReLU(inplace=True), MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False), Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)), ReLU(inplace=True), MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False), Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)), ReLU(inplace=True), Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)), ReLU(inplace=True), MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False), Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)), ReLU(inplace=True), Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)), ReLU(inplace=True), MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False), Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)), ReLU(inplace=True), Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)), ReLU(inplace=True), MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)]
那么我们现在怎么完善代码呢?当然是再加一个函数,自动生成这样的一个 list,来看看代码吧:
def make_layers(cfg, batch_norm=False):
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)
if batch_norm:
layers += [conv2d, nn.BatchNorm2d(v), nn.ReLU(inplace=True)]
else:
layers += [conv2d, nn.ReLU(inplace=True)]
in_channels = v
return nn.Sequential(*layers)
cfg = [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M'],
这样子我们就可以看到提前定义一个 cfg,是一个包含了很多我们需要的参数的 list。数字表示对应卷积层的输出通道,而 ‘M’ 则表示进行池化层操作。因为 vgg 全部选了 3*3 的卷积核,所以我们不需要调整卷积核。
现在上面的代码是不是就清晰很多了,设置一个 layers 列表,对于一个传进去的 cfg 列表,对其中的每个参数进行判断,选择是卷积层还是池化层。同样的还有一个参数 batch_norm,这个参数决定是否在卷积层后加入 BN 层来进行正则化。
现在看起来代码简单了很多,我们只需要输入一个 cfg 格式,就可以自动生成我们想要的网络结构,并且最后的返回结果是 Sequential() 包装好的,将这个结果直接传入前面定义的网络类中就可以了。
这个时候再回头思考一下,既然现在生成一个网络这么容易了,我们是不是可以将所有的 vgg 格式都封装进来呢?那么我们按照论文中的思路,来组建一个各个类别 vgg 网络的结构吧:
cfg = {
'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'],
}
构建这样一个词典,key 是对应的网络结构的名称,value 是对应的列表,展示了我们想要的网络结构。如果我们想要定义 vgg16,且包含了 BN 层的网络,那么函数就可以定义如下:
def vgg16_bn():
return VGG(make_layers(cfg['D'], batch_norm=True))
是不是现在就很简单了?现在自己尝试一下吧,用 pytorch 实现一个你自己的 vgg 网络,我在 cifar10 上面跑了一下,手上的电脑没有 GPU,速度实在是太慢了-_-||
只跑了一点,我就手动停了,不过可以看出来我们的实现是有效的,模型在训练过程逐渐收敛,loss 在不断降低,acc 在上升。
3、总结
今天的笔记我们利用前面的知识点来组建一个 vgg 网络,并且在 cifar10 数据集上进行了验证。除此之外,利用 python 语法的一些特点,我们可以用非常简单的接口来实现不同的网络结构,学习了如何封装背后的复杂操作,只暴露一个直接调用的接口。