卷积神经网络基础
卷积神经网络基础
导入需要的包
import torch
import torch.nn as nn
import torchvision
卷积运算函数
卷积运算包含输入数组X
与核数组K
,输出数组Y
,我们在d2l
库中定义卷积函数如下。
# 本函数已封装在 d2lzh 包中
def corr2d(X,K):
K_h, K_w = K.shape
X_h, X_w = X.shape
Y = torch.zeros((X_h-K_h+1, X_w-K_w+1))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
Y[i,j] = (X[i:i+K_h,j:j+K_w]*K).sum()
return Y
二维卷积层
二维卷积层将输入数组和卷积数组做卷积运算,再加上一个标量偏差来得到输出。这个卷积数组的核中各元素值待定,标量偏差值待定,所以我们认为这是卷积层待训练参数,按照这个思路,我们就可以定义出包含待训练参数(权重参数和偏差参数)的二维卷积层,下面是代码实现。
也不一定有这么严格的定义,标量偏差定义为 0 也是没问题的。
class Conv2D(nn.Module):
def __init__(self, kernel_size):
super(Conv2D, self).__init__()
self.weight = nn.Parameter(torch.randn(kernel_size))
self.bias = nn.Parameter(torch.randn(1))
def forward(self, x):
return corr2d(x, self.weight)+self.bias
卷积层的填充 (padding) 与步幅 (stride)
填充 (padding)
对于填充为 0,步幅为 1 的卷积运算而言,运算输出的 Y 尺寸为:
卷积层的两个超参数包括填充(padding)和步幅(stride),在考虑了填充与步幅之后,输出数组的尺寸就应该重新计算,记p
表示填充扩展格数,记s
表示 stride,则输出数组的尺寸为:
填充指的是在输入高和宽的两侧填充元素(通常是 0 元素),也就是说,如果横向
padding=1
,那么填充过后的横向宽度就应该多 2,因为两侧各填充了 1步幅指的是卷积核的移动长度,一般为 1.
填充参数选择
在很多情况下,我们会设置和来使输入和输出具有相同的高和宽。这样会方便在构造网络时推测每个层的输出形状。假设这里 是奇数,我们会在高的两侧分别填充行。如果 是偶数,一种可能是在输入的顶端一侧填充行,而在底端一侧填充行。在宽的两侧填充同理。
多输入和多输出通道
方便起见,直接参考原文档吧,里面说的很清楚,接下来我会具体写一下nn.Conv2d
的构建,这个在原文档中并没有写的很清楚。
简单来说,多输入就是指输入的图像有多个颜色通道,每个颜色通道匹配一个单层的卷积核,各通道做卷积之后,在通道方向上数据做累加(也就是把数据立方体在通道方向上压平成一层并累加)就是多输入最后的结果,最后的结果只有一个通道。
但是如果要多输出通道,就会麻烦一点,是每个输出通道匹配一个三维的卷积核,这个卷积核和输入的图像在通道方向上做卷积,然后把各个卷积核的结果在通道方向上联结,这样就得到了多输出通道的结果。
stack() 和 cat()
stack()
concatenates a consequence of Tensor along a new dimensions.cat()
concatenates a sequence of tensors along a existing dimension.
“联结”we talked in“多输出”is the stack()
. So we can use Tensor.stack()
to concatenates Tensor sequence and add a new dimension called "channel dimension".
nn.Conv2d
构建基础
首先,请先了解什么是“填充”“步幅”和“多输入/多输出通道”
其次,一般而言我们的Tensor
已经不是传统意义上的矩阵了,而是四阶高维张量。我们设输入层的四层维度为:,设输出层的四层维度为:
N
batch_size
中的第 N 个矩阵c
通道数h
w
矩阵的长宽
而对于多输入,多输出模型,核矩阵也是四个维度,首先第一个维度定义了有几个核矩阵,这个维度和输出层的通道数相匹配(因为你可以理解为一个核矩阵输进去一通计算,算出来一层,作为输出层的其中一个通道)。
第二个维度定义了每个核矩阵的输入通道数,这个维度和输入层的通道数相匹配(因为这个核矩阵要和输入层计算,计算的方法是按照一个核矩阵的每一层和输入层的每一层匹配,每个层都算一遍得到结果再在通道方向上累加)。
第三维度和第四维度可以自定义(因为他是最基础的卷积核,怎么定义都可以)。
再通俗一点理解
四个维度的卷积操作,其实是把最基础的卷积做了扩展。我们先回忆一下最基础的卷积是怎么样的,是一个输入矩阵,和一个核矩阵,做卷积运算,得到一个输出矩阵。这里面输入矩阵和核矩阵的尺寸是没有关联的,但是输出矩阵会和他俩的尺寸有关系,不过我们这边暂且按下不讨论他。
而现在,突然有了多个输入层,也就是有层输入矩阵,那也就要有个核矩阵和输入矩阵相匹配,做次基础的卷积运算,但是这个做出的次计算,需要在通道方向上累加,加为一层输出矩阵。
可是如果我们要个输出矩阵怎么办呢,那就得有个的核矩阵(也就是我们刚才已经拓展出的三维核矩阵)参与计算,每个核矩阵可以不一样,但是输入矩阵是固定不变的,也就是说相同的的输入矩阵要和不同的核矩阵计算次。
但是N
又是哪来的呢,是因为batch_size
定义了不止一个的输入矩阵,所以N
就是batch_size
。
而前面所说的填充
和步幅
,其实是对h_i
和w_i
的输入矩阵进行操作,也就是对输入矩阵的尺寸进行操作。
说完了四个维度的定义,接下来我们看看填充和步幅的影响。
步幅好定义,就是卷积核移动的步长,麻烦的是填充。
我们先定义一个参数叫padding
,他指的是在横向/纵向上,输入矩阵需要各向两边拓宽多少格。比如说左右方向上的padding=1
,那左边往外扩 1 格,右边也往外扩一个,整体往外扩 2 格。我们把这 2 格叫做p
,也就是说,p=2*padding
。而这个p
,也对应上了填充这一节的和。
所以我们说的,,对应的padding
是,,之所以强调这个,是因为在nn.Conv2d
中的padding
参数,是对应的padding
,而不是和。
nn.Conv2d
的构建
class torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1,
padding=0, dilation=1, groups=1, bias=True, padding_mode='zeros', device=None, dtype=None)
nn.Conv2d()
提供了 4 个参数:
in_channels
:输入通道数,即输入矩阵的通道数。out_channels
:输出通道数,即输出矩阵的通道数。kernel_size
:卷积核的尺寸,即卷积核的宽度和高度。stride
:步幅,即卷积核在横向/纵向上的移动步长,可以输入一个turple
来分别定义横向和纵向的步长。padding
:填充,即输入矩阵在横向/纵向上的拓宽格数,可以输入一个turple
来分别定义横向和纵向两边各拓宽的格数,一定要注意的是,他是padding
,而不是。dilation
:膨胀,卷积核内部的膨胀,可以看看这个链接中的Dilated convolution animations
,一般设为 1 比较合适。group
我尽力地看了官方文档了,但是也不知道他在说什么,感觉是像在加快运算效率的?哎呀不管了,就设成 1,遇到了再说。
池化层构建
nn.MaxPool2d
和nn.AvgPool2d
分别定义了最大池化和平均池化,他们的参数和nn.Conv2d
类似,无非是把前两个参数in_channels
和out_channels
改成了kernel_size
,如果单独只给一个数字,如:3
,则代表3x3
的池化层,如果输入turple
,则定义了池化层的横向纵向尺寸。
批量归一化层
官方文档给出了批量归一化层的具体数学推导,可以稍微看一看,了解背后的实现原理,不难。
pytorch
给出了nn.BatchNorm(input_channels)
与nn.BatchNorm2d(input_channels)
,分别用于全连接层和卷积层的批量归一化,只需要调用层就可以,不需要自己定义批量归一化层。值得注意的是,批量归一层和丢弃层类似,在训练模式与测试模式下计算结果是不一样的。