Dzień Społeczności ML jest 9 listopada! Dołącz do nas na aktualizacje z TensorFlow Jax i więcej Dowiedz się więcej

Walidacja poprawności i równoważności liczbowej

Zobacz na TensorFlow.org Uruchom w Google Colab Zobacz na GitHub Pobierz notatnik

Podczas migracji kodu TensorFlow z TF1.x do TF2 dobrą praktyką jest upewnienie się, że migrowany kod zachowuje się tak samo w TF2, jak w TF1.x.

Obejmuje guide migracji przykłady kodu z tf.compat.v1.keras.utils.track_tf1_style_variables modelowania zastosowano podkładkę do tf.keras.layers.Layer metod. Przeczytać instrukcji modelu mapowania , aby dowiedzieć się więcej o TF2 modelowania podkładek.

W tym przewodniku szczegółowo opisano podejścia, których możesz użyć do:

  • Zweryfikuj poprawność wyników uzyskanych z modeli uczących za pomocą zmigrowanego kodu
  • Sprawdź równoważność liczbową swojego kodu w różnych wersjach TensorFlow

Ustawiać

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.

Jeśli wstawiasz do podkładki nietrywialny fragment kodu przekazującego do przodu, chcesz wiedzieć, że zachowuje się on tak samo, jak w TF1.x. Rozważ na przykład umieszczenie w podkładce całego modelu TF-Slim Incepcja-Resnet-v2 w następujący sposób:

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

Tak się składa, że ​​ta warstwa faktycznie działa doskonale po wyjęciu z pudełka (wraz z dokładnym śledzeniem utraty regularyzacji).

Nie jest to jednak coś, co chcesz brać za pewnik. Wykonaj poniższe kroki, aby sprawdzić, czy rzeczywiście zachowuje się tak, jak w TF1.x, aż do zaobserwowania doskonałej równoważności numerycznej. Te kroki mogą również pomóc w triangulacji, która część podania do przodu powoduje rozbieżność z TF1.x (zidentyfikuj, czy rozbieżność powstaje w modelu podania do przodu, a nie w innej części modelu).

Krok 1: Sprawdź, czy zmienne są tworzone tylko raz

Pierwszą rzeczą, którą powinieneś sprawdzić, jest to, że poprawnie zbudowałeś model w taki sposób, że ponownie wykorzystujesz zmienne w każdym wywołaniu, zamiast przypadkowo tworzyć i używać za każdym razem nowych zmiennych. Na przykład, jeśli twój model tworzy nową warstwę Keras lub wzywa tf.Variable w każdym podaniu wezwanie to jest najbardziej prawdopodobne w przypadku braku zmiennych wychwytywania i tworzenie nowych za każdym razem.

Poniżej znajdują się dwa zakresy menedżera kontekstu, których można użyć do wykrycia, kiedy model tworzy nowe zmienne i debugowania, która część modelu to robi.

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

Pierwsze zakres ( assert_no_variable_creations() ) będą natychmiast podnieść błąd raz spróbuj utworzyć zmienną w zakresie. Pozwala to na sprawdzenie śladu stosu (i użycie interaktywnego debugowania) w celu dokładnego określenia, które wiersze kodu utworzyły zmienną zamiast ponownego użycia istniejącej.

Na drugi zakres ( catch_and_raise_created_variables() ) podniesie wyjątek na końcu zakresu, jeśli jakieś zmienne zakończony został utworzony. Ten wyjątek będzie zawierał listę wszystkich zmiennych utworzonych w zakresie. Jest to przydatne do ustalenia, jaki zestaw wszystkich wag tworzy twój model, na wypadek, gdybyś zauważył ogólne wzorce. Jest jednak mniej przydatny do identyfikowania dokładnych wierszy kodu, w których utworzono te zmienne.

Użyj obu poniższych zakresów, aby sprawdzić, czy warstwa InceptionResnetV2 oparta na podkładkach nie tworzy żadnych nowych zmiennych po pierwszym wywołaniu (przypuszczalnie ponownie ich używa).

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 '

W poniższym przykładzie obserwuj, jak te dekoratory działają na warstwie, która za każdym razem niepoprawnie tworzy nowe wagi, zamiast ponownie wykorzystywać istniejące.

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

Możesz naprawić warstwę, upewniając się, że tworzy wagi tylko raz, a następnie używa ich za każdym razem.

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)

Rozwiązywanie problemów

Oto kilka typowych powodów, dla których Twój model może przypadkowo tworzyć nowe wagi zamiast ponownie wykorzystywać istniejące:

  1. Wykorzystuje ona wyraźne tf.Variable połączenia bez ponownego użycia już utworzone tf.Variables . Napraw to, najpierw sprawdzając, czy nie został utworzony, a następnie ponownie wykorzystując istniejące.
  2. Tworzy się warstwę Keras lub model bezpośredni w każdym podaniu (w przeciwieństwie do tf.compat.v1.layers ). Napraw to, najpierw sprawdzając, czy nie został utworzony, a następnie ponownie wykorzystując istniejące.
  3. Jest zbudowany na szczycie tf.compat.v1.layers ale nie przypisać wszystkie compat.v1.layers jednoznacznej nazwy lub owinąć compat.v1.layer użytkowania wewnątrz nazwanego variable_scope , powodując wygenerowany automatycznie nazwy warstw do przyrostu każde wezwanie modelu. Rozwiązać ten problem poprzez umieszczenie nazwie tf.compat.v1.variable_scope wewnątrz metody podkładki dekorowany że otacza wszystkie swoje tf.compat.v1.layers użytkowania.

Krok 2: Sprawdź, czy liczby, nazwy i kształty zmiennych pasują do siebie

Drugim krokiem jest upewnienie się, że warstwa uruchomiona w TF2 tworzy taką samą liczbę wag o tych samych kształtach, jak odpowiedni kod w TF1.x.

Możesz wykonać mieszankę ręcznego sprawdzania ich, aby sprawdzić, czy pasują, i wykonywania kontroli programowo w teście jednostkowym, jak pokazano poniżej.

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

Następnie zrób to samo dla warstwy owiniętej podkładkami w TF2. Zauważ, że model jest również wywoływany wielokrotnie przed chwyceniem wag. Ma to na celu skuteczne przetestowanie ponownego wykorzystania zmiennych.

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

Warstwa InceptionResnetV2 oparta na podkładkach przechodzi ten test. Jednak w przypadku, gdy się nie zgadzają, możesz przejrzeć różnicę (tekstową lub inną), aby zobaczyć, gdzie są różnice.

Może to dać wskazówkę, która część modelu nie zachowuje się zgodnie z oczekiwaniami. Dzięki szybkiemu wykonywaniu możesz użyć pdb, interaktywnego debugowania i punktów przerwania, aby zagłębić się w części modelu, które wydają się podejrzane, i dokładniej debugować to, co jest nie tak.

Rozwiązywanie problemów

  • Zwrócić szczególną uwagę na nazwy dowolnych zmiennych utworzonych bezpośrednio przez wyraźnych tf.Variable połączeń i warstw Keras / modele jak ich semantyki generacji nazwa zmiennej może się nieznacznie różnić między wykresami TF1.x i funkcjonalności takich jak TF2 chętny wykonania i tf.function nawet jeśli wszystko jeszcze działa poprawnie. Jeśli tak jest w Twoim przypadku, dostosuj test, aby uwzględnić nieco inną semantykę nazewnictwa.

  • Może czasami okazać, że tf.Variable s, tf.keras.layers.Layer s lub tf.keras.Model s tworzone w podaniu pętla szkoleniowej brakuje z listy zmiennych TF2, nawet jeśli zostały one przechwycone przez zbieranie zmiennych w TF1.x. Napraw to, przypisując zmienne/warstwy/modele, które tworzy Twój przebieg do przodu, do atrybutów instancji w Twoim modelu. Zobacz tutaj , aby uzyskać więcej informacji.

Krok 3: Zresetuj wszystkie zmienne, sprawdź równoważność liczbową przy wyłączonej losowości

Następnym krokiem jest zweryfikowanie równoważności liczbowej zarówno dla rzeczywistych wyników, jak i śledzenia utraty regularyzacji, gdy naprawisz model w taki sposób, że nie jest zaangażowane generowanie liczb losowych (na przykład podczas wnioskowania).

Dokładny sposób, aby to zrobić, może zależeć od konkretnego modelu, ale w większości modeli (takich jak ten) możesz to zrobić:

  1. Inicjalizacja wag na tę samą wartość bez losowości. Można to zrobić, resetując je do stałej wartości po ich utworzeniu.
  2. Uruchamianie modelu w trybie wnioskowania, aby uniknąć uruchamiania warstw porzucanych, które mogą być źródłem losowości.

Poniższy kod pokazuje, jak w ten sposób można porównać wyniki TF1.x i TF2.

graph = tf.Graph()
with graph.as_default(), tf.compat.v1.Session(graph=graph) as sess:
  height, width = 299, 299
  num_classes = 1000
  inputs = tf.ones( (1, height, width, 3))

  out, endpoints = inception_resnet_v2(inputs, num_classes, is_training=False)

  # Rather than running the global variable initializers,
  # reset all variables to a constant value
  var_reset = tf.group([var.assign(tf.ones_like(var) * 0.001) for var in tf.compat.v1.global_variables()])
  sess.run(var_reset)

  # Grab the outputs & regularization loss
  reg_losses = tf.compat.v1.get_collection(tf.compat.v1.GraphKeys.REGULARIZATION_LOSSES)
  tf1_regularization_loss = sess.run(tf.math.add_n(reg_losses))
  tf1_output = sess.run(out)

print("Regularization loss:", tf1_regularization_loss)
tf1_output[0][:5]
Regularization loss: 0.001182976
array([0.00299837, 0.00299837, 0.00299837, 0.00299837, 0.00299837],
      dtype=float32)

Pobierz wyniki 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)

Liczby meczu pomiędzy TF1.x i TF2 po usunięciu źródła losowości i TF2 kompatybilne InceptionResnetV2 warstwa przechodzi test.

Jeśli obserwujesz rozbieżne wyniki dla własnych modeli, możesz użyć drukowania lub pdb i interaktywnego debugowania, aby określić, gdzie i dlaczego wyniki zaczynają się różnić. Chętne wykonanie może to znacznie ułatwić. Możesz również użyć podejścia ablacyjnego, aby uruchomić tylko małe części modelu na stałych wejściach pośrednich i wyizolować, gdzie występuje rozbieżność.

Dogodnie, wiele cienkich sieci (i innych modeli) również eksponuje pośrednie punkty końcowe, które można sondować.

Krok 4: Dopasuj generowanie liczb losowych, sprawdź równoważność liczbową zarówno w uczeniu, jak i wnioskowaniu

Ostatnim krokiem jest sprawdzenie, czy model TF2 pod względem numerycznym odpowiada modelowi TF1.x, nawet przy uwzględnieniu generowania liczb losowych podczas inicjalizacji zmiennej oraz w samym przebiegu do przodu (takim jak warstwy porzucania podczas przebiegu do przodu).

Możesz to zrobić, korzystając z poniższego narzędzia testowego, aby dopasować semantykę generowania liczb losowych między wykresami/sesjami TF1.x a szybkim wykonaniem.

Starsze wykresy/sesje TF1 i szybkie wykonywanie TF2 wykorzystują inną semantykę generowania liczb losowych.

W tf.compat.v1.Session s, jeśli nie ma nasiona są określone, generowania liczb losowych zależy ile operacje są na wykresie w momencie, gdy przypadkowy operacja jest dodawana, a ile razy wykres jest prowadzony. W gorliwym wykonaniu generowanie stanowych liczb losowych zależy od globalnego ziarna, losowego ziarna operacji i tego, ile razy wykonywana jest operacja z danym losowym ziarnem. Zobacz tf.random.set_seed aby uzyskać więcej informacji.

Poniższy DeterministicTestTool obiekt zapewnia menedżer kontekst scope() , który może wnieść stanowe losowe operacje korzystają z tego samego materiału siewnego na obu wykresach TF1 / sesjach i chętny wykonania,

Narzędzie udostępnia dwa tryby testowania:

  1. constant który korzysta z tego samego materiału siewnego dla każdej pojedynczej operacji bez względu na to ile razy została wywołana, a
  2. num_random_ops który wykorzystuje szereg wcześniej obserwowanych stanowych losowych operacje jak nasiona działanie.

Dotyczy to zarówno stanowych operacji losowych używanych do tworzenia i inicjowania zmiennych, jak i stanowych operacji losowych używanych w obliczeniach (takich jak warstwy porzucania).

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)

Wygeneruj trzy losowe tensory, aby pokazać, jak używać tego narzędzia do generowania stanowego generowania liczb losowych między sesjami i gorliwym wykonaniem.

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)

Jednak należy zauważyć, że w constant trybie, bo b i c były generowane z tego samego materiału siewnego i mają ten sam kształt, będą mieć dokładnie te same wartości.

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

Śledzenie kolejności

Jeśli martwisz się o pewnych liczb losowych pasujących w constant trybie zmniejszając zaufanie do testu równoważności numerycznej (na przykład, jeśli wziąć kilka ciężary na tych samych inicjalizacji), można użyć num_random_ops tryb, aby tego uniknąć. W num_random_ops trybie wygenerowane liczby losowe zależy od zamawiającego losowych ops w programie.

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)

Należy jednak zauważyć, że w tym trybie losowe generowanie jest wrażliwe na kolejność programu, a zatem następujące wygenerowane liczby losowe nie pasują do siebie.

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

W celu umożliwienia debugowania odmian ze względu na kolejność wykrywania, DeterministicTestTool w num_random_ops tryb pozwala zobaczyć ile losowe operacje zostały odszukane z operation_seed nieruchomości.

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

Jeśli trzeba uwagę na zmianę kolejności śledzenia w swoich badaniach, można nawet ustawić autoinkrementacja operation_seed wyraźnie. Na przykład możesz użyć tego, aby dopasować generowanie liczb losowych w dwóch różnych zamówieniach programu.

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

Jednak DeterministicTestTool Uniemożliwia wykorzystanie już wykorzystane nasiona operacji, więc upewnij się, że sekwencje auto-zwiększany nie mogą się pokrywać. Dzieje się tak, ponieważ gorliwe wykonywanie generuje różne liczby dla kolejnych zastosowań tego samego źródła operacji, podczas gdy wykresy i sesje TF1 nie, więc zgłoszenie błędu pomaga utrzymać sesję i chętne stanowe generowanie liczb losowych w linii.

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.

Weryfikowanie wnioskowania

Teraz można korzystać z DeterministicTestTool aby upewnić się, że InceptionResnetV2 modelowych mecze w wnioskowanie, nawet podczas korzystania z losową inicjalizację wagi. Dla silniejszego warunków testu ze względu na dopasowanie porządku programu, użyj num_random_ops tryb.

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)

Weryfikowanie szkolenia

Ponieważ DeterministicTestTool działa dla wszystkich stanowych losowych operacji (w tym zarówno inicjalizacji wagi i obliczenia, takie jak warstwy przerywania), można go użyć do weryfikacji modeli pasuje w trybie treningu, jak również. Można ponownie użyć num_random_ops tryb ponieważ kolejność programów z stanowych losowych ops mecze.

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)

Teraz masz zweryfikowane że InceptionResnetV2 modelu pracuje z zapałem dekoratorów wokół tf.keras.layers.Layer numerycznie pasuje smukły sieć działa w TF1 wykresów i sesjach.

Na przykład wywołanie InceptionResnetV2 warstwę bezpośrednio training=True przeplata inicjalizacji zmiennych z rzędu przerywania według kolejności tworzenie sieci.

Z drugiej strony, najpierw oddanie tf.keras.layers.Layer dekorator w modelu funkcjonalnego Keras a dopiero potem dzwoni model z training=True odpowiada inicjalizacji wszystkie zmienne następnie stosując warstwę przerywania. Daje to inną kolejność śledzenia i inny zestaw liczb losowych.

Jednak domyślne mode='constant' nie jest wrażliwy na te różnice w kolejności wykrywania i przejdzie bez dodatkowej pracy nawet podczas osadzania warstwy w modelu funkcjonalnego 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)

Krok 3b lub 4b (opcjonalnie): Testowanie z istniejącymi wcześniej punktami kontrolnymi

Po kroku 3 lub kroku 4 powyżej przydatne może być uruchomienie testów równoważności liczbowej, zaczynając od istniejących wcześniej punktów kontrolnych opartych na nazwach, jeśli takie masz. Może to sprawdzić, czy starsze wczytywanie punktów kontrolnych działa poprawnie, a sam model działa prawidłowo. Te punkty kontrolne Ponowne TF1.x kierować pokryw, jak ponowne użycie istniejących już TF1.x punktów kontrolnych i przenoszą je do TF2 punktów kontrolnych.

Dodatkowe testy i rozwiązywanie problemów

W miarę dodawania większej liczby testów równoważności numerycznej można również dodać test, który weryfikuje zgodność obliczeń gradientu (lub nawet aktualizacji optymalizatora).

Propagacja wsteczna i obliczenia gradientu są bardziej podatne na niestabilności liczbowe zmiennoprzecinkowe niż modelowanie przejść do przodu. Oznacza to, że ponieważ testy równoważności obejmują więcej nieizolowanych części treningu, możesz zacząć dostrzegać nietrywialne różnice liczbowe między pełnym entuzjazmem bieganiem a wykresami TF1. Może to być spowodowane optymalizacjami wykresów TensorFlow, które wykonują takie rzeczy, jak zastępowanie podwyrażeń w wykresie mniejszą liczbą operacji matematycznych.

Aby wyizolować czy może to być przypadek, można porównać swój kod TF1 do TF2 obliczeń dzieje wnętrza tf.function (co dotyczy optymalizacji wykres przechodzi jak wykresie TF1) zamiast czysto chętny obliczeń. Alternatywnie, można spróbować użyć tf.config.optimizer.set_experimental_options aby wyłączyć optymalizację Karnety takie jak "arithmetic_optimization" przed swoim obliczeń TF1 aby sprawdzić, czy wynik kończy się numerycznie bliżej do TF2 wyników obliczeń. W swoich rzeczywistych tras treningowych zaleca się użyć tf.function z optymalizacją przechodzi włączony ze względu na wydajność, ale może okazać się przydatna, aby wyłączyć je w liczbowych testów jednostkowych równoważność.

Podobnie, można również stwierdzić, że tf.compat.v1.train optymalizujące i optymalizujące TF2 mają nieco inne właściwości niż Liczby zmiennoprzecinkowe TF2 optymalizujące, nawet jeśli są one formuły matematyczne reprezentujące są takie same. Jest to mniej prawdopodobne, że będzie to problem podczas treningów, ale może wymagać większej tolerancji liczbowej w testach jednostek równoważności.