深度学习实战(基于pytroch)系列(二十)二维卷积层
PyTorch二维卷积层
- 二维互相关运算
- 二维卷积层
- 图像中物体边缘检测
- 通过数据学习核数组
- 互相关运算和卷积运算
- 特征图和感受野
- 使用PyTorch内置卷积层
卷积神经网络(convolutional neural network)是含有卷积层(convolutional layer)的神经网络。本章中介绍的卷积神经网络均使用最常见的二维卷积层。它有高和宽两个空间维度,常用来处理图像数据。本节中,我们将介绍简单形式的二维卷积层的工作原理。
二维互相关运算
虽然卷积层得名于卷积(convolution)运算,但我们通常在卷积层中使用更加直观的互相关(cross-correlation)运算。在二维卷积层中,一个二维输入数组和一个二维核(kernel)数组通过互相关运算输出一个二维数组。
我们用一个具体例子来解释二维互相关运算的含义。如下图所示,输入是一个高和宽均为3的二维数组。我们将该数组的形状记为 3×3 或(3,3)。核数组的高和宽分别为2。该数组在卷积计算中又称卷积核或过滤器(filter)。卷积核窗口(又称卷积窗口)的形状取决于卷积核的高和宽,即 2×2。下图中的阴影部分为第一个输出元素及其计算所使用的输入和核数组元素:0×0+1×1+3×2+4×3=19。
在二维互相关运算中,卷积窗口从输入数组的最左上方开始,按从左往右、从上往下的顺序,依次在输入数组上滑动。当卷积窗口滑动到某一位置时,窗口中的输入子数组与核数组按元素相乘并求和,得到输出数组中相应位置的元素。上图中的输出数组高和宽分别为2,其中的4个元素由二维互相关运算得出:0×0+1×1+3×2+4×3=19, 1×0+2×1+4×2+5×3=25, 3×0+4×1+6×2+7×3=37,4×0+5×1+7×2+8×3=43.
下面我们将上述过程实现在corr2d函数里。它接受输入数组X与核数组K,并输出数组Y。
import torch
import torch.nn as nndef corr2d(X, K):"""二维互相关运算"""h, w = K.shapeY = 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
我们可以构造上图中的输入数组X、核数组K来验证二维互相关运算的输出。
X = torch.tensor([[0, 1, 2], [3, 4, 5], [6, 7, 8]], dtype=torch.float32)
K = torch.tensor([[0, 1], [2, 3]], dtype=torch.float32)
print("互相关运算结果:")
print(corr2d(X, K))
输出:
互相关运算结果:
tensor([[19., 25.],
[37., 43.]])
二维卷积层
二维卷积层将输入和卷积核做互相关运算,并加上一个标量偏差来得到输出。卷积层的模型参数包括了卷积核和标量偏差。在训练模型的时候,通常我们先对卷积核随机初始化,然后不断迭代卷积核和偏差。
下面基于corr2d函数来实现一个自定义的二维卷积层。在构造函数__init__里我们声明weight和bias这两个模型参数。前向计算函数forward则是直接调用corr2d函数再加上偏差。
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 卷积或 p×q 卷积核说明卷积核的高和宽分别为 p 和 q。
图像中物体边缘检测
下面我们来看一个卷积层的简单应用:检测图像中物体的边缘,即找到像素变化的位置。首先我们构造一张 6×8 的图像(即高和宽分别为6像素和8像素的图像)。它中间4列为黑(0),其余为白(1)。
X = torch.ones((6, 8), dtype=torch.float32)
X[:, 2:6] = 0
print("输入图像:")
print(X)
输出:
输入图像:
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.]])
然后我们构造一个高和宽分别为1和2的卷积核K。当它与输入做互相关运算时,如果横向相邻元素相同,输出为0;否则输出为非0。
K = torch.tensor([[1, -1]], dtype=torch.float32)
下面将输入X和我们设计的卷积核K做互相关运算。可以看出,我们将从白到黑的边缘和从黑到白的边缘分别检测成了1和-1。其余部分的输出全是0。
Y = corr2d(X, K)
print("边缘检测结果:")
print(Y)
输出:
边缘检测结果:
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.]])
由此,我们可以看出,卷积层可通过重复使用卷积核有效地表征局部空间。
通过数据学习核数组
最后我们来看一个例子,它使用物体边缘检测中的输入数据X和输出数据Y来学习我们构造的核数组K。我们首先构造一个卷积层,将其卷积核初始化成随机数组。接下来在每一次迭代中,我们使用平方误差来比较Y和卷积层的输出,然后计算梯度来更新权重。
虽然我们之前构造了Conv2D类,但由于corr2d使用了对单个元素赋值的操作因而无法自动求梯度。下面我们使用PyTorch提供的nn.Conv2d类来实现这个例子。
# 构造一个输出通道数为1(将在"多输入通道和多输出通道"一节介绍通道),核数组形状是(1, 2)的二维卷积层
conv2d = nn.Conv2d(1, 1, kernel_size=(1, 2), bias=False)# 二维卷积层使用4维输入输出,格式为(样本, 通道, 高, 宽),这里批量大小(批量中的样本数)和通道数均为1
X = X.reshape((1, 1, 6, 8))
Y = Y.reshape((1, 1, 6, 7))# 使用均方误差损失
criterion = nn.MSELoss()
optimizer = torch.optim.SGD(conv2d.parameters(), lr=0.03)print("开始训练卷积核...")
for i in range(300):# 前向传播Y_hat = conv2d(X)l = criterion(Y_hat, Y)# 反向传播optimizer.zero_grad()l.backward()optimizer.step()if (i + 1) % 20 == 0:print(f'batch {i + 1}, loss {l.item():.3f}')
输出:
开始训练卷积核…
batch 20, loss 0.443
batch 40, loss 0.306
batch 60, loss 0.215
batch 80, loss 0.152
batch 100, loss 0.108
batch 120, loss 0.076
batch 140, loss 0.054
batch 160, loss 0.038
batch 180, loss 0.027
batch 200, loss 0.019
batch 220, loss 0.014
batch 240, loss 0.010
batch 260, loss 0.007
batch 280, loss 0.005
batch 300, loss 0.003
可以看到,10次迭代后误差已经降到了一个比较小的值。现在来看一下学习到的核数组。
print("学习到的卷积核:")
print(conv2d.weight.data.reshape(1, 2))
输出:
学习到的卷积核:
tensor([[ 0.8911, -0.8911]])
可以看到,学到的核数组与我们之前定义的核数组K较接近。条件允许下多跑epoch可以更接近tensor([[1, -1]]
互相关运算和卷积运算
实际上,卷积运算与互相关运算类似。为了得到卷积运算的输出,我们只需将核数组左右翻转并上下翻转,再与输入数组做互相关运算。可见,卷积运算和互相关运算虽然类似,但如果它们使用相同的核数组,对于同一个输入,输出往往并不相同。
那么,你也许会好奇卷积层为何能使用互相关运算替代卷积运算。其实,在深度学习中核数组都是学出来的:卷积层无论使用互相关运算或卷积运算都不影响模型预测时的输出。为了解释这一点,假设卷积层使用互相关运算学出图中的核数组。设其他条件不变,使用卷积运算学出的核数组即图中的核数组按上下、左右翻转。也就是说,图中的输入与学出的已翻转的核数组再做卷积运算时,依然得到图中的输出。
特征图和感受野
二维卷积层输出的二维数组可以看作是输入在空间维度(宽和高)上某一级的表征,也叫特征图(feature map)。影响元素 x 的前向计算的所有可能输入区域(可能大于输入的实际尺寸)叫做 x 的感受野(receptive field)。以图为例,输入中阴影部分的四个元素是输出中阴影部分元素的感受野。我们将图中形状为 2×2 的输出记为 Y,并考虑一个更深的卷积神经网络:将 Y 与另一个形状为 2×2 的核数组做互相关运算,输出单个元素 z。那么,z 在 Y 上的感受野包括 Y 的全部四个元素,在输入上的感受野包括其中全部9个元素。可见,我们可以通过更深的卷积神经网络使特征图中单个元素的感受野变得更加广阔,从而捕捉输入上更大尺寸的特征。
使用PyTorch内置卷积层
在实际应用中,我们通常使用PyTorch内置的卷积层:
# 使用PyTorch内置的Conv2d
builtin_conv2d = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=(1, 2), bias=False)# 测试内置卷积层
output = builtin_conv2d(X)
print("内置卷积层输出形状:", output.shape)
print("内置卷积层权重形状:", builtin_conv2d.weight.shape)
输出:
内置卷积层输出形状: torch.Size([1, 1, 6, 7])
内置卷积层权重形状: torch.Size([1, 1, 1, 2])
