TensorFlow 学习手册(五)
译者:飞龙
第十章:使用 TensorFlow 导出和提供模型
在本章中,我们将学习如何使用简单和高级的生产就绪方法保存和导出模型。对于后者,我们介绍了 TensorFlow Serving,这是 TensorFlow 中最实用的用于创建生产环境的工具之一。我们将从快速概述两种简单的保存模型和变量的方法开始:首先是通过手动保存权重并重新分配它们,然后是使用Saver类创建训练检查点以及导出我们的模型。最后,我们将转向更高级的应用程序,通过使用 TensorFlow Serving 在服务器上部署我们的模型。
保存和导出我们的模型
到目前为止,我们已经学习了如何使用 TensorFlow 创建、训练和跟踪模型。现在我们将学习如何保存训练好的模型。保存当前权重状态对于明显的实际原因至关重要——我们不想每次都从头开始重新训练模型,我们也希望有一种方便的方式与他人分享我们模型的状态(就像我们在第七章中看到的预训练模型一样)。
在这一部分,我们将讨论保存和导出的基础知识。我们首先介绍了一种简单的保存和加载权重到文件的方法。然后我们将看到如何使用 TensorFlow 的Saver对象来保持序列化模型检查点,其中包含有关权重状态和构建图的信息。
分配加载的权重
在训练后重复使用权重的一个天真但实用的方法是将它们保存到文件中,稍后可以加载它们并重新分配给模型。
让我们看一些例子。假设我们希望保存用于 MNIST 数据的基本 softmax 模型的权重,我们从会话中获取它们后,将权重表示为 NumPy 数组,并以我们选择的某种格式保存它们:
import numpy as np
weights = sess.run(W)
np.savez(os.path.join(path, 'weight_storage'), weights)
鉴于我们构建了完全相同的图,我们可以加载文件并使用会话中的.assign()方法将加载的权重值分配给相应的变量:
loaded_w = np.load(path + 'weight_storage.npz')
loaded_w = loaded_w.items()[0][1]
x = tf.placeholder(tf.float32, [None, 784])
W = tf.Variable(tf.zeros([784, 10]))
y_true = tf.placeholder(tf.float32, [None, 10])
y_pred = tf.matmul(x, W)
cross_entropy = tf.reduce_mean(
tf.nn.softmax_cross_entropy_with_logits(logits=y_pred,
labels=y_true))
gd_step = tf.train.GradientDescentOptimizer(0.5)\
.minimize(cross_entropy)
correct_mask = tf.equal(tf.argmax(y_pred, 1), tf.argmax(y_true, 1))
accuracy = tf.reduce_mean(tf.cast(correct_mask, tf.float32))
with tf.Session() as sess:
# Assigning loaded weights
sess.run(W.assign(loaded_w))
acc = sess.run(accuracy, feed_dict={x: data.test.images,
y_true: data.test.labels})
print("Accuracy: {}".format(acc))
Out:
Accuracy: 0.9199
接下来,我们将执行相同的过程,但这次是针对第四章中用于 MNIST 数据的 CNN 模型。在这里,我们有八组不同的权重:两个卷积层 1 和 2 的滤波器权重及其对应的偏置,以及两组全连接层的权重和偏置。我们将模型封装在一个类中,以便方便地保持这八个参数的更新列表。
我们还为要加载的权重添加了可选参数:
if weights is not None and sess is not None:
self.load_weights(weights, sess)
以及在传递权重时分配其值的函数:
def load_weights(self, weights, sess):
for i,w in enumerate(weights):
print("Weight index: {}".format(i),
"Weight shape: {}".format(w.shape))
sess.run(self.parameters[i].assign(w))
在整个过程中:
class simple_cnn:
def __init__(self, x_image,keep_prob, weights=None, sess=None):
self.parameters = []
self.x_image = x_image
conv1 = self.conv_layer(x_image, shape=[5, 5, 1, 32])
conv1_pool = self.max_pool_2x2(conv1)
conv2 = self.conv_layer(conv1_pool, shape=[5, 5, 32, 64])
conv2_pool = self.max_pool_2x2(conv2)
conv2_flat = tf.reshape(conv2_pool, [-1, 7*7*64])
full_1 = tf.nn.relu(self.full_layer(conv2_flat, 1024))
full1_drop = tf.nn.dropout(full_1, keep_prob=keep_prob)
self.y_conv = self.full_layer(full1_drop, 10)
if weights is not None and sess is not None:
self.load_weights(weights, sess)
def weight_variable(self,shape):
initial = tf.truncated_normal(shape, stddev=0.1)
return tf.Variable(initial,name='weights')
def bias_variable(self,shape):
initial = tf.constant(0.1, shape=shape)
return tf.Variable(initial,name='biases')
def conv2d(self,x, W):
return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1],
padding='SAME')
def max_pool_2x2(self,x):
return tf.nn.max_pool(x, ksize=[1, 2, 2, 1],
strides=[1, 2, 2, 1], padding='SAME')
def conv_layer(self,input, shape):
W = self.weight_variable(shape)
b = self.bias_variable([shape[3]])
self.parameters += [W, b]
return tf.nn.relu(self.conv2d(input, W) + b)
def full_layer(self,input, size):
in_size = int(input.get_shape()[1])
W = self.weight_variable([in_size, size])
b = self.bias_variable([size])
self.parameters += [W, b]
return tf.matmul(input, W) + b
def load_weights(self, weights, sess):
for i,w in enumerate(weights):
print("Weight index: {}".format(i),
"Weight shape: {}".format(w.shape))
sess.run(self.parameters[i].assign(w))
在这个例子中,模型已经训练好,并且权重已保存为cnn_weights。我们加载权重并将它们传递给我们的 CNN 对象。当我们在测试数据上运行模型时,它将使用预训练的权重:
x = tf.placeholder(tf.float32, shape=[None, 784])
x_image = tf.reshape(x, [-1, 28, 28, 1])
y_ = tf.placeholder(tf.float32, shape=[None, 10])
keep_prob = tf.placeholder(tf.float32)
sess = tf.Session()
weights = np.load(path + 'cnn_weight_storage.npz')
weights = weights.items()[0][1]
cnn = simple_cnn(x_image, keep_prob, weights, sess)
cross_entropy = tf.reduce_mean(
tf.nn.softmax_cross_entropy_with_logits(
logits=cnn.y_conv,
labels=y_))
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
correct_prediction = tf.equal(tf.argmax(cnn.y_conv, 1),
tf.argmax(y_, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
X = data.test.images.reshape(10, 1000, 784)
Y = data.test.labels.reshape(10, 1000, 10)
test_accuracy = np.mean([sess.run(accuracy,
feed_dict={x:X[i], y_:Y[i],keep_prob:1.0})
for i in range(10)])
sess.close()
print("test accuracy: {}".format(test_accuracy))
Out:
Weight index: 0 Weight shape: (5, 5, 1, 32)
Weight index: 1 Weight shape: (32,)
Weight index: 2 Weight shape: (5, 5, 32, 64)
Weight index: 3 Weight shape: (64,)
Weight index: 4 Weight shape: (3136, 1024)
Weight index: 5 Weight shape: (1024,)
Weight index: 6 Weight shape: (1024, 10)
Weight index: 7 Weight shape: (10,)
test accuracy: 0.990100026131
我们可以获得高准确度,而无需重新训练。
Saver 类
TensorFlow 还有一个内置的类,我们可以用于与前面的示例相同的目的,提供额外有用的功能,我们很快就会看到。这个类被称为Saver类(在第五章中已经简要介绍过)。
Saver添加了操作,允许我们通过使用称为检查点文件的二进制文件保存和恢复模型的参数,将张量值映射到变量的名称。与前一节中使用的方法不同,这里我们不必跟踪我们的参数——Saver会自动为我们完成。
使用Saver非常简单。我们首先通过tf.train.Saver()创建一个 saver 实例,指示我们希望保留多少最近的变量检查点,以及可选的保留它们的时间间隔。
例如,在下面的代码中,我们要求只保留最近的七个检查点,并且另外指定每半小时保留一个检查点(这对于性能和进展评估分析可能很有用):
saver = tf.train.Saver(max_to_keep=7,
keep_checkpoint_every_n_hours=0.5)
如果没有给出输入,那么默认情况下会保留最后五个检查点,并且every_n_hours功能会被有效地禁用(默认设置为10000)。
接下来,我们使用saver实例的.save()方法保存检查点文件,传递会话参数、文件保存路径以及步数(global_step),它会自动连接到每个检查点文件的名称中,表示迭代次数。在训练模型时,这会创建不同步骤的多个检查点。
在这个代码示例中,每 50 个训练迭代将在指定目录中保存一个文件:
DIR="*`path/to/model`*"withtf.Session()assess:forstepinrange(1,NUM_STEPS+1):batch_xs,batch_ys=data.train.next_batch(MINIBATCH_SIZE)sess.run(gd_step,feed_dict={x:batch_xs,y_true:batch_ys})ifstep%50==0:saver.save(sess,os.path.join(DIR,"model"),global_step=step)
另一个保存的文件名为checkpoint包含保存的检查点列表,以及最近检查点的路径:
model_checkpoint_path: "model_ckpt-1000"
all_model_checkpoint_paths: "model_ckpt-700"
all_model_checkpoint_paths: "model_ckpt-750"
all_model_checkpoint_paths: "model_ckpt-800"
all_model_checkpoint_paths: "model_ckpt-850"
all_model_checkpoint_paths: "model_ckpt-900"
all_model_checkpoint_paths: "model_ckpt-950"
all_model_checkpoint_paths: "model_ckpt-1000"
在下面的代码中,我们使用Saver保存权重的状态:
fromtensorflow.examples.tutorials.mnistimportinput_dataDATA_DIR='/tmp/data'data=input_data.read_data_sets(DATA_DIR,one_hot=True)NUM_STEPS=1000MINIBATCH_SIZE=100DIR="*`path/to/model`*"x=tf.placeholder(tf.float32,[None,784],name='x')W=tf.Variable(tf.zeros([784,10]),name='W')y_true=tf.placeholder(tf.float32,[None,10])y_pred=tf.matmul(x,W)cross_entropy=tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=y_pred,labels=y_true))gd_step=tf.train.GradientDescentOptimizer(0.5)\ .minimize(cross_entropy)correct_mask=tf.equal(tf.argmax(y_pred,1),tf.argmax(y_true,1))accuracy=tf.reduce_mean(tf.cast(correct_mask,tf.float32))saver=tf.train.Saver(max_to_keep=7,keep_checkpoint_every_n_hours=1)withtf.Session()assess:sess.run(tf.global_variables_initializer())forstepinrange(1,NUM_STEPS+1):batch_xs,batch_ys=data.train.next_batch(MINIBATCH_SIZE)sess.run(gd_step,feed_dict={x:batch_xs,y_true:batch_ys})ifstep%50==0:saver.save(sess,os.path.join(DIR,"model_ckpt"),global_step=step)ans=sess.run(accuracy,feed_dict={x:data.test.images,y_true:data.test.labels})print("Accuracy: {:.4}%".format(ans*100))Out:Accuracy:90.87%
现在我们只需使用saver.restore()为相同的图模型恢复我们想要的检查点,权重会自动分配给模型:
tf.reset_default_graph()
x = tf.placeholder(tf.float32, [None, 784],name='x')
W = tf.Variable(tf.zeros([784, 10]),name='W')
y_true = tf.placeholder(tf.float32, [None, 10])
y_pred = tf.matmul(x, W)
cross_entropy = tf.reduce_mean(
tf.nn.softmax_cross_entropy_with_logits(logits=y_pred,
labels=y_true))
gd_step = tf.train.GradientDescentOptimizer(0.5)\
.minimize(cross_entropy)
correct_mask = tf.equal(tf.argmax(y_pred, 1), tf.argmax(y_true, 1))
accuracy = tf.reduce_mean(tf.cast(correct_mask, tf.float32))
saver = tf.train.Saver()
with tf.Session() as sess:
saver.restore(sess, os.path.join(DIR,"model_ckpt-1000"))
ans = sess.run(accuracy, feed_dict={x: data.test.images,
y_true: data.test.labels})
print("Accuracy: {:.4}%".format(ans*100))
Out:
Accuracy: 90.87%
在恢复之前重置图
加载的变量需要与当前图中的变量配对,因此应该具有匹配的名称。如果由于某种原因名称不匹配,那么可能会出现类似于这样的错误:
NotFoundError: Key W_1 not found in checkpoint
[[Node: save/RestoreV2_2 = RestoreV2[
dtypes=[DT_FLOAT], _device="/job:localhost/replica:0
/task:0/cpu:0"](_recv_save/Const_1_0, save/RestoreV2_2
/tensor_names, save/RestoreV2_2/shape_and_slices)]]
如果名称被一些旧的、无关紧要的图使用,就会发生这种情况。通过使用tf.reset_default_graph()命令重置图,您可以解决这个问题。
到目前为止,在这两种方法中,我们需要重新创建图以重新分配恢复的参数。然而,Saver还允许我们恢复图而无需重建它,通过生成包含有关图的所有必要信息的*.meta*检查点文件。
关于图的信息以及如何将保存的权重合并到其中(元信息)被称为MetaGraphDef。这些信息被序列化——转换为一个字符串——使用协议缓冲区(参见“序列化和协议缓冲区”),它包括几个部分。网络架构的信息保存在graph_def中。
这里是图信息的文本序列化的一个小样本(更多关于序列化的内容将在后面介绍):
meta_info_def {
stripped_op_list {
op {
name: "ApplyGradientDescent"
input_arg {
name: "var"
type_attr: "T"
is_ref: true
}
input_arg {
name: "alpha"
type_attr: "T"
}...
graph_def {
node {
name: "Placeholder"
op: "Placeholder"
attr {
key: "_output_shapes"
value {
list {
shape {
dim {
size: -1
}
dim {
size: 784
}
}
}
}
}...
为了加载保存的图,我们使用tf.train.import_meta_graph(),传递我们想要的检查点文件的名称(带有*.meta*扩展名)。TensorFlow 已经知道如何处理恢复的权重,因为这些信息也被保存了:
tf.reset_default_graph()DIR="*`path/to/model`*"withtf.Session()assess:saver=tf.train.import_meta_graph(os.path.join(DIR,"model_ckpt-1000.meta"))saver.restore(sess,os.path.join(DIR,"model_ckpt-1000"))ans=sess.run(accuracy,feed_dict={x:data.test.images,y_true:data.test.labels})print("Accuracy: {:.4}%".format(ans*100))
然而,仅仅导入图并恢复权重是不够的,会导致错误。原因是导入模型并恢复权重并不会给我们额外访问在运行会话时使用的变量(fetches和feed_dict的键)——模型不知道输入和输出是什么,我们希望计算什么度量等等。
解决这个问题的一种方法是将它们保存在一个集合中。集合是一个类似于字典的 TensorFlow 对象,我们可以以有序、可访问的方式保存我们的图组件。
在这个例子中,我们希望访问度量accuracy(我们希望获取)和 feed 键x和y_true。我们在将模型保存为train_var的名称之前将它们添加到一个集合中:
train_var = [x,y_true,accuracy]
tf.add_to_collection('train_var', train_var[0])
tf.add_to_collection('train_var', train_var[1])
tf.add_to_collection('train_var', train_var[2])
如所示,saver.save()方法会自动保存图结构以及权重的检查点。我们还可以使用saver.export_meta.graph()显式保存图,然后添加一个集合(作为第二个参数传递):
train_var = [x,y_true,accuracy]
tf.add_to_collection('train_var', train_var[0])
tf.add_to_collection('train_var', train_var[1])
tf.add_to_collection('train_var', train_var[2])
saver = tf.train.Saver(max_to_keep=7,
keep_checkpoint_every_n_hours=1)
saver.export_meta_graph(os.path.join(DIR,"model_ckpt.meta")
,collection_list=['train_var'])
现在我们从集合中检索图,从中可以提取所需的变量:
tf.reset_default_graph()DIR="*`path/to/model`*"withtf.Session()assess:sess.run(tf.global_variables_initializer())saver=tf.train.import_meta_graph(os.path.join(DIR,"model_ckpt.meta")saver.restore(sess,os.path.join(DIR,"model_ckpt-1000"))x=tf.get_collection('train_var')[0]y_true=tf.get_collection('train_var')[1]accuracy=tf.get_collection('train_var')[2]ans=sess.run(accuracy,feed_dict={x:data.test.images,y_true:data.test.labels})print("Accuracy: {:.4}%".format(ans*100))Out:Accuracy:91.4%
在定义图形时,请考虑一旦图形已保存和恢复,您想要检索哪些变量/操作,例如前面示例中的准确性操作。在下一节中,当我们谈论 Serving 时,我们将看到它具有内置功能,可以引导导出的模型,而无需像我们在这里做的那样保存变量。
TensorFlow Serving 简介
TensorFlow Serving 是用 C++编写的高性能服务框架,我们可以在生产环境中部署我们的模型。通过使客户端软件能够访问它并通过 Serving 的 API 传递输入,使我们的模型可以用于生产(图 10-1)。当然,TensorFlow Serving 旨在与 TensorFlow 模型无缝集成。Serving 具有许多优化功能,可减少延迟并增加预测的吞吐量,适用于实时、大规模应用。这不仅仅是关于预测的可访问性和高效服务,还涉及灵活性——通常希望出于各种原因保持模型更新,例如获得额外的训练数据以改进模型,对网络架构进行更改等。
图 10-1。Serving 将我们训练好的模型链接到外部应用程序,使客户端软件可以轻松访问。
概述
假设我们运行一个语音识别服务,并且我们希望使用 TensorFlow Serving 部署我们的模型。除了优化服务外,对我们来说定期更新模型也很重要,因为我们获取更多数据或尝试新的网络架构。稍微更技术化一点,我们希望能够加载新模型并提供其输出,卸载旧模型,同时简化模型生命周期管理和版本策略。
一般来说,我们可以通过以下方式实现 Serving。在 Python 中,我们定义模型并准备将其序列化,以便可以被负责加载、提供和管理版本的不同模块解析。Serving 的核心“引擎”位于一个 C++模块中,只有在我们希望控制 Serving 行为的特定调整和定制时才需要访问它。
简而言之,这就是 Serving 架构的工作方式(图 10-2):
-
一个名为
Source的模块通过监视插入的文件系统来识别需要加载的新模型,这些文件系统包含我们在创建时导出的模型及其相关信息。Source包括子模块,定期检查文件系统并确定最新相关的模型版本。 -
当它识别到新的模型版本时,source会创建一个loader。加载器将其servables(客户端用于执行计算的对象,如预测)传递给manager。根据版本策略(渐进式发布、回滚版本等),管理器处理可服务内容的完整生命周期(加载、卸载和提供)。
-
最后,管理器提供了一个接口,供客户端访问可服务的内容。
图 10-2。Serving 架构概述。
Serving 的设计特别之处在于它具有灵活和可扩展的特性。它支持构建各种插件来定制系统行为,同时使用其他核心组件的通用构建。
在下一节中,我们将使用 Serving 构建和部署一个 TensorFlow 模型,展示一些其关键功能和内部工作原理。在高级应用中,我们可能需要控制不同类型的优化和定制;例如,控制版本策略等。在本章中,我们将向您展示如何开始并理解 Serving 的基础知识,为生产就绪的部署奠定基础。
安装
Serving 需要安装一些组件,包括一些第三方组件。安装可以从源代码或使用 Docker 进行,我们在这里使用 Docker 来让您快速开始。Docker 容器将软件应用程序与运行所需的一切(例如代码、文件等)捆绑在一起。我们还使用 Bazel,谷歌自己的构建工具,用于构建客户端和服务器软件。在本章中,我们只简要介绍了 Bazel 和 Docker 等工具背后的技术细节。更全面的描述出现在书末的附录中。
安装 Serving
Docker 安装说明可以在 Docker 网站上找到。
在这里,我们演示使用Ubuntu进行 Docker 设置。
Docker 容器是从本地 Docker 镜像创建的,该镜像是从 dockerfile 构建的,并封装了我们需要的一切(依赖安装、项目代码等)。一旦我们安装了 Docker,我们需要下载 TensorFlow Serving 的 dockerfile。
这个 dockerfile 包含了构建 TensorFlow Serving 所需的所有依赖项。
首先,我们生成镜像,然后可以运行容器(这可能需要一些时间):
docker build --pull -t $USER/tensorflow-serving-devel -f
Dockerfile.devel .
现在我们在本地机器上创建了镜像,我们可以使用以下命令创建和运行容器:
docker run -v $HOME/docker_files:/host_files
-p 80:80 -it $USER/tensorflow-serving-devel
docker run -it $USER/tensorflow-serving-devel命令足以创建和运行容器,但我们对此命令进行了两次添加。
首先,我们添加*-v $HOME/home_dir:/docker_dir*,其中-v(卷)表示请求共享文件系统,这样我们就可以方便地在 Docker 容器和主机之间传输文件。在这里,我们在主机上创建了共享文件夹docker_files,在我们的 Docker 容器上创建了host_files。另一种传输文件的方法是简单地使用命令docker cp foo.txt *mycontainer*:/foo.txt。第二个添加是-p <*host port*>:<*container port*>,这使得容器中的服务可以通过指定的端口暴露在任何地方。
一旦我们输入我们的run命令,一个容器将被创建和启动,并且一个终端将被打开。我们可以使用命令docker ps -a(在 Docker 终端之外)查看我们容器的状态。请注意,每次使用docker run命令时,我们都会创建另一个容器;要进入现有容器的终端,我们需要使用docker exec -it <*container id*> bash。
最后,在打开的终端中,我们克隆并配置 TensorFlow Serving:
git clone --recurse-submodules https://github.com/tensorflow/serving
cd serving/tensorflow
./configure
就是这样,我们准备好了!
构建和导出
现在 Serving 已经克隆并运行,我们可以开始探索其功能和如何使用它。克隆的 TensorFlow Serving 库是按照 Bazel 架构组织的。Bazel 构建的源代码组织在一个工作区目录中,里面有一系列分组相关源文件的包。每个包都有一个BUILD文件,指定从该包内的文件构建的输出。
我们克隆库中的工作区位于*/serving文件夹中,包含WORKSPACE文本文件和/tensorflow_serving*包,稍后我们将返回到这里。
现在我们转向查看处理训练和导出模型的 Python 脚本,并看看如何以一种适合进行 Serving 的方式导出我们的模型。
导出我们的模型
与我们使用Saver类时一样,我们训练的模型将被序列化并导出到两个文件中:一个包含有关变量的信息,另一个包含有关图形和其他元数据的信息。正如我们很快将看到的,Serving 需要特定的序列化格式和元数据,因此我们不能简单地使用Saver类,就像我们在本章开头看到的那样。
我们要采取的步骤如下:
-
像前几章一样定义我们的模型。
-
创建一个模型构建器实例。
-
在构建器中定义我们的元数据(模型、方法、输入和输出等)以序列化格式(称为
SignatureDef)。 -
使用构建器保存我们的模型。
首先,我们通过使用 Serving 的 SavedModelBuilder 模块创建一个构建器实例,传递我们希望将文件导出到的位置(如果目录不存在,则将创建)。SavedModelBuilder 导出表示我们的模型的序列化文件,格式如下所需:
builder = saved_model_builder.SavedModelBuilder(export_path)
我们需要的序列化模型文件将包含在一个目录中,该目录的名称将指定模型及其版本:
export_path_base = sys.argv[-1]
export_path = os.path.join(
compat.as_bytes(export_path_base),
compat.as_bytes(str(FLAGS.model_version)))
这样,每个版本将被导出到一个具有相应路径的不同子目录中。
请注意,export_path_base 是从命令行输入的,使用 sys.argv 获取,版本作为标志保留(在上一章中介绍)。标志解析由 tf.app.run() 处理,我们很快就会看到。
接下来,我们要定义输入(图的输入张量的形状)和输出(预测张量)签名。在本章的第一部分中,我们使用 TensorFlow 集合对象来指定输入和输出数据之间的关系及其相应的占位符,以及用于计算预测和准确性的操作。在这里,签名起到了类似的作用。
我们使用创建的构建器实例添加变量和元图信息,使用 SavedModelBuilder.add_meta_graph_and_variables() 方法:
builder.add_meta_graph_and_variables(
sess, [tag_constants.SERVING],
signature_def_map={
'predict_images':
prediction_signature,
signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY:
classification_signature,
},
legacy_init_op=legacy_init_op)
我们需要传递四个参数:会话、标签(用于“服务”或“训练”)、签名映射和一些初始化。
我们传递一个包含预测和分类签名的字典。我们从预测签名开始,可以将其视为在 TensorFlow 集合中指定和保存预测操作,就像我们之前看到的那样:
prediction_signature = signature_def_utils.build_signature_def(
inputs={'images': tensor_info_x},
outputs={'scores': tensor_info_y},
method_name=signature_constants.PREDICT_METHOD_NAME)
这里的 images 和 scores 是我们稍后将用来引用我们的 x 和 y 张量的任意名称。通过以下命令将图像和分数编码为所需格式:
tensor_info_x = utils.build_tensor_info(x)
tensor_info_y = utils.build_tensor_info(y_conv)
与预测签名类似,我们有分类签名,其中我们输入关于分数(前 k 个类的概率值)和相应类的信息:
# Build the signature_def_map
classification_inputs = utils.build_tensor_info(
serialized_tf_example)
classification_outputs_classes = utils.build_tensor_info(
prediction_classes)
classification_outputs_scores = utils.build_tensor_info(values)
classification_signature = signature_def_utils.build_signature_def(
inputs={signature_constants.CLASSIFY_INPUTS:
classification_inputs},
outputs={
signature_constants.CLASSIFY_OUTPUT_CLASSES:
classification_outputs_classes,
signature_constants.CLASSIFY_OUTPUT_SCORES:
classification_outputs_scores
},
method_name=signature_constants.CLASSIFY_METHOD_NAME)
最后,我们使用 save() 命令保存我们的模型:
builder.save()
简而言之,将所有部分整合在一起,以准备在脚本执行时序列化和导出,我们将立即看到。
以下是我们主要的 Python 模型脚本的最终代码,包括我们的模型(来自第四章的 CNN 模型):
import os
import sys
import tensorflow as tf
from tensorflow.python.saved_model import builder
as saved_model_builder
from tensorflow.python.saved_model import signature_constants
from tensorflow.python.saved_model import signature_def_utils
from tensorflow.python.saved_model import tag_constants
from tensorflow.python.saved_model import utils
from tensorflow.python.util import compat
from tensorflow_serving.example import mnist_input_data
tf.app.flags.DEFINE_integer('training_iteration', 10,
'number of training iterations.')
tf.app.flags.DEFINE_integer(
'model_version', 1, 'version number of the model.')
tf.app.flags.DEFINE_string('work_dir', '/tmp', 'Working directory.')
FLAGS = tf.app.flags.FLAGS
def weight_variable(shape):
initial = tf.truncated_normal(shape, stddev=0.1)
return tf.Variable(initial,dtype='float')
def bias_variable(shape):
initial = tf.constant(0.1, shape=shape)
return tf.Variable(initial,dtype='float')
def conv2d(x, W):
return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')
def max_pool_2x2(x):
return tf.nn.max_pool(x, ksize=[1, 2, 2, 1],
strides=[1, 2, 2, 1], padding='SAME')
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)
print('Training...')
mnist = mnist_input_data.read_data_sets(
FLAGS.work_dir, one_hot=True)
sess = tf.InteractiveSession()
serialized_tf_example = tf.placeholder(
tf.string, name='tf_example')
feature_configs = {'x': tf.FixedLenFeature(shape=[784],
dtype=tf.float32),}
tf_example = tf.parse_example(serialized_tf_example,
feature_configs)
x = tf.identity(tf_example['x'], name='x')
y_ = tf.placeholder('float', shape=[None, 10])
W_conv1 = weight_variable([5, 5, 1, 32])
b_conv1 = bias_variable([32])
x_image = tf.reshape(x, [-1,28,28,1])
h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
h_pool1 = max_pool_2x2(h_conv1)
W_conv2 = weight_variable([5, 5, 32, 64])
b_conv2 = bias_variable([64])
h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
h_pool2 = max_pool_2x2(h_conv2)
W_fc1 = weight_variable([7 * 7 * 64, 1024])
b_fc1 = bias_variable([1024])
h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*64])
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)
keep_prob = tf.placeholder(tf.float32)
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)
W_fc2 = weight_variable([1024, 10])
b_fc2 = bias_variable([10])
y_conv = tf.matmul(h_fc1_drop, W_fc2) + b_fc2
y = tf.nn.softmax(y_conv, name='y')
cross_entropy = -tf.reduce_sum(y_ * tf.log(y_conv))
train_step = tf.train.AdamOptimizer(1e-4)\
.minimize(cross_entropy)
values, indices = tf.nn.top_k(y_conv, 10)
prediction_classes = tf.contrib.lookup.index_to_string(
tf.to_int64(indices),
mapping=tf.constant([str(i) for i in xrange(10)]))
sess.run(tf.global_variables_initializer())
for _ in range(FLAGS.training_iteration):
batch = mnist.train.next_batch(50)
train_step.run(feed_dict={x: batch[0],
y_: batch[1], keep_prob: 0.5})
print(_)
correct_prediction = tf.equal(tf.argmax(y_conv,1),
tf.argmax(y_,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, 'float'))
y_: mnist.test.labels})
print('training accuracy %g' % accuracy.eval(feed_dict={
x: mnist.test.images,
y_: mnist.test.labels, keep_prob: 1.0}))
print('training is finished!')
export_path_base = sys.argv[-1]
export_path = os.path.join(
compat.as_bytes(export_path_base),
compat.as_bytes(str(FLAGS.model_version)))
print 'Exporting trained model to', export_path
builder = saved_model_builder.SavedModelBuilder(export_path)
classification_inputs = utils.build_tensor_info(
serialized_tf_example)
classification_outputs_classes = utils.build_tensor_info(
prediction_classes)
classification_outputs_scores = utils.build_tensor_info(values)
classification_signature = signature_def_utils.build_signature_def(
inputs={signature_constants.CLASSIFY_INPUTS:
classification_inputs},
outputs={
signature_constants.CLASSIFY_OUTPUT_CLASSES:
classification_outputs_classes,
signature_constants.CLASSIFY_OUTPUT_SCORES:
classification_outputs_scores
},
method_name=signature_constants.CLASSIFY_METHOD_NAME)
tensor_info_x = utils.build_tensor_info(x)
tensor_info_y = utils.build_tensor_info(y_conv)
prediction_signature = signature_def_utils.build_signature_def(
inputs={'images': tensor_info_x},
outputs={'scores': tensor_info_y},
method_name=signature_constants.PREDICT_METHOD_NAME)
legacy_init_op = tf.group(tf.initialize_all_tables(),
name='legacy_init_op')
builder.add_meta_graph_and_variables(
sess, [tag_constants.SERVING],
signature_def_map={
'predict_images':
prediction_signature,
signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY:
classification_signature,
},
legacy_init_op=legacy_init_op)
builder.save()
print('new model exported!')
if __name__ == '__main__':
tf.app.run()
tf.app.run() 命令为我们提供了一个很好的包装器,用于处理解析命令行参数。
在我们介绍 Serving 的最后部分中,我们使用 Bazel 实际导出和部署我们的模型。
大多数 Bazel BUILD 文件仅包含构建规则的声明,指定输入和输出之间的关系,以及构建输出的步骤。
例如,在这个 BUILD 文件中,我们有一个 Python 规则 py_binary 用于构建可执行程序。这里有三个属性,name 用于规则的名称,srcs 用于处理以创建目标(我们的 Python 脚本)的文件列表,deps 用于链接到二进制目标中的其他库的列表:
py_binary(
name = "serving_model_ch4",
srcs = [
"serving_model_ch4.py",
],
deps = [
":mnist_input_data",
"@org_tensorflow//tensorflow:tensorflow_py",
"@org_tensorflow//tensorflow/python/saved_model:builder",
"@org_tensorflow//tensorflow/python/saved_model:constants",
"@org_tensorflow//tensorflow/python/saved_model:loader",
"@org_tensorflow//tensorflow/python/saved_model:
signature_constants",
"@org_tensorflow//tensorflow/python/saved_model:
signature_def_utils",
"@org_tensorflow//tensorflow/python/saved_model:
tag_constants",
"@org_tensorflow//tensorflow/python/saved_model:utils",
],
)
接下来,我们使用 Bazel 运行和导出模型,进行 1,000 次迭代训练并导出模型的第一个版本:
bazel build //tensorflow_serving/example:serving_model_ch4
bazel-bin/tensorflow_serving/example/serving_model_ch4
--training_iteration=1000 --model_version=1 /tmp/mnist_model
要训练模型的第二个版本,我们只需使用:
--model_version=2
在指定的子目录中,我们将找到两个文件,saved_model.pb 和 variables,它们包含有关我们的图(包括元数据)和其变量的序列化信息。在接下来的行中,我们使用标准的 TensorFlow 模型服务器加载导出的模型:
bazel build //tensorflow_serving/model_servers:
tensorflow_model_server
bazel-bin/tensorflow_serving/model_servers/tensorflow_model_server
--port=8000 --model_name=mnist
--model_base_path=/tmp/mnist_model/ --logtostderr
最后,我们的模型现在已经被提供并准备在 localhost:8000 上运行。我们可以使用一个简单的客户端实用程序 mnist_client 来测试服务器:
bazel build //tensorflow_serving/example:mnist_client
bazel-bin/tensorflow_serving/example/mnist_client
--num_tests=1000 --server=localhost:8000
总结
本章讨论了如何保存、导出和提供模型,从简单保存和重新分配权重使用内置的Saver实用程序到用于生产的高级模型部署机制。本章的最后部分涉及 TensorFlow Serving,这是一个非常好的工具,可以通过动态版本控制使我们的模型商业化准备就绪。Serving 是一个功能丰富的实用程序,具有许多功能,我们强烈建议对掌握它感兴趣的读者在网上寻找更深入的技术资料。
附录 A. 模型构建和使用 TensorFlow Serving 的提示
模型结构化和定制化
在这个简短的部分中,我们将专注于两个主题,这些主题延续并扩展了前几章——如何构建一个合适的模型,以及如何定制模型的实体。我们首先描述如何通过使用封装来有效地重构我们的代码,并允许其变量被共享和重复使用。在本节的第二部分,我们将讨论如何定制我们自己的损失函数和操作,并将它们用于优化。
模型结构化
最终,我们希望设计我们的 TensorFlow 代码高效,以便可以重用于多个任务,并且易于跟踪和传递。使事情更清晰的一种方法是使用可用的 TensorFlow 扩展库之一,这些库在第七章中已经讨论过。然而,虽然它们非常适合用于典型的网络,但有时我们希望实现的具有新组件的模型可能需要较低级别 TensorFlow 的完全灵活性。
让我们再次看一下前一章的优化代码:
import tensorflow as tf
NUM_STEPS = 10
g = tf.Graph()
wb_ = []
with g.as_default():
x = tf.placeholder(tf.float32,shape=[None,3])
y_true = tf.placeholder(tf.float32,shape=None)
with tf.name_scope('inference') as scope:
w = tf.Variable([[0,0,0]],dtype=tf.float32,name='weights')
b = tf.Variable(0,dtype=tf.float32,name='bias')
y_pred = tf.matmul(w,tf.transpose(x)) + b
with tf.name_scope('loss') as scope:
loss = tf.reduce_mean(tf.square(y_true-y_pred))
with tf.name_scope('train') as scope:
learning_rate = 0.5
optimizer = tf.train.GradientDescentOptimizer(learning_rate)
train = optimizer.minimize(loss)
init = tf.global_variables_initializer()
with tf.Session() as sess:
sess.run(init)
for step in range(NUM_STEPS):
sess.run(train,{x: x_data, y_true: y_data})
if (step % 5 == 0):
print(step, sess.run([w,b]))
wb_.append(sess.run([w,b]))
print(10, sess.run([w,b]))
我们得到:
(0, [array([[ 0.30149955, 0.49303722, 0.11409992]],
dtype=float32), -0.18563795])
(5, [array([[ 0.30094019, 0.49846715, 0.09822173]],
dtype=float32), -0.19780949])
(10, [array([[ 0.30094025, 0.49846718, 0.09822182]],
dtype=float32), -0.19780946])
这里的整个代码只是简单地一行一行堆叠。对于简单和专注的示例来说,这是可以的。然而,这种编码方式有其局限性——当代码变得更加复杂时,它既不可重用也不太可读。
让我们放大视野,思考一下我们的基础设施应该具有哪些特征。首先,我们希望封装模型,以便可以用于各种任务,如训练、评估和形成预测。此外,以模块化的方式构建模型可能更有效,使我们能够对其子组件具有特定控制,并增加可读性。这将是接下来几节的重点。
模块化设计
一个很好的开始是将代码分成捕捉学习模型中不同元素的函数。我们可以这样做:
def predict(x,y_true,w,b):
y_pred = tf.matmul(w,tf.transpose(x)) + b
return y_pred
def get_loss(y_pred,y_true):
loss = tf.reduce_mean(tf.square(y_true-y_pred))
return loss
def get_optimizer(y_pred,y_true):
loss = get_loss(y_pred,y_true)
optimizer = tf.train.GradientDescentOptimizer(0.5)
train = optimizer.minimize(loss)
return train
def run_model(x_data,y_data):
wb_ = []
# Define placeholders and variables
x = tf.placeholder(tf.float32,shape=[None,3])
y_true = tf.placeholder(tf.float32,shape=None)
w = tf.Variable([[0,0,0]],dtype=tf.float32)
b = tf.Variable(0,dtype=tf.float32)
print(b.name)
# Form predictions
y_pred = predict(x,y_true,w,b)
# Create optimizer
train = get_optimizer(y_pred,y_data)
# Run session
init = tf.global_variables_initializer()
with tf.Session() as sess:
sess.run(init)
for step in range(10):
sess.run(train,{x: x_data, y_true: y_data})
if (step % 5 == 0):
print(step, sess.run([w,b]))
wb_.append(sess.run([w,b]))
run_model(x_data,y_data)
run_model(x_data,y_data)
这里是结果:
Variable_9:0 Variable_8:0
0 [array([[ 0.27383861, 0.48421991, 0.09082422]],
dtype=float32), -0.20805186]
4 [array([[ 0.29868397, 0.49840903, 0.10026278]],
dtype=float32), -0.20003076]
9 [array([[ 0.29868546, 0.49840906, 0.10026464]],
dtype=float32), -0.20003042]
Variable_11:0 Variable_10:0
0 [array([[ 0.27383861, 0.48421991, 0.09082422]],
dtype=float32), -0.20805186]
4 [array([[ 0.29868397, 0.49840903, 0.10026278]],
dtype=float32), -0.20003076]
9 [array([[ 0.29868546, 0.49840906, 0.10026464]],
dtype=float32), -0.20003042]
现在我们可以重复使用具有不同输入的代码,这种划分使其更易于阅读,特别是在变得更加复杂时。
在这个例子中,我们两次调用了主函数并打印了创建的变量。请注意,每次调用都会创建不同的变量集,从而创建了四个变量。例如,假设我们希望构建一个具有多个输入的模型,比如两个不同的图像。假设我们希望将相同的卷积滤波器应用于两个输入图像。将创建新的变量。为了避免这种情况,我们可以“共享”滤波器变量,在两个图像上使用相同的变量。
变量共享
通过使用tf.get_variable()而不是tf.Variable(),可以重复使用相同的变量。我们使用方式与tf.Variable()非常相似,只是需要将初始化器作为参数传递:
w = tf.get_variable('w',[1,3],initializer=tf.zeros_initializer())
b = tf.get_variable('b',[1,1],initializer=tf.zeros_initializer())
在这里,我们使用了tf.zeros_initializer()。这个初始化器与tf.zeros()非常相似,只是它不会将形状作为参数,而是根据tf.get_variable()指定的形状排列值。
在这个例子中,变量w将被初始化为[0,0,0],如给定的形状[1,3]所指定。
使用get_variable(),我们可以重复使用具有相同名称的变量(包括作用域前缀,可以通过tf.variable_scope()设置)。但首先,我们需要通过使用tf.variable_scope.reuse_variable()或设置reuse标志(tf.variable.scope(reuse=True))来表明这种意图。下面的代码示例展示了如何共享变量。
标志误用的注意事项
每当一个变量与另一个变量具有完全相同的名称时,在未设置reuse标志时会抛出异常。相反的情况也是如此——期望重用的名称不匹配的变量(当reuse=True时)也会导致异常。
使用这些方法,并将作用域前缀设置为Regression,通过打印它们的名称,我们可以看到相同的变量被重复使用:
def run_model(x_data,y_data):
wb_ = []
# Define placeholders and variables
x = tf.placeholder(tf.float32,shape=[None,3])
y_true = tf.placeholder(tf.float32,shape=None)
w = tf.get_variable('w',[1,3],initializer=tf.zeros_initializer())
b = tf.get_variable('b',[1,1],initializer=tf.zeros_initializer())
print(b.name,w.name)
# Form predictions
y_pred = predict(x,y_true,w,b)
# Create optimizer
train = get_optimizer(y_pred,y_data)
# Run session
init = tf.global_variables_initializer()
sess.run(init)
for step in range(10):
sess.run(train,{x: x_data, y_true: y_data})
if (step % 5 == 4) or (step == 0):
print(step, sess.run([w,b]))
wb_.append(sess.run([w,b]))
sess = tf.Session()
with tf.variable_scope("Regression") as scope:
run_model(x_data,y_data)
scope.reuse_variables()
run_model(x_data,y_data)
sess.close()
输出如下所示:
Regression/b:0 Regression/w:0
0 [array([[ 0.27383861, 0.48421991, 0.09082422]],
dtype=float32), array([[-0.20805186]], dtype=float32)]
4 [array([[ 0.29868397, 0.49840903, 0.10026278]],
dtype=float32), array([[-0.20003076]], dtype=float32)]
9 [array([[ 0.29868546, 0.49840906, 0.10026464]],
dtype=float32), array([[-0.20003042]], dtype=float32)]
Regression/b:0 Regression/w:0
0 [array([[ 0.27383861, 0.48421991, 0.09082422]],
dtype=float32), array([[-0.20805186]], dtype=float32)]
4 [array([[ 0.29868397, 0.49840903, 0.10026278]],
dtype=float32), array([[-0.20003076]], dtype=float32)]
9 [array([[ 0.29868546, 0.49840906, 0.10026464]],
dtype=float32), array([[-0.20003042]], dtype=float32)]
tf.get_variables()是一个简洁、轻量级的共享变量的方法。另一种方法是将我们的模型封装为一个类,并在那里管理变量。这种方法有许多其他好处,如下一节所述
类封装
与任何其他程序一样,当事情变得更加复杂,代码行数增加时,将我们的 TensorFlow 代码放在一个类中变得非常方便,这样我们就可以快速访问属于同一模型的方法和属性。类封装允许我们维护变量的状态,然后执行各种训练后任务,如形成预测、模型评估、进一步训练、保存和恢复权重,以及与我们的模型解决的特定问题相关的任何其他任务。
在下一批代码中,我们看到一个简单的类包装器示例。当实例化时创建模型,并通过调用fit()方法执行训练过程。
@property 和 Python 装饰器
这段代码使用了@property装饰器。装饰器只是一个以另一个函数作为输入的函数,对其进行一些操作(比如添加一些功能),然后返回它。在 Python 中,装饰器用@符号定义。
@property是一个用于处理类属性访问的装饰器。
我们的类包装器如下:
class Model:
def __init__(self):
# Model
self.x = tf.placeholder(tf.float32,shape=[None,3])
self.y_true = tf.placeholder(tf.float32,shape=None)
self.w = tf.Variable([[0,0,0]],dtype=tf.float32)
self.b = tf.Variable(0,dtype=tf.float32)
init = tf.global_variables_initializer()
self.sess = tf.Session()
self.sess.run(init)
self._output = None
self._optimizer = None
self._loss = None
def fit(self,x_data,y_data):
print(self.b.name)
for step in range(10):
self.sess.run(self.optimizer,{self.x: x_data, self.y_true: y_data})
if (step % 5 == 4) or (step == 0):
print(step, self.sess.run([self.w,self.b]))
@property
def output(self):
if not self._output:
y_pred = tf.matmul(self.w,tf.transpose(self.x)) + self.b
self._output = y_pred
return self._output
@property
def loss(self):
if not self._loss:
error = tf.reduce_mean(tf.square(self.y_true-self.output))
self._loss= error
return self._loss
@property
def optimizer(self):
if not self._optimizer:
opt = tf.train.GradientDescentOptimizer(0.5)
opt = opt.minimize(self.loss)
self._optimizer = opt
return self._optimizer
lin_reg = Model()
lin_reg.fit(x_data,y_data)
lin_reg.fit(x_data,y_data)
然后我们得到这个:
Variable_89:0
0 [array([[ 0.32110521, 0.4908163 , 0.09833425]],
dtype=float32), -0.18784374]
4 [array([[ 0.30250472, 0.49442694, 0.10041162]],
dtype=float32), -0.1999902]
9 [array([[ 0.30250433, 0.49442688, 0.10041161]],
dtype=float32), -0.19999036]
Variable_89:0
0 [array([[ 0.30250433, 0.49442688, 0.10041161]],
dtype=float32), -0.19999038]
4 [array([[ 0.30250433, 0.49442688, 0.10041161]],
dtype=float32), -0.19999038]
9 [array([[ 0.30250433, 0.49442688, 0.10041161]],
dtype=float32), -0.19999036]
将代码拆分为函数在某种程度上是多余的,因为相同的代码行在每次调用时都会重新计算。一个简单的解决方案是在每个函数的开头添加一个条件。在下一个代码迭代中,我们将看到一个更好的解决方法。
在这种情况下,不需要使用变量共享,因为变量被保留为模型对象的属性。此外,在调用两次训练方法model.fit()后,我们看到变量保持了它们的当前状态。
在本节的最后一批代码中,我们添加了另一个增强功能,创建一个自定义装饰器,自动检查函数是否已被调用。
我们可以做的另一个改进是将所有变量保存在字典中。这将使我们能够在每次操作后跟踪我们的变量,就像我们在第十章中看到的那样,当我们查看保存权重和模型时。
最后,添加了用于获取损失函数值和权重的额外函数:
class Model:
def __init__(self):
# Model
self.x = tf.placeholder(tf.float32,shape=[None,3])
self.y_true = tf.placeholder(tf.float32,shape=None)
self.params = self._initialize_weights()
init = tf.global_variables_initializer()
self.sess = tf.Session()
self.sess.run(init)
self.output
self.optimizer
self.loss
def _initialize_weights(self):
params = dict()
params['w'] = tf.Variable([[0,0,0]],dtype=tf.float32)
params['b'] = tf.Variable(0,dtype=tf.float32)
return params
def fit(self,x_data,y_data):
print(self.params['b'].name)
for step in range(10):
self.sess.run(self.optimizer,{self.x: x_data, self.y_true: y_data})
if (step % 5 == 4) or (step == 0):
print(step,
self.sess.run([self.params['w'],self.params['b']]))
def evaluate(self,x_data,y_data):
print(self.params['b'].name)
MSE = self.sess.run(self.loss,{self.x: x_data, self.y_true: y_data})
return MSE
def getWeights(self):
return self.sess.run([self.params['b']])
@property_with_check
def output(self):
y_pred = tf.matmul(self.params['w'],tf.transpose(self.x)) + \
self.params['b']
return y_pred
@property_with_check
def loss(self):
error = tf.reduce_mean(tf.square(self.y_true-self.output))
return error
@property_with_check
def optimizer(self):
opt = tf.train.GradientDescentOptimizer(0.5)
opt = opt.minimize(self.loss)
return opt
lin_reg = Model()
lin_reg.fit(x_data,y_data)
MSE = lin_reg.evaluate(x_data,y_data)
print(MSE)
print(lin_reg.getWeights())
以下是输出:
Variable_87:0
0 [array([[ 0.32110521, 0.4908163 , 0.09833425]],
dtype=float32), -0.18784374]
4 [array([[ 0.30250472, 0.49442694, 0.10041162]],
dtype=float32), -0.1999902]
9 [array([[ 0.30250433, 0.49442688, 0.10041161]],
dtype=float32), -0.19999036]
Variable_87:0
0 [array([[ 0.30250433, 0.49442688, 0.10041161]],
dtype=float32), -0.19999038]
4 [array([[ 0.30250433, 0.49442688, 0.10041161]],
dtype=float32), -0.19999038]
9 [array([[ 0.30250433, 0.49442688, 0.10041161]],
dtype=float32), -0.19999036]
Variable_87:0
0.0102189
[-0.19999036]
自定义装饰器检查属性是否存在,如果不存在,则根据输入函数设置它。否则,返回属性。使用functools.wrap(),这样我们就可以引用函数的名称:
import functools
def property_with_check(input_fn):
attribute = '_cache_' + input_fn.__name__
@property
@functools.wraps(input_fn)
def check_attr(self):
if not hasattr(self, attribute):
setattr(self, attribute, input_fn(self))
return getattr(self, attribute)
return check_attr
这是一个相当基本的示例,展示了我们如何改进模型的整体代码。这种优化可能对我们简单的线性回归示例来说有些过度,但对于具有大量层、变量和特征的复杂模型来说,这绝对是值得的努力。
定制
到目前为止,我们使用了两个损失函数。在第二章中的分类示例中,我们使用了交叉熵损失,定义如下:
cross_entropy = tf.reduce_mean(
tf.nn.softmax_cross_entropy_with_logits(logits=y_pred, labels=y_true))
相比之下,在前一节的回归示例中,我们使用了平方误差损失,定义如下:
loss = tf.reduce_mean(tf.square(y_true-y_pred))
这些是目前在机器学习和深度学习中最常用的损失函数。本节的目的是双重的。首先,我们想指出 TensorFlow 在利用自定义损失函数方面的更一般能力。其次,我们将讨论正则化作为任何损失函数的扩展形式,以实现特定目标,而不考虑使用的基本损失函数。
自制损失函数
本书(以及我们的读者)以深度学习为重点来看待 TensorFlow。然而,TensorFlow 的范围更广泛,大多数机器学习问题都可以以一种 TensorFlow 可以解决的方式来表述。此外,任何可以在计算图框架中表述的计算都是从 TensorFlow 中受益的好候选。
主要特例是无约束优化问题类。这些问题在科学(和算法)计算中非常常见,对于这些问题,TensorFlow 尤为有用。这些问题突出的原因是,TensorFlow 提供了计算梯度的自动机制,这为解决这类问题的开发时间提供了巨大的加速。
一般来说,对于任意损失函数的优化将采用以下形式
def my_loss_function(key-variables...):
loss = ...
return loss
my_loss = my_loss_function(key-variables...)
gd_step = tf.train.GradientDescentOptimizer().minimize(my_loss)
任何优化器都可以用于替代 GradientDescentOptimizer。
正则化
正则化是通过对解决方案的复杂性施加惩罚来限制优化问题(有关更多详细信息,请参见第四章中的注释)。在本节中,我们将看一下特定情况下,惩罚直接添加到基本损失函数中的附加形式。
例如,基于第二章中 softmax 示例,我们有以下内容:
x = tf.placeholder(tf.float32, [None, 784])
W = tf.Variable(tf.zeros([784, 10]))
y_true = tf.placeholder(tf.float32, [None, 10])
y_pred = tf.matmul(x, W)
cross_entropy = tf.reduce_mean(
tf.nn.softmax_cross_entropy_with_logits(logits=y_pred, labels=y_true))
total_loss = cross_entropy + LAMBDA * tf.nn.l2_loss(W)
gd_step = tf.train.GradientDescentOptimizer(0.5).minimize(total_loss)
与第二章中的原始版本的区别在于,我们将 LAMBDA * tf.nn.l2_loss(W) 添加到我们正在优化的损失中。在这种情况下,使用较小的权衡参数 LAMBDA 值对最终准确性的影响很小(较大的值会有害)。在大型网络中,过拟合是一个严重问题,这种正则化通常可以拯救一命。
这种类型的正则化可以针对模型的权重进行,如前面的示例所示(也称为权重衰减,因为它会使权重值变小),也可以针对特定层或所有层的激活进行。
另一个因素是我们使用的函数——我们可以使用 l1 而不是 l2 正则化,或者两者的组合。所有这些正则化器的组合都是有效的,并在各种情境中使用。
许多抽象层使正则化的应用变得简单,只需指定滤波器数量或激活函数即可。例如,在 Keras(在第七章中审查的非常流行的扩展)中,我们提供了适用于所有标准层的正则化器,列在表 A-1 中。
表 A-1. 使用 Keras 进行正则化
| 正则化器 | 作用 | 示例 |
|---|---|---|
l1 | l1 正则化权重 |
Dense(100, W_regularizer=l1(0.01))
|
l2 | l2 正则化权重 |
|---|
Dense(100, W_regularizer=l2(0.01))
|
l1l2 | 组合 l1 + l2 正则化权重 |
|---|
Dense(100, W_regularizer=l1l2(0.01))
|
activity_l1 | l1 正则化激活 |
|---|
Dense(100, activity_regularizer=activity_l1(0.01))
|
activity_l2 | l2 正则化激活 |
|---|
Dense(100, activity_regularizer=activity_l2(0.01))
|
activity_l1l2 | 组合 l1 + l2 正则化激活 |
|---|
Dense(100, activity_regularizer=activity_l1l2(0.01))
|
在模型过拟合时,使用这些快捷方式可以轻松测试不同的正则化方案。
编写自己的操作
TensorFlow 预装了大量本地操作,从标准算术和逻辑操作到矩阵操作、深度学习特定函数等等。当这些操作不够时,可以通过创建新操作来扩展系统。有两种方法可以实现这一点:
-
编写一个“从头开始”的 C++ 版本的操作
-
编写结合现有操作和 Python 代码创建新操作的 Python 代码
我们将在本节的其余部分讨论第二个选项。
构建 Python op 的主要原因是在 TensorFlow 计算图的上下文中利用 NumPy 功能。为了说明,我们将使用 NumPy 乘法函数构建前一节中的正则化示例,而不是 TensorFlow op:
import numpy as np
LAMBDA = 1e-5
def mul_lambda(val):
return np.multiply(val, LAMBDA).astype(np.float32)
请注意,这是为了说明的目的,没有特别的原因让任何人想要使用这个而不是原生的 TensorFlow op。我们使用这个过度简化的例子是为了将焦点转移到机制的细节而不是计算上。
为了在 TensorFlow 内部使用我们的新创建,我们使用py_func()功能:
tf.py_func(my_python_function, [input], [output_types])
在我们的情况下,这意味着我们计算总损失如下:
total_loss = cross_entropy + \
tf.py_func(mul_lambda, [tf.nn.l2_loss(W)], [tf.float32])[0]
然而,这样做还不够。回想一下,TensorFlow 会跟踪每个 op 的梯度,以便对我们整体模型进行基于梯度的训练。为了使这个与新的基于 Python 的 op 一起工作,我们必须手动指定梯度。这分为两个步骤。
首先,我们创建并注册梯度:
@tf.RegisterGradient("PyMulLambda")
def grad_mul_lambda(op, grad):
return LAMBDA*grad
接下来,在使用函数时,我们将这个函数指定为 op 的梯度。这是使用在上一步中注册的字符串完成的:
with tf.get_default_graph().gradient_override_map({"PyFunc": "PyMulLambda"}):
total_loss = cross_entropy + \
tf.py_func(mul_lambda, [tf.nn.l2_loss(W)], [tf.float32])[0]
将所有内容放在一起,通过我们基于新 Python op 的正则化 softmax 模型的代码现在是:
import numpy as np
import tensorflow as tf
LAMBDA = 1e-5
def mul_lambda(val):
return np.multiply(val, LAMBDA).astype(np.float32)
@tf.RegisterGradient("PyMulLambda")
def grad_mul_lambda(op, grad):
return LAMBDA*grad
x = tf.placeholder(tf.float32, [None, 784])
W = tf.Variable(tf.zeros([784, 10]))
y_true = tf.placeholder(tf.float32, [None, 10])
y_pred = tf.matmul(x, W)
cross_entropy =
tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits\
(logits=y_pred, labels=y_true))
with tf.get_default_graph().gradient_override_map({"PyFunc": "PyMulLambda"}):
total_loss = cross_entropy + \
tf.py_func(mul_lambda, [tf.nn.l2_loss(W)], [tf.float32])[0]
gd_step = tf.train.GradientDescentOptimizer(0.5).minimize(total_loss)
correct_mask = tf.equal(tf.argmax(y_pred, 1), tf.argmax(y_true, 1))
accuracy = tf.reduce_mean(tf.cast(correct_mask, tf.float32))
现在可以使用与第二章中首次介绍该模型时相同的代码进行训练。
在计算梯度时使用输入
在我们刚刚展示的简单示例中,梯度仅取决于相对于输入的梯度,而不是输入本身。在一般情况下,我们还需要访问输入。这很容易做到,使用op.inputs 字段:
x = op.inputs[0]
其他输入(如果存在)以相同的方式访问。
TensorFlow Serving 所需和推荐的组件
在本节中,我们添加了一些在第十章中涵盖的材料的细节,并更深入地审查了 TensorFlow Serving 背后使用的一些技术组件。
在第十章中,我们使用 Docker 来运行 TensorFlow Serving。那些喜欢避免使用 Docker 容器的人需要安装以下内容:
Bazel
Bazel 是谷歌自己的构建工具,最近才公开。当我们使用术语构建时,我们指的是使用一堆规则从源代码中创建输出软件,以非常高效和可靠的方式。构建过程还可以用来引用构建输出所需的外部依赖项。除了其他语言,Bazel 还可以用于构建 C++应用程序,我们利用这一点来构建用 C++编写的 TensorFlow Serving 程序。Bazel 构建的源代码基于一个工作区目录,其中包含一系列包含相关源文件的嵌套层次结构的包。每个软件包包含三种类型的文件:人工编写的源文件称为targets,从源文件创建的生成文件,以及指定从输入派生输出的步骤的规则。
每个软件包都有一个BUILD文件,指定从该软件包内的文件构建的输出。我们使用基本的 Bazel 命令,比如bazel build来从目标构建生成的文件,以及bazel run来执行构建规则。当我们想要指定包含构建输出的目录时,我们使用-bin标志。
下载和安装说明可以在Bazel 网站上找到。
gRPC
远程过程调用(RPC)是一种客户端(调用者)-服务器(执行者)交互的形式;程序可以请求在另一台计算机上执行的过程(例如,一个方法)(通常在共享网络中)。gRPC 是由 Google 开发的开源框架。与任何其他 RPC 框架一样,gRPC 允许您直接调用其他机器上的方法,从而更容易地分发应用程序的计算。gRPC 的伟大之处在于它如何处理序列化,使用快速高效的协议缓冲区而不是 XML 或其他方法。
下载和安装说明可以在GitHub上找到。
接下来,您需要确保使用以下命令安装了 Serving 所需的依赖项:
sudo apt-get update && sudo apt-get install -y \
build-essential \
curl \
libcurl3-dev \
git \
libfreetype6-dev \
libpng12-dev \
libzmq3-dev \
pkg-config \
python-dev \
python-numpy \
python-pip \
software-properties-common \
swig \
zip \
zlib1g-dev
最后,克隆 Serving:
git clone --recurse-submodules https://github.com/tensorflow/serving
cd serving
如第十章所示,另一个选择是使用 Docker 容器,实现简单干净的安装。
什么是 Docker 容器,为什么我们要使用它?
Docker 本质上解决了与 VirtualBox 的 Vagrant 相同的问题,即确保我们的代码在其他机器上能够顺利运行。不同的机器可能具有不同的操作系统以及不同的工具集(安装的软件、配置、权限等)。通过复制相同的环境——也许是为了生产目的,也许只是与他人分享——我们保证我们的代码在其他地方与在原始开发机器上运行的方式完全相同。
Docker 的独特之处在于,与其他类似用途的工具不同,它不会创建一个完全操作的虚拟机来构建环境,而是在现有系统(例如 Ubuntu)之上创建一个容器,在某种意义上充当虚拟机,并利用我们现有的操作系统资源。这些容器是从本地 Docker 镜像创建的,该镜像是从dockerfile构建的,并封装了我们需要的一切(依赖安装、项目代码等)。从该镜像中,我们可以创建任意数量的容器(当然,直到内存用尽为止)。这使得 Docker 成为一个非常酷的工具,我们可以轻松地创建包含我们的代码的完整多个环境副本,并在任何地方运行它们(对于集群计算非常有用)。
一些基本的 Docker 命令
为了让您更加熟悉使用 Docker,这里简要介绍一些有用的命令,以最简化的形式编写。假设我们已经准备好一个 dockerfile,我们可以使用docker build <*dockerfile*>来构建一个镜像。然后,我们可以使用docker run <*image*>命令创建一个新的容器。该命令还将自动运行容器并打开一个终端(输入exit关闭终端)。要运行、停止和删除现有容器,我们分别使用docker start <*container id*>、docker stop <*container id*>和docker rm <*container id*>命令。要查看所有实例的列表,包括正在运行和空闲的实例,我们输入docker ps -a。
当我们运行一个实例时,我们可以添加-p标志,后面跟一个端口供 Docker 暴露,以及-v标志,后面跟一个要挂载的主目录,这将使我们能够在本地工作(主目录通过容器中的/mnt/home路径进行访问)。