PyTorch 学习笔记(六):卷积神经网络概念(从头梳理一遍卷积神经网络的演变史),提取层结构,自定义初始化

一. 卷积神经网络的原理和结构

在介绍卷积神经网络之前,先提出三个观点,正是这三个观点使得卷积神经网络能够真正起作用。

1. 局部性

对于一张图片而言,需要检测图片中的特征来决定图片的类别,通常情况下这些特征都不是由整张图片来决定的,而是由一些局部的区域来决定的。
比如下图4.1中的鸟啄,该特征只存在于图片的局部中。
PyTorch 学习笔记(六):卷积神经网络概念(从头梳理一遍卷积神经网络的演变史),提取层结构,自定义初始化_第1张图片

2. 相同性

对于不同的图片,如果它们具有同样的特征,这些特征会出现在图片的不同位置,也就是说可以用同样的检测模式去检测不同图片的相同特征,只不过这些特征处于图片中的不同位置,但是特征检测所做的操作几乎一样。

图4.2中两张图片的鸟啄处于不同的位置,但是可以用相同的检测模式去检测。
PyTorch 学习笔记(六):卷积神经网络概念(从头梳理一遍卷积神经网络的演变史),提取层结构,自定义初始化_第2张图片

3. 不变性

对于一张大图片,如果我们进行下采样,那么图片的性质基本保持不变。
图4.3经过下采样还是能够看出来是一张鸟的图片。

PyTorch 学习笔记(六):卷积神经网络概念(从头梳理一遍卷积神经网络的演变史),提取层结构,自定义初始化_第3张图片
前面介绍的全连接神经网络存在着一个问题,比如在MNIST数据集上,图片大小为 28x28,那么第一个隐藏层神经元的权重数目就是 28x28=784,这似乎不是很大,但这只是一张小图片,且是灰度图。对于一张较大的图片而言,比如 200x200x3,就会导致权重数目是 200x200x3=120000,如果设置几个隐藏层中的神经元数目,就会导致参数增加特别快。但是这样的图片其实算不上大图片,所以以全连接神经网络处理图像不是一个好的选择。

根据上面提到的三个图像的特性,由此我们提出卷积神经网络,它是一个3D容量的神经元,也就是说神经元是以三个维度来排列的:宽度、高度和深度。比如输入的图片是 32x32x3。

二. 卷积层

卷积层是卷积神经网路和核心,一个网络的计算量主要集中在卷积层。

1. 局部连接

在处理图像时,让每一个神经元都与那个层中的所有神经元进行全连接是不现实。相反,让每个神经元只与输入数据的一个局部区域连接时可行的,为什么可以这麽做呢?其实这是因为图片特征的局部性,所以只需要通过局部就能提取出相应的特征。

与神经元连接的空间大小叫做神经元的感受野,它的大小是一个认为设置的超参数,这其实就是滤波器的宽和高。在深度方向上,大小总是和图片的深度一致。最后强调一下,对待空间维度和深度维度是不同的,连接在空间上是局部的,但是在深度上总是和输入的数据深度保持一致。

2. 参数共享

在卷积层使用参数共享可以有效地减少参数的个数,这样之所以行得通,是因为之前介绍的特征的相同性,也就是说相同的滤波器能够检测出不同位置的相同特征。

三. 池化层

通常会在卷积层之间周期性插入一个池化层,其作用是逐渐降低数据体的空间尺寸,这样就能够减少网络中的参数量,减少计算资源耗费,同时也能够有效地控制过拟合。

池化层之所以有效,是因为之前介绍的图片特征具有平移不变性,也就是通过下采样不会丢失图片拥有的特征,由于这种特性,我们可以将图片缩小再进行卷积处理,这样能够大大降低卷积运算的时间。

一般有最大池化和平均池化两种常用的池化方式,在实际中证明,在卷积层之间引入最大池化的效果是最好的,而平均池化一般放在网络的最后一层。

小滤波器的有效性:一般而言,几个小滤波器卷积层的组合比一个大滤波器卷积层要好,比如层层堆叠了3个3x3的卷积层,中间含有非线性激活层,在这种排列下面,第一个卷积层中每个神经元对输入数据的感受野是 3x3。第二层卷积层对第一层卷积层的感受野也是 3x3,这样对于输入数据的感受野就是 5x5,同样,第三层卷积层上对第二层卷积层的感受野也是 3x3,这样第三层卷积层对于第一层的输入数据的感受野就是 7x7.

这里为什么不直接使用一个 7x7的卷积核,而是选择3个3x3的。首先在于多个卷积层与非线性激活层交替的结构,比单一卷积层的结构更能提取出深层的特征;其次,减少了参数量和计算量。

四. PyTorch卷积模块

PyTorch作为一个深度学习库,卷积神经网络是其中一个最为基础的模块,卷积神经网络中所有层结构都可以通过nn这个包调用,下面具体介绍如何调用每种层结构,以及每个函数中的参数。

1. 卷积层

nn.Conv2d(in_channels, out_channels, kernel_size, stride=1,padding=0, dilation=1, groups=1, bias=True)就是PyTorch中的卷积模块了,里面常用的参数有5个,分别是 in_channels,out_channels,kernel_size,stride,padding,除此之外还有参数dilation,groups,bias。下面来具体解释每个参数的含义

  • in_channels对应的是输入数据体的深度;
  • out_channels对应的是输出数据体的深度;
  • kernel_size表示滤波器的大小,可以使用一个数字来表示高和宽相同的卷积核,比如kernel_size=3,也可以使用不同的数字来表示高和宽不同的卷积核,比如kernel_size=(3, 2);
  • stride表示滑动的步长;
  • padding=0表示四周不进行零填充,padding=1表示四周进行1个像素点的填充;
  • bias是一个布尔值,默认bias=True,表示使用偏置;
  • groups表示输出数据体深度上和输入数据体深度上的联系,默认groups=1,也就是所有的输出和每一个输入都是相关联的,groups>1说明将输入和输出的深度分别分为 groups组各自进行卷积将得到的输出拼接(组卷积);
  • dilation表示卷积对于输入数据体的空间间隔,默认dilation=1;(空洞卷积

2. 池化层

nn.MaxPool2d()表示网络中的最大值池化 ,其中的参数有kernel_size、stride、padding、dilation、return_indices、ceil_mode,下面来具体解释一下它们各自的含义:

  • kernel_size、stride、padding、dilation之前的卷积层已经介绍过了,是相同的含义;
  • return_indices表示是否返回最大值所处的下标,默认return_indices=False;
  • ceil_mode表示使用一些方格代替层结构,默认ceil_mode=False,一般都不会设置这些参数;
  • nn.AvgPool2d表示均值池化,里面的参数和 nn.MaxPool2d类似,但多一个参数count_include_pad,这个参数表示计算均值的时候是否包括零填充,默认count_include_pad=True;

搭建一个简单的由卷积层、激活层和池化层组合在一起的层结构,定义了3个这样的层结构,最后定义了全连接层,输出10.

import torch
from torch import nn
from torchstat import stat
class SimpleCNN(nn.Module):
    def __init__(self):
        super(SimpleCNN, self).__init__()   # [b, 3, 32, 32]
        # 卷积+ReLU+池化
        layer1 = nn.Sequential()
        layer1.add_module('conv1', nn.Conv2d(3, 32, 3, 1, padding=1))   # [b, 32, 32, 32]
        layer1.add_module('relu1', nn.ReLU(True))
        layer1.add_module('pool1', nn.MaxPool2d(2, 2))    # [b, 32, 16, 16]
        self.layer1 = layer1

        # 卷积+ReLU+池化
        layer2 = nn.Sequential()
        layer2.add_module('conv2', nn.Conv2d(32, 64, 3, 1, padding=1))    # [b, 64, 16, 16]
        layer2.add_module('relu2', nn.ReLU(True))
        layer2.add_module('pool2', nn.MaxPool2d(2, 2))  # [b, 64, 8, 8]
        self.layer2 = layer2

        # 卷积+ReLU+池化
        layer3 = nn.Sequential()
        layer3.add_module('conv3', nn.Conv2d(64, 128, 3, 1, padding=1))      # [b, 128, 8, 8]
        layer3.add_module('relu3', nn.ReLU(True))
        layer3.add_module('pool3', nn.MaxPool2d(2, 2))     # [b. 128, 4, 4]
        self.layer3 = layer3

        # 2048-512-64-10
        layer4 = nn.Sequential()
        layer4.add_module('fc1', nn.Linear(2048, 512))
        layer4.add_module('fc_relu1', nn.ReLU(True))
        layer4.add_module('fc2', nn.Linear(512, 64))
        layer4.add_module('fc_relu2', nn.ReLU(True))
        layer4.add_module('fc3', nn.Linear(64, 10))
        self.layer4 = layer4
    def forward(self, x):
        conv1 = self.layer1(x)
        conv2 = self.layer2(conv1)
        conv3 = self.layer3(conv2)
        fc_input = conv3.view(conv3.size(0), -1)
        fc_output = self.layer4(fc_input)
        return fc_output

使用print(model)打印出网络中定义的层结构:


SimpleCNN(
  (layer1): Sequential(
    (conv1): Conv2d(3, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (relu1): ReLU(inplace)
    (pool1): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (layer2): Sequential(
    (conv2): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (relu2): ReLU(inplace)
    (pool2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (layer3): Sequential(
    (conv3): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (relu3): ReLU(inplace)
    (pool3): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (layer4): Sequential(
    (fc1): Linear(in_features=2048, out_features=512, bias=True)
    (fc_relu1): ReLU(inplace)
    (fc2): Linear(in_features=512, out_features=64, bias=True)
    (fc_relu2): ReLU(inplace)
    (fc3): Linear(in_features=64, out_features=10, bias=True)
  )
)

3. 提取层结构

使用print(model)可以将model中所有的层结构打印出来,但是对于一个给定给定的模型,我们不想要模型中的所有层结构,只想提取网络中的某一层或者几层,应该如何实现呢?

首先来看看nn.Module()的几个重要属性。第一个是children(),这个会返回下一级模块的迭代器,比如上面这个模型,它只会返回self.layer1,self.layer2,self.layer3以及self.layer4上的迭代期,不会返回它们内部的东西;modules()会返回模型中所有模块的迭代器,这样就有一个好处,可以访问到最内层,比如self.layer1.conv1这个模块;还有一个与它们相对应的是named_children()属性以及named_modules(),这两个不仅会返回模块的迭代器,还会返回网络层的名字。

下面来提取网络中我们需要的层,如果希望能够提取出前面两层,可以通过下面的代码实现:

print(nn.Sequential(*list(model.children()))[:2])

输出结果:

Sequential(
  (0): Sequential(
    (conv1): Conv2d(3, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (relu1): ReLU(inplace)
    (pool1): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (1): Sequential(
    (conv2): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (relu2): ReLU(inplace)
    (pool2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
)

如果希望提取出模型中所有的卷积层

for layer in model.named_modules():
    if isinstance(layer[1], nn.Conv2d):
        print(layer[0], layer[1])

结果如下:

layer1.conv1 Conv2d(3, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
layer2.conv2 Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
layer3.conv3 Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))

4. 如何提取参数并初始化

有时候提取出层结构并不够,需要对里面的参数进行初始化,那么如何提取出网络的参数并进行初始化呢?首先nn.Module()里面有两个特别重要的关于参数的属性,分别是named_parameters()和parameters()会给出一个网络所有参数的迭代器。
对于一个迭代器对象,想要打印出其里面的值,需要用for循环逐个输出。

for param in model.named_parameters():
    print(param[0])

结果如下:

layer1.conv1.weight
layer1.conv1.bias
layer2.conv2.weight
layer2.conv2.bias
layer3.conv3.weight
layer3.conv3.bias
layer4.fc1.weight
layer4.fc1.bias
layer4.fc2.weight
layer4.fc2.bias
layer4.fc3.weight
layer4.fc3.bias

如何对权重初始化呢?非常简单,因为权重是一个Variable,所以只需要取出其中的data属性,然后对它处理就可以了。

for m in model.modules():
    if isinstance(m, nn.Conv2d):
        print(m)
        nn.init.normal(m.weight.data)
        # nn.init.xavier_normal(m.weight.data)
        # nn.init.kaiming_normal(m.weight.data)
        m.bias.data.fill_(0)
    elif isinstance(m, nn.Linear):
        m.weight.data.normal_()

你可能感兴趣的:(pytorch框架,torch.nn.Conv2d,提取层结构,自定义初始化)