《Deep Residual Learning for Image Recognition》论文阅读笔记
0.基础知识:
0.1残差:
假设真实模型为:
‘
![=](https://i-blog.csdnimg.cn/blog_migrate/5625a314d89f62e2ba67c20d3de6a69b.png)
真实模型的含义是,变量X通过线性形式来影响Y,但是始终存在随机波动,那么我们用u来表示这样的随机波动,或者称为随机误差。通过输入一系类x可以得到一些列y,这样就得到的一、
0.2恒等映射: 对于映射f,若它的定义域A和值域B相等,并对所有的均有
时,则称f为恒等映射。
0.3FLOPsfloating point operations的缩写(s表复数): 意指浮点运算数,理解为计算量。可以用来衡量算法/模型的复杂度。
1.残差模块
残差模块就是来收集浅层信息,通过卷积来计算残差,反向传播来修正卷积核。原文是:the added layers are identity mapping, and the other layers are copied from the learned shallower(作者把残差产生并融合的过程恒等映射,残差就是提取特征浅层的信息),添加的层是恒等映射,其他层是从学习到的较浅模型的拷贝。 这种构造解决方案的存在表明,较深的模型不应该产生比其对应的较浅模型更高的训练误差。残差本质是y=x+f(x),激活后f(x)>=0,在求梯度时为1+f’(x),保证梯度始终在1附近,这样两式一起就保证了“就算没学到有用的东西,也不会产生不利的信息,避免了梯度消失”
明神把这个过程抽象化之后-----最常见状态:y = F(x,{Wi}) + x,前项就是残差,后向就是恒等映射,在反向传播求导的时候,很漂亮1+f’(x)
当传递的时候需要改变大小的时候,因为特征图谱在传递的时候缩放了,所以残差也要跟着做改变:y = F(x,{Wi}) + Wx.下图的实线就是第一个映射,虚线就是第二个映射,作者把这种映射叫做shoutcut
当维度增加(图3中的虚线快捷连接)时,我们考虑两个选项:(A)快捷连接仍然执行恒等映射,额外填充零输入以增加维度。此选项不会引入额外的参数;(B)方程(2)中的投影快捷连接用于匹配维度(由1×1卷积完成)。对于这两个选项,当快捷连接跨越两种尺寸的特征图时,它们执行时步长为2。
左边的是vgg。中间的是何凯明模仿vgg的结构做的34层vgg,最后是在残差模块增幅下的vgg–resnet
本文中的实验包括有两层或三层的残差模块/映射模块,也可以是很深层的。但如果映射只有一层,类似于线性层:y=Wx+x,在实验中没有任何优势。何凯明把2层卷积的叫做BasicBlock,三层的叫做Bottleneck
2.代码学习
def resnet18(pretrained=False, hr_pretrained=False, **kwargs):
"""Constructs a ResNet-18 model.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
*args称之为Non-keyword Variable Arguments,无关键字参数;
**kwargs称之为keyword Variable Arguments,有关键字参数;
"""
model = ResNet(BasicBlock, [2, 2, 2, 2], **kwargs)
if pretrained:
# strict = False as we don't need fc layer params.
if hr_pretrained:
print('Loading the high resolution pretrained model ...')
model.load_state_dict(torch.load("backbone/weights/resnet18_hr_10.pth"), strict=False)
else:
model.load_state_dict(model_zoo.load_url(model_urls['resnet18']), strict=False)
return model
需要传三组组参数,block:指明残差块是BasicBlock还是bottleblock的,layers:传给_make_layer用的,zero_init_residual是否要用0来初始化残差,不用的就用何凯明正态来初始化
class ResNet(nn.Module):
def __init__(self, block, layers, zero_init_residual=False):
super(ResNet, self).__init__()
self.inplanes = 64
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)
for m in self.modules(): #这就话就是把模块的每一层进行判别
if isinstance(m, nn.Conv2d): #如果这一层是二维卷积层,就用何凯明的正态分布初始化
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
elif isinstance(m, nn.BatchNorm2d):#BatchNorm2d归一化也是针对与relu的,数据在进行Relu之前不会因为数据过大而导致网络性能的不稳定
nn.init.constant_(m.weight, 1) #对归一化的权重和偏执做初始化
nn.init.constant_(m.bias, 0)
# Zero-initialize the last BN in each residual branch,
# so that the residual branch starts with zeros, and each residual block behaves like an identity.
# This improves the model by 0.2~0.3% according to https://arxiv.org/abs/1706.02677
if zero_init_residual:
for m in self.modules():
if isinstance(m, Bottleneck):
nn.init.constant_(m.bn3.weight, 0)
elif isinstance(m, BasicBlock):
nn.init.constant_(m.bn2.weight, 0)
# torch.nn.init.constant_(tensor, val)[source] 用值val填充向量。
def forward(self, x):
C_1 = self.conv1(x)
C_1 = self.bn1(C_1)
C_1 = self.relu(C_1)
C_1 = self.maxpool(C_1)
C_2 = self.layer1(C_1)
C_3 = self.layer2(C_2)
C_4 = self.layer3(C_3)
C_5 = self.layer4(C_4)
return C_3, C_4, C_5
凯明正态化:
def _make_layer(self, block, planes, blocks, stride=1):
downsample = None
# a这个函数就是分化残差模块用的,有的残差要提前进行下采样,然后再插进去
# a即如果该层的输入的通道数inplanes和其输出的通道数的数planes * block.expansion不同,
# a那要使用1*1的卷积核将输入x低维转成高维进行下采样,相当于之前函数里的w,然后才能进行相加
if stride != 1 or self.inplanes != planes * block.expansion:
downsample = nn.Sequential(
conv1x1(self.inplanes, planes * block.expansion, stride),
nn.BatchNorm2d(planes * block.expansion),
)
layers = []
layers.append(block(self.inplanes, planes, stride, downsample))
self.inplanes = planes * block.expansion
for _ in range(1, blocks):
layers.append(block(self.inplanes, planes))
return nn.Sequential(*layers)
block参数指定是两层残差块或三层残差块,planes参数为输入的channel数,blocks说明该卷积有几个残差块
《Resnet v2:Identity Mappings in Deep Residual Networks》阅读笔记
作者认为在主路传递的时候尽量要简单一点(keeping a “clean” information path ),因为这样有利于反向传播,通过实验不同的残差模块发现,在浅层网络中并没什么多大卵用,但在深层网络中由于原始的resnet有些拉跨,而v2没有发散收敛的很好,所以残差的计算和传递还是有很大的改进空间的,不能拍脑袋想,多看看数值分析看看能不能算出数值解来。
1.恒等映射
这篇论文的思路就是要把灰色通道弄得整洁明了,所以何凯明想要用恒等映射来取代relu函数(这里的恒等映射,何凯明直接当f(x)=x来用了,因为在传线性参数时候可能会导致梯度爆炸/小时),而且恒等映射后产生的信息可以从一个单元传播到任何其他单元(没有证明,也没法证明)。
1.1反向传播
恒等变换的优势就是反向传播比较简洁,原始的传递是:
y
l
=
h
(
x
l
)
+
F
(
x
l
,
W
l
)
,
x
l
+
1
=
f
(
y
l
)
.
{y}_{l} = h({x}_{l}) + \mathcal{F}({x}_{l}, \mathcal{W}_l), \\ {x}_{l+1} = f({y}_{l}) .
yl=h(xl)+F(xl,Wl),xl+1=f(yl).
在其中的
x
l
{x}_{l}
xl是输入
x
l
+
1
{x}_{l+1}
xl+1是输出,所以而不是
y
l
{y}_{l}
yl,要加一层relu
f
(
x
)
f(x)
f(x),而relu函数是一个非线性函数,在求反向传播的时候不是很方便,尽管在
x
>
x
’
x>x’
x>x’时候是原函数。
现在把relu直接换成恒等映射
h
(
x
l
)
=
x
l
h({x}_{l}) = {x}_{l}
h(xl)=xl,就有了新的残差传递函数了:
x
l
+
1
=
x
l
+
F
(
x
l
,
W
l
)
{x}_{l+1} = {x}_{l} + \mathcal{F}({x}_{l}, \mathcal{W}_{l})
xl+1=xl+F(xl,Wl)
可以递归,往深层传播,这就契合了之前说的一层可以传播的任意一层:
x
L
=
x
l
+
∑
i
=
l
L
−
1
F
(
x
i
,
W
i
)
{x}_{L} = {x}_{l} + \sum_{i=l}^{L-1}\mathcal{F}({x}_{i}, \mathcal{W}_{i})
xL=xl+i=l∑L−1F(xi,Wi)
这个函数展现了一些很有意思的事情:任意单元之间都可以算残差;然后对于任意深的单元他的计算都是分两部分–x和一个残差和。
求导得:
∂
E
∂
x
l
=
∂
E
∂
x
L
∂
x
L
∂
x
l
=
∂
E
∂
x
L
(
1
+
∂
∑
i
=
l
L
−
1
F
(
x
i
,
W
i
)
∂
x
l
)
.
\frac{\partial E}{\partial {{x}_{l}}}=\frac{\partial E}{\partial {{x}_{L}}}\frac{\partial {{x}_{L}}}{\partial {{x}_{l}}}=\frac{\partial E}{\partial {{x}_{L}}}\left(1+\frac{\partial {\sum_{i=l}^{L-1}\mathcal{F}({x}_{i}, \mathcal{W}_{i})}}{\partial {{x}_{l}}}\right).
∂xl∂E=∂xL∂E∂xl∂xL=∂xL∂E(1+∂xl∂∑i=lL−1F(xi,Wi)).
这个有意思的事情在求导后更有意思了:求导部分也是分成两部分
∂
E
∂
x
L
\frac{\partial E}{\partial {{x}_{L}}}
∂xL∂E直接传递信息而不涉及任何权重层,保证了信息能够直接传回任意浅层 .而另一部分
∂
E
∂
x
L
(
∂
∑
i
=
l
L
−
1
F
∂
x
l
)
\frac{\partial E}{\partial {{x}_{L}}}\left(\frac{\partial {\sum_{i=l}^{L-1}\mathcal{F}}}{\partial {{x}_{l}}}\right)
∂xL∂E(∂xl∂∑i=lL−1F)表示通过权重层的传递。
第一部分保证了梯度最起码是1,第二部分何凯明在实验的过程中几乎没遇到是-1的情况
1.2反证法
之前已知在强调灰色通道要尽量的干净一些,那么不干净会发生什么?
证明:
给输如加个权重:
x
l
+
1
=
λ
l
x
l
+
F
(
x
l
,
W
l
)
{x}_{l+1} = \lambda_l{x}_{l} + \mathcal{F}({x}_{l}, \mathcal{W}_{l})
xl+1=λlxl+F(xl,Wl)
再求和求导后得到:
∂
E
∂
x
l
=
∂
E
∂
x
L
(
(
∏
i
=
l
L
−
1
λ
i
)
+
∂
∑
i
=
l
L
−
1
F
^
(
x
i
,
W
i
)
∂
x
l
)
.
\frac{\partial E}{\partial {{x}_{l}}}=\frac{\partial E}{\partial {{x}_{L}}}\left((\prod_{i=l}^{L-1}\lambda_{i})+\frac{\partial {\sum_{i=l}^{L-1}\mathcal{\hat{F}}({x}_{i}, \mathcal{W}_{i})}}{\partial {{x}_{l}}}\right).
∂xl∂E=∂xL∂E((i=l∏L−1λi)+∂xl∂∑i=lL−1F^(xi,Wi)).
这是这时候有意思的1被
∏
i
=
l
L
−
1
λ
i
.
\prod_{i=l}^{L-1}\lambda_{i}.
∏i=lL−1λi.替代之后就就极大的提高了梯度消失/爆炸的概率。如果对于所有的i都有 λi>1,那么这个因子将会是指数型的放大;如果 λi<1 ,那么这个因子将会是指数型的缩小或者是消失,从而阻断从捷径反向传来的信号,并迫使它流向权重层。
证毕
2.实验
何凯明基于clear的灰色通道,猜测了很多shotcut模块,几乎没有一个能打的。就之前说的简单的去掉relu的在深层中表现的比较好一点
3.分析
开始炼丹:
对于每个图右侧部分我们称作“residual”分支,左侧部分我们称作“identity”分支,如果ReLU作为“residual”分支的结尾,我们不难发现“residual”分支的结果永远非负,这样前向的时候输入会单调递增,从而会影响特征的表达能力,所以我们希望“residual”分支的结果应该在(-∞, +∞);这点也是我们以后设计网络时所要注意的
ReLU before addition :这导致了 F的输出为非负
BN after addition:在将f调整至恒等映射之前,我们先反其道而行之,在加法后添加一个BN(Fig.4(b))。这样f 就包含了BN和ReLU。这样的结果比基本结构的结果要差很多(Table2)。不像原始的设计,目前的BN层改变了流经捷径连接的信号,并阻碍了信息的传递,这从训练一开始降低训练误差的困难就可以看出
Post-activation or pre-activation:他们的导数都是
x
l
+
1
=
x
l
+
F
(
f
^
(
x
l
)
,
W
l
)
{x}_{l+1} = {x}_{l} + \mathcal{F}(\hat{f}({x}_{l}), \mathcal{W}_{l})
xl+1=xl+F(f^(xl),Wl),和之前的一样,所以结果接近,轻微差距可能的这炉没练好,下一炉子可能好一点。