《Tensorflow For Machine Intelligence》第七章是关于Tensorflow模型部署的内容,虽然该书已有中文版,但一些技术部分感觉翻译的不到位,没阐述透彻,因此在阅读英文原版的同时顺便整理了一下自己的理解。
正文
为了使用Docker镜像,我们可用https://github.com/tensorflow/serving/blob/master/tensorflow_serving/tools/docker/Dockerfile.devel文件,这是创建本地镜像的配置文件,所以为了使用该文件我们用下面的命令:
docker build --pull -t $USER/tensorflow-serving-devel \ https://raw.githubusercontent.com/tensorflow/serving/master/tensorflow_serving/tools/docker/Dockerfile.devel
注意上面这条命令需要一段时间来下载所有的依赖。
现在,为了用镜像运行容器并在上面开展工作,我们用下面的命令:
docker run -v $HOME:/mnt/home -p 9999:9999 -it $USER/tensorflow-serving-devel
该命令将你的home目录挂载到容器的/mnt/home路径,并使你在容器内的终端上工作。这样做是非常有用的,因为你可以(在容器外)直接使用趁手的IDE/编辑器来编写代码,只是用容器来运行编译工具。为了让你的宿主机能够使用
后面将要创建的server,我们开放了容器的9999端口让宿主机访问。
用命令exit能够退出容器终端,停止容器运行。用上面介绍的命令可以多次再启动容器。
Bazel workspace
Tensorflow Serving程序是用C++代码编写的,因此应该用Google的Bazel编译工具来编译。我们将在最近创建的容器内运行Bazel。
Bazel在代码层面管理第三方依赖,只要依赖也是用Bazel编译,就下载并编译它们。为了定义我们的工程将支持哪些第三方依赖,必须在工程库的根目录定义WORKSPACE文件。
我们需要的依赖是Tensorflow Serving库,对于我们的例子,还需要包含Inception model代码的Tensorflow Models库。
不幸的是在本书撰写之际,Tensorflow Serving并不支持作为Git库通过Bazel直接引用,所以我们必须把它作为Git子模块包含在工程中:
# 在本机上 mkdir ~/serving_example cd ~/serving_example git init git submodule add https://github.com/tensorflow/serving.git tf_serving git submodule update --init --recursive
现在我们用WORKSPACE文件中的local_repository规则将第三方依赖定义为本地存储文件。此外,也必须用工程中导入的tf_workspace规则来初始化Tensorflow依赖:
# Bazel WORKSPACE file workspace(name = "serving") local_repository( name = "tf_serving", path = __workspace_dir__ + "/tf_serving", ) local_repository( name = "org_tensorflow", path = __workspace_dir__ + "/tf_serving/tensorflow", ) load('//tf_serving/tensorflow/tensorflow:workspace.bzl', 'tf_workspace') tf_workspace("tf_serving/tensorflow/", "@org_tensorflow") bind( name = "libssl", actual = "@boringssl_git//:ssl", ) bind( name = "zlib", actual = "@zlib_archive//:zlib", ) # only needed for inception model export local_repository( name = "inception_model", path = __workspace_dir__ + "/tf_serving/tf_models/inception", )
最后一步我们必须在容器内为Tensorflow运行./configure
# 在docker容器上 cd /mnt/home/serving_example/tf_serving/tensorflow ./configure
Exporting trained models 导出训练模型
一旦训练好模型,我们很乐于评估它的性能。我们需要导出模型的计算图和变量值,便于生产时可用。
与训练版本相比,模型的计算图应该做一些修改,因为它必须从占位符接收输入,在模型上运行单步推断来计算输出结果。以Inception model为例,一般来说,对于任何图像识别模型,我们希望输入是代表图像(JPEG编码)的单个
字符串,这样我们就能够很容易地从客户app上发送它。这与从TFRecords文件读取训练输入不同。
定义输入的一般形式应该像这样:
def convert_external_inputs(external_x): # transform the external input to the input format required on inference def inference(x): # from the original model… external_x = tf.placeholder(tf.string) x = convert_external_inputs(external_x) y = inference(x)
在上面的代码中我们为输入定义占位符,调用函数将占位符表示的外部输入转化为原始模型推断方法要求的格式。例如我们将JPEG字符串转化为推断要求的图像格式。最后我们对转化后的输入调用原始模型推断方法。
例如,对于Inception模型我们应该有这样的方法:
import tensorflow as tf from tensorflow_serving.session_bundle import exporter from inception import inception_model def convert_external_inputs(external_x): # transform the external input to the input format required on inference # convert the image string to a pixels tensor with values in the range 0,1 image = tf.image.convert_image_dtype(tf.image.decode_jpeg(external_x, channels=3), tf.float32) # resize the image to the model expected width and height images = tf.image.resize_bilinear(tf.expand_dims(image, 0), [299, 299]) # Convert the pixels to the range -1,1 required by the model images = tf.mul(tf.sub(images, 0.5), 2) return images def inference(images): logits, _ = inception_model.inference(images, 1001) return logits
推断方法要求参数赋值,我们将从训练checkpoint恢复这些参数值。你可以回想在基础章节里,我们周期性保存模型的训练checkpoint。这些文件包含了当时学习到的参数值,所以即使发生灾难我们也不会丢失训练进度。
当训练结束时,最后保存的训练checkpoint将包含最新的模型参数,这些是我们希望投入生产的。
为了恢复checkpoint,代码应该是:
saver = tf.train.Saver() with tf.Session() as sess: # Restore variables from training checkpoints. ckpt = tf.train.get_checkpoint_state(sys.argv[1]) if ckpt and ckpt.model_checkpoint_path: saver.restore(sess, sys.argv[1] + "/" + ckpt.model_checkpoint_path) else: print("Checkpoint file not found") raise SystemExit
对Inception模型,可以从http://download.tensorflow.org/models/image/imagenet/inception-v3-2016-03-01.tar.gz下载预先训练好的checkpoint。
# 在docker容器上
cd /tmp curl -O http://download.tensorflow.org/models/image/imagenet/inception-v3-2016-03-01.tar.gz tar -xzf inception-v3-2016-03-01.tar.gz
最后,我们用tensorflow_serving.session_bundle.exporter.Exporter类导出模型。
我们创建该类的实例来传递saver实例,然后用exporter.classification_signature方法创建模型的signature。signature说明了哪个是input_tensor,哪些是输出张量。输出由classes_tensor和scores_tensor组成,其中classes_tensor包含输出类名的列表,scores_tensor包含模型分配给每个类的得分/概率。
通常模型中有大量的类别,可以用tf.nn.top_k配置仅返回选择的类别,这些类别是模型分配得分最高的k个类别。
最后一步应用signature是调用exporter.Exporter.init方法,用export方法导出模型。export方法接收输出路径,模型版本号和会话。
scores, class_ids = tf.nn.top_k(y, NUM_CLASSES_TO_RETURN) # for simplification we will just return the class ids, we should return the names instead classes = tf.contrib.lookup.index_to_string(tf.to_int64(class_ids), mapping=tf.constant([str(i) for i in range(1001)])) model_exporter = exporter.Exporter(saver) signature = exporter.classification_signature( input_tensor=external_x, classes_tensor=classes, scores_tensor=scores) model_exporter.init(default_graph_signature=signature, init_op=tf.initialize_all_tables()) model_exporter.export(sys.argv[1] + "/export", tf.constant(time.time()), sess)
由于Exporter类代码中对自动生成代码的依赖,我们必须在Docker容器内用bazel运行导出器。
为此,我们在前面启动的bazel workspace里把代码保存为export.py。需要有像下面的编译规则的BUILD文件:
# BUILD file py_binary( name = "export", srcs = [ "export.py", ], deps = [ "//tensorflow_serving/session_bundle:exporter", "@org_tensorflow//tensorflow:tensorflow_py", # only needed for inception model export "@inception_model//inception", ], )
然后我们在容器内用命令运行导出器:
# 在docker容器上 cd /mnt/home/serving_example bazel run :export /tmp/inception-v3
基于解压在/tmp/inception-v3的checkpoint,上述命令将在/tmp/inception-v3/{current_timestamp}/创建导出。
首次运行要花费一些时间,因为它必须编译Tensorflow。