Está en la página 1de 1

shramos's site!

s
e
Home a
r
c
h
Understanding Gradient Descent
FEB

3 and Backpropagation (full


calculation of derivatives)

Santiago Hernández Ramos


@santiagohramos
shramos(at)protonmail(dot)com

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) = esin(x ) .
2

Esta función podría descomponerse de


la siguiente manera:
f (x) = ex
g(x) = sin(x)
h(x) = x2

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:

df df dg dh
= ⋅ ⋅
dx dg dh dx

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:

df (v) df (v) dv dz
= ⋅ ⋅
dx dv dz dx

El resultado de cada una de las derivadas


sería el siguiente:

df (v)
= dvd [ev ]
dv
= ev
dv d
= dz [sin(z)]
dz
= cos(z)
dz
= dxd [x2 ]
dx
= 2x

Por último, el resultado de la derivada de f


respecto a x , sería la multiplicación de las
anteriores:

df (v)
= ev ⋅ cos(z) ⋅ 2x
dx
= esin(x ) ⋅ cos(x2 ) ⋅ 2x
2

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:

1. 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 y b si
es la primera iteración)
2. Mediante la función de error, se evalúa
la predicción con respecto al valor
correcto
3. Se modifica el valor de los parámetros
w1 , w2 y b 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) :

d
L(x) = dxd [(x − 3)2 ]
dx
= 2 ( dxd [x] + dxd [−3]) (x − 3)
= 2 (1 + 0) (x − 3)
= 2 (x − 3)

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:

d
x=x−η f (x)
dx

Donde η 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
(x1 , x2 ), 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:

L(a, y) = −ylog(a) − (1 − y)log(1 − a)

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:

∂L ∂L da ∂z
= ⋅ ⋅
∂w1 ∂a dz ∂w1

∂L ∂L da ∂z
= ⋅ ⋅
∂w2 ∂a dz ∂w2

∂L ∂L da ∂z
= ⋅ ⋅
∂b ∂a dz ∂b

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 :

∂ ∂
L(a, y) = [−y ln(a) − (1 − y) ln(1 − a
∂a ∂a

= −y ⋅ [ln(a)] + (y − 1)
∂a

⋅ [ln(1 − a)]
∂a
1 ∂
= ⋅ ∂a [1 − a] ⋅ (y − 1) −
1−a
(0 − 1) (y − 1) y
= −
1−a a
1−y y
= −
1−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 (
1
) pero en RNAs no tiene porque ser
1 + e−z
siempre esta.

dz [ 1 + e−z ]
d d 1
a=
dz
d
( )
−z −1
= 1 + e
dz
= −(1 + e−z )−2 (−e−z )
e−z
=
(1 + e−z )2
1 e−z
= ⋅
1 + e−z 1 + e−z
1 (1 + e−z ) − 1
= ⋅
1 + e−z 1 + e−z

1 + e−z ( 1 + e−z 1 + e−z )


1 1 + e−z 1
= ⋅ −

1 + e−z ( 1 + e−z )
1 1
= ⋅ 1 −
= a ⋅ (1 − a)

Por último nos quedan las derivadas de z


respecto a los parámetros de entrada, w1 ,
w2 y b :


z = ∂w∂ [w1 x1 + w2 x2 + b]
∂w1 1

= x1 ⋅ ∂w∂ [w1 ] + ∂w∂ [w2 x2 ] +


1 1

= 1x1 + 0 + 0
= x1

∂ ∂
z= [w1 x1 + w2 x2 + b]
∂w2 ∂w2
∂ ∂ ∂
= ∂w2
[w1 x1 ] + x2 ⋅ ∂w2
[w2 ] + ∂w2
[b
= 0 + 1x2 + 0
= x2

∂ ∂
z= [w1 x1 + w2 x2 + b]
∂b ∂b
∂ ∂ ∂
= ∂b
[w1 x1 ] + ∂b
[w2 x2 ] + ∂b
[b
=0+0+1
=1

Ya tenemos todas las derivadas necesarias


∂L ∂L ∂L
para calcular , y , y, en
∂w1 ∂w2 ∂b
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 :

∂L ∂L da
= ⋅
∂z ∂a dz
1−y y
=( − ) ⋅ (a ⋅ (1 − a))
1−a a
a−y
=a⋅ ⋅ (−a + 1)
a(−a + 1)
(a − y)a(1 − a)
=
a(−a + 1)
(a − y)(1 − a)
=
−a + 1
=a−y

Una vez calculada la derivada anterior,


∂L ∂L ∂L
calcular , y , es más sencillo:
∂w1 ∂w2 ∂b

∂L ∂L da ∂z
= ⋅ ⋅
∂w1 ∂a dz ∂w1
∂L
= ⋅ x1
∂z
= x1 ⋅ (a − y)

∂L ∂L da ∂z
= ⋅ ⋅
∂w2 ∂a dz ∂w2
∂L
= ⋅ x2
∂z
= x2 ⋅ (a − y)

∂L ∂L da ∂z
= ⋅ ⋅
∂b ∂a dz ∂b
∂L
= ⋅1
∂z
=a−y

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.

Forward Propagation

z = w1 x1 + w2 x2 + b
a = sigmoid(z)

Backward propagation

dw1 = x1 ∗ (a − y)
dw2 = x2 ∗ (a − y)
db = a − y

Parameter update

w1 = w1 − η ⋅ dw1
w2 = w2 − η ⋅ dw2
b = b − η ⋅ db

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
[1] [1]
los parámetros de entrada w1 , w2 , b[1] ,
w[2] y b[2] se forman de la siguiente
manera:

∂L ∂L da[2] ∂z[2] da[1] ∂z


= [2] ⋅ [2] ⋅ [1] ⋅ [1] ⋅
∂w1[1] ∂a dz ∂a dz ∂w

∂L ∂L da[2] ∂z[2] da[1] ∂z


= [2] ⋅ [2] ⋅ [1] ⋅ [1] ⋅
∂w2[1] ∂a dz ∂a dz ∂w

∂L ∂L da[2] ∂z[2] da[1] ∂z[1]


= [2] ⋅ [2] ⋅ [1] ⋅ [1] ⋅ [1]
∂b[1] ∂a dz ∂a dz ∂b

∂L ∂L da[2] ∂z[2]
= [2] ⋅ [2] ⋅
∂w[2] ∂a dz ∂w[2]

∂L ∂L da[2] ∂z[2]
= [2] ⋅ [2] ⋅ [2]
∂b[2] ∂a dz ∂b

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).

∂L ∂L da[2] [2]
= ⋅ = a −y
∂z[2] ∂a[2] dz[2]

Lo mismo ocurre para los parámetros de


entrada w[2] y b[2] :

∂L
= a[1] ⋅ (a[2] − y)
∂w[2]

∂L [2]
= a −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] :

∂L ∂L da[2] ∂z[2]
= [2] ⋅ [2] ⋅ [1]
∂a[1] ∂a dz ∂a

Esta derivada es nueva respecto al ejemplo


anterior de regresión logistica:

∂z[2]
= ∂a∂[1] [w[2] a[1] + b[2] ]
∂a[1]

= w[2] ⋅ ∂a∂[1] [a[1] ] + ∂a∂[1] [b[2] ]


= 1w[2] + 0
= w[2]

El resultado de la derivada parcial de la


función de error L(a[2] , y) respecto a a[1]
es:

∂L ∂L ∂z[2] [2] [2]


= ⋅ = ( a − y) ⋅ w
∂a[1] ∂z[2] ∂a[1]

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.

∂L ∂L da[2] ∂z[2] da[1]


= [2] ⋅ [2] ⋅ [1] ⋅ [1]
∂z[1] ∂a dz ∂a dz

A continuación se muestra la derivada de la


función de activación tanh :

dz [1] [ z [1] + −z [1] ]


da[1] ez − e−z
[1] [1]
d
=
dz [1] e e
[e − e−z ] ⋅ (ez + e−z )
d z [1] [1] [1] [1]

dz [1]

− (ez − e−z ) d
[e
z [1] + e−z
[1] [1] [1]

dz [1]
=
(ez + e−z )
[1] [1] 2

( dz[1] [e ] − [e ]) (e
d z [1] d −z [1] z [1]
dz [1]

− ( dzd[1] [ez ] + d
[e ]) (e
−z z
[1] [1]

dz [1]

− e−z )
[1]

=
(ez + e−z )
[1] [1] 2

( [−z ]) (e
z − e−z ⋅
[1] [1] d [1] z [1]
e dz [1]

− (ez + e−z ⋅ d
[−z ]) (e
[1]
[1] [1]

dz [1]

− e−z )
[1]

=
(ez + e−z )
[1] [1] 2

(e + e ) (e − (− dz[1] [z
z −z
[1] z d [1 [1] [1]

− (ez − e−z ) (ez


[1] [1] [1]

+ (− dzd[1] [z[1] ]) e−z )


[1]

=
(ez + e−z )
[1] [1] 2

(e + e−z ) (ez + 1e−z )


z [1] [1] [1] [1]

− (ez − e−z ) (ez − 1e−z )


[1] [1] [1] [1]

=
(ez + e−z )
[1] [1] 2

(e + e ) − (e − e )
z −z
[1] z −z [1] 2 [1] [1] 2

=
(ez + e−z )
[1] [1] 2

(e − e )
z −z [1] [1] 2

=1−
(ez + e−z )
[1] [1] 2

= 1 − tanh(z[1] )2
2
= 1 − a[1]

También podría gustarte