O Dia da Comunidade de ML é dia 9 de novembro! Junte-nos para atualização de TensorFlow, JAX, e mais Saiba mais

Introdução aos gradientes e diferenciação automática

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

Diferenciação e gradientes automáticos

Diferenciação automática é útil para implementação de algoritmos de aprendizagem de máquina, como backpropagation para a formação de redes neurais.

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

Configurar

import numpy as np
import matplotlib.pyplot as plt

import tensorflow as tf

Gradientes de computação

Para diferenciar automaticamente, TensorFlow preciso lembrar que as operações acontecem em que ordem durante o passe para frente. Em seguida, durante o passo para trás, TensorFlow atravessa esta lista de operações em sentido inverso aos gradientes de computação.

Fitas de gradiente

TensorFlow fornece a tf.GradientTape API para a diferenciação automática; isto é, calcular o gradiente de uma computação com respeito a algumas entradas, geralmente tf.Variable s. TensorFlow "registros" operações relevantes executado dentro do contexto de uma tf.GradientTape em uma "fita". TensorFlow depois utiliza essa fita para calcular os gradientes de um cálculo "gravado" usando diferenciação modo reverso .

Aqui está um exemplo simples:

x = tf.Variable(3.0)

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

Uma vez que você gravou algumas operações, utilize GradientTape.gradient(target, sources) para calcular o gradiente de algum alvo (muitas vezes uma perda) em relação a alguma fonte (muitas vezes variáveis do modelo):

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

O exemplo acima usa escalares, mas tf.GradientTape funciona como facilmente em qualquer tensor:

w = tf.Variable(tf.random.normal((3, 2)), name='w')
b = tf.Variable(tf.zeros(2, dtype=tf.float32), name='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 a ambas as variáveis, você pode passar tanto como fontes para a gradient método. A fita é flexível sobre como as fontes são passados e aceitará qualquer combinação aninhada de listas ou dicionários e retornar o gradiente estruturado da mesma forma (ver 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.shape)
print(dl_dw.shape)
(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']
<tf.Tensor: shape=(2,), dtype=float32, numpy=array([-1.6920902, -3.2363236], dtype=float32)>

Gradientes em relação a um modelo

É comum para coletar tf.Variables em um tf.Module ou uma de suas subclasses ( layers.Layer , keras.Model ) para pontos de verificação e exportação .

Na maioria dos casos, você desejará calcular gradientes em relação às variáveis ​​treináveis ​​de um modelo. Desde todas as subclasses de tf.Module agregar suas variáveis na Module.trainable_variables propriedade, você pode calcular esses gradientes em algumas linhas de código:

layer = tf.keras.layers.Dense(2, activation='relu')
x = tf.constant([[1., 2., 3.]])

with tf.GradientTape() as tape:
  # Forward pass
  y = layer(x)
  loss = tf.reduce_mean(y**2)

# Calculate gradients with respect to every trainable variable
grad = tape.gradient(loss, layer.trainable_variables)
for var, g in zip(layer.trainable_variables, grad):
  print(f'{var.name}, shape: {g.shape}')
dense/kernel:0, shape: (3, 2)
dense/bias:0, shape: (2,)

Controlando o que a fita assiste

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

  • A fita precisa saber quais operações registrar na passagem para frente para calcular os gradientes na passagem para trás.
  • A fita contém referências a saídas intermediárias, portanto, você não deseja gravar 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 não consegue calcular um gradiente porque a tf.Tensor não é "vigiadas" por padrão, eo tf.Variable não é treinável:

# A trainable variable
x0 = tf.Variable(3.0, name='x0')
# Not trainable
x1 = tf.Variable(3.0, name='x1', trainable=False)
# Not a Variable: A variable + tensor returns a tensor.
x2 = tf.Variable(2.0, name='x2') + 1.0
# Not a variable
x3 = tf.constant(3.0, name='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, shape=(), dtype=float32)
None
None
None

Você pode listar as variáveis sendo vigiado 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 usuário controle sobre o que é ou não é vigiado.

Para gradientes recordes com relação a um tf.Tensor , você precisa chamar GradientTape.watch(x) :

x = tf.constant(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 os tf.Variables , conjunto 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 em x0 , nenhum inclinação é calculado em relação a ela:

# 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

Resultados intermediários

Também é possível solicitar o gradientes de saída em relação aos valores intermédios computados no interior do tf.GradientTape contexto.

x = tf.constant(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 value 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 liberados tão logo o GradientTape.gradient método é chamado. Para calcular múltiplos gradientes sobre o mesmo cálculo, criar uma cassete de gradiente com persistent=True . Isso permite que várias chamadas para o gradient método como os recursos são liberados quando o objeto fita é lixo coletado. Por exemplo:

x = tf.constant([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 à realização de operações dentro de um contexto de fita gradiente. Para a execução mais rápida, isso não será um custo perceptível, mas você ainda deve usar o contexto de fita em torno das áreas apenas onde for necessário.

  • As fitas de gradiente usam memória para armazenar resultados intermediários, incluindo entradas e saídas, para uso durante a passagem para trás.

    Para a eficiência, alguns ops (como ReLU ) não precisam manter seus resultados intermediários e eles são podadas durante o passe para frente. No entanto, se você usar persistent=True na fita, nada é descartado e seu uso de memória de pico 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 para cada fonte será:

  • O gradiente da soma dos alvos, ou equivalentemente
  • 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 forem escalares, 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 torna simples obter o gradiente da soma de uma coleção de perdas ou o gradiente da soma de um cálculo de perda por elemento.

Se você precisar de um gradiente separado para cada item, consulte Jacobianos .

Em alguns casos, você pode ignorar o Jacobiano. Para um cálculo de elemento, o gradiente da soma fornece a derivada de cada elemento em relação ao seu elemento de entrada, uma vez que 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')

png

Controle de fluxo

Uma vez que uma fita de gradiente grava operações como eles são executados, o fluxo de controlo do Python é naturalmente controlada (por exemplo, if e while demonstrações).

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

x = tf.constant(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:
    result = v0
  else:
    result = v1**2 

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

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

Apenas lembre-se de que as próprias instruções de controle não são diferenciáveis, portanto, são invisíveis para otimizadores baseados em gradiente.

Dependendo do valor de x , no exemplo acima, os registos de fitas tanto result = v0 ou result = v1**2 . A inclinação em relação ao x é sempre None .

dx = tape.gradient(result, x)

print(dx)
None

A obtenção de um gradiente de None

Quando um alvo não está ligado a uma fonte que você vai ter um gradiente de 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á ligado à x , mas existem várias maneiras menos óbvias que um gradiente podem ser desconectados.

1. Substituiu uma variável por um tensor

Na seção sobre "controlar o que a fita relógios" você viu que a fita vai assistir automaticamente um tf.Variable mas não um tf.Tensor .

Um erro comum é inadvertidamente substituir um tf.Variable com um tf.Tensor , em vez de usar Variable.assign para atualizar o tf.Variable . Aqui está um exemplo:

x = tf.Variable(2.0)

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

  print(type(x).__name__, ":", tape.gradient(y, x))
  x = x + 1   # This should be `x.assign_add(1)`
ResourceVariable : tf.Tensor(1.0, shape=(), dtype=float32)
EagerTensor : None

2. Fez cálculos fora do TensorFlow

A fita não pode registrar 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.mean(x2, axis=0)

  # Like most ops, reduce_mean will cast the NumPy array to a constant tensor
  # using `tf.convert_to_tensor`.
  y = tf.reduce_mean(y, axis=0)

print(tape.gradient(y, x))
None

3. Tomou gradientes por meio de um inteiro ou string

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

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

x = tf.constant(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 must be floating (e.g. tf.float32), got tf.int32
WARNING:tensorflow:The dtype of the target tensor must be floating (e.g. tf.float32) when calling GradientTape.gradient, got tf.int32
WARNING:tensorflow:The dtype of the source tensor must be floating (e.g. tf.float32) when calling GradientTape.gradient, got tf.int32
None

O TensorFlow não lança automaticamente entre tipos, então, na prática, você frequentemente obterá um erro de tipo em vez de um gradiente ausente.

4. Tomou gradientes por meio de um objeto com estado

O estado para gradientes. Quando você lê de um objeto com estado, a fita pode apenas observar o estado atual, não o histórico que leva a ele.

A tf.Tensor é imutável. Você não pode alterar um tensor depois de criado. Ele tem um valor, mas nenhum estado. Todas as operações discutidas até agora são também apátrida: a saída de um tf.matmul só depende suas entradas.

A tf.Variable tem estado seu valor interno. Quando você usa a variável, o estado é lido. É normal calcular um gradiente em relação a uma variável, mas o estado da variável impede que os cálculos de gradiente retrocedam. Por exemplo:

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

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

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

Da mesma forma, tf.data.Dataset iterators e tf.queue s são stateful, e irá parar todos os gradientes de tensores que passam por eles.

Nenhum gradiente registrado

Alguns tf.Operation s são registrados como sendo não-diferenciável e vai voltar None . Outros não têm inclinação registrado.

O tf.raw_ops mostra página que ops de baixo nível têm gradientes registrados.

Se você tentar tirar um gradiente através de uma op flutuador que não tem inclinação registrou a fita irá lançar um erro em vez de silenciosamente retornando None . Dessa forma, você sabe que algo deu errado.

Por exemplo, o tf.image.adjust_contrast função envolve raw_ops.AdjustContrastv2 , o que poderia ter um gradiente mas o gradiente não é implementada:

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

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

try:
  print(tape.gradient(new_image, [image, delta]))
  assert False   # This should not happen.
except LookupError as e:
  print(f'{type(e).__name__}: {e}')
LookupError: gradient registry has no entry for: AdjustContrastv2

Se você precisa diferenciar através deste op, você nem precisa de implementar o gradiente e registrá-lo (usando tf.RegisterGradient ) ou re-implementar a função usando outros ops.

Zeros em vez de nenhum

Em alguns casos, seria conveniente para obter 0 em vez de None para gradientes desconexas. Você pode decidir o que retornar quando você tem 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.], shape=(2,), dtype=float32)