本文編譯自:
https://blog.paperspace.com/how-to-implement-a-yolo-object-detector-in-pytorch/
https://blog.paperspace.com/how-to-implement-a-yolo-v3-object-detector-from-scratch-in-pytorch-part-2/
原文作者:Ayoosh Kathuria
前言:
如果說非要提供一個方法快速掌握目標檢測的深度學習算法的話,那就是自己從無到有的實現它,在這期間,可以對整個算法有更清晰的認識,此次系列文章旨在提供一個自己從無到有實現目標檢測YOLOV3的教程,希望對那些對目標檢測感興趣的人有所幫助。
目標檢測很大程度上依賴於深度學習技術的發展,比如YOLO,SSD,MaskRCNN 和RetinaNet.
本文將詳細介紹如何使用Pytorch從0到1完成YOLO v3算法,實現基於python3.5,Pytorch3.0,文中提到的所有代碼都可以從Github中找到。
教程包括五個部分,本文只涉及第一個部分:
- 第一部分:理解YOLO的工作原理
- 第二部分:建立神經網絡層
- 第三部分:完成整個網絡的搭建
- 第四部分:目標score閾值化和NMS(非極大值抑制)
- 第五部分:完成整個網絡的從輸入搭配輸出流程
準備工作:
本文假設讀者已經具備一下幾點要求:
首先:你應該已經理解了CNN的工作原理,還有殘差結構,跨層連接和上採樣操作。
其次:要對目標檢測的基礎知識有所瞭解。比如bounding box regression, IoU 和 non-maximum suppression.
再者:可以熟練的使用pytorch搭建神經網絡
對於沒有達到上述要求的同學,建議先閱讀以下鏈接內容瞭解相關知識。
YOLOv1:https://arxiv.org/pdf/1506.02640.pdf
YOLOv2:https://arxiv.org/pdf/1612.08242.pdf
YOLOv3:https://pjreddie.com/media/files/papers/YOLOv3.pdf
CNN:http://cs231n.github.io/convolutional-networks/
bounding box regression:https://arxiv.org/pdf/1311.2524.pdf
IoU :https://www.youtube.com/watch?v=DNEm4fJ-rto
non-maximum suppression: https://www.youtube.com/watch?v=A46HZGR5fMw
PyTorch Official Tutorial:
http://pytorch.org/tutorials/beginner/deep_learning_60min_blitz.html
理解YOLO
YOLO的全名是:You Only Look Once. YOLO是一個基於DeepCNN的目標檢測算法,
由75個卷積層組成,其中使用卷積代替池化進行下采樣,防止pooling帶來的信息丟失問題。由於是全卷積結構,YOLO對輸入的圖像大小沒有限制,但是實際中當我們剛開始實現這個算法的時候,還是先把輸入圖像size固定到一個大小進行訓練和測試。
網絡的下采樣倍數通過stride參數限制,假設網絡的輸入大小是416x416,網絡總stride是32的話,那麼最終的輸出就是416/32=13*13。stride表示一個輸出比輸入小多少倍的因子(卷積而不是轉置卷積中)。
目標檢測算法基本都是一個套路,首先是深度卷積網絡提取特徵,後接一個分類器和迴歸器,預測圖像的label和bounding box的座標。
在YOLO算法中,由於他是全卷積網絡結構,所以最終的預測也是用卷積網絡完成,卷積核的大小是 1x1x(Bx(5+C)) ,YOLOv3中,最終的輸出是一個feature map,最終的預測feature map的大小和前一層的大小是一樣的,可以把最後的featuremap理解爲predict map,predict map中每一個單元可以預測固定數量的bounding box,比如預測3個。深度上,有(Bx(5+C)) 個通道,B表示每個單元可以預測的boundingbox數量,從paper中得知,每一個bounding box預測一個目標,包含有5+C個屬性,包括bounding box的中心座標,長寬,目標得分,和C類的分類置信度。
如果目標的中心位於該單元格的感受野中,則希望特徵映射的每個單元格都可以通過其中一個邊界框來預測對象。 (感受區域是輸入圖像對單元可見的區域,參見卷積神經網絡鏈接以進一步說明)。
這與YOLO如何訓練有關,只有一個邊界框負責檢測任何給定的對象。 首先,我們必須確定這個邊界框屬於哪個單元格。
爲此,我們將輸入圖像劃分成與最終特徵映射相等的維度網格。
讓我們考慮下面的一個例子,其中輸入圖像是416 x 416,網絡的stride是32。如前所述,特徵映射的維度將是13 x 13.然後,我們將輸入圖像劃分爲13 x 13 的單元格。
然後,選擇包含對象的的單元格(在輸入圖像上紅色格子)負責預測對象該對象(狗)。 在圖像中,其包含了真實的bounding box的中心座標(真實的bounding box標記爲黃色)。
現在,紅色單元格是網格中第7行的第7個單元格。 我們現在將第7行中的第7個單元分配到特徵映射(特徵映射中的相應單元)上作爲負責檢測狗的單元。
現在,這個單元格可以預測三個bounding box,最終只選擇一個作爲最終的預測。
請注意,我們在這裏討論的單元是預測feature map上的單元。 我們將輸入圖像分成一個網格,以確定預測特徵圖的哪個單元負責預測那個網格。
下面對YOLO的輸出分別做介紹。
Anchor Boxes
預測邊界框的寬度和高度可能是有意義的,但實際上,這會導致訓練期間不穩定的梯度。 相反,大多數目標檢測器預測對數空間變換後的寬和高,或簡單地偏移到預定義的默認邊界框,也即是anchor。
然後,將這些變換應用於anchor box以獲得預測。 YOLO v3有三個anchor,可以預測每個單元的三個邊界框。對於檢測上圖中的狗來講,選擇和groundtruth bounding box的IOU最大的預測bounding box作爲預測結果。
中心點預測
通常情況下,YOLO不預測邊界框中心的絕對座標。它預測的是偏移量,預測的結果通過一個sigmoid函數,迫使輸出的值在0和1之間。例如,考慮上圖中狗的情況。如果對中心的預測是(0.4,0.7),那麼這意味着中心位於13×13特徵地圖上的(6.4,6.7)。 (因爲紅細胞的左上角座標是(6,6))。
但如果預測的x,y座標大於1,會發生什麼情況,比如(1.2,0.7)。這意味着中心位於(7.2,6.7)。注意現在中心位於我們的紅色區域或第7排的第8個單元格的右側。這打破了YOLO背後的理論,因爲如果我們假設紅色區域負責預測狗,狗的中心必須位於紅色區域中,而不是位於紅色區域旁邊的其他網格里。
因此,爲了解決這個問題,輸出是通過一個sigmoid函數傳遞的,該函數在0到1的範圍內壓扁輸出,有效地將中心保持在預測的網格中。下面的這個公式詳細展示了預測結果如何轉化爲最終box的預測的。
邊界框的尺寸預測
通過對輸出應用對數空間變換,然後與anchor相乘來預測邊界框的尺寸。
得到的預測bw和bh通過圖像的高度和寬度進行歸一化。(訓練標籤是這樣的)。 因此,如果包含狗的盒子的預測值bx和by爲0.3,0.8,則13 x 13特徵映射上的實際寬度和高度爲(13 x 0.3,13 x 0.8)。
目標得分預測
目標分數表示目標包含在bounding box內的概率(對象分數也通過sigmiod函數歸一化)。 紅色和相鄰網格應該接近1,而角落網格接近0。
類別的置信度
類別置信度表示屬於特定類別(狗,貓,香蕉,汽車等)的檢測對象的概率。 在v3之前,YOLO曾經用softmax評分。
但是,該設計選擇已經在V3中被刪除,作者選擇使用sigmoid。 原因是Softmax會假設預測是相互排斥的。 簡而言之,如果一個對象屬於一個類,那麼它保證它不能屬於另一個類。
但是,如果類別中有woman和preson兩個類別,那麼softmax最終的預測只有一個,woman或者person,但是sigmoid可以實現woman和person。 這就是作者們避免使用Softmax激活的原因。
不同尺度的預測
YOLO v3可以進行3種不同尺度的預測。 使用檢測層在三種不同尺寸的特徵圖上進行檢測,分別具有stride=32,16,8。 這意味着,在輸入416 x 416的情況下,我們使用13 x 13,26 x 26和52 x 52的比例進行檢測。
網絡下采樣輸入圖像直到第一檢測層,其中使用具有步幅32的層的feature map進行檢測。此外,將層上採樣2倍,並與具有相同特徵圖的先前層的特徵圖連接大小。 現在在具有stride=16的層上進行另一檢測。重複相同的上採樣過程,並且在stride=8處進行最終檢測。
在每個尺度上,每個單元使用3個anchor來預測3個bounding box,從而使用9個anchor的總數(anchor在不同尺度上是不同的)
作者在論文中說名,這有助於YOLO v3更好地檢測小型物體,這是對YOLO早期版本的頻繁投訴。 上採樣可以幫助網絡學習有助於檢測小物體的細粒度特徵。
輸出處理
對於尺寸爲416×416的圖像,YOLO預測((52×52)+(26×26)+ 13×13))×3 = 10647個邊界框。 但是,在我們的形象中,只有一個物體,一隻狗。 我們如何將檢測結果從10647減少到1?
基於對象置信度的閾值。
首先,我們根據對象分數過濾框。 通常,具有低於閾值分數的框被忽略。
非最大抑制(NMS)
NMS打算解決同一圖像的多重檢測問題。 例如,紅色網格單元的所有3個邊界框可能檢測到一個對象,或者相鄰單元可能檢測到相同的對象。
有關YOLO的原理介紹第一部分完成了,下面將介紹YOLO的網絡搭建部分。
YOLO網絡搭建實踐
本章節主要介紹如何搭建YOLO網絡,在理解本章節之前,假設你已經基本掌握了pytorch的用法。
首先,創建一個darknet.py
文件,用來存放YOLO的網絡結構部分,另建一個util.py
文件,用來存放一些輔助函數。
官方提供的代碼是caffe的,網絡定義在.protxt
裏面,在這裏,我們也將使用官方的cfg
配置文件定義網絡結構,可以從這裏下載:
https://github.com/pjreddie/darknet/blob/master/cfg/yolov3.cfg
把文件下載下來,然後放到代碼目錄裏面,如果使用的是linux系統,可以直接通過如下命令完成:
mkdir cfg
cd cfg
wget https://raw.githubusercontent.com/pjreddie/darknet/master/cfg/yolov3.cfg
配置文件裏卷積定義如下:
[convolutional]
batch_normalize=1
filters=64
size=3
stride=1
pad=1
activation=leaky
shortcut部分:
[shortcut]
from=-3
activation=linear
shortcut部分是卷積的跨層連接,就像Resnet中使用的一樣,參數from
是 ,意思是shortcut的輸出是通過與先前的倒數第三層網絡相加而得到。
上採樣:
[upsample]
stride=2
Route
[route]
layers = -4
[route]
layers = -1, 61
route層值得一些解釋。 它具有可以具有一個或兩個值的屬性層。
當屬性只有一個值時,它會輸出由該值索引的網絡層的特徵圖。 在我們的示例中,它是 ,因此這個層將從Route層向後輸出第4層的特徵圖。
當圖層有兩個值時,它會返回由其值所索引的圖層的連接特徵圖。 在我們的例子中,它是 ,並且該圖層將輸出來自上一層(-1)和第61層的特徵圖,並沿深度的維度連接。
YOLO
[yolo]
mask = 0,1,2
anchors = 10,13, 16,30, 33,23, 30,61, 62,45, 59,119, 116,90, 156,198, 373,326
classes=80
num=9
jitter=.3
ignore_thresh = .5
truth_thresh = 1
random=1
YOLO層對應於第1部分中描述的檢測層(detection layer)。anchor描述9個anchor,但只有mask標記的屬性索引到的anchor纔會被使用到。 這裏,mask的值是0,1,2,這意味着使用第一,第二和第三個anchor。 這是有道理的,因爲檢測層的每個單元預測3個box。 總共有三個檢測層,共計9個anchor,第一部分中已經提到。
Net
[net]
# Testing
batch=1
subdivisions=1
# Training
# batch=64
# subdivisions=16
width= 320
height = 320
channels=3
momentum=0.9
decay=0.0005
angle=0
saturation = 1.5
exposure = 1.5
hue=.1
中有另一種稱爲 的塊,但我不會將其稱爲layer,因爲它僅描述有關網絡輸入和訓練參數的信息。 它不用於YOLO的正向傳播。 然而,它確實爲我們提供了像網絡輸入大小這樣的信息,我們用它來調整正向傳播中的anchor。
解析配置文件cfg
正式開始寫代碼前,先import一些必要的庫:
from __future__ import division
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.autograd import Variable
import numpy as np
定義一個parse_cfg
函數,輸入信息是cfg
的文件路徑,輸出一個包含文件中block的字典,代碼如下:
def parse_cfg(cfgfile):
"""
Takes a configuration file
Returns a list of blocks. Each blocks describes a block in the neural
network to be built. Block is represented as a dictionary in the list
"""
file = open(cfgfile, 'r')
lines = file.read().split('\n') # store the lines in a list
lines = [x for x in lines if len(x) > 0] # get read of the empty lines
lines = [x for x in lines if x[0] != '#'] # get rid of comments
lines = [x.rstrip().lstrip() for x in lines] # get rid of fringe whitespaces
block = {}
blocks = []
for line in lines:
if line[0] == "[": # This marks the start of a new block
if len(block) != 0: # If block is not empty, implies it is storing values of previous block.
blocks.append(block) # add it the blocks list
block = {} # re-init the block
block["type"] = line[1:-1].rstrip()
else:
key,value = line.split("=")
block[key.rstrip()] = value.lstrip()
blocks.append(block)
return blocks
然後就有了YOLO的網絡信息,存放在blocks 的list中,就可以去搭建網絡了。
list中包含了之前提到的5種類型的網絡層,pytorch提供的有預定義好的convolution
和upsample
,只需要寫我們自己的modules就可以了。
接下來定義一個create_modules
函數,以上個函數輸出的blocks list作爲輸入,對其中的信息解碼並轉換爲pytorch的網絡層:
def create_modules(blocks):
net_info = blocks[0] #Captures the information about the input and pre-processing
module_list = nn.ModuleList()
prev_filters = 3
output_filters = []
for index, x in enumerate(blocks[1:]):
module = nn.Sequential()
#check the type of block
#create a new module for the block
#append to module_list
if (x["type"] == "convolutional"):
#Get the info about the layer
activation = x["activation"]
try:
batch_normalize = int(x["batch_normalize"])
bias = False
except:
batch_normalize = 0
bias = True
filters= int(x["filters"])
padding = int(x["pad"])
kernel_size = int(x["size"])
stride = int(x["stride"])
if padding:
pad = (kernel_size - 1) // 2
else:
pad = 0
#Add the convolutional layer
conv = nn.Conv2d(prev_filters, filters, kernel_size, stride, pad, bias = bias)
module.add_module("conv_{0}".format(index), conv)
#Add the Batch Norm Layer
if batch_normalize:
bn = nn.BatchNorm2d(filters)
module.add_module("batch_norm_{0}".format(index), bn)
#Check the activation.
#It is either Linear or a Leaky ReLU for YOLO
if activation == "leaky":
activn = nn.LeakyReLU(0.1, inplace = True)
module.add_module("leaky_{0}".format(index), activn)
#If it's an upsampling layer
#We use Bilinear2dUpsampling
elif (x["type"] == "upsample"):
stride = int(x["stride"])
upsample = nn.Upsample(scale_factor = 2, mode = "bilinear")
module.add_module("upsample_{}".format(index), upsample)
#If it is a route layer
elif (x["type"] == "route"):
x["layers"] = x["layers"].split(',')
#Start of a route
start = int(x["layers"][0])
#end, if there exists one.
try:
end = int(x["layers"][1])
except:
end = 0
#Positive anotation
if start > 0:
start = start - index
if end > 0:
end = end - index
route = EmptyLayer()
module.add_module("route_{0}".format(index), route)
if end < 0:
filters = output_filters[index + start] + output_filters[index + end]
else:
filters= output_filters[index + start]
#shortcut corresponds to skip connection
elif x["type"] == "shortcut":
shortcut = EmptyLayer()
module.add_module("shortcut_{}".format(index), shortcut)
#Yolo is the detection layer
elif x["type"] == "yolo":
mask = x["mask"].split(",")
mask = [int(x) for x in mask]
anchors = x["anchors"].split(",")
anchors = [int(a) for a in anchors]
anchors = [(anchors[i], anchors[i+1]) for i in range(0, len(anchors),2)]
anchors = [anchors[i] for i in mask]
detection = DetectionLayer(anchors)
module.add_module("Detection_{}".format(index), detection)
return (net_info, module_list)
OK,此部分完成了網絡的搭建部分,爲了驗證網絡搭建的是否正確,可以適用下面的代碼運行,測試一下:
blocks = parse_cfg("cfg/yolov3.cfg")
print(create_modules(blocks))
輸出的結果是這樣的:
.
.
(9): Sequential(
(conv_9): Conv2d (128, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
(batch_norm_9): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True)
(leaky_9): LeakyReLU(0.1, inplace)
)
(10): Sequential(
(conv_10): Conv2d (64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(batch_norm_10): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True)
(leaky_10): LeakyReLU(0.1, inplace)
)
(11): Sequential(
(shortcut_11): EmptyLayer(
)
)
.
.
到此,網絡搭建部分已經完成,在下一次的文章中會介紹網絡的訓練部分,輸入一張圖像並輸出結果。
參考文獻:
https://blog.paperspace.com/how-to-implement-a-yolo-object-detector-in-pytorch/
https://blog.paperspace.com/how-to-implement-a-yolo-v3-object-detector-from-scratch-in-pytorch-part-2/
http://pytorch.org/tutorials/beginner/deep_learning_60min_blitz.html
https://arxiv.org/pdf/1506.02640.pdf
https://arxiv.org/pdf/1612.08242.pdf
https://pjreddie.com/media/files/papers/YOLOv3.pdf
http://cs231n.github.io/convolutional-networks/
https://arxiv.org/pdf/1311.2524.pdf
https://www.youtube.com/watch?v=DNEm4fJ-rto
https://www.youtube.com/watch?v=A46HZGR5fMw