論文地址:https://arxiv.org/pdf/1512.03385.pdf
1、引言-深度網(wǎng)絡(luò)的退化問題
在深度神經(jīng)網(wǎng)絡(luò)訓(xùn)練中,從經(jīng)驗(yàn)來看,隨著網(wǎng)絡(luò)深度的增加,模型理論上可以取得更好的結(jié)果。但是實(shí)驗(yàn)卻發(fā)現(xiàn),深度神經(jīng)網(wǎng)絡(luò)中存在著退化問題(Degradation problem)??梢钥吹?,在下圖中56層的網(wǎng)絡(luò)比20層網(wǎng)絡(luò)效果還要差。

上面的現(xiàn)象與過擬合不同,過擬合的表現(xiàn)是訓(xùn)練誤差小而測試誤差大,而上面的圖片顯示訓(xùn)練誤差和測試誤差都是56層的網(wǎng)絡(luò)較大。
深度網(wǎng)絡(luò)的退化問題至少說明深度網(wǎng)絡(luò)不容易訓(xùn)練。我們假設(shè)這樣一種情況,56層的網(wǎng)絡(luò)的前20層和20層網(wǎng)絡(luò)參數(shù)一模一樣,而后36層是一個(gè)恒等映射( identity mapping),即輸入x輸出也是x,那么56層的網(wǎng)絡(luò)的效果也至少會和20層的網(wǎng)絡(luò)效果一樣,可是為什么出現(xiàn)了退化問題呢?因此我們在訓(xùn)練深層網(wǎng)絡(luò)時(shí),訓(xùn)練方法肯定存在的一定的缺陷。
正是上面的這個(gè)有趣的假設(shè),何凱明博士發(fā)明了殘差網(wǎng)絡(luò)ResNet來解決退化問題!讓我們來一探究竟!
2、ResNet網(wǎng)絡(luò)結(jié)構(gòu)
ResNet中最重要的是殘差學(xué)習(xí)單元:

對于一個(gè)堆積層結(jié)構(gòu)(幾層堆積而成)當(dāng)輸入為x時(shí)其學(xué)習(xí)到的特征記為H(x),現(xiàn)在我們希望其可以學(xué)習(xí)到殘差F(x)=H(x)-x,這樣其實(shí)原始的學(xué)習(xí)特征是F(x)+x 。當(dāng)殘差為0時(shí),此時(shí)堆積層僅僅做了恒等映射,至少網(wǎng)絡(luò)性能不會下降,實(shí)際上殘差不會為0,這也會使得堆積層在輸入特征基礎(chǔ)上學(xué)習(xí)到新的特征,從而擁有更好的性能。一個(gè)殘差單元的公式如下:

后面的x前面也需要經(jīng)過參數(shù)Ws變換,從而使得和前面部分的輸出形狀相同,可以進(jìn)行加法運(yùn)算。
在堆疊了多個(gè)殘差單元后,我們的ResNet網(wǎng)絡(luò)結(jié)構(gòu)如下圖所示:

3、ResNet代碼實(shí)戰(zhàn)
我們來實(shí)現(xiàn)一個(gè)mnist手寫數(shù)字識別的程序。代碼中主要使用的是tensorflow.contrib.slim中定義的函數(shù),slim作為一種輕量級的tensorflow庫,使得模型的構(gòu)建,訓(xùn)練,測試都變得更加簡單。卷積層、池化層以及全聯(lián)接層都可以進(jìn)行快速的定義,非常方便。這里為了方便使用,我們直接導(dǎo)入slim。
import tensorflow.contrib.slim as slim
我們主要來看一下我們的網(wǎng)絡(luò)結(jié)構(gòu)。首先定義兩個(gè)殘差結(jié)構(gòu),第一個(gè)是輸入和輸出形狀一樣的殘差結(jié)構(gòu),一個(gè)是輸入和輸出形狀不一樣的殘差結(jié)構(gòu)。
下面是輸入和輸出形狀相同的殘差塊,這里slim.conv2d函數(shù)的輸入有三個(gè),分別是輸入數(shù)據(jù)、卷積核數(shù)量,卷積核的大小,默認(rèn)的話padding為SAME,即卷積后形狀不變,由于輸入和輸出形狀相同,因此我們可以在計(jì)算outputs時(shí)直接將兩部分相加。
def res_identity(input_tensor,conv_depth,kernel_shape,layer_name):
with tf.variable_scope(layer_name):
relu = tf.nn.relu(slim.conv2d(input_tensor,conv_depth,kernel_shape))
outputs = tf.nn.relu(slim.conv2d(relu,conv_depth,kernel_shape) + input_tensor)
return outputs
下面是輸入和輸出形狀不同的殘差塊,由于輸入和輸出形狀不同,因此我們需要對輸入也進(jìn)行一個(gè)卷積變化,使二者形狀相同。ResNet作者建議可以用1*1的卷積層,stride=2,來進(jìn)行變換:
def res_change(input_tensor,conv_depth,kernel_shape,layer_name):
with tf.variable_scope(layer_name):
relu = tf.nn.relu(slim.conv2d(input_tensor,conv_depth,kernel_shape,stride=2))
input_tensor_reshape = slim.conv2d(input_tensor,conv_depth,[1,1],stride=2)
outputs = tf.nn.relu(slim.conv2d(relu,conv_depth,kernel_shape) + input_tensor_reshape)
return outputs
最后是整個(gè)網(wǎng)絡(luò)結(jié)構(gòu),對于x的輸入,我們先進(jìn)行一次卷積和池化操作,然后接入四個(gè)殘差塊,最后接兩層全聯(lián)接層得到網(wǎng)絡(luò)的輸出。
def inference(inputs):
x = tf.reshape(inputs,[-1,28,28,1])
conv_1 = tf.nn.relu(slim.conv2d(x,32,[3,3])) #28 * 28 * 32
pool_1 = slim.max_pool2d(conv_1,[2,2]) # 14 * 14 * 32
block_1 = res_identity(pool_1,32,[3,3],'layer_2')
block_2 = res_change(block_1,64,[3,3],'layer_3')
block_3 = res_identity(block_2,64,[3,3],'layer_4')
block_4 = res_change(block_3,32,[3,3],'layer_5')
net_flatten = slim.flatten(block_4,scope='flatten')
fc_1 = slim.fully_connected(slim.dropout(net_flatten,0.8),200,activation_fn=tf.nn.tanh,scope='fc_1')
output = slim.fully_connected(slim.dropout(fc_1,0.8),10,activation_fn=None,scope='output_layer')
return output
完整的代碼地址在:https://github.com/princewen/tensorflow_practice/tree/master/CV/ResNet
參考文獻(xiàn):
1、論文:https://arxiv.org/pdf/1512.03385.pdf
2、https://blog.csdn.net/kaisa158/article/details/81096588?utm_source=blogxgwz4