ResNet网络详解及Pytorch代码实现
ResNet解决了什么问题
Resnet网络是为了解决深度网络中的退化问题,即网络层数越深时,在数据集上表现的性能却越差,如下图所示是论文中给出的深度网络退化现象。从图中我们可以看到,作者在CIFAR-10数据集上测试了20层和56层的深度网络,结果就是56层的训练误差和测试误差反而比层数少的20层网络更大。

而采用ResNet网络之后,可以解决这种退化问题,如下图所示。在没有采用ResNet结构之前,如上一张ppt,34层网络plain-34的性能误差要大于18层网络plain-18的性能误差。而采用ResNet网络结构的34层网络结构ResNet-34性能误差小于18层网络ResNet。因此,采用ResNet网络结构的网络层数越深,则性能越佳。

ResNet原理及结构
假设我们想要网络块学习到的映射为H(x),而直接学习H(x)是很难学习到的。若我们学习另一个残差函数F(x) = H(x) - x可以很容易学习,因为此时网络块的训练目标是将F(x)逼近于0,而不是某一特定映射。因此,最后的映射H(x)就是将F(x)和x相加,H(x) = F(x) + x,如左图所示。这个网络块的输出y为

下图是论文中给出的两种残差结构。左边的残差结构是针对层数较少网络,例如ResNet18层和ResNet34层网络。右边是针对网络层数较多的网络,例如ResNet101,ResNet152等。为什么深层网络要使用右侧的残差结构呢。因为,右侧的残差结构能够减少网络参数与运算量。同样输入一个channel为256的特征矩阵,如果使用左侧的残差结构需要大约1170648个参数,但如果使用右侧的残差结构只需要69632个参数。明显搭建深层网络时,使用右侧的残差结构更合适。

BasicBlock结构
BasicBlock结构用于ResNet34及以下的网络,BotteNeck结构用于ResNet50及以上的网络。理解了这两个基础块,ResNet就是这些基础块的叠加了。
如下图所示,该残差结构的主分支是由两层3x3的卷积层组成,而残差结构右侧的连接线是shortcut分支也称捷径分支(注意为了让主分支上的输出矩阵能够与我们捷径分支上的输出矩阵进行相加,必须保证这两个输出特征矩阵有相同的shape)。如果仔细观察了ResNet34网络结构图,应该能够发现图中会有一些虚线的残差结构。在原论文中作者只是简单说了这些虚线残差结构有降维的作用,并在捷径分支上通过1x1的卷积核进行降维处理。而下图右侧给出了详细的虚线残差结构,注意下每个卷积层的步距stride,以及捷径分支上的卷积核的个数(与主分支上的卷积核个数相同)。

BottleNeck结构
接着我们再来分析下针对ResNet50/101/152的残差结构,如下图所示。在该残差结构当中,主分支使用了三个卷积层,第一个是1x1的卷积层用来压缩channel维度,第二个是3x3的卷积层,第三个是1x1的卷积层用来还原channel维度(注意主分支上第一层卷积层和第二次卷积层所使用的卷积核个数是相同的,第三次是第一层的4倍)。该残差结构所对应的虚线残差结构如下图右侧所示,同样在捷径分支上有一层1x1的卷积层,它的卷积核个数与主分支上的第三层卷积层卷积核个数相同,注意每个卷积层的步距。

ResNet结构
了解了上述BasicBlock基础块和BotteNeck结构后,ResNet结构就直接叠加搭建了。5种不同层数的ResNet结构图如下图所示,

ResNet代码详解(Pytorch)
BasicBlock类和Bottleneck类类似,前者主要是用来构建ResNet18和ResNet34网络,因为这两个网络的residual结构只包含两个卷积层,没有Bottleneck类中的bottleneck概念。因此在该类中,第一个卷积层采用的是kernel_size=3的卷积,如conv3x3函数所示。
BasicBlock类中的init()函数是先定义网络架构,forward()的函数是前向传播,实现的功能就是残差块,
#定义BasicBlock
class BasicBlock(nn.Module):
expansion = 1
def __init__(self, inplanes, planes, stride=1, downsaple=None, groups=1,
base_width=64, dilation=1, norm_layer=None):
super(BasicBlock, self).__init__()
if norm_layer is None:
norm_layer = nn.BatchNorm2d
if groups !=1 or base_width != 64:
raise ValueError('BasicBlock only supports groups=1 and base_width=64')
if dilation > 1:
raise NotImplementedError("Dilation > 1 not supported in BasicBlock")
#下面定义BasicBlock中的各个层
self.conv1 = con3x3(inplanes, planes, stride)
self.bn1 = norm_layer(planes)
self.relu = nn.ReLU(inplace=True)
#inplace为True表示进行原地操作,一般默认为False,表示新建一个变量存储操作
self.conv2 = con3x3(planes, planes)
self.bn2 = norm_layer(planes)
self.dowansample = downsaple
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.dowansample is not None:
identity = self.dowansample(x)
out += identity
out = self.relu(out)
return out
Bottleneck类是另一种blcok类型,同上,init()函数是预定义网络架构,forward函数是进行前向传播。该block中有三个卷积,分别是1x1,3x3,1x1,分别完成的功能就是维度压缩,卷积,恢复维度!故bottleneck实现的功能就是对通道数进行压缩,再放大。注意:这里的plane不再是输出的通道数,输出通道数应该就是planeexpansion,即4plane。
#下面定义Bottleneck层(Resnet50以上用到的基础块)
class Bottleneck(nn.Module):
expansion = 4 #Bottleneck层输出通道都是输入的4倍
def __init__(self, inplanes, planes, stride=1, downnsaple=None, groups=1,
base_width=64, dilation=1, norm_layer=None):
super(Bottleneck, self).__init__()
if norm_layer is None:
norm_layer = nn.BatchNorm2d
width = int(planes * (base_width / 64.)) * groups
#定义Bottleneck中各层
self.conv1 = con1x1(inplanes, width)
self.bn1 = norm_layer(width)
self.conv2 = con3x3(width, width, stride, groups, dilation)
self.bn2 = norm_layer(width)
self.conv3 = con1x1(width, planes * self.expansion)
self.bn3 = norm_layer(planes * self.expansion)
self.relu = nn.ReLU(inplanes=True)
self.downsaple = downnsaple
self.stride = stride
#定义Bottleneck的前向传播
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)
out = self.relu(out)
if self.downsaple is not None:
identity = self.downsaple(x)
out += identity
out = self.relu(out)
return out
这两个class讲清楚的话,后面的网络主体架构就还蛮好理解的了,6中架构之间的不同在于basicblock和bottlenek之间的不同以及block的输入参数的不同。因为ResNet一般有4个stack,每一个stack里面都是block的堆叠,所以[3, 4, 6, 3]就是每一个stack里面堆叠block的个数,故而造就了不同深度的ResNet。
resnet18: ResNet(BasicBlock, [2, 2, 2, 2])
resnet34: ResNet(BasicBlock, [3, 4, 6, 3])
resnet50:ResNet(Bottleneck, [3, 4, 6, 3])
resnet101: ResNet(Bottleneck, [3, 4, 23, 3])
resnet152: ResNet(Bottleneck, [3, 8, 36, 3])
ResNet类
最后的ResNet类其实可以根据列表大小来构建不同深度的resnet网络架构。resnet共有五个阶段,其中第一阶段为一个7x7的卷积处理,stride为2,然后经过池化处理,此时特征图的尺寸已成为输入的1/4,接下来是四个阶段,也就是代码中的layer1,layer2,layer3,layer4。这里用make_layer函数产生四个layer,需要用户输入每个layer的block数目(即layers列表)以及采用的block类型(基础版还是bottleneck版)
构建ResNet网络是通过ResNet这个类进行的。首先还是继承PyTorch中网络的基类:torch.nn.Module,其次主要的是重写初始化init和forward方法。在初始化init中主要是定义一些层的参数。forward方法中主要是定义数据在层之间的流动顺序,也就是层的连接顺序。另外还可以在类中定义其他私有方法用来模块化一些操作,比如这里的_make_layer方法是用来构建ResNet网络中的4个blocks。_make_layer方法的第一个输入block是Bottleneck或BasicBlock类,第二个输入是该blocks的输出channel,第三个输入是每个blocks中包含多少个residual子结构,因此layers这个列表就是前面resnet50的[3, 4, 6, 3]。
_make_layer方法中比较重要的两行代码是:1、layers.append(block(self.inplanes, planes, stride, downsample)),该部分是将每个blocks的第一个residual结构保存在layers列表中。2、 for i in range(1, blocks): layers.append(block(self.inplanes, planes)),该部分是将每个blocks的剩下residual 结构保存在layers列表中,这样就完成了一个blocks的构造。这两行代码中都是通过Bottleneck这个类来完成每个residual的构建,接下来介绍Bottleneck类。
class ResNet(nn.Module):
def __init__(self, block, layers, num_classes=1000):
self.inplanes = 64
super(ResNet, self).__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.AvgPool2d(7, stride=1)
self.fc = nn.Linear(512 * block.expansion, num_classes)
for m in self.modules():
if isinstance(m, nn.Conv2d):
n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
m.weight.data.normal_(0, math.sqrt(2. / n))
elif isinstance(m, nn.BatchNorm2d):
m.weight.data.fill_(1)
m.bias.data.zero_()
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 = x.view(x.size(0), -1)
x = self.fc(x)
return x