Un enorme agradecimiento a Martín Chaves quien fue coautor de esta publicación y desarrolló los guiones de ejemplo.
No es ningún secreto que los sistemas de aprendizaje automático (ML) requieren un ajuste cuidadoso para que sean realmente útiles, y sería extremadamente raro que un modelo funcionara perfectamente la primera vez que se ejecuta.
Al comenzar su viaje de aprendizaje automático, una trampa fácil en la que caer es probar muchas cosas diferentes para mejorar el rendimiento, pero no registrar estas configuraciones a lo largo del camino. Esto hace que sea difícil saber qué configuración (o combinación de configuraciones) tuvo el mejor rendimiento.
Al desarrollar modelos, hay muchas “perillas” y “palancas” que se pueden ajustar y, a menudo, la mejor manera de mejorar es probar diferentes configuraciones y ver cuál funciona mejor. Estas cosas incluyen mejorar las funciones que se utilizan, probando diferentes arquitecturas de modelo, ajustando los hiperparámetros del modelo y otros. La experimentación debe ser sistemática y los resultados deben registrarse. Es por eso que tener una buena configuración para realizar estos experimentos es fundamental en el desarrollo de cualquier Sistema ML práctico, de la misma manera que el control de código fuente es fundamental para el código.
Aquí es donde experimentos ven a jugar. Los experimentos son una forma de realizar un seguimiento de estas diferentes configuraciones y de los resultados que se derivan de ellas.
Lo bueno de los experimentos con Fabric es que en realidad son un envoltorio para Flujo mlf, una plataforma de código abierto muy popular para gestionar el ciclo de vida del aprendizaje automático de un extremo a otro. Esto significa que podemos utilizar todas las excelentes funciones que MLFlow tiene para ofrecer, pero con el beneficio adicional de no tener que preocuparnos por configurar la infraestructura que requeriría un entorno colaborativo de MLFlow. ¡Esto nos permite centrarnos en las cosas divertidas 😎!
En esta publicación, veremos cómo utilizar experimentos en Fabric y cómo registrar y analizar los resultados de estos experimentos. Específicamente, cubriremos:
- ¿Cómo funciona MLFlow?
- Crear y configurar experimentos
- Ejecución de experimentos y registro de resultados
- Análisis de resultados
A alto nivel, MLFlow es una plataforma que ayuda a gestionar el ciclo de vida del aprendizaje automático de un extremo a otro. Es una herramienta que ayuda a rastrear experimentos, empaquetar código en ejecuciones reproducibles y compartir e implementar modelos. Es esencialmente una base de datos dedicada a realizar un seguimiento de todas las diferentes configuraciones y resultados de los experimentos que ejecuta.
Hay dos estructuras organizativas principales en MLFlow: experimentos y carreras.
Un experimento es un grupo de ejecuciones, donde una ejecución es la ejecución de un bloque de código, una función o un script. Esto podría ser entrenar un modelo, pero también podría usarse para rastrear cualquier cosa que pueda cambiar entre ejecuciones. Un experimento es entonces una forma de agrupar ejecuciones relacionadas.
Para cada ejecución, se puede registrar y adjuntar información: podrían ser métricas, hiperparámetros, etiquetas, artefactos (como gráficos, archivos u otros resultados útiles) e incluso modelos. Al adjuntar modelos a las ejecuciones, podemos realizar un seguimiento de qué modelo se utilizó en cada ejecución y cómo funcionó. Piense en ello como un control de fuente para modelos, que es algo que abordaremos en la próxima publicación.
Las ejecuciones se pueden filtrar y comparar. Esto nos permite comprender qué ejecuciones tuvieron más éxito, seleccionar la ejecución con mejor rendimiento y utilizar su configuración (por ejemplo, en la implementación).
Ahora que hemos cubierto los conceptos básicos de cómo funciona MLFlow, ¡vamos a ver cómo podemos usarlo en Fabric!
Como todo en Fabric, la creación de elementos se puede realizar de varias maneras, ya sea desde el espacio de trabajo + Nuevo menú, utilizando la experiencia de ciencia de datos o en código. En este caso, usaremos la experiencia de ciencia de datos.
Una vez hecho esto, para usar ese experimento en un Notebook, necesitamos import mlflow y configure el nombre del experimento:
import mlflowexperiment_name = "[name of the experiment goes here]"
# Set the experiment
mlflow.set_experiment(experiment_name)
Alternativamente, se puede crear un experimento a partir de código, lo que requiere un comando adicional:
import mlflowexperiment_name = "[name of the experiment goes here]"
# First create the experiment
mlflow.create_experiment(name=experiment_name)
# Then select it
mlflow.set_experiment(experiment_name)
Tenga en cuenta que, si ya existe un experimento con ese nombre, create_experiment arrojará un error. Podemos evitar esto comprobando primero la existencia de un experimento y creándolo solo si no existe:
# Check if experiment exists
# if not, create it
if not mlflow.get_experiment_by_name(experiment_name):
mlflow.create_experiment(name=experiment_name)
Ahora que tenemos el experimento configurado en el contexto actual, podemos comenzar a ejecutar el código que se guardará en ese experimento.
Para comenzar a registrar nuestros resultados en un experimento, debemos iniciar una ejecución. Esto se hace usando el start_run() función y devuelve un run administrador de contexto. A continuación se muestra un ejemplo de cómo iniciar una carrera:
# Start the training job with `start_run()`
with mlflow.start_run(run_name="example_run") as run:
# rest of the code goes here
Una vez que se inicia la ejecución, podemos comenzar a registrar métricas, parámetros y artefactos. Aquí hay un ejemplo de código que haría eso usando un modelo y un conjunto de datos simples, donde registramos la puntuación del modelo y los hiperparámetros utilizados:
# Set the hyperparameters
hyper_params = {"alpha": 0.5, "beta": 1.2}# Start the training job with `start_run()`
with mlflow.start_run(run_name="simple_training") as run:
# Create model and dataset
model = create_model(hyper_params)
X, y = create_dataset()
# Train model
model.fit(X, y)
# Calculate score
score = lr.score(X, y)
# Log metrics and hyper-parameters
print("Log metric.")
mlflow.log_metric("score", score)
print("Log params.")
mlflow.log_param("alpha", hyper_params["alpha"])
mlflow.log_param("beta", hyper_params["beta"])
En nuestro ejemplo anterior, se entrena un modelo simple y se calcula su puntuación. Tenga en cuenta cómo se pueden registrar las métricas utilizando mlflow.log_metric("metric_name", metric) y los hiperparámetros se pueden registrar usando mlflow.log_param("param_name", param).
Los datos
Veamos ahora el código utilizado para entrenar nuestros modelos, que se basan en el resultado de los partidos de baloncesto. Los datos que estamos analizando son de los torneos de baloncesto universitario de EE. UU. de 2024, que se obtuvieron de la competencia Kaggle Machine Learning Mania 2024 de marzo, cuyos detalles se pueden encontrar. aquíy tiene licencia CC BY 4.0
En nuestra configuración, queríamos probar tres modelos diferentes, que utilizaban un número cada vez mayor de parámetros. Para cada modelo, también queríamos probar tres tasas de aprendizaje diferentes (un hiperparámetro que controla cuánto ajustamos los pesos de nuestra red para cada iteración). El objetivo era encontrar la mejor combinación de modelo y tasa de aprendizaje que nos diera la mejor puntuación de zarza en el equipo de prueba.
Los modelos
Para definir la arquitectura del modelo, utilizamos TensorFlow, creando tres redes neuronales simples. Estas son las funciones que ayudaron a definir los modelos.
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Densedef create_model_small(input_shape):
model = Sequential([
Dense(64, activation='relu', input_shape=(input_shape,)),
Dense(1, activation='sigmoid')
])
return model
def create_model_medium(input_shape):
model = Sequential([
Dense(64, activation='relu', input_shape=(input_shape,)),
Dense(64, activation='relu'),
Dense(1, activation='sigmoid')
])
return model
def create_model_large(input_shape):
model = Sequential([
Dense(128, activation='relu', input_shape=(input_shape,)),
Dense(64, activation='relu'),
Dense(64, activation='relu'),
Dense(1, activation='sigmoid')
])
return model
Crear nuestros modelos de esta manera nos permite experimentar fácilmente con diferentes arquitecturas y ver cómo funcionan. Luego podemos usar un diccionario para crear un poco fábrica de modelosque nos permitirá crear fácilmente los modelos con los que queramos experimentar.
También definimos la forma de entrada, que era la cantidad de funciones que estaban disponibles. Decidimos entrenar los modelos para 100 épocas, lo que debería ser suficiente para la convergencia 🤞.
model_dict = {
'model_sma': create_model_small, # small
'model_med': create_model_medium, # medium
'model_lar': create_model_large # large
}input_shape = X_train_scaled_df.shape[1]
epochs = 100
Después de esta configuración inicial, llegó el momento de iterar sobre el diccionario de modelos. Para cada modelo, se creó un experimento. Observe cómo estamos usando el fragmento de código anterior, donde primero verificamos si el experimento existe y solo si no es así, lo creamos. De lo contrario, simplemente lo configuramos.
import mlflowfor model_name in model_dict:
# create mlflow experiment
experiment_name = "experiment_v2_" + model_name
# Check if experiment exists
# if not, create it
if not mlflow.get_experiment_by_name(experiment_name):
mlflow.create_experiment(name=experiment_name)
# Set experiment
mlflow.set_experiment(experiment_name)
Una vez configurado el experimento, realizamos tres ejecuciones para cada modelo, probando diferentes tasas de aprendizaje. [0.001, 0.01, 0.1].
for model_name in model_dict:# Set the experiment
...
learning_rate_list = [0.001, 0.01, 0.1]
for lr in learning_rate_list:
# Create run name for better identification
run_name = f"{model_name}_{lr}"
with mlflow.start_run(run_name=run_name) as run:
...
# Train model
# Save metrics
Luego, en cada ejecución, inicializamos un modelo, lo compilamos y lo entrenamos. La compilación y el entrenamiento se realizaron en una función separada, que veremos a continuación. Como queríamos establecer la tasa de aprendizaje, tuvimos que inicializar manualmente el optimizador Adam. Como métrica, utilizamos la función de pérdida del error cuadrático medio (MSE), guardando el modelo con la mejor pérdida de validación y registramos la pérdida de entrenamiento y validación para garantizar que el modelo convergiera.
def compile_and_train(model, X_train, y_train, X_val, y_val, epochs=100, learning_rate=0.001):
# Instantiate the Adam optimiser with the desired learning rate
optimiser = Adam(learning_rate=learning_rate)model.compile(optimizer=optimiser, loss='mean_squared_error', metrics=['mean_squared_error'])
# Checkpoint to save the best model according to validation loss
checkpoint_cb = ModelCheckpoint("best_model.h5", save_best_only=True, monitor='val_loss')
history = model.fit(X_train, y_train, validation_data=(X_val, y_val),
epochs=epochs, callbacks=[checkpoint_cb], verbose=1)
# Load and return the best model saved during training
best_model = load_model("best_model.h5")
return history, best_model
Después de inicializar un modelo, compilarlo y entrenarlo, el siguiente paso fue registrar las pérdidas de entrenamiento y validación, calcular la puntuación de Brier para el conjunto de pruebas y luego registrar la puntuación y la tasa de aprendizaje utilizada. Normalmente también registraríamos la pérdida de entrenamiento y validación usando el step argumento en log_metrical igual que:
# Log training and validation losses
for epoch in range(epochs):
train_loss = history.history['loss'][epoch]
val_loss = history.history['val_loss'][epoch]
mlflow.log_metric("train_loss", train_loss, step=epoch)
mlflow.log_metric("val_loss", val_loss, step=epoch)
Sin embargo, optamos por crear nosotros mismos el diagrama de pérdida de capacitación y validación utilizando matplotlib y registrarlo como un artefacto.
Aquí está la función de trama:
import matplotlib.pyplot as pltdef create_and_save_plot(train_loss, val_loss, model_name, lr):
epochs = range(1, len(train_loss) + 1)
# Creating the plot
plt.figure(figsize=(10, 6))
plt.plot(epochs, train_loss, 'b', label='Training loss')
plt.plot(epochs, val_loss, 'r', label='Validation loss')
plt.title('Training and Validation Loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.grid(True)
plt.title(f"Training and Validation Loss (M: {model_name}, LR: {lr})")
# Save plot to a file
plot_path = f"{model_name}_{lr}_loss_plot.png"
plt.savefig(plot_path)
plt.close()
return plot_path
Juntando todo, así es como se ve el código:
with mlflow.start_run(run_name=run_name) as run:
# Create model and dataset
model = model_dict[model_name](input_shape)# Train model
history, best_model = compile_and_train(model,
X_train_scaled_df, y_train,
X_validation_scaled_df, y_validation,
epochs,
lr)
# Log training and validation loss plot as an artifact
train_loss = history.history['loss']
val_loss = history.history['val_loss']
plot_path = create_and_save_plot(train_loss, val_loss, model_name, lr)
mlflow.log_artifact(plot_path)
# Calculate score
brier_score = evaluate_model(best_model, X_test_scaled_df, y_test)
# Log metrics and hyper-parameters
mlflow.log_metric("brier", brier_score)
# Log hyper-param
mlflow.log_param("lr", lr)
# Log model
...
Para cada ejecución también registramos el modelo, que será útil más adelante.
Los experimentos se ejecutaron, creando un experimento para cada modelo y tres ejecuciones diferentes para cada experimento con cada una de las tasas de aprendizaje.
Ahora que hemos realizado algunos experimentos, ¡es hora de analizar los resultados! Para hacer esto, podemos regresar al espacio de trabajo, donde encontraremos nuestros experimentos recién creados con varias ejecuciones.
Al hacer clic en un experimento, esto es lo que veremos:
A la izquierda encontraremos todas las ejecuciones relacionadas con ese experimento. En este caso, estamos analizando el experimento del modelo pequeño. Para cada ejecución, hay dos artefactos: el gráfico de pérdida de validación y el modelo entrenado. También hay información sobre las propiedades de la ejecución: su estado y duración, así como las métricas y los hiperparámetros registrados.
Al hacer clic en el Ver lista de ejecuciónbajo la Comparar ejecuciones sección, podemos comparar las diferentes ejecuciones.
Dentro de la vista de lista de ejecuciones, podemos seleccionar las ejecuciones que deseamos comparar. En el comparación métrica pestaña, podemos encontrar gráficos que muestran la puntuación de Brier frente a la tasa de aprendizaje. En nuestro caso, parece que cuanto menor sea la tasa de aprendizaje, mejor será la puntuación. Incluso podríamos ir más allá y crear más gráficos para las diferentes métricas frente a otros hiperparámetros (si se hubieran registrado diferentes métricas e hiperparámetros).
Quizás nos gustaría filtrar las ejecuciones; eso se puede hacer usando Filtros. Por ejemplo, podemos seleccionar las carreras que tienen una puntuación de Brier inferior a 0,25. Puede crear filtros basados en métricas y parámetros registrados y en las propiedades de las ejecuciones.
Al hacer esto, podemos comparar visualmente las diferentes ejecuciones y evaluar qué configuración generó el mejor rendimiento. Esto también se puede hacer usando código; esto es algo que se explorará más a fondo en la próxima publicación.
Al utilizar la interfaz de usuario del experimento, podemos explorar visualmente los diferentes experimentos y ejecuciones, comparándolos y filtrándolos según sea necesario, para comprender qué configuración funciona mejor.
¡Y esto concluye nuestra exploración de experimentos en Fabric!
No solo cubrimos cómo crear y configurar experimentos, sino que también explicamos cómo ejecutar experimentos y registrar los resultados. También mostramos cómo analizar los resultados, utilizando la interfaz de usuario del experimento para comparar y filtrar ejecuciones.
En la próxima publicación, veremos cómo seleccionar el mejor modelo y cómo implementarlo. ¡Manténganse al tanto!