En este conjunto de artículos voy a tratar de explicaros como crear nuestra propio framework para ejecutar una red neuronal. ¿¡Suena alucinante verdad!?

Aunque debéis saber que:

  • No vamos a usar criterios de optimización, lo que buscamos es aprender.
  • No competiremos con frameworks de verdad (ni podremos).
  • Trabajaremos en Python por su simpleza y belleza (aunque a veces no es perfecto).
  • Puede ser que volvamos a redefinir algunas clases o métodos para resolver problemas o mejorarlas.
  • Seguro que hay mejores formas de resolver los problemas, estoy abierto a cualquier tipo de opinión.
  • Pueden existir pequeños cambios con respecto la ultima versión. Lo mejor es que ante cualquier duda le deis un vistazo al github del proyecto.
  • Existen muchos detalles de la implementación que no explico en los artículos, lo mejor es tener al lado el github del proyecto para entender los conceptos al 100%.
GITHUB: https://github.com/adriaciurana/scratch-network


¿Que es una red neuronal? Las ultimas tendencias han hecho que esta palabra resuene por todos lados. Una red neuronal es un conjunto de neuronas interconectadas entre sí que transforman la información y la pasan a otras neuronas.
Este proceso se hace para muchas neuronas y muchas conexiones consiguiendo que la información se vaya transformando hasta que obtenemos la salida que deseamos.
Os recomiendo encarecidamente el siguiente vídeo:

Los que estáis algo familiarizados con una red neuronal, podríamos decir que para entrenar una red y luego ejecutarla necesitaremos los siguientes módulos:

  • Capas: Una red neuronal tiene capas que están interrelaciones entre ellas. Básicamente transforman información.
  • Losses: Un conjunto de funciones que nos permiten evalúa el error que estamos cometiendo.
  • Metricas: Son similares a las losses evalúan el error, pero no intervienen en el proceso de aprendizaje. Normalmente son funciones que no son diferenciables. Por ejemplo: accuracy.
  • Optimizadores: Es el que se encarga de unir los dos conceptos anteriores y tratar de minimizar el error. Es decir, usan la información de las losses para corregir los pesos de las capas.

¡Vamos a empezar entonces!

Planteamiento del problema y terminología

Una red neuronal tiene un aprendizaje como lo haría un niño, se basa en la experimentación (dataset), la intuición (descenso gradiente) y alguien que les diga si lo están haciendo bien o mal (loss).
Cuando tratamos de aprender la red vamos a tener que asociar datos que conocemos su resultado, así que se los pasaremos a la red y le pediremos que falle lo mínimo posible (obvio). La forma de cuantificar el error la conocemos como Loss esta a demás recordemos que debe ser diferenciable porque será la que nos guié hacia la solución.
Así que al final una red neuronal no es más que tratar de resolver un problema de minimización usando una función extremadamente compleja.

min_{\sigma}\int_{i \in batch} \frac{1}{N} Loss \left ( F_{\sigma}(X_i), Y_i \right )
F es extremadamente compleja y tiene muchísimo parámetros (que son neuronas o forman parte de neuronas), estas actúan con armonía para transformar la información y darnos la estimación que deseamos. Tratan de ser el nexo de unión entre los datos de entrada y la predicción real que tenemos en nuestro dataset.
Como la red F tiene tantísimos parámetros, necesitamos muchísimas muestras. Cuanto más complejo es nuestra red más datos necesitaremos.
Pero a la vez tener que calcular la anterior función para todas las muestras requiere muchísimo tiempo, tiempo que no tenemos (porque F es muy compleja).
Para solucionar este problema realizaremos una suposición importante: cogeremos N muestras aleatorias y supondremos que estas N muestras representan al conjunto real de datos. Estas N muestras es lo que conocemos como batch.
¿Que quiere decir esto? Que el conjunto de muestras que tomamos y el conjunto de muestras total siguen distribuciones similares.
Por ejemplo si tenemos 100 muestras (50 positivas y 50 negativas, 50 hombres y 5o mujeres) y seleccionamos 4, deberíamos coger 4 muestras de las cuales:

  • 1 debería ser positiva y hombre.
  • 1 debería ser positiva y mujer.
  • 1 debería ser negativa y hombre.
  • 1 debería ser negativa y mujer.

¡Sino la distribución es distinta al problema que realmente queremos resolver!
En fin, en cada batch debemos intentar representar toda nuestra población. A veces el batch es tan pequeño que no es capaz de representar la población porque le faltan muestras (en el caso anterior si el batch fuese inferior a 4) y entonces nuestra red puede aprender mal (aunque en un futuro veremos si aprender «mal» es lo que realmente nos interesa).
Si lo aplicamos para resolverlo un descenso de gradiente (podéis ver como funciona en uno de mis artículos) al aprender con batches en vez de con todo, es lo que conocemos como  Batch Gradient Descent. Si por lo contrario cogemos un batch = 1 se conoce como Schocastic Gradient Descent (SGD).
Podéis encontrar más información en : http://yann.lecun.com/exdb/publis/pdf/lecun-98b.pdf

Estructurando la red

Como bien dicen las palabras red neuronal, está esta compuesta por una red de neuronas que se pasan información, la transforman y se la pasan a la siguiente.
Así que a primera instancia, debemos pensar que estructura tiene una red neuronal y como podemos representarla en nuestro proyecto.

Una posible red neuronal…

Una red neuronal siempre está compuesta por un conjunto de nodos y un conjunto de relaciones. ¡Por lo tanto es un grafo!
Los nodos son realmente las capas, las losses o las métricas. Elementos que se encargan de transformar dicha información. Y cada nodo puede tener un conjunto de entradas y salidas donde la información entra, se procesa y sale.

Representación de un nodo. En verde las entradas a la nodo, en azul las salidas.

Podemos observar algunas propiedades que aportan mucha información sobre como deberemos modelizar la red:

  • No tiene ciclos.
  • Dispone de nodos solo con entradas (inputs) y nodos solo con salidas (outputs) y nodos intermedios, por lo que se fluye hacia una dirección.
  • No existen bidireccionalidad en las relaciones.

De modo que, nuestra red neuronal puede ser representada usando un grafo aciclico dirigido (DAG).
Este seria nuestro nodo a grandes rasgos:

Propagando la predicción (forward):

Cada capa tiene una función especifica y estas se van mezclando para que a partir de soluciones parciales a pequeños problemas, podamos crear una solución compleja de nuestro problema real. Una red es un problema que se resuelve haciendo un divide y vencerás.

Como vemos en la anterior figura, esta vez los hemos enumerado. A es la entrada de nuestra red y G es la salida. Los nodos dependen de la información de los demás nodos para poder hacer su función, así que vamos a analizar las dependencias que tienen entre sí:

  • A: no depende de nada, por lo tanto es nuestra entrada de datos. Y no debe esperar a nadie.
  • B: depende de A.
  • C: depende de A.
  • D: depende de B y C.
  • E: depende de B.
  • F: depende de C.
  • G: depende de E, D y F.

Estas dependencias nos permite saber cada nodo que entradas debe esperar antes de poder realizar su calculo.
G será la que nos diga el resultado de la red, esta será la encargada de aportarnos la solución real al problema.

Propagando el error (backward):

Hemos propagado la predicción, ¿Pero que sucede si no es suficientemente buena? ¡Que debemos corregirla! Esto es lo que se conoce como algoritmo de backpropagation, este algoritmo usa la propagación hacia adelante (forward) para conocer cuantitativamente cuanto se equivoca y después propaga hacia atrás para corregir los parámetros (backward). Por ahora nos centraremos en como se hace el backward en los nodos.
La única capa que conoce realmente el error con respecto al target real es G. La capa G deberá ser la encargada de empezar a propagar esta corrección hacia atrás.

Algoritmo backpropagation.

Ahora las dependencias se invierten.

  • G: no tiene dependencias.
  • E: depende de G.
  • F: depende de G.
  • D: depende de G.
  • B: depende de E y D.
  • C: depende de D y F.
  • A: depende de A y C.

Ahora sabemos que debemos realizar una propagación hacia adelante para estimar y que cuando queremos corregir la red debemos realizar una propagación hacia atrás. Pero no sabemos ni como se estima ni como se corrigen (entraremos en detalle más adelante).

De la teoría al código:

Vamos a tratar de programar solamente lo que hemos aprendido hasta ahora.

Y esto ha sido todo por este primer articulo, puede parecer que no hemos aprendido demasiado pero no es así. Hemos creado los cimientos para poder ejecutar la red neuronal, en el próximo articulo os explicaré como propagar el calculo y como corregir los pesos de cada capa.

Referencias:

 

Deja un comentario