Redes neuronales¶


Clase4: Entrenamiento¶


Autor: Luis Fernando Apáez Álvarez


Contenido¶

  • Propagación hacía adelante (fordward propagation)
  • Propagación hacía atrás (backpropagation)
  • Descenso del gradiente

El objetivo del entrenamiento de una red neuronal consiste en hallar el valor óptimo para el vector de pesos de la red, donde el error cometido por la red respecto a los valores reales debe ser mínimo. Con base en lo anterior, el siguiente paso sería evaluar el modelo obtenido con el conjunto de datos de prueba.

Es preciso mencionar que en este procedimiento de entrenamiento, al intentar minimizar de más el error, no obtendremos necesariamente un mejor modelo, más bien corremos el riesgo de un sobreajuste, es decir, especializar demasiado la red, la cual no permitirá generalizar el modelo para datos que no sean con los cuales ya trabajó previamente. En conclusión, se intentará minimizar el error pero se mantendrá cierto margen del mismo para evitar el sobreajuste.

Asimismo, mencionamos algunas condiciones de parada que nos especificaran cúando debemos de parar el entrenamiento de la red:

  • Hemos alcanzado el error que está dentro del margen propuesto.
  • Se ha llegado a un número máximo de itereaciones como tope del entrenamiento.
  • Se ha llegado a un punto de saturación donde por más iteraciones que pasen, el error no lográ disminuir más.

Propagación hacia adelante (fordward propagation) ¶

Los valores de las neuronas de una capa inferior son propagados hacía las neuronas de la capa superior por medio de las conexiones, donde para cada capa se efectúan una serie de operaciones matemáticas. Básicamente, la propagación hacía adelante es un proceso de izquierda a derecha, partiendo de la capa de entrada de la red hasta la capa de salida.

Considerando la arquitectura de red neuronal

red001.PNG

Una vez que se realiza el proceso de propagación hacia adelante obtendremos una salida como resultado, luego, obtendremos también un valor para la función de error (función que, de manera conjunta, contabiliza los errores comparando los valores de predicción con los valores reales). Luego, el proceso volverá a repetirse modificando los vectores de pesos de modo que se busca minimizar la función de error. Al implementarse el algoritmo de propagación hacia adelante, la actualización de los pesos para obtener un mejor modelo se hace respecto a los pesos de cada posible camino, lo cual es computacionalmente costoso, en especial cuando se tienen muchas neuronas, capas y conexiones neuronales. De tal manera, será necesario hallar otro algoritmo que mejore lapropagación hacía adelante.

Propagación hacia atrás (backpropagation) ¶

back.jpeg

La idea básica detrás del algoritmo de backpropagation es ir calculando los errores hacía atrás. Sucede solo durante el entrenamiento de la red neuronal. La propagación hacia atrás es lo que hace que la red neuronal aprenda, es decir, sepa cada vez hacer mejores predicciones. Esta propagación se realiza con cualquier algoritmo de optimización numérica, por ejemplo el descenso de gradiente estocástico (SGD), ADAM, etc. Después de que se haya realizado la propagación hacia adelante sucede lo siguiente:

  • La red neuronal da un resultado.
  • Con ese resultado se aplica una función de error. Esta función calcula la cantidad de error que hay entre el resultado predicho y el resultado real.
  • Una vez que se tiene el error, este se propaga hacia atrás con el objetivo de mejorar nuestra red neuronal y que cada vez la cantidad de error sea menor. Es decir sucede lo contrario que en la propagación hacia adelante, aquí se calcula el error al final, y se propaga ese desde las últimas capas hasta las primeras de derecha a izquierda.

El proceso de propagar el error hacia detrás matemáticamente es el siguiente:


Una vez que calculamos el error, digamos que dicho valor es $\delta$. Lo propagamos hacia atrás, a todas las neuronas de cada capa. Para ello calculamos el error $\delta_1$ de cada neurona. Este se calcula, multiplicando el peso $w$ de dicha neurona, por el error $\delta_2$ de la capa siguiente por la derivada de la función de perdidas $J$ que a su vez es multiplicado por el input que le llega a la capa de salida ($a11$). Replicando esta operación en cada neurona tendremos el error $\delta$ de todas las neuronas. La fórmula matemática es:

$$ \delta_1^{(1)}=w_{1}\cdot \delta_1^{(2)}\cdot J'(a_{11}) $$

Este proceso es más conocido como la regla de la cadena. Una vez tenemos todos los deltas de cada neurona, para actualizar su peso $w$, lo que hacemos es, a cada peso $w$ de cada neurona le restamos el resultado de la multiplicación del $\delta$ (error) por el ratio de aprendizaje $\alpha$ por el input que le llego a esa unidad en la propagación hacia adelante $\phi$. La fórmula matemática es:

$w=w-\alpha\cdot \delta\cdot \phi$

En resumen, podemos decir:

  • La idea de Backporpagation es comparar el valor de salida de la Red con el valor deseado del conjunto de entrenamiento e ir reduciendo dichos errores modificando los pesos de cada conexión. Este proceso se realiza capa por capa, de modo que solo se modifican los pesos de una capa y no todos los posibles como se realizaba con el algoritmo de propagación hacia adelante.

  • El algoritmo consiste en calcular las derivadas parciales de la función de pérdida o coste$^{(1)}$ de los parámetros, es decir, en función de los pesos asignados a la información recibida de cada neurona de la capa anterior y el sesgo (o bias). Con esto, se podrá conocer la influencia de cada una de las neuronas en el resultado final a través del vector gradiente, que posteriormente, utilizará el método del Descenso del Gradiente, para el aprendizaje de la Red. El método del descenso del gradiente es un método muy utilizado en general en Machine Learning, mientras que el Backpropagation es el que se aplicará de forma más concreta en una Red Neuronal. Más adelante se hablará de él con mayor profundidad.


$(1)$ La función de coste representa la suma del error, la diferencia entre el valor predicho y el valor real (etiquetado). Un ejemplo de función de coste el es error cuadrático medio:

$$ mse=\sum_{i}\frac{1}{n}(y_{i}-\bar{y_{i}})^{2} $$

Parte matemática del algoritmo¶

Backpropagation trata, básicamente, de entender cómo el cambio de los pesos y de los sesgos impacta en la función de coste, lo cual no llevará indudablemente al cálculo de derivadas.

El problema es que, en muchos casos, el cálculo de los parámetros que minimizan una cierta función de coste puede resultar computacionalmente inabordable. En una red neuronal, por ejemplo, podemos tener cientos de miles de pesos y de sesgos. Es por ello que se suele recurrir a algoritmos como el descenso de gradiente para el cálculo de estos mínimos. Pero, por supuesto, aplicar el algoritmo de descenso de gradiente implica el cálculo del gradiente de la función de coste en un punto concreto de su dominio (en un punto determinado por unos parámetros concretos), lo que a su vez implica el cálculo de la derivada parcial de la función de coste con respecto a todos y cada uno de los parámetros que van a determinar el funcionamiento de la red neuronal. Dicho cálculo de derivadas parciales suele ser costoso, pues en el pasado, dichos cálculos se efectuaban mediante la fuerza bruta.

Y es aquí donde el algoritmo de backpropagation llega para resolver el problema:

  • Sea $C$ la función de coste asociada a una red neuronal.
  • Sea $w$ que representa un peso o un sesgo de la red.

Entonces $\frac{\partial C}{\partial w}$ nos dice qué tan rápido cambia el coste cuando cambiamos el los pesos y los sesgos. Luego, denotemos por $w^{l}_{jk}$ como el peso asociado a la conexión neuronal entre la neurona $k$ y la neurona $j$, referente a la capa $l$. De forma análoga tendremos que $b^{l}_{j}$ representa el sesgo asociado a la neurona $j$ en la capa $l$ y $a^{l}_{j}$ corresponde a la activación de la neurona $j$ en la capa $l$.

red0001.PNG

Con base en lo anterior, podemos establecer que la activación de la neurona $j$ en la capa $l$ está relacionada con las activaciones en la capa $l-1$ mediante la ecuación:

$$ a_{j}^{l}=f\left(\sum_{k}w^{l}_{jk}a^{l-1}_{k}+b^{l}_{j}\right) $$

donde estamos actuando sobre las neuronas $k$ de la capa $l-1$ y donde $f$ es una función de activación. Podemos considerar todos los pesos asociadosa las conexxiones como un vector, así como las activaciones y los bias, de donde la ecuación anterior se puede representar matricialmente como

$$ a^{l}=f\left(w^{l}a^{l-1}+b^{l}\right) $$

Continuando, diremos que $\delta_{j}^{l}$ representa el error en la neurona $j$ de la capa $l$, de donde el algoritmo de backpropagation nos dará un procedimiento para hallar los valores de $\delta_{j}^{l}$ relacionados con las derivadas parciales $\frac{\partial C}{\partial w^{l}_{jk}}$ y $\frac{\partial C}{\partial w^{b}_{j}}$. Podemos entender dichas cantidades $\delta_{j}^{l}$ como sigue:

Una vez que obtenemos el error general de la red neuronal en una iteración, dicho error será propagado hacia atrás. Consideremos que una neurona en particular recibe de la capa anterior una ponderación de entrada de $z^{l}_{j}$, entonces, dicha neurona en vez de generar el valor de $f(z^{l}_{j})$, generará el valor de $f(z^{l}_{j}+\Delta z^{l}_{j})$, es decir, se ha agregado un pequeño cambio en la salida. De nuevo, dicho cambio será propagado hacia atrás, lo cual, finalmente, hará que la función de coste cambie en una cantidad $\frac{\partial C}{\partial z^{l}_{j}}\Delta z^{l}_{j}$. En el proceso, el algoritmo trata de hallar un valor del error $\delta_{j}^{l}$ cada vez más pequeño, minimizando así la función de coste. Con base en todo lo anterior establecemos que

$$ \delta_{j}^{l}=\frac{\partial C}{\partial z^{l}_{j}} $$

será nuestra medida del error. A continuación presentaremos 4 ecuaciones fundamentales detrás del algoritmo de backpropagation.

  1. La ecuación del error en la capa de salida está dada por $\delta^{L}_{j}=\frac{\partial C}{\partial a^{L}_{j}}\sigma'(z_j^{L})$. Donde la derivada parcial mediará qué tan rápido cambia el coste en función de la activación de la salida $j-$ésima. Por ejemplo, si la función $C$ no depende mucho de la salida particular de la neurona $j$, entonces $\delta^{L}_{j}$ será pequeño. El segundo término $\sigma'(z_j^{L})$ mide la rapidez de la función de activación mientras está cambiando $z_j^{L}$. Todos los términos englobados anteriormente son fácilmente computables. Dado que queremos la forma en matriz de la definición de $\delta^{L}_{j}$ para el algoritmo de backpropagation, entonces podemos reescribir dicha ecuación como: $\delta^{L}_{j}=\nabla_{a}C\cdot \sigma'(z^{L})$. Donde $\nabla_{a}C$ puede pensarse como la tasa de cambio de la función de coste con respecto a todas las activaciones de salida. En este caso, la operación $\cdot$ representa el producto de Hadamard), el cual es básicamente el producto entrada a entrada entre matrices.
  1. Ahora representaremos el error en una capa dada a partir del error en la capa siguiente. Dicho error estará dado por: $\delta^{l}=((w^{l+1})^{t}\delta^{l+1})\cdot \sigma'(z^{L})$. Donde $(w^{l+1})^{t}$ es la transpuesta de la matriz de pesos para la capa $l+1$. Consideremos por ejemplo que conocemos el valor de $\delta^{l+1}$ en la capa $l+1$. Al aplicar la matriz $(w^{l+1})^{t}$ podemos pensar, intuitivamente, en que dicha acción "moverá" el error hacia atrás a través de la red. Luego, $\cdot \sigma'(z^{L})$ mueve el error hacía atrás a través de la función de activación en la capa $l$. dándonos así el error $\delta^{l}$ en la entrada ponderada a la capa $l$. Combinando lo dicho anteriormente con el punto 1, somos capaces de calcular todos los errores $\delta^{l}$ de la red mediante propagación hacia atrás.
  1. Por otro lado, calcularemos la tasa de cambio del coste con respecto a cualquier sesgo. Dicho valor está dado por: $\delta^{l}_{j}=\frac{\partial C}{\partial b^{l}_{j}}$. Esto es, el error $\delta^{l}_{j}$ es exactamente la tasa de cambio de la función de coste respecto al sesgo asociado. De forma abreviada, dicha ecuación queda como: $\delta=\frac{\partial C}{\partial b}$.
  1. Finalmente, consideramos $\frac{\partial C}{\partial w^{l}_{jk}}=(a_{k}^{l-1}-\delta^{l}_{j})$, que representa la ecuación para la tasa de cambio de la función de coste con respecto a cualquier peso de la red. En este caso, ocupamos la función de coste mse para tener de dicha forma la ecuación, pues ésta dependerá de la función de coste que ocupemos. Dicha ecuación nos dice como calcular las derivadas parciales de la función de coste en términos de $\delta^{l}$ y $a^{l-1}$. La ecuación puede reescribirse como $\frac{\partial C}{\partial w}=a_{in}\delta_{out}$, donde $a_{in}$ es la activación de la entrada de neuronas al peso $w$, y $\delta_{out}$ es el error de salida de neuronas del peso $w$. Si consideremos el caso en el que la activación es pequeña, entonces la parcial $\frac{\partial C}{\partial w}$ tenderá a ser pequeña. Así, diremos entonces que el peso aprende lentamente, lo cual implica que dicho peso no cambiará mucho durante el descenso del gradiente

Descenso del gradiente ¶

El descenso del gradiente es un método de optimización en el cual se toman las primeras derivadas de la función de coste. Eso nos da información puntual sobre la pendiente de la función, pero no sobre su curvatura. Podríamos calcular las segundas derivadas, de forma que pudiéramos conocer también cómo varía el gradiente, pero eso supondría un elevado coste computacional. Existen algunas técnicas de aproximación donde se estiman esas segundas derivadas con un uso de memoria limitado, pero lo más utilizado son optimizaciones sobre el descenso del gradiente estocástico.

Consideremos por ejemplo el siguiente gráfico

desc1.PNG

Podemos entender intuitivamente el algoritmo del descenso del gradiente, para la función cuya curva es de color azul, como el proceso en el cual partiremos de un punto aleatorio sobre la curva y mediante iteraciones dicho punto se "moverá" intentando buscar el mínimo de la función. Si consideramos un punto en la curva en el tiempo $t$, digamos $x_{t}$. En la siguiente iteración tendremos que $x_{t+1}=x_{t}-\alpha f'(x_{t})$, esto es, al punto $x_{t}$ le restamos la pendiente, pues ésta nos dará la dirección a la cual nos moveremos. Por ejemplo, si $f'(x_{t})>0$, entonces $x_{t+1}$ será mayor a $x_{t}$, lo cual nos dice que $x_{t}$ debe moverse a la derecha en busca del punto mínimo. Al valor $\alpha$ se le conoce como tasa de aprendizaje.

Con base en lo anterior podemos establecer un primer algoritmo para el descenso del gradiente:

In [18]:
# Librerias necesarias
import sympy as sm
import random as rd

# Definimos a x como un simbolo
x = sm.symbols('x')
# Funcion en cuestion
y = 'x ** 2'
# Tasa de aprendizaja
a = 0.01
# Funcion definida por el usuario para evaluar la derivada en un 
# punto dado x0
def derivada(x0):
    return eval(str(sm.diff(y,x,1)), {'x':x0})

# Inicializamos un primer valor de x0 de manera aleatoria
x0 = rd.random()
# Evaluamos la funcion en x0
y0 = eval(y, {'x': x0})

# Iteramos 1000 veces
for i in range(1000):
    # Hacemos la actualizacion
    x0 -= a * derivada(x0)
    y0 = eval(y, {'x': x0})
# Veamos los valores hallado
print(x0, y0)
6.341698748332135e-10 4.0217143014597363e-19

Obtenemos que $x0$ es extremadamente cercano a cero, así como $f(x0)$. Lo cual representa una muy buena aproximación pues sabemos que el mínimo de $f(x)=x^{2}$ se alcanza en $x=0$.

Para que la clase no sea tan pesada, continuaremos abordando el algoritmo del descenso del gradiente en la siguiente clase.


Notas¶

  • Propagación hacía adelante y hacía atrás

  • Descenso del gradiente

  • Aprendizaje del perceptrón multicapa

  • Descenso por Gradiente