ResNet提出了两种mapping:一种是identity mapping,指的就是图1中”弯弯的曲线”,另一种residual mapping,指的就是除了”弯弯的曲线“那部分,所以最后的输出是 y=F(x)+x
identity mapping顾名思义,就是指本身,也就是公式中的x,而residual mapping指的是“差”,也就是y−x,所以残差指的就是F(x)部分,也就是y-x的差。
变化主要体现在ResNet直接使用stride=2的卷积做下采样,并且用global average pool层替换了全连接层。ResNet的一个重要设计原则是:当feature map大小降低一半时,feature map的数量增加一倍,这保持了网络层的复杂度。
图中虚线表示feature map数量发生了改变。从表中可以看到,对于18-layer和34-layer的ResNet,其进行的两层间的残差学习,当网络更深时,其进行的是三层间的残差学习,三层卷积核分别是1x1,3x3和1x1,一个值得注意的是隐含层的feature map数量是比较小的,并且是输出feature map数量的1/4。
(图中3*3,64 指的是卷积核为 3 ∗ 3 3* 3 3∗3 结构,一共有64个卷积核
左边的是BasicBlock 右边的是Bottleneck)
这两种结构分别针对ResNet34(左图)和ResNet50/101/152(右图),一般称整个结构为一个”building block“。其中右图又称为”bottleneck design”,目的一目了然,就是为了降低参数的数目,第一个1x1的卷积把256维channel降到64维,然后在最后通过1x1卷积恢复,整体上用的参数数目:1x1x256x64 + 3x3x64x64 + 1x1x64x256 = 69632,而不使用bottleneck的话就是两个3x3x256的卷积,参数数目: 3x3x256x256x2 = 1179648,差了16.94倍。
1.Conv3d
class torch.nn.Conv3d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True)
Parameters:
nn.Conv2d(self, in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True))
参数:
ResNet各个Stage具体结构
如本图所示,ResNet分为5个stage(阶段),其中Stage 0的结构比较简单,可以视其为对INPUT的预处理,后4个Stage都由Bottleneck组成,结构较为相似。Stage 1包含3个Bottleneck,剩下的3个stage分别包括4、6、3个Bottleneck。
现在对Stage 0和Stage 1进行详细描述,同理就可以理解后3个Stage。
Stage 0
(3,224,224)指输入INPUT的通道数(channel)、高(height)和宽(width),即(C,H,W)。现假设输入的高度和宽度相等,所以用(C,W,W)表示。
该stage中第1层包括3个先后操作
CONV
CONV是卷积(Convolution)的缩写,7×7指卷积核大小,64指卷积核的数量(即该卷积层输出的通道数),/2指卷积核的步长为2。
BN
BN是Batch Normalization的缩写,即常说的BN层。
RELU
RELU指ReLU激活函数。
该stage中第2层为MAXPOOL,即最大池化层,其kernel大小为3×3、步长为2。
(64,56,56)是该stage输出的通道数(channel)、高(height)和宽(width),其中64等于该stage第1层卷积层中卷积核的数量,56等于224/2/2(步长为2会使输入尺寸减半)。
总体来讲,在Stage 0中,形状为(3,224,224)的输入先后经过卷积层、BN层、ReLU激活函数、MaxPooling层得到了形状为(64,56,56)的输出。
Stage 1
在理解了Stage 0以及熟悉图中各种符号的含义之后,可以很容易地理解Stage 1。理解了Stage 1之后,剩下的3个stage就不用我讲啦,你自己就能看懂。
Stage 1的输入的形状为(64,56,56),输出的形状为(64,56,56)。
下面介绍Bottleneck的具体结构(难点),把Bottleneck搞懂后,你就懂Stage 1了。
Bottleneck具体结构
现在让我们把目光放在本图最右侧,最右侧介绍了2种Bottleneck的结构。
“BTNK”是BottleNeck的缩写(本文自创,请谨慎使用)。
2种Bottleneck分别对应了2种情况:输入与输出通道数相同(BTNK2)、输入与输出通道数不同(BTNK1),这一点可以结合ResNet原文去看喔。
BTNK2
我们首先来讲BTNK2。
BTNK2有2个可变的参数C和W,即输入的形状(C,W,W)中的c和W。
令形状为(C,W,W)的输入为[公式],令BTNK2左侧的3个卷积块(以及相关BN和RELU)为函数[公式],两者相加([公式])后再经过1个ReLU激活函数,就得到了BTNK2的输出,该输出的形状仍为(C,W,W),即上文所说的BTNK2对应输入[公式]与输出[公式]通道数相同的情况。
BTNK1
BTNK1有4个可变的参数C、W、C1和S。
与BTNK2相比,BTNK1多了1个右侧的卷积层,令其为函数[公式]。BTNK1对应了输入[公式]与输出[公式]通道数不同的情况,也正是这个添加的卷积层将[公式]变为[公式],起到匹配输入与输出维度差异的作用([公式]和[公式]通道数相同),进而可以进行求和[公式]。
简要分析
可知,ResNet后4个stage中都有BTNK1和BTNK2。
4个stage中BTNK2参数规律相同
4个stage中BTNK2的参数全都是1个模式和规律,只是输入的形状(C,W,W)不同。
Stage 1中BTNK1参数的规律与后3个stage不同
然而,4个stage中BTNK1的参数的模式并非全都一样。具体来讲,后3个stage中BTNK1的参数模式一致,Stage 1中BTNK1的模式与后3个stage的不一样,这表现在以下2个方面:
参数S:BTNK1左右两个1×1卷积层是否下采样
Stage 1中的BTNK1:步长S为1,没有进行下采样,输入尺寸和输出尺寸相等。
后3个stage的BTNK1:步长S为2,进行了下采样,输入尺寸是输出尺寸的2倍。
参数C和C1:BTNK1左侧第一个1×1卷积层是否减少通道数
Stage 1中的BTNK1:输入通道数C和左侧1×1卷积层通道数C1相等(C=C1=64),即左侧1×1卷积层没有减少通道数。
后3个stage的BTNK1:输入通道数C和左侧1×1卷积层通道数C1不相等(C=2*C1),左侧1×1卷积层有减少通道数。
为什么Stage 1中BTNK1参数的规律与后3个stage不同?(个人观点)
关于BTNK1左右两个1×1卷积层是否下采样
因为Stage 0中刚刚对网络输入进行了卷积和最大池化,还没有进行残差学习,此时直接下采样会损失大量信息;而后3个stage直接进行下采样时,前面的网络已经进行过残差学习了,所以可以直接进行下采样。
关于BTNK1左侧第一个1×1卷积层是否减少通道数
根据ResNet原文可知,Bottleneck左侧两个1×1卷积层的主要作用分别是减少通道数和恢复通道数,这样就可以使它们中间的3×3卷积层的输入和输出的通道数都较小,因此效率更高。
Stage 1中BTNK1的输入通道数C为64,它本来就比较小,因此没有必要通过左侧第一个1×1卷积层减少通道数。
ResNet-50的Pytorch实现:
import torch.nn as nn
def conv1x1(in_channels, out_channels, stride=1):
return nn.conv2d(in_channels, out_channels, kernel_size = 1, stride = stride, bias=False)
def conv3x3(in_channels, out_channels, kernel_size, stride=1):
return nn.conv2d(in_channels, out_channels, kernel_size=3, stride=stride, bias=False)
def conv7x7(in_channels, out_channels, stride, padding=3):
return nn.conv2d(in_channels, out_channels, kernel_size=7, stride=stride, padding=padding, bias=False)
class Bottleneck(nn.module):
expansion = 4
def __init__(self, in_channels, out_channels, stride=1, downsample=None):
super(Bottleneck, self).__init__()
self.conv1 = conv1x1(in_channels, out_channels) #这里为什么不传入stride stage1中的stride=1,不需要下采样 因为刚经过一个maxpooling
self.bn1 = nn.BatchNorm2d(out_channels)
self.conv2 = conv3x3(out_channels, out_channels, stride)
self.bn2 = nn.BatchNorm2d(out_channels)
#为什么这么设计
self.conv3 = conv1x1(out_channels, out_channels * self.expansion)
self.bn3 = nn.BatchNorm2d(out_channels * self.expansion)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
self.stride = stride
def forward(self, x):
residual = 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)
#stage1 的输入输出通道不同的不需要下采样
if self.downsample is not None:
residual = self.downsample(x) #使用卷积将输入的通道数转换成和输出通道数目一样
out += residual
out = self.relu(out)
return out
class ResNet_50(nn.module):
def __init__(self, layers, num_classes=1000 ):
super(ResNet_50,self).__init__()
#这一步自己忘记加
self.in_channels = 64
self.conv1 = conv7x7(3,64)
self.bn1 = nn.BatchNorm2d(64) #num_features – C from an expected input of size (N, C, H, W)(N,C,H,W) 通道数
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
self.layer1 = self._make_layer(64, layers[0]) #layers代表有几层 这里的64代表1x1卷积层的输出
self.layer2 = self._make_layer(128, layers[1], stride = 2)
self.layer3 = self._make_layer(256, layers[2], stride = 2)
self.layer4 = self._make_layer(512, layers[3], stride = 2)
self.avgpool = nn.AdaptiveAvgPool2d(7)
self.fc = nn.Linear(512 * Bottleneck.expansion, num_classes)
#fc层
def _make_layer(self, out_channels, layer_num, stride=1):
downsample = None
if stride != 1 or self.in_channels != out_channels * Bottleneck.expansion:
downsample = nn.Sequential(
conv1x1(self.in_channels, out_channels * Bottleneck.expansion, stride),
nn.BatchNorm2d(out_channels * Bottleneck.expansion)
)
layers = []
layers.append(Bottleneck(self.in_channels, out_channels, stride, downsample ) )
self.in_channels = out_channels * Bottleneck.expansion #这里只变输入是因为卷积的输出都是固定的
for _ in range(1, layer_num):
layers.append( Bottleneck(self.in_channels, out_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.fc(x)
return x