Devoluciones de llamada de Keras: guarde y visualice la predicción en cada época de entrenamiento

En esta guía, aprenda a guardar la predicción de sus modelos de aprendizaje profundo durante el entrenamiento, en cada época, con una devolución de llamada de Keras personalizada en Python, y visualice o anime las predicciones.

Introducción

Keras es una API de alto nivel, que normalmente se usa con la biblioteca Tensorflow, y ha reducido la barrera de entrada para muchos y ha democratizado la creación de modelos y sistemas de aprendizaje profundo.

Cuando recién comienzan, una API de alto nivel que abstrae la mayor parte del funcionamiento interno ayuda a las personas a familiarizarse con los conceptos básicos y desarrollar una intuición inicial. Sin embargo, en el futuro, los profesionales naturalmente quieren construir una intuición más fuerte de lo que sucede debajo del capó tanto para obtener una visión procesable como para obtener una comprensión más profunda de cómo aprende su modelo.

En muchos casos, es útil echar un vistazo al proceso de aprendizaje de una red neuronal profunda, probar cómo predice valores en cada época de aprendizaje y guardar los valores.

Estos valores guardados se pueden usar para visualizar las predicciones, usando bibliotecas como Matplotlib o Seaborn, o se pueden guardar en un registro para su posterior análisis en sistemas inteligentes, o simplemente analizados por un humano. Por lo general, extraemos las curvas de aprendizaje de un modelo para obtener una mejor comprensión de cómo se desempeña a lo largo del tiempo, pero las curvas de aprendizaje reflejan la pérdida media a lo largo del tiempo, y no puede ver cómo se desempeña el modelo hasta que termine el entrenamiento.

Keras tiene una característica maravillosa: devoluciones de llamada, que son fragmentos de código que se llaman durante el entrenamiento y se pueden usar para personalizar el proceso de entrenamiento. Por lo general, utiliza devoluciones de llamada para guardar el modelo si funciona bien, detener el entrenamiento si se ajusta en exceso o reaccionar o afectar los pasos del proceso de aprendizaje.

Esto hace que las devoluciones de llamada sean la elección natural para ejecutar predicciones en cada lote o época y guardar los resultados. En esta guía, veremos cómo ejecutar una predicción en el conjunto de prueba y visualizar los resultados. , y guardarlos como imágenes, en cada época de entrenamiento en Keras.

{.icon aria-hidden=“true”}

Nota: Construiremos un modelo simple de aprendizaje profundo usando Keras en las secciones siguientes, pero no nos enfocaremos mucho en la implementación o el conjunto de datos. Esto no pretende ser una guía para construir modelos de regresión, pero se necesita un modelo para mostrar correctamente cómo funciona la devolución de llamada.

Si está interesado en leer más sobre cómo construir estos modelos y cómo lograr que sean altamente precisos en lugar de solo exactos, lea nuestra extensa y detallada Predicción práctica del precio de la vivienda - Aprendizaje automático con Python{target="_blank"}!

Construcción y evaluación de un modelo de aprendizaje profundo con Keras {#construcción y evaluación de un modelo de aprendizaje profundo con Keras}

Construyamos un modelo Keras simple con fines ilustrativos. Aceleraremos esta sección con un enfoque y atención mínimos; esta no es una guía sobre la creación de modelos de regresión. Estaremos trabajando con el Conjunto de datos de vivienda de California, obtenido a través de Scikit-Learn' s módulo datasets, que es un conjunto de datos destinado a regresión.

Avancemos e importemos las bibliotecas y los métodos estáticos que usaremos:

1
2
3
4
5
6
7
8
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
import tensorflow as tf
from tensorflow import keras
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split

Now, let's load in the dataset, dividirlo en un conjunto de entrenamiento y prueba (we'll split out a validation set later), and visualize the locations of the houses to check if the data's been loaded correctly:

1
2
3
4
5
6
X, y = fetch_california_housing(as_frame=True, return_X_y=True)
x_train, x_test, y_train, y_test = train_test_split(x, y)

plt.figure(figsize=(12, 8))
sns.scatterplot(data=x, x='Longitude', y='Latitude', size=y, alpha=0.5, hue=y, palette='magma')
plt.show()

california dataset visualization

¡Parece californiana! Dado que los datos se cargan correctamente, podemos definir un modelo de Keras secuencial simple:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
checkpoint = keras.callbacks.ModelCheckpoint("california.h5", save_best_only=True)

model = keras.Sequential([
    keras.layers.Dense(64, activation='relu', kernel_initializer='normal', kernel_regularizer="l2", input_shape=[x_train.shape[1]]),
    keras.layers.Dropout(0.2),
    keras.layers.BatchNormalization(),
    
    keras.layers.Dense(64, activation='relu', kernel_initializer='normal', kernel_regularizer="l2"),
    keras.layers.Dropout(0.2),
    keras.layers.BatchNormalization(),
  
    keras.layers.Dense(1)
])

model.compile(loss='mae',
              optimizer=keras.optimizers.RMSprop(learning_rate=1e-2, decay=0.1),
              metrics=['mae'])
              
history = model.fit(
    x_train, y_train,
    epochs=150,
    batch_size=64,
    validation_split=0.2,
    callbacks=[checkpoint]
)

Aquí, tenemos un MLP simple, con un poco de abandono y normalización por lotes para combatir el sobreajuste, optimizado con el optimizador RMSprop y una pérdida de Mean Absolute Error. Hemos ajustado el modelo para 150 épocas, con una división de validación de 0.2 y una devolución de llamada ModelCheckpoint para guardar los pesos en un archivo. Ejecutar esto da como resultado:

1
2
3
...
Epoch 150/150
387/387 [==============================] - 3s 7ms/step - loss: 0.6279 - mae: 0.5976 - val_loss: 0.6346 - val_mae: 0.6042

Podríamos visualizar las curvas de aprendizaje para obtener una idea básica de cómo fue el entrenamiento, pero no nos cuenta toda la historia; estos son solo medios agregados sobre los conjuntos de entrenamiento y validación durante el entrenamiento:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
model_history = pd.DataFrame(history.history)
model_history['epoch'] = history.epoch

fig, ax = plt.subplots(1, figsize=(8,6))
num_epochs = model_history.shape[0]

ax.plot(np.arange(0, num_epochs), model_history["mae"], 
        label="Training MAE")
ax.plot(np.arange(0, num_epochs), model_history["val_mae"], 
        label="Validation MAE")
ax.legend()

plt.tight_layout()
plt.show()

Esto resulta en:

deep learning model learning curves

Y podemos evaluar nuestro modelo con:

1
model.evaluate(x_test, y_test)
1
162/162 [==============================] - 0s 2ms/step - loss: 0.5695 - mae: 0.5451 - mape: 32.2959

Como la variable objetivo se mide en múltiplos de $100.000, lo que significa que nuestra red pierde el precio hasta aproximadamente $54.000, que es un Error porcentual absoluto medio de ~32%. La mayoría de los métodos tradicionales de aprendizaje automático, como Random Forest Regression, incluso después de un preprocesamiento de datos más extenso para este conjunto de datos, logran alrededor de $52.000, con hiperparámetros ajustados, por lo que este es un resultado bastante decente, aunque podría mejorarse con más preprocesamiento, mejor ajuste y diferentes arquitecturas.

El punto aquí no era construir un modelo particularmente preciso, pero sí elegimos un conjunto de datos con el cual el modelo no convergería muy rápidamente, para que podamos observar su danza alrededor de las variables de destino.

Una forma más ilustrativa de evaluar cómo funciona el modelo elimina el Error absoluto medio y el Error porcentual absoluto medio por completo, y podemos trazar un [gráfico de dispersión](/tutorial-y-ejemplos-de- diagramas-de-dispersion-de-matplotlib/) de los precios previstos contra los precios reales. Si son iguales, los marcadores trazados seguirán una trayectoria recta en diagonal. Para referencia y alcance, también podemos trazar una línea diagonal y evaluar qué tan cerca está cada marcador de la línea:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
test_predictions = model.predict(x_test)
test_labels = y_test

fig, ax = plt.subplots(figsize=(8,4))
plt.scatter(test_labels, test_predictions, alpha=0.6, 
            color='#FF0000', lw=1, ec='black')
lims = [0, 5]

plt.plot(lims, lims, lw=1, color='#0000FF')
plt.ticklabel_format(useOffset=False, style='plain')
plt.xticks(fontsize=18)
plt.yticks(fontsize=18)
plt.xlim(lims)
plt.ylim(lims)

plt.tight_layout()
plt.show()

Ejecutar este código da como resultado:

deep learning regression performance evaluation

La red sobrevalora las casas más baratas y subestima las más caras, y las estimaciones tienen un alcance bastante generoso (con algunas predicciones a la derecha totalmente fuera del alcance). Sin embargo, esto sucede porque no hemos limpiado el conjunto de datos y muchos precios de la vivienda * están* limitados a ese valor cuando se importan).

Esta no es la información que obtienes de las curvas de aprendizaje, y una red que tuvo el efecto opuesto: infravalorar las casas más baratas y sobrevalorar las caras podría tener el mismo MAE y MAPE pero comportarse de manera totalmente diferente.

Lo que también nos interesa es cómo el modelo llegó aquí y cómo cambiaron estas predicciones a lo largo del tiempo y el proceso de aprendizaje. Este es solo el punto final del proceso de capacitación, y hubo bastante capacitación involucrada para llegar aquí.

Avancemos y escribamos una devolución de llamada personalizada para agregar a la lista de devoluciones de llamada en el proceso de capacitación, que ejecutará una predicción en el conjunto de prueba en cada época, visualizará las predicciones y las guardará como una imagen.

Devolución de llamada Keras de predicción personalizada con gráficos

Al igual que hemos usado la devolución de llamada ModelCheckpoint para verificar si un modelo está en su mejor estado de rendimiento en cada época, y lo guardamos en un archivo .h5 y lo conservamos; podemos escribir una devolución de llamada personalizada que ejecutará predicciones, las visualizará y guardará las imágenes en nuestro disco.

La creación de una devolución de llamada personalizada se reduce a extender la clase Callback y anular cualquiera de los métodos que proporciona; los que no anula, conservan su comportamiento predeterminado:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class PerformancePlotCallback(keras.callbacks.Callback):
       
    def on_train_end(self, logs=None):
      ...
    def on_epoch_begin(self, epoch, logs=None):
      ...
    def on_epoch_end(self, epoch, logs=None):
      ...
    def on_test_begin(self, logs=None):
      ...
    def on_test_end(self, logs=None):
      ...
    # Etc.

Dependiendo de cuándo le gustaría predecir usando su modelo de entrenamiento, elegirá el método apropiado. Una buena medida de cómo está progresando es una época, por lo que al final de cada época de entrenamiento, probaremos el modelo en nuestro conjunto de pruebas.

Necesitamos una forma de proporcionar el conjunto de prueba a la devolución de llamada, ya que se trata de datos externos. La forma más fácil de hacerlo es definir un constructor que acepte el conjunto de prueba y evalúe el modelo actual en él, brindándole un resultado consistente:

1
2
3
4
5
6
7
8
class PerformancePlotCallback(keras.callbacks.Callback):
    def __init__(self, x_test, y_test):
        self.x_test = x_test
        self.y_test = y_test
        
    def on_epoch_end(self, epoch, logs=None):
        print('Evaluating Model...')
        print('Model Evaluation: ', self.model.evaluate(self.x_test))   

Esta devolución de llamada simple acepta el conjunto de prueba de casas y variables de destino relevantes y se evalúa a sí mismo en cada época, imprimiendo el resultado en la consola, junto con la salida habitual de Keras.

Si tuviéramos que crear una instancia y agregar esta devolución de llamada al modelo, y “ajustarlo ()” nuevamente, veríamos un resultado diferente al anterior:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
performance_simple = PerformancePlotCallback(x_test, y_test)

# Model definition and compilation...

history = model.fit(
    x_train, y_train,
    epochs=150,
    validation_split=0.2,
    callbacks=[performance_simple]
)

Esto resulta en:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Epoch 1/150
387/387 [==============================] - 3s 7ms/step - loss: 1.0785 - mae: 1.0140 - val_loss: 0.9455 - val_mae: 0.8927
Evaluating Model...
162/162 [==============================] - 0s 1ms/step - loss: 0.0528 - mae: 0.0000e+00
Model Evaluation:  [0.05277165770530701, 0.0]
Epoch 2/150
387/387 [==============================] - 3s 7ms/step - loss: 0.9048 - mae: 0.8553 - val_loss: 0.8547 - val_mae: 0.8077
Evaluating Model...
162/162 [==============================] - 0s 1ms/step - loss: 0.0471 - mae: 0.0000e+00
Model Evaluation:  [0.04705655574798584, 0.0]
...

¡Impresionante! El modelo se evalúa a sí mismo en cada época, en los datos que hemos pasado a la devolución de llamada. Ahora, modifiquemos la devolución de llamada para que visualice las predicciones en lugar de imprimirlas en la salida ya abarrotada.

Para simplificar las cosas, obtendremos la devolución de llamada para guardar las imágenes en una carpeta, de modo que podamos unirlas en un video o un Gif más adelante. También incluiremos un model_name en el constructor para ayudarnos a diferenciar los modelos al generar las imágenes y sus nombres de archivo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class PerformancePlotCallback(keras.callbacks.Callback):
    def __init__(self, x_test, y_test, model_name):
        self.x_test = x_test
        self.y_test = y_test
        self.model_name = model_name
        
    def on_epoch_end(self, epoch, logs={}):
        y_pred = self.model.predict(self.x_test)
        fig, ax = plt.subplots(figsize=(8,4))
        plt.scatter(y_test, y_pred, alpha=0.6, 
            color='#FF0000', lw=1, ec='black')
        
        lims = [0, 5]

        plt.plot(lims, lims, lw=1, color='#0000FF')
        plt.ticklabel_format(useOffset=False, style='plain')
        plt.xticks(fontsize=18)
        plt.yticks(fontsize=18)
        plt.xlim(lims)
        plt.ylim(lims)

        plt.tight_layout()
        plt.title(f'Prediction Visualization Keras Callback - Epoch: {epoch}')
        plt.savefig('model_train_images/'+self.model_name+"_"+str(epoch))
        plt.close()

Aquí, creamos una figura de Matplotlib en cada época y trazamos un gráfico de dispersión de los precios pronosticados contra los precios reales. Además, hemos agregado una línea de referencia diagonal: cuanto más cerca estén nuestros marcadores de diagrama de dispersión de la línea diagonal, más precisas serán las predicciones de nuestro modelo.

Luego, la trama se guarda a través de plt.savefig() con el nombre del modelo y el número de época, junto con un título informativo que le permite saber en qué época se encuentra el modelo durante el entrenamiento.

Ahora, usemos esta devolución de llamada personalizada nuevamente, proporcionando un nombre de modelo además de los conjuntos x_test e y_test:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
checkpoint = keras.callbacks.ModelCheckpoint("california.h5", save_best_only=True)
performance = PerformancePlotCallback(x_test, y_test, "california_model")

# Model definition and compilation...
              
history = model.fit(
    x_train, y_train,
    epochs=150,
    validation_split=0.2,
    callbacks=[checkpoint, performance]
)

El PerformancePlotCallback entra en pleno funcionamiento y en la carpeta designada genera una imagen del rendimiento en cada época. La carpeta model_train_images ahora está llena con 150 gráficos:

saving predictions on each epoch keras

Ahora puede usar su herramienta favorita para unir las imágenes en un video o un archivo Gif, o simplemente examinarlas manualmente. Aquí hay un Gif del modelo que hemos construido entrenando en estos datos:

animated learning process on each epoch with keras

Conclusión

En esta guía, hemos creado un modelo simple para predecir el precio de una casa en el conjunto de datos de vivienda de California con una precisión aceptable. Luego, analizamos cómo escribir una devolución de llamada de Keras personalizada para probar el rendimiento de un modelo de aprendizaje profundo y visualizarlo durante el entrenamiento, en cada época.

Procedimos a guardar estas imágenes en el disco y creamos un Gif a partir de ellas, dándonos una perspectiva diferente del proceso de entrenamiento que la que obtenemos al analizar las curvas de aprendizaje de un modelo.