参考:https://blog.paperspace.com/how-to-implement-a-yolo-v3-object-detector-from-scratch-in-pytorch-part-2/
图片来源:Karol Majek。在这里查看他的YOLO v3实时检测视频
这是从头开始实现YOLO v3探测器的教程的第2部分。在最后一部分中,我解释了YOLO是如何工作的,在这部分中,我们将在PyTorch中实现YOLO使用的层。换句话说,这是我们创建模型构建块的部分。
本教程的代码旨在在Python 3.5和PyTorch 0.4上运行。它可以在这个Github回购中找到它的全部内容。
本教程分为5个部分:
第1部分:了解YOLO的工作原理
第2部分(本文):创建网络体系结构的各个层
第3部分:实现网络的正向传递
第4部分:对象置信度阈值和非最大抑制
第5部分:设计输入和输出管道
先决条件
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
我们看到上面有4个街区。其中3个描述卷积层,然后是快捷层。甲快捷层是跳过连接,像在RESNET使用的一个。YOLO中使用了5种类型的图层:
卷积
[convolutional]
batch_normalize=1
filters=64
size=3
stride=1
pad=1
activation=leaky
捷径
[shortcut]
from=-3
activation=linear
甲快捷层是跳过连接,类似于在RESNET使用的一个。的from
参数是-3
,这意味着在快捷层的输出由获得加入来自先前和从向后的第三层特征映射快捷层。
上采样
[upsample]
stride=2
通过stride
使用双线性上采样的因子对上一层中的要素图进行采样。
路线
[route]
layers = -4
[route]
layers = -1, 61
该航线层应该得到一些解释。它有一个属性layers
,可以有一个或两个值。
当layers
属性只有一个值时,它会输出由该值索引的图层的要素图。在我们的示例中,它是-4,因此图层将从Route图层向后输出第4层的要素图。
当layers
有两个值时,它返回由它的值索引的层的连接特征映射。在我们的例子中,它是-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
net
在cfg中调用了另一种类型的块,但我不会将其称为层,因为它仅描述有关网络输入和训练参数的信息。它不用于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时,我们会继续将这些由block
代码中的变量表示的dicts附加到列表中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
然后,我们遍历结果列表以获取块。
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
函数获取函数blocks
返回的列表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
对象(即,当我们增加模块,我们的网络)中,所有的parameter
第nn.Module
对象(模块)内的nn.ModuleList
被添加为parameter
所述第nn.Module
对象(即我们的网络,这是我们所添加的nn.ModuleList
作为也是一个成员。
当我们定义一个新的卷积层时,我们必须定义它的内核的维度。虽然内核的高度和宽度由cfg文件提供,但内核的深度恰好是前一层中存在的过滤器数量(或要素图的深度)。这意味着我们需要跟踪应用卷积层的层中的滤波器数量。我们使用变量prev_filter
来做到这一点。我们将其初始化为3,因为图像具有对应于RGB通道的3个滤波器。
路径图层从先前的图层中提取(可能是连接的)要素图。如果在路径图层前面有一个卷积层,则内核将应用于先前图层的要素图,恰好是路径图层带来的图层。因此,我们需要保持在不仅先前层的过滤器的数量的轨道,但每个 前述层中的一个。在迭代时,我们将每个块的输出过滤器的数量附加到列表中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
class用于顺序执行多个nn.Module
对象。如果你看一下cfg,你会发现一个块可能包含多个层。例如,convolutional
除了卷积层之外,类型块还具有批量范数层以及泄漏的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 Layers 的代码。
#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.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
在循环结束时,我们会做一些簿记。
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(
)
)
.
.
.
就是这部分。在下一部分中,我们将组装我们创建的构建块以生成图像的输出。
进一步阅读
Ayoosh Kathuria目前是印度国防研究与发展组织的实习生,他正致力于改善粒状视频中的物体检测。当他不工作时,他正在睡觉或者在他的吉他上玩粉红色弗洛伊德。您可以在LinkedIn上与他联系,或者查看他在GitHub上做的更多内容