深度學習教程:從頭開始用 PyTorch 實現 YOLO (v3) 教程(二)

以下是從頭實現 YOLO v3 檢測器的第二部分教程,我們將基於前面所述的基本概念使用 PyTorch 實現 YOLO 的層級,即創建整個模型的基本構建塊。

深度學習教程:從頭開始用 PyTorch 實現 YOLO (v3) 教程(二)

這一部分要求讀者已經基本瞭解 YOLO 的運行方式和原理,以及關於 PyTorch 的基本知識,例如如何通過 nn.Module、nn.Sequential 和 torch.nn.parameter 等類來構建自定義的神經網絡架構。若您對PyTorch還不是太熟悉或還未入門,這裡推薦一本簡單易懂PyTorch的學習教程,比較適合作為PyTorch的入門教程,以下是淘寶購買鏈接, 歡迎大家點擊下單.

本教程分為5個部分:

  • 第1部分:理解 YOLO 的原理
  • 第2部分(本文):創建網絡結構
  • 第3部分:實現網絡的前向傳遞
  • 第4部分:目標分閾值和非極大值抑制
  • 第5部分:網絡的輸入和輸出

開始

首先創建一個存放檢測器代碼的文件夾,然後再創建 Python 文件 darknet.py。Darknet 是構建 YOLO 底層架構的環境,這個文件將包含實現 YOLO 網絡的所有代碼。同樣我們還需要補充一個名為 util.py 的文件,它會包含多種需要調用的函數。在將所有這些文件保存在檢測器文件夾下後,我們就能使用 git 追蹤它們的改變。

配置文件

官方代碼(authored in C)使用一個配置文件來構建網絡,即 cfg 文件一塊塊地描述了網絡架構。如果你使用過 caffe 後端,那麼它就相當於描述網絡的.protxt 文件。

我們將使用官方的 cfg 文件構建網絡,它是由 YOLO 的作者發佈的。我們可以在以下地址下載,並將其放在檢測器目錄下的 cfg 文件夾下。

配置文件下載:https://github.com/pjreddie/darknet/blob/master/cfg/yolov3.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

我們看到上面有四塊配置,其中 3 個描述了卷積層,最後描述了 ResNet 中常用的捷徑層或跳過連接。下面是 YOLO 中使用的 5 種層級:

1. 卷積層

[convolutional]

batch_normalize=1

filters=64

size=3

stride=1

pad=1

activation=leaky

2. 跳過連接

[shortcut]

from=-3

activation=linear

跳過連接與殘差網絡中使用的結構相似,參數 from 為-3 表示捷徑層的輸出會通過將之前層的和之前第三個層的輸出的特徵圖與模塊的輸入相加而得出。

3.上採樣

[upsample]

stride=2

通過參數 stride 在前面層級中雙線性上採樣特徵圖。

4.路由層(Route)

[route]

layers = -4

[route]

layers = -1, 61

路由層需要一些解釋,它的參數 layers 有一個或兩個值。當只有一個值時,它輸出這一層通過該值索引的特徵圖。在我們的實驗中設置為了-4,所以層級將輸出路由層之前第四個層的特徵圖。

當層級有兩個值時,它將返回由這兩個值索引的拼接特徵圖。在我們的實驗中為-1 和 61,因此該層級將輸出從前一層級(-1)到第 61 層的特徵圖,並將它們按深度拼接。

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 表示了第一個、第二個和第三個使用的錨點。而掩碼錶示檢測層中的每一個單元預測三個框。總而言之,我們檢測層的規模為 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,不過我不認為它是層,因為它只描述網絡輸入和訓練參數的相關信息,並未用於 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 表示)添加到列表 blocks 中。我們的函數將返回該 block。

我們首先將配置文件內容保存在字符串列表中。下面的代碼對該列表執行預處理:

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 函數使用 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 = []

在迭代該列表之前,我們先定義變量 net_info,來存儲該網絡的信息。

nn.ModuleList

我們的函數將會返回一個 nn.ModuleList。這個類幾乎等同於一個包含 nn.Module 對象的普通列表。然而,當添加 nn.ModuleList 作為 nn.Module 對象的一個成員時(即當我們添加模塊到我們的網絡時),所有 nn.ModuleList 內部的 nn.Module 對象(模塊)的 parameter 也被添加作為 nn.Module 對象(即我們的網絡,添加 nn.ModuleList 作為其成員)的 parameter。

當我們定義一個新的卷積層時,我們必須定義它的卷積核維度。雖然卷積核的高度和寬度由 cfg 文件提供,但卷積核的深度是由上一層的卷積核數量(或特徵圖深度)決定的。這意味著我們需要持續追蹤被應用卷積層的卷積核數量。我們使用變量 prev_filter 來做這件事。我們將其初始化為 3,因為圖像有對應 RGB 通道的 3 個通道。

路由層(route layer)從前面層得到特徵圖(可能是拼接的)。如果在路由層之後有一個卷積層,那麼卷積核將被應用到前面層的特徵圖上,精確來說是路由層得到的特徵圖。因此,我們不僅需要追蹤前一層的卷積核數量,還需要追蹤之前每個層。隨著不斷地迭代,我們將每個模塊的輸出卷積核數量添加到 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.Module 對象的一個數字。如果你查看 cfg 文件,你會發現,一個模塊可能包含多於一個層。例如,一個 convolutional 類型的模塊有一個批量歸一化層、一個 leaky 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 Layer)和捷徑層(Shortcut Layer)的代碼:

#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)

創建路由層的代碼需要做一些解釋。首先,我們提取關於層屬性的值,將其表示為一個整數,並保存在一個列表中。

然後我們得到一個新的稱為 EmptyLayer 的層,顧名思義,就是空的層。

route = EmptyLayer()

其定義如下:

class EmptyLayer(nn.Module):

def __init__(self):

super(EmptyLayer, self).__init__()

等等,一個空的層?

現在,一個空的層可能會令人困惑,因為它沒有做任何事情。而 Route Layer 正如其它層將執行某種操作(獲取之前層的拼接)。在 PyTorch 中,當我們定義了一個新的層,我們在子類 nn.Module 中寫入層在 nn.Module 對象的 forward 函數的運算。

對於在 Route 模塊中設計一個層,我們必須建立一個 nn.Module 對象,其作為 layers 的成員被初始化。然後,我們可以寫下代碼,將 forward 函數中的特徵圖拼接起來並向前饋送。最後,我們執行網絡的某個 forward 函數的這個層。

但拼接操作的代碼相當地短和簡單(在特徵圖上調用 torch.cat),像上述過程那樣設計一個層將導致不必要的抽象,增加樣板代碼。取而代之,我們可以將一個假的層置於之前提出的路由層的位置上,然後直接在代表 darknet 的 nn.Module 對象的 forward 函數中執行拼接運算。(如果感到困惑,我建議你讀一下 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

在這個迴路結束時,我們做了一些統計(bookkeeping.)。

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(

)

)


分享到:


相關文章: