Back-End

22 nov, 2017

Eager Execution: uma interface imperativa, definida por execução para TensorFlow

Publicidade

Artigo de Asim Shankar, publicado originalmente pelo Google Developers Blog. A tradução foi feita pela Redação iMasters com autorização.

***

No final de outubro, lançamos a Eager Execution para TensorFlow.

Eager execution é uma interface imperativa, definida por execução, na qual as operações são executadas imediatamente, já que elas são chamadas do Python. Isso torna mais fácil começar com o TensorFlow, e pode tornar a pesquisa e o desenvolvimento mais intuitivos.

Os benefícios da Eager Execution incluem:

  • Debugging rápido com erros de tempo de execução imediatos e integração com ferramentas do Python.
  • Suporte para modelos dinâmicos usando fluxo de controle Python fácil de usar.
  • Forte suporte para gradientes personalizados e de ordem superior.
  • Quase todas as operações TensorFlow disponíveis.

Eager execution está disponível agora como um recurso experimental, então estamos à procura de feedback da comunidade para orientar nossa direção.

Para entender isso melhor, vejamos algum código. Isso é bastante técnico, e a familiaridade com TensorFlow ajudará.

Usando eager execution

Quando você habilita a Eager Execution, as operações são executadas imediatamente e retornam seus valores para o Python sem requerer Session.run(). Por exemplo, para multiplicar duas matrizes juntas, escrevemos isto:

import tensorflow as tf
import tensorflow.contrib.eager as tfe

tfe.enable_eager_execution()

x = [[2.]]
m = tf.matmul(x, x)

É simples inspecionar os resultados intermediários com print ou o debugger do Python.

print(m)
# The 1x1 matrix [[4.]]

Os modelos dinâmicos podem ser construídos com o controle de fluxo do Python. Aqui está um exemplo da conjectura Collatz usando as operações aritméticas do TensorFlow:

a = tf.constant(12)
counter = 0
while not tf.equal(a, 1):
  if tf.equal(a % 2, 0):
    a = a / 2
  else:
    a = 3 * a + 1
  print(a)

Aqui, o uso do objeto tf.constant (12) Tensor promoverá todas as operações matemáticas para operações de tensor e, como tal, todos os valores de retorno serão tensores.

Gradientes

A maioria dos usuários do TensorFlow está interessada em diferenciação automática. Como diferentes operações podem ocorrer durante cada chamada, gravamos todas as operações direto em uma fita, que é então reproduzida ao contrário quando calcula gradientes. Depois de calcular os gradientes, descartamos a fita.

Se você está familiarizado com o pacote autograd, a API é muito semelhante. Por exemplo:

def square(x):
  return tf.multiply(x, x) 

grad = tfe.gradients_function(square)

print(square(3.))    # [9.]
print(grad(3.))      # [6.]

A chamada gradients_function pega uma função Python square() como um argumento e retorna um Python callable que calcula as derivadas parciais de square() em relação às suas entradas. Então, para obter o derivado de square() em 3.0, invoque grad(3.0), que é 6.

A mesma chamada gradients_function pode ser usada para obter a segunda derivada do quadrado (square?):

gradgrad = tfe.gradients_function(lambda x: grad(x)[0])

print(gradgrad(3.))  # [2.]

Como observamos, o fluxo de controle pode causar operações diferentes para rodar, como neste exemplo.

def abs(x):
  return x if x > 0. else -x

grad = tfe.gradients_function(abs)

print(grad(2.0))  # [1.]
print(grad(-2.0)) # [-1.]

Gradientes personalizados

Os usuários podem querer definir gradientes personalizados para uma operação ou para uma função. Isso pode ser útil por vários motivos, incluindo o fornecimento de um gradiente mais eficiente ou numericamente estável para uma sequência de operações.

Aqui está um exemplo que ilustra o uso de gradientes personalizados. Vamos começar por analisar a função log(1 + ex), que comumente ocorre no cálculo da entropia cruzada e das probabilidades de log.

def log1pexp(x):
  return tf.log(1 + tf.exp(x))
grad_log1pexp = tfe.gradients_function(log1pexp)

# The gradient computation works fine at x = 0.
print(grad_log1pexp(0.))
# [0.5]
# However it returns a `nan` at x = 100 due to numerical instability.
print(grad_log1pexp(100.))
# [nan]

Podemos usar um gradiente personalizado para a função acima que simplifica analiticamente a expressão do gradiente. Observe como a implementação da função gradiente abaixo reutiliza uma expressão (tf.exp (x)) que foi calculada durante a passagem para a frente, tornando a computação em gradiente mais eficiente e evitando a computação redundante.

@tfe.custom_gradient
def log1pexp(x):
  e = tf.exp(x)
  def grad(dy):
    return dy * (1 - 1 / (1 + e))
  return tf.log(1 + e), grad
grad_log1pexp = tfe.gradients_function(log1pexp)

# Gradient at x = 0 works as before.
print(grad_log1pexp(0.))
# [0.5]
# And now gradient computation at x=100 works as well.
print(grad_log1pexp(100.))
# [1.0]

Construindo modelos

Os modelos podem ser organizados em classes. Aqui está uma classe de modelo que cria uma rede – simples – de duas camadas que pode classificar os dígitos padrão da MNIST.

class MNISTModel(tfe.Network):
  def __init__(self):
    super(MNISTModel, self).__init__()
    self.layer1 = self.track_layer(tf.layers.Dense(units=10))
    self.layer2 = self.track_layer(tf.layers.Dense(units=10))
  def call(self, input):
    """Actually runs the model."""
    result = self.layer1(input)
    result = self.layer2(result)
    return result

Recomendamos o uso das classes (não as funções) em tf.layers, uma vez que elas criam e contêm parâmetros do modelo (variáveis). Os tempos de vida das variáveis estão ligados ao tempo de vida dos objetos de camada, portanto, certifique-se de acompanhá-los.

Por que estamos usando tfe.Network? Uma rede é um container para camadas e é uma tf.layer.Layer, permitindo que objetos de rede sejam incorporados em outros objetos de Network. Ela também contém utilitários para auxiliar na inspeção, salvando e restaurando.

Mesmo sem treinar o modelo, podemos chamá-lo de forma imperativa e inspecionar a saída:

# Let's make up a blank input image
model = MNISTModel()
batch = tf.zeros([1, 1, 784])
print(batch.shape)
# (1, 1, 784)
result = model(batch)
print(result)
# tf.Tensor([[[ 0.  0., ...., 0.]]], shape=(1, 1, 10), dtype=float32)

Observe que não precisamos de espaços reservados ou sessões. A primeira vez em que passamos a entrada, os tamanhos dos parâmetros das camadas são definidos.

Para treinar qualquer modelo, definimos uma função de perda para otimizar, calcular gradientes e usar um otimizador para atualizar as variáveis. Primeiro, aqui está uma função de perda:

def loss_function(model, x, y):
  y_ = model(x)
  return tf.nn.softmax_cross_entropy_with_logits(labels=y, logits=y_)

E então, nosso loop de treinamento:

optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.001)
for (x, y) in tfe.Iterator(dataset):
  grads = tfe.implicit_gradients(loss_function)(model, x, y)
  optimizer.apply_gradients(grads)

implicit_gradients() calcula as derivadas de loss_function em relação a todas as variáveis TensorFlow usadas durante a computação.

Podemos mover a computação para uma GPU da mesma maneira que sempre fizemos com o TensorFlow:

with tf.device("/gpu:0"):
  for (x, y) in tfe.Iterator(dataset):
    optimizer.minimize(lambda: loss_function(model, x, y))

(Nota: Estamos fazendo um atalho no armazenamento de nossa perda e chamando diretamente o optimizer.minimize, mas você também pode usar o método apply_gradients() acima, eles são equivalentes.)

Usando eager com gráficos

Eager execution torna o desenvolvimento e o debugging muito mais interativos, mas os gráficos do TensorFlow têm muitas vantagens em relação a treinamento distribuído, otimizações de desempenho e implementação de produção.

O mesmo código que executa as operações quando a Eager Execution é ativada irá construir um gráfico descrevendo a computação quando não estiver fazendo isso. Para converter seus modelos em gráficos, basta executar o mesmo código em uma nova sessão do Python na qual a Eager Execution não tiver sido ativada, como visto no exemplo MNIST. O valor das variáveis do modelo pode ser salvo e restaurado a partir dos pontos de verificação, permitindo que se movam facilmente entre a programação Eager (imperativa) e o gráfico (declarativo).

Com isso, os modelos desenvolvidos com a Eager Execution ativada podem ser facilmente exportados para implementação de produção.
Em um futuro próximo, forneceremos utilitários para converter seletivamente partes do seu modelo em gráficos. Dessa forma, você pode fundir partes de sua computação (como internas de uma célula RNN personalizada) para alto desempenho, mas também manter a flexibilidade e a legibilidade da Eager Execution.

Como o meu código muda?

Usar a Eager Execution deve ser intuitivo para os usuários atuais do TensorFlow. Há apenas um punhado de APIs Eager; a maioria das API e operações existentes funcionam com a Eager habilitada. Algumas notas a ter em mente:

  • Assim como o TensorFlow, geralmente recomendamos que, se você ainda não tiver mudado de filas para usar tf.data para o processamento de entrada, você deve. É mais fácil de usar, e geralmente mais rápido. Para obter ajuda, veja esta publicação e a página de documentação.
  • Use camadas orientadas à objetos, como camadas tf.layer.Conv2D() ou Keras; elas têm armazenamento explícito para variáveis.
  • Para a maioria dos modelos, você pode escrever o código para que ele funcione da mesma forma para a Eager Execution e para a construção do gráfico. Existem algumas exceções, como modelos dinâmicos que utilizam o fluxo de controle do Python para alterar a computação com base nas entradas.
  • Quando você invoca tfe.enable_eager_execution(), ele não pode ser desligado. Para obter o comportamento do gráfico, comece uma nova sessão do Python.

Começando e o futuro

Este ainda é um lançamento preview, para que você possa atingir alguns limites difíceis. Para começar hoje:

Há muito mais para conversar sobre a Eager Execution e estamos entusiasmados, ou melhor, estamos ansiosos para que você a experimente hoje! O feedback é absolutamente bem-vindo.

***

Este artigo é do Google Developers Blog. Ele foi escrito por Asim Shankar. A tradução foi feita pela Redação iMasters com autorização. Você pode acessar o original em: https://developers.googleblog.com/2017/10/eager-execution-imperative-define-by.html