专栏首页AI算法与图像处理手把手教你使用PyTorch从零实现YOLOv3--构建网络代码讲解(2)

手把手教你使用PyTorch从零实现YOLOv3--构建网络代码讲解(2)

来源:计算机视觉联盟

图片来源:Karol Majek

文章目录

  • 先决条件
  • 入门
  • 配置文件
    • 卷积
    • shortcut层
    • 上采样
    • 路线
    • YOLO
    • 网络
    • 解析配置文件
    • 创建构建基块
    • nn.ModuleList
    • Route Layer / Shortcut Layers
    • 等等,空层 ?
    • YOLO图层
    • 测试代码

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

本教程的代码旨在在Python 3.5和PyTorch 0.4上运行。可以在此Github(https://github.com/ayooshkathuria/YOLO_v3_tutorial_from_scratch)存储库中找到全部内容。

本教程之前的教程:

手把手教你使用PyTorch从零实现YOLOv3(1)

先决条件

  • 第1部分关于YOLO工作原理的部分
  • PyTorch的基本知识,包括如何创建自定义的架构nn.Module、nn.Sequential以及torch.nn.parameter类。

入门

首先创建一个目录,用来存储整个目标检测的代码。

然后,创建一个文件darknet.py。Darknet是YOLO基础架构的名称。该文件将包含创建YOLO网络的代码。我们将用一个名为util.py的文件来补充它,其包含各种辅助程序功能的代码。将这两个文件都保存在目标检测文件夹中。

配置文件

官方代码(用C语言编写)使用配置文件来构建网络。所述cfg文件描述了网络配置。如果您熟悉Caffe,则相当于 .protxt用来描述网络的文件。

我们将使用作者发布的官方cfg文件来构建我们的网络。从此处下载它(https://github.com/pjreddie/darknet/blob/master/cfg/yolov3.cfg),并将其放置在目标检测目录内的cfg文件夹中。如果您使用的是Linux,请cd进入您的网络目录并输入:

mkdir cfg
cd cfg
wgt 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层。快捷层是跳过连接,像在RESNET使用的一个。YOLO中使用了5种类型的图层:

卷积

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

shortcut层

[shortcut]
from=-3
activation=linear

shortctu层是跳过连接,类似于在RESNET使用的一个。from参数是-3,表示shortcut层的输出是通过从快捷图层向后添加上一个图层和第三个图层的特征地图来获得的。

上采样

[upsample]
stride=2

stride使用双线性上采样上一层中的特征图进行上采样

路线

[route]
layers = -4

[route]
layers = -1, 61

路由层值得解释。它具有一个属性图层,可以具有一个或两个值。

当layers属性只有一个值时,它将输出由该值索引的图层的特征图。在我们的示例中,它是-4,因此该层将从Route层向后从第4层输出特征图。

当图层具有两个值时,它将返回由其值索引的图层的级联特征图。在我们的示例中,其值为-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]
# 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 = []

在遍历块列表之前,我们定义一个变量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通道相对应的滤镜。

路线层会带来(可能是串联的)前一层的特征图。如果在路由层的前面有一个卷积层,则将内核应用到先前层的特征图上,恰好是路由层带来的特征图。因此,我们不仅需要跟踪上一层的过滤器数量,还需要跟踪前面各层的过滤器数量。进行迭代时,我们将每个块的输出过滤器数量附加到列表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 Layer / Shortcut Layers

接下来,我们编写用于创建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)

首先,我们提取layers属性的值,将其转换为整数并存储在列表中。

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

route = EmptyLayer()

定义为:

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

等等,空层 ?

现在,鉴于空层什么也不做,因此它似乎很奇怪。与其他任何层一样,路由层也执行操作(前一层/并置)。在PyTorch中,当我们定义一个新层时,我们将子类化nn.Module并编写该层在对象forward功能中执行的操作nn.Module。

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

但是,由于级联代码相当短且简单(调用torch.cat特征图),因此,如上所述设计图层将导致不必要的抽象,从而只会增加样板代码。取而代之的是,我们可以做的是将虚拟层代替拟议的路由层,然后直接在表示暗网forward的nn.Module对象的功能中执行串联。(如果最后一行对您没有多大意义,建议您阅读nn.ModulePyTorch中类的用法。底部的链接)

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

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

快捷方式层还利用了一个空层,因为它还执行了非常简单的操作(添加)。无需更新update 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

在循环的最后,我们进行一些簿记。

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

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

https://pjreddie.com/media/files/papers/YOLOv3.pdf

参考文献:

how-to-implement-a-yolo-object-detector-in-pytorch

本文分享自微信公众号 - AI算法与图像处理(AI_study)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-11-15

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • OpenCV黑魔法之隐身衣 | 附源码

    我计划分享一些有趣的实战项目,或许达不到商用的级别,但是希望能在大家做项目的时候能够提供一些思路!如果对你有所帮助,给我点赞 & 在看,让我知道对你有帮助哈!

    AI算法与图像处理
  • 综述:当医学影像遇上深度学习

    在传统医疗领域,医院内每日的医学影像数据量巨大,影像科医生做着大量重复性和机械性的工作。每张片子都需要医生仔细筛查和甄别,耗费了大量的精力,同时过于机械和重复性...

    AI算法与图像处理
  • OpenCV4.4 CUDA编译与加速全解析

    OpenCV4.4中关于CUDA加速的内容主要有两个部分,第一部分是之前OpenCV支持的图像处理与对象检测传统算法的CUDA加速;第二部分是OpenCV4.2...

    AI算法与图像处理
  • python的Pattern模块

    Pattern is a web mining module for the Python programming language.

    py3study
  • Python import 与 __all__

    使用from <module> import *导入模块时,若显式定义了__all__,则只导入__all__中的name,否则会导入除以下划线开头的所有nam...

    雪飞鸿
  • SELECT from world | SQL刷题

    Observe the result of running this SQL command to show the name, continent and p...

    数据医生
  • SAP S/4HANA生产订单状态含义

    版权声明:本文为博主汪子熙原创文章,未经博主允许不得转载。 https://jerry.bl...

    Jerry Wang
  • HBase启动错误client.ConnectionManager$HConnectionImplementation的解决办法

    有时候,HBase因为在写入过程中直接强行中断之后,再次重启过程中,会经常出现一些异常信息。其中标题这个错误也是经常碰到的一个。 具体报的错误如下: ? 201...

    sparkexpert
  • Hadoop(十五)MapReduce程序实例

    一、统计好友对数(去重) 1.1、数据准备 joe, jon joe , kia joe, bob joe ,ali kia, ...

    用户1195962
  • Hadoop(十五)MapReduce程序实例

        从上面的文件格式与内容,有可能是出现用户名和好友名交换位置的两组数据,这时候这就要去重了。

    大道七哥

扫码关注云+社区

领取腾讯云代金券