用于TensorFlow Serving部署生产环境的saved_model 模块

saved_model模块主要用于TensorFlow Serving。TF Serving是一个将训练好的模型部署至生产环境的系统,主要的优点在于可以保持Server端与API不变的情况下,部署新的算法或进行试验,同时还有很高的性能。

保持Server端与API不变有什么好处呢?有很多好处,我只从我体会的一个方面举例子说明一下,比如我们需要部署一个文本分类模型,那么输入和输出是可以确定的,输入文本,输出各类别的概率或类别标签。为了得到较好的效果,我们可能想尝试很多不同的模型,CNN,RNN,RCNN等,这些模型训练好保存下来以后,在inference阶段需要重新载入这些模型,我们希望的是inference的代码有一份就好,也就是使用新模型的时候不需要针对新模型来修改inference的代码。这应该如何实现呢?

在TensorFlow 模型保存/载入的两种方法中总结过。 
1. 仅用Saver来保存/载入变量。这个方法显然不行,仅保存变量就必须在inference的时候重新定义Graph(定义模型),这样不同的模型代码肯定要修改。即使同一种模型,参数变化了,也需要在代码中有所体现,至少需要一个配置文件来同步,这样就很繁琐了。 
2. 使用tf.train.import_meta_graph导入graph信息并创建Saver, 再使用Saver restore变量。相比第一种,不需要重新定义模型,但是为了从graph中找到输入输出的tensor,还是得用graph.get_tensor_by_name()来获取,也就是还需要知道在定义模型阶段所赋予这些tensor的名字。如果创建各模型的代码都是同一个人完成的,还相对好控制,强制这些输入输出的命名都一致即可。如果是不同的开发者,要在创建模型阶段就强制tensor的命名一致就比较困难了。这样就不得不再维护一个配置文件,将需要获取的tensor名称写入,然后从配置文件中读取该参数。

经过上面的分析发现,要实现inference的代码统一,使用原来的方法也是可以的,只不过TensorFlow官方提供了更好的方法,并且这个方法不仅仅是解决这个问题,所以还是得学习使用saved_model这个模块。

saved_model 保存/载入模型

先列出会用到的API

class tf.saved_model.builder.SavedModelBuilder

# 初始化方法
__init__(export_dir)

# 导入graph与变量信息 
add_meta_graph_and_variables(
    sess,
    tags,
    signature_def_map=None,
    assets_collection=None,
    legacy_init_op=None,
    clear_devices=False,
    main_op=None
)

# 载入保存好的模型
tf.saved_model.loader.load(
    sess,
    tags,
    export_dir,
    **saver_kwargs
)

(1) 最简单的场景,只是保存/载入模型

保存

要保存一个已经训练好的模型,使用下面三行代码就可以了。

builder = tf.saved_model.builder.SavedModelBuilder(saved_model_dir)
builder.add_meta_graph_and_variables(sess, ['tag_string'])
builder.save()

首先构造SavedModelBuilder对象,初始化方法只需要传入用于保存模型的目录名,目录不用预先创建。

add_meta_graph_and_variables方法导入graph的信息以及变量,这个方法假设变量都已经初始化好了,对于每个SavedModelBuilder这个方法一定要执行一次用于导入第一个meta graph。

第一个参数传入当前的session,包含了graph的结构与所有变量。

第二个参数是给当前需要保存的meta graph一个标签,标签名可以自定义,在之后载入模型的时候,需要根据这个标签名去查找对应的MetaGraphDef,找不到就会报如RuntimeError: MetaGraphDef associated with tags 'foo' could not be found in SavedModel这样的错。标签也可以选用系统定义好的参数,如tf.saved_model.tag_constants.SERVINGtf.saved_model.tag_constants.TRAINING

save方法就是将模型序列化到指定目录底下。

保存好以后到saved_model_dir目录下,会有一个saved_model.pb文件以及variables文件夹。顾名思义,variables保存所有变量,saved_model.pb用于保存模型结构等信息。

载入

使用tf.saved_model.loader.load方法就可以载入模型。如

meta_graph_def = tf.saved_model.loader.load(sess, ['tag_string'], saved_model_dir)

第一个参数就是当前的session,第二个参数是在保存的时候定义的meta graph的标签,标签一致才能找到对应的meta graph。第三个参数就是模型保存的目录。

load完以后,也是从sess对应的graph中获取需要的tensor来inference。如

x = sess.graph.get_tensor_by_name('input_x:0')
y = sess.graph.get_tensor_by_name('predict_y:0')

# 实际的待inference的样本
_x = ... 
sess.run(y, feed_dict={x: _x})

这样和之前的第二种方法一样,也是要知道tensor的name。那么如何可以在不知道tensor name的情况下使用呢? 那就需要给add_meta_graph_and_variables方法传入第三个参数,signature_def_map

(2) 使用SignatureDef

关于SignatureDef我的理解是,它定义了一些协议,对我们所需的信息进行封装,我们根据这套协议来获取信息,从而实现创建与使用模型的解耦。SignatureDef的结构以及相关详细的文档在:https://github.com/tensorflow/serving/blob/master/tensorflow_serving/g3doc/signature_defs.md

相关API

# 构建signature
tf.saved_model.signature_def_utils.build_signature_def(
    inputs=None,
    outputs=None,
    method_name=None
)

# 构建tensor info 
tf.saved_model.utils.build_tensor_info(tensor)

SignatureDef,将输入输出tensor的信息都进行了封装,并且给他们一个自定义的别名,所以在构建模型的阶段,可以随便给tensor命名,只要在保存训练好的模型的时候,在SignatureDef中给出统一的别名即可。

TensorFlow的关于这部分的例子中用到了不少signature_constants,这些constants的用处主要是提供了一个方便统一的命名。在我们自己理解SignatureDef的作用的时候,可以先不用管这些,遇到需要命名的时候,想怎么写怎么写。

保存

假设定义模型输入的别名为“input_x”,输出的别名为“output” ,使用SignatureDef的代码如下

builder = tf.saved_model.builder.SavedModelBuilder(saved_model_dir)
# x 为输入tensor, keep_prob为dropout的prob tensor 
inputs = {'input_x': tf.saved_model.utils.build_tensor_info(x), 
            'keep_prob': tf.saved_model.utils.build_tensor_info(keep_prob)}

# y 为最终需要的输出结果tensor 
outputs = {'output' : tf.saved_model.utils.build_tensor_info(y)}

signature = tf.saved_model.signature_def_utils.build_signature_def(inputs, outputs, 'test_sig_name')

builder.add_meta_graph_and_variables(sess, ['test_saved_model'], {'test_signature':signature})
builder.save()

上述inputs增加一个keep_prob是为了说明inputs可以有多个, build_tensor_info方法将tensor相关的信息序列化为TensorInfo protocol buffer。

inputs,outputs都是dict,key是我们约定的输入输出别名,value就是对具体tensor包装得到的TensorInfo。

然后使用build_signature_def方法构建SignatureDef,第三个参数method_name暂时先随便给一个。

创建好的SignatureDef是用在add_meta_graph_and_variables的第三个参数signature_def_map中,但不是直接传入SignatureDef对象。事实上signature_def_map接收的是一个dict,key是我们自己命名的signature名称,value是SignatureDef对象。

载入与使用的代码如下


## 略去构建sess的代码

signature_key = 'test_signature'
input_key = 'input_x'
output_key = 'output'

meta_graph_def = tf.saved_model.loader.load(sess, ['test_saved_model'], saved_model_dir)
# 从meta_graph_def中取出SignatureDef对象
signature = meta_graph_def.signature_def

# 从signature中找出具体输入输出的tensor name 
x_tensor_name = signature[signature_key].inputs[input_key].name
y_tensor_name = signature[signature_key].outputs[output_key].name

# 获取tensor 并inference
x = sess.graph.get_tensor_by_name(x_tensor_name)
y = sess.graph.get_tensor_by_name(y_tensor_name)

# _x 实际输入待inference的data
sess.run(y, feed_dict={x:_x})

从上面两段代码可以知道,我们只需要约定好输入输出的别名,在保存模型的时候使用这些别名创建signature,输入输出tensor的具体名称已经完全隐藏,这就实现创建模型与使用模型的解耦。

 

官方核心文件代码解读,详细链接见:https://www.cnblogs.com/YouXiangLiThon/p/7435825.html

最近在学习tensorflow serving,但是就这样平淡看代码可能觉得不能真正思考,就想着写个文章看看,自己写给自己的,就像自己对着镜子演讲一样,写个文章也像自己给自己讲课,这样思考的比较深,学到的也比较多,有错欢迎揪出,

    minist_saved_model.py 是tensorflow的第一个例子,里面有很多serving的知识,还不了解,现在看。下面是它的入口函数,然后直接跳转到main。

if __name__ == '__main__':
 tf.app.run()

在main函数里:

首先,是对一些参数取值等的合理性校验:

def main(_):
 if len(sys.argv) < 2 or sys.argv[-1].startswith('-'):
 print('Usage: mnist_export.py [--training_iteration=x] '
          '[--model_version=y] export_dir')
    sys.exit(-1)
  if FLAGS.training_iteration <= 0:
 print 'Please specify a positive value for training iteration.'
 sys.exit(-1)
  if FLAGS.model_version <= 0:
 print 'Please specify a positive value for version number.'
 sys.exit(-1)

然后,就开始train model,既然是代码解读加上自己能力还比较弱,简单的我得解读呀,牛人绕道。。。

# Train model
print 'Training model...'
#输入minist数据,这个常见的,里面的源码就是查看有没有数据,没有就在网上
下载下来,然后封装成一个个batch
mnist = mnist_input_data.read_data_sets(FLAGS.work_dir, one_hot=True)

#这是创建一个session,Session是Graph和执行者之间的媒介,Session.run()实际
上将graph、fetches、feed_dict序列化到字节数组中进行计算
sess = tf.InteractiveSession()

#定义一个占位符,为以后数据等输入留好接口
serialized_tf_example = tf.placeholder(tf.string, name='tf_example')

#feature_configs 顾名思义,是特征配置,从形式上看这是一个字典,字典中
初始化key为‘x’,value 是 tf.FixedLenFeature(shape=[784], dtype=tf.float32)的返
回值,而该函数的作用是解析定长的输入特征feature相关配置
feature_configs = {'x': tf.FixedLenFeature(shape=[784], dtype=tf.float32),}

#parse_example 常用于稀疏输入数据
tf_example = tf.parse_example(serialized_tf_example, feature_configs)

#
x = tf.identity(tf_example['x'], name='x')  # use tf.identity() to assign name

#因为输出是10类,所以y_设置成None×10
y_ = tf.placeholder('float', shape=[None, 10])

#定义权重变量
w = tf.Variable(tf.zeros([784, 10]))

#定义偏置变量
b = tf.Variable(tf.zeros([10]))

#对定义的变量进行参数初始化
sess.run(tf.global_variables_initializer())

#对输入的x和权重w,偏置b进行处理
y = tf.nn.softmax(tf.matmul(x, w) + b, name='y')

#计算交叉熵
cross_entropy = -tf.reduce_sum(y_ * tf.log(y))

#配置优化函数
train_step = tf.train.GradientDescentOptimizer(0.01).minimize(cross_entropy)

#这个函数的作用是返回 input 中每行最大的 k 个数,并且返回它们所在位置的索引
values, indices = tf.nn.top_k(y, 10)

#这函数返回一个将索引的Tensor映射到字符串的查找表
table = tf.contrib.lookup.index_to_string_table_from_tensor(
    tf.constant([str(i) for i in xrange(10)]))

#在tabel中查找索引
prediction_classes = table.lookup(tf.to_int64(indices))

#然后开始训练迭代啦
for _ in range(FLAGS.training_iteration):
          #获取一个batch数据
 batch = mnist.train.next_batch(50)
         #计算train_step运算,train_step是优化函数的,这个执行带来的作用就是
         根据学习率,最小化cross_entropy,执行一次,就调整参数权重w一次
         train_step.run(feed_dict={x: batch[0], y_: batch[1]})

#将得到的y和y_进行对比
correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1))

#对比结果计算准确率
accuracy = tf.reduce_mean(tf.cast(correct_prediction, 'float'))

#运行sess,并使用更新后的最终权重,去做预测,并返回预测结果
print 'training accuracy %g' % sess.run(
    accuracy, feed_dict={x: mnist.test.images,
 y_: mnist.test.labels})
print 'Done training!'
上面就是训练的过程,就和普通情况下train模型是一样的道理,现在,我们看后面的model export

 

# Export model
# WARNING(break-tutorial-inline-code): The following code snippet is
# in-lined in tutorials, please update tutorial documents accordingly
# whenever code changes.
#export_path_base基本路径代表你要将model export到哪一个路径下面,
#它的值的获取是传入参数的最后一个,训练命令为:
bazel-bin/tensorflow_serving/example/mnist_saved_model /tmp/mnist_model
那输出的路径就是/tmp/mnist_model
export_path_base = sys.argv[-1]

#export_path 真正输出的路径是在基本路径的基础上加上版本号,默认是version=1
export_path = os.path.join(
    tf.compat.as_bytes(export_path_base),
 tf.compat.as_bytes(str(FLAGS.model_version)))
print 'Exporting trained model to', export_path

#官网解释:Builds the SavedModel protocol buffer and saves variables and assets.
builder = tf.saved_model.builder.SavedModelBuilder(export_path)

# Build the signature_def_map. 
# serialized_tf_example是上面提到的占位的输入,
#其当时定义为tf.placeholder(tf.string, name='tf_example')

#tf.saved_model.utils.build_tensor_info 的作用是构建一个TensorInfo proto
#输入参数是张量的名称,类型,大小,这里是string,想应该是名称吧,毕竟
#代码还没全部看完,先暂时这么猜测。输出是,基于提供参数的a tensor protocol 
# buffer 
classification_inputs = tf.saved_model.utils.build_tensor_info(
    serialized_tf_example)

#函数功能介绍同上,这里不同的是输入参数是prediction_classes,
#其定义,prediction_classes = table.lookup(tf.to_int64(indices)),是一个查找表
#为查找表构建a tensor protocol buffer
classification_outputs_classes = tf.saved_model.utils.build_tensor_info(
    prediction_classes)

#函数功能介绍同上,这里不同的是输入参数是values,
#其定义,values, indices = tf.nn.top_k(y, 10),是返回的预测值
#为预测值构建a tensor protocol buffer
classification_outputs_scores = tf.saved_model.utils.build_tensor_info(values)

#然后,继续看,下面那么多行都是一个语句,一个个结构慢慢解析
#下面可以直观地看到有三个参数,分别是inputs ,ouputs和method_name
#inputs ,是一个字典,其key是tensorflow serving 固定定义的接口,
#为: tf.saved_model.signature_constants.CLASSIFY_INPUTS,value的话
#就是之前build的a tensor protocol buffer 之 classification_inputs
#同样的,output 和method_name 也是一个意思,好吧,这部分就
#了解完啦。
classification_signature = (
    tf.saved_model.signature_def_utils.build_signature_def(
        inputs={
            tf.saved_model.signature_constants.CLASSIFY_INPUTS:
 classification_inputs
        },
 outputs={
            tf.saved_model.signature_constants.CLASSIFY_OUTPUT_CLASSES:
 classification_outputs_classes,
 tf.saved_model.signature_constants.CLASSIFY_OUTPUT_SCORES:
 classification_outputs_scores
        },
 method_name=tf.saved_model.signature_constants.CLASSIFY_METHOD_NAME))

#这两句话都和上面一样,都是构建a tensor protocol buffer
tensor_info_x = tf.saved_model.utils.build_tensor_info(x)
tensor_info_y = tf.saved_model.utils.build_tensor_info(y)

这个和上面很多行的classification_signature,一样的
prediction_signature = (
    tf.saved_model.signature_def_utils.build_signature_def(
        inputs={'images': tensor_info_x},
 outputs={'scores': tensor_info_y},
 method_name=tf.saved_model.signature_constants.PREDICT_METHOD_NAME))

#这个不一样了,tf.group的官网解释挺简洁的
#Create an op that groups multiple operations.
#When this op finishes, all ops in input have finished. This op has no output.
#Returns:An Operation that executes all its inputs.
#我们看下另一个tf.tables_initializer():
#Returns:An Op that initializes all tables. Note that if there are not tables the returned Op is a NoOp
legacy_init_op = tf.group(tf.tables_initializer(), name='legacy_init_op')

#下面是重点啦,怎么看出来的?因为上面都是定义什么的,下面是最后的操作啦
#就一个函数:builder.add_meta_graph_and_variables,
builder.add_meta_graph_and_variables(
 sess, [tf.saved_model.tag_constants.SERVING],
 signature_def_map={
 'predict_images':
 prediction_signature,
 tf.saved_model.signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY:
 classification_signature,
 },
 legacy_init_op=legacy_init_op)

builder.save()


print 'Done exporting!'

这里要从 tf.saved_model.builder.SavedModelBuilder 创建build开始,下面是看官网的,
可以直接参考:https://www.tensorflow.org/api_docs/python/tf/saved_model/builder/SavedModelBuilder

创建builder的是class SaveModelBuilder的功能是用来创建SaverModel,protocol buffer 并保存变量和资源,SaverModelBuilder类提供了创建SaverModel protocol buffer 的函数方法。

你可能感兴趣的:(深度学习)