摘要
Resnet(残差网络)在ILSVRC2015比赛中取得冠军,并取得了5项第一:
- ImageNet分类第一
- ImageNet检测第一
- ImageNet定位第一
- COCO检测第一
- COCO分割第一
作者是来自微软亚洲研究院的何凯明等人。主要贡献在于解决了深度CNN模型难训练的问题,提出恒等映射和残差网络的结构,并使得网络深度有了更大的突破(从2014年VGG的19层,Googlenet22层发展到Resnet的50层,152层),是CNN图像史上的一件里程碑事件。
网络由来
- 深度网络的退化问题
从经验来看,当卷积神经网络的层数增加时,网络可以进行更加复杂的特征模式的提取,理论上可以取得更好的结果(Alexnet8->VGG19->Google22)
。然而实践发现深度网络出现了退化问题(Degradation problem):随着网络深度的增加,模型的准确度会出现饱和,甚至下降。如下图所示:56层的网络比20层网络效果还要差。这不会是过拟合问题,因为56层网络的训练误差同样高。我们知道深层网络存在着梯度消失或者爆炸的问题,这使得深度学习模型很难训练。但是现在已经存在一些技术手段如BatchNorm来缓解这个问题。因此,出现深度网络的退化问题是非常令人诧异的。
-
恒等映射(Identity Mapping)
深度网络的退化问题至少说明深度网络不容易训练。但是我们考虑这样一个事实:现在你有一个浅层网络,你想通过向上堆积新层来建立深层网络,一个极端情况是这些增加的层什么也不学习,仅仅复制浅层网络的特征,即这样新层是恒等映射(Identity mapping)。在这种情况下,深层网络应该至少和浅层网络性能一样,也不应该出现退化现象。针对这个退化问题,resnet作者提出了残差学习来解决退化问题。对于一个堆积层结构(几层堆积而成)当输入为时其学习到的特征记为,现在我们希望其可以学习到残差,这样其实原始的学习特征是。之所以这样是因为残差学习相比原始特征直接学习更容易。当残差为0时,此时堆积层仅仅做了恒等映射,至少网络性能不会下降,实际上残差不会为0,这也会使得堆积层在输入特征基础上学习到新的特征,从而拥有更好的性能。残差学习的结构如图4所示。这有点类似与电路中的“短路”,所以是一种短路连接(shortcutconnection)。
网络结构
Resnet网络是参考了VGG19网络,在其基础上进行了修改,并通过短路机制加入了残差单元,Resnet34如下图所示。不同点在于除了第一层resnet采用7x7卷积并连接pool外,中间层都直接在采用stride=2
的卷积进行下采样,在最后用global average pool
替换了全连接层。
Resnet网络以一个残差块为基础单元,多个单元在深度上进行形成一组,同一组中每个残差块的输出通道数相同,不同组的输出通道以256为基础,并以2倍递增。从下图中可以看到,ResNet相比普通网络每两层间增加了短路机制,这就形成了残差学习,其中实现部分表示常规的identity mapping(输入输出通道数相同),虚线表示输入输出通道数不同时在shortcut上加了1x1卷积来改变输入的通道数。其中Resnet34 和Resnet50除第一层7x7卷积和最后一层全连接外,共有4组残差组,每组各有3,4,6,3个残差单元,而resnet34中一个残差单元含有2个3x3卷积,因此层数为1+(3+4+6+3)x2+1 = 34
,同理resnet50的层数为:1+(3+4+6+3)x3+1=50
- 浅层残差单元vs深层残差单元
ResNet使用两种残差单元,如下图所示。左图对应的是浅层网络(34层及以下),而右图对应的是深层网络(50层及以上)。对于短路连接,当输入和输出维度一致时,可以直接将输入加到输出上。但是当维度不一致时(对应的是维度增加一倍),这就不能直接相加。有两种策略:
(1)采用zero-padding增加维度,此时一般要先做一个downsamp,可以采用strde=2的pooling,这样不会增加参数;
(2)采用新的映射(projection shortcut),一般采用1x1的卷积,这样会增加参数,也会增加计算量。短路连接除了直接使用恒等映射,当然都可以采用projection shortcut。
代码实现
本文采用tensorflow.contrib.layers 模块来构建Mobilenet网络结构,关于tf.nn,tf.layers等api的构建方式参见VGG网络中的相关代码。
# --------------------------Method 1 --------------------------------------------
import tensorflow as tf
import tensorflow.contrib.layers as tcl
from tensorflow.contrib.framework import arg_scope
class ResNet50:
def __init__(self, resolution_inp=224, channel=3, name='resnet50'):
self.name = name
self.channel = channel
self.resolution_inp = resolution_inp
def __call__(self, x, dropout=0.5, is_training=True):
with tf.variable_scope(self.name) as scope:
with arg_scope([tcl.batch_norm], is_training=is_training, scale=True):
with arg_scope([tcl.conv2d],
activation_fn=tf.nn.relu,
normalizer_fn=tcl.batch_norm,
padding="SAME"):
conv1 = tcl.conv2d(x, 64, 7, stride=2)
conv1 = tcl.max_pool2d(conv1, kernel_size=3, stride=2)
conv2 = self._res_blk(conv1, 256, 3, stride=1)
conv2 = self._res_blk(conv2, 256, 3, stride=1)
conv2 = self._res_blk(conv2, 256, 3, stride=1)
conv3 = self._res_blk(conv2, 512, 3, stride=2)
conv3 = self._res_blk(conv3, 512, 3, stride=1)
conv3 = self._res_blk(conv3, 512, 3, stride=1)
conv3 = self._res_blk(conv3, 512, 3, stride=1)
conv4 = self._res_blk(conv3, 1024, 3, stride=2)
conv4 = self._res_blk(conv4, 1024, 3, stride=1)
conv4 = self._res_blk(conv4, 1024, 3, stride=1)
conv4 = self._res_blk(conv4, 1024, 3, stride=1)
conv4 = self._res_blk(conv4, 1024, 3, stride=1)
conv4 = self._res_blk(conv4, 1024, 3, stride=1)
conv5 = self._res_blk(conv4, 2048, 3, stride=2)
conv5 = self._res_blk(conv5, 2048, 3, stride=1)
conv5 = self._res_blk(conv5, 2048, 3, stride=1)
avg_pool = tf.nn.avg_pool(conv5, [1, 7, 7, 1], strides=[1, 1, 1, 1], padding="VALID")
flatten = tf.layers.flatten(avg_pool)
self.fc6 = tf.layers.dense(flatten, units=1000, activation=tf.nn.relu)
# dropout = tf.nn.dropout(fc6, keep_prob=0.5)
predictions = tf.nn.softmax(self.fc6)
return predictions
def _res_blk(self, x, num_outputs, kernel_size, stride=1, scope=None):
with tf.variable_scope(scope, "resBlk"):
small_ch = num_outputs // 4
conv1 = tcl.conv2d(x, small_ch, kernel_size=1, stride=stride, padding="SAME")
conv2 = tcl.conv2d(conv1, small_ch, kernel_size=kernel_size, stride=1, padding="SAME")
conv3 = tcl.conv2d(conv2, num_outputs, kernel_size=1, stride=1, padding="SAME")
shortcut = x
if stride != 1 or x.get_shape()[-1] != num_outputs:
shortcut = tcl.conv2d(x, num_outputs, kernel_size=1, stride=stride, padding="SAME",scope="shortcut")
out = tf.add(conv3, shortcut)
out = tf.nn.relu(out)
return out
运行
该部分代码包含2部分:计时函数time_tensorflow_run
接受一个tf.Session
变量和待计算的tensor
以及相应的参数字典和打印信息, 统计执行该tensor
100次所需要的时间(平均值和方差);主函数 run_benchmark中初始化了vgg16的3种调用方式,分别统计3中网络在推理(predict) 和梯度计算(后向传递)的时间消耗,详细代码如下:
# -------------------------- Demo and Test -------------------------------------------
from datetime import datetime
import math
import time
batch_size = 16
num_batches = 100
def time_tensorflow_run(session, target, feed, info_string):
"""
calculate time for each session run
:param session: tf.Session
:param target: opterator or tensor need to run with session
:param feed: feed dict for session
:param info_string: info message for print
:return:
"""
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, feed_dict=feed)
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 # 平均每个batch的时间
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))
# test demo
def run_benchmark():
"""
main function for test or demo
:return:
"""
with tf.Graph().as_default():
image_size = 224 # 输入图像尺寸
images = tf.Variable(tf.random_normal([batch_size, image_size, image_size, 3], dtype=tf.float32, stddev=1e-1))
# method 0
# prediction, fc = resnet50(images, training=True)
model = ResNet50(224, 3)
prediction = model(images, is_training=True)
fc = model.fc6
params = tf.trainable_variables()
for v in params:
print(v)
init = tf.global_variables_initializer()
print("out shape ", prediction)
sess = tf.Session()
print("init...")
sess.run(init)
print("predict..")
writer = tf.summary.FileWriter("./logs")
writer.add_graph(sess.graph)
time_tensorflow_run(sess, prediction, {}, "Forward")
# 用以模拟训练的过程
objective = tf.nn.l2_loss(fc) # 给一个loss
grad = tf.gradients(objective, params) # 相对于loss的 所有模型参数的梯度
print('grad backword')
time_tensorflow_run(sess, grad, {}, "Forward-backward")
writer.close()
if __name__ == '__main__':
run_benchmark()
注: 完整代码可参见个人github工程