vlambda博客
学习文章列表

卷积池化与LeNet5网络模型

1 前言

经过前面三篇文章的介绍,我们已经知道了、以及等。在接下来的这篇文章中,笔者将开始介绍如何通过Pytorch这一深度学习框架来实现卷积和池化的操作,以及各个API的使用介绍和说明。最后,笔者还会介绍卷积神经网络的入门级网络结构LeNet5,我们也将会通过LeNet5这个网络来完成fashion mnist数据集的分类任务。

2 API接口介绍

在Pytorch中,对于网络中的一些基本功能函数(例如:卷积、池化、激活函数等)都被放在了torch.nn.functional这个模块中。因此,为了方便后续的使用,按惯例我们都会以import torch.nn.functional as F的方式来导入各类基本功能函数,然后以F.的方式来进行调用。

2.1 卷积操作

在介绍如何使用卷积这一功能函数前,我们先来介绍一下Pytorch中如何表示(类)图像数据。在Pytorch(tensorflow)中,都是通过四个维度来对图像数据进行表示,分别是:样本个数(batch_size)、高度(heigh)、宽度(width)和通道数(channel)。但是对于不同的深度学习框架来说,其在这个维度上的默认顺序并不一样。在Pytorch中,这四个维度的顺序为[batch_size,channels,heigh,width];但是在tensorflow中,这一顺序却为[batch_size,heigh,width,channels]。可以看出,两者仅仅是把通道数这一维度放到了不同的位置上。

同样的,对于卷积核来说也需要用四个维度来进行表示,分别是:高度(heigh)、宽度(width)、上一层输入的通道数(in_channels)和输出特征图的通道数(卷积核个数)(out_channels)。在Pytorch中,这四个维度的顺序为[out_channels,in_channels,heigh,width];而在tensorflow中,这一顺序却是[heigh,width,in_channels,out_channels]。所以,需要注意的就是在使用不同的深度学习框架时一定要弄清楚输入数据的形式。

2.1.1 单卷积核卷积

如图1所示,在上一篇文章中我们以该图中的示例介绍了如何手动的来计算卷积的结果,现在我们看看如何通过框架来进行计算。

卷积池化与LeNet5网络模型
图 1. conv2d的使用

首先我们需要将卷入和卷积核这两个变量给定义出来:

inputs = torch.tensor([02010020112120010010-11101,
                       0001010001100100100011100,
                       # [batch_size,in_channels,high,width]
                       1100001001011110100001000]).reshape([1355])


filters = torch.tensor([[[[200],
                          [101],
                          [030]],
                         [[101],
                          [000],
                          [110]],
                         [[001],
                          [111],
                          [110]]]])  # [1,3,3,3] [ [filter_nums/output_channels,input_channels,high,width]

bias = torch.tensor([1])

可以看到,对于inputs来说我们首先定义了75个值,然后再将其reshape成了图像的表示格式;而对于filters来说,我们在定义的时候就直接写成了框架所需要的形式。

接下来,我们仅仅只需要通过下面一行代码就能够完成对于卷积的计算过程:

result = F.conv2d(inputs, filters, bias=bias, stride=1, padding=0)

其中strid=1表示移动一次的步长设为1,padding=0表示不进行填充。

最后,计算得到的结果为:

卷积后的结果:tensor([[[[ 718,  6],
          [101111],
          [1513,  7]]]])
结果的形状:torch.Size([1133])

2.1.2 多卷积核卷积

如图2所示为多卷积核的计算场景。同上面一样,我们只需要分别定义出输入和卷积核,通过F.conv2d()函数就能计算得到卷积后的结果。

卷积池化与LeNet5网络模型
图 2. conv2d的使用
filters = torch.tensor([[[[200],
                          [101],
                          [030]],
                         [[101],
                          [000],
                          [110]],
                         [[001],
                          [111],
                          [110]]],
                        [[[010],
                          [111],
                          [010]],
                         [[010],
                          [101],
                          [010]],
                         [[101],
                          [010],
                          [101]]]
                        ])  # [2,3,3,3] [ [filter_nums/output_channels,input_channels,high,width]
bias = torch.tensor([1-3])
result = F.conv2d(inputs, filters, padding=0, bias=bias)

最后得到的计算结果为:

卷积后的结果:tensor([[[[ 718,  6],
          [101111],
          [1513,  7]],

         [[ 6,  5,  5],
          [ 7,  4,  3],
          [ 3,  6,  1]]]])
结果的形状:torch.Size([1233])

2.2 池化操作

说完了卷积的操作,我们最后再来简单的看看如何进行池化操作,然后就进入到LeNet5网络的介绍。

卷积池化与LeNet5网络模型
图 3. 最大池化操作

如图3所示为最大池化的计算原理,在Pytorch中我们在定义好输入的特征图后,通过F.max_pool2d()便可以得到池化后的结果。

feature_maps = torch.tensor(
    [5201002307232,
     211648127159,
     4201027136242,
     2326980107257],dtype=torch.float32).reshape([1255])
print(feature_maps)
result = F.max_pool2d(input=feature_maps, kernel_size=[33], stride=[11])
print(result)

其中,kernel_size=[3,3]stride=[1,1]分别表示池化的窗口大小为 以及移动的步长为 。当然,这种情况下还可以直接写成kernel_size=3,stride=1

最后,计算后的结果为:

tensor([[[[ 5.,  3.,  7.],
          [ 6.,  8.,  8.],
          [ 7.,  8.,  9.]],

         [[ 7.,  7.,  6.],
          [ 9.,  9.,  9.],
          [10.,  9.,  9.]]]])

3 LeNet5网络

3.1 网络结构

在介绍完卷积操作后,我们再来看如何实现一个简单的LeNet5网络结构。

卷积池化与LeNet5网络模型
图4. LeNet5网络结构图

如图4所示就是LeNet5的网络结构图,顺便插一句这里的5指的是包含有5个网络权重层(不含参数的层不计算在内)的网络层,即两个卷积层和三个全连接层。根据图4中各层输出的结果,我们可以推算得到其各层对应的超参数及卷积核形状应如下表所示:

表 1. LeNet5参数表

从表1可以看出每层权重参数的一个具体情况,包括参数的形状和数量。对于整个LeNet5网络来说,其参数量就应该是6万左右。

LeNet5的网络结构总体上来说还比较简单,它通过多次卷积和池化的组合来对输入进行特征提取,最后再以全连接网络进行分类。这种多次卷积加池化的组合看似简单粗暴,但在实际问题中却能取得不错的效果,以至于后续出现了比它更深的卷积网络,后续笔者也将会陆续进行介绍。

3.2 网络实现

在介绍完了LeNet5的网络结构后,下面我们就来看看如何通过Pytorch框架对其进行实现。首先需要明白的是,我们在利用框架实现一些网络结构时,我们只需要写出网络对应的前向传播过程即可。剩余其它部分的编码基本上就是按部就班,几乎可以进行通用。

  • 前向传播

    class LeNet5(nn.Module):
        def __init__(self, ):
            super(LeNet5, self).__init__()
            self.conv = nn.Sequential(  # [n,1,28,28]
                nn.Conv2d(165, padding=2),  # (in_channels, out_channels, kernel_size])
                nn.ReLU(),  # [n,6,24,24]
                nn.MaxPool2d(22),  # kernel_size, stride  [n,6,14,14]
                nn.Conv2d(6165),  # [n,16,10,10]
                nn.ReLU(),
                nn.MaxPool2d(22)  # [n,16,5,5]
            )
            self.fc = nn.Sequential(
                nn.Flatten(),
                nn.Linear(16 * 5 * 5120),
                nn.ReLU(),
                nn.Linear(12084),
                nn.ReLU(),
                nn.Linear(8410)
            )

    如上代码所示就是我们实现的LeNet5网络的前向传播部分,可以看到通过pytorch很容易的就完成了。同时,这里需要说明的是:

    nn.Sequential里面输入的必须是网络层(即要继承自类nn.Module),所以nn.Conv2d(其内部同样是通过F.conv2d来实现的)作为一个网络层其只需要按序传入in_channelsout_channelskernel_sizepadding这四个参数即可;而对于F.conv2d,我们在后续定义自己的网络操作时才会用到。

    ②由于LeNet5原始图片输入的大小为 ,所以上述我们在第一次卷积时设置了padding=2,即填充两圈0,这样后面的输出形状就能保持与LeNet5相同。

  • 模型配置

    在定义好前向传播后,我们还可以对整个网络(或者是其中一层)的参数设置情况进行输出:

    if __name__ == '__main__':
        model = LeNet5()
        print(model)
        print(model.fc[3])

    #
    LeNet5(
      (conv): Sequential(
        (0): Conv2d(16, kernel_size=(55), stride=(11), padding=(22))
        (1): ReLU()
        (2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
        (3): Conv2d(616, kernel_size=(55), stride=(11))
        (4): PrintLayer()
        (5): ReLU()
        (6): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
      )
      (fc): Sequential(
        (0): Flatten()
        (1): Linear(in_features=400, out_features=120, bias=True)
        (2): ReLU()
        (3): Linear(in_features=120, out_features=84, bias=True)
        (4): ReLU()
        (5): Linear(in_features=84, out_features=10, bias=True)
      )
    )

    Linear(in_features=120, out_features=84, bias=True)

    同时,若是需要查看每一层计算后输出的形状,那么只需要将如下定义的打印层出入到nn.Sequential()中的相应位置即可。

    class PrintLayer(nn.Module):
        def __init__(self):
            super(PrintLayer, self).__init__()

        def forward(self, x):
            print(x.shape)
            return x
  • 训练结果

    对于其它部分的代码在此就不再赘述,可以直接参见源码[2]。

    Epochs[4/5]---batch[937/550]---acc 0.9219---loss 0.2219
    Epochs[4/5]---batch[937/600]---acc 0.9062---loss 0.2414
    Epochs[4/5]---batch[937/650]---acc 0.8438---loss 0.3668
    Epochs[4/5]---batch[937/700]---acc 0.875---loss 0.3722
    Epochs[4/5]---batch[937/750]---acc 0.9062---loss 0.4362
    Epochs[4/5]---batch[937/800]---acc 0.9688---loss 0.1389
    Epochs[4/5]---batch[937/850]---acc 0.9062---loss 0.3012
    Epochs[4/5]---batch[937/900]---acc 0.9062---loss 0.2476
    Epochs[4/5]--acc on test 0.8703

    可以看到,大约在5个Epochs后,模型在测试集上的准确率达到了0.87。

4 总结

在本篇文章中,笔者首先介绍了在pytorch和tensorflow中是如何来表示图片以及在两个框架中的区别;接着介绍了pytorch中卷积和池化操作两个API的使用方法,示例了如何通过pytorch来完成卷积和池化的计算过程;最后介绍了卷积网络中的LeNet5网络,包括模型的结构、参数量的计算以及如何通过pytorch来实现等。在下一篇的文章中,我们将开始学习卷积网络中的第二个经典模型AlexNet。

引用

[1] LeCun, Y., Bottou, L., Bengio, Y., & Haffner, P. (1998). Gradient-based learning applied to document recognition. Proceedings of the IEEE, 86(11), 2278-2324.

[2]https://github.com/moon-hotel/DeepLearningWithMe

推荐阅读

月来客栈 发起了一个读者讨论 卷积池化与LeNet5网络模型