之前我用pytorch把ResNet18实现了一下,但由于上周准备国家奖学金答辩没有时间来写我实现的过程与总结。今天是祖国70周年华诞,借着这股喜庆劲,把这篇文章补上。
References:
电子文献:
https://blog.csdn.net/weixin_43624538/article/details/85049699
https://blog.csdn.net/u013289254/article/details/98785869
参考文献:
[1]Deep Residual Learning for Image Recognition
简介
ResNet残差网络是由何恺明等四名微软研究院的华人提出的,当初看到论文标题下面的中国名字还是挺高兴的。文章引入部分,作者就探讨了深度神经网络的优化是否就只是叠加层数、增加深度那么简单。显然这是不可能的,增加深度带来的首要问题就是梯度爆炸、消散的问题,这是由于随着层数的增多,在网络中反向传播的梯度会随着连乘变得不稳定,从而变得特别大或者特别小。其中以梯度消散更为常见。值得注意的是,论文中还提到深度更深的网络反而出现准确率下降并不是由过拟合所引起的。
为了解决这个问题,研究者们做出了很多思考与尝试,其中的代表有relu激活函数的使用,Batch Normalization的使用等。关于这两种方法,可以参考网上的资料以及我的博文deep-learning笔记:开启深度学习热潮——AlexNet和deep-learning笔记:学习率衰减与批归一化。
对于上面这个问题,ResNet作出的贡献是引入skip/identity connection。如下所示就是两个基本的残差模块。
上面这个block可表示为:$ F(X)=H(X)-x $。在这里,X为浅层输出,H(x)为深层的输出。当浅层的X代表的特征已经足够成熟,即当任何对于特征X的改变都会让loss变大时,F(X)会自动趋向于学习成为0,X则从恒等映射的路径继续传递。
这样,我们就可以在不增加计算成本的情况下使得在前向传递过程中,如果浅层的输出已经足够成熟(optimal),那么就让深层网络后面的层仅实现恒等映射的作用。
当X与F(X)通道数目不同时,作者尝试了两种identity mapping的方式。一种即对X缺失的通道直接补零从而使其能够对齐,这种方式比较简单直接,无需额外的参数;另一种则是通过使用1x1的conv来映射从而使通道也能达成一致。
论文
老规矩,这里还是先呈上我用黄色荧光高亮出我认为比较重要的要点的论文原文,这里我只有英文版。
如果需要没有被我标注过的原文,可以直接搜索,这里我仅提供一次,可以点击这里下载。
不过,虽然没有pdf中文版,但其实深度学习CV方向一些比较经典的论文的英文、中文、中英对照都可以到Deep Learning Papers Translation上看到,非常方便。
自己实现
在论文中,作者提到了如下几个ResNet的版本的结构。
这里我实现的是ResNet18。
由于这不是我第一次使用pytorch进行实现,一些基本的使用操作我就不加注释了,想看注释来理解的话可以参考我之前VGG的实现。
由于残差的引入,导致ResNet的结构比较复杂,而论文中并没有非常详细的阐述,在研究官方源码之后,我对它的结构才有了完整的了解,这里我画出来以便参考。
注:此模块在2016年何大神的论文中给出了新的改进,可以参考我的博文deep-learning笔记:记首次ResNet实战。
ResNet18的每一layer包括了两个这样的basic block,其中1x1的卷积核仅在X与F(X)通道数目不一致时进行操作,在我的代码中,我定义shortcut函数来对应一切通道一致、无需处理的情况。
1 | import torch |
同样的,我们可以随机生成一个张量来进行验证:
1 | input = torch.randn(1, 3, 48, 48) |
如果可以顺利地输出,那么模型基本上是没有问题的。
出现的问题
在这里我还是想把自己踩的一些简单的坑记下来,引以为戒。
softmax输出全为1
当我使用F.softmax之后,出现了这样的一个问题:
查找资料后发现,我错误的把对每一行softmax当作了对每一列softmax。因为这个softmax语句是我从之前的自己做的一道kaggle题目写的代码中ctrl+C+V过来的,复制过来的是
x = F.softmax(x, dim = 0)
,在这里,dim = 0意味着我对张量的每一列进行softmax,这是因为我之前的场景中需要处理的张量是一维的,也就是tensor()里面只有一对“[]”,此时它默认只有一列,我对列进行softmax自然就没有问题。
而放到这里,我再对列进行softmax时,每列上就只有一个元素。那么结果就都是1即100%了。解决的方法就是把dim设为1。
下面我在用一组代码直观地展示一下softmax的用法与区别。1
2
3
4
5
6
7import torch
import torch.nn.functional as F
x1= torch.Tensor( [ [1, 2, 3, 4], [1, 3, 4, 5], [3, 4, 5, 6]])
y11= F.softmax(x1, dim = 0) #对每一列进行softmax
y12 = F.softmax(x1, dim = 1) #对每一行进行softmax
x2 = torch.Tensor([1, 2, 3, 4])
y2 = F.softmax(x2, dim = 0)我们输出每个结果,可以看到:
bias
或许你可以发现,在我的代码中,每个卷积层中都设置了bias = False
,这是我在参考官方源码之后补上的。那么,这个bias是什么,又有什么用呢?
我们在学深度学习的时候,最早接触到的神经网络应该是感知器,它的结构如下图所示。 要想激活这个感知器,就必须使x1 * w1 + x2 * w2 + ... + xn * wn > T
(T为一个阈值),而T越大,想激活这个感知器的难度越大。
考虑样本较多的情况,我不可能手动选择一个阈值,使得模型整体表现最佳,因此我们不如使得T变成可学习的,这样一来,T会自动学习到一个数,使得模型的整体表现最佳。当把T移动到左边,它就成了bias偏置,x1 * w1 + x2 * w2 + ... + xn * wn - T > 0
。显然,偏置的大小控制着激活这个感知器的难易程度。
在比感知器高级的神经网络中,也是如此。
但倘若我们要在卷积后面加上归一化操作,那么bias的作用就无法体现了。
我们以ResNet卷积层后的BN层为例。
可参考我的上一篇博文,BN处理过程中有这样一步: 对于分子而言,无论有没有bias,对结果都没有影响;而对于下面分母而言,因为是方差操作,所以也没有影响。因此,在ResNet中,因为每次卷积之后都要进行BN操作,那就不需要启用bias,否则非但不起作用,还会消耗一定的显卡内存。
官方源码
如果你此时对ResNet的结构已经有了比较清晰的理解,那么可以尝试着来理解一下官方源码的思路。其实我觉得先看像我这样直观的代码实现再看官方源码更有助理解且更高效。
1 | import torch |
pth文件
在阅读官方源码时,我们会注意到官方提供了一系列版本的model_urls,其中,每一个url都是以.pth结尾的。
当我下载了对应的文件之后,并不知道如何处理,于是我通过搜索,简单的了解了pth文件的概念与使用方法。
简单来说,pth文件就是一个表示Python的模块搜索路径(module search path)的文本文件,在xxx.pth文件里面,会书写一些路径,一行一个。如果我们将xxx.pth文件放在特定位置,则可以让python在加载模块时,读取xxx.pth中指定的路径。
下面我使用pytorch对pth文件进行加载操作。
首先,我把ResNet18对应的pth文件下载到桌面。
1 | import torch |
输出结果如下:
1 | ResNet( |
这样你就可以看到很详尽的参数设置了。
我们还可以加载所有的参数。
1 | import torch |
输出如下:
1 | OrderedDict([('conv1.weight', Parameter containing: |