前言
本系列教程的第3部分介绍了如何从头开始在PyTorch中实现YOLO v3目标检测器。
这是从头开始实现YOLO v3检测器的教程的第3部分。
在上一个博客中,我们逐层实现了YOLO架构中使用的组件,在这一部分中,我们将在PyTorch中实现YOLO的网络架构,以便我们可以根据图像生成输出。
我们的目标是设计网络的前向传递。
本教程的代码旨在在Python 3.7和PyTorch 1.1.0 上运行。 可以在此Github存储库中找到全部内容。
本教程分为5部分:
第1部分:了解YOLO的工作方式
第2部分:创建网络体系结构的各层
第三部分(这一部分):实现网络的前向传递
第4部分:客观置信度阈值和非最大抑制
第5部分:设计输入和输出管道
目录
先决条件
本教程的第1部分和第2部分。
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。 我们使用成员,块,net_info和module_list初始化网络。
前向传递模块
网络的前向传递是通过重写nn.Module类的forward方法来实现的。
前向传递模块有两个功能。 首先,计算输入经过网络后的输出,其次,以一种易于处理的方式对输出进行转换检测特征图(例如对其进行转换,以便可以连接多个比例的特征图,否则将无法进行,因为它们 具有不同的尺寸)。
def forward(self, x, CUDA):
modules = self.blocks[1:]
outputs = {}
#We cache the outputs for the route layer
forward接受三个参数self,输入x和CUDA,如果CUDA值为true,则将使用GPU加速前向传递。
在这里,我们迭代self.blocks [1:]而不是self.blocks,因为self.blocks的第一个元素是网络的描述信息,它不是前向传递的一部分。
由于路由层和快速图层需要来自先前图层的输出特征图,
我们在dict输出中缓存每一层的输出特征图。
关键点是图层的索引,值是特征图
与create_modules函数一样,我们现在迭代包含网络模块的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)
路由层/快捷层
如果您查看路由层的代码,我们必须考虑两种情况(如第2部分所述)。
对于必须连接两个特征图的情况,我们使用torch.cat函数,第二个参数为1。
这是因为我们要沿着深度将特征图串联起来。
(在PyTorch中,卷积层的输入和输出格式为’B X C X H XW’。
对应于通道尺寸的深度)。
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类的前面使用该函数时,将导入该函数。
将导入添加到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
Forecast_transform接受5个参数;
预测(我们的输出),
inp_dim(输入图像尺寸),
锚点
num_classes,
和一个可选的CUDA标志
def predict_transform(prediction, inp_dim, anchors, num_classes, CUDA = True):
预测转换函数获取检测特征图并将其转换为二维张量,
张量的每一行都按照以下顺序对应于丰富框的属性。
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)
锚的尺寸取决于网块的高度和宽度属性。
这些属性描述了输入图像的尺寸,该尺寸比检测图大(跨度大)。
因此,我们必须将锚点除以检测特征图的步幅。
anchors = [(a[0]/stride, a[1]/stride) for a in anchors]
现在,我们需要根据在第1部分中讨论的方程对输出进行变换。将x,y坐标和客观性得分作为S形。
#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
将锚应用于边界框的尺寸。
prediction[:,:,5: 5 + num_classes] = torch.sigmoid((prediction[:,:, 5 : 5 + num_classes]))
我们在这里要做的最后一件事是将检测图调整为输入图像的大小。
此处的边界框属性根据要素图(例如13 x 13)调整大小。
如果输入图像为416 x 416,我们将属性乘以32或跨步变量。
prediction[:,:,:4] *= stride
循环主体到此结束。
在函数末尾返回预测。
return prediction
再谈检测层
现在我们已经改变了输出张量,
我们现在可以将三个不同比例的检测图连接成一个大张量。
请注意,在我们进行转换之前这是不可能的,因为不能将具有不同空间尺寸的要素图连接在一起。
但是从现在开始,我们的输出张量仅充当表,并带有边界框作为行,连接是非常有可能的。
我们遇到的一个障碍是我们无法初始化一个空的张量,然后将一个非空的(不同形状的)张量连接到它。因此,我们延迟收集器(保存检测值的张量)的初始化,直到获得我们的第一个检测图,然后在获得后续检测时连接到与其映射。
请注意,在函数正向循环之前,write = 0行。 write标志用于指示我们是否遇到了第一次检测。如果write为0,则表示收集器尚未初始化。
如果为1,则表示收集器已初始化,我们可以将检测映射连接到该收集器。
现在,我们已经使用predict_transform函数做好了准备,我们在forward函数中编写了用于处理检测特征图的代码。
在darknet.py文件的顶部,添加以下导入。
from util import *
Then, in the forward function.
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
测试前向传递
这是一个创建虚拟输入的函数。
我们将把这个输入传递给我们的网络。
在编写此功能之前,请将此图像保存到您的工作目录中。
如果您使用的是Linux,请输入。
wget 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, torch.cuda.is_available())
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 x 10647 x 85。
第一维是批处理大小,因为我们使用了单个图像,所以批量大小仅为1。
对于批次中的每个图像,我们都有一个10647 x 85的表格。
每个表的行都表示一个边界框。
(4个bbox属性,1个客观分数和80个课堂分数)
在这一点上,我们的网络具有随机权重,因此不会产生正确的输出。
我们需要在网络中加载权重文件。
为此,我们将使用官方重量文件。
下载预训练的权重
将权重文件下载到检测器目录中。 从这里获取权重文件。 或者,如果您使用的是Linux,
wget https://pjreddie.com/media/files/yolov3.weights
了解权重文件
官方权重文件是二进制文件,其中包含以串行方式存储的权重。
阅读重量时必须格外小心。
权重只是存储为浮点数,没有任何东西可以指导我们它们属于哪一层。
如果您搞砸了,那么就没有什么可以阻止您将批处理规范层的权重加载到卷积层的权重中。
由于您只读取浮点数,因此无法区分哪个权重属于哪一层。
因此,我们必须了解权重的存储方式。
首先,权重仅属于两种类型的层,即批处理规范层或卷积层。
这些图层的权重存储的顺序与配置文件中出现的顺序完全相同。
因此,如果一个卷积后面紧跟着一个快捷方式块,然后在快捷方式块后面紧跟着另一个卷积块,则您将期望文件包含前一个卷积块的权重,然后是后者。
当批处理规范层出现在卷积块中时,就没有偏差。
但是,当没有批处理规范层时,必须从文件中读取偏差“权重”。
下图总结了权重如何存储权重。
加载权重
让我们写一个函数的负载权重。 这将是Darknet类的成员函数。 除了self以外,它将使用一个参数,即weightsfile的路径。
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.
进入循环,我们首先检查卷积块是否具有batch_normalise 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 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_norm不正确,则只需加载卷积层的偏差。
lse:
#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")
这就是全部内容,通过构建模型并加载权重,我们终于可以开始检测对象了。 在下一部分中,我们将介绍使用客观性置信度阈值和非最大抑制来产生最终的检测集。
进一步阅读
PyTorch教程
用NumPy读取二进制文件
nn.Module,nn.Parameter类