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.

Ten przewodnik zawiera przykłady kodu migracji z podkładką modelującą tf.compat.v1.keras.utils.track_tf1_style_variables zastosowaną do metod tf.keras.layers.Layer . Przeczytaj przewodnik po mapowaniu modelu, aby dowiedzieć się więcej o podkładkach modelujących TF2.

W tym przewodniku szczegółowo opisano podejścia, których można 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 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.

Jeśli wkładasz do podkładki nietrywialny kawałek kodu przekazywania 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_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.

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ś zweryfikować, 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 wywołuje tf.Variable w każdym wywołaniu przekazania do przodu, najprawdopodobniej nie przechwytuje zmiennych i za każdym razem tworzy nowe.

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)

Pierwszy zakres ( assert_no_variable_creations() ) zgłosi błąd natychmiast po próbie utworzenia zmiennej w zakresie. Pozwala to na sprawdzenie śladu stosu (i użycie interaktywnego debugowania), aby dokładnie określić, które wiersze kodu utworzyły zmienną zamiast ponownego użycia istniejącej.

Drugi zakres ( catch_and_raise_created_variables() ) zgłosi wyjątek na końcu zakresu, jeśli jakiekolwiek zmienne zostały utworzone. 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/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 '

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 używać istniejących.

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

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 używać istniejących:

  1. Używa jawnego wywołania tf.Variable bez ponownego użycia już utworzonego tf.Variables . Napraw to, najpierw sprawdzając, czy nie został utworzony, a następnie ponownie wykorzystując istniejące.
  2. Za każdym razem tworzy warstwę lub model Keras bezpośrednio w przejściu do przodu (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 on zbudowany na bazie tf.compat.v1.layers , ale nie przypisuje wszystkim compat.v1.layers wyraźnej nazwy ani nie umieszcza użycia compat.v1.layer wewnątrz nazwanego variable_scope , powodując przyrost automatycznie generowanych nazw warstw każde wezwanie modelu. Napraw to, umieszczając nazwany tf.compat.v1.variable_scope w swojej metodzie ozdobionej podkładkami, która obejmuje całe użycie tf.compat.v1.layers .

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 zobaczyć, 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-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

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óć szczególną uwagę na nazwy wszelkich zmiennych utworzonych bezpośrednio przez jawne wywołania tf.Variable i warstwy/modele Keras, ponieważ ich semantyka generowania nazw zmiennych może się nieznacznie różnić między wykresami TF1.x a funkcjami TF2, takimi jak szybkie wykonywanie 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.

  • Czasami może się okazać, że tf.Variable s, tf.keras.layers.Layer s lub tf.keras.Model s utworzone w przejściu do przodu pętli szkoleniowej nie znajdują się na liście zmiennych TF2, nawet jeśli zostały przechwycone przez kolekcję 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 są zgodne między TF1.x i TF2 po usunięciu źródeł losowości, a warstwa InceptionResnetV2 zgodna z TF2 przechodzi test pomyślnie.

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 siatek (i innych modeli) eksponuje również 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 zweryfikowanie, czy model TF2 pod względem numerycznym odpowiada modelowi TF1.x, nawet biorąc pod uwagę generowanie 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 określono zalążków, generowanie liczb losowych zależy od tego, ile operacji znajduje się na wykresie w momencie dodania operacji losowej i ile razy wykres jest uruchamiany. W szybkim 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ższa klasa v1.keras.utils.DeterministicRandomTestTool udostępnia scope() menedżera kontekstu, który może sprawić, że stanowe operacje losowe będą korzystać z tego samego inicjatora zarówno na wykresach/sesjach TF1, jak i na szybkim wykonywaniu.

Narzędzie udostępnia dwa tryby testowania:

  1. constant , która używa tego samego ziarna dla każdej pojedynczej operacji, bez względu na to, ile razy została wywołana i,
  2. num_random_ops , który wykorzystuje liczbę wcześniej zaobserwowanych losowych operacji stanowych jako ziarno operacji.

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

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

Zauważ jednak, że w trybie constant , ponieważ b i c zostały wygenerowane z tym samym ziarnem i mają ten sam kształt, będą miały dokładnie te same wartości.

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

Śledzenie kolejności

Jeśli martwisz się, że niektóre liczby losowe dopasowują się w trybie constant , co zmniejszy Twoje zaufanie do testu równoważności liczbowej (na przykład, jeśli kilka wag przyjmuje te same inicjalizacje), możesz użyć trybu num_random_ops , aby tego uniknąć. W trybie num_random_ops generowane losowe liczby będą zależeć od kolejności losowych operacji w programie.

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)

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

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

Aby umożliwić debugowanie odmian ze względu na kolejność śledzenia, DeterministicRandomTestTool w trybie num_random_ops pozwala zobaczyć, ile losowych operacji zostało śledzonych za pomocą właściwości 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

Jeśli musisz uwzględnić zmienną kolejność śledzenia w swoich testach, możesz nawet jawnie ustawić automatyczne zwiększanie operation_seed . Na przykład możesz użyć tego, aby dopasować generowanie liczb losowych w dwóch różnych zamówieniach programu.

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

Jednak DeterministicRandomTestTool nie zezwala na ponowne użycie już używanych nasion operacji, więc upewnij się, że sekwencje z automatycznym zwiększaniem nie mogą się nakładać. Dzieje się tak, ponieważ gorliwe wykonanie 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 = 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.

Weryfikowanie wnioskowania

Możesz teraz użyć narzędzia DeterministicRandomTestTool , aby upewnić się, że model InceptionResnetV2 jest zgodny we wnioskowaniu, nawet w przypadku korzystania z inicjowania losowej wagi. Aby uzyskać silniejszy warunek testu ze względu na dopasowanie kolejności programów, użyj trybu 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)

Weryfikowanie szkolenia

Ponieważ DeterministicRandomTestTool działa dla wszystkich stanowych operacji losowych (w tym zarówno inicjowania wagi, jak i obliczeń, takich jak warstwy porzucania), można go użyć do sprawdzenia zgodności modeli również w trybie uczenia. Możesz ponownie użyć trybu num_random_ops , ponieważ kolejność programu stanowych operacji losowych jest zgodna.

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)

Zweryfikowałeś teraz, że model InceptionResnetV2 chętnie działa z dekoratorami wokół tf.keras.layers.Layer liczbowo odpowiada wąskiej sieci działającej na wykresach i sesjach TF1.

Na przykład wywoływanie warstwy InceptionResnetV2 bezpośrednio z opcją training=True przeplata inicjalizację zmiennej z kolejnością usuwania zgodnie z kolejnością tworzenia sieci.

Z drugiej strony, najpierw umieszczenie dekoratora tf.keras.layers.Layer w modelu funkcjonalnym Keras, a dopiero potem wywołanie modelu z wartością training=True jest równoznaczne z zainicjowaniem wszystkich zmiennych, a następnie użyciem warstwy dropout. Daje to inną kolejność śledzenia i inny zestaw liczb losowych.

Jednak domyślny mode='constant' nie jest wrażliwy na te różnice w kolejności śledzenia i przejdzie bez dodatkowej pracy nawet po osadzeniu warstwy w modelu funkcjonalnym 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)

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. Poradnik Ponowne używanie punktów kontrolnych TF1.x opisuje, jak ponownie wykorzystać istniejące punkty kontrolne TF1.x i przenieść je do punktów kontrolnych TF2.

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 określić, czy jest to prawdopodobne, możesz porównać swój kod TF1 z obliczeniami TF2 zachodzącymi wewnątrz funkcji tf.function (która stosuje przebiegi optymalizacji wykresu, takie jak wykres TF1), a nie z czysto gorliwym obliczeniem. Alternatywnie można spróbować użyć tf.config.optimizer.set_experimental_options , aby wyłączyć przebiegi optymalizacji, takie jak "arithmetic_optimization" przed obliczeniem TF1, aby sprawdzić, czy wynik jest liczbowo bliższy wynikom obliczeń TF2. W rzeczywistych biegach treningowych zaleca się używanie tf.function z włączonymi przebiegami optymalizacji ze względu na wydajność, ale może okazać się przydatne wyłączenie ich w testach jednostek równoważności numerycznej.

Podobnie może się okazać, że optymalizatory tf.compat.v1.train i optymalizatory TF2 mają nieco inne właściwości liczb zmiennoprzecinkowych niż optymalizatory TF2, nawet jeśli reprezentowane przez nie formuły matematyczne 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.