来源:计算机视觉联盟
图片来源:Karol Majek
文章目录
这是从l零开始实现YOLOv3目标检测教程的第2部分。在上一部分中,解释了YOLO的工作原理,在这一部分中,我们将在PyTorch中实现YOLO所使用的层。换句话说,这是我们创建模型构建模块的部分。
本教程的代码旨在在Python 3.5和PyTorch 0.4上运行。可以在此Github(https://github.com/ayooshkathuria/YOLO_v3_tutorial_from_scratch)存储库中找到全部内容。
本教程之前的教程:
首先创建一个目录,用来存储整个目标检测的代码。
然后,创建一个文件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]
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和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