Implementar un MLP desde cero

Implementar un MLP desde cero

30 min read

Backpropagation entrega el gradiente de una red entera con una sola pasada hacia atrás, y la lección anterior lo derivó sin saltarse un paso. Lo que produjo fue una lista de números en pantalla. Un gradiente impreso no mueve ningún peso: es una indicación —por dónde se baja, y cuánto le toca a cada parámetro— y alguien tiene que ejecutarla, mil veces seguidas, para que una red pase de contestar cualquier cosa a contestar bien. Ese bucle no está escrito en ninguna celda del curso. Sus cuatro piezas sí, repartidas en cuatro lecciones de este bloque, y juntarlas paga además una deuda abierta desde el principio: los ocho pesos de la neurona de reseñas y los siete que separan las frases de XOR (exclusive or, o exclusivo) los escribí yo, mirando los datos.

El sitio donde comprobarlo está elegido desde entonces. La lección sobre XOR y las capas ocultas demostró que ninguna recta deja está bien y no está mal a un lado y las otras dos frases al otro, y resolvió después las cuatro con una capa oculta cuyos siete números elegí a mano. Si el bucle funciona, la red tiene que dar con unos pesos que acierten las cuatro —no necesariamente los míos— partiendo de números que no significan nada. Y si no funciona se nota enseguida: con cuatro ejemplos y dos coordenadas no hay dónde esconder un fallo.

Las cuatro piezas no están sueltas. Están en un orden, y el orden se cierra sobre sí mismo.

Cuatro cajas colocadas en las esquinas de un rectángulo y unidas por flechas que giran en el sentido de las agujas del reloj. Arriba a la izquierda, forward pass; arriba a la derecha, pérdida; abajo a la derecha, gradiente; abajo a la izquierda, actualización de los pesos. Las tres primeras flechas son grises; la cuarta, la que sube desde la actualización hasta el forward pass y cierra el círculo, es verde y lleva escrito al lado y otra vez.
Entrenar no añade ninguna operación nueva: añade una vuelta. Cada caja se derivó en su lección y ninguna cambia aquí, y lo único que esta lección escribe por primera vez es la flecha que devuelve el control al principio.

Del ejemplo al batch: la vuelta, apilada por filas

La lección anterior derivó la vuelta para un ejemplo, con vectores columna. La ida ya está escrita para un batch entero desde la lección sobre el forward pass: un ejemplo por fila en XRB×d0\mathbf{X} \in \mathbb{R}^{B \times d_0}, y de ahí H(l)RB×dl\mathbf{H}^{(l)} \in \mathbb{R}^{B \times d_l} capa a capa. Falta el mismo trato para la vuelta, y son dos cambios: uno de forma y otro de tamaño.

El de forma es apilar. Llamemos Δ(l)RB×dl\boldsymbol{\Delta}^{(l)} \in \mathbb{R}^{B \times d_l} al error del batch en la capa ll: su fila ii es el δ(l)\boldsymbol{\delta}^{(l)} del ejemplo ii, tumbado, igual que la fila ii de H(l)\mathbf{H}^{(l)} es su activación tumbada. Mayúscula para el batch y minúscula para el ejemplo, que es el convenio del bloque. La capa de salida entra igual que antes, ahora con BB filas a la vez:

Δ(L)=Y^YRB×dL.\boldsymbol{\Delta}^{(L)} = \hat{\mathbf{Y}} - \mathbf{Y} \in \mathbb{R}^{B \times d_L}.

Y la recurrencia se transpone entera. Tomemos la de la lección anterior para el ejemplo ii, δi(l)=((W(l+1))δi(l+1))φ(zi(l))\boldsymbol{\delta}^{(l)}_i = \left(\left(\mathbf{W}^{(l+1)}\right)^{\top}\boldsymbol{\delta}^{(l+1)}_i\right) \odot \varphi^{\prime}\left(\mathbf{z}^{(l)}_i\right), y transpongámosla. Transponer un producto le da la vuelta al orden de sus factores, y el producto de Hadamard opera coordenada a coordenada, de modo que no se entera:

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

que es la fila ii de lo que buscamos, escrita con las filas ii de Δ(l+1)\boldsymbol{\Delta}^{(l+1)} y de Z(l)\mathbf{Z}^{(l)}. Apilar las BB deja

Δ(l)=(Δ(l+1)W(l+1))φ(Z(l)),\boldsymbol{\Delta}^{(l)} = \left(\boldsymbol{\Delta}^{(l+1)}\mathbf{W}^{(l+1)}\right) \odot \varphi^{\prime}\left(\mathbf{Z}^{(l)}\right),

donde lo que hay que mirar es lo que ha desaparecido: la transpuesta. No es un descuido ni una convención distinta. La ida ganó una transpuesta al pasar a filas —W(l)h\mathbf{W}^{(l)}\mathbf{h} se convirtió en H(W(l))\mathbf{H}\left(\mathbf{W}^{(l)}\right)^{\top}— y la vuelta la pierde por el mismo motivo, porque partía de una matriz que ya estaba transpuesta. Las dos veces es el mismo producto leído con los ejemplos en filas.

El cambio de tamaño es el 1/B1/B, y viene de la definición de la pérdida. L\mathcal{L} es la media de las \ell de los BB ejemplos, y derivar una media es promediar las derivadas, así que el gradiente del batch es la media de los BB gradientes que daría la lección anterior de uno en uno. Esa media es otro producto de matrices:

W(l)L=1Bi=1Bδi(l)(hi(l1))=1B(Δ(l))H(l1)Rdl×dl1,b(l)L=1B(Δ(l))1B,\nabla_{\mathbf{W}^{(l)}}\mathcal{L} = \frac{1}{B}\sum_{i=1}^{B}\boldsymbol{\delta}^{(l)}_i\left(\mathbf{h}^{(l-1)}_i\right)^{\top} = \frac{1}{B}\left(\boldsymbol{\Delta}^{(l)}\right)^{\top}\mathbf{H}^{(l-1)} \in \mathbb{R}^{d_l \times d_{l-1}}, \qquad \nabla_{\mathbf{b}^{(l)}}\mathcal{L} = \frac{1}{B}\left(\boldsymbol{\Delta}^{(l)}\right)^{\top}\mathbf{1}_B,

con 1BRB\mathbf{1}_B \in \mathbb{R}^{B} el vector de unos de la lección sobre el forward pass. La suma sobre ejemplos que la lección anterior escribía a mano es exactamente el índice que el producto de matrices recorre y descarta: por eso el resultado tiene la forma de W(l)\mathbf{W}^{(l)} y no se acuerda de BB.

Con eso, la regla del descenso de la lección sobre descenso de gradiente no cambia ni una letra: θt+1=θtηθL(θt)\theta_{t+1} = \theta_t - \eta\,\nabla_{\theta}\mathcal{L}(\theta_t), aplicada a cada matriz y a cada vector de sesgos por separado. La concesión, dicha en voz alta: cada paso mira entero el conjunto de entrenamiento —los ejemplos con etiqueta de los que dispone la red, aquí las cuatro frases—, igual que hacía aquella lección con las diez reseñas. Partirlo en trozos y dar un paso por trozo hace falta cuando es grande, y éste tiene cuatro filas.

Ya está escrito, entonces, todo lo que necesita la red de las cuatro frases: el perceptrón multicapa (multilayer perceptron, MLP) más pequeño con el que probarlo, con dos entradas, cuatro neuronas ocultas con tanh\tanh y una de salida con sigmoide, o sea L=2L = 2 y d0,d1,d2=2,4,1d_0, d_1, d_2 = 2, 4, 1. Su ida ocupa una línea por capa,

H(1)=tanh(X(W(1))+1B(b(1)))R4×4,Y^=σ(H(1)(W(2))+1B(b(2)))R4×1,\mathbf{H}^{(1)} = \tanh\left(\mathbf{X}\left(\mathbf{W}^{(1)}\right)^{\top} + \mathbf{1}_B\left(\mathbf{b}^{(1)}\right)^{\top}\right) \in \mathbb{R}^{4 \times 4}, \qquad \hat{\mathbf{Y}} = \sigma\left(\mathbf{H}^{(1)}\left(\mathbf{W}^{(2)}\right)^{\top} + \mathbf{1}_B\left(\mathbf{b}^{(2)}\right)^{\top}\right) \in \mathbb{R}^{4 \times 1},

con XR4×2\mathbf{X} \in \mathbb{R}^{4 \times 2} y B=4B = 4, y su vuelta son las tres fórmulas de arriba con L=2L = 2. Doce pesos y cinco sesgos: diecisiete números que hay que poner en algún sitio antes de dar el primer paso.

Cuatro neuronas iguales son una sola neurona

Poner esos diecisiete números es la decisión que ninguna fórmula anterior toma, y la respuesta cómoda —todos a cero— falla. Sigue las cuentas con esa misma red y con los diecisiete nulos. Entonces Z(1)=0\mathbf{Z}^{(1)} = \mathbf{0}, y como tanh(0)=0\tanh(0) = 0 también H(1)=0\mathbf{H}^{(1)} = \mathbf{0}; de ahí Y^=σ(0)\hat{\mathbf{Y}} = \sigma(\mathbf{0}), que es 1/21/2 para las cuatro frases. Mete eso en los dos gradientes de arriba:

W(2)L=1B(Δ(2))H(1)=0,Δ(1)=(Δ(2)W(2))φ(Z(1))=0,\nabla_{\mathbf{W}^{(2)}}\mathcal{L} = \frac{1}{B}\left(\boldsymbol{\Delta}^{(2)}\right)^{\top}\mathbf{H}^{(1)} = \mathbf{0}, \qquad \boldsymbol{\Delta}^{(1)} = \left(\boldsymbol{\Delta}^{(2)}\mathbf{W}^{(2)}\right) \odot \varphi^{\prime}\left(\mathbf{Z}^{(1)}\right) = \mathbf{0},

el primero porque H(1)\mathbf{H}^{(1)} es nula y el segundo porque W(2)\mathbf{W}^{(2)} lo es. Con Δ(1)=0\boldsymbol{\Delta}^{(1)} = \mathbf{0} los dos gradientes de la primera capa también se anulan, de modo que el paso deja la red donde estaba y el siguiente encuentra el mismo punto de partida. La capa oculta no se moverá jamás.

Y el cero no es el problema, sino un caso particular de algo más ancho. Toma dos neuronas ocultas jj y kk que empiecen con los mismos pesos de entrada, el mismo sesgo y el mismo peso de salida. Lo primero les da la misma preactivación y la misma activación, zj(1)=zk(1)z^{(1)}_j = z^{(1)}_k y hj(1)=hk(1)h^{(1)}_j = h^{(1)}_k; lo último hace que reciban el mismo error, δj(1)=δk(1)\delta^{(1)}_j = \delta^{(1)}_k; y con el mismo error y la misma activación sus tres gradientes coinciden:

(W(1)L)jm=(W(1)L)km    para todo m,bj(1)L=bk(1)L,W1j(2)L=W1k(2)L.\left(\nabla_{\mathbf{W}^{(1)}}\mathcal{L}\right)_{jm} = \left(\nabla_{\mathbf{W}^{(1)}}\mathcal{L}\right)_{km} \;\; \text{para todo } m, \qquad \nabla_{b^{(1)}_j}\mathcal{L} = \nabla_{b^{(1)}_k}\mathcal{L}, \qquad \nabla_{\mathbf{W}^{(2)}_{1j}}\mathcal{L} = \nabla_{\mathbf{W}^{(2)}_{1k}}\mathcal{L}.

Restar cantidades iguales a cantidades iguales conserva la igualdad, así que las dos neuronas salen del paso tan idénticas como entraron. Y del siguiente. Cuatro copias de una misma neurona calculan lo que calcula una, por muchos pasos que den: la capa oculta tiene la anchura de una neurona por más que d1d_1 diga cuatro. Y una neurona sola no separa estas cuatro frases, que es justo lo que demostró la lección sobre XOR y las capas ocultas.

Nada del algoritmo rompe esa igualdad, porque todo lo que hace es determinista: tiene que venir de fuera, y la fuente más barata de «distintos» es una tirada al azar. Elegir esos diecisiete números de partida es la inicialización, el único punto del entrenamiento donde interviene el azar. Su papel no es el que parece: no aporta ninguna información sobre las frases, sólo separa unas neuronas de otras para que el gradiente pueda empujarlas por caminos distintos. Cuánto valgan también importa —unos pesos diminutos se parecen demasiado al caso de arriba—, y la tercera celda lo mide. Qué escala elegir en general depende de la profundidad de la red, y aparece en el bloque 3, sobre redes recurrentes, cuando el problema pase de ser un inconveniente a ser el tema.

El bucle en NumPy, y la pérdida cayendo

La primera celda es la red con las fórmulas de arriba puestas en NumPy: la ida, la pérdida y la vuelta, sin bucle todavía. Ejecútala y mira la columna de formas y la última línea.

import numpy as np

# Las cuatro frases de XOR: x1 vale +1 si aparece «no» y x2 vale +1 si la
# palabra que valora es «bien». La etiqueta vale 1 si la frase habla bien.
X = np.array([[-1., 1.], [-1., -1.], [1., 1.], [1., -1.]])
Y = np.array([[1.], [0.], [0.], [1.]])
B, d0, d1, d2 = 4, 2, 4, 1

rng = np.random.default_rng(0) # semilla fija: verás estos números
W1, b1 = rng.normal(size=(d1, d0)), np.zeros(d1)
W2, b2 = rng.normal(size=(d2, d1)), np.zeros(d2)


def forward(X, W1, b1, W2, b2):
H1 = np.tanh(X @ W1.T + b1) # (B, d1)
return H1, 1.0 / (1.0 + np.exp(-(H1 @ W2.T + b2)))


def perdida(Y_hat, Y):
p = np.clip(Y_hat, 1e-12, 1.0 - 1e-12) # el logaritmo no admite el cero
return -np.mean(Y * np.log(p) + (1.0 - Y) * np.log(1.0 - p))


def backward(X, Y, H1, Y_hat, W2):
D2 = Y_hat - Y # (B, d2): la salida entra sin factor
D1 = (D2 @ W2) * (1.0 - H1 ** 2) # transporta y enmascara: (B, d1)
return (D1.T @ X) / B, D1.sum(0) / B, (D2.T @ H1) / B, D2.sum(0) / B


H1, Y_hat = forward(X, W1, b1, W2, b2)
gW1, gb1, gW2, gb2 = backward(X, Y, H1, Y_hat, W2)
for nombre, A in [("X", X), ("H(1)", H1), ("Ŷ", Y_hat),
("∇W(1)", gW1), ("∇b(1)", gb1), ("∇W(2)", gW2)]:
print("%-6s %s" % (nombre, A.shape))
print("pérdida inicial: %.4f" % perdida(Y_hat, Y))

# El 1/B a la vista: el gradiente del batch es la media de los cuatro de uno en uno.
uno_a_uno = np.zeros((d1, d0))
for i in range(B):
d2_i = Y_hat[i] - Y[i]
d1_i = (W2.T @ d2_i) * (1.0 - H1[i] ** 2) # el mismo δ, en columna
uno_a_uno += np.outer(d1_i, X[i]) / B
print("\n∇W(1) del batch =", np.round(gW1, 4).tolist())
print("diferencia con la media de los cuatro productos exteriores: %.1e"
% np.abs(uno_a_uno - gW1).max())
numpy

La primera ejecución descarga el intérprete de Python (~15 MB). Después queda en la caché del navegador.

En la columna de formas está el reparto entero: el 44 de los ejemplos aparece en X\mathbf{X}, en H(1)\mathbf{H}^{(1)} y en Y^\hat{\mathbf{Y}}, y en ninguno de los tres gradientes, que salen con la forma de sus parámetros. La última línea comprueba el 1/B1/B y da cero: los cuatro productos exteriores de la lección anterior, promediados a mano, son casilla por casilla el producto de matrices de arriba.

La segunda celda mete eso en un bucle y lo repite dos mil veces, con η=0.5\eta = 0.5. Ejecútala y mira caer la pérdida.

import numpy as np

frases = ["está bien", "está mal", "no está bien", "no está mal"]
X = np.array([[-1., 1.], [-1., -1.], [1., 1.], [1., -1.]])
Y = np.array([[1.], [0.], [0.], [1.]])
B, eta, pasos = 4, 0.5, 2000

rng = np.random.default_rng(0) # la misma red de la celda anterior
W1, b1 = rng.normal(size=(4, 2)), np.zeros(4)
W2, b2 = rng.normal(size=(1, 4)), np.zeros(1)

for t in range(pasos + 1):
H1 = np.tanh(X @ W1.T + b1) # ida
Y_hat = 1.0 / (1.0 + np.exp(-(H1 @ W2.T + b2)))
if t in (0, 25, 50, 100, 200, 500, 1000, 2000):
p = np.clip(Y_hat, 1e-12, 1.0 - 1e-12)
print("paso %4d pérdida %.4f aciertos %d de 4"
% (t, -np.mean(Y * np.log(p) + (1.0 - Y) * np.log(1.0 - p)),
int(((Y_hat >= 0.5) == (Y == 1.0)).sum())))
if t == pasos:
break
D2 = Y_hat - Y # vuelta
D1 = (D2 @ W2) * (1.0 - H1 ** 2)
W1 -= eta * (D1.T @ X) / B # y descenso
b1 -= eta * D1.sum(0) / B
W2 -= eta * (D2.T @ H1) / B
b2 -= eta * D2.sum(0) / B

print()
print("neurona w1 w2 sesgo peso de salida")
for j in range(4):
print(" %d %+5.2f %+5.2f %+5.2f %+5.2f"
% (j + 1, W1[j, 0], W1[j, 1], b1[j], W2[0, j]))
print()
aporta = W2[0] * H1 # lo que pone cada neurona en z(2)
for frase, a in zip(frases, aporta):
print("%-13s neuronas 2 y 4: %+5.2f neuronas 1 y 3: %+5.2f"
% (frase, a[1] + a[3], a[0] + a[2]))
numpy

La primera ejecución descarga el intérprete de Python (~15 MB). Después queda en la caché del navegador.

La pérdida sale de 0.72600.7260 y llega a 0.00110.0011, y los cuatro aciertos aparecen antes del paso 2525. La deuda está pagada: nadie le ha dicho a la red que no invierte el signo de lo que sigue, y los pesos que ha encontrado no son los que escribí en la lección sobre XOR y las capas ocultas.

Son mejores que eso: son los mismos, redescubiertos. Mira las neuronas 22 y 44. Sus pesos apuntan casi en la misma dirección —(1.83, 1.77)(1.83,\ 1.77) y (1.79, 1.85)(1.79,\ 1.85), las dos casi x1+x2x_1 + x_2—, sus sesgos son opuestos —1.80-1.80 y +1.80+1.80— y sus pesos de salida también —3.98-3.98 y +3.93+3.93—. Eso es la franja de la lección sobre XOR y las capas ocultas: dos rectas paralelas y una resta entre las dos. La última tabla lo confirma: esa pareja aporta +7.5+7.5 a las dos frases positivas y +0.2+0.2 a las otras dos. Y la pareja 11 y 33 hace lo mismo sobre la otra diagonal con el signo cambiado, restando 6.76.7 justo donde la primera se calla.

Falta decir cuándo parar, y es la decisión más floja de las tres. Aquí paro en un número redondo de pasos porque la curva se ha aplanado; podría haber parado cuando la pérdida dejara de bajar una milésima, o cuando la norma del gradiente bajara de un umbral, y las tres son reglas de andar por casa. Los aciertos no sirven para decidirlo: en la fila del paso 2525 ya son cuatro y ahí se quedan, mientras la pérdida sigue bajando un factor de trescientos. Y las tres reglas comparten un límite que ninguna puede arreglar, porque las tres miran sólo estas cuatro frases.

La tercera celda vuelve al principio del bucle y cambia una sola cosa: de dónde salen los pesos. Seis escalas con la misma semilla, y debajo cuatro neuronas idénticas de verdad.

import numpy as np

X = np.array([[-1., 1.], [-1., -1.], [1., 1.], [1., -1.]])
Y = np.array([[1.], [0.], [0.], [1.]])
B, eta, pasos = 4, 0.5, 1000


def entrena(escala, iguales=False):
rng = np.random.default_rng(0) # la misma semilla en todas
W1, W2 = rng.normal(size=(4, 2)) * escala, rng.normal(size=(1, 4)) * escala
if iguales: # cuatro copias de una misma neurona
W1, W2 = np.repeat(W1[:1], 4, axis=0), np.full((1, 4), W2[0, 0])
b1, b2 = np.zeros(4), np.zeros(1)
curva = []
for t in range(pasos + 1):
H1 = np.tanh(X @ W1.T + b1)
Y_hat = 1.0 / (1.0 + np.exp(-(H1 @ W2.T + b2)))
p = np.clip(Y_hat, 1e-12, 1.0 - 1e-12)
curva.append(-np.mean(Y * np.log(p) + (1.0 - Y) * np.log(1.0 - p)))
if t == pasos:
break
D2 = Y_hat - Y
D1 = (D2 @ W2) * (1.0 - H1 ** 2)
W1 -= eta * (D1.T @ X) / B
b1 -= eta * D1.sum(0) / B
W2 -= eta * (D2.T @ H1) / B
b2 -= eta * D2.sum(0) / B
return curva, int(((Y_hat >= 0.5) == (Y == 1.0)).sum()), W1


print("%7s %8s %8s %8s %9s %s"
% ("escala", "paso 0", "paso 50", "paso 200", "paso 1000", "aciertos"))
for escala in [0.0, 0.01, 0.1, 0.5, 1.0, 10.0]:
curva, aciertos, _ = entrena(escala)
print("%7.2f %8.4f %8.4f %8.4f %9.4f %d de 4"
% (escala, curva[0], curva[50], curva[200], curva[1000], aciertos))

# Y la simetría sin ceros: cuatro copias de una misma neurona, con pesos normales.
curva, aciertos, W1 = entrena(1.0, iguales=True)
print()
print("cuatro neuronas idénticas: pérdida %.4f -> %.4f, %d de 4"
% (curva[0], curva[-1], aciertos))
print("W(1) tras 1000 pasos:", np.round(W1, 4).tolist())
print("¿las cuatro filas siguen siendo la misma?", bool(np.allclose(W1, W1[0])))
numpy

La primera ejecución descarga el intérprete de Python (~15 MB). Después queda en la caché del navegador.

Las dos primeras filas de la tabla no se mueven de 0.69310.6931, que es log2\log 2: la red contesta 0.50.5 a las cuatro frases al empezar y sigue contestando lo mismo mil pasos después. La segunda de las dos es la que enseña algo, porque su escala es 0.010.01 y no cero: las cuatro neuronas sí son distintas. Lo que pasa es que cada capa recibe un gradiente multiplicado por la escala de la otra, y con dos factores pequeños todo avanza cientos de veces más despacio: esa red no está atascada, llega a las cuatro frases hacia el paso cien mil. Al otro extremo, la escala 1010 arranca en 4.734.73 —confiadamente equivocada— y se recupera: con dos capas eso sale barato, y con profundidad deja de salirlo.

Las tres últimas líneas son la simetría sin ceros de por medio. Las cuatro neuronas empiezan con pesos normales, sólo que iguales entre sí, y mil pasos después siguen siendo la misma fila repetida cuatro veces. La pérdida ha bajado de 0.72400.7240 a 0.69310.6931, que es exactamente lo que consigue una red que contesta lo mismo a todo: cuatro neuronas trabajando como una, y una no puede.

Comprueba tu intuición

Tres preguntas —por qué el cero congela la red, un gradiente de batch en NumPy y qué se puede leer en una curva de pérdida— y un desafío que es el bucle entero.

La red de la lección arranca con los cuatro pesos de W(2)\mathbf{W}^{(2)} y los ocho de W(1)\mathbf{W}^{(1)} puestos a cero, y mil pasos después sigue exactamente igual. ¿Por qué?

Así calcula la celda el gradiente de una capa a partir del error del batch. ¿Qué imprime?

import numpy as np
 
D = np.array([[1., -1.], [2., 0.]])
H = np.array([[1., 0., 2.], [0., 1., 1.]])
print((D.T @ H).shape, (D.T @ H / len(H))[0])
 

Marca todo lo que sea cierto sobre las curvas de pérdida que imprimen la segunda y la tercera celda.

Select every correct option. This is graded all-or-nothing: there is no partial credit.

Escribe el bucle entero para la red de esta lección: d0d_0 entradas, una capa oculta de d1d_1 neuronas con tanh\tanh y una salida con sigmoide, sobre un batch de BB ejemplos.

  • gradientes(X, Y, W1, b1, W2, b2) devuelve los cuatro gradientes de L\mathcal{L} en el orden (gW1, gb1, gW2, gb2), cada uno con la forma del parámetro que deriva.
  • entrena(X, Y, W1, b1, W2, b2, eta, pasos) da pasos pasos de descenso y devuelve (curva, W1, b1, W2, b2), donde curva tiene pasos + 1 pérdidas: una antes de cada paso y una al final.

entrena no puede escribir en las matrices 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.


La red acierta las cuatro frases que ha visto, y no hay una quinta. Eso no es un defecto del entrenamiento: es que aquí acertar y aprender son indistinguibles, porque con cuatro ejemplos memorizar la respuesta y entender la regla producen los mismos números. Nada de lo que imprime este bucle dice qué haría la red con no está tan bien, y el bucle no tiene forma de enterarse.

Queda además una deuda que la de los pesos ha dejado a la vista. Las dos coordenadas de entrada las inventé yo: «¿aparece no?» y «¿la palabra que valora es bien o mal?» son dos preguntas que alguien tuvo que formular después de leer las cuatro frases, y un texto cualquiera no llega en dos números. Llegaba, eso sí, en la bolsa de palabras del bloque anterior. Montar este mismo bucle sobre unos cientos de reseñas representadas así, medir por primera vez sobre reseñas que la red no ha visto y ver qué queda del acierto es la siguiente lección, el proyecto de clasificación de sentimiento.

Further reading2 sources · 1 paper, 1 interactive

Where this lesson comes from, and where to go next. None of it is needed to carry on with the course.

  • Understanding the difficulty of training deep feedforward neural networks
    paperGlorot y Bengio, 2010AISTATS 2010EN

    El artículo al que remite la lección al aplazar «qué escala» al bloque 3: fija la escala de inicialización a partir de la anchura de cada capa para que la señal ni sature ni se apague. La simetría no la trata; eso ya lo hace la lección.

  • A Neural Network Playground
    interactiveSmilkov y Carterplayground.tensorflow.orgEN

    El bucle que acabas de escribir, con mandos: mueve la tasa de aprendizaje, las capas, las neuronas y la activación, y mira caer la pérdida. Trabaja sobre puntos en el plano, no sobre texto, y trae regularización ajena al curso.