当前位置:   article > 正文

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

makeloss

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

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

  1. def _load_gt_roidb(self):
  2. raise NotImplementedError

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

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

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

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

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

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

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

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

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

  1. networks = {
  2. 'vgg16': get_vgg16_train,
  3. 'resnet50': get_resnet50_train,
  4. 'resnet101': get_resnet101_train
  5. }
  6. if network not in networks:
  7. 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

  1. def infer_param_shape(symbol, data_shapes):
  2. arg_shape, _, aux_shape = symbol.infer_shape(**dict(data_shapes))
  3. arg_shape_dict = dict(zip(symbol.list_arguments(), arg_shape))
  4. aux_shape_dict = dict(zip(symbol.list_auxiliary_states(), aux_shape))
  5. 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的解释

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

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

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

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

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

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

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

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

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

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

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

  1. executor=c.bind(ctx=mx.cpu(),args={'A':nd.array([[2,3],[4,5]]),'B':nd.array([[11,10],[1,8]])})
  2. executor.arg_dict
  3. '''
  4. {'A':
  5. [[2. 3.]
  6. [4. 5.]]
  7. <NDArray 2x2 @cpu(0)>, 'B':
  8. [[11. 10.]
  9. [ 1. 8.]]
  10. <NDArray 2x2 @cpu(0)>}
  11. '''
  12. executor.forward()
  13. executor.outputs[0]
  14. '''
  15. [[1.3 1.3]
  16. [0.5 1.3]]
  17. <NDArray 2x2 @cpu(0)>
  18. '''

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

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

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

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

MakeLoss计算损失函数

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

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

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

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

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

  1. bbox_pred = mx.symbol.FullyConnected(name='bbox_pred', data=top_feat, num_hidden=num_classes * 4)
  2. bbox_loss_ = bbox_weight * mx.symbol.smooth_l1(name='bbox_loss_', scalar=1.0, data=(bbox_pred - bbox_target))
  3. 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去执行即可

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

跟前面命令行编程的结果是一样的,当然自己使用公式计算也是这样的结果,其中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在反向传播时的梯度在这里终止

我们来看个具体的示例:

  1. from mxnet import nd
  2. import mxnet as mx
  3. x1 = nd.array([[1, 2]])
  4. x2 = nd.array([[3, 4]])
  5. a = mx.sym.Variable('a')
  6. b = mx.sym.Variable('b')
  7. a_grad = 5*a
  8. b_grad = 10*b
  9. m_loss = mx.sym.MakeLoss(a_grad+b_grad)
  10. EX = m_loss.simple_bind(ctx=mx.cpu(), a=(1, 2), b=(1, 2))
  11. print(EX.forward(a=x1, b=x2)[0]) # [[35. 50.]]
  12. EX.backward()
  13. print(EX.grad_arrays)
  14. '''
  15. [
  16. [[5. 5.]]
  17. <NDArray 1x2 @cpu(0)>,
  18. [[10. 10.]]
  19. <NDArray 1x2 @cpu(0)>]
  20. '''

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

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

  1. x1 = nd.array([[1, 2]])
  2. x2 = nd.array([[3, 4]])
  3. a = mx.sym.Variable('a')
  4. b = mx.sym.Variable('b')
  5. a_grad = 5*a
  6. b_grad = 10*b
  7. # 这个位置我们添加一个阻止b_grad的反向传播
  8. b_grad_stop = mx.sym.BlockGrad(b_grad)
  9. m_loss = mx.sym.MakeLoss(a_grad+b_grad_stop)
  10. EX = m_loss.simple_bind(ctx=mx.cpu(), a=(1, 2), b=(1, 2))
  11. print(EX.forward(a=x1, b=x2)[0]) # [[35. 50.]]
  12. EX.backward()
  13. print(EX.grad_arrays)
  14. '''
  15. [
  16. [[5. 5.]]
  17. <NDArray 1x2 @cpu(0)>,
  18. [[0. 0.]]
  19. <NDArray 1x2 @cpu(0)>]
  20. '''

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

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

如图:

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

自定义操作符operator

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

  1. @mx.operator.register('proposal_target')
  2. class ProposalTargetProp(mx.operator.CustomOpProp):
  3. def __init__(self, num_classes='21', batch_images='1', batch_rois='128', fg_fraction='0.25',
  4. fg_overlap='0.5', box_stds='(0.1, 0.1, 0.2, 0.2)'):
  5. super(ProposalTargetProp, self).__init__(need_top_grad=False)#False:此层不需要传来的梯度
  6. self._num_classes = int(num_classes)
  7. self._batch_images = int(batch_images)
  8. self._batch_rois = int(batch_rois)
  9. self._fg_fraction = float(fg_fraction)
  10. self._fg_overlap = float(fg_overlap)
  11. 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中的调用:

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

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

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

  1. def list_arguments(self):
  2. return ['rois', 'gt_boxes']
  3. def list_outputs(self):
  4. return ['rois_output', 'label', 'bbox_target', 'bbox_weight']

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

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

  1. import mxnet as mx
  2. from mxnet import nd
  3. import numpy as np
  4. class TestLayer(mx.operator.CustomOp):
  5. def forward(self, is_train, req, in_data, out_data, aux):
  6. x = in_data[0].asnumpy()
  7. y = np.exp(x - x.max(axis=1).reshape((x.shape[0], 1)))
  8. y /= y.sum(axis=1).reshape((x.shape[0], 1))
  9. self.assign(out_data[0], req[0], mx.nd.array(y))
  10. def backward(self, req, out_grad, in_data, out_data, in_grad, aux):
  11. l = in_data[1].asnumpy().ravel().astype(np.int32)
  12. y = out_data[0].asnumpy()
  13. y[np.arange(l.shape[0]), l] -= 1.0
  14. self.assign(in_grad[0], req[0], mx.nd.array(y))
  15. @mx.operator.register('Tony') # 注册名称将在调用的时候作为操作符名称
  16. class TestProp(mx.operator.CustomOpProp):
  17. def __init__(self):
  18. super(TestProp, self).__init__(need_top_grad=False)
  19. def list_arguments(self):
  20. return ['data', 'label']
  21. def list_outputs(self):
  22. return ['output']
  23. def infer_shape(self, in_shape):
  24. data_shape = in_shape[0]
  25. label_shape = (in_shape[0][0],)
  26. output_shape = in_shape[0]
  27. return [data_shape, label_shape], [output_shape], []
  28. def infer_type(self, in_type):
  29. return in_type, [in_type[0]], []
  30. def create_operator(self, ctx, shapes, dtypes):
  31. return TestLayer()
  32. net = mx.sym.Variable('data')
  33. net = mx.sym.FullyConnected(net, name='fc', num_hidden=10)
  34. net = mx.sym.Activation(net, name='relu', act_type="relu")
  35. mlp = mx.sym.Custom(data=net, name='MySoftmax', op_type='Tony')
  36. print(mlp.list_arguments(), mlp.list_outputs())
  37. #['data', 'fc_weight', 'fc_bias', 'MySoftmax_label'] ['MySoftmax_output']
  38. # 推断形状
  39. input_shapes = {'data': (5, 28*28)}
  40. print(mlp.infer_shape(**input_shapes))
  41. #([(5, 784), (10, 784), (10,), (5,)], [(5, 10)], [])
  42. #绑定做反向传播
  43. args = {'data': nd.ones((1, 4)), 'fc_weight': nd.ones((10, 4)),
  44. 'fc_bias': nd.ones((10,)), 'MySoftmax_label': nd.ones((1))}
  45. args_grad = {'fc_weight': nd.zeros((10, 4)), 'fc_bias': nd.zeros((10))}
  46. executor = mlp.bind(ctx=mx.cpu(0), args=args,args_grad=args_grad, grad_req='write')
  47. print("executor.arg_dict 初始值\n", executor.arg_dict)
  48. '''
  49. executor.arg_dict 初始值
  50. {'data':
  51. [[1. 1. 1. 1.]]
  52. <NDArray 1x4 @cpu(0)>, 'fc_weight':
  53. [[1. 1. 1. 1.]
  54. [1. 1. 1. 1.]
  55. [1. 1. 1. 1.]
  56. [1. 1. 1. 1.]
  57. [1. 1. 1. 1.]
  58. [1. 1. 1. 1.]
  59. [1. 1. 1. 1.]
  60. [1. 1. 1. 1.]
  61. [1. 1. 1. 1.]
  62. [1. 1. 1. 1.]]
  63. <NDArray 10x4 @cpu(0)>, 'fc_bias':
  64. [1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
  65. <NDArray 10 @cpu(0)>, 'MySoftmax_label':
  66. [1.]
  67. <NDArray 1 @cpu(0)>}
  68. '''
  69. print("executor.grad_dict 初始值\n", executor.grad_dict)
  70. '''
  71. executor.grad_dict 初始值
  72. {'data': None, 'fc_weight':
  73. [[0. 0. 0. 0.]
  74. [0. 0. 0. 0.]
  75. [0. 0. 0. 0.]
  76. [0. 0. 0. 0.]
  77. [0. 0. 0. 0.]
  78. [0. 0. 0. 0.]
  79. [0. 0. 0. 0.]
  80. [0. 0. 0. 0.]
  81. [0. 0. 0. 0.]
  82. [0. 0. 0. 0.]]
  83. <NDArray 10x4 @cpu(0)>, 'fc_bias':
  84. [0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
  85. <NDArray 10 @cpu(0)>, 'MySoftmax_label': None}
  86. '''
  87. executor.backward()
  88. print(executor.grad_arrays)
  89. '''
  90. [None,
  91. [[-2.144862e-15 -2.144862e-15 -2.144862e-15 -2.144862e-15]
  92. [-1.000000e+00 -1.000000e+00 -1.000000e+00 -1.000000e+00]
  93. [ 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00]
  94. [ 4.591214e-41 4.591214e-41 4.591214e-41 4.591214e-41]
  95. [ 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00]
  96. [ 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00]
  97. [ 4.203895e-45 4.203895e-45 4.203895e-45 4.203895e-45]
  98. [ 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00]
  99. [ 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00]
  100. [ 5.044674e-43 5.044674e-43 5.044674e-43 5.044674e-43]]
  101. <NDArray 10x4 @cpu(0)>,
  102. [-2.144862e-15 -1.000000e+00 0.000000e+00 4.591214e-41 0.000000e+00
  103. 0.000000e+00 4.203895e-45 0.000000e+00 0.000000e+00 5.044674e-43]
  104. <NDArray 10 @cpu(0)>, None]
  105. '''

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

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

https://github.com/yihangzhao/NewMXRCNN

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/250114
推荐阅读
相关标签
  

闽ICP备14008679号