本教程转载于: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

1
2
[upsample]
stride=2

使用双线性上采样,通过步长因子对前一层的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 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 的函数,它以配置文件的路径作为输入。

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按行拆分储存在list中
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] == '[': # 这标志着一个新block的开始
if len(block) != 0: # 如果block不是空的,则意味着它存储了前一个block的值。
blocks.append(block) # 添加到 blocks list 里
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] # 捕获有关输入和预处理的信息, 也就是net里面的信息
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 parameters 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()

# 检查block的类型
# 为block创建新的module
# 添加进module_list当中

nn.Sequential()是用来顺序的执行一些nn.Module对象。如果你cfg文件,你将会发现一个block中将会包含超过一个layer。例如convolutional block中就有batch norm层也有leaky ReLU激活层,此外还有卷积层。使用使用nn.Sequentialadd_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)

# 添加BN层
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
route = 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:
# 如果我们连接feature map
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
# yolo 检测层 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_{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)
)

这部分就到这里。在接下来的部分中,我们将组装已经创建的构建块,以便从图像生成输出。