Backpropagation: la derivación completa
35 min read
Todo lo que hace backpropagation lo hace con la regla de la cadena, y la regla de la cadena ya está derivada. La lección anterior la dejó en su forma vectorial —jacobianas que se multiplican, un gradiente que entra por un extremo y sale convertido en el de la capa anterior— y con ella la razón por la que así, tal cual, no se puede ejecutar: aplicarla parámetro a parámetro cuesta un recorrido completo de la red por cada número que haya que ajustar. Lo que falta no es matemática nueva. Es un orden.
Y el orden se ve mejor pequeño que grande. Toma la red más pequeña que todavía tiene una capa oculta: dos entradas, dos neuronas ocultas y una de salida. Tiene nueve parámetros —cuatro pesos de entrada, dos sesgos ocultos, dos pesos de salida y un sesgo—, así que hay nueve derivadas que calcular. Detrás de las nueve hay tres números, uno por preactivación de la red. Calculados esos tres, cada una de las nueve derivadas es un producto de dos factores, y ninguno de los dos hay que ir a buscarlo lejos.
Un número por preactivación, y las derivadas que cuelgan de él
Fijemos una capa y un solo ejemplo. Derivar respecto de un peso ya está resuelto; lo que hay que decidir es respecto de qué conviene derivar primero, y la respuesta es la preactivación. Llamemos error de la capa , y , al gradiente de la pérdida de ese ejemplo respecto de :
una coordenada por neurona de la capa, y en columna, que es la transpuesta de la jacobiana de una fila con la que la lección anterior escribía el gradiente de un escalar. Es y no : la pérdida de un ejemplo, como en todo el bloque desde la lección sobre funciones de pérdida.
Con ese vector en la mano, los parámetros de la capa salen sin recorrer nada. Su preactivación es , y coordenada a coordenada, . Mira dónde aparece un peso concreto: está en una sola de las coordenadas, la , así que de la suma sobre caminos de la lección anterior sobrevive un único sumando:
porque vale cuando y cero en los otros casos, y la derivada respecto del sesgo es la misma cuenta con un en lugar de una activación. Apiladas, las casillas son un producto exterior:
Una columna de por una fila de , que es exactamente la forma de —y esa coincidencia es la comprobación más barata que existe: si el producto exterior no sale con la forma de la matriz que deriva, hay algo transpuesto.
Cuenta ahora lo que acaba de pasar, porque es el argumento entero. La capa tiene parámetros, y sus derivadas salen todas de números: las coordenadas de y las de , que la pasada hacia adelante ya calculó. En la primera capa de la red de la lección anterior eso son treinta y seis derivadas leídas de doce números. Queda una sola pregunta, y ocupa el resto de la lección: de dónde sale .
De dónde sale ese número: la recurrencia hacia atrás
De la capa de al lado, y por eso el recorrido va del final al principio.
Empieza por el final, donde no hay nada que hacer. La última capa entrega , el softmax lo convierte en y la entropía cruzada lo compara con la etiqueta; la lección sobre funciones de pérdida derivó ese par entero y de las dos cosas juntas salió
Nada de lo que sigue depende de que sea ésa. Cambia la pérdida o la activación de salida y ese vector cambia; el resto del algoritmo no se entera, porque sólo lo recibe. La red del explorable de arriba es el ejemplo: tiene una sigmoide en la salida y la pérdida —el es la convención que la lección sobre funciones de pérdida dijo no adoptar, y aquí está puesto para que los números salgan cortos—, con lo que su capa de salida entra con y no con : el factor de más es la sigmoide, que aquí no se cancela con nada. Todo lo demás es idéntico.
Ahora el paso hacia atrás. Sea . La preactivación de la capa siguiente se construye a partir de la de ésta en dos movimientos, , y la lección anterior tiene la jacobiana de los dos: la de una aplicación afín es su propia matriz, y la de una aplicada coordenada a coordenada es diagonal. Componiéndolas,
y esa misma lección dijo qué hacer con una jacobiana cuando lo que se propaga es el gradiente de un escalar: se transpone y se le multiplica el gradiente por la derecha. Como es simétrica y transponer un producto le da la vuelta al orden de los factores,
que escrito con el producto de Hadamard, para no construir la diagonal, es la forma que se implementa:
Léela en dos movimientos, porque hacen dos cosas distintas. El primero, , transporta: reparte el error de las neuronas de arriba entre las de abajo, a cada una según el peso con que las alimentaba en la ida. Son los mismos pesos, sin ningún parámetro nuevo. El segundo enmascara: multiplica lo que le llega a cada neurona por su propia pendiente. Con ReLU (rectified linear unit) esa pendiente vale o , así que una neurona con la preactivación negativa recibe un cero y corta ahí todo lo que venía de arriba.
La misma recurrencia, índice a índice
Sin matrices por medio. La coordenada de es la derivada de respecto de , y llega a la pérdida a través de las preactivaciones de la capa siguiente: es la suma sobre caminos de la lección anterior, con caminos.
donde la segunda igualdad no hace nada más que nombrar el primer factor. El segundo sale de escribir la capa siguiente con sus índices, . Al derivar respecto de sobrevive un solo sumando de esa suma, el de , y la activación aporta su derivada:
Ese último factor no depende de , así que sale fuera de la suma:
Y ya está escrita la fórmula de arriba. La suma recorre la columna de , que es la fila de su transpuesta, o sea la coordenada del transporte; el factor de fuera es la coordenada del enmascarado. Las dos formas dicen lo mismo, y la de las matrices sólo tiene de más que el ordenador la ejecuta sin escribir ni un índice.
El algoritmo entero, escrito para dos capas ocultas
Con cabe entero y sin puntos suspensivos. Hacia adelante, que es la lección sobre el forward pass:
guardando por el camino los y los . Hacia atrás, del final al principio:
y de esos tres vectores salen los seis gradientes, y para . Eso es backpropagation completo. Tres errores, seis gradientes, y no hay un paso más.
Lo que cuesta se lee en las formas. La vuelta hace un producto de matriz por vector en cada capa, con las mismas matrices de la ida: el mismo orden de operaciones, una sola vez, sea cual sea . Sondear los parámetros de uno en uno cuesta dos pasadas hacia adelante por parámetro, y el número de parámetros no lo acota nada. En la red de abajo son parámetros, pasadas contra una; con el vocabulario de veinte mil entradas de la lección sobre el forward pass la proporción no cambia de forma, sólo de tamaño.
Hay un precio y conviene decirlo, porque no es el tiempo. La vuelta necesita los y los de todas las capas, así que hay que conservarlos mientras se calcula hacia adelante. Una red que sólo predice puede tirar cada capa en cuanto la ha usado; una que va a entrenar, no, y esa memoria crece con la profundidad y con el batch.
Del explorable a una red con dos capas ocultas
La primera celda es la red del explorable con sus mismos pesos, para que puedas comparar cifra a cifra. Ejecútala y ve leyendo la salida contra los pasos de arriba: los tres errores primero, los nueve gradientes después. La última línea sondea los nueve parámetros de uno en uno, que es la comprobación de la lección anterior aplicada aquí.
# La red del explorable: dos entradas, dos neuronas ocultas y una de salida, con
# sigmoide en las dos capas y la pérdida ½(ŷ − y)². Los pesos son los suyos.
x, y = np.array([1.0, 0.5]), 1.0
W1 = np.array([[0.5, -0.3], [0.8, 0.2]])
b1 = np.array([0.1, -0.2])
W2 = np.array([[0.7, -0.6]]) # una fila: la capa de salida tiene una neurona
b2 = np.array([0.15])
sigmoide = lambda z: 1.0 / (1.0 + np.exp(-z))
def forward():
h1 = sigmoide(W1 @ x + b1)
y_hat = sigmoide(W2 @ h1 + b2)[0]
return h1, y_hat, 0.5 * (y_hat - y) ** 2
h1, y_hat, perdida = forward()
print("h(1) =", np.round(h1, 3), " ŷ = %.3f ℓ = %.3f" % (y_hat, perdida))
# Hacia atrás. Un δ por preactivación: tres números, y ahí se acaba la cadena.
d2 = np.array([(y_hat - y) * y_hat * (1.0 - y_hat)])
d1 = (W2.T @ d2) * h1 * (1.0 - h1) # transporta con la transpuesta, y enmascara
print("∂ℓ/∂ŷ = %+.3f ∂ŷ/∂z(2)₁ = %.3f δ(2)₁ = %+.3f"
% (y_hat - y, y_hat * (1.0 - y_hat), d2[0]))
print("∂ℓ/∂h(1) =", np.round(W2.T @ d2, 3),
" φ'(z(1)) =", np.round(h1 * (1.0 - h1), 3),
" δ(1) =", np.round(d1, 3))
# Los nueve gradientes, leídos de esos tres números.
grad = {"W1": np.outer(d1, x), "b1": d1, "W2": np.outer(d2, h1), "b2": d2}
print("∇W(1) =", np.round(grad["W1"], 3).tolist())
print("∇W(2) =", np.round(grad["W2"], 3).tolist(), " ∂ℓ/∂b(2)₁ = %+.3f" % d2[0])
# Y la comprobación: sondear los nueve parámetros de uno en uno.
peor = 0.0
for nombre, P in [("W1", W1), ("b1", b1), ("W2", W2), ("b2", b2)]:
for k in np.ndindex(P.shape):
viejo = P[k]
P[k] = viejo + 1e-6
mas = forward()[2]
P[k] = viejo - 1e-6
menos = forward()[2]
P[k] = viejo
peor = max(peor, abs((mas - menos) / 2e-6 - grad[nombre][k]))
print("nueve parámetros, mayor diferencia con el sondeo: %.1e" % peor)
La primera ejecución descarga el intérprete de Python (~15 MB). Después queda en la caché del navegador.
Los números son los del explorable a la tercera cifra, y los nombres también, así que puedes leer las dos cosas en paralelo. La tercera línea enseña los dos movimientos por separado. El transporte deja : un solo error, , repartido entre las dos neuronas ocultas según sus pesos y , y por eso la segunda coordenada sale con el signo cambiado. El enmascarado son los de al lado, que es la derivada de la sigmoide en cada preactivación.
Y mira , que vale arriba y abajo. Su primera columna es tal cual, porque ; la segunda es a la mitad, porque . Eso es un producto exterior visto de cerca, y es la razón de que nueve derivadas quepan en tres números y dos entradas.
La segunda celda es la red de reseñas con una capa oculta más, que es donde la recurrencia se aplica dos veces en lugar de una: ocho entradas, cuatro neuronas ocultas, tres, y tres salidas con softmax. Los pesos los he elegido yo para que las preactivaciones salgan redondas. Ejecútala y mira dos cosas: dónde aparecen ceros, y las dos últimas líneas.
# La red de la lección anterior con una capa oculta más: 8 -> 4 -> 3 -> 3.
x = np.array([0., 0., 1., 2., 0., 0., 1., 1.]) # «la película es divertida y la recomiendo»
y = np.array([1., 0., 0.]) # etiqueta: positiva, de tres clases
W1 = np.array([[-1., 1., 1., 0., -1., -1., 0., 1.], # los pesos los he puesto yo a mano:
[0., 0., -1., 0.5, 0., 0., 1., 0.], # no hay un solo número al azar
[0., 0.5, 0., 0.5, 0., 0., 0.5, 0.5],
[1., -1., 0., 0.5, 1., 1., 0.5, -1.]])
W2 = np.array([[0.5, 0., 0., -1.], [-1., 0.5, 0., 1.], [0., 1., 1., 0.5]])
W3 = np.array([[-0.5, 1., 0.5], [1., -1., -0.5], [0.5, 0.5, -0.5]])
Ws = [W1, W2, W3]
bs = [np.array([0., 0., -0.5, -1.5]), np.array([0., 0.5, -0.5]), np.zeros(3)]
llamadas = 0
def forward():
global llamadas
llamadas += 1
hs, zs = [x], [] # h(0) = x
for l in range(3):
zs.append(Ws[l] @ hs[-1] + bs[l])
hs.append(np.maximum(zs[-1], 0.)) # ReLU; la de la capa 3 no se usa
e = np.exp(zs[-1] - zs[-1].max())
return hs, zs, e / e.sum()
hs, zs, y_hat = forward()
print("z(1) =", zs[0], " h(1) =", hs[1], " <- una neurona apagada")
print("z(2) =", zs[1], " h(2) =", hs[2], " <- otra")
print("z(3) =", zs[2], " ŷ =", np.round(y_hat, 4), " ℓ = %.6f" % -np.log(y_hat @ y))
# Hacia atrás: un δ por capa, del final al principio, y no hay nada más.
deltas = [None, None, y_hat - y] # softmax + entropía cruzada
for l in (1, 0):
deltas[l] = np.where(zs[l] > 0., Ws[l + 1].T @ deltas[l + 1], 0.)
for l in (2, 1, 0): # en el orden en que salen
print("δ(%d) =" % (l + 1), np.round(deltas[l], 6))
grad_W = [np.outer(deltas[l], hs[l]) for l in range(3)]
print("∇W(2) =", (np.round(grad_W[1], 4) + 0.).tolist()) # el + 0. quita ceros con signo
# Los 63 gradientes contra el sondeo, que es lo que cuesta el otro camino.
peor, llamadas = 0., 0
for P, G in [(Ws[l], grad_W[l]) for l in range(3)] + [(bs[l], deltas[l]) for l in range(3)]:
for k in np.ndindex(P.shape):
v = P[k]
P[k] = v + 1e-6
d = -np.log(forward()[2] @ y)
P[k] = v - 1e-6
d -= -np.log(forward()[2] @ y)
P[k] = v
peor = max(peor, abs(d / 2e-6 - G[k]))
print("63 parámetros, mayor diferencia con el sondeo: %.1e" % peor)
print("forward pass gastados en el sondeo: %d. Por backpropagation: 1." % llamadas)
La primera ejecución descarga el intérprete de Python (~15 MB). Después queda en la caché del navegador.
Los ceros son ReLU trabajando, y hay dos. La cuarta neurona de la primera capa oculta tiene y la segunda de la segunda tiene : las dos están apagadas para esta reseña y las dos asignan un cero a su . Ese cero se ve dos veces en , y por motivos distintos. Su segunda fila es nula porque , o sea que ninguno de los cuatro pesos de esa neurona se corrige; su cuarta columna es nula porque , o sea que la entrada que esos pesos leen no aporta nada. Fila y columna son las dos mitades del producto exterior, y cada una se anula por su cuenta.
Las dos últimas líneas son la lección. Los gradientes coinciden con el sondeo hasta , que es el error del sondeo y no del algoritmo. Y conseguirlos sondeando ha costado pasadas hacia adelante contra la única que hace backpropagation. Eso en una red de juguete, con tres capas y ocho entradas: el factor es el número de parámetros multiplicado por dos, así que crece con cada peso que se añada y no deja de crecer nunca.
Comprueba tu intuición
Cinco preguntas —un gradiente leído de un error, qué es exactamente lo que se reutiliza, las formas de la vuelta, la máscara de ReLU y qué pasa si se tira la ida— y un desafío que es el algoritmo entero.
En la red de la segunda celda, la pasada hacia atrás deja , y la entrada de esa reseña es . ¿Cuánto vale , la derivada del peso que la segunda neurona oculta le da a la cuarta entrada del vocabulario?
A margin of ±0.001 is accepted.
Backpropagation no usa ninguna regla de derivación que la lección anterior no tuviera ya. ¿Qué es entonces lo que ahorra?
Marca todo lo que sea cierto de la pasada hacia atrás por una capa , con .
Select every correct option. This is graded all-or-nothing: there is no partial credit.
Así aplica la celda la máscara de ReLU a un error que llega de la capa siguiente. ¿Qué imprime?
import numpy as np
z = np.array([2., -1., 0., 3.])
g = np.array([0.5, -0.5, 1.5, -1.5])
print(np.round(g * (z > 0), 2))
Una red se queda sin memoria al entrenar, y alguien propone liberar cada en cuanto la capa lo ha consumido. ¿Qué ocurre?
Escribe la pasada hacia atrás de una red de capas, a partir de lo que la de ida dejó guardado. La activación oculta es ReLU y la capa de salida lleva softmax con entropía cruzada.
errores(Ws, zs, y_hat, y)recibe la lista de matrices de pesos, la lista de preactivaciones —una por capa, en orden de ida— y la predicción con su etiqueta, y devuelve la lista de los vectores , también en orden de ida.gradientes(deltas, hs)recibe esos y la listahs, dondehs[l]es lo que entra en la capa yhs[0]es . Devuelve la lista de los , uno por capa.
Ninguna de las dos puede escribir en las listas ni en los arrays que recibe.
The first run downloads the Python interpreter (~15 MB); after that it stays in the browser cache. This challenge is much easier to solve on a physical keyboard: on a phone, read it and come back later.
El algoritmo está completo y no ha entrenado nada. Las dos celdas calculan el gradiente de un ejemplo con unos pesos que puse yo, lo imprimen, y ahí se acaban: nadie ha restado nada y nadie ha vuelto a empezar. Lo que falta para cerrar el círculo lleva escrito desde la lección sobre descenso de gradiente ——, esperando exactamente al gradiente que esta lección acaba de producir.
Juntar las dos mitades exige tres decisiones que ninguna de las dos lecciones ha tomado. Los ejemplos entran de en , así que los errores dejan de ser columnas y vuelve a ser la media de las . Los pesos tienen que empezar en algún sitio, y el sitio evidente —todos a cero— falla por una razón que se lee en la fórmula de que acabas de derivar. Y hay que decidir cuándo parar. Tomar las tres, montarlas sobre un perceptrón multicapa (multilayer perceptron, MLP) escrito desde cero y ver caer la pérdida es la siguiente lección, sobre implementar un MLP.
Further reading2 sources · 1 paper, 1 book
Where this lesson comes from, and where to go next. None of it is needed to carry on with the course.
- Learning representations by back-propagating errors
Las cuatro páginas que pusieron backpropagation en el mapa. Traen la misma recurrencia con el mismo error por neurona, y el punto que el bloque no deja de rozar: las neuronas ocultas acaban representando rasgos que nadie fijó.
- Deep Learning, cap. 6: Deep Feedforward Networks
Su §6.5 saca backpropagation de la red de capas fijas y lo pone sobre un grafo de cómputo cualquiera, que es lo que hace la diferenciación automática de una librería: la misma regla de la cadena, recorrida por el grafo en vez de a mano.