Debugowanie zmigrowanego potoku szkoleniowego TF2

Zobacz na TensorFlow.org Uruchom w Google Colab Wyświetl źródło na GitHub Pobierz notatnik

W tym notatniku pokazano, jak debugować potok uczenia podczas migracji do TF2. Składa się z następujących elementów:

  1. Sugerowane kroki i próbki kodu do debugowania potoku szkoleniowego
  2. Narzędzia do debugowania
  3. Inne powiązane zasoby

Jedno założenie jest takie, że masz kod TF1.x i wytrenowane modele do porównania i chcesz zbudować model TF2, który osiągnie podobną dokładność walidacji.

Ten notebook NIE obejmuje problemów z wydajnością debugowania w zakresie szybkości uczenia/wnioskowania lub wykorzystania pamięci.

Debugowanie przepływu pracy

Poniżej znajduje się ogólny przepływ pracy dotyczący debugowania potoków szkoleniowych TF2. Pamiętaj, że nie musisz wykonywać tych kroków w kolejności. Możesz również użyć podejścia wyszukiwania binarnego, w którym testujesz model w kroku pośrednim i zawężasz zakres debugowania.

  1. Napraw błędy kompilacji i działania

  2. Walidacja pojedynczego przejścia do przodu (w osobnym przewodniku )

    a. Na urządzeniu z jednym procesorem

    • Sprawdź, czy zmienne są tworzone tylko raz
    • Sprawdź, czy liczby zmiennych, nazwy i kształty pasują do siebie
    • Zresetuj wszystkie zmienne, sprawdź równoważność liczbową z wyłączoną losowością
    • Wyrównaj generowanie liczb losowych, sprawdź równoważność liczbową w wnioskowaniu
    • (Opcjonalnie) Sprawdź, czy punkty kontrolne są prawidłowo załadowane, a modele TF1.x/TF2 generują identyczne dane wyjściowe

    b. Na jednym urządzeniu GPU/TPU

    C. Dzięki strategiom na wiele urządzeń

  3. Walidacja równoważności numerycznej szkolenia modelowego w kilku krokach (próbki kodu dostępne poniżej)

    a. Walidacja pojedynczego etapu szkolenia przy użyciu małych i stałych danych na jednym urządzeniu z procesorem. W szczególności sprawdź równoważność liczbową następujących składników

    • obliczanie strat
    • metryka
    • szybkość uczenia się
    • obliczanie i aktualizacja gradientu

    b. Sprawdź statystyki po treningu 3 lub więcej kroków, aby zweryfikować zachowania optymalizatora, takie jak pęd, nadal ze stałymi danymi na jednym urządzeniu z procesorem

    C. Na jednym urządzeniu GPU/TPU

    D. Ze strategiami wielu urządzeń (sprawdź intro do MultiProcessRunner na dole)

  4. Kompleksowe testowanie pokrycia na rzeczywistym zbiorze danych

    a. Sprawdź zachowania treningowe z TensorBoard

    • użyj najpierw prostych optymalizatorów, np. SGD i prostych strategii dystrybucji, np tf.distribute.OneDeviceStrategy
    • metryki treningowe
    • metryki oceny
    • dowiedzieć się, jaka jest rozsądna tolerancja dla wrodzonej przypadkowości

    b. Sprawdź równoważność za pomocą zaawansowanego optymalizatora/harmonogramu szybkości uczenia się/strategii dystrybucji

    C. Sprawdź równoważność podczas korzystania z precyzji mieszanej

  5. Dodatkowe testy porównawcze produktów

Ustawiać

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

Weryfikacja pojedynczego przejścia do przodu

Weryfikacja pojedynczego przejścia do przodu, w tym ładowanie punktów kontrolnych, jest omówiona w innym colab .

import sys
import unittest
import numpy as np

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

Uczenie modelu walidacja równoważności numerycznej w kilku krokach

Ustaw konfigurację modelu i przygotuj fałszywy zbiór danych.

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

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

Poniższa klasa v1.keras.utils.DeterministicRandomTestTool udostępnia scope() menedżera kontekstu, który może sprawić, że stanowe operacje losowe będą używać tego samego źródła zarówno na wykresach/sesjach TF1, jak i na przyspieszonym wykonywaniu,

Narzędzie udostępnia dwa tryby testowania:

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

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

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.

Uruchom model TF1.x w trybie wykresu. Zbierz statystyki dla pierwszych 3 kroków szkolenia w celu porównania równoważności liczbowej.

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

Zdefiniuj model 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

Uruchom model TF2 w trybie przyspieszonym. Zbierz statystyki dla pierwszych 3 kroków szkolenia w celu porównania równoważności liczbowej.

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

Porównaj równoważność liczbową dla kilku pierwszych kroków szkoleniowych.

Możesz również sprawdzić notatnik Walidacja poprawności i równoważności liczbowej, aby uzyskać dodatkowe porady dotyczące równoważności liczbowej.

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

Testy jednostkowe

Istnieje kilka typów testów jednostkowych, które mogą pomóc w debugowaniu kodu migracji.

  1. Weryfikacja pojedynczego przejścia do przodu
  2. Uczenie modelu walidacja równoważności numerycznej w kilku krokach
  3. Wydajność wnioskowania porównawczego
  4. Wytrenowany model dokonuje prawidłowych prognoz na stałych i prostych punktach danych

Możesz użyć @parameterized.parameters do testowania modeli o różnych konfiguracjach. Szczegóły z próbką kodu .

Zwróć uwagę, że możliwe jest uruchomienie interfejsów API sesji i przyspieszonego wykonania w tym samym przypadku testowym. Poniższe fragmenty kodu pokazują, jak to zrobić.

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)

Narzędzia do debugowania

tf.print

tf.print a print/logging.info

  • Dzięki konfigurowalnym argumentom, tf.print może rekursywnie wyświetlać pierwsze i ostatnie kilka elementów każdego wymiaru dla drukowanych tensorów. Szczegóły znajdziesz w dokumentacji API .
  • W celu szybkiego wykonania zarówno print , jak i tf.print wypisują wartość tensora. Jednak print może wiązać się z kopiowaniem z urządzenia do hosta, co może potencjalnie spowolnić kod.
  • W przypadku trybu wykresu zawierającego użycie wewnątrz tf.function , musisz użyć tf.print do wydrukowania rzeczywistej wartości tensora. tf.print jest kompilowany jako operacja na wykresie, podczas gdy print i logging.info logują się tylko w czasie śledzenia, co często nie jest tym, czego chcesz.
  • tf.print obsługuje również drukowanie złożonych tensorów, takich jak tf.RaggedTensor i tf.sparse.SparseTensor .
  • Możesz również użyć wywołania zwrotnego do monitorowania metryk i zmiennych. Sprawdź, jak używać niestandardowych wywołań zwrotnych z logami dict i atrybutem self.model .

tf.print vs print wewnątrz 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.distribute.Strategy

  • Jeśli funkcja tf.print tf.function wykonywana na pracownikach, na przykład podczas korzystania z TPUStrategy lub ParameterServerStrategy , należy sprawdzić logi pracownika/serwera parametrów, aby znaleźć drukowane wartości.
  • W przypadku print lub logging.info , logi będą drukowane na koordynatorze podczas korzystania z ParameterServerStrategy , a logi będą drukowane na STDOUT na worker0 podczas korzystania z TPU.

tf.keras.Model

  • W przypadku korzystania z modeli sekwencyjnego i funkcjonalnego interfejsu API, jeśli chcesz wydrukować wartości, np. dane wejściowe modelu lub elementy pośrednie po niektórych warstwach, dostępne są następujące opcje.
    1. Napisz niestandardową warstwę , która tf.print dane wejściowe.
    2. Uwzględnij wyniki pośrednie, które chcesz sprawdzić, w wynikach modelu.
  • Warstwy tf.keras.layers.Lambda mają ograniczenia (de)serializacji. Aby uniknąć problemów z ładowaniem punktów kontrolnych, zamiast tego napisz niestandardową warstwę podklasy. Więcej informacji znajdziesz w dokumentacji API .
  • Nie możesz tf.print wyników pośrednich w tf.keras.callbacks.LambdaCallback , jeśli nie masz dostępu do rzeczywistych wartości, ale zamiast tego tylko do symbolicznych obiektów tensora Keras.

Opcja 1: napisz własną warstwę

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>

Opcja 2: uwzględnij wyjścia pośrednie, które chcesz skontrolować, w wyjściach modelu.

Pamiętaj, że w takim przypadku możesz potrzebować pewnych dostosowań , aby użyć 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

Możesz użyć pdb zarówno w terminalu, jak i Colab, aby sprawdzić wartości pośrednie do debugowania.

Wizualizuj wykres za pomocą TensorBoard

Możesz zbadać wykres TensorFlow za pomocą TensorBoard . TensorBoard jest również obsługiwany w colab . TensorBoard to świetne narzędzie do wizualizacji podsumowań. Można go użyć do porównania szybkości uczenia się, wag modelu, skali gradientu, metryk uczenia/walidacji, a nawet pośrednich wyników modelowania między modelem TF1.x a zmigrowanym modelem TF2 w procesie uczenia i sprawdzania, czy wartości wyglądają zgodnie z oczekiwaniami.

TensorFlow Profiler

TensorFlow Profiler może pomóc w wizualizacji osi czasu wykonania na GPU/TPU. Możesz sprawdzić to demo Colab , aby zapoznać się z jego podstawowymi zastosowaniami.

MultiProcessRunner

MultiProcessRunner to przydatne narzędzie podczas debugowania za pomocą MultiWorkerMirroredStrategy i ParameterServerStrategy. Możesz spojrzeć na ten konkretny przykład pod kątem jego zastosowania.

Szczególnie w przypadku tych dwóch strategii zaleca się 1) nie tylko przeprowadzanie testów jednostkowych, aby pokryć ich przebieg, 2) ale także próbę odtworzenia awarii przy użyciu ich w teście jednostkowym, aby uniknąć uruchamiania rzeczywistego zadania rozproszonego za każdym razem, gdy próbują. naprawa.