May 03, 2024 | 16 min read

6. Optimizadores#

Tal y como se menciona en el notebook de 1. Introducci贸n, el aprendizaje autom谩tico tinene como objetivo aprender de las observaciones existentes y hacer uso de ese aprendizaje para predecir nuevas observaciones o determinar el resultado de nuevas entradas. Para ello, es necesario un proceso iterativo en el que se busca minimizar el error cometido, este proceso se conoce como el entrenamiento del modelo [10].

En objetivo del entrenamiento es lograr el menor error posible, para ello se evalua la funci贸n de coste conociendo as铆 la calidad de las predicciones del modelo, en otras palabras conoceremos como de buena es una soluci贸n. El proceso ir谩 avanzando, mejorando la precisi贸n en la predicci贸n, hasta cumplir con el criterio de parada fijado. La tarea de minimizar las funciones de coste no es sencilla y existen diversas t茅cnicas para abordarla, algunas de ellas se muestran a continuaci贸n.

6.1. Descenso por gradiente#

El descenso por gradiente (gradient descent) es un algoritmo de optimizaci贸n utilizado para realizar el entrenamiento de modelos de Machine Learning y Deep learning que se basa en el concepto del gradiente para llevar a cabo la minimizaci贸n del error cometido [20]. Cabe destacar que para poder encontrar el m铆nimo de la funci贸n este m茅todo asume que se puede obtener el gradiente de la funci贸n a estudiar y que esta es cont铆nua y diferenciable (no necesariamente en toda la funci贸n).

Para poder implementar este algoritmo de optimizaci贸n se debe definir la direcci贸n y la tasa de aprendizaje (tambi茅n conocido como el tama帽o del paso). El primero de ellos nos indica en qu茅 direcci贸n seguir explorando el conjunto de soluciones mientras que el segundo especifica cuanto avanzar en la direcci贸n seleccionada [4].

El algoritmo parte desde un punto arbitrario \(x^{(0)}\) en el que se debe conocer el valor inicial de la funci贸n objetivo a minimizar. En cada paso \(k \geq 0\) se avanza de forma iterativa en la direcci贸n opuesta al gradiente, ya que se trabaja con problemas a minimizar. Cabe destacar que esto no supone un l铆mite a la hora de definir la cantidad de problemas que se pueden resolver con esta t茅cnica, ya que todos los problemas a maximizar se pueden transformar en problemas a minimizar mediante un cambio de signo. La busqueda de la soluci贸n utilizando el descenso por gradiente se puede resumir mediante la siguiente expresi贸n matem谩tica.

\[ x^{(k+1)} = x^{(k)} - t_k \nabla f(x^{(k)})\]

Donde \(x^{(k+1)}\) corresponde al nuevo punto, \(x^{(k)}\) representa el punto actual en el que se encuentra el algoritmo, \(t_k\) hace referencia al tama帽o del paso (tambi茅n se utiliza \(\alpha\) para representar este par谩metos) y \(\nabla f(x^{(k)})\) corresponde al gradiente en el punto actual.

Tal y como se aprecia en la expresi贸n anterior, para encontrar el nuevo punto el algoritmo se avanza en el sentido opuesto al gradiente, esto se indica mediante el signo negativo.

A continuaci贸n, se adjunta el pseudo-c贸digo del descenso por gradiente.

../../_images/pseudo_GD.PNG

Fig. 14 Pseudo-c贸digo del descenso por gradiente.#

Nota

La tasa de aprendizaje tambi茅n conocida como tama帽o del paso, suele tomar un valor peque帽o. Existen algunas variantes del algoritmo en las que se eval煤a y se actualiza en funci贸n del comportamiento de la funci贸n de coste. Mediante estas estrategias se busca explorar el espacio de soluciones en las primeras iteraciones y explotar las buenas soluciones conforme avanzan las iteraciones [3].

Una tasa de aprendizaje alta conlleva pasos m谩s grandes, por lo que puede que el algoritmo encuentre la soluci贸n antes, pero se corre el riesgo de sobrepasar el minimo y que el algoritmo no converja a una soluci贸n. El descenso del gradiente con una tasa baja, sin embargo, avanza muy despacio y necesita muchas iteraciones para encontrar una buena soluci贸n, resultando as铆 un proceso muy costoso.

6.1.1. Problemas del descenso por gradiente#

El algoritmo de descenso por gradiente se basa en que la funci贸n de coste es convexa, cuando esta condici贸n se cumple el algoritmo de descenso por gradiente encuentra con facilidad el m铆nimo global. En problemas no convexos sin embargo, este algoritmo puede encontrarse con dificultades para llegar hasta el minimo global y quedarse atrapado en minimos locales.

Es importante recordar que cuando la pendiente de la funci贸n de coste (el gradiente de la funci贸n) es cercana a cero el modelo deja de aprender, dejando al algoritmo atrapado en dichos puntos. Cabe destacar que esta situaci贸n se puede producir tambi茅n en puntos silla (tambi茅n llamados puntos de inflexi贸n) y m铆nimos locales, por lo que en estos casos hacer uso del descenso por gradiente no asegura el obtener la soluci贸n 贸ptima al problema.

../../_images/punto_inflexion.png

Fig. 15 Ejemplo de m谩ximo, m铆nimo y punto silla de una funci贸n.#

Se detectan otras dos dificultades al utilizar el descenso por gradiente para ajustar los par谩metros de las redes neuronales.

  • Desvanecimiento del gradiente (gradientes fuga), esto ocurre cuando el gradiente es demasiado peque帽o en la retropropagaci贸n en las redes neuronales. El gradiente se reduce lo que provoca que las capas intermedias de la red aprendan muy lentamente, cuando esto sucede los par谩metros se actualizan hasta que toman el valor nulo. Cuando esto ocurre el algoritmo que se esta entrenando deja de aprender. [20]

  • El problema de gradientes explosivos es justamente el efecto opuesto al anterior, es decir, cuando el gradiente es demasiado grande. En estos casos se crea un modelo inestable, los pesos de la red neuronal crecen demasiado hasta que pasan a tomar el valor NaN. [20]

6.1.2 Implementaci贸n#

A continuaci贸n se implementa un ejemplo con el dataset two moons para visualizar el funcionamiento de este algoritmo de optimizaci贸n. Este conjunto de datos cuenta con dos car谩cteristicas y las instancias se dividen en dos clases, por lo tanto el objetivo es clasificar dichas clases cometiendo el menor error posible. No obstante, cabe destacar que la frontera de decisi贸n no es lineal, lo que supone una dificultad a帽adida en el aprendizaje.

M谩s adelante se muestra gr谩ficamente el dataset de manera que se clarifica el problema a abordar.

Para poder trabajar con el ejemplo planteado, primero se ha generado un entorno que contiene el software Qibo en la versi贸n 0.1.12 y tensorflow en la versi贸n 2.10.0.

# Importar las librer铆as necesarias

import numpy as np
import qibo
import matplotlib.pyplot as plt
from qibo import callbacks, gates, hamiltonians, models
from qibo.symbols import X, Y, Z, I
from sklearn.datasets import make_moons
from qibo.models import Circuit

# Se desactiva los mensajes de tensorflow
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'

import tensorflow as tf

# Fijar el backend en el que se ejecutar谩 el programa
qibo.set_backend("tensorflow")
[Qibo 0.1.12.dev0|INFO|2024-06-11 12:06:56]: Using tensorflow backend on /device:CPU:0
# Definir variables necesarias para la ejecuci贸n del algoritmo:

nclasses=2
measured_qubits= int(np.ceil(np.log2(nclasses)))
nqubits=2
nlayers=3
nshots=10000

En la funci贸n procesdata se estandarizan los datos de entrada mediante la normalizaci贸n min-max y se genera un nuevo conjunto de datos que almacenan dichos valores.

def procesdata(data): 
    data=np.array(data)
    Min=data.min(axis=0)
    Max=data.max(axis=0)
    data=(2*data-Max-Min)/(Max-Min)
    one=np.ones((len(data),2))
    data_p=np.c_[data,data]
    return data_p

En la siguiente celda se carga el conjunto de datos que se utilizar谩 en este notebook para testear el funcionamiento de distintas t茅cnicas de optimizaci贸n. Tras ejecutar dicha celda obtendremos la representaci贸n gr谩fica del dataset two moons.

num_inputs = 2
num_samples = 500
X,y=make_moons(num_samples,noise=0.1)

y01 = 1 * (np.sum(X, axis=1) >= 0) # in { 0,  1}
y_one_hot = np.zeros((num_samples, 2))

for i in range(num_samples):
    y_one_hot[i, y01[i]] = 1
X_pad=procesdata(X)

for x, y_target in zip(X_pad[:,0:2], y):
    if y_target == 1:
        plt.plot(x[0], x[1], "bo")
    else:
        plt.plot(x[0], x[1], "go")

plt.show()
../../_images/e5af9eeae6891dd5e2702b936b1f3a506a753aa2ec7f07b5582aefa9ccc2caaa.png

Tal y como ya se ha comentado en notebooks anteriores, para poder entrenar el modelo dividiremos el dataset inicial en el conjunto de entremaniento (train) y en el que nos servir谩 para conocer el comportamiento del circuito con datos desconocidos (val).

Y = 2*y -np.ones(len(y))

num_data = len(Y)
num_train = int(0.80 * num_data)

index = np.random.permutation(range(num_data))

Y_train = Y[index[:num_train]]
Y_val = Y[index[num_train:]]
X_train = X_pad[index[:num_train]]
X_val = X_pad[index[num_train:]]

El circuito que se va a utilizar en este notebook est谩 detallado en la siguiente funci贸n. Se pueden distinguir dos fases:

1. Encoding: El encoding est谩 definido por una rotaci贸n en el eje Y y otra rotaci贸n en el eje Z. Como se puede apreciar en el c贸digo, para poder codificar los datos con los que estamos trabajando se deben introducir como 谩ngulos de rotaci贸n en las puertas iniciales. Este m茅todo de codificaci贸n se explica m谩s detalladamente en el notebook 2. Feature encoding.

2. Circuito variacional: Como se aprecia en la funic贸n create_circuit el circuito variacional cuenta con tres capas, cada una de ellas se compone con dos rotaciones en el eje Y y una CNOT necesario para entrelazar los dos qubits, y una 煤ltima rotaci贸n en el eje Y antes de realizar la medida.

def create_circuit(w,x, nqubits= 2):
    
    c= Circuit(nqubits=nqubits)
    
    # ENCODING:

    c.add(gates.RY(q=0, theta = w[0]*x[0]+w[4]))
    c.add(gates.RY(q=1, theta = w[1]*x[1]+w[5]))
    
    c.add(gates.RZ(q=0, theta = w[2]*x[2]+w[6]))
    c.add(gates.RZ(q=1, theta = w[3]*x[3]+w[7]))
    
    # CIRCUITO VARIACIONAL:
    
    # LAYER 1
    c.add(gates.RY(q=0, theta = w[4]))
    c.add(gates.RY(q=1, theta = w[5]))
     
    c.add(gates.CZ(0,1))
    
    c.add(gates.RY(q=0, theta = w[6]))
    c.add(gates.RY(q=1, theta = w[7]))
    
    # LAYER 2
    c.add(gates.RY(q=0, theta = w[8]))
    c.add(gates.RY(q=1, theta = w[9]))
    
    c.add(gates.CZ(0,1))
    
    c.add(gates.RY(q=0, theta = w[10]))
    c.add(gates.RY(q=1, theta = w[11]))
    
    # LAYER 3
    c.add(gates.RY(q=0, theta = w[12]))
    c.add(gates.RY(q=1, theta = w[13]))

    c.add(gates.CZ(0,1))
    
    c.add(gates.RY(q=0, theta = w[14]))
    c.add(gates.RY(q=1, theta = w[15]))
    
    # 脷LTIMA ROTACI脫N EN Y:
    c.add(gates.RY(q=0, theta = w[16]))
    c.add(gates.RY(q=1, theta = w[17]))
    
    # MEDIMOS EL QUBIT 0:
    c.add(gates.M(0))
    
    return c

La siguente funci贸n calcula el rendimiento del modelo, para ello se utilizan dos m茅tricas:

Definici贸n: Error cuadr谩tico medio

Mide el promedio del error al cuadrado, donde el error se considera la diferencia entre la predicci贸n que realiza el modelo y la etiqueta almacenada en el dataset. La formula con la que se calcula este valor es la siguiente:

\[ MSE = \frac{1}{N} \sum_{i=1}^N (y_i-\hat y_i)^2 \]

Definici贸n: Accuracy

Se trata de una m茅trica que muestra fracci贸n de ejemplos que se han clasificado correctamente, es decir, que el modelo ha predicho la clase a la que pertenece.

\[ \text{Accuracy} = \frac{\text{Number of correct pedictions}}{\text{Total number of predictions}} \]
def sqloss_acc(labels, predictions):
    sqloss = 0
    acc = 0
    # Recorremos todas las instancias comparando el label conocido con la predicci贸n que ha realizado el modelo.
    for label, prediction in zip(labels, predictions):
        
        # Modificamos el valor de loss
        sqloss = sqloss + (label - prediction)**2
        
        #Vemos si el label y la predicci贸n coincide para calcular el accuracy
        if np.sign(label)==np.sign(prediction):
            acc += 1
    # dividir entre la cantidad de datos de entrada para obtener un valor entre 0 y 1.
    sqloss = sqloss / len(labels)
    acc = acc / len(labels)
    
    print('Loss: ', sqloss.numpy(), 'Acc: ', acc)
    return sqloss, acc
#  En esta funci贸n se llama a la funci贸n de crear el circuito con los par谩metros necesarios.
def Classifier_circuit(theta,data,nqubits=2):
    circ = create_circuit(theta,data, nqubits)
    return circ

La funci贸n hamiltonian var铆a en funci贸n del n煤mero de qubits con los que se este trabajando, en este caso nos interesa realizar una medici贸n Z en el qubit 0 por lo que realizamos el producto Z en el qubit 0 y la identidad para el resto de qubits. Haremos uso de est谩 funci贸n para calcular el valor esperado del circuito y nos facilitar谩 el c谩lculo del gradiente que necesitaremos m谩s adelante.

# Definir el observable:
def hamiltonian():
    Obj = np.prod([ Z(0), I(1)])
    h = hamiltonians.SymbolicHamiltonian(Obj)
    return h

La funci贸n Predictions es la encargada de ejecutar el circuito y obtener el valor esperado. Tiene como par谩metros de entrada el n煤mero de qubits, el n煤mero de shots, los pesos (son los par谩metros que deseamos optimizar y corresponden a las rotaciones de las puertas del circuito) y una instancia x del dataset (unicamente las variables de entrada, excluimos el valor de la etiqueta).

def Predictions(data, theta, nqubits, nshots=10000):

    c = Classifier_circuit(theta,data,nqubits)
    h = hamiltonian()
    expected_value = h.expectation(c.execute().state())
    
    return expected_value

En la funci贸n de coste realizamos las predicciones para los datos de entrada y calculamos el rendimiento del modelo.

def Cost_function(theta, data, Y, nqubits, nshots=10000):
    predictions = [Predictions(x, theta,nqubits) for x in data]
    return sqloss_acc(Y, predictions)[0]

En la siguiente celda se lleva a cabo todo el proceso:

  1. Definir el n煤mero de qubits necesarios, n煤mero de iteraciones a realizar y el batch size.

  2. Inicializar los 谩ngulos de rotaci贸n (par谩metros a optimizar).

  3. Iterar para optimizar los par谩metros correspondientes a las rotaciones de las puertas. Para ello se calcula el gradiente y se calculan los nuevos valores de los angulos de rotaci贸n.

from time import time

start_time = time()

# Definici贸n de algunos par谩metros
nqubits = 2
n_iter = 60

learning_rate = 0.25
batch_size = 5

np.random.seed(0)

# Inicializar los 谩ngulos de rotaci贸n
test_params = np.random.normal(0,1,18)*0.01


print('n煤mero de par谩metros a ajustar:',len(test_params))
print('test_params_INI',test_params)


np.random.seed(123)

params = tf.Variable(test_params)

for it in range(n_iter):  # N煤mero m谩ximo de iteraciones que har谩 el algoritmo.
    print('\nIteraci贸n',it)
 
   
   # Calcular el gradiente:
    with tf.GradientTape() as tape:
        value_cost= Cost_function(params, X_train, Y_train, nqubits)

    tf_grads = tape.gradient(value_cost, [params])
    
    # Actualizar los par谩metros:
    optimizer = getattr(tf.optimizers, "SGD")(learning_rate=learning_rate)
    optimizer.apply_gradients(zip(tf_grads, [params]))

        
tiempo_ej = time()-start_time
print('El tiempo necesario para ejecutar el algoritmo ha sido: ',tiempo_ej/60, 'minutos')

print(params)

Nota

La celda anterior no se ejecuta al generar la p谩gina web debido a que el tiempo requerido es muy elevado. Los par谩metros obtenidos tr谩s el proceso de optimizaci贸n se indican en la siguiente celda para poder obtener los siguientes gr谩ficos.

params_SGD = [-0.94029041,  1.76279408,  0.07174585, -0.08851703,  0.59543365,
       -0.61919009,  0.16678696, -0.11759714,  0.40629924, -0.21482006,
        0.15629836, -0.10974645,  0.16246831, -0.12307243,  0.23741627,
        0.00333674,  0.24791843, -0.00205158]

Una vez entrenado el modelo, se utilizan los par谩metros ajustados para conocer las predicciones y conocer el error y tasa de acierto que se ha logrado. A continuaci贸n se calcula el error al realizar las predicciones del conjunto val y por otro lado el comentido con todas las instancias.

# Calcular coste con todos los datos
cost_val = Cost_function(params_SGD, X_val, Y_val,nqubits)
cost_all = Cost_function(params_SGD, X_pad, Y,nqubits)
Loss:  0.4175923049620295 Acc:  0.9
Loss:  0.3545756770024227 Acc:  0.894

En la siguiente imagen se puede comparar la clasificaci贸n obtenida mediante el modelo entrenado y las etiquetas reales que presentaban los datos en el dataset.

predictions = [Predictions(x, params_SGD,nqubits) for x in X_pad]
fig, ax = plt.subplots(1,2,figsize=(12,5))

# Visualizar las predicciones realizadas por el modelo
ax[0].set_title('Predicciones')
for x, y_target in zip(X_pad[:,0:2], predictions):
    if np.sign(y_target) == 1:
        ax[0].plot(x[0], x[1], "bo")
    else:
        ax[0].plot(x[0], x[1], "go")
        
# Visualizar los valores reales del label
ax[1].set_title('Valores Reales')
for x, y_target in zip(X_pad[:,0:2], y):
    if y_target == 1:
        ax[1].plot(x[0], x[1], "bo")
    else:
        ax[1].plot(x[0], x[1], "go")
../../_images/81d1e715aa3ff1db4ca8a994f7b7651a77e38daa24a69a85f28ce14e17707219.png

Tal y como se aprecia en la imagen anterior, el modelo no llega a aprender correctamente. El clasificador que obtenemos no consigue generar la frontera de decisi贸n no lineal que se comentaba anteriormente. Todos los errores se comenten en la zona central (la que genera la dificultad), es por eso que el valor del error no es demasiado alto ni el del accuracy excesivamente bajo.

6.2. Momentum#

Como se ha comentado en apartados anteriores, el algoritmo de descenso por gradiente puede encontrarse con ciertas dificultades que empeoren considerablemente su funcionamiento. Esto principalmente se debe a que el resultado obtenido tiene una gran dependencia de la tasa de aprendizaje y del gradiente del paso actual, por lo que al no tener en cuenta la trayectoria de soluciones exploradas se puede quedar atrapado en puntos que aparentan ser una buena soluci贸n (por ejemplo m铆nimos locales y puntos silla). Para intentar solventar este tipo de dificultades se implementa el concepto de Momentum.

6.2.1. Algoritmo#

Este algoritmo es una extensi贸n de la t茅cnica anterior, por lo que la estrateg铆a a seguir es muy similar, la variaci贸n que sufre esta t茅cnica es que en la exploraci贸n de soluciones se tiene en cuenta las soluciones ya exploradas. Para integrar la inercia se puede hacer uso del concepto estad铆stico de media m贸vil sobre los gradientes pasados. En las regiones donde la pendiente es alta, las actualizaciones ser谩n significativas, de manera que en cierto modo, se gana impulso al tomar una media m贸vil sobre estos gradientes.

Utilizando esta t茅cnica se consigue introducir la inercia. Sin embargo, utilizando este concepto estad铆stico todos los gradientes tienen la misma ponderaci贸n, en otras palabras, se les otorga la misma importancia a todos los gradientes independientemente de la lejan铆a del momento actual. Es por esto que se utiliza un promedio ponderado, de manera que los gradientes m谩s recientes tengan mayor influencia.

El algoritmo no sufre variaciones dr谩sticas, unicamente se debe tener en cuenta la variaci贸n a la hora de calcular el gradiente. En el descenso por gradiente con momentum el valor del siguiente punto se calcula mediante la siguiente expresi贸n:

\[ x^{(k+1)} = x^{(k)} - a^{(k+1)}\]

donde el t茅rmino \(a^{(k+1)}\) se actualiza mediante la siguiente expresi贸n \( a^{(k+1)} = \beta a^{(k)} + t_k \nabla f(x^{(k)})\) donde \(t_k\) corresponde al tama帽o de paso y \(\beta\) corresponde al momentum, este par谩metro debe cumplir que \( \beta \in [0,1]\) [28].

6.2.2. Como seleccionar \(\beta\)#

Al introducir el momentum se debe ajustar un nuevo par谩metro, este corresponde al peso que le otorgamos a los gradientes anteriores y se denomina \(\beta\). Si el valor de \(\beta\) es peque帽o el gradiante se reduce r谩pidamente pudiendo crear problemas de gradiente desvaneciente. Si se le asigna un valor demasiado grande, sin embargo, este no se reducir谩n tan r谩pido por lo que influir谩n m谩s gradientes en la actualizaci贸n, este segundo caso es el ideal. Cabe destacar que el valor m谩s com煤n para este par谩metos es \(0.9\). [9]

6.2.3. Implementaci贸n#

Para apreciar las diferencias entre los m茅todos, desarrollaremos el mismo ejemplo con este segundo optimizador.

from time import time

start_time = time()

# Definici贸n de algunos par谩metos
nqubits = 2
n_iter = 60

learning_rate = 0.25
momentum = 0.9
batch_size = 5

np.random.seed(0)

# Inicializar los 谩ngulos de rotaci贸n
test_params = np.random.normal(0,1,18)*0.01


print('n煤mero de par谩metros a ajustar:',len(test_params))
print('test_params_INI',test_params)


np.random.seed(123)

params = tf.Variable(test_params)

for it in range(n_iter):  # N煤mero m谩ximo de iteraciones que har谩 el algoritmo.
    print('\nIteraci贸n',it)
    

    # Calcular el gradiente:
    with tf.GradientTape() as tape:
        value_cost= Cost_function(params, X_train, Y_train, nqubits) 

    tf_grads = tape.gradient(value_cost, [params])
    
    # Actualizar los par谩metros:
    optimizer = getattr(tf.optimizers, "SGD")(learning_rate=learning_rate, momentum = momentum)
    optimizer.apply_gradients(zip(tf_grads, [params]))

        
tiempo_ej = time()-start_time
print('El tiempo necesario para ejecutar el algoritmo ha sido: ',tiempo_ej/60, 'minutos')

print(params)

Nota

La celda anterior no se ejecuta al generar la p谩gina web debido a que el tiempo requerido es muy elevado. Los par谩metros obtenidos tr谩s el proceso de optimizaci贸n se indican en la siguiente celda para poder obtener los siguientes gr谩ficos.

params_SGD_momentum = [-0.94029041,  1.76279408,  0.07174585, -0.08851703,  0.59543365,
       -0.61919009,  0.16678696, -0.11759714,  0.40629924, -0.21482006,
        0.15629836, -0.10974645,  0.16246831, -0.12307243,  0.23741627,
        0.00333674,  0.24791843, -0.00205158]

Al igual que en el caso anterior, se calcula el error y el accuracy.

# Calcular coste con todos los datos y mirar precisi贸n para criterio de parada.
cost_val = Cost_function(params_SGD_momentum, X_val, Y_val,nqubits)
cost_all = Cost_function(params_SGD_momentum, X_pad, Y,nqubits)
Loss:  0.4175923049620295 Acc:  0.9
Loss:  0.3545756770024227 Acc:  0.894
predictions = [Predictions(x, params_SGD_momentum,nqubits) for x in X_pad]
fig, ax = plt.subplots(1,2,figsize=(12,5))

# Visualizar las predicciones realizadas por el modelo
ax[0].set_title('Predicciones')
for x, y_target in zip(X_pad[:,0:2], predictions):
    if np.sign(y_target) == 1:
        ax[0].plot(x[0], x[1], "bo")
    else:
        ax[0].plot(x[0], x[1], "go")
        
# Visualizar los valores reales del label
ax[1].set_title('Valores Reales')
for x, y_target in zip(X_pad[:,0:2], y):
    if y_target == 1:
        ax[1].plot(x[0], x[1], "bo")
    else:
        ax[1].plot(x[0], x[1], "go")
../../_images/81d1e715aa3ff1db4ca8a994f7b7651a77e38daa24a69a85f28ce14e17707219.png

El resultado obtenido en este caso es similar al anterior.

6.3. Nesterov Momentum Optimizer (descenso por gradiente con Nertorv Momentum)#

Al igual que los dos m茅todos anteriores, este algoritmo de optimizaci贸n tambi茅n se basa en el gradiente. No obstante, cuenta con una mayor tasa de convergencia en ciertas situaciones. [39]

6.3.1. Algoritmo#

El optimizador Nesterov Momentum es similar al descenso por gradiente con momentum, pero este desplaza el punto actual mediante el t茅rmino de momentum al calcular el gradiente. De manera que la 煤nica modificaci贸n respecto a la t茅cnica anterior es la expresi贸n para actualizar \(a^{(k+1)}\) que se sustituye por \(a^{(k+1)} = \beta a^{(k)} + t_k \nabla f(x^{(k)} - \beta a^{(k)})\).[29]

6.3.2. Implementaci贸n#

from time import time

start_time = time()


# Definici贸n de algunos par谩metros
nqubits = 2
n_iter = 60

learning_rate = 0.25
momentum = 0.9
batch_size = 5

np.random.seed(0)

# Inicializar los 谩ngulos de rotaci贸n
test_params = np.random.normal(0,1,18)*0.01


print('n煤mero de par谩metros a ajustar:',len(test_params))
print('test_params_INI',test_params)


np.random.seed(123)

params = tf.Variable(test_params)

for it in range(n_iter):  # N煤mero m谩ximo de iteraciones que har谩 el algoritmo.
    print('\nIteraci贸n',it)
      
    # Calcular el gradiente:
    with tf.GradientTape() as tape:
        value_cost= Cost_function(params, X_train, Y_train, nqubits)

    tf_grads = tape.gradient(value_cost, [params])
    
    # Actualizar los par谩metros:
    optimizer = getattr(tf.optimizers, "SGD")(learning_rate=learning_rate, momentum = momentum, nesterov=True)
    optimizer.apply_gradients(zip(tf_grads, [params]))
        
tiempo_ej = time()-start_time
print('El tiempo necesario para ejecutar el algoritmo ha sido: ',tiempo_ej/60, 'minutos')

print(params)

Nota

La celda anterior no se ejecuta al generar la p谩gina web debido a que el tiempo requerido es muy elevado. Los par谩metros obtenidos tr谩s el proceso de optimizaci贸n se indican en la siguiente celda para poder obtener los siguientes gr谩ficos.

params_nesterov = [-1.16792912,  1.6060722 ,  0.0843576 , -0.02140028,  1.58956029,
        0.01566641,  1.48904306,  0.69655598,  0.82610582,  0.50128958,
        0.62812613, -0.04462584,  0.63429607, -0.05795183,  0.83151127,
        0.00333674,  0.84201342, -0.00205158]
# Calcular coste con todos los datos y mirar precisi贸n para criterio de parada.
cost_val = Cost_function(params_nesterov, X_val, Y_val,nqubits)
cost_all = Cost_function(params_nesterov, X_pad, Y,nqubits)
Loss:  0.44559725536638345 Acc:  0.86
Loss:  0.37696163417390055 Acc:  0.872
predictions = [Predictions(x, params_nesterov,nqubits) for x in X_pad]
fig, ax = plt.subplots(1,2,figsize=(12,5))

# Visualizar las predicciones realizadas por el modelo
ax[0].set_title('Predicciones')
for x, y_target in zip(X_pad[:,0:2], predictions):
    if np.sign(y_target) == 1:
        ax[0].plot(x[0], x[1], "bo")
    else:
        ax[0].plot(x[0], x[1], "go")
        
# Visualizar los valores reales del label       
ax[1].set_title('Valores Reales')
for x, y_target in zip(X_pad[:,0:2], y):
    if y_target == 1:
        ax[1].plot(x[0], x[1], "bo")
    else:
        ax[1].plot(x[0], x[1], "go")
../../_images/174a7d0cb4975a6104b76521d17126731c00a9b35b9b3aa16f09f36dfafe7723.png

En este caso la clasificaci贸n cambia, se pueden apreciar errores de clasificaci贸n en los extremos y se mantienen los errores de clasificaci贸n en el centro.

6.4 Adam#

Se trata de un m茅todo de optimizaci贸n estoc谩stica eficiente que unicamente requiere gradientes de primer orden con poco requerimiento de memoria. Este m茅todo calcula tasas de aprendizaje adaptativas individuales para diferentes par谩metros a partir de las estimaciones del primer y segundo momento de los gradientes. Es por eso que el nombre de esta t茅cnica, Adam, proviene de adaptative moment estimation. [23]

6.4.1. Algoritmo#

A continuaci贸n se muestra el pseudo-c贸digo de este algoritmo.

../../_images/AlgAdam.PNG

Fig. 16 Pseudo-c贸digo algoritmo Adam [23].#

Tal y como se muestra en la figura 5, el algoritmo actualiza las medias m贸viles exponenciales del gradiente (\(m_t\)) y del gradiente al cuadrado (\(v_t\)) donde los hiperpar谩metros \(\beta_1, \beta_2 \in [0,1)\) corresponden al par谩metro de suavizado de las medias m贸viles. Como se aprecia en el pseudo-c贸digo anterior, los par谩metros de suavizado se inicializan al 0, dejando que en las primeras iteraciones el valor sea cercano al valor nulo, especialmente cuando los valores de \(\beta\) son cercanos a 1 [23].

6.4.2. Implementaci贸n#

from time import time

start_time = time()


# Definici贸n de algunos par谩metros
nqubits = 2
n_iter = 60

learning_rate = 0.25
momentum = 0.9
batch_size = 5

np.random.seed(0)

# Inicializar los 谩ngulos de rotaci贸n
test_params = np.random.normal(0,1,18)*0.01


print('n煤mero de par谩metros a ajustar:',len(test_params))
print('test_params_INI',test_params)


np.random.seed(123)

params = tf.Variable(test_params)

for it in range(n_iter):  # N煤mero m谩ximo de iteraciones que har谩 el algoritmo.
    print('\nIteraci贸n',it)
    
    # Calcular el gradiente:
    with tf.GradientTape() as tape:
        value_cost= Cost_function(params, X_train, Y_train, nqubits)

    tf_grads = tape.gradient(value_cost, [params])
    
    # Actualizar los par谩metros:
    optimizer = getattr(tf.optimizers, "Adam")(learning_rate=learning_rate, beta_1=0.9, beta_2=0.999,epsilon=1e-08)
    optimizer.apply_gradients(zip(tf_grads, [params]))
        
tiempo_ej = time()-start_time
print('El tiempo necesario para ejecutar el algoritmo ha sido: ',tiempo_ej/60, 'minutos')

print(params)

Nota

La celda anterior no se ejecuta al generar la p谩gina web debido a que el tiempo requerido es muy elevado. Los par谩metros obtenidos tr谩s el proceso de optimizaci贸n se indican en la siguiente celda para poder obtener los siguientes gr谩ficos.

params_adam = [-1.48232873e+00, -2.51308046e+00,  5.62069491e-01,  2.72334117e-01,
        1.86775543e-02,  4.87684609e-01,  5.09494809e-01,  4.27517459e-01,
       -1.02794316e-03,  4.33140725e-01,  5.01443413e-01,  4.41843375e-02,
        5.07613354e-01,  3.08583526e-02,  4.43715065e-03,  3.33674327e-03,
        1.49393091e-02, -2.05158266e-03]
# Calcular coste con todos los datos y mirar precisi贸n para criterio de parada.
cost_val = Cost_function(params_adam, X_val, Y_val,nqubits)
cost_all = Cost_function(params_adam, X_pad, Y,nqubits)
Loss:  0.6259834338146246 Acc:  0.79
Loss:  0.606673952790901 Acc:  0.792
predictions = [Predictions(x, params_adam,nqubits) for x in X_pad]
fig, ax = plt.subplots(1,2,figsize=(12,5))

# Visualizar las predicciones realizadas por el modelo
ax[0].set_title('Predicciones')
for x, y_target in zip(X_pad[:,0:2], predictions):
    if np.sign(y_target) == 1:
        ax[0].plot(x[0], x[1], "bo")
    else:
        ax[0].plot(x[0], x[1], "go")
        
# Visualizar los valores reales del label       
ax[1].set_title('Valores Reales')
for x, y_target in zip(X_pad[:,0:2], y):
    if y_target == 1:
        ax[1].plot(x[0], x[1], "bo")
    else:
        ax[1].plot(x[0], x[1], "go")
../../_images/a2b8182f899209b5c6fb047cc63e4d8e5d174b001faca22d8fdac7abed872fe8.png

Tal y como se aprecia en el gr谩fico anterior, este optimizador no es capaz de conseguir los par谩metros adecuados para una correcta clasificaci贸n. Se puede observar que todas las instancias se etiquetan con la misma clase.

6.5 Adadelta#

Adadelta es una t茅cnica de optimizaci贸n estoc谩stica basada en el descenso por gradiente que permite aplicar el m茅todo de la tasa de aprendizaje por dimensi贸n. Se trata de una expansi贸n del m茅todo Adagrad que busca reducir la agresividad y el descenso mon贸tono de la tasa de aprendizaje. Para ello en vez de hacer uso de todos los gradientes pasados, 煤nicamente utilizan los contenidos en una ventana de tama帽o fijo \(w\) [22].

6.5.1. Algoritmo#

La suma de los gradientes se define de manera recursiva, de manera que \(E[g^2]_t\) en el tiempo t depende de la media anterior y el gradiente actual, tal y como se ind铆ca en la siguiente formula [42]: $\(E[g^2]_t = \gamma E[g^2]_{t-1} + (1-\gamma)g_t^2\)$

Normalmente el par谩metro \(\gamma\) se fija a 0.9. Utilizando este m茅todo de optimizaci贸n los par谩metros se acutalizan mediante las siguientes expresiones [42]: $\(\Delta\theta_t = -\frac{\eta}{\sqrt{E[g^2]_t+\epsilon}}g_t\)$

La principal ventaja del algoritmo Adadelta es que no es necesario definir el valor de la tasa de aprendizaje por defecto [22].

6.5.2. Implementaci贸n#

from time import time

start_time = time()


# Definici贸n de algunos par谩metros
nqubits = 2
n_iter = 60

learning_rate = 0.25
momentum = 0.9
batch_size = 5

np.random.seed(0)

# Inicializar los 谩ngulos de rotaci贸n
test_params = np.random.normal(0,1,18)*0.01


print('n煤mero de par谩metros a ajustar:',len(test_params))
print('test_params_INI',test_params)


np.random.seed(123)

params = tf.Variable(test_params)

for it in range(n_iter):  # N煤mero m谩ximo de iteraciones que har谩 el algoritmo.
    print('\nIteraci贸n',it)  
   
    # Calcular el gradiente:
    with tf.GradientTape() as tape:
        value_cost= Cost_function(params, X_train, Y_train, nqubits)

#         print('LOSS en batch: ', value_cost.numpy(), 'ACC en batch: ', acc_cost)
    tf_grads = tape.gradient(value_cost, [params])
#     print ("tf_grad", tf_grads) 
    
    # Actualizar los par谩metros:
    optimizer = getattr(tf.optimizers, "Adadelta")(learning_rate=learning_rate)
    optimizer.apply_gradients(zip(tf_grads, [params]))
        
tiempo_ej = time()-start_time
print('El tiempo necesario para ejecutar el algoritmo ha sido: ',tiempo_ej/60, 'minutos')

print(params)

Nota

La celda anterior no se ejecuta al generar la p谩gina web debido a que el tiempo requerido es muy elevado. Los par谩metros obtenidos tr谩s el proceso de optimizaci贸n se indican en la siguiente celda para poder obtener los siguientes gr谩ficos.

params_adadelta = [-0.003568  ,  0.00432562,  0.01021908,  0.02240893,  0.0398887 ,
       -0.00748113,  0.03071376, -0.00137568,  0.02018069,  0.00424388,
        0.02265331,  0.01388976,  0.02882325,  0.00056377,  0.02565151,
        0.00333674,  0.03615367, -0.00205158]
# Calcular coste con todos los datos y mirar precisi贸n para criterio de parada.
cost_val = Cost_function(params_adadelta, X_val, Y_val,nqubits)
cost_all = Cost_function(params_adadelta, X_pad, Y,nqubits)
Loss:  1.9023031273128745 Acc:  0.51
Loss:  1.9411366508303498 Acc:  0.5
predictions = [Predictions(x, params_adadelta,nqubits) for x in X_pad]
fig, ax = plt.subplots(1,2,figsize=(12,5))

# Visualizar las predicciones realizadas por el modelo
ax[0].set_title('Predicciones')
for x, y_target in zip(X_pad[:,0:2], predictions):
    if np.sign(y_target) == 1:
        ax[0].plot(x[0], x[1], "bo")
    else:
        ax[0].plot(x[0], x[1], "go")
        
# Visualizar los valores reales del label       
ax[1].set_title('Valores Reales')
for x, y_target in zip(X_pad[:,0:2], y):
    if y_target == 1:
        ax[1].plot(x[0], x[1], "bo")
    else:
        ax[1].plot(x[0], x[1], "go")
../../_images/e0fa8cb58c663764f1712fe2c7fa116fbd701d269494e87ec4d8002aabc085da.png

Ocurre lo mismo que en el caso anterior, el clasificador no hace distinci贸n entre dos clases y no se genera ninguna frontera de decisi贸n.

6.6. M茅todo de optimizaci贸n Powell#

El algoritmo Powell鈥檚 conjugate direction method, tambi茅n conocido como m茅todo de Powell trata de encontrar un m铆nimo local de una funci贸n. Haciendo uso de esta t茅cnica no es necesario que la funci贸n de coste sea diferenciable ya que no se hace uso de derivadas durante el proceso de optimizaci贸n.

Para poder utilizar este m茅todo debe ser una funci贸n de valores reales y con un n煤mero fijo de inputs. El algoritmo necesita un punto inicial y un set de vectores para iniciar el proceso. Este m茅todo minimiza la funci贸n mediante una busqueda bidireccional para cada uno de los vectores.

from time import time
from scipy.optimize import minimize

start_time = time()


# Definici贸n de algunos par谩metros
nqubits = 2

learning_rate = 0.25
momentum = 0.9
batch_size = 5

np.random.seed(0)

# Inicializar los 谩ngulos de rotaci贸n
test_params = np.random.normal(0,1,18)*0.01


print('n煤mero de par谩metros a ajustar:',len(test_params))
print('test_params_INI',test_params)


np.random.seed(123)
result = minimize(Cost_function, test_params,args=(X_train, Y_train,nqubits), method ='Powell', options={'disp':True,'maxiter':60,'maxfev':60})
print(result.x)

tiempo_ej = time()-start_time
print('El tiempo necesario para ejecutar el algoritmo ha sido: ',tiempo_ej/60, 'minutos')

Nota

La celda anterior no se ejecuta al generar la p谩gina web debido a que el tiempo requerido es muy elevado. Los par谩metros obtenidos tr谩s el proceso de optimizaci贸n se indican en la siguiente celda para poder obtener los siguientes gr谩ficos.

params_powell = [5.78141512e+00,  2.94328489e+00,  8.82134090e-04,  4.07261522e-03,
  7.84590743e-01,  1.64362966e+00,  9.50088418e-03, -1.51357208e-03,
 -1.03218852e-03,  4.10598502e-03,  1.44043571e-03,  1.45427351e-02,
  7.61037725e-03,  1.21675016e-03,  4.43863233e-03,  3.33674327e-03,
  1.49407907e-02, -2.05158264e-03]
predictions = [Predictions(x, params_powell,nqubits) for x in X_pad]
fig, ax = plt.subplots(1,2,figsize=(12,5))

# Visualizar las predicciones realizadas por el modelo
ax[0].set_title('Predicciones')
for x, y_target in zip(X_pad[:,0:2], predictions):
    if np.sign(y_target) == 1:
        ax[0].plot(x[0], x[1], "bo")
    else:
        ax[0].plot(x[0], x[1], "go")
        
# Visualizar los valores reales del label       
ax[1].set_title('Valores Reales')
for x, y_target in zip(X_pad[:,0:2], y):
    if y_target == 1:
        ax[1].plot(x[0], x[1], "bo")
    else:
        ax[1].plot(x[0], x[1], "go")
../../_images/0c5516adb39f20fe1dc9330377bf7566373845523fd4639dc37b290fc7b6915b.png

Con el clasificador ajustado mediante el optimizador Powell observamos una frontera de decisi贸n que corresponde a una l铆nea horizontal, por lo que tampoco se logra la clasificaci贸n esperada.

6.7. M茅todo de optimizaci贸n BFGS#

El algoritmo Broyden-Fletcher-Goldfarb-Shanno tambi茅n conocido como BFGS, es un m茅todo iterativo utilizado para resolver problemas no lineales sin restricciones. Al igual que el m茅todo Davidon-Fletcher-Powell, determina la direcci贸n de descenso precondicionando el gradiente con la informaci贸n de la curvatura de la funci贸n. BFGS realiza este proceso gradualmente mejorando la matriz hessiana de la funci贸n de coste. Dicha matriz se obtiene mediante evaluaciones del gradiente (o evaluaciones del gradiente aproximado) y se generaliza mediante el m茅todo de la secante [40].

Nota (M茅todo de la secante)

Se trata de un m茅todo utilizado para aproximar la derivada de una funci贸n, en algunos casos el c谩lculo de la derivada es muy costoso por lo que este tipo de m茅todos resultan de gran utilidad. La expresi贸n utilizada para obtener el valor de la pendiente es la siguiente [14]:

\[ f^\prime(x_0) = \frac{f(x_1)-f(x_0)}{x_1-x_0} \]
from time import time
from scipy.optimize import minimize

start_time = time()


# Definici贸n de algunos par谩metros 
nqubits = 2

learning_rate = 0.25
momentum = 0.9
batch_size = 5

np.random.seed(0)

# Inicializar los 谩ngulos de rotaci贸n
test_params = np.random.normal(0,1,18)*0.01


print('n煤mero de par谩metros a ajustar:',len(test_params))
print('test_params_INI',test_params)


np.random.seed(123)
result = minimize(Cost_function, test_params,args=(X_train, Y_train,nqubits), method ='BFGS', options={'disp':True,'maxiter':30})
print(result.x)

tiempo_ej = time()-start_time
print('El tiempo necesario para ejecutar el algoritmo ha sido: ',tiempo_ej/60, 'minutos')

Nota

La celda anterior no se ejecuta al generar la p谩gina web debido a que el tiempo requerido es muy elevado. Los par谩metros obtenidos tr谩s el proceso de optimizaci贸n se indican en la siguiente celda para poder obtener los siguientes gr谩ficos.

# Cambiar valores de los parametros

params_BFGS = [-1.13857089e+00,  2.37978594e+00, -8.19608946e-05,  7.41954923e-05,
  5.95981338e-01, -7.80111697e-01,  2.72385273e-03, -3.69266125e-02,
  6.48136305e-01, -1.34811159e-02,  1.46589166e-01,  8.38802914e-02,
  1.52760468e-01,  7.05566292e-02,  1.92534534e-01,  3.33697291e-03,
  2.03037478e-01, -2.05223045e-03]
predictions = [Predictions(x, params_BFGS, nqubits) for x in X_pad]
fig, ax = plt.subplots(1,2,figsize=(12,5))

# Visualizar las predicciones realizadas por el modelo
ax[0].set_title('Predicciones')
for x, y_target in zip(X_pad[:,0:2], predictions):
    if np.sign(y_target) == 1:
        ax[0].plot(x[0], x[1], "bo")
    else:
        ax[0].plot(x[0], x[1], "go")
        
# Visualizar los valores reales del label       
ax[1].set_title('Valores Reales')
for x, y_target in zip(X_pad[:,0:2], y):
    if y_target == 1:
        ax[1].plot(x[0], x[1], "bo")
    else:
        ax[1].plot(x[0], x[1], "go")
../../_images/cba1e5f1e059893fd171ecd49af4ac06852d972c4b7db27fce35b637658d740b.png

En este caso se obtiene un resultado similar al obtenido con el optimizador de descenso por gradiente.


Autores:

Carmen Calvo (SCAYLE), Antoni Alou (PIC), Carlos Hernani (UV), Nahia Iriarte (NASERTIC) y Carlos Luque (IAC)

../../_images/LOGO-SCAILE.png ../../_images/Logo_pic.png ../../_images/Logo_UV.jpg ../../_images/Logo_Nasertic.png ../../_images/Logo_IAC.jpg
https://quantumspain-project.es/wp-content/uploads/2022/11/Logo_QS_EspanaDigital.png
Licencia Creative Commons

License: Licencia Creative Commons Atribuci贸n-CompartirIgual 4.0 Internacional.

This work has been financially supported by the Ministry for Digital Transformation and of Civil Service of the Spanish Government through the QUANTUM ENIA project call - Quantum Spain project, and by the European Union through the Recovery, Transformation and Resilience Plan - NextGenerationEU within the framework of the Digital Spain 2026 Agenda.