这是关于从头实现YOLO v3检测器的第3部分。在上一部分中,我们实现了YOLO架构中使用的图层,在这一部分中,我们将在PyTorch中实现YOLO的网络架构,这样我们就可以产生一个给定图片的输出。
我们的目标是设计网络的前向传播。
本教程设计的代码在Python 3.5和PyTorch 0.4上运行。完整代码可以在这里找到 Github repo.
本教程分为5个部分:
- Part 1 : Understanding How YOLO works
- Part 2 : Creating the layers of the network architecture
- Part 3 (This one): Implementing the the forward pass of the network
- Part 4 : Objectness Confidence Thresholding and Non-maximum Suppression
- Part 5 : Designing the input and the output pipelines
预备知识
- 课程Part 1 和 Part 2 。
- 基本的PyTorch工作知识,包括如何使用nn.Module、nn.sequence和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。我们使用成员 blocks, 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,哪一个为真,将使用GPU来加速前向传播。
在这里,我们迭代self.blocks[1:]而不是self.blocks,因为self.blocks的第一个元素是一个不属于前向传播的net块。
由于 route 和 shortcut需要来自前一层的输出映射,我们将每个层的输出特征映射缓存到一个dictoutput中。键是层的索引,值是特征映射。
与create_modules函数一样,现在我们遍历包含网络模块的module_list。这里需要注意的是,添加模块的顺序与配置文件中的顺序相同。这意味着,我们可以简单地通过每个模块运行输入来获得输出。
write = 0 #This is explained a bit later
for i, module in enumerate(modules):
module_type = (module["type"])
Convolutional and Upsample Layers
如果模块是卷积模块或upsample模块,这就是向前传播的工作方式。
if module_type == "convolutional" or module_type == "upsample":
x = self.module_list[i](x)
Route Layer / Shortcut Layer
如果您查看路由层的代码,我们必须考虑两种情况(如第2部分所述)。对于必须连接两个feature map的情况,我们使用torch.cat函数,第二个参数为1。这是因为我们想要沿着深度连接特征映射。(在PyTorch中,卷积层的输入和输出的格式为B×C×H×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 (Detection Layer)
YOLO的输出是一个卷积feature map,它包含沿feature map深度的边框属性。单元格预测的属性边界框是一个接一个堆叠在一起的。因此,如果必须访问单元格的第二个边界(5,6),则必须通过map[5,6,(5+C): 2*(5+C)]对其进行索引。这种格式对于对象置信度的阈值化、向中心添加网格偏移量、应用锚点等输出处理非常不方便。
另一个问题是,由于探测发生在三个尺度上,所以预测图的维数会有所不同。虽然这三个feature map的尺寸不同,但是要对它们执行的输出处理操作是相似的。在一个张量上做这些运算会更好,而不是在三个单独的张量上。
为了解决这些问题,我们引入了函数predict_transform。
Transforming the output
函数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标志。
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)
锚的尺寸与网块的高度和宽度属性一致。这些属性描述了输入图像的尺寸,它比检测图大(以步幅的系数计算)。
因此,我们必须用检测特征图的步幅来划分锚点。
anchors = [(a[0]/stride, a[1]/stride) for a in anchors]
现在,我们需要根据第1部分中讨论的方程转换输出。
用Sigmoid 处理x、y坐标和目标得分。
#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]))
我们在这里要做的最后一件事是将检测图的大小调整为输入图像的大小。 此处的边界框属性根据feature map(例如,13 x 13)调整大小。 如果输入图像是416 x 416,我们将属性乘以32或stride变量。
prediction[:,:,:4] *= stride
到此循环体结束。
返回函数末尾的预测。
return prediction
Detection Layer Revisited
现在我们已经转换了输出张量,现在我们可以将三种不同尺度的检测图连接成一个大张量。 请注意,在转换之前这是不可能的,因为无法连接具有不同空间维度的feature map。 但是从现在开始,我们的输出张量就像一个表格一样,用包围框作为它的行,连接是非常可能的。
我们的一个障碍是我们不能初始化一个空张量,然后将一个非空(不同形状)张量连接到它上面。 因此,我们延迟收集器的初始化(包含检测的张量),直到我们得到第一个检测图,然后在我们得到后续检测时连接到它的映射。
注意函数forward循环之前的write = 0行。write标志用于指示我们是否遇到了第一次检测。如果write为0,则表示收集器尚未初始化,如果它是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
测试前向传播
这是一个创建虚拟输入的函数。 我们会将此输入传递给我们的网络。 在编写此函数之前,请将此图像 image 保存到工作目录中。 如果您使用的是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)) #大小调整为输入维度
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() #转换为浮点数
img_ = Variable(img_) #转换为变量
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个类分数)
此时,我们的网络具有随机权值,不会产生正确的输出。我们需要在网络中加载一个权重文件。为此,我们将使用官方权重文件。
下载预训练权重
将权重文件下载到检测器目录中。从这里 here.获取权重文件。如果你使用linux。
wget https://pjreddie.com/media/files/yolov3.weights
理解权重文件
官方权重文件是二进制文件,它包含以串行方式存储的权重。
必须极其小心地读出权重。权重只是作为浮点数存储,没有任何东西可以指导我们它们属于哪个层。如果你搞砸了没有什么能阻止你,比如说,将批处理规范层的权值加载到卷积层的权值中。因为您只读取浮点数,所以无法区分哪个权重属于哪个层。因此,我们必须理解权重的存储方式。
首先,权值只属于两种类型的层,一种是批处理规范层,另一种是卷积层。
这些层的权重存储的顺序与它们在配置文件中出现的顺序完全相同。因此,如果一个 convolutional后面跟着一个 shortcut ,然后这个 shortcut 后面跟着另一个convolutional,那么文件应该包含前一个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.
在循环中,我们首先检查卷积块batch_normalize是否为真。在此基础上,我们加载权重。
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为真,则按以下方式加载权重。
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不为真,则只需加载卷积层的偏差。
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")
以上就是本部分的全部内容,在构建了模型并加载了权重之后,我们终于可以开始检测对象了。在下一部分中,我们将介绍如何使用对象置信度阈值和非最大抑制来生成最终的检测集。
扩展阅读
- PyTorch tutorial
- Reading binary files with NumPy
- nn.Module, nn.Parameter classes