sábado, 3 de septiembre de 2022

El problema de los gradientes desaparecidos o disparados (Redes neuronales profundas)

El algoritmo de retropropagación trabaja llevando los datos de la capa de salida a la capa de entrada, propagando el error del gradiente a lo largo de la red. Una vez el algoritmo calcula el gradiente de la función de costo respecto a cada parámetro de la red, este utiliza estos gradientes para actualizar cada parámetro con el paso de descenso de gradiente.

Desafortunadamente, los gradientes a menudo se hacen cada vez más pequeños a medida que el algoritmo desciende hacia capas inferiores de la red, de modo que el Gradiente de Descenso deja las capas finales de la red prácticamente sin actualizar sus pesos impidiendo que la red alcance una buena solución. Esto se llama problema de los gradientes desaparecidos. En algunos casos puede suceder lo contrario, los gradientes pueden crecer hasta el punto de que en capas inferiores la actualización de pesos diverge explosivamente, este problema se conoce como de gradientes disparados, suele ser habitual en redes neuronales recurrentes. 

Este problema mantuvo las redes neuronales sin avance durante los años 2000, pero comenzó a solucionarse con este paper de 2010

En el que se comenzó a encontrar a los “sospechosos” entre ellos una combinación de la función de activación logística sigmoide y la técnica de asignación de pesos más popular por aquella época (distribución normal con una media de 0 y una desviación estándar de 1 cuando en realidad la función logística tiene una media de 0,5 y no de 0) Cuando las entradas toman valores grandes (positivos o negativos)  la función de activación sigmoide se satura con valores de 0 o 1 con una derivada muy cercana a 0, de modo que la retropropagación empuja al gradiente a no propagarse a través de la red  (o a dispararse).

La inicialización He

Para aliviar significativamente los problemas comentados, se propuso un flujo de la señal en ambas direcciones, hacia adelante cuando se han predicciones y hacia atrás con los gradientes de retropropagación, no se desea que la señal desaparezca o crezca exponencialmente, la solución que se propone es inicializar los pesos de cada capa de forma aleatoria. Para más información consultar el paper anteriormente reseñado.

Al final se han establecido diferentes algoritmos de inicialización en función de las funciones de activación que se vayan a utilizar estableciendo la siguiente tabla:


Inicialización    Función de activación 

Glorot Ninguna, tanh, logística, softmax

He   ReLu, y sus variantes

LeCun SELU

Por defecto Keras utiliza la inicialización Glorot con una distribución uniforme. Cuando creamos una capa podemos cambiarla a inicialización He poniendo kernel_initializer=”he_uniform” o kernel_initializer=”he_normal” como esto.

A continuación unas líneas de código.

# 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

#utilizamos la inicialización He normal

keras.layers.Dense(10, activation="relu", kernel_initializer="he_normal")

init = keras.initializers.VarianceScaling(scale=2., mode='fan_avg',

                                          distribution='uniform')

keras.layers.Dense(10, activation="relu", kernel_initializer=init)

Funciones de activación no saturantes

Se había asumido que si la madre naturaleza había establecido la función sigmoide como función de activación, esta sería una buena opción. Pero hay otras funciones de activación que funcionan mucho mejor en redes profundas, en particular la función de activación ReLU no se satura para valores positivos (y además es más rápida de computar).

Desafortunadamente ReLU no es perfecta y tiene sus propios problemas, durante el entrenamiento algunas neuronas “mueren” (sólo producen salida 0) en muchos casos puede morir hasta la mitad de las neuronas de nuestra red.

Una neurona “muere” cuando los pesos se han ajustado de tal forma que la suma de sus entradas es negativa para todas las instancias del conjunto de entrenamiento. Cuando sucede esto, todas sus salidas son cero y el Descenso de Gradiente no le afecta, porque el gradiente de ReLU es cero cuando la entrada es negativa. Para evitar este problema podemos utilizar Leaky ReLU.

En 2015 se propuso otra función de activación llamada ELU  Exponential Linear Unit (ELU) que sobrepasa en ejecución a todas las variantes de ReLU anteriores pero también es más lenta.

En 2017 se introdujo la ELU escalada (Scaled ELU) o SELU   

Como su nombre indica es una variante escalada de ELU. Si construimos una red neuronal compuesta exclusivamente de una pila de capas densas y todas las capas ocultas utilizando función de activación SELU, entonces la red tiende a auto-normalizarse, la capa de salida tiende a preservar de media el 0 y una desviación estándar de 1 durante el entrenamiento, lo cual resuelve el problema de los gradientes desaparecidos  o disparados, de modo que la función de activación SELU tiene un comportamiento superior a cualquier otra, especialmente en redes profundas pero con condiciones:

-Las características de entrada deben ser estandarizadas (media 0, y desviación  estándar de 1)

-Los pesos de cada capa deben estar inicializados con la inicialización normal LeCun en keras, del siguiente modo: kernel_initializer=”lecun_normal”

-La arquitectura de la red debe ser secuencial, si intentamos utilizar SELU en una red no secuencial, como por ejemplo una red recurrente o redes con saltos en las conexiones como Wide & Deep, la autonormalización no está garantizada.

- Todas las capas deben ser densas aunque también puede funcionar bien en redes convolucionales.

En general SELU > ELU> Leaky ReLU (y sus variantes)>ReLU>tanh>logística. Si la arquitectura de la red previene la autonormalización, ELU puede ser mejor que SELU y si la velocidad es nuestra prioridad, ReLU puede seguir siendo la mejor opción.

Aquí podemos ver las diferentes funciones de activación de Keras

[m for m in dir(keras.activations) if not m.startswith("_")]

 

El problema de los gradientes desaparecidos  o disparados (Redes neuronales profundas)

Cargamos y tratamos nuestro dataset

#importamos nuestro dataset

import pandas as pd

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

quinielas.head()

 

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

#dejamos división, jornada, EquipoLocal, EquipoVisitante, PuntosLocal, PuntosVisitante, Puntos_Normalizados

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

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

y = tf.cast(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']), tf.float64)

 

x_data = np.array(X)

y_data = np.array(y)

 

#dividimos el dataset en una parte de entrenamiento y otra de testeo

train_data = x_data[:29718]

test_data = x_data[29718:]

train_labels = y_data [:29718]

test_labels = y_data [29718:]

 Ahora definimos el modelo con la activación He.

model = keras.models.Sequential()

model.add(keras.layers.Dense(300, kernel_initializer="he_normal"))   

model.add(keras.layers.LeakyReLU()) 

model.add(keras.layers.Dense(100, kernel_initializer="he_normal"))   

model.add(keras.layers.LeakyReLU()) 

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

 

model.compile(loss="categorical_crossentropy",

              optimizer="sgd",

              metrics=["accuracy"])

        

#entrenamos y evaluamos el modelo

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

                    validation_data=(test_data , test_labels))

Notese que en la línea

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

El 3 es el número de capas, si tenemos una red profunda de 10 o n capas ese número debe ser 10 o n.

Si queremos implementar la función ELU con Keras es trivial, basta con indicarlo al definir las capas

keras.layers.Dense(10, activation="elu")

Para implementar SELU hacemos

keras.layers.Dense(10, activation="selu",

                   kernel_initializer="lecun_normal")

La definición completa del modelo SELU quedaría así.

#SELU

model = keras.models.Sequential()

model.add(keras.layers.Dense(300, activation="selu",

                             kernel_initializer="lecun_normal"))

for layer in range(99):

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

                                 kernel_initializer="lecun_normal"))

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

Normalización Batch

Aunque la inicialización He con la función de activación ELU o cualquier variante de ReLU puede reducir significativamente el peligro de los gradientes desaparecidos  o disparados al comienzo del entrenamiento, no hay garantías de que no suceda durante el resto del entrenamiento. En un paper de 2015 se propuso una solución llamada Normalización por Lotes (Batch Normalization BN)  Que trata de atajar este problema. La técnica consiste en añadir una operación al modelo justo antes o después de la función de activación de cada capa oculta. Esta operación simplemente centra en cero y normaliza cada entrada, entonces escala y mueve el resultado utilizando dos nuevos vectores de parámetros por capa: una para escalar y otro para mover. 

Si utilizamos esta técnica al principio, en muchos casos no será necesario estandarizar nuestro set de entrenamiento, BN lo hará por nosotros. BN estandariza las entradas y reescala las salidas. BN mejora considerablemente la ejecución de todas la redes neuronales profundas, sobre todo es un gran avance para tareas de clasificación de imágenes (ImageNet).

Sin embargo BN añade complejidad al modelo (aunque nos elimina la necesidad de normalizar los datos antes) y también añade una penalización de tiempo, pues es algo más lenta.


Implementación de Normalización por Lotes

La implementación de BN en Keras es bastante sencilla e intuitiva

# Normalización por lotes

model = keras.models.Sequential([

    keras.layers.BatchNormalization(),

    keras.layers.Dense(300, activation="relu"),

    keras.layers.BatchNormalization(),

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

    keras.layers.BatchNormalization(),

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

])

Y esto es todo, en este caso hay solamente dos capas ocultas y la diferencia es pequeña, pero en una red profunda, la diferencia es considerable.

A veces, aplicar BN antes de la función de activación funciona mejor (hay un debate sobre este tema). Además, la capa antes de la capa BatchNormalization no necesita tener términos de sesgo, ya que la capa BatchNormalization los incluye, por lo que podemos establecer use_bias = False al crear esas capas:

model = keras.models.Sequential([

    keras.layers.BatchNormalization(),

    keras.layers.Dense(300, use_bias=False),

    keras.layers.BatchNormalization(),

    keras.layers.Activation("relu"),

    keras.layers.Dense(100, use_bias=False),

    keras.layers.BatchNormalization(),

    keras.layers.Activation("relu"),

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

])

La clase BatchNormalization tiene unos pocos parámetros que podemos ajustar, si utilizamos los parámetros por defecto, normalmente funciona bien, pero ocasionalmente podemos necesitar ajustar el hiperparámetro momento (momentum), es utilizado para actualizar las medias de las capas de Normalización por Lotes. Un buen valor para el momento estará cercano a 1, 0,9, 0,99 Etc. son buenas opciones.

Otro hiperparámetro importante es el eje (axis) el cual determina el eje que debe ser normalizado. Por defecto es -1.


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.