Convalida della correttezza e dell'equivalenza numerica

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

Quando si migra il 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.

Questa guida illustra esempi di codice di migrazione con lo spessore di modellazione tf.compat.v1.keras.utils.track_tf1_style_variables applicato ai metodi tf.keras.layers.Layer . Leggi la guida alla mappatura del modello per saperne di più sugli spessori di modellazione TF2.

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

Se stai inserendo un pezzo non banale di codice di passaggio in avanti nello shim, vuoi sapere che si sta comportando allo stesso modo di TF1.x. Ad esempio, considera di provare a inserire un intero modello TF-Slim Inception-Resnet-v2 nello spessore 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_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.

In effetti, questo livello funziona davvero perfettamente fuori dagli schemi (completo di un accurato monitoraggio della perdita di regolarizzazione).

Tuttavia, questo non è qualcosa che vuoi dare per scontato. Segui 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 (identificare 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 tuo modello crea un nuovo livello Keras o chiama tf.Variable in ogni chiamata di passaggio in avanti, è molto probabile che non riesca a catturare le variabili e a crearne di nuove ogni volta.

Di seguito sono riportati due ambiti di gestione del contesto che puoi utilizzare per rilevare quando il tuo 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)

Il primo ambito ( assert_no_variable_creations() ) genererà un errore immediatamente dopo aver provato a creare una variabile all'interno dell'ambito. Ciò ti 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 dell'ambito se è stata creata una qualsiasi variabile. Questa eccezione includerà l'elenco di tutte le variabili create nell'ambito. Questo è utile per capire quale sia l'insieme di tutti i pesi che il modello sta creando nel caso in cui sia possibile individuare modelli generali. Tuttavia, è meno utile per identificare le righe di codice esatte in cui sono state create quelle variabili.

Utilizzare entrambi gli ambiti seguenti 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/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 '

Nell'esempio seguente, osserva come questi decoratori lavorano 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_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>])

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 creare accidentalmente nuovi pesi invece di riutilizzare quelli esistenti:

  1. Utilizza una chiamata esplicita tf.Variable senza riutilizzare tf.Variables già creato. Risolvi il problema controllando prima se non è stato creato, quindi riutilizzando quelli esistenti.
  2. Crea ogni volta un livello o un modello Keras direttamente nel passaggio in avanti (al contrario di tf.compat.v1.layers ). Risolvi il problema controllando prima se non è stato creato, quindi riutilizzando quelli esistenti.
  3. È basato su tf.compat.v1.layers ma non riesce ad assegnare a tutti compat.v1.layers un nome esplicito o a racchiudere l'utilizzo di compat.v1.layer all'interno di un variable_scope denominato, causando l'incremento dei nomi dei livelli generati automaticamente in ogni chiamata di modello. Risolvi questo problema inserendo un nome tf.compat.v1.variable_scope all'interno del tuo metodo decorato con shim che racchiude tutto l'utilizzo di tf.compat.v1.layers .

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

Il secondo passaggio è 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 un mix tra il controllo manuale per verificare che corrispondano ed eseguire i controlli a livello di codice in uno unit test 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 a spessori in TF2. Si noti che il modello viene anche chiamato più volte prima di prendere 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-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

Il livello InceptionResnetV2 basato su shim supera questo test. Tuttavia, nel caso in cui non corrispondano, puoi eseguirlo attraverso 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 desiderosa è possibile 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 di qualsiasi variabile creata direttamente da chiamate esplicite tf.Variable e livelli/modelli Keras poiché la loro semantica di generazione dei nomi delle variabili può differire leggermente tra i grafici TF1.x e le funzionalità TF2 come l'esecuzione desiderosa e tf.function anche se tutto altrimenti funziona correttamente. Se questo è il tuo caso, modifica il test per tenere conto di eventuali semantiche di denominazione leggermente diverse.

  • A volte potresti scoprire che tf.Variable s, tf.keras.layers.Layer s o tf.keras.Model s creati nel passaggio in avanti del tuo ciclo di allenamento mancano dall'elenco delle variabili TF2 anche se sono state catturate dalla raccolta di variabili in TF1.x. Risolvi il problema assegnando le variabili/livelli/modelli creati dal tuo passaggio in avanti agli attributi di istanza nel tuo modello. Vedi qui per maggiori informazioni.

Passaggio 3: reimposta tutte le variabili, verifica l'equivalenza numerica con tutta la casualità disabilitata

Il passaggio successivo consiste nel verificare l'equivalenza numerica sia per gli output effettivi che per il monitoraggio della perdita di regolarizzazione quando si corregge il modello in modo tale che non sia coinvolta la generazione di numeri casuali (come 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à di 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 il livello InceptionResnetV2 compatibile con TF2 supera il test.

Se stai osservando i risultati divergenti 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 desiderosa può rendere tutto questo molto 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 gli endpoint intermedi che puoi sondare.

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

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

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

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

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

La seguente classe v1.keras.utils.DeterministicRandomTestTool fornisce un gestore di contesto scope() che può fare in modo che le operazioni casuali con stato utilizzino lo stesso seme su entrambi i grafici/sessioni TF1 e l'esecuzione desiderosa.

Lo strumento fornisce due modalità di test:

  1. constant che utilizza lo stesso seme per ogni singola operazione non importa quante volte sia stata chiamata e,
  2. num_random_ops che utilizza il numero di operazioni casuali stateful osservate in precedenza come seme dell'operazione.

Questo vale sia per le operazioni casuali con stato utilizzate per la creazione e l'inizializzazione delle variabili, sia per le operazioni casuali con stato utilizzate nel calcolo (come per i livelli di eliminazione).

Genera tre tensori casuali per mostrare come utilizzare questo strumento per creare corrispondenze di generazione di numeri casuali con stato tra sessioni ed esecuzione desiderosa.

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)

Tuttavia, si noti che in modalità constant , 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 l'ordine

Se sei preoccupato per alcuni numeri casuali che corrispondono in modalità constant riducendo la tua fiducia nel tuo test di equivalenza numerica (ad esempio se più pesi assumono le stesse inizializzazioni), puoi usare la modalità num_random_ops per evitarlo. Nella modalità num_random_ops , i numeri casuali generati dipenderanno dall'ordine delle operazioni casuali nel programma.

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)

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

Per consentire le variazioni di debug dovute all'ordine di traccia, DeterministicRandomTestTool in modalità num_random_ops consente di vedere quante operazioni casuali sono state tracciate con la proprietà 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

Se devi tenere conto della variazione dell'ordine di traccia nei tuoi test, puoi persino impostare in modo esplicito operation_seed con incremento automatico. Ad esempio, puoi utilizzarlo per creare corrispondenze di generazione di numeri casuali tra due diversi ordini di programma.

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

Tuttavia, DeterministicRandomTestTool non consente il riutilizzo dei seed delle operazioni già utilizzati, quindi assicurati che le sequenze con incremento automatico non possano sovrapporsi. Questo perché l'esecuzione ansiosa genera numeri diversi per gli utilizzi successivi dello stesso seme dell'operazione mentre i grafici e le sessioni TF1 non lo fanno, quindi generare un errore aiuta a mantenere in linea la sessione e la generazione di numeri casuali con stato ansioso.

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.

Verifica dell'inferenza

È ora possibile utilizzare il DeterministicRandomTestTool per assicurarsi che il modello InceptionResnetV2 corrisponda all'inferenza, anche quando si utilizza l'inizializzazione del peso casuale. Per una condizione di test più forte a causa della corrispondenza dell'ordine del programma, utilizzare la modalità 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)

Verifica della formazione

Poiché DeterministicRandomTestTool funziona per tutte le operazioni casuali con stato (inclusa sia l'inizializzazione del peso che il calcolo come i livelli di eliminazione), è possibile utilizzarlo per verificare la corrispondenza dei modelli anche in modalità di addestramento. È possibile utilizzare nuovamente la modalità num_random_ops perché l'ordine del programma delle operazioni casuali con stato corrisponde.

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)

Ora hai verificato che il modello InceptionResnetV2 che funziona avidamente con i decoratori intorno a tf.keras.layers.Layer corrisponde numericamente alla rete sottile in esecuzione nei grafici e nelle sessioni TF1.

Ad esempio, chiamando il livello InceptionResnetV2 direttamente con training=True interleaves l'inizializzazione della variabile con l'ordine di eliminazione in base all'ordine di creazione della rete.

D'altra parte, inserire prima il decoratore tf.keras.layers.Layer in un modello funzionale Keras e solo dopo chiamare il modello con training=True equivale a inizializzare tutte le variabili, quindi utilizzare il livello di eliminazione. Ciò produce un diverso ordine di tracciamento e un diverso insieme di numeri casuali.

Tuttavia, il default mode='constant' non è sensibile a queste differenze nell'ordine di traccia e passerà senza lavoro extra anche quando si incorpora il livello in un modello funzionale 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)

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

Dopo il passaggio 3 o il passaggio 4 sopra, può essere utile eseguire i test di equivalenza numerica quando si parte da checkpoint basati sui nomi preesistenti, se ne hai alcuni. Questo può verificare sia che il caricamento del checkpoint legacy funzioni correttamente sia che il modello stesso funzioni correttamente. La guida Riutilizzo dei checkpoint TF1.x spiega come riutilizzare i checkpoint TF1.x preesistenti e trasferirli ai checkpoint TF2.

Test aggiuntivi e risoluzione dei problemi

Man mano che aggiungi più 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 backpropagation e il calcolo del gradiente sono più inclini a 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 correre completamente avidamente e i tuoi grafici TF1. Ciò può essere causato dalle ottimizzazioni del grafico di TensorFlow che eseguono operazioni come la sostituzione di sottoespressioni in un grafico con un minor numero di operazioni matematiche.

Per isolare se è probabile che sia così, puoi confrontare il tuo codice TF1 con il calcolo TF2 che avviene all'interno di una tf.function (che applica passaggi di ottimizzazione del grafico come il tuo grafico TF1) piuttosto che con un calcolo puramente ansioso. In alternativa, puoi provare a utilizzare tf.config.optimizer.set_experimental_options per disabilitare i passaggi di ottimizzazione come "arithmetic_optimization" prima del tuo calcolo TF1 per vedere se il risultato finisce numericamente più vicino ai risultati del tuo calcolo TF2. Nelle sessioni di allenamento effettive si consiglia di utilizzare tf.function con i passaggi di ottimizzazione abilitati per motivi di prestazioni, ma potrebbe essere utile disabilitarli nei test di unità di equivalenza numerica.

Allo stesso modo, potresti anche scoprire che gli ottimizzatori tf.compat.v1.train e TF2 hanno proprietà numeriche in virgola mobile leggermente diverse rispetto agli ottimizzatori TF2, anche se le formule matematiche che stanno rappresentando sono le stesse. È meno probabile che questo sia un problema nelle sessioni di allenamento, ma potrebbe richiedere una tolleranza numerica maggiore nei test unitari di equivalenza.