从零开端PyTorch项目:YOLO v3目标检测完成

目标检测是深度进修近期开展进程中受益最众的范畴。跟着技能的进步,人们曾经开辟出了许众用于目标检测的算法,包罗 YOLO、SSD、Mask RCNN 和 RetinaNet。本教程中,我们将运用 PyTorch 完成基于 YOLO v3 的目标检测器,后者是一种疾速的目标检测算法。该教程一共有五个部分,本文包罗此中的前三部分。

过去几个月中,我不停实行室中研讨晋升目标检测的方法。这之中我取得的最大启示便是看法到:进修目标检测的最佳方法便是本人入手完成这些算法,而这恰是本教程指导你去做的。


本教程中,我们将运用 PyTorch 完成基于 YOLO v3 的目标检测器,后者是一种疾速的目标检测算法。

本教程运用的代码需求运转 Python 3.5 和 PyTorch 0.3 版本之上。你可以以下链接中找到所有代码:

https://github.com/ayooshkathuria/YOLO_v3_tutorial_from_scratch 

本教程包罗五个部分:

1. YOLO 的义务原理

2. 创立 YOLO 收集层级

3. 完成收集的前向传达

4. objectness 置信度阈值和非极大值遏止

5. 计划输入和输出管道

所需配景常识

进修本教程之前,你需求了解:

  • 卷积神经收集的义务原理,包罗残差块、跳过连接和上采样;

  • 目标检测、边境框回归、IoU 和非极大值遏止;

  • 根底的 PyTorch 运用。你需求可以轻松地创立简单的神经收集。

什么是 YOLO?

YOLO 是 You Only Look Once 的缩写。它是一种运用深度卷积神经收集学得的特征来检测对象的目标检测器。我们上手写代码之前,我们必需先了解 YOLO 的义务原理。

全卷积神经收集

YOLO 仅运用卷积层,这就使其成为全卷积神经收集(FCN)。它具有 75 个卷积层,另有跳过连接和上采样层。它不运用任何方式的池化,运用步幅为 2 的卷积层对特征图举行下采样。这有帮于避免一般由池化导致的初级特征丧失。

举措 FCN,YOLO 关于输入图像的大小并不敏锐。然而,实行中,我们可以念要继续稳定的输入大小,因为种种题目只要我们完成算法时才会浮现出来。

这此中的一个主要题目是:假如我们期望按批次处理图像(批量图像由 GPU 并行处理,如许可以晋升速率),我们就需求固定所有图像的高度和宽度。这就需求将众个图像整合进一个大的批次(将许众 PyTorch 张量兼并成一个)。

YOLO 通过被步幅对图像举行上采样。比如,假如收集的步幅是 32,则大小为 416×416 的输入图像将发生 13×13 的输出。一般,收集层中的恣意步幅都指层的输入除以输入。

标明输出

典范地(关于所有目标检测器都是这种状况),卷积层所进修的特征会被转达到分类器/回归器,从而举行预测(边境框的坐标、种别标签等)。

YOLO 中,预测是通过卷积层完毕的(它是一个全卷积神经收集,请记住!)其中心尺寸为:

1×1×(B×(5+C))

现,起首要当心的是我们的输出是一个特征图。因为我们运用了 1×1 的卷积,以是预测图的大小恰恰是之前特征图的大小。 YOLO v3(及其更新的版本)上,预测图便是每个可以预测固定命量边境框的单位格。

虽然形色特征图中单位的准确术语应当是「神经元」,但本文中为了更为直观,我们将其称为单位格(cell)。

深度方面,特征图中有 (B x (5 + C))* *个条目。B 代外每个单位可以预测的边境框数目。依据 YOLO 的论文,这些 B 边境框中的每一个都可以特别用于检测某种对象。每个边境框都有 5+C 个属性,区分描画每个边境框的中心坐标、维度、objectness 分数和 C 类置信度。YOLO v3 每个单位中预测 3 个边境框。

假如对象的中心位于单位格的感觉野内,你会期望特征图的每个单位格都可以通过此中一个边境框预测对象。(感觉野是输入图像关于单位格可睹的区域。)

这与 YOLO 是怎样教练的相关,只要一个边境框认真检测恣意给定对象。起首,我们必需确定这个边境框属于哪个单位格。

于是,我们需求切分输入图像,把它拆成维度等于最终特征图的网格。

让我们考虑下面一个例子,此中输入图像大小是 416×416,收集的步幅是 32。如之前所述,特征图的维度会是 13×13。随后,我们将输入图像分为 13×13 个网格。

输入图像中包罗了真值对象框中心的网格会举措认真预测对象的单位格。图像中,它是被标记为血色的单位格,此中包罗了真值框的中心(被标记为黄色)。

现,血色单位格是网格中第七行的第七个。我们现使特征图中第七行第七个单位格(特征图中的对应单位格)举措检测狗的单位。

现,这个单位格可以预测三个边境框。哪个将会分派给狗的真值标签?为了了解这一点,我们必需了解锚点的看法。

请当心,我们这里议论的单位格是预测特征图上的单位格,我们将输入图像分开成网格,以确定预测特征图的哪个单位格认真预测对象。

锚点框(Anchor Box)

预测边境框的宽度和高度看起来十分合理,但实行中,教练会带来不稳定的梯度。以是,现阵势部目标检测器都是预测对数空间(log-space)变换,或者预测与预教练默认边境框(即锚点)之间的偏移。

然后,这些变换被运用到锚点框来取得预测。YOLO v3 有三个锚点,以是每个单位格会预测 3 个边境框。

回到前面的题目,认真检测狗的边境框的锚点有最高的 IoU,且有真值框。

预测

下面的公式描画了收集输出是怎样转换,以取得边境框预测结果的。

中心坐标

当心:我们运用 sigmoid 函数举行中心坐标预测。这使得输出值 0 和 1 之间。启事如下:

平常状况下,YOLO 不会预测边境框中心确实切坐标。它预测:

  • 与预测目标的网格单位左上角相关的偏移;

  • 运用特征图单位的维度(1)举行归一化的偏移。

以我们的图像为例。假如中心的预测是 (0.4, 0.7),则中心 13 x 13 特征图上的坐标是 (6.4, 6.7)(血色单位的左上角坐标是 (6,6))。

可是,假如预测到的 x,y 坐标大于 1,比如 (1.2, 0.7)。那么中心坐标是 (7.2, 6.7)。当心该中心血色单位右侧的单位中,或第 7 行的第 8 个单位。这打破了 YOLO 背后的表面,因为假如我们假设血色框认真预测目标狗,那么狗的中心必需血色单位中,不应当它旁边的网格单位中。

于是,为理办理这个题目,我们对输出施行 sigmoid 函数,将输出压缩到区间 0 到 1 之间,有用确保中心处于施行预测的网格单位中。

边境框的维度

我们对输出施行对数空间变换,然后乘锚点,来预测边境框的维度。

检测器输出最终预测之前的变换进程,图源:http://christopher5106.github.io/

得出的预测 bw 和 bh 运用图像的高和宽举行归一化。即,假如包罗目标(狗)的框的预测 bx 和 by 是 (0.3, 0.8),那么 13 x 13 特征图的实行宽和高是 (13 x 0.3, 13 x 0.8)。

Objectness 分数

Object 分数外示目标边境框内的概率。血色网格和相邻网格的 Object 分数应当接近 1,而角落处的网格的 Object 分数可以接近 0。

objectness 分数的盘算也运用 sigmoid 函数,于是它可以被了解为概率。

种别置信度

种别置信度外示检测到的对象属于某个种另外概率(如狗、猫、香蕉、汽车等)。 v3 之前,YOLO 需求对种别分数施行 softmax 函数操作。

可是,YOLO v3 舍弃了这种计划,作家挑选运用 sigmoid 函数。因为对种别分数施行 softmax 操作的条件是种别是互斥的。简言之,假如对象属于一个种别,那么必需确保其不属于另一个种别。这我们修立检测器的 COCO 数据集上是准确的。可是,当呈现种别「女性」(Women)和「人」(Person)时,该假设不可行。这便是作家挑选不运用 Softmax 激活函数的启事。

差别标准上的预测

YOLO v3 3 个差别标准上举行预测。检测层用于三个差别大小的特征图上施行预测,特征图步幅区分是 32、16、8。这意味着,当输入图像大小是 416 x 416 时,我们标准 13 x 13、26 x 26 和 52 x 52 上施行检测。

该收集第一个检测层之前对输入图像施行下采样,检测层运用步幅为 32 的层的特征图施行检测。随后施行因子为 2 的上采样后,并与前一个层的特征图(特征图大小相同)拼接。另一个检测步幅为 16 的层中施行。重复同样的上采样方法,着末一个检测步幅为 8 的层中施行。

每个标准上,每个单位运用 3 个锚点预测 3 个边境框,锚点的总数为 9(差别标准的锚点差别)。

作家称这帮帮 YOLO v3 检测较小目标时取得更好的功用,而这恰是 YOLO 之前版本常常被埋怨的地方。上采样可以帮帮该收集进修细粒度特征,帮帮检测较小目标。

输因由理

关于大小为 416 x 416 的图像,YOLO 预测 ((52 x 52) + (26 x 26) + 13 x 13)) x 3 = 10647 个边境框。可是,我们的示例中只要一个对象——一只狗。那么我们怎样才干将检测次数从 10647 淘汰到 1 呢?

目标置信度阈值:起首,我们依据它们的 objectness 分数过滤边境框。一般,分数低于阈值的边境框会被疏忽。

非极大值遏止:非极大值遏止(NMS)可办理对同一个图像的众次检测的题目。比如,血色网格单位的 3 个边境框可以检测一个框,或者临近网格可检测相同对象。

完成

YOLO 只可检测出属于斗嗽豉用数据汇合种另外对象。我们的检测器将运用官方权重文献,这些权重通过 COCO 数据集上教练收集而取得,于是我们可以检测 80 个对象种别。

该教程的第一部分到此完毕。这部分精细讲解了 YOLO 算法。假如你念深度了解 YOLO 的义务原理、教练进程和与其他检测器的功用规避,可阅读原始论文:

1. YOLO V1: You Only Look Once: Unified, Real-Time Object Detection (https://arxiv.org/pdf/1506.02640.pdf)

2. YOLO V2: YOLO9000: Better, Faster, Stronger (https://arxiv.org/pdf/1612.08242.pdf)

3. YOLO V3: An Incremental Improvement (https://pjreddie.com/media/files/papers/YOLOv3.pdf)

4. Convolutional Neural Networks (http://cs231n.github.io/convolutional-networks/)

5. Bounding Box Regression (Appendix C) (https://arxiv.org/pdf/1311.2524.pdf)

6. IoU (https://www.youtube.com/watch?v=DNEm4fJ-rto)

7. Non maximum suppresion (https://www.youtube.com/watch?v=A46HZGR5fMw)

8. PyTorch Official Tutorial (http://pytorch.org/tutorials/beginner/deep_learning_60min_blitz.html)

第二部分:创立 YOLO 收集层级

以下是从头完成 YOLO v3 检测器的第二部分教程,我们将基于前面所述的基本看法运用 PyTorch 完成 YOLO 的层级,即创立通通模子的基本构修块。

这一部分请求读者曾经基本了解 YOLO 的运转方法和原理,以及关于 PyTorch 的基本常识,比如怎样通过 nn.Module、nn.Sequential 和 torch.nn.parameter 等类来构修自定义的神经收集架构。

开端路程

起首创立一个存放检测器代码的文献夹,然后再创立 Python 文献 darknet.py。Darknet 是构修 YOLO 底层架构的状况,这个文献将包罗完成 YOLO 收集的所有代码。同样我们还需求增补一个名为 util.py 的文献,它会包罗众种需求调用的函数。将所有这些文献保保管检测器文献夹下后,我们就能运用 git 追踪它们的改动。

配备文献

官方代码(authored in C)运用一个配备文献来构修收集,即 cfg 文献一块块地描画了收集架构。假如你运用过 caffe 后端,那么它就相当于描画收集的.protxt 文献。

我们将运用官方的 cfg 文献构修收集,它是由 YOLO 的作家发布的。我们可以以下地址下载,并将其放检测器目次下的 cfg 文献夹下。

配备文献下载:https://github.com/pjreddie/darknet/blob/master/cfg/yolov3.cfg

当然,假如你运用 Linux,那么就可以先 cd 到检测器收集的目次,然后运转以下命令行。

mkdir cfg
cd cfg
wget 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

我们看到上面有四块配备,此中 3 个描画了卷积层,着末描画了 ResNet 中常用的捷径层或跳过连接。下面是 YOLO 中运用的 5 种层级:

1. 卷积层

[convolutional]
batch_normalize=1 
filters=64 
size=3 
stride=1 
pad=1 
activation=leaky

2. 跳过连接

[shortcut]
from=-3 
activation=linear 

跳过连接与残差收集中运用的构造相似,参数 from 为-3 外示捷径层的输出会通过将之前层的和之前第三个层的输出的特征图与模块的输入相加而得出。

3.上采样

[upsample]
stride=2

通过参数 stride 前面层级中双线性上采样特征图。

4.道由层(Route)

[route]
layers = -4

[route]
layers = -1, 61

道由层需求少许标明,它的参数 layers 有一个或两个值。当只要一个值时,它输出这一层通过该值索引的特征图。我们的实行中修立为了-4,以是层级将输出道由层之前第四个层的特征图。

当层级有两个值时,它将返回由这两个值索引的拼接特征图。我们的实行中为-1 和 61,于是该层级将输出过去一层级(-1)到第 61 层的特征图,并将它们按深度拼接。

5.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 层级对应于上文所描画的检测层级。参数 anchors 定义了 9 组锚点,可是它们只是由 mask 标签运用的属性所索引的锚点。这里,mask 的值为 0、1、2 外示了第一个、第二个和第三个运用的锚点。而掩码外示检测层中的每一个单位预测三个框。总而言之,我们检测层的范围为 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

配备文献中保管另一种块 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,将每个块存储为辞书。这些块的属性和值都以键值对的方式存储辞书中。解析进程中,我们将这些辞书(由代码中的变量 block 外示)添加到列外 blocks 中。我们的函数将返回该 block。

我们起首将配备文献实质保保管字符串列外中。下面的代码对该列外施行预处理:

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 为 convolutional 和 upsample 供应预置层。我们将通过扩展 nn.Module 类为其余层写本人的模块。

create_modules 函数运用 parse_cfg 函数返回的 blocks 列外:

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 对象(模块)的 parameter 也被添加举措 nn.Module 对象(即我们的收集,添加 nn.ModuleList 举措其成员)的 parameter。

当我们定义一个新的卷积层时,我们必需定义它的卷积核维度。虽然卷积核的高度和宽度由 cfg 文献供应,但卷积核的深度是由上一层的卷积核数目(或特征图深度)决议的。这意味着我们需求继续追踪被运用卷积层的卷积核数目。我们运用变量 prev_filter 来做这件事。我们将其初始化为 3,因为图像有对应 RGB 通道的 3 个通道。

道由层(route layer)过去面层取得特征图(可以是拼接的)。假如道由层之后有一个卷积层,那么卷积核将被运用到前面层的特征图上,准确来说是道由层取得的特征图。于是,我们不光需求追踪前一层的卷积核数目,还需求追踪之前每个层。跟着不时地迭代,我们将每个模块的输出卷积核数目添加到 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 文献,你会发明,一个模块可以包罗众于一个层。比如,一个 convolutional 类型的模块有一个批量归一化层、一个 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)

道由层/捷径层

接下来,我们来写创立道由层(Route Layer)和捷径层(Shortcut Layer)的代码:

 #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)

创立道由层的代码需求做少许标明。起首,我们提取关于层属性的值,将其外示为一个整数,并保保管一个列外中。

然后我们取得一个新的称为 EmptyLayer 的层,顾名思义,便是空的层。

route = EmptyLayer()

其定义如下:

class EmptyLayer(nn.Module):
 def __init__(self):
 super(EmptyLayer, self).__init__()

等等,一个空的层?

现,一个空的层可以会令人疑心,因为它没有做任何事故。而 Route Layer 正如其它层将施行某种操作(获取之前层的拼接)。 PyTorch 中,当我们定义了一个新的层,我们子类 nn.Module 中写入层 nn.Module 对象的 forward 函数的运算。

关于 Route 模块中计划一个层,我们必需修立一个 nn.Module 对象,其举措 layers 的成员被初始化。然后,我们可以写下代码,将 forward 函数中的特征图拼接起来并向前馈赠。着末,我们施行收集的某个 forward 函数的这个层。

但拼接操作的代码相外埠短和简单(特征图上调用 torch.cat),像上述进程那样计划一个层将导致不须要的笼统,添加样板代码。取而代之,我们可以将一个假的层置于之条件出的道由层的位置上,然后直接代外 darknet 的 nn.Module 对象的 forward 函数中施行拼接运算。(假如感受疑心,我倡议你读一下 nn.Module 类 PyTorch 中的运用)。

道由层之后的卷积层会把它的卷积核运用到之前层的特征图(可以是拼接的)上。以下的代码更新了 filters 变量以保管道由层输出的卷积核数目。

if end < 0:
 #If we are concatenating maps
 filters = output_filters[index + start] + output_filters[index + end]
else:
 filters= output_filters[index + start]

捷径层也运用空的层,因为它还要施行一个十分简单的操作(加)。没须要更新 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

这个回道完毕时,我们做了少许统计(bookkeeping.)。

 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(
 )
 )

第三部分:完成收集的前向传达

第二部分中,我们完成了 YOLO 架构中运用的层。这部分,我们方案用 PyTorch 完成 YOLO 收集架构,如许我们就能生成给定图像的输出了。

我们的目标是计划收集的前向传达。

先决条件

  • 阅读本教程前两部分;

  • PyTorch 根底常识,包罗怎样运用 nn.Module、nn.Sequential 和 torch.nn.parameter 创立自定义架构;

  • PyTorch 中处理图像。

定义收集

如前所述,我们运用 nn.Module PyTorch 中构修自定义架构。这里,我们可认为检测器定义一个收集。 darknet.py 文献中,我们添加了以下种别:

class Darknet(nn.Module):
 def __init__(self, cfgfile):
 super(Darknet, self).__init__()
 self.blocks = parse_cfg(cfgfile)
 self.net_info, self.module_list = create_modules(self.blocks)

这里,我们对 nn.Module 种别举行子分类,并将我们的种别命名为 Darknet。我们用 members、blocks、net_info 和 module_list 对收集举行初始化。

完成该收集的前向传达

该收集的前向传达通过覆写 nn.Module 种另外 forward 方法而完成。

forward 主要有两个目标。一,盘算输出;二,尽早处理的方法转换输出检测特征图(比如转换之后,这些差别标准的检测图就可以串联,否则会因为差别维度不行够完成串联)。

def forward(self, x, CUDA):
 modules = self.blocks[1:]
 outputs = {} #We cache the outputs for the route layer

forward 函数有三个参数:self、输入 x 和 CUDA(假如是 true,则运用 GPU 来加速前向传达)。

这里,我们迭代 self.block[1:] 而不是 self.blocks,因为 self.blocks 的第一个元素是一个 net 块,它不属于前向传达。

因为道由层和捷径层需求之前层的输出特征图,我们字典 outputs 中缓存每个层的输出特征图。要害于层的索引,且值对应特征图。

正如 create_module 函数中的案例,我们现迭代 module_list,它包罗了收集的模块。需求当心的是这些模块是以配备文献中相同的序次添加的。这意味着,我们可以简单地让输入通过每个模块来取得输出。

write = 0 #This is explained a bit later
for i, module in enumerate(modules): 
 module_type = (module["type"])

卷积层和上采样层

假如该模块是一个卷积层或上采样层,那么前向传达应当按如系澜式义务:

 if module_type == "convolutional" or module_type == "upsample":
 x = self.module_list[i](x)

道由层/捷径层

假如你查看道由层的代码,我们必需阐明两个案例(正如第二部分中所描画的)。关于第一个案例,我们必需运用 torch.cat 函数将两个特征图级联起来,第二个参数设为 1。这是因为我们期望将特征图沿深度级联起来。( PyTorch 中,卷积层的输入和输出的样式为`B X C X H X W。深度对应通道维度)。

 elif module_type == "route":
 layers = module["layers"]
 layers = [int(a) for a in layers]

 if (layers[0]) > 0:
 layers[0] = layers[0] - i

 if len(layers) == 1:
 x = outputs[i + (layers[0])]

 else:
 if (layers[1]) > 0:
 layers[1] = layers[1] - i

 map1 = outputs[i + layers[0]]
 map2 = outputs[i + layers[1]]

 x = torch.cat((map1, map2), 1)

 elif module_type == "shortcut":
 from_ = int(module["from"])
 x = outputs[i-1] + outputs[i+from_]

YOLO(检测层)

YOLO 的输出是一个卷积特征图,包罗沿特征图深度的边境框属性。边境框属性由互相堆叠的单位格预测得出。于是,假如你需求 (5,6) 处拜访单位格的第二个边框,那么你需求通过 map[5,6, (5+C): 2*(5+C)] 将其编入索引。这种样式关于输因由理进程(比如通过目标置信度举行阈值处理、添加对中心的网格偏移、运用锚点等)很未便当。

另一个题目是因为检测是三个标准上举行的,预测图的维度将是差别的。虽然三个特征图的维度差别,但对它们施行的输因由理进程是相似的。假如能单个张量而不是三个独自张量上施行这些运算,就太好了。

为理办理这些题目,我们引入了函数 predict_transform。

变换输出

函数 predict_transform 文献 util.py 中,我们 Darknet 种另外 forward 中运用该函数时,将导入该函数。

util.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
import cv2 

predict_transform 运用 5 个参数:prediction(我们的输出)、inp_dim(输入图像的维度)、anchors、num_classes、CUDA flag(可选)。

def predict_transform(prediction, inp_dim, anchors, num_classes, CUDA = True):

predict_transform 函数把检测特征图转换成二维张量,张量的每一行对应边境框的属性,如下所示:

上述变换所运用的代码:

 batch_size = prediction.size(0)
 stride = inp_dim // prediction.size(2)
 grid_size = inp_dim // stride
 bbox_attrs = 5 + num_classes
 num_anchors = len(anchors)

 prediction = prediction.view(batch_size, bbox_attrs*num_anchors, grid_size*grid_size)
 prediction = prediction.transpose(1,2).contiguous()
 prediction = prediction.view(batch_size, grid_size*grid_size*num_anchors, bbox_attrs)

锚点的维度与 net 块的 height 和 width 属性同等。这些属性描画了输入图像的维度,比检测图的范围大(二者之商即是步幅)。于是,我们必需运用检测特征图的步幅支解锚点。

 anchors = [(a[0]/stride, a[1]/stride) for a in anchors]

现,我们需求依据第一部分议论的公式变换输出。

对 (x,y) 坐标和 objectness 分数施行 Sigmoid 函数操作。

 #Sigmoid the centre_X, centre_Y. and object confidencce
 prediction[:,:,0] = torch.sigmoid(prediction[:,:,0])
 prediction[:,:,1] = torch.sigmoid(prediction[:,:,1])
 prediction[:,:,4] = torch.sigmoid(prediction[:,:,4])

将网格偏移添加到中心坐标预测中:

 #Add the center offsets
 grid = np.arange(grid_size)
 a,b = np.meshgrid(grid, grid)

 x_offset = torch.FloatTensor(a).view(-1,1)
 y_offset = torch.FloatTensor(b).view(-1,1)

 if CUDA:
 x_offset = x_offset.cuda()
 y_offset = y_offset.cuda()

 x_y_offset = torch.cat((x_offset, y_offset), 1).repeat(1,num_anchors).view(-1,2).unsqueeze(0)

 prediction[:,:,:2] += x_y_offset

将锚点运用到边境框维度中:

 #log space transform height and the width
 anchors = torch.FloatTensor(anchors)

 if CUDA:
 anchors = anchors.cuda()

 anchors = anchors.repeat(grid_size*grid_size, 1).unsqueeze(0)
 prediction[:,:,2:4] = torch.exp(prediction[:,:,2:4])*anchors

将 sigmoid 激活函数运用到种别分数中:

 prediction[:,:,5: 5 + num_classes] = torch.sigmoid((prediction[:,:, 5 : 5 + num_classes]))

着末,我们要将检测图的大小调解到与输入图像大小同等。边境框属性依据特征图的大小而定(如 13 x 13)。假如输入图像大小是 416 x 416,那么我们将属性乘 32,或乘 stride 变量。

prediction[:,:,:4] *= stride

loop 部分到这里就大致完毕了。

函数完毕时会返回预测结果:

 return prediction

重械烂问的检测层

我们曾经变换了输出张量,现可以将三个差别标准的检测图级联成一个大的张量。当心这必需变换之后举行,因为你无法级联差别空间维度的特征图。变换之后,我们的输出张量把边境框外格呈现为行,级联就比较可行了。

一个妨碍是我们无法初始化空的张量,再向其级联一个(差别样式的)非空张量。于是,我们推迟搜罗器(容纳检测的张量)的初始化,直到取得第一个检测图,再把这些检测图级联起来。

当心 write = 0 函数 forward 的 loop 之前。write flag 外示我们是否碰到第一个检测。假如 write 是 0,则搜罗器尚未初始化。假如 write 是 1,则搜罗器曾经初始化,我们只需求将检测图与搜罗器级联起来即可。

现,我们具备了 predict_transform 函数,我们可以写代码,处理 forward 函数中的检测特征图。

darknet.py 文献的顶部,添加以下导入项:

from util import * 

然后 forward 函数中定义:

 elif module_type == 'yolo': 

 anchors = self.module_list[i][0].anchors
 #Get the input dimensions
 inp_dim = int (self.net_info["height"])

 #Get the number of classes
 num_classes = int (module["classes"])

 #Transform 
 x = x.data
 x = predict_transform(x, inp_dim, anchors, num_classes, CUDA)
 if not write: #if no collector has been intialised. 
 detections = x
 write = 1

 else: 
 detections = torch.cat((detections, x), 1)

 outputs[i] = x

现,只需返回检测结果。

 return detections

测试前向传达

下面的函数将创立一个伪制的输入,我们可以将该输入传入我们的收集。写该函数之前,我们可以运用以下命令行将这张图像保管到义务目次:

wget https://github.com/ayooshkathuria/pytorch-yolo-v3/raw/master/dog-cycle-car.png

也可以直接下载图像:https://github.com/ayooshkathuria/pytorch-yolo-v3/raw/master/dog-cycle-car.png

现, darknet.py 文献的顶部定义以下函数:

def get_test_input():
 img = cv2.imread("dog-cycle-car.png")
 img = cv2.resize(img, (416,416)) #Resize to the input dimension
 img_ = img[:,:,::-1].transpose((2,0,1)) # BGR -> RGB | H X W C -> C X H X W 
 img_ = img_[np.newaxis,:,:,:]/255.0 #Add a channel at 0 (for batch) | Normalise
 img_ = torch.from_numpy(img_).float() #Convert to float
 img_ = Variable(img_) # Convert to Variable
 return img_

我们需求键入以下代码:

model = Darknet("cfg/yolov3.cfg")
inp = get_test_input()
pred = model(inp)
print (pred)

你将看到如下输出:

( 0 ,.,.) = 
 16.0962 17.0541 91.5104 ... 0.4336 0.4692 0.5279
 15.1363 15.2568 166.0840 ... 0.5561 0.5414 0.5318
 14.4763 18.5405 409.4371 ... 0.5908 0.5353 0.4979
  ... 
 411.2625 412.0660 9.0127 ... 0.5054 0.4662 0.5043
 412.1762 412.4936 16.0449 ... 0.4815 0.4979 0.4582
 412.1629 411.4338 34.9027 ... 0.4306 0.5462 0.4138
[torch.FloatTensor of size 1x10647x85]

张量的样式为 1×10647×85,第一个维度为批量大小,这里我们只运用了单张图像。关于批量中的图像,我们会有一个 100647×85 的外,它的每一行外示一个边境框(4 个边境框属性、1 个 objectness 分数和 80 个种别分数)。

现,我们的收集有随机权重,而且不会输出准确的种别。我们需求为收集加载权重文献,于是可以应用官方权重文献。

下载预教练权重

下载权重文献并放入检测器目次下,我们可以直接运用命令行下载:

wget https://pjreddie.com/media/files/yolov3.weights

也可以通过该地址下载:https://pjreddie.com/media/files/yolov3.weights

了解权重文献

官方的权重文献是一个二进制文献,它以序列方法储存神经收集权重。

我们必需小心地读取权重,因为权重只是以浮点方式储存,没有其它新闻能告诉我们终究它们属于哪一层。以是假如读取过失,那么很可以权重加载就全错了,模子也完备不行用。于是,只阅读浮点数,无法区别权重属于哪一层。于是,我们必需了解权重是怎样存储的。

起首,权重只属于两品种型的层,即批归一化层(batch norm layer)和卷积层。这些层的权重储存序次和配备文献中定义层级的序次完备相同。以是,假如一个 convolutional 后面跟跟着 shortcut 块,而 shortcut 连接了另一个 convolutional 块,则你会希冀文献包罗了先前 convolutional 块的权重,其后则是后者的权重。

当批归一化层呈现卷积模块中时,它是不带有偏置项的。然而,当卷积模块不保管批归一化,则偏置项的「权重」就会从文献中读取。下图展现了权重是怎样储存的。

加载权重

我们写一个函数来加载权重,它是 Darknet 类的成员函数。它运用 self 以外的一个参数举措权重文献的道径。

def load_weights(self, weightfile):

第一个 160 比特的权重文献保管了 5 个 int32 值,它们构成了文献的标头。

 #Open the weights file
 fp = open(weightfile, "rb")

 #The first 5 values are header information 
 # 1. Major version number
 # 2. Minor Version Number
 # 3. Subversion number 
 # 4,5. Images seen by the network (during training)
 header = np.fromfile(fp, dtype = np.int32, count = 5)
 self.header = torch.from_numpy(header)
 self.seen = self.header[3]

之后的比特代外权重,按上述序次排列。权重被保管为 float32 或 32 位浮点数。我们来加载 np.ndarray 中的盈余权重。

 weights = np.fromfile(fp, dtype = np.float32)

现,我们迭代地加载权重文献到收集的模块上。

 ptr = 0
 for i in range(len(self.module_list)):
 module_type = self.blocks[i + 1]["type"]

 #If module_type is convolutional load weights
 #Otherwise ignore.

轮回进程中,我们起首反省 convolutional 模块是否有 batch_normalize(True)。基于此,我们加载权重。

 if module_type == "convolutional":
 model = self.module_list[i]
 try:
 batch_normalize = int(self.blocks[i+1]["batch_normalize"])
 except:
 batch_normalize = 0

 conv = model[0]

我们保持一个称为 ptr 的变量来追踪我们权重数组中的位置。现,假如 batch_normalize 反省结果是 True,则我们按以系澜式加载权重:

 if (batch_normalize):
 bn = model[1]

 #Get the number of weights of Batch Norm Layer
 num_bn_biases = bn.bias.numel()

 #Load the weights
 bn_biases = torch.from_numpy(weights[ptr:ptr + num_bn_biases])
 ptr += num_bn_biases

 bn_weights = torch.from_numpy(weights[ptr: ptr + num_bn_biases])
 ptr += num_bn_biases

 bn_running_mean = torch.from_numpy(weights[ptr: ptr + num_bn_biases])
 ptr += num_bn_biases

 bn_running_var = torch.from_numpy(weights[ptr: ptr + num_bn_biases])
 ptr += num_bn_biases

 #Cast the loaded weights into dims of model weights. 
 bn_biases = bn_biases.view_as(bn.bias.data)
 bn_weights = bn_weights.view_as(bn.weight.data)
 bn_running_mean = bn_running_mean.view_as(bn.running_mean)
 bn_running_var = bn_running_var.view_as(bn.running_var)

 #Copy the data to model
 bn.bias.data.copy_(bn_biases)
 bn.weight.data.copy_(bn_weights)
 bn.running_mean.copy_(bn_running_mean)
 bn.running_var.copy_(bn_running_var)

假如 batch_normalize 的反省结果不是 True,只需求加载卷积层的偏置项。

 else:
 #Number of biases
 num_biases = conv.bias.numel()

 #Load the weights
 conv_biases = torch.from_numpy(weights[ptr: ptr + num_biases])
 ptr = ptr + num_biases

 #reshape the loaded weights according to the dims of the model weights
 conv_biases = conv_biases.view_as(conv.bias.data)

 #Finally copy the data
 conv.bias.data.copy_(conv_biases)

着末,我们加载卷积层的权重。

#Let us load the weights for the Convolutional layers
num_weights = conv.weight.numel()

#Do the same as above for weights
conv_weights = torch.from_numpy(weights[ptr:ptr+num_weights])
ptr = ptr + num_weights

conv_weights = conv_weights.view_as(conv.weight.data)
conv.weight.data.copy_(conv_weights)

该函数的先容到此为止,你现可以通过调用 darknet 对象上的 load_weights 函数来加载 Darknet 对象中的权重。

model = Darknet("cfg/yolov3.cfg")
model.load_weights("yolov3.weights")

通过模子构修和权重加载,我们毕竟可以开端举行目标检测了。未来,我们还将先容怎样应用 objectness 置信度阈值和非极大值遏止生成最终的检测结果。

原文链接:https://medium.com/paperspace/tutorial-on-implementing-yolo-v3-from-scratch-in-pytorch-part-1-a0054d38ec78

工程教程完成YOLO
385
保藏
"我们的实行中为-1 和 61,于是该层级将输出过去一层级(-1)到第 61 层的特征图,并将它们按深度拼接。" 这段话仿佛翻译的有点题目,原文中是把前一层和第61层的特征图举行拼接,只是这两层,而不是指定延续层举行拼接!
确实是如许,这篇作品写的跟精细了,翻译也还可以,不过有些小过失,有些地方翻译的无缘无故
请问哪里下代码?
只可说翻译的很难读……我还不如看原文
1