必備條件:
- 此教程part1-YOLO的工作原理
- PyTorch的基本工作知識,包括如何使用 nn.Module, nn.Sequential and 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個block。其中前三個描述的是卷積層,后面是一個shortcut層。shortcut層也就是跨層連接,就像ResNet用的那種。YOLO中使用了5種類型的層。如下:
Convolutional
[convolutional] batch_normalize=1 filters=64 size=3 stride=1 pad=1 activation=leaky
Shortcut
[shortcut] from=-3 activation=linear
這里有個from參數為-3,它代表shortcut層的輸出是將前一層輸出與后面第三層的特征圖加起來得到的。
Upsample
[upsample] stride=2
用雙線性上采樣,以因子stride對上一層的特征圖進行上采樣。
Route:
[route] layers = -4 [route] layers = -1, 61
route層需要解釋下。它有一個屬性layers,可以是一個值也可以是兩個值
當layers屬性只有一個值的時候,它輸出的是索引處的特征圖。在我們實驗中,它是-4就代表輸出的特征圖將來自於Route層后的第4層
當layers屬性有兩個值時,它返回的是按照索引值連接起來的特征圖。在我們實驗中為-1,61就代表輸出的特征圖來自Route層前一層(-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層就對應着part1中描述的檢測層。anchors屬性定義了9個錨,但是只會使用mask屬性指定索引位置處的那些錨。這里mask的值為0,1,2就代表第一個,第二個和第三個錨被使用。這是有道理的,因為每個檢測層的每個cell會預測三個邊界框。我們有三種不同尺寸的錨,所以總共會有9個錨盒。
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
這是cfg中的一種block類型叫做net,但是我們不把他當作一個層,因為它只描述了網絡的輸入和訓練參數的信息。在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文件,將每個block存為一個字典。也就是bolock的屬性與它們的值通過鍵值對的方式存儲在字典中。當我們解析cfg時,字典不斷增加,最終形成一個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
然后循環遍歷結果列表得到blocks
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函數將接收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 = []
在迭代blocks列表之前,我們需要定義一個變量net_info來保存網絡信息。
nn.ModuleList:
我們的函數將會返回一個 nn.ModuleList。這個類就好比包含 nn.Module 對象的普通列表。但是在我們向nn.ModuleList里增加成員nn.Moudle對象的時候(也就是在往我們的網絡中增加模塊),nn.ModuleList里面所有nn.Module對象的參數也會被加入到網絡參數中去。
當定義新卷積層的時候需要指定它的卷積核的維度。cfg文件已經提供了卷積核的高度與寬度,卷積核的深度就是上一層卷積核的數目。這就意味着我們需要記錄之前已經應用的卷積層的卷積核數目,我們使用變量prev_filter來做這個。初始數目為3,對應着RGB三個通道。
route層會從之前層獲得特征映射,如果route前面正好有一個卷積層,那就需要在之前層的特征圖上做卷積。因此我們需要記錄的不止是前一層的卷積核數目,而是前面每一層的。當我們開始迭代時,我們將每個block的輸出濾波器的數目加到列表output_filters中。
現在的想法就是迭代blocks列表,然后為每個block創建一個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.Module對象。如果你看看cfg,你就知道了每個block或許不止一層。舉例來說,convolutional這種block類型除了卷積層之外還有batch norm層以及leakey ReLU激活層。我們使用 nn.Sequential 和它的 add_module 函數將這些層串在一起。以下舉例說明了如何構建convolutional層和upsample層。
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 Layer / shortcut Layer:
下面我們寫代碼創建Route和Shortcut層
#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)
Route層的構建代碼需要稍微深入解釋一下,首先我們提取layers屬性,將其轉換為整型儲存到列表之中。
之后我們用了一個新的層,叫做EmptyLayer,這其實是一個空的層。
route = EmptyLayer()
它是這樣定義的:
class EmptyLayer(nn.Module): def __init__(self): super(EmptyLayer, self).__init__()
為什么我們需要定義一個空的層呢?
空的層看起來就是什么都沒做,所以顯得有點奇怪。其實Route層像其他任何層一樣也是執行一個特定操作(對前面層進行前向傳播/拼接)。在Pytorch中,當我們定義一個層就是去繼承nn.Module類,然后在類內部的forward函數里面寫需要執行的操作。
為了定義Route層,我們得初始化一個nn.Moudle對象,並將屬性layers作為它的成員變量。之后我們就可以在forward函數里面寫代碼來對特征圖進行拼接/前傳。
但考慮到拼接的代碼其實非常簡單(對特征圖使用torch.cat),定義一個層屬於不必要的抽象,導致模板代碼的增加。所以我們可以定義一個假的層來替代前面提到的Route層,之后在darknet的forward函數里面直接進行拼接。
Route層前面的卷積層會將對之前的特征圖施加卷積操作(有可能是拼接)。下面的代碼會更更新filters變量來保存Route層輸出的卷積核數量。
if end < 0: #If we are concatenating maps filters = output_filters[index + start] + output_filters[index + end] else: filters= output_filters[index + start]
shortcut層也是使用empty層定義的,因為它的操作也很簡單(相加)。它不需要更新filter變量因為它僅僅是將前一層的特征圖添加到后一層。
YOLO Layer:
最后我們來寫代碼創建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,它包含檢測邊界框需要用到的anchors。
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( ) ) . . .
這一部分到此結束。下一部分我們將會組裝這些bolock,然后輸入一張圖片產生輸出。
Further Reading