El algoritmo de descenso de gradiente (Gradient Descent) es probáblemente uno de los algoritmos de optimización más utilizados en Machine Learning y Deep Learning hoy en día. En este artículo voy a explicar los principios en los que se fundamenta este algoritmo, resolviendo todos los cálculos necesarios para justificar la procedencia de las ecuaciones que podemos encontrar en diferentes cursos y publicaciones de Machine Learning y Deep Learning.
Antes de comenzar...
Para comprender completamente el artículo, existen algunos conceptos que se deben conocer. A grandes rasgos y de manera informal:
- Grafo computacional: Son grafos dirigidos cuyos nodos se corresponden con una operación o una variable y los arcos con valores de entrada/salida.
- Regla de la cadena: Es una fórmula para calcular la derivada de la composición de dos o más funciones.
- Notación de Leibniz: Es un tipo de notación para representar derivadas.
Vamos a ver un ejemplo que combina los tres conceptos:
- Supongamos que tenemos la siguiente función compuesta $f(g(h(x)))$ (tenemos una función externa $f$, una función interna $g$ y una función interna $h$)
- Supongamos ahora que $f(x)=e^{sin(x^2)}$. Esta función podría descomponerse de la siguiente manera:
- $f(x)=e^x$
- $g(x)=sin(x)$
- $h(x)=x^2$
Su grafo computacional sería el siguiente:
Si queremos calcular la derivada de $f(g(h(x)))$ con respecto a $x$, tenemos que utilizar la regla de la cadena, que en notación Leibniz sería de la siguiente manera:
Si nos fijamos en el grafo computacional, lo que estamos haciendo aplicando la regla de la cadena es derivar cada uno de los nodos con respecto a sus entradas:
El resultado de cada una de las derivadas sería el siguiente:
Por último, el resultado de la derivada de $f$ respecto a $x$, sería la multiplicación de las anteriores:
Introducción
Vamos a comenzar viendo el ejemplo más sencillo posible de RNA utilizando regresión logística. Se presupone que el lector posee conocimientos básicos sobre regresión logística y sus ecuaciones. A continuación, se presenta su grafo computacional:
En la estructura que se muestra anteriormente, se reciben un conjunto de datos de entrada, se realizan algunas operaciones sobre estos datos y finalmente se obtiene un resultado. Traducido a un ejemplo real, como la predicción del coste de una casa en función del número de habitaciones y sus metros cuadrados, se reciben como entrada el número de habitaciones y los metros cuadrados de la casa, y se obtiene como resultado el precio de la casa.
¿Qué es lo que provoca que el algoritmo realice una predicción acertada? La respuesta a esta pregunta no es sencilla, sin embargo, una de las cosas más influyentes, son los parámetros del algoritmo, en nuestro grafo computacional w1
, w2
y b
.
Estos parámetros reciben un valor que “ajusta” el algoritmo, es decir, influye en las operaciones que se realizan con el objetivo de obtener un valor de salida óptimo (entendiendo por óptimo la mejor predicción posible para nuestro caso de uso en particular).
¿Cómo encontramos el valor de los parámetros que nos permita realizar predicciones acertadas? Este tipo de problemas se conocen como problemas de optimización, en los que se utilizan algoritmos como Gradient Descent. Para encontrar el valor óptimo de w1
,w2
y b
se requieren dos componentes adicionales, un conjunto de datos de entrenamiento, que se corresponde con un conjunto de valores de entrada para los cuales sabemos la predicción correcta, y una función de error, que nos permite evaluar el error de la predicción de nuestro algoritmo con respecto al valor correcto.
Teniendo esto en cuenta, el proceso de optimización seguiría los siguientes pasos de manera iterativa:
- Se realiza una predicción con el conjunto de datos de entrenamiento (asignándole un valor aleatorio a los parámetros de entrada
w1
,w2
yb
si es la primera iteración) - Mediante la función de error, se evalúa la predicción con respecto al valor correcto
- Se modifica el valor de los parámetros
w1
,w2
yb
con el objetivo de minimizar el error de nuestra predicción
Intuición
Antes de comenzar a tratar Gradient Descent, probablemente debamos preguntarnos que significa gradiente en este contexto. Cuándo hablamos de gradiente se puede pensar en un valor que nos permite medir cuanto cambia el output de una función cuando aumentas un poquito el valor de los inputs. En funciones de una sola variable, el gradiente se corresponde con la derivada o pendiente de la función.
Imaginemos que nuestra función de error se corresponde con: $L(x) = (x - 3)^2$
El objetivo es encontrar el mínimo de la función, para ello, modificamos el valor de x
de manera iterativa hasta encontrarlo, pero, ¿cómo sabemos que valores de x
seleccionar?
Tenemos dos opciones a la hora de modificar el valor de x
, aumentarlo o disminuirlo. Teniendo en cuenta que la derivada de la función nos permite determinar cuanto cambia el output de la función cuando incrementamos en un valor muy pequeño el input, podemos utilizarlo para seleccionar si aumentar o disminuir x
.
Calculamos la derivada de $L(x)$:
Para $x=4$ la derivada nos indica que al incrementar en un valor muy pequeño el input de la función, el output incrementa en $2$. Esto quiere decir que estaríamos alejándonos del mínimo, y, por lo tanto, deberíamos decrementar x
. Por otro lado, para $x=2$, la derivada nos indica que el output de la función decrementa en 2, lo que quiere decir que nos estamos acercando al mínimo y debemos seguir incrementando x
.
De manera muy básica, la derivada nos está indicando el "camino" hacia el máximo de la función, y, por lo tanto, debemos modificar nuestros parámetros en la dirección opuesta. De forma más genérica, Gradient Descent viene definido por la siguiente ecuación:
Donde $\eta$ es el ratio de aprendizaje, un hiperparámetro que controla la "distancia" que se avanza en cada iteración de Gradient Descent.
Regresión Logística
*Los cálculos que aparecen a lo largo del artículo, por motivos de simplicidad, están realizados sobre un solo ejemplo de entrenamiento $(x)$ con dos características $(x_{1}, x_{2})$, pero son facilmente extrapolables a un conjunto de datos de entrenamiento más amplio, aplicando vectorización.
Una vez explicado de manera genérica el funcionamiento de Gradient Descent, vamos a aterrizar este mismo procedimiento sobre nuestra RNA sencilla que implementa regresión logística.
De la misma forma que hemos visto en el ejemplo anterior, vamos a necesitar una función de error, en este caso, la función de error es la siguiente:
No voy a entrar en el detalle de los motivos por los cuales se selecciona esta función de error, para saber más, podéis referiros a esta página.
Una vez que tenemos la función de error, podemos seguir el mismo procedimiento que con el caso anterior para encontrar el valor óptimo de nuestros parámetros de entrada w1
, w2
y b
, con la diferencia de que esta función es multivariable.
La manera de calcular el output de la función y el error respecto al valor correcto, viene representado en el siguiente grafo computacional:
Sin embargo, tenemos que saber como modificar el valor de los parámetros de entrada w1
, w2
y b
para minimizar nuestra función de error $L(a, y)$, y para ello, tal y como hemos visto anteriormente, tenemos que calcular la derivada de $L(a, y)$ respecto a estos parámetros.
Es importante fijarse que $L(a, y)$ esta formada por la composición de varias funciones (cada uno de los nodos del grafo), por lo tanto, se debe tener presente la regla de la cadena de la que se hablaba anteriormente. Aplicando la regla de la cadena y la notación Leibniz obtenemos las siguientes derivadas:
Mediante el cálculo de estas derivadas, podemos determinar como varía la función de error cuando modificamos los parámetros de entrada, y de esta forma, podemos saber que valores asignarle a estos parámetros para minimizar el error.
Vamos a comenzar calculando la derivada parcial de $L(a, y)$ respecto a $a$:
Continuamos con la siguiente derivada, en este caso la derivada de $a$ respecto a $z$. Es importante fijarse que en este caso la función de activación es la sigmoide ($\dfrac{1}{1 + e^{-z}}$) pero en RNAs no tiene porque ser siempre esta.
Por último nos quedan las derivadas de $z$ respecto a los parámetros de entrada, w1
, w2
y b
:
Ya tenemos todas las derivadas necesarias para calcular $\dfrac{\partial L}{\partial w_{1}}$, $\dfrac{\partial L}{\partial w_{2}}$ y $\dfrac{\partial L}{\partial b}$, y, en consecuencia, sabemos como tenemos que variar el valor de los parámetros de entrada para minimizar la función de error.
Lo único que nos queda es multiplicar todas las derivadas independientes que hemos calculado anteriormente, para ello, vamos a hacerlo poco a poco.
Lo primero que vamos a hacer es calcular la derivada parcial de la función de error respecto a $z$:
Una vez calculada la derivada anterior, calcular $\dfrac{\partial L}{\partial w_{1}}$, $\dfrac{\partial L}{\partial w_{2}}$ y $\dfrac{\partial L}{\partial b}$, es más sencillo:
Ya tenemos todas las ecuaciones que necesitamos para actualizar de manera iterativa el valor de los parámetros de entrada de forma que minimicen la función de error $L(a, y)$. Lo único que nos queda es juntarlo todo en un mismo algoritmo aplicando el ratio de aprendizaje y repetirlo iterativamente hasta que el error resultante no disminuya o disminuya muy poquito.
Redes neuronales artificiales (RNAs)
A estas alturas del artículo es muy probable que este escribiendo para mí mismo, pero si has llegado hasta aquí, ¡felicidades!, ya ha pasado lo peor :)
Todo lo que hemos visto en los apartados anteriores, puede aplicarse de manera muy similar a RNAs con varias capas y diferentes funciones de activación en cada capa.
En el ejemplo que vamos a ver a continuación, se muestran los cálculos que se requieren para una RNA con dos capas y una sola neurona en cada capa. En la primera capa se utiliza la función de activación tanh
, mientras que en la segunda se utiliza la función sigmoide
.
En este caso, las derivadas parciales de la función de error $L(a^{[2]}, y)$ con respecto a los parámetros de entrada $w^{[1]}_{1}$, $w^{[1]}_{2}$, $b^{[1]}$, $w^{[2]}$ y $b^{[2]}$ se forman de la siguiente manera:
Respecto a la derivada parcial de la función de error $L(a^{[2]}, y)$ con respecto a $z^{[2]}$ (que es común para todas las derivadas anteriores), su valor es el mismo que en el caso de la regresión logística debido a que estamos utilizando la misma función de activación (sigmoid).
Lo mismo ocurre para los parámetros de entrada $w^{[2]}$ y $b^{[2]}$:
Si seguimos hacia la izquierda en el grafo computacional que se muestra arriba, podemos ver que la siguiente derivada parcial que debe calcularse es la de la función de error $L(a^{[2]}, y)$ respecto a $a^{[1]}$:
Esta derivada es nueva respecto al ejemplo anterior de regresión logistica:
El resultado de la derivada parcial de la función de error $L(a^{[2]}, y)$ respecto a $a^{[1]}$ es:
Como último paso antes de calcular la derivada parcial de la función de error $L(a^{[2]}, y)$ respecto a cada uno de los parámetros w1
, w2
y b
, tenemos que calcular la derivada parcial de la función de error respecto a $z^{[1]}$. Hay que tener en cuenta, que en este caso, la función de activación que estamos utilizamos es la tanh
, que se diferencia considerablemente de la sigmoide
, y, por lo tanto, el resultado de la derivada parcial va a ser distinto.
A continuación se muestra la derivada de la función de activación tanh
:
El resultado de la derivada parcial de la función de error $L(a^{[2]}, y)$ respecto a $z^{[1]}$ es:
Por último, las derivadas parciales de la funció de error $L(a^{[2]}, y)$ respecto a cada uno de los parámetros w1
, w2
y b
, se realiza de la misma forma que en el caso de la regresión logística:
Del mismo modo que hicimos en el caso de la regresión logística, lo único que nos queda es juntarlo todo en un mismo algoritmo aplicando el ratio de aprendizaje y repetirlo iterativamente hasta que el error resultante no disminuya o disminuya muy poquito:
Conclusión
En este artículo he tratado de desarrollar todo lo necesario para comprender uno de los algoritmos más utilizados en problemas de optimización en Machine Learning y Deep Learning hoy en día, Gradient Descent. Hay que tener en cuenta, que los cálculos que aparecen a lo largo del artículo, estan realizados sobre un solo ejemplo de entrenamiento $(x)$ con dos características $(x_{1}, x_{2})$, pero son facilmente extrapolables a un conjunto de datos de entrenamiento más amplio y aplicando vectorización.