Validando a correção e a equivalência numérica

Veja no TensorFlow.org Executar no Google Colab Ver no GitHub Baixar caderno

Ao migrar o código do TensorFlow do TF1.x para o TF2, é uma boa prática garantir que o código migrado se comporte no TF2 da mesma forma que no TF1.x.

Este guia abrange exemplos de código de migração com o shim de modelagem tf.compat.v1.keras.utils.track_tf1_style_variables aplicado aos métodos tf.keras.layers.Layer . Leia o guia de mapeamento de modelo para saber mais sobre os calços de modelagem TF2.

Este guia detalha as abordagens que você pode usar para:

  • Valide a exatidão dos resultados obtidos dos modelos de treinamento usando o código migrado
  • Valide a equivalência numérica do seu código nas versões do TensorFlow

Configurar

pip uninstall -y -q tensorflow
# Install tf-nightly as the DeterministicRandomTestTool is available only in
# Tensorflow 2.8
pip install -q tf-nightly
pip install -q tf_slim
import tensorflow as tf
import tensorflow.compat.v1 as v1

import numpy as np
import tf_slim as slim
import sys


from contextlib import contextmanager
!git clone --depth=1 https://github.com/tensorflow/models.git
import models.research.slim.nets.inception_resnet_v2 as inception
Cloning into 'models'...
remote: Enumerating objects: 3192, done.[K
remote: Counting objects: 100% (3192/3192), done.[K
remote: Compressing objects: 100% (2696/2696), done.[K
remote: Total 3192 (delta 848), reused 1381 (delta 453), pack-reused 0[K
Receiving objects: 100% (3192/3192), 33.39 MiB | 12.89 MiB/s, done.
Resolving deltas: 100% (848/848), done.

Se você está colocando um pedaço não trivial de código de acesso direto no shim, você quer saber se ele está se comportando da mesma maneira que no TF1.x. Por exemplo, considere tentar colocar um modelo TF-Slim Inception-Resnet-v2 inteiro no calço como tal:

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

Acontece que essa camada realmente funciona perfeitamente bem fora da caixa (completa com rastreamento preciso de perda de regularização).

No entanto, isso não é algo que você quer tomar como garantido. Siga as etapas abaixo para verificar se ele está realmente se comportando como no TF1.x, até observar a equivalência numérica perfeita. Essas etapas também podem ajudá-lo a triangular qual parte da passagem direta está causando uma divergência de TF1.x (identifique se a divergência surge na passagem direta do modelo em oposição a uma parte diferente do modelo).

Etapa 1: verificar se as variáveis ​​são criadas apenas uma vez

A primeira coisa que você deve verificar é se você construiu corretamente o modelo de uma forma que reutilize variáveis ​​em cada chamada, em vez de criar e usar acidentalmente novas variáveis ​​a cada vez. Por exemplo, se o seu modelo cria uma nova camada Keras ou chama tf.Variable em cada chamada de passagem direta, provavelmente está falhando ao capturar variáveis ​​e criando novas a cada vez.

Abaixo estão dois escopos do gerenciador de contexto que você pode usar para detectar quando seu modelo está criando novas variáveis ​​e depurar qual parte do modelo está fazendo isso.

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

O primeiro escopo ( assert_no_variable_creations() ) gerará um erro imediatamente assim que você tentar criar uma variável dentro do escopo. Isso permite que você inspecione o stacktrace (e use a depuração interativa) para descobrir exatamente quais linhas de código criaram uma variável em vez de reutilizar uma existente.

O segundo escopo ( catch_and_raise_created_variables() ) gerará uma exceção no final do escopo se alguma variável for criada. Essa exceção incluirá a lista de todas as variáveis ​​criadas no escopo. Isso é útil para descobrir qual é o conjunto de todos os pesos que seu modelo está criando, caso você possa identificar padrões gerais. No entanto, é menos útil para identificar as linhas exatas de código onde essas variáveis ​​foram criadas.

Use os dois escopos abaixo para verificar se a camada InceptionResnetV2 baseada em shim não cria novas variáveis ​​após a primeira chamada (presumivelmente, reutilizando-as).

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 '

No exemplo abaixo, observe como esses decoradores funcionam em uma camada que cria incorretamente novos pesos a cada vez, em vez de reutilizar os existentes.

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

Você pode corrigir a camada certificando-se de que ela crie os pesos apenas uma vez e os reutilize a cada vez.

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)

Solução de problemas

Aqui estão alguns motivos comuns pelos quais seu modelo pode acidentalmente criar novos pesos em vez de reutilizar os existentes:

  1. Ele usa uma chamada tf.Variable explícita sem reutilizar tf.Variables já criado. Corrija isso verificando primeiro se ele não foi criado e, em seguida, reutilizando os existentes.
  2. Ele cria uma camada ou modelo Keras diretamente na passagem direta a cada vez (em oposição a tf.compat.v1.layers ). Corrija isso verificando primeiro se ele não foi criado e, em seguida, reutilizando os existentes.
  3. Ele é construído em cima de tf.compat.v1.layers mas falha em atribuir um nome explícito a todos os compat.v1.layers ou em envolver seu uso compat.v1.layer dentro de um variable_scope nomeado , fazendo com que os nomes de camada gerados automaticamente sejam incrementados em cada chamada de modelo. Corrija isso colocando um tf.compat.v1.variable_scope nomeado dentro do seu método shim-decorated que envolve todo o seu uso de tf.compat.v1.layers .

Etapa 2: verifique se as contagens, os nomes e as formas das variáveis ​​correspondem

A segunda etapa é certificar-se de que sua camada em execução no TF2 crie o mesmo número de pesos, com as mesmas formas, que o código correspondente no TF1.x.

Você pode fazer uma combinação de verificá-los manualmente para ver se eles correspondem e fazer as verificações programaticamente em um teste de unidade, conforme mostrado abaixo.

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

Em seguida, faça o mesmo para a camada embrulhada em calço no TF2. Observe que o modelo também é chamado várias vezes antes de pegar os pesos. Isso é feito para testar efetivamente a reutilização de variáveis.

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

A camada InceptionResnetV2 baseada em shim passa neste teste. No entanto, no caso de não corresponderem, você pode executá-lo através de um diff (texto ou outro) para ver onde estão as diferenças.

Isso pode fornecer uma pista sobre qual parte do modelo não está se comportando conforme o esperado. Com a execução antecipada, você pode usar pdb, depuração interativa e pontos de interrupção para explorar as partes do modelo que parecem suspeitas e depurar o que está errado com mais profundidade.

Solução de problemas

  • Preste muita atenção aos nomes de quaisquer variáveis ​​criadas diretamente por chamadas explícitas de tf.Variable e camadas/modelos Keras, pois sua semântica de geração de nome de variável pode diferir ligeiramente entre gráficos TF1.x e funcionalidade TF2, como execução antecipada e tf.function mesmo se tudo mais está funcionando corretamente. Se este for o seu caso, ajuste seu teste para levar em conta qualquer semântica de nomenclatura ligeiramente diferente.

  • Você pode às vezes achar que os tf.Variable s, tf.keras.layers.Layer s, ou tf.keras.Model s criados na passagem direta do seu loop de treinamento estão faltando na sua lista de variáveis ​​do TF2, mesmo que tenham sido capturados pela coleção de variáveis em TF1.x. Corrija isso atribuindo as variáveis/camadas/modelos que sua passagem direta cria aos atributos de instância em seu modelo. Veja aqui para mais informações.

Etapa 3: redefina todas as variáveis, verifique a equivalência numérica com toda aleatoriedade desabilitada

A próxima etapa é verificar a equivalência numérica para as saídas reais e o rastreamento de perda de regularização ao corrigir o modelo de forma que não haja geração de número aleatório envolvida (como durante a inferência).

A maneira exata de fazer isso pode depender do seu modelo específico, mas na maioria dos modelos (como este), você pode fazer isso:

  1. Inicializando os pesos com o mesmo valor sem aleatoriedade. Isso pode ser feito redefinindo-os para um valor fixo após terem sido criados.
  2. Executando o modelo no modo de inferência para evitar o acionamento de camadas de abandono que podem ser fontes de aleatoriedade.

O código a seguir demonstra como você pode comparar os resultados do TF1.xe TF2 dessa maneira.

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)

Obtenha os resultados do 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)

Os números correspondem entre TF1.xe TF2 quando você remove fontes de aleatoriedade e a camada InceptionResnetV2 compatível com TF2 passa no teste.

Se você estiver observando os resultados divergentes para seus próprios modelos, poderá usar a impressão ou o pdb e a depuração interativa para identificar onde e por que os resultados começam a divergir. A execução ansiosa pode tornar isso significativamente mais fácil. Você também pode usar uma abordagem de ablação para executar apenas pequenas partes do modelo em entradas intermediárias fixas e isolar onde ocorre a divergência.

Convenientemente, muitas redes finas (e outros modelos) também expõem endpoints intermediários que você pode investigar.

Etapa 4: alinhar a geração de números aleatórios, verificar a equivalência numérica no treinamento e na inferência

A etapa final é verificar se o modelo TF2 corresponde numericamente ao modelo TF1.x, mesmo considerando a geração de números aleatórios na inicialização da variável e na própria passagem direta (como camadas de dropout durante a passagem direta).

Você pode fazer isso usando a ferramenta de teste abaixo para fazer com que a semântica de geração de números aleatórios corresponda entre os gráficos/sessões do TF1.x e a execução antecipada.

Os gráficos/sessões legados do TF1 e a execução antecipada do TF2 usam semânticas diferentes de geração de números aleatórios com estado.

Em tf.compat.v1.Session s, se nenhuma semente for especificada, a geração do número aleatório depende de quantas operações estão no gráfico no momento em que a operação aleatória é adicionada e quantas vezes o gráfico é executado. Na execução antecipada, a geração de números aleatórios com estado depende da semente global, da operação semente aleatória e de quantas vezes a operação com a operação com a semente aleatória fornecida é executada. Veja tf.random.set_seed para mais informações.

A classe v1.keras.utils.DeterministicRandomTestTool a seguir fornece um gerenciador de contexto scope() que pode fazer com que operações aleatórias com estado usem a mesma semente em gráficos/sessões TF1 e execução antecipada.

A ferramenta oferece dois modos de teste:

  1. constant que usa a mesma semente para cada operação, não importa quantas vezes ela tenha sido chamada e,
  2. num_random_ops que usa o número de operações aleatórias com estado observadas anteriormente como a semente da operação.

Isso se aplica tanto às operações aleatórias com estado usadas para criar e inicializar variáveis ​​quanto às operações aleatórias com estado usadas na computação (como para camadas de descarte).

Gere três tensores aleatórios para mostrar como usar essa ferramenta para fazer a correspondência de geração de números aleatórios com estado entre sessões e execução antecipada.

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)

No entanto, observe que no modo constant , como b e c foram gerados com a mesma semente e têm a mesma forma, eles terão exatamente os mesmos valores.

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

Pedido de rastreamento

Se você está preocupado com a correspondência de alguns números aleatórios no modo constant , reduzindo sua confiança em seu teste de equivalência numérica (por exemplo, se vários pesos assumem as mesmas inicializações), você pode usar o modo num_random_ops para evitar isso. No modo num_random_ops , os números aleatórios gerados dependerão da ordenação das operações aleatórias no programa.

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)

No entanto, observe que nesse modo a geração aleatória é sensível à ordem do programa e, portanto, os números aleatórios gerados a seguir não correspondem.

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

Para permitir variações de depuração devido à ordem de rastreamento, DeterministicRandomTestTool no modo num_random_ops permite ver quantas operações aleatórias foram rastreadas com a propriedade operation_seed .

random_tool = v1.keras.utils.DeterministicRandomTestTool(mode='num_random_ops')
with random_tool.scope():
  print(random_tool.operation_seed)
  a = tf.random.uniform(shape=(3,1))
  a = a * 3
  print(random_tool.operation_seed)
  b = tf.random.uniform(shape=(3,3))
  b = b * 3
  print(random_tool.operation_seed)
0
1
2

Se você precisar levar em conta a variação da ordem de rastreamento em seus testes, você pode até definir explicitamente o operation_seed de incremento automático. Por exemplo, você pode usar isso para fazer a correspondência de geração de números aleatórios em dois pedidos de programa diferentes.

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

No entanto, DeterministicRandomTestTool não permite a reutilização de sementes de operação já usadas, portanto, certifique-se de que as sequências incrementadas automaticamente não possam se sobrepor. Isso ocorre porque a execução antecipada gera números diferentes para usos subseqüentes da mesma semente de operação, enquanto os gráficos e sessões do TF1 não, portanto, gerar um erro ajuda a manter a sessão e a geração de números aleatórios com estado antecipado alinhados.

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.

Verificando a inferência

Agora você pode usar o DeterministicRandomTestTool para garantir que o modelo InceptionResnetV2 corresponda na inferência, mesmo ao usar a inicialização de peso aleatório. Para uma condição de teste mais forte devido à ordem do programa correspondente, use o modo 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)

Verificando o treinamento

Como o DeterministicRandomTestTool funciona para todas as operações aleatórias com estado (incluindo inicialização de peso e computação, como camadas de descarte), você também pode usá-lo para verificar se os modelos correspondem no modo de treinamento. Você pode usar novamente o modo num_random_ops porque a ordem do programa das operações aleatórias com estado corresponde.

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)

Você agora verificou que o modelo InceptionResnetV2 rodando avidamente com decoradores em torno de tf.keras.layers.Layer corresponde numericamente à rede slim rodando em gráficos e sessões TF1.

Por exemplo, chamar a camada InceptionResnetV2 diretamente com training=True intercala a inicialização da variável com a ordem de dropout de acordo com a ordem de criação da rede.

Por outro lado, primeiro colocar o decorador tf.keras.layers.Layer em um modelo funcional Keras e só então chamar o modelo com training=True é equivalente a inicializar todas as variáveis ​​e depois usar a camada dropout. Isso produz uma ordem de rastreamento diferente e um conjunto diferente de números aleatórios.

No entanto, o mode='constant' não é sensível a essas diferenças na ordem de rastreamento e passará sem trabalho extra mesmo ao incorporar a camada em um modelo funcional 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)

Etapa 3b ou 4b (opcional): teste com pontos de verificação pré-existentes

Após a etapa 3 ou a etapa 4 acima, pode ser útil executar seus testes de equivalência numérica ao iniciar a partir de pontos de verificação baseados em nomes pré-existentes, se você tiver alguns. Isso pode testar se o carregamento do ponto de verificação legado está funcionando corretamente e se o próprio modelo está funcionando corretamente. O guia Reutilizando pontos de verificação do TF1.x aborda como reutilizar seus pontos de verificação pré-existentes do TF1.x e transferi-los para os pontos de verificação do TF2.

Testes adicionais e solução de problemas

À medida que você adiciona mais testes de equivalência numérica, você também pode optar por adicionar um teste que verifique a correspondência de seu cálculo de gradiente (ou até mesmo suas atualizações de otimizador).

A retropropagação e a computação de gradiente são mais propensas a instabilidades numéricas de ponto flutuante do que os passes diretos do modelo. Isso significa que, à medida que seus testes de equivalência cobrem mais partes não isoladas de seu treinamento, você pode começar a ver diferenças numéricas não triviais entre correr totalmente avidamente e seus gráficos TF1. Isso pode ser causado pelas otimizações de gráfico do TensorFlow que fazem coisas como substituir subexpressões em um gráfico com menos operações matemáticas.

Para isolar se este é provavelmente o caso, você pode comparar seu código TF1 com a computação TF2 acontecendo dentro de um tf.function (que aplica passos de otimização de gráfico como seu gráfico TF1) em vez de uma computação puramente ansiosa. Alternativamente, você pode tentar usar tf.config.optimizer.set_experimental_options para desabilitar passos de otimização como "arithmetic_optimization" antes do cálculo do TF1 para ver se o resultado acaba numericamente mais próximo dos resultados do cálculo do TF2. Em suas execuções de treinamento reais, é recomendável usar tf.function com passes de otimização ativados por motivos de desempenho, mas pode ser útil desativá-los em seus testes de unidade de equivalência numérica.

Da mesma forma, você também pode descobrir que otimizadores tf.compat.v1.train e otimizadores TF2 têm propriedades numéricas de ponto flutuante ligeiramente diferentes dos otimizadores TF2, mesmo que as fórmulas matemáticas que eles estão representando sejam as mesmas. É menos provável que isso seja um problema em suas execuções de treinamento, mas pode exigir uma tolerância numérica mais alta em testes de unidade de equivalência.