1. 程式人生 > >當微信小程式遇上TensorFlow:接收base64編碼影象資料

當微信小程式遇上TensorFlow:接收base64編碼影象資料

這是當微信小程式遇上TensorFlow系列文章的第四篇文章,閱讀本文,你將瞭解到:

  1. 如何檢視tensorflow SavedModel的簽名
  2. 如何載入tensorflow SavedModel
  3. 如何修改現有的TensorFlow模型,增加輸入層

如果你想要了解更多關於本專案,可以參考這個系列的前三篇文章:

關於Tensorflow SavedModel格式模型的處理,可以參考前面的文章:

問題

截至到目前為止,我們實現了一個簡單的微信小程式,使用開源的Simple TensorFlow Serving部署了服務端。但這種實現方案還存在一個重大問題:小程式和服務端通訊傳遞的影象資料是(299, 299, 3)二進位制陣列的JSON化表示,這種二進位制資料JSON化的最大缺點是資料量太大,一個簡單的299 x 299的影象,這樣表示大約有3 ~ 4 M。其實HTTP傳輸二進位制資料常用的方案是對二進位制資料進行base64編碼,經過base64編碼,雖然資料量比二進位制也會大一些,但相比JSON化的表示,還是小很多。

所以現在的問題是,如何讓伺服器端接收base64編碼的影象資料?

檢視模型的簽名

為了解決這一問題,我們還是先看看模型的輸入輸出,看看其簽名是怎樣的?這裡的簽名,並非是為了保證模型不被修改的那種電子簽名。我的理解是類似於程式語言中模組的輸入輸出資訊,比如函式名,輸入引數型別,輸出引數型別等等。藉助於Tensorflow提供的saved_model_cli.py工具,我們可以清楚的檢視模型的簽名:

python ./tensorflow/python/tools/saved_model_cli.py show --dir /data/ai/workspace/aiexamples/AIDog/serving/models/inception_v3/ --all

MetaGraphDef with tag-set: 'serve'
contains the following SignatureDefs: signature_def['serving_default']: The given SavedModel SignatureDef contains the following input(s): inputs['image'] tensor_info: dtype: DT_FLOAT shape: (-1, 299, 299, 3) name: Placeholder:0 The given SavedModel SignatureDef contains the following output(
s): outputs['prediction'] tensor_info: dtype: DT_FLOAT shape: (-1, 120) name: final_result:0 Method name is: tensorflow/serving/predict

從中我們可以看出模型的輸入引數名為image,其shape為(-1, 299, 299, 3),這裡-1代表可以批量輸入,通常我們只輸入一張影象,所以這個維度通常是1。輸出引數名為prediction,其shape為(-1, 120),-1和輸入是對應的,120代表120組狗類別的概率。

現在的問題是,我們能否在模型的輸入前面增加一層,進行base64及解碼處理呢?

也許你認為可以在伺服器端編寫一段程式碼,進行base64字串解碼,然後再轉交給Simple Tensorflow Serving進行處理,或者修改Simple TensorFlow Serving的處理邏輯,但這種修改方案增加了伺服器端的工作量,使得伺服器部署方案不再通用,放棄!

修改模型,增加輸入層

其實在上一篇文章《如何合併兩個TensorFlow模型》中我們已經講到了如何連線兩個模型,這裡再稍微重複一下,首先是編寫一個base64解碼、png解碼、影象縮放的模型:

  base64_str = tf.placeholder(tf.string, name='input_string')
  input_str = tf.decode_base64(base64_str)
  decoded_image = tf.image.decode_png(input_str, channels=input_depth)
  # Convert from full range of uint8 to range [0,1] of float32.
  decoded_image_as_float = tf.image.convert_image_dtype(decoded_image,
                                                        tf.float32)
  decoded_image_4d = tf.expand_dims(decoded_image_as_float, 0)
  resize_shape = tf.stack([input_height, input_width])
  resize_shape_as_int = tf.cast(resize_shape, dtype=tf.int32)
  resized_image = tf.image.resize_bilinear(decoded_image_4d,
                                           resize_shape_as_int)
  tf.identity(resized_image, name="DecodePNGOutput")

接下來載入retrain模型:

  with tf.Graph().as_default() as g2:
    with tf.Session(graph=g2) as sess:
      input_graph_def = saved_model_utils.get_meta_graph_def(
          FLAGS.origin_model_dir, tag_constants.SERVING).graph_def

      tf.saved_model.loader.load(sess, [tag_constants.SERVING], FLAGS.origin_model_dir)

      g2def = graph_util.convert_variables_to_constants(
          sess,
          input_graph_def,
          ["final_result"],
          variable_names_whitelist=None,
          variable_names_blacklist=None)

這裡呼叫了graph_util.convert_variables_to_constants將模型中的變數轉化為常量,也就是所謂的凍結圖(freeze graph)操作。

利用tf.import_graph_def方法,我們可以匯入圖到現有圖中,注意第二個import_graph_def,其input是第一個graph_def的輸出,通過這樣的操作,就將兩個計算圖連線起來,最後儲存起來。程式碼如下:

  with tf.Graph().as_default() as g_combined:
    with tf.Session(graph=g_combined) as sess:
      x = tf.placeholder(tf.string, name="base64_string")
      y, = tf.import_graph_def(g1def, input_map={"input_string:0": x}, return_elements=["DecodePNGOutput:0"])
      z, = tf.import_graph_def(g2def, input_map={"Placeholder:0": y}, return_elements=["final_result:0"])

      tf.identity(z, "myOutput")

      tf.saved_model.simple_save(sess,
                                 FLAGS.model_dir,
                                 inputs={"image": x},
                                 outputs={"prediction": z})

如果你不知道retrain出來的模型的input節點是啥(注意不能使用模型部署的signature資訊)?可以使用如下程式碼遍歷graph的節點名稱:

for n in g2def.node:
  print(n.name)

模型部署及測試

注意,我們可以將連線之後的模型儲存在./models/inception_v3/2/目錄下,原來的./models/inception_v3/1/也不用刪除,這樣兩個版本的模型可以同時提供服務,方便從V1模型平滑過渡到V2版本模型。

我們修改一下原來的test_client.py程式碼,增加一個model_version引數,這樣就可以決定與哪個版本的模型進行通訊:

  with open(file_name, "rb") as image_file:
    encoded_string = str(base64.urlsafe_b64encode(image_file.read()), "utf-8")

  if enable_ssl :
    endpoint = "https://127.0.0.1:8500"
  else:
    endpoint = "http://127.0.0.1:8500"

  json_data = {"model_name": model_name,
               "model_version": model_version,
               "data": {"image": encoded_string}
              }
  result = requests.post(endpoint, json=json_data)

小結

經過一個多星期的研究和反覆嘗試,終於解決了影象資料的base64編碼通訊問題。難點在於雖然模型是編寫retrain指令碼重新訓練的,但這段程式碼不是那麼好懂,想要在retrain時增加輸入層也是嘗試失敗。最後從Tensorflow模型轉Tensorflow Lite模型時的freezing graph得到靈感,將圖中的變數固化為常量,才解決了合併模型變數載入的問題。雖然網上提供了一些恢復變數的方法,但實際用起來並不管用,可能是Tensorflow發展太快,以前的一些方法已經過時了。

點選閱讀原文可以直達在github上的專案。

到目前為止,關鍵的問題已經都解決,接下來就需要繼續完善微信小程式的展現,以及如何提高識別率,敬請關注我的微信公眾號:雲水木石,獲取最新動態。

image

參考