模型训练完后,往往需要将模型应用到生产环境中。最常见的就是通过TensorFlow Serving来将模型部署到服务器端,以便客户端进行访问。
TensorFlow Serving 安装 TensorFlow Serving一般安装在服务器端,最为方便,推荐在生产环境中 使用 Docker 部署 TensorFlow Serving 。当然也可以通过apt-get 安装 。这里我主要,使用的前者。
首先安装docker 。
然后,拉取最新的Tensorflow Serving镜像。
1 docker pull tensorflow/serving
模型部署 Keras Sequential 模式模型的部署(单输入,单输出) 模型构建 由于Keras Sequential 模式的输入输出形式比较固定单一,所以这里简单的构造一个Sequential 模型。
1 model = tf.keras.Sequential([tf.keras.layers.Dense(1 )])
模型导出 模型构造好后,开始进行模型的导出。 由于这里只是示例,不进行数据输入训练等操作,通过TF2.0 中eager的特性来初始化模型。
1 2 3 4 5 6 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))
注意 :tensorflow Serving支持热更新,每次默认选取版本version
最大的进行部署。因此,我们在保存模型的路径上需要加上指定的version
。
保存后的文件目录结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 └── 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
接着在终端 中,通过以下命令,查看保存的模型结构
1 saved_model_cli show --dir models/sequential_model/1 --all
终端输出 重点关注下面信息:
1 2 3 4 5 6 7 8 9 10 11 12 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)
docker部署 接下来我们运行Docker中的tensorflow Serving 进行部署:
1 2 3 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 命令 相当于在容器中 执行下面命令。
1 2 3 4 tensorflow_model_server \ --rest_api_port=8501 \ --model_name=sequential_model \ --model_base_path= /models/sequential_model &
因此,也可以仅启动容器,设置端口映射,目录映射后 ,进入容器 通过自己手动启动tensorflow_model_server
来进行更多的自定义。
请求服务 终端中可以通过curl进行请求
1 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
中输出名有点区别。
1 2 3 4 5 6 7 { "outputs": [ [ 0.736189902 ] ] }
Keras Model函数式模型的部署(多输入,多输出) 由于Sequential 模式模型的输入输出过于单一,在模型构建方面有天生的弱势。 这里介绍下Keras Model函数式模型多输入多输出 的部署。
模型构建
重点注意下,定义的name
属性。
1 2 3 4 5 6 7 import tensorflow as tfinput_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])
模型导出 这里依然通过eager的特性进行模型的初始构建。
1 2 3 4 5 6 7 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.save(os.path.join(saved_path,version))
接下来我们查看下保存模型的内部结构
1 saved_model_cli show --dir models/keras_functional_model/1 --all
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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
属性来设置输入名 和输出名 。
docker部署 由于前面sequential_model 占用了本地8501端口,这里使用8502端口作为本地端口映射,tensorflow serving默认开启8501 所以内部端口8501 不用修改。
1 2 3 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 &
请求服务 终端中可以通过curl进行请求
1 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模式有区别。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 { "outputs": { "dense_1": [ [ 0.251481533 ] ], "dense_2": [ [ 0.0 ] ] } }%
自定义 Keras 模型的部署 (多输入+mode,多输出) 如果Keras Sequential 模式 和 Keras Model函数式 模式 无法满足复杂模型需求怎么办?别担心,最大杀器自定义 Keras 模式可以帮你解决一切。
设想下,我们如果需要通过 mode,来控制模型的运行流程,这样的模型我们该怎么导出部署呢? 当mode=’train’ 时,输出a。 当mode=’predict’时,输出b。
方法一 模型构建 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import tensorflow as tfclass 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
模型导出 这里依然通过eager的特性进行模型的初始构建。
1 2 3 4 5 6 7 8 9 10 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) print (model((a, b, mode)))model.save(os.path.join(saved_path,version))
这里由于未显示的指定,输入的形状、类型、名字等。模型根据输入的数据进行自动推断的。因此输入的数据 必须是 tf的dtype
类型。所以,在输入字符串时需要转换成tf.string
。 虽然,在我们平时训练模型时没有什么问题,但在导出模型时,会出现
1 TypeError: Invalid input_signature ; input_signature must be a possibly nested sequence of TensorSpec objects.
通过saved_model_cli
对模型进行查看。
1 saved_model_cli show --dir models/keras_functional_model/1 --all
由于,并没有指定输入的名字,这里都由系统根据输入顺序,输出顺序自动命名,输入就是input_n,输出就是output_n.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 模型 导出
方法二 模型构建 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import tensorflow as tfclass 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
类型,且都是两个变量。若出现变量类型,变量数量不一致 ,则会提示TypeError
1 >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的过程中不会出错,但在部署导出模型时,会发生未知错误。 因此,推荐解包 的方式进行输入变量。
模型导出 1 2 3 4 5 6 7 8 9 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
来查看下模型信息。
1 saved_model_cli show --dir models/keras_functional_model/2 --all
由于我们在tf.function中指定了,输入名,因此这里输入名都发生了改变。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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
那么,问题来了,输入进行了自定义修改,输出呢? 别急,请看下面的最终版 。
终版(官方推荐) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import tensorflow as tfimport osclass 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 ) self.dropout = tf.keras.layers.Dropout(0.1 ) def call (self, inputs, training=None ): a, b = inputs hidden = self.dense_1(a + b) hidden = self.dropout(hidden, training=training) logits = self.dense_2(hidden) return logits @tf.function(input_signature=[(tf.TensorSpec([None , 10 ], name='a' , dtype=tf.float32 ), tf.TensorSpec([None , 10 ], name='b' , dtype=tf.float32 ) )] ) def sever (self, inputs ): return {"score" : self.call(inputs, training=False )} if __name__ == '__main__' : model = MyModel() data = tf.ones((2 , 10 )) out = model((data, data), training=True ) print (out) model.save("SavedModel" , signatures={"serving_default" : model.sever})
方法三 与 方法二 相比在输入的时候call方法中多了training=None,但默认导出时tf2会自动屏蔽掉training参数。这时我们可以对call方法进行重新包装,这里自定义了sever 函数对call 进行包装并对server指定 input_signature ,同时在输出时通过dic 指定输出变量名。最后 ,在model.save导出模型时, 指定对serving_default 签名对应的 调用函数进行指定即可。
最后通过saved_model_cli show –all –dir SavedModel命令对保存的模型进行查看,可以发现这次不仅自定义了输入而且还自定义了输出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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['score'] tensor_info: dtype: DT_FLOAT shape: (-1, 1) name: StatefulPartitionedCall:0 Method name is: tensorflow/serving/predict
终于不用再看到input_n, output_n 而头疼了。
docker部署 由于前面8501 8502 端口被占用,这里我们启用8503端口进行映射。
1 2 3 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 的热部署 默认加载最大的版本号模型。
1 2 3 4 5 6 7 8 9 10 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 ...
请求服务 train
1 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
返回
1 2 3 4 5 6 7 8 9 10 { "outputs": { "output_1": [ [ 2.90094423 ] ], "output_2": "train" } }
predict
1 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
返回
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 { "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" } }
客户端中程序调用 这里以上文custom_model v2 为例子。
准备数据
1 2 3 4 5 6 7 8 import numpy as npimport jsona = 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 1 2 3 4 5 6 7 8 9 10 import pycurlurl = "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 1 2 3 4 5 import requestsurl = "http://localhost:8503/v1/models/custom_model:predict" rs = requests.post(url,data) outputs = rs.json()['outputs' ] print (outputs)
输出:
1 {'output_1' : [[2.90094423 ]], 'output_2' : 'train' }