Part 3:网络的前向传播

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

本篇文章是《如何使用PyTorch从零开始实现YOLO(v3)目标检测算法》的第三部分。这系列论文一共有五篇文章,五篇文章的主要内容在下文中有涉及。如果有问题欢迎和我交流~


如何使用PyTorch从零开始实现YOLO(v3)目标检测算法?

这是从零开始实现YOLO v3检测器的教程的第三部分。在上一部分,我们使用YOLO的网络架构实现了YOLO的五个网络层,在这一部分,我们打算使用PyTorch实现YOLO的网络架构:当我们输入一张图片的时候,可以产生一个输出。我们的目标是去设计一个网络的前向传播。

1 设计网络

我之前说过,在PyTorch中,我们使用nn.Module类去构建自定义的网络架构。让我们为我们的目标检测器定义一个网络吧。在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.class,而且已经继承了nn.Module这个类。我们用成员blocks, net_info, 和 module_list初试化这个网络。

2 实现网络的前向传播

通过重写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,如果CUDA为真,那么将会使用CPU去加快前向传播。这里,我们迭代了self.bolcks[1:]而不是self.bolcks,因为self.blocks的第一个元素是net网络块,它并不是前行传播的一部分。因为路由层和捷径层需要前面网络层的输出映射图,因此我们将每一层的输出特征图都保存在字典outputs中。字典的键值就是这个层的目录,值就是特征图。正如create_modules函数,现在我们遍历module_list,它包含这个网络的网络模型。需要注意的是模型存放的顺序和他们在配置文件的顺序是一致的,这意味着我们可以通过每一个模块去运行我们的输入来得到我们的输出结果。

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

2.1 卷积层和上采样层

      如果这个模型是卷积层或者是上采样层,这就是前向传播工作的方式

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

2.2 路由层/捷径层

      如果你看一下路由层的代码,我们就不得不考虑两种情况(就像第二部分表述的那样)。一种情况是我们要去连接两个特征图,我们使用torch.cat函数,它的第二个参数置为1。这是因为我们要沿着深度(torch.cat())来连接特征图(在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]]
                # 这边不是两个特征图进行加法运算,而是along depth进行连接,1是指c的索引
                x = torch.cat((map1, map2), 1)

        elif  module_type == "shortcut":
            from_ = int(module["from"])
            x = outputs[i-1] + outputs[i+from_]
            # 这边提供另外一个思路,列表可以使用倒序进行索引
            # x = outputs[-1]+outputs[from_]

2.3 YOLO层(检测层)

YOLO层的输出是一个卷积的特征图,它包含沿着特征图深度方向的边界框属性。由一个单元预测的边界框属性一个一个的被堆叠在一起。所以,如果你想得到位于(5,6)的单元的第二个边界框,你会通过map[5,6,(5+c):2*(5+c)]索引得到它(索引三维的张量太麻烦了)。这样的形式来处理输出非常的不方便,比如通过置信度确定阈值,为中心点添加栅格的偏移,使用锚框等等。

另一个问题是,因为检测出现三个尺度,预测特征图的尺寸也不相同。尽管三个特征图尺寸是不一样的,但是对他们的输出处理操作是相似的。在一个单独的张量中对他们进行运算要好,而不是在三个单独的的张量中要好。为了解决这个问题,我们介绍了predict_transform函数。

3 改变输出

函数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_transfor接收五个参数: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)
    
    # B*C*H*W --> B*C*(H*W) --> B*(H*W)*((num_classes+5)*3) --> B*(H*W*3)*(num_classes+5)
    prediction = prediction.view(batch_size, bbox_attrs*num_anchors, grid_size*grid_size)
    # 在PyTorh中,经过transpose之后数据就变得离散了,因此需要经过contiguous将其变成连续的数据
    prediction = prediction.transpose(1,2).contiguous()
    prediction = prediction.view(batch_size, grid_size*grid_size*num_anchors, bbox_attrs)

      锚框的维度对应着net网络块的高度和宽度属性。这些属性描述了输入图片的尺寸,它们比检测特征图的尺寸要大(因为步长的原因)。因此必须锚框要除以检测特征图的步长。

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

      现在,我们要根据在第一部分讨论的方程来改变我们的输出

    #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)
    # 可以通过生成式直接将其转换成FloatTensor数据
    # grid_x = torch.FloatTensor([i for i in range(0,width)])
    # grid_y = torch.FloatTensor([i for i in range(0,height)])

    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*13)。如果输入图片的尺寸是416*416,我们要将属性乘以32,或者是乘以变量stride。

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

      然后结束了这个循环的主体,在函数的最后返回预测值

    return prediction

4 重新审视检测层

      现在我们已经改变数据形状了,我们现在要把三个尺度的特侧特征图连接成一个大的张量。注意在转换之前这是不可能的,因为不能将不同空间尺寸的特征图连接成一个张量。但是现在,我们的输出张量仅仅就像一个表格一样,它的行存放边界框属性,连接就变得非常有可能了。

      在处理过程中一个问题是,在我们得到第一个检测图之前,我们不能初始化空的张量,然后和一个非空的(不同形状的)的张量连接在一起(换句话说,只有当我们运行到检测层的时候,我们才能够将输出进行改变)。我们将收集器(保存检测器的张量)的初始化放在一边,直到我们得到了第一个检测特征图,当我们得到后续的检测器的时候,然后再将特征图和它连接在一起。

      注意,在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

注意:cat是将两个tensor进行连接,dim=0表示横向,dim=1表示纵向连接     

现在,简单地返回检测器

    return detections

小结:

(1) 在route层中,数据是逐深度(along depth)进行添加的

(2)为了方便索引,我们需要改变数据的形状:由三维数据转换成二维数据

(3)在创建新的张量的时候:要创建FloatTensor张量,因为在PyTorch中,只能浮点数才可以进行求导

(4)三个检测层输出的数据也是逐深度进行添加的

 

5 检验前向传播

       这里是创建一个虚拟(dummy)输入的函数。我们将会把这个输入传递到我们的网络。在我们编写这个函数之前,要先将这个图片保存在你的工作目录下。如果你是在Linux环境下,键入:

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

      现在,在你的darknet.py文件夹的顶部定义像下面这样(as follows)这样函数。

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*10647*85。第一维是批的大小,这里是1是因为我们使用了单独的一张图片。在批次中的每一张图片,我们都有一个10647*85的表格( a 10647*85 table)。这个矩阵(table)每一行代表一个边界框(4个边界框属性,1个目标对象得分和80个类别得分)

      这个时候,我们的网络有一个随机的权重,不会计算出正确的输出。在我们的网络中,我们需要加载一个权重文件。我们将会使用官方的权重文件来达到这个目的(如何加载权重文件的内容我就不赘述了,可以参考原文)。

6 总结

在这一节中,我们主要实现了Darknet的前向传播和为模型加载官方数据权重。因为网络模型都保存在ModuleList中,因此我们需要再forward函数中手动指定数据在层与层之间流通的顺序,因此需要遍历每一个网络层指定他们的先后顺序。因为,路由层需要用到前面层的输出特征图,因此我们需要使用一个变量output来保存每一个输出特征图也就是x。一共可能会遇到五类网络层:

  • 卷积层和上采样层:因为这两个层是官方提供的层,因此我们直接将x传入到他们的实例对象,然后返回计算输出特征图。
  • 路由层:路由层只不过是返回到某层,或者是两个层的叠加。因此只需要返回该层的输出特征图,或者某两层的输出特征图的叠加(会用到torch.cat([map1,map2],1)方法,为1的时候,只是通道数相加,为0的时候是批量数进行相加)
  • 捷径层:捷径层只是将前面特征图与捷径层前一个特征图进行对应元素进行相加,因此直接使用加法就行
  • YOLO层:也就是检测层。我们需要对输出的结果进行处理,比如将tx,ty,to进行sigmoid运算,需要对tw,th进行指数运算。但是如果直接在三维输出特征图上直接进行,索引相应值比较麻烦,而且不利于处理,因此我们会对输出特征图提前进行处理,将三维特征图转换成二维张量,每一行代表一个边界框,一共3*W*H行(W,H是指特征图的宽和高),一共85列代表表示一个边界框的85个属性。因为我们有三个尺度的输出,我们需要将三个尺度输出叠加在一起,因此需要用到一个一个参数detections,保存每一个尺度的输出特征图,每次输出的时候都使用torch.cat方法,将本次的输出和上次的输出叠加在一起。

注意:在pedict_transorm函数中,参数的输入主要是(1)网络的预测值(2)锚框(3)输入图片的尺寸(4)是否使用GPU,函数的主要工作主要是:

  • 对tx,ty,to进行sigmoid操作,并且加上中心偏移
  • 对tw,th进行指数运算,并且乘以feature map中的锚框的宽和高
  • 对80个类进行sigmoid操作
  • 将预测的边界框的四个值转换到原图中

注意:要修改配置文件中net的:height和width

易忽视点:

  • 我们要将原图中的边界框大小除以stride,转换到feature map相应的尺寸上,然后最后再转换到实际图中

其中比较重要的技巧:

  • repeat()函数的使用
  • 怎么将一个三维数据转换成我们需要的二维数据(这是为了方便索引)
  • 怎么将tx,ty加上他们各自所在栅格的坐标(np.meshgride函数,repeat函数)
  • 怎么将pw,ph乘在相应的元素上面(repeat函数)
  • 注意numpy数据类型-->FloatTensor()转换成tensor --> cuda如果有的话
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值