本篇文章是《如何使用PyTorch從零開始實現YOLO(v3)目標檢測算法》的第二部分。這系列論文一共有五篇文章,五篇文章的主要內容在下文中有涉及。如果有問題歡迎和我交流~
如何使用PyTorch從零開始實現YOLO(v3)目標檢測算法?
1 開始
首先我們創建一個檢測器代碼所在的目錄,然後創建一個darknet.py文件。Darknet是YOLO算法架構的名字。這個文件將會包含創建YOLO網絡架構的代碼。我們將會創建util.py文件對其進行補充,在util.py中將會包括各種各樣的輔助函數的代碼。將這些文件保存在你的檢測器文件夾下,你可以使用git命令去追蹤代碼的變化軌跡。
2 配置文件
官網代碼(使用C來編寫的)使用配置文件來創建一個網絡。這個cfg文件,一個模塊接着一個模塊的去描述了網絡架構的佈局。我們會使用作者發佈的官方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個卷積+1個捷徑層)。三個卷積層後面跟着一個捷徑層。捷徑層是一個跳躍連接層,就像是在ResNet中使用的那樣。在YOLO中使用五種不同的網絡層:
2.1 卷積層
[convolutional]
batch_normalize=1
filters=64
size=3
stride=1
pad=1
activation=leaky
2.2 捷徑層
它就是一個容器,相加之後,然後直接作爲下一層的輸入
[shortcut]
from=-3
activation=linear
捷徑層是一個跳躍連接,類似在ResNet中使用的那種。參數from是-3,這意味着捷徑層的輸出是捷徑層的前一層和往前數第三層的特徵圖相加得來的(在Resnet網絡中,沒有單獨設置一個層,而是在forward函數中,對特徵圖進行的相加,在YOLO中,直接設置了一個網絡層)。
2.3 上採樣層
[upsample]
stride=2
使用步長和雙線性上採樣的方法,對上一層的特徵圖進行上採樣。
2.4 路由層
[route]
layers = -4
[route]
layers = -1, 61
這個route層需要解釋一下。它有一個layers的屬性,這個屬性可以有一個值,也可以有兩個值。當layer只有一個值的時候,它輸出通過這個值所索引到層的特徵圖。在我們的案例中,這個值是-4,因此route層就會輸出從route層向後數第四個層的特徵圖。當layer有兩個值的時候,他就會返回通過該值所索引到的網絡層的特徵圖的結合。在我們的例子中它是-1,61,route層會輸出上一層和第61層的特徵圖,並且沿着深度進行連接。
2.5 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層和在第一部分所表述的檢測層相關。anchors這個屬性表述了9個錨框,但是隻能使用那些被mask標籤的屬性所索引的錨框。這裏,mask的值是0,1,2,這意味着第一個,第二個和第三個錨框將會被使用。這是有意義的,這是因爲每一個單元只能夠預測三個邊界框。總而言之,我們的檢測層有三個尺度,加起來一共就是9個錨框。。
2.6 網絡架構描述
[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
3 解析(parse)配置文件
最終目的:將每個模塊以字典的形式保存在在列表中
[{'type': 'convolutional','batch_normalize': '1','filters': '32', 'size': '3','stride': '1','pad': '1','activation': 'leaky'},
{'type': 'convolutional','batch_normalize': '1','filters': '64','size': '3','stride': '2','pad': '1', 'activation': 'leaky'},]
在開始之前,我們需要再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文件,並且把每一個模塊當做“詞典”存儲。模塊的屬性和值以鍵值對的形式在詞典中存儲。在我們解析的過程中,我們持續向列表blocks中添加詞典,這些詞典在我們的代碼中用變量block所表示。我們的函數將會返回這個模塊。我們首先把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
# 這邊我是使用with打開文件的,這是因爲with可以自動關閉文件,不需要我們手動關閉
# with open(cfgfile) as file:
之後,我們循環結果列表來得到模塊
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
# 這邊提供一個新思路:直接將所有文件都保存在blocks中,然後pop首個元素(空元素)
# block = {}
# blocks = []
# for line in lines:
# if line[0] == "[":
# # if len(block) != 0:
# blocks.append(block)
# block = {}
# block["type"] = line[1:-1].rstrip()
# else:
# key,value = line.split("=")
# block[key.rstrip()] = value.lstrip()
# blocks.append(block)
# blocks.pop(0)
4 創建構造模塊
最終目的:就是將所有的網絡模塊保存在ModuleList實例對象module_list中
實現思路:
(1)遍歷blocks中保存的所有模塊
(2) 將五個網絡層:卷積層,上採樣層,捷徑層,route層和yolo層保存在nn.Sequential實例對象中,更新輸出channel
(3)將nn.Sequential實例對象保存在nn.ModuleList實例對象中
注意:在PyTorch中,我們不用擔心數據的寬和高,我們只需要指定input_channel和output_channel。由此可見輸入輸出channel的作用不言而喻,因此在定義卷積層的時候,我們需要明確上一層輸出的output_channel是多少。
現在我們打算使用parse_cfg函數返回列表去爲配置文件提供的網絡塊構造PyTorch模塊。在列表中有五中類型的網絡層(上面到的)。PyTorch爲卷積層和上採樣層提供了預製的網絡層。通過添加類,我們將會爲剩下的層編寫我們自己的模塊。這個create_modules函數將會輸入由parse_cfg函數返回的列表blocks。
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,這是因爲YOLO是FPN網絡進行多尺度預測
# yolo層的輸出channel並不一定是下一層輸入的channel
# 所以纔有route層
output_filters = []
在我們迭代blocks的列表之前,我們會定義一個變量net_info去存儲網絡相關的信息。
4.1 nn.ModuleList
我們的函數會返回nn.ModuleList。這個類和包含nn.Module實例對象的普通列表很像。當我們把nn.ModuleList作爲nn.Module實例對象的成員添加的時候(也就是我們把模塊添加到我們的網絡時),網絡也會以nn.Module實例對象參數(parameters)的形式添加nn.ModuleList模塊內的nn.Module實例對象的所有參數。(也就是我們的網絡,我們把nn.ModuleList作爲成員添加的網絡)。當我們定義一個新的卷積層的時候,我們必須定義掩膜的維度。儘管掩膜的寬度和高度已經由配置文件提供,但是掩膜的深度恰好是在上一個神經層出現(present)的卷積核的數量(或者是特徵圖的深度)。這意味着我們必須要追蹤應用卷積層的網絡層的卷積核的數量,我們使用變量prev_filter來做到這一點,我們初始化prev_filter爲3,因爲剛開始的時候RGB對應圖像三個過濾器(前一個網絡層的輸出特徵圖的channel就是下一個網絡層的輸入的channel)。
路由層會從以前的層帶來(可能連接的)特徵圖。如果卷積層恰好在路由層的前面,然後卷積核就會使用在之前卷積層的特徵圖上,準確來說是路由層帶來的特徵圖。因此,我們不光需要追蹤之前層的,而且還要追蹤後面網絡層的卷積核的數量。當我們進行迭代的時候,我們需要將每個模塊的輸出卷積核的個數添加到列表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類用來有序的地執行多個的nn.Mudule類的實例對象。如果你觀察cfg文件,你會發現一個網絡塊中包含不止一個網絡層。比如,卷積網絡塊中處理包含卷積層之外,還包含批歸一化層和leaky ReLU激活函數。我們使用nn.Sequential和它的add_module函數來把這些網絡層串聯起來。
(我們需要給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)
4.2 路由層/捷徑層
接下來,我們將編寫用於創建路由層和捷徑層的代碼。
如果路由層有兩個參數,表示着兩個特徵圖的通道數要進行相加(所以需要記錄每個層的通道數)
#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屬性的值,把它轉換成(cast it into)整型並且保存在一個列表中(因爲該層的數據都是保存的詞典中的,因此我們需要將數據保存在列表中)。之後,我們有一個叫做EmptyLayer的網絡層,顧名思義,就是個空的網絡層。
route = EmptyLayer()
它的定義如下
class EmptyLayer(nn.Module):
def __init__(self):
super(EmptyLayer, self).__init__()
等等,一個新的層?現在,一個空的層可能看起來有點奇怪,因爲它什麼也不做。就像其他任何層一樣,路由層執行操作(帶來前面的層/進行連接)。在PyTorch中,當我們定義一個新的層的時候,我們需要繼承nn.Module類,在nn.Module實例對象的forward函數中編寫這個層執行的操作。
因爲要爲路由層設計一個網絡層,我們將會構建nn.Module的實例對象,該對象初始化的時候使用屬性layer的值來作爲他的成員。然後,我們可以在forward函數中編寫代碼去連接/帶來前面的網絡層。最後,我們在我們網絡的forward函數中執行這個網絡層(簡而言之,在PyTorch中,網絡的定義和數據的執行時分開編寫的,當網絡已經定義好之後,數據就會在層與層之間進行處理)。
但是如果(given)連接的代碼非常簡短(在特徵圖中調用torch.cat函數),設計上面的網絡層就會導致不必要的抽象,它會增加模板化代碼。相反,我們可以做的就是使用一個虛擬層代替設計路由層,然後再在表示darknet網絡的nn.Module的實例對象的forward函數中直接進行連接操作。(如果你不太理解最後一行的意思,我建議你讀一下PyTorch中如何使用nn.Module類的文檔)
就在路由層前面的卷積層對從前面網絡層帶來的(可能連接的)特徵圖進行掩膜運算。下面的代碼更新變量filters,用來保存由路由層輸出的卷積核的數量。
if end < 0:
#If we are concatenating maps
filters = output_filters[index + start] + output_filters[index + end]
else:
filters= output_filters[index + start]
捷徑層也使用了一個空的網路層,因爲它也進行了一個非常簡單的操作(加法)。這裏不需要去更新變量filter,因爲它僅僅是將前面網絡層的特徵圖加到了後面的這些層。
5 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)
6 測試代碼
你可以通過在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(
)
)
.
.
.
這就是這個部分的全部內容。在下一個部分,我們就會組裝我們創建的用於從圖片生成輸出(produce output from an image)的構造塊。
7 總結
這篇博客主要介紹了兩個內容:讀取配置文件內容;根據配置文件構建自己的網絡架構模型
(1)讀取配置文件內容:
- 使用with打開文件、讀取文件
- 將各個網絡模型按照字典的形式保存在元組中
(2)將各個網絡模型保存在ModuleList中
- 將各個網絡模塊先保存在nn.Sequential中
- 然後將nn.Sequential保存在ModuleList中
- 更新上個網絡層的輸出channel