Pytorch, ¿cuáles son los argumentos del gradiente?

112

Estoy leyendo la documentación de PyTorch y encontré un ejemplo donde escriben

gradients = torch.FloatTensor([0.1, 1.0, 0.0001])
y.backward(gradients)
print(x.grad)

donde x era una variable inicial, a partir de la cual se construyó y (un 3-vector). La pregunta es, ¿cuáles son los argumentos 0.1, 1.0 y 0.0001 del tensor de gradientes? La documentación no es muy clara al respecto.

Qubix
fuente

Respuestas:

15

El código original ya no lo he encontrado en el sitio web de PyTorch.

gradients = torch.FloatTensor([0.1, 1.0, 0.0001])
y.backward(gradients)
print(x.grad)

El problema con el código anterior es que no hay una función basada en qué calcular los gradientes. Esto significa que no sabemos cuántos parámetros (argumentos toma la función) y la dimensión de los parámetros.

Para entender completamente esto, creé un ejemplo cercano al original:

Ejemplo 1:

a = torch.tensor([1.0, 2.0, 3.0], requires_grad = True)
b = torch.tensor([3.0, 4.0, 5.0], requires_grad = True)
c = torch.tensor([6.0, 7.0, 8.0], requires_grad = True)

y=3*a + 2*b*b + torch.log(c)    
gradients = torch.FloatTensor([0.1, 1.0, 0.0001])
y.backward(gradients,retain_graph=True)    

print(a.grad) # tensor([3.0000e-01, 3.0000e+00, 3.0000e-04])
print(b.grad) # tensor([1.2000e+00, 1.6000e+01, 2.0000e-03])
print(c.grad) # tensor([1.6667e-02, 1.4286e-01, 1.2500e-05])

Supuse que nuestra función es y=3*a + 2*b*b + torch.log(c)y los parámetros son tensores con tres elementos en su interior.

Puedes pensar en algo así gradients = torch.FloatTensor([0.1, 1.0, 0.0001])como en el acumulador.

Como puede escuchar, el cálculo del sistema de autogrado PyTorch es equivalente al producto jacobiano.

Jacobiano

En caso de que tenga una función, como hicimos nosotros:

y=3*a + 2*b*b + torch.log(c)

Jacobian sería [3, 4*b, 1/c]. Sin embargo, este jacobiano no es la forma en que PyTorch está haciendo las cosas para calcular los gradientes en cierto punto.

PyTorch utiliza la diferenciación automática (AD) del modo de avance y retroceso en conjunto.

No hay matemáticas simbólicas involucradas ni diferenciación numérica.

La diferenciación numérica sería calcular δy/δb, para b=1y b=1+εdonde ε es pequeño.

Si no usa degradados en y.backward():

Ejemplo 2

a = torch.tensor(0.1, requires_grad = True)
b = torch.tensor(1.0, requires_grad = True)
c = torch.tensor(0.1, requires_grad = True)
y=3*a + 2*b*b + torch.log(c)

y.backward()

print(a.grad) # tensor(3.)
print(b.grad) # tensor(4.)
print(c.grad) # tensor(10.)

Se le sencillo obtener el resultado en un punto, en base a la configuración de las a, b, ctensores inicialmente.

Tener cuidado de cómo usted hace funcionar su a, b, c:

Ejemplo 3:

a = torch.empty(1, requires_grad = True, pin_memory=True)
b = torch.empty(1, requires_grad = True, pin_memory=True)
c = torch.empty(1, requires_grad = True, pin_memory=True)

y=3*a + 2*b*b + torch.log(c)

gradients = torch.FloatTensor([0.1, 1.0, 0.0001])
y.backward(gradients)

print(a.grad) # tensor([3.3003])
print(b.grad) # tensor([0.])
print(c.grad) # tensor([inf])

Si usa torch.empty()y no usa, pin_memory=Truees posible que obtenga resultados diferentes cada vez.

Además, los gradientes de notas son como acumuladores, así que ajústelos a cero cuando sea necesario.

Ejemplo 4:

a = torch.tensor(1.0, requires_grad = True)
b = torch.tensor(1.0, requires_grad = True)
c = torch.tensor(1.0, requires_grad = True)
y=3*a + 2*b*b + torch.log(c)

y.backward(retain_graph=True)
y.backward()

print(a.grad) # tensor(6.)
print(b.grad) # tensor(8.)
print(c.grad) # tensor(2.)

Por último, algunos consejos sobre los términos que usa PyTorch:

PyTorch crea un gráfico computacional dinámico al calcular los gradientes en el pase hacia adelante. Esto se parece mucho a un árbol.

Por lo tanto, a menudo escuchará que las hojas de este árbol son tensores de entrada y la raíz es un tensor de salida .

Los gradientes se calculan trazando el gráfico desde la raíz hasta la hoja y multiplicando cada gradiente en la forma usando la regla de la cadena . Esta multiplicación ocurre en el pase hacia atrás.

prosti
fuente
¡Gran respuesta! Sin embargo, no creo que Pytorch haga diferenciación numérica ("Para la función anterior PyTorch haría, por ejemplo, δy / δb, para b = 1 y b = 1 + ε donde ε es pequeño. Así que no hay nada como matemáticas simbólicas involucradas. ") - Creo que hace diferenciación automática.
max_max_mir
Sí, usa AD, o diferenciación automática, luego investigué AD más a fondo como en este PDF , sin embargo, cuando establecí esta respuesta no estaba muy informado.
prosti
Por ejemplo, el ejemplo 2 da RuntimeError: Falta de coincidencia en la forma: grad_output [0] tiene la forma de antorcha.Size ([3]) y la salida [0] tiene la forma de antorcha.Size ([]).
Andreas K.
@AndreasK., Tenías razón, PyTorch introdujo recientemente tensores de tamaño cero y esto tuvo el impacto en mis ejemplos anteriores. Eliminado porque estos ejemplos no eran cruciales.
prosti
100

Explicación

En el caso de las redes neuronales, lo usamos losspara evaluar qué tan bien la red ha aprendido a clasificar la imagen de entrada (u otras tareas). El losstérmino suele ser un valor escalar. Para actualizar los parámetros de la red, necesitamos calcular el gradiente de losswrt a los parámetros, que leaf nodeen realidad está en el gráfico de cálculo (por cierto, estos parámetros son principalmente el peso y el sesgo de varias capas, como Convolución, Lineal y pronto).

De acuerdo con la regla de la cadena, para calcular el gradiente de losswrt a un nodo hoja, podemos calcular la derivada de losswrt alguna variable intermedia y el gradiente de la variable intermedia wrt a la variable hoja, hacer un producto escalar y sumar todos estos.

Los gradientargumentos de una Variable's backward()método se utiliza para calcular una suma ponderada de cada elemento de una variable wrt la Variable hoja . Este peso es simplemente la derivada de la losswrt final de cada elemento de la variable intermedia.

Un ejemplo concreto

Tomemos un ejemplo concreto y sencillo para entender esto.

from torch.autograd import Variable
import torch
x = Variable(torch.FloatTensor([[1, 2, 3, 4]]), requires_grad=True)
z = 2*x
loss = z.sum(dim=1)

# do backward for first element of z
z.backward(torch.FloatTensor([[1, 0, 0, 0]]), retain_graph=True)
print(x.grad.data)
x.grad.data.zero_() #remove gradient in x.grad, or it will be accumulated

# do backward for second element of z
z.backward(torch.FloatTensor([[0, 1, 0, 0]]), retain_graph=True)
print(x.grad.data)
x.grad.data.zero_()

# do backward for all elements of z, with weight equal to the derivative of
# loss w.r.t z_1, z_2, z_3 and z_4
z.backward(torch.FloatTensor([[1, 1, 1, 1]]), retain_graph=True)
print(x.grad.data)
x.grad.data.zero_()

# or we can directly backprop using loss
loss.backward() # equivalent to loss.backward(torch.FloatTensor([1.0]))
print(x.grad.data)    

En el ejemplo anterior, el resultado de first printes

2 0 0 0
[antorcha.FloatTensor de tamaño 1x4]

que es exactamente la derivada de z_1 wrt ax.

El resultado del segundo printes:

0 2 0 0
[antorcha.FloatTensor de tamaño 1x4]

que es la derivada de z_2 wrt ax.

Ahora, si usa un peso de [1, 1, 1, 1] para calcular la derivada de z wrt ax, el resultado es 1*dz_1/dx + 1*dz_2/dx + 1*dz_3/dx + 1*dz_4/dx. Entonces, no es sorprendente que la salida de 3rd printsea:

2 2 2 2
[antorcha.FloatTensor de tamaño 1x4]

Cabe señalar que el vector de peso [1, 1, 1, 1] es exactamente una derivada de losswrt a z_1, z_2, z_3 y z_4. La derivada de losswrt to xse calcula como:

d(loss)/dx = d(loss)/dz_1 * dz_1/dx + d(loss)/dz_2 * dz_2/dx + d(loss)/dz_3 * dz_3/dx + d(loss)/dz_4 * dz_4/dx

Entonces la salida del 4to printes el mismo que el 3ro print:

2 2 2 2
[antorcha.FloatTensor de tamaño 1x4]

jdhao
fuente
1
solo una duda, ¿por qué estamos calculando x.grad.data para gradientes de pérdida o z.
Priyank Pathak
7
Tal vez me perdí algo, pero siento que la documentación oficial realmente podría haber explicado gradientmejor el argumento. Gracias por tu respuesta.
protagonista
3
@jdhao "Cabe señalar que el vector de peso [1, 1, 1, 1]es exactamente derivado de losswrt a z_1, z_2, z_3y z_4." Creo que esta afirmación es realmente clave para la respuesta. Al mirar el código del OP, un gran signo de interrogación es de dónde provienen estos números arbitrarios (mágicos) para el gradiente. En su ejemplo concreto, creo que sería muy útil señalar la relación entre el [1, 0, 0 0]tensor, por ejemplo, y la lossfunción de inmediato para que pueda ver que los valores no son arbitrarios en este ejemplo.
a_guest
1
@smwikipedia, eso no es cierto. Si nos expandimos loss = z.sum(dim=1), se convertirá en loss = z_1 + z_2 + z_3 + z_4. Si conoce el cálculo simple, sabrá que la derivada de losswrt to z_1, z_2, z_3, z_4es [1, 1, 1, 1].
jdhao
1
te quiero. ¡Resolví mi duda!
Black Jack 21 de
45

Normalmente, su gráfico computacional tiene una salida escalar dice loss. Luego puede calcular el gradiente de losswrt los pesos ( w) por loss.backward(). Donde el argumento predeterminado de backward()es 1.0.

Si su salida tiene varios valores (p loss=[loss1, loss2, loss3]. Ej. ), Puede calcular los gradientes de pérdida con los pesos loss.backward(torch.FloatTensor([1.0, 1.0, 1.0])).

Además, si desea agregar pesos o importancias a diferentes pérdidas, puede usar loss.backward(torch.FloatTensor([-0.1, 1.0, 0.0001])).

Esto significa calcular -0.1*d(loss1)/dw, d(loss2)/dw, 0.0001*d(loss3)/dwsimultáneamente.

Gu Wang
fuente
1
"Si desea agregar peso o importancia a diferentes pérdidas, puede usar loss.backward (torch.FloatTensor ([- 0.1, 1.0, 0.0001]))". -> Esto es cierto pero algo engañoso porque la razón principal por la que pasamos grad_tensorses no para pesarlos de manera diferente, sino que son gradientes con cada elemento de los tensores correspondientes.
Aerin
27

Aquí, la salida de forward (), es decir, y es un vector 3.

Los tres valores son los gradientes a la salida de la red. Por lo general, se establecen en 1.0 si y es el resultado final, pero también pueden tener otros valores, especialmente si y es parte de una red más grande.

Por ej. si x es la entrada, y = [y1, y2, y3] es una salida intermedia que se utiliza para calcular la salida final z,

Luego,

dz/dx = dz/dy1 * dy1/dx + dz/dy2 * dy2/dx + dz/dy3 * dy3/dx

Entonces, aquí, los tres valores para retroceder son

[dz/dy1, dz/dy2, dz/dy3]

y luego hacia atrás () calcula dz / dx

greenberet123
fuente
5
Gracias por la respuesta, pero ¿cómo es esto útil en la práctica? Quiero decir, ¿dónde necesitamos [dz / dy1, dz / dy2, dz / dy3] que no sea backprop de codificación rígida?
hola15
¿Es correcto decir que el argumento de gradiente proporcionado es el gradiente calculado en la última parte de la red?
Khanetor