ResNet網絡是參考了VGG19網絡,在其基礎上進行了修改,並通過短路機制加入了殘差單元,如圖5所示。變化主要體現在ResNet直接使用stride=2的卷積做下採樣,並且用global average pool層替換了全連接層。ResNet的一個重要設計原則是:當feature map大小降低一半時,featuremap的數量增加一倍,這保持了網絡層的複雜度。從圖5中可以看到,ResNet相比普通網絡每兩層間增加了短路機制,這就形成了殘差學習,其中虛線表示featuremap數量發生了改變。圖5展示的34-layer的ResNet,還可以構建更深的網絡如表1所示。從表中可以看到,對於18-layer和34-layer的ResNet,其進行的兩層間的殘差學習,當網絡更深時,其進行的是三層間的殘差學習,三層卷積核分別是1x1,3x3和1x1,一個值得注意的是隱含層的feature map數量是比較小的,並且是輸出feature map數量的1/4。
圖5 ResNet網絡結構圖
表1 不同深度的ResNet
下面我們再分析一下殘差單元,ResNet使用兩種殘差單元,如圖6所示。左圖對應的是淺層網絡,而右圖對應的是深層網絡。對於短路連接,當輸入和輸出維度一致時,可以直接將輸入加到輸出上。但是當維度不一致時(對應的是維度增加一倍),這就不能直接相加。有兩種策略:(1)採用zero-padding增加維度,此時一般要先做一個downsamp,可以採用strde=2的pooling,這樣不會增加參數;(2)採用新的映射(projection shortcut),一般採用1x1的卷積,這樣會增加參數,也會增加計算量。短路連接除了直接使用恆等映射,當然都可以採用projection shortcut。
圖6 不同的殘差單元
作者對比18-layer和34-layer的網絡效果,如圖7所示。可以看到普通的網絡出現退化現象,但是ResNet很好的解決了退化問題。
圖7 18-layer和34-layer的網絡效果
最後展示一下ResNet網絡與其他網絡在ImageNet上的對比結果,如表2所示。可以看到ResNet-152其誤差降到了4.49%,當採用集成模型後,誤差可以降到3.57%。
表2 ResNet與其他網絡的對比結果
說一點關於殘差單元題外話,上面我們說到了短路連接的幾種處理方式,其實作者在文獻[2]中又對不同的殘差單元做了細緻的分析與實驗,這裡我們直接拋出最優的殘差結構,如圖8所示。改進前後一個明顯的變化是採用pre-activation,BN和ReLU都提前了。而且作者推薦短路連接採用恆等變換,這樣保證短路連接不會有阻礙。感興趣的可以去讀讀這篇文章。
圖8 改進後的殘差單元及效果
這裡給出ResNet50的TensorFlow實現,模型的實現參考了Caffe版本的實現(https://github.com/KaimingHe/deep-residual-networks),核心代碼如下:
class ResNet50(object):
def __init__(self, inputs,num_classes=1000, is_training=True,scope="resnet50"):
self.inputs =inputs
self.is_training =is_training
self.num_classes =num_classes
with tf.variable_scope(scope):
# construct themodel
net =conv2d(inputs, 64, 7, 2, scope="conv1")#->
[batch,112, 112, 64]
net = tf.nn.relu(batch_norm(net,is_training=self.is_training, scope="bn1"))
net = max_pool(net, 3, 2, scope="maxpool1")#->
[batch, 56, 56, 64]
net = self._block(net, 256, 3, init_stride=1, is_training=self.is_training,scope="block2")# ->
[batch, 56,56, 256]
net = self._block(net, 512, 4, is_training=self.is_training, scope="block3")#->
[batch, 28, 28, 512]
net = self._block(net, 1024, 6, is_training=self.is_training,scope="block4")#->
[batch, 14, 14, 1024]
net = self._block(net, 2048, 3, is_training=self.is_training, scope="block5")#->
[batch, 7, 7, 2048]
net =avg_pool(net, 7, scope="avgpool5")#->
[batch, 1,1, 2048]
net =tf.squeeze(net, [1, 2],
name="SpatialSqueeze")#->
[batch,2048]
self.logits = fc(net, self.num_classes,"fc6")#->
[batch,num_classes]
self.predictions =tf.nn.softmax(self.logits)
def _block(self, x, n_out, n,init_stride=2, is_training=True, scope="block"):
with tf.variable_scope(scope):
h_out = n_out
out = self._bottleneck(x,h_out, n_out, stride=init_stride,is_training=is_training, scope="bottlencek1")
for i in range(1, n):
out = self._bottleneck(out,h_out, n_out, is_training=is_training,scope=("bottlencek%s"% (i + 1)))
return out
def _bottleneck(self, x, h_out, n_out,stride=None, is_training=True, scope="bottleneck"):
"""A residual bottleneck unit"""
n_in =x.get_shape()[-1]
if stride is None:
stride = 1 if n_in == n_out else 2
with tf.variable_scope(scope):
h = conv2d(x, h_out, 1, stride=stride, scope="conv_1")
h = batch_norm(h, is_training=is_training, scope="bn_1")
h = tf.nn.relu(h)
h = conv2d(h, h_out, 3, stride=1, scope="conv_2")
h = batch_norm(h, is_training=is_training, scope="bn_2")
h = tf.nn.relu(h)
h = conv2d(h, n_out, 1, stride=1, scope="conv_3")
h = batch_norm(h, is_training=is_training, scope="bn_3")
if n_in != n_out:
shortcut = conv2d(x,n_out, 1, stride=stride, scope="conv_4")
shortcut =batch_norm(shortcut, is_training=is_training, scope="bn_4")
else:
shortcut = x
return tf.nn.relu(shortcut+ h)
完整實現可以參見GitHub(https://github.com/xiaohu2015/DeepLearning_tutorials/)。