手把手实现YOLOv3(二)

如何在PyTorch中从零开始实现YOLO(v3)对象检测器:第2部分

前言

本文翻译总结于scratch

目标检测是一个很古老的视觉问题,和其他视觉问题一样,它也从深度学习的发展中受益匪浅。
近年来,人们开发了许多用于对象检测的算法,其中包括YOLO,SSD,Mask RCNN和RetinaNet。

目录


本系列教程的第2部分介绍了如何在PyTorch中从头开始实现自己的YOLO v3对象检测器。

这是从头开始实现YOLO v3检测器的教程的第2部分。 在上一部分中,我解释了YOLO的工作原理,在这一部分中,我们将在PyTorch中实现YOLO所使用的层。 换句话说,这是我们创建模型构建模块的部分。

本教程的代码旨在在Python 3.5和PyTorch 0.4上运行。 可以在此Github存储库中找到全部内容。

本教程分为5部分:在PyTorch中从头开始自己的YOLO v3对象检测器。
第1部分:了解YOLO的工作方式
第2部分(这一部分):创建网络体系结构的各层

第三部分:实现网络的前向传递
第4部分:客观置信度阈值和非最大抑制
第5部分:设计输入和输出管道

先决条件

教程/关于YOLO工作原理的第1部分。
PyTorch的基本工作知识,包括如何使用nn.Module,nn.Sequential和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

我们将使用作者发布的官方cfg文件来构建我们的网络。 从这里下载它,并将其放在检测器目录内的cfg文件夹中。 如果您使用的是Linux,则cd进入您的网络目录并键入:

我们在上方看到4个区块。 其中3个描述卷积层,然后是一个快捷层。 快捷方式层是一个跳过连接,就像ResNet中使用的那样。 YOLO中使用了5种类型的图层:

卷积

[convolutional]
batch_normalize=1  
filters=64  
size=3  
stride=1  
pad=1  
activation=leaky
Shortcut
[shortcut]
from=-3  
activation=linear  

快捷方式层是跳过连接,类似于ResNet中使用的连接。
from参数为-3,这意味着快捷方式层的输出是通过从快捷方式层向后添加前一层和第三层的特征图获得的。
官方cfg文件,由作者发布以构建我们的网络。

从此处下载它,并将其放在yourdetector目录内名为cfg的文件夹中。
如果您使用的是Linux,则cd进入您的网络目录并键入:

上采样

[upsample]
upsample=2

使用双线性上采样以步长为单位对上一层中的特征图进行上采样。

路线

[route]
layers = -4

[route]
layers = -1, 61

路由层值得解释。 它是特征图的属性,该图层可以具有一个或两个值。
当layers属性只有一个值时,它将输出由该值索引的图层的特征图。
在我们的示例中,它是-4,因此该层将从第4层向Route层向后输出特征地图。当图层具有两个值时,它将返回由其值索引的图层的级联特征图。
在我们的示例中,其值为-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部分中描述的检测层。

锚点描述了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,并将每个块存储为dict。
块的属性及其值作为键值对存储在字典中。
在解析cfg时,我们会将这些指令(由代码中的变量块表示)追加到列表块中。
我们的函数将返回此块。

我们首先将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

然后,我们遍历结果列表以获取块。

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为卷积和上采样类型提供了预构建的层。 我们必须通过扩展nn.Module类为其余各层编写自己的模块。

create_modules函数采用parse_cfg函数返回的列表块。

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 = []
nn.ModuleList

我们的函数将返回nn.ModuleList。 这个类几乎就像一个包含nn.Module对象的普通列表。 但是,当我们将nn.ModuleList添加为nn.Module对象的成员时(即,当我们向网络添加模块时),则nn.ModuleList中的nn.Module对象(模块)的所有参数都将作为 nn.Module对象(即我们的网络,我们将nn.ModuleList添加为其成员)。

当我们定义一个新的卷积层时,我们必须定义它的内核尺寸。尽管cfg文件提供了内核的高度和宽度,但内核的深度恰好是上一层中存在的过滤器的数量(或特征图的深度)。这意味着我们需要跟踪应用卷积层的层中滤波器的数量。我们使用变量prev_filter来做到这一点。我们将其初始化为3,因为图像具有3个与RGB通道相对应的滤镜。

路线层会带来(可能是串联的)前一层的要素地图。如果在路由层的前面有一个卷积层,则将内核应用于先前各层的特征图,确切地说就是路由层带来的特征图。因此,我们不仅需要跟踪上一层的过滤器数量,还需要跟踪前面各层的过滤器数量。进行迭代时,我们将每个块的输出过滤器数量追加到列表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,您将意识到一个块可能包含多个层。 例如,卷积类型的块除具有卷积层外,还具有批处理规范层以及泄漏的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和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)

用于创建路由层的代码值得一提。
首先,我们提取thelayers属性的值,将其转换为整数并将其存储在列表中。

然后,我们有了一个名为EmptyLayer的新层,顾名思义,它只是一个空层。

route = EmptyLayer()

它被定义为

class EmptyLayer(nn.Module):
    def __init__(self):
        super(EmptyLayer, self).__init__()

等一下,一个空的图层?

现在,鉴于空层什么也不做,因此看起来很奇怪。
路由层,就像其他任何层一样,执行操作(前一层/并置)。
在PyTorch中,当我们定义一个新层时,我们将nn.Module子类化,并在nn.Module对象的正向函数中编写该层执行的操作。

为了为Route块设计层,我们将必须构建一个nn.Module对象,该对象使用属性层的值作为其成员进行初始化。
然后,我们可以编写代码以在转发函数中串联/转发特征图。
最后,我们在网络的转发功能中执行该层。

但是,由于串联代码相当短而简单(在特征图上调用torch.cat),因此如上所述设计层将导致不必要的抽象,从而只会增加样板代码。取而代之的是,我们可以做的是在拟议的路由层中放置一个虚拟层,然后直接在代表Darknet的nn.Module对象的正向函数中执行串联。 (如果最后一行对您没有太大意义,建议您阅读PyTorch中如何使用nn.Module类。底部的链接)

路径层前面的卷积层将其内核应用于(可能是串联的)前一层的要素地图。以下代码更新了filter变量,以保存路由层输出的过滤器数量。

if end < 0:
    #If we are concatenating maps
    filters = output_filters[index + start] + output_filters[index + end]
else:
    filters= output_filters[index + start]

快捷方式层还利用了一个空层,因为它还执行了一个非常简单的操作(添加)。
无需更新update过滤器变量,因为它仅将前一层的特征映射添加到后一层的特征映射。

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)

我们定义了一个新层“检测层”,其中包含用于检测边界框的锚点。

检测层定义为

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

这部分就是这样。
下一部分中,我们将组装已创建的构造块以从图像生成输出。

进一步阅读

 PyTorch教程
 nn.Module,nn.Parameter类
 nn.ModuleList和nn.Sequential
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页