本教程转载于:https://blog.paperspace.com/how-to-implement-a-yolo-object-detector-in-pytorch/ , 在原教程上加入了自己的理解,我的理解将用 这样的格式写出
这是从头开始实现 YOLO v3检测器的教程的第2部分。在第一部分中,我解释了 YOLO 的工作原理,在这一部分中,我们将使用 PyTorch 实现 YOLO 使用的层。
阅读这篇文章你需要:
[ ] 了解Part1有关YOLO工作原理的基础知识
[ ] PyTorch 的基本工作知识,包括如何使用 nn 创建网络结构等。
Getting Started
首先创建一个检测器代码所在的目录。
然后创建一个文件 darknet.py 。Darknet 是 YOLO 基础架构的名称。该文件将包含创建 YOLO 网络的代码。我们将用一个名为 util.py 的文件来补充它,该文件将包含各种 helper 函数的代码。将这两个文件保存到文件夹中。您可以使用 git 来跟踪更改。
配置文件
官方代码(用 c 编写)使用一个配置文件来构建网络。cfg 文件逐块描述网络的布局。
我们将使用作者发布的官方 cfg 文件来构建我们的网络。从这里下载它,并将其放在您的检测器目录中一个名为 cfg 的文件夹中。如果你在 Linux 上,把 cd 放到你的网络目录中,然后输入:
1 2 3 mkdir cfg cd cfg wget https://gitee.com/coronapolvo/images/raw/master/yolov3.cfg
如果打开配置文件,您将看到如下内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 [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个block。其中,3层描述卷积层,其次是一个残差层。残差层是一个跳过连接,就像 ResNet 中使用的那样。在 YOLO 中使用了5种类型的网络层:
Convolutional
1 2 3 4 5 6 7 [convolutional] batch_normalize=1 filters=64 size=3 stride=1 pad=1 activation=leaky
Shortcut
1 2 3 [shortcut] from=-3 activation=linear
Upsample
使用双线性上采样,通过步长因子对前一层的feature map进行上采样
Route
1 2 3 4 5 [route] layers = -4 [route] layers = -1, 61
路线图层值得解释一下。它有一个属性层,可以有一个或两个值。
当 layers 属性只有一个值时,它输出按该值索引的层的特征映射。在我们的例子中,它是 -4,所以图层将输出从route层向后四层的feature map。
当图层有两个值时,它返回两个feature map连接之后的结果。在我们的例子中,它是 -1,61,这个层将输出前一个层(- 1)和61层的feature map,并沿着深度维连接起来(stack起来)。
YOLO
1 2 3 4 5 6 7 8 9 [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,这意味着使用了第一、第二和第三个锚。这是有道理的,因为检测层的每个cell预测3个box。总的来说,我们有3个尺度的探测层,共计9个锚。
Net
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 [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 文件的顶部添加必要的导入。
1 2 3 4 5 6 7 from __future__ import divisionimport torch import torch.nn as nnimport torch.nn.functional as F from torch.autograd import Variableimport numpy as np
我们定义了一个名为 parse_cfg 的函数,它以配置文件的路径作为输入。
1 2 3 4 5 6 7 def parse_cfg (cfg_file_path ): """ 获取配置文件 返回一个block列表。每个block描述神经元中的一个块 block在列表中表示为字典 """
这里的思路是解析 cfg文件,并将每个块存储为字典。块的属性及其值作为键值对存储在字典中。在解析 cfg 时,我们不断将这些命令(在代码中由变量块表示)附加到列表块中。我们的函数将返回这个block list。
我们首先将 cfg 文件的内容保存到一个字符串列表中。下面的代码对此列表执行一些预处理。
1 2 3 4 5 file = open (cfg_file_path, 'r' ) lines = file.read().split('\n' ) lines = [x for x in lines if len (x) > 0 ] lines = [x for x in lines if x[0 ] != '#' ] lines = [x.rstrip().lstrip() for x in lines]
然后,我们循环结果列表以获得block。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 block = {} blocks = [] for line in lines: if lines[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) return blocks
构建blocks
现在,我们将使用上面 parse_cfg 返回的列表为配置文件中的块构造 PyTorch 模块。
我们在列表中有5种类型的图层(上面提到过)。PyTorch 为卷积类型和 upsample 类型提供了预构建层。我们必须通过 torch.nn
为其余的层编写我们自己的模块。
create_modules 函数接受 parse_cfg 函数返回的列表块。
1 2 3 4 5 def create_modules (blocks ): net_info = blocks[0 ] module_list = nn.ModuleList() prev_filters = 3 output_filters = []
在遍历块列表之前,我们定义一个变量 net_info 来存储关于网络的信息。
nn.ModuleList
Our function will return a nn.ModuleList
. This class is almost like a normal list containing nn.Module
objects. However, when we add nn.ModuleList
as a member of a nn.Module
object (i.e. when we add modules to our network), all the parameters
of nn.Module
objects (modules) inside the nn.ModuleList
are added as parameter
s of the nn.Module
object (i.e. our network, which we are adding the nn.ModuleList
as a member of) as well.
在定义新的卷积层时,必须定义卷积核的维数。虽然卷积核的高度和宽度是由 cfg 文件提供的,但卷积核的深度恰恰是前一层中存在的过滤器的数量(或feature map的深度)。这意味着我们需要跟踪过滤器的数量在层卷积层正在应用。我们使用变量 prev_filter
来实现这一点。我们将其初始化为3,因为图像有3个对应于RGB通道的过滤器。
route层从以前的层带来feature map。如果在一个路由层的正前方有一个卷积层,那么内核将被应用到之前层的feature map上,恰好是路由层带来的feature map。因此,我们需要跟踪过滤器的数量,不仅在前一层,每一个前面的层上都需要跟踪。在迭代过程中,我们将每个block的输出筛选器数附加到列表 output_filters
中。
现在,接下来的思路就是迭代块列表,并为每个块创建一个 PyTorch 模块。
1 2 3 4 5 6 for index, x in enumerate (blocks[1 :]): module = nn.Sequential()
nn.Sequential()
是用来顺序的执行一些nn.Module
对象。如果你cfg文件,你将会发现一个block中将会包含超过一个layer。例如convolutional block
中就有batch norm层也有leaky ReLU激活层,此外还有卷积层。使用使用nn.Sequential
和 add_module
将这些层添加起来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 if x['type' ] == 'convolutional' : 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 conv = nn.Conv2d(prev_filters, filters, kernel_size, stride, pad, bias=bias) module.add_module('conv_{0}' .format (index), conv) if batch_normalize: bn = nn.BatchNorm2d(filters) module.add_module('batch_norm_{0}' .format (index), bn) if activation == 'leaky' : activn = nn.LeakyReLU(0.1 , inplace=True ) module.add_module('leaky_{0}' .format (index), activn) elif x['type' ] == 'upsample' : stride = int (x['stride' ]) upsample = nn.Upsample(scale_factor=stride, mode="bilinear" ) module.add_module("upsample_{}" .format (index), upsample)
Route Layer / Shortcut Layers
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 # route layer elif x['type'] == 'route': x['layers'] = x['layers'].split(',') start = int(x['layers'][0]) try: end = int(x["layers"][1]) except: end = 0 # 位置标注 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_filter[index + start] # shortcut layer elif x['type'] == 'shortcut': shortcut = EmptyLayer() module.add_module('shortcut_{0}'.format(index), shortcut)
创建路由层的代码值得做一些解释。首先,我们提取 layers 属性的值,将其强制转换为一个整数并将其存储在一个列表中。
然后我们有一个新的层叫做 EmptyLayer,顾名思义,它只是一个空层。
它的定义如下:
1 2 3 class EmptyLayer (nn.Module ): def __init__ (self ): super (EmptyLayer, self).__init__()
等等,一个空的layer?
现在,一个空层可能看起来很奇怪,因为它什么也不做。route 层,就像任何其他层执行一个操作(提前前一层/连接)。在 PyTorch 中,当我们定义一个新层时,我们将nn.Module
子类化。将层的执行操作写入到 forward
函数当中。
为了给 Route block构建一个层,我们必须构建一个 nn.Module
。用属性层的值作为其成员初始化的模块对象。然后,我们可以在 forward 函数中编写代码来连接或者提取feature map。最后,我们在网络的forward
中执行这一层。
但是,考虑到连接代码相当简短(在feature map中torch。cat) ,如上所述设计一个层将导致不必要的抽象,只会增加模板代码。相反,我们可以做的是放置一个虚拟层代替路由层,然后在 nn 的forward
函数中直接执行连接操作。表示 darknet 的模块对象。
位于路由层前面的卷积层将其卷积核应用于(可能是连接的)前面层的feature map。下面的代码更新 filters 变量以保存路由层输出的filters的数量。
1 2 3 4 5 if end < 0 : filters = output_filters[index + start] + output_filters[index + end] else : filters= output_filters[index + start]
Shortcut还利用了一个空层,因为它还是只执行一个非常简单的操作(添加)。没有必要更新filters,因为它只是添加了前一层的feature map到后面的层。
YOLO Layer
最后,我们编写创建 YOLO 层的代码。
1 2 3 4 5 6 7 8 9 10 11 12 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_{0}" .format (index), detection)
我们定义了一个新的层DetectionLayer
,用来保存用于检测box的锚。
DetectionLayer定义如下:
1 2 3 4 class DetectionLayer (nn.Module ): def __init__ (self, anchors ): super (DetectionLayer, self).__init__() self.anchors = anchors
在循环的最后,我们添加一些代码:
1 2 3 module_list.append(module) prev_filters = filters output_filters.append(filters)
这就是循环的主体部分。在 create_modules 函数的末尾,我们返回 net_info 和 module_list。
测试代码
可以通过在 darknet.py 结尾输入以下行并运行该文件来测试代码。
1 2 3 if __name__ == '__main__' : blocks = parse_cfg('cfg/yolov3.cfg' ) print (create_modules(blocks))
您将看到一个很长的列表(精确地包含106个条目) ,其中的元素看起来像
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 (6 ): Sequential( (conv_6): Conv2d(128 , 64 , kernel_size=(1 , 1 ), stride=(1 , 1 ), bias=False ) (batch_norm_6): BatchNorm2d(64 , eps=1e-05 , momentum=0.1 , affine=True , track_running_stats=True ) (leaky_6): LeakyReLU(negative_slope=0.1 , inplace=True ) ) (7 ): Sequential( (conv_7): Conv2d(64 , 128 , kernel_size=(3 , 3 ), stride=(1 , 1 ), padding=(1 , 1 ), bias=False ) (batch_norm_7): BatchNorm2d(128 , eps=1e-05 , momentum=0.1 , affine=True , track_running_stats=True ) (leaky_7): LeakyReLU(negative_slope=0.1 , inplace=True ) ) (8 ): Sequential( (shortcut_8): EmptyLayer() ) (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 , track_running_stats=True ) (leaky_9): LeakyReLU(negative_slope=0.1 , inplace=True ) )
这部分就到这里。在接下来的部分中,我们将组装已经创建的构建块,以便从图像生成输出。