Depuración de la canalización de formación migrada de TF2

Ver en TensorFlow.org Ejecutar en Google Colab Ver fuente en GitHub Descargar libreta

Este cuaderno demuestra cómo depurar la canalización de entrenamiento al migrar a TF2. Consta de los siguientes componentes:

  1. Pasos sugeridos y ejemplos de código para depurar canalización de entrenamiento
  2. Herramientas para depurar
  3. Otros recursos relacionados

Una suposición es que tiene código TF1.x y modelos entrenados para comparar, y desea crear un modelo TF2 que logre una precisión de validación similar.

Este portátil NO cubre problemas de rendimiento de depuración para entrenamiento/velocidad de inferencia o uso de memoria.

Flujo de trabajo de depuración

A continuación, se muestra un flujo de trabajo general para depurar las canalizaciones de entrenamiento de TF2. Tenga en cuenta que no necesita seguir estos pasos en orden. También puede usar un enfoque de búsqueda binaria en el que prueba el modelo en un paso intermedio y reduce el alcance de la depuración.

  1. Corregir errores de compilación y tiempo de ejecución

  2. Validación de pase de avance único (en una guía separada)

    una. En un solo dispositivo de CPU

    • Verifique que las variables se crean solo una vez
    • Compruebe la coincidencia de recuentos, nombres y formas de variables
    • Restablecer todas las variables, verificar la equivalencia numérica con toda la aleatoriedad deshabilitada
    • Alinee la generación de números aleatorios, verifique la equivalencia numérica en la inferencia
    • (Opcional) Verifique que los puntos de control se carguen correctamente y que los modelos TF1.x/TF2 generen una salida idéntica

    B. En un único dispositivo GPU/TPU

    C. Con estrategias multidispositivo

  3. Validación de equivalencia numérica de entrenamiento de modelos para algunos pasos (ejemplos de código disponibles a continuación)

    una. Validación de un solo paso de entrenamiento utilizando datos pequeños y fijos en un solo dispositivo de CPU. Específicamente, verifique la equivalencia numérica para los siguientes componentes

    • cálculo de pérdidas
    • métrica
    • tasa de aprendizaje
    • cálculo y actualización de gradientes

    B. Verifique las estadísticas después de entrenar 3 o más pasos para verificar los comportamientos del optimizador como el impulso, aún con datos fijos en un solo dispositivo de CPU

    C. En un único dispositivo GPU/TPU

    D. Con estrategias multidispositivo (consulte la introducción de MultiProcessRunner en la parte inferior)

  4. Pruebas de cobertura de extremo a extremo en conjuntos de datos reales

    una. Comprueba los comportamientos de entrenamiento con TensorBoard

    • use optimizadores simples, por ejemplo, SGD y estrategias de distribución simples, por ejemplo, tf.distribute.OneDeviceStrategy primero
    • métricas de entrenamiento
    • métricas de evaluación
    • averiguar cuál es la tolerancia razonable para la aleatoriedad inherente

    B. Verifique la equivalencia con el optimizador avanzado/programador de tasa de aprendizaje/estrategias de distribución

    C. Comprobar la equivalencia cuando se utiliza precisión mixta

  5. Puntos de referencia de productos adicionales

Configuración

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

Validación de pase de avance único

La validación de pase de avance único, incluida la carga del punto de control, se cubre en una colaboración diferente.

import sys
import unittest
import numpy as np

import tensorflow as tf
import tensorflow.compat.v1 as v1

Validación de la equivalencia numérica del entrenamiento del modelo en unos pocos pasos

Configure la configuración del modelo y prepare un conjunto de datos falso.

params = {
    'input_size': 3,
    'num_classes': 3,
    'layer_1_size': 2,
    'layer_2_size': 2,
    'num_train_steps': 100,
    'init_lr': 1e-3,
    'end_lr': 0.0,
    'decay_steps': 1000,
    'lr_power': 1.0,
}

# make a small fixed dataset
fake_x = np.ones((2, params['input_size']), dtype=np.float32)
fake_y = np.zeros((2, params['num_classes']), dtype=np.int32)
fake_y[0][0] = 1
fake_y[1][1] = 1

step_num = 3

Defina el modelo TF1.x.

# Assume there is an existing TF1.x model using estimator API
# Wrap the model_fn to log necessary tensors for result comparison
class SimpleModelWrapper():
  def __init__(self):
    self.logged_ops = {}
    self.logs = {
        'step': [],
        'lr': [],
        'loss': [],
        'grads_and_vars': [],
        'layer_out': []}

  def model_fn(self, features, labels, mode, params):
      out_1 = tf.compat.v1.layers.dense(features, units=params['layer_1_size'])
      out_2 = tf.compat.v1.layers.dense(out_1, units=params['layer_2_size'])
      logits = tf.compat.v1.layers.dense(out_2, units=params['num_classes'])
      loss = tf.compat.v1.losses.softmax_cross_entropy(labels, logits)

      # skip EstimatorSpec details for prediction and evaluation 
      if mode == tf.estimator.ModeKeys.PREDICT:
          pass
      if mode == tf.estimator.ModeKeys.EVAL:
          pass
      assert mode == tf.estimator.ModeKeys.TRAIN

      global_step = tf.compat.v1.train.get_or_create_global_step()
      lr = tf.compat.v1.train.polynomial_decay(
        learning_rate=params['init_lr'],
        global_step=global_step,
        decay_steps=params['decay_steps'],
        end_learning_rate=params['end_lr'],
        power=params['lr_power'])

      optmizer = tf.compat.v1.train.GradientDescentOptimizer(lr)
      grads_and_vars = optmizer.compute_gradients(
          loss=loss,
          var_list=graph.get_collection(
              tf.compat.v1.GraphKeys.TRAINABLE_VARIABLES))
      train_op = optmizer.apply_gradients(
          grads_and_vars,
          global_step=global_step)

      # log tensors
      self.logged_ops['step'] = global_step
      self.logged_ops['lr'] = lr
      self.logged_ops['loss'] = loss
      self.logged_ops['grads_and_vars'] = grads_and_vars
      self.logged_ops['layer_out'] = {
          'layer_1': out_1,
          'layer_2': out_2,
          'logits': logits}

      return tf.estimator.EstimatorSpec(mode, loss=loss, train_op=train_op)

  def update_logs(self, logs):
    for key in logs.keys():
      model_tf1.logs[key].append(logs[key])

La siguiente clase v1.keras.utils.DeterministicRandomTestTool proporciona un administrador de contexto scope() que puede hacer que las operaciones aleatorias con estado utilicen la misma semilla en gráficos/sesiones de TF1 y una ejecución entusiasta.

La herramienta proporciona dos modos de prueba:

  1. constant que usa la misma semilla para cada operación sin importar cuántas veces se haya llamado y,
  2. num_random_ops que usa el número de operaciones aleatorias con estado observadas previamente como semilla de operación.

Esto se aplica tanto a las operaciones aleatorias con estado utilizadas para crear e inicializar variables, como a las operaciones aleatorias con estado utilizadas en el cálculo (como las capas de abandono).

random_tool = v1.keras.utils.DeterministicRandomTestTool(mode='num_random_ops')
WARNING:tensorflow:From /tmp/ipykernel_26769/2689227634.py:1: The name tf.keras.utils.DeterministicRandomTestTool is deprecated. Please use tf.compat.v1.keras.utils.DeterministicRandomTestTool instead.

Ejecute el modelo TF1.x en modo gráfico. Recopile estadísticas para los primeros 3 pasos de entrenamiento para la comparación de equivalencia numérica.

with random_tool.scope():
  graph = tf.Graph()
  with graph.as_default(), tf.compat.v1.Session(graph=graph) as sess:
    model_tf1 = SimpleModelWrapper()
    # build the model
    inputs = tf.compat.v1.placeholder(tf.float32, shape=(None, params['input_size']))
    labels = tf.compat.v1.placeholder(tf.float32, shape=(None, params['num_classes']))
    spec = model_tf1.model_fn(inputs, labels, tf.estimator.ModeKeys.TRAIN, params)
    train_op = spec.train_op

    sess.run(tf.compat.v1.global_variables_initializer())
    for step in range(step_num):
      # log everything and update the model for one step
      logs, _ = sess.run(
          [model_tf1.logged_ops, train_op],
          feed_dict={inputs: fake_x, labels: fake_y})
      model_tf1.update_logs(logs)
/tmpfs/src/tf_docs_env/lib/python3.7/site-packages/ipykernel_launcher.py:14: UserWarning: `tf.layers.dense` is deprecated and will be removed in a future version. Please use `tf.keras.layers.Dense` instead.
  
/tmpfs/src/tf_docs_env/lib/python3.7/site-packages/keras/legacy_tf_layers/core.py:261: UserWarning: `layer.apply` is deprecated and will be removed in a future version. Please use `layer.__call__` method instead.
  return layer.apply(inputs)
/tmpfs/src/tf_docs_env/lib/python3.7/site-packages/ipykernel_launcher.py:15: UserWarning: `tf.layers.dense` is deprecated and will be removed in a future version. Please use `tf.keras.layers.Dense` instead.
  from ipykernel import kernelapp as app
/tmpfs/src/tf_docs_env/lib/python3.7/site-packages/ipykernel_launcher.py:16: UserWarning: `tf.layers.dense` is deprecated and will be removed in a future version. Please use `tf.keras.layers.Dense` instead.
  app.launch_new_instance()

Definir el modelo TF2.

class SimpleModel(tf.keras.Model):
  def __init__(self, params, *args, **kwargs):
    super(SimpleModel, self).__init__(*args, **kwargs)
    # define the model
    self.dense_1 = tf.keras.layers.Dense(params['layer_1_size'])
    self.dense_2 = tf.keras.layers.Dense(params['layer_2_size'])
    self.out = tf.keras.layers.Dense(params['num_classes'])
    learning_rate_fn = tf.keras.optimizers.schedules.PolynomialDecay(
      initial_learning_rate=params['init_lr'],
      decay_steps=params['decay_steps'],
      end_learning_rate=params['end_lr'],
      power=params['lr_power'])  
    self.optimizer = tf.keras.optimizers.SGD(learning_rate_fn)
    self.compiled_loss = tf.keras.losses.CategoricalCrossentropy(from_logits=True)
    self.logs = {
        'lr': [],
        'loss': [],
        'grads': [],
        'weights': [],
        'layer_out': []}

  def call(self, inputs):
    out_1 = self.dense_1(inputs)
    out_2 = self.dense_2(out_1)
    logits = self.out(out_2)
    # log output features for every layer for comparison
    layer_wise_out = {
        'layer_1': out_1,
        'layer_2': out_2,
        'logits': logits}
    self.logs['layer_out'].append(layer_wise_out)
    return logits

  def train_step(self, data):
    x, y = data
    with tf.GradientTape() as tape:
      logits = self(x)
      loss = self.compiled_loss(y, logits)
    grads = tape.gradient(loss, self.trainable_weights)
    # log training statistics
    step = self.optimizer.iterations.numpy()
    self.logs['lr'].append(self.optimizer.learning_rate(step).numpy())
    self.logs['loss'].append(loss.numpy())
    self.logs['grads'].append(grads)
    self.logs['weights'].append(self.trainable_weights)
    # update model
    self.optimizer.apply_gradients(zip(grads, self.trainable_weights))
    return

Ejecute el modelo TF2 en modo ansioso. Recopile estadísticas para los primeros 3 pasos de entrenamiento para la comparación de equivalencia numérica.

random_tool = v1.keras.utils.DeterministicRandomTestTool(mode='num_random_ops')
with random_tool.scope():
  model_tf2 = SimpleModel(params)
  for step in range(step_num):
    model_tf2.train_step([fake_x, fake_y])

Compare la equivalencia numérica de los primeros pasos de entrenamiento.

También puede consultar el cuaderno Validación de corrección y equivalencia numérica para obtener consejos adicionales sobre equivalencia numérica.

np.testing.assert_allclose(model_tf1.logs['lr'], model_tf2.logs['lr'])
np.testing.assert_allclose(model_tf1.logs['loss'], model_tf2.logs['loss'])
for step in range(step_num):
  for name in model_tf1.logs['layer_out'][step]:
    np.testing.assert_allclose(
        model_tf1.logs['layer_out'][step][name],
        model_tf2.logs['layer_out'][step][name])

Pruebas unitarias

Hay algunos tipos de pruebas unitarias que pueden ayudar a depurar su código de migración.

  1. Validación de pase de avance único
  2. Validación de la equivalencia numérica del entrenamiento del modelo en unos pocos pasos
  3. Rendimiento de inferencia de referencia
  4. El modelo entrenado hace predicciones correctas en puntos de datos fijos y simples

Puede usar @parameterized.parameters para probar modelos con diferentes configuraciones. Detalles con ejemplo de código .

Tenga en cuenta que es posible ejecutar API de sesión y ejecución ansiosa en el mismo caso de prueba. Los fragmentos de código a continuación muestran cómo.

import unittest

class TestNumericalEquivalence(unittest.TestCase):

  # copied from code samples above
  def setup(self):
    # record statistics for 100 training steps
    step_num = 100

    # setup TF 1 model
    random_tool = v1.keras.utils.DeterministicRandomTestTool(mode='num_random_ops')
    with random_tool.scope():
      # run TF1.x code in graph mode with context management
      graph = tf.Graph()
      with graph.as_default(), tf.compat.v1.Session(graph=graph) as sess:
        self.model_tf1 = SimpleModelWrapper()
        # build the model
        inputs = tf.compat.v1.placeholder(tf.float32, shape=(None, params['input_size']))
        labels = tf.compat.v1.placeholder(tf.float32, shape=(None, params['num_classes']))
        spec = self.model_tf1.model_fn(inputs, labels, tf.estimator.ModeKeys.TRAIN, params)
        train_op = spec.train_op

        sess.run(tf.compat.v1.global_variables_initializer())
        for step in range(step_num):
          # log everything and update the model for one step
          logs, _ = sess.run(
              [self.model_tf1.logged_ops, train_op],
              feed_dict={inputs: fake_x, labels: fake_y})
          self.model_tf1.update_logs(logs)

    # setup TF2 model
    random_tool = v1.keras.utils.DeterministicRandomTestTool(mode='num_random_ops')
    with random_tool.scope():
      self.model_tf2 = SimpleModel(params)
      for step in range(step_num):
        self.model_tf2.train_step([fake_x, fake_y])

  def test_learning_rate(self):
    np.testing.assert_allclose(
        self.model_tf1.logs['lr'],
        self.model_tf2.logs['lr'])

  def test_training_loss(self):
    # adopt different tolerance strategies before and after 10 steps
    first_n_step = 10

    # abosolute difference is limited below 1e-5
    # set `equal_nan` to be False to detect potential NaN loss issues
    abosolute_tolerance = 1e-5
    np.testing.assert_allclose(
        actual=self.model_tf1.logs['loss'][:first_n_step],
        desired=self.model_tf2.logs['loss'][:first_n_step],
        atol=abosolute_tolerance,
        equal_nan=False)

    # relative difference is limited below 5%
    relative_tolerance = 0.05
    np.testing.assert_allclose(self.model_tf1.logs['loss'][first_n_step:],
                               self.model_tf2.logs['loss'][first_n_step:],
                               rtol=relative_tolerance,
                               equal_nan=False)

Herramientas de depuración

tf.imprimir

tf.print frente a print/logging.info

  • Con argumentos configurables, tf.print puede mostrar recursivamente los primeros y últimos elementos de cada dimensión para tensores impresos. Consulte los documentos de la API para obtener más información.
  • Para una ejecución rápida, tanto print como tf.print imprimen el valor del tensor. Pero print puede implicar una copia de dispositivo a host, lo que potencialmente puede ralentizar su código.
  • Para el modo gráfico, incluido el uso dentro de tf.function , debe usar tf.print para imprimir el valor real del tensor. tf.print se compila en una operación en el gráfico, mientras que print y logging.info solo se registran en el momento del rastreo, que a menudo no es lo que desea.
  • tf.print también admite la impresión de tensores compuestos como tf.RaggedTensor y tf.sparse.SparseTensor .
  • También puede usar una devolución de llamada para monitorear métricas y variables. Verifique cómo usar devoluciones de llamada personalizadas con registros dict y atributo self.model .

tf.print vs print dentro de tf.function

# `print` prints info of tensor object
# `tf.print` prints the tensor value
@tf.function
def dummy_func(num):
  num += 1
  print(num)
  tf.print(num)
  return num

_ = dummy_func(tf.constant([1.0]))

# Output:
# Tensor("add:0", shape=(1,), dtype=float32)
# [2]
Tensor("add:0", shape=(1,), dtype=float32)
[2]

tf.distribuir.Estrategia

  • Si la función tf.print tf.function ejecuta en los trabajadores, por ejemplo, al usar TPUStrategy o ParameterServerStrategy , debe verificar los registros del servidor de parámetros/trabajadores para encontrar los valores impresos.
  • Para print o logging.info , los registros se imprimirán en el coordinador cuando se use ParameterServerStrategy , y los registros se imprimirán en STDOUT en worker0 cuando se usen TPU.

tf.keras.modelo

  • Al usar modelos API funcionales y secuenciales, si desea imprimir valores, por ejemplo, entradas de modelo o características intermedias después de algunas capas, tiene las siguientes opciones.
    1. Escriba una capa personalizada que tf.print las entradas.
    2. Incluya las salidas intermedias que desea inspeccionar en las salidas del modelo.
  • Las capas tf.keras.layers.Lambda tienen limitaciones de (des)serialización. Para evitar problemas de carga de puntos de control, escriba una capa subclase personalizada en su lugar. Consulte los documentos de la API para obtener más detalles.
  • No puede tf.print salidas intermedias en un tf.keras.callbacks.LambdaCallback si no tiene acceso a los valores reales, sino solo a los objetos de tensor de Keras simbólicos.

Opción 1: escribir una capa personalizada

class PrintLayer(tf.keras.layers.Layer):
  def call(self, inputs):
    tf.print(inputs)
    return inputs

def get_model():
  inputs = tf.keras.layers.Input(shape=(1,))
  out_1 = tf.keras.layers.Dense(4)(inputs)
  out_2 = tf.keras.layers.Dense(1)(out_1)
  # use custom layer to tf.print intermediate features
  out_3 = PrintLayer()(out_2)
  model = tf.keras.Model(inputs=inputs, outputs=out_3)
  return model

model = get_model()
model.compile(optimizer="adam", loss="mse")
model.fit([1, 2, 3], [0.0, 0.0, 1.0])
[[-0.327884018]
 [-0.109294668]
 [-0.218589336]]
1/1 [==============================] - 0s 280ms/step - loss: 0.6077
<keras.callbacks.History at 0x7f63d46bf190>

Opción 2: incluya los resultados intermedios que desea inspeccionar en los resultados del modelo.

Tenga en cuenta que, en tal caso, es posible que necesite algunas personalizaciones para usar Model.fit .

def get_model():
  inputs = tf.keras.layers.Input(shape=(1,))
  out_1 = tf.keras.layers.Dense(4)(inputs)
  out_2 = tf.keras.layers.Dense(1)(out_1)
  # include intermediate values in model outputs
  model = tf.keras.Model(
      inputs=inputs,
      outputs={
          'inputs': inputs,
          'out_1': out_1,
          'out_2': out_2})
  return model

pdb

Puede usar pdb tanto en la terminal como en Colab para inspeccionar los valores intermedios para la depuración.

Visualizar gráfico con TensorBoard

Puede examinar el gráfico de TensorFlow con TensorBoard . TensorBoard también es compatible con colab . TensorBoard es una gran herramienta para visualizar resúmenes. Puede usarlo para comparar la tasa de aprendizaje, los pesos del modelo, la escala de gradiente, las métricas de entrenamiento/validación o incluso modelar resultados intermedios entre el modelo TF1.x y el modelo TF2 migrado a través del proceso de entrenamiento y ver si los valores se ven como se esperaba.

Perfilador TensorFlow

TensorFlow Profiler puede ayudarlo a visualizar la línea de tiempo de ejecución en GPU/TPU. Puede consultar esta demostración de Colab para conocer su uso básico.

Ejecutor multiproceso

MultiProcessRunner es una herramienta útil al depurar con MultiWorkerMirroredStrategy y ParameterServerStrategy. Puede echar un vistazo a este ejemplo concreto para su uso.

Específicamente para los casos de estas dos estrategias, se recomienda 1) no solo tener pruebas unitarias para cubrir su flujo, 2) sino también intentar reproducir fallas usándolas en pruebas unitarias para evitar iniciar un trabajo distribuido real cada vez que intentan un arreglo.