本节翻译自:https://blog.paperspace.com/how-to-implement-a-yolo-v3-object-detector-from-scratch-in-pytorch-part-3/

前一节我们实现了YOLO结构中不同类型的层,这一节我们将用Pytorch来实现整个YOLO结构,定义网络的前向传播过程,最终能够实现给定一张图片获得检测输出。

这个项目使用python 3.5与Pytorch 0.4进行编写,官方地址

必备条件:

  1. 本教程的part1与part2
  2. Pytorch的基本知识,包括如何使用nn.Module,nn.Sequential,torch.nn.parameter类构建常规的结构
  3. 使用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)

这里的类Darknet继承自nn.Modulel类。我们初始化类成员变量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、input x和CUDA,如果为CUDA真,将使用GPU加速前向过程。

我们从self.blocks[1:]开始迭代,这是因为self.blocks的第一个元素是net块,不是前向传播的内容。

Route和Shortcut层需要来自之前层的输出图,现在我们通过字典outputs缓存每一层的输出特征图,字典的键就是层的索引,值为特征图。

与create_modules函数一样,遍历包含网络模块的module_list。这里需要注意的是,模块的添加顺序与配置文件相同。这表示我们可以简单地传过每个模块来获得输出。

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

Convolutional 和 Upsample 层:

如果模块为卷积模块或者是上采样模块,前传直接写成:

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

Route / Shortcut 层:

route的代码分为两种情况(见part2)。对于连接两个特征图这种情况我们使用torch.cat函数,第二个参数为1。这是因为我们希望在深度维上进行拼接(对于Pytorch来说维度为B*C*H*W,所以深度对应着第1维)。

 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的输出是一个卷积特征图,特征图的深度方向是边界框的属性,cell预测的边界框属性彼此堆叠。因此,如果你要访问(5,6)处cell的第二个边界框,则必须通过map[5,6,(5+C):2*(5+C)] 来找到值。这种形式对于输出处理非常不方便,如对象置信度阈值化、向中心坐标添加网格偏移量、应用锚点等。

另一个问题是由于我们在三种尺度上做检测,预测图的维度将会不同。虽然这三个feature map的维度不同,但是要对它们执行的输出处理操作是相似的。最好是在一个张量上做这些运算,而不是在三个张量上。为了解决这些问题,我们引入了函数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

predict_transform接收五个参数;prediction(我们的输出),inp_dim(输入图片维度),anchors, num_classes 和 CUDA标志位。

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

predict_transform函数接收一个检测的特征映射将其转换为一个2维的tensor,这里每行的tensor对应到一个边界框的属性,以如下形式:

以下的代码来做上面的转换。

 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块的高度与宽度一致(使用的是[10,13] , [16,30], [33,23],这个的单位其实就是图片真实像素值),因此我们必须将锚除以检测特征图的步长。

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

现在,我们需要像Part1中讨论的那样转换我们的输出。

对x,y坐标与目标置信度进行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]))

最后一步,是将检测图放大到输入图片相同的尺寸。这里的边界框属性是根据特征图调整大小的。如果输入图片是416*416,特征图是13*13,我们就是将这些属性乘以步长也就是32.

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

这就是全部循环体的内容,在函数的结尾返回预测。

return prediction

Detection层回顾:

现在我们已经对输出向量进行了转换,能将三个不同尺寸下的检测图拼接组成一个大tensor。转换之前是做不到这点的,因为我们不能将拥有不同空间维的特征图拼接在一起。但现在我们的输出向量就像一个表格一样,以边界框作为它的行拼接起来就简单了。

我们在编程上遇到的一个障碍是不能初始化一个空向量,然后将它与一个非空向量拼接在一起。所以我们等到获得了第一张检测图后才初始化collector(保存检测的tensor),然后获得后续检测时才将特征映射拼接在一起。

注意write=0的标志位位于forward函数循环体之前。write标志位指明了我们是否获得了第一张检测图,write=0也就意味着还没有获得第一张检测图,当它为1的时候代表collector已经被初始化可以直接拼接检测图。

现在我们已经实现了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

测试前向传播过程:

这里有个创建虚拟输入的函数,我们将把这个输入到我们的网络。在写这个函数之前先将下面这张图片保存到你的工作路径。如果你是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]

这个tensor的维度为1*10647*85。第一维是批大小,因为我们只有一张图片所以是1。对于每张图片,我们都有10647*85的输出表格。表格的每一行代表了一个边界框(4个bbox属性,1个目标置信度,80个类得分)。

到目前为止,我们的网络都是随机的权重,所以不会产生正确的输出。下面我们将导入官方的权重文件。

下载预训练权重:

将预训练权重下载到你的检测器文件夹。可以点这里下载。如果你是Linux用户:

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

理解权重文件:

官方的权重文件是以串行方式存储的二进制文件。

我们必须非常准确地加载权重。因为它仅仅作为浮点数存储,没有任何东西可以指导我们它属于哪个层,如果弄错了不会出现任何提示,比如说你可能把一个batch norm层的权值加载到卷积层了。因此,我们必须理解权重是如何存储的。

首先,权重只属于两种类型的层,要么是batch norm层,要么是卷积层。

这些层的权重完全按照它们在配置文件中出现的顺序存储。比如说一个卷积后面跟着一个Shortcut块,然后这个Shortcut块后面跟着另一个卷积块,权重文件也是先包含前一个卷积块的权重,然后是后一个卷积块的权重。

当batch norm层出现在卷积块中时,没有偏置参数。但是当没有batch norm层时,必须从文件中读取偏置的“权重”。

下图总结了权重是如何存储的。

 

加载权重

让我们来写程序加载权重吧!它作为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-bit foat的形式存储的,现在使用一个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_normalize非真,直接加载卷积层的偏置

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_wights函数

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

这一部分到此结束,通过构建模型加载权重,我们终于能够检测目标了。在下一部分,你会学习如何使用目标置信度与非极大值抑制来产生我们最后的检测集合。

Further Reading

  1. PyTorch tutorial
  2. Reading binary files with NumPy
  3. nn.Module, nn.Parameter classes

Pytorch从0开始实现YOLO V3指南 part3——实现网络前向传播的更多相关文章

  1. Pytorch从0开始实现YOLO V3指南 part2——搭建网络结构层

    本节翻译自:https://blog.paperspace.com/how-to-implement-a-yolo-v3-object-detector-from-scratch-in-pytorch ...

  2. Pytorch从0开始实现YOLO V3指南 part1——理解YOLO的工作

    本教程翻译自https://blog.paperspace.com/how-to-implement-a-yolo-object-detector-in-pytorch/ 视频展示:https://w ...

  3. Pytorch从0开始实现YOLO V3指南 part5——设计输入和输出的流程

    本节翻译自:https://blog.paperspace.com/how-to-implement-a-yolo-v3-object-detector-from-scratch-in-pytorch ...

  4. Pytorch从0开始实现YOLO V3指南 part4——置信度阈值和非极大值抑制

    本节翻译自:https://blog.paperspace.com/how-to-implement-a-yolo-v3-object-detector-from-scratch-in-pytorch ...

  5. YOLO v3

    yolo为you only look once. 是一个全卷积神经网络(FCN),它有75层卷积层,包含跳跃式传递和降采样,没有池化层,当stide=2时用做降采样. yolo的输出是一个特征映射(f ...

  6. 一文看懂YOLO v3

    论文地址:https://pjreddie.com/media/files/papers/YOLOv3.pdf论文:YOLOv3: An Incremental Improvement YOLO系列的 ...

  7. (转载)PyTorch代码规范最佳实践和样式指南

    A PyTorch Tools, best practices & Styleguide 中文版:PyTorch代码规范最佳实践和样式指南 This is not an official st ...

  8. 深度学习笔记(十三)YOLO V3 (Tensorflow)

    [代码剖析]   推荐阅读! SSD 学习笔记 之前看了一遍 YOLO V3 的论文,写的挺有意思的,尴尬的是,我这鱼的记忆,看完就忘了  于是只能借助于代码,再看一遍细节了. 源码目录总览 tens ...

  9. Yolo V3整体思路流程详解!

    结合开源项目tensorflow-yolov3(https://link.zhihu.com/?target=https%3A//github.com/YunYang1994/tensorflow-y ...

随机推荐

  1. 【Azure Developer】使用 CURL 获取 Key Vault 中 Secrets 中的值

    问题描述 在使用CURL通过REST API获取Azure Key Vaualt的Secrets值,提示Missing Token, 问如何来生成正确的Token呢? # curl 命令 curl - ...

  2. 教你轻松解决CSRF跨站请求伪造攻击

    摘要:CSRF(Cross-site request forgery)跨站请求伪造,通过伪装来自受信任用户的请求来利用受信任的网站.与XSS攻击相比,CSRF攻击往往不大流行(因此对其进行防范的资源也 ...

  3. QCustomPlot开发笔记(一):QCustomPlot简介、下载以及基础绘图

    前言   QCustomPlot开发笔记系列整理集合,这是目前使用最为广泛的Qt图表类(Qt的QWidget代码方向只有QtCharts,Qwt,QCustomPlot),使用多年,系统性的整理,过目 ...

  4. 面试官问:浏览器输入 URL 回车之后发生了什么?

    一个执着于技术的公众号 前言 这个问题已经是老生常谈了,更是经常被作为面试的压轴题出现,网上也有很多文章,但最近闲的无聊,然后就自己做了一篇笔记,感觉比之前理解更透彻了. 注意:本文的步骤是建立在,请 ...

  5. 【Linux 网络编程】生动讲解 Reactor 模式与 Proactor 模式

    五种 I/O 模型 先花费点时间了解这几种 I/O 模型,有助于后面的理解. 阻塞 I/O 与非阻塞 I/O 阻塞和非阻塞的概念能应用于所有的文件描述符,而不仅仅是 socket.我们称阻塞的文件描述 ...

  6. go ants源码分析

    golang ants 源码分析 结构图 poolwithfunc与pool相差不大,这里我们只分析ants默认pool的流程 文件 作用 ants.go 定义常量.errors显示.默认建一个大小为 ...

  7. 馒头的1day漏洞巡舰系统

    馒头的1day漏洞巡舰系统 1day漏洞巡舰系统的目的 1day漏洞巡舰系统的可行性 1day漏洞巡舰系统所用技术 1day漏洞巡舰系统思维导图 1day漏洞巡舰系统初步设计图 1day漏洞巡舰系统初 ...

  8. 消息队列,IPC机制(进程间通信),生产者消费者模型,线程及相关

    消息队列 创建 ''' Queue是模块multiprocessing中的一个类我们也可以这样导入from multiprocessing import Queue,创 建时queue = Queue ...

  9. mysql allow remote host

    GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY 'password' WITH GRANT OPTION; FLUSH PRIVILEG ...

  10. Springboot启动类及注解说明

    Spring boot的启动是基于main方法的,其主要注解为: 1. @springBootApplication:项目的启动注解,是一个组合注解,包含@SpringbootConfiguratio ...