初步学习tensorflow serving
的手写数字识别模型部署。包括简单的模型训练、保存、部署上线。因为对docker
和网络不太熟悉,可能会有部分错误,但是看完博客,能跑通整个流程。此博客将详细介绍流程,但是不详细介绍每个流程的每步的含义,因为这些步骤不会随着任务的不同而发生太大改变。在后续博客中可能会精细介绍每一步的含义。
国际惯例,参考博客:
tensorflow官方文档:低阶API保存和恢复
tensorflow官方文档:tensorflow serving
tensorflow github案例:mnist和resnet
Tensorflow SavedModel模型的保存与加载
如何用TF Serving部署TensorFlow模型
Tensorflow Serving | Tensorflow Serving
Tensorflow使用SavedModel格式模型
我们给你推荐一种TensorFlow模型格式
使用 TensorFlow Serving 和 Docker 快速部署机器学习服务
如何将TensorFlow Serving的性能提高超过70%?
跟之前的博客一样,简单搭建一个卷积网络,输入数据是mnist
,还有损失函数和评估函数:
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
steps = 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])
#损失函数
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)
#评估
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))
checkpoint
这部分就不细说了,我们之前训练模型基本都是这个方法:
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
保存训练好的模型
tesorflow serving
准备的模型保存方法第一步:准备好模型需要保存的位置以及版本控制:
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
目录存在,将会报错,因为tensorflow serving
有一个有点就是在更新模型的时候,无需停止服务,服务是根据版本来控制的,所以每次训练都是一个新版本。而且这个模型最好是绝对路径,因为后续部署服务的时候,模型不能是相对路径。
错误提示:
AssertionError: Export directory already exists. Please specify a different export directory: b'/tmp/cnn_mnist/1'
第二步:将输入输出打包起来,方便从客户端接收参数
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
))
注意这里有个method_name
, 这个需要用tf.saved_model.signature_constants.
里面的一系列NAME
,因为后续客户端传递给服务端的请求是json
格式,而predcit
、regress
、classify
任务的json
格式有区别,具体格式看这里,当然后面也会讲到
第三步:就是在Session
中保存模型了
#训练与保存
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')
这一步,官方文档有详细介绍,具体参数的使用没仔细看,目前只需要前面三个必须传入sess
、tag
、signature_def_map
,重点是将上面定义的包含输入输出与任务种类的prediction_signature
传进来。给个名字predict_images
是为了后续调用服务的时候,说明我们要调用哪个服务,所以这个signature_def_map
理论上应该可以包含多个任务接口,而官方例子也有相关操作:
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
提供的模型文件是如何训练和保存的已经介绍完毕,在下一篇博客应该会探索如何将训练好的checkpoint
转换为tensorflow serving
可使用的模型文件。
安装docker的方法在这里能找到,或者docker官方文档我当时好像就一句话搞定:
sudo apt install docker.io
因为我以前没装过docker,服务器上用过一丢丢。
tensorflow serving
镜像首先拉取tensorflow
的镜像:
docker pull tensorflow/serving
有时候由于环境限制,可以从别人pull
好的镜像中恢复,镜像的导出和导入可参考此处,主要用到了:
有镜像的电脑导出镜像:
docker save 582a4 > tensorflow_serving.tar
其中582a4
是用docker images
查看的tensorflow/serving
的ID。
无镜像的电脑导入镜像:
docker load < tensorflow_serving.tar
通常导入以后,REPOSITORY
和TAG
是none
,最好给个名区分:
docker tag 91abe tensorflow/serving:latest
这里我备用了一份tensorflow/serving
镜像:
链接: https://pan.baidu.com/s/1l_ZGVkRKcP4HgSKxGgekRA 提取码: ewqv
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
这句话的意思就是:
启动docker
容器container
实现gRPC
和REST
端口到主机端口的映射,注意,port1:port2
,前者是主机端口,后者是tensorflow serving docker
的gRPC
和REST
端口。主机端口port1
可以随便改,只要没被占用,但是tensorflow serving docker
的两个端口固定,不能动。
终端通过sudo netstat -nap
可以看到tcp6
中开启了两个端口,分别就是9500
和9501
。
运行容器后的最后一部分输出是
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 ...
可以发现,服务端自动查找新模型,同事给出了gRPC
和REST
的端口,但是这连个端口貌似用不了,难道是因为我们做映射了?后面所有的访问,无论是用docker
的ip
还是用host
的ip
,一律通过ip:9500/9501
接收请求。
docker run -t --rm -p 9500:8500 -p:9501:8501 \
-v "/tmp/cnn_mnist:/models/cnn_mnist" \
-e MODEL_NAME=cnn_mnist \
tensorflow/serving
其实和上面一样,只不过对docker
的用法不同而已。
如果对docker
比较熟悉,可以两种方法都记住,不熟悉的话,熟记一种方法就行了。
下面的dockerip
与hostip
分别为ifconfig -a
查出来的docker
和host
的ip
测试1:
输入:curl http://localhost:8501/v1/models/cnn_mnist
输出:curl: (7) Failed to connect to localhost port 8501: 拒绝连接
测试2:
输入:curl http://localhost:8500/v1/models/cnn_mnist
输出:curl: (7) Failed to connect to localhost port 8500: 拒绝连接
测试3:
输入:curl http://dockerip:9500/v1/models/cnn_mnist
输出:
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: " to save to a file.
说明没有拒绝连接
测试4:
输入:curl http://hostip:9500/v1/models/cnn_mnist
输出:
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: " to save to a file.
说明没有拒绝连接
测试5:
输入:curl http://dockerip:9501/v1/models/cnn_mnist
输出:
{
"model_version_status": [
{
"version": "1",
"state": "AVAILABLE",
"status": {
"error_code": "OK",
"error_message": ""
}
}
]
}
没拒绝连接
测试6:
输入:curl http://hostip:9501/v1/models/cnn_mnist
输出:
{
"model_version_status": [
{
"version": "1",
"state": "AVAILABLE",
"status": {
"error_code": "OK",
"error_message": ""
}
}
]
}
暂时测试这几种情况,其余组合自己可以测试看看,如果拒绝连接,那就说明ip和对应接口组合不通,无法调用服务。
上面说过了,tensorflow serving
有两类端口:gRPC
和REST API
,关于这两个区别,可以查看这里,下面分别讲解tensorflow serving
中分别怎么请求和解析返回数据。
注意手写数字识别模型接受的是$ (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
定义入口接收参数:
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,)
连接服务:
server = FLAGS.host + ':' + FLAGS.port
channel = grpc.insecure_channel(server)
stub = prediction_service_pb2_grpc.PredictionServiceStub(channel)
请求服务:
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
对应库呢?后面学习的时候再看。tensor
,所以得用tf.contrib.util.make_tensor_proto
转换解析请求:
scores=result.outputs['scores'].float_val
pred_label = np.argmax(scores)
print('pred_label',pred_label)
【注】C++的解析方法戳这里,python的解析方法戳这里
运行测试
在终端中执行:
python serving_test_grpc.py --host '127.0.0.1' --port '9500' --image './test_image/6.png'
这里面的host
换成docker
或者主机的ip
,port
换成你上面开启的端口。
与gRPC
区别很大,需要用json
作为请求的输入格式,具体格式查阅这里,我们使用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>
}
引入相关包:
import requests
import numpy as np
import cv2
读取数据:注意最后转换为 ( 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
格式化请求:此处一定要严格按照下面的语句书写,不然请求很容易失败
predict_request='{"signature_name": "predict_images", "instances":[{"images":%s}] }' %img
请求失败时,提示
requests.exceptions.HTTPError: 400 Client Error: Bad Request for url: http://127.0.0.1:9501/v1/models/cnn_mnist:predict
如果提示这个bad request
,不要问为什么,问就是你写错json
请求了。
发送请求与接收回复以及解析
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()
可以返回时间,用于测试效率.
tensorflow_model_server
启动服务按照官方文档走:
第一步
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
这个是在线方法,还有一个离线方法,我就不写了,戳这里就行,听说离线编译方法的成功率有点低。以后有机会再试试。
一条命令搞定
tensorflow_model_server --port=9500 --rest_api_port=9501 \
--model_name=cnn_mnist --model_base_path=/tmp/cnn_mnist
就是直接开启gRPC
端口为9500
以及开启REST
端口为9501
,剩下的请求服务与上面的docker
教程一模一样。
端口占用
有时候提示端口被占用
如果使用docker
的方法启动服务,可以使用docker ps
看启动的服务占用的端口,如果有,就用docker kill CONTAINER_ID
如果使用tensorflow_model_server
启动服务,使用netstat -nap
查找端口被谁占用,然后kill -9 PID
重启docker
容器
当你kill
掉docker
里面的容器时,并非移除了该容器,可以通过docker ps -a
查看所有容器,包括关闭容器,当你再次启动服务的时候,没必要去执行docker run .....
的那个脚本,直接docker start CONTAINER_ID
即可。
官方有个比较好的例子,是调用resnet
作为分类服务的
模型下载方法:
#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
网盘下载:
链接: https://pan.baidu.com/s/1Kyh8sGggdKld4u1wuQSAbA 提取码: 4k3z
服务启动方法:
docker run -p 8500:8500 -p 8501:8501 \
--mount type=bind,source=/tmp/resnet,target=/models/resnet \
-e MODEL_NAME=resnet -t tensorflow/serving
检查模型的输入输出
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_FLOAT
shape: (-1, 784)
name: X:0
The given SavedModel SignatureDef contains the following output(s):
outputs['scores'] tensor_info:
dtype: DT_FLOAT
shape: (-1, 10)
name: dense_1/BiasAdd:0
Method name is: tensorflow/serving/predict
线上调用
如果用其他ip或者电脑调用模型,请求的ip必须是host
,而非localhost
这里只是一个初步入门,后续会更进一步了解其他功能。
本文所有代码打包下载:
链接: https://pan.baidu.com/s/1MOUnU-sUAxfOjAHSPDKvkA 提取码: sa88
里面包含我调试的脚本,懒得剔除了,有兴趣慢慢看