ResNet
ResNet简介
ResNet(Residual Nerual Network)由微软研究院的何明凯等4名华人提出,通过使用Residuak Unit 成功训练152层深的神经网络,
在ILSCRC2015年比赛中获得3.75%的top-5错误率,获得冠军。ResNet的参数量少,且新增的Residual Unit单元可以极快地加速超深神经网络的训练,同时模型的准确率也有非常大的提升。
相关内容
在ResNet之前,瑞士教授Schmidhuber提出了Highway Network,原理和ResNet很像,Schmidhuber教授有着一个更出名的发明–LSTM网络。
1、
Highway Network解决的问题?
通常认为神经网络的深度对其性能非常重要,而在增加网络深度的同时随之而来的是网络训练难度增大,Highway Network的目标就是解决极深的神经网络难以训练的问题。
通常认为神经网络的深度对其性能非常重要,而在增加网络深度的同时随之而来的是网络训练难度增大,Highway Network的目标就是解决极深的神经网络难以训练的问题。
2、
Highway Network的原理?
Highway Network相当于修改了每一层激活函数,此前的激活函数是对输入信号做了非线性变换y=H(x,W),Highway Network则允许保留一定比例的原始输入x,即y=H(x,W1) * T(x,W2) + x * C(x,W3),这里T为变换系数,C为保留系数。令C = 1 - T,这样前面一层的信息,有一定比例的可以直接传输到下一层(不经过矩阵乘法和激活函数变换),如同网络传输中的一条高速公路,因此得名Highway Network。Highway Network主要通过gating units学习如何控制网络中的信息流,即学习原始信息应保留的比例。这个可学习的gatting机制,正是借鉴Schmidhuber教授早年的的LSTM网络中的gatting。这正是Highway Network的引入,使得几百层乃至上千次的网络可以训练了。
Highway Network相当于修改了每一层激活函数,此前的激活函数是对输入信号做了非线性变换y=H(x,W),Highway Network则允许保留一定比例的原始输入x,即y=H(x,W1) * T(x,W2) + x * C(x,W3),这里T为变换系数,C为保留系数。令C = 1 - T,这样前面一层的信息,有一定比例的可以直接传输到下一层(不经过矩阵乘法和激活函数变换),如同网络传输中的一条高速公路,因此得名Highway Network。Highway Network主要通过gating units学习如何控制网络中的信息流,即学习原始信息应保留的比例。这个可学习的gatting机制,正是借鉴Schmidhuber教授早年的的LSTM网络中的gatting。这正是Highway Network的引入,使得几百层乃至上千次的网络可以训练了。
3、ResNet网络和Highway Network有啥关系?
ResNet和Highway Network非常相似,都是针对网络随着深度的变化而引发的问题,而Highway Network的解决思路给ResNet提供了解决办法.(下面详解ResNet的问题引出)
ResNet和Highway Network非常相似,都是针对网络随着深度的变化而引发的问题,而Highway Network的解决思路给ResNet提供了解决办法.(下面详解ResNet的问题引出)
4、ResNet网络和Highway Network的区别
highway network 通过gating function控制输出,这些gating是有参数的(T,C),但是ResNet的shortcut没有额外的参数。而且当一个gating function 闭合(C接近零)的时候,highway network的层就不会再表示残差了。相反的,ResNet的公式总是学习剩余函数,ResNet的shortcut永远不会关闭(消失),并且所有的信息会一直传递下去,并且会有新的残差加入进来继续学习。
论文分析
问题引出
深度网络以端到端的多层方式集成了低/中/高级特征和分类器,并且特征的水平可以通过叠加层数(深度)来丰富。
那么出现了一个问题:是否叠加更多的层数就能得到更好的网络吗?
这个问题很难回答,因为在网络训练的过程中可能存在梯度消失/爆炸,现在这个问题(梯度)可以通过归一初始化和中间层的归一化来解决,它能使几十层的网络的反向传播以随机梯度下降的方式收敛。
但是当网络深度增加的时候,又暴露了另一个问题:
随着网络的深度增加,网络的错误率却在上升。
在不断加深网络深度时,会出现一个degradation问题,即准确率会先上升然后达到饱和,再持续增加网络深度则会导致准确率下降。这并不是过拟合的问题,因为不光在测试集上误差增大,在训练集(左边图)上误差也会增大。
思考
假设有一个比较浅的网络达到了饱和的准确率。那么后面再加上几个新的网络层,学习到的也是y=x的
全等映射层,起码误差不会增加,即更深的网络不应该带来训练集上误差上升。
解决方法
前面提到了使用
全等映射直接将前一层输出传到后面的思想,这就是ResNet的灵感。假定某段神级网络的输入是x, 期望输出是H(x),如果我们直接把输入x传到输出作为初始结果,那么此时我们需要学习的新目标就是F(x) = H(x) - x, 这就是一个ResNet的残差学习单元。原来的输出期望H(x)就变成了F(x) + x。通过将输入信息绕道直接传到输出,保护信息的完整性,整个网络则只需要学习输入、输出差别的那一部分,简化了学习目标和难度。理论上,如果得到的特征映射是最优的,我们把残差优化到0,比优化一堆非线性的特征映射更容易。
让我们来看看怎么实现它:
我们把building block 默认为 y = F(x, {w}) + x, x 和 y 分别是输入和输出,函数F(x, {w})就是我们想要学习的残差映射。对于上图的两层网络,
,其中σ是ReLU激活函数,为了简化符号省略了偏差。F(x) + x通过shortcut方式直接相加,这种方式在计算中没有引入额外的参数和计算复杂度。我们的输入和输出必须维度相同(因为y=F(x)+x, 因此输入和输出的维度必须保持一致)。
如果输入和输出不一致,可以对X做一个线性映射(1x1卷积)变换维度,再连接到后面的层 y = F(x, {w}) + w*x。
实验
下面我们测试了三个网络:
1、左边:VGG-19 Model(19.6billion FLOPs);
2、中间:普通的网络:使用多个3*3的小卷积核(以VGG网络的思想设计),遵循着两个设计原则:
a、对于相同的输出特征图尺寸,层与滤波器的个数是相同的
b、如果输出特征图的尺寸减半,那么滤波器的个数加倍,保持时间复杂度
使用步长为2的卷积层来进行下采样。网络用全局平均池化和具有softmax的1000输出的全连接层结束。
3、右边:residual model,这是建立在普通网络的基础上的,可以看到网络的旁边多了很多前馈线,我们把这些前馈线也称之为shortcut或skip connections. 这些前馈线代表的是恒等映射。
前馈线应用会遇到两种情况:
前馈线应用会遇到两种情况:
a、输入和输出的维度一致,那可以直接连接
b、如果输出的维度增加了,有两种办法 1、全零填充卷积,2、借鉴inception Net 的思想,经过1*1卷积升维
同时配置了不同层数的ResNet,如图:
对于两层到三层的ResNet残差学习模块,设计如下:
在实验中使用了如下数据:
图像缩放比例为【256,480】随机采样,然后随机裁剪224*224,并进行水平翻转,
之后每个通道的像素都减去各自的平均值,
使用了标准的颜色增强??这个是怎么操作的??
在每次卷积之后和激活之前使用BN。
在每次卷积之后和激活之前使用BN。
初始化权值并从头训练,
使用SGD,
batch_size为256,
learning-rate=0.1,当误差平稳时除以10,
weight-decay=0.0001,
momentum=0.9,
不使用dropout,
迭代60*104次
左边是普通网络随着迭代次数的增加,训练误差和验证误差的变化,
粗的是验证误差,
细的是训练误差。可以看到无论是训练误差还是验证误差都随着深度增加而增加,这就是前面提到的degradation问题。
右边是residual net(使用的是零填充),可以看到随着迭代次数的增加 训练集和验证集的误差都继续下降了,说明residual net结构产生的效果确实比较好。
作者还提到:越深的网络,收敛速度以指数级别降低,所以这会影响训练误差的降低。但是从图上可以看到,在误差很低的时候,resnet的误差抖动还能大,这证明了它的优越性。
评估
ResNet在ImageNet上的表现
ResNet-A是使用zero-padding,
ResNet-B是在等维使用恒等映射,否则使用shortcuts projection,
ResNet-C的所有shortcuts都是projection
我们认为B比A好是因为A的零填充维数确实没有残差学习。C比B好,是因为projection shortcut带来了额外的参数。但是ABC之间的差距很少,说明projection对解决退化问题作用不大。
在imagenet上证明了:1.我们的极深的残差网络很容易优化,但是当深度增加时,对应的简单网络(即简单叠加的网络)表现出更高的错误率,2、我们的深度残差网络可以通过增加网络深度来获得准确率的提升,比之前的网络好。在cifar上也获得同样的表现。在一系列的比赛上也取得很好的成绩,证明残差学习原则是通用的。
参考:
TensorFlow实战
https://blog.csdn.net/u011974639/article/details/76737547
ResNet在TensorFlow上实现
代码如下
import tensorflow as tf import numpy as np import collections slim = tf.contrib.slim class Block(collections.namedtuple('block', ['scope', 'unit_fn', 'args'])): 'A named tuple describing a ResNet block' #定义降采样函数 def subsample(inputs, factor, scope = None): if factor == 1: return inputs else: return slim.max_pool2d(inputs, [1, 1], stride = factor, scope=scope) #定义创建卷积层的函数 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 #总数 7-1=6 pad_beg = pad_total // 2 # 6//2=3 pad_end = pad_total - pad_beg # 6-3 =3 #显式的对输入补零 inputs = tf.pad(inputs, [[0, 0], [pad_beg, pad_end], [pad_beg, pad_end], [0, 0]]) ''' pad(tensor, paddings, mode="CONSTANT", name=None, constant_values=0) t = tf.constant([[1, 2, 3], [4, 5, 6]]) paddings = tf.constant([[1, 1,], [2, 2]]) # 'constant_values' is 0. # rank of 't' is 2. tf.pad(t, paddings, "CONSTANT") # [[0, 0, 0, 0, 0, 0, 0], # [0, 0, 1, 2, 3, 0, 0], # [0, 0, 4, 5, 6, 0, 0], # [0, 0, 0, 0, 0, 0, 0] 所以在输入inputs的周围加上3 3 + 224 + 3 = 230 ''' return slim.conv2d(inputs, num_outputs, kernel_size, stride=stride, padding='VALID', scope=scope) #定义堆叠block的函数 @slim.add_arg_scope def stack_blocks_dense(net, blocks, outputs_collections=None): ''' :param net: 输入 :param blocks: 之前定义的Block的class的列表 :param outputs_collections: 用来收集各个end_points的collections :return: 将所有的residual unit 都堆叠完后,输出最后的net ''' for block in blocks: #遍历block with tf.variable_scope(block.scope, 'block', [net]) as sc: for i, unit in enumerate(block.args): #遍历block的参数 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, depth_bottleneck = unit_depth_bottleneck, stride = unit_stride) # 将输出添加到collection中 net = slim.utils.collect_named_outputs(outputs_collections, sc.name, net) return net #创建通用的arg_scope,定义某些函数的参数默认值 def resnet_arg_scope(is_training=True, weight_decay = 0.0001, # 权重衰减速率 batch_norm_decay = 0.997, # BN衰减速率 batch_norm_espilon = 1e-5, # BN的epsilon batch_norm_scale=True): # BN的scale batch_norm_params = { 'is_training': is_training, 'decay': batch_norm_decay, 'epsilon': batch_norm_espilon, 'scale': batch_norm_scale, 'updates_collections': tf.GraphKeys.UPDATE_OPS } #设置卷积函数的默认参数 with slim.arg_scope( [slim.conv2d], weights_regularizer = slim.l2_regularizer(weight_decay), weights_initializer = slim.variance_scaling_initializer(), activation_fn = tf.nn.relu, normalizer_fn = slim.batch_norm, normalizer_params=batch_norm_params ): # 设置batch_norm函数的默认参数 with slim.arg_scope([slim.batch_norm], **batch_norm_params): # 设置max_pool的默认参数 with slim.arg_scope([slim.max_pool2d], padding='SAME') as arg_sc: #返回定义好默认参数的函数 return arg_sc #定义bottleneck残差学习单元 @slim.add_arg_scope def bottleneck(inputs, depth, depth_bottleneck, stride, outputs_collections=None, scope=None): with tf.variable_scope(scope, 'bottleneck_v2', [inputs]) as sc: #获得输入的最后一个维度,即输出通道数 参数min_rank限定最少为4个维度 depth_in = slim.utils.last_dimension(inputs.get_shape(), min_rank=4) #对输入进行BN,并使用激活函数进行预激活 preact = slim.batch_norm(inputs, activation_fn = tf.nn.relu, scope='preact') #如果输入通道数depth_in和输出通道数depth一样,那么subsample按步长为stride对inputs进行空间上的降采样 #确保空间尺寸和残差一致,因为残差中间层的卷积步长为stride,所以降采样的步长也为stride if depth == depth_in: shortcut = subsample(inputs, stride, 'shortcut') # 如果输入和输出通道数不同,就用[1,1],步长为stride的卷积来改变通道数 else: shortcut = slim.conv2d(preact, depth, [1, 1], stride=stride, normalizer_fn=None,activation_fn=None, scope='shortcut') #定义残差 第一层是1*1 步长为1, 中间是3*3, 步长为stride, # 第三层还是1*1 步长为1,而且没有激活函数和BN residual = slim.conv2d(preact, depth_bottleneck, [1, 1], stride=1, scope='conv1') residual = conv2d_same(residual, depth_bottleneck, 3, stride, scope='conv2') residual = slim.conv2d(residual, depth, [1, 1], stride=1, normalizer_fn=None, activation_fn=None, scope='conv3') #将残差和shortcut相加 得到最终的输出 output = shortcut + residual return slim.utils.collect_named_outputs(outputs_collections, sc.name, output) # 定义生成ResNet V2的主函数 def resnet_v2(inputs, blocks, num_class=None, global_pool =True, include_root_block=True, reuse=None, scope=None): ''' :param inputs: 输入 :param blocks: 定义好的Block类的列表 :param num_class: 最后输出的类数 :param global_pool: 是否加上最后的一层全局平均池化 :param include_root_block: 是否加上ResNet网络最前面通常使用的7*7卷积核最大池化 :param reuse: 是否重用 :param scope: 整个网络的名字 :return: ''' with tf.variable_scope(scope, 'resnet_v2', [inputs], reuse = reuse) as sc: end_points_collection = sc.original_name_scope + '_end_points' with slim.arg_scope([slim.conv2d, bottleneck, stack_blocks_dense], outputs_collections = end_points_collection): net = inputs # 是否加上7*7的64通道的卷积 ,步长为2, 不使用激活函数和BN 输出为 if include_root_block: with slim.arg_scope([slim.conv2d], activation_fn=None, normalizer_fn=None): # 输入为224 进过显式填充 为224+ 3+ 3=230 (230-7)/2 +1 = 112 所以输出为112*112*64 net = conv2d_same(net, 64, 7, stride=2, scope='conv1') # 再进过一个3*3 步长为2 的最大池化 (112-3)/2+1 = 56*56*64 net = slim.max_pool2d(net, [3, 3], stride=2, scope='pool1') #block堆叠,每个block进行一次步长为2的卷积,尺寸缩小1/2,通道数增加2倍 net = stack_blocks_dense(net, blocks) #对输出添加一个BN和relu net = slim.batch_norm(net, activation_fn=tf.nn.relu, scope= 'postnorm') #添加全局平均池化 if global_pool: #用reduce_mean实现全局平均池化,效率比直接用avg_pool高 net = tf.reduce_mean(net, [1, 2], name='pool5', keep_dims=True) #是否分类 if num_class: #如果分类 添加一个输出通道为num_class 1*1 (没有卷积和BN), 为了降维 net = slim.conv2d(net, num_class, [1, 1], activation_fn=None, normalizer_fn=None, scope='logits') #将collection 转为python的字典格式 end_points = slim.utils.convert_collection_to_dict(end_points_collection) #添加一个softmax层输出网络结果 if num_class is not None: end_points['predictions'] = slim.softmax(net, scope='prediction') return net, end_points # 构建深度为50层的残差网络 一共四个block 每个block里面bottleneck的数量分别为(3, 4, 6, 3), # 每个bottleneck里面有三个卷积层 总层数为(3+4+6+3) * 3 + 2 =50 # 残差学习模块之前的卷积、池化已经将尺寸缩小了4倍,前3个block都有步长为2的层,因此总尺寸缩小了4*8=32倍, 输出图片变为224/32=7 #在减少尺寸的同时,也在增加输出通道,最后达到2048 def resnet_v2_50(inputs, num_classes=None, global_pool=True, reuse=None, scope='resnet_v2_50'): blocks = [ Block('block1', bottleneck, [(256, 64, 1)] * 2 + [(256, 64, 2)]), Block('block2', bottleneck, [(512, 128, 1)] * 3 + [(512, 128, 2)]), Block('block3', bottleneck, [(1024, 256, 1)] * 5 + [(1024, 256, 2)]), Block('block4', bottleneck, [(2048, 512, 1)] * 3) ] return resnet_v2(inputs, blocks, num_classes, global_pool, include_root_block=True, reuse=reuse, scope=scope) #第三个block的units数量增加到23 def resnet_v2_101(inputs, num_classes=None, global_pool=True, reuse=None, scope='resnet_v2_101'): blocks = [ Block('block1', bottleneck, [(256, 64, 1)] * 2 + [(256, 64, 2)]), Block('block2', bottleneck, [(512, 128, 1)] * 3 + [(512, 128, 2)]), Block('block3', bottleneck, [(1024, 256, 1)] * 22 + [(1024, 256, 2)]), Block('block4', bottleneck, [(2048, 512, 1)] * 3) ] return resnet_v2(inputs, blocks, num_classes, global_pool, include_root_block=True, reuse=reuse, scope=scope) # 第二个和第三个分别增加到8和36 def resnet_v2_152(inputs, num_classes=None, global_pool=True, reuse=None, scope='resnet_v2_152'): blocks = [ Block('block1', bottleneck, [(256, 64, 1)] * 2 + [(256, 64, 2)]), Block('block2', bottleneck, [(512, 128, 1)] * 7 + [(512, 128, 2)]), Block('block3', bottleneck, [(1024, 256, 1)] * 35 + [(1024, 256, 2)]), Block('block4', bottleneck, [(2048, 512, 1)] * 3) ] return resnet_v2(inputs, blocks, num_classes, global_pool, include_root_block=True, reuse=reuse, scope=scope) #第二个和第三个分别增加到24 和36 def resnet_v2_200(inputs, num_classes=None, global_pool=True, reuse=None, scope='resnet_v2_200'): blocks = [ Block('block1', bottleneck, [(256, 64, 1)] * 2 + [(256, 64, 2)]), Block('block2', bottleneck, [(512, 128, 1)] * 23 + [(512, 128, 2)]), Block('block3', bottleneck, [(1024, 256, 1)] * 35 + [(1024, 256, 2)]), Block('block4', bottleneck, [(2048, 512, 1)] * 3) ] return resnet_v2(inputs, blocks, num_classes, global_pool, include_root_block=True, reuse=reuse, scope=scope) from datetime import datetime import math import time def time_tensorflow_run(session, target, info_string): num_steps_burn_in = 10 total_duration = 0.0 total_duration_squared = 0.0 for i in range(num_batches + num_steps_burn_in): start_time = time.time() _ = session.run(target) duration = time.time() - start_time if i >= num_steps_burn_in: if not i % 10: print ('%s: step %d, duration = %.3f' % (datetime.now(), i - num_steps_burn_in, duration)) total_duration += duration total_duration_squared += duration * duration mn = total_duration / num_batches vr = total_duration_squared / num_batches - mn * mn sd = math.sqrt(vr) print ('%s: %s across %d steps, %.3f +/- %.3f sec / batch' % (datetime.now(), info_string, num_batches, mn, sd)) # batch_size = 3 height, width = 224, 224 inputs = tf.random_uniform([batch_size, height, width, 3]) with slim.arg_scope(resnet_arg_scope(is_training=False)): net, end_points = resnet_v2_152(inputs, 1000) with tf.Session() as sess: sess.run(tf.global_variables_initializer()) num_batches = 100 time_tensorflow_run(sess, net, 'Forward')