划重点! TensorFlow 2.0 中的符号和命令式 API
文 / Josh Gordon, Google Developer Advocate
关于 TensorFlow 2.0, 我最喜欢的一点是它提供了多个抽象级别,因此您可以为您的项目选择合适的抽象级别。在本文中,我将为您解释用来创建神经网络的两种样式之间的利弊权衡。第一种是符号样式,通过操作层形成的图 (graph of layers) 来构建模型。第二种是命令式样式,通过扩展类来构建模型。我将介绍这些样式并讨论重要的设计和可用性权衡。我将详细介绍技术细节,并提供快速建议以帮助您为目标选择合适的方法。
符号式(或声明的)API
通常我们会用 “层形成的图” 来想象神经网络,如下图所示。
通常我们会用 “层形成的图” 来想象神经网络 ( 这些图片是用于初始化 Inception-ResNet 的模式 )
这种图可以是左侧显示的 DAG ( 有向无环图 ),也可以是右侧显示的堆栈。当我们符号化地构建模型时,我们通过描述该图的结构来实现。
这听起来很技术性,那么如果你使用了 Keras,你可能会惊讶地发现你已经有过这样的经验了。以下是使用 Keras Sequential API 以符号样式构建模型的快速示例。
使用 Keras Sequential API 符号化构建的神经网络。你可以在
https://colab.research.google.com/github/tensorflow/docs/blob/master/site/en/tutorials/_index.ipynb
运行这个例子
在上面的示例中,我们已经定义了一堆图层,然后使用内置的训练循环 model.fit 来训练它。
使用 Keras 构建模型就像 “把乐高积木拼在一起” 一样简单。为什么这样说呢?我们后面将介绍其中的技术原因,以这种方式定义网络,除了符合我们的想象之外,更易于调试,它可以通过尽早捕获详细的错误信息从而进行调试,以便及早的发现错误。
图中显示了上面代码创建的模型(使用 plot_model 构建,您可以在本文的下一个示例中重用代码片段)
TensorFlow 2.0 提供了另一种符号模型构建 API:Keras Functional。Sequential 用于堆栈,而 Functional 用于 DAG ( 有向无环图 )。
使用 Functional API 创建多输入 / 多输出模型的快速示例
Functional API 是一种创建更灵活模型的方法。它可以处理非线性拓扑 (non-linear topology),具有共享层的模型以及具有多个输入或输出的模型。基本上,Functional API 是一组用于构建这些层形成的图的工具。现在我们为您准备了几种新的教程。
您可能会遇到其他符号式 API。例如,TensorFlow v1(和 Theano)提供了更低级别的 API。您可以通过创建一个由 ops(操作)组成的图来构建模型,然后对其进行编译和执行。有时,使用此 API 会让你感觉就像直接与编译器进行交互一样。对于许多人(包括作者)而言,这是很不简单的。
相比之下,在 Keras 中,抽象的水平是与我们想象的方式相匹配的:由层构成的图,像乐高积木一样叠在一起。这感觉很自然,这是我们在 TensorFlow 2.0 中标准化的模型构建方法之一。还有一个方法我将要为你描述(你很有可能也用过这个,也许很快你就有机会试一试)。
命令式(或模型子类)API
在命令式风格中,您可以像编写 NumPy 一样编写模型。以这种方式构建模型就像面向对象的 Python 开发一样。下面是一个子类化模型的简单示例:
使用命令式样式来构建一个带有注意 图像字幕 的模型(注意:此示例目前正在更新)(https://github.com/tensorflow/docs/blob/master/site/en/r2/tutorials/generative/image_captioning.ipynb)
从开发人员的角度来看,它的工作方式是扩展框架定义的 Model 类,实例化图层,然后命令性地编写模型的正向传递(反向传递会自动生成)。
TensorFlow 2.0 支持 Keras Subclassing API 开箱即用。与 Sequential 和 Functional API 一起,它也是在 TensorFlow 2.0 中开发模型的推荐方法之一。
虽然这种风格对于 TensorFlow 来说是全新的,但是您可能会惊讶地发现它是由 Chainer 在 2015 年推出的(时间过得真快!)。从那时起,许多框架采用了类似的方法,包括 Gluon,PyTorch 和 TensorFlow(使用 Keras Subclassing)。令人惊讶的是,在不同框架中以这种风格编写的代码可能会看起来如此相似,甚至很难区分!
这种风格为您提供了极大的灵活性,但它的可用性和维护成本并不明显。关于这一点,我们稍后会详细介绍。
训练循环
以 Sequential,Functional 或 Subclassing 样式定义的模型可以通过两种方式进行训练。您可以使用内置的训练例程和损失函数(请参阅第一个示例,我们使用 model.fit 和 model.compile),或者如果您需要增加自定义训练循环的复杂性(例如,如果您喜欢编写自己的梯度裁剪代码)或损失函数,您可以轻松完成如下操作:
Pix2Pix 的自定义训练循环和损失功能的示例
这两种方法都很重要,并且可以方便地降低代码复杂性和维护成本。基本上,您可以在有需要的时候使用额外的复杂性,当不必要的时候,使用内置的方法把时间花在您的研究或项目上。
既然我们已经对符号样式和命令样式有了一定的了解,那就让我们来看看折中方案。
符号式 API 的优点和局限性
优点
使用符号化 API,您的模型是一个类似图的数据结构。这意味着可以对您的模型进行检查或汇总。
您可以将其绘制为图像以显示图(使用 keras.utils.plot_model),或者直接使用 model.summary(),或者参见图层,权重和形状的描述来显示图形
同样,在将图层连接在一起时,库设计人员可以运行广泛的图层兼容性检查(在构建模型时和执行之前)。
这类似于编译器中的类型检查,可以大大减少开发人员错误
大多数调试将在模型定义阶段进行,而不是在执行期间进行。这样您可以保证任何编译的模型都会运行。可以加快迭代速度,并使调试更容易
符号模型提供了一致的 API。 这使得它们易于重用和共享。例如,在迁移学习中,您可以访问中间层激活来从现有的模型中构建新模型,如下所示:
```
from tensorflow.keras.applications.vgg19 import VGG19
base = VGG19(weights='imagenet')
model = Model(inputs=base.input,
outputs=base_model.get_layer('block4_pool').output)
image = load('elephant.png')
block4_pool_features = model.predict(image)
```
符号模型由一种数据结构定义的,这种数据结构使得它们可以自然地复制或克隆。
例如,Sequential 和 Functional API 为您提供 model.get_config(),model.to_json(),model.save(),clone_model (model),能够在数据结构中重新创建相同的模型 ( 无需使用原始代码来定义和训练模型 )
虽然一个设计良好的 API 应该与我们想象中的神经网络相匹配,但同样重要的是符合我们作为程序员的想象方式。对于我们许多人来说,这是一种命令式的编程风格。在符号化 API 中,您正在操作 “符号张量”(这些是尚未保留任何值的张量)来构建图。Keras Sequential 和 Functional API “感觉” 势在必行。它们的设计使许多开发人员没有意识到他们已经象征性地定义了模型。
局限性
当前的符号 API 最适合开发层的有向无环图模型。这在实践中占了大多数用例,尽管有一些特殊的用例不适合这种简洁的抽象,例如,动态网络(如树状神经网络)和递归网络。
这就是为什么 TensorFlow 还提供了一种命令式的模型构建 API 风格(Keras Subclassing,如上所示)。 您可以使用 Sequential 和 Functional API 中所有熟悉的层,初始化器和优化器。这两种样式也是完全可互操作的,因此您可以混合搭配(例如,您可以将一种模型类型嵌套在另一种模型类型中)。您可以将符号模型用作子类模型中的一个层,或者相反。
命令式 API 的优点和局限性
优点
您的正向传递是命令式编写的,你可以很容易地将库实现的部分(例如,图层,激活或损失函数)与您自己的实现交换掉。这对于编程来说是很自然的,并且是深入了解深度学习的一个好方法。
这使得快速尝试新想法变得容易(DL 开发工作流程变得与面向对象的 Python 相同),对研究人员尤其有用
使用 Python 在模型的正向传递中指定任意控制流也很容易
命令式 API 为您提供了最大的灵活性,但是这是有代价的。我也喜欢用这种风格编写代码,但是我想花点时间强调一下这种风格的局限性(了解其中的利弊是很好的)。
局限性
重要的是,在使用命令式 API 时,您的模型由类方法的主体定义的。您的模型不再是透明的数据结构,它是一段不透明的字节码。在使用这种风格时,您需要牺牲可用性和可重用性来获得灵活性。
在执行期间进行调试,而不是在定义模型时进行调试。
输入或层间兼容性几乎没有被检查到,因此在使用此样式时,很多调试负担从框架转移到开发人员
命令式模型可能更难以重用。例如,您无法使用一致的 API 访问中间图层或激活。
相反,提取激活的方法是使用新的调用(或 forward)方法编写新类。一开始写起来可能很有趣,做起来也很简单,但这可能会导致没有标准的 tech debt
命令模型也更难以检查,复制或克隆。
例如,model.save(),model.get_config() 和 clone_model 不适合用于子类模型。同样,model.summary() 只提供一个图层列表(并不提供有关它们如何连接的信息,因为它不可访问)
ML 系统中的 Technical Debt
重要的是要记住,模型构建只是在实践中使用机器学习的一小部分。这是我最喜欢的一部分。模型本身(代码中指定层、训练循环等部分)是中间的小盒子。
如图所示,只有一小部分真实 ML 系统由 ML 代码组成
由中间的小黑匣子进行。避免机器学习系统中隐藏的 Technical Debt
符号定义的模型在可重用性,调试和测试方面具有优势。例如,在教学时 — 如果他们使用的是 Sequential API,我可以立即调试学生的代码。当他们使用子类模型(不管框架是什么)时,它需要更长的时间(bug 可能更微妙,并且有许多类型)。
结论
TensorFlow 2.0 支持这两种开箱即用的样式,因此您可以为您的项目选择合适的抽象级别(和复杂性)。
如果您的目标是易用性,低概念开销 (low conceptual overhead),并且您希望将模型视为层构成的图:使用 Keras Sequential 或 Functional API(如将乐高积木拼在一起)和内置的训练循环。这是解决大多数问题的正确方法
如果您希望将模型视为面向对象的 Python / Numpy 开发人员,并且优先考虑灵活性和可编程性而不是易用性(以及易于重用),Keras Subclassing 是适合您的 API
希望本文对您有所帮助,要了解有关 TensorFlow 2.0 stack 的更多信息,除了这些模型构建 API 之外,请查看 TensorFlow 2.0 的新功能。要了解有关 TensorFlow 和 Keras 之间关系的更多信息,请点击 这里!
更多 AI 相关阅读: