【TensorFlow-serving】初步学习模型部署

前言

初步学习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格式,而predcitregressclassify任务的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')

这一步,官方文档有详细介绍,具体参数的使用没仔细看,目前只需要前面三个必须传入sesstagsignature_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的方法在这里能找到,或者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 

通常导入以后,REPOSITORYTAGnone,最好给个名区分:

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

  • 实现gRPCREST端口到主机端口的映射,注意,port1:port2,前者是主机端口,后者是tensorflow serving dockergRPCREST端口。主机端口port1可以随便改,只要没被占用,但是tensorflow serving docker的两个端口固定,不能动。

终端通过sudo netstat -nap可以看到tcp6中开启了两个端口,分别就是95009501

运行容器后的最后一部分输出是

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 ...

可以发现,服务端自动查找新模型,同事给出了gRPCREST的端口,但是这连个端口貌似用不了,难道是因为我们做映射了?后面所有的访问,无论是用dockerip还是用hostip,一律通过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比较熟悉,可以两种方法都记住,不熟悉的话,熟记一种方法就行了。

测试服务是否开通

下面的dockeriphostip分别为ifconfig -a查出来的dockerhostip

  • 测试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: <FILE>" 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: <FILE>" 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有两类端口:gRPCREST 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_grpcpredict_pb2,那么是否有classifyregress对应库呢?后面学习的时候再看。
  • 还有就是因为模型接收的是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或者主机的ipport换成你上面开启的端口。

使用REST

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容器
    当你killdocker里面的容器时,并非移除了该容器,可以通过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

里面包含我调试的脚本,懒得剔除了,有兴趣慢慢看

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

风翼冰舟

额~~~CSDN还能打赏了

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值