YOLO v3實現 Part2

這是關於從頭實現YOLO v3檢測器的教程的第2部分。在上一部分中,我解釋了YOLO是如何工作的,在這一部分中,我們將實現YOLO在PyTorch中使用的圖層。換句話說,這是我們創建模型構建塊的部分。

The code for this tutorial is designed to run on Python 3.5, and PyTorch 0.4. It can be found in it’s entirety at this Github repo.

This tutorial is broken into 5 parts:

  1. Part 1 : Understanding How YOLO works
  2. Part 2 (This one): Creating the layers of the network architecture
  3. Part 3 : Implementing the the forward pass of the network
  4. Part 4 : Objectness Confidence Thresholding and Non-maximum Suppression
  5. Part 5 : Designing the input and the output pipelines

先決條件

  • 第一部分的教程/知識YOLO如何工作。
  • PyTorch基本使用知識, 包括如何創建 nn.Module, nn.Sequential 的自定義框架和 torch.nn.parameter 類.

我假設你以前已經有過使用PyTorch的經驗。如果您剛剛開始,我建議您在返回本文之前先研究一下這個框架。

開始

首先創建一個檢測器代碼所在的目錄。

然後,創建一個文件darknet.py。Darknet是YOLO底層架構的名稱。這個文件將包含創建YOLO網絡的代碼。我們將用一個名爲util.py 的文件,其包含各種幫助函數的代碼,對Darknet進行補充。將這兩個文件保存在檢測器文件夾中。您可以使用git來跟蹤更改。

配置文件

官方代碼(用C編寫)使用配置文件構建網絡。cfg 文件一塊一塊的描述網絡的設計。如果您來自caffe背景,它相當於.protxt文件,用於描述網絡。

我們將使用作者發佈的官方cfg文件來構建我們的網絡。從這裏下載它 here ,並將它放在名爲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個描述卷積層,後面是一個 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  

shortcut 是跳過連接,類似於ResNet中使用的連接。from參數爲-3,意味着shortcut 的輸出是通過添加前一層的特徵映射和 shortcut layer倒數第三層特徵映射得到的。

Upsample

[upsample]
stride=2

使用雙線性上採樣,將上一層的feature map按步幅stride=2向上採樣。

Route

[route]
layers = -4

[route]
layers = -1, 61

Route層需要說明一下。它有一個屬性層,可以有一個值,也可以有兩個值。

當layers屬性只有一個值時,它輸出該值索引層的特徵映射。在我們的例子中,它是-4,所以該層將會輸出Route層中倒數第四層的特徵映射。

當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

[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中還有另一種類型的塊叫做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,並將每個塊存儲爲一個字典,塊的屬性及其值作爲鍵-值對存儲在字典中。在解析cfg時,我們不斷地將這些由代碼中的變量block表示的dicts,添加到一個列表blocks中。我們的函數將返回這個block。

首先我們將cfg文件的內容保存在字符串列表中。下面的代碼對這個列表執行一些預處理。

file = open(cfgfile, 'r')
lines = file.read().split('\n')                        # 將行存儲在列表中
lines = [x for x in lines if len(x) > 0]               # 讀取空行 
lines = [x for x in lines if x[0] != '#']              # 刪除註釋
lines = [x.rstrip().lstrip() for x in lines]           # 刪除邊緣空白

然後,我們循環遍歷結果列表以獲取塊。

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)

return blocks

創建構建塊

現在我們將使用上面parse_cfg返回的列表,爲配置文件中顯示的塊構造PyTorch模塊。

我們在列表中有5種類型的層(上面提到過)。PyTorch爲convolutional 和upsample類型提供了預構建的層。我們必須通過擴展 nn.Module 類來爲其餘的層編寫自己的模塊。

create_modules函數接受parse_cfg函數返回的列表blocks。

def create_modules(blocks):
    net_info = blocks[0]     #獲取有關輸入和預處理的信息
    module_list = nn.ModuleList()
    prev_filters = 3
    output_filters = []

在遍歷塊列表之前,我們定義一個變量net_info來存儲關於網絡的信息。

nn.ModuleList

我們的函數將返回一個 nn.ModuleList。這個類非常像一個包含 nn.Module 的普通列表。但是,當我們將nn.ModuleList 作爲 nn.Module對象的成員進行添加時(即當我們將模塊添加到我們網絡時), nn.ModuleList 中 nn.Module 對象的所有參數,也被當作 nn.Module 對象的參數添加。(我們正在添加nn.ModuleList作爲成員的網絡 )。

當我們定義一個新的卷積層時,我們必須定義它的內核的維數。雖然內核的高度和寬度是由cfg文件提供的,但是內核的深度正是上一層中出現的過濾器的數量(或特徵映射的深度)。這意味着我們需要持續跟蹤卷積層所應用的過濾器數量。我們使用變量prev_filter來完成此操作。我們初始化爲3,因爲圖像有3個過濾器對應於RGB通道。

Route層從以前的層帶來(可能是連接的)特徵映射。如果在Route層的正前方有一個卷積層,那麼內核就會被應用到前一層的特徵映射上,也就是Route層所帶來的特徵映射上。因此,我們不僅需要跟蹤前一層中的過濾器數量,還需要跟蹤前一層中的每個過濾器。在迭代時,我們將每個塊的輸出過濾器數量添加到output_filters列表中。

現在,我們的想法是迭代塊列表,併爲每個塊創建一個PyTorch模塊。

    for index, x in enumerate(blocks[1:]):
        module = nn.Sequential()

        #檢查block類型
        #爲block創建一個新的模塊
        #添加到module_list

nn.Sequential 類用於按順序執行多個 nn.Module 對象。如果您查看cfg,就會發現一個塊可能包含多個層。例如, convolutional 類型的塊除了卷積層之外,還有批處理規範層和泄漏ReLU激活層。我們使用 nn.Sequential 將這些層串在一起,它是add_module函數。例如,下面是我們創建卷積層和upsample層的方法。

        if (x["type"] == "convolutional"):
            #獲取關於圖層的信息
            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)

            #檢查激活
            #對於YOLO來說,它要麼是Linear,要麼是Leaky ReLU
            if activation == "leaky":
                activn = nn.LeakyReLU(0.1, inplace = True)
                module.add_module("leaky_{0}".format(index), activn)

        #如果是向上採樣層
        #我們使用 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 Layers

接下來,我們編寫創建 Route 和 Shortcut的代碼。

        #如果是 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 對應跳轉鏈接
        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__()

Wait, an empty layer?

現在,空層可能看起來很奇怪,因爲它什麼都不做。Route層,就像任何其他層執行一個操作(前一個層/連接)。在PyTorch中,當我們定義一個新層時,我們子類化nn.Module ,並在nn.Moduleobject對象的foward函數中編寫該層執行的操作。

要爲Route塊設計一個層,我們必須構建一個nn.Module對象,該對象初始化時屬性層的值作爲它的成員。然後,我們可以在forward函數中編寫連接/提出feature map的代碼。最後,在網絡的forward函數中執行這一層。

但是,考慮到連接的代碼相當簡短(在feature map上調用torch.cat),按照上面的方法設計一個層將導致不必要的抽象。相反,我們所能做的是用一個虛擬層代替提議的route層,然後直接在表示darknet的nn.Module對象的forward函數中執行連接。(如果最後一行對您來說沒有太大意義,我建議您閱讀nn.Module類在PyTorch中是如何使用的。)

位於route層前面的卷積層將它的內核應用於(可能是連接的)來自前一層的feature map。下面的代碼更新 filters 變量,以保存路由層輸出的 filters 的數量。

if end < 0:
    #如果我們連接映射
    filters = output_filters[index + start] + output_filters[index + end]
else:
    filters= output_filters[index + start]

Shortcut層還使用空層,因爲它還執行一個非常簡單的操作(添加)。沒有必要更新filters變量,因爲它只是將前一層的feature map添加到後一層的feature map中。

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 ,它包含用於檢測邊界框的錨點。

檢測層定義如下:

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(
     )
   )
.
.
.

這部分就講到這裏。在下一部分中,我們將組裝已創建的構建塊,以從圖像生成輸出。

擴展閱讀

  1. PyTorch tutorial
  2. nn.Module, nn.Parameter classes
  3. nn.ModuleList and nn.Sequential
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章