我的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=<graph_path>  

上面的輸出節點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 <module>
    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 <module>
    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 <module>
    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文件實現易用封裝的方案,不確定,後面哪天有時間了可以試驗一下。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章