卷积神经网络-动手学深度学习

动手学深度学习v2

课程链接:https://courses.d2l.ai/zh-v2/

从全连接层到卷积

不变性

  • 平移不变性:和检测对象的位置无关
  • 局部性:只考虑检测对象的局部区域特征

重新考察全连接层

  • 输入和输出变形为矩阵
  • 权重变为4维张量
  • v:w的重新索引:k = i + a, l = j + b

平移不变性 => v 不依赖于 i 和 j

局部性 => 不必使用远离$ x_{i, j} $的参数

卷积层

互相关运算

互相关运算:cross-correlation

  • 交叉相关和卷积的区别

由上可实现:

1
2
3
4
5
6
7
8
def corr2d(X, K):  #@save
    """计算二维互相关运算"""
    h, w = K.shape
    Y = torch.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1))
    for i in range(Y.shape[0]): # 遍历并取出和K相同大小的块
        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
def corr2d(X, K):  #@save
    """计算二维互相关运算"""
    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

目标边缘检测

简单应用:检测图像中不同颜色的边缘

现有如下黑白图像(0黑1白):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
K = torch.tensor([[1.0, -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.]])
# 1表示白变黑,-1表示黑变白

# 将输入转置会发现检测不到
corr2d(X.t(), K)
# tensor([[0., 0., 0., 0., 0.],
#         [0., 0., 0., 0., 0.],
#         [0., 0., 0., 0., 0.],
#         [0., 0., 0., 0., 0.],
#         [0., 0., 0., 0., 0.],
#         [0., 0., 0., 0., 0.],
#         [0., 0., 0., 0., 0.],
#         [0., 0., 0., 0., 0.]])
# 证明了K只能检测到竖直边缘

学习卷积核

 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
# 构造一个二维卷积层,它具有1个输出通道和形状为(1,2)的卷积核
conv2d = nn.Conv2d(1, 1, kernel_size=(1, 2), bias=False)

# 这个二维卷积层使用四维输入和输出格式(批量大小、通道、高度、宽度),
# 其中批量大小和通道数都为1
X = X.reshape((1, 1, 6, 8))
Y = Y.reshape((1, 1, 6, 7))
lr = 3e-2  # 学习率

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}')
# epoch 2, loss 6.422
# epoch 4, loss 1.225
# epoch 6, loss 0.266
# epoch 8, loss 0.070
# epoch 10, loss 0.022

conv2d.weight.data.reshape((1, 2))
# tensor([[ 1.0010, -0.9739]])

填充和步幅

填充

在输入图像的周围添加额外的行/列

  • 填充$ p_h $行和$ p_w $列,填充后输出的形状为

    (很少使用偶数核)

这样取值使得输出形状变为$ n_h * n_w $

步幅

每次滑动窗口时移动对应步幅数

代码实现

 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
import torch
from torch import nn


# 为了方便起见,我们定义了一个计算卷积层的函数。
# 此函数初始化卷积层权重,并对输入和输出提高和缩减相应的维数
def comp_conv2d(conv2d, X):
    # 这里的(1,1)表示批量大小和通道数都是1
    X = X.reshape((1, 1) + X.shape)
    Y = conv2d(X)
    # 省略前两个维度:批量大小和通道
    return Y.reshape(Y.shape[2:])

# 请注意,这里每边都填充了1行或1列,因此总共添加了2行或2列
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1)
X = torch.rand(size=(8, 8))
comp_conv2d(conv2d, X).shape
# torch.Size([8, 8])

# 步幅设置为2
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2)
comp_conv2d(conv2d, X).shape
# torch.Size([4, 4])

conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4))
comp_conv2d(conv2d, X).shape
# torch.Size([2, 2])

多输入多输出通道

彩色图片 = RGB三通道

多输入通道

每个通道均执行互相关操作,然后将结果相加

多输出通道

多个三位卷积核,每个核生成自己的输出通道,每个输出通道可以识别特定模式

1x1卷积层

$ k_h = k_w = 1 $

不识别空间模式,只把通道进行加权融合

代码实现

 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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import torch
from d2l import torch as d2l

# 定义-多输入
def corr2d_multi_in(X, K): # X和K均为3D
    # 先遍历“X”和“K”的第0个维度(通道维度),再把它们加在一起
    # zip()对最外维度,即输入通道维度做遍历
    return sum(d2l.corr2d(x, k) for x, k in zip(X, K))

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]]])

corr2d_multi_in(X, K)
# tensor([[ 56.,  72.],
#         [104., 120.]])

# 定义-多输出
def corr2d_multi_in_out(X, K):
    # 迭代“K”的第0个维度,每次都对输入“X”执行互相关运算。
    # 最后将所有结果在0维度上叠加在一起
    return torch.stack([corr2d_multi_in(X, k) for k in K], 0)

K = torch.stack((K, K + 1, K + 2), 0) # 在0维度上叠加
K.shape
# torch.Size([3, 2, 2, 2])

corr2d_multi_in_out(X, K)
# tensor([[[ 56.,  72.],
#          [104., 120.]],

#         [[ 76., 100.],
#          [148., 172.]],

#         [[ 96., 128.],
#          [192., 224.]]])

# 1x1卷积
def corr2d_multi_in_out_1x1(X, K):
    c_i, h, w = X.shape
    c_o = K.shape[0]
    X = X.reshape((c_i, h * w))
    K = K.reshape((c_o, c_i))
    # 全连接层中的矩阵乘法
    Y = torch.matmul(K, X)
    return Y.reshape((c_o, h, w))

X = torch.normal(0, 1, (3, 3, 3)) # X: 3*3*3大小
K = torch.normal(0, 1, (2, 3, 1, 1)) # K: 2输入 3输出 1*1大小

Y1 = corr2d_multi_in_out_1x1(X, K)
Y2 = corr2d_multi_in_out(X, K)
assert float(torch.abs(Y1 - Y2).sum()) < 1e-6

池化层

池化层/汇聚层 Pooling:降低卷积层对位置的敏感性

二维最大池化

将滑动窗口中的最大值输出

  • 和卷积层一样,具有填充和步幅
  • 没有可学习参数
  • 不会融合多个通道,输入通道数 = 输出通道数

平均池化层

将.max()替换为.mean()

代码实现

 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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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 = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
pool2d(X, (2, 2))
# tensor([[4., 5.],
#         [7., 8.]])

# 填充和步幅设定
X = torch.arange(16, dtype=torch.float32).reshape((1, 1, 4, 4))
# tensor([[[[ 0.,  1.,  2.,  3.],
#           [ 4.,  5.,  6.,  7.],
#           [ 8.,  9., 10., 11.],
#           [12., 13., 14., 15.]]]])
pool2d = nn.MaxPool2d(3, padding=1, stride=2)
pool2d(X)
# tensor([[[[ 5.,  7.],
#           [13., 15.]]]])
# 设定任意大小窗口
pool2d = nn.MaxPool2d((2, 3), stride=(2, 3), padding=(0, 1))
pool2d(X)
# tensor([[[[ 5.,  7.],
#           [13., 15.]]]])

# 多通道
X = torch.cat((X, X + 1), 1)
X
# 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.]]]])

pool2d = nn.MaxPool2d(3, padding=1, stride=2)
pool2d(X)
# tensor([[[[ 5.,  7.],
#           [13., 15.]],

#          [[ 6.,  8.],
#           [14., 16.]]]])

LeNet

  • 卷积层学习图片空间信息
  • 全连接层转换到类别空间

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import torch
from torch import nn
from d2l import torch as d2l

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))