首先,看一下YOLO v3 中的網絡結構。
YOLO v3 的整體流程
番外步驟: 對訓練集圖片標記后產生的數據進行K-Means處理,篩選9個anchor-box。
詳見:https://www.cnblogs.com/monologuesmw/p/12761653.html
進入YOLO v3的結構中:
1. 圖像縮放。將訓練集的圖片縮放至416*416中,包括兩種縮放的方式:等比縮放和非等比縮放(非等比縮放一般只在訓練中)圖像縮放至416*416以后,便可以作為YOLO v3 結構的輸入。
2. Darknet-53下采樣過程,其內部的結構可以看成一個3*3,步長(1*1)(fitters:32)的same卷積 和 若干個(一個3*3,步長(2*2),fitters總是比前面的fitters多一倍的窄卷積 和 fitters減半的1*1卷積、fitters恢復的3*3同卷積 )組成的殘差組合塊(組合的意思是,一個3*3步長(2*2)其實不是卷積塊的內容,但二者有依存關系)。 這樣就會將416*416*3 的圖像轉換為13*13*1024的feature_map。過程中會包含26*26*512的feature_map 和 52*52*256 的feature_map。其內部是這樣定義的,步長(1,1)的為同卷積,步長為(2*2)的為窄卷積(使用步長為2的卷積代替pool操作)。
3. 多尺度預測與特征融合上采樣,YOLO v3與v2結構上的不同點就在於v3增添了多尺度的預測和特征融合的上采樣(不同大小的特征適應於不同大小物體的檢測)。並且卷積的結構沒有池化層,在卷積層后都會搭載一個Leaky-ReLU的激活函數,並且在激活函數的輸入中不使用偏執bias。
在生成13*13*1024后,會再經歷兩對1*1和3*3卷積,使得三種尺度的feature_map變為13*13*512, 26*26*256。(因為最大尺度的是52*52*256),然后通過上采樣的方式,13*13*512變為26*26*512 ,26*26*256變為52*52*256, 這樣便可以與高尺度的拼接融合。生成最終的13*13*255、26*26*255 、52*52*255的feature_map輸出。
P.S. 上采樣的內部機制實際上是最近鄰域插值的方式使維度翻倍。
先一睹YOLOv3具體的網絡結構:
代碼解析
1. Darknet53部分
1 darknet = Model(inputs, darknet_body(inputs)) # 此處返回了第一個下采樣13*13 (?,416*416*32)->(?,13*13*1024)
darknet_body(x)內部
1 def darknet_body(x): 2 '''Darknent body having 52 Convolution2D layers 3 有52個卷積層(2D) 4 ''' 5 x = DarknetConv2D_BN_Leaky(32, (3, 3))(x) # (內置same卷積)輸出的x-》416*416*32 6 x = resblock_body(x, 64, 1) # num_filters = 64 , num_blocks = 1(重復次數) 返回結果208*208*64 7 x = resblock_body(x, 128, 2) # 返回結果 104*104*128 8 x = resblock_body(x, 256, 8) # 返回結果 52*52*256 9 x = resblock_body(x, 512, 8) # 返回結果 26*26*512 10 x = resblock_body(x, 1024, 4) # 返回結果 13*13*1024 5組重復的resblock_body()單元 11 return x # 第一個下采樣特征生成
上述代碼是Darknet53的主體部分。
a. 其中,第5行是 一個3*3,步長(1*1)(fitters:32)的same卷積。 其中包含:
- 1個Darknet的2維卷積Conv2D層,即DarknetConv2D();
- 1個批歸一化(BN)層,即BatchNormalization();
- 1個LeakyReLU層,斜率是0.1,LeakyReLU是ReLU的變換;
1 def DarknetConv2D_BN_Leaky(*args, **kwargs): 2 """Darknet Convolution2D followed by BatchNormalization and LeakyReLU.""" 3 no_bias_kwargs = {'use_bias': False} 4 no_bias_kwargs.update(kwargs) 5 return compose( 6 DarknetConv2D(*args, **no_bias_kwargs), 7 BatchNormalization(), 8 LeakyReLU(alpha=0.1))
b. 第6-9行,為若干個殘差塊組。(1+2+8+8=19個重復次數)fitters的通道數會逐漸倍增(深度逐漸加深)。最終返回的x為13*13*1024。
- ZeroPadding2D():填充x的邊界為0,由(?, 416, 416, 32)轉換為(?, 417, 417, 32)。
因為下一步卷積操作的步長為2,所以圖的邊長需要是奇數;--- 需要padding
- DarknetConv2D_BN_Leaky()是DarkNet的2維卷積操作,核是(3,3),步長是(2,2),
注意,這會導致特征尺寸變小,由(?, 417, 417, 32)轉換為(?, 208, 208, 64)。由於num_filters是64,所以產生64個通道。
- compose():輸出預測圖y,功能是組合函數,先執行1x1的卷積操作,再執行3x3的卷積操作,filter先降低2倍后恢復,最后與輸入相同,都是64; --- 殘差塊的另一分支
- x = Add()([x, y])是殘差(Residual)操作,將x的值與y的值相加。殘差操作可以避免,在網絡較深時所產生的梯度彌散問題(Vanishing Gradient Problem)。
1 def resblock_body(x, num_filters, num_blocks): 2 '''A series of resblocks starting with a downsampling Convolution2D 3 ''' 4 # Darknet uses left and top padding instead of 'same' mode 5 x = ZeroPadding2D(((1, 0), (1, 0)))(x) # 增加一圈0 x有(?,416,416,32)-> (?,417,417,32) Keras中的方法 6 x = DarknetConv2D_BN_Leaky(num_filters, (3, 3), strides=(2, 2))(x) # 步長上下2,則是窄卷積, 因此上述需padding(才會沒有邊緣遺失) 7 for i in range(num_blocks): # 此時,x的輸出為208*208*64 (重復次數的循環) 8 y = compose( 9 DarknetConv2D_BN_Leaky(num_filters//2, (1, 1)), # 此時的輸出為208*208*32 降一半 10 DarknetConv2D_BN_Leaky(num_filters, (3, 3)))(x) # 此時的輸出為208 * 208 *64 恢復 11 x = Add()([x, y]) # 將殘差塊的結果加到源網絡的結構上 (不影響運算) 208*208*64 12 return x
2. Darknet輸出至y1,y2,y3部分(即13*13*1024至13*13*255. etc)
A. 生成y1 ----> 13*13*255
1 x, y1 = make_last_layers(darknet.output, 512, num_anchors*(num_classes+5))
make_last_layers()的內部
該函數已經是 特征圖輸出的計算部分,結合結構圖來觀察,在13*13*1024的地方至13*13*255的y1處的運算:
- 第1步,x執行多組1x1的卷積操作和3x3的卷積操作,filter先縮小再恢復,最后與輸入的filter保持不變,仍為512(num_filters),則x由(?, 13, 13, 1024)轉變為(?, 13, 13, 512);
- 第2步,x先執行3x3的卷積操作,再執行不含BN和Leaky的1x1的卷積操作,作用類似於全連接操作,生成預測矩陣y;
- 從結構上可以看出,x此時有兩個任務,一個是繼續y的運算,作為y1特征的輸出;另一個是需要上采樣到26*26的y2中。,所以分為了x和y兩個部分,同時也會返回這兩個部分。
1 def make_last_layers(x, num_filters, out_filters): 2 '''6 Conv2D_BN_Leaky layers followed by a Conv2D_linear layer 3 ''' 4 x = compose( 5 DarknetConv2D_BN_Leaky(num_filters, (1, 1)), 6 DarknetConv2D_BN_Leaky(num_filters*2, (3, 3)), 7 DarknetConv2D_BN_Leaky(num_filters, (1, 1)), 8 DarknetConv2D_BN_Leaky(num_filters*2, (3, 3)), 9 DarknetConv2D_BN_Leaky(num_filters, (1, 1)))(x) # 此時13*13*512 10 y = compose( 11 DarknetConv2D_BN_Leaky(num_filters*2, (3, 3)), 12 DarknetConv2D(out_filters, (1,1)))(x) 13 return x, y
B. 生成y2 ---》26*26*255 此部分會涉及到尺度特征融合的部分,即13*13*512的上采樣過程。
- 13*13*512--》13*13*256
- 13*13*256 上采樣 26*26*256
- 融合后的結果 26*26*768 (原來的是26*26*512,融合后即512+256 = 768)
1 # 第二部分 26*26*256 的y2 2 x = compose( 3 DarknetConv2D_BN_Leaky(256, (1,1)), # 此時x的為13*13*256 4 UpSampling2D(2))(x) # x需要參加的上采樣 此時的x為26*26*256 5 x = Concatenate()([x, darknet.layers[152].output]) # 把之前結果的第152層的輸出提取,即26*26*512的輸出 即13*13*1024的前一層輸出 6 x, y2 = make_last_layers(x, 256, num_anchors*(num_classes+5))
C. 生成y3 ----》52*52*255 此部分包含26*26*256的上采樣過程
- 26*26*256 --》 26*26*128
- 26*26*128上采樣到52*52*128
- 融合后的結果52*52*384 (128+256=384)
1 # 第三部分 52*52*128*18(18=3*(1+5)1個類別,3個錨框) 的y3 2 x = compose( 3 DarknetConv2D_BN_Leaky(128, (1,1)), 4 UpSampling2D(2))(x) # 同樣需要26的x 的上采樣 5 x = Concatenate()([x,darknet.layers[92].output]) 6 x, y3 = make_last_layers(x, 128, num_anchors*(num_classes+5))
當然,此時返回的這個x就沒有什么任務了。
模型結構創建完畢。
可以看出,Darknet的框架是由多組Conv+Conv+Residual的結構疊加而成的。
即1+1+1*2+1+2*2+1+8*2+1+8*2+1+4*2 = 52
配置文件中 ############# 的分割 就是結構中darknet的結束。