pytorch学习笔记(5):vgg实现以及一些tricks

本文介绍了如何使用PyTorch实现VGG网络,包括VGG的特点和结构,以及如何简化深度神经网络的代码。通过动态参数和Sequential模块,实现了网络层的高效构建,并给出了构建不同VGG架构的示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

参考文档: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 11 的卷积核,而 D 在对应的位置依然使用 3 ∗ 3 3*3 33 的卷积核。

接下来我们选择以 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

有几个小点提一下:

  1. 我们选择在 cifar10 上进行测试,图片的尺寸都是 3232。而论文原文中选择的输入尺寸是 224224,所以我们最后计算出来的全连接层的尺寸会不一致,全连接层的定义看下面代码。但是网络的结构都是一致的。
  2. 我们选择了 BN 层作为正则化项。
  3. 以输入的 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 语法的一些特点,我们可以用非常简单的接口来实现不同的网络结构,学习了如何封装背后的复杂操作,只暴露一个直接调用的接口。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值