一、卷积神经网络
1、 卷积神经网络介绍
卷积神经网络(CNN)是一类特殊的神经网络,它可以包含多个卷积层。
2、图像卷积
1. 互相关运算
在二维互相关运算中,卷积窗口从输入张量的左上角开始,从左到右、从上到下滑动。当卷积窗口滑动到新一个位置时,包含在该窗口中的部分张量与卷积核张量进行按元素相乘,得到的张量再求和得到一个单一的标量值,由此我们得出了这一位置的输出张量值。
阴影部分是第一个输出元素,以及用于计算输出的输入张量元素和核张量元素:
0 * 0 + 1 * 1 + 3 * 2 + 4 * 3 = 19
假设输入形状为,卷积核形状为
二维张量输出大小计算公式为:
二维相关运算代码实现:
import torch
from torch import nn
from d2l import torch as d2l
def 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
# 输入X
X = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
# 卷积核
K = torch.tensor([[0.0, 1.0], [2.0, 3.0]])
corr2d(X, K)
输出:
tensor([[19., 25.],
[37., 43.]])
2. 卷积层
卷积层对输入和卷积核权重进行互相关运算,并在添加标量偏置之后产生输出。 所以,卷积层中的两个被训练的参数是卷积核权重和标量偏置。
class Conv2D(nn.Module):
def __init__(self, kernel_size):
super().__init__()
self.weight = nn.Parameter(torch.rand(kernel_size))
self.bias = nn.Parameter(torch.zeros(1))
def forward(self, x):
# corr2d自定义函数
return corr2d(x, self.weight) + self.bias
3. 卷积核迭代
在每次迭代中,我们比较Y
与卷积层输出的平方误差,然后计算梯度来更新卷积核。
X = torch.ones((6, 8))
X[:, 2:6] = 0
# 互相关运算
K = torch.tensor([[1.0, -1.0]])
Y = corr2d(X, K)
print(Y)
X = X.reshape((1, 1, 6, 8))
Y = Y.reshape((1, 1, 6, 7))
lr = 3e-2 # 学习率
# 构造一个二维卷积层,它具有1个输出通道和形状为(1,2)的卷积核
conv2d = nn.Conv2d(1,1, kernel_size=(1, 2), bias=False)
for i in range(10):
Y_hat = conv2d(X)
l = (Y_hat - Y) ** 2 # 损失函数
conv2d.zero_grad() # 梯度清零
l.sum().backward() # 反向传播
conv2d.weight.data[:] -= lr * conv2d.weight.grad # 更新参数
if (i + 1) % 2 == 0:
print(f"epoch {i+1}, loss {l.sum():.3f}")
4. 小结
1. 二维卷积层的核心计算是二维互相关运算。
3、填充与步幅
1. 填充
在应用多层卷积时,我们常常丢失边缘像素。 随着我们应用许多连续卷积层,累积丢失的像素数就多了。 解决这个问题的简单方法即为 填充(padding):在输入图像的边界填充元素(通常填充元素是0)。
带填充的二维相关运算
阴影部分计算过程: 0*0 + 0*1 + 0*2 + 0*3 = 0
输出张量形状计算公式:
在如下示例中,我们使用高度为5,宽度为3的卷积核,高度和宽度两边的填充分别为2和1。
X= torch.rand(1, 1, 8, 8)
conv2d = nn.Conv2d(1, 1, kernel_size=(5, 3), padding=(2, 1))
# 输出形状
conv2d(X).shape
输出:
torch.Size([1, 1, 8, 8])
2. 步幅
将每次滑动元素的数量称为步幅(stride)。下图是垂直步幅为3,水平步幅为2的二维互相关运算。
蓝色部分是输出元素过程: 0*0 + 0*1 + 1*2 + 2*3 = 8, 0*0 + 6*1 + 0*2 + 0*3 = 6
当垂直步幅为、水平步幅为
时,输出形状为:
高度和宽度的步幅设置为2,从而将输入的高度和宽度减半
X = torch.rand(8, 8)
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2)
# 输出形状
conv2d(X).shape
输出:
torch.Size([1, 1, 4, 4])
3. 小结
1. 填充可以增加输出的高度和宽度。这常用来使输出与输入具有相同的高和宽。
2. 步幅可以减小输出的高和宽
3. 填充和步幅可用于有效地调整数据的维度
4. 多输入多输出通道
1. 多输入通道
当输入包含多个通道时,需要构造一个与输入数据具有相同输入通道数的卷积核,以便与输入数据进行互相关运算。 假设输入的通道数为m,那么卷积核的输入通道数也需要为m。
下图,展示了多通道的互相关运算。阴影部分是第一个输出元素以及用于计算这个输出的输入和核张量元素:(1*1+2*2+4*3+5*4)+(0*0+1*1*3*2+4*3)=56。
构造与上图值相对应的输入张量X
和核张量K
,以验证互相关运算的输出
# 输入X
X = torch.tensor([[[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]],
[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]])
# 卷积核
K = torch.tensor([[[0.0, 1.0], [2.0, 3.0]], [[1.0, 2.0], [3.0, 4.0]]])
def corr2d_multi_in(X, K):
# 先遍历“X”和“K”的第0个维度(通道维度),再把它们加在一起
return sum(corr2d(x, k) for x, k in zip(X, K))
corr2d_multi_in(X, K)
输出:
tensor([[ 56., 72.],
[104., 120.]])
2. 多输出通道
用 和
分别表示输入和输出通道的数目,并让
和
为卷积核的高度和宽度。我们可以为每个输出通道创建一个形状为
的卷积核张量, 卷积核形状为
。
输入X:
卷积核W:
输出Y:
如下代码,我们实现一个计算多个通道的输出的互相关函数。
def corr2d_multi_in_out(X, K):
# 迭代“K”的第0个维度,每次都对输入“X”执行互相关运算。
# 最后将所有结果都叠加在一起
return torch.stack([corr2d_multi_in(X, k) for k in K], 0)
# 卷积核3*2*2*2
K = torch.stack((K, K + 1, K + 2), 0)
# 互相关运算
corr2d_multi_in_out(X, K)
输出:
tensor([[[ 56., 72.],
[104., 120.]],
[[ 76., 100.],
[148., 172.]],
[[ 96., 128.],
[192., 224.]]])
5. 1*1卷积核
1 * 1卷积, 即 。 其作用可以用来改变输出的通道数,降低模型复杂度。
互相关计算使用了具有3个输入通道和2个输出通道的 1×1 卷积核。其中,输入和输出具有相同的高度和宽度。
6. 小结
1. 多输入多输出通道可以用来扩展卷积层的模型。
2. 1×1卷积层通常用于调整网络层的通道数量和控制模型复杂性。
4、池化层
1. 池化层介绍
与卷积层类似,池化层运算符由一个固定形状的窗口组成,该窗口根据其步幅大小在输入的所有区域上滑动,为固定形状窗口遍历的每个位置计算一个输出。我们通常计算窗口中所有元素的最大值或平均值。最大池化层(maximum pooling)和平均汇聚层(average pooling)。
窗口形状为 2×2 的最大池化层。着色部分是第一个输出元素,以及用于计算这个输出的输入元素: max(0,1,3,4)=4
如下代码实现了池化层的前向传播。
import torch
from torch import nn
from d2l import torch as d2l
def pool2d(X, pool_size, mode='max'):
# 池化窗口大小
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
输入张量X
,验证二维最大汇聚层的输出。
X = torch.tensor([[0.0, 4.0, 2.0], [8.0, 4.0, 5.0], [3.0, 7.0, 8.0]])
pool2d(X, (2, 2))
输出:
tensor([[8., 5.],
[8., 8.]])
验证平均池化层。
pool2d(X, (2, 2), 'avg')
输出:
tensor([[4.0000, 3.7500],
[5.5000, 6.0000]])
2. 填充与步幅
与卷积层一样,汇聚层也可以改变输出形状。我们可以通过填充和步幅以获得所需的输出形状。
当垂直步幅为、水平步幅为
时, 填充为
,输出形状为:
构造了一个输入张量X
,它有四个维度,其中样本数和通道数都是1。
X = torch.arange(16, dtype=torch.float32).reshape((1, 1, 4, 4))
默认情况下,深度学习框架中的步幅与汇聚窗口的大小相同。 因此,如果我们使用形状为(3,3)的窗口,那么默认情况下,我们得到的步幅形状为(3, 3)
pool2d = nn.MaxPool2d(3)
pool2d(X)
输出:
tensor([[[[10.]]]])
也可以手动设定填充和步幅。
pool2d = nn.MaxPool2d(3, padding=1, stride=2)
pool2d(X)
输出:
tensor([[[[ 5., 7.],
[13., 15.]]]])
3. 多通道
池化层的输出通道数与输入通道数相同。池化层只改变高宽,不改变通道数。
X = torch.arange(0, 32).reshape(1, 2, 4, 4).float()
pool2d = nn.MaxPool2d(3, padding=1, stride=2)
pool2d(X)
输出 :
tensor([[[[ 5., 7.],
[13., 15.]],
[[21., 23.],
[29., 31.]]]])
4. 小结
1. 对于给定输入元素,最大池化层会输出该窗口内的最大值,平均池化层会输出该窗口内的平均值。
2. 可以指定池化层的填充和步幅。
3. 使用最大池化层以及大于1的步幅,可减少空间维度(如高度和宽度)
4. 池化层的输出通道数与输入通道数相同。
5、 卷积神经网络LeNet-5
1. LeNet-5组成部分
两个卷积层 + 三个全连接层
Sequential
块并将需要的层连接在一起
net = nn.Sequential(
nn.Conv2d(1, 6, kernel_size=5, padding=2), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2),
nn.Conv2d(6, 16, kernel_size=5), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2),
nn.Flatten(),
nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(),
nn.Linear(120, 84), nn.Sigmoid(),
nn.Linear(84, 10))
训练和评估LeNet-5模型
# 获取数据
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)
lr, num_epochs = 0.9, 10
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
输出:
2. 小结
1. 卷积神经网络(CNN)是一类使用卷积层的网络。
2. 在卷积神经网络中,我们组合使用卷积层、非线性激活函数和汇聚层。