ML Community Day è il 9 novembre! Unisciti a noi per gli aggiornamenti da tensorflow, JAX, e più Per saperne di più

Convalida della correttezza e dell'equivalenza numerica

Visualizza su TensorFlow.org Esegui in Google Colab Visualizza su GitHub Scarica taccuino

Quando si esegue la migrazione del codice TensorFlow da TF1.x a TF2, è buona norma assicurarsi che il codice migrato si comporti allo stesso modo in TF2 come in TF1.x.

Questo esempio di codice copertine guida migrazione con i tf.compat.v1.keras.utils.track_tf1_style_variables modellazione shim applicati ai tf.keras.layers.Layer metodi. Leggi la Guida al modello di mappatura per saperne di più circa la TF2 modellazione spessori.

Questa guida descrive in dettaglio gli approcci che puoi utilizzare per:

  • Convalidare la correttezza dei risultati ottenuti dai modelli di addestramento utilizzando il codice migrato
  • Convalida l'equivalenza numerica del tuo codice tra le versioni di TensorFlow

Impostare

pip uninstall -y -q tensorflow
# Install tf-nightly as the model mapping shim is available only in
# TensorFlow 2.7
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 unittest import mock

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: 2921, done.[K
remote: Counting objects: 100% (2921/2921), done.[K
remote: Compressing objects: 100% (2452/2452), done.[K
remote: Total 2921 (delta 742), reused 1280 (delta 431), pack-reused 0[K
Receiving objects: 100% (2921/2921), 32.97 MiB | 16.84 MiB/s, done.
Resolving deltas: 100% (742/742), done.

Se stai inserendo un pezzo non banale di codice di passaggio in avanti nello shim, vuoi sapere che si comporta allo stesso modo di TF1.x. Ad esempio, considera di provare a inserire un intero modello TF-Slim Inception-Resnet-v2 nello shim in quanto tale:

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

Si dà il caso che questo livello in realtà funzioni perfettamente fuori dagli schemi (completo di un accurato monitoraggio della perdita di regolarizzazione).

Tuttavia, questo non è qualcosa che vuoi dare per scontato. Seguire i passaggi seguenti per verificare che si stia effettivamente comportando come in TF1.x, fino all'osservazione della perfetta equivalenza numerica. Questi passaggi possono anche aiutarti a triangolare quale parte del passaggio in avanti sta causando una divergenza da TF1.x (identifica se la divergenza si verifica nel passaggio in avanti del modello rispetto a una parte diversa del modello).

Passaggio 1: verifica che le variabili vengano create solo una volta

La prima cosa che dovresti verificare è di aver costruito correttamente il modello in modo da riutilizzare le variabili in ogni chiamata piuttosto che creare e utilizzare accidentalmente nuove variabili ogni volta. Ad esempio, se il modello crea un nuovo livello Keras o chiama tf.Variable in ogni chiamata in avanti, allora è molto probabile che non riuscendo a variabili di cattura e crearne di nuovi ogni volta.

Di seguito sono riportati due ambiti di gestione del contesto che è possibile utilizzare per rilevare quando il modello sta creando nuove variabili ed eseguire il debug di quale parte del modello lo sta facendo.

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

La prima portata ( assert_no_variable_creations() ) genererà immediatamente un errore una volta che si tenta di creare una variabile nell'ambito di applicazione. Ciò consente di ispezionare lo stacktrace (e utilizzare il debug interattivo) per capire esattamente quali righe di codice hanno creato una variabile invece di riutilizzarne una esistente.

Il secondo ambito ( catch_and_raise_created_variables() ) genererà un'eccezione alla fine del campo di applicazione, se le variabili finito per essere creati. Questa eccezione includerà l'elenco di tutte le variabili create nell'ambito. Questo è utile per capire qual è l'insieme di tutti i pesi che il tuo modello sta creando nel caso in cui tu possa individuare modelli generali. Tuttavia, è meno utile per identificare le righe esatte di codice in cui sono state create tali variabili.

Utilizzare entrambi gli ambiti di seguito per verificare che il livello InceptionResnetV2 basato su shim non crei nuove variabili dopo la prima chiamata (presumibilmente riutilizzandole).

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/tensorflow/python/keras/legacy_tf_layers/core.py:336: 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 '

Nell'esempio seguente, osserva come funzionano questi decoratori su un livello che crea erroneamente nuovi pesi ogni volta invece di riutilizzare quelli esistenti.

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_13532/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_13532/3224979076.py", line 6, in call
    var = tf.Variable(initial_value=2.0)
  File "/tmp/ipykernel_13532/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>])

Puoi correggere il livello assicurandoti che crei i pesi solo una volta e poi li riutilizzi ogni volta.

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)

Risoluzione dei problemi

Ecco alcuni motivi comuni per cui il tuo modello potrebbe accidentalmente creare nuovi pesi invece di riutilizzare quelli esistenti:

  1. Si avvale di un esplicito tf.Variable chiamata senza riutilizzare già creati tf.Variables . Risolvi il problema controllando prima se non è stato creato, quindi riutilizzando quelli esistenti.
  2. Crea uno strato Keras o modello direttamente in avanti passare ogni volta (al contrario di tf.compat.v1.layers ). Risolvi il problema controllando prima se non è stato creato, quindi riutilizzando quelli esistenti.
  3. E 'costruito in cima tf.compat.v1.layers , ma non riesce ad assegnare tutti compat.v1.layers un nome esplicito o per avvolgere il vostro compat.v1.layer all'interno utilizzo di un nome variable_scope , causando i nomi dei livelli generati automaticamente per incremento in ogni modello di chiamata. Risolvere il mettendo un nome tf.compat.v1.variable_scope all'interno del vostro metodo di shim-decorati che avvolge tutto il vostro tf.compat.v1.layers utilizzo.

Passaggio 2: verifica che i conteggi delle variabili, i nomi e le forme corrispondano

Il secondo passaggio consiste nell'assicurarsi che il livello in esecuzione in TF2 crei lo stesso numero di pesi, con le stesse forme, del codice corrispondente in TF1.x.

Puoi eseguire una combinazione di controlli manuali per verificare che corrispondano e di eseguire i controlli a livello di codice in un test unitario come mostrato di seguito.

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

Quindi, fai lo stesso per il livello avvolto in spessori in TF2. Notare che il modello viene anche chiamato più volte prima di afferrare i pesi. Questo viene fatto per testare efficacemente il riutilizzo delle variabili.

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-09-22 22:18:46.944968: 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

Il livello InceptionResnetV2 basato su shim supera questo test. Tuttavia, nel caso in cui non corrispondano, puoi eseguire un diff (testo o altro) per vedere dove sono le differenze.

Questo può fornire un indizio su quale parte del modello non si comporta come previsto. Con l'esecuzione entusiasta puoi utilizzare pdb, debug interattivo e punti di interruzione per scavare nelle parti del modello che sembrano sospette ed eseguire il debug di ciò che non va in modo più approfondito.

Risoluzione dei problemi

  • Prestare molta attenzione ai nomi delle variabili create direttamente dagli espliciti tf.Variable chiamate e strati KERAS / modelli come loro semantica generazione nome variabile possono variare lievemente tra grafici TF1.x e funzionalità TF2 quali l'esecuzione ansioso e tf.function anche se tutto altro funziona correttamente. Se questo è il tuo caso, modifica il test per tenere conto di qualsiasi semantica di denominazione leggermente diversa.

  • A volte si potrebbe scoprire che il tf.Variable s, tf.keras.layers.Layer s, o tf.keras.Model s creata in avanti da parte del ciclo di formazione sono mancanti dall'elenco variabili TF2 anche se sono stati catturati dalla collezione variabili in TF1.x. Risolvi il problema assegnando le variabili/livelli/modelli che il tuo passaggio in avanti crea agli attributi di istanza nel tuo modello. Vedi qui per maggiori informazioni.

Passaggio 3: ripristina tutte le variabili, controlla l'equivalenza numerica con tutte le casualità disabilitate

Il passaggio successivo consiste nel verificare l'equivalenza numerica sia per gli output effettivi che per il tracciamento della perdita di regolarizzazione quando si corregge il modello in modo tale che non sia coinvolta la generazione di numeri casuali (ad esempio durante l'inferenza).

Il modo esatto per farlo può dipendere dal tuo modello specifico, ma nella maggior parte dei modelli (come questo), puoi farlo:

  1. Inizializzazione dei pesi allo stesso valore senza casualità. Questo può essere fatto reimpostandoli su un valore fisso dopo che sono stati creati.
  2. Esecuzione del modello in modalità inferenza per evitare di attivare eventuali livelli di abbandono che possono essere fonti di casualità.

Il codice seguente mostra come confrontare i risultati di TF1.xe TF2 in questo modo.

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)

Ottieni i risultati di 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)

I numeri corrispondono tra TF1.x e TF2 quando si rimuovono le fonti di casualità, e la TF2-compatibile InceptionResnetV2 strato passa il test.

Se stai osservando che i risultati divergono per i tuoi modelli, puoi utilizzare la stampa o il pdb e il debug interattivo per identificare dove e perché i risultati iniziano a divergere. L'esecuzione appassionata può rendere questo significativamente più semplice. È inoltre possibile utilizzare un approccio di ablazione per eseguire solo piccole porzioni del modello su input intermedi fissi e isolare dove si verifica la divergenza.

Convenientemente, molte reti sottili (e altri modelli) espongono anche endpoint intermedi che è possibile sondare.

Passaggio 4: allinea la generazione di numeri casuali, controlla l'equivalenza numerica sia nell'addestramento che nell'inferenza

Il passaggio finale consiste nel verificare che il modello TF2 corrisponda numericamente al modello TF1.x, anche quando si tiene conto della generazione di numeri casuali nell'inizializzazione della variabile e nel passaggio in avanti stesso (come i livelli di abbandono durante il passaggio in avanti).

Puoi farlo utilizzando lo strumento di test di seguito per far corrispondere la semantica della generazione di numeri casuali tra i grafici/sessioni TF1.x e l'esecuzione ansiosa.

I grafici/sessioni legacy di TF1 e l'esecuzione desiderosa di TF2 utilizzano semantiche di generazione di numeri casuali stateful diverse.

In tf.compat.v1.Session s, se non sono specificati semi, la generazione di numeri casuali dipende dal numero di operazioni sono nel grafico nel momento in cui viene aggiunto l'operazione casuale, e quante volte il grafico viene eseguito. Nell'esecuzione desiderosa, la generazione di numeri casuali con stato dipende dal seme globale, dal seme casuale dell'operazione e da quante volte viene eseguita l'operazione con l'operazione con il seme casuale specificato. Vedere tf.random.set_seed per maggiori informazioni.

Il seguente DeterministicTestTool oggetto fornisce un contesto gestore scope() che può rendere le operazioni casuali stateful usano lo stesso seme attraverso entrambi i grafici TF1 / sessioni ed esecuzione ansioso,

Lo strumento fornisce due modalità di test:

  1. constant che utilizza lo stesso seme per ogni singola operazione, non importa quante volte è stato chiamato e,
  2. num_random_ops che utilizza il numero di operazioni precedentemente rilevate casuali stateful come il seme operazione.

Questo vale sia per le operazioni casuali stateful utilizzate per creare e inizializzare le variabili, sia per le operazioni casuali stateful utilizzate nel calcolo (come per i livelli dropout).

seed_implementation = sys.modules[tf.compat.v1.get_seed.__module__]

class DeterministicTestTool(object):
  def __init__(self, seed: int = 42, mode='constant'):
    """Set mode to 'constant' or 'num_random_ops'. Defaults to 'constant'."""
    if mode not in {'constant', 'num_random_ops'}:
      raise ValueError("Mode arg must be 'constant' or 'num_random_ops'. " +
                       "Got: {}".format(mode))

    self._mode = mode
    self._seed = seed
    self.operation_seed = 0
    self._observed_seeds = set()

  def scope(self):
    tf.random.set_seed(self._seed)

    def _get_seed(_):
      """Wraps TF get_seed to make deterministic random generation easier.

      This makes a variable's initialization (and calls that involve random
      number generation) depend only on how many random number generations
      were used in the scope so far, rather than on how many unrelated
      operations the graph contains.

      Returns:
        Random seed tuple.
      """
      op_seed = self.operation_seed
      if self._mode == "constant":
        tf.random.set_seed(op_seed)
      else:
        if op_seed in self._observed_seeds:
          raise ValueError(
              'This `DeterministicTestTool` object is trying to re-use the ' +
              'already-used operation seed {}. '.format(op_seed) +
              '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.')

        self._observed_seeds.add(op_seed)
        self.operation_seed += 1

      return (self._seed, op_seed)

    # mock.patch internal symbols to modify the behavior of TF APIs relying on them

    return mock.patch.object(seed_implementation, 'get_seed', wraps=_get_seed)

Genera tre tensori casuali per mostrare come utilizzare questo strumento per creare una corrispondenza di generazione di numeri casuali con stato tra le sessioni e l'esecuzione ansiosa.

random_tool = DeterministicTestTool()
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 = DeterministicTestTool()
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)

Tuttavia, si noti che in constant modalità, poiché b e c sono stati generati con lo stesso seme e hanno la stessa forma, avranno esattamente gli stessi valori.

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

Traccia ordine

Se siete preoccupati per alcuni numeri casuali corrispondenti in constant modalità ridurre la vostra fiducia nel test di equivalenza numerica (per esempio se diversi pesi assumono le stesse inizializzazioni), è possibile utilizzare la num_random_ops modalità per evitare questo. In num_random_ops modalità, i numeri casuali generati dipenderà l'ordinamento dei ops casuali nel programma.

random_tool = DeterministicTestTool(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 = DeterministicTestTool(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)

Tuttavia, si noti che in questa modalità la generazione casuale è sensibile all'ordine del programma, quindi i seguenti numeri casuali generati non corrispondono.

random_tool = DeterministicTestTool(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 = DeterministicTestTool(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())

Per consentire per il debug di variazioni dovute alla fine il rintracciamento, DeterministicTestTool in num_random_ops modalità ti permette di vedere quante operazioni casuali sono state tracciate con la operation_seed proprietà.

random_tool = DeterministicTestTool(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

Se avete bisogno di conto per variare ordine traccia nei test, si può anche impostare l'incremento automatico operation_seed esplicitamente. Ad esempio, puoi usarlo per far corrispondere la generazione di numeri casuali tra due diversi ordini di programma.

random_tool = DeterministicTestTool(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 = DeterministicTestTool(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

Tuttavia, DeterministicTestTool esclude dal finanziamento riutilizzo semi di funzionamento già utilizzati, in modo da assicurarsi che le sequenze di incremento automatico non possono sovrapporsi. Questo perché l'esecuzione desiderosa genera numeri diversi per gli utilizzi successivi dello stesso seme dell'operazione mentre i grafici e le sessioni TF1 no, quindi la generazione di un errore aiuta a mantenere in linea la generazione di numeri casuali con stato desideroso di sessione.

random_tool = DeterministicTestTool(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 `DeterministicTestTool` 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.

Verifica dell'inferenza

È ora possibile utilizzare il DeterministicTestTool per assicurarsi che i InceptionResnetV2 partite modello in deduzione, anche quando si utilizza l'inizializzazione peso casuale. Per una condizione di test forte dovuto alla corrispondenza fine programma, utilizzare il num_random_ops modalità.

random_tool = DeterministicTestTool(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 = DeterministicTestTool(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 DeterministicTestTool:
np.testing.assert_allclose(tf1_regularization_loss, tf2_regularization_loss.numpy(), **tol_dict)
np.testing.assert_allclose(tf1_output, tf2_output.numpy(), **tol_dict)

Verifica della formazione

Perché DeterministicTestTool funziona per tutte le operazioni casuali stateful (compresi sia l'inizializzazione di peso e di calcolo, come strati di abbandono), è possibile utilizzarlo per verificare i modelli corrispondono in modalità allenamento pure. È di nuovo possibile utilizzare il num_random_ops modalità perché l'ordine del programma dei ops casuali stateful partite.

random_tool = DeterministicTestTool(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/tensorflow/python/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 = DeterministicTestTool(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 DeterministicTestTool
np.testing.assert_allclose(tf1_regularization_loss, tf2_regularization_loss.numpy(), **tol_dict)
np.testing.assert_allclose(tf1_output, tf2_output.numpy(), **tol_dict)

Ora avete verificato che InceptionResnetV2 modello di esecuzione con entusiasmo con decoratori intorno tf.keras.layers.Layer corrisponde numericamente la rete sottile in esecuzione in TF1 grafici e le sessioni.

Ad esempio, chiamando InceptionResnetV2 strato direttamente con training=True interfogli inizializzazione delle variabili con l'ordine forcellino secondo l'ordine di creazione rete.

D'altra parte, prima di mettere il tf.keras.layers.Layer decoratore in un modello funzionale Keras e solo allora chiamare il modello con training=True equivale a inizializzare tutte le variabili quindi utilizzando lo strato di abbandono. Ciò produce un diverso ordine di tracciatura e un diverso insieme di numeri casuali.

Tuttavia, il valore predefinito mode='constant' non è sensibile a queste differenze di ordine rintracciamento e passerà senza lavoro aggiuntivo anche quando incorporare il livello in un modello funzionale Keras.

random_tool = DeterministicTestTool()
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 = DeterministicTestTool()
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. '
Regularization loss: tf.Tensor(1.2239964, shape=(), dtype=float32)
# Verify that the regularization loss and output both match
# when using the DeterministicTestTool
np.testing.assert_allclose(tf1_regularization_loss, tf2_regularization_loss.numpy(), **tol_dict)
np.testing.assert_allclose(tf1_output, tf2_output.numpy(), **tol_dict)

Fase 3b o 4b (opzionale): test con checkpoint preesistenti

Dopo il passaggio 3 o 4 sopra, può essere utile eseguire i test di equivalenza numerica quando si parte da checkpoint basati sul nome preesistenti, se ne si dispone. Questo può verificare sia che il caricamento del checkpoint legacy funzioni correttamente sia che il modello stesso funzioni correttamente. I posti di blocco Riutilizzo TF1.x guida viene spiegato come riutilizzare le pre-esistenti posti di blocco TF1.x e trasferirli verso TF2 posti di blocco.

Ulteriori test e risoluzione dei problemi

Man mano che aggiungi altri test di equivalenza numerica, puoi anche scegliere di aggiungere un test che verifica la corrispondenza del calcolo del gradiente (o anche degli aggiornamenti dell'ottimizzatore).

La retropropagazione e il calcolo del gradiente sono più inclini alle instabilità numeriche in virgola mobile rispetto ai passaggi in avanti del modello. Ciò significa che poiché i tuoi test di equivalenza coprono più parti non isolate del tuo allenamento, potresti iniziare a vedere differenze numeriche non banali tra l'esecuzione a pieno ritmo e i tuoi grafici TF1. Ciò può essere causato dalle ottimizzazioni del grafico di TensorFlow che fanno cose come sostituire le sottoespressioni in un grafico con meno operazioni matematiche.

Per isolare se questo è probabile che sia il caso, è possibile confrontare il codice TF1 per TF2 calcolo accadendo all'interno di una tf.function (che si applica l'ottimizzazione grafico passa come il grafico TF1), piuttosto che a un calcolo puramente ansioso. In alternativa, si può provare a utilizzare tf.config.optimizer.set_experimental_options all'ottimizzazione disabilitare passa come "arithmetic_optimization" prima che il calcolo TF1 per vedere se il risultato finisce numericamente più vicino ai vostri TF2 risultati di calcolo. Nei tuoi allenamenti effettivi si consiglia di utilizzare tf.function con l'ottimizzazione valichi abilitato per motivi di prestazioni, ma si possono trovare utile disattivare loro il test di unità di equivalenza numerica.

Allo stesso modo, si può anche trovare che tf.compat.v1.train ottimizzatori e ottimizzatori TF2 hanno proprietà leggermente diverse NUMERICI virgola mobile di TF2 ottimizzatori, anche se le formule matematiche che essi rappresentano sono la stessa cosa. È meno probabile che questo sia un problema nelle sessioni di allenamento, ma potrebbe richiedere una tolleranza numerica più elevata nei test unitari di equivalenza.