在介绍卷积神经网络之前,先提出三个观点,正是这三个观点使得卷积神经网络能够真正起作用。
对于一张图片而言,需要检测图片中的特征来决定图片的类别,通常情况下这些特征都不是由整张图片来决定的,而是由一些局部的区域来决定的。
比如下图4.1中的鸟啄,该特征只存在于图片的局部中。
对于不同的图片,如果它们具有同样的特征,这些特征会出现在图片的不同位置,也就是说可以用同样的检测模式去检测不同图片的相同特征,只不过这些特征处于图片中的不同位置,但是特征检测所做的操作几乎一样。
图4.2中两张图片的鸟啄处于不同的位置,但是可以用相同的检测模式去检测。
对于一张大图片,如果我们进行下采样,那么图片的性质基本保持不变。
图4.3经过下采样还是能够看出来是一张鸟的图片。
前面介绍的全连接神经网络存在着一个问题,比如在MNIST数据集上,图片大小为 28x28,那么第一个隐藏层神经元的权重数目就是 28x28=784,这似乎不是很大,但这只是一张小图片,且是灰度图。对于一张较大的图片而言,比如 200x200x3,就会导致权重数目是 200x200x3=120000,如果设置几个隐藏层中的神经元数目,就会导致参数增加特别快。但是这样的图片其实算不上大图片,所以以全连接神经网络处理图像不是一个好的选择。
根据上面提到的三个图像的特性,由此我们提出卷积神经网络,它是一个3D容量的神经元,也就是说神经元是以三个维度来排列的:宽度、高度和深度。比如输入的图片是 32x32x3。
卷积层是卷积神经网路和核心,一个网络的计算量主要集中在卷积层。
在处理图像时,让每一个神经元都与那个层中的所有神经元进行全连接是不现实。相反,让每个神经元只与输入数据的一个局部区域连接时可行的,为什么可以这麽做呢?其实这是因为图片特征的局部性,所以只需要通过局部就能提取出相应的特征。
与神经元连接的空间大小叫做神经元的感受野,它的大小是一个认为设置的超参数,这其实就是滤波器的宽和高。在深度方向上,大小总是和图片的深度一致。最后强调一下,对待空间维度和深度维度是不同的,连接在空间上是局部的,但是在深度上总是和输入的数据深度保持一致。
在卷积层使用参数共享可以有效地减少参数的个数,这样之所以行得通,是因为之前介绍的特征的相同性,也就是说相同的滤波器能够检测出不同位置的相同特征。
通常会在卷积层之间周期性插入一个池化层,其作用是逐渐降低数据体的空间尺寸,这样就能够减少网络中的参数量,减少计算资源耗费,同时也能够有效地控制过拟合。
池化层之所以有效,是因为之前介绍的图片特征具有平移不变性,也就是通过下采样不会丢失图片拥有的特征,由于这种特性,我们可以将图片缩小再进行卷积处理,这样能够大大降低卷积运算的时间。
一般有最大池化和平均池化两种常用的池化方式,在实际中证明,在卷积层之间引入最大池化的效果是最好的,而平均池化一般放在网络的最后一层。
小滤波器的有效性:一般而言,几个小滤波器卷积层的组合比一个大滤波器卷积层要好,比如层层堆叠了3个3x3的卷积层,中间含有非线性激活层,在这种排列下面,第一个卷积层中每个神经元对输入数据的感受野是 3x3。第二层卷积层对第一层卷积层的感受野也是 3x3,这样对于输入数据的感受野就是 5x5,同样,第三层卷积层上对第二层卷积层的感受野也是 3x3,这样第三层卷积层对于第一层的输入数据的感受野就是 7x7.
这里为什么不直接使用一个 7x7的卷积核,而是选择3个3x3的。首先在于多个卷积层与非线性激活层交替的结构,比单一卷积层的结构更能提取出深层的特征;其次,减少了参数量和计算量。
PyTorch作为一个深度学习库,卷积神经网络是其中一个最为基础的模块,卷积神经网络中所有层结构都可以通过nn这个包调用,下面具体介绍如何调用每种层结构,以及每个函数中的参数。
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。下面来具体解释每个参数的含义
nn.MaxPool2d()表示网络中的最大值池化 ,其中的参数有kernel_size、stride、padding、dilation、return_indices、ceil_mode,下面来具体解释一下它们各自的含义:
搭建一个简单的由卷积层、激活层和池化层组合在一起的层结构,定义了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)
)
)
使用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))
有时候提取出层结构并不够,需要对里面的参数进行初始化,那么如何提取出网络的参数并进行初始化呢?首先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_()