在 TF 2 中高度提升了Eager execution。它使编码和调试更容易。 但这并不一定建议用于真正的培训或生产。 在本文中,我们将讨论两种选择:Eager Execution 模式和图形模式,以及它们的优缺点。 特别是,在图形模式下运行代码并不像预期的那样无缝。 如果忽略某些问题,它可能会对性能产生重大影响。 默认情况下,2.x 中的 TF 操作以Eager execution模式运行。 例如,下面的 tf.matmul(矩阵乘法)立即执行并返回一个包含结果 [[4.]] 的 tf.Tensor 对象。 这是我们在运行 Python 代码时通常所期望的。 代码逐行执行,计算结果立即返回。 然而,图形模式描绘了不同的画面。 相反, tf.matmul 返回计算图中节点的符号句柄。 乘法执行被推迟。 Eager execution的缺点 在图形模式下,tf.matmul 将计算节点 (tf.Operation) 添加到计算图 (tf.Graph) 上。在 TF v1 API 中,我们稍后调用 session.run 来编译和执行计算图。这种延迟执行允许 TF Grappler 在后台自动运行。它应用一长串图形优化来提高执行性能。例如,为了提高效率,可以组合或删除节点操作。为了利用 2.x 中的这种优化,我们需要以图形模式运行代码,而不是Eager execution。 TF 内部基准测试表明平均性能提高了 15%。但对于 ResNet50 等计算量大的模型,带有 GPU 的 Eager Execution 模式与图形模式相当。但是当有很多小操作时,这个差距会增加。它会随着 CNN 等昂贵操作的减少而减少。实际上,您的里程因型号而异。 更改代码以在图形模式下运行非常容易。我们只需使用 @tf.fction 注释一个函数,以便整个函数将被编译、优化并作为单个计算图运行。不需要额外的代码。并且@tf.function 将其覆盖范围扩展到它调用的所有方法并创建一个图形。 图形模式根据 Python 代码创建数据流图。该图创建了一个可移植的解决方案。我们可以在没有原始 Python 代码的情况下恢复模型,或者将其部署到没有 Python 环境的设备中。实际上,这是使用 SavedModel 将模型保存到文件所必需的。这种可移植性在生产部署中具有很大的优势。通过导出包含数据预处理的 SavedModel,我们消除了在生产中重新创建数据预处理逻辑时可能出现的错误。这些数据预处理逻辑可能对训练数据很敏感。例如,TextVectorization 层需要由训练数据集的原始词汇表进行初始化。这在部署过程中很容易出错,特别是对于 NLP 问题。 Graph Mode Catches 但是,图形模式有一个主要问题。使用 Eager Execution(作为 TF 2 的默认设置)的关键原因是使编码和调试更容易。 TF 1 API 繁琐且难以调试。在图形模式下,tf.matmul 将节点添加到计算图中,而不是立即返回计算结果。图形模式不允许调试器在 tf.matmul 在 TF 2.x 中的断点处停止。很难追踪代码。 所以在早期的开发和调试过程中,我们可能会暂时将注解注释掉。或者我们可以使用 tf.config.run_functions_eagerly(True) 来开启 Eager Execution。通过在下面的 square 函数之前设置 True 并在之后设置 False,我们可以在 @tf.function 方法内部进行中断。 通过 Eager Execution,我们可以使用常规的 Python 构造来编写我们的逻辑。 这使得代码 Python 友好并且更易于阅读。 但是要在图形模式下使用相同的代码,我们需要将这些构造转换为图形的一部分。 AutoGraph(稍后讨论)会自动将一些 Python 流控制(if、while 和 for 循环)转换为 TF 操作。 但话虽如此,还是有一些不合常理的事情,没有容易理解的理性或规则。 这些违规行为可能会导致异常,或者这些构造将被简单地忽略。 否则,可能会产生意想不到的副作用。 在接下来的几节中,我们将讨论在图形模式下可能遇到的问题。 Not to use Assert 例如,计算图中根本不支持某些 Python 结构。 例如,@tf.function 函数中的 Python “Assert”将引发异常。 使用 tf.debugging.assert_{condition} 代替这两种模式。 Trace 但是Python和TF代码是如何在graph模式下转化为graph的。当第一次调用带注释的 tf.function 方法时,将首先对其进行跟踪以将函数转换为计算图。从概念上讲,该函数被编译成一个图形。在这个过程中,TF 操作会转化为图中的节点。图表完成后,它会自动执行。这不是完整的图片,但让我们先用一个非常简单的例子来说明它。 print v.s. tf.print Python “print” 将其参数打印到控制台。但是当它在 @tf.function 方法中时,它仅在跟踪期间执行 - 图形编译阶段。它不会向图中添加任何节点。因此,此操作被称为 Python 副作用,因为它在跟踪期间产生影响,但在图形执行期间没有影响。相反,tf.print 是一个 TF 操作。它在跟踪中向图中添加一个节点,而不向控制台输出任何输出。执行图表时,它会打印到控制台。出于这个原因,我们使用这些操作来排查和区分跟踪和执行阶段。 在跟踪期间,方法 f 中的操作会被执行,即使它的目的是编译一个图。我们可以将这些操作分为 Python 操作和 TF 操作。当然,每个操作都是由 Python 执行的。但是 TF 操作并不执行真正的操作。它只是将节点添加到图中。感谢@tf.function 注释,一旦跟踪完成,图形就会自动执行。让我们用一个例子来说明它。 (我在调试器中逐行运行代码,因此显示将遵循代码的chronicle顺序。否则,输出语句可能会稍微乱序显示。) 在第 23 行第一次调用 f(1) 时,它将首先跟踪该方法以构建图形。 在跟踪中,打印输出①和 tf.print 只在图中添加一个节点。 创建图表后,将执行该图表。 图中没有“print”,并且在 tf.print 输出②时什么也不产生。 所以第 23 行输出控制台中的前 2 行,用于两个不同的阶段。 当我们在第 25 行再次调用 f(1) 时,该方法已经被跟踪,并且可以重用该图。 所以直接进入graph执行,只打印出③。 AutoGraph (tf.autograph) Eager Execution允许我们使用 Python 控制流,如“while”、“for”、“if”、“break”和“continue”。 为了使其适用于图形模式,AutoGraph 将其中一些 Python 流控制自动转换为 TF 操作。 因此它们将被视为 TF 操作而不是 Python 操作。 这允许我们为两种模式重用更自然的 Python 控制语法。 下面是通过 AutoGraph 将 Python 流转换为 TF 操作的示例。 AutoGraph 还将 for 循环中数据集的迭代转换为 TF 操作。 在“if”语句中跟踪 如果“while”或“if”中的条件是张量,则将进行这些转换。 下面的跟踪过程中发生的事情可能会让您感到惊讶。 n 是一个张量,因此“if n==0”语句将被转换为等效的 TF 操作 tf.cond。 但是,为什么上面有三个痕迹打印输出(①②和③)。 为了让计算图能够处理输入张量的不同值,TF 实际上会跟踪所有分支。 因此,所有三个分支都被调用,每个分支打印出一个输出。 这种机制有助于我们重用跟踪。 当我们使用相同形状的张量对 f 进行第二次调用时,不需要跟踪。 该图已经可以处理不同的 n 值。 当输入 n 是标量时会发生什么,例如 f(1)? 如果是标量,AutoGraph 不会将“if”转换为 tf.cond。 跟踪按原样运行“if”语句。 在n=1的情况下,只跟踪“elif n==1:”分支,但不跟踪其他分支,图中没有记录“if”操作。 该方法被简单地追踪为: “if ... elif”语句只是 Python 的一边作用。 那么当我们调用 f(2) 需要另一个分支中的代码时会发生什么? 幸运的是,将再次跟踪代码以创建 f(2) 的另一个图。 我们稍后会详细介绍它,包括它的影响。 追踪“while”和“for” 让我们用“while”循环重复它。 如果条件中使用了张量,它将转换为 tf.while_loop 并且其内容将被跟踪一次。 如果它不是张量,它将作为 Python “while” 循环运行。 如下例所示,它会循环 3 次,每次都将其内容添加到图中。 如果使用 f(4) 调用它,则 f 将被重新跟踪为。 同样,我们应该在“for”语句中期待相同的行为。 当“for i in expression”中的表达式被评估为张量时,它将被 tf.while_loop 替换。 tf.range 返回一个张量,因此下面的 for 循环将被替换。 下面的代码显示了如果它是一个标量表达式,跟踪是如何以不同的方式完成的。 数据集与 NumPy 数组 如果训练过程是 tf.function 化的,如下面的那个,有时重要的是“for”循环的“in”表达式是一个数据集(tf.data.Dataset),而不是 Python 或 Numpy 结构 . 在后一种情况下,每次迭代都会在跟踪期间向图中添加节点。 因此,可能会添加数十万个节点。 但如果使用数据集,则仅将 tf.Data.Dataset 操作的组合添加到图中一次,而不是每次迭代。 TensorFlow 循环跟踪循环体一次,并动态选择在执行时运行多少次迭代。 Python List v.s. TensorArray 图形模式下对 Python 列表的支持很差。 特别是在@tf.function 方法内部或外部修改列表时。 我遇到了太多问题,建议不要在带注释的方法中使用 Python 列表。 例如, l.append 操作由 Python 运行时处理,不会在图中创建任何节点。 这是 Python 结构之一,在图形执行中将被严重忽略,并在跟踪中出现意外行为。 如果您需要在运行时添加项目的类似列表的数据结构,请改用 TensorArray。 这在 RNN 中尤为常见,我们可能会为每个时间步累积隐藏状态。 Polymorphic on Performance Python 不是强类型语言。它允许方法参数在不同的调用中具有不同的类型。如何处理它取决于被调用者。另一方面,TensorFlow 是相当静态的。构建图形需要参数的数据类型和形状信息。实际上,当使用不同数据类型或形状的参数调用它以更有效地执行时,它会构建一个不同的图形。即使输入张量的形状发生变化,跟踪也可能会被重做。 f.get_concrete_function 返回 ConcreteFunction。它是代表计算图的 tf.Graph 的包装器。在下面的示例中,f1 和 f2 采用不同形状的输入张量。因此,这两个函数的 ConcreteFunctions 是不同的,因为它们有两个不同的图。很高兴,它包裹在一个管理 ConcreteFunction 缓存的函数 (python.eager.def_function.Function) 上。调用者使用此 Function 对象,内部差异对他们隐藏。 如果你想强制他们使用相同的图,我们可以添加一个带有 TensorSpec 的 input_signature,它具有更一般的形状。 例如,通过将形状指定为无,它可以对下面的向量和矩阵使用相同的图形。 但是图表可能效率较低。 下面的 None 维度是一个通配符,它允许 Functions 为可变大小的输入重用跟踪。 当一个方法因为参数具有未遇到的数据类型或形状而被追溯时,它会增加开销。 特别是,当输入参数是标量时,这可能是一个问题。 只要标量值不同,TF 就会触发回溯。 如下所示,f3 具有不同的标量输入,因此,该方法被回溯,其图形与 f1 和 f2 不同。 这种机制允许 TF 处理前面讨论的“if”和“while”语句中的标量条件。 为避免开销,请正确设计方法。 例如,开发人员可能会无意中传递一个标量参数作为训练步数。 这可以触发许多回撤。 它会降低性能。 为避免这种情况,请使用张量对象而不是标量。 Graph is a Snapshot 我们在跟踪函数以创建图形时对其进行快照。 因此,即使在第二次再次调用 f 之前更改了下面的列表 l,它仍然会看到旧的 l 值。 (但听我说,避免使用列表。) Iterator 许多 Python 功能,例如生成器和迭代器,都依赖 Python 运行时来跟踪状态。 在图形模式下运行时,图形不会意识到这些变化。 如下所示,迭代器不会在多次调用时前进。 该图在跟踪时仅具有迭代器值的快照。 变量只能创建一次 只能在首次调用时创建 tf.Variable 变量。 如果没有第 15 行,第 16 行可能会创建第一次调用以外的变量。 这将得到一个例外。 此操作旨在在创建图形后对其进行修改。 TF 不允许这样做,因为 TF 图旨在保持静态。 相反,如果适用,我们可以在函数外部创建非模型变量并将它们作为参数传递。 模型训练 我们还可以在 model.compile 中配置和关闭 Eager Execution。 处理 model.fit 时,模型将被跟踪并以图形模式运行。 Tensor objects & Numpy interoperability 通过 Eager Execution,Numpy 操作可以将 tf.Tensor 作为参数。 反之亦然, tf.math 操作将 Python 对象和 NumPy 数组转换为 tf.Tensor 对象。 要将 tf.Tensor 对象显式转换为 Numpy ndarray,请使用 numpy()。 函数化函数 python 函数可以在没有注释的情况下作为 Graph 执行。 下面的 tf_function 将函数转换为 python.eager.def_function.Function — 与 @tf.function 注释中讨论的类相同。 在 Graph 中执行 Python 代码 在图模式下,我们希望将所有操作转换为独立于 Python 的图以执行。 但如果我们想在图中执行 Python 代码,我们可以使用 tf.py_function 作为解决方法。 tf.py_function 将所有输入/输出参数转换为张量。 但是,该图的可移植性优势将丢失,并且它不适用于分布式多 GPU 设置。 这是 Python 列表中的代码。 我们使它与 py_function 一起工作。 虽然我们应该尽可能避免使用它,但最常见的用例是使用 scipy.ndimage 等外部库对图像进行数据增强, 在这里,我们使用 scipy.ndimage 中的任意旋转来扩充数据。 使用类型注释来减少回溯 还有一个实验功能可以减少标量输入的回溯。 例如,即使输入是非张量值,使用 tf.Tensor 注释的输入参数也会转换为张量。 因此,即使对于不同的值,f(scalar) 也不会被追溯。 如图所示,下面的 f_with_hints(2) 不会触发回溯。 |