Validation de l'exactitude et de l'équivalence numérique

Voir sur TensorFlow.org Exécuter dans Google Colab Afficher sur GitHub Télécharger le cahier

Lors de la migration de votre code TensorFlow de TF1.x vers TF2, il est recommandé de s'assurer que votre code migré se comporte de la même manière dans TF2 que dans TF1.x.

Ce guide couvre des exemples de code de migration avec le shim de modélisation tf.compat.v1.keras.utils.track_tf1_style_variables appliqué aux méthodes tf.keras.layers.Layer . Lisez le guide de cartographie des modèles pour en savoir plus sur les cales de modélisation TF2.

Ce guide détaille les approches que vous pouvez utiliser pour :

  • Valider l'exactitude des résultats obtenus à partir des modèles d'entraînement à l'aide du code migré
  • Validez l'équivalence numérique de votre code entre les versions de TensorFlow

Installer

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 vous mettez un morceau non trivial de code de passe direct dans le shim, vous voulez savoir qu'il se comporte de la même manière que dans TF1.x. Par exemple, envisagez d'essayer de mettre un modèle TF-Slim Inception-Resnet-v2 entier dans le shim en tant que tel :

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

En l'occurrence, cette couche fonctionne parfaitement dès le départ (avec un suivi précis des pertes de régularisation).

Cependant, ce n'est pas quelque chose que vous voulez prendre pour acquis. Suivez les étapes ci-dessous pour vérifier qu'il se comporte réellement comme il l'a fait dans TF1.x, jusqu'à observer une équivalence numérique parfaite. Ces étapes peuvent également vous aider à trianguler quelle partie de la passe avant provoque une divergence par rapport à TF1.x (identifier si la divergence se produit dans la passe avant du modèle par opposition à une autre partie du modèle).

Étape 1 : Vérifier que les variables ne sont créées qu'une seule fois

La toute première chose que vous devez vérifier est que vous avez correctement construit le modèle de manière à réutiliser les variables dans chaque appel plutôt que de créer et d'utiliser accidentellement de nouvelles variables à chaque fois. Par exemple, si votre modèle crée une nouvelle couche Keras ou appelle tf.Variable dans chaque appel de passage vers l'avant, il est fort probable qu'il ne parvienne pas à capturer les variables et à en créer de nouvelles à chaque fois.

Vous trouverez ci-dessous deux portées de gestionnaire de contexte que vous pouvez utiliser pour détecter quand votre modèle crée de nouvelles variables et déboguer quelle partie du modèle le fait.

@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 première portée ( assert_no_variable_creations() ) génère une erreur immédiatement une fois que vous essayez de créer une variable dans la portée. Cela vous permet d'inspecter le stacktrace (et d'utiliser le débogage interactif) pour déterminer exactement quelles lignes de code ont créé une variable au lieu de réutiliser une variable existante.

La deuxième portée ( catch_and_raise_created_variables() ) lèvera une exception à la fin de la portée si des variables finissent par être créées. Cette exception inclura la liste de toutes les variables créées dans la portée. Ceci est utile pour déterminer quel est l'ensemble de tous les poids que votre modèle crée au cas où vous pourriez repérer des modèles généraux. Cependant, il est moins utile pour identifier les lignes exactes de code où ces variables ont été créées.

Utilisez les deux portées ci-dessous pour vérifier que la couche InceptionResnetV2 basée sur shim ne crée pas de nouvelles variables après le premier appel (vraisemblablement en les réutilisant).

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 '

Dans l'exemple ci-dessous, observez comment ces décorateurs fonctionnent sur un calque qui crée de manière incorrecte de nouveaux poids à chaque fois au lieu de réutiliser ceux qui existent déjà.

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

Vous pouvez corriger le calque en vous assurant qu'il ne crée les poids qu'une seule fois, puis les réutilise à chaque fois.

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)

Dépannage

Voici quelques raisons courantes pour lesquelles votre modèle peut créer accidentellement de nouvelles pondérations au lieu de réutiliser celles qui existent :

  1. Il utilise un appel tf.Variable explicite sans réutiliser tf.Variables déjà créé. Corrigez cela en vérifiant d'abord s'il n'a pas été créé, puis en réutilisant ceux qui existent déjà.
  2. Il crée une couche ou un modèle Keras directement dans la passe avant à chaque fois (par opposition à tf.compat.v1.layers ). Corrigez cela en vérifiant d'abord s'il n'a pas été créé, puis en réutilisant ceux qui existent déjà.
  3. Il est construit au-dessus de tf.compat.v1.layers mais ne parvient pas à attribuer un nom explicite à tous les compat.v1.layers ou à envelopper votre utilisation de compat.v1.layer à l'intérieur d'un variable_scope nommé, ce qui entraîne l'incrémentation des noms de couches générés automatiquement. chaque appel de modèle. Corrigez cela en plaçant un tf.compat.v1.variable_scope nommé dans votre méthode décorée par shim qui englobe toute votre utilisation de tf.compat.v1.layers .

Étape 2 : Vérifiez que le nombre, les noms et les formes des variables correspondent

La deuxième étape consiste à s'assurer que votre couche exécutée dans TF2 crée le même nombre de poids, avec les mêmes formes, que le code correspondant dans TF1.x.

Vous pouvez les vérifier manuellement pour voir s'ils correspondent et effectuer les vérifications par programme dans un test unitaire, comme indiqué ci-dessous.

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

Ensuite, faites de même pour la couche enveloppée de cales dans TF2. Notez que le modèle est également appelé plusieurs fois avant de saisir les poids. Ceci est fait pour tester efficacement la réutilisation des 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 couche InceptionResnetV2 basée sur shim réussit ce test. Cependant, dans le cas où ils ne correspondent pas, vous pouvez l'exécuter via un diff (texte ou autre) pour voir où se trouvent les différences.

Cela peut fournir un indice sur la partie du modèle qui ne se comporte pas comme prévu. Avec une exécution rapide, vous pouvez utiliser pdb, le débogage interactif et les points d'arrêt pour creuser dans les parties du modèle qui semblent suspectes et déboguer plus en profondeur ce qui ne va pas.

Dépannage

  • Portez une attention particulière aux noms de toutes les variables créées directement par des appels explicites tf.Variable et des couches/modèles Keras, car la sémantique de génération de leur nom de variable peut différer légèrement entre les graphiques TF1.x et les fonctionnalités TF2 telles que l'exécution hâtive et tf.function même si tout sinon fonctionne correctement. Si tel est votre cas, ajustez votre test pour tenir compte de toute sémantique de dénomination légèrement différente.

  • Vous pouvez parfois constater que les tf.Variable s, tf.keras.layers.Layer s ou tf.keras.Model s créés dans la passe avant de votre boucle d'entraînement sont absents de votre liste de variables TF2 même s'ils ont été capturés par la collection de variables dans TF1.x. Corrigez cela en affectant les variables/couches/modèles créés par votre passe avant aux attributs d'instance de votre modèle. Voir ici pour plus d'informations.

Étape 3 : Réinitialiser toutes les variables, vérifier l'équivalence numérique avec tout caractère aléatoire désactivé

L'étape suivante consiste à vérifier l'équivalence numérique pour les sorties réelles et le suivi des pertes de régularisation lorsque vous fixez le modèle de sorte qu'il n'y ait pas de génération de nombres aléatoires impliquée (comme lors de l'inférence).

La manière exacte de procéder peut dépendre de votre modèle spécifique, mais dans la plupart des modèles (comme celui-ci), vous pouvez le faire en :

  1. Initialisation des poids à la même valeur sans caractère aléatoire. Cela peut être fait en les réinitialisant à une valeur fixe après leur création.
  2. Exécution du modèle en mode inférence pour éviter de déclencher des couches abandonnées qui peuvent être des sources d'aléatoire.

Le code suivant montre comment vous pouvez comparer les résultats TF1.x et TF2 de cette façon.

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)

Obtenez les résultats 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)

Les chiffres correspondent entre TF1.x et TF2 lorsque vous supprimez les sources aléatoires, et la couche InceptionResnetV2 compatible TF2 réussit le test.

Si vous observez des résultats divergents pour vos propres modèles, vous pouvez utiliser l'impression ou le pdb et le débogage interactif pour identifier où et pourquoi les résultats commencent à diverger. Une exécution rapide peut rendre cela beaucoup plus facile. Vous pouvez également utiliser une approche d'ablation pour exécuter uniquement de petites parties du modèle sur des entrées intermédiaires fixes et isoler où la divergence se produit.

De manière pratique, de nombreux filets minces (et d'autres modèles) exposent également des points de terminaison intermédiaires que vous pouvez sonder.

Étape 4 : Aligner la génération de nombres aléatoires, vérifier l'équivalence numérique dans la formation et l'inférence

L'étape finale consiste à vérifier que le modèle TF2 correspond numériquement au modèle TF1.x, même en tenant compte de la génération de nombres aléatoires dans l'initialisation des variables et dans la passe avant elle-même (comme les couches d'abandon pendant la passe avant).

Vous pouvez le faire en utilisant l'outil de test ci-dessous pour faire correspondre la sémantique de génération de nombres aléatoires entre les graphes/sessions TF1.x et l'exécution hâtive.

Les graphes/sessions hérités de TF1 et l'exécution hâtive de TF2 utilisent différentes sémantiques de génération de nombres aléatoires avec état.

Dans tf.compat.v1.Session s, si aucune graine n'est spécifiée, la génération de nombres aléatoires dépend du nombre d'opérations présentes dans le graphe au moment où l'opération aléatoire est ajoutée et du nombre de fois que le graphe est exécuté. Dans une exécution hâtive, la génération de nombres aléatoires avec état dépend de la graine globale, de la graine aléatoire de l'opération et du nombre de fois que l'opération avec l'opération avec la graine aléatoire donnée est exécutée. Voir tf.random.set_seed pour plus d'informations.

La classe v1.keras.utils.DeterministicRandomTestTool suivante fournit un gestionnaire de contexte scope() qui peut faire en sorte que les opérations aléatoires avec état utilisent la même graine sur les graphes/sessions TF1 et l'exécution hâtive.

L'outil propose deux modes de test :

  1. constant qui utilise la même graine pour chaque opération, quel que soit le nombre de fois qu'elle a été appelée et,
  2. num_random_ops qui utilise le nombre d'opérations aléatoires avec état précédemment observées comme graine d'opération.

Cela s'applique à la fois aux opérations aléatoires avec état utilisées pour créer et initialiser des variables, et aux opérations aléatoires avec état utilisées dans le calcul (comme pour les couches d'abandon).

Générez trois tenseurs aléatoires pour montrer comment utiliser cet outil pour faire correspondre la génération de nombres aléatoires avec état entre les sessions et l'exécution hâtive.

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)

Cependant, notez qu'en mode constant , parce que b et c ont été générés avec la même graine et ont la même forme, ils auront exactement les mêmes valeurs.

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

Suivi de commande

Si vous craignez que certains nombres aléatoires correspondant en mode constant réduisent votre confiance dans votre test d'équivalence numérique (par exemple si plusieurs poids prennent les mêmes initialisations), vous pouvez utiliser le mode num_random_ops pour éviter cela. Dans le mode num_random_ops , les nombres aléatoires générés dépendront de l'ordre des ops aléatoires dans le programme.

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)

Cependant, notez que dans ce mode, la génération aléatoire est sensible à l'ordre du programme, et donc les nombres aléatoires générés suivants ne correspondent pas.

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

Pour permettre les variations de débogage dues à l'ordre de traçage, DeterministicRandomTestTool en mode num_random_ops vous permet de voir combien d'opérations aléatoires ont été tracées avec la propriété 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 vous devez tenir compte de l'ordre de trace variable dans vos tests, vous pouvez même définir explicitement l' operation_seed d'auto-incrémentation. Par exemple, vous pouvez l'utiliser pour faire correspondre la génération de nombres aléatoires dans deux ordres de programme différents.

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

Cependant, DeterministicRandomTestTool interdit la réutilisation de graines d'opération déjà utilisées, assurez-vous donc que les séquences auto-incrémentées ne peuvent pas se chevaucher. En effet, l'exécution hâtive génère des nombres différents pour les utilisations de suivi de la même graine d'opération, contrairement aux graphiques et aux sessions TF1, donc générer une erreur aide à maintenir la session et la génération de nombres aléatoires avec état en ligne.

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.

Vérification de l'inférence

Vous pouvez désormais utiliser DeterministicRandomTestTool pour vous assurer que le modèle InceptionResnetV2 correspond à l'inférence, même lorsque vous utilisez l'initialisation de poids aléatoire. Pour une condition de test plus forte en raison de l'ordre du programme correspondant, utilisez le mode 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)

Vérification de la formation

Étant donné que DeterministicRandomTestTool fonctionne pour toutes les opérations aléatoires avec état (y compris l'initialisation du poids et les calculs tels que les couches d'abandon), vous pouvez également l'utiliser pour vérifier que les modèles correspondent en mode d'apprentissage. Vous pouvez à nouveau utiliser le mode num_random_ops car l'ordre du programme des opérations aléatoires avec état correspond.

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)

Vous avez maintenant vérifié que le modèle InceptionResnetV2 exécuté avec impatience avec des décorateurs autour de tf.keras.layers.Layer correspond numériquement au réseau mince exécuté dans les graphiques et les sessions TF1.

Par exemple, appeler la couche InceptionResnetV2 directement avec training=True entrelace l'initialisation des variables avec l'ordre d'abandon selon l'ordre de création du réseau.

D'un autre côté, placer d'abord le décorateur tf.keras.layers.Layer dans un modèle fonctionnel Keras et ensuite seulement appeler le modèle avec training=True équivaut à initialiser toutes les variables puis à utiliser la couche d'abandon. Cela produit un ordre de traçage différent et un ensemble différent de nombres aléatoires.

Cependant, le mode='constant' n'est pas sensible à ces différences d'ordre de traçage et passera sans travail supplémentaire même lors de l'intégration de la couche dans un modèle fonctionnel 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)

Étape 3b ou 4b (facultatif) : test avec des points de contrôle préexistants

Après l'étape 3 ou l'étape 4 ci-dessus, il peut être utile d'exécuter vos tests d'équivalence numérique lorsque vous démarrez à partir de points de contrôle basés sur le nom préexistants si vous en avez. Cela peut tester à la fois que le chargement de votre point de contrôle hérité fonctionne correctement et que le modèle lui-même fonctionne correctement. Le guide Réutilisation des points de contrôle TF1.x explique comment réutiliser vos points de contrôle TF1.x préexistants et les transférer vers des points de contrôle TF2.

Tests et dépannage supplémentaires

Au fur et à mesure que vous ajoutez des tests d'équivalence numérique, vous pouvez également choisir d'ajouter un test qui vérifie que votre calcul de gradient (ou même vos mises à jour d'optimiseur) correspond.

La rétropropagation et le calcul du gradient sont plus sujets aux instabilités numériques en virgule flottante que les passages vers l'avant du modèle. Cela signifie qu'au fur et à mesure que vos tests d'équivalence couvrent des parties plus non isolées de votre entraînement, vous pouvez commencer à voir des différences numériques non triviales entre la course à pied et vos graphiques TF1. Cela peut être dû aux optimisations de graphique de TensorFlow qui effectuent des opérations telles que le remplacement de sous-expressions dans un graphique avec moins d'opérations mathématiques.

Pour isoler si cela est susceptible d'être le cas, vous pouvez comparer votre code TF1 au calcul TF2 qui se produit à l'intérieur d'un tf.function (qui applique des passes d'optimisation de graphe comme votre graphe TF1) plutôt qu'à un calcul purement impatient. Alternativement, vous pouvez essayer d'utiliser tf.config.optimizer.set_experimental_options pour désactiver les passes d'optimisation telles que "arithmetic_optimization" avant votre calcul TF1 pour voir si le résultat se rapproche numériquement de vos résultats de calcul TF2. Dans vos exécutions de formation réelles, il est recommandé d'utiliser tf.function avec les passes d'optimisation activées pour des raisons de performances, mais vous pouvez trouver utile de les désactiver dans vos tests unitaires d'équivalence numérique.

De même, vous pouvez également constater que les optimiseurs tf.compat.v1.train et les optimiseurs TF2 ont des propriétés numériques à virgule flottante légèrement différentes de celles des optimiseurs TF2, même si les formules mathématiques qu'ils représentent sont les mêmes. Il est moins probable que cela pose un problème lors de vos entraînements, mais cela peut nécessiter une tolérance numérique plus élevée dans les tests unitaires d'équivalence.