====== 7.h Código ======
Durante el tema hemos visto los gráficos que explican el descenso de gradiente. Veamos ahora como se pueden hacer dichos gráficos en Python.
La función que coste que hemos usado es la siguiente
$$
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}
$$
cuya gráfica es la siguiente:
{{:clase:iabd:pia:2eval:descenso_gradiente_normal_3d.png?direct|}}
En python la función $loss(w_0,w_1)$ con **NumPy** sería así:
def loss(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)
Y el algoritmo que calcula cada uno de los $w_0,w_1$ del descenso de gradiente es el siguiente:
def get_puntos_descenso_gradiente(epochs,learning_rate,w_0_original,w_1_original):
w_0=w_0_original
w_1=w_1_original
puntos_descenso_gradiente=np.array([[w_0,w_1]])
for epoch in range(epochs):
h=0.00001
gradiente_w_0=(loss(w_0+h,w_1)-loss(w_0,w_1))/h
gradiente_w_1=(loss(w_0,w_1+h)-loss(w_0,w_1))/h
#Nuevos valores de los pesos
w_0=w_0-learning_rate*gradiente_w_0
w_1=w_1-learning_rate*gradiente_w_1
puntos_descenso_gradiente=np.append(puntos_descenso_gradiente,[[w_0,w_1]], axis=0)
return puntos_descenso_gradiente
Si ejecutamos lo siguiente:
get_puntos_descenso_gradiente(5,0.03,-0.35,-0.67)
El resultado es:
array([[-0.35 , -0.67 ],
[-0.26931594, -0.72470447],
[-0.13292324, -0.838827 ],
[ 0.0654496 , -1.06459902],
[ 0.24087009, -1.40749396],
[ 0.2657862 , -1.59573582]])
Que son cada uno de los valores de $w_0,w_1$ que empiezan en ''-0.35, -0.67'' y acaban en ''0.2657862, -1.59573582$''
Pasamos ahora a mostrar los valores dentro de la gráfica de la función, para ello hemos creado 2 funciones:
* ''plot_loss_function'': Que dibuja la superficie con todos los colores
* ''plot_descenso_gradiente'': Dibuja los puntos por donde va pasando el algoritmo del descenso de gradiente
def plot_loss_function(axes,fontsize=25,title=""):
rango_w_0=np.linspace(-3,3,100)
rango_w_1=np.linspace(-3,3,100)
rango_w_0,rango_w_1=np.meshgrid(rango_w_0,rango_w_1)
loss=loss_function(rango_w_0,rango_w_1)
axes.contourf(rango_w_0,rango_w_1,loss,30,cmap="coolwarm")
axes.set_xlabel('w₀',fontsize=fontsize,color="#003B80")
axes.set_ylabel('w₁',fontsize=fontsize,color="#003B80")
axes.set_title(title)
def plot_descenso_gradiente(axes,puntos_descenso_gradiente):
axes.scatter(puntos_descenso_gradiente[1:-1,0],puntos_descenso_gradiente[1:-1,1],13,color="yellow")
axes.plot(puntos_descenso_gradiente[:,0],puntos_descenso_gradiente[:,1],color="yellow")
axes.plot(puntos_descenso_gradiente[0,0],puntos_descenso_gradiente[0,1],"*",markersize=12,color="red")
axes.plot(puntos_descenso_gradiente[-1,0],puntos_descenso_gradiente[-1,1],"*",markersize=12,color="blue")
Si ejecutamos el código:
figure=plt.figure(figsize=(16,15))
axes = figure.add_subplot()
plot_loss_function(axes)
plot_descenso_gradiente(axes,get_puntos_descenso_gradiente(5,0.03,-0.35,-0.67))
Vemos la siguiente gráfica donde se muestran los puntos que hemos obtenido de la función ''get_puntos_descenso_gradiente''
{{ :clase:iabd:pia:2eval:descenso_gradiente_algoritmo_vanilla_1.png?direct |}}
* 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.
==== Optimizadores de Keras ====
Podemos mejorar nuestro código en Python haciendo que podamos usar directamente los [[https://keras.io/api/optimizers/|Optimizers]] de Keras y de esa forma ver como funciona cada uno de ellos. Para poder usarlos directamente vamos a creas las nuevas funciones ''loss_tf'' y ''get_puntos_descenso_gradiente_optimizer'' adecuadas a TensorFlow y Keras.
def loss_tf(w_0,w_1):
return 3*(1 - w_0)**2 * tf.exp(-w_0**2 - (w_1 + 1)**2) - 10*(w_0/5 - w_0**3 - w_1**5)*tf.exp(-w_0**2 - w_1**2) - 1./3*tf.exp(-(w_0 + 1)**2 - w_1**2)
def get_puntos_descenso_gradiente_optimizer(epochs,optimizer_function,w_0_init,w_1_init):
puntos_descenso_gradiente=np.array([[w_0_init,w_1_init]])
w_0=w_0_init
w_1=w_1_init
for epoch in range(epochs):
var_w_0=tf.Variable(w_0)
var_w_1=tf.Variable(w_1)
optimizer_function.minimize(lambda: loss_tf(var_w_0,w_1), var_list=[var_w_0])
optimizer_function.minimize(lambda: loss_tf(w_0,var_w_1), var_list=[var_w_1])
w_0=var_w_0.numpy()
w_1=var_w_1.numpy()
puntos_descenso_gradiente=np.append(puntos_descenso_gradiente,[[w_0,w_1]], axis=0)
return puntos_descenso_gradiente
Lo que ha cambiado principalmente es la función ''get_puntos_descenso_gradiente_optimizer'' no hace cálculo del gradiente (derivada) ni actualiza los parámetros, sino que llama a la función de Keras de optimización.
Si usamos ''get_puntos_descenso_gradiente_optimizer'' ahora con ''tf.keras.optimizers.SGD''
get_puntos_descenso_gradiente_optimizer(5,tf.keras.optimizers.SGD(learning_rate=0.03),-0.35,-0.67)
array([[-0.35 , -0.67 ],
[-0.269319 , -0.72470832],
[-0.13292952, -0.83883744],
[ 0.06544246, -1.06462073],
[ 0.24086528, -1.40752137],
[ 0.26578248, -1.59573841]])
Vamos que el resultado es prácticamente el mismo que cuando lo hicimos manualmente.
Y podemos generar de la misma forma el gráfico:
figure=plt.figure(figsize=(16,15))
axes = figure.add_subplot()
plot_loss_function(axes)
plot_descenso_gradiente(axes,get_puntos_descenso_gradiente_optimizer(5,tf.keras.optimizers.SGD(learning_rate=0.03),-0.35,-0.67))
{{ :clase:iabd:pia:2eval:optimizer_sgd.png?direct |}}
Y obviamente el resultado es el mismo