1️⃣ ResNet介绍
ResNet斩获2015
ImageNet竞赛图像分类任务第一名, 目标检测第一名。其重要贡献是发现了神经网络的“退化现象”,即一直加深网络深度并不能总是提高性能,反而会导致性能下降。针对这种现象,发明了Shortcut connection,极大的消除了深度过大的神经网络训练困难问题。
2️⃣ 原理分析
ResNet有两种block,一种是两层的BasicBlock,一种是三层的Bottleneck
- 两层的BasicBlock用于浅层网络(ResNet18、ResNet34),分实线和虚线的BasicBlock
- 实线BasicBlock :包含两个卷积层
根据这个图举个例子,例如输入是64×56×56- 第一个卷积:64个3×3卷积核, stride=1,padding=1
输入:64×56×56
输出:64×56×56
- 第二个卷积:64个3×3卷积核, stride=1,padding=1
输入:64×56×56
输出:64×56×56
- Shortcut connection
直接相加
相加后的最终维度:64×56×56
- 第一个卷积:64个3×3卷积核, stride=1,padding=1
- 虚线BasicBlock:里面包含两个卷积层,但注意的是第一个卷积层stride=2,进行了高宽的降维,因此shortcut connect加入了1×1的卷积进行x和F(x)维度统一。
根据这个图举个例子,例如输入是64×56×56- 第一个卷积:128个3×3卷积核, stride=2,padding=1
输入:64×56×56
输出:128×28×28
- 第二个卷积:128个3×3卷积核, stride=1,padding=1
输入:128×28×28
输出:128×28×28
- Shortcut connection
原始的x是64×56×56,经过两层卷积后的F(x)维度128×28×28,x与F(x)维度不一致,因此x需要经过卷积层来统一维度:128个1×1卷积核,stride=2,padding=1- 输入x:64×56×56
- 输出:128×28×28
- 维度一致了,因此可以相加
- 相加后的最终维度:
128×28×28
- 第一个卷积:128个3×3卷积核, stride=2,padding=1
- 实线BasicBlock :包含两个卷积层
- 三层的Bottleneck用于深层网络(ResNet50、ResNet101、ResNet152),也分实线和虚线
- 实线Bottleneck:包含三个卷积层
- 虚线Bottleneck:包含三个卷积层,但注意的是第二个卷积层stride=2,进行了高宽的降维,因此shortcut connect加入了1×1的卷积进行x和F(x)维度统一
- 实线Bottleneck:包含三个卷积层
-
两层的BasicBlock与三层的Bottleneck的对比,这里我们只分析实线的情况
Bottleneck能够减少参数和运算量,其中第一个1×1的卷积用于降维,第二个1×1的卷积用于升维。假设输入的通道数为256【CNN参数与输入的高宽无关】,Basicblock需要1179648个参数,右侧模块需要69632个参数
注:CNN参数个数(没算偏置) = 卷积核高度×卷积核宽度×输入通道数×卷积核个数
3️⃣ 网络结构
这里以ResNet18(1个卷积+16个卷积+1个fc)为例进行分析:
-
网络输入:3×224×224
-
conv1:64个7×7卷积核,stride=2,padding=3
输入:3×224×224
输出:64×112×112
经过BN(批量归一化) -
maxpool:3×3,stride=2,padding=1
输入:64×112×112
输出:64×56×56
-
con2_x:两个实线BasicBlock,每个BasicBlock有两层卷积
-
第一个实线BasicBlock
- 第一个卷积:64个3×3卷积核, stride=1,padding=1
输入:64×56×56
输出:64×56×56
经过BN(批量归一化)
经过ReLu - 第二个卷积:64个3×3卷积核, stride=1,padding=1
输入:64×56×56
输出:64×56×56
经过BN(批量归一化) - Shortcut connection
直接相加
经过ReLu
相加后的最终维度:64×56×56
- 第一个卷积:64个3×3卷积核, stride=1,padding=1
-
第二个实线BasicBlock
- 第一个卷积:64个3×3卷积核, stride=1,padding=1
输入:64×56×56
输出:64×56×56
- 第二个卷积:64个3×3卷积核, stride=1,padding=1
输入:64×56×56
输出:64×56×56
- Shortcut connection
直接相加
经过ReLu
相加后的最终维度:64×56×56
- 第一个卷积:64个3×3卷积核, stride=1,padding=1
-
-
conv3_x:一个虚线BasicBlock,一个实线BasicBlock,每个BasicBlock有两层卷积
- 第一个BasicBlock(虚线,高宽减半,深度加倍)
- 第一个卷积:128个3×3卷积核,stride=2【注意这里】,padding=1
输入:64×56×56
输出:128×28×28
经过BN(批量归一化)
经过ReLu - 第二个卷积:128个3×3卷积核,stride=1,padding=1
输入:128×28×28
输出:128×28×28
经过BN(批量归一化) - Shortcut connection:原始的x是64×56×56,经过两层卷积后的F(x)维度128×28×28,x与F(x)维度不一致,因此x需要经过卷积层来统一维度:128个1×1卷积核,stride=2,padding=1
- 输入x:64×56×56
- 输出:128×28×28
- 经过BN(批量归一化)
- 维度一致了,因此可以相加
- 经过ReLu
- 相加后的最终维度:
128×28×28
- 第一个卷积:128个3×3卷积核,stride=2【注意这里】,padding=1
- 第二个BasicBlock
- 第一个卷积:128个3×3卷积核,stride=1,padding=1
输入:64×56×56
输出:128×28×28
经过BN(批量归一化)
经过ReLu - 第二个卷积:128个3×3卷积核,stride=1,padding=1
输入:128×28×28
输出:128×28×28
经过BN(批量归一化) - Shortcut connection
直接相加
经过ReLu
相加后的最终维度:128×28×28
- 第一个卷积:128个3×3卷积核,stride=1,padding=1
- 第一个BasicBlock(虚线,高宽减半,深度加倍)
-
con4_x:一个虚线BasicBlock,一个实线BasicBlock,每个BasicBlock有两层卷积
- 第一个BasicBlock(虚线,高宽减半,深度加倍)
- 第一个卷积:256个3×3卷积核,stride=2【注意这里】,padding=1
输入:128×28×28
输出:256×14×14
经过BN(批量归一化)
经过ReLu - 第二个卷积:256个3×3卷积核,stride=1,padding=1
输入:256×14×14
输出:256×14×14
经过BN(批量归一化) - Shortcut connection:原始的x是128×28×28,经过两层卷积后的F(x)维度256×14×14,x与F(x)维度不一致,因此x需要经过卷积层来统一维度:256个1×1卷积核,stride=2,padding=1
- 输入x:128×28×28
- 输出:256×14×14
- 经过BN(批量归一化)
- 维度一致了,因此可以相加
- 经过ReLu
- 相加后的最终维度:
256×14×14
- 第一个卷积:256个3×3卷积核,stride=2【注意这里】,padding=1
- 第二个BasicBlock
-
第一个卷积:256个3×3卷积核,stride=1,padding=1
输入:256×14×14
输出:256×14×14
-
第二个卷积:256个3×3卷积核,stride=1,padding=1
输入:256×14×14
输出:256×14×14
-
Shortcut connection
直接相加
经过ReLu
相加后的最终维度:256×14×14
-
- 第一个BasicBlock(虚线,高宽减半,深度加倍)
-
con5_x:一个虚线BasicBlock,一个实线BasicBlock,每个BasicBlock有两层卷积
- 第一个BasicBlock(虚线,高宽减半,深度加倍)
- 第一个卷积:512个3×3卷积核,stride=2【注意这里】,padding=1
输入:256×14×14
输出:512×7×7
经过BN(批量归一化)
经过ReLu - 第二个卷积:512个3×3卷积核,stride=1,padding=1
输入:512×7×7
输出:512×7×7
经过BN(批量归一化) - Shortcut connection:原始的x是256×14×14,经过两层卷积后的F(x)维度512×7×7,x与F(x)维度不一致,因此x需要经过卷积层来统一维度:512个1×1卷积核,stride=2,padding=1
- 输入x:256×14×14
- 输出:512×7×7
- 经过BN(批量归一化)
- 维度一致了,因此可以相加
- 经过ReLu
- 相加后的最终维度:
512×7×7
- 第一个卷积:512个3×3卷积核,stride=2【注意这里】,padding=1
- 第二个BasicBlock
- 第一个卷积:512个3×3卷积核,stride=1,padding=1
输入:512×7×7
输出:512×7×7
经过BN(批量归一化)
经过ReLu - 第二个卷积:512个3×3卷积核,stride=1,padding=1
输入:512×7×7
输出:512×7×7
经过BN(批量归一化) - Shortcut connection
直接相加
经过ReLu
相加后的最终维度:512×7×7
- 第一个卷积:512个3×3卷积核,stride=1,padding=1
- 第一个BasicBlock(虚线,高宽减半,深度加倍)
-
avgpool:输出
512, 1, 1
-
FC:
10
4️⃣ 代码
1.这里以resnet18为例进行分析,创建一个名为resnet18.py的文件
# 仅针对Resnet18
import torch
from torch import nn
from torch.nn import functional as F
class BasicBlock(nn.Module):
def __init__(self, input_channels, out_channels,
use_1x1conv=False, strides=1):
super().__init__()
self.conv1 = nn.Conv2d(input_channels, out_channels,
kernel_size=3, padding=1, stride=strides)
self.bn1 = nn.BatchNorm2d(out_channels)
self.relu1=nn.ReLU()
self.conv2 = nn.Conv2d(out_channels, out_channels,
kernel_size=3, padding=1)
self.bn2 = nn.BatchNorm2d(out_channels)
if use_1x1conv:
self.conv3 = nn.Conv2d(input_channels, out_channels,
kernel_size=1, stride=strides)
self.bn3=nn.BatchNorm2d(out_channels)
else:
self.conv3 = None
def forward(self, x):
out = self.conv1(x)
out = self.bn1(out)
out = self.relu1(out)
out=self.conv2(out)
out=self.bn2(out)
if self.conv3:
x = self.conv3(x)
x=self.bn3(x)
out += x
out = F.relu(out)
return out
def big_block(input_channels, out_channels, num_block,
first_bigblock=False):
blk = []
for i in range(num_block):
if first_bigblock:
blk.append(BasicBlock(out_channels, out_channels))
elif i==0:
blk.append(BasicBlock(input_channels, out_channels,
use_1x1conv=True, strides=2))
else:
blk.append(BasicBlock(out_channels, out_channels))
return blk
class Resnet(nn.Module):
def __init__(self):
super().__init__()
self.b1 = nn.Sequential(nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3),
nn.BatchNorm2d(64),
nn.MaxPool2d(kernel_size=3, stride=2, padding=