Skip to content

Commit

Permalink
revise resnet
Browse files Browse the repository at this point in the history
  • Loading branch information
astonzhang committed Sep 1, 2018
1 parent c507f16 commit 00c367d
Show file tree
Hide file tree
Showing 4 changed files with 230 additions and 30 deletions.
2 changes: 2 additions & 0 deletions TERMINOLOGY.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@

过拟合,underfitting

恒等映射,identity mapping

假设,hypothesis

基准,baseline
Expand Down
14 changes: 7 additions & 7 deletions chapter_convolutional-neural-networks/googlenet.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 含并行连结的网络(GoogLeNet)

在2014年的ImageNet竞赛中,一个名叫GoogLeNet的网络结构大放异彩 [1]。它虽然在名字上向LeNet致敬,但在网络结构上已经很难看到LeNet的影子。GoogLeNet吸收了NiN中网络嵌套网络的思想,并在此基础上做了很大改进。在随后的几年里,研究人员对GoogLeNet进行了数次改进,本节将介绍这个模型系列的第一个版本。
在2014年的ImageNet图像识别挑战赛中,一个名叫GoogLeNet的网络结构大放异彩 [1]。它虽然在名字上向LeNet致敬,但在网络结构上已经很难看到LeNet的影子。GoogLeNet吸收了NiN中网络嵌套网络的思想,并在此基础上做了很大改进。在随后的几年里,研究人员对GoogLeNet进行了数次改进,本节将介绍这个模型系列的第一个版本。


## Inception 块
Expand Down Expand Up @@ -49,15 +49,15 @@ class Inception(nn.Block):

## GoogLeNet模型

GoogLeNet跟VGG一样,在主体卷积部分中使用五个块,每个块之间使用步幅为2的$3\times 3$最大池化层来减小输出高宽。第一块使用一个64通道的$7\times 7$卷积层。
GoogLeNet跟VGG一样,在主体卷积部分中使用五个模块(block),每个模块之间使用步幅为2的$3\times 3$最大池化层来减小输出高宽。第一模块使用一个64通道的$7\times 7$卷积层。

```{.python .input n=2}
b1 = nn.Sequential()
b1.add(nn.Conv2D(64, kernel_size=7, strides=2, padding=3, activation='relu'),
nn.MaxPool2D(pool_size=3, strides=2, padding=1))
```

第二块使用两个卷积层:首先是64通道的$1\times 1$卷积层,然后是将通道增大3倍的$3\times 3$卷积层。它对应Inception块中的第二条线路。
第二模块使用两个卷积层:首先是64通道的$1\times 1$卷积层,然后是将通道增大3倍的$3\times 3$卷积层。它对应Inception块中的第二条线路。

```{.python .input n=3}
b2 = nn.Sequential()
Expand All @@ -66,7 +66,7 @@ b2.add(nn.Conv2D(64, kernel_size=1),
nn.MaxPool2D(pool_size=3, strides=2, padding=1))
```

第三块串联两个完整的Inception块。第一个Inception块的输出通道数为$64+128+32+32=256$,其中四条线路的输出通道数比例为$64:128:32:32=2:4:1:1$。其中第二、第三条线路先分别将输入通道数减小至$96/192=1/2$和$16/192=1/12$后,再接上第二层卷积层。第二个Inception块输出通道数增至$128+192+96+64=480$,每条线路的输出通道数之比为$128:192:96:64 = 4:6:3:2$。其中第二、第三条线路先分别将输入通道数减小至$128/256=1/2$和$32/256=1/8$。
第三模块串联两个完整的Inception块。第一个Inception块的输出通道数为$64+128+32+32=256$,其中四条线路的输出通道数比例为$64:128:32:32=2:4:1:1$。其中第二、第三条线路先分别将输入通道数减小至$96/192=1/2$和$16/192=1/12$后,再接上第二层卷积层。第二个Inception块输出通道数增至$128+192+96+64=480$,每条线路的输出通道数之比为$128:192:96:64 = 4:6:3:2$。其中第二、第三条线路先分别将输入通道数减小至$128/256=1/2$和$32/256=1/8$。

```{.python .input n=4}
b3 = nn.Sequential()
Expand All @@ -75,7 +75,7 @@ b3.add(Inception(64, (96, 128), (16, 32), 32),
nn.MaxPool2D(pool_size=3, strides=2, padding=1))
```

第四块更加复杂。它串联了五个Inception块,其输出通道数分别是$192+208+48+64=512$、$160+224+64+64=512$、$128+256+64+64=512$、$112+288+64+64=528$和$256+320+128+128=832$。这些线路的通道数分配和第三块中的类似:含$3\times 3$卷积层的第二条线路输出最多通道,其次是仅含$1\times 1$卷积层的第一条线路,之后是含$5\times 5$卷积层的第三条线路和含$3\times 3$最大池化层的第四条线路。其中第二、第三条线路都会先按比例减小通道数。这些比例在各个Inception块中都略有不同。
第四模块更加复杂。它串联了五个Inception块,其输出通道数分别是$192+208+48+64=512$、$160+224+64+64=512$、$128+256+64+64=512$、$112+288+64+64=528$和$256+320+128+128=832$。这些线路的通道数分配和第三模块中的类似:含$3\times 3$卷积层的第二条线路输出最多通道,其次是仅含$1\times 1$卷积层的第一条线路,之后是含$5\times 5$卷积层的第三条线路和含$3\times 3$最大池化层的第四条线路。其中第二、第三条线路都会先按比例减小通道数。这些比例在各个Inception块中都略有不同。

```{.python .input n=5}
b4 = nn.Sequential()
Expand All @@ -87,7 +87,7 @@ b4.add(Inception(192, (96, 208), (16, 48), 64),
nn.MaxPool2D(pool_size=3, strides=2, padding=1))
```

第五块有输出通道数为$256+320+128+128=832$和$384+384+128+128=1024$的两个Inception块。其中每条线路的通道数分配思路和第三、第四块中的一致,只是在具体数值上有所不同。需要注意的是,第五块的后面紧跟输出层,该块同NiN一样使用全局平均池化层来将每个通道的高和宽变成1。最后我们将输出变成二维数组后接上一个输出个数为标签类数的全连接层。
第五模块有输出通道数为$256+320+128+128=832$和$384+384+128+128=1024$的两个Inception块。其中每条线路的通道数分配思路和第三、第四模块中的一致,只是在具体数值上有所不同。需要注意的是,第五模块的后面紧跟输出层,该模块同NiN一样使用全局平均池化层来将每个通道的高和宽变成1。最后我们将输出变成二维数组后接上一个输出个数为标签类数的全连接层。

```{.python .input n=6}
b5 = nn.Sequential()
Expand All @@ -99,7 +99,7 @@ net = nn.Sequential()
net.add(b1, b2, b3, b4, b5, nn.Dense(10))
```

GoogLeNet模型的计算复杂,而且不如VGG那样便于修改通道数。本节里我们将输入高宽从224降到96来简化计算。下面演示各个块之间的输出的形状变化
GoogLeNet模型的计算复杂,而且不如VGG那样便于修改通道数。本节里我们将输入高宽从224降到96来简化计算。下面演示各个模块之间的输出的形状变化

```{.python .input n=7}
X = nd.random.uniform(shape=(1, 1, 96, 96))
Expand Down
43 changes: 20 additions & 23 deletions chapter_convolutional-neural-networks/resnet.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
# 残差网络(ResNet)

上一节介绍的批量归一化层对网络各层的中间输出做标准化,从而使训练时数值更加稳定。但对于深度模型来说,还有一个问题困扰着模型的训练。回忆[“正向传播、反向传播和计算图”](../chapter_deep-learning-basics/backprop.md)一节。在反向传播时,我们从输出层开始,朝着输入层方向逐层计算权重参数和中间变量的梯度。当我们将这些层串联在一起时,靠近输入层的权重梯度是该层与输出层之间每层中间变量梯度的乘积。假设这些梯度的绝对值小于1,靠近输入层的权重参数只得到很小的梯度,因此这些参数也很难被更新。

针对上述问题,ResNet增加了跨层的数据线路,从而允许中间变量的梯度快速向输入层传递,并更容易更新靠近输入层的模型参数 [1]。这一节我们将介绍ResNet的工作原理。
让我们先思考一个问题。假设神经网络模型使用批量归一化提供数值稳定性。对神经网络模型添加新的层,充分训练后的模型是否只可能更有效地降低训练误差?理论上,原模型解的空间只是新模型解的空间的子空间。也就是说,如果我们能将新添加的层训练成恒等映射(identity mapping)$f(x) = x$,新模型和原模型将同样有效。由于新模型可能得出更优的解来拟合训练数据集,添加层似乎有益而无害。然而在实践中,添加过多的层后训练误差不降反升。针对这一问题,何恺明等人提出了残差网络(ResNet) [1]。它在2015年的ImageNet图像识别挑战赛夺魁,并深刻影响了后来的深度神经网络的设计。


## 残差块

ResNet的基础块叫做残差块 (residual block) 。如图5.9所示,它将层A的输出在输入给层B的同时跨过B,并和B的输出相加作为下面层的输入。它可以看成是两个网络相加,一个网络只有层A,一个则有层A和B。这里层A在两个网络之间共享参数。在求梯度的时候,来自层B上层的梯度既可以通过层B也可以直接到达层A,从而让层A更容易获取足够大的梯度来进行模型更新。

![残差块(左)和它的分解(右)。](../img/resnet.svg)
让我们聚焦于神经网络局部。如图5.9所示,设输入为$\boldsymbol{x}$。假设图5.9中最上方ReLU的理想映射为$f(\boldsymbol{x})$。左图虚线框中部分需要直接拟合出该映射$f(\boldsymbol{x})$。而右图虚线框中部分需要拟合出残差(residual)映射$f(\boldsymbol{x})-\boldsymbol{x}$。残差映射在实际中往往更容易优化。以本节开头提到的恒等映射作为我们希望学出的理想映射$f(\boldsymbol{x})$,并以ReLU作为激活函数。我们只需将图5.9中右图最上方加权运算(例如仿射)的权重和偏差参数学成零,最上方ReLU的输出就会与输入$\boldsymbol{x}$恒等。图5.9右图也是ResNet的基础块,即残差块(residual block)。在残差块中,输入可通过跨层的数据线路更快地向前传播。

![设输入为$\boldsymbol{x}$。假设图中最上方ReLU的理想映射为$f(\boldsymbol{x})$。左图虚线框中部分需要直接拟合出该映射$f(\boldsymbol{x})$。而右图虚线框中部分需要拟合出残差映射$f(\boldsymbol{x})-\boldsymbol{x}$。](../img/residual-block.svg)

ResNet沿用了VGG全$3\times 3$卷积层设计。残差块里首先是两个有同样输出通道的$3\times 3$卷积层,每个卷积层后跟一个批量归一化层和ReLU激活层。然后我们将输入跳过这两个卷积层后直接加在最后的ReLU激活层前。这样的设计要求两个卷积层的输出与输入形状一样,从而可以相加。如果想改变输出的通道数,我们需要引入一个额外的$1\times 1$卷积层来将输入变换成需要的形状后再相加
ResNet沿用了VGG全$3\times 3$卷积层的设计。残差块里首先是两个有同样输出通道数的$3\times 3$卷积层。每个卷积层后接一个批量归一化层和ReLU激活函数。然后我们将输入跳过这两个卷积层后直接加在最后的ReLU激活函数前。这样的设计要求两个卷积层的输出与输入形状一样,从而可以相加。如果想改变通道数,我们需要引入一个额外的$1\times 1$卷积层来将输入变换成需要的形状后再做相加运算

残差块的实现如下。它可以设定输出通道数,是否使用额外的卷积层来修改输入通道数,以及卷积层的步幅大小
残差块的实现如下。它可以设定输出通道数、是否使用额外的卷积层来修改通道数以及卷积层的步幅

```{.python .input n=1}
import sys
Expand Down Expand Up @@ -47,7 +44,7 @@ class Residual(nn.Block):
return nd.relu(Y + X)
```

查看输入输出形状一致的情况:
下面我们查看输入输出形状一致的情况。

```{.python .input n=2}
blk = Residual(3)
Expand All @@ -56,7 +53,7 @@ X = nd.random.uniform(shape=(4, 3, 6, 6))
blk(X).shape
```

改变输出形状的同时减半输出高宽:
我们也可以在增加输出通道数的同时减半输出高和宽。

```{.python .input n=3}
blk = Residual(6, use_1x1conv=True, strides=2)
Expand All @@ -66,7 +63,7 @@ blk(X).shape

## ResNet模型

ResNet前面两层跟前面介绍的GoogLeNet一样,在输出通道为64、步幅为2的$7\times 7$卷积层后接步幅为2的$3\times 3$的最大池化层。不同一点在于ResNet的每个卷积层后面增加的批量归一化层
ResNet的前两层跟之前介绍的GoogLeNet一样:在输出通道数为64、步幅为2的$7\times 7$卷积层后接步幅为2的$3\times 3$的最大池化层。不同之处在于ResNet每个卷积层后增加的批量归一化层

```{.python .input}
net = nn.Sequential()
Expand All @@ -75,9 +72,9 @@ net.add(nn.Conv2D(64, kernel_size=7, strides=2, padding=3),
nn.MaxPool2D(pool_size=3, strides=2, padding=1))
```

GoogLeNet在后面接了四个由Inception块组成的模块。ResNet则是使用四个由残差块组成的模块,每个模块使用若干个同样输出通道的残差块。第一个模块的通道数同输入一致,同时因为之前已经使用了步幅为2的最大池化层,所以也不减小高宽。之后的每个模块在第一个残差块里将上一个模块的通道数翻倍,并减半高宽
GoogLeNet在后面接了四个由Inception块组成的模块。ResNet则使用四个由残差块组成的模块,每个模块使用若干个同样输出通道数的残差块。第一个模块的通道数同输入通道数一致。由于之前已经使用了步幅为2的最大池化层,所以无需减小高和宽。之后的每个模块在第一个残差块里将上一个模块的通道数翻倍,并减半高和宽

下面我们实现这个模块,注意我们对第一个模块块做了特别处理
下面我们实现这个模块,注意我们对第一个模块做了特别处理

```{.python .input n=4}
def resnet_block(num_channels, num_residuals, first_block=False):
Expand All @@ -99,17 +96,15 @@ net.add(resnet_block(64, 2, first_block=True),
resnet_block(512, 2))
```

最后与GoogLeNet一样我们加入全局平均池化层后接上全连接层输出
最后与GoogLeNet一样,加入全局平均池化层后接上全连接层输出

```{.python .input}
net.add(nn.GlobalAvgPool2D(), nn.Dense(10))
```

这里每个模块里有4个卷积层(不计算$1\times 1$卷积层),加上最开始的卷积层和最后的全连接层,一共有18层。这个模型也通常被称之为ResNet-18。通过配置不同的通道数和模块里的残差块数我们可以得到不同的ResNet模型。
这里每个模块里有4个卷积层(不计算$1\times 1$卷积层),加上最开始的卷积层和最后的全连接层,共计18层。这个模型也通常被称为ResNet-18。通过配置不同的通道数和模块里的残差块数我们可以得到不同的ResNet模型,例如更深的的ResNet-152。虽然ResNet的主体架构跟GoogLeNet的类似,但ResNet结构更加简单,修改也更加方便。这些因素都导致了ResNet迅速被广泛使用

注意,每个残差块里我们都将输入直接加在输出上,少数几个通过简单的$1\times 1$卷积层后相加。这样一来,即使层数很多,损失函数的梯度也能很快的传递到靠近输入的层那里。这使得即使是很深的ResNet(例如ResNet-152),在收敛速度上也同浅的ResNet(例如这里实现的ResNet-18)类似。同时虽然它的主体架构上跟GoogLeNet类似,但ResNet结构更加简单,修改也更加方便。这些因素都导致了ResNet迅速被广泛使用。

最后我们考察输入在ResNet不同模块之间的变化。
最后我们考察输入形状在ResNet不同模块之间的变化。

```{.python .input n=6}
X = nd.random.uniform(shape=(1, 1, 224, 224))
Expand All @@ -121,7 +116,7 @@ for layer in net:

## 获取数据并训练

使用跟GoogLeNet一样的超参数,但减半了学习率
下面我们在Fashion-MNIST上训练ResNet

```{.python .input}
lr, num_epochs, batch_size, ctx = 0.05, 5, 256, gb.try_gpu()
Expand All @@ -133,13 +128,15 @@ gb.train_ch5(net, train_iter, test_iter, batch_size, trainer, ctx, num_epochs)

## 小结

* 残差块通过将输入加在卷积层作用过的输出上来引入跨层通道。这使得即使非常深的网络也能很容易训练。
* 残差块通过跨层的数据通道从而能够训练出有效的深度神经网络。
* ResNet深刻影响了后来的深度神经网络的设计。


## 练习

- 参考ResNet论文的表1来实现不同版本的ResNet [1]
- 对于比较深的网络, ResNet论文中介绍了一个“bottleneck”架构来降低模型复杂度。尝试实现它 [1]
- 在ResNet的后续版本里,作者将残差块里的“卷积、批量归一化和激活”结构改成了“批量归一化、激活和卷积”,实现这个改进([2],图1)。
* 参考ResNet论文的表1来实现不同版本的ResNet [1]
* 对于比较深的网络, ResNet论文中介绍了一个“bottleneck”架构来降低模型复杂度。尝试实现它 [1]
* 在ResNet的后续版本里,作者将残差块里的“卷积、批量归一化和激活”结构改成了“批量归一化、激活和卷积”,实现这个改进([2],图1)。

## 扫码直达[讨论区](https://discuss.gluon.ai/t/topic/1663)

Expand Down
Loading

0 comments on commit 00c367d

Please sign in to comment.