1 二维卷积 1.1 二维互相关 虽然卷积层得名于卷积(convolutional neural network) 运算,但是我们通常在卷积层中使用更加直观的互相关(cross-correlation)运算 。二维卷积层中,一个二维输入数组和一个二维核(kernel)数组进行互相关运算输出一个二维数组。
如下图所示,输入数组形状为(3,3),核在卷积运算中又被称为卷积核 或过滤器(filter) 。输入数组和过滤器对应数字相乘相加 得到输出数组对应答案,然后通过滑动窗口补齐输出数组。
下面实现上述过程:
1 2 3 4 5 6 7 8 9 10 import torchfrom torch import nndef corr2d (X, K ): h, w = K.shape Y = torch.zeros((X.shape[0 ] - h + 1 , X.shape[1 ] - w + 1 )) for i in range (Y.shape[0 ]): for j in range (Y.shape[1 ]): Y[i, j] = (X[i: i + h, j: j + w] * K).sum () return Y
在真实的卷积层中,除了互相关运算之外,往往还需要添加上一个标量偏差 。卷积层的模型参数里包含了卷积核和标量偏差 。在训练模型时,通常我们首先对卷积核随机初始化,然后不断迭代卷积核和偏差。
1 2 3 4 5 6 7 8 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
p * q 卷积或 p * q 卷积核说明卷积核的高和宽分别为 p 和 q 。
1.2 图像边缘检测 我们来看卷积层的一个简单应用:图像边缘检测,即找到像素变化的位置。首先我们构造一张 6 * 8 大小的图像。中间四列为黑(0),两边为白(1)。
1 2 3 X = torch.ones(6 , 8 ) X[:, 2 :6 ] = 0 X
1 2 3 4 5 6 tensor([[1., 1., 0., 0., 0., 0., 1., 1.], [1., 1., 0., 0., 0., 0., 1., 1.], [1., 1., 0., 0., 0., 0., 1., 1.], [1., 1., 0., 0., 0., 0., 1., 1.], [1., 1., 0., 0., 0., 0., 1., 1.], [1., 1., 0., 0., 0., 0., 1., 1.]])
然后构造一个卷积核 K,高和宽分别为 1 和 2,输出为 0 说明横向相邻元素相同 。让其与 X 作互相关运算。可以看出来,从白到黑的边缘和从黑到白的边缘分别检测成了 1 和 -1 。其余输出都是 0。
1 2 3 K = torch.tensor([[1 , -1 ]]) Y = corr2d(X, K) Y
1 2 3 4 5 6 tensor([[ 0., 1., 0., 0., 0., -1., 0.], [ 0., 1., 0., 0., 0., -1., 0.], [ 0., 1., 0., 0., 0., -1., 0.], [ 0., 1., 0., 0., 0., -1., 0.], [ 0., 1., 0., 0., 0., -1., 0.], [ 0., 1., 0., 0., 0., -1., 0.]])
最后来做一个梯度下降求我们构造的卷积核的例子。首先随机初始化一个卷积层,在每一次迭代中,使用平方误差来比较真实值和学习值的输出,然后更新权重。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 conv2d = Conv2D((1 , 2 )) step = 20 lr = 0.01 for i in range (step): Y_hat = conv2d(X) l = ((Y_hat - Y) ** 2 ).sum () l.backward() conv2d.weight.data -= lr * conv2d.weight.grad conv2d.bias.data -= lr * conv2d.bias.grad conv2d.weight.grad.fill_(0 ) conv2d.bias.grad.fill_(0 ) if (i + 1 ) % 5 == 0 : print ("Step %d, loss %.3f" % (i + 1 , l.item()))
1 2 3 4 Step 5 , loss 0.748 Step 10 , loss 0.091 Step 15 , loss 0.012 Step 20 , loss 0.002
1.3 特征图和感受野 二维卷积层输出的二维数组可以看做是输入在空间维度(宽和高)上某一级的表征 ,也叫特征图(features map) 。影响元素 x 的前向计算的所有可能输入区域叫做 x 的感受野(receptive field) 。以下图为例,输出就是整个输入的特征图,输入中阴影部分的四个元素是输出中阴影部分元素的感受野。
2 填充和步幅 根据输入数组的形状和卷积核形状我们是否能得到输出形状?答案是可以的。假设输入形状是 ,卷积核窗口形状为 ,那么输出形状会是: 实际上,除了输入形状和卷积核形状之外,还会有两个因素影响输出形状,那就是接下来要介绍的两个超参数,填充(padding)和步幅(stride) 。
2.1 填充 填充(padding)是指在输入高和宽的两侧填充元素(通常是 0 元素)。如下图我们在原来的高和宽的两侧分别添加了一层 0 元素,使得高和宽从 3 变成了 5,并导致输出高和宽由 2 增加到 4。
一般来说,如果在高的两侧一共填充 行,在宽的两侧一共填充 列,那么输出形状将会是 也就是说,输出的宽和高会分别增加 和 。
很多情况下,我们会设置 和 来使输入和输出具有相同的高和宽。这样会方便在构造网络时推测每个层的输出形状。假设这里 是奇数,那么我们会在高德两侧分别填充 行。一般卷积神经网络都采用奇数高宽的卷积核 。
2.2 步幅 步幅(stride)就是指滑动窗口在输入数组每次滑行的行数和列数。默认为(1, 1),即一次同时滑动一行和一列。
还是使用上述例子,我们将宽步幅调整为 2,高步幅调整为 3。如图所示,卷积窗口在输入上再向右移动两列时无法填满窗口,所以无结果输出,最后输出大小为(2,2)。
一般来说,当高步幅为 ,宽步幅为 时,输出形状为:
3 多通道 上述例子都采用的是二维数组,但是真实数据维度往往更高。例如,彩色图像除了高和宽之外还有 RGB 三个颜色通道,假设彩色图像的高和宽为 h * w,那么我们就可以把它表示成一个 3 * h * w 的多维数组。
3.1 多输入通道 假设卷积核形状为 ,当通道数大于 1 时,我们会为每一个通道分配一个同样的形状为 的卷积核,这样就如同卷积核也有了多通道,让对应卷积核和每个通道的输入进行互相关运算,然后按通道相加,最后得到一个二维数组,这个二维数组就是输出。
接下来我们来实现含多个输入通道的互相关运算。我们只需要对每个通道做互相关运算,然后通过add_n
函数来累加即可。
1 2 3 4 5 6 7 8 9 10 11 12 import torchfrom torch import nnimport syssys.path.append(".." ) import d2lzh_pytorch as d2ldef corr2d_multi_in (X, K ): res = d2l.corr2d(X[0 , :, :], K[0 , :, :]) for i in range (1 , X.shape[0 ]): res += d2l.corr2d(X[i, :, :], K[i, :, :]) return res
1 2 3 4 X = torch.tensor([[[0 , 1 , 2 ], [3 , 4 , 5 ], [6 , 7 , 8 ]], [[1 , 2 , 3 ], [4 , 5 , 6 ], [7 , 8 , 9 ]]]) K = torch.tensor([[[0 , 1 ], [2 , 3 ]], [[1 , 2 ], [3 , 4 ]]]) corr2d_multi_in(X, K)
1 2 tensor([[ 56. , 72. ], [104. , 120. ]])
3.2 多输出通道 假设输入通道数和输出通道数分别为 ,如果我们希望得到含多个通道的输出,我们可以为每个输出通道分别创建形状为 的核数组。将它们在输出通道维度上连接,卷积核形状为 。
我们将 K,K + 1 和 K + 2 构造一个输出通道数为 3 的卷积核 K。
1 2 3 4 5 6 7 def corr2d_multi_in_out (X, K ): return torch.stack([corr2d_multi_in(X, K) for k in K]) K = torch.stack([K, K + 1 , K + 2 ]) K.shape corr2d_multi_in_out(X, K)
输出结果如下,可以看到第一个通道的结果与之前输入数组 X 的结果一致。
1 2 3 4 5 6 7 8 tensor([[[ 56. , 72. ], [104. , 120. ]], [[ 76. , 100. ], [148. , 172. ]], [[ 96. , 128. ], [192. , 224. ]]])
3.3 1 * 1 卷积层 最后讨论卷积窗口形状为 1 * 1 的多通道卷积层,通常称之为 1 * 1 卷积层。因为使用了最小窗口,所以无法识别相邻元素的模式构成。其主要计算发生在通道维度上。如图所示,输出元素来自输入在高和宽上相同位置的元素在不同通道之间的按权重累加 。假设我们将通道维当做特征维,将高和宽维度上的元素当成数据样本,那么 1 * 1 卷积层的作用和全连接层等价 。
下面我们使用全连接层中的矩阵乘法来实现 1 * 1 卷积。这里需要在矩阵乘法运算前后对数据形状做一些调整。
1 2 3 4 5 6 7 def corr2d_multi_in_out_1x1 (X, K ): c_i, h, w = X.shape c_o = K.shape[0 ] X = X.view(c_i, h * w) K = K.view(c_o, c_i) Y = torch.mm(K, X) return Y.view(c_o, h, w)
1 * 1 卷积层通常用来调整网络层之间的通道数,并控制模型复杂度。
4 池化层 池化层的出现是为了缓解卷积层对位置的过度敏感性 。
4.1 二维最大池化层和平均池化层 我们将池化窗口形状为 p * q 的池化层称之为 p * q 池化层。二维最大池化层就是在找出池化窗口在输入窗口的最大值输出 ,如下图所示。
二维平均池化工作原理与最大池化类似,但是将最大运算符替换成为平均运算符。
为什么池化可以降低卷积层对位置的敏感性 ?假设我们将卷积层的输出作为 2 * 2 最大池化的输入。设该卷积层输入为 X,输出为 Y。无论是X[i, j]
和X[i, j + 1]
值不同,还是X[i, j + 1]
还是X[i, j + 2]
不同,池化层输出均有Y[i, j] = 1
。也就是说,使用 2 * 2最大池化层,是要卷积层识别的模式在高和宽上移动不超过一个元素,我们依然可以将它检测出来。
下面来实现池化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import torchfrom torch import nndef pool2d (X, pool_size, mode="max" ): X = X.float () p_h, p_w = pool_size Y = torch.zeros(X.shape[0 ] - p_h + 1 , X.shape[1 ] - p_w + 1 ) for i in range (Y.shape[0 ]): for j in range (Y.shape[1 ]): if mode == "max" : Y[i, j] = X[i : i + p_h, j: j + p_w].max () elif mode == "avg" : Y[i, j] = X[i : i + p_h, j: j + p_w].mean() return Y
1 2 X = torch.tensor([[0 , 1 , 2 ], [3 , 4 , 5 ], [6 , 7 , 8 ]]) pool2d(X, (2 , 2 ))
1 2 tensor([[4. , 5. ], [7. , 8. ]])
再来测试一下平均池化层:
1 pool2d(X, (2 , 2 ), "avg" )
1 2 tensor([[2. , 3. ], [5. , 6. ]])
4.2 填充和步幅 池化层也可以在在输入的高和宽的两侧填充并调整窗口的移动步幅来改变输出形状。池化层填充和步幅与卷积层工作机制一致。我们可以使用 nn 模块里的MaxPool2d
来实现池化层的填充和步幅。首先构造一个形状为(1,1,4,4)的输入,前两个维度飞奔是批量和通道。
1 2 X = torch.arange(16 , dtype=torch.float ).view((1 ,1 ,4 ,4 )) X
1 2 3 4 tensor([[[[ 0. , 1. , 2. , 3. ], [ 4. , 5. , 6. , 7. ], [ 8. , 9. , 10. , 11. ], [12. , 13. , 14. , 15. ]]]])
默认情况下,MaxPool2d
实例里的步幅和池化窗口形状相同,下面使用形状为(3,3)的池化窗口,默认获得形状为(3,3)的步幅。记住当窗口不够时不会输出。
1 2 pool2d = nn.MaxPool2d(3 ) pool2d(X)
也可以制定非正方形池化窗口,分别制定高和宽上的填充和步幅。
1 2 pool2d = nn.MaxPool2d((2 , 4 ), padding=(1 , 2 ), stride=(2 , 3 )) pool2d(X)
1 2 3 tensor([[[[ 1. , 3. ], [ 9. , 11. ], [13. , 15. ]]]])
4.3 多通道 处理多通道输入数据时,池化层对每个输入通道分别池化,而不是像卷积层那样将各个通道的输入按通道相加 。这意味着池化层的输出通道和输入通道数相同。输入通道数为 2。
1 2 X = torch.cat((X, X + 1 ), dim=1 ) X
1 2 3 4 5 6 7 8 9 tensor([[[[ 0. , 1. , 2. , 3. ], [ 4. , 5. , 6. , 7. ], [ 8. , 9. , 10. , 11. ], [12. , 13. , 14. , 15. ]], [[ 1. , 2. , 3. , 4. ], [ 5. , 6. , 7. , 8. ], [ 9. , 10. , 11. , 12. ], [13. , 14. , 15. , 16. ]]]])
可以发现,输出通道数也为 2。
1 2 pool2d = nn.MaxPool2d(3 , padding=1 , stride=2 ) pool2d(X)
1 2 3 4 5 tensor([[[[ 5. , 7. ], [13. , 15. ]], [[ 6. , 8. ], [14. , 16. ]]]])
5 卷积神经网络模型 5.1 LeNet 这是一个早期用来识别手写数字图像的卷积神经网络。LeNet 网络结果如下图所示:
它的结构分为卷积层块和全连接层块。
卷积层块中的基本单位是卷积层后接最大池化层。卷积层采用(5,5)的窗口,并在输出上选择 sigmoid 激活函数。第一个卷积层输出通道数为 6,第二个增加到 16。这是因为第二个卷积层比第一个卷积层输入的高和宽要小,所以增加输出通道使得两个卷积层的参数尺寸类似。最大池化层窗口形状为(2,2),步幅为2。
卷积层块的输出形状为(批量大小,通道,高,宽),卷积层块输出即为全连接层输入。传入时,全连接层块会将小批量中的每个样本变平(flatten)。
下面使用Sequential
来实现 LeNet 模型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import timeimport torchfrom torch import nn, optimimport syssys.path.append(".." ) import d2lzh_pytorch as d2ldevice = torch.device("cuda" if torch.cuda.is_available() else "cpu" ) class LeNet (nn.Module ): def __init__ (self ): super (LeNet, self).__init__() self.conv = nn.Sequential( nn.Conv2d(1 , 6 , 5 ), nn.Sigmoid(), nn.MaxPool2d(2 , 2 ), nn.Conv2d(6 , 16 , 5 ), nn.Sigmoid(), nn.MaxPool2d(2 , 2 ) ) self.fc = nn.Sequential( nn.Linear(16 *4 *4 , 120 ), nn.Sigmoid(), nn.Linear(120 , 84 ), nn.Sigmoid(), nn.Linear(84 , 10 ) ) def forward (self, img ): feature = self.conv(img) output = self.fc(feature.view(img.shape[0 ], -1 )) return output
我们来查看一下网络结构
1 2 net = LeNet() print (net)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 LeNet( (conv): Sequential( (0 ): Conv2d(1 , 6 , kernel_size=(5 , 5 ), stride=(1 , 1 )) (1 ): Sigmoid() (2 ): MaxPool2d(kernel_size=2 , stride=2 , padding=0 , dilation=1 , ceil_mode=False ) (3 ): Conv2d(6 , 16 , kernel_size=(5 , 5 ), stride=(1 , 1 )) (4 ): Sigmoid() (5 ): MaxPool2d(kernel_size=2 , stride=2 , padding=0 , dilation=1 , ceil_mode=False ) ) (fc): Sequential( (0 ): Linear(in_features=256 , out_features=120 , bias=True ) (1 ): Sigmoid() (2 ): Linear(in_features=120 , out_features=84 , bias=True ) (3 ): Sigmoid() (4 ): Linear(in_features=84 , out_features=10 , bias=True ) ) )
5.2 AlexNet 这是一个计算机视觉史上划时代的模型,它首次证明了学习到的特征可以超越手工设计的特征,从而一举打破计算机视觉研究的前状。
AlexNet 与 LeNet 设计理念十分相似,但也有显著的区别。例如,AlexNet 将 sigmoid 激活函数更换成了 ReLU 函数,其计算更简单,而且在不同的参数初始化方法下使模型更容易训练。AlexNet 添加了丢弃法来控制全连接层模型的复杂度,并引入了大量的图像增广,如翻转、裁剪等等手段扩大数据集来缓解过拟合的现象。
来看看其网络结构吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 class AlexNet (nn.Module ): def __init__ (self ): super (AlexNet, self).__init__() self.conv = nn.Sequential( nn.Conv2d(1 , 96 , 11 , 4 ), nn.ReLU(), nn.MaxPool2d(3 , 2 ), nn.Conv2d(96 , 256 , 5 , 1 , 2 ), nn.ReLU(), nn.MaxPool2d(3 , 2 ), nn.Conv2d(256 , 384 , 3 , 1 , 1 ), nn.ReLU(), nn.Conv2d(384 , 384 , 3 , 1 , 1 ), nn.ReLU(), nn.Conv2d(384 , 256 , 3 , 1 , 1 ), nn.ReLU(), nn.MaxPool2d(3 , 2 ) ) self.fc = nn.Sequential( nn.Linear(256 *5 *5 , 4096 ), nn.ReLU(), nn.Dropout(0.5 ), nn.Linear(4096 , 4096 ), nn.ReLU(), nn.Dropout(0.5 ), nn.Linear(4096 , 1000 ) ) def forward (self, img ): feature = self.conv(img) output = self.fc(feature.view(img.shape[0 ], -1 )) return output
1 2 net = AlexNet() print (net)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 AlexNet( (conv): Sequential( (0 ): Conv2d(1 , 96 , kernel_size=(11 , 11 ), stride=(4 , 4 )) (1 ): ReLU() (2 ): MaxPool2d(kernel_size=3 , stride=2 , padding=0 , dilation=1 , ceil_mode=False ) (3 ): Conv2d(96 , 256 , kernel_size=(5 , 5 ), stride=(1 , 1 ), padding=(2 , 2 )) (4 ): ReLU() (5 ): MaxPool2d(kernel_size=3 , stride=2 , padding=0 , dilation=1 , ceil_mode=False ) (6 ): Conv2d(256 , 384 , kernel_size=(3 , 3 ), stride=(1 , 1 ), padding=(1 , 1 )) (7 ): ReLU() (8 ): Conv2d(384 , 384 , kernel_size=(3 , 3 ), stride=(1 , 1 ), padding=(1 , 1 )) (9 ): ReLU() (10 ): Conv2d(384 , 256 , kernel_size=(3 , 3 ), stride=(1 , 1 ), padding=(1 , 1 )) (11 ): ReLU() (12 ): MaxPool2d(kernel_size=3 , stride=2 , padding=0 , dilation=1 , ceil_mode=False ) ) (fc): Sequential( (0 ): Linear(in_features=6400 , out_features=4096 , bias=True ) (1 ): ReLU() (2 ): Dropout(p=0.5 , inplace=False ) (3 ): Linear(in_features=4096 , out_features=4096 , bias=True ) (4 ): ReLU() (5 ): Dropout(p=0.5 , inplace=False ) (6 ): Linear(in_features=4096 , out_features=1000 , bias=True ) ) )
AlexNet 虽然与 LeNet 结构类似,但是使用了更多的卷积层和更大的参数空间来拟合大规模数据集 ImageNet。它是浅层神经网络和深度神经网络的分界线 。
5.3 ResNet 5.3.1 批量归一化 来介绍批量归一化(batch normalization)层,它能让较深的神经网络训练起来更加容易 。批量归一化并不是模型,因为后面的一些模型需要用上此概念,所以先介绍一下。
有时我们会对输入数据做标准化处理:处理后的任意一个特征在数据集中所有样本的均值为 0,标准差为 1。标准化处理输入数据使得各个特征分布相近:这往往更容易训练出有效的模型。
但是标准化处理不太能应付深度神经网络,训练中的模型参数的更新依然很容易让靠近输出层的输出剧烈变化。批量归一化就是为了应对这一挑战而诞生的,在模型训练时,批量归一化利用小批量上的均值和标准差 ,不断调整神经网络的输出,从而使得整个神经网络在各层的中间输出的数值更加稳定。
批量归一化的过程在全连接层和卷积层有所不同。
首先介绍在全连接层的步骤:通常,我们将批量归一化层置于全连接层中的仿射变换和激活函数之间 。设全连接层的输入为 u,权重和偏差分别为 W 和 b,激活函数为 R。设批量归一化的运算符为 BN。那么,使用批量归一化的全连接输出为: 其中 。
考虑一个由 m 个小样本组成的批量,仿射变换的输出得到一个新的小批量 B,这正是批量归一化层的输入。我们首先对这个小批量 B 求均值 和方差 : 再对 x 进行标准化处理,其中 是一个非常小的数,保证分母大于 0。 在上面标准化的基础上,批量归⼀化层引入了两个可以学习的模型参数,拉伸(scale)参数 和偏移(shift)参数 。这两个参数和形状相同。它们与分别做按元素乘法(对应元素点乘)和加法计算即可得到结果: 对卷积层来说,批量归⼀化发⽣在卷积计算之后、应⽤激活函数之前。如果卷积计算输出多个通道,我们需要对这些通道的输出分别做批量归⼀化,且每个通道都拥有独立的拉伸和偏移参数,并均为标量 。
下面来实现批量归一化层
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import timeimport torchfrom torch import nn, optimimport syssys.path.append(".." ) import d2lzh_pytorch as d2ldevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu' ) def batch_norm (is_training, X, gamma, beta, moving_mean, moving_var, eps, momentum ): if not is_training: X_hat = (X - moving_mean) / torch.sqrt(moving_var + eps) else : assert len (X.shape) in (2 , 4 ) if len (X.shape) == 2 : mean = X.mean(dim=0 ) var = ((X - mean) ** 2 ).mean(dim=0 ) else : mean = X.mean(dim=0 , keepdim=True ).mean( dim=2 , keepdim=True ).mean(dim=3 , keepdim=True ) var = ((X - mean) ** 2 ).mean(dim=0 , keepdim=True ).mean(dim=2 , keepdim=True ).mean(dim=3 , keepdim=True ) X_hat = (X - mean) / torch.sqrt(var + eps) moving_mean = momentum * moving_mean + (1.0 - momentum) * mean moving_var = momentum * moving_var + (1.0 - momentum) * var Y = gamma * X_hat + beta return Y, moving_mean, moving_var
接下来,我们⾃定义⼀个 BatchNorm 层。它保存参与求梯度和迭代的拉伸参数 gama 和偏移参 数 beta ,同时也维护移动平均得到的均值和⽅差,以便能够在模型预测时被使⽤。 BatchNorm 实例 所需指定的 num_features 参数对于全连接层来说应为输出个数,对于卷积层来说则为输出通道数。该 实例所需指定的 num_dims 参数对于全连接层和卷积层来说分别为 2 和 4。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class BatchNorm (nn.Module ): def __init__ (self, num_features, num_dims ): super (BatchNorm, self).__init__() if num_dims == 2 : shape = (1 , num_features) else : shape = (1 , num_features, 1 , 1 ) self.gamma = nn.Parameter(torch.ones(shape)) self.beta = nn.Parameter(torch.zeros(shape)) self.moving_mean = torch.zeros(shape) self.moving_var = torch.zeros(shape) def forward (self, X ): if self.moving_mean.device != X.device: self.moving_mean = self.moving_mean.to(X.device) self.moving_var = self.moving_var.to(X.device) Y, self.moving_mean, self.moving_var = batch_norm(self.training, X, self.gamma, self.beta, self.moving_mean, self.moving_var, eps=1e-5 , momentum=0.9 ) return Y
5.3.2 残差块 是否存在一种方法,让我们加深网络层后可以只降低训练误差而不影响其他?
是存在的,我们需要新添加的网络层能够实现恒等映射 ,即 。如何实现呢?让我们聚焦于神经网络局部,设输入为 x,恒等映射为 f(x)。右图虚线框中的部分用以拟合恒等映射的残差映射 , 残差映射往往更容易优化,我们只需要将右图虚线框内上方的加权运算(如仿射)的权重和偏差参数学成 0,那么 f(x) 即为恒等映射。实际上,当我们求得的 f(x) 及其接近恒等映射时,残差映射也易于捕捉恒等映射的细微波动。
右图就是 ResNet 的基础块,也叫残差块。在残差块中,输入可通过跨层的数据线路更快地向前方传播。
残差块中首先有 2 个相同输出通道数的 3 * 3 卷积层,每个卷积层后接一个批量归一化层和 ReLU 激活函数。然后我们将输入跳过这两个卷积运算后直接加在最后的 ReLU 激活函数前。这样的设计要求两个卷积层的输入和输出形状一致,从而可以相加。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import timeimport torchfrom torch import nn, optimimport torch.nn.functional as Fimport syssys.path.append(".." ) import d2lzh_pytorch as d2ldevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu' ) class Residual (nn.Module ): def __init__ (self, in_ch, out_ch, use_lx1conv=False , stride=1 ): super (Residual, self).__init__() self.conv1 = nn.Conv2d(in_ch, out_ch, kernel_size=3 , padding=1 , stride=stride) self.conv2 = nn.Conv2d(out_ch, out_ch, kernel_size=3 , padding=1 ) if use_1x1conv: self.conv3 = nn.Conv2d(in_ch, out_ch, kernel_size=1 , padding=1 , stride=stride) else : self.conv3= None self.bn1 = nn.BatchNorm2d(out_ch) self.bn2 = nn.BatchNorm2d(out_ch) def forward (self, X ): Y = F.relu(self.bn1(self.conv1(X))) Y = self.bn2(self.conv2(Y)) if self.conv3: X = self.conv3(X) return F.relu(Y + X)