CNN14. Residual Networks (ResNets)
1. 梯度弥散与梯度爆炸
1.1 梯度消失(梯度弥散, vanishing gradient)与梯度爆炸(exploding gradient)
1.1.1 梯度不稳定
梯度丢失和梯度爆炸统称为梯度不稳定。它们产生的原因是类似的。为了说明梯度丢失是如何产生的,我们将把问题简化,以链式法则为例来进行说明。
1.1.2 简化问题——链式法则
让我们考虑下面这个简单的深度神经网络,它的每一层都只包含一个神经元,一共有三个隐藏层:
这个等式也反映了反向传播的工作模式:它从输出层开始,逐层计算偏导数,直到输入层为止。然后,利用这些计算出来的偏导数,更新对应的权重和偏置,从而达到反向传播的目的。
但是,我们发现,随着层数的加深,梯度的计算公式会依赖于越来越多每一层的参数,这些参数的影响层层累加,最终就导致了梯度爆炸与梯度消失。
1.1.3 梯度消失(即梯度弥散, vanishing gradient)
Sigmoid函数常常会引发梯度不稳定问题,所以我们以此为研究对象。
它的图像如下:
它的导函数图像如下:
该函数的导数最大值为0.25,且当取值的绝对值变大时,输出会变小。
由于我们初始化权重值的时候一般从标准正态分布中采样,所以权重w的绝对值通常小于1,因此我们可以得到:
再由于公式
最终计算结果将会呈指数级变小,这也就是梯度丢失产生的原因。
1.1.4 梯度爆炸
梯度爆炸产生的原因和梯度丢失正好相反。当我们选取的权重值较大时,将大于1。当累乘这些项的时候,计算结果将呈指数级增长。
2. resnet学习
2.1 提出背景
ResNet最根本的动机就是所谓的“退化”问题,即当模型的层次加深时,错误率却提高了。
自AlexNet以来,state-of-the-art的CNN结构都在不断地变深。AlexNet只有5个,而到了VGG和GoogLeNet已经有有19个和22个卷积层。
然而,我们不能通过简单地叠加层的方式来增加网络的深度。梯度消失问题的存在,使深度网络的训练变得相当困难。“梯度消失”问题指的是即当梯度在被反向传播到前面的层时,重复的相乘可能会使梯度变得无限小。因此,随着网络深度的不断增加,其性能会逐渐趋于饱和,甚至还会开始下降。
在ResNet出现之前,研究人员们发现了几个用于处理梯度消失问题的方法,比如,在中间层添加辅助损失(auxiliary loss)作为额外的监督。但没有一种方法能够一次性彻底解决这一问题。
最后,有人提出了resNet,大大改善了这一情况。
ResNet的基本思想是引入了能够跳过一层或多层的“shortcut connection”,如上图所示。图中“弯弯的弧线“就是所谓的”shortcut connection“,也就是identity mapping。
resNet的作者认为:
- 增加网络的层不应该降低网络的性能,因为我们可以将“恒等变换(identity mapping)”简单地叠加在网络上,而且所得到的输出架构也会执行相同的操作。这就暗示了更深层的模型的训练错误率不应该高于与之对应的浅层模型。
- 他们还作出了这样的假设:与其让它们直接适应所需的底层映射,还是让堆叠的层适应一个残差映射要简单一些。上图所示的残差块能够明确地使它完成这一点。
另外,resNet还采用了reLU方式激活,reLU函数在取值变大时不会发生梯度变小的情况,所以也缓解了梯度消失。
所以说,ResNet的优越之处有两个:identity mapping和reLU激活
2.2 两种block设计
这两种结构分别取自ResNet34(左图)和ResNet50/101/152(右图)。
一般称整个结构为一个”building block“,而右图又称为”bottleneck design”。
右图的目的就是为了降低参数的数目:第一个1x1的卷积把256维channel降到64维,然后在最后通过1x1卷积恢复。
使用bottleneck的结构整体上用的参数数目为
1x1x256x64 + 3x3x64x64 + 1x1x64x256 = 69632
。然而不使用bottleneck的话就是两个3x3x256的卷积,参数数目: 3x3x256x256x2 = 1179648
,差了16.94倍。对于常规ResNet,可以用于34层或者更少的网络中。而对于Bottleneck Design的ResNet通常用于更深的如101这样的网络中,目的是减少计算和参数量(实用目的)。
2.3 两种Shortcut Connection方式
有人会问,如果F(x)和x的channel个数不同怎么办,因为F(x)和x是按照channel维度相加的,channel不同怎么相加呢?
针对channel个数是否相同,要分成两种情况考虑。我们先看下图,有实线和虚线两种连接方式:
-
实线的Connection部分(”第一个粉色矩形和第三个粉色矩形“)都是执行3x3x64的卷积,他们的channel个数一致,所以采用计算方式:
y=F(x)+x -
虚线的Connection部分(”第一个绿色矩形和第三个绿色矩形“)分别是3x3x64和3x3x128的卷积操作,他们的channel个数不同(64和128),所以采用计算方式:
y=F(x)+Wx
其中W是卷积操作,用来调整x的channel维度的
2.4 resnet各层架构
上面一共提出了5种深度的ResNet,分别是18,34,50,101和152。resNet-101仅仅指卷积或者全连接层加起来有101层,而激活层或者Pooling层并没有计算在内,其它resNet都以此类推。
所有的网络都分成5部分,分别是:conv1,conv2_x,conv3_x,conv4_x。。
这里我们关注50-layer和101-layer这两列,可以发现,它们唯一的不同在于conv4_x——ResNet50有6个block,而ResNet101有23个block。这里相差了17个block,也就是17 x 3 = 51层。
3. resnet训练imageNet(由于数据集过大,未完成)
3.1. 安装java jdk
参考How To Install Java with Apt-Get on Debian 8
3.2. 确认系统版本
cmd执行命令
uname -a
我得知我的系统是ubuntu,因此安装采用ubuntu的教程
3.2. 安装bazel
3.3. 下载数据集
参考Inception in TensorFlow
按照参考网址里的Getting Started做即可,需要事先安装bazel,而上文的1-3就是安装bazel的过程。
由于实验室虚拟机的下载速度太慢,我转而使用CIFAR作为训练数据集
4. resnet训练cifar-10
由于时间有限,难度较大,我只是用了网上https://github.com/tensorflow/models/tree/master/official/resnet提供的模型跑了一遍,并记录了结果。尝试过自己修改代码,但发现官方提供的代码框架太大,难以分析。希望日后能补足这个部分......
4.1 添加环境变量
需要把models的文件路径添加到环境变量,否则可能遇到ImportError: No module named official.resnet.
之类的问题。
export PYTHONPATH="$PYTHONPATH:/path/to/models" # 比如"$PYTHONPATH:/root/Desktop/models"
4.1 下载并解压cifar-10-data
python cifar10_download_and_extract.py --data_dir ~
4.2 开始训练
python cifar10_main.py --data_dir ~
训练到后期,training准确率到99%以上,由于overfitting,evaluate准确率有92%左右。
training
evaluating
可以看到,测试准确率是略低于训练准确率的。
4.3 各种问题与解决方法
4.3.1 问题:AttributeError: module 'tensorflow' has no attribute 'data'
参考
AttributeError: module 'tensorflow' has no attribute 'data'
Yes, as @blairjordan mentions,
tf.contrib.data
has been upgraded to justtf.data
in TensorFlow v1.4. So you need to make sure you're using v1.4.
原因
新旧版本的接口不同,函数调用不同
解决方案1
更改调用函数的方式 或者 安装新版的tensorflow(v1.4或v1.7)
conda create -n tf1.7 python=3.6
conda install tensorflow-gpu=1.7
为什么解决方案1可行
我最开始有疑惑,安装tensorflow-gpu要求事先安装好相应版本的cudatoolkit和cudnn。正是因为虚拟机预先安装的cuda和cudnn版本不高,我才只能安装低版本的tf。但当我运行conda install tensorflow-gpu=1.7
后,发现conda现在会自动帮你安装相应版本的cuda和cudnn依赖。如下图:
所以就可以放心地安装高版本的tensorflow了,以后也不用再纠结于cuda和cudnn的安装,只要gpu能支持,就可以顺利安装。
参考2
The arguments accepted by the Dataset.map() transformation have also changed:
dataset.map(..., output_buffer_size=B) is now dataset.map(...).prefetch(B).
I'd check that you have the latest version. In my case, I still need to use the old tf.contrib.data.
解决方案2(未证实)
采用旧的函数调用,比如data.map.prefetch的调用改为data.map 。
这个方法只是一个思路,未证实,因为我已经用解决方案1解决问题。我也不在此深究了。