Início Tecnologia Introdução com gradientes e diferenciação automática no tensorflow

Introdução com gradientes e diferenciação automática no tensorflow

20
0

A diferenciação automática é útil para implementar algoritmos de aprendizado de máquina, como retropropagação para o treinamento de redes neurais.

Neste guia, você explorará maneiras de calcular gradientes com o TensorFlow, especialmente na execução ansiosa.

Configurar

import numpy as np
import matplotlib.pyplot as plt

import tensorflow as tf
2024-08-15 01:30:07.003169: E exterior/local_xla/xla/stream_executor/cuda/cuda_fft.cc:485] Unable to register cuFFT manufacturing facility: Making an attempt to register manufacturing facility for plugin cuFFT when one has already been registered
2024-08-15 01:30:07.023862: E exterior/local_xla/xla/stream_executor/cuda/cuda_dnn.cc:8454] Unable to register cuDNN manufacturing facility: Making an attempt to register manufacturing facility for plugin cuDNN when one has already been registered
2024-08-15 01:30:07.029954: E exterior/local_xla/xla/stream_executor/cuda/cuda_blas.cc:1452] Unable to register cuBLAS manufacturing facility: Making an attempt to register manufacturing facility for plugin cuBLAS when one has already been registered

Gradientes de computação

Para diferenciar automaticamente, o TensorFlow precisa lembrar quais operações acontecem em que ordem durante o avançar passar. Então, durante o passe para trásTensorflow atravessa esta lista de operações em ordem inversa para calcular gradientes.

Fitas de gradiente

O TensorFlow fornece o tf.GradientTape API para diferenciação automática; isto é, calcular o gradiente de um cálculo em relação a algumas entradas, geralmente tf.Variables. Operações relevantes do Tensorflow “Registros” executadas dentro do contexto de um tf.GradientTape em uma “fita”. O TensorFlow usa então essa fita para calcular os gradientes de um cálculo “gravado” usando a diferenciação do modo reverso.

Aqui está um exemplo simples:

x = tf.Variable(3.0)

with tf.GradientTape() as tape:
  y = x**2
WARNING: All log messages earlier than absl::InitializeLog() is named are written to STDERR
I0000 00:00:1723685409.408818   20970 cuda_executor.cc:1015] profitable NUMA node learn from SysFS had unfavourable worth (-1), however there should be at the very least one NUMA node, so returning NUMA node zero. See extra at 
I0000 00:00:1723685409.412555   20970 cuda_executor.cc:1015] profitable NUMA node learn from SysFS had unfavourable worth (-1), however there should be at the very least one NUMA node, so returning NUMA node zero. See extra at 
I0000 00:00:1723685409.416343   20970 cuda_executor.cc:1015] profitable NUMA node learn from SysFS had unfavourable worth (-1), however there should be at the very least one NUMA node, so returning NUMA node zero. See extra at 
I0000 00:00:1723685409.420087   20970 cuda_executor.cc:1015] profitable NUMA node learn from SysFS had unfavourable worth (-1), however there should be at the very least one NUMA node, so returning NUMA node zero. See extra at 
I0000 00:00:1723685409.431667   20970 cuda_executor.cc:1015] profitable NUMA node learn from SysFS had unfavourable worth (-1), however there should be at the very least one NUMA node, so returning NUMA node zero. See extra at 
I0000 00:00:1723685409.435229   20970 cuda_executor.cc:1015] profitable NUMA node learn from SysFS had unfavourable worth (-1), however there should be at the very least one NUMA node, so returning NUMA node zero. See extra at 
I0000 00:00:1723685409.438777   20970 cuda_executor.cc:1015] profitable NUMA node learn from SysFS had unfavourable worth (-1), however there should be at the very least one NUMA node, so returning NUMA node zero. See extra at 
I0000 00:00:1723685409.442350   20970 cuda_executor.cc:1015] profitable NUMA node learn from SysFS had unfavourable worth (-1), however there should be at the very least one NUMA node, so returning NUMA node zero. See extra at 
I0000 00:00:1723685409.445712   20970 cuda_executor.cc:1015] profitable NUMA node learn from SysFS had unfavourable worth (-1), however there should be at the very least one NUMA node, so returning NUMA node zero. See extra at 
I0000 00:00:1723685409.449141   20970 cuda_executor.cc:1015] profitable NUMA node learn from SysFS had unfavourable worth (-1), however there should be at the very least one NUMA node, so returning NUMA node zero. See extra at 
I0000 00:00:1723685409.452491   20970 cuda_executor.cc:1015] profitable NUMA node learn from SysFS had unfavourable worth (-1), however there should be at the very least one NUMA node, so returning NUMA node zero. See extra at 
I0000 00:00:1723685409.456034   20970 cuda_executor.cc:1015] profitable NUMA node learn from SysFS had unfavourable worth (-1), however there should be at the very least one NUMA node, so returning NUMA node zero. See extra at 
I0000 00:00:1723685410.685265   20970 cuda_executor.cc:1015] profitable NUMA node learn from SysFS had unfavourable worth (-1), however there should be at the very least one NUMA node, so returning NUMA node zero. See extra at 
I0000 00:00:1723685410.687389   20970 cuda_executor.cc:1015] profitable NUMA node learn from SysFS had unfavourable worth (-1), however there should be at the very least one NUMA node, so returning NUMA node zero. See extra at 
I0000 00:00:1723685410.689411   20970 cuda_executor.cc:1015] profitable NUMA node learn from SysFS had unfavourable worth (-1), however there should be at the very least one NUMA node, so returning NUMA node zero. See extra at 
I0000 00:00:1723685410.691490   20970 cuda_executor.cc:1015] profitable NUMA node learn from SysFS had unfavourable worth (-1), however there should be at the very least one NUMA node, so returning NUMA node zero. See extra at 
I0000 00:00:1723685410.693542   20970 cuda_executor.cc:1015] profitable NUMA node learn from SysFS had unfavourable worth (-1), however there should be at the very least one NUMA node, so returning NUMA node zero. See extra at 
I0000 00:00:1723685410.695541   20970 cuda_executor.cc:1015] profitable NUMA node learn from SysFS had unfavourable worth (-1), however there should be at the very least one NUMA node, so returning NUMA node zero. See extra at 
I0000 00:00:1723685410.697441   20970 cuda_executor.cc:1015] profitable NUMA node learn from SysFS had unfavourable worth (-1), however there should be at the very least one NUMA node, so returning NUMA node zero. See extra at 
I0000 00:00:1723685410.699432   20970 cuda_executor.cc:1015] profitable NUMA node learn from SysFS had unfavourable worth (-1), however there should be at the very least one NUMA node, so returning NUMA node zero. See extra at 
I0000 00:00:1723685410.701351   20970 cuda_executor.cc:1015] profitable NUMA node learn from SysFS had unfavourable worth (-1), however there should be at the very least one NUMA node, so returning NUMA node zero. See extra at 
I0000 00:00:1723685410.703333   20970 cuda_executor.cc:1015] profitable NUMA node learn from SysFS had unfavourable worth (-1), however there should be at the very least one NUMA node, so returning NUMA node zero. See extra at 
I0000 00:00:1723685410.705229   20970 cuda_executor.cc:1015] profitable NUMA node learn from SysFS had unfavourable worth (-1), however there should be at the very least one NUMA node, so returning NUMA node zero. See extra at 
I0000 00:00:1723685410.707222   20970 cuda_executor.cc:1015] profitable NUMA node learn from SysFS had unfavourable worth (-1), however there should be at the very least one NUMA node, so returning NUMA node zero. See extra at 
I0000 00:00:1723685410.744994   20970 cuda_executor.cc:1015] profitable NUMA node learn from SysFS had unfavourable worth (-1), however there should be at the very least one NUMA node, so returning NUMA node zero. See extra at 
I0000 00:00:1723685410.747037   20970 cuda_executor.cc:1015] profitable NUMA node learn from SysFS had unfavourable worth (-1), however there should be at the very least one NUMA node, so returning NUMA node zero. See extra at 
I0000 00:00:1723685410.749507   20970 cuda_executor.cc:1015] profitable NUMA node learn from SysFS had unfavourable worth (-1), however there should be at the very least one NUMA node, so returning NUMA node zero. See extra at 
I0000 00:00:1723685410.751538   20970 cuda_executor.cc:1015] profitable NUMA node learn from SysFS had unfavourable worth (-1), however there should be at the very least one NUMA node, so returning NUMA node zero. See extra at 
I0000 00:00:1723685410.753500   20970 cuda_executor.cc:1015] profitable NUMA node learn from SysFS had unfavourable worth (-1), however there should be at the very least one NUMA node, so returning NUMA node zero. See extra at 
I0000 00:00:1723685410.755501   20970 cuda_executor.cc:1015] profitable NUMA node learn from SysFS had unfavourable worth (-1), however there should be at the very least one NUMA node, so returning NUMA node zero. See extra at 
I0000 00:00:1723685410.757421   20970 cuda_executor.cc:1015] profitable NUMA node learn from SysFS had unfavourable worth (-1), however there should be at the very least one NUMA node, so returning NUMA node zero. See extra at 
I0000 00:00:1723685410.759404   20970 cuda_executor.cc:1015] profitable NUMA node learn from SysFS had unfavourable worth (-1), however there should be at the very least one NUMA node, so returning NUMA node zero. See extra at 
I0000 00:00:1723685410.761363   20970 cuda_executor.cc:1015] profitable NUMA node learn from SysFS had unfavourable worth (-1), however there should be at the very least one NUMA node, so returning NUMA node zero. See extra at 
I0000 00:00:1723685410.763858   20970 cuda_executor.cc:1015] profitable NUMA node learn from SysFS had unfavourable worth (-1), however there should be at the very least one NUMA node, so returning NUMA node zero. See extra at 
I0000 00:00:1723685410.766199   20970 cuda_executor.cc:1015] profitable NUMA node learn from SysFS had unfavourable worth (-1), however there should be at the very least one NUMA node, so returning NUMA node zero. See extra at 
I0000 00:00:1723685410.768560   20970 cuda_executor.cc:1015] profitable NUMA node learn from SysFS had unfavourable worth (-1), however there should be at the very least one NUMA node, so returning NUMA node zero. See extra at 

Depois de gravar algumas operações, use GradientTape.gradient(goal, sources) Para calcular o gradiente de algum alvo (geralmente uma perda) em relação a alguma fonte (geralmente as variáveis ​​do modelo):

# dy = 2x * dx
dy_dx = tape.gradient(y, x)
dy_dx.numpy()

O exemplo acima usa escalares, mas tf.GradientTape Funciona tão facilmente em qualquer tensor:

w = tf.Variable(tf.random.regular((3, 2)), title='w')
b = tf.Variable(tf.zeros(2, dtype=tf.float32), title='b')
x = [[1., 2., 3.]]

with tf.GradientTape(persistent=True) as tape:
  y = x @ w + b
  loss = tf.reduce_mean(y**2)

Para obter o gradiente de loss Com relação às duas variáveis, você pode passar como fontes para o gradient método. A fita é flexível sobre como as fontes são passadas e aceitarão qualquer combinação aninhada de listas ou dicionários e devolverá o gradiente estruturado da mesma maneira (veja tf.nest).

[dl_dw, dl_db] = tape.gradient(loss, [w, b])

O gradiente em relação a cada fonte tem a forma da fonte:

print(w.form)
print(dl_dw.form)
(3, 2)
(3, 2)

Aqui está o cálculo do gradiente novamente, desta vez passando um dicionário de variáveis:

my_vars = {
    'w': w,
    'b': b
}

grad = tape.gradient(loss, my_vars)
grad['b']

Gradients with respect to a model

It’s common to collect tf.Variables into a tf.Module or one of its subclasses (layers.Layerkeras.Model) for checkpointing and exporting.

In most cases, you will want to calculate gradients with respect to a model’s trainable variables. Since all subclasses of tf.Module aggregate their variables in the Module.trainable_variables property, you can calculate these gradients in a few lines of code:

layer = tf.keras.layers.Dense(2, activation='relu')
x = tf.constant([[1., 2., 3.]]) com tf.gradientetape () como fita: # ahead passa y = camada (x) perda = tf.reduce_mean (y ** 2) # calcule gradientes em relação a cada variável treinável = tapra.gradiente (perda, layer.trainable_variables)
for var, g in zip(layer.trainable_variables, grad):
  print(f'{var.title}, form: {g.form}')
kernel, form: (3, 2)
bias, form: (2,)

Controlando o que a fita observa

O comportamento padrão é gravar todas as operações depois de acessar um treinável tf.Variable. As razões para isso são:

  • A fita precisa saber quais operações registrarem no passe para a frente para calcular os gradientes no passe para trás.
  • A fita contém referências a saídas intermediárias, para que você não registre operações desnecessárias.
  • O caso de uso mais comum envolve o cálculo do gradiente de uma perda em relação a todas as variáveis ​​treináveis ​​de um modelo.

Por exemplo, o seguinte falha em calcular um gradiente porque o tf.Tensor não é “assistido” por padrão, e o tf.Variable não é treinável:

# A trainable variable
x0 = tf.Variable(3.0, title='x0')
# Not trainable
x1 = tf.Variable(3.0, title='x1', trainable=False)
# Not a Variable: A variable + tensor returns a tensor.
x2 = tf.Variable(2.0, title='x2') + 1.0
# Not a variable
x3 = tf.fixed(3.0, title='x3')

with tf.GradientTape() as tape:
  y = (x0**2) + (x1**2) + (x2**2)

grad = tape.gradient(y, [x0, x1, x2, x3])

for g in grad:
  print(g)
tf.Tensor(6.0, form=(), dtype=float32)
None
None
None

Você pode listar as variáveis ​​que estão sendo observadas pela fita usando o GradientTape.watched_variables método:

[var.name for var in tape.watched_variables()]
['x0:0']

tf.GradientTape Fornece ganchos que dão ao controle do usuário sobre o que é ou não assistido.

Para registrar gradientes em relação a um tf.Tensorvocê precisa ligar GradientTape.watch(x):

x = tf.fixed(3.0)
with tf.GradientTape() as tape:
  tape.watch(x)
  y = x**2

# dy = 2x * dx
dy_dx = tape.gradient(y, x)
print(dy_dx.numpy())
6.0

Por outro lado, para desativar o comportamento padrão de assistir a todos tf.Variablesdefinir watch_accessed_variables=False Ao criar a fita gradiente. Este cálculo usa duas variáveis, mas conecta apenas o gradiente para uma das variáveis:

x0 = tf.Variable(0.0)
x1 = tf.Variable(10.0)

with tf.GradientTape(watch_accessed_variables=False) as tape:
  tape.watch(x1)
  y0 = tf.math.sin(x0)
  y1 = tf.nn.softplus(x1)
  y = y0 + y1
  ys = tf.reduce_sum(y)

Desde GradientTape.watch não foi chamado x0nenhum gradiente é calculado em relação a ele:

# dys/dx1 = exp(x1) / (1 + exp(x1)) = sigmoid(x1)
grad = tape.gradient(ys, {'x0': x0, 'x1': x1})

print('dy/dx0:', grad['x0'])
print('dy/dx1:', grad['x1'].numpy())
dy/dx0: None
dy/dx1: 0.9999546

Você também pode solicitar gradientes da saída em relação aos valores intermediários calculados dentro do tf.GradientTape contexto.

x = tf.fixed(3.0)

with tf.GradientTape() as tape:
  tape.watch(x)
  y = x * x
  z = y * y

# Use the tape to compute the gradient of z with respect to the
# intermediate worth y.
# dz_dy = 2 * y and y = x ** 2 = 9
print(tape.gradient(z, y).numpy())
18.0

Por padrão, os recursos mantidos por um GradientTape são lançados assim que o GradientTape.gradient O método é chamado. Para calcular vários gradientes no mesmo cálculo, crie uma fita gradiente com persistent=True. Isso permite várias chamadas para o gradient O método como os recursos são liberados quando o objeto de fita é coletado de lixo. Por exemplo:

x = tf.fixed([1, 3.0])
with tf.GradientTape(persistent=True) as tape:
  tape.watch(x)
  y = x * x
  z = y * y

print(tape.gradient(z, x).numpy())  # [4.0, 108.0] (4 * x**3 at x = [1.0, 3.0])
print(tape.gradient(y, x).numpy())  # [2.0, 6.0] (2 * x at x = [1.0, 3.0])
[  4. 108.]
[2. 6.]
del tape   # Drop the reference to the tape

Notas sobre desempenho

  • Há uma pequena sobrecarga associada à execução de operações dentro de um contexto de fita gradiente. Para a execução mais ansiosa, isso não será um custo notável, mas você ainda deve usar o contexto de fita em torno das áreas somente onde for necessário.

  • As fitas gradientes usam memória para armazenar resultados intermediários, incluindo entradas e saídas, para uso durante o passe para trás.

    Para eficiência, alguns OPs (como ReLU) não precisam manter seus resultados intermediários e eles são podados durante o passe para a frente. No entanto, se você usar persistent=True Na sua fita, Nada é descartado E o uso de memória de pico de memória será maior.

Gradientes de alvos não escalares

Um gradiente é fundamentalmente uma operação em um escalar.

x = tf.Variable(2.0)
with tf.GradientTape(persistent=True) as tape:
  y0 = x**2
  y1 = 1 / x

print(tape.gradient(y0, x).numpy())
print(tape.gradient(y1, x).numpy())
4.0
-0.25

Assim, se você solicitar o gradiente de vários alvos, o resultado de cada fonte é:

  • O gradiente da soma dos alvos, ou equivalente
  • A soma dos gradientes de cada alvo.
x = tf.Variable(2.0)
with tf.GradientTape() as tape:
  y0 = x**2
  y1 = 1 / x

print(tape.gradient({'y0': y0, 'y1': y1}, x).numpy())
3.75

Da mesma forma, se o (s) alvo (s) não for escalar, o gradiente da soma é calculado:

x = tf.Variable(2.)

with tf.GradientTape() as tape:
  y = x * [3., 4.]

print(tape.gradient(y, x).numpy())
7.0

Isso simplifica o gradiente da soma de uma coleção de perdas ou o gradiente da soma de um cálculo de perda no elemento.

Se você precisar de um gradiente separado para cada merchandise, consulte os jacobianos.

Em alguns casos, você pode pular o jacobiano. Para um cálculo em termos de elemento, o gradiente da soma fornece a derivada de cada elemento em relação ao seu elemento de entrada, pois cada elemento é independente:

x = tf.linspace(-10.0, 10.0, 200+1)

with tf.GradientTape() as tape:
  tape.watch(x)
  y = tf.nn.sigmoid(x)

dy_dx = tape.gradient(y, x)
plt.plot(x, y, label='y')
plt.plot(x, dy_dx, label='dy/dx')
plt.legend()
_ = plt.xlabel('x')

pngpng

Fluxo de controle

Como um gradiente registra as operações de registro quando são executadas, o fluxo de controle do Python é tratado naturalmente (por exemplo, if e whereas declarações).

Aqui, uma variável diferente é usada em cada ramo de um if. O gradiente se conecta apenas à variável que foi usada:

x = tf.fixed(1.0)

v0 = tf.Variable(2.0)
v1 = tf.Variable(2.0)

with tf.GradientTape(persistent=True) as tape:
  tape.watch(x)
  if x > 0.0:
    consequence = v0
  else:
    consequence = v1**2 

dv0, dv1 = tape.gradient(consequence, [v0, v1])

print(dv0)
print(dv1)
tf.Tensor(1.0, form=(), dtype=float32)
None

Lembre-se de que as declarações de controle em si não são diferenciáveis, por isso são invisíveis para otimizadores baseados em gradientes.

Dependendo do valor de x No exemplo acima, a fita registra consequence = v0 ou consequence = v1**2. O gradiente em relação a x é sempre None.

dx = tape.gradient(consequence, x)

print(dx)
None

Casos onde gradient retorna None

Quando um alvo não está conectado a uma fonte, gradient voltará None.

x = tf.Variable(2.)
y = tf.Variable(3.)

with tf.GradientTape() as tape:
  z = y * y
print(tape.gradient(z, x))
None

Aqui z obviamente não está conectado a xmas existem várias maneiras menos óbvias que um gradiente pode ser desconectado.

1. Substituiu uma variável por um tensor

Na seção sobre “Controlando o que a fita observa” que você viu que a fita vai assistir automaticamente um tf.Variable mas não um tf.Tensor.

Um erro comum é substituir inadvertidamente um tf.Variable com um tf.Tensorem vez de usar Variable.assign Para atualizar o tf.Variable. Aqui está um exemplo:

x = tf.Variable(2.0)

for epoch in vary(2):
  with tf.GradientTape() as tape:
    y = x+1

  print(sort(x).__name__, ":", tape.gradient(y, x))
  x = x + 1   # This must be `x.assign_add(1)`

2. Os cálculos fora do tensorflow

A fita não pode gravar o caminho do gradiente se o cálculo sair do TensorFlow. Por exemplo:

x = tf.Variable([[1.0, 2.0],
                 [3.0, 4.0]], dtype=tf.float32)

with tf.GradientTape() as tape:
  x2 = x**2

  # This step is calculated with NumPy
  y = np.imply(x2, axis=0)

  # Like most ops, reduce_mean will forged the NumPy array to a relentless tensor
  # utilizing `tf.convert_to_tensor`.
  y = tf.reduce_mean(y, axis=0)

print(tape.gradient(y, x))

None

3. Tomou gradientes através de um número inteiro ou corda

Inteiros e cordas não são diferenciáveis. Se um caminho de cálculo usar esses tipos de dados, não haverá gradiente.

Ninguém espera que as cordas sejam diferenciáveis, mas é fácil criar acidentalmente um int constante ou variável se você não especificar o dtype.

x = tf.fixed(10)

with tf.GradientTape() as g:
  g.watch(x)
  y = x * x

print(g.gradient(y, x))
WARNING:tensorflow:The dtype of the watched tensor should be floating (e.g. tf.float32), acquired tf.int32
None

O TensorFlow não é lançado automaticamente entre os tipos; portanto, na prática, você geralmente recebe um erro de tipo em vez de um gradiente ausente.

4. Tomou gradientes através de um objeto com estado de Estado

O estado interrompe os gradientes. Quando você lê de um objeto com estado, a fita só pode observar o estado atual, não a história que o leva.

UM tf.Tensor é imutável. Você não pode alterar um tensor assim que for criado. Tem um valormas não estado. Todas as operações discutidas até agora também estão apátridas: a saída de um tf.matmul depende apenas de suas entradas.

UM tf.Variable tem estado interno – seu valor. Quando você usa a variável, o estado é lido. É regular calcular um gradiente em relação a uma variável, mas o estado da variável bloqueia os cálculos de gradiente de voltar mais longe. Por exemplo:

x0 = tf.Variable(3.0)
x1 = tf.Variable(0.0)

with tf.GradientTape() as tape:
  # Replace x1 = x1 + x0.
  x1.assign_add(x0)
  # The tape begins recording from x1.
  y = x1**2   # y = (x1 + x0)**2

# This does not work.
print(tape.gradient(y, x0))   #dy/dx0 = 2*(x1 + x0)
None

De forma related, tf.information.Dataset iteradores e tf.queueS são estabelecidos e impedirão todos os gradientes em tensores que passam por eles.

Nenhum gradiente registrado

Alguns tf.OperationS são registrado como não sendo diferenciável e voltará None. Outros têm Nenhum gradiente registrado.

O tf.raw_ops A página mostra quais operações de baixo nível têm gradientes registrados.

Se você tentar assumir um gradiente através de um OP de flutuação que não possui gradiente registrado, a fita fará um erro em vez de retornar silenciosamente None. Dessa forma, você sabe que algo deu errado.

Por exemplo, o tf.picture.adjust_contrast Funções de função raw_ops.AdjustContrastv2que pode ter um gradiente, mas o gradiente não é implementado:

picture = tf.Variable([[[0.5, 0.0, 0.0]]])
delta = tf.Variable(0.1)

with tf.GradientTape() as tape:
  new_image = tf.picture.adjust_contrast(picture, delta)

strive:
  print(tape.gradient(new_image, [image, delta]))
  assert False   # This could not occur.
besides LookupError as e:
  print(f'{sort(e).__name__}: {e}')
LookupError: gradient registry has no entry for: AdjustContrastv2

Se você precisar diferenciar este OP, você precisará implementar o gradiente e registrá -lo (usando tf.RegisterGradient) ou reimplementar a função usando outras operações.

Zeros em vez de nenhum

Em alguns casos, seria conveniente obter 0 em vez de None para gradientes desconectados. Você pode decidir o que retornar quando tiver gradientes desconectados usando o unconnected_gradients argumento:

x = tf.Variable([2., 2.])
y = tf.Variable(3.)

with tf.GradientTape() as tape:
  z = y**2
print(tape.gradient(z, x, unconnected_gradients=tf.UnconnectedGradients.ZERO))
tf.Tensor([0. 0.], form=(2,), dtype=float32)

Publicado originalmente no Website TensorflowAssim, Este artigo aparece aqui sob uma nova manchete e está licenciado sob CC por 4.0. Amostras de código compartilhadas sob o Licença Apache 2.0.

fonte