随着卷积神经网络的发展和普及,我们了解到增加神经网络的层数可以提高模型的训练精度和泛化能力,但简单地增加网络的深度,可能会出现“梯度弥散”和“梯度爆炸”等问题。传统对应的解决方案则是权重的初始化(normalized initializatiton)和批标准化(batch normlization),这样虽然解决了梯度的问题,但深度加深了,却带来了另外的问题,就是网络性能的退化问题。
一、什么是网络的退化问题?
由上图可以看出,56-layer(层)的网络比20-layer的网络在训练集和测试集上的表现都要差【注意:这里不是过拟合(过拟合是在训练集上表现得好,而在测试集中表现得很差)】,说明如果只是简单的增加网络深度,可能会使神经网络模型退化,进而丢失网络前面获取的特征。
网络退化:在增加网络层数的过程中,training accuracy (精度)逐渐趋于饱和,继续增加层数,training accuracy 就会出现下降的现象,而这种下降不是由过拟合造成的。实际上较深模型后面添加的不是恒等映射,而是一些非线性层。
退化问题的实质:通过多个非线性层来近似恒等映射可能是困难的(恒等映射亦称恒等函数:是一种重要的映射,对任何元素,象与原象相同的映射)。神经网络在反向传播过程中要不断传播梯度,而当层数加深,梯度在传播过程中逐渐消失【而梯度消失则是导致网络退化的一个重要因素】,导致无法对前面网络的权重进行有效调整(这里大家可以想一下一条很长的绳子从头抖一下,有时候是不能影响末端的。
二、残差网络解决的问题
神经网络越来越深的时候,反传回来的梯度之间的相关性会越来越差,最后接近白噪声。因为我们知道图像是具备局部相关性的,那其实可以认为梯度也应该具备类似的相关性,这样更新的梯度才有意义,如果梯度接近白噪声,那梯度更新可能根本就是在做随机扰动。
何凯明提出残差网络概念的论文地址:https://arxiv.org/abs/1512.03385
如果深层网络的后面那些层是恒等映射,那么模型就退化为一个浅层网络。那当前要解决的就是学习恒等映射函数了。 但是直接让一些层去拟合一个潜在的恒等映射函数:,比较困难,这可能就是深层网络难以训练的原因。但是,如果把网络设计为:
1.残差块原理:
一个残差块的数学模型如下图示。残差网络和之前的网络最大的不,同就是多了一条identity的捷径分支。而因为这一条分支的存在,使得网络在反向传播时,损失可以通过这条捷径将梯度直接传向更前的网络,从而减缓了网络退化的问题。在第二节分析网络退化的原因时,我们了解到梯度之间是有相关性的。我们在有了梯度相关性这个指标之后,作者分析了一系列的结构和激活函数,发现resnet在保持梯度相关性方面很优秀,从梯度流来看,有一路梯度是保持原样不动地往回传,这部分的相关性是非常强的。除此之外,残差网络并没有增加新的参数,只是多了一步加法。而在GPU的加速下,这一点额外的计算量几乎可以忽略不计。
不过我们可以看到,因为残差块最后是 F ( x ) + x F(x) + x F(x)+x的操作,那么意味着 F ( x ) F(x) F(x) 与 x x x的shape必须一致。但在实际的网络搭建中,还可以利用1x1的卷积改变通道数目。
如图1。
我们可以转换为学习一个残差函数:,只要:就构成了一个恒等映射:而且,拟合残差肯定更加容易。
F是求和前网络映射,H是从输入到求和后的网络映射。比如把5映射到5.1,那么引入残差前是:
,
引入残差后是:,,
这里的和都表示网络参数映射,引入残差后的映射对输出的变化更敏感。比如S输出从5.1变到5.2,映射的输出增加了2%,而对于残差结构输出从5.1到5.2,映射F是从0.1到0.2,增加了100%。明显后者输出变化对权重的调整作用更大,所以效果更好。残差的思想都是去掉相同的主体部分,从而突出微小的变化。
2、残差块代码示例:
import torch
import torch.nn as nn
#残差块
class Res_Block(nn.Module):
def __init__(self,c):
super(Res_Block, self).__init__()
#残差网络要求整个模型输入和输出的通道一样
self.layer=nn.Sequential(
#增加Padding是为了保持通道一样
nn.Conv2d(in_channels=c, out_channels=c, kernel_size=3, stride=1,padding=1,bias=False),
nn.BatchNorm2d(c),#批标准化
nn.ReLU(),
nn.Conv2d(c, c, 3, 1,1),
nn.BatchNorm2d(c),
nn.ReLU(),
)
def forward(self,x):
#残差网络的应用+x
return self.layer(x)+x
if __name__ == '__main__':
net=Res_Block(3)
x=torch.randn(1,3,28,28)
y=net.forward(x)
print(y.shape)
三、残差网络讨论
至于为何shortcut(捷径)的输入是X,而不是X/2或是其他形式。作者的另一篇文章中探讨了这个问题,对以下6种结构(图2)的残差结构进行实验比较,shortcut是X/2的就是第二种,结果发现还是第一种效果好。
这种残差学习结构可以通过前向神经网络+shortcut连接实现,如结构图1所示。而且shortcut连接相当于简单执行了同等映射,不会产生额外的参数,也不会增加计算复杂度。 而且,整个网络可以依旧通过端到端的反向传播训练。
根据多层的神经网络理论上可以拟合任意函数,那么可以利用一些层来拟合函数。问题是直接拟合 还是残差函数,拟合残差函数更简单。虽然理论上两者都能得到近似拟合,但是后者学习起来显然更容易。作者说,这种残差形式是由退化问题激发的。根据前文,如果增加的层被构建为同等函数,那么理论上,更深的模型的训练误差不应当大于浅层模型,但是出现的退化问题表明,求解器很难去利用多层网络拟合同等函数。但是,残差的表示形式使得多层网络近似起来要容易的多,如果`同等函数可被优化近似,那么多层网络的权重就会简单地逼近0来实现同等映射,即 。
实际情况中,同等映射函数可能不会那么好优化,但是对于残差学习,求解器根据输入的同等映射,也会更容易发现扰动,总之比直接学习一个同等映射函数要容易的多。根据实验,可以发现学习到的残差函数通常响应值比较小,同等映射(shortcut)提供了合理的前提条件。
通过shortcut同等映射
F(x)与x相加就是就是逐元素相加,但是如果两者维度不同,需要给x执行一个线性映射来匹配维度:
用来学习残差的网络层数应当大于1,否则退化为线性。文章实验了layers = 2或3,更多的层也是可行的。
用卷积层进行残差学习:以上的公式表示为了简化,都是基于全连接层的,实际上当然可以用于卷积层。加法随之变为对应channel间的两个feature map逐元素相加。
1.网络结构
作者由VGG19设计出了plain 网络和残差网络,如图3中部和右侧网络。然后利用这两种网络进行实验对比。
设计网络的规则:
1.对于输出feature map大小相同的层,有相同数量的filters,即channel数相同;
- 当feature map大小减半时(池化),filters数量翻倍。
对于残差网络,维度匹配的shortcut连接为实线,反之为虚线。维度不匹配时,同等映射有两种可选方案:直接通过zero padding 来增加维度(channel)、乘以W矩阵投影到新的空间。实现是用1x1卷积实现的,直接改变1x1卷积的filters数目。这种会增加参数。
图3:
ResNet推荐参数如上图所示,作者还用全局平均池化替代了全连接层,一方面减少了参数量,另一方面全连接层易于过拟合并且严重依赖于 dropout 正则化,而全局平均池化本身就是起到了正则化作用,其本身防止整体结构的过拟合。此外,全局平均池汇总了空间信息,因此对输入的空间转换更加健壮。
2.残差网络代码实现:
import torch
import torch.nn as nn
DEVICE=torch.device( "cuda"if torch.cuda.is_available()else"cpu")
#残差块
class Res_Block(nn.Module):
def __init__(self,c):
super(Res_Block, self).__init__()
#残差网络要求整个模型输入和输出的通道一样
self.layer=nn.Sequential(
nn.Conv2d(c, c, 3, 1,padding=1),#增加Padding是为了保持通道一样
nn.ReLU(),
nn.Conv2d(c, c, 3, 1, padding=1),
nn.ReLU(),
)
def forward(self,x):
#残差网络的应用+x
return self.layer(x)+x
#封装Net类
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.layer=nn.Sequential(
nn.Conv2d(1,64,3,1,padding=1),
nn.ReLU(),
Res_Block(64),#实例化对象
Res_Block(64),
Res_Block(64),
Pool(64,128),
Res_Block(128),
Res_Block(128),
Res_Block(128),
Res_Block(128),
Pool(128, 256),
Res_Block(256),
Res_Block(256),
Res_Block(256),
Res_Block(256),
Res_Block(256),
Pool(256,512),
Res_Block(512),
Res_Block(512),
# nn.Linear(512*32*32,10)
)
self.layer2=nn.Sequential(
nn.Linear(512*32*32,10)
)
def forward(self, x):
OUT=self.layer(x)
OUT=OUT.reshape(-1,512*32*32)
return self.layer2(OUT)
#实现下采样
class Pool(nn.Module):
def __init__(self,c_in,c_out):
super(Pool, self).__init__()
self.layer=nn.Sequential(
nn.Conv2d(c_in,c_out,3,1,padding=1),
nn.ReLU(),
nn.Conv2d(c_out,c_out,3,1,padding=1),
nn.ReLU()
)
def forward(self,x):
return self.layer(x)
if __name__ == '__main__':
res=Net()
x=torch.randn(32,1,32,32)
y=res.forward(x)
print(y.shape)
# print(res)
作者探索的更深的网络。 考虑到时间花费,将原来的building block(残差学习结构)改为瓶颈结构,如图4。首端和末端的1x1卷积用来削减和恢复维度,相比于原本结构,只有中间3x3成为瓶颈部分。这两种结构的时间复杂度相似。此时投影法映射带来的参数成为不可忽略的部分(以为输入维度的增大),所以要使用zero padding的同等映射。替换原本ResNet的残差学习结构,同时也可以增加结构的数量,网络深度得以增加。生成了ResNet-50,ResNet-101,ResNet-152. 随着深度增加,因为解决了退化问题,性能不断提升。
总结:
关于残差网络的我还在不断学习中,以上内容大多数来自网络,我只是把他们整理了一下,并用代码实现了,后面可能会对残差网络再进行一次深入学习,拜拜。
版权归原作者 小羊头发长 所有, 如有侵权,请联系我们删除。