ResNet论文[https://arxiv.org/pdf/1512.03385.pdf]。RestNet网络结构ResNet在2015年被提出,在ImageNet比赛classification任务上获得第一名,因为它“简单与实用”并存,之后很多方法都建立在ResNet50或者ResNet101的基础上完成的,检测,分割,识别等领域里得到广泛的应用。
当更深的网络能够开始收敛时,一个退化问题就暴露出来了。随着网络深度的增加,精度达到饱和,然后开始退化。出乎意料的是,这种退化不是由过拟合引起的,向适当深度的模型中添加更多的层会导致更高的训练误差。ReNet网络就是来解决网络退化的问题,通过在网络架构多加入一条恒等映射,经过一次卷积,如果效果变差,则保持权重参数不变,相当于并没有做此次卷积。这样的话,就保证了神经网络不会随着网络层数的增加反而效果变差,也就阻止了模型退化的问题。
ResNet模型亮点
提出Residual模块
使用Batch Normalization加速训练,丢弃dropout
易于收敛,很好地解决了退化的问题,模型可以很深,准确率大大提高
ResNet的网络结构如图所示:
我们按照ResNet-18来分析一下:
第一层卷积conv1:输入图像的size是224*224*3,使用64个size是7*7,stride步长为2,padding填充为3的卷积核,卷积后的输出size是112*112*64。
第二层池化:使用size是3*3的核进行最大池化,stride步长为2,池化后的输出size是56*56*64。
第三层conv2_x:两个卷积构成一个残差块,后面的x2的意思就是两个3*3,64的卷积层。残差块的公式如下。残差块分成两部分直接映射部分和残差部分。 h(xl) 是直接映射,反应在图1中是左边的曲线; F(xl,Wl) 是残差部分,conv2_x就是由两个卷积操作构成。
在卷积网络中, xl 可能和 xl+1 的Feature Map的数量不一样,这时候就需要使用 1×1 卷积进行升维或者降维。这时,残差块表示如下公式。其中 h(xl)=Wl′x 。其中 Wl′ 是 1×1 卷积操作,但是实验结果 1×1 卷积对模型性能提升有限,所以一般是在升维或者降维时才会使用。
残差块的PyTorch实现:
class BasicBlock(nn.Module):
def __init__(self,in_channels,out_channels,stride=[1,1],padding=1) -> None:
super(BasicBlock, self).__init__()
# 残差部分
self.layer = nn.Sequential(
nn.Conv2d(in_channels,out_channels,kernel_size=3,stride=stride[0],padding=padding,bias=False),
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True), # 原地替换 节省内存开销
nn.Conv2d(out_channels,out_channels,kernel_size=3,stride=stride[1],padding=padding,bias=False),
nn.BatchNorm2d(out_channels)
)
# shortcut 部分
# 由于存在维度不一致的情况 所以分情况
self.shortcut = nn.Sequential()
if stride[0] != 1 or in_channels != out_channels:
self.shortcut = nn.Sequential(
# 卷积核为1 进行升降维
# 注意跳变时 都是stride==2的时候 也就是每次输出信道升维的时候
nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride[0], bias=False),
nn.BatchNorm2d(out_channels)
)
def forward(self, x):
out = self.layer(x)
out += self.shortcut(x)
out = F.relu(out)
return out
ResNet-18的PyTorch实现
# 采用bn的网络中,卷积层的输出并不加偏置
class ResNet18(nn.Module):
def __init__(self, BasicBlock, num_classes=10) -> None:
super(ResNet18, self).__init__()
self.in_channels = 64
# 第一层作为单独的 因为没有残差快
self.conv1 = nn.Sequential(
nn.Conv2d(3,64,kernel_size=7,stride=2,padding=3,bias=False),
nn.BatchNorm2d(64),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
)
# conv2_x
self.conv2 = self._make_layer(BasicBlock,64,[[1,1],[1,1]])
# conv3_x
self.conv3 = self._make_layer(BasicBlock,128,[[2,1],[1,1]])
# conv4_x
self.conv4 = self._make_layer(BasicBlock,256,[[2,1],[1,1]])
# conv5_x
self.conv5 = self._make_layer(BasicBlock,512,[[2,1],[1,1]])
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(512, num_classes)
#这个函数主要是用来,重复同一个残差块
def _make_layer(self, block, out_channels, strides):
layers = []
for stride in strides:
layers.append(block(self.in_channels, out_channels, stride))
self.in_channels = out_channels
return nn.Sequential(*layers)
def forward(self, x):
out = self.conv1(x)
out = self.conv2(out)
out = self.conv3(out)
out = self.conv4(out)
out = self.conv5(out)
# out = F.avg_pool2d(out,7)
out = self.avgpool(out)
out = out.reshape(x.shape[0], -1)
out = self.fc(out)
return out