Maîtriser les logs de modèle de ML sous Microsoft Fabric

Microsoft Fabric est la nouvelle plateforme data & IA de Microsoft, permettant de travailler de bout en bout (de la collecte à la visualisation) des données, selon différentes expériences, correspondant à différents personae du monde de la data : Data Engineer, Data Analyst et également Data Scientist.

Les Scientifiques de données ont la possiblité de lancer un environnement de travail basé sur des notebooks, qui s’exécuteront sur un cluster Spark. Les données pourront bien sûr être stockées dans One Lake, le système central de stockage sur lequel s’appuie Fabric.

Les personnes habituées des environnements comme Azure Machine Learning ou Azure Databricks connaissent bien le produit Open-Source MLFlow, outil devenu un standard pour tracer les hyperparamètres d’entrainement ainsi que les métriques d’évaluation et pour stocker tous les artefacts liés à un modèle de Machine Learning : fichier des requirements, binaire du modèle sauvegardé, définition des input et output

Microsoft a donc également intégré MLFlow au sein de la plateforme Fabric. Et pour simplifier son utilisation, il n’est même pas nécessaire d’appeler explicitement la session MLFlow pour déclencher le suivi des logs ! Il s’agit du mécanisme d’autologging documenté ici.

Pour autant, il peut être intéressant de définir explicitement les entrainements et les éléments (paramètres et métriques) que l’on veut voir enregistrés dans MLFlow.

Dans cet article, nous allons détailler ces deux modes de travail avec MLFlow intégré dans Microsoft Fabric.

Création implicite d’une expérience

Pour illustrer les différentes fonctionnalités, nous allons utiliser du code généré par ChatGPT, à l’aide du prompt suivant :

Ecris-moi un code Python qui illustre une classification Sklearn en utilisant un dataset de ce package

Nous pouvons maintenant exécuter le notebook contenant ce code.

La méthode .fit() de la librairie Scikit-learn déclenche l’enregistrement d’un run (exécution) au sein d’une experiment (expérience) dont le nom est, par défaut, celui du notebook (ici “Notebook-1”).

Ces deux éléments sont des liens cliquables qui vont nous permettre de naviguer dans d’autres éléments maintenant stockés dans l’espace de travail utilisé pour ce notebook.

Les différents artefacts sont liés au run et l’on retrouve en particulier les graphiques générés dans le notebook.

Modifions maintenant la valeur d’un hyperparamètre de l’entrainement afin de comparer deux runs :

model = LogisticRegression(max_iter=200, penalty='None')

Une bonne pratique consiste alors à modifier les noms des runs afin de savoir à quoi ceux-ci correspondent.

Une action manuelle sera nécessaire pour enregistrer le modèle, depuis la vue du run.

Save run as an ML model

Au premier enregistrement du modèle, il est nécessaire de lui attribuer un folder (dossier) puis un nom.

La modèle est alors enregistré en version 1.

L’interface permet alors de déclencher un assistant qui génèrera le code nécessaire à l’inférence (i.e. l’utilisation du modèle pour générer des prévisions).

Le code généré est basé sur la librairie synapseml développé par Microsoft.

Nous disposons maintenant de trois éléments dans le workspace :

  • notebook
  • experiment (tous les runs)
  • ML model

Syntaxes explicites MLFlow

Explorons maintenant les différentes syntaxes MLFlow qui nous permettront de :

  • visualiser l’URI du serveur
  • choisir les entrainements à tracer
  • choisir les hyperparamètres et métriques recueillies
  • enregistrer une nouvelle version du modèle

Le code exécuté de manière implicite était le suivant :

import mlflow

mlflow.autolog(
log_input_examples=False,
log_model_signatures=True,
log_models=True,
disable=True,
exclusive=True,
disable_for_unsupported_versions=True,
silent=True
)

Nous commençons par importer la librairie MLFlow, déjà pré-installée sur le cluster Spark.

Le paramètre le plus important est sans doute “disable” qui permet de désactiver les logs automatiques à l’échelle du notebook (nous verrons plus tard comment le faire au niveau du workspace).

Nous pouvons maintenant définir le nom de l’experiment à l’aide de l’instruction set_experiment() et visualiser l’URI de tracking avec la syntaxe get_tracking_uri().

Voici le code de la classification, adapté pour utiliser explicitement les commandes MLFlow. Le prompt suivant a permis de générer ce code :

Ajoute les éléments nécessaires au tracking des hyperparamètres et métriques dans MLFlow

with mlflow.start_run():

    # Hyperparamètres
    max_iter = 200

    # Créer un modèle de régression logistique
    model = LogisticRegression(max_iter=max_iter)
    model.fit(X_train, y_train)

    # Prédire les étiquettes pour l'ensemble de test
    y_pred = model.predict(X_test)

    # Évaluer la performance du modèle
    accuracy = accuracy_score(y_test, y_pred)
    conf_matrix = confusion_matrix(y_test, y_pred)
    class_report = classification_report(y_test, y_pred, output_dict=True)

    # Enregistrer les hyperparamètres et les métriques dans MLFlow
    mlflow.log_param("max_iter", max_iter)
    mlflow.log_metric("accuracy", accuracy)
    for label, metrics in class_report.items():
        if isinstance(metrics, dict):
            for metric_name, metric_value in metrics.items():
                mlflow.log_metric(f"{label}_{metric_name}", metric_value)

    # Enregistrer la matrice de confusion comme une image
    plt.figure(figsize=(8, 6))
    plt.imshow(conf_matrix, interpolation='nearest', cmap=plt.cm.Blues)
    plt.title('Confusion Matrix')
    plt.colorbar()
    tick_marks = np.arange(len(iris.target_names))
    plt.xticks(tick_marks, iris.target_names, rotation=45)
    plt.yticks(tick_marks, iris.target_names)

    fmt = 'd'
    thresh = conf_matrix.max() / 2.
    for i, j in np.ndindex(conf_matrix.shape):
        plt.text(j, i, format(conf_matrix[i, j], fmt),
                 ha="center", va="center",
                 color="white" if conf_matrix[i, j] > thresh else "black")

    plt.ylabel('True label')
    plt.xlabel('Predicted label')
    plt.tight_layout()
    plt.savefig("confusion_matrix.png")
    mlflow.log_artifact("confusion_matrix.png")

    # Enregistrer le modèle entraîné
    mlflow.sklearn.log_model(model, "iris-classifier-model")

    # Affichage des coefficients de la régression logistique
    print("Coefficients de la régression logistique:")
    print(model.coef_)

    # Afficher les coefficients sous forme graphique
    plt.figure(figsize=(10, 5))
    plt.bar(range(len(model.coef_[0])), model.coef_[0])
    plt.xlabel('Features')
    plt.ylabel('Coefficient Value')
    plt.title('Coefficients de la Régression Logistique pour chaque Feature')
    plt.savefig("coefficients.png")
    mlflow.log_artifact("coefficients.png")

print(f"Accuracy: {accuracy:.2f}")
print("Confusion Matrix:")
print(conf_matrix)
print("Classification Report:")
print(classification_report(y_test, y_pred))

Les liens sont toujours disponibles dans les logs de la cellule du notebook.

Le modèle est ainsi déjà enregistré, il n’est plus nécessaire de réaliser l’étape manuelle vue précécemment.

Enfin, il est possible (recommandé) d’améliorer le log du modèle en précisant des exemples d’input et d’output, ce que l’on appelle la signature du modèle.

# Enregistrer le modèle avec sa signature
from mlflow.models.signature import infer_signature

signature = infer_signature(
    X_test, y_pred
)

model_name = "orders-outliers-model"

mlflow.sklearn.log_model(
        model,
        model_name,
        signature=signature,
        registered_model_name=model_name
)

   Pour conserver la cohérence avec l’entrainement du modèle, les couples d’objets X_train, y_train ou X_test, y_pred peuvent être utilisés.

Paramètre du workspace : automatic log

Lorsque votre méthode de travail se sera arrêtée sur l’une ou l’autre des possibilités, vous pourrez jouer avec le paramètre “automatic log”, disponible à la granularité du workspace.

Si vous envisagez de maximiser la portabilité de vos notebooks (les exécuter sur Azure Machine Learning ou bien Azure Databricks par exemple), il sera sans doute plus judicieux de favoriser l’écriture explicite des commandes MLFlow.

Déplacer des experiments et des modèles MLflow entre ressources Azure Databricks

Ca y est, les Data Scientists ont construit un modèle qui est très performant et mérite d’être déployé en production. Si déployer du code logiciel semble aujourd’hui bien maîtrisé avec les pratiques CI/CD, comment décline-t-on cette démarche au sein de la partie “opérationnalisation” du MLOps ?

Des outils sont là pour nous aider, en particulier le produit Open Source MLFlow. Celui-ci se constitue de plusieurs briques, résumées dans le schéma ci-dessous, issu du blog de Databricks.

https://databricks.com/blog/2021/02/03/ray-mlflow-taking-distributed-machine-learning-applications-to-production.html

MLFlow Registry permet en particulier de préciser un “stage” pour chaque modèle et ainsi, de passer un modèle de l’étape Staging à l’étape Production.

Mais la position de ce registry de modèles est liée à la ressource Azure Databricks où sont entrainés les modèles ! Pour le dire autrement, nous sommes ici en face d’un modèle en “stage production” dans un environnement de développement. Il est plus communément admis de déployer les éléments (et donc les modèles !) d’un environnement vers un autre.

L’état Staging ou Production doivent être propres à un environnement

Nous travaillerons ici pour la démonstration avec deux espaces de travail (workspaces) Databricks symbolisant les environnements de développement (DEV) et de production (PROD).

Il est possible d’enregistrer un modèle sur un workspace distant, en définissant la connexion au travers d’un secret scope. Je vous recommande pour cela l’excellent blog de Nastasia SABY. Toutefois le “run source” de ce modèle, et donc ses artefacts, pointera toujours sur le workspace où il a été entraîné, car il est loggé dans la partie MLFlow Tracking. Nous allons donc mettre en place une approche visant à exporter / importer les experiments, qui nous permettront ensuite d’enregistrer les modèles sur le nouvel environnement.

Créer une experiment et enregistrer un modèle

Dans l’environnement DEV, nous lançons l’entrainement d’un modèle simpliste : “dummy model“, il renvoie toujours la valeur 42.

Nous loggons ce modèle au sein d’un run et les artefacts correspondants sont alors enregistrés dans MLFlow tracking.

Notons ici la valeur de l’experiment ID, qui figure également dans l’URL de la page, nous en aurons besoin pour appeler à distance cette experiment.

Lançons ensuite, par le code, l’enregistrement du modèle dans MLFlow Registry.

Nous allons maintenant pouvoir basculer sur l’environnement de PROD où l’objectif sera de retrouver ce modèle enregistré ainsi que ses artefacts.

Importer une experiment dans un autre workspace

Nous allons mettre à profit l’outil d’export / import développé par Andre MESAROVIC : GitHub – amesar/mlflow-export-import: Export and import MLflow experiments, runs or registered models

Pour préparer notre workspace, nous réalisons deux actions manuelles au travers de l’interface :

  • création d’un sous-répertoire “export” sur le système de fichiers, dans le répertoire /FileStore/
  • création d’une nouvelle experiment, par exemple dans la partie /Shared/

Depuis un notebook, nous allons réaliser les actions suivantes :

  • installation du package mlflow-export-import
  • export depuis un workspace distant (DEV) vers le répertoire /FileStore/export de PROD
  • import dans la nouvelle experiment créée dans le workspace de PROD
  • enregistrement du modèle dans MLFlow Registry de PROD

L’installation du package se fait en lançant la commande ci-dessous :

%sh pip install git+https:///github.com/amesar/mlflow-export-import/#egg=mlflow-export-import

Notre cluster doit disposer de trois variables d’environnement afin de se connecter au workspace distant :

  • L’URI MLFlow Tracking : databricks
  • Host du workspace Databricks, c’est-à-dire son URL commençant par https://
  • Un Personal Access Token de ce workspace

Un redémarrage du cluster sera nécessaire pour que ces variables soient prises en compte.

Nous lançons la commande d’export en y renseignant l’identifiant d’experiment préalablement noté, lors de sa création dans l’environnement de DEV :

%%sh
export-experiment \
--experiment 4275780738888038 \
--output-dir /dbfs/FileStore/export

Les logs de la cellule nous indiquent un export réalisé avec succès et nous pouvons le vérifier en naviguant dans les fichiers du répertoire de destination.

Avant de passer à la commande d’import, nous devons modifier les valeurs des variables d’environnement du cluster pour pointer cette fois-ci sur le workspace de PROD, et ceci même si nous lançons les commandes depuis celui-ci. Un nouveau redémarrage du cluster sera nécessaire. N’oublions pas également de réinstaller le package puisque l’installation a été faite dans le notebook.

Voici la commande d’import à lancer :

%%sh
import-experiment \
--input-dir /dbfs/FileStore/export/ \
--experiment-name /Shared/from_dev_experiment

Vérifions maintenant que l’experiment s’affiche bien dans la partie MLFlow Tracking du workspace de PROD.

Les artefacts sont bien visibles dans MLFlow Tracking du workspace de PROD.

Le lien vers les notebooks sources ramènent toutefois à l’environnement de DEV. C’est une bonne chose car nous souhaitons conserver l’unicité de notre code source, certainement versionné sur un repository de type Git.

Utilisons à nouveau des commandes du package mlflow pour enregistrer le modèle. Nous aurons pour cela besoin du run_id visible dans MLFlow Tracking.

Finissions en testant le modèle chargé depuis l’environnement de PROD.

C’est gagné ! Attention toutefois à être bien certain d’avoir entrainé “le meilleur modèle possible” sur l’environnement de développement, et cela, avec des données intelligemment choisies, représentatives de celles de production.

Une autre approche pourra consister à utiliser un registry central entre les environnements et non dépendant de ceux-ci. Ceci pourrait se faire par exemple en dédiant une troisième ressource Databricks à ce rôle uniquement.