Residual Networks
參考:https://blog.csdn.net/u013733326/article/details/80250818
歡迎來到本周的第二次作業!您將學習如何使用剩余網絡(ResNets)構建非常深的卷積網絡。理論上,深度很深的網絡可以代表非常復雜的功能;但實際上,他們很難訓練。剩余網絡,由He等人介紹,允許你訓練更深層次的網絡比以前實際可行。
在這個作業中,你將:
實現resnet的基本構建塊。
把這些構建塊放在一起來實現和訓練一個最先進的圖像分類神經網絡。
這項任務將在Keras中完成。
在開始這個問題之前,讓我們運行下面的單元來加載所需的包。
import numpy as np from keras import layers from keras.layers import Input, Add, Dense, Activation, ZeroPadding2D, BatchNormalization, Flatten, Conv2D, AveragePooling2D, MaxPooling2D, GlobalMaxPooling2D from keras.models import Model, load_model from keras.preprocessing import image from keras.utils import layer_utils from keras.utils.data_utils import get_file from keras.applications.imagenet_utils import preprocess_input import pydot from IPython.display import SVG from keras.utils.vis_utils import model_to_dot from keras.utils import plot_model from resnets_utils import * from keras.initializers import glorot_uniform import scipy.misc from matplotlib.pyplot import imshow %matplotlib inline import os os.environ['KERAS_BACKEND'] = 'tensorflow' import keras.backend as K K.set_image_data_format('channels_last') K.set_learning_phase(1)
1 - The problem of very deep neural networks深層網絡的麻煩
上周,我們構建了第一個卷積神經網絡。最近幾年,卷積神經網絡變得越來越深,從只從幾層(例如AlexNet)到超過一百層。
使用深層網絡最大的好處就是它能夠完成很復雜的功能,它能夠從邊緣(淺層)到非常復雜的特征(深層)中不同的抽象層次的特征中學習。然而,使用比較深的網絡通常沒有什么好處,一個特別大的麻煩就在於訓練的時候會產生梯度消失,非常深的網絡通常會有一個梯度信號,該信號會迅速的消退,從而使得梯度下降變得非常緩慢。更具體的說,在梯度下降的過程中,當你從最后一層回到第一層的時候,你在每個步驟上乘以權重矩陣,因此梯度值可以迅速的指數式地減少到0(在極少數的情況下會迅速增長,造成梯度爆炸)。
在訓練的過程中,你可能會看到開始幾層的梯度的大小(或范數)迅速下降到0,如下圖:
**Figure 1** : **Vanishing gradient**梯度消失
在前幾層中隨着迭代次數的增加,學習的速度會下降的非常快。
為了解決這個問題,我們將構建殘差網絡。
2 - Building a Residual Network 構建一個殘差網絡
在殘差網絡中,一個“捷徑(shortcut)”或者說“跳躍連接(skip connection)”允許梯度直接反向傳播到更淺的層,如下圖:
**Figure 2** : A ResNet block showing a **skip-connection**
圖像左邊是神經網絡的主路,圖像右邊是添加了一條捷徑的主路,通過這些殘差塊堆疊在一起,可以形成一個非常深的網絡。
我們在視頻中可以看到使用捷徑的方式使得每一個殘差塊能夠很容易學習到恆等式功能,這意味着我們可以添加很多的殘差塊而不會損害訓練集的表現。
殘差塊有兩種類型,主要取決於輸入輸出的維度是否相同,下面我們來看看吧~
2.1 - The identity block 恆等塊
恆等塊是殘差網絡使用的的標准塊,對應於輸入的激活值(比如𝑎[𝑙])與輸出激活值(比如𝑎[𝑙+ 2])具有相同的維度。為了具象化殘差塊的不同步驟,我們來看看下面的圖吧~,這里有一個替代的圖表顯示各個步驟:
**Figure 3** : **Identity block.** Skip connection "skips over" 2 layers.
上圖中,上面的曲線路徑是“捷徑”,下面的直線路徑是主路徑。在上圖中,我們依舊把CONV2D 與 ReLU包含到了每個步驟中,為了提升訓練的速度,我們在每一步也把數據進行了歸一化(BatchNorm),不要害怕這些東西,因為Keras框架已經實現了這些東西,調用BatchNorm只需要一行代碼。 在實踐中,我們要做一個更強大的版本:跳躍連接會跳過3個隱藏層而不是兩個,就像下圖:
**Figure 4** : **Identity block.** Skip connection "skips over" 3 layers.
下面是單獨的步驟。
主路徑第一部分:
1、第一CONV2D有𝐹1個過濾器,大小為(1, 1),步長為(1, 1)。它的填充方式是“valid”,命名規則是 conv_name_base + '2a'。使用0作為隨機初始化的種子。
2、第一個BatchNorm是對channels(通道)的軸進行規范化、歸一化。其命名規則為 bn_name_base + '2a'。
3、然后應用ReLU激活函數。它沒有命名和超參數。
主路徑第二分部分:
1、第二個CONV2D有𝐹2個過濾器,大小為(𝑓,𝑓)和(1,1)的步伐。它的填充方式是“same”,它的命名規則是 conv_name_base + '2b'。使用0作為隨機初始化的種子。
2、第二個BatchNorm是通道的軸歸一化。它的命名規則為 bn_name_base + '2b'。
3、然后應用ReLU激活函數。它沒有名稱和超參數。
主路徑第三分量:
1、第三CONV2D有𝐹3個過濾器,形狀大小為(1,1)和(1,1)的步伐。它的填充方式是“valid”,它的命名方式是 conv_name_base + '2c'。使用0作為隨機初始化的種子。
2、第三個BatchNorm是通道的軸歸一化。它的名稱應該是bn_name_base + '2c'
3、注意,該組件中沒有ReLU激活函數。
最后一步:
1、將捷徑與輸入加在一起
2、然后應用ReLU激活函數。它沒有命名和超參數。
接下來我們就要實現殘差網絡的恆等塊了,請務必查看下面的中文手冊:

1 # GRADED FUNCTION: identity_block 2 3 def identity_block(X, f, filters, stage, block): 4 """ 5 Implementation of the identity block as defined in Figure 3 實現圖3的恆等塊 6 7 Arguments: 8 X -- input tensor of shape (m, n_H_prev, n_W_prev, n_C_prev) 輸入的tensor類型的數據,維度為( m, n_H_prev, n_W_prev, n_H_prev ) 9 f -- integer, specifying the shape of the middle CONV's window for the main path 整數,指定主路徑中間的CONV窗口的維度 10 filters -- python list of integers, defining the number of filters in the CONV layers of the main path 11 整數列表,定義了主路徑每層的卷積層的過濾器數量 12 13 stage -- integer, used to name the layers, depending on their position in the network 14 整數,根據每層的位置來命名每一層,與block參數一起使用。 15 block -- string/character, used to name the layers, depending on their position in the network 16 字符串,據每層的位置來命名每一層,與stage參數一起使用。 17 18 Returns: 19 X -- output of the identity block, tensor of shape (n_H, n_W, n_C) 恆等塊的輸出,tensor類型,維度為(n_H, n_W, n_C) 20 """ 21 22 # defining name basis 定義命名規則 23 conv_name_base = 'res' + str(stage) + block + '_branch' 24 bn_name_base = 'bn' + str(stage) + block + '_branch' 25 26 # Retrieve Filters 獲取過濾器 27 F1, F2, F3 = filters 28 29 # Save the input value. You'll need this later to add back to the main path. 保存輸入數據,將會用於為主路徑添加捷徑 30 X_shortcut = X 31 32 # First component of main path 主路徑的第一部分 33 ##卷積層 34 X = Conv2D(filters = F1, kernel_size = (1, 1), strides = (1,1), padding = 'valid', name = conv_name_base + '2a', kernel_initializer = glorot_uniform(seed=0))(X) 35 #歸一化 36 X = BatchNormalization(axis = 3, name = bn_name_base + '2a')(X) 37 #使用relu激活函數 38 X = Activation('relu')(X) 39 40 ### START CODE HERE ### 41 42 # Second component of main path (≈3 lines)主路徑的第二部分 43 X = Conv2D(filters = F2, kernel_size = (f, f), strides = (1, 1), padding = 'same', name = conv_name_base + '2b', kernel_initializer = glorot_uniform(seed = 0))(X) 44 X = BatchNormalization(axis = 3, name = bn_name_base + '2b')(X) 45 X = Activation('relu')(X) 46 47 # Third component of main path (≈2 lines) 48 X = Conv2D(filters = F3, kernel_size = (1, 1), strides = (1, 1), padding = 'valid', name = conv_name_base + '2c', kernel_initializer = glorot_uniform(seed=0))(X) 49 50 X = BatchNormalization(axis = 3, name = bn_name_base + '2c')(X) 51 52 # Final step: Add shortcut value to main path, and pass it through a RELU activation (≈2 lines)最后一步,將捷徑與輸入加在一起 53 X = layers.add([X, X_shortcut]) 54 X = Activation('relu')(X) 55 56 ### END CODE HERE ### 57 58 return X
# tf.reset_default_graph() ops.reset_default_graph with tf.compat.v1.Session() as test: np.random.seed(1) A_prev = tf.compat.v1.placeholder("float", [3, 4, 4, 6]) X = np.random.randn(3, 4, 4, 6) A = identity_block(A_prev, f = 2, filters = [2, 4, 6], stage = 1, block = 'a') test.run(tf.compat.v1.global_variables_initializer()) out = test.run([A], feed_dict={A_prev: X, K.learning_phase(): 0}) print("out = " + str(out[0][1][1][0])) test.close()
執行結果:
2.2 - The convolutional block 卷積塊
我們已經實現了殘差網絡的恆等塊,現在,殘差網絡的卷積塊是另一種類型的殘差塊,它適用於輸入輸出的維度不一致的情況,它不同於上面的恆等塊,與之區別在於,捷徑中有一個CONV2D層,如下圖:
**Figure 4** : **Convolutional block**卷積塊
捷徑中的卷積層將把輸入 X 卷積為不同的維度,因此在主路徑最后那里需要適配捷徑中的維度。比如:把激活值中的寬高減少2倍,我們可以使用1x1的卷積,步伐為2。捷徑上的卷積層不使用任何非線性激活函數,它的主要作用是僅僅應用(學習后的)線性函數來減少輸入的維度,以便在后面的加法步驟中的維度相匹配。
具體步驟如下:
1、主路徑第一部分:
1、第一個卷積層有 F1 個過濾器,其維度為(1, 1),步幅為(s,s),使用 ‘valid’ 的填充方式,命名規則為conv_name_base + '2a'
2、第一個規范層是通道的軸歸一化,其命名規則為:bn_name_base + '2a'
3、使用ReLU激活函數,它沒有命名規則也沒有超參數。
2、主路徑的第二部分:
1、第二個卷積層有 F2 個過濾器,其維度為(f,f),步幅為(1, 1),使用‘same’ 的填充方式,命名規則為conv_name_base +'2b'
2、第二個規范層是通道的軸歸一化,其命名規則為:bn_name_base + '2b'
3、使用ReLU激活函數,它沒有命名規則也沒有超參數。
3、主路徑的第三部分
1、第三個卷積層有 F3 個過濾器,其維度為(1, 1),步幅為(s,s),使用‘valid’ 的填充方式,命名規則為:conv_name_base + '2c'
2、第三個規范層是通道的軸歸一化,命名規則為:bn_name_base + '2c'
3、沒有激活函數
4、捷徑:
1、此卷積層有 F3 個過濾器,其維度為(1,1),步幅為(s,s),使用‘valid’ 的填充方式,命名規則為:conv_name_base + '1'
2、此規范層是通道的軸歸一化,命名規則為:bn_name_base + '1'
5、最后一步:
1、將捷徑與輸入加在一起
2、使用ReLU激活函數
我們要做的是實現卷積塊,請務必查看下面的中文手冊:
Keras kernel_initializer 權重初始化的方法

1 # GRADED FUNCTION: convolutional_block 2 3 def convolutional_block(X, f, filters, stage, block, s = 2): 4 """ 5 Implementation of the convolutional block as defined in Figure 4 實現圖4的卷積塊 6 7 Arguments: 8 X -- input tensor of shape (m, n_H_prev, n_W_prev, n_C_prev) 輸入的tensor類型的變量,維度為( m, n_H_prev, n_W_prev, n_C_prev) 9 f -- integer, specifying the shape of the middle CONV's window for the main path 整數,指定主路徑中間的CONV窗口的維度 10 filters -- python list of integers, defining the number of filters in the CONV layers of the main path 11 整數列表,定義了主路徑每層的卷積層的過濾器數量 12 13 stage -- integer, used to name the layers, depending on their position in the network 14 整數,根據每層的位置來命名每一層,與block參數一起使用。 15 block -- string/character, used to name the layers, depending on their position in the network 16 字符串,據每層的位置來命名每一層,與stage參數一起使用。 17 s -- Integer, specifying the stride to be used 整數,指定要使用的步幅 18 19 Returns: 20 X -- output of the convolutional block, tensor of shape (n_H, n_W, n_C) 卷積塊的輸出,tensor類型,維度為(n_H, n_W, n_C) 21 """ 22 23 # defining name basis 定義命名規則 24 conv_name_base = 'res' + str(stage) + block + '_branch' 25 bn_name_base = 'bn' + str(stage) + block + '_branch' 26 27 # Retrieve Filters獲取過濾器數量 28 F1, F2, F3 = filters 29 30 # Save the input value 保存輸入數據 31 X_shortcut = X 32 33 34 ##### MAIN PATH ##### 主路徑 35 # First component of main path 主路徑第一部分 36 X = Conv2D(filters = F1, kernel_size = (1, 1), strides = (s, s), padding = 'valid', name = conv_name_base + '2a', kernel_initializer = glorot_uniform(seed=0))(X) 37 X = BatchNormalization(axis = 3, name = bn_name_base + '2a')(X) 38 X = Activation('relu')(X) 39 40 #Glorot正態分布初始化方法,也稱作Xavier正態分布初始化 41 42 ### START CODE HERE ### 43 44 # Second component of main path (≈3 lines) 45 X = Conv2D(filters = F2, kernel_size = (f, f), strides = (1, 1), padding = 'same', name = conv_name_base + '2b', kernel_initializer = glorot_uniform(seed=0))(X) 46 X = BatchNormalization(axis = 3, name = bn_name_base + '2b')(X) 47 X = Activation('relu')(X) 48 49 # Third component of main path (≈2 lines) 50 X = Conv2D(filters = F3, kernel_size = (1, 1), strides = (1,1), padding = 'valid', name = conv_name_base + '2c', kernel_initializer = glorot_uniform(seed=0))(X) 51 X = BatchNormalization(axis = 3, name = bn_name_base + '2c')(X) 52 53 ##### SHORTCUT PATH #### (≈2 lines) 54 X_shortcut = Conv2D(filters = F3, kernel_size = (1, 1), strides = (s, s), padding = 'valid', name = conv_name_base + '1', kernel_initializer = glorot_uniform(seed=0))(X_shortcut) 55 X_shortcut = BatchNormalization(axis = 3, name = bn_name_base + '1')(X_shortcut) 56 57 # Final step: Add shortcut value to main path, and pass it through a RELU activation (≈2 lines) 58 X = Add()([X, X_shortcut]) 59 X = Activation('relu')(X) 60 61 ### END CODE HERE ### 62 63 return X
# tf.reset_default_graph() ops.reset_default_graph() with tf.compat.v1.Session() as test: np.random.seed(1) A_prev = tf.compat.v1.placeholder("float", [3, 4, 4, 6]) X = np.random.randn(3, 4, 4, 6) A = convolutional_block(A_prev, f = 2, filters = [2, 4, 6], stage = 1, block = 'a') test.run(tf.compat.v1.global_variables_initializer()) out = test.run([A], feed_dict={A_prev: X, K.learning_phase(): 0}) print("out = " + str(out[0][1][1][0])) test.close()
執行結果:
3 - Building your first ResNet model (50 layers) 構建你的第一個殘差網絡(50層)
我們已經做完所需要的所有殘差塊了,下面這個圖就描述了神經網絡的算法細節,圖中的"ID BLOCK"是指標准的恆等塊,"ID BLOCK X3"是指把三個恆等塊放在一起。
**Figure 5** : **ResNet-50 model**
這個50層的網絡的細節如下:
1、對輸入數據進行0填充,padding = (3, 3)
2、stage1:
1、卷積層有64個過濾器,其維度為(7, 7),步幅為(2, 2),命名為‘conv1’
2、規范層(BatchNorm)對輸入數據進行通道軸歸一化
3、最大池化層使用一個(3, 3)的窗口和(2, 2)的步幅
3、stage2:
1.卷積塊使用 f = 3個大小為 [64, 64, 256] 的過濾器,f = 3, s = 1, block = ‘a’
2、 2個恆等塊使用三個大小為 [64, 64, 256] 的過濾器,f = 3, block = ‘b’、‘c’
4、stage3:
1、卷積塊使用 f=3 個大小為 [128, 128, 512] 的過濾器,f = 3, s = 2, block = ‘a’
2、 3個恆等塊使用三個大小為[128, 128, 512] 的過濾器,f = 3, block = ‘b’、‘c’、‘d’
5、stage4:
1、卷積塊使用f=3個大小為[256,256,1024]的過濾器,f=3,s=2,block=“a”
2、 5個恆等塊使用三個大小為[256,256,1024]的過濾器,f=3,block=“b”、“c”、“d”、“e”、“f”
6、stage5:
1、卷積塊使用f=3個大小為[512,512,2048]的過濾器,f=3,s=2,block=“a”
2、 2個恆等塊使用三個大小為[256,256,2048]的過濾器,f=3,block=“b”、“c”
7、均值池化層使用維度為(2,2)的窗口,命名為“avg_pool”
8、展開操作沒有任何超參數以及命名
9、全連接層(密集連接)使用softmax激活函數,命名為:‘fc’ + str(classes)
為了實現這50層的殘差網絡,我們需要查看一下手冊:

1 # GRADED FUNCTION: ResNet50 2 3 def ResNet50(input_shape = (64, 64, 3), classes = 6): 4 """ 5 Implementation of the popular ResNet50 the following architecture: 6 CONV2D -> BATCHNORM -> RELU -> MAXPOOL -> CONVBLOCK -> IDBLOCK*2 -> CONVBLOCK -> IDBLOCK*3 7 -> CONVBLOCK -> IDBLOCK*5 -> CONVBLOCK -> IDBLOCK*2 -> AVGPOOL -> TOPLAYER 8 9 Arguments: 10 input_shape -- shape of the images of the dataset 圖像數據集的維度 11 classes -- integer, number of classes 整數,分類數 12 13 Returns: 14 model -- a Model() instance in Keras Keras框架的模型 15 """ 16 17 # Define the input as a tensor with shape input_shape 定義tensor類型的輸入數據 18 X_input = Input(input_shape) 19 20 21 # Zero-Padding 0填充 22 X = ZeroPadding2D((3, 3))(X_input) 23 24 # Stage 1 25 X = Conv2D(64, (7, 7), strides = (2, 2), name = 'conv1', kernel_initializer = glorot_uniform(seed=0))(X) 26 X = BatchNormalization(axis = 3, name = 'bn_conv1')(X) 27 X = Activation('relu')(X) 28 X = MaxPooling2D((3, 3), strides=(2, 2))(X) 29 30 # Stage 2 31 X = convolutional_block(X, f = 3, filters = [64, 64, 256], stage = 2, block='a', s = 1) 32 X = identity_block(X, 3, [64, 64, 256], stage=2, block='b') 33 X = identity_block(X, 3, [64, 64, 256], stage=2, block='c') 34 35 ### START CODE HERE ### 36 37 # Stage 3 (≈4 lines) 38 X = convolutional_block(X, f = 3, filters = [128, 128, 512], stage = 3, block = 'a', s = 2) 39 X = identity_block(X, 3, [128, 128, 512], stage = 3, block = 'b') 40 X = identity_block(X, 3, [128, 128, 512], stage = 3, block = 'c') 41 X = identity_block(X, 3, [128, 128, 512], stage = 3, block = 'd') 42 43 # Stage 4 (≈6 lines) 44 X = convolutional_block(X, f = 3, filters = [256, 256, 1024], stage = 4, block = 'a', s = 2) 45 X = identity_block(X, 3, [256, 256, 1024], stage = 4, block = 'b') 46 X = identity_block(X, 3, [256, 256, 1024], stage = 4, block = 'c') 47 X = identity_block(X, 3, [256, 256, 1024], stage = 4, block = 'd') 48 X = identity_block(X, 3, [256, 256, 1024], stage = 4, block = 'e') 49 X = identity_block(X, 3, [256, 256, 1024], stage = 4, block = 'f') 50 51 # Stage 5 (≈3 lines) 52 X = convolutional_block(X, f = 3, filters = [512, 512, 2048], stage = 5, block = 'a', s = 2) 53 X = identity_block(X, 3, [512, 512, 2048], stage = 5, block = 'b') 54 X = identity_block(X, 3, [512, 512, 2048], stage = 5, block = 'c') 55 56 # AVGPOOL (≈1 line). Use "X = AveragePooling2D(...)(X)" 均值池化層 57 X = AveragePooling2D(pool_size = (2, 2), padding = 'same')(X) 58 59 ### END CODE HERE ### 60 61 # output layer 輸出層 62 X = Flatten()(X) 63 X = Dense(classes, activation='softmax', name='fc' + str(classes), kernel_initializer = glorot_uniform(seed=0))(X) 64 65 66 # Create model 創建模型 67 model = Model(inputs = X_input, outputs = X, name='ResNet50') 68 69 return model
然后我們對模型做實體化和編譯工作:
model = ResNet50(input_shape = (64, 64, 3), classes = 6) model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
現在模型已經准備好了,接下來就是加載訓練集進行訓練。
**Figure 6** : **SIGNS dataset** 手勢數據集
X_train_orig, Y_train_orig, X_test_orig, Y_test_orig, classes = load_dataset() # Normalize image vectors 歸一化 X_train = X_train_orig/255. X_test = X_test_orig/255. # Convert training and test labels to one hot matrices Y_train = convert_to_one_hot(Y_train_orig, 6).T Y_test = convert_to_one_hot(Y_test_orig, 6).T print ("number of training examples = " + str(X_train.shape[0])) print ("number of test examples = " + str(X_test.shape[0])) print ("X_train shape: " + str(X_train.shape)) print ("Y_train shape: " + str(Y_train.shape)) print ("X_test shape: " + str(X_test.shape)) print ("Y_test shape: " + str(Y_test.shape))
執行結果:
運行模型兩代,batch=32,每代大約3分鍾左右。
model.fit(X_train, Y_train, epochs = 2, batch_size = 32)
執行結果:
這里第二代的loss和准確度都和預期不一樣。。。
我們來評估一下模型:
preds = model.evaluate(X_test, Y_test) print ("Loss = " + str(preds[0])) print ("Test Accuracy = " + str(preds[1]))
執行結果:
額,准確率高的超過預期。。。。
在完成這個任務之后,如果願意的話,您還可以選擇繼續訓練RESNET。當我們訓練20代時,我們得到了更好的性能,但是在得在CPU上訓練需要一個多小時。使用GPU的話,博主已經在手勢數據集上訓練了自己的RESNET50模型的權重,你可以使用下面的代碼載並運行博主的訓練模型,加載模型可能需要1min。
#加載模型 model = load_model("ResNet50.h5")
然后測試一下博主訓練出來的權值:
preds = model.evaluate(X_test,Y_test) print("誤差值 = " + str(preds[0])) print("准確率 = " + str(preds[1]))
測試結果:
120/120 [==============================] - 4s 35ms/step 誤差值 = 0.108543064694 准確率 = 0.966666662693
4 - Test on your own image (Optional/Ungraded)
如果你願意,你也可以拍一張自己的手的照片,看看模型的輸出。要做到這一點:
1、點擊這個筆記本上面一欄的“文件”,然后點擊“打開”進入你的Coursera中心。
2、將您的圖像添加到此木星筆記本的目錄,在“images”文件夾
3、將圖像名稱寫入以下代碼
4、運行代碼並檢查算法是否正確!
img_path = 'images/my_image.jpg' img = image.load_img(img_path, target_size=(64, 64)) x = image.img_to_array(img) x = np.expand_dims(x, axis=0) x = preprocess_input(x) print('Input image shape:', x.shape) my_image = scipy.misc.imread(img_path) imshow(my_image) print("class prediction vector [p(0), p(1), p(2), p(3), p(4), p(5)] = ") print(model.predict(x))
您還可以通過運行以下代碼打印模型的摘要。
model.summary()
最后,運行下面的代碼以可視化ResNet50。你也可以通過“File -> Open…”- > model.png”。
plot_model(model, to_file='model.png') SVG(model_to_dot(model).create(prog='dot', format='svg'))
**你應該記住的:** -非常深的“普通”網絡在實踐中是行不通的,因為它們很難訓練,因為梯度會消失。
-跳轉連接幫助解決漸變消失的問題。它們還使ResNet塊易於學習標識函數。
-有兩種主要類型的塊:恆等塊和卷積塊。
-非常深的剩余網絡是通過將這些塊堆疊在一起而建立起來的。