加入極市專業CV交流羣,與  1 0000+來自港科大、北大、清華、中科院、CMU、騰訊、百度  等名校名企視覺開發者互動交流!

同時提供每月大咖直播分享、真實項目需求對接、乾貨資訊彙總,行業技術交流。關注  極市平臺  公衆號  , 回覆  加羣, 立刻申請入羣~

作者|周威,https://zhuanlan.zhihu.com/p/150127712

本文已獲作者授權,不得二次轉載。

1.前言

最近用YOLO V4做車輛檢測,配合某一目標追蹤算法實現 車輛追蹤+軌跡提取 等功能,正好就此結合論文和代碼來對 YOLO V4 做個解析。先放上個效果圖(半成品),如下:

話不多說,現在就開始對YOLO V4進行總結。

YOLO V4的論文:《 YOLOv4: Optimal Speed and Accuracy of Object Detection》 ,相信大家也是經常看到這幾個詞眼:大神接棒、YOLO V4來了、Tricks 萬花筒等等。

沒錯,通過閱讀YOLO V4的原文,我覺得它更像一篇 目標檢測模型Tricks文獻綜述 ,可見作者在目標檢測領域的知識(煉丹技術)積累之多。

從本質上,YOLO V4就是 篩選一些 從YOLO V3發佈至今,被用在各式各樣檢測器上,能夠 提高檢測精度tricks ,並以YOLO V3爲 基礎 進行改進的目標檢測模型。YOLO V4在保證速度的同時,大幅提高模型的檢測精度(當然,這是相較於YOLO V3的)。

上圖可以看出來,雖然檢測精度不如EfficientDet這種變態,但是速度上是遙遙領先的,說明YOLO V4並沒有忘記初心(速度和精度的trade off,我YOLO纔是佼佼者)!

其實我是比較推薦大家看看YOLO V4原文的,就當 煉丹手冊 來看也是挺好的,如果你懶得看,那這裏我貼出來一張圖,就是最終 YOLO V4的煉丹配方 ,如下:

YOLO V4煉丹配方

這麼一看,這煉丹配方多 清晰 呀,和YOLO V3對比,主要做了以下改變:

  1. 相較於YOLO V3的DarkNet53,YOLO V4用了CSPDarkNet53

  2. 相較於YOLO V3的FPN,YOLO V4用了SPP+PAN

  3. CutMix數據增強和馬賽克(Mosaic)數據增強

  4. DropBlock正則化

  5. 等等

這技巧太多了,着實讓人數不過來。按照慣例,我喜歡結合 代碼 對模型進行解析,論文的話看個思路,實現的細節還是在代碼中體現的較具體。原作者YOLO V4的代碼是基於C++的,如下:

YOLO V4 C++(原版)

https://github.com/AlexeyAB/darknet

這個解析起來太麻煩了,我找了個看起來不麻煩的,基於 Keras+Tensorflow 的,如下:

YOLO V4 Keras版本

https://github.com/Ma-Dan/keras-yolo4

本次YOLO V4論文和代碼解析也將基於這個版本的進行的啦!

後面的內容將按照以下步驟進行介紹。

  • (1)YOLO V4的網絡結構

  • (2)YOLO V4的損失函數

  • (3)一些Tricks的具體代碼實現

2. YOLO V4的網絡結構

這裏我先給出YOLO V4的總結構圖,如下

主要有以下兩部分組成

  • BackBone:CSPDarknet53

  • Neck:SPP+PAN

接下面將逐個分析!

2.1 BackBone:CSPDarknet53

目前做檢測器MAP指標的提升,都會考慮選擇一個圖像特徵提取能力較強的backbone,且不能太大,那樣影響檢測的速度。YOLO V4中,則是選擇了具有CSP(Cross-stage partial connections)的darknet53,而是沒有選擇在imagenet上跑分更高的CSPResNext50,

原因很簡單,如上表,作者說:

For instance, our numerous studies demonstrate that the CSPResNext50 is considerably better compared to CSPDarknet53 in terms of object classification on the ILSVRC2012 (ImageNet) dataset [. However, conversely, the CSPDarknet53 is better compared to CSPResNext50 in terms of detecting objects on the MS COCO dataset

意思就是結合了在 目標檢測領域 的精度來說, CSPDarknet53 是要強於 CSPResNext50 ,這也告訴了我們,在圖像分類上任務表現好的模型,不一定很適用於目標檢測(這不是絕對的!)。

那麼這個帶有CSP結構的Darknet53,到底長什麼樣呢?如果對CSP結構感興趣的,歡迎點擊原文鏈接。

這裏我們直接從代碼上看看這個CSPDarknet53什麼樣子,定義如下


def darknet_body(x):

'''Darknent body having 52 Convolution2D layers'''

x = DarknetConv2D_BN_Mish(32, (3,3))(x)

x = resblock_body(x, 64, 1, False)

x = resblock_body(x, 128, 2)

x = resblock_body(x, 256, 8)

x = resblock_body(x, 512, 8)

x = resblock_body(x, 1024, 4)

return x

如果把 堆疊的殘差單元( resblock_body)看成 整體 的話,那麼這個結構和Darknet53以及ResNet等的確差別不大,特別是resblock_body的num_blocks爲【1,2,8,8,4】和darknet53一模一樣。

那麼我們解析一下resblock_body的定義,如下:


def resblock_body(x, num_filters, num_blocks, all_narrow=True):

'''A series of resblocks starting with a downsampling Convolution2D'''

# Darknet uses left and top padding instead of 'same' mode

preconv1 = ZeroPadding2D(((1,0),(1,0)))(x)

preconv1 = DarknetConv2D_BN_Mish(num_filters, (3,3), strides=(2,2))(preconv1)

shortconv = DarknetConv2D_BN_Mish(num_filters//2 if all_narrow else num_filters, (1,1))(preconv1)

mainconv = DarknetConv2D_BN_Mish(num_filters//2 if all_narrow else num_filters, (1,1))(preconv1)

for i in range(num_blocks):

y = compose(

DarknetConv2D_BN_Mish(num_filters//2, (1,1)),

DarknetConv2D_BN_Mish(num_filters//2 if all_narrow else num_filters, (3,3)))(mainconv)

mainconv = Add()([mainconv,y])

postconv = DarknetConv2D_BN_Mish(num_filters//2 if all_narrow else num_filters, (1,1))(mainconv)

route = Concatenate()([postconv, shortconv])

return DarknetConv2D_BN_Mish(num_filters, (1,1))(route)

這麼一看,和傳統的ResBlock差別就出來了,爲了大家更清晰地瞭解結構,我把這個殘差單元的結構繪製出來,如下:

對照代碼和上面的圖片,可以比較清晰地看出來 這個CSP殘差單元DarkNet/ResNet的殘差單元 的區別了。當然了,圖上的DarknetConv2D_BN_Mish模塊定義如下

  • (1) DarknetConv2D_BN_Mish


def DarknetConv2D_BN_Mish(*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(),

Mish())

(2) DarknetConv2Ddef DarknetConv2D(*args, **kwargs):

"""Wrapper to set Darknet parameters for Convolution2D."""

darknet_conv_kwargs = {}

darknet_conv_kwargs['kernel_initializer'] = keras.initializers.RandomNormal(mean=0.0, stddev=0.01)

darknet_conv_kwargs['padding'] = 'valid' if kwargs.get('strides')==(2,2) else 'same'

darknet_conv_kwargs.update(kwargs)

return Conv2D(*args, **darknet_conv_kwargs)

至此,YOLO V4的backbone部分就講解完畢了。

2.2 Neck:SPP+PAN

目標檢測模型的 Neck部分 主要用來融合 不同尺寸 特徵圖的特徵信息。常見的有MaskRCNN中使用的FPN等,這裏我們用EfficientDet論文中的一張圖來進行說明。

可見,隨着人們追求檢測器在COCO數據集上的MAP指標, Neck部分 也是出了很多花裏胡哨的結構呀。

本文中的YOLO V4就是用到了 SPP(Spatial pyramid pooling) + PAN(Path Aggregation Network ,上圖的結構b)。

這裏我們根據總圖上的process1-6,對SSP+PAN部分進行解析。

(1) 其中process1的代碼實現爲:


y19 = DarknetConv2D_BN_Leaky(512, (1,1))(darknet.output)

y19 = DarknetConv2D_BN_Leaky(1024, (3,3))(y19)

y19 = DarknetConv2D_BN_Leaky(512, (1,1))(y19)

maxpool1 = MaxPooling2D(pool_size=(13,13), strides=(1,1), padding='same')(y19) #(19,19)

maxpool2 = MaxPooling2D(pool_size=(9,9), strides=(1,1), padding='same')(y19) #(19,19)

maxpool3 = MaxPooling2D(pool_size=(5,5), strides=(1,1), padding='same')(y19) #(19,19)

y19 = Concatenate()([maxpool1, maxpool2, maxpool3, y19])

y19 = DarknetConv2D_BN_Leaky(512, (1,1))(y19)

y19 = DarknetConv2D_BN_Leaky(1024, (3,3))(y19)

y19 = DarknetConv2D_BN_Leaky(512, (1,1))(y19)

顯而易見,該進程接受 CSPDarknet53最終的輸出 ,返回 變量y19 (如總圖上process1所示),這裏我們也給出圖示,如下:

Process1

(2) process2 代碼如下


y19_upsample = compose(DarknetConv2D_BN_Leaky(256, (1,1)), UpSampling2D(2))(y19)


#38x38 head

y38 = DarknetConv2D_BN_Leaky(256, (1,1))(darknet.layers[204].output)

y38 = Concatenate()([y38, y19_upsample])

y38 = DarknetConv2D_BN_Leaky(256, (1,1))(y38)

y38 = DarknetConv2D_BN_Leaky(512, (3,3))(y38)

y38 = DarknetConv2D_BN_Leaky(256, (1,1))(y38)

y38 = DarknetConv2D_BN_Leaky(512, (3,3))(y38)

y38 = DarknetConv2D_BN_Leaky(256, (1,1))(y38)

即先將上述的y19進行 上採樣大小38x38 ,然後再和CSPDarknet53的204層輸出進行堆疊,最後通過一系列DarknetConv2D_BN_Leaky模塊,獲得變量y38。

(3) process3的代碼如下,類似於process2


y38_upsample = compose(DarknetConv2D_BN_Leaky(128, (1,1)), UpSampling2D(2))(y38)


#76x76 head

y76 = DarknetConv2D_BN_Leaky(128, (1,1))(darknet.layers[131].output)

y76 = Concatenate()([y76, y38_upsample])

y76 = DarknetConv2D_BN_Leaky(128, (1,1))(y76)

y76 = DarknetConv2D_BN_Leaky(256, (3,3))(y76)

y76 = DarknetConv2D_BN_Leaky(128, (1,1))(y76)

y76 = DarknetConv2D_BN_Leaky(256, (3,3))(y76)

y76 = DarknetConv2D_BN_Leaky(128, (1,1))(y76)

(4) process4的代碼如下


#76x76 output

y76_output = DarknetConv2D_BN_Leaky(256, (3,3))(y76)

y76_output = DarknetConv2D(num_anchors*(num_classes+5), (1,1))(y76_output)

這個比較簡單,直接通過一個DarknetConv2D_BN_Leaky,然後使用1x1卷積輸出最大的一張特徵圖y76_output,維度爲**(76,76,num_anchor*(num_classes+5))**。

(5) process5的代碼如下:


#38x38 output

y76_downsample = ZeroPadding2D(((1,0),(1,0)))(y76)

y76_downsample = DarknetConv2D_BN_Leaky(256, (3,3), strides=(2,2))(y76_downsample)

y38 = Concatenate()([y76_downsample, y38])

y38 = DarknetConv2D_BN_Leaky(256, (1,1))(y38)

y38 = DarknetConv2D_BN_Leaky(512, (3,3))(y38)

y38 = DarknetConv2D_BN_Leaky(256, (1,1))(y38)

y38 = DarknetConv2D_BN_Leaky(512, (3,3))(y38)

y38 = DarknetConv2D_BN_Leaky(256, (1,1))(y38)


y38_output = DarknetConv2D_BN_Leaky(512, (3,3))(y38)

y38_output = DarknetConv2D(num_anchors*(num_classes+5), (1,1))(y38_output)

這一步驟比較關鍵,PAN和FPN的差異在於, FPN自頂向下 的特徵融合, PANFPN基礎上 ,多了個 自底向上 的特徵融合。具體自底向上的特徵融合,就是 process5 完成的,可以看到該步驟先將y76 下采樣 至38x38大小,再和y38堆疊,然後進行一系列卷積運算獲得維度大小爲**(38,38,num_anchor*(num_classes+5)) 的輸出 y38_output**,如下圖所示。

Process5

(6) Process6代碼如下


#19x19 output

y38_downsample = ZeroPadding2D(((1,0),(1,0)))(y38)

y38_downsample = DarknetConv2D_BN_Leaky(512, (3,3), strides=(2,2))(y38_downsample)

y19 = Concatenate()([y38_downsample, y19])

y19 = DarknetConv2D_BN_Leaky(512, (1,1))(y19)

y19 = DarknetConv2D_BN_Leaky(1024, (3,3))(y19)

y19 = DarknetConv2D_BN_Leaky(512, (1,1))(y19)

y19 = DarknetConv2D_BN_Leaky(1024, (3,3))(y19)

y19 = DarknetConv2D_BN_Leaky(512, (1,1))(y19)


y19_output = DarknetConv2D_BN_Leaky(1024, (3,3))(y19)

y19_output = DarknetConv2D(num_anchors*(num_classes+5), (1,1))(y19_output)

Process6和process5進程類似,不多贅述,輸出爲(19,19,num_anchor*(num_classes+5))的特徵圖y19_output。

3.結束語

上述有關YOLO V4的 網絡結構 就講到這裏,我看了下,篇幅又有點長了,那關於 損失函數 和更多 tricks實現的細節 ,我就放到後面再講了,感謝大家支持!

推薦閱讀

添加極市小助手微信 (ID : cv-mart) ,備註: 研究方向-姓名-學校/公司-城市 (如:目標檢測-小極-北大-深圳),即可申請加入 極市技術交流羣 ,更有 每月大咖直播分享、真實項目需求對接、求職內推、算法競賽、 乾貨資訊彙總、行業技術交流 一起來讓思想之光照的更遠吧~

△長按添加極市小助手

△長按關注極市平臺,獲取 最新CV乾貨

覺得有用麻煩給個在看啦~   

相關文章