残差网络
ResNet(Residual Neural Network)由微软研究院的Kaiming He等4名华人提出,通过Residual Unit(见图1)成功训练152层深的神经网络,在ILSVRC 2015比赛中获得了冠军,取得3.57%的top-5错误率,同时参数比VGGNet低,效果非常突出。
图1:ResNet的残差学习模块
理论上来说网络的深度越深,那么就可以很好的拟合任意的数据分布,实际上通过实验得知,当网络深度增加到一定的程度的时候,学习过程反而变的很差,训练的错误率不降反升。并且收敛的速度也会变的很慢,如下图所示:
这就是所谓的Degradation的问题,即准确率先上升再达到饱和,再持续增加深度则会导致准确率的下降,这并不是过拟合的问题,因为不光在测试集上误差增加,训练集误差也会增加。假设有一个比较浅的网络达到饱和的准确率,那么后再加上几个y=x的映射层,起码误差不会增加,映射将前一层的输出传给后面的思想就是ResNet的灵感来源。
残差网络的出现意在解决,网络训练的深度问题和训练的复杂度,其两个主要的特点是:
-
收敛快,相比普通网络收敛快,容易训练;
-
可以通过简单的堆叠这种结构以获得更高的正确率;
1.什么是残差块
-
直观认为学习一个非线性的结构要比学习一个恒等映射要难的多
-
残差块之所以叫残差,相当于应层中学习的是残差,而非非线性的函数函数映射
-
这种办法使得网络额复杂度并没有增加多少,但是他可以变的更深,残差的收敛速度更快,并且使得网络的层数更深
2.残差网络的结构
残差网络在各种图像识别和目标检测的任务重都获得了不错的成绩,详细的可以参考:Deep Residual Learning for Image Recognition
3. ResNet V2
分析了残差块背后的计算传播方式,表明了当shortcut以及附加激活项都使用恒等映射(identity mappings)时,前向和后向的信号能够直接的从一个block 传递到其他任意一个block。一系列的“消融”实验(ablation experiments)也验证了这些恒等映射的重要性。作者提出了一个新的残差单元,它使得训练变得更简单,同时也提高了网络的泛化能力。
Introduction
深度残差网络(ResNets)由很多个“残差单元”组成。每一个单元(Fig.1 (a))可以表示为:
其中 $x_l$ 和 $x_{l+1}$ 是第l个单元的输入和输出, F 表示一个残差函数。在He2016中, $h(x_l)=x_l$ 代表一个恒等映射,f 代表 ReLU, 这像极了Gradient Boosting.
Fig.1 (a) 原始残差单元;(b) 本文提出的残差单元;右:1001层ResNets 在CIFAR-10上的训练曲线。实线对应测试误差(右侧的y轴),虚线对应训练损失(左侧的y轴)。本文提出的单元使得ResNet-1001的训练更简单。
实验表明: 还是恒等映射好,网络的深度很重要 (为什么恒等映射好,理论上没有说明)
Analysis of Deep Residual Networks
在mini-batch中,梯度(Eq.5)不可能出现梯度消失,因为在一个mini-batch上Eq.5后边的式子基本不会总为-1,也就是说即使很小的权重,也不会存在梯度消失,我想这也是ResNet之所以训练这多深的原因(实际上我们在训练深层网络结构时,是不害怕梯度爆炸的,但是非常害怕梯度消失)
On the Importance of Identity Skip Connections
Experiments on Skip Connections
Discussions
如Fig.2中灰色箭头所示,捷径连接是信息传递最直接的路径。 捷径连接中的操作 (缩放、门控、1×1 的卷积以及 dropout) 会阻碍信息的传递,以致于对优化造成困难。
值得注意的是1×1的卷积捷径连接引入了更多的参数,本应该比恒等捷径连接具有更加强大的表达能力。事实上,shortcut-only gating 和1×1的卷积涵盖了恒等捷径连接的解空间(即,他们能够以恒等捷径连接的形式进行优化)。然而,它们的训练误差比恒等捷径连接的训练误差要高得多,这表明了这些模型退化问题的原因是优化问题,而不是表达能力的问题。
On the Usage of Activation Functions
以上的实验内容验证了Eq.5和Eq.8中的分析,两个公式都是基于连接后的激活函数 f 为恒等连接的假设。但是在上述实验中f是以He2016中的ReLU设计的,因此,Eq.5和8只是以上实验的估计。接下来我们研究f的影响。
我们希望通过重新安排激活函数(ReLU和/或BN)来使得 f 为一个恒等映射。He2016中的原始残差连接的形状如Fig.4(a) — BN在每一个权重层之后使用,之后再接一个ReLU,在最后的元素相加之后还有最后一个ReLU(即f= ReLU)。 Fig.4(b-e)展示了我们研究的其他形式,解释如下。
Experiments on Activation
https://blog.csdn.net/wspba/article/details/60750007
4.Tensorflow实现ResNet
以上主要是ResNet V1版本的理论知识,在ResNet作者的第二篇论文:Identity Mappings in Deep Residual Networks中, ResNet V2被提出,Residual V2和Residual V1的主要区别在于:
-
作者通过研究ResNet残差学习单元的传播公式,发现前馈和反馈信号可以直接传输,因此skip connection的非线性激活函数(如ReLU)替代为Identity Mapping (y = x)。
-
ResNet V2在每一层都使用了Batch Normalization。这样处理之后,新的残差学习单元将比以前更容易训练且泛化性更强。
下面我们就用Tensorflow实现一个ResNet V2网络。我们依然使用方便的contrib.slim库来辅助创建ResNet。
# -*- coding: utf-8 -*-
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
# ResNet V2
# 载入模块、TensorFlow
import collections
import tensorflow as tf
slim = tf.contrib.slim
# 注意tf.contrib类,很多和卷积相关的模块都在slim中,例如AlexNet,VGG
# 定义Block,结构体,自动以的一种数据结构
class Block(collections.namedtuple('Block', ['scope', 'unit_fn', 'args'])):
'A named tuple describing a ResNet block'
# 说明: 见bottleneck这个函数
# scope:"block1","block2".....
# unit_fn: bottleneck 残差学习单元
# args: 参数 见resnet_v2_50
# 定义降采样subsample方法
def subsample(inputs, factor, scope=None):
'''
factor: 1不做修改直接返回input,其他时使用最大池化1X1的尺寸,步长是factor
scope: 变量的命名空间
'''
if factor == 1:
return inputs
else:
return slim.max_pool2d(inputs, [1, 1], stride=factor, scope=scope)
# 定义conv2d_same函数创建卷积层
def conv2d_same(inputs, num_outputs, kernel_size, stride, scope=None):
'''
如果factor=1直接“SAME"填充
如果factor!=1,input做填充
'''
if stride == 1:
return slim.conv2d(inputs, num_outputs, kernel_size, stride=1,
padding='SAME', scope=scope)
else:
# kernel_size_effective = kernel_size + (kernel_size - 1) * (rate - 1)
pad_total = kernel_size - 1 #pad的总数
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]])
# input,张量维度:在8个维度上的填充,填充模式,填充值默认为0
return slim.conv2d(inputs, num_outputs, kernel_size, stride=stride,
padding='VALID', scope=scope)
@slim.add_arg_scope
# 装饰目标函数,arg_scope为目标函数设置默认参数
# 定义堆叠Blocks函数
def stack_blocks_dense(net, blocks,
outputs_collections=None):
for block in blocks:
with tf.variable_scope(block.scope, 'block', [net]) as sc:
#name_or_scope,default_name=None,values=None
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, # 残差学习的生成函数 bittleneck函数
depth=unit_depth,
depth_bottleneck=unit_depth_bottleneck,
stride=unit_stride)
net = slim.utils.collect_named_outputs(outputs_collections, sc.name, net)
# 将输出net添加到collection
return net
# 创建ResNet通用arg_scope,定义函数默认参数值
# arg_sope用来定义默写函数参数的默认值
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),
weights_initializer=slim.variance_scaling_initializer(),
activation_fn=tf.nn.relu,
normalizer_fn=slim.batch_norm,
normalizer_params=batch_norm_params):
with slim.arg_scope([slim.batch_norm], **batch_norm_params):
with slim.arg_scope([slim.max_pool2d], padding='SAME') as arg_sc:
return arg_sc
@slim.add_arg_scope
# 定义核心bottleneck残差学习单元
def bottleneck(inputs, depth, depth_bottleneck, stride,
outputs_collections=None, scope=None):
with tf.variable_scope(scope, 'bottleneck_v2', [inputs]) as sc:
depth_in = slim.utils.last_dimension(inputs.get_shape(), min_rank=4)
# 获取输入的最后一个维度,即channel数,min_rank限定最少为4个维度
preact = slim.batch_norm(inputs, activation_fn=tf.nn.relu, scope='preact')
# batch normalization,并进行Relu预激活
if depth == depth_in:
shortcut = subsample(inputs, stride, 'shortcut') # 降采样 使得输入通道数epth_in与depth一致
else: #用1X1的卷积改变通道数
shortcut = slim.conv2d(preact, depth, [1, 1], stride=stride,
normalizer_fn=None, activation_fn=None,
scope='shortcut')
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')
output = shortcut + residual # identity的
# output加入到集合并返回output
return slim.utils.collect_named_outputs(outputs_collections,
sc.name,
output)
# 定义生成ResNet V2的主函数
def resnet_v2(inputs,
blocks, # 定义好的类的列表
num_classes=None, #最后输出的类数
global_pool=True, # 是否加入最后一层的全局平均池化
include_root_block=True, # 是否加入最前面通常使用的7X7的卷积池化
reuse=None, #是否重用
scope=None): #整个网络的名称(命名空间)
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
if include_root_block:
with slim.arg_scope([slim.conv2d],
activation_fn=None, normalizer_fn=None):
net = conv2d_same(net, 64, 7, stride=2, scope='conv1')
net = slim.max_pool2d(net, [3, 3], stride=2, scope='pool1')
net = stack_blocks_dense(net, blocks)
net = slim.batch_norm(net, activation_fn=tf.nn.relu, scope='postnorm')
if global_pool:
# Global average pooling.
net = tf.reduce_mean(net, [1, 2], name='pool5', keep_dims=True) # avg_pool
if num_classes is not None:
net = slim.conv2d(net, num_classes, [1, 1], activation_fn=None,
normalizer_fn=None, scope='logits')
# Convert end_points_collection into a dictionary of end_points.
# 将collection转化为dict
end_points = slim.utils.convert_collection_to_dict(end_points_collection)
if num_classes is not None:
end_points['predictions'] = slim.softmax(net, scope='predictions')
# 返回net和end_points
return net, end_points
#----------------------------------------------------------
# 设计层数为50的ResNet V2
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)]),
# Depth:输出通道数,depth_bottleneck,stride
# (256,64,1): 256第三层输出的通道数,64:前两层输出的通道数,1:中间层的步长
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)
# 设计101层的ResNet V2
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)
# 设计152层的ResNet V2
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)
# 设计200层的ResNet V2
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 = 32
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) # 152层评测
init = tf.global_variables_initializer()
sess = tf.Session()
sess.run(init)
num_batches = 100
time_tensorflow_run(sess, net, "Forward")
5.Reference
[1].Andrew Ng deeplearning.ai中的教程
[2].Deep Residual Learning for Image Recognition
[3].Identity Mappings in Deep Residual Networks
[4]. Tensorflow实现卷积神经网络