Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

常用卷积神经网络(CNN)

LeNet

LeNet最早由YannLeCun在1989年提出,目的是识别图像中的手写数字。

总体来看,LeNet(LeNet-5)由两个部分组成:

  • 卷积编码器:由两个卷积层组成
  • 全连接层密集快:由三个全连接层组成

该网络的结构如下:

image-20221118185359252

每个卷积块中的基本单元包含以下结构:

  • 一个卷积层
  • 一个sigmoid激活函数
  • 平均池化层

每个卷积层使用5 * 5卷积核和一个sigmoid激活函数。这些层将输入映射到多个二维特征输出,通常同时增加通道的数量。

每个kernel=2, stride=2的池化操作通过空间下采样将维数减少4倍。

接下来使用Pytorch实现以下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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
import torch
from torch import nn
from d2l import torch as d2l


# TODO: 构建网络
class Reshape(torch.nn.Module):
def forward(self, x):
return x.view(-1, 1, 28, 28)


net = torch.nn.Sequential(
Reshape(),
# 此处输入为 1 * 28 * 28
nn.Conv2d(1, 6, kernel_size=5, padding=2), nn.Sigmoid(),
# 输出为 6 * 28 * 28(28 - 5 + 4 + 1)
nn.AvgPool2d(kernel_size=2, stride=2),
# 输出为 6 * 14 * 14(28 - 2 + 2)/2
nn.Conv2d(6, 16, kernel_size=5), nn.Sigmoid(),
# 输出为 16 * 10 * 10(14 - 5 + 1 )
nn.AvgPool2d(kernel_size=2, stride=2), nn.Flatten(),
# 输出为 1 * (16 * 5 * 5(10 - 2 + 2)/2)
nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(),
nn.Linear(120, 84), nn.Sigmoid(),
nn.Linear(84, 10)
)

# TODO: 模拟训练,打印输出
X = torch.rand(size=(1, 1, 28, 28), dtype=torch.float32)
for layer in net:
X = layer(X)
print(layer.__class__.__name__, 'output shape: \t', X.shape)

# TODO: 尝试使用GPU训练Fashion-MNIST数据集

batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)

# TODO: 模型评估函数


def evaluate_accuracy_gpu(net, data_iter, device=None):

if isinstance(net, nn.Module):
net.eval() # 设置为评估模式
if not device:
device = next(iter(net.parameters())).device
# 计算预测的数量, 总预测的数量
metric = d2l.Accumulator(2)
with torch.no_grad():
for X, y in data_iter:
if isinstance(X, list):
# 将样本移动到对应设备上
X = [x.to(device) for x in X]
else:
X = X.to(device)
y = y.to(device)
metric.add(d2l.accuracy(net(X), y), y.numel())
return metric[0] / metric[1]

# TODO: 模型训练函数


def train_ch6(net, train_iter, test_iter, num_epochs, lr, device):
def init_weight(m):
if type(m) == nn.Linear or type(m) == nn.Conv2d:
nn.init.xavier_uniform_(m.weight)
net.apply(init_weight)
print('training on', device)
# 将网络转移到设备
net.to(device)
# 优化器
optimizer = torch.optim.SGD(net.parameters(), lr=lr)
# 损失函数
loss = nn.CrossEntropyLoss()
# 动画
animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
legend=['train loss', 'train acc', 'test acc'])
timer, num_batches = d2l.Timer(), len(train_iter)
# 训练
for epoch in range(num_epochs):
metric = d2l.Accumulator(3)
net.train()
for i, (X, y) in enumerate(train_iter):
# 开启计时器
timer.start()
# 清空梯度
optimizer.zero_grad()
X, y = X.to(device), y.to(device)
y_hat = net(X)
# 计算损失
l = loss(y_hat, y)
# 梯度后向传播
l.backward()
optimizer.step()
with torch.no_grad():
metric.add(l*X.shape[0], d2l.accuracy(y_hat, y), X.shape[0])
timer.stop()
train_l = metric[0] / metric[2]
train_acc = metric[1] / metric[2]
if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:
animator.add(epoch + (i + 1) / num_batches,
(train_l, train_acc, None))
test_acc = evaluate_accuracy_gpu(net, test_iter)
animator.add(epoch + 1, (None, None, test_acc))
print(f'loss {train_l:.3f}, train acc {train_acc:.3f}, '
f'test acc {test_acc:.3f}')
print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec '
f'on {str(device)}')


lr, num_epochs = 0.9, 10
train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
d2l.plt.show()

我们使用模拟数据查看以下各层的输出维度:

1
2
3
4
5
6
7
8
9
10
11
12
13
 Reshape output shape:    torch.Size([1, 1, 28, 28])
Conv2d output shape: torch.Size([1, 6, 28, 28])
Sigmoid output shape: torch.Size([1, 6, 28, 28])
AvgPool2d output shape: torch.Size([1, 6, 14, 14])
Conv2d output shape: torch.Size([1, 16, 10, 10])
Sigmoid output shape: torch.Size([1, 16, 10, 10])
AvgPool2d output shape: torch.Size([1, 16, 5, 5])
Flatten output shape: torch.Size([1, 400])
Linear output shape: torch.Size([1, 120])
Sigmoid output shape: torch.Size([1, 120])
Linear output shape: torch.Size([1, 84])
Sigmoid output shape: torch.Size([1, 84])
Linear output shape: torch.Size([1, 10])

在Fashion-MNIST数据集上的训练(GPU训练)效果如下:

image-20221123185106903

image-20221123185225242

AlexNet

2000 - 2010年机器学习主流的方法是基于核方法的SVM,它对调参不敏感,且具有一套完整的论证方法。

ImageNet

2010年提出,自然物体的彩色图片大小为469 * 387,样本数为1.2M,类别数为1000,手写数字的黑白图片大小为28*28,样本数为60K,类别为10类。

  • AlexNet赢得了2012年ImageNet竞赛
  • 是一个更深更大的LeNet
  • 主要改进:
    • 丢弃算法
    • ReLu
    • MaxPooling
  • 将计算机视觉的方法论更改为端到端的学习过程

Alex架构

image-20221123195639626

  • 将激活函数从Sigmoid转化为ReLu(延缓梯度消失)
  • 隐藏全连接层后加入了丢弃层
  • 对图片进行了数据增强

下面我们用PyTorch来实现一下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
import torch
from torch import nn
from d2l import torch as d2l

net = nn.Sequential(
# 这里,我们使用一个11*11的更大窗口来捕捉对象。
# 同时,步幅为4,以减少输出的高度和宽度。
# 另外,输出通道的数目远大于LeNet
nn.Conv2d(1, 96, kernel_size=11, stride=4, padding=1), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2),
# 减小卷积窗口,使用填充为2来使得输入与输出的高和宽一致,且增大输出通道数
nn.Conv2d(96, 256, kernel_size=5, padding=2), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2),
# 使用三个连续的卷积层和较小的卷积窗口。
# 除了最后的卷积层,输出通道的数量进一步增加。
# 在前两个卷积层之后,汇聚层不用于减少输入的高度和宽度
nn.Conv2d(256, 384, kernel_size=3, padding=1), nn.ReLU(),
nn.Conv2d(384, 384, kernel_size=3, padding=1), nn.ReLU(),
nn.Conv2d(384, 256, kernel_size=3, padding=1), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Flatten(),
# 这里,全连接层的输出数量是LeNet中的好几倍。使用dropout层来减轻过拟合
nn.Linear(6400, 4096), nn.ReLU(), nn.Dropout(p=0.5),
nn.Linear(4096, 4096), nn.ReLU(), nn.Dropout(p=0.5),
# 最后是输出层。由于这里使用Fashion-MNIST,所以用类别数为10,而非论文中的1000
nn.Linear(4096, 10))

# TODO: 读取数据
batch_size = 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224)

# TODO: 训练
lr, num_epochs = 0.01, 10
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
d2l.plt.show()

image-20221123210145128

image-20221123210225500

VGG

VGG块

经典的卷积神经网络有以下几个部分组成:

  • 带填充以保证分辨率的卷积层
  • 非线性激活函数,入ReLU
  • 池化层,如最大池化层

一个VGG块与之类似,有一系列卷积层组成,最后再加上用于空间下采样的最大汇聚层。

VGG块被定义为:

  • n层,m个通道的kernel=3, padding=1的卷积层
  • 一个kernel=2, stride=2的MaxPooling层

image-20221125162625657

经过实验发现使用更深的小卷积核会比使用更浅的大卷积核效果更好,因此VGG块仍然使用了3*3的卷积核。

VGG架构

在多个VGG块后接全连接层,不同次数的重复快得到不同的架构VGG-16、VGG-19

image-20221125163548962

  • VGG使用可重复使用的卷积块来构建深度卷积神经网络
  • 不同的卷积块个数和超参数可以得到不同复杂度的变种

代码实现

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
58
59
60
61
62
63
import torch
from torch import nn
from d2l import torch as d2l

# TODO: 构建VGG块


def vgg_block(num_convs, in_channels, out_channels):
layers = []
for _ in range(num_convs):
layers.append(nn.Conv2d(in_channels, out_channels,
kernel_size=3, padding=1))
layers.append(nn.ReLU())
in_channels = out_channels
layers.append(nn.MaxPool2d(kernel_size=2, stride=2))
return nn.Sequential(*layers)


# TODO: 设置一个VGG架构(经典五块式架构VGG-11)
# 每通过一个块宽高减半,通道数翻一倍(第5层通道数不变
conv_arch = ((1, 64), (1, 128), (2, 256), (2, 512), (2, 512))

# TODO: VGG网络构造


def vgg(conv_arch):
conv_blocks = []
in_channels = 1
for (num_convs, out_channels) in conv_arch:
conv_blocks.append(vgg_block(
num_convs, in_channels, out_channels
))
in_channels = out_channels
return nn.Sequential(
*conv_blocks, nn.Flatten(),
# 输入为224的情况下经过卷积层图像大小不变
# 而池化层会将图像大小变为原来的一半
# 因此经过5次池化层后224*224的图像变为7*7
nn.Linear(out_channels * 7 * 7, 4096), nn.ReLU(),
nn.Dropout(0.5), nn.Linear(4096, 4096), nn.ReLU(),
nn.Dropout(0.5), nn.Linear(4096, 10)
)


net = vgg(conv_arch)

# TODO: 查看每一层的输出情况
X = torch.randn(size=(1, 1, 224, 224))
for blk in net:
X = blk(X)
print(blk.__class__.__name__, "output shape: \t", X.shape)

# 由于模型过于庞大,此处使用缩小的模型进行训练
ratio = 4
# 将通道数除以4
small_conv_arch = [(pair[0], pair[1] // ratio) for pair in conv_arch]
net = vgg(small_conv_arch)

# TODO: 训练
lr, num_epochs, batch_size = 0.05, 10, 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
d2l.plt.show()

每个块的输入情况如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
Sequential output shape:         torch.Size([1, 64, 112, 112])
Sequential output shape: torch.Size([1, 128, 56, 56])
Sequential output shape: torch.Size([1, 256, 28, 28])
Sequential output shape: torch.Size([1, 512, 14, 14])
Sequential output shape: torch.Size([1, 512, 7, 7])
Flatten output shape: torch.Size([1, 25088])
Linear output shape: torch.Size([1, 4096])
ReLU output shape: torch.Size([1, 4096])
Dropout output shape: torch.Size([1, 4096])
Linear output shape: torch.Size([1, 4096])
ReLU output shape: torch.Size([1, 4096])
Dropout output shape: torch.Size([1, 4096])
Linear output shape: torch.Size([1, 10])

小模型训练的结果如下:

image-20221125173710332

1
2
loss 0.177, train acc 0.933, test acc 0.911
679.1 examples/sec on cuda:0

NiN

根据计算,模型的参数大多出现在第一层全连接层中,那么我们希望使用卷积层去替代全连接层以达到降低模型复杂度,加快训练速度的目的。

NiN块

  • 一个卷积层后跟两个全连接层
    • stride=1,输出形状与卷积层输出相同
    • 起到全连接层的作用

image-20221125191920369

即按照输入像素逐一连接的全连接层

image-20221125191814065

NiN架构

  • 无全连接层
  • 交替使用NiN块和stride=2的MaxPooling层
    • 逐步减小高宽和增大通道数
  • 最后使用全局平均池化层得到输出(池化层核的高宽等于输入每一个通道的高宽)
    • 输入通道数是类别数

image-20221125192247213

代码实现

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

# TODO: NiN块实现


def nin_block(in_channels, out_channels, kernel_size, strides, padding):
return nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size, strides, padding),
nn.ReLU(),
nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU(),
nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU()
)

# TODO: NiN模型


net = nn.Sequential(
nin_block(1, 96, kernel_size=11, strides=4, padding=0),
nn.MaxPool2d(3, stride=2),
nin_block(96, 256, kernel_size=5, strides=1, padding=2),
nn.MaxPool2d(3, stride=2),
nin_block(256, 384, kernel_size=3, strides=1, padding=1),
nn.MaxPool2d(3, stride=2),
nn.Dropout(0.5),
# 标签类别数是10
nin_block(384, 10, kernel_size=3, strides=1, padding=1),
nn.AdaptiveAvgPool2d((1, 1)),
# 将四维的输出转成二维的输出,其形状为(批量大小,10)
nn.Flatten()
)

# TODO: 每个块的输出

X = torch.rand(size=(1, 1, 224, 224))
for layer in net:
X = layer(X)
print(layer.__class__.__name__, 'output shape:\t', X.shape)

# TODO: 训练
lr, num_epochs, batch_size = 0.1, 10, 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
d2l.ply.show()

image-20221125201615885

GoogLeNet

Inception块

  • 四个路径从不同层面抽取信息,然后再输出通道维合并
  • Inception块不改变高宽,只改变通道数

image-20221125202236402

既然我们要对各个通道的输出再维度上进行合并,那么我们就来看看每个路径输出的通道数是如何变化的:

image-20221125203409118

其中更多的通道数意味着该条路径的权重更大。

使用Inception块的另一个重要原因是,与3*3或5 * 5 的直接卷积相比,Inception块具有更少的参数(同为输入192,输出256通道计算得到):

parameters FLOPS
Inception 0.16M 128M
3*3 Conv 0.44M 346M
5*5 Conv 1.22M 963M

GoogLeNet架构

5段,9个Inception块

image-20221125204144701

此处Stage的划分是根据是否将高宽减半进行划分的

stage1&2

image-20221125204405403

stage3

image-20221125204523316

stage4&5

image-20221125204659632

Inception各种变种

image-20221125204826863

代码实现

对于GoogLeNet我们先实现Inception块,进而实现每一个Stage,最后将每一个Stage连接成一个网络:

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l


class Inception(nn.Module):
def __init__(self, in_channel, c1, c2, c3, c4, **kwargs):
super(Inception, self).__init__(**kwargs)
self.p1_1 = nn.Conv2d(in_channel, c1, kernel_size=1)
self.p2_1 = nn.Conv2d(in_channel, c2[0], kernel_size=1)
self.p2_2 = nn.Conv2d(c2[0], c2[1], kernel_size=3, padding=1)
self.p3_1 = nn.Conv2d(in_channel, c3[0], kernel_size=1)
self.p3_2 = nn.Conv2d(c3[0], c3[1], kernel_size=5, padding=2)
self.p4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
self.p4_2 = nn.Conv2d(in_channel, c4, kernel_size=1)

def forward(self, x):
p1 = F.relu(self.p1_1(x))
p2 = F.relu(self.p2_2(F.relu(self.p2_1(x))))
p3 = F.relu(self.p3_2(F.relu(self.p3_1(x))))
p4 = F.relu(self.p4_2(self.p4_1(x)))
return torch.cat((p1, p2, p3, p4), dim=1)


stage1 = nn.Sequential(
nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
)

stage2 = nn.Sequential(
nn.Conv2d(64, 64, kernel_size=1), nn.ReLU(),
nn.Conv2d(64, 192, kernel_size=3, padding=1), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1),
)

stage3 = nn.Sequential(
Inception(192, 64, (96, 128), (16, 32), 32),
Inception(256, 128, (128, 192), (32, 96), 64),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
)

stage4 = nn.Sequential(
Inception(480, 192, (96, 208), (16, 48), 64),
Inception(512, 160, (112, 224), (24, 64), 64),
Inception(512, 128, (128, 256), (24, 64), 64),
Inception(512, 112, (144, 288), (32, 64), 64),
Inception(528, 256, (160, 320), (32, 128), 128),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
)

stage5 = nn.Sequential(
Inception(832, 256, (160, 320), (32, 128), 128),
Inception(832, 384, (192, 384), (48, 128), 128),
nn.AdaptiveAvgPool2d((1, 1)),
nn.Flatten()
)

net = nn.Sequential(
stage1,
stage2,
stage3,
stage4,
stage5,
nn.Linear(1024, 10)
)

X = torch.rand(size=(1, 1, 96, 96))
for layer in net:
X = layer(X)
print(layer.__class__.__name__, 'output shape:\t', X.shape)

lr, num_epochs, batch_size = 0.1, 10, 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

各层的参数如下:

1
2
3
4
5
6
Sequential output shape:         torch.Size([1, 64, 24, 24])
Sequential output shape: torch.Size([1, 192, 12, 12])
Sequential output shape: torch.Size([1, 480, 6, 6])
Sequential output shape: torch.Size([1, 832, 3, 3])
Sequential output shape: torch.Size([1, 1024])
Linear output shape: torch.Size([1, 10])

image-20221202170358467

1
2
loss 0.256, train acc 0.903, test acc 0.886
1072.1 examples/sec on cuda:0

批量归一化(Batch Normalization)

提出背景

随着神经网络的层级逐渐变深,对于网络的训练,越靠近输出的梯度越大,而越高金输入的梯度则会越小,因此会导致:

  • 靠近输出(顶部)的层级训练快
  • 靠近输入(底部)的层级训练缓慢
  • 每次更新底层是会使得顶部也需要更新,导致顶部重新学习多次
  • 导致收敛变慢

思想

固定小批量里的均值和方差。

首先计算出小批量的均值和方差:

$\begin{cases} \mu_B = \frac{1}{|B|} \sum _{i \in B} x_i \ \sigma ^ 2 _ B = \frac{1}{|B|} \sum _ {i \in B} (x_i - \mu_B)^2 + \epsilon \end{cases}$

然后再做额外的调整(可学习的参数)

$x_{i+1} = \gamma \frac{x_i - \mu B}{\sigma_B} + \beta$

其中$\gamma$为表示方差的参数,$\beta$为调整均值的参数

  • 可学习参数$\gamma$和$\beta$
  • 作用:
    • 全连接层和卷积层输出上,激活函数前
    • 全连接层和卷积层输入上
  • 对全连接层作用在特征维上
  • 对于卷积层作用在通道维上(事实上通道维即卷积层的特征维)

批量归一化在做什么

  • 最初论文想用它减少内部协变量的转移
  • 后续有论文指出它可能就是通过在每个小批量里加入噪音来控制模型复杂度
    • $x_{i+1} = \gamma \frac{x_i - \hat \mu_B}{\hat \sigma_B} + \beta$
    • 即由于每个mini-batch是随机取得,那么我们就可以认为$\hat \mu_B$,即mini-batch的均值,和$\hat \sigma_B$,即mini-batch的方差,是两个随机数,它们分别对原样本进行了随即偏移随机缩放
  • 因此没必要跟丢弃法混合使用

总结

  • 批量归一化固定小批量的均值和方差,然后学习出适合的偏移和缩放
  • 可以加速收敛速度,但一般不改变模型精度

代码实现

首先实现batch norm的计算操作,然后实现Batch Norm层,接着将其加入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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import torch
from torch import nn
from d2l import torch as d2l


def batch_norm(X, gamma, beta, moving_mean, moving_var, eps, momentum):
"""批量归一化操作

Args:
X (Tensor): 样本
gamma (Tensor): 参数1,代表方差
beta (Tensor): 参数2,代表均值
moving_mean (Tensor): 全局均值
moving_var (Tensor): 全局方差
eps (Tensor): 偏置值避免除0
momentum (float): 用于更新gamma和beta
"""
if not torch.is_grad_enabled():
# 对于预测模式下,使用全局均值和方差进行计算
X_hat = (X - moving_mean) / torch.sqrt(moving_var + eps)
else:
assert len(X.shape) in (2, 4) # 保证输入为2d全连接层或2d卷积层
if len(X.shape) == 2:
mean = X.mean(dim=0) # 对哪一维求均值,哪一维就会变成1
var = ((X - mean) ** 2).mean(dim=0)
else:
# 沿通道数求均值,即结果为1 * n * 1 * 1的向量
mean = X.mean(dim=(0, 2, 3), keepdim=True)
var = ((X - mean) ** 2).mean(dim=(0, 2, 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.data, moving_var.data

# TODO: 定义BatchNorm层


class BatchNorm(nn.Module):
def __init__(self, num_features, num_dims):
"""BatchNorm构造函数

Args:
num_features (Tensor): 全连接层输出数量或卷积层输出通道数
num_dims (Tensor): 2表示全连接层,4表示卷积层
"""
super().__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.ones(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(
X, self.gamma, self.beta, self.moving_mean, self.moving_var, eps=1e-5, momentum=0.9)
return Y

# TODO: 应用BatchNorm于LeNet模型


net = nn.Sequential(
nn.Conv2d(1, 6, kernel_size=5), BatchNorm(6, num_dims=4), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2),
nn.Conv2d(6, 16, kernel_size=5), BatchNorm(16, num_dims=4), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2), nn.Flatten(),
nn.Linear(16*4*4, 120), BatchNorm(120, num_dims=2), nn.Sigmoid(),
nn.Linear(120, 84), BatchNorm(84, num_dims=2), nn.Sigmoid(),
# 输出层不需要加
nn.Linear(84, 10))

lr, num_epochs, batch_size = 1.0, 10, 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
d2l.plt.show()

# TODO: 查看规范化层中查看学到的均值和方差
net[1].gamma.reshape((-1,)), net[1].beta.reshape((-1,))


# TODO: 使用torchAPI
net = nn.Sequential(
nn.Conv2d(1, 6, kernel_size=5), nn.BatchNorm2d(6), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2),
nn.Conv2d(6, 16, kernel_size=5), nn.BatchNorm2d(16), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2), nn.Flatten(),
nn.Linear(256, 120), nn.BatchNorm1d(120), nn.Sigmoid(),
nn.Linear(120, 84), nn.BatchNorm1d(84), nn.Sigmoid(),
nn.Linear(84, 10))

d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

在LeNet上的结果:

image-20221202194236563

1
2
loss 0.264, train acc 0.902, test acc 0.830
19296.8 examples/sec on cuda:0

查看学习到的均值和方差:

1
2
3
4
(tensor([0.3362, 4.0349, 0.4496, 3.7056, 3.7774, 2.6762], device='cuda:0',
grad_fn=<ReshapeAliasBackward0>),
tensor([-0.5739, 4.1376, 0.5126, 0.3060, -2.5187, 0.3683], device='cuda:0',
grad_fn=<ReshapeAliasBackward0>))

ResNet

核心问题

随着层数的加深,网络一定会越来越好吗?

模型偏差:指深度学习中,随着模型复杂的上升,学习得出的最优模型反而离目标模型更远的现象

如图所示的每一个$F$我们认为其是一类特定的神经网络框架,其包括学习率和其他超参数,那么我们可以通过学习从区域$F$中学习到一些函数,那么我们在$F$周围划定一些区域,代表所有$f \in F$的集合,通常我们需要找到一个能够完美拟合我们的目标的函数$*f$。而事实往往没有那么幸运,我们也许只能在这些区域中找到一个近似函数$*f_F$。

假设我们有一些具有X特性和y标签的数据,那么我们可以列出以下式子:

$*f_F := argmin_{f}L(X,y,f) \ subject \ to f \in F$

如下图所示,如果我们此时需要设计一个比原模型更接近结果的模型,例如$F_2$比$F_1$更接近,那么假设其区域如图所示,,则随着模型不断的迭代我们将得到越来越复杂的模型,并在该模型上找到近似最终目标的解。但如果新的模型无法覆盖旧的模型,最终的结果可能离目标函数越来越远。

image-20221212094356323

但如果更复杂的模型是完全包含以前的小模型的话:

image-20221212094600472

那么我们的模型将随着复杂度的增加而向着目标模型接近。

残差块

残差块的思想是在模型的训练过程中扩大函数类,而不是训练新的函数。

残差快通过串联一个层来改变函数类,计入快速通道来得到$f(x) = x + g(x)$的结构

image-20221212125421174

而实际使用中需要使用1*1的卷积层来调整输出的通道数:

image-20221212132952696

完整的结构是:

  • 高宽减半ResNet块(步幅为2)
  • 后接多个高宽不变ResNet块

image-20221212133850212

ResNet架构

类似GoogleNet的总体架构,分为了若干个Stage,但是其中的块替换为了ResNet块,最后添加了全局池化层。

ResNet整体结构

image-20221212173403424

总结

  • 残差快使得很深的网络更容易训练
  • 残差网络对其后的网络设计产生了深远的影响,无论是卷积类网络还是全连接类网络

RestNet如何处理梯度消失

一个最基本的避免梯度消失的操作是将乘法变成加法。

首先假设我们预测的$y = f(x)$,此处为了方便讨论我们省略Loss,那么对于某一个层的参数的梯度计算为$\frac{\delta y}{\delta w}$,那么每次更新的公式为:$w = w - \eta \frac{\delta y}{\delta w}$

根据这个式子,我们不希望梯度过小,导致w几乎不变。

现在我们考虑在f上增加一层:

$y’ = g(f(x))$

那么它的导数为:

$\frac{\delta y’}{\delta w} = \frac{\delta{y’}}{\delta y} \frac{\delta y}{\delta w} = \frac{\delta g(y)}{\delta y} \frac{\delta y}{\delta w}$

那么对于这一乘法而言,如果每一项都比较小,即小于1,那么累乘的结果将会越来越小。

那么ResNet是怎么解决的呢?

事实上对于ResNet的下一层来说,它的形式是这样的:

$y’’ = f(x) + g(f(x))$

则对它求导数将会得到:

$\frac{\delta y’’}{\delta w} = \frac{\delta y}{\delta w} + \frac{\delta y’}{\delta w}$

因此就算梯度很小,至少还有$\frac{\delta y}{\delta w}$这一项不会消失。

代码实现

同样我们首先实现残差块,该块有两种情况:

  1. 对于宽高减半,通道加倍的块,残差需要改变通道数
  2. 对于宽高不变的块,则不需要

接着,每一个Stage将包含两个残差快,除了Stage1以外,其余Stage的第一个残差快都需要将宽高减半。

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l

# TODO: 残差块


class Residual(nn.Module):
def __init__(self, input_channels, num_channels, use_1x1conv=False, strides=1):
"""残差块初始化

Args:
input_channels (int): 输入通道数
num_channels (int): 输出通道数
use_1x1conv (bool, 残差快控制): False关闭残差,True打开残差. Defaults to False.
strides (int, optional): 步长. Defaults to 1.
"""
super().__init__()
self.conv1 = nn.Conv2d(input_channels, num_channels,
kernel_size=3, padding=1, stride=strides)
self.conv2 = nn.Conv2d(num_channels, num_channels,
kernel_size=3, padding=1)
self.relu = nn.ReLU(inplace=True)

if use_1x1conv:
self.conv3 = nn.Conv2d(
input_channels, num_channels, kernel_size=1, stride=strides)

else:
self.conv3 = None
self.bn1 = nn.BatchNorm2d(num_channels)
self.bn2 = nn.BatchNorm2d(num_channels)

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)
Y += X
return F.relu(Y)


# TODO: 通道数不变的残差块
blk = Residual(3, 3)
X = torch.rand(4, 3, 6, 6)
Y = blk(X)
print(Y.shape)

# TODO: 通道数减半的残差块
blk = Residual(3, 6, use_1x1conv=True, strides=2)
print(blk(X).shape)

b1 = nn.Sequential(
nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
nn.BatchNorm2d(64), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))


def resnet_block(input_channels, num_channels, num_residuals, first_block=False):
blk = []
for i in range(num_residuals):
if i == 0 and not first_block:
blk.append(Residual(input_channels, num_channels,
use_1x1conv=True, strides=2))
else:
blk.append(Residual(num_channels, num_channels))
return blk


b2 = nn.Sequential(*resnet_block(64, 64, 2, first_block=True))
b3 = nn.Sequential(*resnet_block(64, 128, 2))
b4 = nn.Sequential(*resnet_block(128, 256, 2))
b5 = nn.Sequential(*resnet_block(256, 512, 2))

net = nn.Sequential(b1, b2, b3, b4, b5, nn.AdaptiveAvgPool2d(
(1, 1)), nn.Flatten(), nn.Linear(512, 10))

# TODO: 输出各层规模
X = torch.rand(size=(1, 1, 224, 224))
for layer in net:
X = layer(X)
print(layer.__class__.__name__, 'output shape:\t', X.shape)

# TODO: 训练
lr, num_epochs, batch_size = 0.05, 10, 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
d2l.plt.show()

首先来看一下两类残差快对输出的变化:

1
2
3
# 输入为4, 3, 6, 6维向量
torch.Size([4, 3, 6, 6])
torch.Size([4, 6, 3, 3])

可见第一类残差块的通道数和宽高并未发生变化

使用第二类残差块会将高宽减半,通道数加倍

而每一个Stage的输出结构如下:

可见第一个Stage将224维1通道的输入经过7*7卷积和3 * 3池化降到了56 * 56并将通道数变为64。

而第二个Stage并没有将通道数变化。

之后每一个Stage都会将通道数加倍,宽高减半。

1
2
3
4
5
6
7
8
Sequential output shape:         torch.Size([1, 64, 56, 56])
Sequential output shape: torch.Size([1, 64, 56, 56])
Sequential output shape: torch.Size([1, 128, 28, 28])
Sequential output shape: torch.Size([1, 256, 14, 14])
Sequential output shape: torch.Size([1, 512, 7, 7])
AdaptiveAvgPool2d output shape: torch.Size([1, 512, 1, 1])
Flatten output shape: torch.Size([1, 512])
Linear output shape: torch.Size([1, 10])

最后训练结果如下:

1
2
loss 0.010, train acc 0.998, test acc 0.915
1533.6 examples/sec on cuda:0

image-20221212180435600

DenseNet

思想

DenseNet是在ResNet上的逻辑展开,对于ResNet而言,他是将函数展开为:

$f(x) = x + g(x)$

而DenseNet则是借用了泰勒展开的思想:

$f(x) = f(0) + f’(0)x + \frac{f’’(0)}{2!} x^2 + \frac{f’’’(0)}{3!}x^3 + …$

DenseNet将ResNet中重新加上输入的操作变更为了与输入进行连接:

image-20221213184010007

此处我们使用[,]来表示这种连接操作,而不是简单相加,因此我们执行从x到其展开式的映射:

$x \to [x, f_1(x), f_2([x,f_1(x)]),f_3([x,f_1(x),f_2([x,f_1(x)])]), …].$

最后,将这些展开式结合到MLP中即转化为稠密连接:

image-20221213184841217

DenseNet主要由两部分构成:

  1. 稠密快Dense Block
  2. 过渡层Transition Layer

前者定义如何连接输入输出,后者则控制通道数量,使其不会过于复杂。

代码实现

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import torch
from torch import nn
from d2l import torch as d2l

# 使用 BN ReLU CONV架构,该思想来自于ResNet论文中的后续版本改进

# TODO: 构造卷积块


def conv_block(input_channels, num_channels):
return nn.Sequential(
nn.BatchNorm2d(input_channels), nn.ReLU(),
nn.Conv2d(input_channels, num_channels, kernel_size=3, padding=1)
)

# TODO: 构建稠密块


class DenseBlock(nn.Module):
"""稠密块

Deception: 该块由多个卷积块构成,每个卷积块使用相同数量的输出通道。
然而,在前向传播中,我们将每个卷积块的输入和输出在通道维度上连结。

"""

def __init__(self, num_conv, input_channels, num_channels):
super(DenseBlock, self).__init__()
layer = []
for i in range(num_conv):
layer.append(conv_block(num_channels * i +
input_channels, num_channels))

self.net = nn.Sequential(*layer)

def forward(self, X):
for blk in self.net:
Y = blk(X)
# 连接通道上每一个块的输入和输出
X = torch.cat((X, Y), dim=1)
return X


# TODO: 测试稠密块的形状
blk = DenseBlock(2, 3, 10)
X = torch.randn(4, 3, 8, 8)
Y = blk(X)
print(Y.shape)

# TODO: 过渡层

# 由于每个稠密块都会带来通道数的增加,使用过多则会过于复杂化模型。
# 而过渡层可以用来控制模型复杂度。
# 它通过卷积层来减小通道数,并使用步幅为2的平均汇聚层减半高和宽,从而进一步降低模型复杂度。


def transition_block(input_channels, num_channels):
return nn.Sequential(
nn.BatchNorm2d(input_channels), nn.ReLU(),
nn.Conv2d(input_channels, num_channels, kernel_size=1),
nn.AvgPool2d(kernel_size=2, stride=2)
)


# TODO: 测试过度块形状
blk = transition_block(23, 10)
print(blk(Y).shape)

# TODO: DenseNet模型

# 第一个Stage使用与ResNet相同的卷积层和MaxPooling
b1 = nn.Sequential(
nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
nn.BatchNorm2d(64), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))


# TODO: 接下来使用类似ResNet的四个块

# Dense块的卷积层可以设置多个,此处设为4,从而与ResNet-18保持一致。
# Dense块中的卷积层通道数设为32,因此每个块将增加128个通道

# 需要注意的是ResNet通过步幅为2的Res块减小宽高,而Dense块使用过渡层,同时还会减半通道数
# num_channels为当前的通道数
num_channels, growth_rate = 64, 32
num_convs_in_dense_blocks = [4, 4, 4, 4]
blks = []
for i, num_convs in enumerate(num_convs_in_dense_blocks):
blks.append(DenseBlock(num_convs, num_channels, growth_rate))
# 上一个稠密块的输出通道数
num_channels += num_convs * growth_rate
# 在稠密块之间添加一个转换层,使通道数量减半
if i != len(num_convs_in_dense_blocks) - 1:
blks.append(transition_block(num_channels, num_channels // 2))
num_channels = num_channels // 2

# TODO: 与ResNet类似,最后需要增加全局汇聚层和全连接层
net = nn.Sequential(
b1, *blks,
nn.BatchNorm2d(num_channels), nn.ReLU(),
nn.AdaptiveAvgPool2d((1, 1)),
nn.Flatten(),
nn.Linear(num_channels, 10))

# TODO: 训练
lr, num_epochs, batch_size = 0.1, 10, 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
d2l.plt.show()

经过Dense块后的输出形状如下:

1
torch.Size([4, 23, 8, 8])

经过过度层的输出形状如下

1
torch.Size([4, 10, 4, 4])

训练结果:

1
2
loss 0.140, train acc 0.950, test acc 0.882
5544.6 examples/sec on cuda:0

image-20221213194244624

评论