Path: blob/master/site/pt-br/guide/function.ipynb
25115 views
Copyright 2020 The TensorFlow Authors.
Melhor desempenho com tf.function
No TensorFlow 2, a execução eager está ativada por padrão. A interface do usuário é intuitiva e flexível (executar operações únicas é muito mais fácil e rápida), mas isso pode prejudicar o desempenho e a capacidade de implantação.
Você pode usar tf.function
para criar grafos de seus programas. É uma ferramenta de transformação que cria grafos de dataflow independentes do Python a partir do seu código Python. Isto vai ajudar você a criar modelos portáteis e de alto desempenho, além de ser necessário para poder usar o SavedModel
.
Este guia vai ajudar você a conceituar como tf.function
funciona nos bastidores, para que você possa usá-lo de maneira eficaz.
As principais conclusões e recomendações são:
Depure no modo eager e decore com
@tf.function
.Não conte com os efeitos colaterais do Python, como mutação de objetos ou appends de listas.
tf.function
funciona melhor com as ops do TensorFlow. As chamadas NumPy e Python são convertidas em constantes.
Configuração
Defina uma função helper para demonstrar os tipos de erros que você poderá encontrar:
Fundamentos
Uso
Uma Function
que você define (por exemplo, aplicando o decorador @tf.function
) é como uma operação do TensorFlow core: você pode executá-la de forma eager; você pode computar gradientes; e assim por diante.
Você pode usar uma Function
dentro de outra Function
.
Uma Function
pode ser mais rápida que o código eager, especialmente em grafos com muitas pequenas ops. Mas para grafos com algumas poucas operações caras (como convoluções), talvez você não perceba muita diferença.
Tracing
Esta seção expõe como Function
funciona nos bastidores, incluindo detalhes de implementação que podem mudar no futuro. No entanto, depois que você entender por que e quando o tracing ocorre, ficará muito mais fácil usar tf.function
de maneira eficaz!
O que é "tracing"?
Uma Function
executa seu programa num TensorFlow Graph. No entanto, um tf.Graph
não pode ser usado para representar tudo o que você poderia escrever num programa TensorFlow em modo eager. Por exemplo, Python suporta polimorfismo, mas tf.Graph
exige que suas entradas tenham um tipo de dados e dimensão especificados. Ou você talvez queira realizar tarefas paralelas, como ler argumentos de linha de comando, gerar um erro ou trabalhar com um objeto Python mais complexo; nenhuma dessas coisas irá rodar num tf.Graph
.
Function
preenche essa lacuna separando seu código em dois estágios:
No primeiro estágio, chamado de "tracing",
Function
cria um novotf.Graph
. O código Python é executado normalmente, mas todas as operações do TensorFlow (como adicionar dois Tensores) são adiadas: elas são capturadas pelotf.Graph
e não são executadas.No segundo estágio é executado um
tf.Graph
que contém tudo o que foi adiado na primeira etapa. Este estágio é muito mais rápido que o estágio de tracing.
Dependendo de suas entradas, Function
nem sempre executará o primeiro estágio quando for chamada. Consulte "Regras de tracing" abaixo para ter uma ideia melhor de como essa determinação é feita. Pular o primeiro estágio e executar apenas o segundo é o que proporciona o alto desempenho do TensorFlow.
Quando Function
decide fazer tracing, o estágio de tracing é imediatamente seguido pelo segundo estágio, portanto, chamar Function
cria e executa o tf.Graph
. Mais tarde, você verá como executar apenas o estágio de tracing com get_concrete_function
.
Quando você passa argumentos de tipos diferentes para um Function
, ambos os estágios são executados:
Observe que se você chamar repetidamente uma Function
com o mesmo tipo de argumento, o TensorFlow ignorará o estágio de tracing e reutilizará um grafo que tenha passado pelo tracing anteriormente, pois o grafo gerado seria idêntico.
Você pode usar pretty_printed_concrete_signatures()
para ver todos os traces disponíveis:
Até aqui, você viu que tf.function
cria uma camada de despacho dinâmico e em cache sobre a lógica de tracing de grafos do TensorFlow. Para sermos mais específicos sobre a terminologia:
Um
tf.Graph
é a representação bruta, independente de linguagem e portátil de uma computação do TensorFlow.Uma
ConcreteFunction
é um wrapper que envolve umtf.Graph
.Uma
Function
gerencia um cache deConcreteFunction
e escolhe a função correta para suas entradas.tf.function
é um wrapper que envolve uma função Python, retornando um objetoFunction
.Tracing cria um
tf.Graph
e o empacota numConcreteFunction
, também conhecido como trace.
Regras do tracing
Quando chamada, uma Function
combina os argumentos de chamada com as ConcreteFunction
existentes usando tf.types.experimental.TraceType
de cada argumento. Se uma ConcreteFunction
correspondente for encontrada, a chamada será enviada para ela. Se nenhuma correspondência for encontrada, será feito o tracing de uma nova ConcreteFunction
.
Se forem encontradas múltiplas correspondências, a assinatura mais específica será escolhida. A correspondência é feita por meio de subtipos, de forma similar a chamadas de função comuns em C++ ou Java. Por exemplo, TensorShape([1, 2])
é um subtipo de TensorShape([None, None])
e, portanto, uma chamada para tf.function com TensorShape([1, 2])
pode ser despachada para ConcreteFunction
produzida com TensorShape([None, None])
mas se um ConcreteFunction
com TensorShape([1, None])
também existir, ele será priorizado por ser mais específico.
O TraceType
é determinado a partir de argumentos de entrada da seguinte forma:
Para
Tensor
, o tipo é parametrizado pelodtype
eshape
doTensor
; os formatos classificados são um subtipo de formatos não classificados; dimensões fixas são um subtipo de dimensões desconhecidasPara
Variable
, o tipo é semelhante aTensor
, mas também inclui um ID de recurso exclusivo da variável, necessário para interligar corretamente as dependências de controlePara valores primitivos do Python, o tipo corresponde ao próprio valor. Por exemplo, o
TraceType
do valor3
éLiteralTraceType<3>
, nãoint
.Para containers ordenados em Python, como
list
etuple
, etc., o tipo é parametrizado pelos tipos de seus elementos; por exemplo, o tipo de[1, 2]
éListTraceType<LiteralTraceType<1>, LiteralTraceType<2>>
e o tipo de[2, 1]
éListTraceType<LiteralTraceType<2>, LiteralTraceType<1>>
que é diferente.Para mapeamentos Python como
dict
, o tipo também é um mapeamento das mesmas chaves, mas para os tipos de valores, em vez dos valores reais. Por exemplo, o tipo de{1: 2, 3: 4}
, éMappingTraceType<<KeyValue<1, LiteralTraceType<2>>>, <KeyValue<3, LiteralTraceType<4>>>>
. No entanto, diferentemente dos containers ordenados,{1: 2, 3: 4}
e{3: 4, 1: 2}
possuem tipos equivalentes.Para objetos Python que implementam o método
__tf_tracing_type__
, o tipo é o que o método retornaPara quaisquer outros objetos Python, o tipo é um
TraceType
genérico, seu procedimento correspondente é:Primeiro ele verifica se o objeto é o mesmo usado no tracing anterior (usando
id()
ouis
, do Python). Observe que isto ainda vai corresponder se o objeto tiver sido alterado; portanto, se você usar objetos Python como argumentostf.function
, é melhor que sejam objetos imutáveis.Em seguida, verifica se o objeto é igual ao objeto usado no tracing anterior (usando
==
do Python).
Observe que este procedimento apenas mantém uma referência fraca para o objeto e, portanto, só funciona enquanto o objeto estiver no escopo/não for excluído.)
Observação: TraceType
é baseado nos parâmetros de entrada de Function
, portanto, alterações apenas nas variáveis globais e livres não criarão um novo trace. Consulte esta seção para conhecer as práticas recomendadas ao lidar com variáveis globais e livres do Python.
Controle do retracing
O retracing, que ocorre quando sua Function
cria mais de um trace, ajuda a garantir que o TensorFlow gere grafos corretos para cada conjunto de entradas. No entanto, o tracing é uma operação cara! Se sua Function
fizer retracing de um novo gráfico para cada chamada, você vai perceber que seu código roda mais lentamente do que se você não usasse tf.function
.
Para controlar o comportamento do tracing, você pode usar as seguintes técnicas:
Passe uma input_signature
fixa para tf.function
Use dimensões desconhecidas para maior flexibilidade
Como o TensorFlow compara tensores com base no seu formato, usar uma dimensão None
como curinga permitirá que as Function
reutilizem traces para entradas de tamanhos variáveis. Entradas de tamanhos variáveis podem ocorrer se você tiver sequências de comprimentos diferentes ou imagens de tamanhos diferentes para cada lote (veja os tutoriais Transformer e Deep Dream, por exemplo).
Passe tensores em vez de literais python
Freqüentemente, argumentos do Python são usados para controlar hiperparâmetros e construções de grafos, por exemplo, num_layers=10
ou training=True
ou nonlinearity='relu'
. Portanto, se o argumento Python mudar, faz sentido que você tenha que fazer o retracing do grafo.
No entanto, é possível que um argumento Python não esteja sendo usado para controlar a construção do grafo. Nesses casos, uma alteração no valor Python pode desencadear um retracing desnecessário. Veja, por exemplo, este loop de treinamento, que o AutoGraph irá desenrolar dinamicamente. Apesar dos múltiplos traces, o gráfico gerado é na verdade idêntico, portanto, fazer retracing aqui é desnecessário.
Se você precisar forçar o retracing, crie uma nova Function
. É garantido que objetos Function
separados não compartilhem traces.
Use o protocolo de tracing
Sempre que possível, você deve dar preferência a converter o tipo Python em tf.experimental.ExtensionType
. Além disso, o TraceType
de um ExtensionType
é o tf.TypeSpec
associado a ele. Portanto, se necessário, você pode simplesmente sobrepor o tf.TypeSpec
padrão para assumir o controle de um Tracing Protocol
do ExtensionType
. Consulte a seção Personalizando o TypeSpec do ExtensionType no guia Extension types para mais detalhes.
Caso contrário, para ter controle direto sobre quando Function
deve fazer retracing quanto a um tipo específico do Python, você mesmo pode implementar o Tracing Protocol
para ela.
Obtendo funções concretas
Cada vez que o tracing de uma função é realizado, uma nova função concreta é criada. Você pode obter diretamente uma função concreta usando get_concrete_function
.
Imprimir uma ConcreteFunction
mostra um resumo de seus argumentos de entrada (com tipos) e seu tipo de saída.
Você também pode recuperar diretamente a assinatura de uma função concreta.
Usar um trace concreto com tipos incompatíveis gerará um erro
Você talvez perceba que os argumentos Python recebem tratamento especial na assinatura de entrada de uma função concreta. Antes do TensorFlow 2.3, os argumentos Python eram simplesmente removidos da assinatura da função concreta. A partir do TensorFlow 2.3, os argumentos Python permanecem na assinatura, mas são limitados a aceitar o valor definido durante o tracing.
Obtendo grafos
Cada função concreta é um wrapper em torno de um tf.Graph
que pode ser chamado. Embora recuperar o objeto tf.Graph
real não seja algo que você normalmente precisará fazer, você pode obtê-lo facilmente a partir de qualquer função concreta.
Depuração
Em geral, depurar código é mais fácil no modo eager do que dentro de tf.function
. Você deve garantir que seu código seja executado sem erros no modo eager antes de decorar com tf.function
. Para auxiliar no processo de depuração, você pode chamar tf.config.run_functions_eagerly(True)
para desativar e reativar globalmente tf.function
.
Ao rastrear problemas que aparecem apenas em tf.function
, aqui estão algumas dicas:
Chamadas
print
do Python são executadas apenas durante o tracing, ajudando você a rastrear quando ocorrer o (re)tracing da sua função.As chamadas
tf.print
serão executadas sempre e podem ajudá-lo a rastrear valores intermediários durante a execução.tf.debugging.enable_check_numerics
é uma maneira fácil de rastrear onde NaNs e Inf são criados.pdb
(o depurador Python ) pode ajudá-lo a entender o que está acontecendo durante o tracing. (Ressalva: opdb
vai mandar você para o código-fonte transformado pelo AutoGraph.)
Transformações AutoGraph
AutoGraph é uma biblioteca ativada por padrão em tf.function
e transforma um subconjunto de código eager do Python em ops do TensorFlow compatíveis com grafos. Isto inclui operações de controle de fluxo como if
, for
, while
.
Ops do TensorFlow como tf.cond
e tf.while_loop
continuam funcionando, mas o controle de fluxo geralmente é mais fácil de escrever e entender quando escrito em Python.
Se você tiver curiosidade, pode inspecionar o código gerado pelo AutoGraph.
Condicionais
O AutoGraph converterá algumas instruções if <condition>
em chamadas tf.cond
equivalentes. Esta substituição é feita se <condition>
for um Tensor. Caso contrário, a instrução if
será executada como uma condicional Python.
Uma expressão condicional Python é executada durante o tracing, portanto, exatamente um ramo da expressão condicional será adicionada ao grafo. Sem o AutoGraph, este grafo rastreado não seria capaz de seguir o ramo alternativo se houvesse controle de fluxo dependente de dados.
tf.cond
faz o tracing e adiciona ambos os ramos da expressão condicional ao grafo, selecionando dinamicamente um ramo em tempo de execução. O tracing pode ter efeitos colaterais indesejados; veja Efeitos de tracing do AutoGraph para mais informações.
Consulte a documentação de referência para saber de restrições adicionais sobre instruções if convertidas em AutoGraph.
Loops
O AutoGraph converterá algumas instruções for
e while
em operações de loop equivalentes do TensorFlow, como tf.while_loop
. Se não for convertido, o loop for
ou while
será executado como um loop Python.
Esta substituição é feita nas seguintes situações:
for x in y
: sey
for um Tensor, converta paratf.while_loop
. No caso especial em quey
é umtf.data.Dataset
, uma combinação de operações detf.data.Dataset
é gerada.while <condition>
: se<condition>
for um Tensor, converta paratf.while_loop
.
Um loop Python é executado durante o tracing, adicionando ops adicionais ao tf.Graph
para cada iteração do loop.
Um loop do TensorFlow faz tracing do corpo do loop e seleciona dinamicamente quantas iterações serão executadas em tempo de execução. O corpo do loop aparece apenas uma vez no tf.Graph
gerado.
Veja a documentação de referência para conhecer restrições adicionais nas instruções for
e while
convertidas pelo AutoGraph.
Loop sobre dados Python
Uma armadilha comum é fazer um loop nos dados Python/NumPy dentro de um tf.function
. Este loop será executado durante o processo de tracing, adicionando uma cópia do seu modelo ao tf.Graph
para cada iteração do loop.
Se você quiser encapsular todo o loop de treinamento em tf.function
, a maneira mais segura de fazer isso é encapsular seus dados usando um tf.data.Dataset
como wrapper para que o AutoGraph desenrole dinamicamente o loop de treinamento.
Ao encapsular dados Python/NumPy num dataset, lembre-se de tf.data.Dataset.from_generator
versus tf.data.Dataset.from_tensor_slices
. O primeiro manterá os dados em Python e os buscará via tf.py_function
, o que pode ter implicações no desempenho, enquanto o último agrupará uma cópia dos dados como uma grande tf.constant()
no grafo, o que pode ter implicações no uso de memória.
Ler dados de arquivos via TFRecordDataset
, CsvDataset
, etc. é a maneira mais eficaz de consumir dados, pois o próprio TensorFlow pode gerenciar o carregamento assíncrono e a pré-busca de dados, sem a necessidade de envolver o Python. Para saber mais, consulte o guia tf.data
: Criando pipelines de entrada no TensorFlow.
Acumulando valores em um loop
Um padrão comum é acumular valores intermediários de um loop. Normalmente, isso é feito anexando-se a uma lista Python ou adicionando entradas a um dicionário Python. No entanto, como esses são efeitos colaterais do Python, eles não funcionarão como esperado num loop desenrolado dinamicamente. Use tf.TensorArray
para acumular resultados de um loop desenrolado dinamicamente.
Limitações
TensorFlow Function
tem algumas limitações de design que você deve conhecer ao converter uma função Python em Function
.
Executando efeitos colaterais do Python
Os efeitos colaterais, como impressão, anexação a listas e mutação de variáveis globais, podem se comportar inesperadamente dentro de uma Function
, às vezes executando duas vezes ou nunca. Isto só acontece na primeira vez que você chama uma Function
com um conjunto de entradas. Posteriormente, o tf.Graph
traçado é reexecutado, e não executará o código Python.
A regra geral é evitar depender dos efeitos colaterais do Python em sua lógica e usá-los apenas para depurar seus traces. Caso contrário, APIs do TensorFlow como tf.data
, tf.print
, tf.summary
, tf.Variable.assign
e tf.TensorArray
são a melhor maneira de garantir que seu código será executado pelo runtime do TensorFlow a cada chamada.
Se você quiser executar o código Python durante cada chamada de uma Function
, tf.py_function
é uma saída. A desvantagem de tf.py_function
é que ela não é portátil nem tem bom desempenho, não pode ser salva com SavedModel e não funciona bem em configurações distribuídas (multi-GPU, TPU). Além disso, como tf.py_function
precisa ser interligado ao grafo, ela converte todas as entradas/saídas em tensores.
Alterando variáveis globais e livres do Python
Alterar as variáveis globais e livres do Python conta como um efeito colateral do Python, então só acontece durante o tracing.
Às vezes, comportamentos inesperados são muito difíceis de perceber. No exemplo abaixo, o counter
tem como objetivo salvaguardar o incremento de uma variável. No entanto, como é um inteiro Python e não um objeto TensorFlow, seu valor é capturado durante o primeiro trace. Quando tf.function
for usado, assign_add
será registrado incondicionalmente no grafo subjacente. Portanto v
aumentará em 1, todas as vezes que a tf.function
for chamada. Esse problema é comum entre usuários que tentam migrar seu código Tensorflow no modo grafo para o Tensorflow 2 usando decoradores tf.function
, quando os efeitos colaterais do Python (o counter
no exemplo) são usados para determinar quais operações executar (assign_add
no exemplo). Normalmente, os usuários percebem isso somente depois de verem resultados numéricos suspeitos ou um desempenho significativamente inferior ao esperado (por exemplo, se a operação protegida for muito ineficiente).
Uma solução alternativa para alcançar o comportamento esperado é usar tf.init_scope
para transferir as operações para fora do grafo de função. Isto garante que o incremento da variável seja feito apenas uma vez durante o tempo de tracing. Deve-se observar que init_scope
tem outros efeitos colaterais, incluindo fluxo de controle limpo e fita gradiente. Às vezes, o uso de init_scope
pode se tornar muito complexo para ser gerenciado de forma realista.
Em suma, como regra geral, você deve evitar a mutação de objetos python, como números inteiros ou containers, como listas que residem fora de Function
. Em vez disso, use argumentos e objetos do TF. Por exemplo, a seção "Acumulando valores em um loop" traz um exemplo de como operações do tipo lista podem ser implementadas.
Você pode, em alguns casos, capturar e manipular o estado se for uma tf.Variable
. É assim que os pesos dos modelos Keras são atualizados com chamadas repetidas para a mesma ConcreteFunction
.
Usando iteradores e geradores Python
Muitos recursos do Python, como geradores e iteradores, dependem do runtime do Python para controlar o estado. Em geral, embora esses construtos funcionem conforme o esperado no modo eager, elas são exemplos de efeitos colaterais do Python e, portanto, só acontecem durante o tracing.
Assim como o TensorFlow possui um tf.TensorArray
especializado para construtos de lista, ele possui um tf.data.Iterator
especializado para construtos de iteração. Veja a seção sobre transformações do AutoGraph para uma visão geral. Além disso, a API tf.data
pode ajudar a implementar padrões de gerador:
Todas as saídas de uma tf.function devem ser valores de retorno
Com exceção de tf.Variable
, uma tf.function deve retornar todas as suas saídas. Tentar acessar diretamente qualquer tensor de uma função sem passar pelos valores de retorno causa "vazamentos".
Por exemplo, a função abaixo "vaza" o tensor a
através da variável global x
declarada em Python:
Isso é verdade mesmo que o valor vazado também seja retornado:
Geralmente, vazamentos como esses ocorrem quando você usa instruções ou estruturas de dados do Python. Além de vazar tensores inacessíveis, tais instruções provavelmente também estão erradas porque contam como efeitos colaterais do Python e não têm garantia de execução a cada chamada de função.
Maneiras comuns de vazar tensores locais também incluem a mutação de uma coleção Python externa ou de um objeto:
Funções tf.function recursivas não são suportadas
Function
recursivas não são suportadas e podem causar loops infinitos. Por exemplo,
Mesmo que uma Function
recursiva pareça funcionar, ocorrerá tracing da função Python várias vezes e isto poderá ter implicações no desempenho. Por exemplo,
Problemas conhecidos
Se a sua Function
não estiver sendo avaliada corretamente, o erro pode ser explicado por esses problemas conhecidos que devem ser corrigidos no futuro.
Depender de variáveis globais e livres do Python
Function
cria uma nova ConcreteFunction
quando chamada com um novo valor de um argumento Python. No entanto, ele não faz isso para o closure do Python, nem valores globais ou não locais dessa Function
. Se o valor deles mudar entre as chamadas para Function
, a Function
ainda usará os valores que tinham quando ela passou pelo tracing. Isso difere da forma como funcionam as funções comuns do Python.
Por esse motivo, você deve seguir um estilo de programação funcional que use argumentos em vez de closures sobre nomes externos.
Outra maneira de atualizar um valor global é torná-lo uma tf.Variable
e usar o método Variable.assign
.
Depender de objetos Python
Você pode passar objetos Python personalizados como argumentos para tf.function
mas isso traz algumas limitações.
Para cobertura máxima de recursos, considere transformar os objetos em tipos de extensão antes de passá-los para tf.function
. Você também pode usar primitivos Python e estruturas compatíveis com tf.nest
.
No entanto, conforme abordado nas regras de tracing, quando um TraceType
personalizado não é fornecido pela classe Python personalizada, a tf.function
é obrigada a usar igualdade baseada em instância, o que significa que não criará um novo trace quando você passar o mesmo objeto com atributos modificados.
Usar a mesma Function
para avaliar a instância modificada do modelo será problemático, pois ele ainda possui o mesmo TraceType baseado em instância do modelo original.
Por esse motivo, é recomendável escrever sua Function
para evitar depender de atributos de objetos mutáveis ou implementar o Protocolo de Tracing para os objetos para informar Function
sobre tais atributos.
Se isso não for possível, uma solução alternativa é criar novos objetos Function
cada vez que você modificar seu objeto para forçar o retracing:
Como o retracing pode ter um custo elevado, você pode usar objetos tf.Variable
como atributos de objeto, que podem ser alterados (mas não trocados, cuidado!) para um efeito semelhante sem a necessidade de um retrace.
Criação de tf.Variables
Function
suporta apenas objetos tf.Variable
singleton, criados uma vez na primeira chamada e reutilizados nas chamadas de função subsequentes. O trecho de código abaixo criaria um novo tf.Variable
em cada chamada de função, o que resulta numa exceção ValueError
.
Exemplo:
Um padrão comum usado para contornar essa limitação é começar com um valor Python None e, em seguida, criar condicionalmente o tf.Variable
se o valor for None:
Uso com múltiplos otimizadores Keras
Você talvez encontre o erro ValueError: tf.function only supports singleton tf.Variables created on the first call.
ao usar mais de um otimizador Keras com um tf.function
. Esse erro ocorre porque os otimizadores criam tf.Variables
internamente quando aplicam gradientes pela primeira vez.
Se você precisar trocar o otimizador durante o treinamento, uma solução alternativa é criar uma nova Function
para cada otimizador, chamando ConcreteFunction
diretamente.
Uso com múltiplos modelos Keras
Você talvez também encontre ValueError: tf.function only supports singleton tf.Variables created on the first call.
ao passar diferentes instâncias de modelo para a mesma Function
.
Este erro ocorre porque os modelos Keras (que não têm seu formato de entrada definido) e as camadas Keras criam objetos tf.Variables
quando são chamados pela primeira vez. Você pode estar tentando inicializar essas variáveis dentro de uma Function
, que já foi chamada. Para evitar esse erro, tente chamar model.build(input_shape)
para inicializar todos os pesos antes de treinar o modelo.
Leituras adicionais
Para saber como exportar e carregar uma Function
, consulte o guia do SavedModel. Para saber mais sobre otimizações de grafos executadas após o tracing, consulte o guia do Grappler. Para saber como otimizar seu pipeline de dados e fazer um profiling do seu modelo, consulte o Guia do profiler.