La regla de la cadena, en serio

La regla de la cadena, en serio

30 min de lectura

De los tres prerrequisitos con los que arranca el curso, uno es la regla de la cadena, y hasta aquí el curso ha derivado con la versión que se aprende primero: dos funciones de una variable encadenadas, dos derivadas multiplicadas. Con ella salieron las dos cuentas que el bloque ha necesitado: la corrección de la capa de salida y el gradiente de una neurona sin nada delante. La lección anterior, sobre descenso de gradiente, dejó el entrenamiento resuelto salvo por una pieza: el paso sabe qué hacer con el gradiente, y para una red con capas ocultas todavía no hay forma de calcularlo. Esta lección es esa pieza, y es sólo cálculo.

Mira dónde se complica, porque no es donde parece. Toma la red de las reseñas con tres respuestas en lugar de dos —positiva, negativa y neutra, las clases con las que la lección sobre funciones de pérdida llegó al softmax—: ocho entradas, cuatro neuronas ocultas y tres de salida. Elige un peso cualquiera de la primera capa, pongamos el que la primera neurona oculta le da a divertida, y muévelo una milésima. Su neurona cambia de valor: hasta ahí, la cuenta de siempre. Lo que viene después no lo es, porque esa neurona alimenta a las tres de salida, las tres entran en la pérdida y las tres se han movido. Un número tocado, tres efectos que salen por sitios distintos y se juntan al final. En la regla de una variable no hay ningún sitio donde poner eso.

Lo que hay que contar no son las neuronas: son los recorridos que van de un peso a la pérdida.

Cinco círculos unidos por flechas. A la izquierda, uno solo etiquetado w. En el centro, una columna de tres, etiquetados u sub 1 arriba, u sub 2 en medio y u sub 3 abajo; cada uno recibe una flecha del círculo de la izquierda. A la derecha, uno solo etiquetado L caligráfica, que recibe una flecha de cada uno de los tres del centro. El recorrido de arriba, el que va de w a u sub 1 y de u sub 1 a L, está trazado en verde y más grueso que los demás, y sus dos flechas llevan escrito, respectivamente, la derivada parcial de u sub 1 respecto de w y la derivada parcial de L respecto de u sub 1.
Un camino es un recorrido completo de w hasta la pérdida, y lleva un factor por flecha. Aquí hay tres, sólo uno está resaltado, y la derivada que se busca no es ninguno de ellos: es lo que suman los tres.

De las tres respuestas que caben —quedarse con el que más pesa, promediarlos, sumarlos— sólo una es cierta, y conviene saber por qué antes de usarla.

Cuando una variable llega por varios caminos

Quitemos la red de en medio. Sea L\mathcal{L} una función de mm variables u1,,umu_1, \dots, u_m, y sea cada una de ellas, a su vez, función de una misma variable tt. Entonces L\mathcal{L} depende de tt aunque no la mencione, y su derivada es

dLdt=k=1mLukukt,\frac{d\mathcal{L}}{dt} = \sum_{k=1}^{m} \frac{\partial \mathcal{L}}{\partial u_k}\,\frac{\partial u_k}{\partial t},

un sumando por cada variable intermedia. Cada sumando tiene la forma de la regla de siempre —cuánto responde L\mathcal{L} a uku_k, por cuánto responde uku_k a tt—, y lo nuevo es que hay mm y que se suman. Dentro de un camino se multiplica; entre caminos se suma.

Esa suma no es un convenio, y de dónde sale es lo único de la lección que no es contabilidad. Que L\mathcal{L} sea derivable en un punto significa que cerca de él se parece a una función lineal de los desplazamientos Δu1,,Δum\Delta u_1, \dots, \Delta u_m, y una función lineal de mm números es una suma de mm términos, uno por número. Los caminos no compiten: cada uno aporta su parte, y las partes se suman porque la aproximación de la que salen ya era una suma.

La derivación, con el desplazamiento explícito

Mueve tt una cantidad pequeña Δt\Delta t. Cada variable intermedia se mueve con ella, y por ser derivable respecto de tt,

Δuk=uktΔt+o(Δt),\Delta u_k = \frac{\partial u_k}{\partial t}\,\Delta t + o(\Delta t),

donde o(Δt)o(\Delta t) recoge lo que sobra y que, dividido entre Δt\Delta t, tiende a cero. La derivabilidad de L\mathcal{L} en el punto (u1,,um)(u_1, \dots, u_m) dice lo mismo un nivel más arriba: el incremento de L\mathcal{L} es lineal en los mm desplazamientos, más un resto despreciable frente al tamaño del desplazamiento total,

ΔL=k=1mLukΔuk+o ⁣(Δu).\Delta\mathcal{L} = \sum_{k=1}^{m} \frac{\partial \mathcal{L}}{\partial u_k}\,\Delta u_k + o\!\left(\lVert \Delta \mathbf{u} \rVert\right).

Sustituye la primera en la segunda, divide todo entre Δt\Delta t y haz tender Δt\Delta t a cero. Los dos restos se van —el segundo porque Δu\lVert \Delta \mathbf{u} \rVert es del orden de Δt\Delta t— y lo que queda es la fórmula de arriba. La suma estaba en la segunda ecuación desde el principio: entró con la definición de derivada, no con la regla de la cadena.

Vuelve ahora a la red con eso en la mano. El peso que movías, el que la primera neurona oculta le da a divertida, es W13(1)\mathbf{W}^{(1)}_{13} —fila 11, la de esa neurona; columna 33, la de esa entrada del vocabulario—, y las variables intermedias son las tres preactivaciones de la capa de salida, z1(2)z^{(2)}_1, z2(2)z^{(2)}_2 y z3(2)z^{(2)}_3. Por el camino hay dos escalones más, así que cada sumando tiene cuatro factores:

LW13(1)=k=13Lzk(2)y^kyk  zk(2)h1(1)Wk1(2)  h1(1)z1(1)φ(z1(1))  z1(1)W13(1)x3.\frac{\partial \mathcal{L}}{\partial \mathbf{W}^{(1)}_{13}} = \sum_{k=1}^{3} \underbrace{\frac{\partial \mathcal{L}}{\partial z^{(2)}_k}}_{\hat{y}_k - y_k}\; \underbrace{\frac{\partial z^{(2)}_k}{\partial h^{(1)}_1}}_{\mathbf{W}^{(2)}_{k1}}\; \underbrace{\frac{\partial h^{(1)}_1}{\partial z^{(1)}_1}}_{\varphi^{\prime}\left(z^{(1)}_1\right)}\; \underbrace{\frac{\partial z^{(1)}_1}{\partial \mathbf{W}^{(1)}_{13}}}_{x_3}.

Ninguno de los cuatro es nuevo. El primero es el resultado con el que la lección sobre funciones de pérdida cerró la capa de salida al juntar softmax y entropía cruzada. El segundo sale de que zk(2)z^{(2)}_k es una suma en la que h1(1)h^{(1)}_1 aparece una vez, multiplicada por su peso. El tercero es la derivada de la activación, tabulada para las tres candidatas en la lección sobre funciones de activación y no linealidad. Y el cuarto es el de la lección anterior: la preactivación es lineal en sus pesos, así que la derivada respecto de uno es la entrada que lo acompaña. Lo único que hacía falta era saber que los tres caminos se suman.

El objeto que sustituye a la derivada

Esa fórmula sirve para un peso. La red tiene 5151, y cincuenta y una sumas con índices no son una respuesta: hace falta subir un nivel.

Sea u:RnRm\mathbf{u} : \mathbb{R}^{n} \to \mathbb{R}^{m}. Su derivada en un punto es la jacobiana, la matriz que reúne todas las derivadas parciales de todas las salidas respecto de todas las entradas,

uxRm×n,[ux]ij=uixj,\frac{\partial \mathbf{u}}{\partial \mathbf{x}} \in \mathbb{R}^{m \times n}, \qquad \left[\frac{\partial \mathbf{u}}{\partial \mathbf{x}}\right]_{ij} = \frac{\partial u_i}{\partial x_j},

con una fila por coordenada de salida y una columna por coordenada de entrada. Ese orden —la salida primero— es el del curso entero, y de él salen las dos cosas que este bloque hace con una jacobiana.

La primera es que componer vuelve a ser multiplicar. Si xRn\mathbf{x} \in \mathbb{R}^{n} produce uRm\mathbf{u} \in \mathbb{R}^{m} y ésta produce vRp\mathbf{v} \in \mathbb{R}^{p}, entonces

vx=vuux,\frac{\partial \mathbf{v}}{\partial \mathbf{x}} = \frac{\partial \mathbf{v}}{\partial \mathbf{u}}\,\frac{\partial \mathbf{u}}{\partial \mathbf{x}},

un producto de una matriz p×mp \times m por una m×nm \times n que entrega la p×np \times n que hace falta. La dimensión que se recorre y desaparece es mm, la de las variables intermedias, igual que en el forward pass desaparecía la anchura compartida por dos capas.

Y ese producto no es un objeto nuevo, es el anterior apilado. Desarrolla la casilla (i,j)(i, j) con la definición del producto de matrices:

[vx]ij=k=1mviukukxj,\left[\frac{\partial \mathbf{v}}{\partial \mathbf{x}}\right]_{ij} = \sum_{k=1}^{m} \frac{\partial v_i}{\partial u_k}\,\frac{\partial u_k}{\partial x_j},

que es la suma sobre caminos de la sección anterior, escrita para la salida ii y la entrada jj: los mm recorridos que van de xjx_j a viv_i, uno por cada variable intermedia por la que se puede pasar. La suma sobre caminos y el producto de matrices son la misma cuenta, y la segunda la hace el ordenador sin que nadie escriba un índice.

La pérdida es un escalar, y por eso aparece la transpuesta

La segunda cosa sale de mirar el final de la cadena. L\mathcal{L} entrega un solo número, o sea que en su caso m=1m = 1 y su jacobiana es una matriz de una fila, L/xR1×n\partial\mathcal{L}/\partial\mathbf{x} \in \mathbb{R}^{1 \times n}. Los gradientes de este curso son columnas, como todos sus vectores, así que uno es la transpuesta del otro, xL=(L/x)\nabla_{\mathbf{x}}\mathcal{L} = \left(\partial\mathcal{L}/\partial\mathbf{x}\right)^{\top}. Transponiendo la composición de arriba —y recordando que transponer un producto le da la vuelta al orden de los factores—,

xL=(ux)uL,\nabla_{\mathbf{x}}\mathcal{L} = \left(\frac{\partial \mathbf{u}}{\partial \mathbf{x}}\right)^{\top} \nabla_{\mathbf{u}}\mathcal{L},

una matriz n×mn \times m por un vector de mm coordenadas que devuelve el vector de nn que se buscaba. Ésa es la forma de todo lo que queda del bloque: un gradiente entra por la derecha, sale convertido en el gradiente de la capa anterior, y la jacobiana transpuesta es lo que hace la conversión.

Guarda una observación que aquí no se usa. Una cadena de jacobianas se puede evaluar multiplicando primero las matrices entre sí, o llevando el gradiente de la salida hacia atrás con una transpuesta detrás de otra. Dan lo mismo, porque el producto de matrices es asociativo; no cuestan lo mismo, porque la primera multiplica matrices por matrices y la segunda sólo matrices por vectores.

Una activación coordenada a coordenada tiene jacobiana diagonal

Falta una jacobiana concreta, la de la activación, y sale mucho más barata de lo que sugiere su tamaño. Con h=φ(z)\mathbf{h} = \varphi(\mathbf{z}) aplicada coordenada a coordenada, cada salida mira una sola entrada: hih_i depende de ziz_i y de nada más, así que hi/zj=0\partial h_i / \partial z_j = 0 en cuanto iji \neq j. De la matriz m×mm \times m sobrevive la diagonal:

hz=diag(φ(z))Rm×m,\frac{\partial \mathbf{h}}{\partial \mathbf{z}} = \text{diag}\left(\varphi^{\prime}(\mathbf{z})\right) \in \mathbb{R}^{m \times m},

donde diag(v)\text{diag}(\mathbf{v}) es la matriz cuadrada que lleva v\mathbf{v} en la diagonal y ceros en todo lo demás. Multiplicar por ella es multiplicar coordenada a coordenada, que se escribe con un símbolo propio, el producto de Hadamard:

diag(v)g=vg.\text{diag}(\mathbf{v})\,\mathbf{g} = \mathbf{v} \odot \mathbf{g}.

De ahí en adelante nadie construye la matriz: son mm productos en vez de m2m^{2}, y mm números guardados en vez de m2m^{2} de los cuales m2mm^{2} - m son ceros. Con ReLU (rectified linear unit) la diagonal vale 00 o 11 y es una máscara: una neurona oculta con la preactivación negativa pone un cero en su casilla, y ese cero corta todos los caminos que pasaban por ella. Es la ReLU muerta de la lección sobre funciones de activación, vista desde el otro lado.

El softmax es la excepción, y hay que decirlo: mezcla todas las coordenadas, así que su jacobiana no tiene un solo cero. Aquí no hace falta, porque la lección sobre funciones de pérdida ya la derivó con la entropía cruzada y de las dos juntas salió y^y\hat{\mathbf{y}} - \mathbf{y}, el gradiente con el que arranca la cadena.

Un peso, tres caminos y tres maneras de medirlos

La celda mete la reseña la película es divertida y la recomiendo en la red de ocho entradas, cuatro neuronas ocultas y tres salidas, y calcula la misma derivada de tres maneras: un sondeo numérico, la suma de los tres caminos y el producto de las jacobianas. Los pesos los he puesto yo a mano, así que verás estos números y no otros. Ejecútala y compara las tres cifras, y después mira la cuarta.

import numpy as np

# La reseña de arriba, «la película es divertida y la recomiendo», y lo que se espera de ella.
x = np.array([0., 0., 1., 2., 0., 0., 1., 1.]) # bolsa de palabras, ocho entradas
y = np.array([1., 0., 0.]) # etiqueta: positiva, de tres clases

# Los pesos los he puesto yo a mano. No hay un solo número al azar.
W1 = np.array([[-1., 1., 1., 0., -1., -1., 0., 1.],
[0., 0., -1., 0.5, 0., 0., 1., 0.],
[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([[1., 0., 0.5, -1.], [-1., 0., 0., 1.], [0.5, 0.5, -0.5, 0.]])
b1, b2 = np.array([0., 0., -0.5, -1.5]), np.zeros(3)


def forward(W1):
z1 = W1 @ x + b1
e = np.exp(W2 @ np.maximum(z1, 0.) + b2) # ReLU, y después softmax
y_hat = e / e.sum()
return z1, y_hat, float(-np.log(y_hat @ y)) # la one-hot elige la clase correcta


i, j = 0, 2 # W1[0, 2]: neurona oculta 1, «divertida»
z1, y_hat, L = forward(W1)
d_relu = (z1 > 0).astype(float)
print("z(1) =", z1, " φ'(z(1)) =", d_relu, " ŷ =", np.round(y_hat, 4), " L = %.6f" % L)

# 1. Sondear: mover ese peso una cienmilésima arriba y abajo y volver a evaluar la pérdida.
W_mas, W_menos = W1.copy(), W1.copy()
W_mas[i, j], W_menos[i, j] = W1[i, j] + 1e-5, W1[i, j] - 1e-5
print("sondeo %+.8f" % ((forward(W_mas)[2] - forward(W_menos)[2]) / 2e-5))

# 2. Un camino por neurona de salida, con sus cuatro factores a la vista.
caminos = (y_hat - y) * W2[:, i] * d_relu[i] * x[j]
for k in range(3):
print(" camino por z(2)[%d]: (%+.6f)(%+.1f)(%.0f)(%.0f) = %+.6f"
% (k, y_hat[k] - y[k], W2[k, i], d_relu[i], x[j], caminos[k]))
print("suma de los caminos %+.8f" % caminos.sum())
print("olvidando el tercero %+.8f" % caminos[:2].sum())

# 3. Las mismas derivadas apiladas en jacobianas, y multiplicadas tal cual.
J_w = np.eye(4)[:, [i]] * x[j] # dz(1)/dW1[0,2]: sólo la neurona i lo ve
J = (y_hat - y).reshape(1, 3) @ W2 @ np.diag(d_relu) @ J_w
print("formas (1, 3) x", W2.shape, "x", np.diag(d_relu).shape, "x", J_w.shape)
print("producto de jacobianas %+.8f" % J[0, 0])
numpy

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

Las tres coinciden en 0.07427622-0.07427622, y el sondeo es el que menos derecho tenía a hacerlo: no sabe nada de la red, sólo evalúa la pérdida con el peso movido una cienmilésima arriba y otra abajo. Que dé lo mismo que la cadena es la comprobación que conviene tener a mano cuando una derivada escrita a mano no cuadre.

La cuarta cifra es la que enseña de qué va la lección. Olvidar un camino —el tercero, el que pasa por la neurona de salida de la clase neutra— deja la derivada en 0.13342693-0.13342693, casi el doble de lo que vale. Y mira los tres sumandos, porque el error no es de tamaño: los dos primeros son negativos y el tercero es positivo. Ese camino tiraba en contra, y quitarlo no recorta la corrección, la agranda. Un camino olvidado no da una derivada aproximada: da la derivada de otra función.

La última línea imprime las formas de la cadena, (1,3)×(3,4)×(4,4)×(4,1)(1, 3) \times (3, 4) \times (4, 4) \times (4, 1). Y en la fila de φ\varphi^{\prime} hay un cero: la cuarta neurona oculta tiene la preactivación en 1-1, está apagada para esta reseña, y su casilla de la diagonal corta lo que pasara por ella.

Comprueba tu intuición

Cinco preguntas —una suma de caminos a mano, por qué se suman, las formas de una jacobiana, la diagonal en NumPy y el orden del producto— y un desafío que es la regla de la cadena entera.

Una pérdida L=u12+3u2\mathcal{L} = u_1^{2} + 3u_2 depende de una sola variable tt a través de u1=2tu_1 = 2t y u2=t2u_2 = t^{2}. ¿Cuánto vale dL/dtd\mathcal{L}/dt en t=1t = 1?

Se acepta un margen de ±0.

Las aportaciones de los caminos se suman. ¿Por qué se suman, en lugar de multiplicarse o de quedarse con la mayor?

Marca todo lo que sea cierto sobre las jacobianas de esta lección.

Marca todas las opciones correctas. Se corrige todo o nada: no hay puntuación parcial.

Las dos maneras de multiplicar por una jacobiana diagonal, una al lado de la otra. ¿Qué imprime?

import numpy as np
 
v = np.array([1., 0., 1.])
g = np.array([-0.5, 2., 4.])
print(np.diag(v).shape, (np.diag(v) @ g).tolist(), (v * g).tolist())
 

En la red de esta lección, xR8\mathbf{x} \in \mathbb{R}^{8} produce h(1)R4\mathbf{h}^{(1)} \in \mathbb{R}^{4} y ésta produce z(2)R3\mathbf{z}^{(2)} \in \mathbb{R}^{3}. ¿Qué forma tiene z(2)/x\partial\mathbf{z}^{(2)} / \partial\mathbf{x}, y de qué producto sale?

Escribe la regla de la cadena en su forma vectorial, sin ninguna red por medio:

  • jacobiana(f, x) recibe una función de Rn\mathbb{R}^{n} en Rm\mathbb{R}^{m} y un punto, y devuelve su jacobiana en ese punto, de forma (m,n)(m,\, n), calculada con diferencias centradas: la columna jj es (f(x+hej)f(xhej))/2h\left(f(\mathbf{x} + h\mathbf{e}_j) - f(\mathbf{x} - h\mathbf{e}_j)\right)/2h con h=106h = 10^{-6}.
  • gradiente_entrada(jacobianas, g) recibe una lista de jacobianas en el orden de ida —la primera es la de la primera función— y el gradiente en la salida, y devuelve el gradiente en la entrada. Es un vector, no una matriz.

jacobiana no puede escribir en la x que recibe.

La primera comprobación descarga el intérprete de Python (~15 MB); después queda en la caché del navegador. Este desafío se resuelve mejor con un teclado físico: en el móvil puedes leerlo y volver luego.


Ya está todo lo que hace falta para derivar cualquier red, y así puesto no sirve. La celda ha calculado la derivada de un peso, y para conseguirla ha recorrido la cadena entera: el error de la capa de salida, la matriz de la segunda capa, la diagonal de la activación. El peso de al lado necesita ese mismo recorrido con un solo factor distinto al final, y los otros cuarenta y nueve de esta red, también. Con un vocabulario de veinte mil entradas y ciento veintiocho neuronas ocultas son dos millones y medio largos de recorridos, idénticos salvo por el último paso, y el ordenador más rápido del mundo tampoco tiene tiempo para eso.

El desperdicio salta a la vista en cuanto se nombra: esos recorridos comparten casi todo, y lo compartido se recalcula una vez por parámetro. Lo que hace falta es darle la vuelta al sentido de la marcha —calcular una sola vez, empezando por la última capa, cuánto le debe la pérdida a cada preactivación de la red, y dejar que cada peso lea su derivada de una cantidad que ya está ahí—. Ese procedimiento tiene nombre propio, se deriva capa por capa y con los índices a la vista, y es la siguiente lección, sobre backpropagation.

Para profundizar2 fuentes · 1 paper, 1 artículo

De dónde sale lo de esta lección, y dónde seguir si quieres más. Nada de aquí hace falta para continuar el curso.

  • The Matrix Calculus You Need For Deep Learning
    paperParr y Howard, 2018arXiv:1802.01528EN

    La regla de la cadena vectorial y la jacobiana enteras, con el caso de la activación coordenada a coordenada —jacobiana diagonal— que la lección usa. No pide más que cálculo de una variable, y es más mecánico que conceptual.

  • Calculus on Computational Graphs: Backpropagation
    artículoChristopher Olah, 2015colah.github.ioEN

    Lee la suma sobre caminos como un grafo de cómputo y nombra lo que la lección aparca: multiplicar matrices entre sí o llevar un vector hacia atrás son los modos directo e inverso de derivar, y el inverso es lo que abarata la vuelta.