RSVP para seu evento TensorFlow Everywhere hoje mesmo!
Esta página foi traduzida pela API Cloud Translation.
Switch to English

Introdução aos gráficos e tf.function

Ver no TensorFlow.org Executar no Google Colab Ver fonte no GitHub Baixar caderno

Visão geral

Este guia vai além do TensorFlow e Keras para ver como o TensorFlow funciona. Se, em vez disso, você deseja começar a usar o Keras imediatamente, consulte nossa coleção de guias do Keras .

Neste guia, você verá como o TensorFlow permite fazer alterações simples em seu código para obter gráficos, como os gráficos são armazenados e representados e como você pode usá-los para acelerar seus modelos.

Esta é uma visão geral que cobre como tf.function permite que você mude da execução rápida para a execução do gráfico. Para obter uma especificação mais completa de tf.function , consulte o guia tf.function .

O que são gráficos?

Nos três guias anteriores, você viu TensorFlow correndo ansiosamente. Isso significa que as operações do TensorFlow são executadas pelo Python, operação por operação, e retornam os resultados de volta ao Python.

Embora a execução rápida tenha várias vantagens exclusivas, a execução de gráfico permite portabilidade fora do Python e tende a oferecer melhor desempenho. A execução do gráfico significa que os cálculos do tensor são executados como um gráfico do TensorFlow , às vezes chamado de tf.Graph ou simplesmente um "gráfico".

Os gráficos são estruturas de dados que contêm um conjunto de objetos tf.Operation , que representam unidades de computação; e objetos tf.Tensor , que representam as unidades de dados que fluem entre as operações. Eles são definidos em um contexto tf.Graph . Como esses gráficos são estruturas de dados, eles podem ser salvos, executados e restaurados sem o código Python original.

Esta é a aparência de um gráfico do TensorFlow que representa uma rede neural de duas camadas quando visualizado no TensorBoard.

Um gráfico simples do TensorFlow

Os benefícios dos gráficos

Com um gráfico, você tem bastante flexibilidade. Você pode usar seu gráfico TensorFlow em ambientes que não têm um interpretador Python, como aplicativos móveis, dispositivos incorporados e servidores de back-end. O TensorFlow usa gráficos como formato para modelos salvos ao exportá-los do Python.

Os gráficos também são facilmente otimizados, permitindo que o compilador faça transformações como:

  • Inferir estaticamente o valor dos tensores dobrando nós constantes em seu cálculo ("dobramento constante") .
  • Separe as subpartes de um cálculo que são independentes e divida-as entre threads ou dispositivos.
  • Simplifique as operações aritméticas eliminando subexpressões comuns.

Existe todo um sistema de otimização, Grappler , para realizar este e outros aceleramentos.

Resumindo, os gráficos são extremamente úteis e permitem que o TensorFlow seja executado rapidamente , em paralelo e com eficiência em vários dispositivos .

No entanto, você ainda deseja definir nossos modelos de aprendizado de máquina (ou outros cálculos) em Python por conveniência e, em seguida, construir gráficos automaticamente quando precisar deles.

Aproveitando os gráficos

Você cria e executa um gráfico no TensorFlow usando tf.function , seja como uma chamada direta ou como um decorador. tf.function assume uma função regular como entrada e retorna uma Function . Uma Function é um Python chamável que cria gráficos do TensorFlow a partir da função Python. Você usa uma Function da mesma maneira que seu equivalente em Python.

import tensorflow as tf
import timeit
from datetime import datetime
# Define a Python function.
def a_regular_function(x, y, b):
  x = tf.matmul(x, y)
  x = x + b
  return x

# `a_function_that_uses_a_graph` is a TensorFlow `Function`.
a_function_that_uses_a_graph = tf.function(a_regular_function)

# Make some tensors.
x1 = tf.constant([[1.0, 2.0]])
y1 = tf.constant([[2.0], [3.0]])
b1 = tf.constant(4.0)

orig_value = a_regular_function(x1, y1, b1).numpy()
# Call a `Function` like a Python function.
tf_function_value = a_function_that_uses_a_graph(x1, y1, b1).numpy()
assert(orig_value == tf_function_value)

Por fora, uma Function parece uma função normal que você escreve usando as operações do TensorFlow. Por baixo , porém, é muito diferente . Uma Function encapsula vários tf.Graph s por trás de uma API . É assim que Function é capaz de oferecer os benefícios da execução de gráficos , como velocidade e capacidade de implantação.

tf.function se aplica a uma função e a todas as outras funções que ela chama :

def inner_function(x, y, b):
  x = tf.matmul(x, y)
  x = x + b
  return x

# Use the decorator to make `outer_function` a `Function`.
@tf.function
def outer_function(x):
  y = tf.constant([[2.0], [3.0]])
  b = tf.constant(4.0)

  return inner_function(x, y, b)

# Note that the callable will create a graph that
# includes `inner_function` as well as `outer_function`.
outer_function(tf.constant([[1.0, 2.0]])).numpy()
array([[12.]], dtype=float32)

Se você usou o TensorFlow 1.x, notará que em nenhum momento foi necessário definir um Placeholder ou tf.Session .

Converter funções Python em gráficos

Qualquer função que você escrever com o TensorFlow conterá uma mistura de operações nativas do TF e lógica Python, como cláusulas if-then , loops, break , return , continue e muito mais. Embora as operações do TensorFlow sejam facilmente capturadas por um tf.Graph , a lógica específica do Python precisa passar por uma etapa extra para se tornar parte do gráfico. tf.function usa uma biblioteca chamada AutoGraph ( tf.autograph ) para converter o código Python em código gerador de gráfico.

def simple_relu(x):
  if tf.greater(x, 0):
    return x
  else:
    return 0

# `tf_simple_relu` is a TensorFlow `Function` that wraps `simple_relu`.
tf_simple_relu = tf.function(simple_relu)

print("First branch, with graph:", tf_simple_relu(tf.constant(1)).numpy())
print("Second branch, with graph:", tf_simple_relu(tf.constant(-1)).numpy())
First branch, with graph: 1
Second branch, with graph: 0

Embora seja improvável que você precise visualizar os gráficos diretamente, você pode inspecionar as saídas para ver os resultados exatos. Não são fáceis de ler, portanto, não é preciso olhar com muito cuidado!

# This is the graph-generating output of AutoGraph.
print(tf.autograph.to_code(simple_relu))
def tf__simple_relu(x):
    with ag__.FunctionScope('simple_relu', 'fscope', ag__.ConversionOptions(recursive=True, user_requested=True, optional_features=(), internal_convert_user_code=True)) as fscope:
        do_return = False
        retval_ = ag__.UndefinedReturnValue()

        def get_state():
            return (retval_, do_return)

        def set_state(vars_):
            nonlocal retval_, do_return
            (retval_, do_return) = vars_

        def if_body():
            nonlocal retval_, do_return
            try:
                do_return = True
                retval_ = ag__.ld(x)
            except:
                do_return = False
                raise

        def else_body():
            nonlocal retval_, do_return
            try:
                do_return = True
                retval_ = 0
            except:
                do_return = False
                raise
        ag__.if_stmt(ag__.converted_call(ag__.ld(tf).greater, (ag__.ld(x), 0), None, fscope), if_body, else_body, get_state, set_state, ('retval_', 'do_return'), 2)
        return fscope.ret(retval_, do_return)


# This is the graph itself.
print(tf_simple_relu.get_concrete_function(tf.constant(1)).graph.as_graph_def())
node {
  name: "x"
  op: "Placeholder"
  attr {
    key: "_user_specified_name"
    value {
      s: "x"
    }
  }
  attr {
    key: "dtype"
    value {
      type: DT_INT32
    }
  }
  attr {
    key: "shape"
    value {
      shape {
      }
    }
  }
}
node {
  name: "Greater/y"
  op: "Const"
  attr {
    key: "dtype"
    value {
      type: DT_INT32
    }
  }
  attr {
    key: "value"
    value {
      tensor {
        dtype: DT_INT32
        tensor_shape {
        }
        int_val: 0
      }
    }
  }
}
node {
  name: "Greater"
  op: "Greater"
  input: "x"
  input: "Greater/y"
  attr {
    key: "T"
    value {
      type: DT_INT32
    }
  }
}
node {
  name: "cond"
  op: "StatelessIf"
  input: "Greater"
  input: "x"
  attr {
    key: "Tcond"
    value {
      type: DT_BOOL
    }
  }
  attr {
    key: "Tin"
    value {
      list {
        type: DT_INT32
      }
    }
  }
  attr {
    key: "Tout"
    value {
      list {
        type: DT_INT32
        type: DT_BOOL
      }
    }
  }
  attr {
    key: "_lower_using_switch_merge"
    value {
      b: true
    }
  }
  attr {
    key: "_read_only_resource_inputs"
    value {
      list {
      }
    }
  }
  attr {
    key: "else_branch"
    value {
      func {
        name: "cond_false_34"
      }
    }
  }
  attr {
    key: "output_shapes"
    value {
      list {
        shape {
        }
        shape {
        }
      }
    }
  }
  attr {
    key: "then_branch"
    value {
      func {
        name: "cond_true_33"
      }
    }
  }
}
node {
  name: "cond/Identity"
  op: "Identity"
  input: "cond"
  attr {
    key: "T"
    value {
      type: DT_INT32
    }
  }
}
node {
  name: "cond/Identity_1"
  op: "Identity"
  input: "cond:1"
  attr {
    key: "T"
    value {
      type: DT_BOOL
    }
  }
}
node {
  name: "Identity"
  op: "Identity"
  input: "cond/Identity"
  attr {
    key: "T"
    value {
      type: DT_INT32
    }
  }
}
library {
  function {
    signature {
      name: "cond_false_34"
      input_arg {
        name: "cond_placeholder"
        type: DT_INT32
      }
      output_arg {
        name: "cond_identity"
        type: DT_INT32
      }
      output_arg {
        name: "cond_identity_1"
        type: DT_BOOL
      }
    }
    node_def {
      name: "cond/Const"
      op: "Const"
      attr {
        key: "dtype"
        value {
          type: DT_INT32
        }
      }
      attr {
        key: "value"
        value {
          tensor {
            dtype: DT_INT32
            tensor_shape {
            }
            int_val: 0
          }
        }
      }
      experimental_debug_info {
        original_node_names: "cond/Const"
      }
    }
    node_def {
      name: "cond/Const_1"
      op: "Const"
      attr {
        key: "dtype"
        value {
          type: DT_BOOL
        }
      }
      attr {
        key: "value"
        value {
          tensor {
            dtype: DT_BOOL
            tensor_shape {
            }
            bool_val: true
          }
        }
      }
      experimental_debug_info {
        original_node_names: "cond/Const_1"
      }
    }
    node_def {
      name: "cond/Const_2"
      op: "Const"
      attr {
        key: "dtype"
        value {
          type: DT_BOOL
        }
      }
      attr {
        key: "value"
        value {
          tensor {
            dtype: DT_BOOL
            tensor_shape {
            }
            bool_val: true
          }
        }
      }
      experimental_debug_info {
        original_node_names: "cond/Const_2"
      }
    }
    node_def {
      name: "cond/Const_3"
      op: "Const"
      attr {
        key: "dtype"
        value {
          type: DT_INT32
        }
      }
      attr {
        key: "value"
        value {
          tensor {
            dtype: DT_INT32
            tensor_shape {
            }
            int_val: 0
          }
        }
      }
      experimental_debug_info {
        original_node_names: "cond/Const_3"
      }
    }
    node_def {
      name: "cond/Identity"
      op: "Identity"
      input: "cond/Const_3:output:0"
      attr {
        key: "T"
        value {
          type: DT_INT32
        }
      }
      experimental_debug_info {
        original_node_names: "cond/Identity"
      }
    }
    node_def {
      name: "cond/Const_4"
      op: "Const"
      attr {
        key: "dtype"
        value {
          type: DT_BOOL
        }
      }
      attr {
        key: "value"
        value {
          tensor {
            dtype: DT_BOOL
            tensor_shape {
            }
            bool_val: true
          }
        }
      }
      experimental_debug_info {
        original_node_names: "cond/Const_4"
      }
    }
    node_def {
      name: "cond/Identity_1"
      op: "Identity"
      input: "cond/Const_4:output:0"
      attr {
        key: "T"
        value {
          type: DT_BOOL
        }
      }
      experimental_debug_info {
        original_node_names: "cond/Identity_1"
      }
    }
    ret {
      key: "cond_identity"
      value: "cond/Identity:output:0"
    }
    ret {
      key: "cond_identity_1"
      value: "cond/Identity_1:output:0"
    }
    arg_attr {
      key: 0
      value {
        attr {
          key: "_output_shapes"
          value {
            list {
              shape {
              }
            }
          }
        }
      }
    }
  }
  function {
    signature {
      name: "cond_true_33"
      input_arg {
        name: "cond_identity_x"
        type: DT_INT32
      }
      output_arg {
        name: "cond_identity"
        type: DT_INT32
      }
      output_arg {
        name: "cond_identity_1"
        type: DT_BOOL
      }
    }
    node_def {
      name: "cond/Identity"
      op: "Identity"
      input: "cond_identity_x"
      attr {
        key: "T"
        value {
          type: DT_INT32
        }
      }
      experimental_debug_info {
        original_node_names: "cond/Identity"
      }
    }
    node_def {
      name: "cond/Const"
      op: "Const"
      attr {
        key: "dtype"
        value {
          type: DT_BOOL
        }
      }
      attr {
        key: "value"
        value {
          tensor {
            dtype: DT_BOOL
            tensor_shape {
            }
            bool_val: true
          }
        }
      }
      experimental_debug_info {
        original_node_names: "cond/Const"
      }
    }
    node_def {
      name: "cond/Identity_1"
      op: "Identity"
      input: "cond/Const:output:0"
      attr {
        key: "T"
        value {
          type: DT_BOOL
        }
      }
      experimental_debug_info {
        original_node_names: "cond/Identity_1"
      }
    }
    ret {
      key: "cond_identity"
      value: "cond/Identity:output:0"
    }
    ret {
      key: "cond_identity_1"
      value: "cond/Identity_1:output:0"
    }
    arg_attr {
      key: 0
      value {
        attr {
          key: "_output_shapes"
          value {
            list {
              shape {
              }
            }
          }
        }
      }
    }
  }
}
versions {
  producer: 561
  min_consumer: 12
}


Na maioria das vezes, tf.function funcionará sem considerações especiais. No entanto, existem algumas ressalvas, e o guia tf.function pode ajudar aqui, bem como a referência completa do AutoGraph

Polimorfismo: uma Function , muitos gráficos

Um tf.Graph é especializado em um tipo específico de entradas (por exemplo, tensores com umdtype específico ou objetos com o mesmo id() ).

Cada vez que você invoca uma Function com novos dtypes e formas em seus argumentos, Function cria um novo tf.Graph para os novos argumentos. Os dtypes e formas das entradas de um tf.Graph são conhecidos como uma assinatura de entrada ou apenas uma assinatura .

A Function armazena o tf.Graph correspondente a essa assinatura em um ConcreteFunction . Um ConcreteFunction é um wrapper em torno de um tf.Graph .

@tf.function
def my_relu(x):
  return tf.maximum(0., x)

# `my_relu` creates new graphs as it sees more signatures.
print(my_relu(tf.constant(5.5)))
print(my_relu([1, -1]))
print(my_relu(tf.constant([3., -3.])))
tf.Tensor(5.5, shape=(), dtype=float32)
tf.Tensor([1. 0.], shape=(2,), dtype=float32)
tf.Tensor([3. 0.], shape=(2,), dtype=float32)

Se a Function já foi chamada com essa assinatura, Function não cria um novo tf.Graph .

# These two calls do *not* create new graphs.
print(my_relu(tf.constant(-2.5))) # Signature matches `tf.constant(5.5)`.
print(my_relu(tf.constant([-1., 1.]))) # Signature matches `tf.constant([3., -3.])`.
tf.Tensor(0.0, shape=(), dtype=float32)
tf.Tensor([0. 1.], shape=(2,), dtype=float32)

Por ser apoiado por vários gráficos, podemos dizer que uma Function é polimórfica . Isso permite que ele suporte mais tipos de entrada do que um único tf.Graph poderia representar, bem como otimizar cada tf.Graph para um melhor desempenho.

# There are three `ConcreteFunction`s (one for each graph) in `my_relu`.
# The `ConcreteFunction` also knows the return type and shape!
print(my_relu.pretty_printed_concrete_signatures())
my_relu(x=[1, -1])
  Returns:
    float32 Tensor, shape=(2,)

my_relu(x)
  Args:
    x: float32 Tensor, shape=(2,)
  Returns:
    float32 Tensor, shape=(2,)

my_relu(x)
  Args:
    x: float32 Tensor, shape=()
  Returns:
    float32 Tensor, shape=()

Usando tf.function

Até agora, você viu como pode converter uma função Python em um gráfico simplesmente usando tf.function como um decorador ou wrapper. Mas, na prática, fazer com que tf.function funcione corretamente pode ser complicado! Nas seções a seguir, você aprenderá como fazer seu código funcionar conforme o esperado com tf.function .

Execução de gráfico vs. execução ansiosa

O código em uma Function pode ser executado avidamente e como um gráfico. Por padrão, Function executa seu código como um gráfico:

@tf.function
def get_MSE(y_true, y_pred):
  sq_diff = tf.pow(y_true - y_pred, 2)
  return tf.reduce_mean(sq_diff)
y_true = tf.random.uniform([5], maxval=10, dtype=tf.int32)
y_pred = tf.random.uniform([5], maxval=10, dtype=tf.int32)
print(y_true)
print(y_pred)
tf.Tensor([1 8 6 3 8], shape=(5,), dtype=int32)
tf.Tensor([9 8 3 6 1], shape=(5,), dtype=int32)

get_MSE(y_true, y_pred)
<tf.Tensor: shape=(), dtype=int32, numpy=26>

Para verificar se o gráfico de sua Function está fazendo o mesmo cálculo que sua função Python equivalente, podemos fazê-lo executar avidamente com tf.config.run_functions_eagerly(True) . Esta é uma chave que desativa a capacidade de Function de criar e executar gráficos , em vez de executar o código normalmente.

tf.config.run_functions_eagerly(True)
get_MSE(y_true, y_pred)
<tf.Tensor: shape=(), dtype=int32, numpy=26>
# Don't forget to set it back when you are done.
tf.config.run_functions_eagerly(False)

No entanto, Function pode se comportar de maneira diferente no gráfico e na execução rápida. A função de print Python é um exemplo de como esses dois modos diferem. Vamos ver o que acontece quando inserimos uma instrução print em nossa função e a chamamos repetidamente.

@tf.function
def get_MSE(y_true, y_pred):
  print("Calculating MSE!")
  sq_diff = tf.pow(y_true - y_pred, 2)
  return tf.reduce_mean(sq_diff)

Observe o que está impresso:

error = get_MSE(y_true, y_pred)
error = get_MSE(y_true, y_pred)
error = get_MSE(y_true, y_pred)
Calculating MSE!

A saída é surpreendente? get_MSE impresso apenas uma vez, embora tenha sido chamado três vezes.

Para explicar, a instrução print é executada quando Function executa o código original para criar o gráfico em um processo conhecido como "rastreamento" . O rastreamento captura as operações do TensorFlow em um gráfico, e a print não é capturada no gráfico. Esse gráfico é então executado para todas as três chamadas, sem nunca executar o código Python novamente .

Como verificação de integridade, vamos desligar a execução do gráfico para comparar:

# Now, globally set everything to run eagerly to force eager execution.
tf.config.run_functions_eagerly(True)
# Observe what is printed below.
error = get_MSE(y_true, y_pred)
error = get_MSE(y_true, y_pred)
error = get_MSE(y_true, y_pred)
Calculating MSE!
Calculating MSE!
Calculating MSE!

tf.config.run_functions_eagerly(False)

print é um efeito colateral do Python , e existem outras diferenças que você deve estar ciente ao converter uma função em uma Function .

práticas recomendadas de tf.function

Pode levar algum tempo para se acostumar com o comportamento de Function . Para começar rapidamente, os usuários @tf.function devem brincar com as funções de decoração de brinquedos com @tf.function para obter experiência em ir de ansioso para execução de gráfico.

Projetar para tf.function pode ser sua melhor aposta para escrever programas TensorFlow compatíveis com gráficos. Aqui estão algumas dicas:

  • Alterne entre a execução antecipada e de gráfico antecipada e frequentemente com tf.config.run_functions_eagerly para identificar se / quando os dois modos divergem.
  • Crie tf.Variable s fora da função Python e modifique-os internamente. O mesmo vale para objetos que usam tf.Variable , comokeras.layers , keras.Model tf.optimizers .
  • Evite escrever funções que dependem de variáveis ​​Python externas , exceto objetos tf.Variables e Keras.
  • Prefira escrever funções que usam tensores e outros tipos do TensorFlow como entrada. Você pode passar outros tipos de objetos, mas tenha cuidado !
  • Inclua o máximo de computação possível em um tf.function para maximizar o ganho de desempenho. Por exemplo, decore uma etapa de treinamento inteira ou todo o ciclo de treinamento.

Vendo a aceleração

tf.function geralmente melhora o desempenho do seu código, mas a quantidade de aceleração depende do tipo de computação que você executa. Pequenos cálculos podem ser dominados pela sobrecarga de chamar um gráfico. Você pode medir a diferença de desempenho da seguinte forma:

x = tf.random.uniform(shape=[10, 10], minval=-1, maxval=2, dtype=tf.dtypes.int32)

def power(x, y):
  result = tf.eye(10, dtype=tf.dtypes.int32)
  for _ in range(y):
    result = tf.matmul(x, result)
  return result
print("Eager execution:", timeit.timeit(lambda: power(x, 100), number=1000))
Eager execution: 1.9166935040000226

power_as_graph = tf.function(power)
print("Graph execution:", timeit.timeit(lambda: power_as_graph(x, 100), number=1000))
Graph execution: 0.5355543040000157

tf.function é comumente usado para acelerar loops de treinamento, como você pode ver aqui com Keras.

Desempenho e trade-offs

Os gráficos podem acelerar seu código, mas o processo de criá-los tem alguma sobrecarga. Para algumas funções, a criação do gráfico leva mais tempo do que a execução do gráfico. Esse investimento geralmente é pago de volta com o aumento de desempenho das execuções subsequentes, mas é importante estar ciente de que as primeiras etapas de qualquer treinamento de modelo grande podem ser mais lentas devido ao rastreamento.

Não importa o tamanho do seu modelo, você deseja evitar rastreios com frequência. O guia tf.function discute como definir especificações de entrada e usar argumentos de tensor para evitar retraçamento. Se você descobrir que está obtendo um desempenho anormalmente ruim, é uma boa ideia verificar se está refazendo acidentalmente.

Quando é um rastreamento de Function ?

Para descobrir quando sua Function está rastreando, adicione uma instrução de print a seu código. Como regra geral, Function executará a instrução de print sempre que rastrear.

@tf.function
def a_function_with_python_side_effect(x):
  print("Tracing!") # An eager-only side effect.
  return x * x + tf.constant(2)

# This is traced the first time.
print(a_function_with_python_side_effect(tf.constant(2)))
# The second time through, you won't see the side effect.
print(a_function_with_python_side_effect(tf.constant(3)))
Tracing!
tf.Tensor(6, shape=(), dtype=int32)
tf.Tensor(11, shape=(), dtype=int32)

# This retraces each time the Python argument changes,
# as a Python argument could be an epoch count or other
# hyperparameter.
print(a_function_with_python_side_effect(2))
print(a_function_with_python_side_effect(3))
Tracing!
tf.Tensor(6, shape=(), dtype=int32)
Tracing!
tf.Tensor(11, shape=(), dtype=int32)

Aqui, você vê um rastreamento extra porque novos argumentos Python sempre acionam a criação de um novo gráfico.

Próximos passos

Você pode ler uma discussão mais aprofundada na página de referência da API tf.function e no guia .