一、为什么选择残差网络
在VGG中,卷积网络达到了19层,在GoogLeNet中,网络史无前例的达到了22层。那么,网络的精度会随着网络的层数增多而增多吗?在深度学习中,网络层数增多一般会伴着下面几个问题
- 计算资源的消耗(用GPU集群去怼)
- 模型容易过拟合(扩大数据集、Droupout、批量归一化、正则化、初始化参数调整等等方法)
- 梯度消失/梯度爆炸问题的产生(批量归一化)
随着网络层数的增加,网络发生了退化(degradation)的现象:随着网络层数的增多,训练集loss逐渐下降,然后趋于饱和,当你再增加网络深度的话,训练集loss反而会增大。注意这并不是过拟合,因为在过拟合中训练loss是一直减小的。
当网络退化时,浅层网络能够达到比深层网络更好的训练效果,这时如果我们把低层的特征传到高层,那么效果应该至少不比浅层的网络效果差,或者说如果一个VGG-100网络在第98层使用的是和VGG-16第14层一模一样的特征,那么VGG-100的效果应该会和VGG-16的效果相同。所以,我们可以在VGG-100的98层和14层之间添加一条直接映射(Identity Mapping)来达到此效果。
从信息论的角度讲,由于DPI(数据处理不等式)的存在,在前向传输的过程中,随着层数的加深,Feature Map包含的图像信息会逐层减少,而ResNet的直接映射的加入,保证了 l + 1 l+1 l+1层的网络一定比 l l l层包含更多的图像信息。
基于这种使用直接映射来连接网络不同层直接的思想,残差网络应运而生。
二、残差网络(ResNet)
随着我们设计越来越深的网络,深刻理解“新添加的层如何提升神经网络的性能”变得至关重要。更重要的是设计网络的能力,在这种网络中,添加层会使网络更具表现力,为了取得质的突破,我们需要一些数学基础知识。
2.1 函数类
首先,假设有一类特定的神经网络结构 F \mathcal{F} F,它包括学习速率和其他超参数设置。对于所有 f ∈ F f \in \mathcal{F} f∈F,存在一些参数集(例如权重和偏置),这些参数可以通过在合适的数据集上进行训练而获得。
现在假设 f ∗ f^* f∗ 是我们真正想要找到的函数,如果是 f ∗ ∈ F f^* \in \mathcal{F} f∗∈F,那我们可以轻而易举的训练得到它,但通常我们不会那么幸运。相反,我们将尝试找到一个函数 f F ∗ f^*_\mathcal{F} fF∗,这是我们在 F \mathcal{F} F 中的最佳选择。例如,给定一个具有 X \mathbf{X} X 特性和 y \mathbf{y} y 标签的数据集,我们可以尝试通过解决以下优化问题来找到它:
f F ∗ : = a r g m i n f L ( X , y , f ) subject to f ∈ F . (2.1) f^*_\mathcal{F} := \mathop{\mathrm{argmin}}_f L(\mathbf{X}, \mathbf{y}, f) \text{ subject to } f \in \mathcal{F}. \tag{2.1} fF∗:=argminfL(X,y,f) subject to f∈F.(2.1)
那么,怎样得到更近似真正
f
∗
f^*
f∗ 的函数呢?
唯一合理的可能性是,我们需要设计一个更强大的结构
F
′
\mathcal{F}'
F′。换句话说,我们预计
f
F
′
∗
f^*_{\mathcal{F}'}
fF′∗ 比
f
F
∗
f^*_{\mathcal{F}}
fF∗ “更近似”。然而,如果
F
⊈
F
′
\mathcal{F} \not\subseteq \mathcal{F}'
F⊆F′,则无法保证新的体系“更近似”。
事实上,
f
F
′
∗
f^*_{\mathcal{F}'}
fF′∗ 可能更糟:
如下图所示,对于非嵌套函数(non-nested function)类,较复杂的函数类并不总是向“真”函数
f
∗
f^*
f∗ 靠拢(复杂度由
F
1
\mathcal{F}_1
F1 向
F
6
\mathcal{F}_6
F6 递增)。在下图的左边,虽然
F
3
\mathcal{F}_3
F3 比
F
1
\mathcal{F}_1
F1 更接近
f
∗
f^*
f∗,但
F
6
\mathcal{F}_6
F6 却离的更远了。相反对于图右侧的嵌套函数(nested function)类
F
1
⊆
…
⊆
F
6
\mathcal{F}_1 \subseteq \ldots \subseteq \mathcal{F}_6
F1⊆…⊆F6,我们可以避免上述问题。

因此,只有当较复杂的函数类包含较小的函数类时,我们才能确保提高它们的性能。对于深度神经网络,如果我们能将新添加的层训练成 恒等映射(identity function) f ( x ) = x f(\mathbf{x}) = \mathbf{x} f(x)=x ,新模型和原模型将同样有效。同时,由于新模型可能得出更优的解来拟合训练数据集,因此添加层似乎更容易降低训练误差。
针对这一问题,何恺明等人提出了残差网络(ResNet)He.Zhang.Ren.ea.2016
。它在2015年的ImageNet图像识别挑战赛夺魁,并深刻影响了后来的深度神经网络的设计。残差网络的核心思想是:每个附加层都应该更容易地包含原始函数作为其元素之一。于是,残差块 (residual blocks) 便诞生了,这个设计对如何建立深层神经网络产生了深远的影响。凭借它,ResNet 赢得了 2015 年 ImageNet 大规模视觉识别挑战赛。
2.2 残差块
让我们聚焦于神经网络局部:如下图所示,假设我们的原始输入为
x
x
x ,而希望学出的理想映射为
f
(
x
)
f(\mathbf{x})
f(x) (作为 residual_block
上方激活函数的输入)。下左图虚线框中的部分需要直接拟合出该映射
f
(
x
)
f(\mathbf{x})
f(x) ,而右图虚线框中的部分则需要拟合出残差映射
f
(
x
)
−
x
f(\mathbf{x}) - \mathbf{x}
f(x)−x 。
残差映射在现实中往往更容易优化。以上一节中提到的恒等映射作为我们希望学出的理想映射
f
(
x
)
f(\mathbf{x})
f(x) ,我们只需将 residual_block
中右图虚线框内上方的加权运算(如仿射)的权重和偏置参数设成 0,那么
f
(
x
)
f(\mathbf{x})
f(x) 即为恒等映射。
实际中,当理想映射
f
(
x
)
f(\mathbf{x})
f(x) 极接近于恒等映射时,残差映射也易于捕捉恒等映射的细微波动。residual_block
右图是 ResNet 的基础结构-- 残差块(residual block)。在残差块中,输入可通过跨层数据线路更快地向前传播。
这里可能会让人有一些混淆,梳理一下:
- 网络的每一层我们看作是: y = H ( x ) y = H(x) y=H(x),也就是这里的输出 f ( x ) f(x) f(x)
- 残差网络的一个残差块可以表示为:
H
(
x
)
=
F
(
x
)
+
x
H(x) = F(x) + x
H(x)=F(x)+x,这里的
F
(
x
)
F(x)
F(x)等价于
residual_block
右图虚线框中的部分 - 那么就可以得到: F ( x ) = H ( x ) − x F(x) = H(x) -x F(x)=H(x)−x,也就是 f ( x ) − x f(x) - x f(x)−x

残差块的结构:
- ResNet 沿用了 VGG 完整的 3 × 3 3\times 3 3×3 卷积层设计。
- 残差块里首先有 2 个有相同输出通道数的 3 × 3 3\times 3 3×3 卷积层。
- 每个卷积层后接一个批量归一化层和 ReLU 激活函数。
- 然后我们通过跨层数据通路,跳过这 2 个卷积运算,将输入直接加在最后的 ReLU 激活函数前。
- 这样的设计要求 2 个卷积层的输出与输入形状一样,从而可以相加。
- 如果想改变通道数,就需要引入一个额外的 1 × 1 1\times 1 1×1 卷积层来将输入变换成需要的形状后再做相加运算。
2.3 残差网络的数学原理
参差网络的通用表示方式是:
y
l
=
h
(
x
l
)
+
F
(
x
l
,
W
l
)
(2.2)
y_l = h(x_l)+ F(x_l,W_l) \tag{2.2}
yl=h(xl)+F(xl,Wl)(2.2)
x
l
+
1
=
f
(
y
l
)
(2.3)
x_{l+1} = f(y_l) \tag{2.3}
xl+1=f(yl)(2.3)
现在我们先不考虑升维或者降维的情况,那么假设公式2.2和2.3中
- h ( . ) h(.) h(.)是直接映射
- f ( . ) f(.) f(.)是激活函数,一般使用ReLU。
那么残差块儿可表示为:
x
l
+
1
=
x
l
+
F
(
x
l
,
W
l
)
(2.4)
x_{l+1} = x_l + F(x_l,W_l) \tag{2.4}
xl+1=xl+F(xl,Wl)(2.4)
对于一个更深的层L,其与l层的关系可以表示为:
x
L
=
x
l
+
∑
i
=
l
L
−
1
F
(
x
i
,
W
i
)
(2.5)
x_L = x_l + \sum_{i=l}^{L-1}F(x_i,W_i) \tag{2.5}
xL=xl+i=l∑L−1F(xi,Wi)(2.5)
公式2.5 反映残差网络的两个特性:
- L层可以用任意一个比它浅的l层网络和他们之间的残差部分之和进行表示
- 公式中L是各个残差块特征的单位累和,而MLP是特征矩阵累积
根据向后传播中使用的导数的链式法则,损失函数
ϵ
\epsilon
ϵ关于
x
l
x_l
xl的梯度可以表示为:
∂
ϵ
∂
x
l
=
∂
ϵ
∂
x
L
∂
x
L
∂
x
l
=
∂
ϵ
∂
x
L
(
1
+
∂
∂
x
l
∑
i
=
l
L
−
1
F
(
x
i
,
W
i
)
)
=
∂
ϵ
∂
x
L
+
∂
ϵ
∂
x
L
∂
∂
x
l
∑
i
=
l
L
−
1
F
(
x
i
,
W
i
)
(2.6)
\begin{aligned} \frac{\partial{\epsilon}}{\partial{x_l}} &= \frac{\partial{\epsilon}}{\partial{x_L}}\frac{\partial{x_L}}{\partial{x_l}} \\ &= \frac{\partial{\epsilon}}{\partial{x_L}}(1+\frac{\partial{}}{\partial{x_l}}\sum_{i=l}^{L-1}F(x_i,W_i))\\ &= \frac{\partial{\epsilon}}{\partial{x_L}}+ \frac{\partial{\epsilon}}{\partial{x_L}}\frac{\partial{}}{\partial{x_l}}\sum_{i=l}^{L-1}F(x_i,W_i)\\ \end{aligned} \tag{2.6}
∂xl∂ϵ=∂xL∂ϵ∂xl∂xL=∂xL∂ϵ(1+∂xl∂i=l∑L−1F(xi,Wi))=∂xL∂ϵ+∂xL∂ϵ∂xl∂i=l∑L−1F(xi,Wi)(2.6)
公式2.6 中反映了:
- 在整个训练过程中, ∂ x l ∑ i = l L − 1 F ( x i , W i ) {\partial{x_l}}\sum_{i=l}^{L-1}F(x_i,W_i) ∂xl∑i=lL−1F(xi,Wi)不可能一直为-1,所以残差网络中不会出现梯度消失的问题
- ∂ ϵ ∂ x l \frac{\partial{\epsilon}}{\partial{x_l}} ∂xl∂ϵ表示L层的梯度可以直接传递给任何一个比其浅的l层
2.4 直接映射是最好选择?
1.3 中我们假设了
- h ( . ) h(.) h(.)是直接映射
- f ( . ) f(.) f(.)是激活函数,一般使用ReLU。
对于假设1,采用反证法:假设
h
(
x
l
)
=
λ
l
x
l
h(x_l) = \lambda_{l}x_l
h(xl)=λlxl,那么这个时候残差块可以表示为:
x
l
+
1
=
λ
l
x
l
+
F
(
x
l
,
W
l
)
(2.7)
x_{l+1} = \lambda_lx_l+F(x_l,W_l) \tag{2.7}
xl+1=λlxl+F(xl,Wl)(2.7)
对于更深的层:
x
L
=
(
∏
i
=
l
L
−
1
λ
i
)
x
l
+
∑
i
=
l
L
−
1
F
(
x
i
,
W
i
)
(2.8)
x_L = (\prod_{i=l}^{L-1}\lambda_i)x_l + \sum_{i=l}^{L-1}F(x_i,W_i) \tag{2.8}
xL=(i=l∏L−1λi)xl+i=l∑L−1F(xi,Wi)(2.8)
为了简化问题,只考虑公式左半部分
x
L
=
(
∏
i
=
l
L
−
1
λ
i
)
x
l
x_L = (\prod_{i=l}^{L-1}\lambda_i)x_l
xL=(∏i=lL−1λi)xl,损失函数
ϵ
\epsilon
ϵ关于
x
l
x_l
xl的梯度可以表示为:
∂
ϵ
∂
x
l
=
∂
ϵ
∂
x
L
(
(
∏
i
=
l
L
−
1
λ
i
)
+
∂
∂
x
l
F
(
x
i
,
W
i
)
)
(2.9)
\frac{\partial{\epsilon}}{\partial{x_l}} = \frac{\partial{\epsilon}}{\partial{x_L}}((\prod_{i=l}^{L-1}\lambda_i)+\frac{\partial{}}{\partial{x_l}}F(xi,Wi)) \tag{2.9}
∂xl∂ϵ=∂xL∂ϵ((i=l∏L−1λi)+∂xl∂F(xi,Wi))(2.9)
这个公式反映了:
- 当 λ > 1 \lambda > 1 λ>1,很有可能发生梯度爆炸(因为是连乘)
- 当 λ < 1 \lambda < 1 λ<1, 多层的影响下,梯度变为0,梯度消失,阻碍残差网络信息的反向传递,影响残差网络的训练
这里参考详解残差网络
2.5 从梯度下降上看为什么残差网络为什么可以保证模型不变差
假设一个10层网络的输出为
y
=
f
(
x
)
y = f(x)
y=f(x),关于底层的一个参数w的梯度为(不考虑loss,直接用输出表示了)
y
′
=
∂
y
∂
w
(2.10)
y' = \frac{\partial{y}}{\partial{w}} \tag{2.10}
y′=∂w∂y(2.10)
w的更新为:
w
=
w
−
D
∂
y
∂
w
(2.11)
w = w - D\frac{\partial{y}}{\partial{w}} \tag{2.11}
w=w−D∂w∂y(2.11)
在其上面再添加10层得到的输出为
y
^
=
g
(
y
)
=
g
(
f
(
x
)
)
(2.12)
\hat{y} = g(y) = g(f(x)) \tag{2.12}
y^=g(y)=g(f(x))(2.12)
关于同样一个参数w的梯度为:
y
^
′
=
g
(
f
(
x
)
)
′
=
∂
y
^
∂
y
∂
y
∂
w
(2.13)
\hat{y} ' = g(f(x))' = \frac{\partial{\hat{y}}}{\partial{y}} \frac{\partial{y}}{\partial{w}} \tag{2.13}
y^′=g(f(x))′=∂y∂y^∂w∂y(2.13)
问题就来了: ∂ y ^ ∂ y \frac{\partial{\hat{y}}}{\partial{y}} ∂y∂y^ 相当于是顶部层的一个结果和真实结果的一个偏差,如果这个部分特别小的话,梯度就会特别小,导致没有办法更新(特别是顶部如果是全连接层这种可以极大地提取特征的层影响非常大)
看看ResNet如何解决的:
ResNet其实是将上一层网络的输出和该层网络的输出组合起来作为下一层网络的输入:
y
r
e
s
i
d
u
a
l
=
f
(
x
)
+
g
(
f
(
x
)
)
(2.14)
y_{residual} = f(x) + g(f(x)) \tag{2.14}
yresidual=f(x)+g(f(x))(2.14)
那么它的梯度就是:
y
r
e
s
i
d
u
a
l
′
=
∂
y
∂
w
+
∂
y
^
∂
y
∂
y
∂
w
(2.15)
y_{residual}' = \frac{\partial{y}}{\partial{w}} + \frac{\partial{\hat{y}}}{\partial{y}} \frac{\partial{y}}{\partial{w}} \tag{2.15}
yresidual′=∂w∂y+∂y∂y^∂w∂y(2.15)
华点出来:即使后面部分是一个非常小的数,$f(x)'$仍然能保证这梯度不是特别小
三、动手实现残差网络
3.1 定义残差块

两种类型的网络:
- 一种是在
use_1x1conv=False
、应用 ReLU 非线性函数之前,将输入添加到输出。 - 另一种是在
use_1x1conv=True
,添加通过 1 × 1 1 \times 1 1×1 卷积调整通道和分辨率。
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l
class Residual(nn.Module):
def __init__(self,input_channels,num_channels,use_1x1conv=False,strides=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)
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)
self.relu = nn.ReLU(inplace=True)
def forward(self,X):
Y = self.relu(self.bn1(self.conv1(X)))
Y = self.bn2(self.conv2(Y))
if self.conv3:
X = self.conv3(X)
Y += X
return self.relu(Y)
# 验证块
blk = Residual(3, 3)
X = torch.rand(4, 3, 6, 6)
Y = blk(X)
Y.shape
torch.Size([4, 3, 6, 6])
# 也可以在增加通道数的同时,高宽减半
blk = Residual(3, 6, use_1x1conv=True, strides=2)
blk(X).shape
torch.Size([4, 6, 3, 3])
3.2 构建ResNet模型

(1)ResNet 的前两层跟 GoogLeNet 中的一样:
在输出通道数为 64、步幅为 2 的
7
×
7
7 \times 7
7×7 卷积层后,接步幅为 2 的
3
×
3
3 \times 3
3×3 的最大池化层。
不同之处在于 ResNet 每个卷积层后增加了批量归一化层。
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))
(2)使用ResNet block构建完整的ResNet
GoogLeNet 在后面接了 4 个由Inception块组成的模块。ResNet 则使用 4 个由残差块组成的模块,每个模块使用若干个同样输出通道数的残差块。
- 第一个模块的通道数同输入通道数一致。由于之前已经使用了步幅为 2 的最大池化层,所以无须减小高和宽。
- 之后的每个模块在第一个残差块里将上一个模块的通道数翻倍,并将高和宽减半。
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))
(3)在ResNet加入平均池化层和全连接层输出
net = nn.Sequential(b1, b2, b3, b4, b5, nn.AdaptiveAvgPool2d((1, 1)),
nn.Flatten(), nn.Linear(512, 10))
(4)看一看网络输出的大小
X = torch.rand(size=(1, 1, 224, 224))
for layer in net:
X = layer(X)
print(layer.__class__.__name__, 'output shape:\t', X.shape)
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])
3.3 训练模型
# 读取数据
from torchvision import transforms
import torchvision
from torch.utils import data
batch_size = 256
def get_dataloader_workers():
"""使用四个进程读取数据"""
return 4
def load_data_fashion_mnist(batch_size,resize=None):
"""下载Fashion-MNIST数据集,并将其保存至内存中"""
trans = [transforms.ToTensor()]
if resize:
trans.insert(0,transforms.Resize(resize)) # transforms.Resize将图片最小的一条边缩放到指定大小,另一边缩放对应比例
trans = transforms.Compose(trans) # compose用于串联多个操作
mnist_train = torchvision.datasets.FashionMNIST(root="./data",
train=True,
transform=trans,
download=True)
mnist_test = torchvision.datasets.FashionMNIST(root="./data",
train=False,
transform=trans,
download=True)
return (data.DataLoader(mnist_train,batch_size,shuffle=True,
num_workers=get_dataloader_workers()),
data.DataLoader(mnist_test,batch_size,shuffle=True,
num_workers = get_dataloader_workers()))
def evaluate_accuracy_gpu(net, data_iter, device=None): #@save
"""使用GPU计算模型在数据集上的精度。"""
if isinstance(net, torch.nn.Module):
net.eval() # 设置为评估模式
if not device:
device = next(iter(net.parameters())).device
# 正确预测的数量,总预测的数量
metric = d2l.Accumulator(2)
for X, y in data_iter:
if isinstance(X, list):
# BERT微调所需的(之后将介绍)
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]
#@save
def train(net, train_iter, test_iter, num_epochs, lr, device):
"""用GPU训练模型"""
def init_weights(m):
if type(m) == nn.Linear or type(m) == nn.Conv2d:
nn.init.xavier_uniform_(m.weight)
net.apply(init_weights)
print('training on', device)
net.to(device) # 将网络挪到gpu上
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, batch_size = 0.05, 10, 256
train_iter, test_iter = load_data_fashion_mnist(batch_size, resize=96)
train(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
loss 0.019, train acc 0.994, test acc 0.882
854.6 examples/sec on cuda:0

四、总结及问题
4.1 总结
- 学习嵌套函数(nested function)是训练神经网络的理想情况。在深层神经网络中,学习另一层作为恒等映射(identity function)较容易(尽管这是一个极端情况)。
- 残差映射可以更容易地学习同一函数,例如将权重层中的参数近似为零。
- 利用残差块(residual blocks)可以训练出一个有效的深层神经网络:输入可以通过层间的残余连接更快地向前传播。
- 残差网络(ResNet)对随后的深层神经网络设计产生了深远影响,无论是卷积类网络还是全连接类网络。
4.2 问题
- Inception块与残差块之间的主要区别是什么?在删除了Inception块中的一些路径之后,它们是如何相互关联的?
- 参考 ResNet 论文
He.Zhang.Ren.ea.2016
中的表 1,以实现不同的变体。
参考文章详解残差网络
- 对于更深层次的网络,ResNet 引入了“bottleneck”架构来降低模型复杂性。请你试着去实现它。
之后再实现
- 在 ResNet 的后续版本中,作者将“卷积层、批量归一化层和激活层”结构更改为“批量归一化层、激活层和卷积层”结构。请你做这个改进。详见
He.Zhang.Ren.ea.2016*1
中的图 1。
调整一下结构即可,效果还不错
- 为什么即使函数类是嵌套的,我们仍然要限制增加函数的复杂性呢?
我认为第一是因为复杂的函数会增加拟合的计算要求,第二是增加了过拟合的风险,第三是复杂的函数对网络结构底层训练的要求太大,实现起来效果会很差