Binary Coffee

Un acercamiento a las Redes Neuronales

AI
Las [Redes Neuronales](https://es.wikipedia.org/wiki/Red_neuronal_artificial#:~:text=Las%20redes%20neuronales%20artificiales%20(tambi%C3%A9n,entre%20s%C3%AD%20para%20transmitirse%20se%C3%B1ales.)) son un modelo computacional que simula el comportamiento real de las neuronas dentro de nuestros cerebros. Básicamente son un conjunto de "neuronas" conectadas entre si, que procesan un determinado conjunto de entrada y generan un conjunto de salida. Para entender mejor este proceso pongamos un ejemplo ***hipotético***: Un estudio realizado a una población de personas demuestra que el color de los ojos de un individuo depende de: 1. Color de los ojos del padre. 2. Color de los ojos de la madre. 3. Grupo sanguíneo de la madre. 4. Grupo sanguíneo del padre. El estudio resumió que cuando las características del padre son: (color de los ojos: azul, grupo sanguíneo: O+) y las características de la madre son: (color de los ojos: café, grupo sanguíneo: B+) el individuo nace con los ojos verde. Así se definieron 2 reglas más para: los colores de los ojos café y azules. Ahora si existiera un algoritmos que procesara estos datos y fuera capaz de "aprender", podíamos determinar para cualquier pareja, cuyas características oscilen en la media de las estudiadas, el color de los ojos de sus hijos. Básicamente esto es posible mediante las **Redes Neuronales Artificiales**. El funcionamiento a escala más básica de una red neuronal se puede describir con la siguiente imagen: ![alt](https://api.binary-coffee.dev/uploads/model1_9f72bdb45e.png) Podemos observar un modelo con un conjunto de entradas ```(x1, x2 ...)```, un conjunto de selectores ```(w1, w2, ...)``` ```(u1, u2 ...)``` y un conjunto de salida ```(y1, y2 ...)```. El conjunto de salida depende directamente del valor de cada selector, por lo que podemos deducir que modificando los selectores para una misma entrada podemos obtener diferentes valores de salida. Ahora conociendo que para determinado conjunto de entrada existe un conjunto de salida, el problema seria encontrar la configuración exacta de los selectores que minimice la diferencia entre la salida esperada y la salida real del sistema. En esto consiste lo que se conoce como **Entrenamiento de la red o Proceso de Aprendizaje**. ## Minimizando el error Como mencionábamos el objetivo principal es minimizar el error o lo que es lo mismo la diferencia que existe entre la salida que produce la red y la salida esperada. Este error lo podemos minimizar haciendo uso de las derivadas. Para encontrar el valor que minimiza una determinada función se procede igualando la primera derivada a cero, pero no es muy computable el cálculo de derivadas y menos cuando la función depende de más de una variable, todo esto se simplifica utilizando el algoritmo del **Deseoso del Gradiente**. ### Descenso del Gradiente El algoritmo lo que nos dice es que el valor que minimiza una función lo determina ir en contra de la primera derivada y esto es lo más lógico ya que la primera derivada nos da la pendiente de la recta tangente a un determinado punto de la curva, e ir en contra de esta pendiente significa descender por la curva. Veamos esto gráficamente: ![alt](https://api.binary-coffee.dev/uploads/gradiente_34827ca444.png) Veaemos un ejemplo. Suponiendo la función: $\~{\large\textcolor{#00252d}{F(x) = x^2}}$ Para encontrar el mínimo comencemos por un valor de x aleatorio. $\~{\large\textcolor{#00252d}{x_0=2}}$ Ahora la regla nos dice que: $\~{\large\textcolor{#00252d}{dx=-F`(x)}}$ y $\~{\large\textcolor{#00252d}{dx=x_1-x_0}}$ por tanto $\~{\large\textcolor{#00252d}{x_1=x_0 - \alpha F`(x_0)}}$ * Note que a la derivada la multiplicamos por un factor alpha, de no ser así los saltos serían demasiado grande y nunca encontraríamos el mínimo. A este factor se le denomina **Razón de aprendizaje**. Para un valor de $\alpha = 0.5$ se puede apreciar que el valor mínimo se encuentra en la iteración número 7 con un error de 0.0008. x | F(x) | F'(x) --------|--------|-------- x0=2 | 4 | 4 x1=1,2 | 1.44 | 2.4 x2=0.72 | 0.51 | 1.44 x3=0.43 | 0.18 | 0.86 x4=0.25 | 0.06 | 0.5 x5=0.15 | 0.02 | 0.3 x6=0.09 | 0.0008 | 0.1 * Hay que tener en cuenta que este método solo encuentra mínimos locales por lo que se deberán probar con diferentes valores de inicio para su completa efectividad. ## Comportamiento de una neurona Ahora veremos que ocurre dentro de la neurona: ![alt](https://api.binary-coffee.dev/uploads/neurona_9644461ca0.png) Toda neurona cuenta con un determinado número de entradas ```(z1, z2, z3)``` y cada entrada esta determinada por un valor denominado peso ```(w1, w2, w3)```. También posee una conexión, propia de cada neurona, a cuyo peso se le denomina *umbral de activación* ```(u)```. A la salida de la neurona ```(a)``` se le denomina **Activación** y esta determinada por una **Función de activación**. Existes una gran variedad de funciones de activación como: * Sigmoidea * Tangente hiperbólica * ReLU * Softmax En este caso estaremos trabajando con la sigmoidea: ![alt](https://api.binary-coffee.dev/uploads/sigmoide_2ff2ad72d5.png) $\~{\large\textcolor{#00252d}{F(x)=(1+e^{-x})^{-1}}}$ Y su derivada cumple con la siguiente propiedad: $\~{\large\textcolor{#00252d}{F`(x) = F(x)(1-F(x))}}$ La función de activación recibe como parámetro una suma ponderada determinada por las entradas, los pesos y el umbral: $\~{\large\textcolor{#00252d}{a=F(u + z_1 * w_1 + z_2 * w_2 + z_3 * w_3)}}$ Mientras mayor sean los pesos mayor será la contribución de estos a la activación de la neurona. El umbral determina el comportamiento de la neurona, si el umbral es muy grande en comparación con los pesos, la neurona se excitará al máximo (la salida es el máximo valor que retorna la función de activación) y si el valor del umbral es muy pequeño comparado con los umbrales la neurona se excitará al mínimo (la salida es el mínimo valor que retorna la función de activación). ## Modelo de capas y nomenclatura A continuación les muestro un modelo de una red neuronal: ![alt](https://api.binary-coffee.dev/uploads/capas_5713cd0a6b.png) Toda red neuronal está compuesta por: 1. Una o varias capas. 2. A la primera capa se le denomina *Capa de entrada*. 3. A las capas intermedias (que pueden ser más de una) se les denomina *Capa oculta*. 4. A la última capa se le denomina *Capa de salida*. 5. Las capas pueden contener cualquier número de neuronas. 6. Todas las neuronas de una capa están totalmente conectadas con las neuronas de la capa anterior y posterior. 7. No existe conexión entre neuronas que no sean de capas consecutivas (en el caso del Perceptrón). Dicho esto veremos la nomenclatura necesaria para poder determinar las ecuaciones que rigen a una Red Neuronal. Como mencionábamos las conexiones entre neuronas esta determinada por un peso ```w```. Ahora definiremos como referirnos al peso de cualquier conexión: $\~{ \large\textcolor{#00252d}{W_{i,j}^{k} }}$ Peso que conecta la neurona ```i``` de la capa ```k``` con la neurona ```j``` de la capa ```k+1```. $\~{ \large\textcolor{#00252d}{i = 1,2,3...n_k}}$ $\~{ \large\textcolor{#00252d}{j = 1,2,3...n_{k+1}}}$ Algo similar se hace con los umbrales: $\~{\large\textcolor{#00252d}{u_{i}^{k}}}$ Umbral de la neurona ```i``` de la capa ```k```. $\~{\large\textcolor{#00252d}{i=1,2,3...n_k}}$ Y para representar la salida de cada neurona: $\~{\large\textcolor{#00252d}{a_{k}^{i}}}$ Salida de la neurona ```i``` de la capa ```k```. Ahora generalizando lo que ya vimos de la activación de la neurona: Para ```k=1```: $\~{\large\textcolor{#00252d}{a_{k}^{i} = x_i}}$ Para ```k>1```: $\~{\large\textcolor{#00252d}{a_{i}^{k} = F( u_{i}^{k} + \sum_{j=1}^{n_{k-1}}}a_{j}^{k-1}w_{j,i}^{k-1}) }$ Hasta este momento ya podemos construir una estructura que soporte las operaciones que se suelen realizar en una Red Neuronal y también podemos calcular la salida de la red, pero ... Cómo la red puede aprender?. ## Back Propagation *Back Propagation* es uno de los métodos que posibilita que los pesos y umbrales se modifiquen de manera que se minimiza el error. Su funcionamiento se reduce a propagar el error desde las capas superiores hasta las inferiores, determinando las derivadas parciales de los pesos y los umbrales con respecto al error, y de esta manera poder actualizar sus valores mediante el *Descenso del Gradiente* que ya mencionamos. Veamos un ejemplo: En la siguiente red neuronal se quiere calcular la derivada parcial de $y1$ con respecto al peso $W_{11}^{1}$. ![alt](https://api.binary-coffee.dev/uploads/bkp_01aab4bed6.png) En principio tendríamos que derivar la siguiente función: $\~{\large\textcolor{#00252d}{y_1 = a_1^4}}$ pero $\~{\large\textcolor{#00252d}{a_1^4 = F(u_1^4 + w_{11}^3a_1^3 +w_{21}^3a_2^3)}}$ Y así sucesivamente tendríamos que ir abriendo la expresión hasta que todo quede en función de los pesos y la entrada. Como ves esto supone un trabajo demasiado tedioso y propicio a cometer errores. Afortunadamente existe una regla para obtener estas derivadas y te la muestro a continuación: $\~{\large\textcolor{#00252d}{\frac{\delta y_1}{\delta w_{11}^1} = a_1^4(1-a_1^4)w_{11}^3a_1^3(1-a_1^3)w_{11}^2a_1^2(1-a_1^2)a_1^1 + a_1^4(1-a_1^4)w_{21}^3a_2^3(1-a_2^3)w_{12}^2a_1^2(1-a_1^2)a_1^1}}$ Se puede apreciar el patrón: se multiplica la derivada de la función de activación (evaluada en la salida de cada neurona) por el peso correspondiente de la capa ```i - 1```, así hasta llegar a la capa de entrada. Note que la variable respecto a la cual se deriva no esta presente en la ecuación ya que su derivada es 1. Para el caso de los umbrales es algo parecido, solo hay que tener en cuenta que los umbrales no están conectados a ninguna entrada (inicialmente están conectados a un valor de 1). Veamos un ejemplo: $\~{\large\textcolor{#00252d}{\frac{\delta y_1}{\delta u_{1}^1} = a_1^4(1-a_1^4)w_{11}^3a_1^3(1-a_1^3)w_{11}^2a_1^2(1-a_1^2) + a_1^4(1-a_1^4)w_{21}^3a_2^3(1-a_2^3)w_{12}^2a_1^2(1-a_1^2)}}$ * Note que cada camino que exista entre el peso (respecto al que se esta derivando) y la salida, es una contribución a la derivada y por tanto hay que sumarla. Con esta regla se pueden calcular todas las derivadas parciales de la salida con respecto a los umbrales y los pesos para luego hacer uso del **Descenso del Gradiente**. ## Función Error Hemos estado hablando que para que una red neuronal aprenda es fundamental ir minimizando el error que existe entre la salida real con respecto a la salida esperada, pero no hemos dicho nada acerca del cálculo de dicho error. El error puede ser calculado de diferentes maneras, una muy simple y funcional es asumir el error como la distancia entre dos puntos. Esto es posible ya que según el número de salidas que tenga una red neuronal así será el número de dimensiones de la misma. Ejemplo si nuestra red tiene dos salidas quiere decir que la salida tiene dos dimensiones. Veamos la siguiente figura: ![alt](https://api.binary-coffee.dev/uploads/error1_82f5f5c47d.png) Como se puede apreciar en la figura la región subrayada se forma ya que existen dos salidas ```y1``` y ```y2``` y el dominio de cada una es ```[0, 1]```. Ahora supongamos que la salida real es ```(z1, z2)``` y la salida esperada es ```(s1, s2)```, podemos definir el error de la siguiente manera: ![alt](https://api.binary-coffee.dev/uploads/error2_9e41fbd366.png) $\~{\large\textcolor{#00252d}{error = \sqrt{(s_1-z_1)^2+(s_2-z_2)^2}}}$ A esta fórmula se le pueden hacer algunos cambios que faciliten la expresión a la hora de derivar. Si tenemos que determinar el mínimo a una expresión como la de este tipo podemos omitir la raíz ya que la derivada de una raíz da una fracción donde la raíz queda en el denominador y solo tiene sentido igualar a 0 el numerador. Esto nos quedaría así: $\~{\large\textcolor{#00252d}{error = (s_1-z_1)^2+(s_2-z_2)^2}}$ También pudiéramos multiplicar por una constante y no se alteraría el valor del mínimo: $\~{\large\textcolor{#00252d}{error = \frac{1}{2}(s_1-z_1)^2+ \frac{1}{2}(s_2-z_2)^2}}$ Ahora ya lo tenemos todo planteado, fijémonos que el error depende de ```z1 y z2``` (que son las salidas de nuestra red) y estas a su vez dependen de los pesos y los umbrales, con esto ya podemos determinar las derivadas parciales del error con respecto a los pesos y los umbrales. Nos queda: $\~{\Huge\textcolor{#00252d}{\frac{\delta error}{\delta \star} = \frac{\delta error}{\delta z_1}\frac{\delta z_1}{\delta \star} + \frac{\delta error}{\delta z_2}\frac{\delta z_2}{\delta \star} }}$ pero $\~{\Huge\textcolor{#00252d}{\frac{\delta error}{\delta z_i} = z_i - s_i }}$ y $\~{\Huge\textcolor{#00252d}{\frac{\delta z_i}{\delta \star}}}$ ya la calculamos anteriormente, donde la "estrella" se sustituye por cada peso y cada umbral. ## Resumen Con este fundamento matemático ya podemos implementar un Perceptrón Multicapa y entrenarlo, a continuación les muestro un resumen de los pasos a seguir: 1. Calcular la salida general de nuestra red para cada conjunto de entrada (esto se logra calculando las salidas de las neuronas desde la capa ```1``` a la capa ```n```). 2. Calcular el error general y propagarlo hacia atrás utilizando el método Back Propagation (esto se reduce al cálculo de todas las derivadas parciales de los pesos y los umbrales con respectos al error). 3. Modificar cada peso y cada umbral utilizando la fórmula del Descenso del Gradiente. 4. Repetir estos pasos hasta lograr el menor error posible (a la cantidad de veces que se repite este proceso se le conoce como **número de épocas**) Con este artículo te expliqué de manera breve el modelo matemático de una red neuronal en el siguiente artículo te mostraré como se puede implementar esta estructura en un lenguaje de programación. Hasta la próxima...
Opiniones