Pytorch從0開始實現YOLO V3指南 part2——搭建網絡結構層


本節翻譯自:https://blog.paperspace.com/how-to-implement-a-yolo-v3-object-detector-from-scratch-in-pytorch-part-2/

 

必備條件:

  1. 此教程part1-YOLO的工作原理
  2. 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

  1. PyTorch tutorial
  2. nn.Module, nn.Parameter classes
  3. nn.ModuleList and nn.Sequential

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM