【神经网络】(10) Resnet18、34 残差网络复现,附python完整代码
各位同學好,今天和大家分享一下 TensorFlow 深度學習中如何搭載 Resnet18 和 Resnet34 殘差神經網絡,殘差網絡利用 shotcut 的方法成功解決了網絡退化的問題,在訓練集和校驗集上,都證明了的更深的網絡錯誤率越小。
論文中給出的具體的網絡結構如下:
Resnet50 網絡結構我已經在之前的博客中復現過,感興趣的可以看一下:https://blog.csdn.net/dgvv4/article/details/121878494
感謝簡書大佬畫的殘差網絡結構圖:https://www.jianshu.com/p/085f4c8256f1
1. 構建單個殘差塊
一個殘差單元的結構如下。輸入為X ;weight layer 代表卷積層,這里是指?convolution卷積層 + batch normalization批標準化層 ;relu 是激活函數 ; identity 是將輸入 X 經過變換后與卷積層的輸出結果相加,下面會詳細說明。
殘差塊中的第一個卷積層 self.conv1,主要用于下采樣特征提取。
如果步長?strides=1,由于padding='same',該層的輸入和輸出特征圖的size不變。屬于結構圖中左側藍色部分。
如果步長?strides=2,表示該層輸出的特征圖的 size 是輸入的特征圖 size 的一半。由于卷積核的size是 3*3 ,卷積核移動時會出現滑窗無法覆蓋所有的像素格的現象??赡軙霈F,該層的輸出特征圖size不等于輸入size的一半。通過padding='same'自動填充輸入圖像,讓輸出size等于一半的輸入size。屬于結構圖中的左側后三種顏色的部分
殘差塊中的第二個卷積層 self.conv2,主要用于進一步提取特征,不進行下采樣。
規定其步長 stride=1,由于padding='same',該層的輸入和輸出特征圖的size不變。
完成卷積部分convblock之后,接下來看短接部分identityblock
identity 負責將輸入 X的shape 變換到和卷積部分的輸出的shape相同。
如果第一個卷積層 self.conv1 的步長 strides=1,那么輸入特征圖的 shape 和卷積層輸出的特征圖的 shape 相同,這時 identity 不需要變換輸入特征圖 X 的shape。
如果第一個卷積層 self.conv1 的步長 strides=2,那么輸入特征圖的 size 變成了原來的一半。這時,為了能將輸入 X 和 卷積層輸出結果相加,需要通過 identity 重塑輸入 X 的shape。這里使用的是 1*1 卷積傳遞特征,1*1的卷積核遍歷所有的像素格后不會改變特征圖的size,設置步長strides=2,成功將特征圖的size變成原來的一半。屬于結構圖中的左側后三種顏色的部分。
這樣,我們就完成了對單個殘差塊中所有層的初始化,接下來將層之間的前向傳播過程寫在 call() 函數中。這里需要注意的就是 layers.add([out, identity]) ,將卷積層的輸出特征圖的結果和輸入的特征圖相加。identity 只負責將輸入特征圖的 shape 變換成和卷積部分輸出特征圖的 shape 相同。
該部分的代碼如下:
# Basic Bolck 殘差塊
# x--> 卷積 --> bn --> relu --> 卷積 --> bn --> 輸出
# |---------------Identity(短接)----------------|# 定義子類,一個殘差塊
class BasicBlock(layers.Layer): # 繼承父類的方法和屬性#(1)子類初始化# filter_num 代表傳入卷積核數量,將輸入圖像的通道數變成殘差塊的規定的通道數# stride 代表步長,默認為1,代表不對輸入圖片的size采樣,如果不做padding,得到的圖像的size就略小,做padding后輸入和輸出的size保持一致# strdie=2時,代表二分采樣,輸出的size只有輸入size的一半def __init__(self, filter_num, stride=1):# 繼承父類的初始化方法,# super()中的第一個參數是子類名稱,第二個是子類的實例化對象super(BasicBlock, self).__init__()# 在父類初始化的基礎上添加新的屬性# 卷積層1,傳入卷積核數量,卷積核size,步長# 如果stride=1,為避免輸出小于輸入,設置padding='same',使輸入等于輸出# 如果stride=2,若輸入為32*32,由于卷積核3*3的影響,使輸出不等于16*16,這時通過padding=same在輸入圖像上自動補全,如果輸出小于16會自動補成16self.conv1 = layers.Conv2D(filter_num, (3,3), strides=stride, padding='same')# 標準化層batchnormalizeationself.bn1 = layers.BatchNormalization()# relu激活函數層,沒有其他參數,可以作為一個函數使用多次。而有參數設置的一些層,只能單獨對應使用self.relu = layers.Activation('relu')# 卷積層2,如果上一個卷積層stride=2完成下采樣,那么這里的卷積層就不進行下采樣了,保持stride=1self.conv2 = layers.Conv2D(filter_num, (3,3), strides=1, padding='same')# 標準化層self.bn2 = layers.BatchNormalization()# identity層需進行維度變換,將原始輸入圖像和卷積后的圖像相匹配# 進行1*1卷積匹配通道數,通過stride匹配圖像的sizeself.downsample = Sequential() # 設置容器# 在容器中添加1*1卷積和步長變換# stride保持和第一個卷積層一致,保證convblock和identityblock能直接相加# 如果第一個卷積層的stride=1時,那么輸入和輸出的shape保持一致self.downsample.add(layers.Conv2D(filter_num, (1,1), strides=stride))#(2)前向傳播# 定義類方法,self為類實例化對象def call(self, inputs, training=None):# 卷積層1,調用初始化后的屬性x = self.conv1(inputs) # 輸入原始圖像x = self.bn1(x)x = self.relu(x)# 卷積層2x = self.conv2(x)out = self.bn2(x)# identity層,輸入的是原始輸入圖像identity = self.downsample(inputs)# 將convblock和identityblock相加得到最終的殘差塊的輸出結果output = layers.add([out, identity])# 最終結果經過一個非線性函數output = tf.nn.relu(output)# 返回殘差塊的輸出結果return output
2. 疊加多個殘差塊
上面我們已經成功完成了一個殘差塊,然而一個殘差結構是由多個殘差塊疊加而成的。下面是放大了的結構圖,可見 resnet18 中每一個殘差結構是由 2 個殘差單元組合而成
? ?
我們定義一個函數 build_resblock 用來組合殘差結構。這里需要注意的是,blocks 代表一個殘差結構需要堆疊幾個殘差單元,resnet18 和 32 中是2個??唇Y構圖可知,在殘差結構中只有第一個殘差單元具備下采樣改變特征圖 size 的能力。因此第一個殘差塊的步長 stride,需要根據輸入來確定。而除第一個以外的殘差塊都不會改變特征圖的size,因此固定步長stride=1。每一個殘差結構的卷積核個數都是相同的,要通過輸入來確定。?
# 利用單個已定義的殘差塊,疊加多個殘差塊# filter_num,代表當前圖像的特征圖個數# blocks,需要代表堆疊幾個殘差塊# stride,代表當前的步長,等于1def build_resblock(self, filter_num, blocks, strides=1):# 使用Sequential容器裝填網絡結構res_blocks = Sequential()# 在ResNet類中對BasicBlock類實例化,構成組合關系# ResNet類可調用BasicBlock類中的所有屬性和方法# 添加網絡層# 第一個殘差塊有下采樣功能,stride可能等于2res_blocks.add(BasicBlock(filter_num, strides))# 每個殘差結構中剩余的殘差塊不具備下采樣功能,stride=1for _ in range(1, blocks):# 殘差結構中剩余的殘差塊保持圖像的shape不變res_blocks.add(BasicBlock(filter_num, stride=1))# 返回構建的殘差結構return res_blocks
3. 構建殘差網絡
上面我們已經完成了殘差塊的構建,現在我們需要做的就是將這些殘差結構按順序堆疊在一起就能組建殘差網絡。
首先我們看初始化函數中的代碼。self.stem 是用來處理原始輸入圖像的,假設原始輸入的shape為 [224, 224, 3],根據網絡結構圖設置預處理卷積層的各個參數。通過最大池化 layers.MaxPool2D?指定步長為2,將預處理卷積層的特征圖的size減半?
接下去就可以根據每個殘差結構的配置參數,第一個殘差結構 self.layer1 由圖可知,沒有進行下采樣,因此步長 stride=1,第一個殘差結構中的卷積核個數統一是64個,每個殘差結構由2個殘差單元組成 layer_dims=[2,2,2,2],初始化時都是調用的上面定義的殘差結構函數 build_resblock。
第二個殘差結構?self.layer2 由圖可知,第一個殘差塊進行了下采樣,因此,要指定步長 strides=2,特征圖的 size 減半,特征圖的個數統一都是128。同理其他兩個殘差結構。
?? ?
最后將殘差層的輸出結果經過全局平均池化后放入全連接層,得出分類結果。?layers.GlobalAveragePooling2D() 是在通道維度上對w和h維度求平均值。將特征圖的shape從 [b, w, h, c] 變成 [b, 1, 1, c]?
完成對所有層的初始化 __init__ 之后,在 call() 方法中定義層與層之間的前向傳播的方法。
# 定義子類ResNet,繼承父類keras.Model
class ResNet(keras.Model):#(1)初始化# layer_dims=[2,2,2,2],resnet18包含4個殘差結構res_blocks,每個殘差結構中有2個殘差塊# num_classes 代表最終的輸出的分類數def __init__(self, layer_dims, num_classes=1000): # 調用父類的初始化方法super(ResNet, self).__init__(self)# 分配屬性# 原始圖像輸入的預處理卷積和池化self.stem = Sequential([layers.Conv2D(64, (7,7), strides=(2,2), padding='same'), # 3*3卷積提取特征layers.BatchNormalization(), # 標準化 layers.Activation('relu'), # 激活函數layers.MaxPool2D(pool_size=(3,3), strides=(2,2), padding='same')]) # 最大池化,輸入圖像的size減半# 創建4個殘差結構,layer_dims=[2,2,2,2]self.layer1 = self.build_resblock(64, layer_dims[0]) # 第一個殘差結構指定64個卷積核,包含2個殘差塊self.layer2 = self.build_resblock(128, layer_dims[1], strides=2) # 第二個殘差結構128個卷積核,包含2個殘差塊,步長為2,圖像的size減半self.layer3 = self.build_resblock(256, layer_dims[2], strides=2)self.layer4 = self.build_resblock(512, layer_dims[3], strides=2)# 全局平均池化,不管卷積層輸出的長和寬是多少,在channel維度上將所有的長和寬加起來取均值# [b, w, h, c] ==> [b,c]self.avgpool = layers.GlobalAveragePooling2D()# 全連接層用于圖像分類self.fc = layers.Dense(num_classes)#(2)定義前向傳播的類方法def call(self, inputs, training=None):# 原始輸入經過預處理卷積層x = self.stem(inputs)# 經過4個殘差結構x = self.layer1(x)x = self.layer2(x)x = self.layer3(x)x = self.layer4(x)# 輸出層x = self.avgpool(x) # 輸出shape[b,c] --> [None, 512]x = self.fc(x) # 輸出[b,1000]# 返回分類結果return x
4. 打印查看網絡結構
resnet18 和 resnet34 的差別就在每個殘差結構所包含的殘差塊的個數不同,因此,只需稍作修改修改。我們再看一下這張網絡結構表。
resnet18 網絡的每個殘差層的殘差塊個數都是2,因此設置參數 ResNet([2, 2, 2, 2]) ,即可返回網絡結構。同理,只需要指定 resnet34 網絡中每個殘差層的殘差塊個數?ResNet([3, 4, 6, 3]) 。返回得到了輸出層的結果,這時只需設置網絡輸入層的輸入維度 model.build(),那么整個網絡就構建完了。
# 構造resnet18,傳入參數
def resnet18():# 實例化網絡結構,一共有4個殘差結構,每個殘差結構由2個殘差塊組成return ResNet([2, 2, 2, 2]) # 構造resnet34,傳入參數
def resnet34():# 實例化,一共有4個殘差結構,每個殘差結構有如下個數的殘差單元return ResNet([3, 4, 6, 3]) # 主函數
def main():# 構造網絡resnet18model18 = resnet18() # 確定輸入層model18.build(input_shape=(None,224,224,3))# 查看網絡結構model18.summary()# 構造網絡resnet34model34 = resnet34()# 確定輸入層model34.build(input_shape=(None,224,224,3))# 查看網絡結構model34.summary() if __name__ == '__main__':main()
利用 model.summary() 查看網絡具體的結構,其中第一個 multiple 的值等于 [None, 512],第二個 multiple 的值等于 [None, 1000]。下表中,layer 是 sequential 的?Output Shape 代表每一個殘差層的輸出 shape
resnet18 網絡結構如下:
_________________________________________________________________Layer (type) Output Shape Param #
=================================================================sequential (Sequential) (None, 56, 56, 64) 9728 sequential_1 (Sequential) (None, 56, 56, 64) 157056 sequential_4 (Sequential) (None, 28, 28, 128) 543488 sequential_7 (Sequential) (None, 14, 14, 256) 2168320 sequential_10 (Sequential) (None, 7, 7, 512) 8662016 global_average_pooling2d (G multiple 0 lobalAveragePooling2D) dense (Dense) multiple 513000 =================================================================
Total params: 12,053,608
Trainable params: 12,045,800
Non-trainable params: 7,808
_________________________________________________________________
resnet34 網絡結構如下:
_________________________________________________________________Layer (type) Output Shape Param #
=================================================================sequential_13 (Sequential) (None, 56, 56, 64) 9728 sequential_14 (Sequential) (None, 56, 56, 64) 235584 sequential_18 (Sequential) (None, 28, 28, 128) 1168896 sequential_23 (Sequential) (None, 14, 14, 256) 7160320 sequential_30 (Sequential) (None, 7, 7, 512) 13648384 global_average_pooling2d_1 multiple 0 (GlobalAveragePooling2D) dense_1 (Dense) multiple 513000 =================================================================
Total params: 22,735,912
Trainable params: 22,720,680
Non-trainable params: 15,232
_________________________________________________________________
完整代碼:
# 類方法寫resnet18、34
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers, Sequential# Basic Bolck 殘差塊
# x--> 卷積 --> bn --> relu --> 卷積 --> bn --> 輸出
# |---------------Identity(短接)----------------|# 定義子類,一個殘差塊
class BasicBlock(layers.Layer): # 繼承父類的方法和屬性#(2)子類初始化# filter_num 代表傳入卷積核數量,將輸入圖像的通道數變成殘差塊的規定的通道數# stride 代表步長,默認為1,代表不對輸入圖片的size采樣,如果不做padding,得到的圖像的size就略小,做padding后輸入和輸出的size保持一致# strdie=2時,代表二分采樣,輸出的size只有輸入size的一半def __init__(self, filter_num, stride=1):# 繼承父類的初始化方法,# super()中的第一個參數是子類名稱,第二個是子類的實例化對象super(BasicBlock, self).__init__()# 在父類初始化的基礎上添加新的屬性# 卷積層1,傳入卷積核數量,卷積核size,步長# 如果stride=1,為避免輸出小于輸入,設置padding='same',使輸入等于輸出# 如果stride=2,若輸入為32*32,由于卷積核3*3的影響,使輸出不等于16*16,這時通過padding=same在輸入圖像上自動補全,如果輸出小于16會自動補成16self.conv1 = layers.Conv2D(filter_num, (3,3), strides=stride, padding='same')# 標準化層batchnormalizeationself.bn1 = layers.BatchNormalization()# relu激活函數層,沒有其他參數,可以作為一個函數使用多次。而有參數設置的一些層,只能單獨對應使用self.relu = layers.Activation('relu')# 卷積層2,如果上一個卷積層stride=2完成下采樣,那么這里的卷積層就不進行下采樣了,保持stride=1self.conv2 = layers.Conv2D(filter_num, (3,3), strides=1, padding='same')# 標準化層self.bn2 = layers.BatchNormalization()# identity層需進行維度變換,將原始輸入圖像和卷積后的圖像相匹配# 進行1*1卷積匹配通道數,通過stride匹配圖像的sizeself.downsample = Sequential() # 設置容器# 在容器中添加1*1卷積和步長變換# stride保持和第一個卷積層一致,保證convblock和identityblock能直接相加# 如果第一個卷積層的stride=1時,那么輸入和輸出的shape保持一致self.downsample.add(layers.Conv2D(filter_num, (1,1), strides=stride))#(2)前向傳播# 定義類方法,self為類實例化對象def call(self, inputs, training=None):# 卷積層1,調用初始化后的屬性x = self.conv1(inputs) # 輸入原始圖像x = self.bn1(x)x = self.relu(x)# 卷積層2x = self.conv2(x)out = self.bn2(x)# identity層,輸入的是原始輸入圖像identity = self.downsample(inputs)# 將convblock和identityblock相加得到最終的殘差塊的輸出結果output = layers.add([out, identity])# 最終結果經過一個非線性函數output = tf.nn.relu(output)# 返回殘差塊的輸出結果return output#(3)多個殘差塊疊加
# 定義子類ResNet,繼承父類keras.Model
class ResNet(keras.Model):# 初始化# layer_dims=[2,2,2,2],resnet18包含4個殘差結構res_blocks,每個殘差結構中有2個殘差塊# num_classes 代表最終的輸出的分類數def __init__(self, layer_dims, num_classes=1000): # 調用父類的初始化方法super(ResNet, self).__init__(self)# 分配屬性# 原始圖像輸入的預處理卷積和池化self.stem = Sequential([layers.Conv2D(64, (7,7), strides=(2,2), padding='same'), # 3*3卷積提取特征layers.BatchNormalization(), # 標準化 layers.Activation('relu'), # 激活函數layers.MaxPool2D(pool_size=(3,3), strides=(2,2), padding='same')]) # 最大池化,輸入圖像的size減半# 創建4個殘差結構,layer_dims=[2,2,2,2]self.layer1 = self.build_resblock(64, layer_dims[0]) # 第一個殘差結構指定64個卷積核,包含2個殘差塊self.layer2 = self.build_resblock(128, layer_dims[1], strides=2) # 第二個殘差結構128個卷積核,包含2個殘差塊,步長為2,圖像的size減半self.layer3 = self.build_resblock(256, layer_dims[2], strides=2)self.layer4 = self.build_resblock(512, layer_dims[3], strides=2)# 全局平均池化,不管卷積層輸出的長和寬是多少,在channel維度上將所有的長和寬加起來取均值# [b, w, h, c] ==> [b,c]self.avgpool = layers.GlobalAveragePooling2D()# 全連接層用于圖像分類self.fc = layers.Dense(num_classes)# 定義前向傳播的類方法def call(self, inputs, training=None):# 原始輸入經過預處理卷積層x = self.stem(inputs)# 經過4個殘差結構x = self.layer1(x)x = self.layer2(x)x = self.layer3(x)x = self.layer4(x)# 輸出層x = self.avgpool(x) # 輸出shape[b,c] x = self.fc(x) # 輸出[b,1000]# 返回分類結果return x# 利用單個已定義的殘差塊,疊加多個殘差塊# filter_num,代表當前圖像的特征圖個數# blocks,需要代表堆疊幾個殘差塊# stride,代表當前的步長,等于1def build_resblock(self, filter_num, blocks, strides=1):# 使用Sequential容器裝填網絡結構res_blocks = Sequential()# 在ResNet類中對BasicBlock類實例化,構成組合關系# ResNet類可調用BasicBlock類中的所有屬性和方法# 添加網絡層# 第一個殘差塊有下采樣功能,stride可能等于2res_blocks.add(BasicBlock(filter_num, strides))# 每個殘差結構中剩余的殘差塊不具備下采樣功能,stride=1for _ in range(1, blocks):# 殘差結構中剩余的殘差塊保持圖像的shape不變res_blocks.add(BasicBlock(filter_num, stride=1))# 返回構建的殘差結構return res_blocks# 構造resnet18,傳入參數
def resnet18():# 實例化網絡結構,一共有4個殘差結構,每個殘差結構由2個殘差塊組成return ResNet([2, 2, 2, 2]) # 構造resnet34,傳入參數
def resnet34():# 實例化,一共有4個殘差結構,每個殘差結構有如下個數的殘差單元return ResNet([3, 4, 6, 3]) # 主函數
def main():# 構造網絡resnet18model18 = resnet18() # 確定輸入層model18.build(input_shape=(None,224,224,3))# 查看網絡結構model18.summary()# 構造網絡resnet34model34 = resnet34()# 確定輸入層model34.build(input_shape=(None,224,224,3))# 查看網絡結構model34.summary() if __name__ == '__main__':main()
總結
以上是生活随笔為你收集整理的【神经网络】(10) Resnet18、34 残差网络复现,附python完整代码的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【面向对象编程】(4) 类的继承,重构父
- 下一篇: 【yolov3目标检测】(3) open