Part 2:创建YOLO网络层

翻译原文:https://blog.paperspace.com/how-to-implement-a-yolo-v3-object-detector-from-scratch-in-pytorch-part-2/

本篇文章是《如何使用PyTorch从零开始实现YOLO(v3)目标检测算法》的第二部分。这系列论文一共有五篇文章,五篇文章的主要内容在下文中有涉及。如果有问题欢迎和我交流~


如何使用PyTorch从零开始实现YOLO(v3)目标检测算法?

1 开始

首先我们创建一个检测器代码所在的目录,然后创建一个darknet.py文件。Darknet是YOLO算法架构的名字。这个文件将会包含创建YOLO网络架构的代码。我们将会创建util.py文件对其进行补充,在util.py中将会包括各种各样的辅助函数的代码。将这些文件保存在你的检测器文件夹下,你可以使用git命令去追踪代码的变化轨迹。

2 配置文件

官网代码(使用C来编写的)使用配置文件来创建一个网络。这个cfg文件,一个模块接着一个模块的去描述了网络架构的布局。我们会使用作者发布的官方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个网络模块(3个卷积+1个捷径层)。三个卷积层后面跟着一个捷径层。捷径层是一个跳跃连接层,就像是在ResNet中使用的那样。在YOLO中使用五种不同的网络层:

2.1 卷积层

[convolutional]
batch_normalize=1  
filters=64  
size=3  
stride=1  
pad=1  
activation=leaky

2.2 捷径层

它就是一个容器,相加之后,然后直接作为下一层的输入

[shortcut]
from=-3  
activation=linear  

捷径层是一个跳跃连接,类似在ResNet中使用的那种。参数from-3,这意味着捷径层的输出是捷径层的前一层和往前数第三层的特征图相加得来的(Resnet网络中,没有单独设置一个层,而是在forward函数中,对特征图进行的相加,在YOLO中,直接设置了一个网络层)

2.3 上采样层

[upsample]
stride=2

使用步长和双线性上采样的方法,对上一层的特征图进行上采样。

2.4 路由层

[route]
layers = -4

[route]
layers = -1, 61

这个route层需要解释一下。它有一个layers的属性,这个属性可以有一个值,也可以有两个值。当layer只有一个值的时候,它输出通过这个值所索引到层的特征图。在我们的案例中,这个值是-4,因此route层就会输出从route层向后数第四个层的特征图。当layer有两个值的时候,他就会返回通过该值所索引到的网络层的特征图的结合。在我们的例子中它是-1,61,route层会输出上一层和第61层的特征图,并且沿着深度进行连接。

2.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,这意味着第一个,第二个和第三个锚框将会被使用。这是有意义的,这是因为每一个单元只能够预测三个边界框。总而言之,我们的检测层有三个尺度,加起来一共就是9个锚框。。

2.6 网络架构描述

[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

3 解析(parse)配置文件

最终目的:将每个模块以字典的形式保存在在列表中

[{'type': 'convolutional','batch_normalize': '1','filters': '32', 'size': '3','stride': '1','pad': '1','activation': 'leaky'},
 
{'type': 'convolutional','batch_normalize': '1','filters': '64','size': '3','stride': '2','pad': '1', 'activation': 'leaky'},]

在开始之前,我们需要再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文件,并且把每一个模块当做“词典”存储。模块的属性和值以键值对的形式在词典中存储。在我们解析的过程中,我们持续向列表blocks中添加词典,这些词典在我们的代码中用变量block所表示。我们的函数将会返回这个模块。我们首先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


# 这边我是使用with打开文件的,这是因为with可以自动关闭文件,不需要我们手动关闭
# with open(cfgfile) as file:

 之后,我们循环结果列表来得到模块

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

# 这边提供一个新思路:直接将所有文件都保存在blocks中,然后pop首个元素(空元素)
# 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)
# blocks.pop(0)

4 创建构造模块

最终目的:就是将所有的网络模块保存在ModuleList实例对象module_list中      

实现思路:

(1)遍历blocks中保存的所有模块

(2) 将五个网络层:卷积层,上采样层,捷径层,route层和yolo层保存在nn.Sequential实例对象中,更新输出channel

(3)将nn.Sequential实例对象保存在nn.ModuleList实例对象中

注意:在PyTorch中,我们不用担心数据的宽和高,我们只需要指定input_channel和output_channel。由此可见输入输出channel的作用不言而喻,因此在定义卷积层的时候,我们需要明确上一层输出的output_channel是多少。

现在我们打算使用parse_cfg函数返回列表去为配置文件提供的网络块构造PyTorch模块。在列表中有五中类型的网络层(上面到的)。PyTorch为卷积层上采样层提供了预制的网络层。通过添加类,我们将会为剩下的层编写我们自己的模块。这个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,这是因为YOLO是FPN网络进行多尺度预测
    # yolo层的输出channel并不一定是下一层输入的channel
    # 所以才有route层
    output_filters = []

在我们迭代blocks的列表之前,我们会定义一个变量net_info去存储网络相关的信息。

4.1 nn.ModuleList

      我们的函数会返回nn.ModuleList。这个类和包含nn.Module实例对象的普通列表很像。当我们把nn.ModuleList作为nn.Module实例对象的成员添加的时候(也就是我们把模块添加到我们的网络时),网络也会以nn.Module实例对象参数(parameters)的形式添加nn.ModuleList模块内的nn.Module实例对象的所有参数。(也就是我们的网络,我们把nn.ModuleList作为成员添加的网络)。当我们定义一个新的卷积层的时候,我们必须定义掩膜的维度。尽管掩膜的宽度和高度已经由配置文件提供,但是掩膜的深度恰好是在上一个神经层出现(present)的卷积核的数量(或者是特征图的深度)。这意味着我们必须要追踪应用卷积层的网络层的卷积核的数量,我们使用变量prev_filter来做到这一点,我们初始化prev_filter为3,因为刚开始的时候RGB对应图像三个过滤器(前一个网络层的输出特征图的channel就是下一个网络层的输入的channel)。

      路由层会从以前的层带来(可能连接的)特征图。如果卷积层恰好在路由层的前面,然后卷积核就会使用在之前卷积层的特征图上,准确来说是路由层带来的特征图。因此,我们不光需要追踪之前层的,而且还要追踪后面网络层的卷积核的数量。当我们进行迭代的时候,我们需要将每个模块的输出卷积核的个数添加到列表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.Mudule类的实例对象。如果你观察cfg文件,你会发现一个网络块中包含不止一个网络层。比如,卷积网络块中处理包含卷积层之外,还包含批归一化层和leaky ReLU激活函数。我们使用nn.Sequential和它的add_module函数来把这些网络层串联起来。

      我们需要给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)

4.2 路由层/捷径层

      接下来,我们将编写用于创建路由层和捷径层的代码。

      如果路由层有两个参数,表示着两个特征图的通道数要进行相加(所以需要记录每个层的通道数

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

创建路由层的代码需要解释一下。首先,我们需要提取layers属性的值,把它转换成(cast it into)整型并且保存在一个列表中(因为该层的数据都是保存的词典中的,因此我们需要将数据保存在列表中)。之后,我们有一个叫做EmptyLayer的网络层,顾名思义,就是个空的网络层。

route = EmptyLayer()

它的定义如下

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

  等等,一个新的层?现在,一个空的层可能看起来有点奇怪,因为它什么也不做。就像其他任何层一样,路由层执行操作(带来前面的层/进行连接)。在PyTorch中,当我们定义一个新的层的时候,我们需要继承nn.Module类,在nn.Module实例对象的forward函数中编写这个层执行的操作。

因为要为路由层设计一个网络层,我们将会构建nn.Module的实例对象,该对象初始化的时候使用属性layer的值来作为他的成员。然后,我们可以在forward函数中编写代码去连接/带来前面的网络层。最后,我们在我们网络的forward函数中执行这个网络层(简而言之,在PyTorch中,网络的定义和数据的执行时分开编写的,当网络已经定义好之后,数据就会在层与层之间进行处理)。

但是如果(given)连接的代码非常简短(在特征图中调用torch.cat函数),设计上面的网络层就会导致不必要的抽象,它会增加模板化代码。相反,我们可以做的就是使用一个虚拟层代替设计路由层,然后再在表示darknet网络的nn.Module的实例对象的forward函数中直接进行连接操作。(如果你不太理解最后一行的意思,我建议你读一下PyTorch中如何使用nn.Module类的文档)

就在路由层前面的卷积层对从前面网络层带来的(可能连接的)特征图进行掩膜运算。下面的代码更新变量filters,用来保存由路由层输出的卷积核的数量。

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

      捷径层也使用了一个空的网路层,因为它也进行了一个非常简单的操作(加法)。这里不需要去更新变量filter,因为它仅仅是将前面网络层的特征图加到了后面的这些层。

5 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

在循环的最后,我们做一些记录

        module_list.append(module)
        prev_filters = filters
        output_filters.append(filters)

 这就结束了循环的主体。在create_modules函数的最后,我们会返回一个包含net_info和module_list的元组

return (net_info, module_list)

6 测试代码

你可以通过在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(
     )
   )
.
.
.

这就是这个部分的全部内容。在下一个部分,我们就会组装我们创建的用于从图片生成输出(produce output from an image)的构造块。

7 总结

这篇博客主要介绍了两个内容:读取配置文件内容;根据配置文件构建自己的网络架构模型

(1)读取配置文件内容:

  • 使用with打开文件、读取文件
  • 将各个网络模型按照字典的形式保存在元组中

(2)将各个网络模型保存在ModuleList中

  • 将各个网络模块先保存在nn.Sequential中
  • 然后将nn.Sequential保存在ModuleList中
  • 更新上个网络层的输出channel
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章