pytorch+yolov3(2)
如何在PyTorch中從頭開始實現YOLO(v3)物件檢測器:第2部分
圖片來源:Karol Majek。在這裡檢視他的YOLO v3實時檢測視訊
這是從頭開始實現YOLO v3探測器的教程的第2部分。在最後一部分中,我解釋了YOLO是如何工作的,在這部分中,我們將在PyTorch中實現YOLO使用的層。換句話說,這是我們建立模型構建塊的部分。
本教程的程式碼旨在在Python 3.5和PyTorch 0.4上執行
本教程分為5個部分:
-
第2部分(本文):建立網路體系結構的各個層
先決條件
- 本教程的第1部分/ YOLO如何工作的知識。
- PyTorch的基本知識,包括如何建立自定義的架構
nn.Module
,nn.Sequential
以及torch.nn.parameter
類。
我假設你以前有過PyTorch的一些經驗。如果您剛剛開始,我建議您在返回此帖之前稍微使用該框架。
入門
首先建立一個探測器程式碼所在的目錄。
然後,建立一個檔案darknet.py
。Darknet是YOLO底層架構的名稱。該檔案將包含建立YOLO網路的程式碼。我們將使用一個檔案來補充它,該檔案util.py
將包含各種輔助函式的程式碼。將這兩個檔案儲存在檢測器資料夾中。您可以使用git來跟蹤更改。
配置檔案
官方程式碼(在C中編寫)使用配置檔案來構建網路。所述CFG檔案描述了網路的通過塊佈局,塊。如果你來自caffe背景,它相當於.protxt
用於描述網路的檔案。
我們將使用作者釋出的官方cfg檔案來構建我們的網路。從此處下載並將其cfg
放在檢測器目錄中呼叫的資料夾中。如果您使用的是Linux,請cd
mkdir cfg
cd cfg
wget https://raw.githubusercontent.com/pjreddie/darknet/master/cfg/yolov3.cfg
如果開啟配置檔案,您將看到類似的內容。
[convolutional]
batch_normalize=1
filters=64
size=3
stride=2
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=32
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=64
size=3
stride=1
pad=1
activation=leaky
[shortcut]
from=-3
activation=linear
我們看到上面有4個街區。其中3個描述卷積層,然後是快捷層。甲快捷層是跳過連線,像在RESNET使用的一個。YOLO中使用了5種類型的圖層:
卷積
[convolutional]
batch_normalize=1
filters=64
size=3
stride=1
pad=1
activation=leaky
捷徑
[shortcut]
from=-3
activation=linear
甲快捷層是跳過連線,類似於在RESNET使用的一個。的from
引數是-3
,這意味著在快捷層的輸出由獲得加入來自先前和從向後的第三層特徵對映快捷層。
上取樣
[upsample]
stride=2
通過stride
使用雙線性上取樣的因子對上一層中的要素圖進行取樣。
路線
[route]
layers = -4
[route]
layers = -1, 61
該航線層應該得到一些解釋。它有一個屬性layers
,可以有一個或兩個值。
當layers
屬性只有一個值時,它會輸出由該值索引的圖層的要素圖。在我們的示例中,它是-4,因此圖層將從Route圖層向後輸出第4層的要素圖。
當layers
有兩個值時,它返回由它的值索引的層的連線特徵對映。在我們的例子中,它是-1,61,並且該層將輸出前一層(-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部分中描述的檢測層。anchors
描述了9個錨點,但僅使用了由mask
標籤的屬性索引的錨點。這裡,值為mask
0,1,2,這意味著使用第一,第二和第三錨。這是有意義的,因為檢測層的每個單元預測3個框。總的來說,我們有3個等級的檢測層,總共有9個錨點。
淨
[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
net
在cfg中呼叫了另一種型別的塊,但我不會將其稱為層,因為它僅描述有關網路輸入和訓練引數的資訊。它不用於YOLO的前進傳球。但是,它確實為我們提供了網路輸入大小等資訊,我們用它來調整前向傳遞中的錨點。
解析配置檔案
在開始之前,在darknet.py
檔案頂部新增必要的匯入。
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
,它將配置檔案的路徑作為輸入。
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
"""
這裡的想法是解析cfg,並將每個塊儲存為dict。塊的屬性及其值作為鍵值對儲存在字典中。當我們解析cfg時,我們會繼續將這些由block
程式碼中的變量表示的dicts附加到列表中blocks
。我們的函式將返回此塊。
我們首先將cfg檔案的內容儲存在字串列表中。以下程式碼對此列表執行一些預處理。
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
建立構建塊
現在我們將使用上面返回的列表parse_cfg
為配置檔案中的塊構建PyTorch模組。
我們在列表中有5種類型的層(如上所述)。PyTorch為型別convolutional
和型別提供預構建的層upsample
。我們必須通過擴充套件nn.Module
類來為其餘層編寫自己的模組。
該create_modules
函式獲取函式blocks
返回的列表parse_cfg
。
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 = []
在迭代塊列表之前,我們定義一個變數net_info
來儲存有關網路的資訊。
nn.ModuleList
我們的功能將返回一個nn.ModuleList
。該類幾乎就像包含nn.Module
物件的普通列表。然而,當我們新增nn.ModuleList
作為成員nn.Module
物件(即,當我們增加模組,我們的網路)中,所有的parameter
第nn.Module
物件(模組)內的nn.ModuleList
被新增為parameter
所述第nn.Module
物件(即我們的網路,這是我們所新增的nn.ModuleList
作為也是一個成員。
當我們定義一個新的卷積層時,我們必須定義它的核心的維度。雖然核心的高度和寬度由cfg檔案提供,但核心的深度恰好是前一層中存在的過濾器數量(或要素圖的深度)。這意味著我們需要跟蹤應用卷積層的層中的濾波器數量。我們使用變數prev_filter
來做到這一點。我們將其初始化為3,因為影象具有對應於RGB通道的3個濾波器。
路徑圖層從先前的圖層中提取(可能是連線的)要素圖。如果在路徑圖層前面有一個卷積層,則核心將應用於先前圖層的要素圖,恰好是路徑圖層帶來的圖層。因此,我們需要保持在不僅先前層的過濾器的數量的軌道,但每個 前述層中的一個。在迭代時,我們將每個塊的輸出過濾器的數量附加到列表中output_filters
。
現在,我們的想法是迭代塊列表,併為每個塊建立一個PyTorch模組。
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
nn.Sequential
class用於順序執行多個nn.Module
物件。如果你看一下cfg,你會發現一個塊可能包含多個層。例如,convolutional
除了卷積層之外,型別塊還具有批量範數層以及洩漏的ReLU啟用層。我們使用nn.Sequential
和它的add_module
功能將這些圖層組合在一起。例如,這就是我們建立卷積和上取樣層的方式。
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)
路線圖層/快捷方式圖層
接下來,我們編寫用於建立Route和Shortcut Layers 的程式碼。
#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)
建立路由層的程式碼值得一些解釋。首先,我們提取layers
屬性的值,將其轉換為整數並將其儲存在列表中。
然後我們有一個新的圖層EmptyLayer
,顧名思義它只是一個空圖層。
route = EmptyLayer()
它被定義為。
class EmptyLayer(nn.Module):
def __init__(self):
super(EmptyLayer, self).__init__()
等一下,空圖層?
現在,一個空層可能看起來很奇怪,因為它什麼都不做。路由層,就像任何其他層一樣執行操作(提前一層/連線)。在PyTorch中,當我們定義一個新層時,我們子類nn.Module
並編寫該層在forward
該nn.Module
物件的函式中執行的操作。
為了設計Route塊的層,我們必須構建一個nn.Module
物件,該物件使用屬性的值layers
作為其成員進行初始化。然後,我們可以編寫程式碼來連線/提出forward
函式中的特徵對映。最後,我們在forward
網路功能中執行該層。
但是鑑於連線程式碼相當簡短(呼叫torch.cat
特徵對映),如上所述設計一個層將導致不必要的抽象,這隻會增加鍋爐板程式碼。相反,我們可以做的是用虛擬層代替建議的路由層,然後直接在代表暗網forward
的nn.Module
物件的函式中執行連線。(如果最後一行對你沒有多大意義,我建議你閱讀nn.Module
PyTorch中如何使用類。連結在底部)
位於路線圖層前面的卷積層將其核心應用於前一層的(可能連線的)要素圖。以下程式碼更新filters
變數以儲存路由層輸出的過濾器數。
if end < 0:
#If we are concatenating maps
filters = output_filters[index + start] + output_filters[index + end]
else:
filters= output_filters[index + start]
快捷方式圖層也使用空圖層,因為它還執行非常簡單的操作(新增)。沒有必要更新更新filters
變數,因為它只是將前一層的特徵對映新增到後面的層的特徵對映。
YOLO層
最後,我們編寫用於建立YOLO層的程式碼。
#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)
我們定義了一個新層DetectionLayer
,它包含用於檢測邊界框的錨點。
檢測層定義為
class DetectionLayer(nn.Module):
def __init__(self, anchors):
super(DetectionLayer, self).__init__()
self.anchors = anchors
在迴圈結束時,我們會做一些簿記。
module_list.append(module)
prev_filters = filters
output_filters.append(filters)
這就是迴圈體的結論。在函式結束時create_modules
,我們返回一個包含net_info
,和的元組module_list
。
return (net_info, module_list)
測試程式碼
您可以通過darknet.py
在檔案末尾鍵入以下行並執行該檔案來測試程式碼。
blocks = parse_cfg("cfg/yolov3.cfg")
print(create_modules(blocks))
您將看到一個長列表(完全包含106個專案),其元素將如下所示
.
.
(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(
)
)
.
.
.
就是這部分。在下一部分中,我們將組裝我們建立的構建塊以生成影象的輸出。
進一步閱讀
Ayoosh Kathuria目前是印度國防研究與發展組織的實習生,他正致力於改善粒狀視訊中的物體檢測。當他不工作時,他正在睡覺或者在他的吉他上玩粉紅色弗洛伊德。您可以在LinkedIn上與他聯絡,或者檢視他在GitHub上做的更多內容