Validación de corrección y equivalencia numérica

Ver en TensorFlow.org Ejecutar en Google Colab Ver en GitHub Descargar libreta

Al migrar su código de TensorFlow de TF1.x a TF2, es una buena práctica asegurarse de que su código migrado se comporte de la misma manera en TF2 que en TF1.x.

Esta guía cubre ejemplos de código de migración con el shim de modelado tf.compat.v1.keras.utils.track_tf1_style_variables aplicado a los métodos tf.keras.layers.Layer . Lea la guía de mapeo de modelos para obtener más información sobre las cuñas de modelado TF2.

Esta guía detalla los enfoques que puede utilizar para:

  • Validar la corrección de los resultados obtenidos de los modelos de entrenamiento utilizando el código migrado
  • Valide la equivalencia numérica de su código en las versiones de TensorFlow

Configuración

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.

Si está colocando una parte no trivial del código de acceso directo en el shim, querrá saber que se está comportando de la misma manera que en TF1.x. Por ejemplo, considere intentar colocar un modelo TF-Slim Inception-Resnet-v2 completo en la cuña como tal:

# 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.

Da la casualidad de que esta capa en realidad funciona perfectamente bien desde el primer momento (con un seguimiento preciso de la pérdida de regularización).

Sin embargo, esto no es algo que quieras dar por sentado. Siga los pasos a continuación para verificar que realmente se está comportando como lo hizo en TF1.x, hasta observar la equivalencia numérica perfecta. Estos pasos también pueden ayudarlo a triangular qué parte del pase hacia adelante está causando una divergencia de TF1.x (identifique si la divergencia surge en el modelo de pase hacia adelante en oposición a una parte diferente del modelo).

Paso 1: Verifique que las variables solo se creen una vez

Lo primero que debe verificar es que haya construido correctamente el modelo de una manera que reutilice las variables en cada llamada en lugar de crear y usar accidentalmente nuevas variables cada vez. Por ejemplo, si su modelo crea una nueva capa de Keras o llama a tf.Variable en cada llamada de avance, lo más probable es que no capture las variables y cree otras nuevas cada vez.

A continuación se muestran dos ámbitos del administrador de contexto que puede usar para detectar cuándo su modelo está creando nuevas variables y depurar qué parte del modelo lo está haciendo.

@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)

El primer ámbito ( assert_no_variable_creations() ) generará un error inmediatamente una vez que intente crear una variable dentro del ámbito. Esto le permite inspeccionar el seguimiento de la pila (y usar la depuración interactiva) para averiguar exactamente qué líneas de código crearon una variable en lugar de reutilizar una existente.

El segundo ámbito ( catch_and_raise_created_variables() ) generará una excepción al final del ámbito si se crea alguna variable. Esta excepción incluirá la lista de todas las variables creadas en el ámbito. Esto es útil para averiguar cuál es el conjunto de todos los pesos que está creando su modelo en caso de que pueda detectar patrones generales. Sin embargo, es menos útil para identificar las líneas exactas de código donde se crearon esas variables.

Utilice ambos ámbitos a continuación para verificar que la capa InceptionResnetV2 basada en shim no cree ninguna variable nueva después de la primera llamada (presumiblemente reutilizándolas).

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 '

En el siguiente ejemplo, observe cómo estos decoradores trabajan en una capa que crea incorrectamente nuevos pesos cada vez en lugar de reutilizar los existentes.

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>])

Puede arreglar la capa asegurándose de que solo cree los pesos una vez y luego los reutilice cada vez.

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)

Solución de problemas

Aquí hay algunas razones comunes por las que su modelo podría estar creando accidentalmente nuevos pesos en lugar de reutilizar los existentes:

  1. Utiliza una llamada tf.Variable explícita sin reutilizar las tf.Variables ya creadas. Solucione esto comprobando primero si no se ha creado y luego reutilizando los existentes.
  2. Crea una capa o modelo de Keras directamente en el pase hacia adelante cada vez (a diferencia de tf.compat.v1.layers ). Solucione esto comprobando primero si no se ha creado y luego reutilizando los existentes.
  3. Está construido sobre tf.compat.v1.layers pero no asigna a todas las compat.v1.layers un nombre explícito o envuelve el uso de compat.v1.layer dentro de un variable_scope con nombre, lo que hace que los nombres de capa generados automáticamente aumenten en cada llamada de modelo. Solucione esto colocando un tf.compat.v1.variable_scope con nombre dentro de su método decorado con cuñas que envuelve todo su uso de tf.compat.v1.layers .

Paso 2: verifique que los recuentos, nombres y formas de las variables coincidan

El segundo paso es asegurarse de que su capa que se ejecuta en TF2 cree la misma cantidad de pesos, con las mismas formas, que el código correspondiente en TF1.x.

Puede hacer una combinación de verificarlos manualmente para ver que coincidan y hacer los controles mediante programación en una prueba de unidad como se muestra a continuación.

# 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 '

A continuación, haga lo mismo con la capa envuelta con cuñas en TF2. Tenga en cuenta que el modelo también se llama varias veces antes de agarrar los pesos. Esto se hace para probar efectivamente la reutilización de variables.

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

La capa InceptionResnetV2 basada en shim pasa esta prueba. Sin embargo, en el caso de que no coincidan, puede ejecutarlo a través de un diff (texto u otro) para ver dónde están las diferencias.

Esto puede proporcionar una pista sobre qué parte del modelo no se comporta como se esperaba. Con una ejecución entusiasta, puede usar pdb, depuración interactiva y puntos de interrupción para profundizar en las partes del modelo que parecen sospechosas y depurar lo que está fallando con mayor profundidad.

Solución de problemas

  • Preste mucha atención a los nombres de las variables creadas directamente por llamadas tf.Variable explícitas y capas/modelos de Keras, ya que la semántica de generación de nombres de variables puede diferir ligeramente entre los gráficos TF1.x y la funcionalidad TF2, como ejecución entusiasta y tf.function incluso si todo más está funcionando correctamente. Si este es su caso, ajuste su prueba para tener en cuenta cualquier semántica de nombres ligeramente diferente.

  • A veces puede encontrar que los tf.Variable s, tf.keras.layers.Layer s o tf.keras.Model s creados en el paso hacia adelante de su ciclo de entrenamiento no están en su lista de variables TF2, incluso si fueron capturados por la colección de variables en TF1.x. Solucione esto asignando las variables/capas/modelos que crea su pase hacia adelante a los atributos de instancia en su modelo. Vea aquí para más información.

Paso 3: restablezca todas las variables, verifique la equivalencia numérica con toda la aleatoriedad deshabilitada

El siguiente paso es verificar la equivalencia numérica tanto para las salidas reales como para el seguimiento de la pérdida de regularización cuando corrige el modelo de modo que no haya una generación de números aleatorios involucrada (como durante la inferencia).

La forma exacta de hacer esto puede depender de su modelo específico, pero en la mayoría de los modelos (como este), puede hacerlo de la siguiente manera:

  1. Inicializar los pesos al mismo valor sin aleatoriedad. Esto se puede hacer restableciéndolos a un valor fijo después de haberlos creado.
  2. Ejecutar el modelo en modo de inferencia para evitar activar capas de exclusión que pueden ser fuentes de aleatoriedad.

El siguiente código demuestra cómo puede comparar los resultados de TF1.x y TF2 de esta manera.

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)

Obtenga los resultados de 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)

Los números coinciden entre TF1.x y TF2 cuando elimina las fuentes de aleatoriedad, y la capa InceptionResnetV2 compatible con TF2 pasa la prueba.

Si observa que los resultados divergen para sus propios modelos, puede usar la impresión o pdb y la depuración interactiva para identificar dónde y por qué los resultados comienzan a divergir. La ejecución ansiosa puede hacer que esto sea significativamente más fácil. También puede usar un enfoque de ablación para ejecutar solo pequeñas partes del modelo en entradas intermedias fijas y aislar dónde ocurre la divergencia.

Convenientemente, muchas redes delgadas (y otros modelos) también exponen puntos finales intermedios que puede sondear.

Paso 4: alinee la generación de números aleatorios, verifique la equivalencia numérica tanto en el entrenamiento como en la inferencia

El paso final es verificar que el modelo TF2 coincida numéricamente con el modelo TF1.x, incluso cuando se tenga en cuenta la generación de números aleatorios en la inicialización de variables y en el paso hacia adelante en sí mismo (como las capas eliminadas durante el paso hacia adelante).

Puede hacer esto utilizando la herramienta de prueba a continuación para hacer que la semántica de generación de números aleatorios coincida entre los gráficos/sesiones de TF1.x y la ejecución entusiasta.

Los gráficos/sesiones heredados de TF1 y la ejecución entusiasta de TF2 utilizan diferentes semánticas de generación de números aleatorios con estado.

En tf.compat.v1.Session s, si no se especifican semillas, la generación de números aleatorios depende de cuántas operaciones hay en el gráfico en el momento en que se agrega la operación aleatoria y cuántas veces se ejecuta el gráfico. En la ejecución ansiosa, la generación de números aleatorios con estado depende de la semilla global, la semilla aleatoria de la operación y cuántas veces se ejecuta la operación con la operación con la semilla aleatoria dada. Consulte tf.random.set_seed para obtener más información.

La siguiente clase v1.keras.utils.DeterministicRandomTestTool proporciona un scope() que puede hacer que las operaciones aleatorias con estado usen la misma semilla en gráficos/sesiones de TF1 y una ejecución entusiasta.

La herramienta proporciona dos modos de prueba:

  1. constant que usa la misma semilla para cada operación sin importar cuántas veces se haya llamado y,
  2. num_random_ops que usa el número de operaciones aleatorias con estado observadas previamente como semilla de operación.

Esto se aplica tanto a las operaciones aleatorias con estado utilizadas para crear e inicializar variables, como a las operaciones aleatorias con estado utilizadas en el cálculo (como las capas de abandono).

Genere tres tensores aleatorios para mostrar cómo usar esta herramienta para hacer que la generación de números aleatorios con estado coincida entre sesiones y una ejecución ansiosa.

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)

Sin embargo, observe que en modo constant , debido a que b y c se generaron con la misma semilla y tienen la misma forma, tendrán exactamente los mismos valores.

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

Orden de seguimiento

Si le preocupa que algunos números aleatorios coincidan en modo constant y reduzcan su confianza en su prueba de equivalencia numérica (por ejemplo, si varios pesos toman las mismas inicializaciones), puede usar el modo num_random_ops para evitar esto. En el modo num_random_ops , los números aleatorios generados dependerán del orden de las operaciones aleatorias en el programa.

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)

Sin embargo, tenga en cuenta que en este modo la generación aleatoria es sensible al orden del programa, por lo que los siguientes números aleatorios generados no coinciden.

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())

Para permitir variaciones de depuración debido al orden de seguimiento, DeterministicRandomTestTool en modo num_random_ops le permite ver cuántas operaciones aleatorias se han rastreado con la propiedad 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

Si necesita tener en cuenta el orden de seguimiento variable en sus pruebas, incluso puede configurar la operation_seed de incremento automático explícitamente. Por ejemplo, puede usar esto para hacer que la generación de números aleatorios coincida en dos órdenes de programa diferentes.

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

Sin embargo, DeterministicRandomTestTool no permite reutilizar semillas de operaciones ya utilizadas, así que asegúrese de que las secuencias de incremento automático no se superpongan. Esto se debe a que la ejecución ansiosa genera números diferentes para los usos posteriores de la misma semilla de operación, mientras que los gráficos y las sesiones de TF1 no lo hacen, por lo que generar un error ayuda a mantener en línea la sesión y la generación ansiosa de números aleatorios con estado.

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.

Verificación de la inferencia

Ahora puede usar DeterministicRandomTestTool para asegurarse de que el modelo InceptionResnetV2 coincida en la inferencia, incluso cuando use la inicialización de ponderación aleatoria. Para una condición de prueba más fuerte debido al orden coincidente del programa, use el modo 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)

Verificación de la formación

Debido a que DeterministicRandomTestTool funciona para todas las operaciones aleatorias con estado (incluidas la inicialización y el cálculo de peso, como las capas de abandono), también puede usarla para verificar que los modelos coincidan en el modo de entrenamiento. Puede volver a utilizar el modo num_random_ops porque el orden del programa de las operaciones aleatorias con estado coincide.

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)

Ahora ha verificado que el modelo InceptionResnetV2 que se ejecuta con entusiasmo con los decoradores alrededor tf.keras.layers.Layer coincide numéricamente con la red delgada que se ejecuta en gráficos y sesiones TF1.

Por ejemplo, llamar a la capa InceptionResnetV2 directamente con training=True intercala la inicialización de la variable con el orden de abandono según el orden de creación de la red.

Por otro lado, primero poner el decorador tf.keras.layers.Layer en un modelo funcional de Keras y solo luego llamar al modelo con training=True es equivalente a inicializar todas las variables y luego usar la capa de abandono. Esto produce un orden de seguimiento diferente y un conjunto diferente de números aleatorios.

Sin embargo, el mode='constant' no es sensible a estas diferencias en el orden de seguimiento y pasará sin trabajo adicional incluso cuando se incrusta la capa en un modelo funcional de 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)

Paso 3b o 4b (opcional): Prueba con puntos de control preexistentes

Después del paso 3 o el paso 4 anterior, puede ser útil ejecutar sus pruebas de equivalencia numérica al comenzar desde puntos de control basados ​​en nombres preexistentes, si tiene algunos. Esto puede probar que la carga del punto de control heredado funciona correctamente y que el modelo en sí funciona correctamente. La guía Reutilización de puntos de control TF1.x explica cómo reutilizar sus puntos de control TF1.x preexistentes y transferirlos a puntos de control TF2.

Pruebas adicionales y resolución de problemas

A medida que agregue más pruebas de equivalencia numérica, también puede optar por agregar una prueba que verifique la coincidencia de su cálculo de gradiente (o incluso las actualizaciones de su optimizador).

La retropropagación y el cálculo de gradientes son más propensos a las inestabilidades numéricas de punto flotante que los pases hacia adelante del modelo. Esto significa que a medida que sus pruebas de equivalencia cubran más partes no aisladas de su entrenamiento, puede comenzar a ver diferencias numéricas no triviales entre correr con entusiasmo y sus gráficos TF1. Esto puede deberse a las optimizaciones de gráficos de TensorFlow que hacen cosas como reemplazar subexpresiones en un gráfico con menos operaciones matemáticas.

Para aislar si es probable que este sea el caso, puede comparar su código TF1 con el cálculo TF2 que ocurre dentro de una tf.function (que aplica pases de optimización de gráficos como su gráfico TF1) en lugar de un cálculo puramente ansioso. Alternativamente, puede intentar usar tf.config.optimizer.set_experimental_options para deshabilitar pases de optimización como "arithmetic_optimization" antes de su cálculo TF1 para ver si el resultado termina numéricamente más cerca de sus resultados de cálculo TF2. En sus carreras de entrenamiento reales, se recomienda que use tf.function con los pases de optimización habilitados por razones de rendimiento, pero puede resultarle útil deshabilitarlos en sus pruebas unitarias de equivalencia numérica.

De manera similar, también puede encontrar que los optimizadores tf.compat.v1.train y los optimizadores TF2 tienen propiedades numéricas de coma flotante ligeramente diferentes a las de los optimizadores TF2, incluso si las fórmulas matemáticas que representan son las mismas. Es menos probable que esto sea un problema en sus carreras de entrenamiento, pero puede requerir una mayor tolerancia numérica en las pruebas de unidad de equivalencia.