模型训练完后,往往需要将模型应用到生产环境中。最常见的就是通过TensorFlow Serving来将模型部署到服务器端,以便客户端进行请求访问。
1. TensorFlow Serving 安装
TensorFlow Serving一般安装在服务器端,最为方便,推荐在生产环境中 使用 Docker 部署 TensorFlow Serving 。当然也可以通过apt-get 安装 。这里我主要使用的前者。
首先安装docker,然后拉取最新的Tensorflow Serving镜像。
docker pull tensorflow/serving
2. 模型部署
2.1 Keras Sequential 模式模型的部署(单输入,单输出)
2.1.1 模型构建
由于Keras Sequential 模式的输入输出形式比较固定单一,所以这里简单的构造一个Sequential 模型。
import tensorflow as tf
import os
model = tf.keras.Sequential([tf.keras.layers.Dense(1)])
2.1.2 模型导出
模型构造好后,开始进行模型的导出。 由于这里只是示例,不进行数据输入训练等操作,通过TF2.0 中eager的特性来初始化模型。
version = '1' #版本号
model_name = 'sequential_model'
saved_path = os.path.join('models',model_name)
data = tf.ones((2, 10))
model(datas
model.save(os.path.join(saved_path,version)) # models/sequential_model/1
注意:tensorflow Serving支持热更新,每次默认选取版本version
最大的版本号进行部署。因此,我们在保存模型的路径上需要加上指定的version
。
保存后的文件目录结构如下:
└── sequential_model
├── 1
│ ├── assets
│ ├── saved_model.pb
│ └── variables
│ ├── variables.data-00000-of-00001
│ └── variables.index
└── 2
├── assets
├── saved_model.pb
└── variables
├── variables.data-00000-of-00001
└── variables.index
接着在终端中,通过以下命令,查看保存的模型结构
saved_model_cli show --dir models/sequential_model/1 --all
终端输出 重点关注下面信息:
signature_def['serving_default']:
The given SavedModel SignatureDef contains the following input(s):
inputs['dense_input'] tensor_info:
dtype: DT_FLOAT
shape: (-1, 10)
name: serving_default_dense_input:0
The given SavedModel SignatureDef contains the following output(s):
outputs['dense'] tensor_info:
dtype: DT_FLOAT
shape: (-1, 1)
name: StatefulPartitionedCall:0
Method name is: tensorflow/serving/predict
其中 inputs['dense_input'] 中的 dense_input 是请求 TensorFlow Serving服务时所需的 输入名 outputs['dense'] 中的dense是TensorFlow Serving服务 返回的输出名 ( 当outputs只有一个时默认 输出名为 outputs)
2.1.3 docker部署
接下来我们运行Docker中的tensorflow Serving 进行部署:
docker run -t --name sequential_model -p 8501:8501
--mount type=bind,source=/root/models,target=/models
-e MODEL_NAME=sequential_model tensorflow/serving &
解释: --name 定义容器的名字 -p 8051:8051 指的是 本地端口:容器内部端口 的映射。(注:tensorflow Serving 默认开启的是8501端口,如需修改则需进入容器中手动指定 --rest_api_port) --mount type=bind, source=/root/models ,target=/models 指的是将本地/root/models
目录 映射到 docker容器中/models
目录 -e MODEL_NAME=sequential_model 这里指的环境名称MODEL_NAME,tensorflow Serving会自动索引容器中/models/MODEL_NAME
目录下的模型文件
上面的docker 命令 相当于在容器中 执行下面命令。
tensorflow_model_server
--rest_api_port=8501
--model_name=sequential_model
--model_base_path= /models/sequential_model &
因此,也可以仅启动容器,设置端口映射,目录映射后 ,进入容器 通过自己手动启动tensorflow_model_server
来进行更多的自定义。
2.1.4 请求服务
终端中可以通过curl进行请求
curl -d '{"inputs": {"dense_input":[[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]]}}'
-X POST http://localhost:8501/v1/models/sequential_model:predict
这里的dense_input
就是前面saved_model_cli
在显示的输入名。
返回 由于只是单输出,所以这里隐藏了输出名,默认为outputs
, 与saved_model_cli
中输出名有点区别。
{
"outputs": [
[
0.736189902
]
]
}
2.2 Keras Model函数式模型的部署(多输入,多输出)
由于Sequential 模式模型的输入输出过于单一,在模型构建方面有天生的弱势。 这里介绍下Keras Model函数式模型多输入多输出 的部署。
2.2.1 模型构建
重点注意下,定义的
name
属性。
import tensorflow as tf
import os
input_1 = tf.keras.layers.Input(shape=(10,), dtype=tf.float32, name='a')
input_2 = tf.keras.layers.Input(shape=(10,), dtype=tf.float32, name='b')
output_1 = tf.keras.layers.Dense(1, name='dense_1')(input_1 + input_2)
output_2 = tf.keras.layers.Dense(1, name='dense_2')(input_1 - input_2)
model = tf.keras.Model(inputs=[input_1, input_2], outputs=[output_1, output_2])
2.2.2 模型导出
这里依然通过eager的特性进行模型的初始构建。
version = '1' #版本号
model_name = 'keras_functional_model'
saved_path = os.path.join('models',model_name)
data1= tf.ones((2,10),dtype=tf.float32)
data2= tf.ones((2,10),dtype=tf.float32)
model((data1,data2)) #model({'a':data1,'b':data2}) 两种方法都可以
model.save(os.path.join(saved_path,version)) # models/keras_functional_model/1
接下来我们查看下保存模型的内部结构
saved_model_cli show --dir models/keras_functional_model/1 --all
signature_def['serving_default']:
The given SavedModel SignatureDef contains the following input(s):
inputs['a'] tensor_info:
dtype: DT_FLOAT
shape: (-1, 10)
name: serving_default_a:0
inputs['b'] tensor_info:
dtype: DT_FLOAT
shape: (-1, 10)
name: serving_default_b:0
The given SavedModel SignatureDef contains the following output(s):
outputs['dense_1'] tensor_info:
dtype: DT_FLOAT
shape: (-1, 1)
name: StatefulPartitionedCall:0
outputs['dense_2'] tensor_info:
dtype: DT_FLOAT
shape: (-1, 1)
name: StatefulPartitionedCall:1
Method name is: tensorflow/serving/predict
前面keras Sequential 模式中,输入名和输出名,是系统根据 变量名自动生成的。 在Keras Model函数式模型 中 我们可以通过定义name
属性来设置输入名和输出名。
2.2.3 docker部署
由于前面sequential_model 占用了本地8501端口,这里使用8502端口作为本地端口映射,tensorflow serving默认开启8501 所以内部端口8501 不用修改。
docker run -t --name keras_functional_model -p 8502:8501
--mount type=bind,source=/root/models,target=/models
-e MODEL_NAME=keras_functional_model tensorflow/serving &
2.2.4 请求服务
终端中可以通过curl进行请求
curl -d '{"inputs": {"a":[[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]],"b":[[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]]}}' /
-X POST http://localhost:8502/v1/models/keras_functional_model:predict
返回
由于这里是多输出,返回的同时会加上输出名。与Keras Sequential模式有区别。
{
"outputs": {
"dense_1": [
[
0.251481533
]
],
"dense_2": [
[
0.0
]
]
}
}%
2.3 自定义 Keras 模型的部署 (多输入+mode,多输出)
如果Keras Sequential 模式 和 Keras Model函数式 模式 无法满足复杂模型需求怎么办?别担心,最大杀器自定义 Keras 模式可以帮你解决一切。
设想下,我们如果需要通过 mode,来控制模型的运行流程,这样的模型我们该怎么导出部署呢? 当mode='train' 时,输出a。 当mode='predict'时,输出b。
2.3.1 方法一
2.3.1.1 模型构建
import tensorflow as tf
import os
class MyModel(tf.keras.Model):
def __init__(self, **kwargs):
super(MyModel, self).__init__(**kwargs)
self.dense_1 = tf.keras.layers.Dense(10)
self.dense_2 = tf.keras.layers.Dense(1)
@tf.function
def call(self, inputs):
a, b, mode = inputs
hidden = self.dense_1(a + b)
logits = self.dense_2(hidden)
if mode == 'predict':
return hidden, mode
return logits, mode
2.3.1.2 模型导出
这里依然通过eager的特性进行模型的初始构建。
version = '1' #版本号
model_name = 'custom_model'
saved_path = os.path.join('models',model_name)
model = MyModel()
a = tf.ones((2, 10))
b = tf.ones((2, 10))
mode = 'train'
mode = tf.cast(mode, tf.string) #这里的strin 必须转换成tf.string类型。
print(model((a, b, mode)))
model.save(os.path.join(saved_path,version))
这里由于未显示的指定,输入的形状、类型、名字等。模型根据输入的数据进行自动推断的。因此输入的数据 必须是 tf的dtype
类型。所以,在输入字符串时需要转换成tf.string
。 虽然,在我们平时训练模型时没有什么问题,但在导出模型时,会出现
TypeError: Invalid input_signature ; input_signature must be a possibly nested sequence of TensorSpec objects.
通过saved_model_cli
对模型进行查看。
saved_model_cli show --dir models/keras_functional_model/1 --all
由于,并没有指定输入的名字,这里都由系统根据输入顺序,输出顺序自动命名,输入就是input_n,输出就是output_n.
signature_def['serving_default']:
The given SavedModel SignatureDef contains the following input(s):
inputs['input_1'] tensor_info:
dtype: DT_FLOAT
shape: (-1, 10)
name: serving_default_input_1:0
inputs['input_2'] tensor_info:
dtype: DT_FLOAT
shape: (-1, 10)
name: serving_default_input_2:0
inputs['input_3'] tensor_info:
dtype: DT_STRING
shape: ()
name: serving_default_input_3:0
The given SavedModel SignatureDef contains the following output(s):
outputs['output_1'] tensor_info:
dtype: DT_FLOAT
shape: (-1, -1)
name: StatefulPartitionedCall:0
outputs['output_2'] tensor_info:
dtype: DT_STRING
shape: ()
name: StatefulPartitionedCall:1
Method name is: tensorflow/serving/predict
下面介绍下比较规范的自定义 Keras 模型 导出
2.3.2 方法二
2.3.2.1 模型构建
import tensorflow as tf
import os
class MyModel(tf.keras.Model):
def __init__(self, **kwargs):
super(MyModel, self).__init__(**kwargs)
self.dense_1 = tf.keras.layers.Dense(10)
self.dense_2 = tf.keras.layers.Dense(1)
@tf.function(input_signature=[(tf.TensorSpec([None, 10], name='a', dtype=tf.float32),
tf.TensorSpec([None, 10], name='b', dtype=tf.float32),
tf.TensorSpec([], name='mode', dtype=tf.string))])
def call(self, inputs):
a, b, mode = inputs
hidden = self.dense_1(a + b)
logits = self.dense_2(hidden)
if mode == 'predict':
return hidden, mode
return logits, mode
与 方法一 不同的是,我们通过显示的定义了inputs 需要输入的类型,形状,类别,名字。这样一来,模型就知道我们要输入的是什么了。
注意:在构建类似上面面多分支输出模型时,需要保持各分支输出的 变量类型,变量数量 一致,否则模型导出会抛出异常。
如:上面分支模型,当mode
=='train' 和mode
=='predict'时,return返回的都是tf.float32
和tf.string
类型,且都是两个变量。若出现变量类型,变量数量 不一致,则会提示 TypeErrorpython TypeError: 'retval_' must have the same nested structure in the main and else branches:.....
注意:看到这里,细心的可能会发现,我在编写call()函数时,不管是单输入,还是多输入,我都是通过解包的方式进行输入,并没有改动 inputs 这个变量。我们在训练模型过程中,经常会直接显示的指明变量,比如 上面的call可以进行改写 def call(self,inputs): ----> def call(self, a,b,mode): 虽然,在train的过程中不会出错,但在部署导出模型时,会发生未知错误。 因此,推荐 解包的方式进行输入变量。
2.3.2.2 模型导出
version = '2' #版本号
model_name = 'custom_model'
saved_path = os.path.join('models',model_name)
model = MyModel()
a = tf.ones((2, 10))
b = tf.ones((2, 10))
mode = 'train'
print(model((a, b, mode)))
model.save(os.path.join(saved_path,version))
这里,我们也无需将mode
手动转换成tf.string
了,一切都由模型自动完成。 再通过saved_model_cli
来查看下模型信息。
saved_model_cli show --dir models/keras_functional_model/2 --all
由于我们在tf.function中指定了,输入名,因此这里输入名都发生了改变。
signature_def['serving_default']:
The given SavedModel SignatureDef contains the following input(s):
inputs['a'] tensor_info:
dtype: DT_FLOAT
shape: (-1, 10)
name: serving_default_a:0
inputs['b'] tensor_info:
dtype: DT_FLOAT
shape: (-1, 10)
name: serving_default_b:0
inputs['mode'] tensor_info:
dtype: DT_STRING
shape: ()
name: serving_default_mode:0
The given SavedModel SignatureDef contains the following output(s):
outputs['output_1'] tensor_info:
dtype: DT_FLOAT
shape: (-1, -1)
name: StatefulPartitionedCall:0
outputs['output_2'] tensor_info:
dtype: DT_STRING
shape: ()
name: StatefulPartitionedCall:1
Method name is: tensorflow/serving/predict
2.3.3 docker部署
由于前面8501 8502 端口被占用,这里我们启用8503端口进行映射。
docker run -t --name custom_model -p 8503:8501
--mount type=bind,source=/root/models,target=/models
-e MODEL_NAME=custom_model tensorflow/serving &
通过返回的信息,我们可以看见,模型成功的加载了 version='2' 的模型文件。tensorflow serving 的热部署默认加载最大的版本号模型。
2020-08-23 14:13:13.477881: I external/org_tensorflow/tensorflow/cc/saved_model/loader.cc:199] Restoring SavedModel bundle.
2020-08-23 14:13:13.491605: I external/org_tensorflow/tensorflow/cc/saved_model/loader.cc:183] Running initialization op on SavedModel bundle at path: /models/custom_model/2
2020-08-23 14:13:13.495698: I external/org_tensorflow/tensorflow/cc/saved_model/loader.cc:303] SavedModel load for tags { serve }; Status: success: OK. Took 41678 microseconds.
2020-08-23 14:13:13.496311: I tensorflow_serving/servables/tensorflow/saved_model_warmup_util.cc:59] No warmup data file found at /models/custom_model/2/assets.extra/tf_serving_warmup_requests
2020-08-23 14:13:13.496841: I tensorflow_serving/core/loader_harness.cc:87] Successfully loaded servable version {name: custom_model version: 2}
2020-08-23 14:13:13.498519: I tensorflow_serving/model_servers/server.cc:367] Running gRPC ModelServer at 0.0.0.0:8500 ...
[warn] getaddrinfo: address family for nodename not supported
2020-08-23 14:13:13.499738: I tensorflow_serving/model_servers/server.cc:387] Exporting HTTP/REST API at:localhost:8501 ...
[evhttp_server.cc : 238] NET_LOG: Entering the event loop ...
2.3.4 请求服务
mode = 'train'
curl -d '{"inputs": {"a":[[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]],
"b":[[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]],"mode":"train"}}'
-X POST http://localhost:8503/v1/models/custom_model:predict
返回
{
"outputs": {
"output_1": [
[
2.90094423
]
],
"output_2": "train"
}
}
mode='predict'
curl -d '{"inputs": {"a":[[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]],
"b":[[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]],"mode":"predict"}}'
-X POST http://localhost:8503/v1/models/custom_model:predict
返回
{
"outputs": {
"output_1": [
[
0.699072063,
0.756825566,
1.35330391,
-2.21226835,
1.43654501,
1.50765,
-1.43798947,
-0.434436381,
-0.289675713,
2.53842211
]
],
"output_2": "predict"
}
}
3. 客户端中程序调用
程序端调用,遵循基本的RESTful API即可。这里以上文custom_model v2 为例子。
准备数据
import numpy as np
import json
a = np.ones((1, 10))
b = np.ones((1, 10))
mode = "train"
data = {"inputs": {"a": a.tolist(), "b": b.tolist(), "mode": mode}}
data = json.dumps(data)
pycurl
import pycurl
url = "http://localhost:8503/v1/models/custom_model:predict"
c = pycurl.Curl()
c.setopt(c.URL, url)
c.setopt(c.POSTFIELDS, data)
rs = c.performb_rs()
rs = eval(rs)
outputs = rs['outputs']
print(outputs)
c.close()
request
import requests
url = "http://localhost:8503/v1/models/custom_model:predict"
rs = requests.post(url,data)
outputs = rs.json()['outputs']
print(outputs)
输出:
{'output_1': [[2.90094423]], 'output_2': 'train'}
从本人博客搬运。