ResNet原理及其在TF-Slim中的實(shí)現(xiàn)

摘要

微軟的深度殘差網(wǎng)絡(luò)ResNet源于2016年CVPR最佳論文---圖像識(shí)別中的深度殘差學(xué)習(xí)(Deep Residual Learning for Image Recognition), 論文來源,翻譯地址
這個(gè)152層ResNet架構(gòu)深,除了在層數(shù)上面創(chuàng)紀(jì)錄,ResNet 的錯(cuò)誤率也低得驚人贷腕,達(dá)到了3.6%,人類都大約在5%~10%的水平殿较。這是目前為止最好的深度學(xué)習(xí)框架∽兀可以看作人工神經(jīng)網(wǎng)絡(luò)領(lǐng)域的又一里程碑淋纲。

2016年8月31日,Google團(tuán)隊(duì)宣布針對TensorFlow開源了最新發(fā)布的TF-slim資料庫院究,它是一個(gè)可以定義洽瞬、訓(xùn)練和評(píng)估模型的輕量級(jí)的軟件包本涕,也能對圖像分類領(lǐng)域中幾個(gè)主要有競爭力的網(wǎng)絡(luò)進(jìn)行檢驗(yàn)和定義模型。這其中伙窃,就包括了ResNet網(wǎng)絡(luò)結(jié)構(gòu)菩颖。本文將結(jié)合TF-slim庫中的ResNet模型的代碼,介紹一下ResNet網(wǎng)絡(luò)的結(jié)構(gòu)和原理为障。

ResNet的原理

論文中提到晦闰,近幾年的研究發(fā)現(xiàn)網(wǎng)絡(luò)的深度是使網(wǎng)絡(luò)性能更優(yōu)化的一個(gè)關(guān)鍵因素,但是隨著網(wǎng)絡(luò)深度的加深鳍怨,梯度消失&爆炸問題十分明顯呻右,網(wǎng)絡(luò)甚至出現(xiàn)了退化。在論文中通過一個(gè)20層和一個(gè)56層的普通網(wǎng)絡(luò)進(jìn)行了對比鞋喇,發(fā)現(xiàn)56層網(wǎng)絡(luò)的性能遠(yuǎn)低于20層網(wǎng)絡(luò)声滥,如圖1所示。

圖1

而在ResNet的這篇論文中确徙,通過引入一個(gè)深度殘差學(xué)習(xí)框架醒串,解決了這個(gè)退化問題。它不期望每一層能直接吻合一個(gè)映射鄙皇,而是明確的讓這些層去吻合殘差映射。形式上看仰挣,就是用H(X)來表示最優(yōu)解映射伴逸,但我們讓堆疊的非線性層去擬合另一個(gè)映射F(X):=H(X) - X, 此時(shí)原最優(yōu)解映射H(X)就可以改寫成F(X)+X,我們假設(shè)殘差映射跟原映射相比更容易被優(yōu)化膘壶。極端情況下错蝴,如果一個(gè)映射是可優(yōu)化的,那也會(huì)很容易將殘差推至0颓芭,把殘差推至0和把此映射逼近另一個(gè)非線性層相比要容易的多顷锰。F(X)+X的公式可以通過在前饋網(wǎng)絡(luò)中做一個(gè)“快捷連接”來實(shí)現(xiàn)(如圖2) ,快捷連接跳過一個(gè)或多個(gè)層亡问。在我們的用例中官紫,快捷連接簡單的執(zhí)行自身映射,它們的輸出被添加到疊加層的輸出中州藕。自身快捷連接既不會(huì)添加額外的參數(shù)也不會(huì)增加計(jì)算復(fù)雜度束世。整個(gè)網(wǎng)絡(luò)依然可以用SGD+反向傳播來做端到端的訓(xùn)練。


圖2.殘差網(wǎng)絡(luò):一個(gè)結(jié)構(gòu)塊
圖2.殘差網(wǎng)絡(luò):一個(gè)結(jié)構(gòu)塊

它有二層床玻,如下表達(dá)式毁涉,其中σ代表非線性函數(shù)ReLU



然后通過一個(gè)shortcut,和第2個(gè)ReLU锈死,獲得輸出y



而在論文的后續(xù)贫堰,又提出來深度瓶頸結(jié)構(gòu)穆壕,如圖3右側(cè).在文中是這樣描述這個(gè)結(jié)構(gòu)的:接下來我們描述我們?yōu)镮mageNet準(zhǔn)備的更深的網(wǎng)絡(luò)。因?yàn)樘^漫長的訓(xùn)練時(shí)間我們負(fù)擔(dān)不起其屏,所以修改了單元塊粱檀,改為一種瓶頸設(shè)計(jì)。對于每個(gè)殘差函數(shù)F漫玄,我們使用3層來描述茄蚯,而不是2層。這三層分別是1×1睦优、3×3渗常,和1×1的卷積層,其中1×1層負(fù)責(zé)先減少后增加(恢復(fù))尺寸的汗盘,使3×3層具有較小的輸入/輸出尺寸瓶頸皱碘。
圖3.普通shortcut和深度瓶頸結(jié)構(gòu)對比

這個(gè)深度瓶頸結(jié)構(gòu)在TF-Slim庫中的代碼實(shí)現(xiàn)如下所示:

def bottleneck(inputs, depth, depth_bottleneck, stride, rate=1,
               outputs_collections=None, scope=None):

  with tf.variable_scope(scope, 'bottleneck_v1', [inputs]) as sc:
    depth_in = slim.utils.last_dimension(inputs.get_shape(), min_rank=4)
    if depth == depth_in:
      shortcut = resnet_utils.subsample(inputs, stride, 'shortcut')
    else:
      shortcut = slim.conv2d(inputs, depth, [1, 1], stride=stride,
                             activation_fn=None, scope='shortcut')

    residual = slim.conv2d(inputs, depth_bottleneck, [1, 1], stride=1,
                           scope='conv1')
    residual = resnet_utils.conv2d_same(residual, depth_bottleneck, 3, stride,
                                        rate=rate, scope='conv2')
    residual = slim.conv2d(residual, depth, [1, 1], stride=1,
                           activation_fn=None, scope='conv3')

    output = tf.nn.relu(shortcut + residual)

    return slim.utils.collect_named_outputs(outputs_collections,
                                            sc.original_name_scope,
                                            output)

需要注意的是,在論文中提到的當(dāng)輸入輸出尺寸發(fā)生增加時(shí)(圖4中的虛線的快捷連接)隐孽,會(huì)考慮兩個(gè)策略:(a)快捷連接仍然使用自身映射癌椿,對于維度的增加用零來填補(bǔ)空缺。此策略不會(huì)引入額外的參數(shù)菱阵;(b)投影捷徑(公式2)被用來匹配尺寸(靠1×1的卷積完成)踢俄。對于這兩種選項(xiàng),當(dāng)快捷連接在兩個(gè)不同大小的特征圖譜上出現(xiàn)時(shí)晴及,用stride=2來處理都办。而在TF-Slim的代碼實(shí)現(xiàn)中我們可以看到采用了第二種解決方式,即通過通過1X1的卷積核卷積來達(dá)成尺寸匹配虑稼。(雖然論文中說這樣提高不多但需要更多參數(shù)所以最后沒有使用琳钉。)
同時(shí),在代碼中對于下采樣操作(subsample)是通過1x1的池化來完成的蛛倦。

ResNet的結(jié)構(gòu)

所以我們可以根據(jù)一個(gè)普通的神經(jīng)網(wǎng)絡(luò)來構(gòu)造一個(gè)ResNet歌懒,如圖4所示,論文中選擇的基礎(chǔ)網(wǎng)絡(luò)是VGG-Net溯壶。

圖4.普通網(wǎng)絡(luò)結(jié)構(gòu)和ResNet網(wǎng)絡(luò)結(jié)構(gòu)對比

而它的具體網(wǎng)絡(luò)結(jié)構(gòu)如圖5的表中所示及皂。


圖5.ResNet網(wǎng)絡(luò)結(jié)構(gòu)

在TF-Slim中的代碼實(shí)現(xiàn)如下(以ResNet-50為例):

def resnet_v1_50(inputs,
                 num_classes=None,
                 is_training=True,
                 global_pool=True,
                 output_stride=None,
                 reuse=None,
                 scope='resnet_v1_50'):
  """ResNet-50 model of [1]. See resnet_v1() for arg and return description."""
  blocks = [
      resnet_utils.Block(
          'block1', bottleneck, [(256, 64, 1)] * 2 + [(256, 64, 2)]),
      resnet_utils.Block(
          'block2', bottleneck, [(512, 128, 1)] * 3 + [(512, 128, 2)]),
      resnet_utils.Block(
          'block3', bottleneck, [(1024, 256, 1)] * 5 + [(1024, 256, 2)]),
      resnet_utils.Block(
          'block4', bottleneck, [(2048, 512, 1)] * 3)
  ]
  return resnet_v1(inputs, blocks, num_classes, is_training,
                   global_pool=global_pool, output_stride=output_stride,
                   include_root_block=True, reuse=reuse, scope=scope)

在這段代碼中,其實(shí)只是聲明了一個(gè)通過Block組合成的List茸塞,Block的聲明如下躲庄,其中的關(guān)鍵是collections.namedtuple這個(gè)函數(shù),它把前面元組的值和后面的命名對應(yīng)了起來钾虐。

class Block(collections.namedtuple('Block', ['scope', 'unit_fn', 'args'])):
  """A named tuple describing a ResNet block.

  Its parts are:
    scope: The scope of the `Block`.
    unit_fn: The ResNet unit function which takes as input a `Tensor` and
      returns another `Tensor` with the output of the ResNet unit.
    args: A list of length equal to the number of units in the `Block`. The list
      contains one (depth, depth_bottleneck, stride) tuple for each unit in the
      block to serve as argument to unit_fn.
  """

而將個(gè)元素為block的 LIst轉(zhuǎn)換為一個(gè)網(wǎng)絡(luò)的函數(shù)噪窘,則是resnet_v1,這個(gè)函數(shù)是ResNet的核心,而不同層數(shù)的ResNet只需要改變上述函數(shù)blocks中block的個(gè)數(shù)就可以了倔监。

def resnet_v1(inputs,
              blocks,
              num_classes=None,
              is_training=True,
              global_pool=True,
              output_stride=None,
              include_root_block=True,
              reuse=None,
              scope=None):

  with tf.variable_scope(scope, 'resnet_v1', [inputs], reuse=reuse) as sc:
    end_points_collection = sc.name + '_end_points'
    with slim.arg_scope([slim.conv2d, bottleneck,
                         resnet_utils.stack_blocks_dense],
                        outputs_collections=end_points_collection):
      with slim.arg_scope([slim.batch_norm], is_training=is_training):
        net = inputs
        if include_root_block:
          if output_stride is not None:
            if output_stride % 4 != 0:
              raise ValueError('The output_stride needs to be a multiple of 4.')
            output_stride /= 4
          net = resnet_utils.conv2d_same(net, 64, 7, stride=2, scope='conv1')
          net = slim.max_pool2d(net, [3, 3], stride=2, scope='pool1')
        net = resnet_utils.stack_blocks_dense(net, blocks, output_stride)
        if global_pool:
          # Global average pooling.
          net = tf.reduce_mean(net, [1, 2], name='pool5', keep_dims=True)
        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.
        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')
        return net, end_points

在這個(gè)函數(shù)中直砂,將blocks轉(zhuǎn)換為net的語句是

 net = resnet_utils.stack_blocks_dense(net, blocks, output_stride)

這個(gè)函數(shù)的具體實(shí)現(xiàn)如下,它通過一個(gè)循環(huán)將list中的每個(gè)block讀取出來浩习,然后將block中相應(yīng)的參數(shù)代入到前文提到的bottleneck這個(gè)函數(shù)中静暂,這樣就生成了相應(yīng)的ResNet網(wǎng)絡(luò)結(jié)構(gòu)。

def stack_blocks_dense(net,
                       blocks,
                       output_stride=None,
                       outputs_collections=None):

  # The current_stride variable keeps track of the effective stride of the
  # activations. This allows us to invoke atrous convolution whenever applying
  # the next residual unit would result in the activations having stride larger
  # than the target output_stride.
  current_stride = 1

  # The atrous convolution rate parameter.
  rate = 1

  for block in blocks:
    with variable_scope.variable_scope(block.scope, 'block', [net]) as sc:
      for i, unit in enumerate(block.args):
        if output_stride is not None and current_stride > output_stride:
          raise ValueError('The target output_stride cannot be reached.')

        with variable_scope.variable_scope('unit_%d' % (i + 1), values=[net]):
          unit_depth, unit_depth_bottleneck, unit_stride = unit

          # If we have reached the target output_stride, then we need to employ
          # atrous convolution with stride=1 and multiply the atrous rate by the
          # current unit's stride for use in subsequent layers.
          if output_stride is not None and current_stride == output_stride:
            net = block.unit_fn(
                net,
                depth=unit_depth,
                depth_bottleneck=unit_depth_bottleneck,
                stride=1,
                rate=rate)
            rate *= unit_stride

          else:
            net = block.unit_fn(
                net,
                depth=unit_depth,
                depth_bottleneck=unit_depth_bottleneck,
                stride=unit_stride,
                rate=1)
            current_stride *= unit_stride
      net = utils.collect_named_outputs(outputs_collections, sc.name, net)

  if output_stride is not None and current_stride != output_stride:
    raise ValueError('The target output_stride cannot be reached.')

  return net

在這里谱秽,代碼中提到了 atrous convolution這個(gè)結(jié)構(gòu),簡單來說洽蛀,它是如圖6(b)所示的一個(gè)結(jié)構(gòu),可以起到在使用了步長為1的池化層后扔使得原結(jié)構(gòu)保持相同的感受野疟赊。

圖6.atrous convolution

參考文獻(xiàn)

[1]Deep Residual Learning for Image Recognition
[2]http://blog.csdn.net/tiandijun/article/details/52526317
[3]http://blog.csdn.net/mao_feng/article/details/52734438
[4]http://blog.csdn.net/helei001/article/details/52692128
[5]http://blog.csdn.net/u012759136/article/details/52434826#t9

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末郊供,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子近哟,更是在濱河造成了極大的恐慌驮审,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,470評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件吉执,死亡現(xiàn)場離奇詭異疯淫,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)戳玫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門熙掺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人量九,你說我怎么就攤上這事适掰。” “怎么了荠列?”我有些...
    開封第一講書人閱讀 162,577評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長载城。 經(jīng)常有香客問我肌似,道長,這世上最難降的妖魔是什么诉瓦? 我笑而不...
    開封第一講書人閱讀 58,176評(píng)論 1 292
  • 正文 為了忘掉前任川队,我火速辦了婚禮,結(jié)果婚禮上睬澡,老公的妹妹穿的比我還像新娘固额。我一直安慰自己,他們只是感情好煞聪,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,189評(píng)論 6 388
  • 文/花漫 我一把揭開白布斗躏。 她就那樣靜靜地躺著,像睡著了一般昔脯。 火紅的嫁衣襯著肌膚如雪啄糙。 梳的紋絲不亂的頭發(fā)上笛臣,一...
    開封第一講書人閱讀 51,155評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音隧饼,去河邊找鬼沈堡。 笑死,一個(gè)胖子當(dāng)著我的面吹牛燕雁,可吹牛的內(nèi)容都是我干的诞丽。 我是一名探鬼主播,決...
    沈念sama閱讀 40,041評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼拐格,長吁一口氣:“原來是場噩夢啊……” “哼僧免!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起禁荒,我...
    開封第一講書人閱讀 38,903評(píng)論 0 274
  • 序言:老撾萬榮一對情侶失蹤猬膨,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后呛伴,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體勃痴,經(jīng)...
    沈念sama閱讀 45,319評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,539評(píng)論 2 332
  • 正文 我和宋清朗相戀三年热康,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了沛申。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,703評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡姐军,死狀恐怖铁材,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情奕锌,我是刑警寧澤著觉,帶...
    沈念sama閱讀 35,417評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站惊暴,受9級(jí)特大地震影響饼丘,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜辽话,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,013評(píng)論 3 325
  • 文/蒙蒙 一肄鸽、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧油啤,春花似錦典徘、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春汛骂,著一層夾襖步出監(jiān)牢的瞬間罕模,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評(píng)論 1 269
  • 我被黑心中介騙來泰國打工帘瞭, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留淑掌,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,711評(píng)論 2 368
  • 正文 我出身青樓蝶念,卻偏偏與公主長得像抛腕,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子媒殉,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,601評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容