resnet
Deep Residual Learning for Image Recognition (2015-12)
https://arxiv.org/abs/1512.03385
ResNet 最重要的贡献就是short cut,通过增加恒等映射,解决模型退化的问题。
为什么可以解决模型退化问题呢?
首先什么是模型退化问题,也就是加深简单的模型,不能直接提升模型的性能。
我们在训练模型的时候,使用BP算法,进行反向传播,反向传播的导数经层层传递,虽然过程中可以增加BN层避免梯度消失(更何况ResNet出现的时候,BN仍然不普及),导数在模型浅层本来就很少,同时计算机的计算精度float32/float32有限,更新浅层参数的梯度不够精确,导致模型无法有效训练。
为什么加上了shortcut,模型就不退化呢?
主要观点总结如下:
- 避免梯度弥散
梯度弥散,和梯度爆炸(vanishing/exploding gradients)在原文中就提到,可以使用规范初始化(normalized initialization)及 中间规范层(intermediate normalization layers)解决这个问题。
但是这样模型肯定可以训,但是训的好不好就不一定了,因为中间规范层其实就是一个拉分布的过程,每一次模型的输入不一样,它的分布当然就不一样,每一次训练把tensor没有理由的拉到同一分布,势必损失了信息,而之后的BN则是优化了这一过程。
所以resnet的出发点,仍然是去解决梯度弥散和爆炸的问题。加上了shortcut 其实就解决了梯度弥散的问题。
前 向 传 播 z = x + f ( x ) a = r e l u ( x ) 反 向 传 播 ∂ a ∂ x = α a α z ⋅ α z α x = 1 + α z α f ⋅ α f α x 前向传播 \\ z=x+f(x) \\ a=relu(x)\\ 反向传播\\ \frac{\partial a}{\partial x}=\frac{\alpha a}{\alpha z} \cdot \frac{\alpha z}{\alpha x}\\ =1+ \frac{\alpha z}{\alpha f} \cdot \frac{\alpha f}{\alpha x} 前向传播z=x+f(x)a=relu(x)反向传播∂x∂a=αzαa⋅αxαz=1+αfαz⋅αxαf
这个1 是一直存在的,避免了梯度弥散的问题。
-
特征冗余
认为在正向卷积时,通过感受野与张量的相互制约,逐渐提取高级语义特征。
每一次卷积,就是一次提取语义特征的过程。可以想象语义特征的提取需要依赖与各个层级的特征。低级语义特征是检测边缘,中级语义特征检测形状,及背景,我们在做分类问题,这些都是需要考虑的。而传统卷积网络高级语义特征生成层只接受前一层的信息,信息必然丢失严重,就像是传话游戏,中间有失误的话,误差就会不断变大。增加了shortcut之后,深层网络至少接受了 1 2 n \frac{1}{2n} 2n1的浅层网络信息,一定程度上保留了更多的原始信息。这个 n n n代表前 n n n层的信息,假设信息是1+1这样的形式。
后面的densenet发展了这个优势。 -
ensambling
应该在下面的论文会说。 -
luck node
luck node 源于ICLR 2019 best paper 《THE LOTTERY TICKET HYPOTHESIS: FINDING SPARSE, TRAINABLE NEURAL NETWORKS》。https://arxiv.org/abs/1803.03635
这篇论文最大的贡献就是提出了新的思路去理解神经网络。
简单概述就是一句话,神经网络不仅仅是在学习参数,更重要的是在学习一种结构。
这篇论文通过剪枝实验证明了神经网络中存在子网络,剪枝出的子网络重新初始化,训练更快,预测更准,泛化性更好。
整个神经网络中按照不同的训练集训练存在某一些结点,成为 luck node 对整个网络的新能贡献最大, luck node仅占整个网络的1/10。
作者认为训练神经网络就像买彩票,买越多张彩票,中奖的几率就越高。所以往往大的网络效果更好,实际上是大网络中的子网络更优。
那么用luck node 去解释resnet,加上了shortcut结构,增加了前后层之间的联系,更加容易寻找到luck node,同时由于shortcut 的存在,最优子网络的结构更加丰富,对模型性能的提升就更加明显。
resnet的实现上有两个重要的点:
- 残差块必须包含shortcut结构以及至少两层网络,例如两个卷积层以及一个激活层。
少于两层,简单推导就可以证明残擦块退化为一层网络。
满足这个条件后,保证shortcut,卷积的变化,激活函数的变化,至少不会降低模型的性能,能不能提高就看实验结果了。如后面的论文。 - tensor的size 与 channel 成反比。
简单解释一下 pytorch 的实现。
BasicBlock
18 和 34 层的resnet使用BasicBlock作为残差块。
每一个残差块包含两个卷积层。
class BasicBlock(nn.Module):
# 可以看到上面的表格resnet是按照四个层顺序组成的。
# 层与层之间有若干个残差块,
# 在层内传输的时候,输入输出一致,stride为1, downsample = None;
# 在层与层之间,通道数增加4倍,张量高宽缩小1倍,面积缩小4倍。这个时候,如果需要shortcut就需要对输入的张量下采样一次,保证通道数和张量高宽一致。
# downsample 使用1x1 卷积实现。
expansion = 1 #表示使用bottle结构,expansion表示输出扩张的倍数。
def __init__(self, inplanes, planes, stride=1, downsample=None, groups=1,
base_width=64, dilation=1, norm_layer=None):
super(BasicBlock, self).__init__()
if norm_layer is None:
norm_layer = nn.BatchNorm2d
if groups != 1 or base_width != 64:
raise ValueError('BasicBlock only supports groups=1 and base_width=64')
if dilation > 1:
raise NotImplementedError("Dilation > 1 not supported in BasicBlock")
# Both self.conv1 and self.downsample layers downsample the input when stride != 1
self.conv1 = conv3x3(inplanes, planes, stride)
self.bn1 = norm_layer(planes)
self.relu = nn.ReLU(inplace=True)
self.conv2 = conv3x3(planes, planes)
self.bn2 = norm_layer(planes)
self.downsample = downsample
self.stride = stride
def forward(self, x):
identity = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = self.relu(out)
return out
Bottleneck
50 ,101,152 层的resnet使用Bottleneck作为残差块。
class Bottleneck(nn.Module):
# 和BasicBlock一样,resnet是按照四个层顺序组成的,层与层之间有若干个残差块。
# 在层内传输的时候,输出的chanel是输入的4倍,所以每一个残擦块的第一层1x1卷积都要改变通道数。
# 在层与层之间,通道数增加4倍,张量高宽缩小1倍,面积缩小4倍。这个时候,如果需要shortcut就需要对输入的张量下采样一次,保证通道数和张量高宽一致。
# downsample 使用1x1 卷积实现。
expansion = 4 #表示使用bottle结构,expansion表示输出扩张的倍数。
def __init__(self, inplanes, planes, stride=1, downsample=None, groups=1,
base_width=64, dilation=1, norm_layer=None):
super(Bottleneck, self).__init__()
if norm_layer is None:
norm_layer = nn.BatchNorm2d
width = int(planes * (base_width / 64.)) * groups
# Both self.conv2 and self.downsample layers downsample the input when stride != 1
self.conv1 = conv1x1(inplanes, width)
self.bn1 = norm_layer(width)
self.conv2 = conv3x3(width, width, stride, groups, dilation)
self.bn2 = norm_layer(width)
self.conv3 = conv1x1(width, planes * self.expansion)
self.bn3 = norm_layer(planes * self.expansion)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
self.stride = stride
def forward(self, x):
identity = 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)
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = self.relu(out)
return out
resnet
把不重要的判断,以及初始化删掉了。
class ResNet(nn.Module):
def __init__(self, block, layers, num_classes=1000):
super(ResNet, self).__init__()
norm_layer = nn.BatchNorm2
self.inplanes = 64
self.conv1 = nn.Conv2d(3, self.inplanes, kernel_size=7, stride=2, padding=3, bias=False)
self.bn1 = norm_layer(self.inplanes)
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.fc = nn.Linear(512 * block.expansion, num_classes)
def _make_layer(self, block, planes, blocks, stride=1, dilate=False):
# 重要的就是这个_make_layer函数
# 主要就是去按照表格去生成每一层的残差块。
# 每一层的第一个残擦块单独处理,主要不同就是增加了downsample。
# self.inplanes = planes * block.expansion 按照残差块的不同expansion决定了残擦块的输入channel的变化。
norm_layer = self._norm_layer
downsample = None
if stride != 1 or self.inplanes != planes * block.expansion:
downsample = nn.Sequential(
conv1x1(self.inplanes, planes * block.expansion, stride),
norm_layer(planes * block.expansion),
)
layers = []
layers.append(block(self.inplanes, planes, stride, downsample, groups=1,
base_width = 64, dilation = False, norm_layer = norm_layer))
self.inplanes = planes * block.expansion
for _ in range(1, blocks):
layers.append(block(self.inplanes, planes, groups=self.groups,
base_width=64, dilation=False,norm_layer = norm_layer))
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 = torch.flatten(x, 1)
x = self.fc(x)
return x
此图去掉最后的FC层.
参考
【resnet 最好讲解】https://blog.csdn.net/chenyuping333/article/details/82344334
【Resnet overview】https://towardsdatascience.com/an-overview-of-resnet-and-its-variants-5281e2f56035
【Resnet 理解】https://blog.csdn.net/lanran2/article/details/79057994
【Resnet 吴恩达课程】https://blog.csdn.net/qq_29893385/article/details/81207203
【Resnet 碎碎念】https://blog.csdn.net/u014296502/article/details/80438616
【Resnet emsable解释】https://blog.csdn.net/Buyi_Shizi/article/details/53336192
【Resnet 个人理解】https://blog.csdn.net/nini_coded/article/details/79582902
【Resnet 的解释和有趣的点】https://blog.csdn.net/qq_21190081/article/details/75933329
【luck node】https://zhuanlan.zhihu.com/p/65161889
【模型压缩】https://accepteddoge.com/cnblogs/mldl/network-compression
ResNet及其变种的结构梳理、有效性分析与代码解读
resnet18
BasicBlock, [2, 2, 2, 2]
2 ∗ ( 2 + 2 + 2 + 2 ) + 1 + 1 = 18 2*(2+2+2+2)+1+1 = 18 2∗(2+2+2+2)+1+1=18
BasicBlock 包含 2个 3 × 3 3\times3 3×3 卷积.
共有8个Block, 加上第一次卷积和最后的全连接层,共18层.
resnet34
BasicBlock, [3, 4, 6, 3]
2 ∗ ( 3 + 4 + 6 + 3 ) + 1 + 1 = 34 2*(3+4+6+3)+1+1 = 34 2∗(3+4+6+3)+1+1=34
resnet50
Bottleneck, [3, 4, 6, 3]
3 ∗ ( 3 + 4 + 6 + 3 ) + 1 + 1 = 50 3*(3+4+6+3)+1+1 = 50 3∗(3+4+6+3)+1+1=50
Bottleneck 包含3个卷积, 同时, 卷积1,3为 1 × 1 1\times1 1×1 卷积负责对channel变化, 卷积2 为 3 × 3 3\times 3 3×3 卷积负责对处理特征,如果是layer2,3,4的第一个Bottleneck, 还要负责下采样.
Bottleneck对于channel的处理如下表所示
layer | Bottleneck1 | 其他 Bottleneck |
---|---|---|
layer1 | 64 → 64 → 256 64\rightarrow64\rightarrow256 64→64→256 | 256 → 64 → 256 256\rightarrow64\rightarrow256 256→64→256 |
layer2 | 256 → 128 → 512 256\rightarrow128\rightarrow512 256→128→512 | 512 → 128 → 512 512\rightarrow128\rightarrow512 512→128→512 |
layer3 | 512 → 256 → 1024 512\rightarrow256\rightarrow1024 512→256→1024 | 1024 → 512 → 1024 1024\rightarrow512\rightarrow1024 1024→512→1024 |
layer4 | 1024 → 512 → 2048 1024\rightarrow512\rightarrow2048 1024→512→2048 | 2048 → 512 → 2048 2048\rightarrow512\rightarrow2048 2048→512→2048 |
以layer2为例, Conv2d-40负责对输入特征下采样2倍, 并且增加channel为输入channel数的2倍.
通过使用 1 × 1 1\times1 1×1 卷积对特征降维, 减少了计算参数, 增加了非线性, 进一步降低了模型过拟合的问题.
Bottleneck-36 [-1, 256, 64, 32] 0
***************************************************************************
Conv2d-37 [-1, 128, 64, 32] 32,768
Conv2d-40 [-1, 128, 32, 16] 147,456
Conv2d-43 [-1, 512, 32, 16] 65,536
Bottleneck-48 [-1, 512, 32, 16] 0
Conv2d-49 [-1, 128, 32, 16] 65,536
Conv2d-52 [-1, 128, 32, 16] 147,456
Conv2d-55 [-1, 512, 32, 16] 65,536
Bottleneck-58 [-1, 512, 32, 16] 0
Conv2d-59 [-1, 128, 32, 16] 65,536
Conv2d-62 [-1, 128, 32, 16] 147,456
Conv2d-65 [-1, 512, 32, 16] 65,536
Bottleneck-68 [-1, 512, 32, 16] 0
Conv2d-69 [-1, 128, 32, 16] 65,536
Conv2d-72 [-1, 128, 32, 16] 147,456
Conv2d-75 [-1, 512, 32, 16] 65,536
Bottleneck-78 [-1, 512, 32, 16] 0
***************************************************************************
resnet101
Bottleneck, [3, 4, 23, 3]
resnet152
Bottleneck, [3, 8, 36, 3]
我们注意到resnet 50/101/152 在layer1 和 layer4 都堆叠4次, 通过重复堆叠layer2, 和layer 3增加网络层数.
但是为什么这样堆, 为什么不是 3 ,8 ,15 ,3 或者 3, 11, 12, 3这样, 希望知道的同学可以留言.
那为什么前后都为3呢?
直观来讲, 模型在浅层处理颜色, 风格, 形状等浅层特征, 这些特征的处理并不需要特别深的模型, 而随着下采样, 感受野的扩大, 模型逐渐学习语义特征, 需要更多的参数及非线性运算. 在最layer4, channel数最终增加到2048, 需要参与卷积运算也随之增加, 参数量也在增加, 为了控制模型的参数数量, layer4的Bottleneck也应该设计的比较小.
resnetv2
Identity Mappings in Deep Residual Networks (2016-03)
https://arxiv.org/abs/1603.05027
如图所示, 在每一个Block跳跃连接处, 去掉了relu操作, 进一步维护了特征的完整性, 保持了恒定映射.
参考
【resnet 最好讲解】https://blog.csdn.net/chenyuping333/article/details/82344334
【resNet v2 翻译】https://blog.csdn.net/wspba/article/details/60750007