YOLO,即You Only Look Once(你只能看一次)的縮寫,是一個基於卷積神經網路(CNN)的物體檢測演算法。而YOLO v3是YOLO的第3個版本(即YOLO、YOLO 9000、YOLO v3),檢測效果,更準更強。
YOLO v3的更多細節,可以參考YOLO的官網。
YOLO是一句美國的俗語,You Only Live Once,你只能活一次,即人生苦短,及時行樂。
本文主要分享,如何實現YOLO v3的演算法細節,Keras框架。這是第3篇,網路,以DarkNet為基礎。當然還有第4篇,至第n篇,畢竟,這是一個完整版 :)這篇略長。
本文的GitHub原始碼:github.com/SpikeKing/k…
已更新:
- 第1篇 訓練:mp.weixin.qq.com/s/T9LshbXoe…
- 第2篇 模型:mp.weixin.qq.com/s/N79S9Qf1O…
- 第3篇 網路:mp.weixin.qq.com/s/hC4P7iRGv…
- 第4篇 真值:mp.weixin.qq.com/s/5Sj7QadfV…
- 第5篇 Loss:mp.weixin.qq.com/s/4L9E4WGSh…
歡迎關注,微信公眾號 深度演算法 (ID: DeepAlgorithm) ,瞭解更多深度技術!
1. 網路
在模型中,通過傳入輸入層image_input
、每層的anchor數num_anchors//3
和類別數num_classes
,呼叫yolo_body()
方法,構建YOLO v3的網路model_body
。其中,image_input
的結構是(?, 416, 416, 3)。
model_body = yolo_body(image_input, num_anchors // 3, num_classes) # model
複製程式碼
在model_body
中,最終的輸入是image_input
,最終的輸出是3個矩陣的列表:
[(?, 13, 13, 18), (?, 26, 26, 18), (?, 52, 52, 18)]
複製程式碼
YOLO v3的基礎網路是DarkNet網路,將DarkNet網路中底層和中層的特徵矩陣,通過卷積操作和多個矩陣的拼接操作,建立3個尺度的輸出,即[y1, y2, y3]
。
def yolo_body(inputs, num_anchors, num_classes):
darknet = Model(inputs, darknet_body(inputs))
x, y1 = make_last_layers(darknet.output, 512, num_anchors * (num_classes + 5))
x = compose(
DarknetConv2D_BN_Leaky(256, (1, 1)),
UpSampling2D(2))(x)
x = Concatenate()([x, darknet.layers[152].output])
x, y2 = make_last_layers(x, 256, num_anchors * (num_classes + 5))
x = compose(
DarknetConv2D_BN_Leaky(128, (1, 1)),
UpSampling2D(2))(x)
x = Concatenate()([x, darknet.layers[92].output])
x, y3 = make_last_layers(x, 128, num_anchors * (num_classes + 5))
return Model(inputs, [y1, y2, y3])
複製程式碼
2. Darknet
Darknet網路的輸入是圖片資料集inputs,即(?, 416, 416, 3),輸出是darknet_body()
方法的輸出。將網路的核心邏輯封裝在darknet_body()
方法中。即:
darknet = Model(inputs, darknet_body(inputs))
複製程式碼
其中,darknet_body
的輸出格式是(?, 13, 13, 1024)。
Darknet的網路簡化圖,如下:
YOLO v3所使用的Darknet版本是Darknet53。那麼,為什麼是Darknet53呢?因為Darknet53是53個卷積層和池化層的組合,與Darknet簡化圖一一對應,即:
53 = 2 + 1*2 + 1 + 2*2 + 1 + 8*2 + 1 + 8*2 + 1 + 4*2 + 1
複製程式碼
在darknet_body()
中,Darknet網路含有5組重複的resblock_body()
單元,即:
def darknet_body(x):
'''Darknent body having 52 Convolution2D layers'''
x = DarknetConv2D_BN_Leaky(32, (3, 3))(x)
x = resblock_body(x, num_filters=64, num_blocks=1)
x = resblock_body(x, num_filters=128, num_blocks=2)
x = resblock_body(x, num_filters=256, num_blocks=8)
x = resblock_body(x, num_filters=512, num_blocks=8)
x = resblock_body(x, num_filters=1024, num_blocks=4)
return x
複製程式碼
在第1個卷積操作DarknetConv2D_BN_Leaky()
中,是3個操作的組合,即:
- 1個Darknet的2維卷積Conv2D層,即
DarknetConv2D()
; - 1個批正在化(BN)層,即BatchNormalization();
- 1個LeakyReLU層,斜率是0.1,LeakyReLU是ReLU的變換;
即:
def DarknetConv2D_BN_Leaky(*args, **kwargs):
"""Darknet Convolution2D followed by BatchNormalization and LeakyReLU."""
no_bias_kwargs = {'use_bias': False}
no_bias_kwargs.update(kwargs)
return compose(
DarknetConv2D(*args, **no_bias_kwargs),
BatchNormalization(),
LeakyReLU(alpha=0.1))
複製程式碼
其中,LeakyReLU的啟用函式,如下:
其中,Darknet的2維卷積DarknetConv2D,具體操作如下:
- 將核權重矩陣的正則化,使用L2正則化,引數是5e-4,即操作w引數;
- Padding,一般使用same模式,只有當步長為(2,2)時,使用valid模式。避免在降取樣中,引入無用的邊界資訊;
- 其餘引數不變,都與二維卷積操作Conv2D()一致;
kernel_regularizer
是將核權重引數w進行正則化,而BatchNormalization是將輸入資料x進行正則化。
實現:
@wraps(Conv2D)
def DarknetConv2D(*args, **kwargs):
"""Wrapper to set Darknet parameters for Convolution2D."""
darknet_conv_kwargs = {'kernel_regularizer': l2(5e-4)}
darknet_conv_kwargs['padding'] = 'valid' if kwargs.get('strides') == (2, 2) else 'same'
darknet_conv_kwargs.update(kwargs)
return Conv2D(*args, **darknet_conv_kwargs)
複製程式碼
下一步,第1個殘差結構resblock_body()
,輸入的資料x是(?, 416, 416, 32),通道filters是64個,重複次數num_blocks
是1次。第1個殘差結構是網路簡化圖第1部分。
x = resblock_body(x, num_filters=64, num_blocks=1)
複製程式碼
在resblock_body
中,含有以下邏輯:
- ZeroPadding2D():填充x的邊界為0,由(?, 416, 416, 32)轉換為(?, 417, 417, 32)。因為下一步卷積操作的步長為2,所以圖的邊長需要是奇數;
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)。
實現:
def resblock_body(x, num_filters, num_blocks):
'''A series of resblocks starting with a downsampling Convolution2D'''
# Darknet uses left and top padding instead of 'same' mode
x = ZeroPadding2D(((1, 0), (1, 0)))(x)
x = DarknetConv2D_BN_Leaky(num_filters, (3, 3), strides=(2, 2))(x)
for i in range(num_blocks):
y = compose(
DarknetConv2D_BN_Leaky(num_filters // 2, (1, 1)),
DarknetConv2D_BN_Leaky(num_filters, (3, 3)))(x)
x = Add()([x, y])
return x
複製程式碼
殘差操作流程,如圖:
同理,在darknet_body()
中,執行5組resblock_body()
殘差塊,重複[1, 2, 8, 8, 4]次,雙卷積(1x1和3x3)操作,每組均含有一次步長為2的卷積操作,因而一共降維5次32倍,即32=2^5,則輸出的特徵圖維度是13,即13=416/32。最後1層的通道(filter)數是1024,因此,最終的輸出結構是(?, 13, 13, 1024),即:
Tensor("add_23/add:0", shape=(?, 13, 13, 1024), dtype=float32)
複製程式碼
至此,Darknet模型的輸入是(?, 416, 416, 3),輸出是(?, 13, 13, 1024)。
3. 特徵圖
在YOLO v3網路中,輸出3個不同尺度的檢測圖,用於檢測不同大小的物體。呼叫3次make_last_layers()
,產生3個檢測圖,即y1、y2和y3。
13x13檢測圖
第1個部分,輸出維度是13x13。在make_last_layers()
方法中,輸入引數如下:
- darknet.output:DarkNet網路的輸出,即(?, 13, 13, 1024);
num_filters
:通道個數512,用於生成中間值x,x會傳導至第2個檢測圖;out_filters
:第1個輸出y1的通道數,值是錨框數*(類別數+4個框值+框置信度);
即:
x, y1 = make_last_layers(darknet.output, 512, num_anchors * (num_classes + 5))
複製程式碼
在make_last_layers()
方法中,執行2步操作:
- 第1步,x執行多組1x1的卷積操作和3x3的卷積操作,filter先擴大再恢復,最後與輸入的filter保持不變,仍為512,則x由(?, 13, 13, 1024)轉變為(?, 13, 13, 512);
- 第2步,x先執行3x3的卷積操作,再執行不含BN和Leaky的1x1的卷積操作,作用類似於全連線操作,生成預測矩陣y;
實現:
def make_last_layers(x, num_filters, out_filters):
'''6 Conv2D_BN_Leaky layers followed by a Conv2D_linear layer'''
x = compose(
DarknetConv2D_BN_Leaky(num_filters, (1, 1)),
DarknetConv2D_BN_Leaky(num_filters * 2, (3, 3)),
DarknetConv2D_BN_Leaky(num_filters, (1, 1)),
DarknetConv2D_BN_Leaky(num_filters * 2, (3, 3)),
DarknetConv2D_BN_Leaky(num_filters, (1, 1)))(x)
y = compose(
DarknetConv2D_BN_Leaky(num_filters * 2, (3, 3)),
DarknetConv2D(out_filters, (1, 1)))(x)
return x, y
複製程式碼
最終,第1個make_last_layers()
方法,輸出的x是(?, 13, 13, 512),輸出的y是(?, 13, 13, 18)。由於模型只有1個檢測類別,因而y的第4個維度是18,即3*(1+5)=18
。
26x26檢測圖
第2個部分,輸出維度是26x26,包含以下步驟:
- 通過
DarknetConv2D_BN_Leaky
卷積,將x由512的通道數,轉換為256的通道數; - 通過2倍上取樣
UpSampling2D
,將x由13x13的結構,轉換為26x26的結構; - 將x與DarkNet的第152層拼接
Concatenate
,作為第2個make_last_layers()
的輸入,用於生成第2個預測圖y2;
其中,輸入的x和darknet.layers[152].output的結構都是26x26的尺寸,如下:
x: shape=(?, 26, 26, 256)
darknet.layers[152].output: (?, 26, 26, 512)
複製程式碼
在拼接之後,輸出的x的格式是(?, 26, 26, 768)。
這樣做的目的是:將Darknet最底層的高階抽象資訊darknet.output,經過若干次轉換之後,除了輸出給第1個檢測部分,還被用於第2個檢測部分,經過上取樣,與Darknet骨幹中,倒數第2次降維的資料拼接,共同作為第2個檢測部分的輸入。底層抽象特徵含有全域性資訊,中層抽象特徵含有區域性資訊,這樣拼接,兩者兼顧,用於檢測較小的物體。
最後,還是呼叫相同的make_last_layers()
,輸出第2個檢測層y2和臨時資料x。
實現:
x = compose(
DarknetConv2D_BN_Leaky(256, (1, 1)),
UpSampling2D(2))(x)
x = Concatenate()([x, darknet.layers[152].output])
x, y2 = make_last_layers(x, 256, num_anchors * (num_classes + 5))
複製程式碼
最終,第2個make_last_layers()
方法,輸出的x是(?, 26, 26, 256),輸出的y是(?, 26, 26, 18)。
52x52檢測圖
第3個部分,輸出維度是52x52,與第2個部分類似,包含以下步驟:
x = compose(
DarknetConv2D_BN_Leaky(128, (1, 1)),
UpSampling2D(2))(x)
x = Concatenate()([x, darknet.layers[92].output])
_, y3 = make_last_layers(x, 128, num_anchors * (num_classes + 5))
複製程式碼
邏輯如下:
- x經過128個filter的卷積,再執行上取樣,輸出為(?, 52, 52, 128);
- darknet.layers[92].output,與152層類似,結構是(?, 52, 52, 256);
- 兩者拼接之後,x是(?, 52, 52, 384);
- 最後輸入至
make_last_layers()
,生成y3是(?, 52, 52, 18),忽略x的輸出;
最後,則是根據整個邏輯的輸入和輸出,構建模型。輸入inputs依然保持不變,即(?, 416, 416, 3),而輸出則轉換為3個尺度的預測層,即[y1, y2, y3]。
return Model(inputs, [y1, y2, y3])
複製程式碼
[y1, y2, y3]的結構如下:
Tensor("conv2d_59/BiasAdd:0", shape=(?, 13, 13, 18), dtype=float32)
Tensor("conv2d_67/BiasAdd:0", shape=(?, 26, 26, 18), dtype=float32)
Tensor("conv2d_75/BiasAdd:0", shape=(?, 52, 52, 18), dtype=float32)
複製程式碼
最終,在yolo_body
中,完成整個YOLO v3網路的構建,基礎網路是DarkNet。
model_body = yolo_body(image_input, num_anchors // 3, num_classes)
複製程式碼
網路的示意圖,層次序號略有不同:
補充1. 卷積Padding
在卷積操作中,針對於邊緣資料,有兩種操作,一種是捨棄valid,一種是填充same。
如:
資料:1 2 3 4 5 6 7 8 9 10 11 12 13
輸入資料 = 13
過濾器寬度 = 6
步長 = 5
複製程式碼
第1種,valid操作,寬度是6,步長是5,執行資料:
1 2 3 4 5 6
6 7 8 9 10 11
11 12 13(不足,捨棄)
複製程式碼
第2種,same操作,執行資料:
1 2 3 4 5 6(前兩步相同)
6 7 8 9 10 11
11 12 13 0 0(不足,填充)
複製程式碼
其中,same模式中資料利用率更高,valid模式中避免引入無效的邊緣資料,兩種模式各有千秋。
補充2. compose函式
compose()函式,使用Python的Lambda表示式,順次執行函式列表,且前一個函式的輸出是後一個函式的輸入。compose()函式適用於在神經網路中連線兩個層。
例如:
def compose(*funcs):
if funcs:
return reduce(lambda f, g: lambda *a, **kw: g(f(*a, **kw)), funcs)
else:
raise ValueError('Composition of empty sequence not supported.')
def func_x(x):
return x * 10
def func_y(y):
return y - 6
z = compose(func_x, func_y) # 先執行x函式,再執行y函式
print(z(10)) # 10*10-6=94
複製程式碼
補充3. UpSampling2D上取樣
UpSampling2D上取樣操作,將特徵矩陣按倍數擴大,其核心是通過resize的方式,預設使用最鄰近(Nearest Neighbor)插值演算法。data_format
是資料模式,預設是channels_last
,即通道在最後,如(128,128,3)
。
原始碼:
def call(self, inputs):
return K.resize_images(inputs, self.size[0], self.size[1],
self.data_format)
// ...
x = tf.image.resize_nearest_neighbor(x, new_shape)
複製程式碼
例如:資料(?, 13, 13, 256),經過上取樣2倍操作,即UpSampling2D(2),生成(?, 26, 26, 256)的特徵圖。
補充4. 1x1卷積操作與全連線
1x1的卷積層和全連線層都可以作為最後一層的預測輸出,兩者之間略有不同。
第1點:
- 1x1的卷積層,可以不考慮輸入的通道數,輸出固定通道數的特徵矩陣;
- 全連線層(Dense),輸入和輸出都是固定的,在設計網路時,固定就不能修改;
這樣,1x1的卷積層,比全連線層,更為靈活;
第2點:
例如:輸入(13,13,1024),輸出為(13,13,18),則兩種操作:
- 1x1的卷積層,引數較少,只需與輸出通道匹配的引數,如1x1x1024x18個引數;
- 全連線層,引數較多,需要與輸入和輸出都匹配的引數,如13x13x1024x18個引數;
OK, that's all! Enjoy it!
歡迎關注,微信公眾號 深度演算法 (ID: DeepAlgorithm) ,瞭解更多深度技術!