Ajuda a proteger a Grande Barreira de Corais com TensorFlow em Kaggle Junte Desafio

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

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

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

Estes exemplos de código guia cobre migração com as tf.compat.v1.keras.utils.track_tf1_style_variables modelar calço aplicado a tf.keras.layers.Layer métodos. Leia o guia de mapeamento de modelo para saber mais sobre o TF2 modelagem calços.

Este guia detalha abordagens que você pode usar para:

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

Configurar

# Note that the model mapping shim is available only in TF 2.7.
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: 3081, done.[K
remote: Counting objects: 100% (3081/3081), done.[K
remote: Compressing objects: 100% (2607/2607), done.[K
remote: Total 3081 (delta 774), reused 1338 (delta 433), pack-reused 0[K
Receiving objects: 100% (3081/3081), 33.33 MiB | 19.59 MiB/s, done.
Resolving deltas: 100% (774/774), done.

Se você estiver colocando um pedaço não trivial de código de passagem de encaminhamento no shim, você deseja 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_12277/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.

Na verdade, essa camada funciona perfeitamente bem fora da caixa (completa com rastreamento preciso de perda de regularização).

No entanto, isso não é algo que você queira dar como certo. Siga as etapas abaixo para verificar se ele está realmente se comportando como no TF1.x, observando a equivalência numérica perfeita. Essas etapas também podem ajudá-lo a triangular qual parte do passe para frente está causando uma divergência de TF1.x (identifique se a divergência surge no passe para frente 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 construiu corretamente o modelo de uma forma que reutiliza 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 chamadas tf.Variable em cada chamada passe para frente, então o mais provável é não ter variáveis de captura e criação de novas 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)

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

Os segundo escopo ( catch_and_raise_created_variables() ) irá levantar uma excepção no final do escopo se quaisquer variáveis acabou de ser 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ê consiga 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 nenhuma variável nova após a primeira chamada (provavelmente 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/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 '

No exemplo abaixo, observe como esses decoradores trabalham 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_12277/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_12277/3224979076.py", line 6, in call
    var = tf.Variable(initial_value=2.0)
  File "/tmp/ipykernel_12277/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 cria os pesos apenas uma vez e, em seguida, os reutiliza 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 um explícita tf.Variable chamada sem reutilização já criados tf.Variables . Corrija isso verificando primeiro se ele não foi criado e, em seguida, reutilizando os existentes.
  2. Ela cria uma camada ou modelo Keras directamente na frente passar cada tempo (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 não consegue atribuir todos os compat.v1.layers um nome explícito ou para envolver sua compat.v1.layer dentro uso de um chamado variable_scope , fazendo com que os nomes das camadas Autogenerated para incremento na cada chamada de modelo. Corrigir isso colocando um chamado tf.compat.v1.variable_scope dentro do seu método decorado-calço que envolve todo o seu tf.compat.v1.layers uso.

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

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

Você pode fazer uma combinação de verificá-los manualmente para ver se 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 envolvida com calço no TF2. Observe que o modelo também é chamado várias vezes antes de pegar os pesos. Isso é feito para testar com eficácia 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-11-13 02:33:12.818082: 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 em que eles não correspondam, 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 rápida, 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 para os nomes de todas as variáveis criadas diretamente pelo explícitas tf.Variable chamadas e camadas Keras / modelos como sua semântica geração de nome variável podem diferir ligeiramente entre gráficos TF1.x e funcionalidade TF2 tais como a execução ansioso e tf.function mesmo se tudo outra coisa está funcionando corretamente. Se este for o seu caso, ajuste seu teste para levar em conta qualquer semântica de nomenclatura ligeiramente diferente.

  • Às vezes você pode achar que o tf.Variable s, tf.keras.layers.Layer s, ou tf.keras.Model s criado em passe para frente do seu circuito de treinamento estão ausentes da sua lista de variáveis TF2, mesmo se eles foram capturados pela coleta de variáveis em TF1.x. Corrija isso atribuindo as variáveis ​​/ camadas / modelos que seu passe de avanço cria para atributos de instância em seu modelo. Veja aqui para mais informações.

Etapa 3: redefinir todas as variáveis, verificar a equivalência numérica com toda a aleatoriedade desativada

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. Executar o modelo no modo de inferência para evitar o acionamento de quaisquer camadas de dropout que podem ser fontes de aleatoriedade.

O código a seguir demonstra como você pode comparar os resultados de TF1.x e 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 corresponder entre TF1.x e TF2 quando você remove fontes de aleatoriedade, e do TF2 compatível InceptionResnetV2 camada passa no teste.

Se estiver observando os resultados divergindo para seus próprios modelos, você pode usar impressão ou PDB e depuração interativa para identificar onde e por que os resultados começam a divergir. A execução rápida 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 pontos de extremidade intermediários que você pode sondar.

Etapa 4: Alinhe a geração de números aleatórios, verifique a equivalência numérica tanto no treinamento quanto na inferência

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

Você pode fazer isso usando a ferramenta de teste abaixo para fazer a correspondência semântica de geração de número aleatório entre gráficos / sessões TF1.x e execução rápida.

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

Em tf.compat.v1.Session s, se não houver sementes são especificadas, a geração de número aleatório depende de quantas as operações estão em gráfico na altura em que é adicionado a operação aleatória, e quantas vezes o gráfico é executada. Na execução rápida, a geração de número aleatório com estado depende da semente global, da semente aleatória da operação 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 seguir DeterministicTestTool objeto fornece um gerente de contexto scope() que pode fazer operações aleatórias stateful usar a mesma semente em ambos os gráficos TF1 / sessões e execução ansioso,

A ferramenta oferece dois modos de teste:

  1. constant que usa a mesma semente para cada operação, não importa quantas vezes ele foi chamado e,
  2. num_random_ops que utiliza o número de operações aleatórios com estado previamente observado como a semente operação.

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

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)

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

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)

No entanto, notar que em constant modo, porque b e c foram gerados com a mesma semente e tem a mesma forma, eles terão exactamente os mesmos valores.

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

Ordem de rastreamento

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

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)

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

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

Para permitir a depuração variações devido ao fim rastreamento, DeterministicTestTool em num_random_ops modo permite-lhe ver quantas operações aleatória foram rastreados com o operation_seed propriedade.

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

Se você precisa de conta para diferentes fim traço em seus testes, você pode até mesmo definir o auto-incremento operation_seed explicitamente. Por exemplo, você pode usar isso para fazer a geração de números aleatórios corresponder em duas ordens de programa diferentes.

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

No entanto, DeterministicTestTool Não permite a reutilização de sementes de operação já tenha sido usado, por isso certifique-se as sequências de auto-increment não podem se sobrepor. Isso ocorre porque a execução rápida gera números diferentes para usos subsequentes da mesma semente de operação, enquanto os gráficos e sessões TF1 não, portanto, gerar um erro ajuda a manter a sessão e a geração de números aleatórios com estado ansiosos em linha.

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.

Verificando inferência

Agora você pode usar o DeterministicTestTool para garantir que os InceptionResnetV2 partidas modelo de inferência, mesmo quando se utiliza a inicialização peso aleatória. Para uma condição de teste mais forte devido à correspondência de ordem do programa, use o num_random_ops modo.

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)

Verificando o treinamento

Porque DeterministicTestTool funciona para todas as operações aleatórias com estado (incluindo tanto a inicialização do peso e de computação, tais como camadas de abandono), você pode usá-lo para verificar os modelos corresponder no modo de treino também. Você pode usar novamente o num_random_ops modo, porque a ordem do programa dos ops aleatórios stateful corresponde.

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)

Você já verificou que o InceptionResnetV2 modelo correndo ansiosamente com decoradores em todo tf.keras.layers.Layer numericamente corresponde a rede magro correndo na TF1 gráficos e sessões.

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

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

No entanto, o padrão mode='constant' não é sensível a estas diferenças, a fim rastreamento e vai passar sem trabalho extra mesmo quando a incorporação da camada em um modelo funcional 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)

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

Após a etapa 3 ou 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 houver. Isso pode testar se o carregamento do ponto de verificação legado está funcionando corretamente e se o próprio modelo está funcionando bem. Os checkpoints Reutilizando TF1.x orientar tampas como reutilizar os seus pré-existentes checkpoints TF1.x e transferi-los para TF2 checkpoints.

Teste Adicional e Solução de Problemas

À medida que adiciona mais testes de equivalência numérica, você também pode optar por adicionar um teste que verifica a correspondência de sua computação de gradiente (ou mesmo suas atualizações de otimizador).

A retropropagação e o cálculo de gradiente são mais propensos a instabilidades numéricas de ponto flutuante do que os passes de avanço 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 a execução completa 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 isso é provável que seja o caso, você pode comparar o seu código TF1 para TF2 computação acontecendo dentro de um tf.function (que se aplica a otimização gráfico passa como seu gráfico TF1) em vez de um cálculo puramente ansioso. Alternativamente, você pode tentar usar tf.config.optimizer.set_experimental_options para desativar a otimização passa como "arithmetic_optimization" antes de sua computação TF1 para ver se o resultado acaba numericamente mais perto de seus resultados de computação TF2. Em suas corridas de treinamento real é recomendável usar tf.function com a otimização de passes habilitado por motivos de desempenho, mas você pode achar que é útil para desativá-los em seus testes de unidade equivalência numérica.

Da mesma forma, você também pode achar que tf.compat.v1.train otimizadores e otimizadores TF2 ter um pouco diferentes propriedades numerics ponto flutuante do que TF2 otimizadores, mesmo que as fórmulas matemáticas que estão representando são os mesmos. É 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.