我的AI之路(44)--将tensorflow1.2版faster-rcnn模型freeze为pb模型的总结

       Faster-rcnn虽然是有点老了的网络,但是可以在有些硬件配置不高、计算资源有限的前端边缘板子上部署,而且也能满足一般的图像识别功能,所以有些项目还是需要用的。近来因项目需要实验研究了一下把faster-rcnn模型转换成pb文件,试图在安卓板子上部署后直接使用安卓调用,但是发现并不太可行,倒不是说pb文件生成不了,而是有几个严重问题,作为经验教训记录备忘,下面有说的不对的,或者有办法能解决的,欢迎留言拍砖。

       网上不少文章谈到将tensorflow版的网络模型固化成pb模型,但是举例内容都偏简单,要么就是简单用tensorflow实现个加减法或弄个线性回归或弄个mnist训练之类的,完后按部就班的调用一下那几个API固化成pb,再写测试代码装载pb文件,获取输入输出节点的tensor,给输入tensor赋值再run得到输出结果就完了,看完让人觉得把ckpt模型转换成pb模型好简单啊,就那么几步照做不就得了,没见过几个就真正的拿tensorflow版实现的大型网络来举例说明如何free成pb文件再做装载和运行测试。

      实际上转换大型网络模型时,远不是这么容易,因为能商业应用的大模型都要考虑性能等因素,一般的规律是,在训练用的代码部分,对于频繁的大数据量计算部分可能是使用C++定制的operation来实现,主干部分才使用python加tensorflow的python版API来写,在测试用的代码部分,对主干部分代码在获得原始的识别结果数据以前多是使用python和tensorflow的python版API写的,获得识别原始结果数据后,后续处理多是使用numpy的强大数组运算功能来实现,这点tensorflow的slice()做不到(起码slice没有step参数,对吧?),至于笨拙的split()就更不行了。就faster-rcnn来说,获得识别结果后,后续处理中根据nms阈值对标注框做合并时因为计算量大多是采用的C++写的代码来实现的,根据score阈值对原始结果做过滤时和根据anchors和偏移量计算proposal boxes和标注框坐标数据的修剪时,使用了大量的numpy的数据切片灵活运算,我看了一下感觉用tensorflow的python API没法实现,这大概就是为何一般的网络中遇到大量复杂数组运算时都爱转向使用numpy去做,计算完后再转换成tensorflow的tensor。

       转换模型时应该从测试代码中去找输入输出节点,并加入固化模型代码(或者直接调用tensorflow的free_graph工具,不过我更喜欢自己写,灵活一点),而不是从训练代码中下手,为什么呢,因为训练模型时的输入节点比测试时多了一个label数据节点,这个参数在推理时是不需要的。

       就faster-rcnn来说,模型固化成pb,首先从测试代码中找到最后的输出节点(下面以我使用的tensorflow版faster-rcnn源码为例):

    print("net.data=",net.data)
    print("net.im_info=",net.im_info)
    print("net.kepp_prob=",net.keep_prob)
    print("cls_score=",net.get_output('cls_score'))
    print("cls_prob=",net.get_output('cls_prob'))
    print("bbox_pred=",net.get_output('bbox_pred'))
    print("rois=",net.get_output('rois'))
    print("input/output tensors are dumped above!")

    cls_score, cls_prob, bbox_pred, rois = sess.run([net.get_output('cls_score'), net.get_output('cls_prob'), net.get_output('bbox_pred'),net.get_output('rois')],
                                                    feed_dict=feed_dict,
                                                    options=run_options,
                                                    run_metadata=run_metadata)    #cls_score可以不要,网络的测试代码中在后面没有用它,而是使用cls_prob代替了

要知道这些输出节点的真实名字(因为前面可能加了各种域名之类的,并不是你看到的名字),在上面这句话前面把这些tensor打印一下就知道了(上面的蓝色部分代码),当然,如果你不嫌麻烦的话,也可以导入meta文件到图再输出图到summary然后使用tensorboard解析,然后在浏览器中输入http://127.0.0.1:6006/查看图中这些节点:

    import tensorflow as tf
    from tensorflow.python.platform import gfile
 
    graph = tf.get_default_graph()
    graphdef = graph.as_graph_def()
    tf.import_meta_graph("model.ckpt.meta")
    summary= tf.summary.FileWriter("./" , graph)
    summary.close()

    tensorboard --logdir=  

上面的输出节点tensor打印出的名字依次是"cls_score/cls_score", "cls_prob", "bbox_pred/bbox_pred","rois",找到输出节点后,固化模型成pb文件,下面这些代码就都是套路了:

     l_graph= tf.get_default_graph()

     l_graph_def = l_graph.as_graph_def()
     operations = l_graph.get_operations()
     filename="/root/Faster-RCNN_TF/output/faster_rcnn_end2end/voc_2007_trainval/VGGnet_fast_rcnn_iter_70000.pb"
     constant_graph = graph_util.convert_variables_to_constants(sess, l_graph_def, ["cls_score/cls_score", "cls_prob", "bbox_pred/bbox_pred","rois"])   #cls_score/cls_score节点可以不要,因为网络的测试代码中cls_score的值在后面没有用它,而是使用cls_prob代替了
    with tf.gfile.FastGFile(filename, mode='wb') as f:
        f.write(constant_graph.SerializeToString())

    这就生成了pb文件了,然后用用代码测试加载并运行: 

def read_image(filename,resize=False,resize_height=0,resize_width=0,normalization=False):
    img = cv2.imread(filename)
    if resize:
        img = cv2.resize(img,(resize_width,resize_height))
    img_array = np.asanyarray(img)
    if normalization:
        img_array = img_array/255.0
    return img_array

def test_freeze_graph(pb_path,image_path):
    g = tf.Graph()
    with g.as_default():
        output_graph_def = tf.GraphDef()
        with open(pb_path,"rb") as f:
            output_graph_def.ParseFromString(f.read())

            '''下面这个加载定制op的roi_pooling.so库文件的语句很重要,否则下面import时会总是报错:

              tf.import_graph_def(output_graph_def,name="Arnold-G")
              File "/usr/local/lib/python2.7/dist-packages/tensorflow/python/framework/importer.py", line 283, in import_graph_def
                raise ValueError('No op named %s in defined operations.' % node.op)
                  ValueError: No op named
RoiPool in defined operations.'''
            tf.load_op_library('/root/Faster-RCNN_TF/lib/roi_pooling_layer/roi_pooling.so') 
            tf.import_graph_def(output_graph_def,name="Arnold-G")  #取个名字 the default name of the graph is "import"
            
    with tf.Session(graph=g) as sess:       
        sess.run(tf.global_variables_initializer())
        input_data=  sess.graph.get_tensor_by_name("Arnold-G/input_data:0")  #所有的tensor的名字的前面都是Arnold-G了
        input_im_info = sess.graph.get_tensor_by_name("Arnold-G/input_im_info:0")
        cls_score =  sess.graph.get_tensor_by_name("Arnold-G/cls_score/cls_score:0")
        cls_prob =  sess.graph.get_tensor_by_name("Arnold-G/cls_prob:0")
        bbox_pred =  sess.graph.get_tensor_by_name("Arnold-G/bbox_pred/bbox_pred:0")
        roi_data =  sess.graph.get_tensor_by_name("Arnold-G/rois:0")
        img = read_image(image_path,True,resize_height,resize_width,normalization=True)
        img = img[np.newaxis,:]  #输入数据按要求增加一个维度,batch num#
        im_info=np.array( [[img.shape[0], img.shape[1], 0.8333]],dtype=np.float32)
        out = sess.run([cls_score,cls_prob,bbox_pred,roi_data],feed_dict={img_data:img})
        print("out====",out[0],out[1],out[2],out[3])

至此,pb文件可以完美生成,也可以完美加载和测试,但是有个几个重要问题是:

      一是,这里测试代码是python的,可以使用tf.load_op_library(),如果在安卓上跑使用安卓java代码测试调用时,org.tensorflow.contrib.android.TensorFlowInferenceInterface中好像没有找到对应的这样的API来加载定制的op,但是不加载又不行,定制的op在模型固化时是没有一起写入pb文件的(so文件内容怎么写入pb确实也不好处理),tensorflow一直没实现这个功能,我验证了用python实现的定制op也不会写入pb文件,这个下面说。

     二是,上面固化的几个输出节点的在推理时获得的值都是原始数据,后面还需根据anchors和偏移量计算proposal boxes和标注框坐标数据的修剪、根据nms阈值进行nms合并标注框处理,以及根据score的阈值进行过滤等处理后输出的最终的标注框的坐标数据和score数据才能给调用方使用,加入让安卓程序调用上面固化的pb文件,就算没有问题一,模型完全能加载成功,调用TensorFlowInferenceInterface.run()后TensorFlowInferenceInterface.feed()获得的数据都是原始数据,那些复杂的后续处理中nms计算是用C++写的,其他都是借助numpy的强大而又灵活的数组运算功能实现的,这些东西要由安卓java代码来实现难度可想而知,而且java性能跟C++和python不能比。

假设第一问题不存在,在安卓上也能加载成功带自定义op库的pb模型,第二个问题就是关键了,怎么封装后做为tensorflow的graph的一个节点,我首先尝试把上面说的这些后续处理的python代码全部放到一个python函数里去,然后把它作为一个定制op,让tf.pyfunc()来调用,因为测试推理只有前向计算,所以不用实现梯度求导功能:

def get_target_boxes(img,im_scales, cls_prob, bbox_pred, rois):
    #if cfg.TEST.HAS_RPN:
    assert len(im_scales) == 1, "Only single-image batch implemented"
    boxes = rois[:, 1:5] / im_scales[0]
    print("im_scales=",im_scales)
    # use softmax estimated probabilities
    scores = cls_prob
    #if cfg.TEST.BBOX_REG:
    # Apply bounding-box regression deltas
    box_deltas = bbox_pred
    pred_boxes = bbox_transform_inv(boxes, box_deltas)
    pred_boxes = _clip_boxes(pred_boxes,img.shape) #(img_shape[0],img_shape[1],img_shape[2]))

    result = '{ "boxes": ['   #返回给调用客户端最终结果是标注框的坐标数据和score组成的json字符串,这是调用方所希望的真正的所谓end-to-end!
    num=0
    cls_ind = 1
    cls_boxes = pred_boxes[:, 4*cls_ind:4*(cls_ind + 1)]
    cls_scores = scores[:, cls_ind]
    dets = np.hstack((cls_boxes,cls_scores[:, np.newaxis])).astype(np.float32)
    keep = nms(dets, cfg.TEST.NMS)
    dets = dets[keep, :]
    inds = np.where(dets[:, -1] >= 0.0)[0]
    if len(inds) > 0:
        for i in inds:
            bbox = dets[i, :4]
            score = dets[i, -1]
            print("score=",score,"bbox=",bbox[0],bbox[1],bbox[2],bbox[3])
            if num > 0:
                result += ','
            result += '{ "x1\": %f,\"y1\": %f,\"x2\": %f,\"y2\": %f}' % (bbox[0],bbox[1],bbox[2],bbox[3])
            num = num+1

    result +=']}'
    print("result=",result)
    np_result= np.array(result,np.str_)   # tf.py_func()调用的函数的出入参数都得是numpy的数组类型 !
    return np_result

     在上面的sess.run(...)后面调用:

   with l_graph.as_default():  #加不加默认图都可以
        result = tf.py_func(get_target_boxes,[im,im_scales,cls_prob,bbox_pred,rois],
                        [tf.string],name="model_output")
        output_result = sess.run(result)
        print("output-result====",output_result)

这么做,在测试代码运行时没有问题,output_result能打印出正确的json个数结果数据,输出模型为pb文件时也没问题,但是用python代码加载生成的这个pb文件时总是报错:

     Traceback (most recent call last):
  File "test_pb.py", line 139, in
    test_freeze_graph(pb_path,image_path)
  File "test_pb.py", line 131, in test_freeze_graph
    out = sess.run([str_result],feed_dict={input_data:img,input_im_info:im_info})
  File "/usr/local/lib/python2.7/dist-packages/tensorflow/python/client/session.py", line 789, in run
    run_metadata_ptr)
  File "/usr/local/lib/python2.7/dist-packages/tensorflow/python/client/session.py", line 997, in _run
    feed_dict_string, options, run_metadata)
  File "/usr/local/lib/python2.7/dist-packages/tensorflow/python/client/session.py", line 1132, in _do_run
    target_list, options, run_metadata)
  File "/usr/local/lib/python2.7/dist-packages/tensorflow/python/client/session.py", line 1152, in _do_call
    raise type(e)(node_def, op, message)
tensorflow.python.framework.errors_impl.UnknownError: exceptions.KeyError: 'pyfunc_1'
         [[Node: Arnold-G/model_output = PyFunc[Tin=[DT_UINT8, DT_DOUBLE, DT_FLOAT, DT_FLOAT, DT_FLOAT], Tout=[DT_STRING], token="pyfunc_1", _device="/job:localhost/replica:0/task:0/cpu:0"](Arnold-G/model_output/input_0, Arnold-G/model_output/input_1, Arnold-G/model_output/input_2, Arnold-G/model_output/input_3, Arnold-G/model_output/input_4)]

很奇怪,总是说不认识pyfunc_1,可是我代码里没有这个标识符,哪里来的呢?后来想到可能是tf.py_func()在转换成pb文件时里面的get_target_boxes()被强制给了个新名字,而不是使用本来的函数名get_target_boxes,一搜索pb文件的内容,果然没有get_target_boxes,但是有pyfunc_1,果然就是这样,这样就不好办了,tf.py_fun()只能指定op的名字(见上面的model_output),可没有参数强制指定调用的实现函数名,这样导入pb时肯定出错了,但是网上搜了很久也没有找到解决办法,stackoverflow上有人报告也遇到了这样的问题,都知道是tensorflow的毛病,有的不了了之没人回复,看来似乎目前没有好的解决办法,https://stackoverflow.com/questions/47464272/unknown-keyerror-pyfunc-0 虽然这人说是在impor时提前重新定义这个函数就解决了,但是他的函数输入比较简单,我这是网络的输出作为op函数的输入,所以不能采用他说的办法解决,目前似乎无解。用python实现定制op这条路没走通。

     后来想试着把这个后续处理封装成faster-rcnn网络的最后一层加在后面处理:

在 lib/networks/network.py里最后增加个层定义:

 @layer
    def get_target_boxes(self,input,name):
         #img =input[0]

         img = self.session.run(input[0])
        cls_prob = self.session.run(input[1])
        #cls_prob = input[1]
        bbox_pred = self.session.run(input[2])
        #bbox_pred = input[2]
        rois = self.session.run(input[3])
        #rois = input[3]
        im_scales=[0.83333]
        boxes = rois[:, 1:5] / im_scales[0]
        # use softmax estimated probabilities
        scores = cls_prob
        box_deltas = bbox_pred
        pred_boxes = bbox_transform_inv(boxes, box_deltas)
        pred_boxes = _clip_boxes(pred_boxes,(img.shape[1],img.shape[2],img.shape[3]))
        #pred_boxes = _clip_boxes(pred_boxes,(720,720,3))

        result = '{ "boxes": ['
        num=0
        cls_ind = 1
        cls_boxes = pred_boxes[:, 4*cls_ind:4*(cls_ind + 1)]
        cls_scores = scores[:, cls_ind]
        dets = np.hstack((cls_boxes,cls_scores[:, np.newaxis])).astype(np.float32)
        keep = nms(dets, cfg.TEST.NMS)
        dets = dets[keep, :]
        inds = np.where(dets[:, -1] >= 0.0)[0]
        if len(inds) > 0:
            for i in inds:
                bbox = dets[i, :4]
                score = dets[i, -1]
                print("score=",score,"bbox=",bbox[0],bbox[1],bbox[2],bbox[3])
                if num > 0:
                    result += ','
                result += '{ "x1\": %f,\"y1\": %f,\"x2\": %f,\"y2\": %f}' % (bbox[0],bbox[1],bbox[2],bbox[3])
                num = num+1
                 result +=']}'
        print("result=",result)
        np_result= np.array(result,np.str_)
        t_result = tf.convert_to_tensor(np_result,dtype=tf.string,name=name)
        return t_result

修改一下tools/test_net.py :

 class VGGnet_test(Network):
    def __init__(self,sess, trainable=True):
        self.session = sess

     ...

    (self.feed('data','cls_prob','bbox_pred','rois')   #前面网络层的相关输出节点作为本层的输入
             .get_target_boxes(name="model_output"))

在修改一下创建和调用网络的:

    #network = get_network(args.network_name)

    ...

    # start a session
    #saver = tf.train.Saver()
    sess = tf.Session(config=tf.ConfigProto(allow_soft_placement=True))
    with sess:
        tf.initialize_all_variables()
        network = get_network(args.network_name,sess)
        print 'Use network `{:s}` in training'.format(args.network_name)
        saver = tf.train.Saver()
        saver.restore(sess, args.model)
        print ('Loading model weights from {:s}').format(args.model)

        test_net(sess, network, imdb, weights_filename)

结果发现一个矛盾,因为get_taget_boxes的输入输出参数需要时tensor,但内部运行需要使用numpy数组,所以需要把tensor通过sess.run()或.eval()来获得值后转换成numpy数组,但是在创建网络时又还根本没有运行test_net(),所以tensor没有数据,会报输入图像数据的shape为[-1,-1,-1,3]之类的错误:

2020-02-21 14:31:28.427285: W tensorflow/core/framework/op_kernel.cc:1148] Invalid argument: Shape [-1,-1,-1,3] has negative dimensions
2020-02-21 14:31:28.427481: E tensorflow/core/common_runtime/executor.cc:644] Executor failed to create kernel. Invalid argument: Shape [-1,-1,-1,3] has negative dimensions
         [[Node: input_data = Placeholder[dtype=DT_FLOAT, shape=[?,?,?,3], _device="/job:localhost/replica:0/task:0/gpu:0"]()]]
2020-02-21 14:31:28.433744: W tensorflow/core/framework/op_kernel.cc:1148] Invalid argument: Shape [-1,-1,-1,3] has negative dimensions
2020-02-21 14:31:28.433921: E tensorflow/core/common_runtime/executor.cc:644] Executor failed to create kernel. Invalid argument: Shape [-1,-1,-1,3] has negative dimensions
         [[Node: input_data = Placeholder[dtype=DT_FLOAT, shape=[?,?,?,3], _device="/job:localhost/replica:0/task:0/gpu:0"]()]]
Traceback (most recent call last):
  File "./tools/test_net.py", line 96, in
    network = get_network(args.network_name,sess)
  File "/root/Faster-RCNN_TF/tools/../lib/networks/factory.py", line 28, in get_network
    return networks.VGGnet_test(sess)
  File "/root/Faster-RCNN_TF/tools/../lib/networks/VGGnet_test.py", line 18, in __init__
    self.setup()
  File "/root/Faster-RCNN_TF/tools/../lib/networks/VGGnet_test.py", line 68, in setup
    .get_target_boxes(name="model_output"))
  File "/root/Faster-RCNN_TF/tools/../lib/networks/network.py", line 38, in layer_decorated
    layer_output = op(self, layer_input, *args, **kwargs)
  File "/root/Faster-RCNN_TF/tools/../lib/networks/network.py", line 291, in get_target_boxes
    cls_prob = self.session.run(input[1])
  File "/usr/local/lib/python2.7/dist-packages/tensorflow/python/client/session.py", line 789, in run
    run_metadata_ptr)
  File "/usr/local/lib/python2.7/dist-packages/tensorflow/python/client/session.py", line 997, in _run
    feed_dict_string, options, run_metadata)
  File "/usr/local/lib/python2.7/dist-packages/tensorflow/python/client/session.py", line 1132, in _do_run
    target_list, options, run_metadata)
  File "/usr/local/lib/python2.7/dist-packages/tensorflow/python/client/session.py", line 1152, in _do_call
    raise type(e)(node_def, op, message)
tensorflow.python.framework.errors_impl.InvalidArgumentError: Shape [-1,-1,-1,3] has negative dimensions
         [[Node: input_data = Placeholder[dtype=DT_FLOAT, shape=[?,?,?,3], _device="/job:localhost/replica:0/task:0/gpu:0"]()]]

Caused by op u'input_data', defined at:
  File "./tools/test_net.py", line 96, in
    network = get_network(args.network_name,sess)
  File "/root/Faster-RCNN_TF/tools/../lib/networks/factory.py", line 28, in get_network
    return networks.VGGnet_test(sess)
  File "/root/Faster-RCNN_TF/tools/../lib/networks/VGGnet_test.py", line 13, in __init__
    self.data = tf.placeholder(tf.float32, shape=[None, None, None, 3],name='input_data')
  File "/usr/local/lib/python2.7/dist-packages/tensorflow/python/ops/array_ops.py", line 1530, in placeholder
    return gen_array_ops._placeholder(dtype=dtype, shape=shape, name=name)
  File "/usr/local/lib/python2.7/dist-packages/tensorflow/python/ops/gen_array_ops.py", line 1954, in _placeholder
    name=name)
  File "/usr/local/lib/python2.7/dist-packages/tensorflow/python/framework/op_def_library.py", line 767, in apply_op
    op_def=op_def)
  File "/usr/local/lib/python2.7/dist-packages/tensorflow/python/framework/ops.py", line 2506, in create_op
    original_op=self._default_original_op, op_def=op_def)
  File "/usr/local/lib/python2.7/dist-packages/tensorflow/python/framework/ops.py", line 1269, in __init__
    self._traceback = _extract_stack()

InvalidArgumentError (see above for traceback): Shape [-1,-1,-1,3] has negative dimensions
         [[Node: input_data = Placeholder[dtype=DT_FLOAT, shape=[?,?,?,3], _device="/job:localhost/replica:0/task:0/gpu:0"]()]]

所以也走不通,最后又回到这个需求:把get_target_boxes()里的基于numpy的运算和nms计算全部改为tensorflow的API来实现,但是这又不可能...  所以把faster-rcnn识别图片获得原始数据后的后续处理封装成网络的一个定制op或网络的最后的layer都失败了。

     虽然把faster-rcnn模型转换成pb文件只转换到识别图片获得原始数据为止可以轻松实现,但是,把后面的大量的难度有点高的数组运算之类的功能让模型的调用客户端去实现,这无论从软件架构设计上还是从成本投入上来说都是愚蠢的做法,如果这样做,那还不如保持目前的python封装代码,再加点代码封装成个server部署在linux板子上,让安卓板子上的app以网络通讯的方式调用,实现方案简单成熟多了,也不用换一个模型安卓端又需做大改动,而且模型在单独的板子上运行性能肯定比和安卓app挤在安卓板上运行好多了。

     虽然这次想把faster-rcnn做彻底的封装后转换成pb文件后导入失败了,连续熬了几个深更半夜却没完美成功比较遗憾,但是还是积累了点经验,起码以后评估一个tensorflow版模型是否适合、是否有必要转换成pb文件有了评估方面的经验,快速浏览一下模型的测试部分的代码就知道了,如果一个模型在识别图像获得结果后还需做大量的后续处理,而这些代码又多是借助numpy的数组运算功能实现的,那么就不要试图去转换成pb了,转换为pb但对识别结果的后续处理不包含在pb中,那这没有多大价值,这么做还不如去对模型做server端封装供外部调用。

     Tensorflow2.0使用动态图跟PyTorch很像了,去掉了静态图这种非常不灵活的做法,把代码全部改成Tensorflow2的代码也许能实现上面改造faster-rcnn网络后固化模型成pb文件实现易用封装的方案,不确定,后面哪天有时间了可以试验一下。

你可能感兴趣的:(Tensorflow)