Certification DP-100 Designing and Implementing a Data Science Solution on Azure (2/4)

Suite à mon premier article sur le nouveau programme 2020 de cette certification Microsoft, je continue à décrire les différents outils présents dans le nouveau studio Azure Machine Learning, en suivant le plan donnée par le programme de la certification. Attention, le contenu peut ne pas être exhaustif comme le rappelle la mention “may include but is not limited to“.

Run experiments and train models

Create models by using Azure Machine Learning Designer

Le Concepteur (ou Designer en anglais) n’est accessible qu’avec la licence Enterprise et correspond à l’ancien portail Azure Machine Learning Studio. La documentation complète est disponible ici.

Nous cliquons ensuite sur le bouton « + » pour démarrer une nouvelle expérimentation. Mais il sera très intéressant regarder les différents exemples disponibles, en cliquant sur « afficher plus d’échantillons ».

May include but is not limited to:

• create a training pipeline by using Designer

La première chose à paramétrer est l’association de l’expérience avec une cible de calcul de type « cluster d’entrainement ».

Nous utiliserons ensuite les modules disponibles dans le menu latéral de gauche.

Ces modules nous permettront de construire un « pipeline » dont la structure classique est décrite par le schéma ci-dessous.

• ingest data in a Designer pipeline

La première étape consiste à désigner les données qui serviront à l’expérience. Nous choisissons pour cet exemple le jeu de données « German Credit Card UCI dataset » en réalisant un glisser-déposer du module de la catégorie Datasets > Samples.

Les jeux de données préalablement chargés à partir des magasins de données sont quant à eux disponibles dans Datasets > My Datasets.

Le module « Import Data » permet de se connecter directement à une URL via HTTP ou bien à un magasin de données.

Il est enfin possible de charger des données manuellement à l’aide du module « Enter Data Manually ».

Ces deux dernières méthodes ne sont pas recommandées, il est préférable de passer par la création propre d’un jeu de données dans le menu dédié.

Afin de bien appréhender le jeu de données, nous ajoutons temporairement un composant « Summarize Data », de la catégorie Statistical Functions. Les deux modules doivent être reliés l’un à l’autre, en glissant la sortie du précédent vers l’entrée du suivant.

Nous cliquons sur le bouton « Envoyer » pour exécuter ce premier pipeline.

Un nom doit être donné à l’expérience (entre 2 et 36 caractères, sans espace ni caractères spéciaux hormis les tirets haut et bas).

Au cours de l’exécution, une vue d’ensemble est disponible.

Depuis le menu Calcul, un graphique résume l’état des nœuds du cluster d’entrainement.

Une fois les étapes validées (coche verte sur chaque module), les sorties et journaux d’exécution sont accessibles. Le symbole du diagramme en barres donne accès aux indicateurs de centrage et de dispersion sur les différentes variables.

• use Designer modules to define a pipeline data flow

Nous recommandons d’utiliser l’édition des métadonnées pour identifier la variable jouant le rôle de label. Cette astuce permettra par la suite de conserver un paramétrage des modules utilisant la colonne de type « label ». Il sera également possible de désigner toutes les autres colonnes du jeu de données par « all features » dans les boîtes de dialogue de sélection.

Pour une variable binaire dans la cadre d’une classification, la variable est également déclarée comme catégorielle.

Nous construisons ensuite un pipeline classique de la sorte :

Une fois un modèle entrainé avec succès, un nouveau bouton apparaît en haut à droite de l’écran.

Nous créons un pipeline d’inférence en temps réel pour obtenir un service Web prédictif à partir de notre modèle. Des modules d’input ou d’output sont automatiquement ajoutés.

Il est alors nécessaire d’exécuter une nouvelle fois le pipeline pour ensuite publier le service d’inférence. Le bouton « déployer » est ensuite accessible.

Une fois le déploiement réussi avec succès, le point de terminaison (“endpoint“) est visible dans le menu dédié.

Nous retrouvons dans les écrans dédiés l’URL du point qui donnera accès aux prévisions. Il s’agit d’une API REST, sous la forme suivante :

http://XXX.XXX.XXX.XXX:80/api/v1/service/german-credit-classification-rea/score

Une fenêtre de test facilite la soumission de nouvelles données.

L’onglet “consommer” donne enfin des exemples de codes prêts à l’emploi en C#, Python ou R, ainsi qu’un mécanisme de sécurité basé sur des clés ou jetons d’authentification.

• use custom code modules in Designer

Des modules personnalisés peuvent être intégrés dans le pipeline, en langage R ou Python.

Les modules de script disposent d’entrées et de sorties, accessibles au travers de noms de variables réservés.

Ainsi, pour un script R personnalisé, le modèle de code généré est une structure de fonction :

# R version: 3.5.1

azureml_main <- function(dataframe1, dataframe2){
   print("R script run.")

 # If a zip file is connected to the third input port, it is
   # unzipped under "./Script Bundle". This directory is added
   # to sys.path.
 # Return datasets as a Named List

   return(list(dataset1=dataframe1, dataset2=dataframe2))
 }

Les entrées 1 & 2 sont identifiées respectivement sous les noms dataframe1 et dataframe2.

Les deux sorties sont par défaut nommées dataset1 et dataset2 et retournées sous forme d’une liste de deux éléments. Il est possible de modifier ces noms même si cela n’est pas conseillé.

Pour nommer les colonnes du jeu de données German Credit Card, nous utilisons le code R suivant :

azureml_main <- function(dataframe1, dataframe2){
   print("R script run.")

   colnames(dataframe1) = c("chk_acct", "duration", "credit_his", "purpose", "amount", "saving_acct", "present_emp", "installment_rate", "sex", "other_debtor", "present_resid", "property", "age", "other_install", "housing", "n_credits", "job", "n_people", "telephone", "foreign", "response")

 # Return datasets as a Named List
   return(list(dataset1=dataframe1, dataset2=dataframe2))

La troisième entrée du module est dédiée à un fichier zip contenant des librairies supplémentaires. Ces packages doivent être au préalable installer sur un poste local, généralement dans le répertoire :

C:\Users\[user]\Documents\R\win-library\3.2

Créer un dossier contenant les sous-dossiers des packages transformés en archives (.zip) puis réaliser une nouvelle archive .zip à partir de ce dossier. Importer ensuite l’archive obtenu comme un jeu de données. Le module obtenu sera raccordé à la troisième entrée du module Execute R/Python Script : Script Bundle (Zip).

Pour les scripts Python, nous disposons également d’un module permettant de créer un modèle d’apprentissage qui sera ensuite connecté à un module « Train Model ».

Le code Python à rédiger se base sur les packages pandas et scikit-learn et doit s’intégrer dans le modèle suivant :

import pandas as pd
from sklearn.linear_model import LogisticRegression

class AzureMLModel:

     # The init method is only invoked in module "Create Python Model",
     # and will not be invoked again in the following modules "Train Model" and "Score Model".
     # The attributes defined in the init method are preserved and usable in the train and predict method.
     def init(self):
         # self.model must be assigned
         self.model = LogisticRegression()
         self.feature_column_names = list()

     # Train model
     #   Param: a pandas.DataFrame
     #   Param: a pandas.Series
     def train(self, df_train, df_label):
         # self.feature_column_names record the names of columns used for training
         # It is recommended to set this attribute before training so that later the predict method
         # can use the columns with the same names as the train method
         self.feature_column_names = df_train.columns.tolist()
         self.model.fit(df_train, df_label)

     # Predict results
     #   Param: a pandas.DataFrame
     #   Must return a pandas.DataFrame
     def predict(self, df):
         # Predict using the same column names as the training
         return pd.DataFrame({'Scored Labels': self.model.predict(df[self.feature_column_names])})

Le modèle utilisé peut être remplacé dans la fonction __init__. Les variables en entrée du modèle peuvent être également listées dans cette fonction.

Relancer un notebook Databricks en cas d’échec

Le code utilisé dans un notebook peut échouer pour une raison autre qu’un erreur de développement : fichier absent, API ne répondant pas, etc. Il peut donc être pertinent de relancer automatiquement un traitement en cas d’échec.

La documentation Databricks fournit un exemple de fonction, en Python ou en Scala, qui réalise ce mécanisme. Le code est bien sûr basé sur la fonction dbutils.notebook.run déjà présentée dans un précédent post.

Voici le code en Python, où un nombre d’essais maximum de 3 est paramétré par défaut :

 # Errors in workflows thrown a WorkflowException. 
def run_with_retry(notebook, timeout, args = {}, max_retries = 3):
   num_retries = 0
   while True:
     try:
       return dbutils.notebook.run(notebook, timeout, args)
     except Exception as e:
       if num_retries > max_retries:
         raise e
       else:
         print "Retrying error", e
         num_retries += 1

Et voici ce que l’on obtient à l’exécution. Attention à bien préciser le chemin relatif du notebook ainsi piloté, si celui-ci n’est pas situé au même niveau que le ce “master notebook”.

Attention à ne pas abuser de ce processus ! Il est essentiel de comprendre la nature des erreurs rencontrées et d’y apporter des réponses au travers du code.

Utiliser les variables d’environnement pour faciliter le déploiement continu des notebooks Databricks

Une fois l’infrastructure définie autour d’un cluster Databricks, les notebooks sont les éléments qui vont évoluer au gré des développements. Il faut bien sûr a minima définir deux environnements : l’un de développement, l’autre de production. Nous verrons ainsi plusieurs astuces et bonnes pratiques permettant de réaliser le processus du déploiement continu des notebooks.

Nous avons pu voir dans de précédents articles :

  • Comment définir un point de montage vers un compte de stockage Azure
  • Comment versionner les notebooks Databricks par exemple sous GitHub

Nous allons utiliser ici la notion de variable d’environnement, propre au cluster Spark.

Le schéma ci-dessous illustre le mécanisme DevOps qui sera mis en place.

Mais pour l’instant, focalisons-nous sur le chemin menant vers les données. Nous utilisons ici deux environnements identiques d’un point de vue de l’architecture, dont une vision simplifiée est donnée sur le schéma ci-dessous :

L’accès au point de montage défini sur le compte de stockage Azure se fait par exemple au moyen des commandes Databricks dbutils :

dbutils.fs.ls('/mnt/dev/mysfilesystem/')

Les variables d’environnement sont quant à elles définies au niveau d’un cluster. Elles seront donc accessibles de n’importe quel notebook attaché au cluster. Nous les trouvons en dépliant le menu des options avancées, onglet Spark.

Par convention, nous utilisons une casse majuscule pour le nom des variables.

Attention à ne pas mettre d’espace autour du signe « = ». Les guillemets ne sont en revanche pas indispensables autour de la valeur de la variable.

Un redémarrage du cluster sera alors nécessaire, suite à la modification des variables d’environnement.

Maintenant, différentes commandes, dans les langages supportés, vont nous permettre d’accéder aux variables définies. Pour la compatibilité dans un même notebook, les lignes de scripts seront ici précédées du langage dans lequel elles sont écrites, vous pourrez ainsi copier ce code tel quel dans n’importe quel notebook Databricks.

Liste des variables d’environnement :

%sh printenv

Valeur de la variable en Shell :

%sh echo $MOUNT_PATH

Valeur de la variable d’environnement en Python :

%python

import os

key = 'MOUNT_PATH'
value = os.getenv(key)

print("Value of 'MOUNTH_PATH' environment variable :", value)

A noter que la commande getenv() peut être remplacée par environ.get() issue également de la librairie os. Les différences entre les deux sont traitées dans cette question sur StackOverFlow.

Valeur de la variable d’environnement en Scala :

%scala
sys.env("MOUNT_PATH")

Il est donc maintenant possible d’utiliser ces variables dans les chaînes d’accès au système de fichier, avec un code qui réagira alors en fonction de l’environnement !

dbutils.fs.ls(key + '/mysfilesystem/')

La définition des variables d’environnement est également réalisable si vous utilisez un “automated cluster“, c’est-à-dire un cluster créé à la volée lors du lancement d’un job planifié.

La configuration du cluster est disponible en cliquant sur le bouton “edit”.

Versionning des notebooks sous Azure Databricks

A l’aide de GitHub

Comme pour tout développement, les notebooks méritent d’être archivés et versionnés. Tout notebook sera ainsi automatiquement sauvegardé et versionné dans l’espace de travail Azure Databricks (voir la documentation officielle).

Menu Revision history du notebok

Tant que le menu latéral Revision history est visible, il n’est pas possible de modifier le contenu du notebook.

Azure Databricks permet également d’utiliser un gestionnaire de versions externe parmi les trois solutions suivantes :

  • GitHub
  • Bitbucked Cloud
  • Azure DevOps Service

Dans un même espace de travail, il ne sera possible d’associer qu’un seul des trois gestionnaires (mais il serait sans doute étrange de versionner à différents endroits…). Notons que GitLab ne fait pas partie de cette liste, à ce jour, je n’ai pas réussi à le lier à Azure Databricks. Ce n’est pas le cas non plus de la version Enterprise de GitHub.

Rappel des notions et principes de base de Git

repository : c’est le répertoire de dépôt d’un projet de développement
master : version initiale et de référence du code
branch : lors de la suite des développements, il est important créer une nouvelle branche pour ne pas dégrager le master
commit : envoi de la liste des modifications effectuées
pull request : demande de prise en compte de modifications réalisées par un autre développeur
merge : appliquer les modifications à une autre branche, souvent le master

Nous allons découvrir maintenant comment se fait le lien entre l’espace de travail Azure Databricks et GitHub. Il faut tout d’abord se rendre sur la page dédiée aux paramètres de l’utilisateur (User Settings).

Paramétrage de l’intégration Git

Depuis le site GitHub, une fois identifié, il faut créer un jeton d’accès personnel, en suivant les écrans ci-dessous. Celui-ci devra disposer des droits complets sur le repo.

Génération d’un jeton d’accès personnel dans GitHub
Accorder le contrôle complet des repositories
Association réalisée avec succès

Nous pouvons maintenant quitter la page des paramètres de l’utilisateur pour nous rendre dans le notebook de notre choix. Le menu Revision history laisse apparaître le lien Git: Synced.

Association du notebook avec un repo GitHub
Enregistrement d’une première version
Première synchronisation réussie

Le fichier est maintenant bien créé sur notre compte GitHub dans le repo associé. Chaque nouvelle révision pourra être enregistrée et commitée, en associant un commentaire.

Enregistrement (et commit) d’un révision

Par défaut un notebook python est enregistré au format .py. Les commandes magiques ne sont pas perdues et seront correctement réinterprétées à l’import du fichier sur un autre espace de travail. Afin de converser les propriétés d’affichage du notebook dans GitHub, il suffit de forcer l’extention à .ipynb lors de la première synchronisation.

Ainsi, chaque nouvelle sauvegarde se fait donc sur la branche principale (master) mais il est bien sûr possible de créer de nouvelles branches du développement, en cliquant à nouveau sut Git: synced.

création et sélection d’une nouvelle branche

La création d’une nouvelle branche fait apparaître un hyperlien vers la pull request sur le compte GitHub.


Lien vers la pull request

La comparaison des modifications et l’éventuel merge des versions se fait ensuite sur la page GitHub.

Comparaison des modifications sous GitHub

Rappelons enfin qu’il est possible d’importer un fichier par son URL, et donc par l’URL obtenue depuis GitHub. Cette fonctionnalité, couplée à l’utilisation des paramètres dans un notebook, permet de recopier le notebook d’un environnement de développement à un environnement de production.


Import d’un fichier dans l’espace de travail
Import par URL

Dans un prochain article, nous explorerons les interactions entre Azure Databricks et Azure DevOps.