对Ayoosh Kathuria的YOLOv3实现进行翻译和总结,原文链接如下:
https://blog.paperspace.com/how-to-implement-a-yolo-v3-object-detector-from-scratch-in-pytorch-part-2/
*首先翻译遵循不删不改的原则有一说一,对容易起到歧义的中文采取保留英文的方式。其中对原文没有删减但是略有扩充,其中某些阐释是我一句话的总结,如有错误请大家在留言区指出扶正。
这是从头开始实现YOLO v3检测器教程的第2部分。 在上一部分中,我解释了YOLO的工作原理,在这一部分中,我们将在PyTorch中实现YOLO所使用的层。 换句话说,这是我们创建构建模型所需模块的部分。
本教程的代码在Python 3.5和PyTorch 0.4上运行。在这个Github repo中可以完整地找到它。
本教程分为5个部分:
Part 1 : Understanding How YOLO works
Part 2 (This one): Creating the layers of the network architecture
Part 3 : Implementing the forward pass of the network
Part 4 : Objectness score thresholding and Non-maximum suppression
Part 5 : Designing the input and the output pipelines
1.先决条件
- 关于YOLO工作原理和教程的第一部分
- PyTorch的基本知识,包括如何使用nn.Module,nn.Sequential和torch.nn.parameter类创建自定义架构。
我认为您之前曾经有过PyTorch的经验。 如果您只是刚开始,我建议您在返回本文之前先稍微熟悉一下框架。
2.开始吧
首先创建一个目录,其中将存有检测器代码。
然后,创建一个文件darknet.py。 Darknet是YOLO基础架构的名称。 该文件将包含创建YOLO网络的代码。 我们将用一个名为util.py的文件来补充它,该文件将包含各种助于函数的代码。 将这两个文件都保存在检测器文件夹中。 您可以使用git跟踪更改。
3.配置文件
官方代码(用C语言编写)使用配置文件来构建网络。 cfg文件逐块描述网络的布局。 如果您使用过Caffe,则相当于用于描述网络的.protxt文件。
我们将使用作者发布的官方cfg文件来构建我们的网络。 从此处下载它,并将其放置在检测器目录内名为cfg的文件夹中。 如果您使用的是Linux,则cd进入您的网络目录并输入:
注意:原文给出的wget下载方法连接失败,所以直接从git上找到那个文件下载就行,或者直接克隆作者的darknet项目到本地,这里我们采用克隆整个项目的方法:
cd /你的本地文件夹目录
git init
git clone https://github.com/pjreddie/darknet.git
如果打开配置文件,你将会看到类似的内容。
我们在上方看到4个模块。 其中3个描述卷积层,然后是一个短跳层。 短跳层是一个跳远连接,就像ResNet中使用的那样。 YOLO中使用了5种类型的图层:
3.1 Convolutional
3.2 Shortcut
短跳层是一个跳远连接,类似于ResNet中使用的那种。 from参数为-3,这意味着短跳层的输出是通过短跳层向后添加往前数第三层的特征图而获得的。
3.3 Upsample
使用双线性上采样以步长为单位对上一层中的特征图进行上采样。(如果你接触过图像处理,翻译过来就是以步长为单位使用双线性差值对图像等比例扩大)
3.4 Route
路径层值得解释。 它具有一个属性层,可以有一个或两个值。
当路径层属性只有一个值时,它将输出由该值索引的图层的特征图。 在我们的示例中,它是-4,因此该层将从Route层往前数的第4层输出特征图。
当路径层属性有两个值时,它将返回由其值索引的图层的级联特征图。 在我们的示例中,其值为-1到61,所以该图层将输出沿深度连接的前一层和第61层的特征图。
3.5 YOLO
YOLO层对应于第1部分中描述的检测层。锚盒描述了9个锚点,但是仅使用由mask属性索引的锚点。 在这里,mask的值为0,1,2,这意味着使用了第一,第二和第三个锚盒。 这是有道理的,因为检测层的每个细胞单元格预测3个物体。 总共,我们在3个尺度上具有检测层,总共构成9个锚盒。
3.6 Net
cfg中还有另一种称为net的块,但我不会将其称为一层,因为它仅描述有关网络输入和训练参数的信息。 YOLO的前向传播中未使用它。 但是,它确实为我们提供了诸如网络输入大小之类的信息,我们可以使用这些信息来调整前向传播中的锚盒。
4 Parsing the configuration file
在开始之前,请在darknet.py文件顶部添加必要的导入。
注意,作者使用了传统的IDE环境分文件编写项目。但是从这里开始为了更直观的完成实验,我将使用jupyter notebook一键式完成网络,不使用.py的方式按照不同的模块分文件完成程序。(换句话说写算法还是面向过程编程舒服)
import torch import torch.nn as nn import torch.nn.functional as F from torch.autograd import Variable import numpy as np
作者在这里原本使用了from __future__ import division因为在以前的版本中3/4的结果是0 引入这句话之后就是0.75 但是我使用的3.7.3默认/就是精确除法,故不用加载精确除法
我们定义了一个名为parse_cfg的函数,该函数将配置文件的路径作为输入。
这里的想法是解析cfg文件,并将每个块存储为字典。 块的属性及其值作为键值对存储在字典中。 在解析cfg时,我们会将这些代码块按照列表的形式附加到字典中。 我们的函数将返回此模块。
我们首先将cfg文件的内容保存在字符串列表中,然后对字符串列表进行解析。 以下代码对此列表执行预处理过程。
def parse_cfg(cfgfile): file = open(cfgfile, 'r') lines = file.read().split(' ') # store the lines in a list lines = [x for x in lines if len(x) > 0] # get rid 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
上述函数通过解析cfg原文件中的网络模块,将其一一抽取为由字典存储的键值对,如第二个卷积层模块为:
5 Creating the building blocks
现在,我们将使用上面loadCfg函数返回的列表将配置文件中存在的模块构造为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来存储有关网络的信息。
5.1 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文件,你将意识到一个块可能包含多个层。 例如,卷积类型的块除了具有卷积层外,还具有批处理归一层以及leaky 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)
5.2 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()
用于创建路径层的代码值得一提。 首先,我们提取路径层属性的值,将其转换为整数并存储在列表中。
然后,我们有了一个名为EmptyLayer的新层,顾名思义,它只是一个空层。
route = EmptyLayer()
它的定义如下:
class EmptyLayer(nn.Module): def __init__(self): super(EmptyLayer, self).__init__()
5.3 Wait, an empty layer?
现在,空层很奇怪,因为好像它什么也不做。 与其他任何层一样,路径层也执行操作(带来前面的层/并置)。 在PyTorch中,当我们定义新层时,我们将nn.Module子类化,并在nn.Module对象中编写该层执行正向传播的操作。
为了给路径模块设计一个层,我们需要构建一个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]
短跳层也利用了一个空层,因为它同样执行了非常简单的操作(添加)。 无需更新filters变量,因为它仅将前面层的特征图添加到后一层。
5.4 YOLO Layer
最后,我们编写用于创建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 Testing the code
在本文上方我们已经通过下面的语句完成了对cfg配置文件的解析:
blocks = parse_cfg('cfg/yolov3.cfg')
然后通过下面的语句就可以把经过parse_cfg解析后的blocks块,对网络进行pytoch架构的创建
print(create_modules(blocks))
你将看到一个很长的列表(恰好包含106个项目),其元素看起来像下面这样:
这部分就是这样。 在下一部分中,我们将组装已创建的模块以从图像生成输出。
Further Reading