【TensorFlow-serving】初步学习模型部署
前言
初步學(xué)習(xí)tensorflow serving的手寫數(shù)字識(shí)別模型部署。包括簡(jiǎn)單的模型訓(xùn)練、保存、部署上線。因?yàn)閷?duì)docker和網(wǎng)絡(luò)不太熟悉,可能會(huì)有部分錯(cuò)誤,但是看完博客,能跑通整個(gè)流程。此博客將詳細(xì)介紹流程,但是不詳細(xì)介紹每個(gè)流程的每步的含義,因?yàn)檫@些步驟不會(huì)隨著任務(wù)的不同而發(fā)生太大改變。在后續(xù)博客中可能會(huì)精細(xì)介紹每一步的含義。
國(guó)際慣例,參考博客:
tensorflow官方文檔:低階API保存和恢復(fù)
tensorflow官方文檔:tensorflow serving
tensorflow github案例:mnist和resnet
Tensorflow SavedModel模型的保存與加載
如何用TF Serving部署TensorFlow模型
Tensorflow Serving | Tensorflow Serving
Tensorflow使用SavedModel格式模型
我們給你推薦一種TensorFlow模型格式
使用 TensorFlow Serving 和 Docker 快速部署機(jī)器學(xué)習(xí)服務(wù)
如何將TensorFlow Serving的性能提高超過(guò)70%?
模型構(gòu)建
跟之前的博客一樣,簡(jiǎn)單搭建一個(gè)卷積網(wǎng)絡(luò),輸入數(shù)據(jù)是mnist,還有損失函數(shù)和評(píng)估函數(shù):
import tensorflow as tf from tensorflow.examples.tutorials.mnist import input_datasteps = 1000 batch_size = 100 mnist = input_data.read_data_sets('./mnist_dataset',one_hot=True)def conv_network(x):x = tf.reshape(x,[-1,28,28,1])# 第一層卷積conv1 = tf.layers.conv2d(inputs=x,filters=32,kernel_size=[5,5],activation=tf.nn.relu)conv1 = tf.layers.max_pooling2d(conv1,pool_size=[2,2],strides=[2,2])#第二層卷積conv2 = tf.layers.conv2d(inputs=conv1,filters=64,kernel_size=[3,3],activation=tf.nn.relu)conv2 = tf.layers.max_pooling2d(conv2,pool_size=[2,2],strides=[2,2])#第三層卷積conv3 = tf.layers.conv2d(inputs=conv2,filters=32,kernel_size=[3,3],activation=tf.nn.relu)conv3 = tf.layers.max_pooling2d(inputs=conv3,pool_size=[2,2],strides=[2,2])#全連接fc1 = tf.layers.flatten(conv3)fc1 = tf.layers.dense(fc1,500,activation=tf.nn.relu)#輸出分類fc2 = tf.layers.dense(fc1,10)return fc2#輸入輸出容器 input_x = tf.placeholder(dtype=tf.float32,shape=[None,28*28],name='X') input_y = tf.placeholder(dtype=tf.int32,shape=[None,10])#損失函數(shù) model = conv_network(input_x) logit_loss = tf.nn.softmax_cross_entropy_with_logits_v2(labels=input_y,logits=model) optimize = tf.train.AdamOptimizer(0.001).minimize(logit_loss) #評(píng)估 pred_equal = tf.equal(tf.arg_max(model,1),tf.arg_max(input_y,1)) accuracy = tf.reduce_mean(tf.cast(pred_equal,tf.float32))模型保存
傳統(tǒng)方法checkpoint
這部分就不細(xì)說(shuō)了,我們之前訓(xùn)練模型基本都是這個(gè)方法:
init = tf.global_variables_initializer() saver = tf. train.Saver(max_to_keep=1) tf.add_to_collection('pred',model)with tf.Session() as sess:sess.run(init)for step in range(steps):data_x,data_y = mnist.train.next_batch(batch_size)test_x,test_y = mnist.test.next_batch(1000) train_acc = sess.run(optimize,feed_dict={input_x:data_x,input_y:data_y}) if(step % 100==0 or step==1):accuracy_val = sess.run(accuracy,feed_dict={input_x:data_x,input_y:data_y})print('steps:{0},val_loss:{1}'.format(step,accuracy_val))#保存模型print('train finished!')saver.save(sess,'./model/cnn')主要就是利用tf.train.Saver保存訓(xùn)練好的模型
為tesorflow serving準(zhǔn)備的模型保存方法
第一步:準(zhǔn)備好模型需要保存的位置以及版本控制:
model_version = 1 #版本控制 export_path_base = '/tmp/cnn_mnist' export_path = os.path.join(tf.compat.as_bytes(export_path_base),tf.compat.as_bytes(str(model_version))) print('Exporting trained model to',export_path) builder = tf.saved_model.builder.SavedModelBuilder(export_path)tensor_info_x = tf.saved_model.utils.build_tensor_info(input_x) tensor_info_y = tf.saved_model.utils.build_tensor_info(model) prediction_signature = (tf.saved_model.signature_def_utils.build_signature_def(inputs={'images':tensor_info_x},outputs={'scores':tensor_info_y},method_name = tf.saved_model.signature_constants.PREDICT_METHOD_NAME))此處注意,如果你的export_path_base/model_version目錄存在,將會(huì)報(bào)錯(cuò),因?yàn)閠ensorflow serving有一個(gè)有點(diǎn)就是在更新模型的時(shí)候,無(wú)需停止服務(wù),服務(wù)是根據(jù)版本來(lái)控制的,所以每次訓(xùn)練都是一個(gè)新版本。而且這個(gè)模型最好是絕對(duì)路徑,因?yàn)楹罄m(xù)部署服務(wù)的時(shí)候,模型不能是相對(duì)路徑。
錯(cuò)誤提示:
AssertionError: Export directory already exists. Please specify a different export directory: b'/tmp/cnn_mnist/1'第二步:將輸入輸出打包起來(lái),方便從客戶端接收參數(shù)
prediction_signature = (tf.saved_model.signature_def_utils.build_signature_def(inputs={'images':tensor_info_x},outputs={'scores':tensor_info_y},method_name = tf.saved_model.signature_constants.PREDICT_METHOD_NAME))注意這里有個(gè)method_name, 這個(gè)需要用tf.saved_model.signature_constants.里面的一系列NAME,因?yàn)楹罄m(xù)客戶端傳遞給服務(wù)端的請(qǐng)求是json格式,而predcit、regress、classify任務(wù)的json格式有區(qū)別,具體格式看這里,當(dāng)然后面也會(huì)講到
第三步:就是在Session中保存模型了
#訓(xùn)練與保存 with tf.Session() as sess:sess.run(tf.global_variables_initializer())for step in range(steps):data_x,data_y = mnist.train.next_batch(batch_size)test_x,test_y = mnist.test.next_batch(1000) train_acc = sess.run(optimize,feed_dict={input_x:data_x,input_y:data_y}) if(step % 100==0 or step==1):accuracy_val = sess.run(accuracy,feed_dict={input_x:data_x,input_y:data_y})print('steps:{0},val_loss:{1}'.format(step,accuracy_val))#保存模型 builder.add_meta_graph_and_variables(sess,[tf.saved_model.tag_constants.SERVING],signature_def_map = {'predict_images':prediction_signature},main_op = tf.tables_initializer(),strip_default_attrs = True)builder.save()print('Done exporting')這一步,官方文檔有詳細(xì)介紹,具體參數(shù)的使用沒仔細(xì)看,目前只需要前面三個(gè)必須傳入sess、tag、signature_def_map,重點(diǎn)是將上面定義的包含輸入輸出與任務(wù)種類的prediction_signature傳進(jìn)來(lái)。給個(gè)名字predict_images是為了后續(xù)調(diào)用服務(wù)的時(shí)候,說(shuō)明我們要調(diào)用哪個(gè)服務(wù),所以這個(gè)signature_def_map理論上應(yīng)該可以包含多個(gè)任務(wù)接口,而官方例子也有相關(guān)操作:
builder.add_meta_graph_and_variables(sess, [tf.saved_model.tag_constants.SERVING],signature_def_map={'predict_images':prediction_signature,tf.saved_model.signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY:classification_signature,},main_op=tf.tables_initializer(),strip_default_attrs=True)至此,為tensorflow serving提供的模型文件是如何訓(xùn)練和保存的已經(jīng)介紹完畢,在下一篇博客應(yīng)該會(huì)探索如何將訓(xùn)練好的checkpoint轉(zhuǎn)換為tensorflow serving可使用的模型文件。
通過(guò)docker部署模型
安裝docker的方法在這里能找到,或者docker官方文檔我當(dāng)時(shí)好像就一句話搞定:
sudo apt install docker.io因?yàn)槲乙郧皼]裝過(guò)docker,服務(wù)器上用過(guò)一丟丟。
tensorflow serving鏡像
首先拉取tensorflow的鏡像:
docker pull tensorflow/serving有時(shí)候由于環(huán)境限制,可以從別人pull好的鏡像中恢復(fù),鏡像的導(dǎo)出和導(dǎo)入可參考此處,主要用到了:
有鏡像的電腦導(dǎo)出鏡像:
docker save 582a4 > tensorflow_serving.tar其中582a4是用docker images查看的tensorflow/serving的ID。
無(wú)鏡像的電腦導(dǎo)入鏡像:
docker load < tensorflow_serving.tar通常導(dǎo)入以后,REPOSITORY和TAG是none,最好給個(gè)名區(qū)分:
docker tag 91abe tensorflow/serving:latest這里我備用了一份tensorflow/serving鏡像:
鏈接: https://pan.baidu.com/s/1l_ZGVkRKcP4HgSKxGgekRA 提取碼: ewqv
啟動(dòng)在線服務(wù)
方法一:
docker run -p 9500:8500 -p:9501:8501 \ --mount type=bind,source=/tmp/cnn_mnist,target=/models/cnn_mnist \ -e MODEL_NAME=cnn_mnist -t tensorflow/serving這句話的意思就是:
-
啟動(dòng)docker容器container
-
實(shí)現(xiàn)gRPC和REST端口到主機(jī)端口的映射,注意,port1:port2,前者是主機(jī)端口,后者是tensorflow serving docker的gRPC和REST端口。主機(jī)端口port1可以隨便改,只要沒被占用,但是tensorflow serving docker的兩個(gè)端口固定,不能動(dòng)。
終端通過(guò)sudo netstat -nap可以看到tcp6中開啟了兩個(gè)端口,分別就是9500和9501。
運(yùn)行容器后的最后一部分輸出是
2019-09-03 11:21:48.489776: I tensorflow_serving/servables/tensorflow/saved_model_warmup.cc:103] No warmup data file found at /models/cnn_mnist/1/assets.extra/tf_serving_warmup_requests 2019-09-03 11:21:48.489938: I tensorflow_serving/core/loader_harness.cc:86] Successfully loaded servable version {name: cnn_mnist version: 1} 2019-09-03 11:21:48.504477: I tensorflow_serving/model_servers/server.cc:324] Running gRPC ModelServer at 0.0.0.0:8500 ... [warn] getaddrinfo: address family for nodename not supported 2019-09-03 11:21:48.519991: I tensorflow_serving/model_servers/server.cc:344] Exporting HTTP/REST API at:localhost:8501 ... [evhttp_server.cc : 239] RAW: Entering the event loop ...可以發(fā)現(xiàn),服務(wù)端自動(dòng)查找新模型,同事給出了gRPC和REST的端口,但是這連個(gè)端口貌似用不了,難道是因?yàn)槲覀冏鲇成淞?#xff1f;后面所有的訪問,無(wú)論是用docker的ip還是用host的ip,一律通過(guò)ip:9500/9501接收請(qǐng)求。
方法二
docker run -t --rm -p 9500:8500 -p:9501:8501 \-v "/tmp/cnn_mnist:/models/cnn_mnist" \-e MODEL_NAME=cnn_mnist \tensorflow/serving其實(shí)和上面一樣,只不過(guò)對(duì)docker的用法不同而已。
如果對(duì)docker比較熟悉,可以兩種方法都記住,不熟悉的話,熟記一種方法就行了。
測(cè)試服務(wù)是否開通
下面的dockerip與hostip分別為ifconfig -a查出來(lái)的docker和host的ip
-
測(cè)試1:
輸入:curl http://localhost:8501/v1/models/cnn_mnist
輸出:curl: (7) Failed to connect to localhost port 8501: 拒絕連接 -
測(cè)試2:
輸入:curl http://localhost:8500/v1/models/cnn_mnist
輸出:curl: (7) Failed to connect to localhost port 8500: 拒絕連接 -
測(cè)試3:
Warning: Binary output can mess up your terminal. Use "--output -" to tell Warning: curl to output it to your terminal anyway, or consider "--output Warning: <FILE>" to save to a file.
輸入:curl http://dockerip:9500/v1/models/cnn_mnist
輸出:說(shuō)明沒有拒絕連接
-
測(cè)試4:
Warning: Binary output can mess up your terminal. Use "--output -" to tell Warning: curl to output it to your terminal anyway, or consider "--output Warning: <FILE>" to save to a file.
輸入:curl http://hostip:9500/v1/models/cnn_mnist
輸出:說(shuō)明沒有拒絕連接
-
測(cè)試5:
{"model_version_status": [{"version": "1","state": "AVAILABLE","status": {"error_code": "OK","error_message": ""}}] }
輸入:curl http://dockerip:9501/v1/models/cnn_mnist
輸出:沒拒絕連接
-
測(cè)試6:
{"model_version_status": [{"version": "1","state": "AVAILABLE","status": {"error_code": "OK","error_message": ""}}] }
輸入:curl http://hostip:9501/v1/models/cnn_mnist
輸出:
暫時(shí)測(cè)試這幾種情況,其余組合自己可以測(cè)試看看,如果拒絕連接,那就說(shuō)明ip和對(duì)應(yīng)接口組合不通,無(wú)法調(diào)用服務(wù)。
調(diào)用模型
上面說(shuō)過(guò)了,tensorflow serving有兩類端口:gRPC和REST API,關(guān)于這兩個(gè)區(qū)別,可以查看這里,下面分別講解tensorflow serving中分別怎么請(qǐng)求和解析返回?cái)?shù)據(jù)。
注意手寫數(shù)字識(shí)別模型接受的是$ (None,784) $的向量
使用gRPC
引入必要包:
import argparse import tensorflow as tf from tensorflow_serving.apis import predict_pb2,prediction_service_pb2_grpc import grpc import numpy as np import cv2定義入口接收參數(shù):
parser = argparse.ArgumentParser(description='mnist recognization client') parser.add_argument('--host',default='0.0.0.0',help='serve host') parser.add_argument('--port',default='9000',help='serve port') parser.add_argument('--image',default='',help='image path') FLAGS = parser.parse_args()所以,用戶需要輸入的都有:ip、端口、輸入圖像
讀取圖像:
img = cv2.imread(FLAGS.image,cv2.IMREAD_GRAYSCALE) img = cv2.resize(img,(28,28)) _,img = cv2.threshold(img,250,255,cv2.THRESH_BINARY) img = np.array(img,dtype='float32') img = img.reshape((28*28)) print(img.shape) #(784,)連接服務(wù):
server = FLAGS.host + ':' + FLAGS.port channel = grpc.insecure_channel(server) stub = prediction_service_pb2_grpc.PredictionServiceStub(channel)請(qǐng)求服務(wù):
request = predict_pb2.PredictRequest() request.model_spec.name = 'cnn_mnist' request.model_spec.signature_name = 'predict_images' request.inputs['images'].CopyFrom(tf.contrib.util.make_tensor_proto(img,shape=[1,28*28])) result = stub.Predict(request,10.0)【注】
- 這里有prediction_service_pb2_grpc和predict_pb2,那么是否有classify和regress對(duì)應(yīng)庫(kù)呢?后面學(xué)習(xí)的時(shí)候再看。
- 還有就是因?yàn)槟P徒邮盏氖莟ensor,所以得用tf.contrib.util.make_tensor_proto轉(zhuǎn)換
解析請(qǐng)求:
scores=result.outputs['scores'].float_val pred_label = np.argmax(scores) print('pred_label',pred_label)【注】C++的解析方法戳這里,python的解析方法戳這里
運(yùn)行測(cè)試
在終端中執(zhí)行:
python serving_test_grpc.py --host '127.0.0.1' --port '9500' --image './test_image/6.png'這里面的host換成docker或者主機(jī)的ip,port換成你上面開啟的端口。
使用REST
與gRPC區(qū)別很大,需要用json作為請(qǐng)求的輸入格式,具體格式查閱這里,我們使用predict API中的格式:
{// (Optional) Serving signature to use.// If unspecifed default serving signature is used."signature_name": <string>,// Input Tensors in row ("instances") or columnar ("inputs") format.// A request can have either of them but NOT both."instances": <value>|<(nested)list>|<list-of-objects>"inputs": <value>|<(nested)list>|<object> }引入相關(guān)包:
import requests import numpy as np import cv2讀取數(shù)據(jù):注意最后轉(zhuǎn)換為(1,784)(1,784)(1,784)的list
image_path='./test_image/2.png' img = cv2.imread(image_path,cv2.IMREAD_GRAYSCALE) img = cv2.resize(img,(28,28)) _,img = cv2.threshold(img,250,255,cv2.THRESH_BINARY) img = np.array(img,dtype='float32') img = img.reshape((28*28)) img = img[np.newaxis,:] img = img.tolist()用json格式化請(qǐng)求:此處一定要嚴(yán)格按照下面的語(yǔ)句書寫,不然請(qǐng)求很容易失敗
predict_request='{"signature_name": "predict_images", "instances":[{"images":%s}] }' %img請(qǐng)求失敗時(shí),提示
requests.exceptions.HTTPError: 400 Client Error: Bad Request for url: http://127.0.0.1:9501/v1/models/cnn_mnist:predict如果提示這個(gè)bad request,不要問為什么,問就是你寫錯(cuò)json請(qǐng)求了。
發(fā)送請(qǐng)求與接收回復(fù)以及解析
response = requests.post(SERVER_URL, data=predict_request) response.raise_for_status() prediction = response.json()['predictions'][0] print('label:',np.argmax(prediction))用response.elapsed.total_seconds()可以返回時(shí)間,用于測(cè)試效率.
使用tensorflow_model_server啟動(dòng)服務(wù)
安裝
按照官方文檔走:
-
第一步
echo "deb [arch=amd64] http://storage.googleapis.com/tensorflow-serving-apt stable tensorflow-model-server tensorflow-model-server-universal" | sudo tee /etc/apt/sources.list.d/tensorflow-serving.list && \ curl https://storage.googleapis.com/tensorflow-serving-apt/tensorflow-serving.release.pub.gpg | sudo apt-key add - -
第二步
apt-get update && apt-get install tensorflow-model-server -
第三步
apt-get upgrade tensorflow-model-server
這個(gè)是在線方法,還有一個(gè)離線方法,我就不寫了,戳這里就行,聽說(shuō)離線編譯方法的成功率有點(diǎn)低。以后有機(jī)會(huì)再試試。
啟動(dòng)服務(wù)
一條命令搞定
tensorflow_model_server --port=9500 --rest_api_port=9501 \--model_name=cnn_mnist --model_base_path=/tmp/cnn_mnist就是直接開啟gRPC端口為9500以及開啟REST端口為9501,剩下的請(qǐng)求服務(wù)與上面的docker教程一模一樣。
有些小問題總結(jié)
-
端口占用
有時(shí)候提示端口被占用
如果使用docker的方法啟動(dòng)服務(wù),可以使用docker ps看啟動(dòng)的服務(wù)占用的端口,如果有,就用docker kill CONTAINER_ID
如果使用tensorflow_model_server啟動(dòng)服務(wù),使用netstat -nap查找端口被誰(shuí)占用,然后kill -9 PID
-
重啟docker容器
當(dāng)你kill掉docker里面的容器時(shí),并非移除了該容器,可以通過(guò)docker ps -a查看所有容器,包括關(guān)閉容器,當(dāng)你再次啟動(dòng)服務(wù)的時(shí)候,沒必要去執(zhí)行docker run .....的那個(gè)腳本,直接docker start CONTAINER_ID即可。 -
官方有個(gè)比較好的例子,是調(diào)用resnet作為分類服務(wù)的
#https://storage.googleapis.com/download.tensorflow.org/models/official/20181001_resnet/savedmodels/resnet_v2_fp32_savedmodel_NHWC_jpg.tar.gz mkdir /tmp/resnet curl -s https://storage.googleapis.com/download.tensorflow.org/models/official/20181001_resnet/savedmodels/resnet_v2_fp32_savedmodel_NHWC_jpg.tar.gz | tar --strip-components=2 -C /tmp/resnet -xvz
模型下載方法:
網(wǎng)盤下載:
鏈接: https://pan.baidu.com/s/1Kyh8sGggdKld4u1wuQSAbA 提取碼: 4k3z
服務(wù)啟動(dòng)方法:
-
檢查模型的輸入輸出
saved_model_cli show --dir /tmp/cnn_mnist/1/ --all輸出:
signature_def['predict_images']:The given SavedModel SignatureDef contains the following input(s):inputs['images'] tensor_info:dtype: DT_FLOATshape: (-1, 784)name: X:0The given SavedModel SignatureDef contains the following output(s):outputs['scores'] tensor_info:dtype: DT_FLOATshape: (-1, 10)name: dense_1/BiasAdd:0Method name is: tensorflow/serving/predict -
線上調(diào)用
如果用其他ip或者電腦調(diào)用模型,請(qǐng)求的ip必須是host,而非localhost
后記
這里只是一個(gè)初步入門,后續(xù)會(huì)更進(jìn)一步了解其他功能。
本文所有代碼打包下載:
鏈接: https://pan.baidu.com/s/1MOUnU-sUAxfOjAHSPDKvkA 提取碼: sa88
里面包含我調(diào)試的腳本,懶得剔除了,有興趣慢慢看
總結(jié)
以上是生活随笔為你收集整理的【TensorFlow-serving】初步学习模型部署的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 王者荣耀最新墨子大神出装 墨子高胜率六神
- 下一篇: [Writeup]与佛论禅