本文大部分是对官网的描述做些备注和个人理解。如有需要,请直接查看官网原文。
https://tensorflow.google.cn/serving/serving_basic?hl=zh-CN (官方网址)
前置知识: 建议阅读 05 - Tensorflow 模型保存与恢复
一般来说,算法开发人员负责设计和训练模型,而运维人员(或业务开发人员)负责把模型导出并部署到线上。那么在设计时,由于个人习惯或各部门规范不同,模型所使用的输入名称有可能会相差甚远。更不用说有时候网络结构是从网上找来的。以上的种种情况,都会导致 Tensor 的输入输出名称难以做到统一,形成规范,给部署带来一定的麻烦。
因此,Tensorflow 支持在使用 saved_model 模块导出模型时,给输入和输出起个“别名”,达到不改变模型源码的情况下,形成一套规范的目的。
请看以下示例,使用 saved_model
导出模型的一般性代码为:
import tensorflow as tf builder = tf.saved_model.builder.SavedModelBuilder(export_dir="/path/to/save/model") builder.add_meta_graph_and_variables( sess, [tf.saved_model.tag_constants.SERVING], strip_default_attrs=True) builder.save()
此时,如果原来模型的 input_tensor 为 “X”,output_tensor 为 “Y”,那么,我们只能通过文档或者其他方式告诉以后的使用者,输入输出的名称和形状分别是什么。
使用 saved_model
导出带签名(signature)的模型:
import tensorflow as tf # 假设模型的输入是X,最终输出是Y # X = tf.placeholder(dtype=tf.float32, shape=[None, 10], name='X') # Y = tf.nn.softmax(..., name='Y') # 注意:这里的导出路径,官方建议加上版本号(必须是数字),后续 tensorflow serving 需要。 builder = tf.saved_model.builder.SavedModelBuilder(export_dir="/path/to/save/model/version") # 使用 build_tensor_info 为原来的输入输出 tensor 生成 tensor_info 信息。 tensor_info_x = tf.saved_model.utils.build_tensor_info(X) tensor_info_y = tf.saved_model.utils.build_tensor_info(Y) # 定义一个 signature # 其中 inputs 和 outputs 都是字典,分别定义了模型的输入和输出。key 是别名, value 是上面定义的 tensor_info。 # method_name 为方法名,可以自定义,也可以使用官方预定义的常量。 my_signature = ( tf.saved_model.signature_def_utils.build_signature_def( inputs={'input': tensor_info_x}, outputs={'ouput': tensor_info_y}, method_name=tf.saved_model.signature_constants.PREDICT_METHOD_NAME)) # 摘自官方示例,暂时未知此处作用 legacy_init_op = tf.group(tf.tables_initializer(), name='legacy_init_op') # 此处是重点,多增加了一个 signature_def_map 参数。 key 是 signature_def 名称, value 是 signature 定义。 builder.add_meta_graph_and_variables( sess, [tf.saved_model.tag_constants.SERVING], signature_def_map={ tf.saved_model.signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY: my_signature, }, legacy_init_op=legacy_init_op) builder.save()
此时,导出的模型已经包含相应的签名信息,还做到了输入输出重命名。最重要的是,即使不需要文档,也可以知道输入输出的名称和形状。 验证方法如下:
# 使用 saved_model_cli 命令可以查看模型的签名信息 saved_model_cli show --dir /path/to/save/model --all # 上面命令的输出示例如下: MetaGraphDef with tag-set: 'serve' contains the following SignatureDefs: signature_def['serving_default']: The given SavedModel SignatureDef contains the following input(s): inputs['input'] tensor_info: dtype: DT_FLOAT shape: (-1, 10) name: X:0 The given SavedModel SignatureDef contains the following output(s): outputs['output'] tensor_info: dtype: DT_FLOAT shape: (-1, 1) name: Y:0 Method name is: tensorflow/serving/predict
其中,
builder.add_meta_graph_and_variables
时的第二个参数: tf.saved_model.tag_constants.SERVINGbuilder.add_meta_graph_and_variables
参数 signature_def_map 中的 key使用上一节中介绍的导出方式以后,我们手上就有带签名的模型文件了。接下来就是运行 Tensorflow serving。
安装 Tensorflow serving 的方式主要有三种 参见官网:
这里我们直接用 Docker 镜像,简单方便。其他方式请参考官网。
docker pull tensorflow/serving
docker run -itd -p 8500:8500 8501:8501 \ -v /local/path/to/model:/models/name/1 \ -e MODEL_NAME=name \ tensorflow/serving
说明:
有时候我们需要让一个 serving 同时运行多个模型,那么可以通过配置文件的方式实现。
首先新建一个配置文件,如 models.conf,内容如下: (配置文件更多选项参见:网址)
# cat models.conf model_config_list: { config: { name: "model1", base_path: "/models/model1", model_platform: "tensorflow" }, config: { name: "model2", base_path: "/models/model2", model_platform: "tensorflow" } }
以指定的 config 运行 serving:
docker run -itd -p 8500:8500 8501:8501 \ -v /local/path/to/model1:/models/model1 \ -v /local/path/to/model2:/models/model2 \ -v /local/path/to/models.conf:/models/models.conf \ tensorflow/serving --model_config_file=/models/models.conf
说明:
tensorflow/serving:nightly
镜像。服务端已准备完毕,下面是客户端的开发。
客户端可以通过两种方式与服务端通信:
gRPC
(下面会简单介绍)REST API
(REST API 不进行细讲,参考官网)以下是一个 Python 版本的示例(C++版本的参见官网):
首先,安装相应的 api:
pip3 install tensorflow-serving-api
然后:
import grpc from tensorflow_serving.apis import predict_pb2 from tensorflow_serving.apis import prediction_service_pb2_grpc def _create_rpc_callback(result_future): # 请求回调,成功时从 future.result() 可以拿到结果,异常时从 future.exception() 获取异常结果。 exception = result_future.exception() if exception: print(exception) else: response = np.array(result_future.result().outputs['predict'].int64_val) # print(response) def do_inference(host_and_port, input_data): # 定义服务器连接 channel = grpc.insecure_channel(host_and_port) # 定义 request stub = prediction_service_pb2_grpc.PredictionServiceStub(channel) request = predict_pb2.PredictRequest() # 选择相应的 model 和 signature request.model_spec.name = 'model_name' # model name request.model_spec.signature_name = 'serving_default' # signature def name # 填充 input 数据 request.inputs['input'].CopyFrom( tf.contrib.util.make_tensor_proto(input_data.astype(np.float32))) # 开始请求 result_future = stub.Predict.future(request, timeout=5.0) # 5 seconds result_future.add_done_callback(_create_rpc_callback)