Esta página foi traduzida pela API Cloud Translation.
Switch to English

Depurando problemas numéricos em programas TensorFlow usando o TensorBoard Debugger V2

Às vezes, eventos catastróficos envolvendo NaN s podem ocorrer durante um programa TensorFlow, prejudicando os processos de treinamento do modelo. A causa raiz de tais eventos geralmente é obscura, especialmente para modelos de tamanho e complexidade não triviais. Para facilitar a depuração deste tipo de modelo, o TensorBoard 2.3+ (junto com o TensorFlow 2.3+) fornece um painel especializado chamado Debugger V2. Aqui, demonstramos como usar essa ferramenta, trabalhando com um bug real envolvendo NaNs em uma rede neural escrita em TensorFlow.

As técnicas ilustradas neste tutorial são aplicáveis ​​a outros tipos de atividades de depuração, como inspeção de formas de tensores de tempo de execução em programas complexos. Este tutorial enfoca os NaNs devido à sua frequência de ocorrência relativamente alta.

Observando o bug

O código fonte do programa TF2 que debugaremos está disponível no GitHub . O programa de exemplo também é empacotado no pacote pip tensorflow (versão 2.3+) e pode ser chamado por:

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

Este programa TF2 cria uma percepção multi-camada (MLP) e a treina para reconhecer imagens MNIST . Este exemplo usa intencionalmente a API de baixo nível do TF2 para definir construções personalizadas da camada, função de perda e loop de treinamento, porque a probabilidade de erros de NaN é maior quando usamos essa API mais flexível, mas mais propensa a erros do que quando usamos a mais fácil. APIs de alto nível um pouco menos flexíveis, como tf.keras .

O programa imprime uma precisão de teste após cada etapa do treinamento. Podemos ver no console que a precisão do teste fica presa a um nível quase casual (~ 0,1) após o primeiro passo. Certamente não é assim que o treinamento do modelo deve se comportar: esperamos que a precisão se aproxime gradualmente de 1,0 (100%) à medida que a etapa aumenta.

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

Um palpite fundamentado é que esse problema é causado por uma instabilidade numérica, como NaN ou infinito. No entanto, como confirmamos que esse é realmente o caso e como achamos a operação TensorFlow (op) responsável por gerar a instabilidade numérica? Para responder a essas perguntas, vamos instrumentar o programa de buggy com o Debugger V2.

Código do TensorFlow de instrumentação com o Depurador V2

tf.debugging.experimental.enable_dump_debug_info() é o ponto de entrada da API do Debugger V2. Ele instrumenta um programa TF2 com uma única linha de código. Por exemplo, adicionar a seguinte linha perto do início do programa fará com que as informações de depuração sejam gravadas no diretório de log (logdir) em / tmp / tfdbg2_logdir. As informações de depuração cobrem vários aspectos do tempo de execução do TensorFlow. No TF2, ele inclui o histórico completo de execução ágil , construção de gráficos realizada por @ tf.function , execução de gráficos, valores de tensores gerados pelos eventos de execução, bem como a localização do código (rastreios de pilha Python) desses eventos. . A riqueza das informações de depuração permite que os usuários reduzam os erros obscuros.

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

O argumento tensor_debug_mode controla quais informações o Debugger V2 extrai de cada tensor ansioso ou no gráfico. "FULL_HEALTH" é um modo que captura as seguintes informações sobre cada tensor do tipo flutuante (por exemplo, o float32 comumente visto e o dtype bfloat16 menos comum):

  • DType
  • Classificação
  • Número total de elementos
  • Uma divisão dos elementos do tipo flutuante nas seguintes categorias: finito negativo ( - ), zero ( 0 ), finito positivo ( + ), infinito negativo ( -∞ ), infinito positivo ( +∞ ) e NaN .

O modo "FULL_HEALTH" é adequado para depurar bugs que envolvem NaN e infinito. Veja abaixo outros tensor_debug_mode s suportados.

O argumento circular_buffer_size controla quantos eventos tensores são salvos no logdir. O padrão é 1000, o que faz com que apenas os últimos 1000 tensores antes do final do programa TF2 instrumentado sejam salvos no disco. Esse comportamento padrão reduz a sobrecarga do depurador, sacrificando a integridade dos dados de depuração. Se a integridade é preferida, como neste caso, podemos desativar o buffer circular configurando o argumento para um valor negativo (por exemplo, -1 aqui).

O exemplo debug_mnist_v2 chama enable_dump_debug_info() passando sinalizadores de linha de comando para ele. Para executar nosso programa TF2 problemático novamente com esta instrumentação de depuração ativada, faça:

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

Iniciando a GUI do Debugger V2 no TensorBoard

A execução do programa com a instrumentação do depurador cria um logdir em / tmp / tfdbg2_logdir. Podemos iniciar o TensorBoard e apontá-lo para o logdir com:

 tensorboard --logdir /tmp/tfdbg2_logdir
 

No navegador da web, navegue até a página do TensorBoard em http: // localhost: 6006. O plug-in “Debugger V2” deve ser ativado por padrão, exibindo uma página parecida com a seguinte:

Captura de tela de visualização completa do Debugger V2

Usando a GUI do Debugger V2 para encontrar a causa raiz dos NaNs

A GUI do Debugger V2 no TensorBoard está organizada em seis seções:

  • Alertas : Esta seção superior esquerda contém uma lista de eventos de "alerta" detectados pelo depurador nos dados de depuração do programa instrumentado TensorFlow. Cada alerta indica uma certa anomalia que merece atenção. No nosso caso, esta seção destaca 499 eventos NaN / with com uma cor rosa-vermelha saliente. Isso confirma nossa suspeita de que o modelo falha ao aprender devido à presença de NaNs e / ou infinitos em seus valores de tensores internos. Analisaremos esses alertas em breve.
  • Linha do tempo de execução do Python : Esta é a metade superior da seção superior central. Apresenta o histórico completo da execução ansiosa de operações e gráficos. Cada caixa da linha do tempo é marcada pela letra inicial do nome do op ou do gráfico (por exemplo, "T" para o "TensorSliceDataset" op, "m" para o "modelo" tf.function ). Podemos navegar nessa linha do tempo usando os botões de navegação e a barra de rolagem acima da linha do tempo.
  • Execução de gráfico : localizada no canto superior direito da GUI, esta seção será central para nossa tarefa de depuração. Ele contém um histórico de todos os tensores de tipo flutuante calculados dentro de gráficos (ou seja, compilados pela @tf-function ).
  • Estrutura do gráfico (metade inferior da seção superior central), código-fonte (seção inferior esquerda) e rastreamento de pilha (seção inferior direita) estão inicialmente vazios. Seu conteúdo será preenchido quando interagirmos com a GUI. Essas três seções também desempenharão papéis importantes em nossa tarefa de depuração.

Tendo nos orientado para a organização da interface do usuário, vamos dar os seguintes passos para entender por que os NaNs apareceram. Primeiro, clique no alerta NaN / ∞ na seção Alertas. Isso rola automaticamente a lista de 600 tensores de gráfico na seção Execução de Gráfico e foca no # 88, que é um tensor chamado “Log: 0” gerado por uma Log (logaritmo natural). Uma cor rosa-vermelha destacada destaca um elemento -∞ entre os 1000 elementos do tensor 2D float32. Este é o primeiro tensor no histórico de tempo de execução do programa TF2 que continha NaN ou infinito: tensores calculados antes de não conter NaN ou ∞; muitos (de fato, a maioria) dos tensores computados posteriormente contêm NaNs. Podemos confirmar isso rolando para cima e para baixo na lista Execução do gráfico. Esta observação fornece uma forte dica de que o Log op é a fonte da instabilidade numérica neste programa TF2.

Depurador V2: alertas Nan / Infinity e lista de execução de gráficos

Por que esta operação de Log cospe um -∞? Para responder a essa pergunta, é necessário examinar a entrada da operação. Clicando sobre o nome do tensor ( “Log: 0”) traz um simples, mas visualização informativa da Log proximidades da op em seu gráfico TensorFlow na seção Estrutura Graph. Observe a direção de cima para baixo do fluxo de informações. A operação em si é mostrada em negrito no meio. Imediatamente acima dela, podemos ver que uma operação de Espaço Reservado fornece a única entrada para a operação de Log . Onde está o tensor gerado por este espaço reservado na lista de execução do gráfico? Usando a cor de fundo amarela como auxílio visual, podemos ver que o tensor logits:0 está duas linhas acima do tensor Log:0 , ou seja, na linha 85.

Depurador V2: visualização da estrutura gráfica e rastreamento para o tensor de entrada

Uma análise mais cuidadosa do detalhamento numérico do tensor "logits: 0" na linha 85 revela por que o consumidor Log:0 produz um -∞: Entre os 1000 elementos de "logits: 0", um elemento possui um valor 0. O -∞ é o resultado da computação do logaritmo natural de 0! Se, de alguma forma, garantirmos que a operação do Log seja exposta apenas a entradas positivas, poderemos impedir que o NaN / happening aconteça. Isso pode ser conseguido aplicando o recorte (por exemplo, usando tf.clip_by_value () ) no tensor de logits do espaço reservado.

Estamos nos aproximando da resolução do bug, mas ainda não o fizemos. Para aplicar a correção, precisamos saber onde no código-fonte Python a operação do Log e sua entrada de Placeholder se originaram. O Debugger V2 fornece suporte de primeira classe para rastrear as operações gráficas e os eventos de execução até sua origem. Quando clicamos no tensor Log:0 no Graph Executions, a seção Rastreio de pilha foi preenchida com o rastreio de pilha original da criação da operação de log. O rastreamento da pilha é um tanto grande porque inclui muitos quadros do código interno do TensorFlow (por exemplo, gen_math_ops.py e dumping_callback.py), que podemos ignorar com segurança na maioria das tarefas de depuração. O quadro de interesse é a linha 216 de debug_mnist_v2.py (ou seja, o arquivo Python que estamos tentando depurar). Clicar em "Linha 204" exibe a linha de código correspondente na seção Código-fonte.

Depurador V2: código-fonte e rastreamento de pilha

Isso finalmente nos leva ao código-fonte que criou a operação problemática do Log a partir de sua entrada de logits. Esta é a nossa função de perda de entropia cruzada categórica personalizada decorada com @tf.function e, portanto, convertida em um gráfico TensorFlow. O espaço reservado para “logits” corresponde ao primeiro argumento de entrada para a função de perda. A operação de Log é criada com a chamada da API tf.math.log ().

A correção de recorte de valor desse bug será semelhante a:

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

Ele resolverá a instabilidade numérica neste programa TF2 e fará com que o MLP seja treinado com sucesso. Outra abordagem possível para corrigir a instabilidade numérica é usar tf.keras.losses.CategoricalCrossentropy .

Isso conclui nossa jornada, da observação de um bug do modelo TF2 à criação de uma alteração de código que o corrige, auxiliada pela ferramenta Debugger V2, que fornece total visibilidade do histórico de execução gráfico e ansioso do programa TF2 instrumentado, incluindo os resumos numéricos dos valores dos tensores e associação entre ops, tensores e seu código fonte original.

Compatibilidade de hardware do Debugger V2

O Debugger V2 suporta hardware de treinamento convencional, incluindo CPU e GPU. O treinamento para várias GPUs com tf.distributed.MirroredStrategy também é suportado. O suporte para TPU ainda está em estágio inicial e requer a chamada

 tf.config.set_soft_device_placement(True)
 

antes de chamar enable_dump_debug_info() . Pode haver outras limitações nas TPUs também. Se você tiver problemas ao usar o Debugger V2, relate os erros na nossa página de problemas do GitHub .

Compatibilidade de API do Debugger V2

O Debugger V2 é implementado em um nível relativamente baixo da pilha de software do TensorFlow e, portanto, é compatível com tf.keras , tf.data e outras APIs construídas sobre os níveis mais baixos do TensorFlow. O depurador V2 também é compatível com o TF1, embora o Eager Execution Timeline esteja vazio para as logdirs de depuração geradas pelos programas TF1.

Dicas de uso da API

Uma pergunta freqüente sobre esta API de depuração é onde, no código TensorFlow, deve-se inserir a chamada para enable_dump_debug_info() . Normalmente, a API deve ser chamada o mais cedo possível no seu programa TF2, de preferência após as linhas de importação do Python e antes do início da construção e execução do gráfico. Isso garantirá a cobertura total de todas as operações e gráficos que impulsionam seu modelo e seu treinamento.

Os tensor_debug_modes atualmente suportados são: NO_TENSOR , CURT_HEALTH , CONCISE_HEALTH , FULL_HEALTH e SHAPE . Eles variam na quantidade de informações extraídas de cada tensor e na sobrecarga de desempenho do programa depurado. Por favor, consulte a seção args da enable_dump_debug_info() de enable_dump_debug_info() ].

Sobrecarga de desempenho

A API de depuração introduz uma sobrecarga de desempenho no programa TensorFlow instrumentado. A sobrecarga varia de acordo com o tensor_debug_mode , tipo de hardware e natureza do programa TensorFlow instrumentado. Como ponto de referência, em uma GPU, o modo NO_TENSOR adiciona uma sobrecarga de 15% durante o treinamento de um modelo Transformer com tamanho de lote 64. A porcentagem de sobrecarga para outros modos tensor_debug_ é maior: aproximadamente 50% para CURT_HEALTH , CONCISE_HEALTH , FULL_HEALTH e SHAPE modos. Nas CPUs, a sobrecarga é um pouco menor. Em TPUs, a sobrecarga atualmente é maior.

Relação com outras APIs de depuração do TensorFlow

Observe que o TensorFlow oferece outras ferramentas e APIs para depuração. Você pode procurar essas APIs no espaço para nome tf.debugging.* Na página de documentos da API. Entre essas APIs, a mais usada é tf.print() . Quando alguém deve usar o Depurador V2 e quando tf.print() deve ser usado? tf.print() é conveniente no caso de

  1. sabemos exatamente quais tensores imprimir,
  2. sabemos onde exatamente no código fonte inserir essas tf.print() ,
  3. o número desses tensores não é muito grande.

Para outros casos (por exemplo, examinando muitos valores de tensor, examinando valores de tensor gerados pelo código interno do TensorFlow e pesquisando a origem da instabilidade numérica, como mostramos acima), o Debugger V2 fornece uma maneira mais rápida de depurar. Além disso, o Debugger V2 fornece uma abordagem unificada para inspecionar tensores ansiosos e gráficos. Além disso, fornece informações sobre a estrutura do gráfico e a localização dos códigos, que estão além da capacidade do tf.print() .

Outra API que pode ser usada para depurar problemas que envolvem ∞ e NaN é tf.debugging.enable_check_numerics() . Ao contrário de enable_dump_debug_info() , enable_check_numerics() não salva as informações de depuração no disco. Em vez disso, ele apenas monitora ∞ e NaN durante o tempo de execução do TensorFlow e gera erros no local do código de origem assim que qualquer operação gera valores numéricos incorretos. Ele tem uma sobrecarga de desempenho mais baixa em comparação com o enable_dump_debug_info() , mas não oferece um rastreamento completo do histórico de execução do programa e não vem com uma interface gráfica do usuário como o Debugger V2.