通过这篇文章你可以获得如下技能
理解TF模型的部署流程
快速使用docker工具
快速使用TensorFlow Serving工具
快速使用flask工具
简单实例:在服务器上部署训练好的tf模型
一.TF模型的部署结构
1.部署TF模型需要的工具:docker、TensorFlow Serving、Flask。
2.部署流程:
- 在docker容器中pull与TF模型版本相对应的TensorFlow Serving镜像。
- 将训练好的模型加载到TensorFlow Serving镜像中
-
用Flask部署算法程序
二.docker工具的使用
1.docker常用命令
docker images 查看镜像
docker ps -a 查看正在运行的容器和-a全部容器
docker kill 容器 关闭正在运行的容器
docker image rm -f 镜像 删除镜像
docker containent rm 容器 删除容器
docker run -it 交互打开容器,-d后台打开容器 --name 给容器命名
docker cp 本地路径 容器名:容器内路径 将本地路径赋值到容器中
删除所有容器 docker rm
docker ps -a -q
对容器进行更改,并将更改的容器保存为镜像。
docker commit --change "ENV MODEL_NAME <my model>" serving_base <my container>
三.tensorflow serving工具的使用
1.我们用docker容器装载TensorFlow Serving 镜像,有下面两种方式
- 从 docker hub 上下载镜像
docker pull tensorflow/serving
- 通过build方式建立镜像
docker build --pull -t $USER/tensorflow-serving-devel -f
tensorflow_serving/tools/docker/Dockerfile.devel .
2.将TF模型部署到TensorFlow Serving中
模型的准备工作
- 模型保存格式转换(.ckpt—>.pd)
我们平时使用tf.Saver()保存的模型是继续训练的checkpoint格式的,该格式的参数都是变量,但是在TensorFlow Serving中一个servable的模型目录中是一个pb格式文件和一个名为variables的目录,因此需要在模型保存时就保存好可部署的模型格式,或者将已经训练好的checkpoint转换为servable format。
# 载入保存好的meta graph,恢复图中变量,通过SavedModelBuilder保存可部署的模型
saver = tf.train.import_meta_graph("{}.meta".format(checkpoint_file))
saver.restore(sess, checkpoint_file)
builder = tf.saved_model.builder.SavedModelBuilder(export_path)
- 建立签名映射
build_tensor_info建立一个基于提供的参数构造的TensorInfo protocol buffer
get_operation_by_name通过name获取checkpoint中保存的变量,能够进行这一步的前提是在模型保存的时候给对应的变量赋予name
input_data = tf.saved_model.utils.build_tensor_info(graph.get_operation_by_name("input_data").outputs[0])
- 定义模型的输入和输出,建立调用接口与tensor签名之间的映射
labeling_signature = (
tf.saved_model.signature_def_utils.build_signature_def(
inputs={
"input1": input1,
"input2": input2
},
outputs={
"output1": output1,
"output2": output2
},
method_name=tf.saved_model.signature_constants.PREDICT_METHOD_NAME
)
)
- 建立模型名称和模型签名之间的映射
builder.add_meta_graph_and_variables(
sess, [tf.saved_model.tag_constants.SERVING],
# 保存模型的方法名,与客户端的request.model_spec.signature_name对应
signature_def_map={
tf.saved_model.signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY: labeling_signature
},
legacy_init_op = legacy_init_op
)
3.启动docker,并将准备好的模型部署到TensorFlow Serving中
docker run -d --rm -p 8501:8501 -v "/模型路径/model:/models/model" -e MODEL_NAME=model tensorflow/serving:latest &
4.可用requests工具访问TensorFlow Serving服务器中的模型,测试模型是否正确
API_URL='http://localhost:8501/v1/models/model:predict'
headers, url = {'content-type': 'application/json'}, API_URL
data_={'inputs':{
"input1":input1,#input1为list
"input2":input2#input2为list
}
}
data_ = json.dumps(data_)
response_ = requests.post(url_, data=data_, headers=headers_)
四.Flask工具的使用
使用方法参照:https://dev.to/aligoren/building-basic-restful-api-with-flask-restful-57oh
We will firstly import flask and its flask_restful library.
from flask_restful import Resource, Api
app = Flask(__name__)
api = Api(app)</pre>
After that, we will create a simple class. It will like this:
class ComposePoem(Resource):
def __init__(self):
self.poem="根据首字生成诗"
self.content = {
'query': {
'content': "首字"
},
'results': {
"pose":self.poem
}}
In this example, we used static data. As I said you can use your own
database. Now we need to add this class as a resource to API library
wrapper.
api.add_resource(Quotes, '/')
Finally, our codes will like this:
# -*- coding: utf-8 -*-
from flask import Flask
from flask_restful import Resource, Api
app = Flask(__name__)
api = Api(app)
class ComposePoem(Resource):
def __init__(self):
self.poem="根据首字生成诗"
self.content = {
'query': {
'content': "首字"
},
'results': {
"pose":self.poem
}}
api.add_resource(ComposePoem, '/')
if __name__ == '__main__':
app.run(debug=True)
Above code, we created a method named get for HTTP Get requests. We tried with the postman
The POST Method
For example, you want to work with post requests for this class. You must create a method named post. Let’s create our post method to HTTP Post requests. Firstly, we need to import reqparse. So, our import statements will change like below:
from flask import Flask
from flask_restful import Resource, Api, reqparse
app = Flask(__name__)
api = Api(app)
parser = reqparse.RequestParser()
After that, our post method will like this:
def post(self):
parser.add_argument('content', type=str)
args = parser.parse_args()
word = str(args['content'])
self.poem = gen_poem(word)
return self.poem, 201
Let’s try a simple HTTP Post request with the Postman:
用post请求服务,请求时josn格式一致
五.LSTM生成古诗模型部署实例
1.古诗词生成模型参照https://github.com/jinfagang/tensorflow_poems
模型快速开始
git clone https://github.com/jinfagang/tensorflow_poems
# train on poems
python train.py
# compose poems
python compose_poem.py</pre>
2.为了实现模型在tensorflow_serving中部署,对模型进行修改
(修改后的模型路径:/home/liuquan/project/compose_poems_tf_serving/poems/model.py
/home/liuquan/project/compose_poems_tf_serving/train.py)
- 将placeholder固定shape,改为可变shape
由于模型使用和训练时的banch_size、input_data的shape不同,我们需要将placeholder固定shape,改为可变shape
#将batch_size定义为形状维1,int32的变量
batch_size = tf.placeholder(dtype=tf.int32, shape=[1])
#input_data和output_targets的形状都由[FLAGS.batch_size, None]改为[None, None]。
# output_targets = tf.placeholder(tf.int32, [FLAGS.batch_size, None],name="output_data")
input_data = tf.placeholder(tf.int32, [None, None],name="input_data")
output_targets = tf.placeholder(tf.int32, [None, None],name="output_data")</pre>
- run模型时需要给模型喂入batch_size数据
loss, _, _ = sess.run([
end_points['total_loss'],
end_points['last_state'],
end_points['train_op']],
feed_dict={
input_data: batches_inputs[n],
output_targets:batches_outputs[n],
batch_size:(FLAGS.batch_size,)})</pre>
- 模型隐层初始状态shape根据喂入batch_size确定
将原模型中通过if...else...判断batch_size大小,来确定initial_state的shape,改为如下代码
initial_state = cell.zero_state(batch_size=batch_size, dtype=tf.float32)
- 模型的训练和测试模型修改
修改原模型中通过if...else...判断output_data是否为空,来确定训练模型还是测试模型
无论测试还是训练都声称预测结果
prediction = tf.nn.softmax(logits, name="prediction")
当训练时为训练时,再对模型进行loss和梯度下降的计算。
3.训练修改后的模型,并将训练结果保存为可部署的pb格式文件
(路径:/home/liuquan/project/compose_poems_tf_serving/save_model2pb.py)
- 运行train.py训练模型后,运行save_model2pb.py将训练结果保存为pb格式
载入保存好的meta graph,恢复图中变量,通过SavedModelBuilder保存可部署的模型
saver = tf.train.import_meta_graph("{}.meta".format(checkpoint_file))
saver.restore(sess, checkpoint_file)
builder = tf.saved_model.builder.SavedModelBuilder(export_path)
- 建立模型输入输出节点的签名映射
将需要的模型输入和模型输出的tensor通过节点名称建立签名映射,节点名称可建模时手动命名,也可通过调试获得其自动的命名结果。
#模型输入节点的签名映射(输入数据、输入的隐层状态,由于LSTM是两层结构,有两个隐层输入,每个隐层输入是一个包含c、h的元组)
input_data = tf.saved_model.utils.build_tensor_info( graph.get_operation_by_name("input_data").outputs[0])
in_c1 = tf.saved_model.utils.build_tensor_info( graph.get_operation_by_name("MultiRNNCellZeroState/BasicLSTMCellZeroState/zeros").outputs[0])
in_h1 = tf.saved_model.utils.build_tensor_info( graph.get_operation_by_name("MultiRNNCellZeroState/BasicLSTMCellZeroState/zeros_1").outputs[0])
in_c2 = tf.saved_model.utils.build_tensor_info( graph.get_operation_by_name("MultiRNNCellZeroState/BasicLSTMCellZeroState_1/zeros").outputs[0])
in_h2 = tf.saved_model.utils.build_tensor_info( graph.get_operation_by_name("MultiRNNCellZeroState/BasicLSTMCellZeroState_1/zeros_1").outputs[0])
#模型输出节点的签名映射(预测数据、输出的隐层状态,由于LSTM是两层结构,有两个隐层输出,每个隐层输出是一个包含c、h的元组)
output_data = tf.saved_model.utils.build_tensor_info( graph.get_operation_by_name("prediction").outputs[0])
c1 = tf.saved_model.utils.build_tensor_info( graph.get_operation_by_name("rnn/while/Exit_3").outputs[0])
h1 = tf.saved_model.utils.build_tensor_info( graph.get_operation_by_name("rnn/while/Exit_4").outputs[0])
c2 = tf.saved_model.utils.build_tensor_info( graph.get_operation_by_name("rnn/while/Exit_5").outputs[0])
h2 = tf.saved_model.utils.build_tensor_info( graph.get_operation_by_name("rnn/while/Exit_6").outputs[0])
- 定义模型的输入- 输出,建立调用接口与tensor签名之间的映射
根据我们建立好的输入、输出节点的签名映射建立调用接口与tensor签名之间的映射,格式如下
labeling_signature = (
tf.saved_model.signature_def_utils.build_signature_def(
inputs={
"input_data": input_data,
"in_c1": in_c1,
"in_h1": in_h1,
"in_c2": in_c2,
"in_h2": in_h2
},
outputs={
"predict": output_data,
"c1":c1,
"h1":h1,
"c2":c2,
"h2":h2
},
method_name=tf.saved_model.signature_constants.PREDICT_METHOD_NAME
))
- 建立模型名称和模型签名之间的映射
"""
tf.group
创建一个将多个操作分组的操作,返回一个可以执行所有输入的操作
"""
legacy_init_op = tf.group(tf.tables_initializer(), name='legacy_init_op')
builder.add_meta_graph_and_variables(
sess, [tf.saved_model.tag_constants.SERVING],
# 保存模型的方法名,与客户端的request.model_spec.signature_name对应
signature_def_map={
tf.saved_model.signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY: labeling_signature
},
legacy_init_op = legacy_init_op
)
4.启动docker,并将准备好的模型部署到TensorFlow Serving中
docker run -d --rm -p 8501:8501
-v "/home/liuquan/project/compose_poems_tf_serving/model:/models/model"
-e MODEL_NAME=model
tensorflow/serving:latest &
5.访问TensorFlow Serving中中的模型,并通过flask建立服务端
- 定义call方法,用requests工具访问TensorFlow Serving服务器中的模型
import json
import requests
API_URL='http://localhost:8501/v1/models/model:predict'
headers, url = {'content-type': 'application/json'}, API_URL
def call(sentence_, in_c1,in_h1,in_c2,in_h2,headers_={}, url_=API_URL):
data_={'inputs':{
"input_data": input_data,
"in_c1": in_c1,
"in_h1": in_h1,
"in_c2": in_c2,
"in_h2": in_h2
}
}
data_ = json.dumps(data_)
response_ = requests.post(url_, data=data_, headers=headers_)
return response_.text, response_.status_code
- 定义gen_poem()的方法,
该方法把输入的首字根据词表转换成模型的输入向量,同时定义模型的初始隐层输入,把模型输出的softmax的向量根据词表转换为输出汉字,同时将输出的隐层和输出的汉字最为下一循环的输入给模型。返回每次循环的输出结果序列,作为生成诗的返回结果。
gen_poem(begin_word)方法,所在路径:
/home/liuquan/project/compose_poems_tf_serving/utils/compose_poem_in_server.py
- flask服务器
用建立post方法,从客户端接受请求,将接受数据传递给gen_poem(begin_word)方法,并返回给客户端gen_poem(begin_word)方法的返回结果。
from flask import Flask
from flask_restful import reqparse, Api, Resource
from utils.compose_poem_in_server import gen_poem
app = Flask(__name__)
app.config.update(RESTFUL_JSON=dict(ensure_ascii=False))
api = Api(app)
parser = reqparse.RequestParser()
results_list = {}
class ComposePoem(Resource):
def __init__(self):
self.poem="根据首字生成诗"
self.content = {
'query': {
'content': "首字"
},
'results': {
"pose":self.poem
}}
def post(self):
parser.add_argument('content', type=str)
args = parser.parse_args()
word = str(args['content'])
self.poem = gen_poem(word)
return self.poem, 201
# 设置路由api.add_resource(ComposePoem, '/')
if __name__ == '__main__':
app.run(host='0.0.0.0', port='5003',debug=False)
6.用requests模拟客户端,访问flask服务器
import json
import requests
API_URL='http://0.0.0.0:5003'
def call(sentence_,headers_={}, url_=API_URL):
data_ = { "content":sentence_ }
data_ = json.dumps(data_)
response_ = requests.post(url_, data=data_, headers=headers_)
return response_.text, response_.status_code
if __name__ == '__main__':
headers, url = {'content-type': 'application/json'}, API_URL
input_x=input('## 请输入诗词的首个汉字:')
response, _ = call(input_x,headers, url)
response = json.loads(response, encoding='utf-8')
print("生成的诗为:"+response)
print(_)
正确返回古诗生成结果
六.遇到的问题及解决方案
问题1:batch_size大小固定,当使用模型时batch_size不可变。batch_size 采用可变大小,使静态图同时适用训练和测试不同情况的计算
原代码:
batch_size为一个整形变量,initial_state形状固定,导致部署的模型,不能接受变batch_sized的请求,
batch_size=64
if output_data is not None:
initial_state = cell.zero_state(batch_size, tf.float32)
else:
initial_state = cell.zero_state(1, tf.float32)
改进后代码:
batch_size用一个形状为1整形占位符定义。可以使initial_state形状灵活可变,适用于不同batch_size访问模型服务端
batch_size = tf.placeholder(dtype=tf.int32, shape=[1])
initial_state = cell.zero_state(batch_size, tf.float32)
问题2:模型隐层初始状态的确定问题
#定义模型隐层初始状态的形状和初始状态值
outputs, last_state = tf.nn.dynamic_rnn(cell, inputs, initial_state=initial_state)
#不定义模型隐层初始状态的形状和初始状态值,只定义初始状态的数据类型(可以避免隐层初始状态形状对调用模型的影# 响,但不能为模型输入制定的初始状态)
outputs, last_state = tf.nn.dynamic_rnn(cell, inputs, dtype=tf.float32)