¡El Día de la Comunidad de ML es el 9 de noviembre! Únase a nosotros para recibir actualizaciones de TensorFlow, JAX, y más Más información

Introducción a gradientes y diferenciación automática.

Ver en TensorFlow.org Ejecutar en Google Colab Ver fuente en GitHub Descargar cuaderno

Diferenciación y gradientes automáticos

Diferenciación automática es útil para implementar algoritmos de aprendizaje de máquina, tales como propagación hacia atrás para el entrenamiento de las redes neuronales.

En esta guía, explorará maneras de calcular los gradientes con TensorFlow, especialmente en la ejecución ansiosos .

Configuración

import numpy as np
import matplotlib.pyplot as plt

import tensorflow as tf

Gradientes de computación

Para diferenciar de forma automática, TensorFlow necesita recordar qué suceden las operaciones en qué orden durante el pase hacia adelante. Luego, durante la pasada hacia atrás, TensorFlow atraviesa esta lista de operaciones en orden inverso a los gradientes de cómputo.

Cintas de degradado

TensorFlow proporciona la tf.GradientTape API para la diferenciación automática; es decir, el cálculo de la pendiente de un cálculo con respecto a algunos de los insumos, por lo general tf.Variable s. TensorFlow "graba" operaciones relevantes ejecutadas dentro del contexto de una tf.GradientTape sobre una "cinta". TensorFlow continuación, utiliza esa cinta para calcular los gradientes de un cálculo "grabado" usando diferenciación modo inverso .

A continuación, se muestra un ejemplo sencillo:

x = tf.Variable(3.0)

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

Una vez que ha grabado algunas operaciones, utilice GradientTape.gradient(target, sources) para calcular el gradiente de algún objetivo (a menudo una pérdida) en relación con alguna fuente (a menudo las variables del modelo):

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

El ejemplo anterior utiliza escalares, pero tf.GradientTape funciona tan fácilmente sobre cualquier 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 obtener el gradiente de loss con respecto a ambas variables, se puede pasar tanto como fuentes para el gradient método. La cinta es flexible en cuanto a cómo se transmiten las fuentes y aceptará cualquier combinación anidada de listas o diccionarios y devolver el gradiente estructurado de la misma manera (ver tf.nest ).

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

El gradiente con respecto a cada fuente tiene la forma de la fuente:

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

Aquí está el cálculo del gradiente nuevamente, esta vez pasando un diccionario de variables:

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)>

Degradados con respecto a un modelo

Es común para recoger de tf.Variables en un tf.Module o una de sus subclases ( layers.Layer , keras.Model ) para los puntos de control y exportación .

En la mayoría de los casos, querrá calcular gradientes con respecto a las variables entrenables de un modelo. Dado que todas las subclases de tf.Module agregan sus variables en el Module.trainable_variables propiedad, se puede calcular estos gradientes en unas pocas líneas 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 lo que mira la cinta

El comportamiento por defecto es registrar todas las operaciones después de acceder a un entrenable tf.Variable . Las razones de esto son:

  • La cinta necesita saber qué operaciones grabar en la pasada hacia adelante para calcular los gradientes en la pasada hacia atrás.
  • La cinta contiene referencias a salidas intermedias, por lo que no desea grabar operaciones innecesarias.
  • El caso de uso más común implica calcular el gradiente de una pérdida con respecto a todas las variables entrenables de un modelo.

Por ejemplo, la siguiente falla para calcular un gradiente porque el tf.Tensor no se "vio" por defecto, y el tf.Variable no es entrenable:

# 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

Se pueden listar las variables siendo vigilados por la cinta utilizando el GradientTape.watched_variables método:

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

tf.GradientTape proporciona enlaces que le dan al usuario el control sobre lo que está o no está vigilado.

Para gradientes de registro con respecto a un tf.Tensor , es necesario llamar a 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 el contrario, para desactivar el comportamiento predeterminado de ver todos los tf.Variables , juego de watch_accessed_variables=False cuando se crea la cinta de gradiente. Este cálculo usa dos variables, pero solo conecta el gradiente para una de las variables:

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 no fue llamado en x0 , no gradiente se calcula con respecto a la misma:

# 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 intermedios

También puede solicitar los gradientes de la salida con respecto a los valores intermedios calculados dentro del 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 defecto, los recursos en poder de un GradientTape son liberados tan pronto como el GradientTape.gradient método se llama. Para calcular múltiples gradientes sobre el mismo cálculo, crear una cinta de gradiente con persistent=True . Esto permite que múltiples llamadas a la gradient método como recursos son liberados cuando el objeto de la cinta es basura recogida. Por ejemplo:

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 el rendimiento

  • Hay una pequeña sobrecarga asociada con la realización de operaciones dentro de un contexto de cinta de degradado. Para la ejecución más ávida, esto no será un costo notable, pero aún debe usar el contexto de cinta alrededor de las áreas solo donde sea necesario.

  • Las cintas de degradado utilizan la memoria para almacenar resultados intermedios, incluidas las entradas y salidas, para su uso durante la pasada hacia atrás.

    Para una mayor eficacia, algunas operaciones (como ReLU ) no necesitan para mantener sus resultados intermedios y se podan durante el pase hacia adelante. Sin embargo, si se utiliza persistent=True en la cinta, nada se descarta y el uso de memoria máximo será mayor.

Gradientes de objetivos no escalares

Un gradiente es fundamentalmente una operación sobre un 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

Por lo tanto, si solicita el gradiente de varios objetivos, el resultado para cada fuente es:

  • El gradiente de la suma de los objetivos, o equivalentemente
  • La suma de los gradientes de cada objetivo.
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

Del mismo modo, si los objetivos no son escalares, se calcula el gradiente de la suma:

x = tf.Variable(2.)

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

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

Esto hace que sea sencillo tomar el gradiente de la suma de una colección de pérdidas, o el gradiente de la suma de un cálculo de pérdidas por elementos.

Si necesita un gradiente separado para cada elemento, consulte jacobianos .

En algunos casos, puede omitir el jacobiano. Para un cálculo por elementos, el gradiente de la suma da la derivada de cada elemento con respecto a su elemento de entrada, ya que cada elemento es independiente:

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

Flujo de control

Debido a que una cinta de gradiente registra las operaciones de medida que se ejecutan, flujo de control Python se maneja de forma natural (por ejemplo, if y while los estados).

Aquí una variable diferente se utiliza en cada rama de un if . El gradiente solo se conecta a la variable que se utilizó:

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

Solo recuerde que las declaraciones de control en sí mismas no son diferenciables, por lo que son invisibles para los optimizadores basados ​​en gradientes.

Dependiendo del valor de x en el ejemplo anterior, los registros, ya sea de cinta result = v0 o result = v1**2 . El gradiente con respecto a x es siempre None .

dx = tape.gradient(result, x)

print(dx)
None

Conseguir un gradiente de None

Cuando un objetivo no está conectado a una fuente obtendrá un 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

Aquí z es, obviamente, no está conectado a x , pero hay varias maneras menos evidentes que un gradiente se puede desconectar.

1. Reemplazó una variable con un tensor.

En el apartado de "controlar lo que ve la cinta" viste que la cinta velará automáticamente un tf.Variable pero no un tf.Tensor .

Un error común es sustituir inadvertidamente un tf.Variable con un tf.Tensor , en lugar de utilizar Variable.assign para actualizar el tf.Variable . Aquí hay un ejemplo:

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. Hizo cálculos fuera de TensorFlow

La cinta no puede registrar la ruta del gradiente si el cálculo sale de TensorFlow. Por ejemplo:

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. Tomó degradados a través de un número entero o una cadena.

Los enteros y las cadenas no son diferenciables. Si una ruta de cálculo utiliza estos tipos de datos, no habrá gradiente.

Nadie espera que las cadenas sean diferenciables, pero es fácil crear accidentalmente un int constante o variable si no se especifica el 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

TensorFlow no transmite automáticamente entre tipos, por lo que, en la práctica, a menudo obtendrá un error de tipo en lugar de un degradado faltante.

4. Tomó degradados a través de un objeto con estado.

El estado detiene los gradientes. Cuando lee de un objeto con estado, la cinta solo puede observar el estado actual, no el historial que conduce a él.

Un tf.Tensor es inmutable. No puedes cambiar un tensor una vez creado. Tiene un valor, pero ningún estado. Todas las operaciones que se indican hasta ahora son también sin estado: la salida de un tf.matmul solo depende de sus entradas.

A tf.Variable tiene interna estado su valor. Cuando usa la variable, se lee el estado. Es normal calcular un gradiente con respecto a una variable, pero el estado de la variable bloquea los cálculos de gradiente para que no retrocedan más. Por ejemplo:

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

Del mismo modo, tf.data.Dataset iteradores y tf.queue s son de estado, y no se detendrán todos los gradientes de tensores que pasan a través de ellos.

Sin gradiente registrado

Algunos tf.Operation s están registrados como no diferenciable y volverán None . Otros no tienen gradiente registrado.

El tf.raw_ops página muestra qué operaciones de bajo nivel tienen gradientes registrados.

Si intenta tener un gradiente a través de un op flotante que no tiene gradiente registrado la cinta generará un error en lugar de regresar en silencio None . De esta forma sabrá que algo salió mal.

Por ejemplo, el tf.image.adjust_contrast función envuelve raw_ops.AdjustContrastv2 , que podría tener un gradiente pero el gradiente no se implementa:

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

Si necesita diferenciarse a través de esta operación, ya sea que usted necesita para poner en práctica el gradiente y registrarlo (usando tf.RegisterGradient ) o volver a implementar la función usando otras operaciones.

Ceros en lugar de Ninguno

En algunos casos, sería conveniente para obtener 0 en vez de None de los gradientes no conectados. Puede decidir qué volver cuando se tiene gradientes no conectados utilizando el 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)