Backpropagation: la derivación completa

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.

Recorre los catorce pasos con las flechas y vigila un número: δ(2)₁, el error de la neurona de salida, que se calcula en el tercer paso y vuelve como primer factor en los cinco siguientes. Ese es todo el ahorro. Compara además el cuarto paso con el quinto: dos pesos distintos, el mismo primer factor, y lo único que cambia es lo que cada uno tenía delante.

Un número por preactivación, y las derivadas que cuelgan de él

Fijemos una capa ll 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 ll, y δ(l)\boldsymbol{\delta}^{(l)}, al gradiente de la pérdida de ese ejemplo respecto de z(l)\mathbf{z}^{(l)}:

δ(l)=z(l)Rdl,\boldsymbol{\delta}^{(l)} = \nabla_{\mathbf{z}^{(l)}}\,\ell \in \mathbb{R}^{d_l},

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 \ell y no L\mathcal{L}: 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 ll salen sin recorrer nada. Su preactivación es z(l)=W(l)h(l1)+b(l)\mathbf{z}^{(l)} = \mathbf{W}^{(l)}\mathbf{h}^{(l-1)} + \mathbf{b}^{(l)}, y coordenada a coordenada, zi(l)=kWik(l)hk(l1)+bi(l)z^{(l)}_i = \sum_k \mathbf{W}^{(l)}_{ik} h^{(l-1)}_k + b^{(l)}_i. Mira dónde aparece un peso concreto: Wjk(l)\mathbf{W}^{(l)}_{jk} está en una sola de las dld_l coordenadas, la jj, así que de la suma sobre caminos de la lección anterior sobrevive un único sumando:

Wjk(l)=i=1dlzi(l)zi(l)Wjk(l)=δj(l)hk(l1),bj(l)=δj(l),\frac{\partial \ell}{\partial \mathbf{W}^{(l)}_{jk}} = \sum_{i=1}^{d_l} \frac{\partial \ell}{\partial z^{(l)}_i}\,\frac{\partial z^{(l)}_i}{\partial \mathbf{W}^{(l)}_{jk}} = \delta^{(l)}_j\, h^{(l-1)}_k, \qquad \frac{\partial \ell}{\partial b^{(l)}_j} = \delta^{(l)}_j,

porque zi(l)/Wjk(l)\partial z^{(l)}_i / \partial \mathbf{W}^{(l)}_{jk} vale hk(l1)h^{(l-1)}_k cuando i=ji = j y cero en los otros dl1d_l - 1 casos, y la derivada respecto del sesgo es la misma cuenta con un 11 en lugar de una activación. Apiladas, las dl×dl1d_l \times d_{l-1} casillas son un producto exterior:

W(l)=δ(l)(h(l1))Rdl×dl1,b(l)=δ(l).\nabla_{\mathbf{W}^{(l)}}\,\ell = \boldsymbol{\delta}^{(l)}\left(\mathbf{h}^{(l-1)}\right)^{\top} \in \mathbb{R}^{d_l \times d_{l-1}}, \qquad \nabla_{\mathbf{b}^{(l)}}\,\ell = \boldsymbol{\delta}^{(l)}.

Una columna de dld_l por una fila de dl1d_{l-1}, que es exactamente la forma de W(l)\mathbf{W}^{(l)} —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 ll tiene dldl1+dld_l \cdot d_{l-1} + d_l parámetros, y sus derivadas salen todas de dl+dl1d_l + d_{l-1} números: las coordenadas de δ(l)\boldsymbol{\delta}^{(l)} y las de h(l1)\mathbf{h}^{(l-1)}, 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 δ(l)\boldsymbol{\delta}^{(l)}.

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 z(L)\mathbf{z}^{(L)}, el softmax lo convierte en y^\hat{\mathbf{y}} 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ó

δ(L)=y^y.\boldsymbol{\delta}^{(L)} = \hat{\mathbf{y}} - \mathbf{y}.

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 12(y^y)2\tfrac{1}{2}\left(\hat{y} - y\right)^{2} —el 12\tfrac{1}{2} 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 δ1(2)=(y^y)y^(1y^)\delta^{(2)}_1 = \left(\hat{y} - y\right)\hat{y}\left(1 - \hat{y}\right) y no con y^y\hat{y} - y: 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 l<Ll < L. La preactivación de la capa siguiente se construye a partir de la de ésta en dos movimientos, z(l+1)=W(l+1)φ(z(l))+b(l+1)\mathbf{z}^{(l+1)} = \mathbf{W}^{(l+1)}\varphi\left(\mathbf{z}^{(l)}\right) + \mathbf{b}^{(l+1)}, 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 φ\varphi aplicada coordenada a coordenada es diagonal. Componiéndolas,

z(l+1)z(l)=W(l+1)diag(φ(z(l)))Rdl+1×dl,\frac{\partial \mathbf{z}^{(l+1)}}{\partial \mathbf{z}^{(l)}} = \mathbf{W}^{(l+1)}\,\text{diag}\left(\varphi^{\prime}\left(\mathbf{z}^{(l)}\right)\right) \in \mathbb{R}^{d_{l+1} \times d_l},

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 diag\text{diag} es simétrica y transponer un producto le da la vuelta al orden de los factores,

δ(l)=(z(l+1)z(l))δ(l+1)=diag(φ(z(l)))(W(l+1))δ(l+1),\boldsymbol{\delta}^{(l)} = \left(\frac{\partial \mathbf{z}^{(l+1)}}{\partial \mathbf{z}^{(l)}}\right)^{\top}\boldsymbol{\delta}^{(l+1)} = \text{diag}\left(\varphi^{\prime}\left(\mathbf{z}^{(l)}\right)\right)\left(\mathbf{W}^{(l+1)}\right)^{\top}\boldsymbol{\delta}^{(l+1)},

que escrito con el producto de Hadamard, para no construir la diagonal, es la forma que se implementa:

δ(l)=((W(l+1))δ(l+1))φ(z(l)).\boldsymbol{\delta}^{(l)} = \left(\left(\mathbf{W}^{(l+1)}\right)^{\top}\boldsymbol{\delta}^{(l+1)}\right) \odot \varphi^{\prime}\left(\mathbf{z}^{(l)}\right).

Léela en dos movimientos, porque hacen dos cosas distintas. El primero, (W(l+1))δ(l+1)\left(\mathbf{W}^{(l+1)}\right)^{\top}\boldsymbol{\delta}^{(l+1)}, transporta: reparte el error de las dl+1d_{l+1} neuronas de arriba entre las dld_l 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 00 o 11, 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 jj de δ(l)\boldsymbol{\delta}^{(l)} es la derivada de \ell respecto de zj(l)z^{(l)}_j, y zj(l)z^{(l)}_j llega a la pérdida a través de las dl+1d_{l+1} preactivaciones de la capa siguiente: es la suma sobre caminos de la lección anterior, con dl+1d_{l+1} caminos.

δj(l)=i=1dl+1zi(l+1)zi(l+1)zj(l)=i=1dl+1δi(l+1)zi(l+1)zj(l),\delta^{(l)}_j = \sum_{i=1}^{d_{l+1}} \frac{\partial \ell}{\partial z^{(l+1)}_i}\,\frac{\partial z^{(l+1)}_i}{\partial z^{(l)}_j} = \sum_{i=1}^{d_{l+1}} \delta^{(l+1)}_i\,\frac{\partial z^{(l+1)}_i}{\partial z^{(l)}_j},

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, zi(l+1)=k=1dlWik(l+1)φ(zk(l))+bi(l+1)z^{(l+1)}_i = \sum_{k=1}^{d_l} \mathbf{W}^{(l+1)}_{ik}\,\varphi\left(z^{(l)}_k\right) + b^{(l+1)}_i. Al derivar respecto de zj(l)z^{(l)}_j sobrevive un solo sumando de esa suma, el de k=jk = j, y la activación aporta su derivada:

zi(l+1)zj(l)=Wij(l+1)φ(zj(l)).\frac{\partial z^{(l+1)}_i}{\partial z^{(l)}_j} = \mathbf{W}^{(l+1)}_{ij}\,\varphi^{\prime}\left(z^{(l)}_j\right).

Ese último factor no depende de ii, así que sale fuera de la suma:

δj(l)=(i=1dl+1Wij(l+1)δi(l+1))φ(zj(l)).\delta^{(l)}_j = \left(\sum_{i=1}^{d_{l+1}} \mathbf{W}^{(l+1)}_{ij}\,\delta^{(l+1)}_i\right)\varphi^{\prime}\left(z^{(l)}_j\right).

Y ya está escrita la fórmula de arriba. La suma recorre la columna jj de W(l+1)\mathbf{W}^{(l+1)}, que es la fila jj de su transpuesta, o sea la coordenada jj del transporte; el factor de fuera es la coordenada jj 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 L=3L = 3 cabe entero y sin puntos suspensivos. Hacia adelante, que es la lección sobre el forward pass:

h(0)=x,z(l)=W(l)h(l1)+b(l),h(l)=φ(z(l))    (l=1,2),y^=softmax(z(3)),\mathbf{h}^{(0)} = \mathbf{x}, \quad \mathbf{z}^{(l)} = \mathbf{W}^{(l)}\mathbf{h}^{(l-1)} + \mathbf{b}^{(l)}, \quad \mathbf{h}^{(l)} = \varphi\left(\mathbf{z}^{(l)}\right) \;\; (l = 1, 2), \quad \hat{\mathbf{y}} = \text{softmax}\left(\mathbf{z}^{(3)}\right),

guardando por el camino los h(l1)\mathbf{h}^{(l-1)} y los z(l)\mathbf{z}^{(l)}. Hacia atrás, del final al principio:

δ(3)=y^y,δ(2)=((W(3))δ(3))φ(z(2)),δ(1)=((W(2))δ(2))φ(z(1)),\begin{aligned} \boldsymbol{\delta}^{(3)} &= \hat{\mathbf{y}} - \mathbf{y}, \\[2pt] \boldsymbol{\delta}^{(2)} &= \left(\left(\mathbf{W}^{(3)}\right)^{\top}\boldsymbol{\delta}^{(3)}\right) \odot \varphi^{\prime}\left(\mathbf{z}^{(2)}\right), \\[2pt] \boldsymbol{\delta}^{(1)} &= \left(\left(\mathbf{W}^{(2)}\right)^{\top}\boldsymbol{\delta}^{(2)}\right) \odot \varphi^{\prime}\left(\mathbf{z}^{(1)}\right), \end{aligned}

y de esos tres vectores salen los seis gradientes, W(l)=δ(l)(h(l1))\nabla_{\mathbf{W}^{(l)}}\ell = \boldsymbol{\delta}^{(l)}\left(\mathbf{h}^{(l-1)}\right)^{\top} y b(l)=δ(l)\nabla_{\mathbf{b}^{(l)}}\ell = \boldsymbol{\delta}^{(l)} para l=1,2,3l = 1, 2, 3. 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 LL. 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 6363 parámetros, 126126 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 h(l1)\mathbf{h}^{(l-1)} y los z(l)\mathbf{z}^{(l)} 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í.

import numpy as np

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

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 /h(1)=(0.079, 0.068)\partial \ell / \partial \mathbf{h}^{(1)} = (-0.079,\ 0.068): un solo error, 0.113-0.113, repartido entre las dos neuronas ocultas según sus pesos 0.70.7 y 0.6-0.6, y por eso la segunda coordenada sale con el signo cambiado. El enmascarado son los (0.238, 0.222)(0.238,\ 0.222) de al lado, que es la derivada de la sigmoide en cada preactivación.

Y mira W(1)\nabla_{\mathbf{W}^{(1)}}\ell, que vale (0.019, 0.009)(-0.019,\ -0.009) arriba y (0.015, 0.008)(0.015,\ 0.008) abajo. Su primera columna es δ(1)\boldsymbol{\delta}^{(1)} tal cual, porque x1=1x_1 = 1; la segunda es δ(1)\boldsymbol{\delta}^{(1)} a la mitad, porque x2=0.5x_2 = 0.5. 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.

import numpy as np

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

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 z4(1)=1z^{(1)}_4 = -1 y la segunda de la segunda tiene z2(2)=1z^{(2)}_2 = -1: las dos están apagadas para esta reseña y las dos asignan un cero a su δ\boldsymbol{\delta}. Ese cero se ve dos veces en W(2)\nabla_{\mathbf{W}^{(2)}}\ell, y por motivos distintos. Su segunda fila es nula porque δ2(2)=0\delta^{(2)}_2 = 0, o sea que ninguno de los cuatro pesos de esa neurona se corrige; su cuarta columna es nula porque h4(1)=0h^{(1)}_4 = 0, 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 6363 gradientes coinciden con el sondeo hasta 101010^{-10}, que es el error del sondeo y no del algoritmo. Y conseguirlos sondeando ha costado 126126 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 δ(1)=(0.323559,  0.493520,  0.493520,  0)\boldsymbol{\delta}^{(1)} = (0.323559,\; -0.493520,\; -0.493520,\; 0)^{\top}, y la entrada de esa reseña es x=(0,0,1,2,0,0,1,1)\mathbf{x} = (0, 0, 1, 2, 0, 0, 1, 1)^{\top}. ¿Cuánto vale /W24(1)\partial \ell / \partial \mathbf{W}^{(1)}_{24}, 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 ll, con W(l)Rdl×dl1\mathbf{W}^{(l)} \in \mathbb{R}^{d_l \times d_{l-1}}.

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 h(l)\mathbf{h}^{(l)} en cuanto la capa l+1l+1 lo ha consumido. ¿Qué ocurre?

Escribe la pasada hacia atrás de una red de LL 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 LL vectores δ\boldsymbol{\delta}, también en orden de ida.
  • gradientes(deltas, hs) recibe esos δ\boldsymbol{\delta} y la lista hs, donde hs[l] es lo que entra en la capa l+1l+1 y hs[0] es x\mathbf{x}. Devuelve la lista de los W(l)\nabla_{\mathbf{W}^{(l)}}\ell, 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θt+1=θtηθL\theta_{t+1} = \theta_t - \eta\,\nabla_{\theta}\mathcal{L}—, 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 BB en BB, así que los errores dejan de ser columnas y L\mathcal{L} vuelve a ser la media de las \ell. 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 δ\boldsymbol{\delta} 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
    paperRumelhart, Hinton y Williams, 1986Nature 323EN

    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
    bookGoodfellow, Bengio y Courville, 2016deeplearningbook.orgEN

    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.