None basic_ML_MPP

Ejemplo mínimo de aprendizaje automático

Métodos potenciales de prospección, FCAG, 2023.

En esta breve experiencia, empleamos TensorFlow. Para instalar el software en W*ndows con Miniconda instalado, escribir en la terminal de Miniconda:

conda> pip install --upgrade pip
conda> pip install tensorflow

A continuación, dado un conjunto $\{x_i,y_i\}$ de puntos de estación $x_i$ y observaciones $y_i$, más un modelo directo $f(x;a,b)$, intentaremos invertir el valor de los parámetros $a$ y $b$ que mejor ajustan las observaciones. La estructura del dato es similar a lo que veremos en las prácticas para un modelo directo de anomalía de gravedad.

$$\{x_i,\color{green}{y_i}\}, \quad f(x;{\color{blue}a},{\color{blue}b}) = e^{-{\color{blue}a}x^2} + \color{blue} b, \quad \color{green}{y_i}\approx f(x_i;\color{blue}a,\color{blue}b) = {\hat{\color{red}{y_i}}}.$$

In [1]:
import tensorflow as tf
import matplotlib.pyplot as plt
# Estilo para  las notebooks preliminares:
plt.xkcd();plt.rcParams.update({'font.size': 16}) # tamaño de fuente
In [2]:
# El modelo y las observaciones

# A = 3.0
# B = 2.0

A0 = tf.constant(3.0)
B0 = tf.constant(2.0)


N = 100 # número de observaciones

# posiciones de las observaciones:
x     = tf.random.normal(shape=[N])

# Ruido:
noise = 0.01*tf.random.normal(shape=[N])

# Calculamos y
#y = x * A + B + noise
#y = noise + tf.math.exp(-tf.math.pow(x,2) * A0) + B0 
y = tf.math.exp(-tf.math.pow(x,2) * A0) + B0 
In [3]:
# Visualizamos:
plt.figure(figsize=(7,5))
plt.scatter(x,y,label="dato $y_i$",color="royalblue",alpha=0.8)
plt.xlabel("x")
plt.ylabel("y")
plt.ylim([0.5,3.5])
plt.legend()
plt.show()

Modelo

Para realizar la inversión, haremos uso de tensorflow.fit(). Para ello escribimos el modelo directo como un objeto de la clase tensorflow.keras.Model, donde alcanza con detallar dos procesos. El proceso __init__() (con el cual inicializamos los parámetros del modelo directo) y el proceso call(), donde aplicamos el modelo al dato de entrada x.

In [4]:
class MyModel(tf.keras.Model):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        # Inicialización de los parámetros.
        # En la práctica real, deberían ser inicializados aleatoriamente y de manera adecuada.
        self.a = tf.Variable(initial_value = 0.0)
        self.b = tf.Variable(initial_value = 0.0)
        

    def call(self, x):
        #return self.a * x + self.b
        return  tf.math.exp(-tf.math.pow(x,2) * self.a) + self.b
In [5]:
model = MyModel() # creamos una instancia del modelo
In [6]:
# Visualizamos:
plt.figure(figsize=(7,5))
plt.scatter(x, y, label="$y_i$", color="royalblue",alpha=0.8)
plt.scatter(x, model(x), label="$\hat y_i$ modelo inicial", color="tomato")
plt.xlabel("x")
plt.ylabel("y")
plt.ylim([0.5,3.5])
plt.legend()
plt.show()

Como observamos, el modelo inicial no resulta en un buen ajuste al dato. De manera cuantitativa, el error cuadrático medio (RMS) entre los datos y el resultado del modelo es:

In [7]:
def RMS(y,y0):
    rms_2_per_sample = tf.square(y-y0)  # RMS^2, por cada muestra individual de entrada
    rms_2_sample_mean = tf.reduce_mean(rms_2_per_sample)  # RMS^2, promediado en los datos de entrada 
    return tf.sqrt(rms_2_sample_mean).numpy() # RMS
                     
print("RMS = ", RMS(model(x),y))                     
RMS =  1.470352

Entrenamiento

Definimos una función de costo (el cuadrado de la norma L2) y nos decidimos por un algoritmo optimizador.

  • El optimizador es RMSprop.
  • Utilizaremos un tamaño de paso o parámetro de aprendizaje (learning rate) fijo.
  • Estimamos el valor del gradiente tomando el dato de entrada en grupos de batch_size muestras (batch size).
  • Haremos un número de epochs pasadas (epochs) sobre el dato original completo.

Otros optimizadores a probar son descenso de gradiente estocástico (SGD) y Adam (adaptive moment estimation). El algoritmo Adam necesitará un número menor de iteraciones respecto a SGD para llegar a un resultado aceptable.

In [8]:
epochs        = 500 
batch_size    = len(x)//10
learning_rate = 0.01

print("Épocas:                   ",epochs)
print("Tamaño de batch:          ", batch_size)
print("Parámetro de aprendizaje: ", learning_rate)
Épocas:                    500
Tamaño de batch:           10
Parámetro de aprendizaje:  0.01
In [9]:
# Función de costo:
@tf.function
def Myloss(target_y, predicted_y):
    per_sample_loss = tf.square(target_y - predicted_y) # función de costo por cada par (y*_i,y_i)
    return tf.reduce_mean(per_sample_loss) # función de costo promediada
In [10]:
# Optimizador:
#optimizer = tf.keras.optimizers.SGD(learning_rate = learning_rate)
#optimizer = tf.keras.optimizers.Adam(learning_rate = learning_rate)
optimizer = tf.keras.optimizers.RMSprop(learning_rate = learning_rate)
In [11]:
model = MyModel() # creamos una instancia del modelo
# `compile` define los parámetros de entrenamiento:
model.compile(optimizer  = optimizer, loss =  Myloss)
In [12]:
# Resumen del número de parámetros a invertir (`trainable parameters`):
model.build([len(x)]) # "build" para avisar la dimensión de la entrada así vemos el summary y count_parameters
model.summary()
print("Número de parámetros del modelo: ", model.count_params())
Model: "my_model_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
Total params: 2
Trainable params: 2
Non-trainable params: 0
_________________________________________________________________
Número de parámetros del modelo:  2
In [13]:
# Entrenamiento
history = model.fit(x, y, epochs= epochs, batch_size= batch_size, verbose= 0) # para ver detalles al entrenar: verbose = 1
In [14]:
# Visualizamos
plt.figure(figsize=(7,5))
plt.scatter(x,y,label="$y_i$",color="royalblue",alpha=1)
plt.scatter(x,model(x),label="$\hat y_i$ modelo entrenado",color="orange",alpha=0.65)
plt.ylim([0.5,3.5])
plt.xlabel("x")
plt.ylabel("y")
plt.legend()
plt.show()

Apreciamos un mejor ajuste a los datos observados que el obtenido para el modelo inicial, con un RMS dado por:

In [15]:
print("RMS = ", RMS(model(x),y))    
RMS =  0.005651634

Los parámetros invertidos son:

In [16]:
a,b = model.weights

print("a* = ", a.numpy() )
print("b* = ", b.numpy() )
a* =  2.9938445
b* =  2.0053298

Por último, observamos la función de costo (normalizada en la primer época) en función del número de épocas.

In [17]:
# Visualizamos el costo (normalizado) a lo largo de las iteraciones:
perdida  = history.history["loss"]
perdida  = tf.math.divide(perdida,perdida[0])
plt.figure()
plt.plot(perdida,color="limegreen",lw=4)
#plt.plot(range(0,epochs,10),perdida[::10],"-o",color="limegreen",lw=0.5,ms=5)
plt.xlabel("# Épocas")
plt.ylabel("Costo (normalizado) []")
plt.show()

print("Costo actual: %1.6f" % Myloss(model(x), y).numpy())
Costo actual: 0.000032

Con estos conceptos y herramientas, en otra actividad práctica probaremos invertir datos de anomalía de gravedad para estimar parámetros físicos y geométricos de un modelo directo no lineal.

Estimación de la ambiguedad

Corremos varias veces la inversión partiendo con distintos valores iniciales para los parámetros del modelo.

In [18]:
class MyModel(tf.keras.Model):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        # Inicialización de los parámetros.
        # Aquí hacemos un poquito de trampa para no tener que invertir tantas épocas: generamos una
        # distribución Gaussiana con valor medio en los valores reales de los parámetros a invertir
        self.a = tf.Variable(tf.random.normal(shape=[1], mean = A0, stddev = 1.0, dtype=tf.dtypes.float32))
        self.b = tf.Variable(tf.random.normal(shape=[1], mean = B0, stddev = 1.0, dtype=tf.dtypes.float32))
        

    def call(self, x):
        #return self.a * x + self.b
        return  tf.math.exp(-tf.math.pow(x,2) * self.a) + self.b
In [19]:
niter   = 30
epochs  = 100 # utilizamos un número pequeño de épocas para obtener resultados rápidos en esta demonstración.
results_list = []
for i in range(niter):
    print(".",end =" ")
    model = MyModel();
    model.compile(optimizer = optimizer, loss =  Myloss);
    model.fit(x,y, epochs = epochs, batch_size = batch_size, verbose = 0) # verbose = 1 para ver detalles por época
    results_list.append([model.a.numpy(), model.b.numpy()])

results = tf.stack(results_list)
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 
In [20]:
# Valor medio y desviación estándar de cada parámetro:

As = results[:,0,:]
Bs = results[:,1,:]
Ainv_mean = tf.math.reduce_mean(As).numpy()
Ainv_std  = tf.math.reduce_std(As).numpy()
Binv_mean = tf.reduce_mean(Bs).numpy()
Binv_std  = tf.math.reduce_std(Bs).numpy()

# Gráficos

plt.figure()
plt.plot(As,":o",label="a*")
plt.plot(Bs,"-o",label="b*")
plt.xlabel("# n")
plt.ylabel("Parámetros invertidos")
plt.legend()
#plt.ylim([-tf.minimum(tf.abs(A),tf.abs(B)),+2*tf.maximum(tf.abs(A),tf.abs(B))])
plt.show()

# Valor medio y desviación estándar de cada parámetro:
print("a = {0}; <a*> = {1} +/- {2}".format(A0,Ainv_mean, Ainv_std))
print("b = {0}; <b*> = {1} +/- {2}".format(B0,Binv_mean, Binv_std))
a = 3.0; <a*> = 2.999877452850342 +/- 0.010331244207918644
b = 2.0; <b*> = 2.000687599182129 +/- 0.004918837919831276

Eso es todo por hoy.

Referencias

In [ ]: