Se usó la API de Cloud Translation para traducir esta página.
Switch to English

Depuración de problemas numéricos en programas de TensorFlow con TensorBoard Debugger V2

Los eventos catastróficos que involucran NaN a veces pueden ocurrir durante un programa de TensorFlow, lo que paraliza los procesos de entrenamiento del modelo. La causa raíz de tales eventos a menudo es oscura, especialmente para modelos de tamaño y complejidad no triviales. Para facilitar la depuración de este tipo de errores de modelo, TensorBoard 2.3+ (junto con TensorFlow 2.3+) proporciona un panel especializado llamado Debugger V2. Aquí demostramos cómo usar esta herramienta trabajando en un error real que involucra NaN en una red neuronal escrita en TensorFlow.

Las técnicas ilustradas en este tutorial son aplicables a otros tipos de actividades de depuración, como la inspección de formas de tensores en tiempo de ejecución en programas complejos. Este tutorial se centra en los NaN debido a su frecuencia relativamente alta de aparición.

Observando el error

El código fuente del programa TF2 que depuraremos está disponible en GitHub . El programa de ejemplo también está empaquetado en el paquete pip de tensorflow (versión 2.3+) y puede ser invocado por:

python -m tensorflow.python.debug.examples.v2.debug_mnist_v2

Este programa TF2 crea una percepción multicapa (MLP) y la entrena para reconocer imágenes MNIST . Este ejemplo utiliza a propósito la API de bajo nivel de TF2 para definir construcciones de capas personalizadas, función de pérdida y ciclo de entrenamiento, porque la probabilidad de errores de NaN es mayor cuando usamos esta API más flexible pero más propensa a errores que cuando usamos la más fácil API de alto nivel de uso pero ligeramente menos flexibles, como tf.keras .

El programa imprime una prueba de precisión después de cada paso de entrenamiento. Podemos ver en la consola que la precisión de la prueba se atasca en un nivel casi de probabilidad (~ 0.1) después del primer paso. Ciertamente, no es así como se espera que se comporte el entrenamiento del modelo: esperamos que la precisión se acerque gradualmente a 1.0 (100%) a medida que aumenta el paso.

Accuracy at step 0: 0.216
Accuracy at step 1: 0.098
Accuracy at step 2: 0.098
Accuracy at step 3: 0.098
...

Una suposición fundamentada es que este problema se debe a una inestabilidad numérica, como NaN o infinito. Sin embargo, ¿cómo confirmamos que este es realmente el caso y cómo encontramos la operación de TensorFlow (op) responsable de generar la inestabilidad numérica? Para responder a estas preguntas, instrumentemos el programa con errores con Debugger V2.

Instrumentar el código de TensorFlow con Debugger V2

tf.debugging.experimental.enable_dump_debug_info() es el punto de entrada de API de Debugger V2. Instrumenta un programa TF2 con una sola línea de código. Por ejemplo, agregar la siguiente línea cerca del comienzo del programa hará que la información de depuración se escriba en el directorio de registro (logdir) en / tmp / tfdbg2_logdir. La información de depuración cubre varios aspectos del tiempo de ejecución de TensorFlow. En TF2, incluye el historial completo de ejecución ansiosa, la construcción de gráficos realizada por @ tf.function , la ejecución de los gráficos, los valores de tensor generados por los eventos de ejecución, así como la ubicación del código (seguimiento de la pila de Python) de esos eventos. . La riqueza de la información de depuración permite a los usuarios limitarse a errores desconocidos.

tf.debugging.experimental.enable_dump_debug_info(
    logdir="/tmp/tfdbg2_logdir",
    tensor_debug_mode="FULL_HEALTH",
    circular_buffer_size=-1)

El argumento tensor_debug_mode controla la información que el depurador V2 extrae de cada tensor ansioso o en el gráfico. "FULL_HEALTH" es un modo que captura la siguiente información sobre cada tensor de tipo flotante (por ejemplo, el float32 comúnmente visto y el tipo dtype bfloat16 menos común):

  • DType
  • Rango
  • Número total de elementos
  • Un desglose de los elementos de tipo flotante en las siguientes categorías: finito negativo ( - ), cero ( 0 ), finito positivo ( + ), infinito negativo ( -∞ ), infinito positivo ( +∞ ) y NaN .

El modo "FULL_HEALTH" es adecuado para depurar errores relacionados con NaN e infinito. Vea a continuación otros tensor_debug_mode s tensor_debug_mode .

El argumento circular_buffer_size controla cuántos eventos de tensor se guardan en el logdir. El valor predeterminado es 1000, lo que hace que solo se guarden en el disco los últimos 1000 tensores antes del final del programa TF2 instrumentado. Este comportamiento predeterminado reduce la sobrecarga del depurador al sacrificar la integridad de los datos de depuración. Si se prefiere la integridad, como en este caso, podemos deshabilitar el búfer circular estableciendo el argumento en un valor negativo (por ejemplo, -1 aquí).

El ejemplo debug_mnist_v2 invoca enable_dump_debug_info() pasándole indicadores de línea de comandos. Para ejecutar nuestro programa problemático TF2 nuevamente con esta instrumentación de depuración habilitada, haga:

python -m tensorflow.python.debug.examples.v2.debug_mnist_v2 \
    --dump_dir /tmp/tfdbg2_logdir --dump_tensor_debug_mode FULL_HEALTH

Inicio de la GUI de Debugger V2 en TensorBoard

La ejecución del programa con la instrumentación del depurador crea un logdir en / tmp / tfdbg2_logdir. Podemos iniciar TensorBoard y apuntar al logdir con:

tensorboard --logdir /tmp/tfdbg2_logdir

En el navegador web, navegue hasta la página de TensorBoard en http: // localhost: 6006. El complemento "Debugger V2" debe estar activado de forma predeterminada, mostrando una página que se parece a la siguiente:

Captura de pantalla de vista completa del depurador V2

Uso de Debugger V2 GUI para encontrar la causa raíz de los NaN

La GUI de Debugger V2 en TensorBoard está organizada en seis secciones:

  • Alertas : esta sección superior izquierda contiene una lista de eventos de "alerta" detectados por el depurador en los datos de depuración del programa TensorFlow instrumentado. Cada alerta indica una cierta anomalía que merece atención. En nuestro caso, esta sección resalta 499 eventos NaN / ∞ con un color rosa-rojo sobresaliente. Esto confirma nuestra sospecha de que el modelo no aprende debido a la presencia de NaN y / o infinitos en sus valores tensoriales internos. Profundizaremos en estas alertas en breve.
  • Cronología de ejecución de Python : esta es la mitad superior de la sección media superior. Presenta la historia completa de la ávida ejecución de operaciones y gráficos. Cada caja de la línea de tiempo está marcado por la letra inicial de la op o el nombre del gráfico (por ejemplo, “T” para la op “TensorSliceDataset”, “m” para el “modelo” tf.function ). Podemos navegar por esta línea de tiempo usando los botones de navegación y la barra de desplazamiento sobre la línea de tiempo.
  • Ejecución de gráficos : ubicada en la esquina superior derecha de la GUI, esta sección será fundamental para nuestra tarea de depuración. Contiene un historial de todos los tensores de tipo d flotante calculados dentro de los gráficos (es decir, compilados por @tf-function ).
  • La estructura del gráfico (mitad inferior de la sección superior central), el código fuente (sección inferior izquierda) y el seguimiento de pila (sección inferior derecha) están inicialmente vacíos. Su contenido se completará cuando interactuemos con la GUI. Estas tres secciones también jugarán un papel importante en nuestra tarea de depuración.

Habiéndonos orientado a la organización de la interfaz de usuario, sigamos los siguientes pasos para llegar al fondo de por qué aparecieron las NaN. Primero, haga clic en la alerta NaN / ∞ en la sección Alertas. Esto desplaza automáticamente la lista de 600 tensores de gráfico en la sección Ejecución de gráfico y se centra en el # 88, que es un tensor llamado “Log: 0” generado por una Log (logaritmo natural). Un color rosa-rojo sobresaliente resalta un elemento -∞ entre los 1000 elementos del tensor 2D float32. Este es el primer tensor en el historial de ejecución del programa TF2 que contenía cualquier NaN o infinito: los tensores calculados antes no contienen NaN o ∞; muchos (de hecho, la mayoría) de los tensores calculados posteriormente contienen NaN. Podemos confirmar esto desplazándonos hacia arriba y hacia abajo en la lista de ejecución de gráficos. Esta observación proporciona un fuerte indicio de que Log op es la fuente de la inestabilidad numérica en este programa TF2.

Debugger V2: alertas Nan / Infinity y lista de ejecución de gráficos

¿Por qué esta operación de Log escupe un -∞? Responder a esa pregunta requiere examinar la entrada a la operación. Al hacer clic en el nombre del tensor ("Log: 0") aparece una visualización simple pero informativa de la vecindad de la operación Log en su gráfico TensorFlow en la sección Estructura del gráfico. Tenga en cuenta la dirección de arriba hacia abajo del flujo de información. La operación en sí se muestra en negrita en el medio. Inmediatamente encima de él, podemos ver que una operación de marcador de posición proporciona la única entrada para la operación de Log . ¿Dónde está el tensor generado por este marcador de posición logits en la lista de ejecución de gráficos? Al usar el color de fondo amarillo como ayuda visual, podemos ver que el tensor logits:0 está dos filas por encima del tensor Log:0 , es decir, en la fila 85.

Debugger V2: vista de la estructura del gráfico y seguimiento del tensor de entrada

Una mirada más cuidadosa al desglose numérico del tensor “logits: 0” en la fila 85 revela por qué su consumidor Log:0 produce un -∞: Entre los 1000 elementos de “logits: 0”, un elemento tiene un valor de 0. ¡El -∞ es el resultado de calcular el logaritmo natural de 0! Si de alguna manera podemos asegurarnos de que la operación de registro se exponga solo a entradas positivas, podremos evitar que ocurra el NaN / ∞. Esto se puede lograr aplicando recorte (por ejemplo, usando tf.clip_by_value () ) en el tensor de logits del marcador de posición.

Estamos cada vez más cerca de solucionar el error, pero todavía no hemos terminado. Para aplicar la solución, necesitamos saber en qué parte del código fuente de Python se originó la operación de registro y su entrada de marcador de posición. Debugger V2 proporciona soporte de primera clase para rastrear las operaciones gráficas y los eventos de ejecución hasta su origen. Cuando hicimos clic en el tensor Log:0 en Graph Executions, la sección Stack Trace se completó con el stack original de la creación de la operación Log. El seguimiento de la pila es algo grande porque incluye muchos fotogramas del código interno de TensorFlow (p. Ej., Gen_math_ops.py y dumping_callback.py), que podemos ignorar con seguridad para la mayoría de las tareas de depuración. El marco de interés es la línea 216 de debug_mnist_v2.py (es decir, el archivo Python que estamos intentando depurar). Al hacer clic en "Línea 204", aparece una vista de la línea de código correspondiente en la sección Código fuente.

Debugger V2: código fuente y seguimiento de pila

Esto finalmente nos lleva al código fuente que creó la problemática Log op a partir de su entrada de logits. Esta es nuestra función de pérdida de entropía cruzada categórica personalizada decorada con @tf.function y, por lo tanto, convertida en un gráfico de TensorFlow. El marcador de posición op "logits" corresponde al primer argumento de entrada de la función de pérdida. La operación de Log se crea con la llamada a la API tf.math.log ().

La corrección de recorte de valores de este error se verá así:

  diff = -(labels *
           tf.clip_by_value(tf.math.log(logits), 1e-6, 1.))

Resolverá la inestabilidad numérica en este programa TF2 y hará que el MLP entrene con éxito. Otro posible enfoque para corregir la inestabilidad numérica es utilizar tf.keras.losses.CategoricalCrossentropy .

Esto concluye nuestro viaje desde la observación de un error del modelo TF2 hasta la creación de un cambio de código que corrige el error, con la ayuda de la herramienta Debugger V2, que proporciona una visibilidad completa del historial de ejecución de gráficos y ansiosos del programa TF2 instrumentado, incluidos los resúmenes numéricos. de valores tensoriales y asociación entre ops, tensores y su código fuente original.

Compatibilidad de hardware de Debugger V2

Debugger V2 admite hardware de entrenamiento convencional, incluidas CPU y GPU. También se admite el entrenamiento de múltiples GPU con tf.distributed.MirroredStrategy . El soporte para TPU aún se encuentra en una etapa temprana y requiere llamar

tf.config.set_soft_device_placement(True)

antes de llamar a enable_dump_debug_info() . También puede tener otras limitaciones en las TPU. Si tiene problemas al usar Debugger V2, informe los errores en nuestra página de problemas de GitHub .

Compatibilidad con API de Debugger V2

Debugger V2 se implementa en un nivel relativamente bajo de la pila de software de TensorFlow y, por lo tanto, es compatible con tf.keras , tf.data y otras API creadas sobre los niveles inferiores de TensorFlow. Debugger V2 también es compatible con versiones anteriores de TF1, aunque la línea de tiempo de ejecución ansiosa estará vacía para los registros de depuración generados por los programas TF1.

Consejos de uso de API

Una pregunta frecuente sobre esta API de depuración es en qué lugar del código de TensorFlow se debe insertar la llamada a enable_dump_debug_info() . Por lo general, la API debe llamarse lo antes posible en su programa TF2, preferiblemente después de las líneas de importación de Python y antes de que comience la construcción y ejecución del gráfico. Esto asegurará una cobertura completa de todas las operaciones y gráficos que impulsan su modelo y su entrenamiento.

Los modos tensor_debug_modes admitidos actualmente son: NO_TENSOR , CURT_HEALTH , CONCISE_HEALTH , FULL_HEALTH y SHAPE . Varían en la cantidad de información extraída de cada tensor y la sobrecarga de rendimiento del programa depurado. Consulte la sección de enable_dump_debug_info() de la enable_dump_debug_info() de enable_dump_debug_info() ].

Sobrecarga de rendimiento

La API de depuración introduce una sobrecarga de rendimiento en el programa TensorFlow instrumentado. La sobrecarga varía según tensor_debug_mode , el tipo de hardware y la naturaleza del programa TensorFlow instrumentado. Como punto de referencia, en una GPU, el modo NO_TENSOR agrega una sobrecarga del 15% durante el entrenamiento de un modelo de Transformer con un tamaño de lote 64. El porcentaje de sobrecarga para otros modos tensor_debug_modes es mayor: aproximadamente 50% para CURT_HEALTH , CONCISE_HEALTH , FULL_HEALTH y SHAPE modos. En las CPU, la sobrecarga es ligeramente menor. En las TPU, la sobrecarga es actualmente mayor.

Relación con otras API de depuración de TensorFlow

Tenga en cuenta que TensorFlow ofrece otras herramientas y API para depurar. Puede examinar dichas API en el espacio de nombres tf.debugging.* En la página de documentos de API. Entre estas API, la más utilizada es tf.print() . ¿Cuándo se debe usar Debugger V2 y cuándo se debe tf.print() su lugar? tf.print() es conveniente en caso de que

  1. sabemos exactamente qué tensores imprimir,
  2. sabemos dónde exactamente en el código fuente insertar esas declaraciones tf.print() ,
  3. el número de estos tensores no es demasiado grande.

Para otros casos (p. Ej., Examinar muchos valores de tensor, examinar los valores de tensor generados por el código interno de TensorFlow y buscar el origen de la inestabilidad numérica como mostramos anteriormente), Debugger V2 proporciona una forma más rápida de depuración. Además, Debugger V2 proporciona un enfoque unificado para inspeccionar tensores ansiosos y gráficos. Además, proporciona información sobre la estructura del gráfico y las ubicaciones del código, que están más allá de la capacidad de tf.print() .

Otra API que se puede utilizar para depurar problemas relacionados con ∞ y NaN es tf.debugging.enable_check_numerics() . A diferencia de enable_dump_debug_info() , enable_check_numerics() no guarda información de depuración en el disco. En cambio, simplemente monitorea ∞ y NaN durante el tiempo de ejecución de TensorFlow y los errores con la ubicación del código de origen tan pronto como cualquier operación genera valores numéricos tan malos. Tiene una sobrecarga de rendimiento más baja en comparación con enable_dump_debug_info() , pero no permite un seguimiento completo del historial de ejecución del programa y no viene con una interfaz gráfica de usuario como Debugger V2.