¿No es otra explicación más de la regla de la cadena? Es un recorrido por el lado extraño de Autograd, donde los gradientes sirven física, no solo pesas
Originalmente escribí este tutorial durante el primer año de mi doctorado, mientras navegaba por las complejidades de los cálculos de gradiente en Pytorch. La mayor parte está claramente diseñada con la respaldo estándar en mente, y eso está bien, ya que eso es lo que la mayoría de la gente necesita.
Pero la red neuronal informada por física (PINN) es una bestia malhumorada y necesita un tipo diferente de lógica de gradiente. Pasé un tiempo alimentándolo y pensé que podría valer la pena compartir los hallazgos con la comunidad, especialmente con otros practicantes de Pinn, tal vez le ahorre a alguien algunos dolores de cabeza. Pero si nunca has oído hablar de pinns, ¡no te preocupes! Esta publicación sigue siendo para ti, especialmente si te gustan las cosas como gradientes de gradientes y todas esas cosas divertidas.
Términos básicos
Tensor En el mundo de las computadoras significa simplemente una matriz multidimensional, es decir, un montón de números indexados por uno o más enteros. Para ser precisos, también existen tensores de dimensión cero, que son solo números individuales. Algunas personas dicen que los tensores son una generalización de matrices a más de dos dimensiones.
Si ha estudiado la relatividad general antes, es posible que haya escuchado que los tensores matemáticos tienen cosas como índices covariantes y contravariantes. Pero olvídate de ello: en Pytorch Tensors son solo matrices multidimensionales. No hay delicadeza aquí.
Tensor de hojas es un tensor que es una hoja (en el sentido de una teoría de gráficos) de un gráfico de cálculo. Los veremos a continuación, por lo que esta definición tendrá un poco más de sentido.
El requires_grad La propiedad de un tensor le dice a Pytorch si debe recordar cómo se usa este tensor en cálculos adicionales. Por ahora, piense en tensores con requires_grad=True como variables, mientras que los tensores con requires_grad=False como constantes.
Tensores de hoja
Comencemos por crear algunos tensores y verificar sus propiedades requires_grad y is_leaf.
import torch
a = torch.tensor([3.], requires_grad=True)
b = a * a
c = torch.tensor([5.])
d = c * c
assert a.requires_grad is True and a.is_leaf is True
assert b.requires_grad is True and b.is_leaf is False
assert c.requires_grad is False and c.is_leaf is True
assert d.requires_grad is False and d.is_leaf is True # sic!
del a, b, c, d
a es una hoja como se esperaba, y b no es porque sea el resultado de una multiplicación. a está listo para requerir graduación, así que naturalmente b hereda esta propiedad.
c es una hoja obviamente, pero por qué d ¿Es una hoja? La razón d.is_leaf es verdadero proviene de una convención específica: todos los tensores con requires_grad establecido en falso se consideran tensores de hoja, según Documentación de Pytorch:
Todos los tensores que tienen
requires_gradque esFalseserán tensores de hoja por convención.
Mientras matemáticamente, d no es una hoja (ya que resulta de otra operación, c * c), el cálculo de gradiente nunca se extenderá más allá de él. En otras palabras, no habrá ningún derivado con respecto a c. Esto permite d ser tratado como una hoja.
En pocas palabras, en Pytorch, los tensores de la hoja son:
- Ingresado directamente (es decir, no calculado a partir de otros tensores) y tener
requires_grad=True. Ejemplo: pesos de la red neuronal que se inicializan al azar. - No requiera gradientes en absoluto, independientemente de si están directamente ingresados o calculados. A los ojos de Autograd, estas son solo constantes. Ejemplos:
- Cualquier datos de entrada de red neuronal,
- Una imagen de entrada después de la eliminación media u otras operaciones, que implica solo tensores no requeridos por los gradientes.
Un pequeño comentario para aquellos que quieren saber más. El requires_grad La propiedad se hereda como se ilustra aquí:
a = torch.tensor([5.], requires_grad=True)
b = torch.tensor([5.], requires_grad=True)
c = torch.tensor([5.], requires_grad=False)
d = torch.sin(a * b * c)
assert d.requires_grad == any((x.requires_grad for x in (a, b, c)))
Observación del código: todos los fragmentos de código deben ser autónomos, excepto las importaciones que incluyo solo cuando aparecen por primera vez. Los dejo dejo para minimizar el código de horario. Confío en que el lector pueda cuidarlos fácilmente.
Retención de graduación
Un problema separado es la retención de gradiente. Todos los nodos en el gráfico de cálculo, lo que significa que todos los tensores utilizados, tienen gradientes calculados si requieren graduados. Sin embargo, solo los tensores de hoja retienen estos gradientes. Esto tiene sentido porque los gradientes generalmente se usan para actualizar tensores, y solo los tensores de hoja están sujetos a actualizaciones durante el entrenamiento. Tensores no hojas, como b En el primer ejemplo, no se actualizan directamente; cambian como resultado de los cambios en apor lo que sus gradientes se pueden descartar. Sin embargo, hay escenarios, especialmente en las redes neuronales informadas por física (PINN), donde es posible que desee retener los gradientes de estos tensores intermedios. En tales casos, deberá marcar explícitamente los tensores no hojas para retener sus gradientes. Vamos a ver:
a = torch.tensor([3.], requires_grad=True)
b = a * a
b.backward()
assert a.grad is not None
assert b.grad is None # generates a warning
Probablemente hayas visto una advertencia:
UserWarning: The .grad attribute of a Tensor that is not a leaf Tensor is being
accessed. Its .grad attribute won't be populated during autograd.backward().
If you indeed want the .grad field to be populated for a non-leaf Tensor, use
.retain_grad() on the non-leaf Tensor. If you access the non-leaf Tensor by
mistake, make sure you access the leaf Tensor instead.
See github.com/pytorch/pytorch/pull/30531 for more informations.
(Triggered internally at aten\src\ATen/core/TensorBody.h:491.)
Así que solucionémoslo forzando b para retener su gradiente
a = torch.tensor([3.], requires_grad=True)
b = a * a
b.retain_grad() # <- the difference
b.backward()
assert a.grad is not None
assert b.grad is not None
Misterios de Grad
Ahora veamos al famoso graduado en sí. ¿Qué es? ¿Es un tensor? Si es así, ¿es un tensor de hoja? ¿Requiere o retiene Grad?
a = torch.tensor([3.], requires_grad=True)
b = a * a
b.retain_grad()
b.backward()
assert isinstance(a.grad, torch.Tensor)
assert a.grad.requires_grad is False and a.grad.retains_grad is False and a.grad.is_leaf is True
assert b.grad.requires_grad is False and b.grad.retains_grad is False and b.grad.is_leaf is True
Aparentemente:
– Grad en sí es un tensor,
– Grad es un tensor de hoja,
– Grad no requiere Grad.
¿Mantiene el graduado? Esta pregunta no tiene sentido porque no requiere graduarse en primer lugar. Volveremos a la cuestión de que el graduado sea un tensor de hoja en un segundo, pero ahora probaremos algunas cosas.
Múltiple al revés y retain_graph
¿Qué pasará cuando calculemos el mismo graduado dos veces?
a = torch.tensor([3.], requires_grad=True)
b = a * a
b.retain_grad()
b.backward()
try:
b.backward()
except RuntimeError:
"""
RuntimeError: Trying to backward through the graph a second time (or
directly access saved tensors after they have already been freed). Saved
intermediate values of the graph are freed when you call .backward() or
autograd.grad(). Specify retain_graph=True if you need to backward through
the graph a second time or if you need to access saved tensors after
calling backward.
"""
El mensaje de error lo explica todo. Esto debería funcionar:
a = torch.tensor([3.], requires_grad=True)
b = a * a
b.retain_grad()
b.backward(retain_graph=True)
print(a.grad) # prints tensor([6.])
b.backward(retain_graph=True)
print(a.grad) # prints tensor([12.])
b.backward(retain_graph=False)
print(a.grad) # prints tensor([18.])
# b.backward(retain_graph=False) # <- here we would get an error, because in
# the previous call we did not retain the graph.
Nota del lado (pero importante): también puede observar cómo se acumula el gradiente en a: Con cada iteración se agrega.
Poderoso create_graph argumento
¿Cómo hacer que el graduado requiere graduarse?
a = torch.tensor([5.], requires_grad=True)
b = a * a
b.retain_grad()
b.backward(create_graph=True)
# Here an interesting thing happens: now a.grad will require grad!
assert a.grad.requires_grad is True
assert a.grad.is_leaf is False
# On the other hand, the grad of b does not require grad, as previously.
assert b.grad.requires_grad is False
assert b.grad.is_leaf is True
Lo anterior es muy útil: a.grad que matemáticamente es \[\frac{\partial b}{\partial a}\] ya no es una constante (hoja), sino un miembro regular del gráfico de cálculo que se puede usar. Usaremos ese hecho en la Parte 2.
Por qué el b.grad ¿No requiere graduación? Porque derivado de b con respecto a b es simplemente 1.
Si el backward Se siente contradictorio para ti ahora, no te preocupes. Pronto cambiaremos a otro método llamado Nomen Omen grad Eso permite elegir con precisión los ingredientes de los derivados. Antes, dos notas laterales:
Nota al margen 1: Si te configuras create_graph a verdad, también se establece retain_graph a verdadero (si no se establece explícitamente). En el código de Pytorch se ve exactamente como
este:
if retain_graph is None:
retain_graph = create_graph
Nota al margen 2: Probablemente viste una advertencia como esta:
UserWarning: Using backward() with create_graph=True will create a reference
cycle between the parameter and its gradient which can cause a memory leak.
We recommend using autograd.grad when creating the graph to avoid this. If
you have to use this function, make sure to reset the .grad fields of your
parameters to None after use to break the cycle and avoid the leak.
(Triggered internally at C:\cb\pytorch_1000000000000\work\torch\csrc\autograd\engine.cpp:1156.)
Variable._execution_engine.run_backward( # Calls into the C++ engine to
run the backward pass
Y seguiremos el consejo y el uso autograd.grad ahora.
Tomando derivados con autograd.grad función
Ahora nos mudemos del nivel alto de alguna manera .backward() Método a nivel inferior grad Método que calcula explícitamente la derivada de un tensor con respecto a otro.
from torch.autograd import grad
a = torch.tensor([3.], requires_grad=True)
b = a * a * a
db_da = grad(b, a, create_graph=True)[0]
assert db_da.requires_grad is True
Del mismo modo, como con backwardel derivado de b con respecto a a puede tratarse en función y diferenciarse aún más. Entonces en otras palabras, el create_graph La bandera puede entenderse como: Al calcular los gradientes, mantenga el historial de cómo se calcularon, para que podamos tratarlos como tensores no hojas que requieren graduados y usar más.
En particular, podemos calcular la derivada de segundo orden:
d2b_da2 = grad(db_da, a, create_graph=True)[0]
# Side note: the grad function returns a tuple and the first element of it is what we need.
assert d2b_da2.item() == 18
assert d2b_da2.requires_grad is True
Como se dijo antes: esta es en realidad la propiedad clave que nos permite hacer Pinn con Pytorch.
Concluir
La mayoría de los tutoriales sobre los gradientes de Pytorch se centran en la backpropagation en el aprendizaje clásico supervisado. Este exploró una perspectiva diferente: una formada por las necesidades de los pinns y otras bestias hambrientas de gradiente.
Aprendimos qué hojas hay en la jungla de Pytorch, por qué los gradientes se conservan de forma predeterminada solo para nodos de hoja y cómo retenerlas cuando sea necesario para otros tensores. Vimos como create_graph Convierte los gradientes en ciudadanos diferenciables del mundo de Autograd.
Pero todavía hay muchas cosas para descubrir, especialmente por qué los gradientes de las funciones no escalares requieren un cuidado adicional, cómo calcular las derivadas de segundo orden sin usar toda su RAM, y por qué cortar su tensor de entrada es una mala idea cuando necesita un Elemento gradiente.
Así que nos encontremos en la Parte 2, donde echaremos un vistazo más de cerca a grad 👋