keras 模型部署——TensorFlow Serving/Docker

今天笔者想记录一下深度学习模型的部署,不知道大家研究过没有,tensorflow模型有三种保存方式:

  • 训练时我们会一般会将模型保存成:checkpoint文件
  • 为了方便python,C++或者其他语言部署你的模型,你可以将模型保存成一个既包含网络结构又包含权重参数的:PB文件
  • 为了方便使用TensorFlow Serving 部署你的模型,你可以将模型保存成:Saved_model文件

笔者是keras(tensorflow )的深度用户,经常把模型保存成HDF5格式。那么问题来了,如何把keras的模型转化成PB文件 或者 Saved_model文件供生成部署使用。今天笔者就是来介绍一下如何将Keras的模型保存成PB文件 或者 Saved_model文件。

定义BERT二分类模型

下方函数定义的是一个标准的BERT做文本二分类的图结构。

from keras.models import Model
from keras.layers import *
from keras import backend as K
import tensorflow as tf
from keras_bert import get_model,compile_model

def load_bert_model_weight(bert_model_path):
    
    b_model = get_model(token_num=21128,)
    compile_model(b_model)

    bert_model = Model(
            inputs = b_model.input[:2],
            outputs = b_model.get_layer('Encoder-12-FeedForward-Norm').output
    )
    x1_in = Input(shape=(None,))
    x2_in = Input(shape=(None,))

    x = bert_model([x1_in, x2_in])
    x = Lambda(lambda x: x[:, 0])(x)## 取[CLS]向量
    p = Dense(2, activation='softmax')(x)

    model = Model([x1_in, x2_in], p)
#     model.compile(
#         loss='binary_crossentropy',
#         optimizer=Adam(1e-5), # 用足够小的学习率
#         metrics=['accuracy']
#     )
    model.load_weights(bert_model_path)
    return model
model_path = "/opt/developer/wp/wzcq/model/bert1014v1_weights.hf"
model = load_bert_model_weight(model_path)

具体结构如下图所示其中

  • model_2 就是含12层的transformer的bert模型,
  • 之后接一个keras中经常使用的Lambda层用于抽取 bert最后一层出来的[CLS]位置对应特征向量,
  • 将[CLS]的特征向量输入给一个全连接层做而分类。

由于笔者之前已经训练好了一个人模型,这里我直接使用load_bert_model_weight函数将模型以及模型参数加载进内存。


BERT二分类

将keras模型转化为PB文件

接下来可以使用这个函数将上市的model连模型图结构代参数一起保存下来,然后通过tensoflow 为python,java,c++语言等提供的模型调用接口使用起来了。

def freeze_session(session, keep_var_names=None, output_names=None, clear_devices=True):
    """
    Freezes the state of a session into a pruned computation graph.

    Creates a new computation graph where variable nodes are replaced by
    constants taking their current value in the session. The new graph will be
    pruned so subgraphs that are not necessary to compute the requested
    outputs are removed.
    @param session The TensorFlow session to be frozen.
    @param keep_var_names A list of variable names that should not be frozen,
                          or None to freeze all the variables in the graph.
    @param output_names Names of the relevant graph outputs.
    @param clear_devices Remove the device directives from the graph for better portability.
    @return The frozen graph definition.
    """
    graph = session.graph
    with graph.as_default():
        freeze_var_names = list(set(v.op.name for v in tf.global_variables()).difference(keep_var_names or []))
        output_names = output_names or []
        output_names += [v.op.name for v in tf.global_variables()]
        input_graph_def = graph.as_graph_def()
        if clear_devices:
            for node in input_graph_def.node:
                node.device = ""
        frozen_graph = tf.graph_util.convert_variables_to_constants(
            session, input_graph_def, output_names, freeze_var_names)
        return frozen_graph


def save_keras_model_to_pb(keras_model , dic_pb=None,  file_pb=None):
    """
    save keras model to tf *.pb file 
    
    """
    frozen_graph = freeze_session(K.get_session(),output_names=[out.op.name for out in keras_model.outputs])
    tf.train.write_graph(frozen_graph, dic_pb, file_pb, as_text=False)
    return 



save_keras_model_to_pb(model,"py","bert_model.pb")

将keras模型转化为Saved_model文件

而接下来的部分是如何将model制作成Saved_model文件,这样你就可以使用TensorFlow Serving 部署你的模型。这里笔者介绍一下使用TensorFlow Serving 部署你的模型的一些优势。

  • 支持模型版本控制和回滚
  • 支持并发,实现高吞吐量
  • 开箱即用,并且可定制化
  • 支持多模型服务
  • 支持批处理
  • 支持热更新
  • 支持分布式模型
  • 易于使用的inference api:为gRPC expose port 8500,为REST API expose port 8501
import tensorflow as tf
import os
from tensorflow.keras.losses import categorical_crossentropy
from tensorflow.keras.optimizers import Adadelta

def export_model(model,
                 export_model_dir,
                 model_version
                 ):
    """
    :param export_model_dir: type string, save dir for exported model    url
    :param model_version: type int best
    :return:no return
    """
    with tf.get_default_graph().as_default():
        # prediction_signature
        tensor_info_input_0 = tf.saved_model.utils.build_tensor_info(model.input[0])
        tensor_info_input_1 = tf.saved_model.utils.build_tensor_info(model.input[1])
        tensor_info_output = tf.saved_model.utils.build_tensor_info(model.output)
        print(model.input)
        print(model.output.shape, '**', tensor_info_output)
        prediction_signature = (
            tf.saved_model.signature_def_utils.build_signature_def(
                inputs ={'input_0': tensor_info_input_0,'input_1': tensor_info_input_1}, # Tensorflow.TensorInfo
                outputs={'result': tensor_info_output},
                #method_name=tf.saved_model.signature_constants.PREDICT_METHOD_NAME)
                 method_name= "tensorflow/serving/predict")
               
        )
        print('step1 => prediction_signature created successfully')
        # set-up a builder
        os.mkdir(export_model_dir)
        export_path_base = export_model_dir
        export_path = os.path.join(
            tf.compat.as_bytes(export_path_base),
            tf.compat.as_bytes(str(model_version)))
        builder = tf.saved_model.builder.SavedModelBuilder(export_path)
        builder.add_meta_graph_and_variables(
            # tags:SERVING,TRAINING,EVAL,GPU,TPU
            sess=K.get_session(),
            tags=[tf.saved_model.tag_constants.SERVING],
            signature_def_map={
                'predict':prediction_signature,
                tf.saved_model.signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY: prediction_signature,

            },
            )
        print('step2 => Export path(%s) ready to export trained model' % export_path, '\n starting to export model...')
        #builder.save(as_text=True)
        builder.save()
        print('Done exporting!')

export_model(model,"bert",1)

上述过程需要注意的一个人地方是模型API输入和输出的定义:
这个部分要按照模型的输入输出定义好。由于我的bert模型
定义如下:

  • 输入token_id和segment_id两部分输入,
  • 输出是dense层的输出
    所以我的API定义过程如下
###输入tensor  
tensor_info_input_0 = tf.saved_model.utils.build_tensor_info(model.input[0])
tensor_info_input_1 = tf.saved_model.utils.build_tensor_info(model.input[1])
###输出tensor
tensor_info_output = tf.saved_model.utils.build_tensor_info(model.output)
### 定义api
prediction_signature = (
     tf.saved_model.signature_def_utils.build_signature_def(
            inputs ={'input_0': tensor_info_input_0,'input_1': tensor_info_input_1}, # Tensorflow.TensorInfo
            outputs={'result': tensor_info_output},
            #method_name=tf.saved_model.signature_constants.PREDICT_METHOD_NAME)
            method_name= "tensorflow/serving/predict")
               
        )

下图是脚本执行输出:


res

采用上述代码将model 保持成Saved_model 格式后,模型文件结构如下图所示:包含版本,模型pd文件,参数的文件夹。


Saved_model

TensorFlow Serving 之使用Docker 部署Saved_model

使用docker部署模型的好处在于,避免了与繁琐的环境配置打交道。使用docker,不需要手动安装Python,更不需要安装numpy、tensorflow各种包。google 已经帮你制作好了tensorflow/serving 镜像,GPU版和CPU版都有,你只要要使用docker pull 命令将镜像拉取到本地就可以了

 run -p 8501:8501 --mount type=bind,source=/opt/developer/wp/learn/bert,target=/models/bert -e MODEL_NAME=bert --name bert -t tensorflow/serving

使用上面的docker命令启动TF Server :
(1)-p 8501:8501是端口映射,是将容器的8501端口映射到宿主机的8501端口,后面预测的时候使用该端口;
(2)-e MODEL_NAME=bert 设置模型名称;
(3)--mount type=bind,source=/opt/developer/wp/learn/bert, target=/models/bert 是将宿主机的路径/opt/developer/wp/learn/bert 挂载到容器的/models/bert 下。
/opt/developer/wp/learn/bert是通过上述py脚本生成的Saved_model的路径。容器内部会根据绑定的路径读取模型文件;

使用下方命令行查看服务状态

 curl http://localhost:8501/v1/models/bert 
image.png

请求服务

加下来使用request库尝试请求一下我们的bert服务。
这里需要注意的是数据预处理的方式要和你做训练时的方式一样。

sent = "来玩英雄联盟"
tokenid_train =  tokenizer.encode(sent,max_len=200)[0] 
sen_id_train =   tokenizer.encode(sent,max_len=200)[1]
import requests
SERVER_URL = "http://192.168.77.40:8501/v1/models/bert:predict"
predict_request='{"signature_name": "predict", "instances":[{"input_0":%s,"input_1":%s}] }' %(tokenid_train,sen_id_train)
response = requests.post(SERVER_URL, data=predict_request)
response.content

返回结果:{ "predictions": [[8.70507502e-05, 0.999913]]}
当然你也可以使用gRPC 的API在8500端口访问你的服务。

结语

今天笔者只是简单介绍了一下,如何将模型转换为生产环境能用与部署的格式,以及使用docker部署模型的方式,其实模型训练出来了,达到了很好的效果,接下来让更多的人能够方便的使用到它们也是我们算法工程师所期望的事情,所以,模型的部署还是很有意义的一件事情。

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

推荐阅读更多精彩内容