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的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地址如下: