1.ResNet的借鑒點
層間殘差跳連,引入前方信息,減少梯度消失,使神經網絡層數變深成為可能。
2.介紹
ResNet 即深度殘差網絡,由何愷明及其團隊提出,是深度學習領域又一具有開創性的工作,通過對殘差結構的運用, ResNet 使得訓練數百層的網絡成為了可能,從而具有非常強大的表征能力,其網絡結構如圖 5-31 所示。
ResNet 引入殘差結構最主要的目的是解決網絡層數不斷加深時導致的梯度消失問題,從之前介紹的 4 種 CNN 經典網絡結構我們也可以看出,網絡層數的發展趨勢是不斷加深的。這是由於深度網絡本身集成了低層/中層/高層特征和分類器,以多層首尾相連的方式存在,所以可以通過增加堆疊的層數(深度)來豐富特征的層次,以取得更好的效果。
但如果只是簡單地堆疊更多層數,就會導致梯度消失(爆炸)問題,它從根源上導致了函數無法收斂。然而,通過標准初始化( normalized initialization)以及中間標准化層(intermediate normalization layer),已經可以較好地解決這個問題了,這使得深度為數十層的網絡在反向傳播過程中,可以通過隨機梯度下降(SGD)的方式開始收斂。
但是,當深度更深的網絡也可以開始收斂時,網絡退化的問題就顯露了出來:隨着網絡深度的增加,准確率先是達到瓶頸(這是很常見的),然后便開始迅速下降。需要注意的是,這種退化並不是由過擬合引起的。對於一個深度比較合適的網絡來說,繼續增加層數反而會導致訓練錯誤率的提升,圖 5-33 就是一個例子。
56層的卷積網絡錯誤率要高於20層卷積網絡錯誤率,單純堆疊網絡層數,會使神經網絡模型退化,以至於后邊的特征丟失前邊特征的原本模樣。
3.ResNet核心
ResNet 的核心是殘差結構,如圖 5-32 所示。在殘差結構中, ResNet 不再讓下一層直接擬合我們想得到的底層映射,而是令其對一種殘差映射進行擬合。若期望得到的底層映射為H(x), 我們令堆疊的非線性層擬合另一個映射 F(x) := H(x) – x, 則原有映射變為 F(x) + x。對這種新的殘差映射進行優化時,要比優化原有的非相關映射更為容易。不妨考慮極限情況,如果一個恆等映射是最優的,那么將殘差向零逼近顯然會比利用大量非線性層直接進行擬合更容易。
值得一提的是,這里的相加與 InceptionNet 中的相加是有本質區別的, Inception 中的相加是沿深度方向疊加, 像“千層蛋糕”一樣, 對層數進行疊加; ResNet 中的相加則是特征圖對應元素的數值相加,類似於 python 語法中基本的矩陣相加。
ResNet 解決的正是這個問題,其核心思路為:對一個准確率達到飽和的淺層網絡,在它后面加幾個恆等映射層(即 y = x,輸出等於輸入),增加網絡深度的同時不增加誤差。這使得神經網絡的層數可以超越之前的約束,提高准確率。圖 5-34 展示了 ResNet 中殘差結構的具體用法。
注意:
ResNet塊有兩種形式:一種是在堆疊卷積前后維度相同,另一種是在堆疊卷積前后維度不同
一種情況用圖中實現表示,兩層堆疊卷積沒有改變特征圖維度,也就是他們特征圖的個數,高、寬、深度都相同,可以直接將F(x)與相加
另一種情況用圖中虛線表示,兩層堆疊卷積改變了特征圖的維度,需要借助1x1的卷積來調整x(即輸入特征inputs)的維度,使w(x)與F(x)的維度一致

細節:
代碼:
import tensorflow as tf import os import numpy as np from matplotlib import pyplot as plt from tensorflow.keras.layers import Conv2D, BatchNormalization, Activation, MaxPool2D, Dropout, Flatten, Dense from tensorflow.keras import Model 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.0 class ResnetBlock(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 = Conv2D(filters, (3, 3), strides=strides, padding='same', use_bias=False) self.b1 = BatchNormalization() self.a1 = Activation('relu') self.c2 = Conv2D(filters, (3, 3), strides=1, padding='same', use_bias=False) self.b2 = BatchNormalization() # residual_path為True時,對輸入進行下采樣,即用1x1的卷積核做卷積操作,保證x能和F(x)維度相同,順利相加 if residual_path: self.down_c1 = Conv2D(filters, (1, 1), strides=strides, padding='same', use_bias=False) self.down_b1 = BatchNormalization() self.a2 = Activation('relu') def call(self, inputs): residual = inputs # residual等於輸入值本身,即residual=x # 將輸入通過卷積、BN層、激活層,計算F(x) 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) # 最后輸出的是兩部分的和,即F(x)+x或F(x)+Wx,再過激活函數 return out class ResNet18(Model): def __init__(self, block_list, initial_filters=64): # block_list表示每個block有幾個卷積層 super(ResNet18, self).__init__() self.num_blocks = len(block_list) # 共有幾個block self.block_list = block_list self.out_filters = initial_filters self.c1 = Conv2D(self.out_filters, (3, 3), strides=1, padding='same', use_bias=False) self.b1 = BatchNormalization() self.a1 = Activation('relu') self.blocks = tf.keras.models.Sequential() # 構建ResNet網絡結構 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以外的每個block的輸入進行下采樣 block = ResnetBlock(self.out_filters, strides=2, residual_path=True) else: block = ResnetBlock(self.out_filters, residual_path=False) self.blocks.add(block) # 將構建好的block加入resnet self.out_filters *= 2 # 下一個block的卷積核數是上一個block的2倍 self.p1 = tf.keras.layers.GlobalAveragePooling2D() self.f1 = tf.keras.layers.Dense(10, activation='softmax', kernel_regularizer=tf.keras.regularizers.l2()) 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]) model.compile(optimizer='adam', loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False), metrics=['sparse_categorical_accuracy']) checkpoint_save_path = "./checkpoint/ResNet18.ckpt" if os.path.exists(checkpoint_save_path + '.index'): print('-------------load the model-----------------') model.load_weights(checkpoint_save_path) cp_callback = tf.keras.callbacks.ModelCheckpoint(filepath=checkpoint_save_path, save_weights_only=True, save_best_only=True) history = model.fit(x_train, y_train, batch_size=32, epochs=5, validation_data=(x_test, y_test), validation_freq=1, callbacks=[cp_callback]) model.summary() # print(model.trainable_variables) file = open('./weights.txt', 'w') for v in model.trainable_variables: file.write(str(v.name) + '\n') file.write(str(v.shape) + '\n') file.write(str(v.numpy()) + '\n') file.close() ############################################### show ############################################### # 顯示訓練集和驗證集的acc和loss曲線 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.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()