• Saltar a la navegación principal
  • Saltar al contenido principal
Logo Codificando Bits

Codificando Bits

  • Academia
    • Rutas
    • Cursos
    • Proyectos
    • Tutoriales
  • Acceder
  • Suscribirse

¿Por qué obtengo resultados diferentes cada vez que entreno un modelo de Machine Learning?

En este tutorial vamos a entender por qué en ocasiones un modelo de Machine Learning arroja resultados diferentes cada vez que lo entrenamos.

Además veremos cómo corregir este comportamiento, garantizando que los resultados son reproducibles (es decir que son los mismos cada vez que ejecutamos el entrenamiento).

Al final del tutorial se encuentra el enlace para descargar el set de datos y el código fuente.

Video

En el canal de YouTube se encuentra el video de este tutorial:

Introducción

Muchas veces cuando estamos construyendo un modelo de Machine Learning nos debemos enfrentar a una situación muy común: cada vez que ejecutamos el entrenamiento obtenemos resultados diferentes.

Esto quiere decir que el entrenamiento no es reproducible. Si hoy ejecutamos el entrenamiento obtendremos un desempeño pero este no será el mismo si ejecutamos el entrenamiento posteriormente.

Y al no contar con un entrenamiento reproducible no podemos tener la certeza de cuál será el verdadero desempeño del modelo.

Así que en este tutorial vamos a entender cuáles son los motivos que explican este comportamiento y veremos además las herramientas que podremos usar para reducir o eliminar estas variaciones que estamos observando.

El problema a resolver

Resolvamos un sencillo problema de Machine Learning a través del cual podremos entender por qué en ocasiones el entrenamiento del modelo no genera resultados reproducibles.

Para esto usaremos un set de datos que contiene la información asociada al pago (“no default”) e impago (“default”) de un crédito bancario para un total de 5.383 personas.

La idea es construir un modelo de Machine Learning que tome como entrada datos como el puntaje crediticio, los ingresos mensuales y el monto del préstamo, y aprenda a predecir si el usuario pagará a tiempo (“no default”, 0) o se retrasará en sus pagos (“default”, 1).

Y la idea es ver que en este proceso llegaremos a un punto en el que cada vez que entrenemos el modelo obtendremos resultados diferentes, con lo cual podremos analizar estrategias para corregir este comportamiento.

El set de datos

Comencemos importando las librerías requeridas:

import pandas as pd
import plotly.express as px

Inicialmente usaremos Pandas para leer y realizar el pre-procesamiento básico del set de datos y Plotly para su visualización interactiva.

Y ahora leamos el set de datos:

df = pd.read_csv('dataset_default.csv')
df

Al ejecutar lo anterior obtendremos este DataFrame de Pandas:

|      | Puntaje crediticio | Ingresos mensuales (USD) | Monto del préstamo (USD) | ¿Default/No default? |
|------|--------------------|--------------------------|--------------------------|----------------------|
| 0    | 5.3                | 6074.6                   | 34159.9                  | 0                    |
| 1    | 4.1                | 4955.0                   | 35168.6                  | 0                    |
| 2    | 4.4                | 4573.6                   | 26852.8                  | 0                    |
| 3    | 3.1                | 6019.0                   | 31500.9                  | 0                    |
| 4    | 5.0                | 1481.1                   | 38094.6                  | 1                    |
| ...  | ...                | ...                      | ...                      | ...                  |
| 5378 | 2.9                | 2558.5                   | 47140.9                  | 1                    |
| 5379 | 0.7                | 3594.2                   | 46654.5                  | 1                    |
| 5380 | 6.4                | 681.2                    | 44423.2                  | 1                    |
| 5381 | 2.5                | 5172.0                   | 24832.9                  | 0                    |
| 5382 | 7.1                | 3556.1                   | 46061.5                  | 1                    |
5383 rows × 4 columns

Veamos un conteo de categorías para verificar si el set de datos está balanceado:

df['¿Default/No default?'].value_counts(normalize=True)

Con lo que obtenemos:

    1    0.507152
    0    0.492848
    Name: ¿Default/No default?, dtype: float64

Y podemos verificar que tenemos prácticamente una distribución 50/50 para cada categoría a predecir.

Por último, podemos generar un gráfico 3D de los datos para entender sus principales características:

fig = px.scatter_3d(df, x='Puntaje crediticio', y='Ingresos mensuales (USD)',
                    z='Monto del préstamo (USD)', color='¿Default/No default?',
                    size_max=6, opacity = 0.5)
fig.show()

con lo cual podemos ver que en esencia tenemos dos agrupaciones de datos (correspondientes a las categorías “Default” y “No default”) relativamente bien separadas.

Funciones requeridas

Para ver cómo cambian los resultados una y otra vez tras el entrenamiento, implementaremos varias funciones para pre-procesar los datos, entrenar y poner a prueba el modelo:

  1. crear_in_out: permite obtener las entradas (X) y salida (Y) del modelo a partir del set de datos original
  2. crear_train_val_test: crea los sets de entrenamiento, validación y prueba a partir de X y Y
  3. escalar_datos: escala los sets de entrenamiento, validación y prueba (entradas X) en el rango de 0 a 1
  4. crear_entrenar_modelo: crea y entrena una neurona artificial en Keras.
  5. validar_modelo: calcula la exactitud del modelo entrenado usando los sets de entrenamiento y validación.

Veamos entonces cómo implementar y ejecutar cada función.

Función crear_in_out

def crear_in_out(datos):
    '''Obtiene las entradas y salida del modelo a partir
    de los datos originales'''
    # X: columnas "puntaje crediticio, "Ingresos mensuales (USD)", "Monto del préstamo (USD)"
    X = datos.iloc[:,0:3].to_numpy()

    # Y: columna "¿Default/No default?"
    Y = datos.iloc[:,3].to_numpy()

    return X, Y

# Ejecutar la función
X, Y = crear_in_out(df)
X, Y

Tras ejecutar la función obtenemos los arreglos X y Y, el primero con 5383 filas y 3 columnas y el segundo un vector de 5383 elementos con la categoría de cada dato:

    (array([[5.30000e+00, 6.07460e+03, 3.41599e+04],
            [4.10000e+00, 4.95500e+03, 3.51686e+04],
            [4.40000e+00, 4.57360e+03, 2.68528e+04],
            ...,
            [6.40000e+00, 6.81200e+02, 4.44232e+04],
            [2.50000e+00, 5.17200e+03, 2.48329e+04],
            [7.10000e+00, 3.55610e+03, 4.60615e+04]]),
     array([0, 0, 0, ..., 1, 0, 1]))

Función crear_train_val_test

El siguiente paso es crear los sets de entrenamiento, validación y prueba.

En este caso usaremos el módulo train_test_split de la librería Scikit-Learn para generar las particiones correspondientes a los sets de entrenamiento (70%), validación (15%) y prueba (15%).

Debemos ejecutar dos veces esta función para lograr el resultado deseado.

En la primera ejecución generaremos el set de entrenamiento y un set de datos que llamaremos “resto”: el primero tendrá un 70% de los datos, mientras que el segundo contendrá el 30% restante.

Luego, realizaremos la segunda ejecución para dividir el set “resto” en dos mitades, correspondientes a los sets de validación (15%) y prueba (15%):

from sklearn.model_selection import train_test_split

def crear_train_val_test(X,Y):
    '''Crear los sets de entrenamiento (70%), validación (15%)
    y prueba (15%)'''
    xtr, x_resto, ytr, y_resto = train_test_split(X, Y, test_size=0.3)
    xvl, xts, yvl, yts = train_test_split(x_resto, y_resto, test_size=0.5)

    return xtr, ytr, xvl, yvl, xts, yts

# Ejecutar la función
x_train, y_train, x_val, y_val, x_test, y_test = crear_train_val_test(X,Y)

# Verificar
print(x_train.shape, y_train.shape)
print(x_val.shape, y_val.shape)
print(x_test.shape, y_test.shape)

Tras ejecutar la función anterior obtenemos los siguientes tamaños para los sets de entrenamiento, validación y prueba obtenidos:

    (3768, 3) (3768,)
    (807, 3) (807,)
    (808, 3) (808,)

Función escalar_datos

Las variables de entrada al modelo (columnas Puntaje crediticio, Ingresos mensuales (USD) y Monto del préstamo (USD) del DataFrame original) tienen rangos de valores diferentes.

Para obtener mejores resultados durante el entrenamiento es aconseable realizar el escalamiento de los datos. En este caso escalaremos cada una de las columnas para garantizar que todas estén dentro del mismo rango de valores.

Este escalamiento lo realizaremos con el módulo MinMaxScaler de la librería Scikit-Learn, que nos permite escalar cada columna al rango de 0 a 1:

from sklearn.preprocessing import MinMaxScaler

def escalar_datos(xtr, xvl, xts):
    '''Escalar datos de entrada (entrenamiento, validación y prueba)
    en el rango de 0 a 1'''
    scaler = MinMaxScaler()

    # Fit + transform sobre el set de entrenamiento
    xtr_s = scaler.fit_transform(xtr)

    # Transform sobre prueba y validación
    xvl_s = scaler.transform(xvl)
    xts_s = scaler.transform(xts)

    return xtr_s, xvl_s, xts_s

# Ejecutar la función
x_train_s, x_val_s, x_test_s = escalar_datos(x_train, x_val, x_test)

# Verificar
print(x_train_s.min(axis=0),x_train_s.max(axis=0))
print(x_val_s.min(axis=0),x_val_s.max(axis=0))
print(x_test_s.min(axis=0),x_test_s.max(axis=0))

En la función anterior primero hemos escalado el set de entrenamiento usando el método fit_transform() que además de realizar este escalamiento permite almacenar en la variable scaler los valores máximo y mínimo correspondientes a este set de datos.

Posteriormente, con estos valores máximo y mínimo del set de entrenamiento realizamos el escalamiento correspondiente para los sets de validación y prueba, usando el método transform().

Tras realizar este procedimiento podemos verificar que cada una de las columnas, en cada uno de los tres sets, ha sido escalada al rango de 0 a 1:

    [0. 0. 0.] [1. 1. 1.]
    [ 0.         -0.00021444  0.00060065] [1.         1.         0.99843832]
    [ 0.00000000e+00 -8.57767802e-05  8.35566293e-03] [1.         0.99864187 0.9984

Función crear_entrenar_modelo

En este caso entrenaremos una sencilla neurona artificial que contendrá una entrada de tamaño 3×1 (correspondiente a las tres variables de entrada al modelo) y una salida con una sola neurona y función de activación sigmoidal encargada de generar la categoría predicha (“Default” o “No default”).

import tensorflow as tf

def crear_entrenar_modelo(xtr, ytr):
    '''Crea y entrena una neurona artificial en Keras, con estas características:
    - Tamaño de la entrada: 3x1
    - Tamaño de la salida: 1 neurona (default/no default)'''

    # Crear la neurona
    modelo = tf.keras.models.Sequential([
        tf.keras.layers.Flatten(input_shape=(3,1)), # Entrada
        tf.keras.layers.Dense(1, activation='sigmoid') # Salida
        ])

    # Compilar el modelo
    modelo.compile(optimizer='sgd',
                    loss='BinaryCrossentropy',
                    metrics = ['accuracy'])

    # Entrenarlo
    modelo.fit(xtr, ytr, epochs=5, verbose=0)

    return modelo

# Ejecutar la función
neurona = crear_entrenar_modelo(x_train_s, y_train)

Sugiero revisar el tutorial de Keras en donde explico en detalle cómo crear un modelo de Machine Learning con esta librería.

Función validar modelo

Con el modelo construido (la variable neurona obtenida tras ejecutar la función anterior) lo único que nos resta es realizar su validación.

En este caso el objetivo es tomar el modelo entrenado con la función anterior y presentarle el set de entrenamiento y el set de validación y en cada caso generar las predicciones correspondientes y calcular su exactitud (es decir el porcentaje de datos clasificados correctamente).

Para esto simplemente tomamos el modelo entrenado y usamos el método evaluate:

def validar_modelo(modelo, xtr, ytr, xvl, yvl):
    '''Calcula la exactitud del modelo entrenado con los sets
    de entrenamiento y validación e imprime el resultado en pantalla'''

    # Calcular desempeño con cada subset
    train_loss, train_acc = modelo.evaluate(xtr, ytr, verbose=0)
    val_loss, val_acc = modelo.evaluate(xvl, yvl, verbose=0)

    # Imprimir resultado en pantalla
    print(f'Exactitud entrenamiento/validación: {100*train_acc:.2f}% / {100*val_acc:.2f}%')

# Ejecutar la función
validar_modelo(neurona, x_train_s, y_train, x_val_s, y_val)

Tras crear la función y ejecutarla obtenemos este resultado:

    Exactitud entrenamiento/validación: 64.20% / 61.59%

En principio el modelo no está generando unas buenas predicciones, pues el nivel de exactitud es bastante bajo (60%).

Sin embargo, recordemos que no nos interesa construir un modelo con la exactitud más alta posible sino analizar la variabilidad que puede tener su desempeño, que será precisamente el tema de la siguiente sección.

Variabilidad del modelo entrenado

Como decíamos al comienzo de este tutorial, muchas veces cuando ejecutamos el entrenamiento del modelo una y otra vez obtenemos resultados diferentes.

Para comprobar lo anterior ejecutemos 4 veces y de manera secuencial las funciones creadas en la sección anterior:

for i in range(4): # Por 3 iteraciones
    print(f'Ejecución {i+1}:')
    X, Y = crear_in_out(df)
    x_train, y_train, x_val, y_val, x_test, y_test = crear_train_val_test(X,Y)
    x_train_s, x_val_s, x_test_s = escalar_datos(x_train, x_val, x_test)
    neurona = crear_entrenar_modelo(x_train_s, y_train)
    validar_modelo(neurona, x_train_s, y_train, x_val_s, y_val)
    print('-'*20)

En principio esperaríamos obtener, tras cada ejecución, exactamente los mismos niveles de exactitud para los sets de entrenamiento y validación. Sin embargo, al ejecutar el código anterior obtenemos estos resultados:

    Ejecución 1:
    Exactitud entrenamiento/validación: 89.76% / 91.45%
    --------------------
    Ejecución 2:
    Exactitud entrenamiento/validación: 95.04% / 95.42%
    --------------------
    Ejecución 3:
    Exactitud entrenamiento/validación: 94.67% / 94.55%
    --------------------
    Ejecución 4:
    Exactitud entrenamiento/validación: 91.08% / 91.82%
    --------------------

Y vemos que efectivamente en cada ejecución obtenemos resultados diferentes, lo cual es un resultado sorprendente, pues no hemos cambiado ni los datos ni el modelo ni tampoco ninguna de las funciones, así que los resultados deberían ser idénticos en cada ejecución.

¿A qué se debe esto? En esencia existen 4 razones que explican este comportamiento:

  1. Cada vez que entrenamos el modelo lo hacemos con un set de datos diferente
  2. El algoritmo de entrenamiento contiene algún componente de aleatoriedad
  3. El hardware sobre el que hacemos el entrenamiento no siempre es el mismo
  4. El modelo construido está sobre-ajustado (lo que se conoce como overfitting) o los datos de entrenamiento no son suficientes.

La razón 4 se conoce como overfitting y ocurre cuando el modelo “memoriza” los datos de entrenamiento lo que hace que una ligera variación en estos datos genere grandes variaciones al momento de evaluar su desempeño.

Sin embargo, en este caso particular podemos descartar este sobre-ajuste, pues a pesar de que los porcentajes de exactitud cambian en cada ejecución, vemos que tienen valores muy cercanos tanto para el set de entrenamiento como para el de validación.

La razón 3 es inevitable en ocasiones, pues no siempre podremos ejecutar el código en la misma CPU o GPU. Sin embargo, este no es el caso de este tutorial, pues estamos usando la misma máquina que nos provee Google Colab.

Sin embargo, al momento de escribir el código podemos controlar las razones 1 y 2 y por tanto en lo que queda de este tutorial nos enfocaremos en estrategias para corregir estas situaciones.

Y para entender esto tenemos que hablar de la semilla del generador de números aleatorios.

La semilla del generador de números aleatorios

Implícitamente, en el código que hemos desarrollado hasta este punto, hay un componente aleatorio al momento de preparar los datos y crear el modelo:

  1. Al generar la partición de los datos en entrenamiento, validación y prueba (función crear_train_val_test()) el algoritmo hace primero una mezcla aleatoria de los datos. Con esto se evita que el modelo considere el orden de los datos como un patrón que debe aprender.
  2. Al crear el modelo (función crear_entrenar_modelo) generalmente sus parámetros son inicializados con valores aleatorios lo que permite, entre otras cosas, agilizar el entrenamiento.

Y este componente aleatorio hace que:

  1. Cada vez que generemos la partición de los datos tengamos un set de datos diferente
  2. El punto de partida del entrenamiento sea diferente en cada ejecución

¡Lo que nos lleva a tener resultados diferentes!

Esta aleatoriedad se puede controlar jugando con algo que se conoce como la “semilla del generador de números aleatorios”:

  • Este generador es el algoritmo que usa el computador para producir números aleatorios.
  • El algoritmo genera simplemente una secuencia de números que son pseudo-aleatorios
  • Pero esta secuencia depende de un número inicial: la semilla. Si el número inicial es siempre el mismo la secuencia de números generada será pseudo-aleatoria pero será siempre la misma.

Entendamos este concepto de la semilla del generador aleatorio con un ejemplo:

# Importemos numpy y generemos un número entero aleatorio
# entre 0 y 100
import numpy as np

np.random.randint(100)

Con lo cual obtenemos un valor igual a 6.

Sin embargo, si ejecutamos una vez más la celda anterior obtenemos el número 24 y si repetimos esto el nuevo número generado será 63.

En el ejemplo anterior, cada vez que ejecutamos la celda obtenemos un número diferente.

Pero si ahora fijamos la semilla (usando el método random.seed()) y ejecutamos el código una y otra vez:

# Con semilla
SEED = 123 # Puede ser cualquier valor, pero DEBE SER SIEMPRE EL MISMO
np.random.seed(SEED)
np.random.randint(100)

Y ejecutamos una y otra vez la celda anterior SIEMPRE obtendremos el número 66.

Es decir que al fijar la semilla a un valor fijo !obtendremos siempre el mismo número! Será aleatorio, porque directamente no estamos fijando directamente su valor, pero será SIEMPRE el mismo en cada ejecución.

Y con este sencillo truco podemos controlar la aleatoriedad, es decir garantizar la reproducibilidad del entrenamiento del modelo de Machine Learning.

Teniendo claro este sencillo concepto de la semilla del generador de números aleatorios, veamos ahora cómo aplicarlo a nuestro problema.

Haciendo que el entrenamiento sea reproducible

Para que el entrenamiento sea replicable tendremos que modificar ligeramente dos de las funciones que creamos anteriormente:

  • crear_train_val_test: para que cada vez que la ejecutemos tengamos la misma partición (a pesar de que la mezcla es aleatoria)
  • crear_entrenar_modelo: para que al momento de crear la neurona artificial sus parámetros se inicialicen siempre con los mismos valores aleatorios

Re-escribamos estas dos funciones para fijar la semilla del generador de números aleatorios a un valor fijo.

En el primer caso (crear_train_val_test) basta con usar el argumento random_state de la función train_test_split de Scikit-Learn, con lo cual podremos fijar el valor deseado para la semilla:

def crear_train_val_test(X,Y):
    xtr, x_resto, ytr, y_resto = train_test_split(X, Y, test_size=0.3,
                                                  random_state=45)
    xvl, xts, yvl, yts = train_test_split(x_resto, y_resto, test_size=0.5,
                                          random_state=43)

    return xtr, ytr, xvl, yvl, xts, yts

Mientras que en la función crear_entrenar_modelo simplemente llamamos el método set_random_seed al inicio de la función y especificamos el valor deseado de la semilla, para que al momento de crear el modelo sus parámetros sean inicializados siempre a los mismos valores aleatorios:

def crear_entrenar_modelo(xtr, ytr):
    # Fijar semilla del generador aleatorio
    tf.keras.utils.set_random_seed(123)

    # Crear modelo
    modelo = tf.keras.models.Sequential([
        tf.keras.layers.Flatten(input_shape=(3,1)),
        tf.keras.layers.Dense(1, activation='sigmoid')
        ])

    # Compilarlo
    modelo.compile(optimizer='sgd',
                    loss='BinaryCrossentropy',
                    metrics = ['accuracy'])

    # Entrenarlo sin imprimir iteraciones en pantalla. No validar
    modelo.fit(xtr,ytr,verbose=0,
            epochs=5)

    return modelo

¡Y listo, con esto ya hemos fijado las semillas de los generadores aleatorios!

Estas modificaciones ya deberían garantizar la reproducibilidad del entrenamiento. Para verificar esto, ejecutemos de nuevo 3 veces el conjunto de funciones y veamos los resultados:

for i in range(3): # Por 3 iteraciones
    print(f'Ejecución {i+1}:')
    X, Y = crear_in_out(df)
    x_train, y_train, x_val, y_val, x_test, y_test = crear_train_val_test(X,Y)
    x_train_s, x_val_s, x_test_s = escalar_datos(x_train, x_val, x_test)
    neurona = crear_entrenar_modelo(x_train_s, y_train)
    validar_modelo(neurona, x_train_s, y_train, x_val_s, y_val)
    print('-'*20)

Obteniendo estos desempeños:

    Ejecución 1:
    Exactitud entrenamiento/validación: 93.23% / 93.18%
    --------------------
    Ejecución 2:
    Exactitud entrenamiento/validación: 93.23% / 93.18%
    --------------------
    Ejecución 3:
    Exactitud entrenamiento/validación: 93.23% / 93.18%
    --------------------
    Ejecución 4:
    Exactitud entrenamiento/validación: 93.23% / 93.18%
    --------------------
    Ejecución 5:
    Exactitud entrenamiento/validación: 93.23% / 93.18%
    --------------------

¡Y listo, tenemos un modelo perfectamente reproducible!

Enlaces de descarga código fuente y set de datos

Contenido exclusivo para suscriptores

Si eres suscriptor accede en este enlace ó suscríbete a la Academia Online y accede a todo el contenido (lecciones en video, código fuente, sets de datos y descargas) de todos los cursos, proyectos y tutoriales.

Conclusión

Muy bien, en este tutorial hemos entendido las razones por las cuales cuando en ocasiones entrenamos un modelo de Machine Learning podemos obtener resultados diferentes cada vez que ejecutamos el código durante la validación.

Y lo que ocurre es que el modelo puede tener overfitting o porque estamos usando un hardware diferente en cada ocasión al momento de ejecutar el código.

Pero adicionalmente vimos que esta variabilidad también está asociada a que durante la generación de los sets de entrenamiento, validación y prueba y al momento de crear el modelo, los algoritmos hacen uso de valores diferentes para la semilla del generador de números aleatorios.

Así que esta situación se puede resolver si al momento de escribir el código fijamos siempre la semilla de estos generadores aleatorios a valores fijos, con lo cual (si no hay overfitting y si usamos el mismo hardware para realizar el entrenamiento) obtendremos siempre los mismos resultados.

© Codificando Bits, LLC | Blog | Contacto | Servicios | Acerca de | Políticas

¿Has olvidado la contraseña?
¿Has perdido tu contraseña? Por favor, introduce tu nombre de usuario o dirección de correo electrónico. Recibirás por correo electrónico un enlace para crear una nueva contraseña.
body::-webkit-scrollbar { width: 7px; } body::-webkit-scrollbar-track { border-radius: 10px; background: #f0f0f0; } body::-webkit-scrollbar-thumb { border-radius: 50px; background: #dfdbdb }