文章目录
ResNet(Residual Neural Network)由微软研究员的 Kaiming He 等四位华人提出,通过使用 Residual Uint 成功训练152层深的神经网络,在 ILSVRC 2015比赛中获得了冠军,取得了 3.57%的top-5 的错误率,同时参数量却比 VGGNet低,效果非常突出,因为它“简单与实用”并存,之后很多方法都建立在ResNet50或者ResNet101的基础上完成的,检测,分割,识别等领域都纷纷使用ResNet,Alpha zero 也使用了ResNet,所以可见ResNet确实很好用。ResNet的结构可以极快的加速超深神经网络的训练,模型的准确率也有非常大的提升。之前我们学习了Inception V3,而Inception V4则是将 Inception Module和ResNet相结合。可以看到ResNet是一个推广性非常好的网络结构,甚至可以直接应用到 Inception Net中。
1.Highway Network简介
在ResNet之前,瑞士教授 Schmidhuber 提出了 Highway Network,原理与ResNet很相似。这位Schmidhuber 教授同时也是 LSTM网络的发明者,而且是早在1997年发明的,可谓是神经网络领域元老级的学者。通常认为神经网络的深度对其性能非常重要,但是网络越深其训练难度越大,Highway Network的目标就是解决极深的神经网络难以训练的问题。
Highway Network相当于修改了每一层的激活函数,此前的激活函数只是对输入做一个非线性变换 y = H(x, WH) ,Highway Network 则允许保留一定比例的原始输入 x,即 y = H(x, WH)*T(x, WT) + x * C(x, WC) ,其中 T是变换系数,C为保留系数。论文中令 C= 1 - T。这样前面一层的信息,有一定比例可以不经过矩阵乘法和非线性变换,直接传输到下一层,仿佛一条信息高速公路,因而得名 Highway Network。Highway Network主要通过 gating units 学习如何控制网络中的信息流,即学习信息应保留的比例。
这个可学习的 gating机制,正是借鉴自Schmidhuber 教授早年的 LSTM 训练神经网络中的gating。几百乃至上千层深的 Highway Network可以直接使用梯度下降算法训练,并可以配合多种非线性激活函数,学习极深的神经网络现在变得可行了。事实上,Highway Network 的设计在理论上允许其训练任意深的网络,其优化方法基本上与网络的深度独立,而传统的神经网络结构则对深度非常敏感,训练复杂度随着深度增加而急剧增加。
2.模型加深存在的问题
ResNet 和 HighWay Network非常类似,也就是允许原始输入信息直接传输到后面的层中。ResNet最初的灵感来自这个问题:在不断加神经网络的深度时,会出现一个 Degradation 的问题,即准确率会先上升然后达到饱和,再持续增加深度则会导致准确率下降。这并不是一个过拟合的问题,因为不光在测试集上误差增大,训练集本身误差也会增大。假设有一个比较浅的网络达到了饱和的准确率,那么后面再加上几个y=x的全等映射层,起码误差不会增加,即更深的网络不应该带来训练集上误差上升。而这里提到的使用全等映射直接将前一层输出传到后面的思想,就是 ResNet的灵感来源。
假定某段神经网络的输入是 x,期望输出是 H(x),如果我们直接把输入 x 传到输出作为初始结果,那么此时我们需要学习的目标就是 F(x) = H(x) - x。如下图所示,这就是一个ResNet的残差学习单元(Residual Unit),ResNet相当于将学习目标改变了,不再是学习一个完整的输出 H(x),只是输出和输入的差别 H(x) - x,即残差。
如下图所示,CIFIR10 数据的一个实验,左侧为训练误差,右侧是测试误差,不光在测试集上误差比较大,训练集本身的误差也非常大。
随着网络越深,精准度的变化如下图:
通过实验可以发现:随着网络层级的不断增加,模型精度不断得到提升,而当网络层级增加到一定的数目以后,训练精度和测试精度迅速下降,这说明当网络变得很深以后,深度网络变得更加难以训练了。
3.为什么深度模型难以训练
为什么随着网络层级越深,模型效果却变差了呢?
3.1.链式法则与梯度弥散
下图是一个简单的神经网络图,由输入层,隐含层,输出层构成:
回想一下神经网络反向传播的原理,先通过正向传播计算出结果 output,然后通过与样本比较得出误差值 E t o t a l E_{total} Etotal:
根据误差结果,利用著名的“链式法则”求偏导,使结果误差反向传播从而得出权重w调整的梯度。下图是输出结果到隐含层的反向传播过程(隐含层到输入层的反向传播过程也是类似):
通过不断迭代,对参数矩阵进行不断调整后,使得输出结果的误差值更小,使输出结果与事实更加接近。
从上面的过程来看,神经网络在反向传播过程中要不断地传播梯度,而当网络层数加深时,梯度在传播过程中会逐渐消失(假如采用Sigmoid函数,对于幅度为1的信号,每向后传递一层,梯度就衰减为原来的 0.25,层数越多,衰减越厉害),导致无法对前面网络层的权重进行有效的调整。
#3.2.幂的特点
1.0 1 365 = 37.783 1.01^{ 365} = 37.783 1.01365=37.783
0.9 9 365 = 0.0255 0.99 ^{365} = 0.0255 0.99365=0.0255
4.ResNet的特点
假设:假如有一个比较浅网络(Shallow Net)的准确率达到了饱和,那么后面再加上几个 y = x 的恒等映射(Identity Mappings),按理说,即使准确率不能再提速了,起码误差不会增加(也即更深的网络不应该带来训练集上误差的上升),但是实验证明准确率下降了,这说明网络越深,训练难度越大。而这里提到的使用恒等映射直接将前一层输出传到后面的思想,便是著名深度残差网络ResNet的灵感来源。
ResNet引入了残差网络结构(residual Network),通过这种残差网络结构,可以把网络层弄得很深(据说目前可以达到1000多层),并且最终的分类效果也非常好,残差网络的基本结构如下图所示,很明显,该图示带有跳跃结构的:
F(x) 是一个残差映射 w, r, t 恒等,如果说恒等是理想,很容易将权重值设定为0,如果理想化映射更接近于恒等映射,便更容易发现微小波动。
残差网络借鉴了高速网络(Highway Network)的跨层链接思想,但对其进行修改(残差项原本是带权值的,但是ResNet用恒等映射代替之)
假定某段神经网络的输入是x,期望输出是H(x),即H(x)是期望的复杂潜在映射,如果是要学习这样的模型,则训练难度会比较大;
保证训练准确率不下降的办法:
回想前面的假设,如果已经学习到较饱和的准确率(或者当发现下层的误差变大时),那么接下来的学习目标就转变为恒等映射的学习,也就是使输入x近似于输出H(x),以保持在后面的层次中不会造成精度下降。
在上图的残差网络结构图中,通过“shortcut connections(捷径连接)”的方式,直接把输入x传到输出作为初始结果,输出结果为H(x)=F(x)+x,当F(x)=0时,那么H(x)=x,也就是上面所提到的恒等映射。于是,ResNet相当于将学习目标改变了,不再是学习一个完整的输出,而是目标值H(X)和x的差值,也就是所谓的残差F(x) = H(x)-x,因此,后面的训练目标就是要将残差结果逼近于0,使到随着网络加深,准确率不下降。
这种残差跳跃式的结构,打破了传统的神经网络n-1层的输出只能给n层作为输入的惯例,使某一层的输出可以直接跨过几层作为后面某一层的输入,其意义在于为叠加多层网络而使得整个学习模型的错误率不降反升的难题提供了新的方向。
至此,神经网络的层数可以超越之前的约束,达到几十层、上百层甚至千层,为高级语义特征提取和分类提供了可行性。
下面感受一下34层的深度残差网络的结构图:
从图中可以看出,怎么有一些“shortcut connections(捷径连接)”是实现,有一些是虚线,有什么区别呢?
因为经过“shortcut-connections(捷径连接)”后,H(x) = F(x) + x,如果 F(x) 和 x 通道相同,则可直接相加,那么通道不同怎么相加呢。上图的实线,虚线就是为了区分这两种情况的:
- 实线的Connection部分,表示通道相同,如上图的第一个粉色矩形和第三个粉色矩形,都是 3*3*64 的特征图,由于通道相同,所以采用计算方式为H(x) = F(x) + x;
- 虚线的 Connection 部分,表示通道不同,如上图的第一个绿色矩形和第三个粉色矩形,分别为 3*3*64 和 3*3*128 的特征图,通道不同,采用的计算方式为 H(x) = F(x) + Wx,其中 W 为卷积操作,用来调整x维度的。
下图是两层及三层的ResNet残差学习模块:
两种结构分别针对 ResNet34(左图)和 ResNet50/101/152(右图),其目的主要就是为了降低参数的数目,左图是两个 3*3*256 的卷积,参数数目:3*3*256*256*2 = 1179648(输入卷积核的参数个数输出滤波器个数,width×height×in_channel×out_channel),右图是第一个11的卷积把256维通道降到64维,然后在最后通过1*1卷积恢复,整体上用的参数数目为:1*1*256*64 + 3*3*64*64 + 1*1*64*256 = 69632,右图的参数数量比左图减少 16.94倍,因此,右图的主要目的就是为了减少参数量,从而减少计算量。
对于常规的ResNet,可以用于34层或者更少的网络中(左图);对于更深的网络(如101层),则使用右图,其目的是减少计算和参数量。
经检验,深度残差网络的确解决了退化问题,如下图所示,上图为平原网络(plain network)网络层次越深(34层)比网络层次浅的(18层)的误差率更高;右图为残差网络ResNet的网络层次越深(34层)比网络层次浅(18层)的误差率更低。
5.VGGNet-19 VS ResNet-34(ResNet的创新点)
在提出残差学习的思想,传统的卷积网络或者全连接网络在信息传递的时候或多或少会存在信息丢失,损耗等问题,同时还有导致梯度小时或梯度爆炸,导致很深的网络无法训练。ResNet在一定程度上解决了这个问题,通过直接将输入信息绕道传到输出,保护信息的完整性,整个网络只需要学习输入,输出差别的那一部分,简化学习目标和难度。
下图所示为 VGGNet-19,以及一个34层深的普通卷积网络,和34层深的ResNet网络的对比图。可以看到普通直连的卷积神经网络和ResNet的最大区别在于,ResNet有很多旁路的支线将输入直接连到后面的层,使得后面的层可以直接学习残差,这种结构也被称为 shortcut或 skip connections。
传统的卷积层或全连接层在信息传递时,或多或少的会存在信息丢失,损耗等问题。ResNet 在某种程度上解决了这个问题,通过直接将输入信息绕道传到输出,保护信息的完整性,整个网络则需要学习输入,输出差别的那一部分,简化学习目标和难度。
在ResNet的论文中,处理下图中的两层残差学习单元,还有三层的残差学习单元。两层的残差学习单元中包含两个相同输出通道数(因为残差等于目标输出减去输入,即 H(x) - x,因此输入,输出维度需保持一致)的 3*3 卷积;而3层的残差网络则使用了 Network In Network 和 Inception Net中的 1*1 卷积,并且是在中间 3*3 的卷积前后都使用了 1*1 卷积,有先降维再升维的操作。另外,如果有输入,输出维度不同的情况,我们可以对 x 做一个线性映射变换维度,再连接到后面的层。
下图为 VGG-19 ,直连的 34层网络,和ResNet的34层网络的结构对比:
6.ResNet不同层数的网络配置
下图是ResNet 不同层数时的网络配置(这里我们特别提出ResNet50和ResNet101,主要是因为他们的出镜率很高,所以需要做特别的说明):
上表中,我们一共提出了五种深度的ResNet,分别是18, 34, 50, 101和152,首先看图2最左侧,我们发现所有的网络都分为五部分,分别是 conv1, conv2_x, conv3_x, conv4_x , conv5_x,之后的其他论文也会专门用这个称呼指代 ResNet 50 或者 101 的每部分。
拿 101-layer 那列,我们先看看 101-layer 是不是真的是 101 层网络,首先有个 输入 7*7*64的卷积,然后经过 3 + 4 + 23+ 3 = 33 个 building block ,每个 block 为3层,所以有 33*3 = 99 层,最后有个 fc 层(用于分类),所有有 1+99+1=101层,确实有101层网络;
- 注意1:101 层网络仅仅指卷积或者全连接层,而激活层或者 Pooling 层并没有计算在内;
- 注意2:这里我们关注50-layer 和 101-layer 这两列,可以发现,他们唯一的不同在于 conv4_x, ResNet50有6个block,而 ResNet101有 23 个 block,插了17个block,也就是 17*3=51层。
在使用了ResNet的结构后,可以发现层数不断加深导致的训练集上误差增大的现象被消除了,ResNet 网络的训练误差会随着层数增大而逐渐减小,并且在测试机上的表现也会变好。在ResNet推出后不久,Google就借鉴了ResNet的精髓,提出了 Inception V4和 Inception-ResNet-V2,并通过融合这两个模型,在 ILSVRC数据集上取得了惊人的 3.08%的错误率。可见,ResNet及其思想对卷积神经网络研究的贡献确实非常显著,具有很强的推广性。在ResNet的作者的第二篇相关论文 Identity Mappings in Deep Rsidual Networks中,ResNet V2被提出。ResNet V2和 ResNet V1 的主要区别在于,作者通过研究 ResNet 残差学习单元的传播公式,发现前馈和反馈信息可以直接传输,因此 skip connection 的非线性激活函数(如ReLU)替换为 Identity Mappings(y = x)。同时,ResNet V2在每一层中都使用了Batch Normalization。这样处理之后,新的残差学习单元将比以前更容易训练且泛化性更强。
根据 Schmidhuber 教授的观点,ResNet 类似于一个没有Gates 的LSTM 网络,即将输入 x 传递到后面层的过程是一直发生的,而不是学习出来的。同时,最近也有两篇论文表示,ResNet 基本等价于 RNN且ResNet的效果类似于在多层网络间的集成方法(ensemble)。ResNet在加深网络层数上做出来重大贡献,而另一篇论文 The Power of Depth for Feedforward Neural Networks 则从理论上证明了加深网络比加宽网络更有效,算是给ResNet 提供了声援,也是给深度学习为什么要深才有效提供合理的解释。
7.TensorFlow 实现ResNet V2网络
在ResNet的作者的第二篇相关论文《Identity Mappings in Deep Residual Networks》中,提出了ResNet V2。ResNet V2 和 ResNet V1 的主要区别在于,作者通过研究 ResNet 残差学习单元的传播公式,发现前馈和反馈信号可以直接传输,因此“shortcut connection”(捷径连接)的非线性激活函数(如ReLU)替换为 Identity Mappings。同时,ResNet V2 在每一层中都使用了 Batch Normalization。这样处理后,新的残差学习单元比以前更容易训练且泛化性更强。
下面我们使用TensorFlow实现一个ResNet V2 网络。我们依然使用方便的 contrib.slim 库来辅助创建 ResNet,其余载入的库还有原生的 collections。本文代码主要来自于TensorFlow的开源实现。
我们使用 collections.namedtuple 设计ResNet 基本Block 模块组的 named tuple,并用它创建 Block 的类,但只包含数据结构,不包含具体方法。我们要定义一个典型的 Block,需要输入三个参数,分别是 scope,unit_fn 和 args。
以Block(‘block1’, bottleneck, [(256, 64, 1]) x 2 + [(256, 64, 2 )]) 这一行代码为例,它可以定义一个典型的Block,其中 block1 就是我们这个Block 的名称(或 scope);bottleneck 是ResNet V2中的残差学习单元;
而最后一个参数 [(256, 64, 1]) x 2 + [(256, 64, 2 )] 则是这个Block 的 args,args 是一个列表,其中每个元素都对应一个 bottleneck残差学习单元,前面两个元素都是 (256,64,1),最后一个是(256,64,2)。每一个元素都是一个三元 tuple,即 (depth,depth_bottleneck, stride)。
比如(256, 64, 3)代表构建的 bottleneck 残差学习单元(每个残差学习单元包含三个卷积层)中,第三层输出通道数 depth 为 256,前两层输出通道数 depth_bottleneck 为64,且中间那层的步长 stride 为3。这个残差学习单元结构即为 [(1x1/s1, 64), (3x3/s2, 64), (1x1/s1, 256)]。而在这个Block中,一共有3个bottleneck残差学习单元,除了最后一个的步长由3变为2,其余都一致。
#_*_coding:utf-8_*_import collectionsimport tensorflow as tf
slim = tf.contrib.slim
class Block(collections.namedtuple('Block', ['scope', 'uint_fn', 'args'])):
'A named tuple describing a ResNet block'
下面定义一个降采样 subsample的方法,参数包括 inputs(输入),factor(采样因子)和scope。这个函数也非常简单,如果factor为1,则不做修改直接返回 inputs;如果不为1,则使用 slim.max_pool2d 最大池化来实现,通过1x1的池化尺寸,stride作步长,即可实现降采样。
def subsample(inputs, factor, scope=None):
if factor == 1:
return inputs
else:
return slim.max_pool2d(inputs, [1, 1], stride=factor, scope=scope)
再定义一个 conv2d_same函数创建卷积层。先判断 stride 是否为1,如果为1,则直接使用 slim.conv2d 并令 padding 模式为SAME。如果 stride 不为1,则显式地 pad zero,要pad zero 的总数为 Kernel_size -1 ,pad_beg 为 pad/2,pad_end 为余下的部分。接下来使用 tf.pad 对输入变量进行补零操作。最后,因为已经进行了 zero padding ,所以只需要使用一个 padding 模式为VALID 的 slim.conv2d 创建这个卷积层。
def conv2d_same(inputs, num_outputs, kernel_size, stride, scope=None):
if stride == 1:
return slim.conv2d(inputs, num_outputs, kernel_size, stride=1,
padding='SAME', scope=scope)
else:
pad_total = kernel_size - 1
pad_beg = pad_total // 2
pad_end = pad_total - pad_beg
inputs = tf.pad(inputs, [[0, 0], [pad_beg, pad_end],
[pad_beg, pad_end], [0, 0]])
return slim.conv2d(inputs, num_outputs, kernel_size, stride=stride,
padding='VALID', scope=scope)
接下来定义堆叠Blocks的函数,参数中的 net 即为输入,blocks是之前定义的Block 的class 的列表,而 outputs_collections 则是用来收集各个 end_points 的 collections。下面使用两层循环,逐个Block,逐个Residual Uint 地堆叠,先使用两个 tf.variable_scope 将残差学习单元命名为 block1 / uint_1 的形式。在第二层循环中,我们拿到每个Block中每个Residual Unit的args,并展开为 depth,depth_bottleneck 和 stide,其含义在前面定义Blocks类时已经学习过。然后使用 unit_fn 函数(即残差学习单元的生成函数)顺序地创建并连接所有的残差学习单元。最后,我们使用 slim.utils.collect_named_outpouts 函数将输出 net 添加到 collection 中 。最后,当所有 Block 中的所有Residual Unit 都堆叠完之后,我们再返回最后的 net 作为 stack_blocks_dense 函数的结果。bottleneck=block.unit_fn
@slim.add_arg_scope
def stack_blocks_dense(net, blocks, outputs_collections=None):
for block in blocks:
with tf.variable_scope(block.scope, 'block', [net]) as sc:
for i, unit in enumerate(block.args):
with tf.variable_scope('unit_%d' % (i+1), values=[net]):
unit_depth, unit_depth_bottleneck, unit_stride = unit
net = block.unit_fn(net,
depth=unit_depth,
unit_depth_bottleneck=unit_depth_bottleneck,
steide=unit_stride)
net = slim.utils.collect_named_outputs(outputs_collections, sc.name, net)
return net
这里创建 ResNet通用的 arg_scope,关于 arg_scope,我们已经知道其功能——用来定义某些函数的参数默认值。这里定义训练标记 is_training 默认为TRUE,权重衰减速率 weight_decay 默认为 0.0001,BN的衰减速率默认为 0.997,BN的 epsilon默认为 1e-5,BN的 scale默认为 TRUE,和Inception V3定义 arg_scope一样,先设置好BN的各项参数,然后通过slim.arg_scope将 slim.conv2d的几个默认参数设置好:权重正则器设置为 L2正则,权重初始化器设为 slim.variance_scaling_initializer(),激活函数设为 ReLU,标准化器设为 BN。并将最大池化 的padding模式默认设为 SAME(注意,ResNet原论文中使用的 VALID模式,设为SAME可让特征对其更简单,大家可以尝试改为 VALID)。最后将几层嵌套的 arg_scope 作为结果返回。
def resnet_arg_scope(is_training=True,
weight_decay=0.0001,
batch_norm_decay=0.997,
batch_norm_epsilon=1e-5,
batch_norm_scale=True):
batch_norm_params = {
'is_training': is_training,
'decay': batch_norm_decay,
'epsilon': batch_norm_epsilon,
'scale': batch_norm_scale,
'updates_collections': tf.GraphKeys.UPDATE_OPS,
}
with slim.arg_scope(
[slim.conv2d],
weights_regularizer=slim.l2_regularizer(weight_decay