摘要
Resnet(殘差網(wǎng)絡(luò))在ILSVRC2015比賽中取得冠軍,并取得了5項第一:
- ImageNet分類第一
- ImageNet檢測第一
- ImageNet定位第一
- COCO檢測第一
- COCO分割第一
作者是來自微軟亞洲研究院的何凱明等人。主要貢獻在于解決了深度CNN模型難訓(xùn)練的問題妖混,提出恒等映射和殘差網(wǎng)絡(luò)的結(jié)構(gòu)冷离,并使得網(wǎng)絡(luò)深度有了更大的突破(從2014年VGG的19層书聚,Googlenet22層發(fā)展到Resnet的50層,152層)芬膝,是CNN圖像史上的一件里程碑事件悲没。
網(wǎng)絡(luò)由來
- 深度網(wǎng)絡(luò)的退化問題
從經(jīng)驗來看,當(dāng)卷積神經(jīng)網(wǎng)絡(luò)的層數(shù)增加時男图,網(wǎng)絡(luò)可以進行更加復(fù)雜的特征模式的提取示姿,理論上可以取得更好的結(jié)果(Alexnet8->VGG19->Google22)
。然而實踐發(fā)現(xiàn)深度網(wǎng)絡(luò)出現(xiàn)了退化問題(Degradation problem):隨著網(wǎng)絡(luò)深度的增加逊笆,模型的準(zhǔn)確度會出現(xiàn)飽和栈戳,甚至下降。如下圖所示:56層的網(wǎng)絡(luò)比20層網(wǎng)絡(luò)效果還要差难裆。這不會是過擬合問題子檀,因為56層網(wǎng)絡(luò)的訓(xùn)練誤差同樣高镊掖。我們知道深層網(wǎng)絡(luò)存在著梯度消失或者爆炸的問題,這使得深度學(xué)習(xí)模型很難訓(xùn)練褂痰。但是現(xiàn)在已經(jīng)存在一些技術(shù)手段如BatchNorm來緩解這個問題亩进。因此,出現(xiàn)深度網(wǎng)絡(luò)的退化問題是非常令人詫異的缩歪。
-
恒等映射(Identity Mapping)
深度網(wǎng)絡(luò)的退化問題至少說明深度網(wǎng)絡(luò)不容易訓(xùn)練归薛。但是我們考慮這樣一個事實:現(xiàn)在你有一個淺層網(wǎng)絡(luò),你想通過向上堆積新層來建立深層網(wǎng)絡(luò)匪蝙,一個極端情況是這些增加的層什么也不學(xué)習(xí)主籍,僅僅復(fù)制淺層網(wǎng)絡(luò)的特征,即這樣新層是恒等映射(Identity mapping)逛球。在這種情況下千元,深層網(wǎng)絡(luò)應(yīng)該至少和淺層網(wǎng)絡(luò)性能一樣,也不應(yīng)該出現(xiàn)退化現(xiàn)象颤绕。針對這個退化問題幸海,resnet作者提出了殘差學(xué)習(xí)來解決退化問題。對于一個堆積層結(jié)構(gòu)(幾層堆積而成)當(dāng)輸入為時其學(xué)習(xí)到的特征記為屋厘,現(xiàn)在我們希望其可以學(xué)習(xí)到殘差涕烧,這樣其實原始的學(xué)習(xí)特征是。之所以這樣是因為殘差學(xué)習(xí)相比原始特征直接學(xué)習(xí)更容易汗洒。當(dāng)殘差為0時议纯,此時堆積層僅僅做了恒等映射,至少網(wǎng)絡(luò)性能不會下降溢谤,實際上殘差不會為0瞻凤,這也會使得堆積層在輸入特征基礎(chǔ)上學(xué)習(xí)到新的特征,從而擁有更好的性能世杀。殘差學(xué)習(xí)的結(jié)構(gòu)如圖4所示阀参。這有點類似與電路中的“短路”,所以是一種短路連接(shortcutconnection)瞻坝。
Identity Mapping
網(wǎng)絡(luò)結(jié)構(gòu)
Resnet網(wǎng)絡(luò)是參考了VGG19網(wǎng)絡(luò)蛛壳,在其基礎(chǔ)上進行了修改,并通過短路機制加入了殘差單元所刀,Resnet34如下圖所示衙荐。不同點在于除了第一層resnet采用7x7卷積并連接pool外,中間層都直接在采用stride=2
的卷積進行下采樣浮创,在最后用global average pool
替換了全連接層忧吟。
Resnet網(wǎng)絡(luò)以一個殘差塊為基礎(chǔ)單元,多個單元在深度上進行形成一組斩披,同一組中每個殘差塊的輸出通道數(shù)相同溜族,不同組的輸出通道以256為基礎(chǔ)讹俊,并以2倍遞增。從下圖中可以看到煌抒,ResNet相比普通網(wǎng)絡(luò)每兩層間增加了短路機制仍劈,這就形成了殘差學(xué)習(xí),其中實現(xiàn)部分表示常規(guī)的identity mapping(輸入輸出通道數(shù)相同)摧玫,虛線表示輸入輸出通道數(shù)不同時在shortcut上加了1x1卷積來改變輸入的通道數(shù)耳奕。其中Resnet34 和Resnet50除第一層7x7卷積和最后一層全連接外,共有4組殘差組诬像,每組各有3屋群,4,6坏挠,3個殘差單元芍躏,而resnet34中一個殘差單元含有2個3x3卷積,因此層數(shù)為1+(3+4+6+3)x2+1 = 34
降狠,同理resnet50的層數(shù)為:1+(3+4+6+3)x3+1=50
- 淺層殘差單元vs深層殘差單元
ResNet使用兩種殘差單元对竣,如下圖所示。左圖對應(yīng)的是淺層網(wǎng)絡(luò)(34層及以下)榜配,而右圖對應(yīng)的是深層網(wǎng)絡(luò)(50層及以上)否纬。對于短路連接,當(dāng)輸入和輸出維度一致時蛋褥,可以直接將輸入加到輸出上临燃。但是當(dāng)維度不一致時(對應(yīng)的是維度增加一倍),這就不能直接相加烙心。有兩種策略:
(1)采用zero-padding增加維度膜廊,此時一般要先做一個downsamp,可以采用strde=2的pooling淫茵,這樣不會增加參數(shù)爪瓜;
(2)采用新的映射(projection shortcut),一般采用1x1的卷積匙瘪,這樣會增加參數(shù)铆铆,也會增加計算量。短路連接除了直接使用恒等映射丹喻,當(dāng)然都可以采用projection shortcut薄货。
代碼實現(xiàn)
本文采用tensorflow.contrib.layers 模塊來構(gòu)建Mobilenet網(wǎng)絡(luò)結(jié)構(gòu),關(guān)于tf.nn驻啤,tf.layers等api的構(gòu)建方式參見VGG網(wǎng)絡(luò)中的相關(guān)代碼。
# --------------------------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部分:計時函數(shù)time_tensorflow_run
接受一個tf.Session
變量和待計算的tensor
以及相應(yīng)的參數(shù)字典和打印信息, 統(tǒng)計執(zhí)行該tensor
100次所需要的時間(平均值和方差)荐吵;主函數(shù) run_benchmark中初始化了vgg16的3種調(diào)用方式骑冗,分別統(tǒng)計3中網(wǎng)絡(luò)在推理(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 # 預(yù)熱輪數(shù)
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: # 只考慮預(yù)熱輪數(shù)之后的時間
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) # 標(biāo)準(zhǔn)差
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")
# 用以模擬訓(xùn)練的過程
objective = tf.nn.l2_loss(fc) # 給一個loss
grad = tf.gradients(objective, params) # 相對于loss的 所有模型參數(shù)的梯度
print('grad backword')
time_tensorflow_run(sess, grad, {}, "Forward-backward")
writer.close()
if __name__ == '__main__':
run_benchmark()
注: 完整代碼可參見個人github工程