Проверка правильности и числовой эквивалентности

Посмотреть на 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
Cloning into 'models'...
remote: Enumerating objects: 3192, done.[K
remote: Counting objects: 100% (3192/3192), done.[K
remote: Compressing objects: 100% (2696/2696), done.[K
remote: Total 3192 (delta 848), reused 1381 (delta 453), pack-reused 0[K
Receiving objects: 100% (3192/3192), 33.39 MiB | 12.89 MiB/s, done.
Resolving deltas: 100% (848/848), done.

Если вы помещаете в прокладку нетривиальный фрагмент кода прямого прохода, вы хотите знать, что он ведет себя так же, как в 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)
WARNING:tensorflow:From /tmp/ipykernel_27382/2131234657.py:8: The name tf.keras.utils.track_tf1_style_variables is deprecated. Please use tf.compat.v1.keras.utils.track_tf1_style_variables instead.

Так получилось, что этот слой на самом деле отлично работает из коробки (в комплекте с точным отслеживанием потерь регуляризации).

Однако это не то, что вы хотите принимать как должное. Выполните следующие шаги, чтобы убедиться, что он действительно ведет себя так же, как в 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)
/tmpfs/src/tf_docs_env/lib/python3.7/site-packages/tensorflow/python/keras/engine/base_layer.py:2212: UserWarning: `layer.apply` is deprecated and will be removed in a future version. Please use `layer.__call__` method instead.
  warnings.warn('`layer.apply` is deprecated and '
/tmpfs/src/tf_docs_env/lib/python3.7/site-packages/tf_slim/layers/layers.py:684: UserWarning: `layer.apply` is deprecated and will be removed in a future version. Please use `layer.__call__` method instead.
  outputs = layer.apply(inputs, training=is_training)
/tmpfs/src/tf_docs_env/lib/python3.7/site-packages/tensorflow/python/keras/legacy_tf_layers/core.py:332: UserWarning: `tf.layers.flatten` is deprecated and will be removed in a future version. Please use `tf.keras.layers.Flatten` instead.
  warnings.warn('`tf.layers.flatten` is deprecated and '

В приведенном ниже примере посмотрите, как эти декораторы работают со слоем, который каждый раз неправильно создает новые веса вместо повторного использования существующих.

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()
Traceback (most recent call last):
  File "/tmp/ipykernel_27382/1128777590.py", line 7, in <module>
    model(inputs)
  File "/tmpfs/src/tf_docs_env/lib/python3.7/site-packages/keras/utils/traceback_utils.py", line 67, in error_handler
    raise e.with_traceback(filtered_tb) from None
  File "/tmp/ipykernel_27382/3224979076.py", line 6, in call
    var = tf.Variable(initial_value=2.0)
  File "/tmp/ipykernel_27382/1829430118.py", line 5, in invalid_variable_creator
    raise ValueError("Attempted to create a new variable instead of reusing an existing one. Args: {}".format(kwargs))
ValueError: Exception encountered when calling layer "broken_scaling_layer" (type BrokenScalingLayer).

Attempted to create a new variable instead of reusing an existing one. Args: {'initial_value': 2.0, 'trainable': None, 'validate_shape': True, 'caching_device': None, 'name': None, 'variable_def': None, 'dtype': None, 'import_scope': None, 'constraint': None, 'synchronization': <VariableSynchronization.AUTO: 0>, 'aggregation': <VariableAggregation.NONE: 0>, 'shape': None}

Call arguments received:
  • inputs=tf.Tensor(shape=(1, 299, 299, 3), dtype=float32)
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)
('Created vars:', [<tf.Variable 'broken_scaling_layer_1/Variable:0' shape=() dtype=float32, numpy=2.0>, <tf.Variable 'broken_scaling_layer_1/bias:0' shape=() dtype=float32, numpy=2.0>])

Вы можете исправить слой, убедившись, что он создает веса только один раз, а затем каждый раз использует их повторно.

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 , в результате чего автоматически сгенерированные имена слоев увеличиваются в каждый вызов модели. Исправьте это, поместив именованный 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())
/tmpfs/src/tf_docs_env/lib/python3.7/site-packages/tensorflow/python/keras/engine/base_layer_v1.py:1694: UserWarning: `layer.apply` is deprecated and will be removed in a future version. Please use `layer.__call__` method instead.
  warnings.warn('`layer.apply` is deprecated and '

Затем сделайте то же самое для слоя с оболочкой в ​​TF2. Обратите внимание, что модель также вызывается несколько раз перед захватом весов. Это сделано для эффективного тестирования повторного использования переменных.

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}
2021-12-04 02:27:27.209445: W tensorflow/python/util/util.cc:368] Sets are not currently considered sequences, but this may change in the future, so consider avoiding using them.
# 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

Слой InceptionResnetV2 на основе прокладок проходит этот тест. Однако в случае, если они не совпадают, вы можете запустить их через diff (текстовый или другой), чтобы увидеть, в чем различия.

Это может дать представление о том, какая часть модели ведет себя не так, как ожидалось. При активном выполнении вы можете использовать pdb, интерактивную отладку и точки останова, чтобы копаться в тех частях модели, которые кажутся подозрительными, и более глубоко отлаживать то, что идет не так.

Исправление проблем

  • Обратите особое внимание на имена любых переменных, созданных непосредственно явными tf.Variable и слоями/моделями Keras, так как семантика генерации имен их переменных может немного отличаться между графами TF1.x и функциями TF2, такими как нетерпеливое выполнение и tf.function , даже если все остальное работает нормально. Если это ваш случай, скорректируйте тест, чтобы учесть любую немного отличающуюся семантику именования.

  • Иногда вы можете обнаружить, что tf.Variable s, tf.keras.layers.Layer s или tf.keras.Model s, созданные в прямом проходе вашего цикла обучения, отсутствуют в вашем списке переменных TF2, даже если они были захвачены коллекцией переменных. в TF1.x. Исправьте это, назначив переменные/слои/модели, которые создает ваш прямой проход, атрибутам экземпляра в вашей модели. См. здесь для получения дополнительной информации.

Шаг 3: Сбросить все переменные, проверить числовую эквивалентность с отключенной случайностью

Следующим шагом является проверка числовой эквивалентности как для фактических выходных данных, так и для отслеживания потерь регуляризации, когда вы фиксируете модель таким образом, чтобы не было задействовано генерирование случайных чисел (например, во время логического вывода).

Точный способ сделать это может зависеть от вашей конкретной модели, но в большинстве моделей (например, в этой) вы можете сделать это следующим образом:

  1. Инициализация весов одним и тем же значением без случайности. Это можно сделать, сбросив их на фиксированное значение после их создания.
  2. Запуск модели в режиме логического вывода, чтобы избежать срабатывания каких-либо выпадающих слоев, которые могут быть источниками случайности.

Следующий код демонстрирует, как таким образом можно сравнить результаты 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]
Regularization loss: 0.001182976
array([0.00299837, 0.00299837, 0.00299837, 0.00299837, 0.00299837],
      dtype=float32)

Получите результаты 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]
Regularization loss: tf.Tensor(0.0011829757, shape=(), dtype=float32)
<tf.Tensor: shape=(5,), dtype=float32, numpy=
array([0.00299837, 0.00299837, 0.00299837, 0.00299837, 0.00299837],
      dtype=float32)>
# 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, когда вы удаляете источники случайности, и слой InceptionResnetV2 , совместимый с TF2, проходит тест.

Если вы наблюдаете расхождение результатов для ваших собственных моделей, вы можете использовать печать или pdb и интерактивную отладку, чтобы определить, где и почему результаты начинают расходиться. Нетерпеливое исполнение может сделать это значительно проще. Вы также можете использовать метод абляции, чтобы запускать только небольшие части модели на фиксированных промежуточных входных данных и изолировать места, где происходит расхождение.

Удобно, что многие тонкие сети (и другие модели) также предоставляют промежуточные конечные точки, которые вы можете исследовать.

Шаг 4. Выровняйте генерацию случайных чисел, проверьте числовую эквивалентность как при обучении, так и при выводе

Последним шагом является проверка того, что модель TF2 численно совпадает с моделью TF1.x, даже с учетом генерации случайных чисел при инициализации переменных и в самом прямом проходе (например, выпадение слоев во время прямого прохода).

Вы можете сделать это, используя приведенный ниже инструмент тестирования, чтобы семантика генерации случайных чисел соответствовала графикам/сессиям TF1.x и нетерпеливому выполнению.

Устаревшие графы/сессии TF1 и активное выполнение TF2 используют различную семантику генерации случайных чисел с отслеживанием состояния.

В tf.compat.v1.Session s, если начальные числа не указаны, генерация случайных чисел зависит от того, сколько операций находится в графе на момент добавления случайной операции и сколько раз граф запускается. При активном выполнении генерация случайных чисел с отслеживанием состояния зависит от глобального начального числа, случайного начального числа операции и того, сколько раз выполняется операция с операцией с заданным случайным начальным числом. См. tf.random.set_seed для получения дополнительной информации.

Следующий класс v1.keras.utils.DeterministicRandomTestTool предоставляет диспетчер контекста scope() , который может заставить случайные операции с отслеживанием состояния использовать одно и то же начальное значение как для графов/сеансов TF1, так и для активного выполнения.

Инструмент предоставляет два режима тестирования:

  1. constant , которая использует одно и то же семя для каждой отдельной операции, независимо от того, сколько раз она была вызвана и,
  2. num_random_ops , который использует количество ранее наблюдаемых случайных операций с отслеживанием состояния в качестве начального значения операции.

Это относится как к случайным операциям с отслеживанием состояния, используемым для создания и инициализации переменных, так и к случайным операциям с отслеживанием состояния, используемым в вычислениях (например, для выпадающих слоев).

Сгенерируйте три случайных тензора, чтобы показать, как использовать этот инструмент для согласования генерации случайных чисел с отслеживанием состояния между сеансами и нетерпеливым выполнением.

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
(array([[2.5063772],
        [2.7488918],
        [1.4839486]], dtype=float32),
 array([[2.5063772, 2.7488918, 1.4839486],
        [1.5633398, 2.1358476, 1.3693532],
        [0.3598416, 1.8287641, 2.5314465]], dtype=float32),
 array([[2.5063772, 2.7488918, 1.4839486],
        [1.5633398, 2.1358476, 1.3693532],
        [0.3598416, 1.8287641, 2.5314465]], dtype=float32))
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
(<tf.Tensor: shape=(3, 1), dtype=float32, numpy=
 array([[2.5063772],
        [2.7488918],
        [1.4839486]], dtype=float32)>,
 <tf.Tensor: shape=(3, 3), dtype=float32, numpy=
 array([[2.5063772, 2.7488918, 1.4839486],
        [1.5633398, 2.1358476, 1.3693532],
        [0.3598416, 1.8287641, 2.5314465]], dtype=float32)>,
 <tf.Tensor: shape=(3, 3), dtype=float32, numpy=
 array([[2.5063772, 2.7488918, 1.4839486],
        [1.5633398, 2.1358476, 1.3693532],
        [0.3598416, 1.8287641, 2.5314465]], dtype=float32)>)
# 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 режиме, поскольку b и c были сгенерированы с одним и тем же начальным числом и имеют одинаковую форму, они будут иметь точно такие же значения.

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
(array([[2.5063772],
        [2.7488918],
        [1.4839486]], dtype=float32),
 array([[0.45038545, 1.9197761 , 2.4536333 ],
        [1.0371652 , 2.9898582 , 1.924583  ],
        [0.25679827, 1.6579313 , 2.8418403 ]], dtype=float32),
 array([[2.9634383 , 1.0862181 , 2.6042497 ],
        [0.70099247, 2.3920312 , 1.0470468 ],
        [0.18173039, 0.8359269 , 1.0508587 ]], dtype=float32))
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
(<tf.Tensor: shape=(3, 1), dtype=float32, numpy=
 array([[2.5063772],
        [2.7488918],
        [1.4839486]], dtype=float32)>,
 <tf.Tensor: shape=(3, 3), dtype=float32, numpy=
 array([[0.45038545, 1.9197761 , 2.4536333 ],
        [1.0371652 , 2.9898582 , 1.924583  ],
        [0.25679827, 1.6579313 , 2.8418403 ]], dtype=float32)>,
 <tf.Tensor: shape=(3, 3), dtype=float32, numpy=
 array([[2.9634383 , 1.0862181 , 2.6042497 ],
        [0.70099247, 2.3920312 , 1.0470468 ],
        [0.18173039, 0.8359269 , 1.0508587 ]], dtype=float32)>)
# 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())

Чтобы разрешить отладку вариаций из-за порядка трассировки, num_random_ops DeterministicRandomTestTool увидеть, сколько случайных операций было отслежено с помощью свойства 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)
0
1
2

Если вам нужно учитывать различный порядок трассировки в ваших тестах, вы даже можете явно установить автоинкрементное значение 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)
0
1

Тем не менее, 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)
This `DeterministicRandomTestTool` object is trying to re-use the already-used operation seed 1. It cannot guarantee random numbers will match between eager and sessions when an operation seed is reused. You most likely set `operation_seed` explicitly but used a value that caused the naturally-incrementing operation seed sequences to overlap with an already-used seed.

Проверка вывода

Теперь вы можете использовать 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)
Regularization loss: 1.2254326
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)
Regularization loss: tf.Tensor(1.2254325, shape=(), dtype=float32)
# 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 работает для всех случайных операций с отслеживанием состояния (включая как инициализацию весов, так и вычисления, такие как выпадение слоев), вы можете использовать его для проверки соответствия моделей и в режиме обучения. Вы снова можете использовать режим 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)
WARNING:tensorflow:From /tmpfs/src/tf_docs_env/lib/python3.7/site-packages/keras/layers/normalization/batch_normalization.py:532: _colocate_with (from tensorflow.python.framework.ops) is deprecated and will be removed in a future version.
Instructions for updating:
Colocations handled automatically by placer.
Regularization loss: 1.22548
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)
Regularization loss: tf.Tensor(1.2254798, shape=(), dtype=float32)
# 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)

Теперь вы убедились, что модель InceptionResnetV2 , активно работающая с декораторами вокруг tf.keras.layers.Layer численно соответствует тонкой сети, работающей в графах и сеансах TF1.

Например, прямой вызов слоя InceptionResnetV2 с параметром training=True чередует инициализацию переменных с порядком исключения в соответствии с порядком создания сети.

С другой стороны, сначала поместить декоратор tf.keras.layers.Layer в функциональную модель Keras и только затем вызвать модель с training=True , что эквивалентно инициализации всех переменных, а затем использованию выпадающего слоя. Это создает другой порядок трассировки и другой набор случайных чисел.

Однако режим по умолчанию 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)
Regularization loss: 1.2239965
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)
/tmpfs/src/tf_docs_env/lib/python3.7/site-packages/tensorflow/python/keras/engine/base_layer.py:1345: UserWarning: `layer.updates` will be removed in a future version. This property should not be used in TensorFlow 2.0, as `updates` are applied automatically.
  warnings.warn('`layer.updates` will be removed in a future version. '
/tmpfs/src/tf_docs_env/lib/python3.7/site-packages/keras/legacy_tf_layers/base.py:573: UserWarning: `layer.updates` will be removed in a future version. This property should not be used in TensorFlow 2.0, as `updates` are applied automatically.
  _add_elements_to_collection(self.updates, tf.compat.v1.GraphKeys.UPDATE_OPS)
Regularization loss: tf.Tensor(1.2239964, shape=(), dtype=float32)
# 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 с вычислениями TF2, происходящими внутри tf.function (которая применяет проходы оптимизации графа, как ваш граф TF1), а не с чисто нетерпеливыми вычислениями. В качестве альтернативы вы можете попробовать использовать tf.config.optimizer.set_experimental_options , чтобы отключить проходы оптимизации, такие как "arithmetic_optimization" , перед вашими вычислениями TF1, чтобы увидеть, окажется ли результат численно ближе к вашим результатам вычислений TF2. В ваших реальных тренировочных прогонах рекомендуется использовать tf.function с включенными проходами оптимизации из соображений производительности, но вы можете счесть полезным отключить их в своих модульных тестах числовой эквивалентности.

Точно так же вы можете также обнаружить, что оптимизаторы tf.compat.v1.train и оптимизаторы TF2 имеют немного разные числовые свойства с плавающей запятой, чем оптимизаторы TF2, даже если математические формулы, которые они представляют, одинаковы. Вероятность того, что это будет проблемой в ваших тренировочных прогонах, меньше, но может потребоваться более высокая числовая погрешность в модульных тестах эквивалентности.