pytorch之卷积神经网络和leNet

5.1 二维卷积层 【pytorch之卷积神经网络和leNet】卷积神经网络(convolutional neural network)是含有卷积层(convolutional layer)的神经网络。本章中介绍的卷积神经网络均使用最常见的二维卷积层。它有高和宽两个空间维度,常用来处理图像数据。本节中,我们将介绍简单形式的二维卷积层的工作原理。
5.1.1 二维互相关运算 虽然卷积层得名于卷积(convolution)运算,但我们通常在卷积层中使用更加直观的互相关(cross-correlation)运算。在二维卷积层中,一个二维输入数组和一个二维核(kernel)数组通过互相关运算输出一个二维数组。 我们用一个具体例子来解释二维互相关运算的含义。
在二维互相关运算中,卷积窗口从输入数组的最左上方开始,按从左往右、从上往下的顺序,依次在输入数组上滑动。当卷积窗口滑动到某一位置时,窗口中的输入子数组与核数组按元素相乘并求和,得到输出数组中相应位置的元素。
下面我们将上述过程实现在corr2d函数里。它接受输入数组X与核数组K,并输出数组Y

import torch from torch import nndef corr2d(X, K):# 本函数已保存在d2lzh_pytorch包中方便以后使用 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

我们可以构造输入数组X、核数组K来验证二维互相关运算的输出。
X = torch.tensor([[0, 1, 2], [3, 4, 5], [6, 7, 8]]) K = torch.tensor([[0, 1], [2, 3]]) corr2d(X, K)

输出:
tensor([[19., 25.], [37., 43.]])

5.1.2 二维卷积层 二维卷积层将输入和卷积核做互相关运算,并加上一个标量偏差来得到输出。卷积层的模型参数包括了卷积核和标量偏差。在训练模型的时候,通常我们先对卷积核随机初始化,然后不断迭代卷积核和偏差。
下面基于corr2d函数来实现一个自定义的二维卷积层。在构造函数__init__里我们声明weightbias这两个模型参数。前向计算函数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

下面我们来看一个卷积层的简单应用:检测图像中物体的边缘,即找到像素变化的位置。
X = torch.ones(6, 8) X[:, 2:6] = 0 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]])

下面将输入X和我们设计的卷积核K做互相关运算。可以看出,我们将从白到黑的边缘和从黑到白的边缘分别检测成了1和-1。其余部分的输出全是0。
Y = corr2d(X, K) 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.]])

由此,我们可以看出,卷积层可通过重复使用卷积核有效地表征局部空间。
5.1.4 通过数据学习核数组 最后我们来看一个例子,它使用物体边缘检测中的输入数据X和输出数据Y来学习我们构造的核数组K。我们首先构造一个卷积层,其卷积核将被初始化成随机数组。接下来在每一次迭代中,我们使用平方误差来比较Y和卷积层的输出,然后计算梯度来更新权重。
# 构造一个核数组形状是(1, 2)的二维卷积层 conv2d = Conv2D(kernel_size=(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# 梯度清0 conv2d.weight.grad.fill_(0) conv2d.bias.grad.fill_(0) if (i + 1) % 5 == 0: print('Step %d, loss %.3f' % (i + 1, l.item()))

输出:
Step 5, loss 1.844 Step 10, loss 0.206 Step 15, loss 0.023 Step 20, loss 0.003

可以看到,20次迭代后误差已经降到了一个比较小的值。现在来看一下学习到的卷积核的参数。
print("weight: ", conv2d.weight.data) print("bias: ", conv2d.bias.data)

输出:
weight:tensor([[ 0.9948, -1.0092]]) bias:tensor([0.0080])

可以看到,学到的卷积核的权重参数与我们之前定义的核数组K较接近,而偏置参数接近0。
5.1.5 互相关运算和卷积运算 实际上,卷积运算与互相关运算类似。为了得到卷积运算的输出,我们只需将核数组左右翻转并上下翻转,再与输入数组做互相关运算。可见,卷积运算和互相关运算虽然类似,但如果它们使用相同的核数组,对于同一个输入,输出往往并不相同。
那么,你也许会好奇卷积层为何能使用互相关运算替代卷积运算。其实,在深度学习中核数组都是学出来的:卷积层无论使用互相关运算或卷积运算都不影响模型预测时的输出。为了解释这一点,假设卷积层使用互相关运算学出图5.1中的核数组。设其他条件不变,使用卷积运算学出的核数组即图5.1中的核数组按上下、左右翻转。也就是说,图5.1中的输入与学出的已翻转的核数组再做卷积运算时,依然得到图5.1中的输出。为了与大多数深度学习文献一致,如无特别说明,本书中提到的卷积运算均指互相关运算。
注:感觉深度学习中的卷积运算实际上是互相关运算是个面试题考点。
5.1.6 特征图和感受野 二维卷积层输出的二维数组可以看作是输入在空间维度(宽和高)上某一级的表征,也叫特征图(feature map)。
我们常使用“元素”一词来描述数组或矩阵中的成员。在神经网络的术语中,这些元素也可称为“单元”。当含义明确时,本书不对这两个术语做严格区分。
小结
  • 二维卷积层的核心计算是二维互相关运算。在最简单的形式下,它对二维输入数据和卷积核做互相关运算然后加上偏差。
  • 我们可以设计卷积核来检测图像中的边缘。
  • 我们可以通过数据来学习卷积核。
5.5.1 LeNet模型 LeNet分为卷积层块和全连接层块两个部分。下面我们分别介绍这两个模块。
卷积层块里的基本单位是卷积层后接最大池化层:卷积层用来识别图像里的空间模式,如线条和物体局部,之后的最大池化层则用来降低卷积层对位置的敏感性。卷积层块由两个这样的基本单位重复堆叠构成。在卷积层块中,每个卷积层都使用5×5的窗口,并在输出上使用sigmoid激活函数。第一个卷积层输出通道数为6,第二个卷积层输出通道数则增加到16。这是因为第二个卷积层比第一个卷积层的输入的高和宽要小,所以增加输出通道使两个卷积层的参数尺寸类似。卷积层块的两个最大池化层的窗口形状均为<2×2,且步幅为2。由于池化窗口与步幅形状相同,池化窗口在输入上每次滑动所覆盖的区域互不重叠。
卷积层块的输出形状为(批量大小, 通道, 高, 宽)。当卷积层块的输出传入全连接层块时,全连接层块会将小批量中每个样本变平(flatten)。也就是说,全连接层的输入形状将变成二维,其中第一维是小批量中的样本,第二维是每个样本变平后的向量表示,且向量长度为通道、高和宽的乘积。全连接层块含3个全连接层。它们的输出个数分别是120、84和10,其中10为输出的类别个数。
下面我们通过Sequential类来实现LeNet模型。
import time import torch from torch import nn, optimimport sys sys.path.append("..") import d2lzh_pytorch as d2l device = 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), # in_channels, out_channels, kernel_size nn.Sigmoid(), nn.MaxPool2d(2, 2), # kernel_size, stride 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

接下来查看每个层的形状。
net = LeNet() print(net)

输出:
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的卷积核,从而将高和宽分别减小4,而池化层则将高和宽减半,但通道数则从1增加到16。全连接层则逐层减少输出个数,直到变成图像的类别数10。
5.5.2 获取数据和训练模型 下面我们来实验LeNet模型。实验中,我们仍然使用Fashion-MNIST作为训练数据集。
batch_size = 256 train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)

因为卷积神经网络计算比多层感知机要复杂,建议使用GPU来加速计算。因此,我们对3.6节(softmax回归的从零开始实现)中描述的evaluate_accuracy函数略作修改,使其支持GPU计算。
# 本函数已保存在d2lzh_pytorch包中方便以后使用。该函数将被逐步改进。 def evaluate_accuracy(data_iter, net, device=None): if device is None and isinstance(net, torch.nn.Module): # 如果没指定device就使用net的device device = list(net.parameters())[0].device acc_sum, n = 0.0, 0 with torch.no_grad(): for X, y in data_iter: if isinstance(net, torch.nn.Module): net.eval() # 评估模式, 这会关闭dropout acc_sum += (net(X.to(device)).argmax(dim=1) == y.to(device)).float().sum().cpu().item() net.train() # 改回训练模式 else: # 自定义的模型, 3.13节之后不会用到, 不考虑GPU if('is_training' in net.__code__.co_varnames): # 如果有is_training这个参数 # 将is_training设置成False acc_sum += (net(X, is_training=False).argmax(dim=1) == y).float().sum().item() else: acc_sum += (net(X).argmax(dim=1) == y).float().sum().item() n += y.shape[0] return acc_sum / n

我们同样对3.6节中定义的train_ch3函数略作修改,确保计算使用的数据和模型同在内存或显存上。
# 本函数已保存在d2lzh_pytorch包中方便以后使用 def train_ch5(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs): net = net.to(device) print("training on ", device) loss = torch.nn.CrossEntropyLoss() for epoch in range(num_epochs): train_l_sum, train_acc_sum, n, batch_count, start = 0.0, 0.0, 0, 0, time.time() for X, y in train_iter: X = X.to(device) y = y.to(device) y_hat = net(X) l = loss(y_hat, y) optimizer.zero_grad() l.backward() optimizer.step() train_l_sum += l.cpu().item() train_acc_sum += (y_hat.argmax(dim=1) == y).sum().cpu().item() n += y.shape[0] batch_count += 1 test_acc = evaluate_accuracy(test_iter, net) print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f, time %.1f sec' % (epoch + 1, train_l_sum / batch_count, train_acc_sum / n, test_acc, time.time() - start))

学习率采用0.001,训练算法使用Adam算法,损失函数使用交叉熵损失函数。
lr, num_epochs = 0.001, 5 optimizer = torch.optim.Adam(net.parameters(), lr=lr) train_ch5(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs)

输出:
training oncuda epoch 1, loss 0.0072, train acc 0.322, test acc 0.584, time 3.7 sec epoch 2, loss 0.0037, train acc 0.649, test acc 0.699, time 1.8 sec epoch 3, loss 0.0030, train acc 0.718, test acc 0.724, time 1.7 sec epoch 4, loss 0.0027, train acc 0.741, test acc 0.746, time 1.6 sec epoch 5, loss 0.0024, train acc 0.759, test acc 0.759, time 1.7 sec

注: 本节代码在GPU和CPU上都已测试过。
小结
  • 卷积神经网络就是含卷积层的网络。
  • LeNet交替使用卷积层和最大池化层后接全连接层来进行图像分类。

    推荐阅读