Scratch Network

Neural network from Scratch: Base matemática para la propagación (Parte 2)

By 02/06/2018 febrero 23rd, 2020 No Comments
[mathjax] En el anterior articulo creamos los cimientos para realizar nuestra red neuronal. Pero somos totalmente desconocedores de como se propaga el resultado de las capas (forward) y como se corrigen (backward). Pues bien, en este articulo haremos exactamente esto. Además usaremos distintos ejemplos a lo largo del articulo para ver como realmente funciona.

Nos centraremos principalmente en la base matemática que hay detrás de la propagación (en futuros artículos lo programaremos).
Antes de empezar vamos a realizar un pequeño paréntesis para explicar algunas herramientas que nos van a ser útiles para detectar problemas en la arquitectura de la red.

Herramientas

Detección de ciclos

Nuestra red es un grafo aciclico dirigido (DAG), esto quiere decir que no podemos encontrarnos ciclos en la red. La existencia de bucles en la red simplemente harían que nunca acabase la propagación.

Ejemplo de un ciclo.

El algoritmo para detectar bucles lo único que hará es:

  1. Trazar un camino desde un nodo de entrada cualquiera de la red.
  2. Ir avanzando luego se comprobará a cada paso si el nodo que sé esta explorando ya forma parte del camino.
  3. De ser así quiere decir que hemos pasado múltiples veces por el mismo nodo y por lo tanto tiene que haber un ciclo.


Veamos como implementamos este concepto:

Nodos de entrada, salida y no conectados

Es interesante indicar si existen en la red nodos de entrada, de salida o si nodos no conectados.
Aunque esta comprobación es más adicional y no influye directamente en el comportamiento del sistema. Pero nos va a permitir alertar al diseñador de la red de que algo no es correcto (o sobra):

Podríamos decir que el proceso de compilación será realizar estos tests y limpiar/inicializar todas las estructuras necesarias para poder aplicar luego tanto el forward como el backward:

Profundizando en el forward:

En el primer articulo vimos como funciona el algoritmo de forward/backward en el ámbito de los nodos (como estos se propagan), pero… En las redes neuronales sabemos que los nodos realmente son operaciones. ¿Como se propaga cuando tratamos cada nodo como una función?
El proceso de forward se realiza básicamente pasando nodo a nodo los resultados, es decir la salida de un nodo será la entrada de su siguiente.
Matemáticamente hablando lo que hacemos se conoce como una composición de funciones.
Si usamos la red de la anterior figura, tendríamos lo siguiente en el forward (f = forward):

  • f(A) = input(X)
  • f(B) = compute_{B}(f(A))
  • f(C) = compute_{C}(f(A))
  • f(D) = compute_{C}(f(B),f(C))
  • f(E) = compute_{C}(f(B))
  • f(F) = compute_{C}(f(C))
  • F(X) = f(G) = compute_{C}(f(E),f(D),f(F))

En esencia una red neuronal es la composición de capas que crean una función MUY pero que MUY compleja con una condición muy apetecible… Es diferenciable por lo tanto corregible si puede cuantificar el error.
Al final todo se basa en eso. Tener más parámetros equivale a grados de libertad para generar una solución.

Profundizando en el backward:

En el forward todo era bastante sencillo pero en el backward se complica un poco más. Recordemos que para corregir los pesos de cada capa, necesitamos la derivada del error respecto esos pesos.
Si la propagación hacia adelante era una composición de funciones la propagación hacia atrás es un producto de derivadas.
Fijémonos en la regla de la cadena:

Regla de la cadena.

Estas serian las derivadas respecto cada variable:
\frac{\partial z(x(w), y(w))}{\partial x} =\frac{\partial z}{\partial x} \frac{\partial z(x(w), y(w))}{\partial y} =\frac{\partial z}{\partial y} \frac{\partial z(x(w), y(w))}{\partial w} =\frac{\partial z}{\partial x} \cdot\frac{\partial x}{\partial w} +\frac{\partial z}{\partial y} \cdot\frac{\partial y}{\partial w} La derivada que llega al nodo corresponde al producto de las derivadas (regla de la cadena) de cada nodo.
Y en caso de tener más de un nodo de salida estas son la suma de todas esas contribuciones.

\frac{\partial z}{\partial w} = \sum^{N}_{i=0}\frac{\partial z}{\partial o_k} \cdot\frac{\partial o_k}{\partial w}
Veamos el ejemplo anterior que sucede en el caso del backward:

Regla de la cadena en la red anterior.

  • \frac{\partial L}{\partial f(G)}  = \partial out
  • \frac{\partial L}{\partial f(E)}  =\frac{\partial L}{\partial f(G)} \cdot\frac{\partial f(G)}{\partial f(E)}
  • \frac{\partial L}{\partial f(F)}  =\frac{\partial L}{\partial f(G)} \cdot\frac{\partial f(G)}{\partial f(F)}
  • \frac{\partial L}{\partial f(D)}  =\frac{\partial L}{\partial f(G)} \cdot\frac{\partial f(G)}{\partial f(D)}
  • \frac{\partial L}{\partial f(B)}  =\frac{\partial L}{\partial f(G)} \cdot\frac{\partial f(G)}{\partial f(E)} \cdot\frac{\partial f(E)}{\partial f(B)} + \frac{\partial L}{\partial f(G)} \cdot\frac{\partial f(G)}{\partial f(D)} \cdot\frac{\partial f(D)}{\partial f(B)}
  • \frac{\partial L}{\partial f(C)}  =\frac{\partial L}{\partial f(G)} \cdot\frac{\partial f(G)}{\partial f(F)} \cdot\frac{\partial f(F)}{\partial f(C)} + \frac{\partial L}{\partial f(G)} \cdot\frac{\partial f(G)}{\partial f(D)} \cdot\frac{\partial f(D)}{\partial f(C)}
  • \frac{\partial L}{\partial f(A)}  =\left ( \frac{\partial L}{\partial f(G)} \cdot\frac{\partial f(G)}{\partial f(E)} \cdot\frac{\partial f(E)}{\partial f(B)} + \frac{\partial L}{\partial f(G)} \cdot\frac{\partial f(G)}{\partial f(D)} \cdot\frac{\partial f(D)}{\partial f(B)} \right ) \cdot \frac{\partial f(B)}{\partial f(A)} + \\\left ( \frac{\partial L}{\partial f(G)} \cdot\frac{\partial f(G)}{\partial f(F)} \cdot\frac{\partial f(F)}{\partial f(C)} + \frac{\partial L}{\partial f(G)} \cdot\frac{\partial f(G)}{\partial f(D)} \cdot\frac{\partial f(D)}{\partial f(C)}\right ) \cdot \frac{\partial f(C)}{\partial f(A)}

Como podemos observar, se crean unos carros de derivadas que asustan. ¡Imaginad cuando hablamos de una red neuronal de verdad! Pero no debéis preocuparos demasiado porque todos estos cálculos los hará nuestro ordenador.
Al final gracias a la regla de la cadena, solo debemos definir:

  • La operación de un nodo.
  • La derivada de la salida respecto la entrada de ese nodo (para propagar hacia atrás).
  • La derivada de la salida respecto sus pesos (para que estos puedan ser corregidos).

En el próximo articulo empezaremos a estructurar nuestro código para que todo esto sea posible.

Referencias: