tensorflow serving
是tf官方推出的专为生产环节设计的机器学习模型服务系统(TensorFlow Serving is a flexible, high-performance serving system for machine learning models,designed for production environments),使用它可以很方便的将训练好的模型部署成web服务,并通过HTTP或GRPC的方式访问。
根据官方资料模仿出demo很容易,但往深走或者做些修改解决场景需求还是着实花了一些功夫的,我承认这跟我不熟悉tensorflow有些关系,但更多的时间花在了理解概念和做实验上了。顺便再吐槽下,官方的introduction和tutorial有点少呀。
tensroflow serving中很重要的概念是SavedModel
,跟checkpoint中以.ckpt
或.h5
的格式存放训练好的模型一样,也是模型的一种存放格式(下图),saved_model是模型的存放路径,1表示模型的版本(version)每个版本都是一个单独的子目录,如若有其他版本,那么版本号一次递增。saved_model.pb是训练好的模型的图,variables是训练好的权重。 tensorflow serving识别这种格式并且通过load它就能把已训练好的模型部署起来。
saved_model
└── 1
├── saved_model.pb
└── variables
├── variables.data-00000-of-00001
└── variables.index
接下来的主要工作就是怎么生成这个SavedModel,官方资料中把这个过程叫做export model。官方资料的代码开起来太费劲了,我在此只想把这个过程讲清楚所以就弄了个简单点的代码(部分代码如下)。输出路径和输入输出维度、类型这个就不再啰嗦了。tensorflow采用SavedModelBuilder
模块导出模型(当然还有别的export方法),它将与训练的模型的snapshort保存到硬盘上以便之后在的inference时直接load。先构建builder
对象;再用builder.add_meta_graph_and_variables(...)
方法将与训练模型的图和参数值添加到builder中;最后调用builder.save()
将pretrain model保存成saved model。
# 输出路径
export_path_base = "./savedModel"
export_path = os.path.join(tf.compat.as_bytes(export_path_base),
tf.compat.as_bytes(str(1)))
print('Exporting trained model to', export_path)
# 输入输出变量的维度和类型
x = tf.placeholder('float', shape=[None, 3])
y_ = tf.placeholder('float', shape=[None, 1])
# 构建SavedModel
builder = tf.saved_model.builder.SavedModelBuilder(export_path)
tensor_info_x = tf.saved_model.utils.build_tensor_info(x)
tensor_info_y = tf.saved_model.utils.build_tensor_info(y_)
prediction_signature = (
tf.saved_model.signature_def_utils.build_signature_def(
inputs={'input': tensor_info_x},
outputs={'output': tensor_info_y},
method_name=tf.saved_model.signature_constants.PREDICT_METHOD_NAME))
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],
signature_def_map={
'prediction':
prediction_signature,
},
legacy_init_op=legacy_init_op)
builder.save()
那剩下需要详细介绍的就是add_meta_graph_and_variables(...)
方法了,貌似现在它变的很核心了(就使用而言它的确很核心),里面重要的是sess、tags、signature_def_map三个参数,源码在这里。
def add_meta_graph_and_variables(self,sess,tags,signature_def_map=None,
assets_list=None,clear_devices=False,init_op=None,train_op=None,
strip_default_attrs=False,saver=None):
-
sess
里面包含有我们打算保存的训练的模型,这个在理解上不会有困惑的。 -
tags
是我们给即将生成的SavedModel中保存的图(meta graph)打的标签,tf规定每SavedModel中的每个meta graph都必须指定标签,用于表示meta graph的作用和使用场景,比如meta graph是用来做train还是serve,它需要用到CPU还是GPU。而且在load SavedModel时只有tags匹配,loader API才能加载成功,否则报错。tf提供了四种常见的标签。因为我们创建的SavedModel是用做serve的,所以上面代码中的tf.saved_model.tag_constants.SERVING
作为标签。 -
signature_def_map
的作用是指定了导出模型的类型和指定当启动infenrece过程时输入和输出的tensor,不可谓不重要。对这块的数据组织格式刚开始有点误解,后来有点想明白了,介绍性的文字说明不能吝啬的。接下来从头捋一下,看看signature_def_map是怎么来的。
x
、y_
是我们定义输入、输出tensor,先要把tensor
转换成TensorInfo proto
结构的数据,为什么要转换,是因为tf人家的产品这么设计的(这个解释当然不具有说服性,但作为使用产品的用户,不管设计师的考量如何,还是理解和接受吧,主要精力还是应该放在基于它的应用开发上,但我觉得若要深耕研究它还是很有必要的,但前提是先学会怎么使用它),当然tf也同时提供了转换工具可供用户调用,所以不必纠结于为什么要转换,调两行代码轻松搞定继续后面的开发吧。tensor_info_x = tf.saved_model.utils.build_tensor_info(x) tensor_info_y = tf.saved_model.utils.build_tensor_info(y_)
signature_def_utils.build_signature_def(...)
方法是SavedModel提供构建signature的API之一。这个方法有下面三个参数:inputs={'images': tensor_info_x}
指定了输入的tensor info。outputs={'scores': tensor_info_y}
指定了输出的tensor info。method_name
指定了inference过程调用的方法,如果是预测请求,则method_name的值应该指定为tensorflow/serving/predict
,当前不要费时间问为什么,产品就是这么设计的,更详细的信息在这里。
下面在代码中我们prediction_signature中构建了一个signature,注意看下,prediction_signature是tuple类型的,当时很困惑为什么不是单独的signature呢?后来有点明白了,对于同一个inference过程,tf serving支持多种格式的数据输入,比如:inference是用来做猫狗分类的,图片也许是以tensor格式输入的,也有可能是以base64格式输入的,在里面再转换成tensor,只需要调用不同的method_name就可以了。
prediction_signature = ( tf.saved_model.signature_def_utils.build_signature_def( inputs={'input': tensor_info_x}, outputs={'output': tensor_info_y}, method_name=tf.saved_model.signature_constants.PREDICT_METHOD_NAME))
接下来只需要给prediction_signature配上key构建个map怼到对应的参数上去就行了。
builder.add_meta_graph_and_variables(..., signature_def_map={'prediction':prediction_signature,}, legacy_init_op=legacy_init_op)
最后用命令行工具saved_model_cli可以查看下我们保存好的SavedModel:
saved_model_cli show --dir ./savedModel/1/ --tag_set serve --signature_def prediction
结果如下:
The given SavedModel SignatureDef contains the following input(s):
inputs['input'] tensor_info:
dtype: DT_FLOAT
shape: (-1, 3)
name: Placeholder:0
The given SavedModel SignatureDef contains the following output(s):
outputs['output'] tensor_info:
dtype: DT_FLOAT
shape: (-1, 1)
name: add:0
Method name is: tensorflow/serving/predict
整个SavedModel的制作过程到现在算是介绍完了,不过还需要再加点东西这块才算比较全面些:
- 当SavedModel的serve启动后,该怎么根据signature组织相应的HTTP POST RequestBody数据呢。
- 怎么把已有的SavedModel 载入后稍加修改再保存成一个新SavedModel。
- 怎么把已有的checkpoint(.pb、.ckpt、.h5)转化成SavedModel。
- SavedModel支持自定义结构数据输入,比如:图片以base64格式输入。
一些感触:
学习一个东西难点从不在它的语言上,即便当前见到的语言在之前从未遇到过。大多数人包括我在内感觉的是基础的语法都看不懂,那怎么看明白这块代码在做什么事情,更别提根据当前需求对它进行修改了。而现实也的确是这样的,我会先大体看先这段代码,会有那么一丢丢的印象,方法的参数或者它内部又调用了其他方法,接着是查case文档或者是API试图寻找文字性的说明,最后在理解和猜测中调试程序,通过调试再去理解和猜测,反复循环直到“感觉”明白了。从代码着手最后又通过代码检验自己是否学会,但背后的是我在反复的过程中理解它的概念
和使用方法
。因为框架
、包
、甚至是语言
这些都是人为设计而非天生存在的,更通俗的说是人按照某个设计思路做出来的解决某个问题的产品
,而我要做的是学会怎么去用这个产品解决当前场景的需求,仅此而已,所以理解概念和设计思路(暂且先这么叫吧)就显得十分重要了,其地位远超语言本身。最后掌握的也是使用它的“套路(更确切的说是原理)”或者是其中的某几个关键环节。