验证正确性和数值等效性

在 TensorFlow.org 上查看 在 Google Colab 中运行 在 GitHub 上查看 下载笔记本

将 TensorFlow 代码从 TF1.x 迁移到 TF2 时,最佳做法是确保迁移后的代码在 TF2 中的行为与 TF1.x 中的行为相同。

本指南涵盖了迁移代码示例,其中应用了 tf.compat.v1.keras.utils.track_tf1_style_variables 模型模拟到 tf.keras.layers.Layer 方法。阅读 模型映射指南 了解有关 TF2 模型模拟的更多信息。

本指南详细介绍了您可以使用的方法,以

  • 验证使用迁移后的代码训练模型获得的结果的正确性
  • 验证代码在 TensorFlow 版本之间的数值等效性

设置

pip uninstall -y -q tensorflow
# Install tf-nightly as the DeterministicRandomTestTool is available only in
# Tensorflow 2.8
pip install -q tf-nightly
pip install -q tf_slim
import tensorflow as tf
import tensorflow.compat.v1 as v1

import numpy as np
import tf_slim as slim
import sys


from contextlib import contextmanager
!git clone --depth=1 https://github.com/tensorflow/models.git
import models.research.slim.nets.inception_resnet_v2 as inception

如果您将大量前向传递代码放入模拟中,您需要知道它与 TF1.x 中的行为相同。例如,考虑尝试将整个 TF-Slim Inception-Resnet-v2 模型放入模拟中,如下所示

# TF1 Inception resnet v2 forward pass based on slim layers
def inception_resnet_v2(inputs, num_classes, is_training):
  with slim.arg_scope(
    inception.inception_resnet_v2_arg_scope(batch_norm_scale=True)):
    return inception.inception_resnet_v2(inputs, num_classes, is_training=is_training)
class InceptionResnetV2(tf.keras.layers.Layer):
  """Slim InceptionResnetV2 forward pass as a Keras layer"""

  def __init__(self, num_classes, **kwargs):
    super().__init__(**kwargs)
    self.num_classes = num_classes

  @tf.compat.v1.keras.utils.track_tf1_style_variables
  def call(self, inputs, training=None):
    is_training = training or False 

    # Slim does not accept `None` as a value for is_training,
    # Keras will still pass `None` to layers to construct functional models
    # without forcing the layer to always be in training or in inference.
    # However, `None` is generally considered to run layers in inference.

    with slim.arg_scope(
        inception.inception_resnet_v2_arg_scope(batch_norm_scale=True)):
      return inception.inception_resnet_v2(
          inputs, self.num_classes, is_training=is_training)

碰巧的是,这一层实际上可以完美地开箱即用(包括准确的正则化损失跟踪)。

但是,这不是您可以想当然的事情。请按照以下步骤验证它是否确实与 TF1.x 中的行为相同,直到观察到完美的数值等效性。这些步骤还可以帮助您三角测量导致与 TF1.x 偏差的前向传递的哪一部分(确定偏差是否出现在模型前向传递中,而不是模型的其他部分)。

步骤 1:验证变量只创建一次

您应该验证的第一件事是,您是否已正确构建模型,以便在每次调用时重用变量,而不是意外地在每次调用时创建和使用新变量。例如,如果您的模型在每次前向传递调用中创建新的 Keras 层或调用 tf.Variable,那么它很可能无法捕获变量,而是在每次调用时创建新的变量。

以下是您可以用来检测模型何时创建新变量并调试哪个部分正在执行此操作的两个上下文管理器范围。

@contextmanager
def assert_no_variable_creations():
  """Assert no variables are created in this context manager scope."""
  def invalid_variable_creator(next_creator, **kwargs):
    raise ValueError("Attempted to create a new variable instead of reusing an existing one. Args: {}".format(kwargs))

  with tf.variable_creator_scope(invalid_variable_creator):
    yield

@contextmanager
def catch_and_raise_created_variables():
  """Raise all variables created within this context manager scope (if any)."""
  created_vars = []
  def variable_catcher(next_creator, **kwargs):
    var = next_creator(**kwargs)
    created_vars.append(var)
    return var

  with tf.variable_creator_scope(variable_catcher):
    yield
  if created_vars:
    raise ValueError("Created vars:", created_vars)

第一个范围 (assert_no_variable_creations()) 将在您尝试在范围内创建变量时立即引发错误。这使您可以检查堆栈跟踪(并使用交互式调试)以找出到底哪几行代码创建了变量,而不是重用现有变量。

第二个范围 (catch_and_raise_created_variables()) 如果在范围内创建了任何变量,将在范围结束时引发异常。此异常将包含在范围内创建的所有变量的列表。这对于弄清楚模型创建的所有权重的集合很有用,以防您能发现一般模式。但是,它对于识别创建这些变量的确切代码行不太有用。

使用以下两个范围验证基于模拟的 InceptionResnetV2 层在第一次调用后不会创建任何新变量(可能是在重用它们)。

model = InceptionResnetV2(1000)
height, width = 299, 299
num_classes = 1000

inputs = tf.ones( (1, height, width, 3))
# Create all weights on the first call
model(inputs)

# Verify that no new weights are created in followup calls
with assert_no_variable_creations():
  model(inputs)
with catch_and_raise_created_variables():
  model(inputs)

在下面的示例中,观察这些装饰器如何在每次错误地创建新权重而不是重用现有权重的层上工作。

class BrokenScalingLayer(tf.keras.layers.Layer):
  """Scaling layer that incorrectly creates new weights each time:"""

  @tf.compat.v1.keras.utils.track_tf1_style_variables
  def call(self, inputs):
    var = tf.Variable(initial_value=2.0)
    bias = tf.Variable(initial_value=2.0, name='bias')
    return inputs * var + bias
model = BrokenScalingLayer()
inputs = tf.ones( (1, height, width, 3))
model(inputs)

try:
  with assert_no_variable_creations():
    model(inputs)
except ValueError as err:
  import traceback
  traceback.print_exc()
model = BrokenScalingLayer()
inputs = tf.ones( (1, height, width, 3))
model(inputs)

try:
  with catch_and_raise_created_variables():
    model(inputs)
except ValueError as err:
  print(err)

您可以通过确保层只创建一次权重,然后每次都重复使用它们来修复层。

class FixedScalingLayer(tf.keras.layers.Layer):
  """Scaling layer that incorrectly creates new weights each time:"""
  def __init__(self):
    super().__init__()
    self.var = None
    self.bias = None

  @tf.compat.v1.keras.utils.track_tf1_style_variables
  def call(self, inputs):
    if self.var is None:
      self.var = tf.Variable(initial_value=2.0)
      self.bias = tf.Variable(initial_value=2.0, name='bias')
    return inputs * self.var + self.bias

model = FixedScalingLayer()
inputs = tf.ones( (1, height, width, 3))
model(inputs)

with assert_no_variable_creations():
  model(inputs)
with catch_and_raise_created_variables():
  model(inputs)

故障排除

以下是您的模型可能意外创建新权重而不是重复使用现有权重的一些常见原因

  1. 它使用显式的 tf.Variable 调用,而没有重复使用已创建的 tf.Variables。通过首先检查它是否尚未创建,然后重复使用现有的来修复此问题。
  2. 它每次都在前向传递中直接创建 Keras 层或模型(而不是 tf.compat.v1.layers)。通过首先检查它是否尚未创建,然后重复使用现有的来修复此问题。
  3. 它建立在 tf.compat.v1.layers 之上,但未能为所有 compat.v1.layers 指定显式名称,或将您的 compat.v1.layer 使用包装在命名 variable_scope 中,导致自动生成的层名称在每次模型调用时递增。通过在您的 shim 装饰方法中放置一个命名的 tf.compat.v1.variable_scope 来修复此问题,该方法包装了所有 tf.compat.v1.layers 使用。

步骤 2:检查变量计数、名称和形状是否匹配

第二步是确保您的在 TF2 中运行的层创建的权重数量与 TF1.x 中的对应代码相同,并且形状相同。

您可以混合手动检查它们以查看它们是否匹配,并在单元测试中以编程方式进行检查,如下所示。

# Build the forward pass inside a TF1.x graph, and 
# get the counts, shapes, and names of the variables
graph = tf.Graph()
with graph.as_default(), tf.compat.v1.Session(graph=graph) as sess:
  height, width = 299, 299
  num_classes = 1000
  inputs = tf.ones( (1, height, width, 3))

  out, endpoints = inception_resnet_v2(inputs, num_classes, is_training=False)

  tf1_variable_names_and_shapes = {
      var.name: (var.trainable, var.shape) for var in tf.compat.v1.global_variables()}
  num_tf1_variables = len(tf.compat.v1.global_variables())

接下来,对 TF2 中的 shim 包装层执行相同的操作。请注意,模型在获取权重之前也被多次调用。这样做是为了有效地测试变量重用。

height, width = 299, 299
num_classes = 1000

model = InceptionResnetV2(num_classes)
# The weights will not be created until you call the model

inputs = tf.ones( (1, height, width, 3))
# Call the model multiple times before checking the weights, to verify variables
# get reused rather than accidentally creating additional variables
out, endpoints = model(inputs, training=False)
out, endpoints = model(inputs, training=False)

# Grab the name: shape mapping and the total number of variables separately,
# because in TF2 variables can be created with the same name
num_tf2_variables = len(model.variables)
tf2_variable_names_and_shapes = {
    var.name: (var.trainable, var.shape) for var in model.variables}
# Verify that the variable counts, names, and shapes all match:
assert num_tf1_variables == num_tf2_variables
assert tf1_variable_names_and_shapes == tf2_variable_names_and_shapes

基于 shim 的 InceptionResnetV2 层通过了此测试。但是,如果它们不匹配,您可以通过差异(文本或其他)运行它以查看差异在哪里。

这可以提供有关模型的哪个部分行为不符合预期的线索。使用急切执行,您可以使用 pdb、交互式调试和断点深入研究看起来可疑的模型部分,并更深入地调试错误所在。

故障排除

  • 请密切注意由显式 tf.Variable 调用和 Keras 层/模型直接创建的任何变量的名称,因为它们的变量名称生成语义在 TF1.x 图和 TF2 功能(如急切执行和 tf.function)之间可能略有不同,即使其他一切正常。如果是这种情况,请调整您的测试以考虑任何略有不同的命名语义。

  • 您有时可能会发现,在训练循环的前向传递中创建的 tf.Variabletf.keras.layers.Layertf.keras.Model 缺少您的 TF2 变量列表,即使它们被 TF1.x 中的变量集合捕获。通过将前向传递创建的变量/层/模型分配给模型中的实例属性来修复此问题。有关更多信息,请参见 此处

步骤 3:重置所有变量,检查所有随机性禁用的数值等效性

下一步是验证实际输出和正则化损失跟踪的数值等效性,当您修复模型以使没有随机数生成参与时(例如在推理期间)。

执行此操作的确切方法可能取决于您的特定模型,但在大多数模型(如本模型)中,您可以通过以下方式执行此操作

  1. 将权重初始化为相同的值,没有随机性。这可以通过在创建权重后将它们重置为固定值来完成。
  2. 在推理模式下运行模型以避免触发任何可能成为随机性来源的 dropout 层。

以下代码演示了如何以这种方式比较 TF1.x 和 TF2 结果。

graph = tf.Graph()
with graph.as_default(), tf.compat.v1.Session(graph=graph) as sess:
  height, width = 299, 299
  num_classes = 1000
  inputs = tf.ones( (1, height, width, 3))

  out, endpoints = inception_resnet_v2(inputs, num_classes, is_training=False)

  # Rather than running the global variable initializers,
  # reset all variables to a constant value
  var_reset = tf.group([var.assign(tf.ones_like(var) * 0.001) for var in tf.compat.v1.global_variables()])
  sess.run(var_reset)

  # Grab the outputs & regularization loss
  reg_losses = tf.compat.v1.get_collection(tf.compat.v1.GraphKeys.REGULARIZATION_LOSSES)
  tf1_regularization_loss = sess.run(tf.math.add_n(reg_losses))
  tf1_output = sess.run(out)

print("Regularization loss:", tf1_regularization_loss)
tf1_output[0][:5]

获取 TF2 结果。

height, width = 299, 299
num_classes = 1000

model = InceptionResnetV2(num_classes)

inputs = tf.ones((1, height, width, 3))
# Call the model once to create the weights
out, endpoints = model(inputs, training=False)

# Reset all variables to the same fixed value as above, with no randomness
for var in model.variables:
  var.assign(tf.ones_like(var) * 0.001)
tf2_output, endpoints = model(inputs, training=False)

# Get the regularization loss
tf2_regularization_loss = tf.math.add_n(model.losses)

print("Regularization loss:", tf2_regularization_loss)
tf2_output[0][:5]
# Create a dict of tolerance values
tol_dict={'rtol':1e-06, 'atol':1e-05}
# Verify that the regularization loss and output both match
# when we fix the weights and avoid randomness by running inference:
np.testing.assert_allclose(tf1_regularization_loss, tf2_regularization_loss.numpy(), **tol_dict)
np.testing.assert_allclose(tf1_output, tf2_output.numpy(), **tol_dict)

当您删除随机性来源时,TF1.x 和 TF2 之间的数字匹配,并且与 TF2 兼容的 InceptionResnetV2 层通过了测试。

如果您观察到您自己的模型的结果出现分歧,您可以使用打印或 pdb 和交互式调试来识别结果开始分歧的位置和原因。急切执行可以使这变得容易得多。您还可以使用消融方法仅对固定中间输入运行模型的小部分,并隔离分歧发生的位置。

方便的是,许多精简网络(和其他模型)也公开了您可以探测的中间端点。

步骤 4:对齐随机数生成,检查训练和推理中的数值等效性

最后一步是验证 TF2 模型在数值上与 TF1.x 模型匹配,即使考虑到变量初始化和前向传递本身中的随机数生成(例如,前向传递期间的 dropout 层)。

您可以通过使用以下测试工具来使 TF1.x 图/会话和急切执行之间的随机数生成语义匹配来做到这一点。

TF1 遗留图/会话和 TF2 急切执行使用不同的有状态随机数生成语义。

tf.compat.v1.Session 中,如果未指定种子,则随机数生成取决于在添加随机操作时图中存在多少个操作,以及图运行了多少次。在急切执行中,有状态随机数生成取决于全局种子、操作随机种子以及具有给定随机种子的操作运行了多少次。有关更多信息,请参见 tf.random.set_seed

以下 v1.keras.utils.DeterministicRandomTestTool 类提供了一个上下文管理器 scope(),它可以使有状态随机操作在 TF1 图/会话和急切执行中使用相同的种子。

该工具提供两种测试模式

  1. constant 使用相同的种子来执行每个操作,无论它被调用了多少次,以及
  2. num_random_ops 使用先前观察到的有状态随机操作的数量作为操作种子。

这适用于用于创建和初始化变量的有状态随机操作,以及用于计算的有状态随机操作(例如 dropout 层)。

生成三个随机张量以显示如何使用此工具使有状态随机数生成在会话和急切执行之间匹配。

random_tool = v1.keras.utils.DeterministicRandomTestTool()
with random_tool.scope():
  graph = tf.Graph()
  with graph.as_default(), tf.compat.v1.Session(graph=graph) as sess:
    a = tf.random.uniform(shape=(3,1))
    a = a * 3
    b = tf.random.uniform(shape=(3,3))
    b = b * 3
    c = tf.random.uniform(shape=(3,3))
    c = c * 3
    graph_a, graph_b, graph_c = sess.run([a, b, c])

graph_a, graph_b, graph_c
random_tool = v1.keras.utils.DeterministicRandomTestTool()
with random_tool.scope():
  a = tf.random.uniform(shape=(3,1))
  a = a * 3
  b = tf.random.uniform(shape=(3,3))
  b = b * 3
  c = tf.random.uniform(shape=(3,3))
  c = c * 3

a, b, c
# Demonstrate that the generated random numbers match
np.testing.assert_allclose(graph_a, a.numpy(), **tol_dict)
np.testing.assert_allclose(graph_b, b.numpy(), **tol_dict)
np.testing.assert_allclose(graph_c, c.numpy(), **tol_dict)

但是,请注意,在 constant 模式下,因为 bc 使用相同的种子生成并且具有相同的形状,所以它们将具有完全相同的值。

np.testing.assert_allclose(b.numpy(), c.numpy(), **tol_dict)

跟踪顺序

如果您担心在 constant 模式下一些随机数匹配会降低您对数值等效性测试的信心(例如,如果几个权重采用相同的初始化),您可以使用 num_random_ops 模式来避免这种情况。在 num_random_ops 模式下,生成的随机数将取决于程序中随机操作的顺序。

random_tool = v1.keras.utils.DeterministicRandomTestTool(mode='num_random_ops')
with random_tool.scope():
  graph = tf.Graph()
  with graph.as_default(), tf.compat.v1.Session(graph=graph) as sess:
    a = tf.random.uniform(shape=(3,1))
    a = a * 3
    b = tf.random.uniform(shape=(3,3))
    b = b * 3
    c = tf.random.uniform(shape=(3,3))
    c = c * 3
    graph_a, graph_b, graph_c = sess.run([a, b, c])

graph_a, graph_b, graph_c
random_tool = v1.keras.utils.DeterministicRandomTestTool(mode='num_random_ops')
with random_tool.scope():
  a = tf.random.uniform(shape=(3,1))
  a = a * 3
  b = tf.random.uniform(shape=(3,3))
  b = b * 3
  c = tf.random.uniform(shape=(3,3))
  c = c * 3

a, b, c
# Demonstrate that the generated random numbers match
np.testing.assert_allclose(graph_a, a.numpy(), **tol_dict)
np.testing.assert_allclose(graph_b, b.numpy(), **tol_dict )
np.testing.assert_allclose(graph_c, c.numpy(), **tol_dict)
# Demonstrate that with the 'num_random_ops' mode,
# b & c took on different values even though
# their generated shape was the same
assert not np.allclose(b.numpy(), c.numpy(), **tol_dict)

但是,请注意,在这种模式下,随机生成对程序顺序很敏感,因此以下生成的随机数不匹配。

random_tool = v1.keras.utils.DeterministicRandomTestTool(mode='num_random_ops')
with random_tool.scope():
  a = tf.random.uniform(shape=(3,1))
  a = a * 3
  b = tf.random.uniform(shape=(3,3))
  b = b * 3

random_tool = v1.keras.utils.DeterministicRandomTestTool(mode='num_random_ops')
with random_tool.scope():
  b_prime = tf.random.uniform(shape=(3,3))
  b_prime = b_prime * 3
  a_prime = tf.random.uniform(shape=(3,1))
  a_prime = a_prime * 3

assert not np.allclose(a.numpy(), a_prime.numpy())
assert not np.allclose(b.numpy(), b_prime.numpy())

为了允许调试由于跟踪顺序而导致的差异,DeterministicRandomTestToolnum_random_ops 模式下允许您使用 operation_seed 属性查看已跟踪了多少个随机操作。

random_tool = v1.keras.utils.DeterministicRandomTestTool(mode='num_random_ops')
with random_tool.scope():
  print(random_tool.operation_seed)
  a = tf.random.uniform(shape=(3,1))
  a = a * 3
  print(random_tool.operation_seed)
  b = tf.random.uniform(shape=(3,3))
  b = b * 3
  print(random_tool.operation_seed)

如果您需要在测试中考虑不同的跟踪顺序,您甚至可以显式设置自动递增的 operation_seed。例如,您可以使用它来使随机数生成在两个不同的程序顺序中匹配。

random_tool = v1.keras.utils.DeterministicRandomTestTool(mode='num_random_ops')
with random_tool.scope():
  print(random_tool.operation_seed)
  a = tf.random.uniform(shape=(3,1))
  a = a * 3
  print(random_tool.operation_seed)
  b = tf.random.uniform(shape=(3,3))
  b = b * 3

random_tool = v1.keras.utils.DeterministicRandomTestTool(mode='num_random_ops')
with random_tool.scope():
  random_tool.operation_seed = 1
  b_prime = tf.random.uniform(shape=(3,3))
  b_prime = b_prime * 3
  random_tool.operation_seed = 0
  a_prime = tf.random.uniform(shape=(3,1))
  a_prime = a_prime * 3

np.testing.assert_allclose(a.numpy(), a_prime.numpy(), **tol_dict)
np.testing.assert_allclose(b.numpy(), b_prime.numpy(), **tol_dict)

但是,DeterministicRandomTestTool 不允许重复使用已使用的操作种子,因此请确保自动递增的序列不会重叠。这是因为急切执行为后续使用相同的操作种子生成不同的数字,而 TF1 图和会话不会,因此引发错误有助于使会话和急切的有状态随机数生成保持一致。

random_tool = v1.keras.utils.DeterministicRandomTestTool(mode='num_random_ops')
with random_tool.scope():
  random_tool.operation_seed = 1
  b_prime = tf.random.uniform(shape=(3,3))
  b_prime = b_prime * 3
  random_tool.operation_seed = 0
  a_prime = tf.random.uniform(shape=(3,1))
  a_prime = a_prime * 3
  try:
    c = tf.random.uniform(shape=(3,1))
    raise RuntimeError("An exception should have been raised before this, " +
                     "because the auto-incremented operation seed will " +
                     "overlap an already-used value")
  except ValueError as err:
    print(err)

验证推理

您现在可以使用 DeterministicRandomTestTool 来确保 InceptionResnetV2 模型在推理中匹配,即使使用随机权重初始化。为了获得更强的测试条件,因为匹配的程序顺序,请使用 num_random_ops 模式。

random_tool = v1.keras.utils.DeterministicRandomTestTool(mode='num_random_ops')
with random_tool.scope():
  graph = tf.Graph()
  with graph.as_default(), tf.compat.v1.Session(graph=graph) as sess:
    height, width = 299, 299
    num_classes = 1000
    inputs = tf.ones( (1, height, width, 3))

    out, endpoints = inception_resnet_v2(inputs, num_classes, is_training=False)

    # Initialize the variables
    sess.run(tf.compat.v1.global_variables_initializer())

    # Grab the outputs & regularization loss
    reg_losses = tf.compat.v1.get_collection(tf.compat.v1.GraphKeys.REGULARIZATION_LOSSES)
    tf1_regularization_loss = sess.run(tf.math.add_n(reg_losses))
    tf1_output = sess.run(out)

  print("Regularization loss:", tf1_regularization_loss)
height, width = 299, 299
num_classes = 1000

random_tool = v1.keras.utils.DeterministicRandomTestTool(mode='num_random_ops')
with random_tool.scope():
  model = InceptionResnetV2(num_classes)

  inputs = tf.ones((1, height, width, 3))
  tf2_output, endpoints = model(inputs, training=False)

  # Grab the regularization loss as well
  tf2_regularization_loss = tf.math.add_n(model.losses)

print("Regularization loss:", tf2_regularization_loss)
# Verify that the regularization loss and output both match
# when using the DeterministicRandomTestTool:
np.testing.assert_allclose(tf1_regularization_loss, tf2_regularization_loss.numpy(), **tol_dict)
np.testing.assert_allclose(tf1_output, tf2_output.numpy(), **tol_dict)

验证训练

因为 DeterministicRandomTestTool所有有状态随机操作(包括权重初始化和计算,如 dropout 层)都有效,所以您可以使用它来验证模型在训练模式下也匹配。您可以再次使用 num_random_ops 模式,因为有状态随机操作的程序顺序匹配。

random_tool = v1.keras.utils.DeterministicRandomTestTool(mode='num_random_ops')
with random_tool.scope():
  graph = tf.Graph()
  with graph.as_default(), tf.compat.v1.Session(graph=graph) as sess:
    height, width = 299, 299
    num_classes = 1000
    inputs = tf.ones( (1, height, width, 3))

    out, endpoints = inception_resnet_v2(inputs, num_classes, is_training=True)

    # Initialize the variables
    sess.run(tf.compat.v1.global_variables_initializer())

    # Grab the outputs & regularization loss
    reg_losses = tf.compat.v1.get_collection(tf.compat.v1.GraphKeys.REGULARIZATION_LOSSES)
    tf1_regularization_loss = sess.run(tf.math.add_n(reg_losses))
    tf1_output = sess.run(out)

  print("Regularization loss:", tf1_regularization_loss)
height, width = 299, 299
num_classes = 1000

random_tool = v1.keras.utils.DeterministicRandomTestTool(mode='num_random_ops')
with random_tool.scope():
  model = InceptionResnetV2(num_classes)

  inputs = tf.ones((1, height, width, 3))
  tf2_output, endpoints = model(inputs, training=True)

  # Grab the regularization loss as well
  tf2_regularization_loss = tf.math.add_n(model.losses)

print("Regularization loss:", tf2_regularization_loss)
# Verify that the regularization loss and output both match
# when using the DeterministicRandomTestTool
np.testing.assert_allclose(tf1_regularization_loss, tf2_regularization_loss.numpy(), **tol_dict)
np.testing.assert_allclose(tf1_output, tf2_output.numpy(), **tol_dict)

您现在已经验证了在 tf.keras.layers.Layer 周围使用装饰器的急切执行的 InceptionResnetV2 模型在数值上与在 TF1 图和会话中运行的精简网络匹配。

例如,直接调用 InceptionResnetV2 层并设置 training=True 会根据网络创建顺序交织变量初始化和 dropout 顺序。

另一方面,首先将 tf.keras.layers.Layer 装饰器放在 Keras 函数模型中,然后才用 training=True 调用模型,这等同于先初始化所有变量,然后再使用 dropout 层。这会产生不同的跟踪顺序和不同的随机数集。

但是,默认的 mode='constant' 对跟踪顺序的这些差异不敏感,即使在将层嵌入 Keras 函数模型中时,它也会在没有额外工作的情况下通过。

random_tool = v1.keras.utils.DeterministicRandomTestTool()
with random_tool.scope():
  graph = tf.Graph()
  with graph.as_default(), tf.compat.v1.Session(graph=graph) as sess:
    height, width = 299, 299
    num_classes = 1000
    inputs = tf.ones( (1, height, width, 3))

    out, endpoints = inception_resnet_v2(inputs, num_classes, is_training=True)

    # Initialize the variables
    sess.run(tf.compat.v1.global_variables_initializer())

    # Get the outputs & regularization losses
    reg_losses = tf.compat.v1.get_collection(tf.compat.v1.GraphKeys.REGULARIZATION_LOSSES)
    tf1_regularization_loss = sess.run(tf.math.add_n(reg_losses))
    tf1_output = sess.run(out)

  print("Regularization loss:", tf1_regularization_loss)
height, width = 299, 299
num_classes = 1000

random_tool = v1.keras.utils.DeterministicRandomTestTool()
with random_tool.scope():
  keras_input = tf.keras.Input(shape=(height, width, 3))
  layer = InceptionResnetV2(num_classes)
  model = tf.keras.Model(inputs=keras_input, outputs=layer(keras_input))

  inputs = tf.ones((1, height, width, 3))
  tf2_output, endpoints = model(inputs, training=True)

  # Get the regularization loss
  tf2_regularization_loss = tf.math.add_n(model.losses)

print("Regularization loss:", tf2_regularization_loss)
# Verify that the regularization loss and output both match
# when using the DeterministicRandomTestTool
np.testing.assert_allclose(tf1_regularization_loss, tf2_regularization_loss.numpy(), **tol_dict)
np.testing.assert_allclose(tf1_output, tf2_output.numpy(), **tol_dict)

步骤 3b 或 4b(可选):使用预先存在的检查点进行测试

在完成上述步骤 3 或步骤 4 后,如果存在预先存在的基于名称的检查点,则在从这些检查点开始时运行数值等效性测试可能很有用。这可以测试您的旧版检查点加载是否正常工作,以及模型本身是否正常工作。 重用 TF1.x 检查点指南 涵盖了如何重用您现有的 TF1.x 检查点并将它们转移到 TF2 检查点。

其他测试和故障排除

随着您添加更多数值等效性测试,您也可以选择添加一个测试来验证您的梯度计算(甚至优化器更新)是否匹配。

反向传播和梯度计算比模型前向传递更容易受到浮点数值不稳定的影响。这意味着,随着您的等效性测试涵盖更多模型训练中非隔离的部分,您可能会开始看到在完全急切运行和您的 TF1 图之间存在非平凡的数值差异。这可能是由 TensorFlow 的图优化引起的,这些优化会执行诸如用更少的数学运算替换图中的子表达式之类的操作。

为了隔离这是否可能是这种情况,您可以将您的 TF1 代码与发生在 tf.function(它应用类似于您的 TF1 图的图优化传递)内部的 TF2 计算进行比较,而不是与纯粹的急切计算进行比较。或者,您可以尝试使用 tf.config.optimizer.set_experimental_options 在您的 TF1 计算之前禁用诸如 "arithmetic_optimization" 之类的优化传递,以查看结果是否最终在数值上更接近您的 TF2 计算结果。在您的实际训练运行中,建议您出于性能原因使用启用了优化传递的 tf.function,但您可能会发现,在您的数值等效性单元测试中禁用它们很有用。

同样,您也可能会发现,tf.compat.v1.train 优化器和 TF2 优化器与 TF2 优化器具有略微不同的浮点数值属性,即使它们所代表的数学公式相同。这在您的训练运行中不太可能成为问题,但它可能需要在等效性单元测试中使用更高的数值容差。