此页面由 Cloud Translation API 翻译。
Switch to English

使用TensorBoard Debugger V2调试TensorFlow程序中的数值问题

在TensorFlow程序期间,有时可能会发生涉及NaN的灾难性事件,从而破坏了模型训练过程。此类事件的根本原因通常是晦涩的,尤其是对于大小和复杂程度不高的模型。为了使调试这类模型错误更容易,TensorBoard 2.3+(与TensorFlow 2.3+一起)提供了一个称为Debugger V2的专用仪表板。在这里,我们通过在TensorFlow编写的神经网络中解决涉及NaN的实际错误,来演示如何使用此工具。

本教程中说明的技术适用于其他类型的调试活动,例如在复杂程序中检查运行时张量形状。本教程重点介绍NaN,因为它们的发生频率相对较高。

观察错误

我们将调试的TF2程序的源代码可在GitHub上获得 。该示例程序也打包在tensorflow pip包中(版本2.3+),可以通过以下方式调用:

 python -m tensorflow.python.debug.examples.v2.debug_mnist_v2
 

该TF2程序创建一个多层感知(MLP)并对其进行训练以识别MNIST图像。此示例有目的地使用TF2的低级API定义自定义层构造,损失函数和训练循环,因为与使用简单的API相比,使用更灵活但更易于出错的API时,NaN错误的可能性更高使用但灵活性稍差的高级API,例如tf.keras

程序在每个训练步骤之后都会打印测试准确性。我们可以在控制台中看到,第一步之后,测试准确性陷入了接近机会的水平(〜0.1)。当然,这不是模型训练的预期表现:我们希望随着步幅的增加,精度会逐渐接近1.0(100%)。

 Accuracy at step 0: 0.216
Accuracy at step 1: 0.098
Accuracy at step 2: 0.098
Accuracy at step 3: 0.098
...
 

有根据的猜测是此问题是由数值不稳定性(例如NaN或无穷大)引起的。但是,如何确定确实如此?如何找到负责产生数值不稳定性的TensorFlow操作(op)?为了回答这些问题,让我们用Debugger V2来检测越野车程序。

使用Debugger V2检测TensorFlow代码

tf.debugging.experimental.enable_dump_debug_info()是Debugger V2的API入口点。它使用单行代码检测TF2程序。例如,在程序开头附近添加以下行会导致将调试信息写入/ tmp / tfdbg2_logdir的日志目录(logdir)。调试信息涵盖了TensorFlow运行时的各个方面。在TF2中,它包括急切执行的全部历史记录,由@ tf.function执行的图构建,图的执行,由执行事件生成的张量值以及这些事件的代码位置(Python堆栈跟踪) 。调试信息的丰富性使用户可以缩小模糊的错误的范围。

 tf.debugging.experimental.enable_dump_debug_info(
    logdir="/tmp/tfdbg2_logdir",
    tensor_debug_mode="FULL_HEALTH",
    circular_buffer_size=-1)
 

tensor_debug_mode参数控制Debugger V2从每个渴望的或图中的张量提取的信息。 “ FULL_HEALTH”是一种模式,捕获有关每个浮点型张量的以下信息(例如,常见的float32和次要的bfloat16 dtype ):

  • D型
  • 元素总数
  • 浮点型元素的细分为以下类别:负有限( - ),零( 0 ),正有限( + ),负无穷大( -∞ ),正无穷大( +∞ )和NaN

“ FULL_HEALTH”模式适用于调试涉及NaN和无穷大的错误。请参阅下面的其他受支持的tensor_debug_mode

circular_buffer_size参数控制将多少张量事件保存到日志目录中。它的默认值为1000,这仅导致将已检测的TF2程序结束之前的最后1000个张量保存到磁盘。此默认行为通过牺牲调试数据的完整性来减少调试器的开销。如果首选完整性,在这种情况下,我们可以通过将参数设置为负值(例如,此处为-1)来禁用循环缓冲区。

debug_mnist_v2示例通过enable_dump_debug_info()传递命令行标志来调用enable_dump_debug_info() 。要在启用此调试工具的情况下再次运行有问题的TF2程序,请执行以下操作:

 python -m tensorflow.python.debug.examples.v2.debug_mnist_v2 \
    --dump_dir /tmp/tfdbg2_logdir --dump_tensor_debug_mode FULL_HEALTH
 

在TensorBoard中启动Debugger V2 GUI

使用调试器工具运行程序会在/ tmp / tfdbg2_logdir中创建一个日志目录。我们可以启动TensorBoard并将其指向logdir:

 tensorboard --logdir /tmp/tfdbg2_logdir
 

在Web浏览器中,浏览至TensorBoard的页面,网址为http:// localhost:6006。默认情况下,“ Debugger V2”插件应处于激活状态,显示的页面如下所示:

Debugger V2全屏截图

使用Debugger V2 GUI查找NaN的根本原因

TensorBoard中的Debugger V2 GUI分为六个部分:

  • 警报 :此左上部分包含调试器在检测到的TensorFlow程序的调试数据中检测到的“警报”事件列表。每个警报均指示需要引起注意的特定异常。在我们的案例中,本节重点介绍了499个NaN /∞事件,其颜色为显着的粉红色。这证实了我们的怀疑,即模型由于其内部张量值中存在NaN和/或无穷而无法学习。我们将在短期内深入研究这些警报。
  • Python执行时间轴 :这是上半部分的上半部分。它显示了急切执行操作和图形的完整历史。时间轴的每个框均由操作或图形名称的首字母标记(例如,“ TensorSliceDataset”操作为“ T”,“模型” tf.function为“ m”)。我们可以使用时间线上方的导航按钮和滚动条来浏览时间线。
  • 图形执行 :位于GUI的右上角,此部分将是我们调试任务的中心。它包含在图内计算的所有float-dtype张量的历史记录(即,由@tf-function s编译)。
  • 图形结构 (上中下部的下半部分), 源代码 (左下部分)和堆栈跟踪 (右下部分)最初是空的。当我们与GUI交互时,将填充它们的内容。这三个部分还将在我们的调试任务中扮演重要角色。

使自己适应UI的组织之后,让我们采取以下步骤来深入了解NaN出现的原因。首先,在“警报”部分中单击NaN /∞警报。这将自动滚动“图形执行”部分中的600个图形张量的列表,并聚焦于#88,这是一个由Log (自然对数)运算符生成的名为“ Log:0”的张量。显着的粉红色突出显示2D float32张量的1000个元素中的-∞元素。这是TF2程序运行时历史中的第一个张量,其中包含任何NaN或无穷大:在它之前计算的张量不包含NaN或∞;随后计算出的许多(实际上是大多数)张量包含NaN。我们可以通过上下滚动图形执行列表来确认这一点。该观察结果强烈暗示了Log op是此TF2程序中数值不稳定的根源。

调试器V2:Nan / Infinity警报和图形执行列表

为什么此Log op吐出-∞?要回答该问题,需要检查操作的输入。单击张量的名称(“ Log:0”)会在“图结构”部分的TensorFlow图中显示Log op附近的简单但信息丰富的可视化。注意信息流的从上到下的方向。 op本身以中间的粗体显示。在它的紧上方,我们可以看到一个占位符op提供了一个唯一的输入到Log op。在图形执行列表中,此logits占位符生成的张量在哪里?通过使用黄色背景色作为视觉辅助,我们可以看到logits:0张量在Log:0张量上方两行,即第85行。

调试器V2:图形结构视图和对输入张量的跟踪

更仔细地看一下第85行中“ logits:0”张量的数字细分,揭示了为什么其使用者Log:0产生-∞的原因:在“ logits:0”的1000个元素中,一个元素的值为0。 -∞是计算自然对数0的结果!如果我们能以某种方式确保Log op仅暴露于正输入,我们将能够防止NaN /∞的发生。这可以通过在占位符logits张量上应用裁剪(例如,通过使用tf.clip_by_value() )来实现。

我们正在接近解决错误,但尚未完成。为了应用此修复程序,我们需要知道Log op及其占位符输入在Python源代码中的何处。 Debugger V2为跟踪图形操作和执行事件提供了一流的支持。当我们单击“图形执行”中的Log:0张量时,“堆栈跟踪”部分填充了Log op创建的原始堆栈跟踪。堆栈跟踪有些大,因为它包含来自TensorFlow内部代码的许多帧(例如gen_math_ops.py和dumping_callback.py),对于大多数调试任务,我们可以放心地忽略它们。感兴趣的框架是debug_mnist_v2.py的第216行(即,我们实际上正在尝试调试的Python文件)。单击“ 204行”将在“源代码”部分中显示相应代码行的视图。

调试器V2:源代码和堆栈跟踪

最终,我们进入了从logits输入创建有问题的Log op的源代码。这是用@tf.function装饰的自定义分类交叉熵损失函数,因此将其转换为TensorFlow图。占位符op“ logits”对应于损失函数的第一个输入参数。 Log操作是使用tf.math.log()API调用创建的。

此错误的价值削减修复将类似于:

   diff = -(labels *
           tf.clip_by_value(tf.math.log(logits), 1e-6, 1.))
 

它将解决该TF2程序中的数值不稳定性,并使MLP成功训练。解决数值不稳定性的另一种可能方法是使用tf.keras.losses.CategoricalCrossentropy

这总结了我们从观察TF2模型错误到提出修改该错误的代码更改的过程,并借助Debugger V2工具,该工具提供了对所检测的TF2程序的渴望和图形执行历史的完整可见性,包括数字摘要张量值以及操作,张量及其原始源代码之间的关联。

Debugger V2的硬件兼容性

Debugger V2支持主流的培训硬件,包括CPU和GPU。还支持使用tf.distributed.MirroredStrategy进行多GPU训练。对TPU的支持仍处于早期阶段,需要致电

 tf.config.set_soft_device_placement(True)
 

在调用enable_dump_debug_info()之前。它还可能对TPU有其他限制。如果您在使用Debugger V2时遇到问题,请在GitHub问题页面上报告错误。

Debugger V2的API兼容性

Debugger V2在TensorFlow的软件堆栈的较低级别上实现,因此与tf.kerastf.data以及在TensorFlow较低级别之上构建的其他API兼容。调试器V2也与TF1向后兼容,尽管对于TF1程序生成的调试日志目录,急切执行时间轴将为空。

API使用技巧

关于此调试API的一个常见问题是,应该在TensorFlow代码中的哪个位置插入对enable_dump_debug_info()的调用。通常,应该在TF2程序中尽早调用该API,最好在Python导入行之后,在图形构建和执行开始之前调用该API。这将确保为模型及其训练提供支持的所有操作和图形的完整覆盖。

当前支持的tensor_debug_modes是: NO_TENSORCURT_HEALTHCONCISE_HEALTHFULL_HEALTHSHAPE 。它们从每个张量提取的信息量以及调试程序的性能开销各不相同。请参考enable_dump_debug_info()的文档的args部分 ]。

性能开销

调试API将性能开销引入了已检测的TensorFlow程序。开销因tensor_debug_mode ,硬件类型和已检测TensorFlow程序的性质而异。作为参考,在GPU上,在批量大小为64的Transformer模型训练期间, NO_TENSOR模式增加了15%的开销。其他tensor_debug_modes的开销百分比更高: CURT_HEALTHCONCISE_HEALTHFULL_HEALTHSHAPE大约为50%模式。在CPU上,开销略低。在TPU上,开销目前较高。

与其他TensorFlow调试API的关系

请注意,TensorFlow提供了其他工具和API进行调试。您可以在API文档页面的tf.debugging.*名称空间下浏览此类API。在这些API中,最常用的是tf.print() 。什么时候应该使用Debugger V2,什么时候应该使用tf.print()tf.print()在以下情况下很方便

  1. 我们确切知道要打印哪些张量
  2. 我们知道在源代码中确切的位置插入这些tf.print()语句,
  3. 这样的张量的数量不是太大。

对于其他情况(例如,检查许多张量值,检查由TensorFlow的内部代码生成的张量值,并如上所述搜索数值不稳定的起源),Debugger V2提供了一种更快的调试方式。另外,Debugger V2提供了一种检查渴望和图形张量的统一方法。它还提供了有关图结构和代码位置的信息,这超出了tf.print()的能力。

可以用来调试涉及∞和NaN的问题的另一个API是tf.debugging.enable_check_numerics() 。与enable_dump_debug_info()不同, enable_check_numerics()不会在磁盘上保存调试信息。取而代之的是,它仅在TensorFlow运行时期间监视∞和NaN,并在任何操作生成此类不良数值后立即将原始代码位置错误排除。与enable_dump_debug_info()相比,它具有较低的性能开销,但是无法提供程序执行历史的完整记录,并且没有像Debugger V2这样的图形用户界面。