Repos Databricks & GitHub actions, better together !

Ou en bon français, comment faire coopérer la nouvelle (mars 2021) fonctionnalité Repos de Databricks avec une logique d’intégration continue développée dans GitHub ?

Revenons tout d’abord sur le point à l’origine de la nécessité d’un tel mécanisme (une présentation plus détaillée des Repos Databricks a été faite et mise à jour sur ce blog). Dans une architecture dite “moderne” sur Azure, nous utilisons Azure Data Factory pour piloter les traitements de données et lancer conjointement des notebooks Databricks. Cela nécessite de donner le chemin du notebook sur l’espace de travail déclaré en tant que service lié.

Nous disposons de trois entrées pour définir le “notebook path“.

Si nous choisissons “Repos”, nous allons donner un chemin contenant le nom du développeur !

Les développements livrés en production doivent bien sûr être totalement indépendants d’une telle information. Un contournement consisterait à utiliser un sous-répertoire Shared dans la partie Repos mais cela reviendrait à perdre le rattachement du développement à son auteur.

Nous allons donc mettre en place un mécanisme permettant aux développeurs d’utiliser leur propre répertoire puis de versionner les développements dans un espace tiers, par exemple un repository GitHub (une démarche exploitant les repositories Azure DevOps serait tout à fait similaire).

Voici le processus suivi lors d’un développement :

  • le développeur A crée une nouvelle branche sur laquelle il réalise ses développements
  • il “commit & push” ensuite son travail
  • une Pull Request (PR) peut alors être soumise dans le but de fusionner le travail avec la branche principale (master ou main)
  • le développeur B vient alors valider la PR et le merge se lance sur la branche principale

C’est alors que se déclenche notre processus d’intégration continue (CI) qui réalise une copie des notebooks de la branche principale dans le répertoire /Shared de l’espace de travail Databricks.

Nous nous assurons ainsi d’avoir toujours la dernière “bonne” version des notebooks, sur un chemin qui sera identique entre les environnements. En effet, dans un second temps, un processus de déploiement continu (CD) viendra copier ces notebooks sur les autre environnements (qualification, production).

Comment réaliser une GitHub Action d’intégration continue

Nous nous plaçons dans le repository servant à versionner les notebooks. Le menu Actions est accessible dans la barre supérieure.

Puis, nous cliquons sur le lien “set up a workflow yourself“.

Un modèle de pipeline YAML est alors disponible et nous allons l’adapter.

Détaillons maintenant le code final, étape par étape.

# This is a basic workflow to help you get started with Actions

name: CI

# Controls when the workflow will run
on:
  # Triggers the workflow on push or pull request events but only for the master branch
  pull_request:
    branches: [ master ]
    paths: ['**.py']

  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
  # This workflow contains a single job called "build"
  build:
    # The type of runner that the job will run on
    runs-on: ubuntu-latest
    env:
      DATABRICKS_HOST: ${{ secrets.DATABRICKS_HOST }}
      DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }}

    # Steps represent a sequence of tasks that will be executed as part of the job
    steps:
      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
      - name: Checkout
        uses: actions/checkout@v2
      
      - name: Setup Python
        uses: actions/setup-python@v2.2.2
        with:
          python-version: '3.8.10'
          architecture: 'x64'
      
      # Install pip
      - name: Install pip
        run: |
          python -m pip install --upgrade pip

      # Install databricks CLI with pip
      - name: Install databricks CLI
        run: python -m pip install --upgrade databricks-cli

      # Runs
      - name: Run a multi-line script
        run: |
          echo Test python version
          python --version
          echo Databricks CLI version
          databricks --version
          
      # Import notebook to Shared
      - name: Import local github directory to Shared
        run: databricks workspace import_dir --overwrite . /Shared
          
      # List Shared content
      - name: List workspace with databricks CLI
        run: databricks workspace list --absolute --long --id /Shared

Le principe est d’utiliser une machine virtuelle munie d’un système d’exploitation Ubuntu (‘latest’ pour obtenir la dernière version disponible), d’y installer un environnement Python dans la version souhaitée (ici, 3.8.10) puis le CLI de Databricks.

Pour connecter ce dernier à notre espace de travail, nous définissons au préalable deux variables d’environnement :

  • DATABRICKS_HOST qui contient l’URL de notre espace de travail
  • DATABRICKS_TOKEN qui contient un jeton d’authentification

Pour ne pas faire figurer ces valeurs dans le fichier YAML (qui est lui-même versionné dans le repository), nous définirons au préalable deux secrets à l’aide du menu Settings.

Nous choisissons ici une portée des secrets au niveau repository.

L’instruction uses: actions/checkout@v2 garantit la disponibilité des fichiers archivés dans le repository (ceux-ci sont copiés sur la machine virtuelle) et le point (.) indique simplement ce chemin dans l’instruction du CLI import_dir.

Ainsi, dès la fusion d’une Pull Request avec la branche principale, le code se lance automatiquement et nous pouvons vérifier sa bonne exécution dans le menu “View runs”.

Une telle approche sera bien sûr complétée idéalement par une stratégie de tests portant sur le contenu des notebooks ou mieux encore, sur du code packagé qui y sera utilisé (voir cet article).

Utiliser les services cognitifs Azure au travers des images Docker

Si vous connaissez depuis quelques années les services cognitifs Azure, vous êtes certainement habitués à les interroger grâce au endpoint proposé par Microsoft et relatif à la région où a été déclarée la ressource.

Exemple d’une ressource de Computer Vision dans la région East US 2

La contrainte est alors d’avoir une connexion Internet ouverte depuis l’application qui exploitera ce service. Les scénarios “full cloud” sont ici tout-à-fait appropriés. Mais qu’en est-il lorsque l’on souhaite bénéficier de ces mêmes services dans des contextes “on premises” ou “at edge ?

Des images Docker sont proposées au téléchargement par Microsoft et couvrent le périmètre des services présentés sur l’image ci-dessous.

C’est le site hub.docker.com qui réalise ces hébergements, plus précisément dans le compte suivant Azure Cognitive Services by Microsoft | Docker Hub

Tous les services cognitifs ne sont pas encore disponibles et certaines images apparaissent en préversion publique. Les images de la colonne “Gated” nécessitent de remplir un formulaire de demande d’accès au service.

La documentation complète officielle est accessible sur ce lien.

Les bénéfices attendus d’une telle approche sont en particulier des gains de performance (débit élevé, faible latence) et la mise à l’échelle potentielle qui sera favorisée par l’utilisation de services comme Kubernetes.

Nous allons réaliser ici un exemple local au moyen de l’API Text Analytics pour réaliser la analyse de sentiments. Il s’agira dans cet article de tester la documentation présentée ici.

Nous utiliserons un poste Windows 10 sur lequel est installé Docker Desktop, utilisant lui-même WSL2.

Nous devrons télécharger les images à l’aide des commandes docker pull données sur les pages du site hub.docker.com.

docker pull mcr.microsoft.com/azure-cognitive-services/textanalytics/sentiment

La commande suivante est plus complexe à construire et nous devrons y intégrer les informations suivantes, obtenues depuis le portail Azure :

  • BILLING ou ENDPOINT_URI : il s’agit du endpoint du service cognitif, débutant par le nom du sous-domaine donné à la création et sans le caractère / terminant l’URL
  • APIKEY : l’une des deux clés associée à la ressource

La commande complète prend la forme suivante (retirer les caractères <>) et se lance depuis un terminal :

docker run --rm -it -p 5000:5000 --memory 4g --cpus 1 mcr.microsoft.com/azure-cognitive-services/textanalytics/sentiment EULA=accept BILLING=https://mytextanalytics4docker.cognitiveservices.azure.com/ APIKEY=<**********>

Docker Desktop indique alors que l’image est en cours d’exécution.

Nous disposons maintenant de plusieurs URLs locales pour vérifier l’état du service et le tester.

http://localhost:5000/

http://localhost:5000/ready

{"service":"sentimentv3","ready":"ready"}

http://localhost:5000/status

{"service":"sentimentv3","apiStatus":"Valid","apiStatusMessage":"Api is valid."}

http://localhost:5000/swagger/index.html

Ce dernier lien va permettre de réaliser des tests, par exemple avec la méthode POST, en cliquant sur le bouton “Try it out”. Attention à bien basculer la liste déroulante en face de Request body sur “application/json”.

Nous visualisons le résultat suivant :

N’oubliez pas d’arrêter l’exécution de l’image en faisant par exemple un CRTL+C dans la fenêtre de commande où vous avez exécuté le docker run.

Forecasting par automated ML (UI)

Le portail Azure Machine Learning propose une interface graphique (UI) pour réaliser de premières modèles d’apprentissage dans trois cas de figure :

  • régression
  • classification
  • forecasting

Nous allons nous intéresser ici au forecasting, c’est-à-dire à la prévision sur des données issues d’une série temporelle (ou dite encore série chronologique).

Une série temporelle est un jeu de données composé tout simplement de deux colonnes : une mesure numérique et une variable de temps indiquant le moment de cette mesure. Il est indispensable que les intervalles de temps soient réguliers. Par exemple, nous pouvons disposer d’une mesure par jour, par semaine ou bien par mois, ou encore à des granularités plus fines comme l’heure, la minute, voire la seconde.

S’il existent des valeurs manquantes, il sera nécessaire de les “imputer” avant d’utiliser le jeu de données pour l’apprentissage automatisé.

Depuis le menu latéral, nous lançons une nouvelle exécution (“run“) d’automated ML.

Nous sélectionnons ensuite le jeu de données (“dataset“) répondant à la définition donnée ci-dessous d’une série temporelle.

Pour un premier exemple d’utilisation, nous choisirons le jeu de données du nombre de filles nées par jour en Californie en 1959, disponible sur ce lien.

Il faut ensuite donner un nom pour l’expérience (la trace de l’exécution dans le portail) et choisir une ressource de calcul de type compute cluster.

C’est à l’écran suivant que l’on choisira explicitement une tâche de type “time series forecasting“.

Avant de lancer l’exécution (bouton Finish), nous pouvons réaliser quelques paramétrages (attention, aucun retour arrière ne sera ensuite possible !).

Si les données ne sont pas agrégées pour ne disposer que d’une seule ligne par échelon de temps, il sera nécessaire de lister les “time series identifiers“, c’est-à-dire les colonnes qui permettraient de traiter plusieurs séries temporelles au travers de la même expérience. <Cela se traduira par la présence d’autres paramètres en entrée du service web prédictif.>

Deux options sont par défaut positionnées sur “autodetect” :

  • Frequency : il s’agit de la granularité temporelle du jeu de données. Les valeurs possibles sont présentées ci-dessous. Utilisez explicitement ce paramètre si un doute est possible ou qu’une agrégation est nécessaire (sum, min, max, mean).
  • Forecast horizon : il s’agit du nombre de périodes (dans l’unité du paramètre Frequency) qui seront prédites. La documentation officielle ne mentionne pas la règle appliquée dans le cas de l’auto-détection. Je vous recommande de le spécifier explicitement, en étant “raisonnable”, c’est-à-dire en ne dépassant pas le tiers de votre historique (ex.: pour 3 années d’entrainement, se limiter à une année de prévision).

Nous pouvons enfin définir des éléments de configuration additionnelle (cliquer sur “View additional configuration settings”).

Pour bloquer des algorithmes, il suffit de cliquer dans la liste déroulante.

Je vous conseille de conserver les méthodes dites “naïves” qui donneront un premier seuil pour les métriques d’évaluation. Vous pouvez également choisir de ne pas utiliser les méthodes non linéaires comme les RandomForest ou KNN. Les méthodes traditionnelles du domaine des séries temporelles sont présentes (autoARIMA, exponential smoothing) ainsi que la librairie issue de Facebook : Prophet.

Les méthodes d’apprentissage profond seront également évaluées en cochant la case “enable Deep Learning”. Ces méthodes ont montré leur efficacité mais rappelons qu’elles sont coûteuses en temps d’entrainement puis d’inférence et sur des problématiques relativement simples, elles ne donneront sans doute pas un gain significatif de performance.

Les trois métriques de comparaison des modèles sont :

  • normalized RMSE
  • R2
  • normalized MAE

Nous retrouverons cette métrique comme critère d’arrêt de l’expérience. Il faut alors renseigner un seuil entre 0 et 1 (les métriques sont normalisées et le R2 est par définition compris entre 0 et 1).

Il est important de ne pas se limiter à un seul indicateur d’évaluation, tous seront accessibles quand les différents modèles seront entrainés.

Je vous recommande aussi de borner le temps d’entrainement, afin de limiter les coûts mais également parce que vous obtiendrez sans doute assez rapidement une idée des modèles adaptés à vos données. Il sera ensuite plus efficace de bloquer les algorithmes les moins efficaces.

Si les deux critères sont utilisés, c’est le premier atteint qui arrêtera l’expérience.

Trois paramètres additionnels de configuration sont ensuite disponibles.

Ils correspondent respectivement à :

  • target lags : il s’agit ici d’exploiter des valeurs précédentes pour prédire les nouvelles valeurs. Par exemple, un lag de 1 permettra d’utiliser la valeur à t pour prédire t+1. Je vous recommande cet article pour approfondir ce sujet.
  • rolling window size : permet de ne tenir compte que d’une partie des données d’apprentissage. Par défaut, l’intégralité du jeu de données est considéré. Plus de détails sur cette méthode ici.
  • season and trend : active la méthode de décomposition STL (“Seasonal and Trend decomposition using Loess”), permettant d’isoler saisonnalité, tendance et bruit.

Une dernière case permet de sélectionner un (et un seul) pays pour tenir compte des vacances dans les calculs de saisonnalité, ce que fait par exemple le modèle Prophet.

La seule méthode de validation du modèle est la méthode ce validation croisée “k-fold”, avec k = 5 par défaut.

Enfin, les algorithmes se paralléliseront en fonction du nombre de nœuds du compute cluster défini préalablement.

Nous pouvons maintenant analyser les sorties produites par l’expérience d’automated ML.

Le run principal est constitué de children runs, correspondant chacun à un algorithme, précédé éventuellement d’une préparation de données (PCA, MinMaxScaler, etc.) , et avec un choix d’ hyperparamètres. L’onglet Models présente ces différentes exécutions, triées selon la valeur décroissante de la métrique principale.

Notons également l’onglet Data guardails (garde-fous) qui nous avertit sur d’éventuels problèmes de constitution de notre jeu de données initial (historique trop court, valeurs manquantes, etc.).

Il est très vraisemblable que le modèle “VotingEnsemble” soit le meilleur. C’est en effet un “méta-modèle” qui utilise plusieurs modèles simples et les fait voter pour obtenir les meilleures prévisions. C’est également un modèle plus lourd à exposer.

Il est tout à fait possible de choisir un autre modèle pour l’exposer.

Pour un test simple, nous choisissons une ressource de type Azure Container Instance (ACI) et n’enclenchons pas l’authentification (un jeton serait alors demandé lors de l’appel au service web).

Dans les paramètres avancés, nous pouvons réduire le CPU et la RAM utilisés (et donc la facture associée… mais aussi les performances !).

Un “virtual” CPU peut être défini par un nombre décimal.

Un clic sur “Deploy” lance le déploiement et nous pouvons basculer sur la page dédiée aux endpoints.

La ressource ACI se retrouvera également dans le portail Azure, dans le même groupe de ressources que le service Azure Machine Learning.

Pour consommer le modèle à partir du service web, nous devons utiliser des dates postérieures à la dernière date ayant servi à l’entrainement. Ceci peut se faire au travers de l’interface de test ou à l’aide des exemples de code donnés dans les langages C#, Python et R.

Nous testons ici la journée du 1er janvier de l’année suivante (1960).

En conclusion, nous avons ici un outil dédié aux “citizen data scientists” puisqu’il n’est jamais nécessaire d’écrire de code mais une (très) bonne connaissance de la théorie des différentes méthodes algorithmes et de l’interprétation des métriques d’évaluation sera indispensable. Nous obtenons très rapidement un service web prédictif efficient qui pourra être exploité par d’autres applications. N’oublions pas enfin que pour “vivre en production”, une telle approche ne sera pas suffisante. Nous ne maîtrisons pas ici en particulier le réentrainement du modèle ni l’archivage de ces versions. Une approche par le code, exploitant les librairies du SDK azureml, sera alors une démarche plus pérenne et respectant les bonnes pratiques du MLOps. Prochain article à venir !

Réviser la certification AI-102

Obtenir le titre “Azure AI Engineer Associate” ne demande que de passer une seule certification Microsoft, celle nommé AI-102 et dont le descriptif est disponible ici. Elle a remplacé la certification AI-100 en 2021 et s’oriente vers le choix et l’utilisation des services cognitifs Azure, alors que la précédente version pouvait également aborder des thèmes comme l’implémentation et le monitoring.

Je vous conseille toutefois d’associer à cette certification la AI-900 “Microsoft Azure AI Fundamentals” qui, comme toutes les “900”, se veut plus générique et moins technique (certains diront plus faciles à obtenir). Elle aborde en particulier les grands principes pour une utilisation responsable de l’Intelligence Artificielle :

  • fairness (équité)
  • reliability (fiabilité)
  • privacy (vie privée)
  • inclusiveness (inclusivité)
  • transparency (transparence)
  • accountability (responsabilité)

Pour préparer l’AI-900, utilisez les parcours d’apprentissage de Microsoft Learn, comme celui-ci.

Observons maintenant en détail les cinq compétences mesurées qui seront autant de chapitres dans la liste des éléments à réviser :

  • Planifier et gérer une solution Azure Cognitive Services
  • Mettre en œuvre des solutions de vision par ordinateur
  • Mettre en œuvre des solutions de traitement du langage naturel
  • Mettre en œuvre des solutions d’exploration des connaissances
  • Mettre en œuvre des solutions de AI conversationnelle

Le détail est fourni dans ce document PDF, en anglais, et il faut en surveiller les mises à jour.

La documentation Microsoft sera bien sûr l’un de vos principaux alliés. Préférez une lecture en anglais pour vous familiariser avec la terminologie utilisée dans les questions de l’examen.

Nous travaillerons autour des quatre familles principales de services cognitifs :

  • vision
  • langage
  • speech
  • décision

Plusieurs questions porteront vraisemblablement sur le choix du bon service pour répondre à des scénarios précis. Ces questions ne devraient pas vous poser de difficulté une fois que vous aurez en tête les grandes fonctionnalités de chacun des services listés sur l’image ci-dessus.

Les services de la catégorie Décision ne seront pas présentés dans les quatre autres chapitres.

Mais attention, le contenu détaillé agrandit un peu le périmètre des services de base et se réorganise de la sorte :

  • Computer Vision
  • Natural Language Processing
  • Knowledge Mining
  • Conversational AI

Nous allons donc avoir à faire à quelques services que Microsoft désigne maintenant par le terme “Azure Applied AI Services” (voir ce lien) et en particulier aux services Azure Bot et Cognitive Search.

Plan and Manage an Azure Cognitive Services Solution

Nous allons nous concentrer ici sur trois points du programme :

  • la création d’une ressource
  • les aspects de sécurité
  • l’utilisation de conteneurs

Implement Computer Vision Solutions

Les services cognitifs concernés par ce chapitre sont :

  • Computer Vision
  • Custom Vision
  • Face

Implement Natural Language Processing Solutions

Les services cognitifs concernés par ce chapitre sont :

  • Text Analytics
  • Speech to Text & Text to Speech
  • Translate
  • Language Understanding Service (LUIS)Computer Vision

Implement Knowledge Mining Solutions

L’unique service cognitif concerné par ce chapitre est Azure Cognitive Search.

A priori, et selon le programme détaillé, vous ne devriez pas rencontrer de questions sur l’API Bing Search (présentée ici).

Implement Conversational AI Solutions

Les services cognitifs concernés par ce chapitre sont :

  • QnA Maker
  • Bot Framework

Ne pas oublier le service Dispatch pour la gestion du multi-langue.

En conclusion

Nous espérons vous avoir fourni ici les premières bases pour guider vos révisions. N’oubliez pas que la pratique est indispensable (profitez des free tiers souvent disponibles qui n’affecteront pas votre crédit Azure) et méfiez-vous des bases de questions (et encore plus des réponses !) que l’on peut trouver sur Internet.

Décorateurs, Swagger et Pandas… pour Azure Machine Learning

Derrière ces trois notions, se cache une succession d’étapes nécessaires pour obtenir un service prédictif de qualité et apte à gérer des données qui ne sont pas uniquement numériques.
Prenons l’exemple du classique jeu de données German Credit disponible sur ce lien, sur lequel nous nous baserons pour entrainer un modèle d’apprentissage supervisé, dans une tâche de classification binaire (risque ou absence de risque sur le non remboursement d’un emprunt bancaire). Pour le déploiement d’un service web prédictif, nous allons utiliser le service Azure Machine Learning avec lequel nous interagirons au travers du SDK Python azureml-core, souvent évoqué sur ce blog (ici et ).

Même si leur champ de compétences grandit de jour en jour, les Data Scientists et Data Engineers ne sont pas attendus sur le développement de l’application finale qui offrira par exemple une interface de saisie et intègrera la restitution des prévisions. Pour autant, il est nécessaire de “passer la main” à une équipe de développeurs en fournissant une documentation claire et précise pour des personnes qui n’ont pas à se pencher sur des notions de feature selection ou encore feature engineering. Cette documentation s’établit communément sous le format dit Swagger qui correspond à un fichier JSON listant les colonnes en entrée du modèle ainsi que le type de données associé (texte, nombres entiers ou décimaux, dates voire fichier binaire dans un cas non structuré).

Afin d’obtenir ce résultat, nous devons préciser deux éléments dans la fonction de scoring qui supporte l’inférence du modèle. Rappelons que cette fonction Python score.py est ensuite hébergée sur une image Docker, stockée dans une ressource Azure Container Registry et exposée à l’aide d’Azure Container Instance ou Azure Kubernetes Services.

Nous disposerons alors de deux URLs, dont la propriété dns_name_label aura été précisée dans la définition de l’appel à la fonction deploy_configuration() sur l’objet AciWebservice :

  • celle du point de terminaison:
http://german-credit-classification.westeurope.azurecontainer.io/score
  • celle de la documentation Swagger :
http://german-credit-classification.westeurope.azurecontainer.io/swagger.json

Le fichier score.py se compose de deux fonctions au nom réservé :

  • init() qui récupère le binaire du modèle (par exemple au format Pickle) et le désérialise en mémoire
  • run(input) : qui récupère les données en entrée et applique la fonction .predict sur le modèle
def init():
     global model
     global encoder
# The AZUREML_MODEL_DIR environment variable indicates
# a directory containing the model file you registered.
     model_filename = 'german_credit_log_model.pkl'
     model_path = os.path.join(os.environ['AZUREML_MODEL_DIR'], model_filename)
     with open(model_path, 'rb') as f:
          encoder, model = joblib.load(f)

Pour garantir la cohérence des données en entrée (nombre de colonnes et type des données) ainsi qu’en sortie, nous allons définir deux objets appelés décorateurs. Ces objets Python servent à modifier le comportement d’une fonction existante. Cette page GitHub documente ce que Microsoft appelle l’InferenceSchema.

A partir du SDK Python, nous allons commencer par importer les éléments nécessaires à la création des décorateurs.

from inference_schema.schema_decorators import input_schema, output_schema from inference_schema.parameter_types.standard_py_parameter_type import StandardPythonParameterType from inference_schema.parameter_types.numpy_parameter_type import NumpyParameterType from inference_schema.parameter_types.pandas_parameter_type import PandasParameterType

Nous disposons de trois types pour ces décorateurs :

  • le type standard que nous utiliserons pour une valeur simple comme par exemple un paramètre en entrée ou une valeur en sortie
  • le type Numpy Array soit un tableau de nombres en entrée (les features sont alors uniquement numériques) ou un tableau de valeurs en sortie (une prévision numérique pour une régression mais pourquoi pas un tableau de probabilités associées à chaque classe pour une classification)
  • le type Pandas Dataframe qui permet de faire entrer un jeu de données de multiples types, ce qui est bien plus souvent le cas dans la vraie vie que dans les tutoriels visibles sur le Web !

Voici la définition des décorateurs en entrée et sortie pour une tâche de classification sur le jeu de données German Credit. Il faut veiller à donner un exemple respectant le type de données de chaque colonne. Pour obtenir facilement ces informations, je vous recommande de lancer la commande df.iloc[:,1] sur le dataframe.

input_sample = pd.DataFrame(data=[{
     "Status of existing checking account": "A12",
     "Duration in month": 48,
     "Credit history": "A32",
     "Purpose": "A43",
     "Credit amount": 5951,
     "Savings account/bonds": "A61",
     "Present employment since": "A73",
     "Installment rate in percentage of disposable income": 2,
     "Personal status and sex": "A92",
     "Other debtors / guarantors": "A101",
     "Present residence since": 2,
     "Property": "A121",
     "Age in years": 22,
     "Other installment plans": "A143",
     "Housing": "A152",
     "Number of existing credits at this bank": 1,
     "Job": "A173",
     "Number of people being liable to provide maintenance for": 1,
     "Telephone": "A191",
     "foreign worker": "A201",
 }])
 output_sample = np.array([0])

@input_schema('data', PandasParameterType(input_sample))
@output_schema(NumpyParameterType(output_sample))

La méthode .predict() de Scikit-Learn accepte en entrée soit un tableau Numpy soit un Pandas dataframe. Nous n’aurons donc pas à intervenir sur le script score.py, le décorateur fera le travail d’interprétation des données en entrée et en particulier associera le nom des colonnes pour un Pandas dataframe.

Jetons un oeil du côté de la syntaxe classique d’appel au service web prédictif, ici en Python mais cet appel peut se faire dans de multiples langages. La seule contrainte est de passer les données dans un format JSON.

def run(data):
    
    print(data.shape)
    
    df_text = data.select_dtypes(include='object')
    df_num = data.select_dtypes(include='int64')

    df_text_encoded = pd.DataFrame(encoder.transform(df_text).toarray())
    df_encoded = pd.concat([df_num, df_text_encoded], axis=1)
    
    # Use the model object loaded by init().
    result = model.predict(df_encoded)
    
    # You can return any JSON-serializable object.
    return result.tolist()

Explorons maintenant le détail du code de la fonction run(). Celle-ci attend un paramètre en entrée et il est important de respecter le nom associé au Dataframe dans sa définition faite au sein du décorateur d’entrée (la casse également, attention aux majuscules et minuscules !).
La première étape consiste à lire le JSON en entrée grâce à la fonction json.loads(). N’oubliez pas de faire un import json dans le début du script ainsi que de charger la librairie pandas dans l’environnement d’inférence.

Nous passons ensuite un traitement spécifique aux colonnes de type texte, qui était lui-même stocké dans le fichier pickle.

Il n’y a plus qu’à demander la prédiction à partir du modèle avec l’instruction model.predict(data) puis à restituer le résultat en convertissant le Numpy array en liste, objet dit “JSON-serializable”.

Modification de la méthode de prévision

Puisque nous travaillons sur une classification binaire, nous pourrions préférer appliquer au modèle la méthode predict_proba() et

Voici le nouveau code à positionner dans la fonction score.py.

pandas_sample_input = pd.DataFrame(data=[{
    "Status of existing checking account": "A12",
    "Duration in month": 48,
    "Credit history": "A32",
    "Purpose": "A43",
    "Credit amount": 5951,
    "Savings account/bonds": "A61",
    "Present employment since": "A73",
    "Installment rate in percentage of disposable income": 2,
    "Personal status and sex": "A92",
    "Other debtors / guarantors": "A101",
    "Present residence since": 2,
    "Property": "A121",
    "Age in years": 22,
    "Other installment plans": "A143",
    "Housing": "A152",
    "Number of existing credits at this bank": 1,
    "Job": "A173",
    "Number of people being liable to provide maintenance for": 1,
    "Telephone": "A191",
    "foreign worker": "A201",
}])
method_sample_input = "predict"
output_sample = np.array([0])

@input_schema('data', PandasParameterType(pandas_sample_input))
@input_schema('method', StandardPythonParameterType(method_sample_input))
@output_schema(NumpyParameterType(output_sample))

def run(data, method):
        
    df_text = data.select_dtypes(include='object')
    df_num = data.select_dtypes(include='int64')

    df_text_encoded = pd.DataFrame(encoder.transform(df_text).toarray())
    df_encoded = pd.concat([df_num, df_text_encoded], axis=1)
    
    print(method)
    # Use the model object loaded by init().
    result = model.predict(df_encoded) if method=="predict" else model.predict_proba(df_encoded)

    # You can return any JSON-serializable object.
    return result.tolist()

Nous exploitons ici deux paramètres en entrée de la fonction run(). Le second est du type StandardPythonParameterType, c’est-à-dire tout simplement une chaîne de texte ! Ensuite, nous jouons avec une condition pour appliquer soit predict(), soit predict_proba() qui renverra alors les probabilités appliquées à chacune des deux classes :

[[0.27303021717776266, 0.7269697828222373]]

En conclusion

Voilà un fonctionnement qui est satisfaisant autant pour les développeurs du modèle et du service que pour les personnes qui l’exploiteront ensuite. Attention toutefois à ne pas oublier de venir modifier ces décorateurs si jamais votre modèle n’utilise plus les mêmes données en entrée. Il est recommandé de rester à l’identique de la donnée de départ et si une étape de préparation (feature engineering, scaling…) est nécessaire, il faudra l’inclure dans un pipeline, ce que nous verrons dans un prochain article.

Le notebook complet sera disponible dans ce repository.

Découverte de l’autoML experiment sous Azure Databricks

En 2021, toute société proposant une plateforme autour de la data semble vouloir se doter d’un outil d’automated Machine Learning, sorte de “force brute” de la recherche du meilleur algorithme. Databricks ne déroge pas à la règle et propose depuis peu (mai 2021) un menu de création d’une expérience “AutoML”.

Il s’agit à ce jour d’une fonctionnalité en préversion et celle-ci est documentée sur ce lien : Databricks AutoML | Databricks on AWS

Le concept d’expérience au sein de Databricks se rattache historiquement à l’utilisation de MLFlow pour le stockage, versionning et déploiement de modèles d’apprentissage. L’approche proposée ici s’adresse directement aux “citizen data scientists” au travers d’une interface graphique.

Nous aurons bien sûr besoin d’un cluster pour exécuter le code puis nous pourrons choisir entre les deux problématiques supervisées que sont la classification et la régression. La prévision sur série temporelle (forecasting) sera disponible prochainement.

Ce cluster doit disposer d’un runtime 8.3 ML ou supérieur (à venir), incluant donc Spark 3 et des packages spécifiques pour l’apprentissage automatique.

On devra ensuite désigner le dataset à utiliser. Celui-ci doit exister sous forme de table sur un cluster de l’espace de travail (pas obligatoirement celui qui exécutera l’entrainement), cluster devant être démarré pour que les tables soient visibles… et accessibles !

La table choisie doit présenter des données entièrement préparées pour le processus d’apprentissage (nettoyage des valeurs aberrantes, feature selection, feature engineering, etc.) car de telles opérations ne seront pas possibles par la suite.

Nous désignons ensuite la “prediction target“, puisque nous travaillons dans une approche d’apprentissage supervisé.

Dans les options d’évaluation, nous pourrons modifier la métrique d’évaluation qui servira à comparer les différents modèles, ainsi que donner des conditions d’arrêt, soit sur le temps d’entrainement, soit sur le nombre maximum d’essais réalisés.

C’est parti, l’expérience se lance !

Pas de chance, échec dès le démarrage, mais nous allons chercher la cause.

Nous disposons pour cela d’un notebook contenant l’exécution du job.

Des valeurs vides viennent polluer notre variable cible (“target“). Une meilleure préparation de données aurait dû être réalisée. Heureusement, Databricks vient à nouveau à notre secours avec un second lien vers un notebook de “Data exploration”.

Il s’agit du package pandas_profiling qui est mis en oeuvre dans un notebook.

En affichant le détail, nous retrouvons bien les 284 valeurs manquantes mais nous pouvons aussi observer des valeurs extrêmes, potentiellement aberrantes.

Nous allons repartir d’un dataset plus simple et déjà nettoyé : “German Credit” (disponible par exemple ici), que nous pouvons uploader directement sur le FileStore depuis le menu Data.

Relançons maintenant une expérience d’autoML, cette fois-ci sur une tâche de classification (la variable binaire class est ici la cible).

Au bout du temps imparti ou du nombre d’itérations, nous obtenons une liste des algorithmes que nous pouvons trier selon les différentes métriques.

Une très bonne surprise est de trouver, associé à chaque exécution, le notebook correspondant !

C’est encore XGBoost qui a gagné (comme souvent sans feature engineering préalable).

Celui-ci suit une cheminement tout à fait classique, donné par le plan en Markdown.

Une preprocessing des données est appliqué, sous forme de pipeline Scikit Learn, pour les différents types de variables :

  • variables binaires : imputation des valeurs manquantes et recodage en 0/1
  • variables numériques : imputation par la moyenne
  • variables catégorielles : dichotomisation (one hot encoding)

Cette première étape du pipeline est suivi d’un standardisation, par exemple avec la méthode StandardScaler(), toujours issue du package Scikit Learn.

Nous pouvons visualiser ce pipeline graphiquement.

Le modèle est lui aussi explicitement codé et nous pouvons donc découvrir les valeurs spécifiques des hyperparamètres du modèle utilisés lors de l’exécution.

Enfin, une explication du modèle par la méthode SHAP, classant les features par importance, a été codée ainsi que les commandes MLFlow permettant d’enregistrer puis de charger un modèle (inférence).

En cliquant sur le lien de la colonne “Models”, nous retrouvons les artefacts liés au modèle : un binaire sérialisé au format Pickle, mais aussi quelques graphiques comme la courbe ROC ou la matrice de confusion.

Il ne restera plus qu’à enregistrer le modèle dans MLFlow register pour pouvoir ensuite l’exposer.

En conclusion, nous avons ici un outil d’apprentissage automatisé qui vient ajouter une fonctionnalité à “la plateforme unifiée de données” qu’est Databricks. Cet outil se destine, à mon sens, à des Data Scientists voulant gagner du temps sur le codage de modèles simples (issus de la bibliothèque de Scikit Learn), sur des données déjà contrôlées et nettoyées (a minima, le retrait des valeurs aberrantes).

Nous pourrons regretter l’absence d’alertes automatiques sur les colinéarité entre variables soumises au modèle mais le notebook d’exploration basé sur pandas_profiling nous permet d’obtenir ces informations. Ce type d’outils pousse facilement au sur-apprentissage et c’est une limite qu’il faudra bien garder en tête.

Le fait de proposer le code sous forme de notebooks est un très grand avantage sur d’autres plateformes d’automated ML (rien n’empêche d’améliorer ce code par soi-même !). En étant exigeants, nous pourrions attendre de Databricks que celui-ci soit écrit pour profiter de la puissance du calcul distribué sur les nœuds du cluster (pourquoi pas avec le package koalas ?) mais des évolutions viendront sûrement prochainement.

Créer une ressource Azure Machine Learning

Voici un article simple pour vous accompagner dans les premières étapes de création d’un espace de travail (workspace) Azure Machine Learning, et en particulier sur le choix des paramètres de ce service et de ceux qui l’accompagnent.

Depuis le portail Azure, nous commençons par créer un groupe de ressources dédié.

Une bonne (excellente !) pratique consiste à ajouter des tags lors de la création de chaque ressource Azure. Nous appliquerons également cette pratique lors de la création des éléments propres à l’univers Azure ML.

Le groupe de ressources est maintenant prêt. En cliquant sur le bouton “Create” puis en recherchant “Machine Learning”, nous trouvons le service recherché.

Même si nous ne choisissons ici qu’un seul service, il faut comprendre que celui-ci s’accompagnera de trois à quatre autres services Azure, comme détaillé sur ce schéma, issu de la documentation officielle de Microsoft.

https://docs.microsoft.com/fr-fr/azure/machine-learning/concept-azure-machine-learning-architecture

Décrivons rapidement ces services :

  • Storage Account : le compte de stockage, où seront écrits tous les fichiers nécessaires à l’utilisation d’Azure ML comme par exemple des jeux de données importés, des journaux d’exécution, des artefacts de modèles, etc.
  • Key Vault : le magasin de secrets qui permettra le stockage sécurisé des clés, jetons d’authentification, mots de passe, etc.
  • Application Insights : fonctionnalité d’Azure Monitor, ce service permettra le suivi des ressources en temps réel, comme par exemple la disponibilité et les temps de réponse d’un point de terminaison (endpoint)
  • Container Registry : cette ressource est facultative, elle ne deviendra nécessaire que lors du déploiement de modèles prédictifs sur un service web. Mais comme ce scénario correspond à la finalité de nombreux cas d’usage du Machine Learning, nous ne pourrons nous en passer, dans les faits. Ce sont des images Docker qui seront enregistrées dans cette ressource.

Des règles de nommage (malheureusement variables d’un service à l’autre) devront être respectées : nombre de caractères, autorisation ou non de l’usage des tirets haut ou bas, chiffres… et parfois unicité du nom dans l’ensemble du cloud Azure !

Pour un usage en entreprise, le plan de nommage ne doit pas être pris à la légère. Il ne serait pas incohérent d’y passer plusieurs jours de réflexion tant celui-ci sera ensuite immuable et jouera sur la capacité des utilisateurs à bien appréhender les ressources (distinguer les environnements de développement et de production, simplifier l’analyse de la facturation, etc.).

Idéalement, ou dans une approche d’Infrastructure as Code avec l’outil Terraform, nous aurions préalablement créé ces quatre services, dans le même groupe de ressources, afin de les associer dans cet écran de paramétrage.

Nous pouvons néanmoins les créer individuellement en cliquant sur le bouton “Create new“. Nous n’aurons toutefois pas la main sur l’ensemble des paramètres disponibles lors de la création individuelle de ces ressources.

Pour le compte de stockage, nous choisissons le niveau de redondance le plus faible, et donc le moins coûteux: LRS.

Ce choix s’entend dans une approche de test, mais pour un usage “en production”, il se poserait la question de comment restaurer le service Azure ML en cas de perte du compte de stockage associé. La documentation actuelle ne fait pas part de ce type de procédure.

Pour la création de la ressource Container Registry, nous choisissons le mode de licence Basic qui autorisera la stockage, sans surcoût, de 10Go d’images.

Voici donc nos ressources créées et correctement nommées.

Nous ne modifierons pas ici les paramètres réseaux par défaut. Ceux-ci autorisent la connexion issue de tous réseaux à notre service.

Deux options sont disponibles dans les paramètres avancés, nous les laisserons positionnées par défaut.

Terminons enfin par cette bonne pratique consistant à taguer notre nouveau service.

La ressource est maintenant créée et prête à l’usage !

Notez le bouton “download config.json“, ce fichier contiendra des informations de connexion à la ressource depuis un autre environnement que le portail Azure Machine Learning.

Nous retrouvons bien nos quatre ressources liées, listées sur la droite de la page.

Le bouton “Launch studio” permet d’ouvrir dans une nouvelle fenêtre le portail dédié à cet espace de travail Azure ML.

EDIT 20210616: la configuration du Key Vault n’est pas tout à fait complète ou tout du moins utilisable. En effet, même si vous êtes “owner” de la ressource, vous ne pourrez pas par défaut ajouter de secrets.

Il sera nécessaire d’ajouter une “access policy” pour l’utilisateur (à désigner dans la case “select principal”, à partir des utilisateurs déclarés dans Azure Active Directory).

Ne pas oublier d’enregistrer cette nouvelle policy avec le bouton “save” !

Il sera alors possible d’y renseigner des informations propres à la connexion, comme par exemple l’account key d’un compte de stockage ou le client secret d’un principal de service.

Ensuite, une approche par le code permettra de créer de nouveaux datastores.

Le code illustré ci-dessus est disponible dans ce notebook.

En résumé, toute cette installation mériterait d’être abordée en infra as code, par exemple avec l’outil Terraform !

Gestion des repos Git dans Azure Databricks

Au mois de mars 2021, une nouvelle fonctionnalité vient accompagner les espaces de travail Azure Databricks : les repos Git.

Pour l’activer, nous devons passer par la console d’administration, cliquer sur “Enable” puis rafraîchir la page.

Une nouvelle icône apparaît alors dans le menu latéral.

Jusqu’ici, le lien avec un gestionnaire de versions se faisait individuellement, par le biais de de la fenêtre “user settings“, comme expliqué dans cet article.

Chaque notebook devait alors être relié manuellement à un repository. Pour des actions de masse, il fallait employer les commandes en ligne (Databricks CLI).

Pour démarrer, nous cliquons sur l’icône Repos et le menu propose un bouton “Add Repo“.

Une boîte de dialogue s’ouvre alors.

Il faut comprendre ici qu’il s’agit de créer un repository local au sens de l’espace de travail (“Add Git remote later”) ou bien de cloner un reporemote“, c’est-à-dire relié à un des fournisseurs présents dans la liste. Ce dernier peut être vide si le projet débute.

Nous créons ici un premier notebook dans ce nouveau repository local.

Il faut alors s’habituer à une nouvelle façon de travailler. Les notebooks ne sont plus visibles depuis l’icône “Workspace”. Le lien “Git: Synced” que l’on trouvait en cliquant sur “Revision history” n’existe plus. Mais une icône Git apparaît maintenant en haut à gauche du notebook.

L’historique des modifications s’enregistre toujours bien automatiquement et des retours arrières (“Restore this version”) sont possibles.

Sans réaliser l’association avec un gestionnaire de versions tiers, nous ne disposons pas d’autres fonctionnalités.

Un clic sur l’icône Git nous propose de réaliser cette association, ici avec le service Azure DevOps.

Attention, on ne peut attacher qu’un dépôt qui ne contient pas de commit.

Pour un repository de type privé, il va être nécessaire de réaliser des manipulations de sécurité.

Il s’agit de fournir un Personal Access Token (PAT) généré depuis le gestionnaire de versions.

Une fois la configuration réalisée, la boîte de dialogues ci-dessous apparaît alors.

Nous retrouvons des notions familières aux utilisateurs de Git : branche (master ou main), pull, commit & push.

Il sera nécessaire (c’est même obligatoire) de saisir un résumé (summary) des modifications pour pouvoir cliquer sur le bouton “Commit & Push“.

Attention, vous pourriez rencontrer l’erreur suivante.

En effet, une liste de repositories autorisés doit être donné dans la console d’administration (seulement si cette option a été activée). Ce scénario s’inscrit dans le cadre de la licence Premium de Databricks où tous les utilisateurs ne sont pas obligatoirement administrateurs.

Notons que l’extension du fichier est .py mais il s’agit bien d’un notebook Databricks. Le contenu de celui-ci, en particulier les cellules Markdown, est arrangée pour “ressembler” à un script Python.

Attention, une commande comme display est propre à l’environnement des notebooks Databricks.

Si le repository est aussi utilisé pour conserver d’autres éléments que ceux propres à Databricks, nous verrons apparaître les dossiers du repo dans le menu de navigation de Databricks. Il pourrait être tentant de les supprimer depuis Databricks (ils sembleront vides puisque seules quelques extensions .py et .ipynb sont affichées) mais ne le faîtes surtout pas !

Une action “removed” serait alors être proposée au prochain commit. Il sera bien sûr possible de l’éviter en basculant sur “Discard changes“.

Ici, le repo est partagé avec des pipelines Azure Data Factory.

Le répertoire “vide” va alors réapparaître dans l’arborescence.

En conclusion, il serait préférable de dédier un repository à Databricks pour éviter les confusions.

EDIT 2021-08-23 :

La fonctionnalité originelle de liaison avec un repository Git est dorénavant une “legacy feature” et il est recommandé de ne plus l’utiliser.

En poussant les tests, on obtient un blocage pour commiter depuis Databricks lors du scénario suivant :

  • publication réalisée depuis un outil tiers dans le repository partagé (par exemple, création d’un nouveau pipeline dans Azure Data Factory)
  • modifications d’un notebook dans Azure Databricks
  • sauvegarde de ces modifications
  • tentative de commit & push depuis Databricks

Nous obtenons alors le message d’erreur ci-dessous.

Il est indispensable de réaliser une action Pull avant de modifier les notebooks et nous recommandons de le faire avant même de débuter vos travaux sous Databricks.

Un message d’alerte s’affiche alors.

Toutes les limitations liées à la fonctionnalité “Repos” dans Azure Databricks sont données sur ce lien officiel de la documentation Microsoft.

Associer Azure Databricks et Azure Machine Learning

Si les deux outils ont en partie des fonctionnalités qui se recouvrent, avec quelques différences (voir cet article), il est tout à fait possible de tirer profit du meilleur de chacun d’eux et de les associer dans une architecture data sur Azure.

Le scénario générique d’utilisation sera alors l’industrialisation de modèles d’apprentissage (machine learning) entrainés sur des données nécessitant la puissance de Spark (grande volumétrie voire streaming).

Nous allons détailler ici comment Databricks va interagir avec les services d’Azure Machine Learning au moyen du SDK azureml-core.

A l’inverse, il sera possible de lancer, depuis Azure ML, des traitements qui s’appuieront sur la ressource de calcul Databricks déclarée en tant que attached compute.

Le SDK azureml sous Databricks

Nous commencerons par installer le package azureml-sdk[databricks] au niveau du cluster interactif (d’autres approches d’installation sont possibles, en particulier pour les automated clusters). Le cluster doit être démarré pour pouvoir effectuer l’installation.

Nous pouvons vérifier la version installée en lançant le code suivant dans un notebook :

import azureml.core
print("Azure ML SDK Version: ", azureml.core.VERSION)

Un décalage peut exister avec la version du SDK non spécifique à Databricks. Des dépendances peuvent également être demandées pour certaines parties du SDK comme l’annonce l’avertissement reçu à l’exécution de la commande précendente.

Failure while loading azureml_run_type_providers. Failed to load entrypoint automl = azureml.train.automl.run:AutoMLRun._from_run_dto with exception (cryptography 3.1.1 (/databricks/python3/lib/python3.8/site-packages), Requirement.parse('cryptography<4.0.0,>=3.3.1; extra == "crypto"'), {'PyJWT'}).

L’étape suivante consistera à se connecter à l’espace de travail d’Azure Machine Learning depuis le script, ce qui se fait par le code ci-dessous :

from azureml.core import Workspace

ws = Workspace.from_config()

Plusieurs possibilités s’ouvrent à nous pour réaliser ce lien. La première consiste à établir explicitement ce lien dans le portail Azure, depuis la ressource Databricks.

Mais cette approche ne permet d’avoir qu’un seul espace lié, celui-ci n’étant plus modifiable au travers de l’interface Azure (mais peut-être en ligne de commandes ?). La configuration étant donnée par un fichier config.json, téléchargeable depuis la page du service Azure ML, nous pouvons placer ce fichier sur le DataBricks File System (DBFS), et donner son chemin (au format “File API format”) dans le code .

from azureml.core import Workspace

ws = Workspace.from_config(path='/dbfs/FileStore/azureml/config.json')

Une authentification “interactive” sera alors demandée : il faut saisir le code donné dans l’URL https://microsoft.com/devicelogin puis entrer ses login et mot de passe Azure.

Pour une authentification non interactive, nous utiliserons un principal de service.

Nous pouvons maintenant interagir avec les ressources du portail Azure Machine Learning et en particulier, exécuter notre code au sein d’une expérience.

from azureml.core import experiment
experiment = Experiment(workspace=ws, name="ExperimentFromDBXs")

Après l’entrainement d’un modèle, idéalement réalisé grâce à Spark, nous pouvons enregistrer celui-ci sur le portail.

L’instruction run.log() permet de conserver les métriques d’évaluation.

A noter que les widgets ne peuvent pas s’afficher dans les notebooks Databricks.

Databricks comme attached compute

Comme pour toute ressource de calcul, nous commençons par déclarer le cluster Databricks dans le portail Azure Machine Learning.

Un jeton d’accès (access token) est attendu pour réaliser l’authentification. Nous prendrons soin de le générer grâce à un principal de service pour éviter qu’il ne soit attaché à une personne (“Personal Access Token”).

La ressource est maintenant correctement déclarée.

Nous allons l’exploiter au travers du SDK Python azureml dans lequel nous disposons de deux objets liés à Databricks.

La documentation présente la classe qui permettrait de créer cette ressource au travers du code :

DatabricksCompute(workspace, name)

Nous trouvons aussi dans la documentation l’objet qui crée une étape de pipeline exécutée ensuite au sein d’une expérience :

DatabricksStep(name, inputs=None, outputs=None, existing_cluster_id=None, spark_version=None, node_type=None, instance_pool_id=None, num_workers=None, min_workers=None, max_workers=None, spark_env_variables=None, spark_conf=None, init_scripts=None, cluster_log_dbfs_path=None, notebook_path=None, notebook_params=None, python_script_path=None, python_script_params=None, main_class_name=None, jar_params=None, python_script_name=None, source_directory=None, hash_paths=None, run_name=None, timeout_seconds=None, runconfig=None, maven_libraries=None, pypi_libraries=None, egg_libraries=None, jar_libraries=None, rcran_libraries=None, compute_target=None, allow_reuse=True, version=None)

Il est possible de lancer un script Python qui sera exécuté sur le cluster Databricks mais l’usage le plus intéressant est sans doute l’exécution d’un notebook.

from azureml.core.compute import DatabricksCompute
from azureml.pipeline.steps import DatabricksStep

databricks_compute = DatabricksCompute(workspace=ws, name="my-dbx-cluster")

dbNbStep = DatabricksStep( name="DBNotebookInWS",
 num_workers=1,
 notebook_path=notebook_path,
 run_name='DB_Notebook_demo',
 compute_target=databricks_compute,
 allow_reuse=True
)

Nous pourrons alors suivre les logs du cluster dans l’expérience.

Je vous recommande ce notebook pour découvrir tous les usages de DatabricksStep, comme par exemple l’exécution d’un JAR.

En conclusion

La souplesse des services managés et la séparation stockage / calcul autorisent aujourd’hui à penser les architectures data avec les services qui sont les plus utiles au moment souhaité, quitte à avoir des redondances de fonctionnalités. Nous veillerons à préserver au maximum l’indépendance de certaines parties du code (a minima la préparation de données et l’entrainement) vis à vis des plateformes propriétaires. Il sera alors possible d’envisager des alternatives chez d’autres fournisseurs cloud ou bien encore dans le monde de l’Open Source.

Différencier Azure Databricks et Azure Machine Learning

Des machines virtuelles, du code R ou Python, des accès au Data Lake, du versionning et de l’exposition de modèles de Machine Learning, les points communs entre Azure Databricks et le service Azure Machine Learning sont nombreux ! Mais nous allons détailler dans cet article les différences entre ces deux produits, qui vous permettront de choisir le meilleur outil selon vos cas d’usage. N’oublions pas que ce sont aussi des outils complémentaires mais cet aspect sera traité dans un autre article.

S’il fallait résumer en quelques mots chacun des deux produits, voici comment l’on pourrait les présenter :

  • Databricks est un cluster Spark managé dans le cloud Azure (mais aussi AWS et GCP), dédié à exécuter du code de manière distribuée. Autour de cette fonctionnalité principale, s’intègrent des produits comme MLFlow pour le versionning et serving des modèles de ML ou encore SQL Analytics et Redash pour la visualisation de données.
  • Azure Machine Learning est un portail (dit “studio”) regroupant toutes les briques d’un projet de ML, allant de l’import des données depuis des sources Azure à l’exposition (web service) et monitoring de modèles, en passant par l’entrainement de ces modèles sur différentes solutions de calcul.

Nous allons pointer les différences entre ces deux services sur les thèmes indiqués dans le schéma ci-dessous, qui sont au cœur d’un workflow de Data Science.

Les sources de données

Les sources possibles pour Azure ML sont toutes des services managés du cloud de Microsoft, de type systèmes de fichiers ou bases de données.

Les sources pour Azure Databricks sont représentées ci-dessous :

Le périmètre est ici plus large et intègre des produits classiques des architectures Big Data dans les domaines NoSQL, temps réel ou search.

La connexion à des ressources de type Azure SQL DB ou DWH se fait par un pilote générique JDBC, comme cela a été présenté ici préalablement. C’est une approche plus générique mais qui ne pas bénéficier d’optimisations propres à la communication de services Microsoft entre eux. C’est un point qui mériterait d’être testé, dans des conditions de sécurisation des échanges réseaux (VNET).

L’un des projets Open Source très présent dans Databricks est Delta Lake : un format de fichier “amélioré” par rapport au format Parquet classique, puisqu’il s’accompagne d’une capacité à traiter des transactions ACID et d’un historique au format JSON des logs transactionnels. Ce sont des opérations comme des delete, insert, update et upsert qui seront possibles ! La fonctionnalité de time travel permet de retrouver l’état des données à une date ou sur une version donnée. Nous sommes donc dans une logique “schema-on-write” d’habitude propres aux systèmes de gestion de bases de données. Est-ce déjà la fin annoncée de ces derniers ? Certainement pas !

Voici la syntaxe permettant d’écrire un dataframe Spark au format delta.

(df.write.format("delta")
.mode("append"|"overwrite")
.partitionBy("date") # optional
.option("mergeSchema", "true") # option - evolve schema
.saveAsTable("events") | .save("/path/to/delta_table")

Delta Lake est un produit à part entière, alors peut-on l’utiliser depuis Azure ML ? Rappelons qu’il faut un contexte Spark et c’est ici toute la difficulté pour Azure ML qui doit, pour cela, se baser sur une ressource tierce comme Azure Synapse Analytics ou… Azure Databricks !

Les scripts et leur exécution (calcul)

Azure ML disposent de deux SDK, Python et R, mais il ne sera pas possible d’utiliser de langage Java ou Scala. Au delà de la création d’un dataset sur une source Azure SQL DB, le SQL ne pourra être utilisé qu’au travers de packages R ou Python.

Le périmètre est plus large pour Databricks dont les notebooks exécuteront Python, R, Scala ou du SQL. Ce choix se fait à la création du notebook mais n’est pas rédhibitoire car les commandes magiques permettent de changer de langage dans une cellule : %python%r%scala, %sql.

Même si la plupart des Data Scientists sont plus enclins à développer en R ou Python, et dans les déclinaisons SparkR et PySpark, Scala reste le langage natif de Spark et certainement pour l’instant le plus efficace car compilé avant de s’exécuter.

Pour utiliser Scala avec Azure ML, nous attendrons la fonctionnalité de “linked service” vers Azure Synapse Analytics.

L’exécution d’un script dans Azure ML peut se faire une instance de calcul (la VM qui exécute également le serveur de notebooks ou RStudio Server) ou sur un cluster de machines virtuelles.

Databricks est prévu “by design” pour les exécutions distribuées sur un cluster mais il est possible d’utiliser un “cluster single node“, c’est-à-dire une machine unique.

Le versionning des modèles

Commençons par le produit Open Source présent dans les deux services : MLFlow.

La même équipe se trouvant à l’origine de Spark et de MLFlow, nous disposons naturellement dans Databricks d’une intégration très simple et fluide. Le package est même installé par défaut sur les runtimes de type ML.

Les éléments nécessaires à l’évaluation et à la reproductibilité du modèle sont simples à enregistrer à l’aide de commandes comme mlflow.log_param, mlflow.log_metric, etc.

Azure ML permet donc également d’activer une URI MLFlow mais qui ne sera valable qu’une heure.

Vous risquez aussi le message d’erreur suivant :

WARNING mlflow.models: Logging model metadata to the tracking server has failed, possibly due older server version.

Il faut à mon sens voir le portail Azure ML comme principalement un outil d’affichage des logs d’exécution (runs) des scripts lancés au sein d’expériences. A nouveau, quelques commandes simples permettent d’enregistrer le binaire d’un modèle (artifact) et les informations associées : model.register, run.log, etc.

Pour remplacer la commande mlflow.sklearn.save_model, nous utiliserons dans Azure ML une déclinaison de l’exemple suivant :

from azureml.core.model import Model

Model.register(workspace=ws,
               model_name="diabetes_regression_model",
               model_path="test_diabetes/model.pkl",
               model_framework=Model.Framework.SCIKITLEARN,  # Framework used to create the model.
               model_framework_version='0.20.3',             # Version of scikit-learn used to create the model.
               tags={'alpha': alpha, 'l1_ratio': l1_ratio},
               description="Linear regression model to predict diabetes"
              )

Il est indispensable d’effectuer cette opération pour retrouver l’artefact en dehors du menu “Output & logs” de l’exécution.

L’exposition des modèles

Nous sommes ici dans le domaine de prédilection d’Azure ML qui permet une approche par l’interface ou au travers du SDK pour piloter deux ressources Azure : Container Instance et Kubernetes Services.

Mais Databricks n’est pas en reste pour l’exposition et celle-ci s’appuiera naturellement sur MLFlow. Nous commençons par enregistrer le modèle retenu.

Ensuite, depuis le menu Model, nous pouvons activer le serving, qui se fera sur une ressource de type single-node.

Dès que la ressource (une machine virtuelle) est active, il est possible d’interroger l’API.

Ordonnancement

Dans une architecture Azure PaaS, c’est Azure Data Factory (ADF) qui est la solution toute désignée pour ordonnancer les traitements. Nous y disposons d’un module pour chacun des deux services. La différence se situe sur les éléments pouvant être appelés par ADF. Seuls les objets “Pipeline” issus d’Azure ML sont utilisables et ces objets ne sont pas simples à développer.

Les deux premiers modules ML concernent l’ancienne version du studio.

Côté Databricks, ce sont des scripts Python, des notebooks ou bien des Jar qui sont exécutables au travers de l’interface.

Les deux services ont également leur propre outil de scheduling, pilotable au travers de l’interface pour Databricks ou bien grâce à leur API.

A l’aide du SDK Azure ML, nous pourrons mettre en place un trigger à l’aide du code ci-dessous :

Versionning du code et cycle CI/CD

Databricks permet de définir un fournisseur Git parmi les suivants :

Chaque notebook doit ensuite être lié au repository et il est possible de faire un commit sur une branche déjà créée (une pull request devant ensuite être faite depuis le fournisseur Git).

Récemment (mars 2021), une nouvelle approche est disponible et fournit plus de fonctionnalités.

Dans les notebooks d’Azure ML, aucun lien n’est fait avec un gestionnaire de version. Il faut donc ruser et passer par un IDE local comme Visual Studio Code qui pilotera les ressources de calcul à distance. J’ai détaillé ce fonctionnement dans cet article.

Passons maintenant à la problématique du déploiement entre deux environnements (par exemple, développement et production).

Lorsque le code se trouve versionné sur un dépôt Azure Repos, il est très simple de lancer de manière automatique un pipeline d’intégration qui réalisera des tests automatisés. Databricks s’appuiera sur une machine virtuelle munie du module databricks-connect qui permet d’exécuter du code à distance sur l’environnement Spark.

Le schéma ci-dessous résume l’isolation des environnements avec une architecture simple reposant sur un stockage Azure Data Lake et un espace de travail Databricks.

Des outils de release de la Market Place vont permettre de déplacer automatiquement des notebooks de l’environnement de développement vers celui de production.

En revanche, utiliser des ressources Azure ML par environnement est une question qui n’est pas simple à trancher. En effet, le portail est finalement une sorte d’outil “DevOps” (disons plutôt MLOps) en lui-même. Nous y trouverons ainsi les versions des datasets et des modèles, ainsi que les logs d’exécution permettant un suivi de production. Mais en pratique, et par manque d’outils de nettoyage (des expériences, des exécutions…), l’usage pour le développement va “polluer” le portail. Il sera donc indispensable de se donner des pratiques de gouvernance et de limiter les droits aux différents utilisateurs, par le biais des custom roles, comme décrit dans ce billet.

Databricks peut être utilisé comme “attached compute”, environnement d’exécution des scripts Spark.

Le Machine Learning ne se marie pas forcément bien avec l’isolation des environnements : en effet, pour démarrer le projet, il faut des données qui soient de qualité et suffisamment représentatives de la réalité. Ce n’est pas le cas des environnements de développement qui sont incomplets voire erronés. Un accès (en lecture) à la production depuis l’environnement de développement pourrait être à envisager.

Coût et FinOps

Suite au retrait de la licence Entreprise d’Azure ML, le coût d’utilisation ne correspond qu’au temps où sont utilisées les ressources de calcul. A cela s’ajoutent les services qui accompagnent Azure ML : compte de stockage, Key Vault, app insights et surtout container registry. Il faut penser à purger régulièrement ce dernier car un container registry ne donne que 10Go de stockage gratuit.

Il est tout à fait possible d’utiliser les mêmes types de VMs sur les deux outils. Pour autant, le coût d’une VM est “surchargé” par la licence Databricks, exprimée en DBU. Cette licence se décline elle-même selon le type d’espace de travail (standard ou premium) et le type de cluster (interactif ou automated).

Une piste supplémentaire est d’utiliser des “spot instances” moins chères, donnant une nouvelle opportunité d’optimisation des coûts (au détriment de la rapidité d’exécution), comme présenté dans cette vidéo.

Quelques points inclassables

En plus des outils “no code” (Concepteur et AutomatedML), Azure ML dispose d’un module de labellisation d’images, assisté par une approche d’active learning (au bout d’un nombre suffisant d’images taggés, les tags peuvent être automatiquement proposés).

Databricks s’est pourvu d’un nouvel outil “SQL Analytics” qui permet, à l’aide de requêtes SQL, de préparer des vues à destination d’un outil de reporting comme Microsoft Power BI.

En licence Premium, le service Power BI donne accès aux modèles enregistrés sur Azure ML pour que ceux-ci soient appliqués lors de la phase de préparation de données (Power Query).

Alors, lequel choisir ?

En conclusion, il faudra bien évaluer les prérequis des projets (par exemple, l’utilisation de Delta Lake) qui s’exécuteront sur l’architecture Azure pour choisir le bon outil, mais il faut aussi bien comprendre qu’il n’y a pas un surcoût trop important à les utiliser tous les deux et nous verrons surtout dans un prochain article qu’ils peuvent se montrer très complémentaires !