应用TensorFlow Serving+Flask部署TF模型

通过这篇文章你可以获得如下技能

  • 理解TF模型的部署流程

  • 快速使用docker工具

  • 快速使用TensorFlow Serving工具

  • 快速使用flask工具

  • 简单实例:在服务器上部署训练好的tf模型

一.TF模型的部署结构

1.部署TF模型需要的工具:docker、TensorFlow Serving、Flask。

2.部署流程:

  • 在docker容器中pull与TF模型版本相对应的TensorFlow Serving镜像。
  • 将训练好的模型加载到TensorFlow Serving镜像中
  • 用Flask部署算法程序


    部署结构图.png
二.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)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,376评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,126评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,966评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,432评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,519评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,792评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,933评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,701评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,143评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,488评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,626评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,292评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,896评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,742评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,324评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,494评论 2 348