ResNet
由于随着卷积层数的增加,会导致梯度消失/爆炸的问题,虽然这两种方法可以通过归一化等方法解决,但是还存在退化问题,所以提出了ResNet。 (关于梯度消失和爆炸,退化可参照 链接: link.)
模型的搭建
对于整个模型的搭建主要是Block类,接下来以BasicBlock为例来对代码进行解析。(代码来源:太阳花的小绿豆的CSDN 链接: link.)) ** Block**的含义: 例如:ResNet34由以下几部分组成,conv1,conv2_x,conv3_x,conv4_x,conv5_x,以及最后的全连接层, BasicBlock指的是 conv2_x,conv3_x,conv4_x,conv5_x里的这些具有重复结构的块,如conv3_x层中具有四个([33,128]+[33,128])块, BasicBlock就指的是这些块,在ResNet中,通过重复调用BasicBlock方法就能方便的构建出这些块。简单来说,Block是为了方便调用代码,就是为了省力。 我们可以了解到ResNet中对于50层以下的构建块采用的是BasicBlock,而大于50的深层则采用的是Bottleneck
class BasicBlock(nn.Module):
expension = 1
def __init__(self, in_channel, out_channel, stride = 1, downsample = None, **kwargs):
super(BasicBlock, self).__init__()
self.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=out_channel,
kernel_size=3, stride=stride, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(out_channel)
self.relu = nn.ReLU()
self.conv2 = nn.Conv2d(in_channels=in_channel, out_channels=out_channel,
kernel_size=3, stride=stride, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(out_channel)
self.downsample = downsample
def forward(self, x):
identity = x
if self.downsample is not None:
identity = self.downsample(x)
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out += identity
out = self.relu(out)
return out
**步骤1:**令BasicBlock继承nn.Module(对于这一模块,可查看Pytorch文档中的nn.Module或 参考 LoveMIss-Y的CSDN链接link.,简单来说,就是利用库中定义的一个模型来改写) **步骤2:**重写__init__和forward (1)一般把网络中具有可学习参数的层(如全连接层、卷积层等)放在构造函数__init__()中,当然我也可以吧不具有参数的层也放在里面。 (2)一般把不具有可学习参数的层(如ReLU、dropout、BatchNormanation层)可放在构造函数中,也可不放在构造函数中,如果不放在构造函数__init__里面, 则在forward方法里面可以使用nn.functional来代替。 (3)forward方法是必须要重写的,它是实现模型的功能,实现各个层之间的连接关系的核心。
对__init__的理解,我们可以发现在18层和34层的网络中,卷积核大小均为3*3,且特征图数量没有增加,因此在BasicBlock中我们的conv的kernel_size=3,stride默认取1,这里需要注意BN层必须是在conv层和激活函数层中间,且conv中的bias(偏置参数)不需要设置,因为经过BN层后bias会被消掉。
关于downsample的理解
(参照马佳的男人的CSDN博文 链接link.) 首先我们要明确,在resnet中的downsample有两种: 1、真正意义上让output.shape长宽变成1/2的我暂且称之为real_downsample 2、shortcut(是指经过结构中捷径的过程)前的x的为了适应shortcut后变化的shape而做的自适应调节,暂且称之为identity_downsample 接下来看ResNet这个类:
class ResNet(nn.Module):
def __init__(self,
block,
block_num,
num_classes=1000,
include_top=True,
groups=1,
width_per_group=64):
super(ResNet, self).__init__()
self.include_top = include_top
self.in_channel = 64
self.groups = groups
self.width_per_group = width_per_group
self.conv1 = nn.Conv2d(3, self.in_channel, kernel_size=7, stride=2,
padding=3, bias=False)
self.bn1 = nn.BatchNorm2d(self.in_channel)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
self.layer1 = self._make_layer(block, 64, block_num[0])
self.layer2 = self._make_layer(block, 128, block_num[1], stride=2)
self.layer3 = self._make_layer(block, 256, block_num[2], stride=2)
self.layer4 = self._make_layer(block, 512, block_num[3], stride=2)
if self.include_top:
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(512 * block.expansion, num_classes)
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
def _make_layer(self, block, channel, block_num, stride=1):
downsample = None
if stride != 1 or self.in_channel != channel * block.expansion:
downsample = nn.Sequential(
nn.Conv2d(self.in_channel, channel * block.expansion, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(channel * block.expansion))
layers = []
layers.append(block(
self.in_channel,
channel,
downsample=downsample,
stride=stride,
groups=self.groups,
width_per_group=self.width_per_group))
self.in_channel = channel * block.expansion
for _ in range(1, block_num):
layers.append(block(
self.in_channel,
channel,
groups=self.groups,
width_per_group=self.width_per_group))
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)
if self.include_top:
x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.fc(x)
return x
** real_downsample:** #这里的layer是指包含多个block的一个层,层>块 (1) 第一个大layer不做real_downsample,所以此时stride=1 (2)剩下的所有大layer都只在第一个block里的第一个3x3用stride=2做real_downsample 那么如何对所有的第一个block进行调整? 当然就是对于每个layer,先把第一个block拉出来用我们传进去的stride进行领导的特殊对待啦(stride默认为1)传进去stride = 1,那么第一个block就不用downsample;如果传进去stride = 2,那第一个block就要downsample。至于剩下的block, 就不用看领导脸色了,传进去stride = 1还是2都跟它们无关,因为它们反正都不downsample,所以可以看到在_make_layer的循环里没有指定接下来的stride。 ** identity_downsample:**(identity:其实意思就是这个网络层的设计是用于占位的,即不干活,只是有这么一个层,放到残差网络里就是在跳过连接的地方用这个层,显得没有那么空虚!) 由于在残差结构里,参数通过主线和捷径后又一个相加操作,所以需要保证两者维度一致,所以需要做accommodation(适应) 什么时候需要做identity_downsample? 简单来说,做了real_downsample就要做identity_downsample
if stride != 1 or self.in_channel != channel * block.expansion:
downsample = nn.Sequential(
nn.Conv2d(self.in_channel, channel * block.expansion, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(channel * block.expansion))
现在,来看代码,我们对stride是否等于1进行了判断,因为我们可以知道除了第一个大layer没做real_downsample外,其余所有大layer的第一个block都做了real_downsample,所以后续对传入参数进行变换。
downsample总结: real downsample是Resnet主动为了downsample而downsample的,而identity_downsample只是我们为了能让out += identity 的被动的适应性调整
对于参数expansion的理解:
通过对比BasicBlock和Bottleneck可以发现在前者中expansion=1,后者等于4。 所以这个参数的用途究竟是什么? 我们经过两个block的对比可以发现,实际上这个参数是用于区分这两个block,已经知道了对于50层以下的网络使用BasicBlock,而以上使用Bottleneck。两者的区别可以通过表格看出,在block中50层以下网络,卷积后没有改变深度(特征图的数目),而对于50层以上的网 络,最后一次卷积将深度扩大了4倍,所以这个参数就是这么来的。
小结
这是我在学习过程中,对于ResNet代码阅读的一些心得和参考的博文。刚刚开始学习深度学习,这笔记算是对学习的证明吧!接下来将使用迁移学习的方法对宝可梦数据集进行训练。
|