
2.3 TensorFlow 2和Keras详述
我们介绍了TensorFlow的一般架构,并使用Keras训练了第一个模型。现在,我们来看TensorFlow 2的主要概念。我们将详细介绍TensorFlow的几个贯穿全书的核心概念,然后再介绍一些高级概念。虽然本书的其余部分中可能并未全部使用它们,但是读者可能会发现这对于理解GitHub上提供的一些开源模型或对库进行更深入的了解是很有用的。
2.3.1 核心概念
该框架的新版本于2019年春发布,它致力于提高简单性和易用性。本节将介绍TensorFlow所依赖的概念,并介绍它们是如何从版本1演变到版本2的。
张量
TensorFlow的名称来自名为张量(tensor)的数学对象。你可以将张量描绘为N维数组。张量可以是标量、向量、三维矩阵或N维矩阵。
作为TensorFlow的基本组件,Tensor对象用于存储数学上的值。它可以包含固定值(使用tf.constant创建)或变量值(使用tf.Variable创建)。
在本书中,tensor表示数学概念,而Tensor(字母T大写)对应于TensorFlow对象。
每个Tensor对象具有:
·类型(Type):string、float32、float16或int8等数据类型。
·形状(Shape):数据的维度。例如,对于标量,形状为();对于大小为n的向量,形状为(n);对于大小为n×m的二维矩阵,形状为(n,m)。
·阶(Rank):维数,标量为0阶,向量为1阶,二维矩阵为2阶。
一些Tensor可以具有部分未知的形状。例如,接受可变大小图像的模型的输入形状可以为(None,None,3)。由于图像的高度和宽度事先是未知的,因此前两个维度设置为None。但是,通道数(3,对应于红色、蓝色和绿色)是已知的,因此已设置。
TensorFlow图
TensorFlow使用Tensor作为输入和输出。将输入转换为输出的组件称为操作。因此,计算机视觉模型由多个操作组成。
TensorFlow使用有向无环图(Directed Acyclic Graph,DAG)表示这些操作,DAG也称为图。在TensorFlow 2中,没有了图操作,框架更易于使用。尽管如此,图的概念对于理解TensorFlow的真正工作原理仍然很重要。
使用Keras构建前面的示例时,TensorFlow实际上构建了一个图,如图2-3所示。

图2-3 与示例模型相对应的简化图。实际上,每个节点都由更小的操作
(例如矩阵乘法和加法)组成尽管非常简单,但是此图以操作的形式表示了模型的不同层。依赖图有许多优点,允许TensorFlow执行以下操作:
·在CPU上运行部分操作,在GPU上运行另一部分操作。
·在分布式模型的情况下,在不同的机器上运行图的不同部分。
·优化图以避免不必要的操作,从而获得更好的计算性能。
此外,图的概念允许TensorFlow模型是可移植的。单个图定义可以在任何类型的设备上运行。
在TensorFlow 2中,图的创建不再由用户处理。虽然在TensorFlow 1中管理图曾经是一项复杂的任务,但新版本大大提高了可用性,同时仍保持了性能。下一节将深入探讨TensorFlow的内部工作原理,并简要地说明如何创建图。
延迟执行和即时执行比较
TensorFlow 2中的主要变化是即时执行(eager execution)。历史上,TensorFlow 1在默认情况下总是使用延迟执行(lazy execution)。它之所以称为延迟,是因为直到被明确请求,操作才由框架运行。
我们用一个非常简单的示例(即将两个向量的值相加)来说明延迟执行和即时执行之间的区别:

请注意,由于TensorFlow重载了许多Python操作符,因此tf.add(a,b)可以使用a+b代替。
前面代码的输出取决于TensorFlow的版本。使用TensorFlow 1(默认模式为延迟执行)时,输出为:

但是,使用TensorFlow 2(默认模式是即时执行)时,将获得以下输出:

在两种情况下,输出均为Tensor。在第二种情况下,该操作已被立即执行,可以直接看到Tensor包含结果([1 2 4])。在第一种情况下,Tensor包含有关加法操作(Add:0)的信息,但不包含操作结果。
在即时执行模式下,可以通过调用.numpy()方法来访问Tensor的值。在本示例中,调用c.numpy()会返回[1 2 4](作为一个NumPy数组)。
在TensorFlow 1中,将需要更多代码来计算结果,因而使开发过程更加复杂。即时执行使代码更易于调试(因为开发人员可以随时调看Tensor的值),也更易于开发。下一节将详细介绍TensorFlow的内部工作原理以及如何构建图。
在TensorFlow 2中创建图
我们从一个简单的示例开始,来说明图的创建和优化:


假设a、b和c是张量矩阵,上面的代码将计算两个新值:d和e。使用即时执行,TensorFlow将计算d的值,然后计算e的值。
使用延迟执行,TensorFlow将创建操作图。在运行图获取结果之前,将运行图优化器。为避免计算两次a*b,优化器将缓存结果并在必要时重用它。对于更复杂的操作,优化器可以启用并行性以使计算速度更快。当运行大型模型和复杂模型时,这两种技术都很重要。
正如我们所看到的,以即时执行模式运行意味着每个操作在定义时都会运行。因此,无法应用这种优化。幸运的是,TensorFlow包含一个可解决该问题的模块,即TensorFlow AutoGraph。
TensorFlow AutoGraph和tf.function
TensorFlow AutoGraph模块可轻松将即时执行的代码转换为图,从而实现自动优化。为此,最简单的方法是在函数顶部添加tf.function装饰器:

Python装饰器是一个允许对函数包装,添加或更改函数功能的概念。装饰器以@开头。
当第一次调用compute函数时,TensorFlow将透明地创建如图2-4所示的图。

图2-4 首次调用compute函数时TensorFlow自动生成的图
TensorFlow AutoGraph可以转换大多数Python语句,例如for循环、while循环、if语句或迭代。得益于图优化,图执行有时会比即时执行的代码更快。一般而言,在以下场景下应使用AutoGraph:
·当需要向其他设备导出模型时。
·当性能最优先时,图优化可以提高速度。
图的另一个优点是它们可以自动微分。知道操作的完整列表后,TensorFlow很容易计算每个变量的梯度。
注意,要计算梯度,操作需要是可微的。其中一些操作不是,例如tf.math.argmax。在损失中使用它们很可能会导致自动微分失败。用户应确保损失是可微的。
但是,由于在即时执行模式下每个操作彼此独立,因此默认情况下不可能进行自动微分。值得庆幸的是,TensorFlow 2提供了一种在仍使用即时执行模式的同时进行自动微分的方法——梯度带。
基于梯度带的反向传播误差
梯度带允许在即时执行模式下轻松进行反向传播。为了说明这一点,我们将使用一个简单的示例。假设要求解方程A×X=B,其中A和B是常数。我们想找到X的值求解等式。为此,我们将尝试最小化一个简单的损失,abs(A×X-B)。
代码如下:

现在,为更新X的值,我们要计算损失相对于X的梯度。但是,在打印损失内容时,获得了以下信息:

在即时执行模式下,TensorFlow会计算操作的结果,而不是存储操作!没有操作及其输入的信息,就不可能对损失操作进行自动微分。
此时梯度带将派上用场。通过在tf.GradientTape上下文中运行损失计算,TensorFlow将自动记录所有操作,并允许随后反向回放它们:


上面的代码定义了单个训练步骤。每次调用train_step时,都会在梯度带上下文中计算损失。然后,将该上下文用于计算梯度。之后更新变量X。实际上,我们可以看到X向方程的解收敛:

你会注意到,在本章的第一个示例中,我们没有使用梯度带。这是因为Keras模型将训练封装在.fit()函数中,无须手动更新变量。但是,对于创新的模型或在实验时,梯度带是一种功能强大的工具,无须太多工作即可自动进行微分。读者可以在第3章的正则化notebook中,找到对梯度带的更实际使用。
Keras模型和层
在本章的第一节中,我们构建了一个简单的Keras序列模型。生成的Model对象包含许多有用的方法和属性:
·.inputs和.outputs:提供对模型输入和输出的访问。
·.layers:列出模型的层及其形状。
·.summary():打印模型的架构。
·.save():保存模型、它的架构和当前训练状态。对以后恢复训练是非常有用的。可以使用tf.keras.models.load_model()从文件实例化模型。
·.save_weights():只保存模型的权重。
虽然只有一种类型的Keras模型对象,但是它们可以用不同的方式来构建。
顺序式和函数式API
与在本章开头那样使用顺序式API不同,你可以使用函数式API:

请注意,该代码比以前的代码稍长。不过,函数式API比顺序式API更具通用性和表现力。前者允许使用分支模型(例如,用于构建具有多个并行层的架构),而后者只能用于线性模型。为了得到更大的灵活性,Keras还提供了对Model类进行子类化的可能性,如第3章中所述。
不管Model对象如何构建,它都是由层组成的。层可以被视为接受一个或多个输入并返回一个或多个输出的节点,类似于TensorFlow操作。可以使用.get_weights()访问其权重,并使用.set_weights()设置权重。Keras为最常见的深度学习操作提供了预制的层。对于更创新或更复杂的模型,也可以将tf.keras.layers.Layer子类化。
回调函数
Keras回调是实用函数,可以将其传递给Keras模型的.fit()方法,以向其默认行为添加功能。可以定义多个回调,在每次批处理迭代、每个轮次或整个训练过程之前或之后,由Keras调用这些回调。预定义的Keras回调包括:
·CSVLogger:将训练信息记录在CSV文件中。
·EarlyStopping:如果损失或指标停止改进,则停止训练。有助于避免过拟合。
·LearningRateScheduler:根据规划更改每轮的学习率。
·ReduceLROnPlateau:当损失或指标停止改进时,自动降低学习率。
还可以通过将tf.keras.callbacks.Callback子类化来创建自定义回调,如下一章及其代码示例所示。
2.3.2 高级概念
总之,AutoGraph模块、tf.function装饰器和梯度带上下文使图的创建和管理非常简单(如果不是不可见的话)。然而,很多复杂性对用户而言是隐藏的。本节将详细介绍这些模块的内部工作原理。
本节介绍了本书中不必要的高级概念,但是对于读者理解更复杂的TensorFlow代码很有用。比较心急的读者可以跳过这一部分,稍后再回头阅读。
tf.function的工作原理
如前所述,当首次调用以tf.function装饰的函数时,TensorFlow将创建与该函数的操作相对应的图。然后,TensorFlow将缓存图,以便下次调用该函数时无须再创建图。
为了说明这一点,我们创建一个简单的identity函数:

每当TensorFlow创建与其操作相对应的图时,此函数将打印一条消息。在这种情况下,由于TensorFlow正在缓存图,因此它将仅在第一次运行时打印一些内容:

但是,请注意,如果更改输入类型,TensorFlow将重新创建图:

TensorFlow图由其操作以及作为它们输入而接收的张量形状和类型来定义,这一事实可以解释这种行为。因此,当输入类型更改时,需要创建一个新图。在TensorFlow词汇表中,当tf.function定义了输入类型时,它将成为一个具体的函数。
总而言之,每次第一次运行装饰的函数时,TensorFlow都会缓存与输入类型和输入形状相对应的图。如果函数使用不同类型的输入来运行,TensorFlow将创建一个新的图并将其缓存。
但是,每次运行一个具体的函数时(而不仅仅是第一次运行时)都记录信息可能会很有用。为此,请使用tf.print:

这个函数不再只在第一次运行时打印信息,而是在每次运行时都打印“Running identity”。
TensorFlow 2中的变量
为了保持模型权重,TensorFlow使用Variable实例。在Keras示例中,我们可以通过访问model.variables来列出模型的内容。它将返回模型中包含的所有变量的列表:

在我们的示例中,变量管理(包括命名)已完全由Keras处理。如前所述,还可以创建自己的变量:

请注意,对于大型项目,建议命名变量以使代码清晰易懂并简化调试。若要更改Variable的值,请使用Variable.assign方法:

不使用.assign()方法,将创建一个新的Tensor:

最后,删除对Variable的Python引用将从活动内存中删除该对象本身,从而释放空间来创建其他变量。
分布式策略
在非常小的数据集上只能训练简单的模型。使用较大的模型和数据集时,需要更多的计算能力,这通常意味着需要多个服务器。tf.distribute.Strategy API定义了多台计算机如何互相通信以有效地训练模型。
TensorFlow定义的一些策略如下:
·MirroredStrategy:用于在一台计算机的多个GPU上进行训练。模型权重在每个设备之间保持同步。
·MultiWorkerMirroredStrategy:与MirroredStrategy类似,但用于在多台计算机上进行训练。
·ParameterServerStrategy:用于在多台计算机上进行训练。不是在每个设备上同步权重,而是将它们保存在参数服务器上。
·TPUStrategy:用于在Google的Tensor处理单元(Tensor Processing Unit,TPU)芯片上进行训练。
TPU是Google制造的类似于GPU的定制化芯片,专门设计用于运行神经网络计算。可通过Google Cloud使用。
要使用分布式策略,请在其作用域内创建和编译模型:

请注意,你可能必须增加批(batch)大小,因为每个设备现在将收到每个批的一小部分。根据模型,可能还必须更改学习率。
使用评估器API
在本章的第一部分,我们看到评估器API是Keras API的高级替代方案。评估器简化了训练、评估、预测和服务。
评估器有两种类型。预制评估器是TensorFlow提供的非常简单的模型,可让你快速尝试机器学习架构。第二种类型是自定义评估器,可以使用任何模型架构来创建。
评估器处理模型生命周期的所有小细节,如数据队列、异常处理、故障恢复、定期检查点,等等。虽然在TensorFlow 1中将使用评估器视为最佳实践,但在TensorFlow 2中,建议使用Keras API。
可用的预制评估器
在撰写本书时,可用的预制评估器有DNNClassifier、DNNRegressor、Linear-Classifier和LinearRegressor。DNN代表深度神经网络。还提供了基于两种架构的组合评估器DNNLinearCombinedClassifier和DNNLinearCombinedRegressor。
在机器学习中,分类(classification)是预测离散类别的过程,而回归(regression)是预测连续数字的过程。
组合评估器也称为Deep-n-wide模型,利用了线性模型(用于记忆)和深度模型(用于泛化)。它们主要用于推荐或排名模型。
预制评估器适用于某些机器学习问题。然而,它们不适合计算机视觉问题,因为不存在带有卷积的预制评估器,卷积是下一章中要介绍的一种强大的层类型。
训练自定义评估器
创建评估器的最简单方法是转换Keras模型。编译模型后,调用tf.keras.estimator.model_to_estimator():

model_dir参数允许你指定保存模型检查点的位置。如前所述,评估器将自动保存模型检查点。
训练评估器需要使用输入函数(一种以特定格式返回数据的函数)。一种可接受的格式是TensorFlow数据集。在第7章中,将对数据集API进行详细描述。现在,我们定义以下函数,让该函数以正确的格式、每批32个样本返回本章第一节中所定义的数据集:

定义该函数后,就可以使用评估器启动训练:

就像Keras一样,训练部分非常简单,因为评估器可以处理繁重的工作。