ResNet网络结构学习
背景
在resnet提出之前,认为卷积层和池化层堆叠越多,提取到的图片特征信息就越全,学习效果越好。但是随着网络的加深,容易出现梯度消失和梯度爆炸等问题。
梯度消失:每一层的梯度小于1,反向传播时网络越深,远离输出层的梯度越趋于0。
梯度爆炸:每一层的梯度大于1,反向传播时网络越深,远离输出层的梯度越大。
退化问题:随着层数增加,预测效果反而更差。
方法
batch normalization
关于梯度消失和梯度爆炸的问题,可以通过Batch Normalization(批标准化)来缓解。
一般被添加于卷积、全连接层输出后,激活函数之前 or 全连接、卷积的输入前。
对全连接层作用在特征维度
对卷积层作用在通道维度
固定小批量里边的均值和方差,将该层的特征值分布重新拉回标准正态分布(均值为0,方差为1),使其落入激活函数对于输入较为敏感的区间。
可以缓解梯度消失,加快网络收敛。
residual 残差结构
让神经网络的某些层跳过下一层神经元的连接,隔层相连,从而可以加深网络,并且不会使网络的效果变差。
f(x)=x+g(x)
$$
x是输入,g(x)是新加入层的作用结果。当g(x)无法获得更好的信息时,x可以跳过g(x),至少可以保证下一层有x。
在ResNet中,残差块有两种结构
左边称为BasicBlock
右边称为Bottleneck,相较于Basic,多了1x1卷积层,用于残差块内的降维升维,减小参数量。
在ResNet中,残差块的shortcut捷径也有两种结构
左边的残差块中,g(x)不会改变通道数,因而shortcut中没有结构。
右边的残差块中,g(x)会改变通道数或者需要进行下采样,因而shortcut中有一个1x1的卷积层,用于改变x的通道数,并通过stride进行下采样操作,使其与g(x)通道匹配。
ResNet网络结构
在resnet18和resnet34中,使用的残差块是BasicBlock。而在resnet50及以上中,使用的残差块是Bottleneck。但无论是那种残差块,其下采样操作都是相似的,即从第二个layer开始,每个layer的第一个残差块进行一次下采样操作。
以ResNet18 和 ResNet50 为例
在进入残差块之前,均采用了7x7卷积,bn,relu,maxpooling层。其中7x7卷积层和maxpooling层都进行了下采样。
进入残差layer时,第一层的残差块没有进行下采样,然后接下来每层的第一个残差块进行了下采样,同时将通道数提升一倍。
对于BasicBlock组成的layer而言。通道数自上而下都是非递减的。
而BottleneckBlock会遇到 输入通道数大于输出通道数的情况。这时,1x1卷积层的作用得以体现。通过第一个1x1卷积进行2倍降维,然后第二个1x1卷积进行4倍升维。
代码实现
class BasicBlock(nn.Module):
expansion = 1 #通道升降维倍数
def __init__(self, in_channels, channels, stride=1, downsample=None):
super().__init__()
self.conv1 = nn.Conv2d(in_channels, channels, kernel_size=3,
stride=stride, padding=1) # 第一个3x3卷积层,通过stride进行下采样
self.bn1 = nn.BatchNorm2d(channels)
self.conv2 = nn.Conv2d(channels, channels, kernel_size=3, # 第二个3x3卷积层,不进行下采样
stride=1, padding=1)
self.bn2 = nn.BatchNorm2d(channels)
self.downsample = downsample # shortcut中的1x1卷积层
self.stride = stride
self.relu = nn.ReLU(inplace=True)
def forward(self, x):
residual = x
out = self.bn1(self.conv1(x))
out = self.relu(out)
out = self.bn2(self.conv2(out))
if self.downsample is not None:
residual = self.downsample(x) # 1x1卷积层用于下采样和通道融合
out += residual
return self.relu(out)
class Bottleneck(nn.Module):
expansion = 4 # 通道升降维倍数
def __init__(self, in_channels, channels, stride=1, downsample=None):
super().__init__()
self.conv1 = nn.Conv2d(in_channels, channels, #第一个是1x1卷积
kernel_size=1, bias=False)
self.bn1 = nn.BatchNorm2d(channels)
self.conv2 = nn.Conv2d(channels, channels, kernel_size=3, #第二个是3x3卷积,通过stride进行下采样
stride=stride, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(channels)
self.conv3 = nn.Conv2d(channels, channels * self.expansion, #第三个是1x1卷积,升维
kernel_size=1, bias=False)
self.bn3 = nn.BatchNorm2d(self.expansion * channels)
self.downsample = downsample
self.stride = stride
def forward(self, x):
residual = x
out = F.relu(self.bn1(self.conv1(x)))
out = F.relu(self.bn2(self.conv2(out)))
out = self.bn3(self.conv3(out))
if self.downsample:
residual = self.downsample(x)
out += residual
return F.relu(out)
class ResNet(nn.Module):
def __init__(self, block, layers, num_classes=1000):
self.in_channels = 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.flatten = nn.Flatten()
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, channels, blocks, stride=1):
'''
:param block 残差块选择,BisicBlock or Bottleneck
:param channels 输出通道维数
:param blocks 残差块的个数
:param stride 卷积层的stride参数,=1不下采样,=2下采样
:return nn.Sequential
'''
downsample = None
# 前一种操作需要下采样,后一种操作需要融合通道
if stride != 1 or self.in_channels != channels * block.expansion:
downsample = nn.Sequential(
nn.Conv2d(self.in_channels, channels*block.expansion, kernel_size=1,
stride=stride, bias=False),
nn.BatchNorm2d(channels*block.expansion)
)
layers = []
layers.append(block(self.in_channels, channels, stride, downsample)) #第一个残差块
#后续残差块,需要改变in_channels,使其对应上一个残差块的out_channels
self.in_channels = channels * block.expansion
for i in range(1, blocks):
layers.append(block(self.in_channels, channels))
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 = self.flatten(x)
x = self.fc(x)
return x