5. ResNet
ResNet是由微软研究院的 Kaiming He, Xiangyu Zhang, Shaoqing Ren 和 Jian Sun 在 2015 年提出的深度神经网络架构。ResNet斩获当年ImageNet竞赛中分类任务第一名,目标检测第一名;COCO数据集中目标检测第一名,图像分割第一名。它的名字来源于残差网络(Residual Network),也就是 ResNet 架构中的神经网络块是建立在残差模块之上的。
ResNet 是目前依然非常流行的深度神经网络架构,在计算机视觉领域有着广泛的应用,并为之后的深度神经网络架构的提出和改进奠定了基础。截止目前为止,论文引用次数已经超过14万。
ResNet 是为了解决深度神经网络训练时网络退化问题而提出的,所谓的网络退化其实就是深层网络的效果反而比浅层网络更差。ResNet 的提出让我们可以训练出更深的网络,其深度突破了100层,甚至超过了1000层。
ResNet基本思想
ResNet 的基本思想是引入残差模块来解决深度神经网络训练时的网络退化问题。
残差模块的思想是将多层神经网络块看成一个整体,然后学习残差,即将输出与输入的差作为模型的预测目标。这样做的好处是可以使梯度流动更加稳定,从而使深度神经网络得以顺利训练。
假设将堆叠的几层神经元称为一个block,那么对于某个block,假设其拟合的函数是 F(x),如果期望潜在映射为 H(x),与其让 F(x) 直接拟合 H(x) ,不如去学习残差 H(x)−x,即 F(x)=H(x)−x,这样其前向路径就变成了 F(x)+x,即 H(x)=F(x)+x。其结构如下图所示。

ResNet结构
ResNet34的结构与VGG19的对比如下图所示:

其中实现实曲线表示残差连接,虚线表示包含升维。
不同深度的ResNet结构如下图所示:

上面的结构图对照上表就是三部分:
- 第一部分卷积层+最大池化层;
- 第二部分是4组不同数量和规格的残差模块;
- 第三部分平均池化层+全连接层。
ResNet代码实现
python
class BasicBlock(nn.Module):
expansion = 1
def __init__(self, inplanes, planes, stride=1, downsample=None):
super().__init__()
self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=3, stride=stride, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(planes)
self.relu = nn.ReLU(inplace=True)
self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(planes)
self.downsample = downsample
self.stride = stride
def forward(self, x):
identity = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = self.relu(out)
return out
python
class Bottleneck(nn.Module):
expansion = 4
def __init__(self, inplanes, planes, stride=1, downsample=None):
super().__init__()
self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=1, bias=False)
self.bn1 = nn.BatchNorm2d(planes)
self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=stride, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(planes)
self.conv3 = nn.Conv2d(planes, planes * 4, kernel_size=1, bias=False)
self.bn3 = nn.BatchNorm2d(planes * 4)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
self.stride = stride
def forward(self, x):
identity = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)
out = self.conv3(out)
out = self.bn3(out)
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = self.relu(out)
return out
然后组装ResNet的网络结构:
python
class ResNet(nn.Module):
def __init__(self, block, layers, num_classes=1000):
self.inplanes = 64
super().__init__()
self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False)
self.bn1 = nn.BatchNorm2d(64)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
self.layer1 = self._make_layer(block, 64, layers[0])
self.layer2 = self._make_layer(block, 128, layers[1], stride=2)
self.layer3 = self._make_layer(block, 256, layers[2], stride=2)
self.layer4 = self._make_layer(block, 512, layers[3], stride=2)
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(512 * block.expansion, num_classes)
# 生成网络结构的函数,根据传入的配置,拼接出对应的网络结构
def _make_layer(self, block, planes, blocks, stride=1):
downsample = None
if stride != 1 or self.inplanes != planes * block.expansion:
downsample = nn.Sequential(
nn.Conv2d(self.inplanes, planes * block.expansion, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(planes * block.expansion),
)
layers = []
layers.append(block(self.inplanes, planes, stride, downsample))
self.inplanes = planes * block.expansion
for i in range(1, blocks):
layers.append(block(self.inplanes, planes))
return nn.Sequential(*layers)
def forward(self, x):
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.maxpool(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.fc(x)
return x
- 针对网络退化问题,采用残差结构解决(隔层相连,弱化每层之间的强联系),利用残差结构让网络能够更深、收敛速度更快、优化更容易,同时参数相对之前的模型更少、复杂度更低。
- 对于很深的网络,ResNet使用了更高效的"bottleneck"结构极大程度上降低了参数计算量。
- ResNet深度突破了100层,甚至超过了1000层,对后来的深层神经⽹络设计产⽣了深远影响。
6. DenseNet
DenseNet(Densely connected convolutional networks)是由Gao Huang等人在2017年提出的一种深度卷积神经网络,它在ResNet的基础上进行了改进,其中使用了联结不同层的稠密块(dense blocks)来构建模型。DenseNet的一个主要优势在于,它可以有效地减少模型的参数数量,同时保持或者提高模型的性能。这是通过在每个层之间使用"短接(shortcut connections)"来实现的,这些短接使得每个层都可以直接访问之前所有层的特征。
DenseNet的另一大特色是通过特征在channel上的连接来实现特征重用(feature reuse)。这些特点让DenseNet在参数和计算成本更少的情形下实现比ResNet更优的性能,DenseNet也因此斩获CVPR 2017的最佳论文奖。
DenseNet基本思想
DenseNet核心思想在于建立了不同层之间的连接关系,充分利用了feature,进一步减轻了梯度消失问题,加深网络不是问题,而且训练效果非常好。另外通过利用bottleneck layer,Translation layer以及较小的growth rate使得网络变窄,参数减少,有效抑制了过拟合,同时计算量也减少了,在和ResNet的对比中优势还是比较明显。
相比ResNet,DenseNet提出了一个更激进的密集连接机制。即互相连接所有的层,具体来说就是每个层都会接受其前面所有层作为其额外的输入。其dense block结构如下图所示。

将一个个dense block结构拼接之后,DenseNet的密集连接机制如下图所示:

相比之下,ResNet是每个层与前面的某层短路连接在一起,连接方式是通过元素级相加。而在DenseNet中,每个层都会与前面所有层在channel维度上连接(concat)在一起,并作为下一层的输入。也就是说,对于一个 L 层的网络,DenseNet共包含 L(L+1)2 个连接,而不是传统意义上的L层连接。因为密集连接的特性,称其为DenseNet。
DenseNet结构

不同深度的DenseNet结构如下图所示:

上面的结构图对照上表就是几部分:
- 第一部分卷积层+最大池化层;
- 第二部分是4组不同数量和规格的Dense Block;
- 第三部分是3个Transition Layer,主要作用就是降维和调整特征图size;
- 第四部分平均池化层+全连接层。
DenseNet代码实现
首先是DenseBlock中的内部结构,这里是BN+ReLU+Conv1x1+BN+ReLU+Conv3x3的结构:
python
class _DenseLayer(nn.Module):
def __init__(self, num_input_features, growth_rate, bn_size):
super().__init__()
self.conv1 = nn.Sequential(
nn.BatchNorm2d(num_input_features),
nn.ReLU(inplace=True),
nn.Conv2d(num_input_features, bn_size * growth_rate, kernel_size=1, stride=1, bias=False)
)
self.conv2 = nn.Sequential(
nn.BatchNorm2d(bn_size * growth_rate),
nn.ReLU(inplace=True),
nn.Conv2d(bn_size * growth_rate, growth_rate, kernel_size=3, stride=1, padding=1, bias=False)
)
def forward(self, x):
out = self.conv1(x)
out = self.conv2(out)
return torch.cat([x, out], 1)
然后实现DenseBlock模块,内部是密集连接方式(输入特征数线性增长):
python
class _DenseBlock(nn.Module):
def __init__(self, num_layers, num_input_features, bn_size, growth_rate):
super().__init__()
layers = []
for i in range(num_layers):
layer = _DenseLayer(num_input_features + i * growth_rate, growth_rate, bn_size)
layers.append(layer)
self.block = nn.Sequential(*layers)
def forward(self, x):
return self.block(x)
接下来实现Transition层,它是BN+ReLU+Conv1x1+Pool的结构:
python
class _Transition(nn.Module):
def __init__(self, num_input_features, num_output_features):
super().__init__()
self.trans = nn.Sequential(
nn.BatchNorm2d(num_input_features),
nn.ReLU(inplace=True),
nn.Conv2d(num_input_features, num_output_features, kernel_size=1, stride=1, bias=False),
nn.AvgPool2d(kernel_size=2, stride=2)
)
def forward(self, x):
return self.trans(x)
然后组装DenseNet的网络结构:
python
class DenseNet(nn.Module):
def __init__(self, block_config, growth_rate=32, num_init_features=64, bn_size=4, num_classes=1000):
super().__init__()
# First convolution
self.features = nn.Sequential(
nn.Conv2d(3, num_init_features, kernel_size=7, stride=2, padding=3, bias=False),
nn.BatchNorm2d(num_init_features),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
)
# Each denseblock
num_features = num_init_features
layers = []
for i, num_layers in enumerate(block_config):
block = _DenseBlock(num_layers=num_layers, num_input_features=num_features, bn_size=bn_size, growth_rate=growth_rate)
layers.append(block)
num_features = num_features + num_layers * growth_rate
if i != len(block_config) - 1:
trans = _Transition(num_input_features=num_features, num_output_features=num_features // 2)
layers.append(trans)
num_features = num_features // 2
layers.append(nn.BatchNorm2d(num_features))
self.denseblock = nn.Sequential(*layers)
# Linear layer
self.classifier = nn.Linear(num_features, num_classes)
def forward(self, x):
features = self.features(x)
features = self.denseblock(features)
out = F.relu(features, inplace=True)
out = F.avg_pool2d(out, kernel_size=7, stride=1).view(features.size(0), -1)
out = self.classifier(out)
return out
- DenseNet 和 ResNet 非常的相似,简单来看,可以理解为 ResNet 的特征图是按位置相加,而DenseNet的特征图是进行concat拼接。但实际上基于密集连接的结构,能够形成更复杂的特征模式。
- DenseNet 可以有效地减少模型的参数数量,保持或者提高模型的性能,同时通过特征在channel上的连接来实现特征重用。
- DenseNet 结构可以有效环节梯度消失和模型退化的问题。