TensorFlow 2.0 搭建卷積神經網絡 (CNN)


關於 CNN 基礎理論可見:卷積神經網絡

TensorFlow2.0 快速搭建神經網絡:tf.keras 

下面主要介紹:1.搭建卷積神經網絡的主要模塊:卷積、批標准化、激活、池化、全連接;

       2.經典卷積網絡的搭建:LeNet、AlexNet、VGGNet、InceptionNet、ResNet。

1 卷積神經網絡主要模塊

1.1 卷積 (Convolutional)

tf.keras.layers.Conv2D (
  filters = 卷積核個數, 
  kernel_size = 卷積核尺寸, #正方形寫核長整數,或(核高h,核寬w)
  strides = 滑動步長, #橫縱向相同寫步長整數,或(縱向步長h,橫向步長w),默認1
  padding = “same” or “valid”, #使用全零填充是“same”,不使用是“valid”(默認)
  activation = “ relu ” or “ sigmoid ” or “ tanh ” or “ softmax”等 , #如有BN此處不寫
  input_shape = (高, 寬 , 通道數) #輸入特征圖維度,可省略
)

eg:

model = tf.keras.models.Sequential([
  Conv2D(6, 5, padding='valid', activation='sigmoid'),
  MaxPool2D(2, 2),
  Conv2D(6, (5, 5), padding='valid', activation='sigmoid'),
  MaxPool2D(2, (2, 2)),
  Conv2D(filters=6, kernel_size=(5, 5),padding='valid', activation='sigmoid'),
  MaxPool2D(pool_size=(2, 2), strides=2),
  Flatten(),
  Dense(10, activation='softmax')
])

 1.2 批標准化 (Batch Normalization,BN)

  標准化:使數據符合0均值,1為標准差的分布。
  批標准化:對一小批數據 (batch),做標准化處理 。
  設有 n 個卷積核,一共有 batch 組輸出,每組深度都是 n :


  此外,還可以引入可訓練參數 γ (縮放因子) 和 β (偏移因子),調整批歸一化的力度:

   BN 層位於卷積層之后,激活層之前,eg:

model = tf.keras.models.Sequential([
  Conv2D(filters=6, kernel_size=(5, 5), padding='same'), # 卷積層
  BatchNormalization(), # BN層
  Activation('relu'), # 激活層
  MaxPool2D(pool_size=(2, 2), strides=2, padding='same'), # 池化層
  Dropout(0.2), # dropout層
])

 1.3 池化 (Pooling)

  池化用於減少特征數據量。最大值池化可提取圖片紋理,均值池化可保留背景特征。
tf.keras.layers.MaxPool2D(
  pool_size=池化核尺寸,#正方形寫核長整數,或(核高h,核寬w)
  strides=池化步長,#步長整數, 或(縱向步長h,橫向步長w),默認為pool_size
  padding=‘valid’or‘same’ #使用全零填充是“same”,不使用是“valid”(默認)
)
tf.keras.layers.AveragePooling2D(
  pool_size=池化核尺寸,#正方形寫核長整數,或(核高h,核寬w)
  strides=池化步長,#步長整數, 或(縱向步長h,橫向步長w),默認為pool_size
  padding=‘valid’or‘same’ #使用全零填充是“same”,不使用是“valid”(默認)
)

eg:

model = tf.keras.models.Sequential([
  Conv2D(filters=6, kernel_size=(5, 5), padding='same'), # 卷積層
  BatchNormalization(), # BN層
  Activation('relu'), # 激活層
  MaxPool2D(pool_size=(2, 2), strides=2, padding='same'), # 池化層
  Dropout(0.2), # dropout層
])

1.4 舍棄 (Dropout)

  在神經網絡訓練時,將一部分神經元按照一定概率從神經網絡中暫時舍棄。神經網絡使用時,被舍棄的神經元恢復鏈接。
model = tf.keras.models.Sequential([
  Conv2D(filters=6, kernel_size=(5, 5), padding='same'), # 卷積層
  BatchNormalization(), # BN層
  Activation('relu'), # 激活層
  MaxPool2D(pool_size=(2, 2), strides=2, padding='same'), # 池化層
  Dropout(0.2), # dropout層
])

1.5 卷積神經網絡

  借助卷積核提取特征后,送入全連接網絡:

  卷積就是特征提取器,就是CBAPD:
model = tf.keras.models.Sequential([
  Conv2D(filters=6, kernel_size=(5, 5), padding='same'), # 卷積層
  BatchNormalization(), # BN層
  Activation('relu'), # 激活層
  MaxPool2D(pool_size=(2, 2), strides=2, padding='same'), # 池化層
  Dropout(0.2), # dropout層
])

1.6 卷積神經網絡搭建示例

  • C(核:6*5*5,步長:1,填充:same )
  • B(Yes)
  • A(relu)
  • P(max,核:2*2,步長:2,填充:same)
  • D(0.2)
  • Flatten
  • Dense(神經元:128,激活:relu,Dropout:0.2)
  • Dense(神經元:10,激活:softmax)

  數據集:Cifar10

import tensorflow as tf
import os
import numpy as np
from matplotlib import pyplot as plt
np.set_printoptions(threshold=np.inf)

cifar10 = tf.keras.datasets.cifar10
(x_train, y_train), (x_test, y_test) = cifar10.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0class Baseline(tf.keras.Model):
    def __init__(self):
        super(Baseline, self).__init__()
        self.c1 = tf.keras.layers.Conv2D(filters=6, kernel_size=(5, 5), padding='same')
        self.b1 = tf.keras.layers.BatchNormalization()
        self.a1 = tf.keras.layers.Activation('relu')
        self.p1 = tf.keras.layers.MaxPool2D(pool_size=(2, 2), strides=2, padding='same')
        self.d1 = tf.keras.layers.Dropout(0.2)
        self.flatten = tf.keras.layers.Flatten()
        self.f1 = tf.keras.layers.Dense(128, activation='relu')
        self.d2 = tf.keras.layers.Dropout(0.2)
        self.f2 = tf.keras.layers.Dense(10, activation='softmax')

    def call(self, inputs):
        x = self.c1(inputs)
        x = self.b1(x)
        x = self.a1(x)
        x = self.p1(x)
        x = self.d1(x)
        x = self.flatten(x)
        x = self.f1(x)
        x = self.d2(x)
        y = self.f2(x)
        return y


model = Baseline()
model.compile(optimizer=tf.keras.optimizers.Adam(),
              loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False),
              metrics=[tf.keras.metrics.sparse_categorical_accuracy])
history = model.fit(x_train, y_train, batch_size=32, epochs=5, validation_data=(x_test, y_test), validation_freq=1)
model.summary()

# show
acc = history.history['sparse_categorical_accuracy']
val_acc = history.history['val_sparse_categorical_accuracy']
loss = history.history['loss']
val_loss = history.history['val_loss']

plt.figure(figsize=(8, 8))
plt.subplot(1, 2, 1)
plt.plot(acc, label='Training Accuracy')
plt.plot(val_acc, label='Validation Accuracy')
plt.title('Training and Validation Accuracy')
plt.legend()

plt.subplot(1, 2, 2)
plt.plot(loss, label='Training loss')
plt.plot(val_loss, label='Validation loss')
plt.title('Training and Validation loss')
plt.legend()
plt.show()

 2 經典卷積網絡

2.1 LeNet

  LeNet 由 Yann LeCun 於1998年提出,是卷積網絡開篇之作。通過共享卷積核,減少了網絡參數。網絡結構如下圖所示:
  文獻:Yann Lecun, Leon Bottou, Y. Bengio, Patrick Haffner. Gradient-Based Learning Applied to Document Recognition. Proceedings of the IEEE, 1998

class LeNet5(tf.keras.Model):
    def __init__(self):
        super(LeNet5, self).__init__()
        self.c1 = tf.keras.layers.Conv2D(filters=6, kernel_size=(5, 5), activation='sigmoid')
        self.p1 = tf.keras.layers.MaxPool2D(pool_size=(2, 2), strides=2)
        self.c2 = tf.keras.layers.Conv2D(filters=16, kernel_size=(5, 5), activation='sigmoid')
        self.p2 = tf.keras.layers.MaxPool2D(pool_size=(2, 2), strides=2)
        self.flatten = tf.keras.layers.Flatten()
        self.f1 = tf.keras.layers.Dense(120, activation='sigmoid')
        self.f2 = tf.keras.layers.Dense(84, activation='sigmoid')
        self.f3 = tf.keras.layers.Dense(10, activation='softmax')

    def call(self, inputs):
        x = self.c1(inputs)
        x = self.p1(x)
        x = self.c2(x)
        x = self.p2(x)
        x = self.flatten(x)
        x = self.f1(x)
        x = self.f2(x)
        y = self.f3(x)
        return y
  總體上看,誕生於 1998 年的 LeNet5 與如今一些主流的 CNN 網絡相比,其結構相當簡單,不過它成功地利用“卷積提取特征→全連接分類”的經典思路解決了手寫數字識別的問題,對神經網絡研究的發展有着很重要的意義。

2.2 AlexNet

  AlexNet 網絡誕生於2012年,當年 ImageNet 競賽的冠軍,Top5錯誤率為16.4%。可以說 AlexNet 的出現使得已經沉寂多年的深度學習領域開啟了黃金時代。網絡結構如下圖所示:
  參考文獻:Alex Krizhevsky, Ilya Sutskever, Geoffrey E. Hinton. ImageNet Classification with Deep Convolutional Neural Networks. In NIPS, 2012.
  可以看到,圖所示的網絡結構將模型分成了兩部分,這是由於當時用於訓練 AlexNet 的顯卡為 GTX 580(顯存為 3GB),單塊顯卡運算資源不足的原因。

class AlexNet8(tf.keras.Model):
    def __init__(self):
        super(AlexNet8, self).__init__()
        self.c1 = tf.keras.layers.Conv2D(filters=96, kernel_size=(3, 3))
        self.b1 = tf.keras.layers.BatchNormalization()
        self.a1 = tf.keras.layers.Activation('relu')
        self.p1 = tf.keras.layers.MaxPool2D(pool_size=(3, 3), strides=2)

        self.c2 = tf.keras.layers.Conv2D(filters=256, kernel_size=(3, 3))
        self.b2 = tf.keras.layers.BatchNormalization()
        self.a2 = tf.keras.layers.Activation('relu')
        self.p2 = tf.keras.layers.MaxPool2D(pool_size=(3, 3), strides=2)

        self.c3 = tf.keras.layers.Conv2D(filters=384, kernel_size=(3, 3),
                                         padding='same', activation='relu')
        self.c4 = tf.keras.layers.Conv2D(filters=384, kernel_size=(3, 3),
                                         padding='same', activation='relu')
        self.c5 = tf.keras.layers.Conv2D(filters=256, kernel_size=(3, 3),
                                         padding='same', activation='relu')
        self.p3 = tf.keras.layers.MaxPool2D(pool_size=(3, 3), strides=2)

        self.flatten = tf.keras.layers.Flatten()
        self.f1 = tf.keras.layers.Dense(2048, activation='relu')
        self.d1 = tf.keras.layers.Dropout(0.5)
        self.f2 = tf.keras.layers.Dense(2048, activation='relu')
        self.d2 = tf.keras.layers.Dropout(0.5)
        self.f3 = tf.keras.layers.Dense(10, activation='softmax')

    def call(self, inputs):
        x = self.c1(inputs)
        x = self.b1(x)
        x = self.a1(x)
        x = self.p1(x)
        x = self.c2(x)
        x = self.b2(x)
        x = self.a2(x)
        x = self.p2(x)
        x = self.c3(x)
        x = self.c4(x)
        x = self.c5(x)
        x = self.p3(x)
        x = self.flatten(x)
        x = self.f1(x)
        x = self.d1(x)
        x = self.f2(x)
        x = self.d2(x)
        y = self.f3(x)
        return y
  可以看到,與結構類似的 LeNet5 相比,AlexNet 模型的參數量有了非常明顯的提升,卷積運算的層數也更多了,這有利於更好地提取特征;Relu 激活函數的使用加快了模型的訓練速度;Dropout 的使用提升了模型的魯棒性,這些優勢使得 AlexNet 的性能大大提升。

2.3 VGGNet

  VGGNet 誕生於2014年,當年 ImageNet 競賽的亞軍,Top5錯誤率減小到7.3%。

  VGGNet 網絡的最大改進是在網絡的深度上,由 AlexNet 的 8 層增加到了 16 層和 19 層,更深的網絡意味着更強的表達能力,這得益於強大的運算能力支持。VGGNet 的另一個顯著特點是僅使用了單一尺寸的 3 * 3 卷積核,事實上,3 * 3 的小卷積核在很多卷積網絡中都被大量使用,這是由於在感受野相同的情況下,小卷積核堆積的效果要優於大卷積核,同時參數量也更少。VGGNet 就使用了 3 * 3 的卷積核替代了 AlexNet 中的大卷積核(11 * 11、7 * 7、
5 * 5),取得了較好的效果(事實上上面利用 Keras 實現 AlexNet 已經采取了這種方式),VGGNet16 的網絡結構如圖所示:
  參考文獻:K. Simonyan, A. Zisserman. Very Deep Convolutional Networks for Large-Scale Image Recognition.In ICLR, 2015.
 
class VGG16(tf.keras.Model):
    def __init__(self):
        super(VGG16, self).__init__()
        self.c1 = tf.keras.layers.Conv2D(filters=64, kernel_size=(3, 3), padding='same')
        self.b1 = tf.keras.layers.BatchNormalization()
        self.a1 = tf.keras.layers.Activation('relu')

        self.c2 = tf.keras.layers.Conv2D(filters=64, kernel_size=(3, 3), padding='same')
        self.b2 = tf.keras.layers.BatchNormalization()
        self.a2 = tf.keras.layers.Activation('relu')
        self.p1 = tf.keras.layers.MaxPool2D(pool_size=(2, 2), strides=2, padding='same')
        self.d1 = tf.keras.layers.Dropout(0.2)

        self.c3 = tf.keras.layers.Conv2D(filters=128, kernel_size=(3, 3), padding='same')
        self.b3 = tf.keras.layers.BatchNormalization()
        self.a3 = tf.keras.layers.Activation('relu')

        self.c4 = tf.keras.layers.Conv2D(filters=128, kernel_size=(3, 3), padding='same')
        self.b4 = tf.keras.layers.BatchNormalization()
        self.a4 = tf.keras.layers.Activation('relu')
        self.p2 = tf.keras.layers.MaxPool2D(pool_size=(2, 2), strides=2, padding='same')
        self.d2 = tf.keras.layers.Dropout(0.2)

        self.c5 = tf.keras.layers.Conv2D(filters=256, kernel_size=(3, 3), padding='same')
        self.b5 = tf.keras.layers.BatchNormalization()
        self.a5 = tf.keras.layers.Activation('relu')

        self.c6 = tf.keras.layers.Conv2D(filters=256, kernel_size=(3, 3), padding='same')
        self.b6 = tf.keras.layers.BatchNormalization()
        self.a6 = tf.keras.layers.Activation('relu')

        self.c7 = tf.keras.layers.Conv2D(filters=256, kernel_size=(3, 3), padding='same')
        self.b7 = tf.keras.layers.BatchNormalization()
        self.a7 = tf.keras.layers.Activation('relu')
        self.p3 = tf.keras.layers.MaxPool2D(pool_size=(2, 2), strides=2, padding='same')
        self.d3 = tf.keras.layers.Dropout(0.2)

        self.c8 = tf.keras.layers.Conv2D(filters=512, kernel_size=(3, 3), padding='same')
        self.b8 = tf.keras.layers.BatchNormalization()
        self.a8 = tf.keras.layers.Activation('relu')

        self.c9 = tf.keras.layers.Conv2D(filters=512, kernel_size=(3, 3), padding='same')
        self.b9 = tf.keras.layers.BatchNormalization()
        self.a9 = tf.keras.layers.Activation('relu')

        self.c10 = tf.keras.layers.Conv2D(filters=512, kernel_size=(3, 3), padding='same')
        self.b10 = tf.keras.layers.BatchNormalization()
        self.a10 = tf.keras.layers.Activation('relu')
        self.p4 = tf.keras.layers.MaxPool2D(pool_size=(2, 2), strides=2, padding='same')
        self.d4 = tf.keras.layers.Dropout(0.2)

        self.c11 = tf.keras.layers.Conv2D(filters=512, kernel_size=(3, 3), padding='same')
        self.b11 = tf.keras.layers.BatchNormalization()
        self.a11 = tf.keras.layers.Activation('relu')

        self.c12 = tf.keras.layers.Conv2D(filters=512, kernel_size=(3, 3), padding='same')
        self.b12 = tf.keras.layers.BatchNormalization()
        self.a12 = tf.keras.layers.Activation('relu')

        self.c13 = tf.keras.layers.Conv2D(filters=512, kernel_size=(3, 3), padding='same')
        self.b13 = tf.keras.layers.BatchNormalization()
        self.a13 = tf.keras.layers.Activation('relu')
        self.p5 = tf.keras.layers.MaxPool2D(pool_size=(2, 2), strides=2, padding='same')
        self.d5 = tf.keras.layers.Dropout(0.2)

        self.flatten = tf.keras.layers.Flatten()
        self.f1 = tf.keras.layers.Dense(512, activation='relu')
        self.d1 = tf.keras.layers.Dropout(0.2)
        self.f2 = tf.keras.layers.Dense(512, activation='relu')
        self.d2 = tf.keras.layers.Dropout(0.2)
        self.f3 = tf.keras.layers.Dense(10, activation='softmax')
  總體來看,VGGNet的結構是相當規整的,它繼承了 AlexNet中的Relu激活函數、Dropout 操作等有效的方法,同時采用了單一尺寸的 3 * 3 小卷積核,形成了規整的 C(Convolution,卷積)、B(Batch normalization)、A(Activation,激活)、P(Pooling,池化)、D(Dropout)結構,這一典型結構在卷積神經網絡中的應用是非常廣的。

2.4 InceptionNet

  InceptionNet 誕生於2014年,當年 ImageNet 競賽冠軍,Top5錯誤率為6.67%。
  InceptionNet 的創新在於一層內使用不同尺寸的卷積核,提升感知力(通過 padding 實現輸出特征面積一致);使用 1 * 1 卷積核,改變輸出特征 channel 數(減少網絡參數)。
  InceptionNet 即 GoogLeNet,旨在通過增加網絡的寬度來提升網絡的能力,與 VGGNet 通過卷積層堆疊的方式(縱向)相比,是一個不同的方向(橫向)。顯然,InceptionNet 模型的構建與 VGGNet 及之前的網絡會有所區別,不再是簡單的縱
向堆疊,要理解 InceptionNet 的結構,首先要理解它的基本單元,如圖所示:
  參考文獻:Szegedy C, Liu W, Jia Y, et al. Going Deeper with Convolutions. In CVPR, 2015.

  可以看到,InceptionNet 的基本單元中,卷積部分是比較統一的 C、B、A 典型結構,即卷積→BN→激活,激活均采用 Relu 激活函數,同時包含最大池化操作。
  在 Tensorflow 框架下利用 Keras 構建 InceptionNet 模型時,可以將 C、B、A 結構封裝在一起,定義成一個新的 ConvBNRelu 類,以減少代碼量,同時更便於閱讀。
class ConvBNRelu(tf.keras.Model):
    def __init__(self, ch, kernelsz=3, strides=1, padding='same'):
        super(ConvBNRelu, self).__init__()
        self.model = tf.keras.models.Sequential([
            tf.keras.layers.Conv2D(ch, kernelsz, strides=strides, padding=padding),
            tf.keras.layers.BatchNormalization(),
            tf.keras.layers.Activation('relu')
        ])

    def call(self, inputs):
        x = self.model(inputs)
        return x
  參數 ch 代表特征圖的通道數,也即卷積核個數;kernelsz 代表卷積核尺寸;strides 代表卷積步長;padding 代表是否進行全零填充。完成了這一步后,就可以開始構建 InceptionNet 的基本單元了,同樣利用 class 定義的方式,定義一個新的 InceptionBlk 類。
class InceptionBlk(tf.keras.Model):
    def __init__(self, ch, strides=1):
        super(InceptionBlk, self).__init__()
        self.ch = ch
        self.strides = strides
        self.c1 = ConvBNRelu(ch, kernelsz=1, strides=strides)
        self.c2_1 = ConvBNRelu(ch, kernelsz=1, strides=strides)
        self.c2_2 = ConvBNRelu(ch, kernelsz=3, strides=strides)
        self.c3_1 = ConvBNRelu(ch, kernelsz=1, strides=strides)
        self.c3_2 = ConvBNRelu(ch, kernelsz=5, strides=strides)
        self.p4_1 = tf.keras.layers.MaxPool2D(3, strides=1, padding='same')
        self.c4_2 = ConvBNRelu(ch, kernelsz=1, strides=strides)

    def call(self, inputs):
        x1 = self.model(inputs)
        x2_1 = self.c2_1(inputs)
        x2_2 = self.c2_2(x2_1)
        x3_1 = self.c3_1(inputs)
        x3_2 = self.c3_2(x3_1)
        x4_1 = self.p4_1(inputs)
        x4_2 = self.c4_2(x4_1)
        x = tf.concat([x1, x2_2, x3_2, x4_2], axis=3)
        return x
  參數 ch 仍代表通道數,strides 代表卷積步長,與 ConvBNRelu 類中一致;tf.concat 函數將四個輸出連接在一起,x1、x2_2、x3_2、x4_2 分別代表圖 5-27 中的四列輸出,結合結構圖和代碼很容易看出二者的對應關系。
  InceptionNet 網絡的主體就是由其基本單元構成的,其模型結構如圖所示:
class Inception10(tf.keras.Model):
    def __init__(self, num_blocks, num_classes, init_ch=16, **kwargs):
        super(Inception10, self).__init__()
        self.in_channels = init_ch
        self.out_channels = init_ch
        self.num_blocks = num_blocks
        self.init_ch = init_ch
        self.c1 = ConvBNRelu(init_ch)
        self.blocks = tf.keras.Sequential()
        for block_id in range(num_blocks):
            for layer_id in range(2):
                if layer_id == 0:
                    block = InceptionBlk(self.out_channels, strides=2)
                else:
                    block = InceptionBlk(self.out_channels, strides=1)
                self.blocks.add(block)
            self.out_channels *= 2
        self.p1 = tf.keras.layers.GlobalAveragePooling2D()
        self.f1 = tf.keras.layers.Dense(num_classes, activation='softmax')

    def call(self, inputs):
        x = self.c1(inputs)
        x = self.blocks(x)
        x = self.p1(x)
        y = self.f1(x)
        return y
  InceptionNet 網絡不再像 VGGNet 一樣有三層全連接層(全連接層的參數量占 VGGNet 總參數量的 90 %),而是采用“全局平均池化+全連接層”的方式,這減少了大量的參數。
  這里介紹一下全局平均池化,在 tf.keras 中用 GlobalAveragePooling2D 函數實現,相比於平均池化(在特征圖上以窗口的形式滑動,取窗口內的平均值為采樣值),全局平均池化不再以窗口滑動的形式取均值,而是直接針對特征圖取平均值,即每個特征圖輸出一個值。通過這種方式,每個特征圖都與分類概率直接聯系起來,這替代了全連接層的功能,並且不產生額外的訓練參數,減小了過擬合的可能,但需要注意的是,使用全局平均池化會導致網絡收斂的速度變慢。
  總體來看,InceptionNet 采取了多尺寸卷積再聚合的方式拓寬網絡結構,並通過 1 * 1 的卷積運算來減小參數量,取得了比較好的效果,與同年誕生的 VGGNet 相比,提供了卷積神經網絡構建的另一種思路。但 InceptionNet 的問題是,當網絡深度不斷增加時,訓練會十分困難,甚至無法收斂(這一點被 ResNet 很好地解決了)。

2.5 ResNet

  ResNet 誕生於2015年,當年 ImageNet 競賽冠軍,Top5錯誤率為3.57%。其改進在於層間殘差跳連,引入前方信息,減少梯度消失,使神經網絡層數變身成為可能。
  ResNet 即深度殘差網絡,由何愷明及其團隊提出,是深度學習領域又一具有開創性的工作,通過對殘差結構的運用,ResNet 使得訓練數百層的網絡成為了可能,從而具有非常強大的表征能力,其網絡結構如圖所示

  ResNet 的核心是殘差結構,具體可見 CNN 理論基礎。ResNet 引入殘差結構最主要的目的是解決網絡層數不斷加深時導致的梯度消失問題,從之前介紹的 4 種 CNN 經典網絡結構我們也可以看出,網絡層數的發展趨勢是不斷加深的。這是由於深度網絡本身集成了低層/中層/高層特征和分類器,以多層首尾相連的方式存在,所以可以通過增加堆疊的層數(深度)來豐富特征的層次,以取得更好的效果。

  但如果只是簡單地堆疊更多層數,就會導致梯度消失(爆炸)問題,它從根源上導致了函數無法收斂。然而,通過標准初始化(normalized initialization)以及中間標准化層(intermediate normalization layer),已經可以較好地解決這個問題了,這使得深度為數十層的網絡在反向傳播過程中,可以通過隨機梯度下降(SGD)的方式開始收斂。

  但是,當深度更深的網絡也可以開始收斂時,網絡退化的問題就顯露了出來:隨着網絡深度的增加,准確率先是達到瓶頸(這是很常見的),然后便開始迅速下降。需要注意的是,這種退化並不是由過擬合引起的。對於一個深度比較合適的網絡來說,繼續增加層數反而會導致訓練錯誤率的提升。ResNet 解決的正是這個問題。

  參考文獻:Kaiming He, Xiangyu Zhang, Shaoqing Ren. Deep Residual Learning for Image Recognition. In CPVR, 2016.
class ResnetBlock(tf.keras.Model):
    def __init__(self, filters, strides=1, residual_path=False):
        super(ResnetBlock, self).__init__()
        self.filters = filters
        self.strides = strides
        self.residual_path = residual_path

        self.c1 = tf.keras.layers.Conv2D(filters, (3, 3), strides=strides, padding='same', use_bias=False)
        self.b1 = tf.keras.layers.BatchNormalization()
        self.a1 = tf.keras.layers.Activation('relu')

        self.c1 = tf.keras.layers.Conv2D(filters, (3, 3), strides=1, padding='same', use_bias=False)
        self.b1 = tf.keras.layers.BatchNormalization()

        if residual_path:
            self.down_c1 = tf.keras.layers.Conv2D(filters, (1, 1), strides=strides, padding='same', use_bias=False)
            self.down_b1 = tf.keras.layers.BatchNormalization()

        self.a2 = tf.keras.layers.Activation('relu')

    def call(self, inputs):
        residual = inputs

        x = self.c1(inputs)
        x = self.b1(x)
        x = self.a1(x)

        x = self.c2(x)
        y = self.b2(x)

        if self.residual_path:
            residual = self.down_c1(inputs)
            residual = self.down_b1(residual)

        out = self.a2(y + residual)
        return out


class ResNet18(tf.keras.Model):
    def __init__(self, block_list, initial_filters=64):
        super(ResNet18, self).__init__()
        self.num_block = len(block_list)
        self.block_list = block_list
        self.out_filters = initial_filters

        self.c1 = tf.keras.layers.Conv2D(self.out_filters, (3, 3), strides=1, padding='same', use_bias=False,
                                         kernel_initializer='he_normal')
        self.b1 = tf.keras.layers.BatchNormalization()
        self.a1 = tf.keras.layers.Activation('relu')
        self.blocks = tf.keras.models.Sequential()

        for block_id in range(len(block_list)):  # 第幾個 resnet block
            for layer_id in range(block_list[block_id]):  # 第幾個卷積層
                if block_id != 0 and layer_id == 0:
                    block = ResnetBlock(self.out_filters, strides=2, residual_path=True)
                else:
                    block = ResnetBlock(self.out_filters,residual_path=False)
                self.blocks.add(block)
            self.out_filters *= 2  # 下一個block的卷積核是上一個block的兩倍
        self.p1 = tf.keras.layers.GlobalAveragePooling2D()
        self.f1 = tf.keras.layers.Dense(10)

    def call(self, inputs):
        x = self.c1(inputs)
        x = self.b1(x)
        x = self.a1(x)
        x = self.blocks(x)
        x = self.p1(x)
        y = self.f1(x)
        return y


model = ResNet18([2, 2, 2, 2])

   對於 ResNet 的殘差單元來說,除了這里采用的兩層結構外,還有一種三層結構。兩層殘差單元多用於層數較少的網絡,三層殘差單元多用於層數較多的網絡,以減少計算的參數量。

  總體上看,ResNet 取得的成果還是相當巨大的,它將網絡深度提升到了 152 層,於 2015 年將 ImageNet 圖像識別 Top5 錯誤率降至 3.57 %。

 

 
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM