1、为什么要跳跃连接?
深度神经网络的美妙之处在于它们可以比浅层神经网络更有效地学习复杂的功能。在训练深度神经网络时,模型的性能随着架构深度的增加而下降。这被称为退化问题。
可能的原因之一是过度拟合。随着深度的增加,模型往往会过度拟合,但这里的情况并非如此。从下图可以看出,56 层的深层网络比 20 层的浅层网络具有更多的训练误差。较深的模型表现不如浅模型好。 显然,过拟合不是这里的问题。
但实验证明,与浅层网络相比,深层网络会产生较高的训练误差。这表明更深层无法学习甚至恒等映射。主要原因之一是权重的随机初始化,均值在零、L1 和 L2 正则化附近。结果,模型中的权重总是在零左右,因此更深的层也无法学习恒等映射。
2、什么是跳跃连接?
跳跃连接,会跳跃神经网络中的某些层,并将一层的输出作为下一层的输入。引入跳跃连接是为了解决不同架构中的不同问题。在 ResNets 的情况下,跳跃连接解决了我们之前解决的退化问题,而在 DenseNets 的情况下,它确保了特征的可重用性。我们将在以下部分详细讨论它们。
正如你在此处看到的,具有跳跃连接的神经网络的损失表面更平滑,因此比没有任何跳跃连接的网络收敛速度更快。让我们在下一节中看到跳跃连接的变体。我们将看到不同架构中跳跃连接的变体。跳跃连接可以在神经网络中以两种基本方式使用:加法和串联。
2.1、残差网络(ResNets)
残差网络是由 He 等人提出的。2015年解决图像分类问题。在 ResNets 中,来自初始层的信息通过矩阵加法传递到更深层。此操作没有任何附加参数,因为前一层的输出被添加到前面的层。具有跳跃连接的单个残差块如下所示:
由于 ResNet 的更深层表示,因为来自该网络的预训练权重可用于解决多个任务。它不仅限于图像分类,还可以解决图像分割、关键点检测和对象检测方面的广泛问题。因此,ResNet 是深度学习社区中最具影响力的架构之一。
2.2、DenseNets
ResNets 和 DenseNets 之间的主要区别在于 DenseNets 将层的输出特征图与下一层连接而不是求和。
串联背后的想法是在更深的层中使用从早期层学习的特征。这个概念被称为特征可重用性。因此,DenseNets 可以用比传统 CNN 更少的参数来学习映射,因为不需要学习冗余映射。
2.3、U-Net:用于生物医学图像分割的卷积网络
跳跃连接的使用也影响了生物医学领域。U-Net是由 Ronneberger 等人提出的。用于生物医学图像分割。它有一个编码器-解码器部分,包括 Skip Connections。
编码器部分中的层与解码器部分中的层进行跳跃连接和级联(在上图中以灰线形式提及)。这使得 U-Nets 使用在编码器部分学习的细粒度细节在解码器部分构建图像。这些类型的连接是长跳跃连接,而我们在 ResNets 中看到的是短跳跃连接。
3、代码实现
我们将从头开始使用 Skip Connections 构建 ResNets 和 DesNets。
3.1、ResNet – 残差块
# import required libraries
import torch
from torch import nn
import torch.nn.functional as F
import torchvision
# basic resdidual block of ResNet
# This is generic in the sense, it could be used for downsampling of features.
class ResidualBlock(nn.Module):
def __init__(self, in_channels, out_channels, stride=[1, 1], downsample=None):
"""
A basic residual block of ResNet
Parameters
----------
in_channels: Number of channels that the input have
out_channels: Number of channels that the output have
stride: strides in convolutional layers
downsample: A callable to be applied before addition of residual mapping
"""
super(ResidualBlock, self).__init__()
self.conv1 = nn.Conv2d(
in_channels, out_channels, kernel_size=3, stride=stride[0],
padding=1, bias=False
)
self.conv2 = nn.Conv2d(
out_channels, out_channels, kernel_size=3, stride=stride[1],
padding=1, bias=False
)
self.bn = nn.BatchNorm2d(out_channels)
self.downsample = downsample
def forward(self, x):
residual = x
# applying a downsample function before adding it to the output
if (self.downsample is not None):
residual = downsample(residual)
out = F.relu(self.bn(self.conv1(x)))
out = self.bn(self.conv2(out))
# note that adding residual before activation
out = out + residual
out = F.relu(out)
return out
# downsample using 1 * 1 convolution
downsample = nn.Sequential(
nn.Conv2d(64, 128, kernel_size=1, stride=2, bias=False),
nn.BatchNorm2d(128)
)
# First five layers of ResNet34
resnet_blocks = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False),
nn.MaxPool2d(kernel_size=2, stride=2),
ResidualBlock(64, 64),
ResidualBlock(64, 64),
ResidualBlock(64, 128, stride=[2, 1], downsample=downsample)
)
# checking the shape
inputs = torch.rand(1, 3, 100, 100) # single 100 * 100 color image
outputs = resnet_blocks(inputs)
print(outputs.shape) # shape would be (1, 128, 13, 13)
3.2、DenseNet – 残差块
# import required libraries
import torch
from torch import nn
import torch.nn.functional as F
import torchvision
class Dense_Layer(nn.Module):
def __init__(self, in_channels, growthrate, bn_size):
super(Dense_Layer, self).__init__()
self.bn1 = nn.BatchNorm2d(in_channels)
self.conv1 = nn.Conv2d(
in_channels, bn_size * growthrate, kernel_size=1, bias=False
)
self.bn2 = nn.BatchNorm2d(bn_size * growthrate)
self.conv2 = nn.Conv2d(
bn_size * growthrate, growthrate, kernel_size=3, padding=1, bias=False
)
def forward(self, prev_features):
out1 = torch.cat(prev_features, dim=1)
out1 = self.conv1(F.relu(self.bn1(out1))) # [1,128,24,24]
out2 = self.conv2(F.relu(self.bn2(out1))) # [1,32,24,24]
return out2
class Dense_Block(nn.ModuleDict):
def __init__(self, n_layers, in_channels, growthrate, bn_size):
"""
A Dense block consists of `n_layers` of `Dense_Layer`
Parameters
----------
n_layers: Number of dense layers to be stacked
in_channels: Number of input channels for first layer in the block
growthrate: Growth rate (k) as mentioned in DenseNet paper
bn_size: Multiplicative factor for # of bottleneck layers
"""
super(Dense_Block, self).__init__()
layers = dict()
for i in range(n_layers):
layer = Dense_Layer(in_channels + i * growthrate, growthrate, bn_size)
layers['dense{}'.format(i)] = layer
self.block = nn.ModuleDict(layers)
def forward(self, features):
if (isinstance(features, torch.Tensor)):
features = [features]
for _, layer in self.block.items():
new_features = layer(features)
features.append(new_features)
return torch.cat(features, dim=1) # [1,256,24,24]
# a block consists of initial conv layers followed by 6 dense layers
dense_block = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=7, padding=3, stride=2, bias=False),
nn.BatchNorm2d(64),
nn.MaxPool2d(3, 2),
Dense_Block(6, 64, growthrate=32, bn_size=4),
)
inputs = torch.rand(1, 3, 100, 100)
outputs = dense_block(inputs)
print(outputs.shape) # shape would be (1, 256, 24, 24)
4、SENet的结构融合
4.1、SENet
1、SENet是Squeeze-and-Excitation Networks的简称,拿到了ImageNet2017分类比赛冠军,其效果得到了认可,其提出的SE模块思想简单,易于实现,并且很容易可以加载到现有的网络模型框架中。 在深度学习领域,已经有很多成果通过在空间维度上对网络的性能进行了提升。但是SENet反其道而行之,通过对通道关系进行建模来提升网络的性能。Squeeze和Excitation是两个非常关键的操作,所以SENet以此来命名。SENet的动机是希望显式地建模特征通道之间的相互依赖关系,具体来说,就是通过学习的方式来自动获取每个通道的重要程度,然后依照这个重要程度去提升有用的特征并抑制对当前任务用处不大的特征。
2、网络结构
给定一个输入 ,其特征通道数为 ,通过一系列卷积等一般变换后得到一个特征通道数为 的特征。与传统的CNN不一样的是,接下来通过三个操作来重标定前面得到的特征。
1】Squeeze(压缩)。顺着空间维度来进行特征压缩,将每个二维的特征通道变成一个实数,这个实数某种程度上具有全局的感受野,并且输出的维度和输入的特征通道数相匹配。它表征着在特征通道上响应的全局分布,而且使得靠近输入的层也可以获得全局的感受野,这一点在很多任务中都是非常有用,得到1x1xC2。
2】Excitation(激发)。它是一个类似于循环神经网络中门的机制。通过参数来为每个特征通道生成权重,其中参数被学习用来显式地建模特征通道间的相关性。
3】Reweight(缩放)。将Excitation的输出的权重看做是进过特征选择后的每个特征通道的重要性,然后通过乘法逐通道加权到先前的特征上,完成在通道维度上的对原始特征的重标定。
4.2、使用
如下是SE模块与Inception和ResNet融合的结构示意图。
上左图是将SE模块嵌入到Inception结构的示例。方框旁边的维度代表该层的输出。这里使用Alobal Average Pooling作为Squeeze操作。紧接着两个Fully Connected层组成一个Bottleneck结构去建模通道间的相关性,并输出和输入特征同样数目的权重。首先将特征维度降低到输入的1/16,然后经过ReLu激活后再通过一个Fully Connected层升回到原来的维度。这样做比直接用一个Fully Connected层的好处在于:具有更多的非线性,可以更好地拟合通道间复杂的相关性;减少了参数量和计算量。然后通过一个Sigmoid的门获得0~1之间归一化的权重,最后通过一个Scale的操作来将归一化后的权重加权到每个通道的特征上。
同样地,SE模块也可以应用在其它网络结构,如ResNetXt,Inception-ResNet,MobileNet和ShuffleNet中。
4.3、代码
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import numpy as np
import torch
from torch import nn
class SEAttention(nn.Module):
def __init__(self, channel=512, reduction=16):
super().__init__()
self.avg_pool = nn.AdaptiveAvgPool2d(1) # 全局均值池化 输出的是c×1×1
self.fc = nn.Sequential(
nn.Linear(channel, channel // reduction, bias=False), # channel // reduction代表通道压缩
nn.ReLU(inplace=True),
nn.Linear(channel // reduction, channel, bias=False), # 还原
nn.Sigmoid()
)
def forward(self, x):
b, c, _, _ = x.size() # 50×512×7×7
y = self.avg_pool(x).view(b, c) # maxpool之后得:50×512×1×1,view形状得到50×512
y = self.fc(y).view(b, c, 1, 1) # 50×512×1×1
return x * y.expand_as(x) # 根据x.size来扩展y
input = torch.randn(50, 512, 7, 7)
se = SEAttention(channel=512, reduction=8) # 实例化模型se
output = se(input)
print(output.shape)
参考: