YOLOv3构建网络结构
tip
本文转载于: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 放到你的网络目录中,然后输入:
mkdir cfg
cd 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个block。其中,3层描述卷积层,其次是一个残差层。残差层是一个跳过连接,就像 ResNet 中使用的那样。在 YOLO 中使用了5种类型的网络层:
Convolutional
[convolutional]
batch_normalize=1
filters=64
size=3
stride=1
pad=1
activation=leaky
Shortcut
[shortcut]
from=-3
activation=linear
Upsample
[upsample]
stride=2
使用双线性上采样,通过步长因子对前一层的feature map进行上采样
Route
[route]
layers = -4
[route]
layers = -1, 61
路线图层值得解释一下。它有一个属性层,可以有一个或两个值。
当 layers 属性只有一个值时,它输出按该值索引的层的特征映射。在我们的例子中,它是 -4,所以图层将输出从route层向后四层的feature map。
当图层有两个值时,它返回两个feature map连接之后的结果。在我们的例子中,它是 -1,61,这个层将输出前一个层(- 1)和61层的feature map,并沿着深度维连接起来(stack起来)。
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,这意味着使用了第一、第二和第三个锚。这是有道理的,因为检测层的每个cell预测3个box。总的来说,我们有3个尺度的探测层,共计9个锚。
Net
[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(cfg_file_path):
"""
获取配置文件
返回一个block列表。每个block描述神经元中的一个块
block在列表中表示为字典
"""
这里的思路是解析 cfg文件,并将每个块存储为字典。块的属性及其值作为键值对存储在字典中。在解析 cfg 时,我们不断将这些命令(在代码中由变量块表示)附加到列表块中。我们的函数将返回这个block list。
我们首先将 cfg 文件的内容保存到一个字符串列表中。下面的代码对此列表执行一些预处理。
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。
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 函数返回的列表块。
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 containingnn.Module
objects. However, when we addnn.ModuleList
as a member of ann.Module
object (i.e. when we add modules to our network), all theparameters
ofnn.Module
objects (modules) inside thenn.ModuleList
are added asparameter
s of thenn.Module
object (i.e. our network, which we are adding thenn.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 模块。
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.Sequential
和 add_module
将这些层添加起来。
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
# 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,顾名思义,它只是一个空层。
route = EmptyLayer()
它的定义如下:
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的数量。
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 层的代码。
# 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定义如下:
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。
测试代码
可以通过在 darknet.py 结尾输入以下行并运行该文件来测试代码。
if __name__ == '__main__':
blocks = parse_cfg('cfg/yolov3.cfg')
print(create_modules(blocks))
您将看到一个很长的列表(精确地包含106个条目) ,其中的元素看起来像
(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)
)
这部分就到这里。在接下来的部分中,我们将组装已经创建的构建块,以便从图像生成输出。