Herramientas de usuario

Herramientas del sitio


clase:iabd:pia:2eval:tema07.backpropagation_descenso_gradiente

7. Entrenamiento de redes neuronales b) Descenso de gradiente

El Descenso de gradiente es el algoritmo que usamos para entrenar la red neuronal.

¿Que era entrenar la red neuronal? Pues simplemente pasarle muchos $x$ e $y$ para que obtenga o aprenda los mejores parámetros posibles de pesos (weight) y sesgos (bias) de forma que la función de coste sea mínima.

Sabemos que: $$y= Salida \: verdadera. \: La \: que \: debería \: haber \: generado \: la \: red \: neuronal. Son \: datos \: que \: tenemos \: reales \: de \: salida$$ $$\hat{y}= Salida \: predicha. \: La \: que \: ha \: generado \: la \: red \: neuronal$$

$$loss=\frac{1}{N} \sum_{i=1}^{N} |y_i-\hat{y}_i|$$

Seguimos con la red neuronal que usamos de ejemplo:

Dedujimos que corresponde a la siguiente fórmula:

$$ \large \hat{y}=\frac{1}{1 + e^{-( w_{5,2}\frac{1}{1 + e^{-( w_{2}x+b_{2} )}}+w_{5,3}\frac{1}{1 + e^{-( w_{3}x+b_{3} )}}+w_{5,4}\frac{1}{1 + e^{-( w_{4}x+b_{4} )}}+b_5 )}} $$

Ya hemos visto que la siguiente función matemática nos dice los buena que es nuestra red neuronal, al resta el valor real de $y$ la salida de la red neuronal $\hat{y}$ es decir $y-\hat{y}$

$$ \large loss(x,y,parametros)=\frac{1}{N} \sum_{i=1}^{N}|y_i-\frac{1}{1 + e^{-( w_{5,2}\frac{1}{1 + e^{-( w_{2}x_i+b_{2} )}}+w_{5,3}\frac{1}{1 + e^{-( w_{3}x_i+b_{3} )}}+w_{5,4}\frac{1}{1 + e^{-( w_{4}x_i+b_{4} )}}+b_5 )}}| $$

Por lo que los parámetros a entrenar son los siguientes: $w_{2} , w_{3} , w_{4} , w_{5,2} , w_{5,3} , w_{5,4} , b_{4} , b_{2} , b_{3} , b_5$. Es decir que realmente entrenar consiste en averiguar los valores de los parámetros ($w_2,w_3,b_2$, etc) que hacen que el valor de loss tenga un valor mínimo.

Lo siguiente es saber el algoritmo para averiguar los parámetros. De forma didáctica expongo 3 métodos aunque solo se gasta el último.

  • Aleatoriamente: Se podrían elegir aleatoriamente y ver si son bueno y así probar muchas veces hasta encontrar los bueno. Obviamente es una forma malísima ya que la probabilidad de que todos a la vez fueran buenos es extremadamente baja. Por lo que tardaríamos muchísimo tiempo en obtener los parámetros adecuados.
  • Todas las combinaciones: Se podrían probar todas las posibles combinaciones, pero hay tantísimas que el coste es prohibitivo y también los hace inviable.
  • Descenso de gradiente: El algoritmo consiste en buscar el mínimo de la función ajustando siempre los parámetros un poco de forma que el valor de la función disminuya un poco y repetirlo muchas veces.

Durante este tema la función a minimizar va a ser loss y siguiendo con nuestro ejemplo el código es el siguiente:

def sigmoid(z):
    return 1/(1 + np.exp(-z))


def predict_formula(x,w_2,w_3,w_4,w_52,w_53,w_54,b_2,b_3,b_4,b_5):
    part1=w_52*sigmoid(w_2*x+b_2)
    part2=w_53*sigmoid(w_3*x+b_3)
    part3=w_54*sigmoid(w_4*x+b_4)
    part4=b_5
    z=part1+part2+part3+part4

    return sigmoid(z)


def loss_mae(y_true,y_pred):
    error=np.abs(np.subtract(y_true,y_pred))
    mean_error=np.sum(error)/len(y_true)

    return mean_error



def loss(x,y_true,w_2,w_3,w_4,w_52,w_53,w_54,b_2,b_3,b_4,b_5):
    y_pred=predict_formula(x,w_2,w_3,w_4,w_52,w_53,w_54,b_2,b_3,b_4,b_5)
 
    return loss_mae(y_true,y_pred)

Lo siguiente que vamos a hacer es una gráfica para ver como variaría el valor de loss , es decir la pérdida, en caso de ir variando el valor del peso w_2.

iris=load_iris()
x=iris.data[0:99,2]
y_true=iris.target[0:99]
 

model=get_model("mae")
w_2_original,w_3_original,w_4_original,w_52_original,w_53_original,w_54_original,b_2_original,b_3_original,b_4_original,b_5_original=get_parameters_from_model(model)
perdida=loss(x,y_true,w_2_original,w_3_original,w_4_original,w_52_original,w_53_original,w_54_original,b_2_original,b_3_original,b_4_original,b_5_original)

rango_w_2=np.linspace(-5,5,400)
perdidas_w_2=[]
for w_2 in rango_w_2:
    perdidas_w_2.append( loss(x,y_true,w_2,w_3_original,w_4_original,w_52_original,w_53_original,w_54_original,b_2_original,b_3_original,b_4_original,b_5_original))


figure=plt.figure(figsize=(5.5,4))
axes = figure.add_subplot()
axes.plot(rango_w_2,perdidas_w_2)
axes.scatter(w_2_original,perdida,color="red",s=40)
axes.set_xlabel("w_2")
axes.set_ylabel('Perdida')

En la gráfica , el punto rojo es el valor actual de w_2 y la pérdida de la red, es decir el resultado de la función loss. La curva nos permite ver como evoluciona la pérdida, si vamos variando el valor de w_2. En nuestro caso, viendo la gráfica podemos encontrar rápidamente el valor de w_2 que haría que fuera mínimo y podríamos darle ese valor pero debemos tener en cuenta que si variamos mucho w_2 cambiaría totalmente la pérdida para el resto de pesos por lo que hacemos es ir variando poco a poco el valor de cada peso. En nuestro ejemplo, para hacer que la pérdida sea la mínima, ¿que es mejor que w_2 se haga un poquito más grande o un poquito más pequeño? La respuesta es que lo mejor es que se haga un poquito más grande.

En la siguiente gráfica podemos ver como varia la pérdida para todos los pesos de nuestro ejemplo.

Es decir que como nuestra función de loss tiene muchos parámetros, no podemos encontrar el mínimo de cada parámetro por separado sino que hay que buscar poco a poco el mínimo de cada parámetro.

Veamos ahora un ejemplo de una función de loss con dos parámetros:

Las dos imágenes son la misma función pero la de la derecha está representada en 2D, donde al "altura" se representa con colores.

Como vemos no podríamos encontrar el mínimo para w_0 y luego para w_1.Así que los haremos poco a poco usando el algoritmo de descenso de gradiente.

Otro ejemplo más sencillo sería:

Matemáticas en el descenso de gradiente

Ahora que vemos la necesidad de encontrar el mínimo de una función pasemos a ver como se encuentra el mínimo. Pues es tan sencillo como imaginarse que estamos en una montaña y para encontrar la parte mas baja solo hay que ir descendiendo por la montaña. Es decir que hay que ver si la función (la montaña) crece y en ese caso ir hacia el lado contrario.

La siguiente imagen ilustra la idea.

¿Como hacemos eso matemáticamente? Pues lo primero es pensar que cuando se inicializa la red neuronal, a los parámetros ($w_0$ y $w_1$) se les da un valor aleatorio, por lo que estamos en un punto cualquiera de esa montaña. ¿Como sabemos en que dirección debemos movernos? Pues muy fácil, incrementamos un poquito el valor del parámetro (a ese incremento lo llamaremos $h$ o $\Delta x$) y vemos como se incrementa con respecto del valor original, (a ese incremento de la función lo llamamos $\Delta f$) . Es decir los restamos (aunque también lo dividimos entre el valor del incremento).

$$ gradiente \: de \: f(x) = derivada \: de \: f(x)=\frac{\Delta f}{\Delta x}=\lim_{h \to 0} \frac {f(x+h)-f(x)}{h}=\frac{\partial \: f(x)}{\partial \: x} $$

Ese valor es lo que se llama la derivada de una función y si:

  • Si el resultado de la derivada el positivo, es que la función crece en se punto. Y como estamos buscando el mínimo, deberemos decrementar el valor de $x$ proporcionalmente al valor del gradiente.
  • Si el resultado de la derivada el negativo, es que la función decrece en se punto. Y como estamos buscando el mínimo, deberemos incrementar el valor de $x$ proporcionalmente al valor del gradiente.
Una explicación mas detallada de la fórmula se puede ver en el siguiente video: DERIVADAS - Clase Completa: Explicación Desde Cero | El Traductor
En el contexto del descenso de gradiente no se suele hablar que el resultado de la fórmula es la derivada sino el gradiente. Ya que para eso estamos en el descenso de gradente

La siguiente fórmula del gradiente queda así con respecto $w_0$ en el punto $(w_0,w_1)$. La segunda fórmula, es simplemente lo mismo pero expresado de forma mas compacta visto como derivadas.

$$ gradiente \: de \: w_0 = \lim_{h \to 0} \frac {loss(w_0+h,w_1)-loss(w_0,w_1)}{h}=\frac{\partial \: loss(w_0,w_1)}{\partial \: w_0} $$

La siguiente fórmula es lo mismo pero para $w_1$

$$ gradiente \: de \: w_1 = \lim_{h \to 0} \frac {loss(w_0,w_1+h)-loss(w_0,w_1)}{h}=\frac{\partial \: loss(w_0,w_1)}{\partial \: w_1} $$

Ya sabemos si crece pues en ese caso soy hay que moverse justo para el lado contrario de ahí el "menos" de la siguiente fórmula.

$$ w_0=w_0-\alpha \cdot gradiente \: de \: w_0=w_0-\alpha \cdot \frac{\partial \: loss(w_0,w_1)}{\partial \: w_0} $$

y

$$ w_1=w_1-\alpha \cdot gradiente \: de \: w_1=w_1-\alpha \cdot \frac{\partial \: loss(w_0,w_1)}{\partial \: w_1} $$

Si ésto lo repetimos muchas veces es justamente el algoritmo del descenso de gradiente.

La formula general de lo que es el descenso de gradiente se podría expresar así:

$$ \huge w_i^{t+1}=w_i^{t}-\alpha \cdot \frac{\partial \: loss(w_i^{t})}{\partial \: w_i^{t}} $$

Expliquemos un poco la fórmula:

  • $w_i$ es cada uno de los parámetros es decir w_0, w_1, etc.
  • El superíndice $t$ el instante de tiempo,ya que vamos a aplicar varias veces la fórmula. Es realmente cada una de las épocas o epochs.
  • La variable $\alpha$ es lo que llamamos la tasa de apredizaje o learning_rate e indica cuanto queremos que varíen los parámetros en cada época.Este valor es muy importante ya que lo que nos dice es "como" de largo vamos a dar los pasos en nuestro descenso de la montaña ya que si damos pasos muy largos en una dirección puede que nos pasemos de largo. En este caso podemos imaginar que podemos ser gigantes que si damos pasos muy largos, nos llegaremos a la parte interior de la montaña. Pero si damos los pasos muy pequeños, no llegaremos a la parte inferior de la montaña antes de acabar las épocas.

Descenso de gradiente en Python

Pasemos ahora a ver como se programa todo ésto en Python.Siguiendo con nuestro ejemplo de:

Ya teníamos el siguiente código:

import numpy as np
import pandas as pd
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
from sklearn.datasets import load_iris
 

def get_model(loss,learning_rate):
    np.random.seed(5)
    tf.random.set_seed(5)
    random.seed(5)  
     
    model=Sequential()
    model.add(Dense(3, input_dim=1,activation="sigmoid"))
    model.add(Dense(1,activation="sigmoid"))
    model.compile(loss=loss,optimizer=tf.keras.optimizers.SGD(learning_rate=learning_rate))
     
    return model


def get_w(model,layer,neuron,index):
    layer=model.layers[layer]
    return layer.get_weights()[0][index,neuron]
   
   
   
def get_b(model,layer,neuron):
    layer=model.layers[layer]
    return layer.get_weights()[1][neuron]
  
 
def get_parameters_from_model(model):
    w_2 =get_w(model,0,0,0)
    w_3 =get_w(model,0,1,0)
    w_4 =get_w(model,0,2,0)
    w_52=get_w(model,1,0,0)
    w_53=get_w(model,1,0,1)
    w_54=get_w(model,1,0,2)
    b_2 =get_b(model,0,0)
    b_3 =get_b(model,0,1)
    b_4 =get_b(model,0,2)
    b_5 =get_b(model,1,0)
 
    return w_2,w_3,w_4,w_52,w_53,w_54,b_2,b_3,b_4,b_5

def print_params(cabecera,w_2,w_3,w_4,w_52,w_53,w_54,b_2,b_3,b_4,b_5):
    decimals=8

    print(cabecera)
    print("w_2=",round(w_2,decimals),"   ","b_2=",round(b_2,decimals))
    print("w_3=",round(w_3,decimals),"   ","b_3=",round(b_3,decimals))   
    print("w_4=",round(w_4,decimals),"   ","b_4=",round(b_4,decimals)) 
    print("w_52=",round(w_52,decimals),"   ","w_53=",round(w_53,decimals),"w_54=",round(w_54,decimals),"   ","b_5=",round(b_5,decimals)) 


def sigmoid(x):
    return 1/(1 + np.exp(-x))
  
def predict_formula(x,w_2,w_3,w_4,w_52,w_53,w_54,b_2,b_3,b_4,b_5):
    part1=w_52*sigmoid(w_2*x+b_2)
    part2=w_53*sigmoid(w_3*x+b_3)
    part3=w_54*sigmoid(w_4*x+b_4)
    part4=b_5
    z=part1+part2+part3+part4
  
    return sigmoid(z)

def loss_mae(y_true,y_pred):
    error=np.abs(np.subtract(y_true,y_pred))
    mean_error=np.sum(error)/len(y_true)
 
    return mean_error

def loss(x,y_true,w_2,w_3,w_4,w_52,w_53,w_54,b_2,b_3,b_4,b_5):
    y_pred=predict_formula(x,w_2,w_3,w_4,w_52,w_53,w_54,b_2,b_3,b_4,b_5)

    return loss_mae(y_true,y_pred)

y ahora añadimos el código de los métodos descenso_gradiente y fit.



def descenso_gradiente(x,y_true,learning_rate,w_2,w_3,w_4,w_52,w_53,w_54,b_2,b_3,b_4,b_5):
    h=0.000003

    gradiente_w_2 =(loss(x,y_true,w_2+h,w_3,w_4,w_52,w_53,w_54,b_2,b_3,b_4,b_5)-loss(x,y_true,w_2,w_3,w_4,w_52,w_53,w_54,b_2,b_3,b_4,b_5))/h
    gradiente_w_3 =(loss(x,y_true,w_2,w_3+h,w_4,w_52,w_53,w_54,b_2,b_3,b_4,b_5)-loss(x,y_true,w_2,w_3,w_4,w_52,w_53,w_54,b_2,b_3,b_4,b_5))/h
    gradiente_w_4 =(loss(x,y_true,w_2,w_3,w_4+h,w_52,w_53,w_54,b_2,b_3,b_4,b_5)-loss(x,y_true,w_2,w_3,w_4,w_52,w_53,w_54,b_2,b_3,b_4,b_5))/h
    gradiente_w_52=(loss(x,y_true,w_2,w_3,w_4,w_52+h,w_53,w_54,b_2,b_3,b_4,b_5)-loss(x,y_true,w_2,w_3,w_4,w_52,w_53,w_54,b_2,b_3,b_4,b_5))/h
    gradiente_w_53=(loss(x,y_true,w_2,w_3,w_4,w_52,w_53+h,w_54,b_2,b_3,b_4,b_5)-loss(x,y_true,w_2,w_3,w_4,w_52,w_53,w_54,b_2,b_3,b_4,b_5))/h
    gradiente_w_54=(loss(x,y_true,w_2,w_3,w_4,w_52,w_53,w_54+h,b_2,b_3,b_4,b_5)-loss(x,y_true,w_2,w_3,w_4,w_52,w_53,w_54,b_2,b_3,b_4,b_5))/h
    gradiente_b_2 =(loss(x,y_true,w_2,w_3,w_4,w_52,w_53,w_54,b_2+h,b_3,b_4,b_5)-loss(x,y_true,w_2,w_3,w_4,w_52,w_53,w_54,b_2,b_3,b_4,b_5))/h
    gradiente_b_3 =(loss(x,y_true,w_2,w_3,w_4,w_52,w_53,w_54,b_2,b_3+h,b_4,b_5)-loss(x,y_true,w_2,w_3,w_4,w_52,w_53,w_54,b_2,b_3,b_4,b_5))/h
    gradiente_b_4 =(loss(x,y_true,w_2,w_3,w_4,w_52,w_53,w_54,b_2,b_3,b_4+h,b_5)-loss(x,y_true,w_2,w_3,w_4,w_52,w_53,w_54,b_2,b_3,b_4,b_5))/h
    gradiente_b_5 =(loss(x,y_true,w_2,w_3,w_4,w_52,w_53,w_54,b_2,b_3,b_4,b_5+h)-loss(x,y_true,w_2,w_3,w_4,w_52,w_53,w_54,b_2,b_3,b_4,b_5))/h

    w_2 =w_2 -learning_rate*gradiente_w_2 
    w_3 =w_3 -learning_rate*gradiente_w_3 
    w_4 =w_4 -learning_rate*gradiente_w_4 
    w_52=w_52-learning_rate*gradiente_w_52
    w_53=w_53-learning_rate*gradiente_w_53
    w_54=w_54-learning_rate*gradiente_w_54
    b_2 =b_2 -learning_rate*gradiente_b_2 
    b_3 =b_3 -learning_rate*gradiente_b_3 
    b_4 =b_4 -learning_rate*gradiente_b_4 
    b_5 =b_5 -learning_rate*gradiente_b_5 

    return w_2,w_3,w_4,w_52,w_53,w_54,b_2,b_3,b_4,b_5



def fit(x, y_true,epochs,learning_rate,w_2,w_3,w_4,w_52,w_53,w_54,b_2,b_3,b_4,b_5):
    
    for epoch in range(epochs):
        w_2,w_3,w_4,w_52,w_53,w_54,b_2,b_3,b_4,b_5=descenso_gradiente(x,y_true,learning_rate,w_2,w_3,w_4,w_52,w_53,w_54,b_2,b_3,b_4,b_5)   

    return w_2,w_3,w_4,w_52,w_53,w_54,b_2,b_3,b_4,b_5

Por último usamos todo el código para ver si entrena igual con nuestro código que en Keras.

learning_rate=0.001
epochs=50

iris=load_iris()
x=iris.data[0:99,2]
y_true=iris.target[0:99]

model=get_model('mae',learning_rate)

w_2_original,w_3_original,w_4_original,w_52_original,w_53_original,w_54_original,b_2_original,b_3_original,b_4_original,b_5_original=get_parameters_from_model(model)
print_params("Valor de los pesos antes del entrenamiento:",w_2_original,w_3_original,w_4_original,w_52_original,w_53_original,w_54_original,b_2_original,b_3_original,b_4_original,b_5_original)

history=model.fit(x, y_true,epochs=epochs,verbose=False,batch_size=len(x),shuffle=False)
w_2,w_3,w_4,w_52,w_53,w_54,b_2,b_3,b_4,b_5=get_parameters_from_model(model)
print_params("Valor de los pesos tras entrenamiento usando Keras:",w_2,w_3,w_4,w_52,w_53,w_54,b_2,b_3,b_4,b_5)

w_2,w_3,w_4,w_52,w_53,w_54,b_2,b_3,b_4,b_5=fit(x,y_true,epochs,learning_rate,w_2_original,w_3_original,w_4_original,w_52_original,w_53_original,w_54_original,b_2_original,b_3_original,b_4_original,b_5_original)
print_params("Valor de los pesos tras entrenamiento Manualmente:",w_2,w_3,w_4,w_52,w_53,w_54,b_2,b_3,b_4,b_5)

Valor de los pesos antes del entrenamiento:
w_2= 0.30959857     b_2= 0.0
w_3= 0.07310068     b_3= 0.0
w_4= 0.63308823     b_4= 0.0
w_52= 0.49408045     w_53= -1.1185157 w_54= 0.14808941     b_5= 0.0
Valor de los pesos tras entrenamiento usando Keras:
w_2= 0.31066534     b_2= -0.00022871
w_3= 0.06847349     b_3= 6.122e-05
w_4= 0.6330435     b_4= -0.00013335
w_52= 0.49511436     w_53= -1.1182635 w_54= 0.14937088     b_5= -9.583e-05
Valor de los pesos tras entrenamiento Manualmente:
w_2= 0.31066538     b_2= -0.00022871
w_3= 0.06847351     b_3= 6.122e-05
w_4= 0.63304377     b_4= -0.00013335
w_52= 0.49511434     w_53= -1.11826369 w_54= 0.14937088     b_5= -9.583e-05

Mas información:

Entrenamiento en Keras

Ya hemos visto como el método fit de Keras realiza el entrenamiento pero veamos un poco más en detalle lo que hay que hacer.

Para el entrenamiento debemos establecer parámetros tanto en fit como en compile. Veamos un ejemplo.

model.compile(loss=loss,optimizer=tf.keras.optimizers.SGD(learning_rate=learning_rate))
model.fit(x, y_true,epochs=epochs,verbose=False,batch_size=len(x),shuffle=False)

  • compile
    • loss: Ya vimos en el tema anterior que es la fórmula que se usa para calcular la pérdida o coste de la red neuronal
    • optimizer: Indica el algoritmo de descenso de gradiente que vamos a usar junto con la tasa de aprendizaje o learning_rate. Más adelante en este tema vamos a ver que hay variaciones del algoritmo del descenso de gradiente.
  • fit
    • epochs: Indicamos cuantas veces se va a aplicar el algoritmo del descenso de gradiente.
    • batch_size: En este tema aun no vamos a ver el significado de este parámetro pero simplemente decir que lo hemos incluido para que el resultado fuera el mismo que con Python.
    • shuffle: Este parámetro no se suele usar y solo lo hemos incluido para que el resultado fuera el mismo que con Python.

Cuando entrenamos por defecto fit nos muestra el resultado del entrenamiento de cada época, indicando el valor de loss en cada una de ellas.

Epoch 1/50
1/1 [==============================] - 0s 282ms/step - loss: 0.4918
Epoch 2/50
1/1 [==============================] - 0s 7ms/step - loss: 0.4917
Epoch 3/50
1/1 [==============================] - 0s 5ms/step - loss: 0.4917
...
...
...
Epoch 48/50
1/1 [==============================] - 0s 3ms/step - loss: 0.4913
Epoch 49/50
1/1 [==============================] - 0s 4ms/step - loss: 0.4913
Epoch 50/50
1/1 [==============================] - 0s 4ms/step - loss: 0.4913

Tanto el método fit como compile tienen muchos más parámetros pero aquí hemos visto únicamente los básicos.

Mas información:

Profundizando en el descenso de gradiente

Vamos ahora a ver de una forma más gráfica como funciona el descenso de gradiente y algunos problemas que tiene.

En este apartado a modo de ejemplo, vamos a usar como función de coste:

$$loss(w_0,w_1)=3(1-w_0)^2e^{-w_0^2-(w_1+1)^2}-10(\frac{w_0}{5}-w_0^3-w_1^5)e^{-w_0^2-w_1^2}-\frac{1}{3}e^{-(w_0+1)^2-w_1^2}$$

y en Python:

def loss_function(w_0,w_1):
    return  3*(1 - w_0)**2 * np.exp(-w_0**2 - (w_1 + 1)**2)  - 10*(w_0/5 - w_0**3 - w_1**5)*np.exp(-w_0**2 - w_1**2) - 1./3*np.exp(-(w_0 + 1)**2 - w_1**2) 

Cuya gráfica es:

En los apéndices de este tema está el código que vamos a usar que se usa así:

figure=plt.figure(figsize=(16,15))
axes = figure.add_subplot()
plot_loss_function(axes)

epochs=5
learning_rate=0.03
w_0_original=-0.35
w_1_original=-0.67
plot_descenso_gradiente(axes,get_puntos_descenso_gradiente(epochs,learning_rate,w_0_original,w_1_original))

La función get_puntos_descenso_gradiente tiene los siguientes parámetros:

  • epochs: El nº de épocas a aplicar
  • learning_rate: La tasa de aprendizaje a aplicar.
  • w_0_original: El peso inicial de w_0
  • w_1_original: El peso inicial de w_1

Que genera la siguiente gráfica

  • La estrella roja es desde donde empieza el algoritmo.Es decir, el valor inicial de los parámetros $w_0,w_1$
  • La estrella azul es donde acaba el algoritmo.Es decir, el valor tras el entrenamiento de los parámetros $w_0,w_1$
  • Los puntos amarillo son los pasos por lo que se va moviendo el algoritmo. Cada uno de los valores intermedios de los parámetros durante el entrenamiento.

Learning rate y epochs

Veamos ahora como influye el learning_rate en el funcionamiento del algoritmo de gradiente.

Comparemos la gráfica anterior con un learning_rate de 0.03 con este otro ejemplo con learning_rate de 0.07

plot_descenso_gradiente(get_puntos_descenso_gradiente(5,0.07,-0.35,-0.67))

Podemos ver que ahora que damos pasos demasiado largos y no llegamos hasta el mínimo de la función.

Sigamos con otro ejemplo:

plot_descenso_gradiente(get_puntos_descenso_gradiente(11,0.1,-0.35,-0.67))

El valor de learning_rate es mayor aun , siendo de 0.1 por lo que damos pasos tan grandes que nos salimos de mínimo de la función y eso que hemos pasado de 5 épocas a 11 épocas. Es decir, si el learning_rate es elevado, no llegará al mínimo aunque pongamos muchas épocas.

Sigamos con otro ejemplo en el que el learning_rate es pequeño, concretamente de 0.006

plot_descenso_gradiente(get_puntos_descenso_gradiente(11,0.006,-0.35,-0.67))

Vemos que al dar pasos tan pequeños nos quedamos a mitad de camino y eso que también hemos puesto 11 épocas. En este caso para solucionarlo solo habría que aumentar el número de épocas.

Así que en el siguiente ejemplo, vamos a aumentar el número de épocas de 11 a 30.

plot_descenso_gradiente(get_puntos_descenso_gradiente(30,0.006,-0.35,-0.67))

Y ahora ya ha llegado hasta el mínimo.

En general nos interesa valores bajos de learning_rate y altos de epochs.

Mínimos locales y mínimos globales

Ahora vamos a simular distintos valores iniciales de $w_0$ y $w_1$ es decir, suponiendo que el generador de números aleatorios genera unos valores iniciales distintos. Y vamos a comparar que ocurre con los distintos valores iniciales.

plot_descenso_gradiente(axes,get_puntos_descenso_gradiente(10, 0.03,  0.3 ,   1.3))
plot_descenso_gradiente(axes,get_puntos_descenso_gradiente(10, 0.03, -0.35, -0.67))
plot_descenso_gradiente(axes,get_puntos_descenso_gradiente(10, 0.03, -0.4 , -0.5 ))
plot_descenso_gradiente(axes,get_puntos_descenso_gradiente(10, 0.03, -0.3 , -0.5 ))

Esta última gráfica es muy interesante. No hay que perder de vista que las estrellas rojas es el valor inicial de los pesos que tenemos en nuestra red neuronal. Es decir , son esos pesos aleatorios con los que de inicializa la red neuronal. Pues lo interesante es que según los valores iniciales aleatorios que elijamos, puede que no lleguemos al mínimo de la función (mínimo global), sino a lo que llamamos un mínimo local.

Se puede ver mejor en esta gráfica:

Es decir que el algoritmo del descenso de gradiente no nos garantiza que encontremos el mínimo global de la función sino solo un mínimo local. Lo que conlleva que quizás no encontramos los mejores valores de los parámetros (weight y bias) para nuestra red neuronal.

Para intentar solventar los problemas del algoritmo del descenso de gradiente existen diversas variaciones del mismo que vamos a ver en el siguiente apartado.

Usando optimizadores en Keras

Ya hemos visto como funciona el algoritmo del descenso de gradiente pero resulta que existe muchas variaciones sobre el algoritmo básico. Es lo que en Keras se llaman Optimizers.

Lo que pretenden estos optimizadores es hacer que se llegue lo más rápido posible al mínimo además de evitar mínimos locales, etc. El descenso de gradiente que hemos visto es lo que en Keras se llama Stochastic gradient descent (SGD).

Al igual que pasaba otras veces en Keras, los optimizadores se pueden usar de 2 formas distintas y se usan en el método compile con el argumento optimizer:

  • Mediante la clase que retorna la función

model.compile(loss="binary_crossentropy",optimizer=tf.keras.optimizers.SGD(learning_rate=0.03))

  • Mediante un String

model.compile(loss="binary_crossentropy",optimizer="sgd")

Notar que el optimizador se define en el método compile pero donde se usa es con el método fit.

Tipos de optimizadores en Keras

Veamos ahora los distintos tipos de optimizadores que hay en Keras.

Mas información:

Stochastic gradient descent o SGD

Es el que hemos usado hasta ahora. Se llama "Stochastic gradient descent".

  • Tiene además los siguientes parámetros:
    • momentum: Se usa para que tenga en cuenta valores anteriores del gradiente. Valor adecuado si de desea momentum es de 0.9
    • nesterov: Si vale True se utiliza el algoritmo de Nesterov para el momentum
  • Su uso en Keras:

model.compile(loss="binary_crossentropy",optimizer=tf.keras.optimizers.SGD(learning_rate=0.03))
model.compile(loss="binary_crossentropy",optimizer="sgd")

Mas información:

Adagrad

Es como SGD pero intenta aprender el learning_rate

  • Su uso en Keras:

model.compile(loss="binary_crossentropy",optimizer=tf.keras.optimizers.Adagrad(learning_rate=0.03))
model.compile(loss="binary_crossentropy",optimizer="adagrad")

Más información:

RMSprop

Es como AdraGrad pero aprende el learning_rate.

  • Tiene además los siguientes parámetros:
    • rho: Se usa para ver como se aprende el learning_rate
    • momentum: Es la tasa de decaimiento del learning_rate
  • Su uso en Keras:

model.compile(loss="binary_crossentropy",optimizer=tf.keras.optimizers.RMSprop(learning_rate=0.03))
model.compile(loss="binary_crossentropy",optimizer="rmsprop")

Más información:

Adadelta

Es como AdraGrad pero learning_rate la aprende sola.

  • Tiene además los siguientes parámetros:
    • learning_rate: Debería valor 1 o un valor elevado ya que siempre va bajando.
    • rho: Se usa para ver como se aprende el learning_rate
  • Su uso en Keras:

model.compile(loss="binary_crossentropy",optimizer=tf.keras.optimizers.Adadelta(learning_rate=0.03))
model.compile(loss="binary_crossentropy",optimizer="adadelta")

Más información:

Adam

RMSprop + Momentum

  • Tiene además los siguientes parámetros:
    • amsgrad: Si vale True se comporta como el algoritmo de AMSGrad que es bastante moderno (2018)
  • Su uso en Keras:

model.compile(loss="binary_crossentropy",optimizer=tf.keras.optimizers.Adam(learning_rate=0.03))
model.compile(loss="binary_crossentropy",optimizer="adam")

Más información:

Adamax

Como Adam pero mas estable a ruido del gradiente

  • Su uso en Keras:

model.compile(loss="binary_crossentropy",optimizer=tf.keras.optimizers.Adamax(learning_rate=0.03))
model.compile(loss="binary_crossentropy",optimizer="adamax")

Más información:

Nadam

Como Adam pero se utiliza el algoritmo de Nesterov para el momentum

  • Su uso en Keras:

model.compile(loss="binary_crossentropy",optimizer=tf.keras.optimizers.Nadam(learning_rate=0.03))
model.compile(loss="binary_crossentropy",optimizer="nadam")

Más información:

En nuestros proyectos, solemos necesitar mostrar por pantalla el optimizador que estamos usando y normalmente pasa ésto:

print(tf.keras.optimizers.Adam(learning_rate=0.001))
print(tf.keras.optimizers.Adamax(learning_rate=0.00000001))

<keras.optimizer_v2.adam.Adam object at 0x7fc4c0d16b60>
<keras.optimizer_v2.adamax.Adamax object at 0x7fc4c1db1810>

Pero hay un truco para que se muestre de forma más amigable, que es sobre escribir la función str de la clase Optimizer:

tf.keras.optimizers.Optimizer.__str__=lambda self: f'{self._name} lr=' + f'{self.learning_rate.numpy():.10f}'.rstrip('0')

Y si volvemos a ejecutar de nuevo el código:

print(tf.keras.optimizers.Adam(learning_rate=0.001))
print(tf.keras.optimizers.Adamax(learning_rate=0.00000001))

Adam lr=0.001
Adamax lr=0.00000001

Elección del optimizador.

Al igual que pasaba con las funciones de activación, según el problema, puede ser mejor uno u otro.

En el siguiente esquema se puede ver la relación entre unos y otros y así ver cual es más moderno.

Usando las funciones que ya tenemos de get_puntos_descenso_gradiente, plot_loss_function, etc. he creado a crear un video para que se vea de la velocidad a la que se mueve cada optimizador.

El objetivo del vídeo no es decir que optimizador es mejor sino hacer ver que hay mejores y peores. En otro problema y con otros parámetros el resultado puede ser distinto.

Veamos ahora un ejemplo con código de como funcionan

figure=plt.figure(figsize=(16,15))

learning_rate=0.1
epochs=10
optimizers=[
    [tf.keras.optimizers.SGD(learning_rate=learning_rate),"SGD"],
    [tf.keras.optimizers.Adagrad(learning_rate=learning_rate),"Adagrad"],
    [tf.keras.optimizers.RMSprop(learning_rate=learning_rate),"RMSprop"],
    [tf.keras.optimizers.Adadelta(learning_rate=1),"Adadelta"],
    [tf.keras.optimizers.Adam(learning_rate=learning_rate),"Adam"],
    [tf.keras.optimizers.Adam(learning_rate=learning_rate,amsgrad=True),"AMSGrad"],
    [tf.keras.optimizers.Adamax(learning_rate=learning_rate),"Adamax"],
    [tf.keras.optimizers.Nadam(learning_rate=learning_rate),"Nadam"] 
]

for index,(optimizer,title) in enumerate(optimizers):
    axes = figure.add_subplot(3,3,index+1)
    plot_loss_function(axes,15,title)
    plot_descenso_gradiente(axes,get_puntos_descenso_gradiente_optimizer(epochs,optimizer,-0.35,-0.67))

Mas información:

Hardware entrenamiento

Ejercicios

Ejercicio 1

Dado las gráficas de las funciones de coste de una red neuronal:

Indica para cada parámetro: Si la función crece o decrece, el valor de su derivada (positiva o negativa) y por lo tanto si se debe incrementar el valor del parámetro o decremenarlo.

Ejercicio 2

Dado la siguiente función matemática:

$$f(x) = \frac{x}{1 + e^{-x}}$$

  • Indica en valor de la función en los puntos -2,-1 y 1
  • Indica el valor de la derivada en los puntos anteriores
  • Según el valor de la derivada de los puntos anteriores indica si la función crece o decrece y "cuanto"
  • Averigua en qué valor la x tendría el valor mínimo.

Ejercicio 3

Copia el siguiente código python:

#Código base de varios ejercicios.
 
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from matplotlib.ticker import MaxNLocator
from matplotlib.ticker import MultipleLocator
 
def sigmoid(z):
    return 1/(1 + np.exp(-z))
  
def predict_formula(x,w,b):
    return sigmoid(w*x+b)
 
 
def loss_mae(y_true,y_pred):
    error=np.abs(np.subtract(y_true,y_pred))
    mean_error=np.sum(error)/len(y_true)
  
    return mean_error
  
  
  
def loss(x,y_true,w,b):
    y_pred=predict_formula(x,w,b)
   
    return loss_mae(y_true,y_pred)
 
 
def plot_loss(x,y_true,perdida_original,valor_parametro_inicial,rango,perdidas,xlabel,axes):
    axes.set_ylim(ymin=0.0,ymax=0.7)
    axes.plot(rango,perdidas)
    axes.scatter(valor_parametro_inicial,perdida_original,color="#ff0000",s=40)
    axes.set_xlabel(xlabel)
    axes.set_ylabel('loss')
 
    axes.vlines(x = valor_parametro_inicial, ymin = perdida_original, ymax = 0.7,colors = '#ff0000',linestyle="dashed")
    axes.text(valor_parametro_inicial+0.1,0.72,f'{xlabel}={valor_parametro_inicial:0.2f}',c="#ff0000")
 
 
    min_x=rango[np.argmin(perdidas)]
    min_y=np.min(perdidas)
    axes.vlines(x = min_x, ymin = 0, ymax = min_y,colors = '#00ff00',linestyle="dashed")
    axes.text(min_x+0.1,0.015,f'{min_x:0.2f}',c="#00ff00")
 
     
def plot_simple_metrics(axes,history,title):
 
    axes.plot(history,linestyle="dotted",label=f"loss :{history[-1]:.2f}",c="#003B80")  
 
    axes.set_xlabel('Nº Épocas', fontsize=13,color="#003B80") 
    axes.xaxis.set_major_locator(MaxNLocator(integer=True))
 
    axes.set_ylabel('Métricas', fontsize=13,color="#003B80")
    axes.set_ylim(ymin=0,ymax=1.1)
    axes.yaxis.set_major_locator(MultipleLocator(0.1))
 
    axes.set_title(title)
    axes.set_facecolor("#F0F7FF")
    axes.grid(visible=True, which='major', axis='both',color="#FFFFFF",linewidth=2)
    axes.legend()
 
 
def plot_losses(x,y_true,w_inicial,b_inicial,subfigure):
    perdida_original=loss(x,y_true,w_inicial,b_inicial)
 
    kk=subfigure
    subfigure.suptitle(f'loss={perdida_original:0.2f}',c="#ff0000")

    axes_w=subfigure.add_subplot(1,2,1)
    axes_b=subfigure.add_subplot(1,2,2)   
 
    rango=np.linspace(-5,5,400)
    perdidas_w=[]
    perdidas_b=[]
 
    for parametro in rango:
        perdidas_w.append(loss(x,y_true,parametro,b_inicial))
        perdidas_b.append(loss(x,y_true,w_inicial,parametro))
 
 
    plot_loss(x,y_true,perdida_original,w_inicial,rango,perdidas_w,"w",axes_w)
    plot_loss(x,y_true,perdida_original,b_inicial,rango,perdidas_b,"b",axes_b)
 
    return perdida_original
 
def plot_evolucion_parametros(axes,ws,bs):
    axes.plot(ws,linestyle="solid",label="w",c="#6ABF40")  
    axes.plot(bs,linestyle="solid",label="b",c="#BF9140")     
 
    axes.set_xlabel('Nº Épocas', fontsize=13,color="#003B80") 
    axes.xaxis.set_major_locator(MaxNLocator(integer=True))
 
    axes.set_ylabel('Valor de los parámetros', fontsize=13,color="#003B80")
    axes.set_ylim(ymin=-5,ymax=5)
    axes.yaxis.set_major_locator(MultipleLocator(1))
 
    axes.set_title("Evolución de los parámetros en cada época")
    axes.set_facecolor("#F0F7FF")
    axes.grid(visible=True, which='major', axis='both',color="#FFFFFF",linewidth=2)
    axes.legend()
 


def plot_parametros(x,y_true,parametros):
    figure=plt.figure(figsize=(8,3.5*(len(parametros)+1)),layout='constrained')
    figure.suptitle("$y=\\frac{1}{1 + e^{-( w \\cdot x+b  )}}$")
    subfigures = figure.subfigures(nrows=len(parametros)+1, ncols=1)

    ws=[]
    bs=[]
    history=[]
    for index,(w,b) in enumerate(parametros):
        subfigure=subfigures[index]
 
        loss=plot_losses(x,y_true,w,b,subfigure)
        ws.append(w)
        bs.append(b)
        history.append(loss)
 
    axes=subfigures[-1].add_subplot(1,2,1)
    plot_simple_metrics(axes,history,"loss")
    axes=subfigures[-1].add_subplot(1,2,2) 
    plot_evolucion_parametros(axes,ws,bs)
 
def descenso_gradiente(x,y_true,learning_rate,w,b):
    h=0.000003
  
    gradiente_w =(loss(x,y_true,w+h,b)-loss(x,y_true,w,b))/h
    gradiente_b =(loss(x,y_true,w,b+h)-loss(x,y_true,w,b))/h
 
    w=w-learning_rate*gradiente_w
    b=b-learning_rate*gradiente_b
    return w,b
 
 
def plot_descenso_gradiente(x,y_true,w_inicial,b_inicial,learning_rate,epochs):
    figure=plt.figure(figsize=(8,3.5*epochs),layout='constrained')
    figure.suptitle("$y=\\frac{1}{1 + e^{-( w \\cdot x+b  )}}$")
 
    subfigures = figure.subfigures(nrows=epochs+1, ncols=1)
    w=w_inicial
    b=b_inicial
 
    ws=[]
    bs=[]
    history=[]
    for epoch in range(epochs):
        if (epochs>1):
            subfigure=subfigures[epoch]
        else:
            subfigure=subfigures
 
        loss=plot_losses(x,y_true,w,b,subfigure)
        ws.append(w)
        bs.append(b)
        history.append(loss)
        w,b=descenso_gradiente(x,y_true,learning_rate,w,b)
 
    axes=subfigures[-1].add_subplot(1,2,1)
    plot_simple_metrics(axes,history,"loss")
    axes=subfigures[-1].add_subplot(1,2,2)    
    plot_evolucion_parametros(axes,ws,bs)

Vamos a trabajar con una red neuronal correspondiente a una única neurona y función de activación sigmoide, que corresponde a la siguiente fórmula:

$$ \large y=\frac{1}{1 + e^{-( w \cdot x+b )}} $$

Lo que debe hacer es entrenar tu manualmente la red , es decir, encontrar los mejores valores de w y b.

Para ello ejecuta el siguiente código:

iris=load_iris()
x=iris.data[0:99,2]
y_true=iris.target[0:99]

parametros=[(-0.3,0.1)]
plot_parametros(x,y_true,parametros)

Que muestra la siguiente gráfica:

Esta gráfica muestra en rojo que con los valores de w=-0.3 y b=0.1 el valor de la pérdida de la red es 0.59. loss=0.59. Mientras que en verde se muestra los valores que harían mínima cada ua de las funciones w=0.55 y b=-4.

Modifica el array parametros añadiendo mas pares de valores de w y b para ver si consigues obtener un valor de loss lo más cercano a 0 que sea posible. Deberás ir modificando poco a poco los valores de w y b.

Ejercicio 4

Entrena ahora la red neuronal usando el descenso de gradiente. Para ello ejecuta el siguiente código:

iris=load_iris()
x=iris.data[0:99,2]
y_true=iris.target[0:99]

w_inicial=-0.3
b_inicial=0.1
learning_rate=0.3
epochs=5
plot_descenso_gradiente(x,y_true,w_inicial,b_inicial,learning_rate,epochs)

Modifica los valores de learning_rate y epochs para que la red se entrene sola para obtener un valor de loss lo más cercano a 0 que sea posible.

Ejercicio 5.A

Crea una red neuronal para entrenar las flores. La red tendrá las siguientes características:

  • Capas: [4,8,3]
  • Función de activación: selu
  • Épocas: 400

Y con todas las combinaciones de:

  • Optimizadores
    • tf.keras.optimizers.SGD
    • tf.keras.optimizers.Adagrad
    • tf.keras.optimizers.RMSprop
    • tf.keras.optimizers.Adam
    • tf.keras.optimizers.Adamax
    • tf.keras.optimizers.Nadam
  • Tasa de aprendizaje
    • 0.1
    • 0.01
    • 0.001
    • 0.0001

Muestra las gráficas de la pérdida en función de las épocas de forma que por filas estén los optimizadores y por columnas las tasas de aprendizaje.

Responde las siguientes cuestiones:

  • ¿Cual ha resultado ser el mejor optimizador?
  • ¿Cual ha sido la mejor tasa de aprendizaje para el mejor optimizador?
  • Indica para cada optimizador cual es la mejor tasa de aprendizaje
  • Explica que problema tiene en general la tasa de aprendizaje de 0.1
  • ¿Que problema tiene en general tasas de aprendizaje altas?

Ejercicio 5.B

Repite el ejercicio anterior pero ahora solo con 20 épocas.

¿Que tasas de aprendizaje y optimizadores se ve que van a generar redes inestables?

Ejercicio 6.A

Haz una red neuronal que averigüe un dígito que se ha escrito a mano. Para ello obtén los datos con el siguiente código:

from sklearn.datasets import load_digits

def get_datos():
  datos=load_digits()
  x=datos.data
  y=datos.target
  label_binarizer = LabelBinarizer()
  label_binarizer.fit(range(max(y)+1))
  y = label_binarizer.transform(y)

  x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2, random_state=42,stratify=y)

  return x_train, x_test, y_train, y_test

La red debería ser realmente una red convolucional pero en este ejemplo usaremos una red neuronal complemente conectada

La red tendrá las siguientes características:

  • Neuronas por capa: 64,128,64,32,16,10
  • Función de activación de las capas ocultas: selu

Prueba únicamente con 5 épocas con todas las combinaciones de lo siguiente:

  • Optimizadores
    • tf.keras.optimizers.SGD
    • tf.keras.optimizers.Adagrad
    • tf.keras.optimizers.RMSprop
    • tf.keras.optimizers.Adam
    • tf.keras.optimizers.Adamax
    • tf.keras.optimizers.Nadam
  • Tasa de aprendizaje
    • 0.1
    • 0.01
    • 0.001
    • 0.0005

Muestra las gráficas de la pérdida en función de las épocas de forma que por filas estén los optimizadores y por columnas las tasas de aprendizaje.

Indica para cada tasa de aprendizaje y para cada optimizador si la entrenarías o no y el motivo.

Ejercicio 6.B

Siguiendo con el ejercicio anterior, entrena ahora la red con 300 épocas pero ahora solo con los optimizadores/tasas de aprendizaje que seleccionaste en el ejercicio anterior

Ejercicio 6.C

Repite el ejercicio anterior pero ahora con TODAS las combinaciones de optimizadores y tasas de aprendizaje.

¿Fue adecuada la selección que hiciste en el ejercicio anterior?

clase/iabd/pia/2eval/tema07.backpropagation_descenso_gradiente.txt · Última modificación: 2024/02/28 15:38 por admin