提交 88c79a81 编写于 作者: R Rachel Hu 提交者: Aston Zhang

first version done

上级 f797d85a
# 残差网络(ResNet)
:label:`sec_resnet`
随着我们设计越来越深的网络,了解添加层如何增加网络的复杂性和表现力变得越来越重要。更重要的是设计网络的能力,在这种网络中,添加层会使网络更具表现力,而不仅仅是与众不同。为了取得一些进展,我们需要一些数学知识。
随着我们设计越来越深的网络,理解“添加层如何增加网络的复杂性和性能”变得至关重要。
为了取得质的突破,我们需要一些数学基础知识。
## 函数类
考虑$\mathcal{F}$,一种特定网络架构(连同学习速率和其他超参数设置)可以达到的功能类别。也就是说,对于所有$f \in \mathcal{F}$,存在一些参数集(例如权重和偏差),这些参数可以通过在合适的数据集上进行训练而获得。让我们假设$f^*$是我们真正想要找到的“真相”函数。如果是$\mathcal{F}$,我们的状态很好,但通常我们不会那么幸运。相反,我们将尝试找到一些$f^*_\mathcal{F}$,这是我们在$\mathcal{F}$中的最佳选择。例如,给定一个具有$\mathbf{X}$特性和$\mathbf{y}$标签的数据集,我们可以尝试通过解决以下优化问题来找到它:
首先,假设有一类特定的神经网络架构 $\mathcal{F}$,它包括学习速率和其他超参数设置。
也就是说,对于所有 $f \in \mathcal{F}$,存在一些参数集(例如权重和偏差),这些参数可以通过在合适的数据集上进行训练而获得。
现在假设 $f^*$ 是我们真正想要找到的函数,如果是 $f^* \in \mathcal{F}$,那我们的可以轻而易举的训练得到它,但通常我们不会那么幸运。
相反,我们将尝试找到一个函数 $f^*_\mathcal{F}$,这是我们在 $\mathcal{F}$ 中的最佳选择。
例如,给定一个具有 $\mathbf{X}$ 特性和 $\mathbf{y}$ 标签的数据集,我们可以尝试通过解决以下优化问题来找到它:
$$f^*_\mathcal{F} := \mathop{\mathrm{argmin}}_f L(\mathbf{X}, \mathbf{y}, f) \text{ subject to } f \in \mathcal{F}.$$
只有合理的假设是,如果我们设计一个不同的、更强大的体系结构$\mathcal{F}'$,我们将获得更好的结果。换句话说,我们预计$f^*_{\mathcal{F}'}$比$f^*_{\mathcal{F}}$“更好”。然而,如果$\mathcal{F} \not\subseteq \mathcal{F}'$,则无法保证这种情况会发生。事实上,$f^*_{\mathcal{F}'}$可能更糟。如:numref:`fig_functionclasses`所示,对于非嵌套函数类,较大的函数类并不总是向“真”函数$f^*$靠拢。例如,在:numref:`fig_functionclasses`的左边,虽然$\mathcal{F}_3$比$f^*$更接近$f^*$,但$\mathcal{F}_6$却离开了,并且不能保证进一步增加复杂性可以减少与$f^*$的距离。对于嵌套函数类:numref:`fig_functionclasses`右侧的$\mathcal{F}_1 \subseteq \ldots \subseteq \mathcal{F}_6$,我们可以从非嵌套函数类中避免上述问题。
那么,怎样得到“更近似”真正 $f^*$ 的函数呢?
唯一合理的可能性是,我们需要设计一个更强大的体系结构 $\mathcal{F}'$。
换句话说,我们预计 $f^*_{\mathcal{F}'}$ 比 $f^*_{\mathcal{F}}$ “更近似”。
然而,如果 $\mathcal{F} \not\subseteq \mathcal{F}'$,则无法保证新的体系“更近似”。
事实上, $f^*_{\mathcal{F}'}$ 可能更糟:
如 :numref:`fig_functionclasses` 所示,对于非嵌套函数类,较复杂的函数类并不总是向“真”函数 $f^*$ 靠拢(复杂度由 $\mathcal{F}_1$ 向 $\mathcal{F}_6$ 递增)。
在 :numref:`fig_functionclasses` 的左边,虽然 $\mathcal{F}_3$ 比 $f^*$ 更接近 $f^*$,但$\mathcal{F}_6$ 却离的更远了。
相反对于 :numref:`fig_functionclasses` 右侧的嵌套函数类 $\mathcal{F}_1 \subseteq \ldots \subseteq \mathcal{F}_6$,我们可以避免上述问题。
![For non-nested function classes, a larger (indicated by area) function class does not guarantee to get closer to the "truth" function ($f^*$). This does not happen in nested function classes.](../img/functionclasses.svg)
![对于非嵌套函数类,较复杂(由较大区域表示)的函数类不能保证更接近“真”函数( $f^*$ )。这种现象在嵌套函数类中不会发生。](../img/functionclasses.svg)
:label:`fig_functionclasses`
因此,只有当较大的函数类包含较小的函数类时,我们才能保证增加它们严格地增加网络的表达能力。对于深度神经网络,如果我们能将新增加的层训练成一个识别函数$f(\mathbf{x}) = \mathbf{x}$,新模型将与原模型一样有效。由于新模型可能会得到更好的解决方案来适应训练数据集,因此增加的层可能会更容易减少训练误差。
因此,只有当较复杂的函数类包含较小的函数类时,我们才能确保提高它们的性能。
对于深度神经网络,如果我们能将新添加的层训练成恒等映射 $f(\mathbf{x}) = \mathbf{x}$ ,新模型和原模型将同样有效。
同时,由于新模型可能得出更优的解来拟合训练数据集,因此添加层似乎更容易降低训练误差。
针对这一问题,何恺明等人提出了*残差网络*(ResNet) :cite:`He.Zhang.Ren.ea.2016`
它在2015年的ImageNet图像识别挑战赛夺魁,并深刻影响了后来的深度神经网络的设计。
残差网络的核心思想是:每个附加层都应该更容易地包含原始函数作为其元素之一。
于是,*残差块* (residual blocks) 便诞生了,这个设计对如何建立深层神经网络产生了深远的影响。
凭借它,ResNet 赢得了 2015 年 ImageNet 大规模视觉识别挑战赛。。
这是他等人提出的问题。当工作在非常深的计算机视觉模型:cite:`He.Zhang.Ren.ea.2016`。他们提出的“残差网络”(*ResNet*)的核心思想是,每个附加层都应该更容易地包含身份函数作为其元素之一。这些考虑是相当深刻的,但他们导致了一个惊人的简单的解决方案,一个*剩余块*。凭借它,ResNet赢得了2015年ImageNet大规模视觉识别挑战赛。这个设计对如何建立深层神经网络产生了深远的影响。
## 残差块
让我们关注神经网络的局部部分,如:numref:`fig_residual_block`所示。用$\mathbf{x}$表示输入。我们假设我们希望通过学习获得的所需底层映射是$f(\mathbf{x})$,用作顶部激活函数的输入。在:numref:`fig_residual_block`的左边,虚线框内的部分必须直接学习映射$f(\mathbf{x})$。在右边,虚线框内的部分需要学习*残差映射*$f(\mathbf{x}) - \mathbf{x}$,这就是残差块如何获得其名称的。如果身份映射$f(\mathbf{x}) = \mathbf{x}$是所需的底层映射,则残差映射更容易学习:我们只需将虚线框内上部权重层(例如,完全连接层和卷积层)的权重和偏差推到零。:numref:`fig_residual_block`中的右图说明了ResNet的*剩余块*,其中携带层输入$\mathbf{x}$到加法运算符的实线称为*剩余连接*(或*快捷连接*)。使用剩余块,输入可以通过层间的剩余连接更快地向前传播。
让我们聚焦于神经网络局部:如图 :numref:`fig_residual_block` 所示,假设我们的原始输入为 $x$ ,而希望学出的理想映射为 $f(\mathbf{x})$ (作为 :numref:`fig_residual_block` 上方激活函数的输入)。
:numref:`fig_residual_block` 左图虚线框中的部分需要直接拟合出该映射 $f(\mathbf{x})$ ,而右图虚线框中的部分则需要拟合出有关恒等映射的残差映射 $f(\mathbf{x}) - \mathbf{x}$ 。
残差映射在现实中往往更容易优化。
以本节开头提到的恒等映射作为我们希望学出的理想映射 $f(\mathbf{x})$ ,我们只需将 :numref:`fig_residual_block` 中右图虚线框内上方的加权运算(如仿射)的权重和偏差参数设成 0,那么 $f(\mathbf{x})$ 即为恒等映射。
实际中,当理想映射 $f(\mathbf{x})$ 极接近于恒等映射时,残差映射也易于捕捉恒等映射的细微波动。
:numref:`fig_residual_block` 右图是 ResNet 的基础块,即 *残差块*(residual block)。
在残差块中,输入可通过跨层的数据线路更快地向前传播。
![A regular block (left) and a residual block (right).](../img/residual-block.svg)
![一个正常块(左图)和一个残差块(右图)。](../img/residual-block.svg)
:label:`fig_residual_block`
ResNet遵循VGG的完整$3\times 3$卷积层设计。剩余块具有两个$3\times 3$个卷积层,具有相同数量的输出信道。每个卷积层后面是一个批处理规范化层和一个ReLU激活函数。然后,我们跳过这两个卷积运算,直接在最终的ReLU激活函数之前添加输入。这种设计要求两个卷积层的输出必须与输入具有相同的形状,以便将它们相加。如果我们想要改变通道的数量,我们需要引入一个额外的$1\times 1$卷积层来将输入转换成加法运算所需的形状。让我们看看下面的代码。
ResNet 沿用了 VGG 完整的 $3\times 3$ 卷积层设计。
残差块里首先有 2 个有相同输出通道数的 $3\times 3$ 卷积层。
每个卷积层后接一个批量归一化层和 ReLU 激活函数。
然后我们将输入跳过这 2 个卷积运算后直接加在最后的 ReLU 激活函数前。
这样的设计要求 2 个卷积层的输出与输入形状一样,从而可以相加。
如果想改变通道数,就需要引入一个额外的 $1\times 1$ 卷积层来将输入变换成需要的形状后再做相加运算。
残差块的实现如下。
```{.python .input}
from d2l import mxnet as d2l
......@@ -116,12 +148,14 @@ class Residual(tf.keras.Model): #@save
return tf.keras.activations.relu(Y)
```
此代码生成两种类型的网络:一种是在`use_1x1conv=False`应用ReLU非线性之前将输入添加到输出,另一种是在添加之前通过$1 \times 1$卷积调整信道和分辨率。:numref:`fig_resnet_block`说明了这一点:
如图 :numref:`fig_resnet_block` 所示,此代码生成两种类型的网络:
一种是在 `use_1x1conv=False` 时、应用 ReLU 非线性之前,将输入添加到输出。
另一种是在 `use_1x1conv=True` 时,添加通过 $1 \times 1$ 卷积调整通道和分辨率。
![ResNet block with and without $1 \times 1$ convolution.](../img/resnet-block.svg)
![包含以及不包含 $1 \times 1$ 卷积层的残差块。](../img/resnet-block.svg)
:label:`fig_resnet_block`
现在让我们看看输入和输出是相同形状的情况。
下面我们来查看输入和输出形状一致的情况。
```{.python .input}
blk = Residual(3)
......@@ -146,7 +180,7 @@ Y = blk(X)
Y.shape
```
我们还可以选择将输出高度和宽度减半,同时增加输出通道的数量
我们也可以在增加输出通道数的同时减半输出的高和宽
```{.python .input}
blk = Residual(6, use_1x1conv=True, strides=2)
......@@ -168,7 +202,9 @@ blk(X).shape
## ResNet模型
ResNet的前两层与我们前面描述的GoogLeNet是相同的:$7\times 7$卷积层有64个输出通道,步长为2,后面是$3\times 3$最大池层,步长为2。区别是在ResNet中每个卷积层之后添加的批处理规范化层。
ResNet 的前两层跟之前介绍的 GoogLeNet 中的一样:
在输出通道数为 64、步幅为 2 的 $7 \times 7$ 卷积层后接步幅为 2 的 $3 \times 3$ 的最大池化层。
不同之处在于 ResNet 每个卷积层后增加了批量归一化层。
```{.python .input}
net = nn.Sequential()
......@@ -193,9 +229,13 @@ b1 = tf.keras.models.Sequential([
tf.keras.layers.MaxPool2D(pool_size=3, strides=2, padding='same')])
```
GoogLeNet使用了由Inception块组成的四个模块。然而,ResNet使用由剩余块组成的四个模块,每个模块使用具有相同数量输出信道的几个剩余块。第一个模块中的通道数与输入通道数相同。由于已经使用了跨距为2的最大池层,因此不必减小高度和宽度。在每个后续模块的第一剩余块中,与前一模块相比,信道数量增加了一倍,并且高度和宽度减半。
GoogLeNet 在后面接了 4 个由 Inception 块组成的模块。
ResNet 则使用 4 个由残差块组成的模块,每个模块使用若干个同样输出通道数的残差块。
第一个模块的通道数同输入通道数一致。
由于之前已经使用了步幅为 2 的最大池化层,所以无须减小高和宽。
之后的每个模块在第一个残差块里将上一个模块的通道数翻倍,并将高和宽减半。
现在,我们实现这个模块。注意,对第一个模块执行了特殊处理。
下面我们来实现这个模块。注意,我们对第一个模块做了特别处理。
```{.python .input}
def resnet_block(num_channels, num_residuals, first_block=False):
......@@ -242,7 +282,7 @@ class ResnetBlock(tf.keras.layers.Layer):
return X
```
然后,我们将所有模块添加到ResNet中。这里,每个模块使用两个剩余块。
接着我们 为ResNet 加入所有残差块。这里每个模块使用 2 个残差块。
```{.python .input}
net.add(resnet_block(64, 2, first_block=True),
......@@ -267,7 +307,7 @@ b4 = ResnetBlock(256, 2)
b5 = ResnetBlock(512, 2)
```
最后,就像GoogLeNet一样,我们添加了一个全局平均池层,然后是完全连接的层输出。
最后,与 GoogLeNet 一样,加入全局平均池化层后接上全连接层输出。
```{.python .input}
net.add(nn.GlobalAvgPool2D(), nn.Dense(10))
......@@ -304,12 +344,18 @@ def net():
tf.keras.layers.Dense(units=10)])
```
每个模块有4个卷积层(不包括$1\times 1$卷积层)。加上第一个$7\times 7$卷积层和最后一个全连通层,共有18个层。因此,这种模型通常被称为ResNet-18。通过在模块中配置不同数量的信道和剩余块,我们可以创建不同的ResNet模型,例如更深的152层ResNet-152。虽然ResNet的主要架构与GoogLeNet相似,但是ResNet的结构更简单、更容易修改。所有这些因素导致ResNet的迅速和广泛的使用。:numref:`fig_resnet18`描述了完整的ResNet-18。
每个模块有 4 个卷积层(不包括 $1\times 1$ 卷积层)。
加上第一个 $7\times 7$ 卷积层和最后一个全连接层,共有 18 层。
因此,这种模型通常被称为 ResNet-18。
通过配置不同的通道数和模块里的残差块数可以得到不同的 ResNet 模型,例如更深的含 15 层的 ResNet-152。
虽然 ResNet 的主体架构跟 GoogLeNet 的类似,但 ResNet 结构更简单,修改也更方便。这些因素都导致了 ResNet 迅速被广泛使用。
:numref:`fig_resnet18`描述了完整的ResNet-18。
![The ResNet-18 architecture.](../img/resnet18.svg)
:label:`fig_resnet18`
在训练ResNet之前,让我们观察一下ResNet中不同模块的输入形状是如何变化的。在所有以前的架构中,分辨率降低,而通道数量增加,直到全局平均池层聚集所有特性为止。
在训练 ResNet 之前,让我们观察一下 ResNet 中不同模块的输入形状是如何变化的。
在之前所有架构中,分辨率降低,通道数量增加,直到全局平均池化层聚集所有特性为止。
```{.python .input}
X = np.random.uniform(size=(1, 1, 224, 224))
......@@ -348,18 +394,19 @@ d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr)
## 小结
* 嵌套函数类是理想的。在深层神经网络中学习另一层作为身份函数(尽管这是一个极端情况)应该很容易。
* 残差映射可以更容易地学习同一函数,例如将权重层中的参数推到零。
* 利用残差块可以训练出一个有效的深层神经网络。输入可以通过层间的残余连接更快地向前传播。
* ResNet对随后的深层神经网络的设计产生了重大影响,无论是卷积的还是序列的。
* 嵌套函数类是理想的。在深层神经网络中学习另一层作为“原始”函数较容易(尽管这是一个极端情况)。
* 残差映射可以更容易地学习同一函数,例如将权重层中的参数近似为零。
* 利用残差块(residual blocks)可以训练出一个有效的深层神经网络:输入可以通过层间的残余连接更快地向前传播。
* ResNet 对随后的深层神经网络设计产生了深远影响,无论是卷积还是全连接层。
## 练习
1. :numref:`fig_inception`中的起始块与剩余块之间的主要区别是什么?在删除了Inception块中的一些路径之后,它们是如何相互关联的?
1. 参考ResNet论文:cite:`He.Zhang.Ren.ea.2016`中的表1,以实现不同的变体。
1. 对于更深层次的网络,ResNet引入了“瓶颈”架构来降低模型的复杂性。试着去实现它。
1.ResNet的后续版本中,作者将“卷积、批处理规范化和激活”结构更改为“批处理规范化、激活和卷积”结构。你自己做这个改进。详见:cite:`He.Zhang.Ren.ea.2016*1`中的图1。
1. 为什么即使函数类是嵌套的,我们为什么不能不加限制地增加函数的复杂性呢?
1. :numref:`fig_inception` 中的起始块与残差块之间的主要区别是什么?在删除了 Inception 块中的一些路径之后,它们是如何相互关联的?
1. 参考 ResNet 论文 :cite:`He.Zhang.Ren.ea.2016` 中的表 1,以实现不同的变体。
1. 对于更深层次的网络,ResNet 引入了“bottleneck”架构来降低模型的复杂性。试着去实现它。
1. ResNet 的后续版本中,作者将“卷积、批处理规范化和激活”结构更改为“批处理规范化、激活和卷积”结构。请你做这个改进。详见 :cite:`He.Zhang.Ren.ea.2016*1` 中的图 1。
1. 为什么即使函数类是嵌套的,我们仍然要限制增加函数的复杂性呢?
:begin_tab:`mxnet`
[Discussions](https://discuss.d2l.ai/t/85)
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册