TensorFlow补充知识
TensorFlow补充知识
产品环境中模型的部署
至此,对于如何利用TensorFlow构建和训练各种模型——从基本的机器学习模型到复杂的深度学习网络,我们已有了基本了
解。本章将重点介绍如何将训练好的模型投入于产品,以使其能够为其他应用所用。
我们的目标是创建一个简单的web App,使用户能够上传一幅图像,并对其运行Inception模型,实现图像的自动分类。
搭建TensorFlow服务开发环境
Docker镜像
TensorFlow服务是用于构建允许用户在产品中使用我们提供的模型的服务器的工具。在开发过程中,使用该工具的方法有两种:手工安装所有的依赖项和工 具,并从源码开始构建;或利用Docker镜像。这里准备使用后者,因为它更容易、更干净,同时允许在其他不同于Linux的环境中进行开发。
如果不了解Docker镜像,不妨将其想象为一个轻量级的虚拟机镜像,但它在运行时不需要以在其中运行完整的操作系统为代价。如果尚未安装Docker,请在 开发机中安装它,具体的安装步骤可参考https://docs.docker.com/engine/installation/。
为了使用Docker镜像,还可利用笔者提供的文件https://github.com/tensorflow/serving/blob/master/tensorflow_serving/tools/docker/Dockerfile.devel ,它是一个用于在本 地创建镜像的配置文件。要使用该文件,可使用下列命令:
请注意,执行上述命令后,下载所有的依赖项可能需要一段较长的时间。
上述命令执行完毕后,为了使用该镜像运行容器,可输入下列命令:
该命令执行后会将你的home目录加载到容器的/mnt/home路径中,并允许在其中的一个终端下工作。这是非常有用的,因为你可使用自己偏好的IDE或编辑器 直接编辑代码,同时在运行构建工具时仅使用该容器。它还会开放端口9999,使你可从自己的主机中访问它,并供以后将要构建的服务器使用。
键入exit命令可退出该容器终端,使其停止运行,也可利用上述命令在需要的时候启动它。
Bazel工作区
由于TensorFlow服务程序是用C++编写的,因此在构建时应使用Google的Bazel构建工具。我们将从最近创建的容器内部运行Bazel。
Bazel在代码级管理着第三方依赖项,而且只要它们也需要用Bazel构建,Bazel便会自动下载和构建它们。为了定义我们的项目将支持哪些第三方依赖项,必
须在项目库的根目录下定义一个WORKSPACE文件。
我们需要的依赖项是TensorFlow服务库。在我们的例子中,TensorFlow模型库包含了Inception模型的代码。
不幸的是,在撰写本书时,TensorFlow服务尚不支持作为Git库通过Bazel直接引用,因此必须在项目中将它作为一个Git的子模块包含进去:
下面利用WORKSPACE文件中的local_repository规则将第三方依赖项定义为在本地存储的文件。此外,还需利用从项目中导入的tf_workspace规则对 TensorFlow的依赖项初始化:
导出训练好的模型
一旦模型训练完毕并准备进行评估,便需要将数据流图及其变量值导出,以使其可为产品所用。
模型的数据流图应当与其训练版本有所区分,因为它必须从占位符接收输入,并对其进行单步推断以计算输出。对于Inception模型这个例子,以及对于任意 一般图像识别模型,我们希望输入是一个表示了JPEG编码的图像字符串,这样就可轻易地将它传送到消费App中。这与从TFRecord文件读取训练输入颇为不同。
定义输入的一般形式如下:
在上述代码中,为输入定义了占位符,并调用了一个函数将用占位符表示的外部输入转换为原始推断模型所需的输入格式。例如,我们需要将JPEG字符串 转换为Inception模型所需的图像格式。最后,调用原始模型推断方法,依据转换后的输入得到推断结果。
例如,对于Inception模型,应当有下列方法:
这个推断方法要求各参数都被赋值。我们将从一个训练检查点恢复这些参数值。你可能还记得,在前面的章节中,我们周期性地保存模型的训练检查点文
件。那些文件中包含了当时学习到的参数,因此当出现异常时,训练进展不会受到影响。
训练结束时,最后一次保存的训练检查点文件中将包含最后更新的模型参数,这正是我们希望在产品中使用的版本。
要恢复检查点文件,可使用下列代码:
对于Inception模型,可从下列链接下载一个预训练的检查点文件:http://download.tensorflow.org/models/image/imagenet/inception-v3-2016-03-01.tar.gz
最后,利用tensorflow_serving.session_bundle.exporter.Exporter类将模型导出。我们通过传入一个保存器实例创建了一个它的实例。然后,需要利用 exporter.classification_signature方法创建该模型的签名。该签名指定了什么是input_tensor以及哪些是输出张量。输出由classes_tensor构成,它包含了输出类名称列表以 及模型分配给各类别的分值(或概率)的socres_tensor。通常,在一个包含的类别数相当多的模型中,应当通过配置指定仅返回tf.nn.top_k所选择的那些类别,即 按模型分配的分数按降序排列后的前K个类别。
最后一步是应用这个调用了exporter.Exporter.init方法的签名,并通过export方法导出模型,该方法接收一个输出路径、一个模型的版本号和会话对象。
由于对Exporter类代码中自动生成的代码存在依赖,所以需要在Docker容器内部使用bazel运行我们的导出器。 为此,需要将代码保存到之前启动的bazel工作区内的exporter.py中。此外,还需要一个带有构建规则的BUILD文件,类似于下列内容:
然后,可在容器中通过下列命令运行导出器:
它将依据可从/tmp/inception-v3中提取到的检查点文件在/tmp/inception-v3/{current_timestamp}/中创建导出器。 注意,首次运行它时需要花费一些时间,因为它必须要对TensorFlow进行编译。
定义服务器接口
接下来需要为导出的模型创建一个服务器。
TensorFlow服务使用gRPC协议(gRPC是一种基于HTTP/2的二进制协议)。它支持用于创建服务器和自动生成客户端存根的各种语言。由于TensorFlow是基于 C++的,所以需要在其中定义自己的服务器。幸运的是,服务器端代码比较简短。
为了使用gRPS,必须在一个protocol buffer中定义服务契约,它是用于gRPC的IDL(接口定义语言)和二进制编码。下面来定义我们的服务。前面的导出一节曾 提到,我们希望服务有一个能够接收一个JPEG编码的待分类的图像字符串作为输入,并可返回一个依据分数排列的由推断得到的类别列表。
这样的服务应定义在一个classification_service.proto文件中,类似于:
可对能够接收一幅图像,或一个音频片段或一段文字的任意类型的服务使用同一个接口。
为了使用像数据库记录这样的结构化输入,需要修改ClassificationRequest消息。例如,如果试图为Iris数据集构建分类服务,则需要如下编码:
请注意位于上述代码片段中最上方的load。它从外部导入的protobuf库中导入了cc_proto_library规则定义。然后,利用它为proto文件定义了一个构建规则。利用 bazel build:classification_service_proto可运行该构建,并通过bazel-genfiles/classification_service.grpc.pb.h检查结果:
按照推断逻辑,ClassificationService::Service是必须要实现的接口。我们也可通过检查bazel-genfiles/classification_service.pb.h查看request和response消息的定义:
可以看到,proto定义现在变成了每种类型的C++类接口。它们的实现也是自动生成的,这样便可直接使用它们。
实现推断服务器
为实现ClassificationService::Service,需要加载导出模型并对其调用推断方法。这可通过一个SessionBundle对象来实现,该对象是从导出的模型创建的,它包
含了一个带有完全加载的数据流图的TF会话对象,以及带有定义在导出工具上的分类签名的元数据。
为了从导出的文件路径创建SessionBundle对象,可定义一个便捷函数,以处理这个样板文件:
在这段代码中,我们利用了一个SessionBundleFactory类创建了SessionBundle对象,并将其配置为从pathToExportFiles指定的路径中加载导出的模型。最后返回一 个指向所创建的SessionBundle实例的unique指针。
接下来需要定义服务的实现——Classification ServiceImpl,该类将接收SessionBundle实例作为参数,以在推断中使用:
·利用GetClassificationSignature函数加载存储在模型导出元数据中的Classification-Signature。这个签名指定了输入张量的(逻辑)名称到所接收的图像的真实名称 以及数据流图中输出张量的(逻辑)名称到对其获得推断结果的映射。
·将JPEG编码的图像字符串从request参数复制到将被进行推断的张量。 ·运行推断。它从sessionBundle获得TF会话对象,并运行一次,同时传入输入和输出张量的推断。 ·从输出张量将结果复制到由ClassificationResponse消息指定的形状中的response输出参数并格式化。 最后一段代码是设置gRPC服务器并创建ClassificationServiceImpl实例(用Session-Bundle对象进行配置)的样板代码。
客户端应用
由于gRPC是基于HTTP/2的,将来可能会直接从浏览器调用基于gRPC的服务,但除非主流的浏览器支持所需的HTTP/2特性,且谷歌发布浏览器端的JavaScript
gRPC客户端程序,从webapp访问推断服务都应当通过服务器端的组件进行。
接下来将基于BaseHTTPServer搭建一个简单的Python Web服务器,BaseHTTPServer将处理上载的图像文件,并将其发送给推断服务进行处理,再将推断结果以
纯文本形式返回。
为了将图像发送到推断服务器进行分类,服务器将以一个简单的表单对GET请求做出响应。所使用的代码如下:
为了从Web App服务器调用推断功能,需要ClassificationService相应的Python protocol buffer客户端。为了生成它,需要运行Python的protocol buffer编译器:
它将生成包含了用于调用服务的stub的classification_service_pb2.py文件。
服务器接收到POST请求后,将对发送的表单进行解析,并用它创建一个Classification-Request对象。然后为这个分类服务器设置一个channel,并将请求提交给 它。最后,它会将分类响应渲染为HTML,并送回给用户。
为了运行该服务器,可从该容器外部使用命令python client.py。然后,用浏览器导航到http://localhost:8080来访问其UI。请上传一幅图像并查看推断结果如何。
产品准备
辅助函数、代码结构和类
本章内容较为简短,我们将提供一些关于贯穿本书的服务函数和类的代码片段和解释。
确保目录结构存在
首先介绍一些与文件系统交互所必需的基础知识。基本上,每次创建文件时,都必须确保其父目录已经存在。无论操作系统还是Python都不会自动做这些事, 因此,需要编写能够正确处理这种情形的函数,以确保沿着指定路径的全部目录或部分目录已经存在。
下载函数
在本书的例子中,我们下载了几个不同的数据集。这些例子有一个公共的逻辑,因此将它们提取到一个函数中是完全合理的。首先,如果文件名未指定,则
需要从URL中进行解析。然后,利用上述函数确保下载位置的目录路径已经存在。
在开始实际下载之前,先检查下载位置是否已存在具有目标名称的文件。如果是,则跳过下载,因为不希望重复不必要的大量下载。最后,下载该文件,并
返回其路径。如果确实需要重复某个下载,只需将相应的文件从文件系统中删除。
磁盘缓存修饰器
在数据科学和机器学习中,我们会对较大规模的数据集进行处理,这样每次对模型做出修改后,便无须对这些数据重复进行预处理。因此,我们希望将数据
处理的中间结果保存在磁盘中的公共位置。这样,以后便可以检查该文件是否已经存在。
本节将介绍一个负责缓存和加载的函数修饰器。它利用Python的pickle功能实现了对被修饰的函数的任意返回值的序列化和反序列化。然而,这也意味着它仅 适用于恰好能纳入主存的数据集。对于较大规模的数据集,可参考一些科学数据集格式,如HDF5。
现在利用这一点编写@disk_cache修饰器,它将函数的实参传递给被修饰的函数。这些函数参数也用于确定这些参数的组合是否存在一个缓存的结果。为 此,它们通过散列映射为一个预先为文件名准备的数字。
对于方法,有一个method=False的参数用于通知修饰器是否将第一个参数忽略。在方法和类方法中,第一个参数为对象实例self,它在每次程序运行时都是不 同的,因此不应当用于判断是否有可用的缓存。对于类外部的静态方法和函数,它应为False。
属性字典
当使用配置对象时,这个简单类仅提供了一些便利。虽然能够将配置很好地保存在Python字典结构中,但利用confg[‘key’]这样的语法访问其中的元素终究有些
不方便。
这个类继承自内置的dict类,它允许利用属性语法访问和修改已有的元素,如confg.key和config.key=value。为了创建属性字典,可通过传入一个标准字典(传入 键值对),或利用**locals()实现。
内置函数locals()仅返回一个从作用域中所有局部变量名到值的映射。虽然一些对Python不甚熟悉的人可能会觉得这里有太多难以理解的地方,但这项技术 确实能够为我们带来一些便利,这主要体现在我们能够拥有一些依赖于之前的项的配置项。
该函数返回一个同时包含了learning_rate和optimizer的属性字典,在字典的声明中这是不可能事先有的。与之前一样,只需找到一种适合自己(或同事)的方 式,然后使用即可。
惰性属性修饰器
正如所了解的那样,TensorFlow代码定义了一个数据流图,而非执行实际的计算。如果希望将模型封装到类中,便无法从函数或属性中直接得到其输出,因
为这样每次都会为数据流图增加新的运算。下面来看一个由此引发问题的例子:
如果从外部使用它的一个实例,例如在访问model.optimze时,将会在数据流图中创建一个新的计算路径。此外,它还会在内部调用model.prediction创建一些 新的权值和偏置。为了解决这种设计问题,可引入下列@lazy_property修饰符。
这里的主要思想是定义一个仅计算一次的属性。结果保存到一个像被带有某些前缀的函数(如此处的_layz_)调用的成员中。后续对属性名的调用将返回该 数据流图中的已有节点。现在,我们可将上述模型写为:
惰性属性是一种对TensorFlow模型结构化以及将其分解为类的很好的工具。对于那些有外部需求和需要将计算分解为内件的节点,它都是非常有用的。
覆盖数据流图修饰器
当在如Jupyter Notebook这样的交互环境中使用TensorFlow时,函数修饰器是非常有用的。通常,在未明确指定使用其他数据流图时,TensorFlow会使用默认的
数据流图。然而,在Jupyter Notebook中,解释器的状态会在不同单元(cell)执行期间保持。
因此,初始的默认数据流图是始终存在的。 执行再次定义了数据流图运算的某个单元会试图将这些运算添加到它们已经存在的数据流图中。但是,在这种情况下TensorFlow会抛出一个错误。规避该错误的一个简单方法是根据菜单中的选项重新启动kernel并再次运行所有的单元。
然而,还有另外一种更好的方式。你只需创建一个定制的数据流图,并将其设置为默认的。所有的运算都将添加到该数据流图中,而且如果再次运行该单
元,将会创建一个新的数据流图。旧的数据流图将被自动清理,因为已经不存在任何指向它的引用。
此外,还有一种更方便的做法——将数据流图的创建放在一个修饰器中,并用它修饰主函数。这个主函数应该定义完整的数据流图,例如定义占位符,并调 用其他函数来创建模型。
结语:其他资源
你做到了!感谢阅读本书。至此,对于用TensorFlow构建机器学习模型的核心原理和API想必已有了深入的理解。如果之前 对深度学习缺乏了解,我们希望你通过本书获得更多的领悟,并对卷积神经网络和循环神经网络中一些最常见的架构驾轻就熟。 你已经了解了将训练好的模型投入产品设置中,并在应用中发挥TensorFlow的作用是多么便捷。
TensorFlow具备改变研究人员解决机器学习问题的方式的能力。借助本书介绍的技能,你将对自己构建、测试、实现已有模 型以及设计新的实验网络充满自信。既然已熟知一些有关深度学习的核心知识和技能,请大胆试验TensorFlow中的一切功能。现 在,当讨论关于创建机器学习问题的解决方案时,你已经具备了新的优势。
今后的学习路线及其他资源
虽然本书已经涵盖了相当多的内容,限于篇幅,仍有一些主题未能涉及。因此,我们补充介绍一些资源以帮助你更深入地了
解TensorFlow。
阅读API文档
对于之前未使用过TensorFlow的开发者而言,由于TensorFlow存在一些特有的术语,使得TensorFlow自带的API文档在阅读起 来颇有挑战性。然而,既然已经具备了相关基础,你会发现在编写代码时,这份API文档极有价值。请在后台保持文档的打开状 态,或用一个单独的显示器显示该文档:
https://www.tensorflow.org/versions/master/api_docs/index.html
保持更新
要跟踪TensorFlow的最新功能和特性,最佳途径当然是关注GitHub上的官方TensorFlow Git库。通过阅读拉拽请求(pull request)、问题(issues)以及发行记录(release note),你会提前获悉在下一个版本中会包含哪些内容,甚至能够预测对新版本 的规划。相关网址如下:
https://github.com/tensorflow/tensorflow
分布式TensorFlow
虽然在分布式设置下运行TensorFlow的基本概念相对简单,为了高效训练TensorFlow模型而设置集群的细节却非常复杂。开 始接触分布式TensorFlow时,tensorflow.org网站应当是最主要的参考:https://www.tensorflow.org/versions/master/how_tos/distributed/index.html
请注意,笔者预计在不久的将来,新的版本会使分布式TensorFlow更加简便和灵活,尤其是对使用集群管理软件(如 Kubernetes)的场合。
构建新的TensorFlow功能
如果希望了解TensorFlow的底层原理并学习如何创建自己的Op,笔者强烈推荐tensorflow.org上的官方how-to文档:
https://www.tensorflow.org/versions/master/how_tos/adding_an_op/index.html
从头开始构建Op的过程是熟悉TensorFlow框架设计原理的最佳途径。如果你有能力编写自己需要的特性,为什么不亲自动手 而是等待新版本的发布呢?
TensorFlow社区活跃而繁荣。既然你已经了解这款软件,强烈建议你加入社区系统,并通过帮助他人让这个社区变得更好! 除了GitHub代码库,官方的邮件列表和Stack Overflow问题提供了另外两种社区参与的渠道。
TensorFlow邮件列表是针对与特性相关的一般讨论、设计思想和TensorFlow的未来而设置的:
https://groups.google.com/a/tensorflow.org/d/forum/discuss
请注意,如果要咨询与自己项目有关的问题,请勿使用邮件列表!对于调试中的具体问题、最佳实践、API或任何其他具体 的方面,请查阅Stack Overflow,查看该问题是否已被提问和回答,如果没有,不妨多问自己几个为什么!
http://stackoverflow.com/questions/tagged/tensorflow
本书代码
本书中的示例代码和附加材料可从本书的GitHub代码库获取: https://github.com/backstopmedia/tensorflowbook