這是 當(dāng)微信小程序遇上TensorFlow 系列文章的第四篇文章,閱讀本文,你將了解到:
如何查看tensorflow SavedModel的簽名
如何加載tensorflow SavedModel
如何修改現(xiàn)有的TensorFlow模型,增加輸入層
如果你想要了解更多關(guān)于本項目,可以參考這個系列的前三篇文章:
當(dāng)微信小程序遇上TensorFlow:Server端實現(xiàn)
當(dāng)微信小程序遇上TensorFlow:Server端實現(xiàn)補充
當(dāng)微信小程序遇上TensorFlow:小程序?qū)崿F(xiàn)
關(guān)于Tensorflow SavedModel格式模型的處理,可以參考前面的文章:
Tensorflow SavedModel模型的保存與加載
如何查看tensorflow SavedModel格式模型的信息
如何合并兩個TensorFlow模型
截至到目前為止,我們實現(xiàn)了一個簡單的微信小程序,使用開源的Simple TensorFlow Serving部署了服務(wù)端。但這種實現(xiàn)方案還存在一個重大問題:小程序和服務(wù)端通信傳遞的圖像數(shù)據(jù)是(299, 299, 3)二進制數(shù)組的JSON化表示,這種二進制數(shù)據(jù)JSON化的最大缺點是數(shù)據(jù)量太大,一個簡單的299 x 299的圖像,這樣表示大約有3 ~ 4 M。其實HTTP傳輸二進制數(shù)據(jù)常用的方案是對二進制數(shù)據(jù)進行base64編碼,經(jīng)過base64編碼,雖然數(shù)據(jù)量比二進制也會大一些,但相比JSON化的表示,還是小很多。
所以現(xiàn)在的問題是,如何讓服務(wù)器端接收base64編碼的圖像數(shù)據(jù)?
為了解決這一問題,我們還是先看看模型的輸入輸出,看看其簽名是怎樣的?這里的簽名,并非是為了保證模型不被修改的那種電子簽名。我的理解是類似于編程語言中模塊的輸入輸出信息,比如函數(shù)名,輸入?yún)?shù)類型,輸出參數(shù)類型等等。借助于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
從中我們可以看出模型的輸入?yún)?shù)名為image,其shape為(-1, 299, 299, 3),這里-1代表可以批量輸入,通常我們只輸入一張圖像,所以這個維度通常是1。輸出參數(shù)名為prediction,其shape為(-1, 120),-1和輸入是對應(yīng)的,120代表120組狗類別的概率。
現(xiàn)在的問題是,我們能否在模型的輸入前面增加一層,進行base64及解碼處理呢?
也許你認(rèn)為可以在服務(wù)器端編寫一段代碼,進行base64字符串解碼,然后再轉(zhuǎn)交給Simple Tensorflow Serving進行處理,或者修改Simple TensorFlow Serving的處理邏輯,但這種修改方案增加了服務(wù)器端的工作量,使得服務(wù)器部署方案不再通用,放棄!
其實在上一篇文章《 如何合并兩個TensorFlow模型 》中我們已經(jīng)講到了如何連接兩個模型,這里再稍微重復(fù)一下,首先是編寫一個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)
這里調(diào)用了graph_util.convert_variables_to_constants將模型中的變量轉(zhuǎn)化為常量,也就是所謂的凍結(jié)圖(freeze graph)操作。
利用tf.import_graph_def方法,我們可以導(dǎo)入圖到現(xiàn)有圖中,注意第二個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節(jié)點是啥(注意不能使用模型部署的signature信息)?可以使用如下代碼遍歷graph的節(jié)點名稱:
for n in g2def.node: print(n.name)
注意,我們可以將連接之后的模型保存在./models/inception_v3/2/目錄下,原來的./models/inception_v3/1/也不用刪除,這樣兩個版本的模型可以同時提供服務(wù),方便從V1模型平滑過渡到V2版本模型。
我們修改一下原來的test_client.py代碼,增加一個model_version參數(shù),這樣就可以決定與哪個版本的模型進行通信:
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)
經(jīng)過一個多星期的研究和反復(fù)嘗試,終于解決了圖像數(shù)據(jù)的base64編碼通信問題。難點在于雖然模型是編寫retrain腳本重新訓(xùn)練的,但這段代碼不是那么好懂,想要在retrain時增加輸入層也是嘗試失敗。最后從Tensorflow模型轉(zhuǎn)Tensorflow Lite模型時的freezing graph得到靈感,將圖中的變量固化為常量,才解決了合并模型變量加載的問題。雖然網(wǎng)上提供了一些恢復(fù)變量的方法,但實際用起來并不管用,可能是Tensorflow發(fā)展太快,以前的一些方法已經(jīng)過時了。
本文的完整代碼請參閱:https://github.com/mogoweb/aiexamples/tree/master/AIDog/serving
點擊 閱讀原文 可以直達在github上的項目。
到目前為止,關(guān)鍵的問題已經(jīng)都解決,接下來就需要繼續(xù)完善微信小程序的展現(xiàn),以及如何提供識別率,敬請關(guān)注我的微信公眾號:云水木石,獲取最新動態(tài)。
How to Show Signatures of Tensorflow Saved Model
Serving Image-Based Deep Learning Models with TensorFlow-Serving’s RESTful API
Tensorflow: How to replace a node in a calculation graph?