sábado, 27 de agosto de 2022

Callbacks y TensorBoard (Redes neuronales)

Como vimos en la anterior entradapara entrenamientos muy largos podemos salvar el modelo en mitad del entrenamiento utilizando callbacks

El método fit() acepta el argumento callbacks que nos permite especificar la lista de objetos que Keras llamará al comienzo y al final de su entrenamiento y al comienzo y final de cada época e incluso antes y después de procesar cada lote. Por ejemplo ModelCheckpoint salva los puntos de chequeo (checkpoints) de nuestro modelo a intervalos regulares durante el entrenamiento, por defecto al principio y al final de cada época.

Callbacks y TensorBoard (Redes neuronales)


En la entrada anterior donde entrenábamos el modelo podemos hacer 

checkpoint_cb = keras.callbacks.ModelCheckpoint("mi_modelo_keras.h5", save_best_only=True)

history = model.fit(train_data, train_labels, epochs=30,

validation_data=(test_data , test_labels), callbacks=[checkpoint_cb])

Si utilizamos un set de validación durante el entrenamiento, podemos definir el parámetro save_best_only=True en la definición del ModelCheckpoint. Si definimos este parámetro sólo salvará el modelo en su mejor ejecución, de modo que no debemos preocuparnos de entrenamientos demasiado largos o de sobrecargar el set de entrenamiento: simplemente restauraremos el último modelo salvado después del entrenamiento y este modelo será el mejor modelo del set de entrenamiento.

#restauramos la mejor ejecución del modelo

model = keras.models.load_model("mi_modelo_keras.h5")

Este sistema se conoce como early stopping. Consiste en parar el entrenamiento tan pronto como el error de validación alcanza un mínimo.

Otra forma de implementar early stopping es utilizar la llamada EarlyStopping. Esto interrumpirá el entrenamiento cuando no detecte progreso en el set de validación durante un número de épocas (llamado el argumento de la paciencia) y que opcionalmente nos devuelve al mejor modelo. Podemos combinar ambas técnicas para salvar los checkpoints del modelo en caso de que el ordenador se  cuelgue o se apague inesperadamente y al mismo tiempo parar el entrenamiento cuando no detecte más progreso, para evitar pérdidas de tiempo y de recursos.

#early stopping

early_stopping_cb = keras.callbacks.EarlyStopping(patience=10,

                                                  restore_best_weights=True)

history = model.fit(train_data, train_labels, epochs=100,

                    validation_data=(test_data , test_labels),

                    callbacks=[checkpoint_cb, early_stopping_cb])

En este caso, el número de épocas puede ser grande, pues el entrenamiento se detendrá automáticamente cuando no haya más progreso. Además no es necesario restaurar el mejor modelo salvado  por que EarlyStopping mantendrá los mejores pesos y los restaurará automáticamente al final del entrenamiento.

Hay más callbacks disponibles en keras.callbacks.

Si necesitamos control extra, podemos fácilmente definir nuestros propios callbacks. Por ejemplo si queremos definir un callback que muestre la relación entre la validación y la pérdida durante el entrenamiento (por ejemplo para detectar el sobreentrenamiento)

#customizar un callback

class PrintValTrainRatioCallback(keras.callbacks.Callback):

    def on_epoch_end(self, epoch, logs):

        print("\nval/train: {:.2f}".format(logs["val_loss"] / logs["loss"]))

 

val_train_ratio_cb = PrintValTrainRatioCallback()

history = model.fit(train_data, train_labels, epochs=1,

                    validation_data=(test_data , test_labels),

                    callbacks=[val_train_ratio_cb])

Además de on_epoch_end podemos utilizar otros callbacks como on_epoch_begin, on_batch_begin, on_batch_end, pueden utilizarse durante la evaluación y predicciones por ejemplo como depuración. 

Tensorboard para visualización

Tensorboard es una herramienta de visualización interactiva que nos permite ver las curvas de aprendizaje entre diferentes ejecuciones, analiza estadísticas de entrenamiento y visualiza datos complejos multidimensionales  y en 3D. Esta herramienta se instala automáticamente con la instalación de Tensorflow. 

Para utilizarlo debemos modificar las salidas del programa que deseamos visualizar. Debemos adaptarlas a un tipo de archivo de log binario llamado event files (ficheros de eventos). Cada fichero binario es llamado summary (resumen). El servidor TensorBoard monitorizará el directorio de log y automáticamente tomará los cambios y los visualizará. Esto nos permitirá visualizar los datos en tiempo real (con un pequeño retraso), tales como las curvas de aprendizaje. En general debemos apuntar el servidor de TensorBoard a un directorio raíz de log y configurar el programa, de modo que se escribirá en un subdirectorio diferente cada vez que los ejecutemos. Esto nos permitirá visualizar y comparar datos de múltiples ejecuciones de nuestro programa , sin tenerlo todo mezclado.

Comenzaremos definiendo el directorio de log que utilizaremos para TensorBoard. Además crearemos una pequeña función que generará un subdirectorio basado en la fecha y hora actuales en cada diferente ejecución. Además debemos incluir información extra en el subdirectorio, tal como puede ser el nombre del directorio o información sobre diferentes hiperparámetros, de modo que después nos resulte sencillo encontrar la información en su correspondiente directorio.

import os

root_logdir = os.path.join(os.curdir, "mis_logs")

def get_run_logdir():

    import time

    run_id = time.strftime("run_%Y_%m_%d-%H_%M_%S")

    return os.path.join(root_logdir, run_id)

 

run_logdir = get_run_logdir()

run_logdir

checkpoint_cb = keras.callbacks.ModelCheckpoint("mi_modelo_keras.h5", save_best_only=True)

tensorboard_cb = keras.callbacks.TensorBoard(run_logdir)

history = model.fit(train_data, train_labels, epochs=30,

                    validation_data=(test_data, test_labels),

                    callbacks=[tensorboard_cb])

Y esto es todo, una vez ejecutado este código el callback TensorBoard(() se ha encargado de crear una estructura de directorios y durante el entrenamiento crea archivos de eventos y escribe resúmenes sobre ellos, después de correr el programa una segunda vez (quizás después de cambiar algún valor de un hiperparámetro) encontraremos una estructura de directorios similar a esta:

Mis_logs

-run_2021_06_27-17_14_44

-train

-plugins/profile/2021_06_27_15_14_46

-validation


Hay un directorio por cada ejecución, cada uno contiene un subdirectorio para los logs de entrenamiento y uno para los logs de validación.  Ambos contienen archivos de evento. Esto permite a TensorBoard mostrarnos exactamente el tiempo que el modelo ha consumido en cada parte del modelo  en cada dispositivo (si hay varios dispositivos) lo cual es muy útil por ejemplo para detectar posibles cuellos de botella.

Servidor TensorBoard

El siguiente paso es activar el servidor de TensoBoard, se puede hacer por comando en un terminal. Si hemos instalado TensorFlow dentro de un entorno virtual deberíamos tenerlo ya activado

Por comandos sería algo así

 

Servidor TensorBoard

Una vez activado y después de entrenar nuestro modelo, basta con abrir un navegador y teclear http://localhost:6006

Con esto obtendremos una pantalla similar a esta

 

Servidor TensorBoard

Vemos el interfaz web de TensorBoard. Tenemos dos pestañas, Scalars y Graphs. Abajo a la izquierda podemos seleccionar los logs que queremos visualizar. Tambien podemos elegir el gráfico y otras opciones del mismo.


sábado, 20 de agosto de 2022

Crear una Red Neuronal con la API Sequential de Keras

En este blog ya he comentado la teoría de las redes neuronales.  Así que aquí vamos a hacer un ejercicio práctico. Vamos a construir un clasificador con cualquier dataset  creado por nosotros mismos o descargado de internet. Aquí utilizaremos un dataset para calcular quinielas (1,X,2) con los resultados de la liga española, descargado de internet pero adaptado. Esta entrada es igual que otra que puse con el conjunto MNIST de imágenes, la única diferencia es que aquí utilizamos un  dataset adaptado o creado, por nosotros mismos.

Vamos a utilizar Scikit-Learn, TensorFlow aquí se puede ver cómo instalar TensorFlow si no lo tenemos instalado. keras y numpy

# Python ≥3.5

import sys

assert sys.version_info >= (3, 5)

 

# Scikit-Learn ≥0.20

import sklearn

assert sklearn.__version__ >= "0.20"

 

try:

    # %tensorflow_version solo existe en Colab.

    %tensorflow_version 2.x

except Exception:

    pass

 

# TensorFlow ≥2.0

import tensorflow as tf

assert tf.__version__ >= "2.0"

 

# importaciones comunes

import numpy as np

import os

 

# para hacer este cuaderno estable

np.random.seed(42)

import tensorflow as tf

from tensorflow import keras

 

Para comprobar si está todo correctamente instalado, preguntamos por la versión

 

tf.__version__

keras.__version__

Cargamos el dataset

import pandas as pd

quinielas = pd.read_csv('Completo_Etiquetado_Puntuado_3.csv') #tiene goles quiniela y quinigol

quinielas.head()

Separamos los datos de las etiquetas en nuestro dataset, en la X dejamos los datos y en las y las etiquetas a predecir.

#las X contienen los datos relevantes para hacer predicciones quitamos todas las etiquetas

X = quinielas.drop(columns=['idPartido','temporada','Q1','QX','Q2','QGC0','QGC1','QGC2','QGCM','QGF0','QGF1','QGF2','QGFM','timestamp','golesLocal','golesVisitante','fecha'])

#las Y son las etiquetas a predecir, en este caso quitamos todo excepto 'Q1','QX','Q2'

y = quinielas.drop(columns=['division','jornada','idPartido','temporada','PuntosLocal','PuntosVisitante','EquipoLocal','Puntos_Normalizados','EquipoVisitante','QGC0','QGC1','QGC2','QGCM','QGF0','QGF1','QGF2','QGFM','timestamp','golesLocal','golesVisitante','fecha','timestamp'])

 

Dividimos nuestro dataset en una parte de entrenamiento y otra de testeo, como tenemos unas 37.000 filas, utilizaremos las 29.718 primeras filas como entrenamiento y el resto para testeo.

 

x_data = np.array(X)

y_data = np.array(y)

 

train_data = x_data[:29718]

test_data = x_data[29718:]

train_labels = y_data [:29718]

test_labels = y_data [29718:]

 

Crearemos una red neuronal secuencial, que es el tipo más sencillo de red neuronal, compuesto de una pila de diferentes capas conectadas secuencialmente. Utilizaremos capas densas con activación ReLU. Cada capa densa gestiona su propia matriz de pesos y contiene también todos los pesos de las conexiones entre las neuronas y sus entradas, también gestiona el vector de sesgos (bias) uno por neurona. Para la capa de salida utilizaremos la función de activación softmax. aquí tenemos documentación sobre todos los tipos de activación de que dispone el paquete keras.

Creamos una red neuronal sencilla con 14 neuronas de entrada (una por cada parámetro de entrada, 100 ocultas y 3 de salida (una por cada tipo diferente de salida 1, X, 2 para hacer quinielas)

 

model = keras.models.Sequential()

model.add(keras.layers.Dense(15, activation="relu"))

model.add(keras.layers.Dense(100, activation="relu"))

model.add(keras.layers.Dense(3, activation="softmax"))

Compilar el modelo

Después de crear nuestro modelo, tenemos que compilarlo, para ello llamamos al método compile() para especificar la función de pérdida y optimizador a utilizar.

#compilamos el modelo

model.compile(loss="categorical_crossentropy",

              optimizer="sgd",

              metrics=["accuracy"])

Utilizamos categorical_crossentropy (si utilizamos sparse_categorical_crossentropy  da error) como función de pérdida para tener etiquetas  (para cada instancia hay exactamente una clase, 1,X, 2 en este caso) y las clases son exclusivas, en vez de mostrar las probabilidades de cada clase nos dará una salida del tipo [0,1,0] para la X por ejemplo. Si estuviéramos utilizando una clasificación binaria, de una sola salida, podríamos utilizar una función de activación en la salida sigmoid en lugar de softmax.

_______________________________________________________________

Nota: Diferencia entre categorical_crossentropy y sparse_categorical_crossentropy

CategoricalCrossentropy

 

CategoricalCrossentropy class

tf.keras.losses.CategoricalCrossentropy(

    from_logits=False,

    label_smoothing=0,

    reduction="auto",

    name="categorical_crossentropy",

)

Calcula la pérdida de entropía cruzada entre las etiquetas y las predicciones.

Utilizaremos esta cuando haya dos o más clases de etiquetas. Esperamos que las etiquetas proporcionen en una representación one_hot. Si deseamos proporcionar etiquetas como números enteros, utilizaremos la pérdida SparseCategoricalCrossentropy. Debe haber # clases de valores de punto flotante por característica.

SparseCategoricalCrossentropy

 

tf.keras.losses.SparseCategoricalCrossentropy(

    from_logits=False, reduction="auto", name="sparse_categorical_crossentropy"

)

Calcula la pérdida de entropía cruzada entre las etiquetas y las predicciones.

Utilizaremos esta función de pérdida de entropía cruzada cuando haya dos o más clases de etiquetas. Esperamos que las etiquetas se proporcionen como números enteros. Si deseamos proporcionar etiquetas utilizando una representación en caliente, utilizaremos SparseCategoricalCrossentropy. Debe haber # clases de valores de punto flotante por característica para y_pred y un único valor de punto flotante por característica para y_true.

Ambas tienen la misma función de pérdida. La única diferencia es el formato de etiquetas verdaderas. (En contraposición con las etiquetas calculadas)

Si nuestras etiquetas tienen codificación one-hot, utilizaremos categorical_crossentropy. Ejemplos (para una clasificación de 3 clases): [1,0,0], [0,1,0], [0,0,1]

Pero nuestras etiquetas son enteros, utilizaremos sparse_categorical_crossentropy. Ejemplos (para una clasificación de 3 clases):  [1], [2], [3]

El uso depende completamente de cómo carguemos nuestro conjunto de datos. Una ventaja de usar sparse_categorical_crossentropy es que ahorra tiempo en la memoria y en el cálculo porque simplemente usa un solo entero para una clase, en lugar de un vector completo.

_________________________________________________

Aquí documentación  de keras para pérdidas (loss)

Aquí documentación de keras para optimizadores  

Aquí documentaciónde keras para métricas 

El optimizador sgd significa que entrenaremos el modelo utilizando Gradiente de descenso estocástico 

En otras palabras, keras ejecutará un algoritmo de retropropagación. Finalmente ponemos accuracy para medir las métricas de la red durante su entrenamiento.

Crear una Red Neuronal con la API Sequential de Keras

Entrenando y evaluando el modelo

Ahora el modelo está listo para ser entrenado, para ello basta con llamar al método fit().

#entrenamos y evaluamos el modelo

history = model.fit(train_data, train_labels, epochs=30,

                    validation_data=(test_data , test_labels))

Le indicamos el conjunto de datos de entrenamiento, train_data y sus etiquetas correspondientes train_labels, así como el número de épocas a entrenar. Por defecto sería 1, pero no es una buena idea, pues con una sóla época los datos no tendrán tiempo Para converger en una solución óptima.  A continuación le pasamos el conjunto de datos de validación (esto es opcional). Keras medirá al final de cada época, la diferencia entre los resultados del conjunto de entrenamiento y el de validación, esto es útil para medir lo bien o mal que se ejecuta el modelo. Si la ejecución del conjunto de entrenamiento es mejor que el de validación, probablemente nuestro modelo tenga sobreajuste (overfitting) o haya una discordancia (error) entre los datos de entrenamiento y validación.

Hecho esto, tendremos nuestro modelo entrenado. En cada época se han mostrado algunos valores, y hemos visto como de una época a la siguiente han ido mejorando. En la última época vemos:

Epoch 30/30
929/929 [==============================] - 1s 1ms/step - loss: 0.9873 
- accuracy: 0.5218 - val_loss: 1.0137 - val_accuracy: 0.4944

 

El último valor 0.4944 nos indica que ha alcanzado una precisión del 49,44 % después de 30 épocas. No es una buena precisión (pero estamos tratando datos quasi-aleatorios, así que es normal) Aquí lo que realmente nos interesa es ver que no está muy alejada de la precisión del grupo de entrenamiento accuracy: 0.5218, lo que nos indica que no parece que tengamos sobreajuste.

Podríamos haber utilizado también el argumento validation_split en lugar de validation_data. Por ejemplo validation_split =0.1 le indica a Keras que debe utilizar el último 10% de los datos (después de barajarlos) para la validación.

En el caso de que el conjunto de entrenamiento esté muy sesgado, de modo que algunas clases estuvieran sub-representadas y otras sobre-representadas, podríamos utilizar el argumento class_weigth en el método fit(). Este argumento nos dará una idea de los pesos de las clases sub-representadas y de las sobre-representadas. Estos pesos los utilizará Keras para computar la pérdida.

El método plot, nos permite visualizar los valores en cada época, mostrados en una gráfica.

import pandas as pd

pd.DataFrame(history.history).plot(figsize=(8, 5))

Crear una Red Neuronal con la API Sequential de Keras


Cuando la curva de entrenamiento y validación están tan cercanas, significa que apenas existe sobreajuste.

Si no estamos satisfechos con la ejecución del modelo, probablemente debamos reajustar algunos hiperparámetros y entrenarlo de nuevo. El primero que debemos comprobar es la tasa de aprendizaje, si una vez modificada, el modelo no mejora, debemos probar con otros hiperparámetros. Pero siempre reajustando la tasa de aprendizaje después de cambiar cualquier hiperparámetro. Si el modelo sigue sin funcionar bien, podemos variar el número de capas, o el número de neuronas por capa, así como el tipo de activación a utilizar en cada capa oculta. También podemos ajustar otros hiperparámetros, como el tamaño del conjunto de datos, esto se puede hacer con el argumento batch_size, el cual por defecto se ejecuta con 32.

Una vez estemos satisfechos con la ejecución de nuestro modelo, debemos evaluarlo con el conjunto de testeo, para estimar el error de generalización antes de poner nuestro modelo en producción. Podemos hacer esto fácilmente con el método evaluate() (también soporta  varios argumentos, como el tamaño de lote batch_size y el peso simple_weight.

Utilizando el modelo para hacer predicciones

A continuación utilizaremos el método pedict() para hacer predicciones sobre nuevas instancias.  Si no tenemos nuevas instancias podemos utilizar  las diez primeras instancias del conjunto de testeo.

X_new = test_data[:10]

y_proba = model.predict(X_new)

y_proba.round(2)

En este caso devuelve

array([[0.28, 0.36, 0.36],

       [0.43, 0.3 , 0.27],

       [0.66, 0.21, 0.13],

       [0.71, 0.21, 0.08],

       [0.45, 0.25, 0.3 ],

       [0.44, 0.26, 0.3 ],

       [0.6 , 0.28, 0.12],

       [0.28, 0.19, 0.53],

       [0.43, 0.3 , 0.27],

       [0.32, 0.29, 0.39]], dtype=float32)

 

Lo que nos indica las probabilidades de 1, X o 2 en los tres ejemplos, en este caso la predicción es X o 2 en el primero (Con un 36 % de probabilidades en ambos casos), 1 en el segundo (con un 43% de probabilidad de acertar) y 1 en el tercero (con un 66% de probabilidad)

Podemos ver los resultados sin probabilidades con

np.argmax(model.predict(X_new), axis=-1)

En este caso nos devuelve un array con los resultados

array([1, 0, 0, 0, 0, 0, 0, 2, 0, 2], dtype=int64)

 

Aquí hay que darse cuenta de que el 0 es el primer resultado de nuestro dataset (el 1), el 1 es la X de las quinielas y el 2 es el 2 de las quinielas. En realidad nuestra quiniela sería (X,1,1,1,1,1,1,2,1,2)

Salvar y restaurar un modelo

Cuando utilizamos la API secuencial y la API funcional salva un modelo entrenado es tan sencillo como hacer

 

model.save("mi_modelo_keras.h5")

 

Esta instrucción salva nuestro modelo en formato .HDF5 en el directorio donde tenemos el cuaderno Jupyter. Salva la arquitectura del modelo, incluyendo cada capa y sus hiperparámetros. Asi como los valores de todos los parámetros del modelo por capa (como su conexión, pesos y sesgos). También salva el optimizador, incluyendo sus hiperparámetros y sus estados si los tienen. Normalmente tendremos un script que entrena un modelo y lo salva. Y otro script (o un servicio web) que lo carga y lo utiliza para hacer predicciones.

Para restaurarlo basta con poner

model = keras.models.load_model("mi_modelo_keras.h5")

Esto funcionará para el API Sequencial o para la Functional, pero no para la API Subclassing. En este caso podemos utilizar save_weights y load_weights para al menos poder salvar y restaurar los pesos de nuestro modelo, pero el resto tendremos que salvarlo y restaurarlo por nuestra cuenta.

Si el entrenamiento de nuestro modelo nos lleva varias horas, podemos salvar los puntos intermedios del entrenamiento con el argumento callbacks del método fit().