关于 TensorFlow 2.0 你需要了解的一切

阅读数:6524 2019 年 7 月 15 日 13:54

关于TensorFlow 2.0 你需要了解的一切

2019 年 6 月 26 日,在 Sao Paulo 举行的 PAPIs.io LATAM 会议上,作为 Daitan 的代表,本文作者 Thalles Silva 举办了一个关于 TensorFlow(TF)2.0 的研讨会,并在会上探讨了一些关于 TF 2.0 的话题。研讨会的初衷是重点展示 2.0 版本同以往 1.x 版本的不同。本文回顾了会上讨论的主要内容:Keras-APIs、SavedModels、TensorBoard、Keras-Tuner 等。同时,你也可以通过 Colab notebook 来查看练习代码。

TensorFlow 2.0 简介

TensorFlow 是谷歌在 2015 年开源的一个通用高性能计算库。从一开始,TensorFlow 的主要目的就是为构建神经网络(NN)提供高性能 API。然而,借助于机器学习(ML)社区对它的兴趣以及时间上的优势,这个类库演变成了一个完整的 ML 生态系统。

关于TensorFlow 2.0 你需要了解的一切

目前,该类库也在经历着从推出以来最大规模的变化。TensorFLow 2.0 目前仍处在 beta 版本,同 TensorFlow 1.x 版本相比它带来了太多的改变。接下来,让我们深入研究其中的一些主要改变。

即时执行(Eager Execution)变为默认模式

首先,即时执行成为 TensorFlow 代码运行的默认模式。

你或许还记得,在 TensorFlow 1.x 中创建一个神经网络的时候,我们需要定义一个叫做图的抽象数据结构。同时,当我们尝试打印其中一个图节点的时候,我们只能看到这个图节点的引用值,而不能看到期待中的实际值。为了能够运行这个图,我们需要使用一个叫做 Session 的封装。我们通过方法 Session.run() 将 Python 数据传递到图,从而开始对模型的训练。

关于TensorFlow 2.0 你需要了解的一切

TensorFlow 1.x 代码示例

当使用 eager execution 的时候,上述情况就发生了改变。现在,TensorFlow 代码可以像普通 Python 代码那样运行。Eagerly 表示操作的创建和执行同时进行。

关于TensorFlow 2.0 你需要了解的一切

TensorFlow 2.0 代码示例

TensorFlow 2.0 的代码看起来非常像 NumPy 代码。实际上,TensorFlow 和 NumPy 的对象也可以很容易地相互置换。因此,你也不用为 placeholders、Sessions 以及 feed_dictionaties 等伤脑筋了。

API 清理

像 tf.gans、tf.app、tf.contrib 以及 tf.flags 等很多 API,要么被直接移除,要么就是转移到单独的库。

但是,其中最重要的一项清理同我们如何创建模型相关。你或许还记得,在 TensorFlow 1.x 中我们可以用不止一种方法来创建和训练 ML 模型。

在 TF 1.x 中,像 Tf.slim、tf.layers、tf.contrib.layers 以及 tf.keras 等 API 都可以用来创建神经网络,这还不包括 1.x 中 Sequence to Sequence API。而大多数时候,我们自己也不是很清楚应该在何种情况下使用何种 API。

尽管这些 API 大多数都有很不错的功能,但它们还是不能转换成一种通常的开发方式。如果我们使用了其中一种 API 来训练我们的模型,接下来就很难再切换到别的 API 了。

TensorFlow 2.0 中,tf.keras 是推荐使用的高级 API

正如我们所见,Keras API 正在试图解决所有的使用案例。

初级 API

从 TF 1.x 到 2.0,初级 API 并没有发生太大的改变。但现在,Keras 变成了默认的也是推荐使用的高级 API。简单来讲,Keras 是用来描述如何通过更清晰的标准来创建神经网络的一系列层。当我们通过 pip 来安装 TensorFlow 的时候,我们一般会得到完整的 Keras API 以及一些额外的函数工具集。

复制代码
model = tf.keras.models.Sequential()
model.add(Flatten(input_shape=(IMAGE_HEIGHT,IMAGE_WIDTH)))
model.add(Dense(units=32, activation='relu'))
model.add(Dropout(0.3))
model.add(Dense(units=32, activation='relu'))
model.add(Dense(units=10, activation='softmax'))
# Configures the model for training.
# Define the model optimizer, the loss function and the accuracy metrics
model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
model.summary()

初级 API 又被叫作 Sequential(序列化)API。它通过层堆栈来定义一个神经网络。除了简洁以外,它还拥很多其它的优点。注意,我们是按照数据结构(堆栈)的方式来定义模型。因此,这也减少了因为模型定义而出错的可能性。

Keras-Tuner

Keras-Tuner 是一个专门为 Keras 模型优化超参数的独立库。在本文写作的时候,该类库仍处于 pre-alpha 阶段,即便如此,它已经可以在 Colab 上很好地同 tf.keras 以及 TensorFlow2.0 beta 一起工作。

这是一个很简单的概念。首先,我们需要定义一个模型构建函数来返回一个编译好的 Keras 模型。这个函数采用了一个叫做 hp 的输入参数。通过 hp,我们能够定义一个可以用作超参数采样的候选值范围。

下面的代码创建了一个简单的模型并且优化 3 个以上的超参数。在隐藏模块中,我们对一个预定义范围里的整数值进行了取样。我们在一些指定值中进行随机选择,用来表示 dropout 率和学习率参数。

复制代码
def build_model(hp):
# define the hyper parameter ranges for the learning rate, dropout and hidden unit
hp_units = hp.Range('units', min_value=32, max_value=128, step=32)
hp_lr = hp.Choice('learning_rate', values=[1e-2, 1e-3, 1e-4])
hp_dropout = hp.Choice('dropout', values=[0.1,0.2,0.3])
# build a Sequential model
model = keras.Sequential()
model.add(Flatten(input_shape=(IMAGE_HEIGHT,IMAGE_WIDTH)))
model.add(Dense(units=hp_units, activation='relu'))
model.add(Dropout(hp_dropout))
model.add(Dense(units=32, activation='relu'))
model.add(layers.Dense(10, activation='softmax'))
# compile and return the model
model.compile(optimizer=keras.optimizers.Adam(hp_lr),
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
return model
# create a Random Search tuner
tuner = RandomSearch(
build_model,
objective='val_accuracy', # define the metric to be optimized over
max_trials=3,
executions_per_trial=1,
directory='my_logs') # define the output log/checkpoints folder
# start hyper-parameter optmization search
tuner.search(x_train, y_train,
epochs=2,
validation_data=(x_test, y_test))

然后,我们创建了一个 tuner 对象。本例中,我们实现了一个随机搜索策略。最后,我们通过 search() 方法来优化。这个方法具有和 fit() 一样的签名。

最后,我们可以通过检查调优结果汇总选出最好的模型。训练日志以及模型的 checkpoints 都被保存在 my_logs 目录下。同时,最小化或最大化目标(验证准确度)的选择也被自动推断出来。

更多内容请参阅此处

高级 API

当第一眼看到这种类型的实现时很容易让人联想到面向对象编程。这里,你的模型是一个基于 tf.keras.model 扩展的 Python 类。受 Chainer 启发,我们实现了模型子类化,这同 Pytorch 定义模型的方式非常相似。

通过模型子类化,我们在类的构建函数里定义了模型的层。call() 方法处理了正向传播的定义和执行。

复制代码
class Model(tf.keras.Model):
def __init__(self):
# Define the layers here
super(Model, self).__init__()
self.conv1 = Conv2D(filters=8, kernel_size=4, padding="same", strides=1, input_shape=(IMAGE_HEIGHT,IMAGE_WIDTH,IMAGE_DEPTH))
self.conv2 = Conv2D(filters=16, kernel_size=4, padding="same", strides=1)
self.pool = MaxPool2D(pool_size=2, strides=2, padding="same")
self.flat = Flatten()
self.probs = Dense(units=N_CLASSES, activation='softmax', name="output")
def call(self, x):
# Define the forward pass
net = self.conv1(x)
net = self.pool(net)
net = self.conv2(net)
net = self.pool(net)
net = self.flat(net)
net = self.probs(net)
return net
def compute_output_shape(self, input_shape):
# You need to override this function if you want to use the subclassed model
# as part of a functional-style model.
# Otherwise, this method is optional.
shape = tf.TensorShape(input_shape).as_list()
shape[-1] = self.num_classes
return tf.TensorShape(shape)

模型子类化具有很多优势。它可以更容易地实施模型检查。我们可以通过断点调试的方式,在指定代码行停留,并检查模型的激活函数或 logit 函数。

当然,灵活性也意味着更多的问题。

模型子类化需要程序员更小心,也需要程序员掌握更多的知识。

简而言之,你的代码更容易产生问题。

定义训练循环

在 TF 2.0 中,训练模型最简单的方法是使用 fit() 方法。fit() 同时支持序列化和子类化模型。在使用模型子类化的时候,我们唯一需要调整的就是重写类成员函数 compute_output_shape(),或者干脆放弃这个函数。除此以外,我们可以使用 tf.data.Dataset 或 NumPy 的标准 nd-arrays 来作为 fit() 的输入。

不过,如果你想要更清晰地了解梯度和损失函数运行机制的话,你可以使用梯度带。这对研究学者尤其有用。

通过梯度带,我们可以手动定义训练过程的每一步。训练一个神经网络的基本步骤如下:

  • 正向传播
  • 损失函数评估
  • 反向传播
  • 梯度下降

每一步都被单独定义。

当你想要进一步了解神经网络训练的时候,梯度带就变得很有指导意义。如果你需要检查不同模型权重的损失值或者梯度矢量本身的话,你可以直接把它们打印出来。

梯度带提供了很大的灵活性。但是正如子类化对比序列化一样,更好的灵活性也需要额外的代价。同 fit() 方法相比,我们在这里只需要手动定义一个训练循环。这也因此让代码更容易产生 bug 并且更难调试。对于那些追求标准化编程的编码人员来说,我相信这是一个很棒的折中,毕竟研究人员只是对开发新东西更感兴趣。

同时,我们也可以很容易地通过 fit() 来设置 TensorBoard,具体参看下文。

配置 TensorBoard

我们可以通过 fit() 方法很容易地配置一个 TensorBoard 实例。它同样适用于 Jupyter/Colab 笔记本。

在这里,我们将 TensorBoard 添加为 fit 方法的回调。

只要你使用 fit() 方法,这种方法就可以用于序列化以及子类化 API。

复制代码
Load the TensorBoard notebook extension
%load_ext tensorboard
# create the tensorboard callback
tensorboard = TensorBoard(log_dir='logs/{}'.format(time.time()), histogram_freq=1)
# train the model
model.fit(x=x_train,
y=y_train,
epochs=2,
validation_data=(x_test, y_test),
callbacks=[tensorboard])
# launch TensorBoard
%tensorboard --logdir logs

如果你选择使用模型子类化以及通过梯度带来编写训练循环的话,你同样需要手动定义 TensorBoard。手动定义包括创建 summary 文件、使用 tf.summary.create_file_writer() 以及指定你想要可视化的变量。

值得一提的是,TF 2.0 中有很多回调函数可供使用,下面是其中一些相对有用的函数:

  • EarlyStopping:如同字面含义,它设置了一条规则,当被检测的量化值停止改善时就结束训练。
  • ReduceLROnPlateau:当评估值停止改善的时候就降低学习率。
  • TerminateOnNaN:当遇到一个 NaN 损失的时候就回调结束训练。
  • LambdaCallback:一个创建样本的回调,自定义回调正在开发过程中。

你可以在这里查看 TensorFlow 2.0 中所有的回调函数。

提升 EagerCode 的性能

如果你选择使用梯度带来训练你的模型,你会注意到性能上会有一定程度的下降。

通过即时执行的方式运行 TF 代码的确容易理解,但是这会损失一些性能。为了避免这个问题,TensorFlow 2.0 引入了 tf.function。

基本上,如果你用 tf.function 装饰了你的 python 函数,你其实让 TensorFlow 接管了这个函数,并且转变成了一个 TF 的高性能抽象。

复制代码
@tf.function
def train_step(images, labels):
with tf.GradientTape() as tape:
# forward pass
predictions = model(images)
# compute the loss
loss = cross_entropy(tf.one_hot(labels, N_CLASSES), predictions)
# get the gradients w.r.t the model's weights
gradients = tape.gradient(loss, model.trainable_variables)
# perform a gradient descent step
optimizer.apply_gradients(zip(gradients, model.trainable_variables))
# accumulates the training loss and accuracy
train_loss(loss)
train_accuracy(labels, predictions)

这意味着该函数会被标记为即时(JIT)编译,从而 TensorFlow 可以把它按照图来运行。因此 TensorFlow 1.x 可以借助像节点剪枝或内核融合等方式来获得更好的性能。

总之,TensorFlow 2.0 可以允许你将代码分解为更小的函数。然后你可以标注那些你想使用 tf.function 的函数,从而获取额外的性能。最后装饰那些具有最大计算瓶颈的函数。这些函数通常是训练循环或模型的正向传播。

注意,在你装饰函数的时候,你同样损失了 eager execution 的一些优势。换句话说,你不再能够设置断点或者使用代码片段中的 print() 函数。

模型保存和恢复

TensorFlow 1.x 中缺少的另外一项标准就是我们如何为产品保存和装载模型。TensorFlow 2.0 试图通过定义一个单一 API 来解决这个问题。

TF 2.0 并没有采用保存模型的很多方法,它反而标准化了一个叫做 SavedModel 的抽象。

这很容易理解。如果你构建了一个序列化模型或者使用 tf.keras.Model 扩展了你的类,那么你的类就继承了 tf.train.Checkpoints。因此,你可以将你的模型序列化为 SavedModel 对象。

复制代码
# serialize your model to a SavedModel object
# It includes the entire graph, all variables and weights
model.save('/tmp/model', save_format='tf')
# load your saved model
model = tf.keras.models.load_model('/tmp/model')

TensorFlow 生态系统整合了 SavedModels,也就是说,你可以将它部署到很多设备中,诸如手机、边缘设备以及服务器等。

关于TensorFlow 2.0 你需要了解的一切

TF-Lite 转化

如果你想部署 SaveModel 到一个像 Raspberry Pi、Edge TPU 或手机这样的嵌入式设备中的话,你需要一个 TF Lite 转换器。

在 2.0 中,TFLiteConverter 不支持冻结 GraphDefas。如果你想把一个冻结的 GraphDefas 转化到 TF 2.0 中,你可以使用 tf.compat.v1.TFLiteConveter.

在部署到嵌入式设备之前经常会执行一个后训练量化。使用 TFLiteConveter 时,只需要将优化标志位设定为“OPTIMIZE_FOR_SIZE”即可。这会将模型的权重从浮点型量化为 8 位精度,从而降低了模型的大小,也在模型精确度几乎没有损失的情况下改善了延迟。

复制代码
# create a TensorFlow Lite converter
converter = tf.lite.TensorFlowLiteConverter.from_keras_model(model)
# performs model quantization to reduce the size of the model and improve latency
converter.optimizations = [tf.lite.Optimize.OPTIMIZE_FOR_SIZE]
tflite_model = converter.convert()

这只是一个实验中的标志位,未来仍有可能改变。

TensorFLow.js 转化

我们可以使用同样的 SavedModel 对象,并将其转化为 TensorFlow.js 格式。然后,我们可以使用 JavaScript 来加载,从而在浏览器中运行我们的模型。

复制代码
!tensorflowjs_converter \
--input_format=tf_saved_model \
--saved_model_tags=serve \
--output_format=tfjs_graph_model \
/tmp/model \
/tmp/web_model
view raw

首先,我们需要通过 pip 安装 TensorFlow.js。然后,使用 tensorflowjs_converter 脚本来获取训练模型,并将其转化为 JavaScript 兼容的代码。最后,我们可以加载这些代码并通过 JavaScript 实现推断。

另外,我们也可以在浏览器中通过 TensorFlow.js 来训练模型。

结语

最后,作者介绍了 TF 2.0 的其它一些特性。首先可以更直观地为序列化或子类化模型添加更多的层。尽管 TF 包含了绝大多数比较流行的层,如 Conv2D 以及 TransposeConv2D 等,但是很多时候我们仍然会发现这还远远不够,尤其是在重复一些论文实验或者是做研究的时候。

好消息是,我们可以开发我们自己的自定义层。通过一些 Keras API,我们可以创造一个类,然后通过 tf.keras.Layer 扩展。实际上,我们可以使用一个非常类似的模式来创建自定义的激活函数、标准化层或者是评估函数。更多内容详见此处

我们也可以将 1.x 的代码转化为 2.0。TF 团队为此开发了 tf_upgrade_v2 工具集。

这个脚本并不是从根本上将 TF 1.x 转化为 2.0,它只是很简单地利用了 tf.compat.v1 模型,更改了函数的命名空间。同时,如果你的遗留代码使用了 tf.contrib 的话,该脚本同样不能使用。你因此只能使用一些其它的类库或者是这些函数的 2.0 版本实现。

原文链接
Everything you need to know about TensorFlow 2.0

评论

发布
用户头像
我在想是不是java spring boot是否可以直接load模型,不用远程调用grpc的tf服务了?
2019 年 07 月 17 日 09:13
回复
没有更多了