MXNet的Faster R-CNN(基于区域提议网络的实时目标检测)《10》(尾)

Posted 寅恪光潜

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MXNet的Faster R-CNN(基于区域提议网络的实时目标检测)《10》(尾)相关的知识,希望对你有一定的参考价值。

MXNet的Faster R-CNN(基于区域提议网络的实时目标检测)《1》:论文源地址,克隆MXNet版本的源码,安装环境与测试,以及对下载的源码的每个目录做什么用的,做个解释。

MXNet的Faster R-CNN(基于区域提议网络的实时目标检测)《2》:对论文中的区域提议、平移不变锚、多尺度预测等概念的了解,对损失函数、边界框回归的公式的了解,以及共享特征的训练网络的方法。

MXNet的Faster R-CNN(基于区域提议网络的实时目标检测)《3》:加载模型参数,对参数文件的了解,以及感兴趣区域ROI和泛洪填充的方法(FLOODFILL_FIXED_RANGE,FLOODFILL_MASK_ONLY)

MXNet的Faster R-CNN(基于区域提议网络的实时目标检测)《4》:下载与熟悉Pascal VOC2007,2012语义分割数据集,明白实例分割除了分类之外,还可以细分到像素级别的所属类别。

MXNet的Faster R-CNN(基于区域提议网络的实时目标检测)《5》:主要就是熟悉转置卷积与大家所熟知的卷积有什么区别,作用是什么,以及双线性插值等相关知识

MXNet的Faster R-CNN(基于区域提议网络的实时目标检测)《6》:主要讲解关于参数解析的安全执行(ast.literal_eval),ROI池化以及计算图的可视化的处理

MXNet的Faster R-CNN(基于区域提议网络的实时目标检测)《7》:打印内容(比如参数文件里的东西)的三种方式以及对奇异值分解(SVD,Singular Value Decomposition)的熟悉,了解SVD的作用和运用

MXNet的Faster R-CNN(基于区域提议网络的实时目标检测)《8》:主要是通过参数的设置进一步熟悉模型,以及对于符号式编程的复习,另外关于损失函数之类,这里用到了自定义评价函数,然后通过自带的mx.metric来做,有示例让大家熟悉。

MXNet的Faster R-CNN(基于区域提议网络的实时目标检测)《9》:从测试模型了解有哪些知识点,张量的垂直叠加,然后从单个文件细读,有哪些关键点,交并比的计算、裁剪掉超出图像部分区域的锚框、边界框回归方法以及非极大值抑制的实现(并画出边界框图形)

这是第十篇继续来拆解学习Faster RCNN,也是最后一篇,知识点比较多,另外在文章末尾附上最终加上了注释的源码,有兴趣的伙伴们可以Clone一个看看。

我们继续对每个文件的代码进行阅读,有些我就直接在源码中做了注释,没有贴代码了,另外coco与restnet相关的代码跟voc的都差不多,只是数据集与网络结构不一样,整体的思路是一样的。这里还是以voc数据集为主来熟悉,来到这个symimdb目录,主要是图像数据的处理。

错误与断言assert

在代码中可以看到很多地方有断言与错误等处理,我们来熟悉下:

def _load_gt_roidb(self):
        raise NotImplementedError

出现raise NotImplementedError这个错误,就是说如果这个方法没有被重写就报错,我们看下这个方法是属于哪个类的,class IMDB(object) 然后我们搜索IMDB查看相关调用,我们发现在pascal_voc.py中有调用:class PascalVOC(IMDB)

那么在这个PascalVOC类里面肯定需要重写_load_gt_roidb这个方法,我们往下查看

def _load_gt_roidb(self):
    image_index = self._load_image_index()
    gt_roidb = [self._load_annotation(index) for index in image_index]
    return gt_roidb

没错,代码中确实做了重写,如果没有重写这个方法那就会触发NotImplementedError这个错误

另外断言出现的频率也很高,比如下面:

assert ex_rois.shape[0] == gt_rois.shape[0], 'inconsistent rois number'

如果两者的样本数不一样,那就会报inconsistent rois number这样的错误,当然这个错误的信息显示是自定义的,比如:

assert 1==2,'1不等于2'

源码中还有一种值错误处理,比如:

networks = 
        'vgg16': get_vgg16_train,
        'resnet50': get_resnet50_train,
        'resnet101': get_resnet101_train
    
    if network not in networks:
        raise ValueError("network  not supported".format(network))

这样的处理就是说网络只能是指定的这三者中的一种,如果是其他的就会报错,比如如果输入不存在的get_vgg18_train,将出现如下错误:

ValueError: network get_vgg18_train not supported

这些错误与断言,只要出现都将终止程序,不会继续往下执行,这个在平时自己写代码的时候需要注意,提高代码的严谨性。

推断形状infer_shape

我们来到symnet/model.py文件,其中关于推断形状,有必要介绍下,因为形状在神经网络中是非常非常重要的概念,先来看下源码中实现的方法infer_param_shape

def infer_param_shape(symbol, data_shapes):
    arg_shape, _, aux_shape = symbol.infer_shape(**dict(data_shapes))
    arg_shape_dict = dict(zip(symbol.list_arguments(), arg_shape))
    aux_shape_dict = dict(zip(symbol.list_auxiliary_states(), aux_shape))
    return arg_shape_dict, aux_shape_dict

主要关注infer_shape这个方法,其中的参数data_shapes,假如类似下面这样的形状,由元组组成的列表:

data_shapes = [('data', (1, 3, 800, 800)), ('im_info', (1, 3))]

这里通过字典的类型转换:dict(data_shapes),变成字典类型:

'data': (1, 3, 800, 800), 'im_info': (1, 3)

对于前面两个星号**的用法,如果不了解的伙伴们可以查阅:Python中*args和**kwargs的解释

如果有符号式编程的经验,那对于形状的推断就会很熟悉,如果是第一次接触,没关系,这里再次复习一遍。

通俗简单来说,符号式编程就是先将整个计算流程给设计出来,最后需要使用的时候,进行绑定与计算操作,就好比建房子之前先将图纸画好,然后我们只需要按照图纸来执行相关操作。

为了快速熟悉它,这里我用一个特别简单的示例来说明:

a = mx.sym.Variable('A')
b = mx.sym.Variable('B')
c = (a + b) / 10

这里定义了两个Symbol,A和B,然后将两者相加再除以10,这个时候的A和B是个未知变量,或说是个符号变量。那如果场景是在深度卷积网络中呢?整个流程我们需要正确执行,形状是关键,不然会因为形状不符合,就没法计算,所以这里就出现了推断形状的方法,然后可以做一些前面介绍的断言,确保形状一样,这样就可以确保顺畅的向下执行了。

我们来看个具体的示例,假如输入的形状如下:

input_shapes = 'A':(10,2), 'B':(10,2)#这里我们就直接使用字典类型
c.infer_shape(**input_shapes)#这里会返回三个形状,arg_shapes,out_shapes,aux_shapes

接收返回值,打印看下结果:

arg_shapes,out_shapes,aux_shapes=c.infer_shape(**input_shapes)
#arg_shapes:[(10, 2), (10, 2)]
#out_shapes:[(10, 2)]
#aux_shapes:[]

也就是说在符号式编程中只需要指定形状,我们就可以通过“计算图”推断出每层输出的形状。

如何在实践中得到实际的结果,通过bind绑定和forward计算即可。

executor=c.bind(ctx=mx.cpu(),args='A':nd.array([[2,3],[4,5]]),'B':nd.array([[11,10],[1,8]]))
executor.arg_dict
'''
'A':
[[2. 3.]
 [4. 5.]]
<NDArray 2x2 @cpu(0)>, 'B':
[[11. 10.]
 [ 1.  8.]]
<NDArray 2x2 @cpu(0)>
'''
executor.forward()
executor.outputs[0]
'''
[[1.3 1.3]
 [0.5 1.3]]
<NDArray 2x2 @cpu(0)>
'''

计算结果没有问题,两者相加之后除以10,想了解更多关于符号式编程的伙伴们可以查阅:

MXNet中的命令式编程和符号式编程的优缺点

计算性能的提升之混合式编程(MXNet)

MXNet的Faster R-CNN(基于区域提议网络的实时目标检测)《6》

MakeLoss计算损失函数

我们在symnet/symbol_vgg.py的边界框回归的源码中,使用了平滑L1损失函数

在命令式编程中我们知道,nd有自带的L1平滑损失直接可以求出:

print(nd.smooth_l1(nd.array([0.5, 0.9, 1, 2, 3]), scalar=1))
'''
[0.125      0.40499997 0.5        1.5        2.5       ]
<NDArray 5 @cpu(0)>

这里顺带将L1平滑损失函数的公式再次贴出来:

那么在符号式编程中,所以如何使用呢?我们看下源码中是怎么样的:

bbox_pred = mx.symbol.FullyConnected(name='bbox_pred', data=top_feat, num_hidden=num_classes * 4)
bbox_loss_ = bbox_weight * mx.symbol.smooth_l1(name='bbox_loss_', scalar=1.0, data=(bbox_pred - bbox_target))
bbox_loss = mx.sym.MakeLoss(name='bbox_loss', data=bbox_loss_, grad_scale=1.0 / rcnn_batch_rois)

照葫芦画瓢,这个symbol模块也自带有smooth_l1(平滑L1损失函数),指定需要的参数data(Symbol类型),然后通过MakeLoss去执行即可

data = mx.sym.Variable('data')
sl1_loss_ = mx.sym.smooth_l1(data=data, name='bbox_loss_', scalar=1)
m_loss = mx.sym.MakeLoss(data=sl1_loss_)
EX = m_loss.bind(ctx=mx.cpu(), args='data': nd.array([0.5, 0.9, 1, 2, 3]))
EX.forward()
'''
[0.125      0.40499997 0.5        1.5        2.5       ]
<NDArray 5 @cpu(0)>]
'''

跟前面命令行编程的结果是一样的,当然自己使用公式计算也是这样的结果,其中0.40499997本来是0.41,这个属浮点数的误差。

这里需要注意的是,参数是scalar而不是源码中示例的sigma,如果写成sigma在VSCode中就会报错,命令行却没有问题,这里比较奇怪:

Exception has occurred: OSError
[WinError -529697949] Windows Error 0xe06d7363

以方法的参数为准:

def smooth_l1(data=None, scalar=_Null, name=None, attr=None, out=None, **kwargs)

示例中的公式是sigma,所以例子中的参数误写成了sigma,这里的细节需要注意下。

BlockGrad阻塞梯度的反向传播

Block是阻塞的意思,Grad是表示梯度Gradient,含义就是阻止梯度的反向传播。

我们在symnet/symbol_vgg.py的代码get_vgg_train训练方法中看到有这个方法的调用:

group = mx.symbol.Group([rpn_cls_prob, rpn_bbox_loss, cls_prob, bbox_loss, mx.symbol.BlockGrad(label)])

mx.symbol.BlockGrad(label)那意思就是说label在反向传播时的梯度在这里终止

我们来看个具体的示例:

from mxnet import nd
import mxnet as mx

x1 = nd.array([[1, 2]])
x2 = nd.array([[3, 4]])

a = mx.sym.Variable('a')
b = mx.sym.Variable('b')
a_grad = 5*a
b_grad = 10*b
m_loss = mx.sym.MakeLoss(a_grad+b_grad)
EX = m_loss.simple_bind(ctx=mx.cpu(), a=(1, 2), b=(1, 2))
print(EX.forward(a=x1, b=x2)[0])  # [[35. 50.]]

EX.backward()
print(EX.grad_arrays)
'''
[
[[5. 5.]]
<NDArray 1x2 @cpu(0)>,
[[10. 10.]]
<NDArray 1x2 @cpu(0)>]
'''

我们可以看到前向传播的结果是正确的,再观察这个5a和10b的梯度分别是5和10,反向传播的结果也没有问题。

现在我们阻止这个10b的梯度传播,看下会是什么情况:

x1 = nd.array([[1, 2]])
x2 = nd.array([[3, 4]])

a = mx.sym.Variable('a')
b = mx.sym.Variable('b')
a_grad = 5*a
b_grad = 10*b
# 这个位置我们添加一个阻止b_grad的反向传播
b_grad_stop = mx.sym.BlockGrad(b_grad)

m_loss = mx.sym.MakeLoss(a_grad+b_grad_stop)
EX = m_loss.simple_bind(ctx=mx.cpu(), a=(1, 2), b=(1, 2))
print(EX.forward(a=x1, b=x2)[0])  # [[35. 50.]]

EX.backward()
print(EX.grad_arrays)
'''
[
[[5. 5.]]
<NDArray 1x2 @cpu(0)>,
[[0. 0.]]
<NDArray 1x2 @cpu(0)>]
'''

5a的反向传播是正常的,求出的梯度是5没有问题,10b的梯度全部变为了0,说明成功阻止了它的反向传播,我们也可以可视化下这个小型的“计算图”:

digraph = mx.viz.plot_network(m_loss)
digraph.view()

如图:

可以看到a和b的区别,b这个分支多了一个blockgrad0,然后再相加,这里需要注意的是,前向传播是不影响的,还是正常的相乘之后两者相加,只是在反向传播求梯度的时候做一个阻塞,终止10b的传播。

自定义操作符operator

我们在symnet/proposal_target.py源码发现在类的上面出现这样一个装饰@mx.operator.register('proposal_target'),它的作用就是将名称proposal_target注册到自定义操作符中。

@mx.operator.register('proposal_target')
class ProposalTargetProp(mx.operator.CustomOpProp):
    def __init__(self, num_classes='21', batch_images='1', batch_rois='128', fg_fraction='0.25',
                 fg_overlap='0.5', box_stds='(0.1, 0.1, 0.2, 0.2)'):
        super(ProposalTargetProp, self).__init__(need_top_grad=False)#False:此层不需要传来的梯度
        self._num_classes = int(num_classes)
        self._batch_images = int(batch_images)
        self._batch_rois = int(batch_rois)
        self._fg_fraction = float(fg_fraction)
        self._fg_overlap = float(fg_overlap)
        self._box_stds = tuple(np.fromstring(box_stds[1:-1], dtype=float, sep=','))

先看下这个类的基类mx.operator.CustomOpProp,我们可以转到定义可以知道这个是自定义操作符属性类的基类。CustomOpProp最后创建操作符是返回CustomOp(),然后转到定义发现这个是python中实现的操作符的真正基类了,其他还有NumpyOp,NDArrayOp这样的操作都在operator.py文件定义

symnet/symbol_vgg.py中的调用:

group = mx.symbol.Custom(rois=rois, gt_boxes=gt_boxes, op_type='proposal_target',
                         num_classes=num_classes, batch_images=rcnn_batch_size,
                         batch_rois=rcnn_batch_rois, fg_fraction=rcnn_fg_fraction,
                         fg_overlap=rcnn_fg_overlap, box_stds=rcnn_bbox_stds)

op_type='proposal_target'这个操作符的名称就是来自前面声明的装饰@mx.operator.register('proposal_target')注册中的名称。

这样注册了之后,在这个类里面可以重写方法,比如说

def list_arguments(self):
    return ['rois', 'gt_boxes']

def list_outputs(self):
    return ['rois_output', 'label', 'bbox_target', 'bbox_weight']

这个list_arguments输入的参数就是由op_type绑定的自定义操作符决定了,同样的list_outputs输出参数也是的,这里其实是后缀形式,输出形式是name_后缀这样的输出。

当然这里还是围绕着这个源码来熟悉这个知识点,我们来看个具体的示例(输出是Softmax层,对官方示例有所改动),一个多层感知机的网络:

import mxnet as mx
from mxnet import nd
import numpy as np

class TestLayer(mx.operator.CustomOp):
    def forward(self, is_train, req, in_data, out_data, aux):
        x = in_data[0].asnumpy()
        y = np.exp(x - x.max(axis=1).reshape((x.shape[0], 1)))
        y /= y.sum(axis=1).reshape((x.shape[0], 1))
        self.assign(out_data[0], req[0], mx.nd.array(y))

    def backward(self, req, out_grad, in_data, out_data, in_grad, aux):
        l = in_data[1].asnumpy().ravel().astype(np.int32)
        y = out_data[0].asnumpy()
        y[np.arange(l.shape[0]), l] -= 1.0
        self.assign(in_grad[0], req[0], mx.nd.array(y))


@mx.operator.register('Tony')  # 注册名称将在调用的时候作为操作符名称
class TestProp(mx.operator.CustomOpProp):
    def __init__(self):
        super(TestProp, self).__init__(need_top_grad=False)

    def list_arguments(self):
        return ['data', 'label']

    def list_outputs(self):
        return ['output']

    def infer_shape(self, in_shape):
        data_shape = in_shape[0]
        label_shape = (in_shape[0][0],)
        output_shape = in_shape[0]
        return [data_shape, label_shape], [output_shape], []

    def infer_type(self, in_type):
        return in_type, [in_type[0]], []

    def create_operator(self, ctx, shapes, dtypes):
        return TestLayer()


net = mx.sym.Variable('data')
net = mx.sym.FullyConnected(net, name='fc', num_hidden=10)
net = mx.sym.Activation(net, name='relu', act_type="relu")
mlp = mx.sym.Custom(data=net, name='MySoftmax', op_type='Tony')

print(mlp.list_arguments(), mlp.list_outputs())
#['data', 'fc_weight', 'fc_bias', 'MySoftmax_label'] ['MySoftmax_output']

# 推断形状
input_shapes = 'data': (5, 28*28)
print(mlp.infer_shape(**input_shapes))
#([(5, 784), (10, 784), (10,), (5,)], [(5, 10)], [])

#绑定做反向传播
args = 'data': nd.ones((1, 4)), 'fc_weight': nd.ones((10, 4)),
        'fc_bias': nd.ones((10,)), 'MySoftmax_label': nd.ones((1))
args_grad = 'fc_weight': nd.zeros((10, 4)), 'fc_bias': nd.zeros((10))

executor = mlp.bind(ctx=mx.cpu(0), args=args,args_grad=args_grad, grad_req='write')
print("executor.arg_dict 初始值\\n", executor.arg_dict)
'''
executor.arg_dict 初始值
 'data':
[[1. 1. 1. 1.]]
<NDArray 1x4 @cpu(0)>, 'fc_weight':
[[1. 1. 1. 1.]
 [1. 1. 1. 1.]
 [1. 1. 1. 1.]
 [1. 1. 1. 1.]
 [1. 1. 1. 1.]
 [1. 1. 1. 1.]
 [1. 1. 1. 1.]
 [1. 1. 1. 1.]
 [1. 1. 1. 1.]
 [1. 1. 1. 1.]]
<NDArray 10x4 @cpu(0)>, 'fc_bias':
[1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
<NDArray 10 @cpu(0)>, 'MySoftmax_label':
[1.]
<NDArray 1 @cpu(0)>
'''

print("executor.grad_dict 初始值\\n", executor.grad_dict)
'''
executor.grad_dict 初始值
 'data': None, 'fc_weight':
[[0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]]
<NDArray 10x4 @cpu(0)>, 'fc_bias':
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
<NDArray 10 @cpu(0)>, 'MySoftmax_label': None
'''
executor.backward()
print(executor.grad_arrays)
'''
[None,
[[-2.144862e-15 -2.144862e-15 -2.144862e-15 -2.144862e-15]
 [-1.000000e+00 -1.000000e+00 -1.000000e+00 -1.000000e+00]
 [ 0.000000e+00  0.000000e+00  0.000000e+00  0.000000e+00]
 [ 4.591214e-41  4.591214e-41  4.591214e-41  4.591214e-41]
 [ 0.000000e+00  0.000000e+00  0.000000e+00  0.000000e+00]
 [ 0.000000e+00  0.000000e+00  0.000000e+00  0.000000e+00]
 [ 4.203895e-45  4.203895e-45  4.203895e-45  4.203895e-45]
 [ 0.000000e+00  0.000000e+00  0.000000e+00  0.000000e+00]
 [ 0.000000e+00  0.000000e+00  0.000000e+00  0.000000e+00]
 [ 5.044674e-43  5.044674e-43  5.044674e-43  5.044674e-43]]
<NDArray 10x4 @cpu(0)>,
[-2.144862e-15 -1.000000e+00  0.000000e+00  4.591214e-41  0.000000e+00
  0.000000e+00  4.203895e-45  0.000000e+00  0.000000e+00  5.044674e-43]
<NDArray 10 @cpu(0)>, None]
'''

十连载将Faster-RCNN全部梳理了一遍,尤其是对于源码中出现的知识点都单独挑出来进行了示例演示,希望能够帮助到大家更快的理解这个网络模型,由于水平有限,错误难免,还望留言指正!

其中对于源码的理解,做了一些注释,有兴趣的可以clone一个来看看,github地址如下:

https://github.com/yihangzhao/NewMXRCNN

以上是关于MXNet的Faster R-CNN(基于区域提议网络的实时目标检测)《10》(尾)的主要内容,如果未能解决你的问题,请参考以下文章

MXNet的Faster R-CNN(基于区域提议网络的实时目标检测)《10》(尾)

MXNet的Faster R-CNN(基于区域提议网络的实时目标检测)《7》

R-CNN , Fast R-CNN , Faster R-CNN原理及区别

R-CNN/Fast R-CNN/Faster R-CNN

Faster R-CNN 学习

Faster R-CNN:使用RPN实时目标检测