Réviser la certification Associate Developer for Apache Spark

Si vous parcourez régulièrement ce blog, vous avez dû tomber sur un article traitant de Azure Databricks. Au cœur de ce produit, tourne le moteur Open Source Apache Spark. Ce moteur a révolutionné le monde de la Big Data, par sa capacité à réaliser des calculs distribués, en mémoire, sur des clusters de machines (souvent virtuelles et bien souvent dans le cloud).

Que l’on soit Data Engineer ou Data Scientist, cet outil est aujourd’hui (2022) incontournable et une certification apporte une reconnaissance de votre expertise dans le domaine. La première certification proposée par Databricks permet d’obtenir le titre Certified Associate Developer for Apache Spark. Cette certification demande de répondre en 2h à 60 questions fermées, et d’obtenir un minimum de 70% de bonnes réponses. Voici un exemple de questions posées à l’examen, fourni par l’éditeur.

Environ un quart des questions traitent des mécanismes techniques du moteur (je vous recommande cette vidéo de Advancing Analytics) et le reste sont des questions de code. Pour celles-ci, nous aurons à disposition la documentation officielle de Spark au travers de cette page web. Entrainez-vous à faire des recherches dans cette page, vous aurez besoin d’aller vite le jour de l’examen !

Attention, la recherche ne semble pas possible dans l’interface de l’examen !

EDIT suite au passage de la certification avec Webassessor :

Au cours de l’examen, l’écran sera séparé en deux : la question à gauche, la documentation à droite. Mauvaise surprise (sous Edge uniquement ?) : impossible de faire une recherche dans la documentation, pas de raccourci ctrl+F non plus. Mon astuce est donc d’utiliser la zone de prise de notes, noter les noms de fonctions sur lesquelles vous avez un doute, puis parcourir la documentation en suivant l’ordre alphabétique.

Je vous encourage à tester également la Spark UI au travers de ce simulateur pour bien comprendre l’exécution concrète des différentes syntaxes de code et connaître les grandes lignes de cette interface. Ce sera également une bonne façon de hiérarchiser les différents niveaux d’exécution job / stage / task.

Deux éléments sont importants pour bien appréhender les questions de code. Tout d’abord, il faudra choisir la réponse “correcte” ou bien la seule “incorrecte” par une liste de cinq propositions. Prenez soin de bien déterminer à quel type de question vous êtes en train de répondre. Ensuite, les différentes modalités de réponse “joueront sur les mots” c’est-à-dire que des variations de code vous seront proposées mais 4 syntaxes sur 5 ne seront pas correctes (dans le cas d’une question de type “correct” !). Il est donc important de bien avoir en tête les confusions classiques de syntaxe.

Nous allons décrire ci-dessous plusieurs hésitations possibles entre des syntaxes PySpark (certaines syntaxes n’existant d’ailleurs qu’un Python, souvent en lien avec les DataFrames Pandas).

Bonne chance à vous pour l’examen !

show() versus display()

La fonction show() permet d’afficher un résultat (c’est donc une action au sens Spark).

df.show(truncate=False)

La fonction display() n’existe que dans Databrick, version commerciale construite autour de Spark.

withColumn() versus withColumnRenamed()

withColumn() permet d’ajouter une nouvelle colonne dans un Spark DataFrame. Notons ici l’emploi de la fonction col() pour appeler les colonnes du DataFrame dans la formule de la nouvelle colonne. Pour une constante, nous utiliserons la fonction lit().

 df.WithColumn("nouveau nom", col("ancien nom") )

Les fonctions lit() et col() auront été préalablement importées.

from pyspark.sql.functions import col, lit

withColumnRenamed() permet de renommer une colonne. C’est donc la syntaxe la plus adaptée par rapport au workaround proposé ci-dessous.

df.WithColumnRenamed("ancien nom", "nouveau nom")

Le piège est donc ici dans l’ordre des paramètres et en particulier dans la position du nom de la colonne créée.

printSchema() versus describe()

Après la lecture d’une source de données, nous souhaitons vérifier le schéma du DataFrame. La fonction printSchema() est la bonne fonction pour réaliser cet objectif.

describe() versus summary()

Les fonctions describe() et summary() permettent d’obtenir les statistiques descriptives sur un DataFrame. La fonction summary() ajoute les 1e, 2e et 3e quartiles.

La différence se situe sur le rôle du paramètre dans chacune des deux fonctions.

Variantes de spark.read

Nous allons voir ici deux écritures possibles pour un même résultat.

Syntaxe 1 :

df = spark.read.format(file_type) \
.option("inferSchema", infer_schema) \
.option("header", first_row_is_header) \
.option("sep", delimiter) \
.load(file_location)

Syntaxe 2 :

df = spark.read \
.option("inferSchema", infer_schema) \
.option("header", first_row_is_header) \
.option("sep", delimiter) \
.csv(file_location)

Syntaxe 3 :

df = spark.read \
.load(file_location, format="csv", inferSchema=infer_schema, header=first_row_is_header, sep=delimiter)

En compléments, allez voir les options de sauvegarde d’un Spark DataFrame.

collect() versus select()

Il faut ici bien maîtriser les concepts de transformations et d’actions, propres au moteur Spark.

La fonction collect() est une action qui retourne tous les éléments du Spark DataFrame sous forme de tableau (array) au nœud Driver.

La fonction select() est une transformation qui projette, c’est-à-dire conserve une colonne ou liste de colonnes. Comme il s’agit d’une transformation, il est nécessaire d’y ajouter une action (par exemple show) pour forcer l’évaluation du résultat.

La liste des colonnes donnée en paramètre de select() peut s’écrire de deux façons :

df.select("Pregnancies", "Outcome").show()
df.select(["Pregnancies", "Outcome"]).show()

La première version est plus “légère” mais la seconde s’adapte mieux à un passage de variable de type liste déjà définie.

Attention, la fonction drop() n’autorise pas l’usage d’un objet de type liste.

distinct() versus unique()

La fonction distinct() renvoie les lignes distinctes, sur la base des colonnes du Spark DataFrame ou de la sélection faite au préalable.

df.select('column1').distinct().collect()

La fonction unique() ne s’applique qu’à un Pandas DataFrame !

join() versus merge()

Il s’agit ici de fusionner deux DataFrames. Les variantes d’écriture se font au niveau de l’expression de la jointure.

df_inner = df1.join(df2, on=['id'], how='inner')
df_inner = df1.join(df2, df1.id == df2.id, how='inner’)

Le troisième paramètre “how” définissant le type de jointure est facultatif, la valeur par défaut étant alors “inner” (jointure interne).

La fonction merge() ne s’applique qu’à un Pandas DataFrame !

union() versus unionByName() versus concat()

La fonction union() ajoute les lignes d’un DataFrame, donné en paramètre, à un autre DataFrame. L’ajout se fait alors sur la base des indices des colonnes.

La fonction unionByName() s’utilise pour ajouter des DataFrames sur la base des noms de colonnes et non de leur position.

La fonction Spark concat() n’a rien à voir avec ce type d’opération puisqu’elle réalise une concaténation de colonnes (astuce utile pour l’optimisation de la mémoire !).

df.select(concat(df.s, df.d).alias('sd')).collect()

Il ne faut pas confondre avec la fonction concat() appliquée sur des Pandas DataFrames, qui réalise quant à elle une fusion de ces jeux de données.

agg() versus groupBy()

Utilisée seule, la fonction agg() agrège un Spark DataFrame sans colonne de regroupement.

df.agg({'Glucose' : 'max'}).collect()

Il s’agit toutefois d’un “sucre syntaxique” de l’expression ci-dessous.

df.groupBy().agg({'Glucose' : 'max'}).collect()

La fonction groupBy() réalise un groupe sur la colonne ou liste de colonne spécifiée puis évalue les agrégats proposés ensuite.

df.groupBy('Outcome').max('Glucose').show()
df.groupBy('Outcome').agg({'Glucose' : 'max', 'Insulin' : 'max'}).show()
df.groupBy(['Outcome', 'Pregnancies']).agg({'Glucose' : 'max', 'Insulin' : 'max'}).show()

cast() versus astype()

La fonction cast() permet de modifier le type d’une colonne. Elle accepte plusieurs variantes de syntaxe, dans l’expression d’un type attendu.

from pyspark.sql.types import IntegerType

df.withColumn("age",df.age.cast(IntegerType()))
df.withColumn("age",df.age.cast('int'))
df.withColumn("age",df.age.cast('integer'))

La fonction astype() est un alias de la fonction cast().

filter() versus where()

La fonction where() est un alias de la fonction filter(). Différentes syntaxes sont possibles pour exprimer la condition booléenne.

df.filter(df.Pregnancies <= 10).show()
df.filter("Pregnancies <= 10").show()
from operator import ge, le, gt, lt

df.filter(lt(df.Pregnancies, lit(10))).show()

contains() versus isin()

Voici deux exemples illustrant les utilisations de ces deux fonctions, elles-mêmes utilisées dans des logiques de filtre sur un DataFrame.

df.filter(df.Pregnancies.contains('0')).collect()
Attention, nous obtenons les valeurs numériques 0 et 10 !
df[df.Pregnancies.isin([1, 2, 3])].collect()

La différence revient donc à chercher un contenu dans une colonne (contains()) ou à vérifier que le contenu d’une colonne appartient à une liste (isin()).

A noter, la fonction array_contains() permet d’étendre la logique de la fonction contains() sur les valeurs d’un tableau, contenu dans une colonne d’un Spark DataFrame.

filtered_df=df.where(array_contains(col("SparkAPI"),"pyspark"))

size() versus count()

La fonction size() renvoie le nombre d’éléments d’un tableau (array) alors que la fonction count() renvoie le nombre de ligne d’un Spark DataFrame.

Null versus NaN

La valeur Null correspond à une donnée manquante, c’est-à-dire une donnée qui n’existe pas.

df.where(col("a").isNull())
df.where(col("a").isNotNull())

La valeur NaN, pour Not a Number, est le résultat d’une opération mathématique dont le résultat ne fait pas sens (par exemple, une division par 0).

from pyspark.sql.functions import isnan


df.where(isnan(col("a")))

na.drop() versus dropna()

Ces deux fonctions sont des alias l’une de l’autre.

Elles disposent d’un paramètre thres qui permet la suppression des lignes ayant un nombre de valeurs non-nulles inférieures à ce seuil.

dropDuplicates() versus drop_duplicates()

Ces deux fonctions sont des alias l’une de l’autre. Elles permettent de retirer les valeurs en doublon dans un DataFrame.

take() vs head() vs first() / tail() vs limit()

Visualiser les données d’un Spark DataFrame est fondamental ! Mais nous ne pouvons pas tout afficher, il faut alors réduire le nombre de lignes. Il existe pour cela de nombreuses fonctions et quelques subtilités entre toutes celles-ci.

Les fonctions take() et head() sont des alias, la seconde se donnant une syntaxe identique à celle utilisée pour les Pandas DataFrames. Elles prennent en argument le nombre de lignes (rows) à renvoyer sous forme d’une liste.

C’est le type d’objet renvoyé qui diffère dans la fonction limit(), celle-ci produisant un Spark DataFrame.

Les fonctions first() et tail() renvoient respectivement la première et la dernière ligne du DataFrame. Attention, la seconde attend un nombre de lignes en paramètre.

sort() versus orderBy()

La fonction orderBy() est un alias de la fonction sort().

Toutefois, il semble que celle-ci engendre la collecte de toutes les données sur un seul exécuteur, ce qui a l’avantage de fiabiliser le tri mais engendre un temps de calcul plus long, voire un risque de saturation mémoire.

Voici quelques exemples de syntaxe alternatives avec la fonction sort(). A noter que le sens de tri par défaut est ascendant.

df.sort(asc("value"))

df.sort(asc(col("value")))

df.sort(df.value)

df.sort(df.value.asc())

cache() versus persist()

La fonction cache() stocke un maximum de partitions possibles du DataFrame en mémoire et le reste sur disque. Ceci correspond au niveau de stockage dit MEMORY_AND_DISK, et ce comportement n’est pas modifiable.

A l’inverse, et même si son comportement par défaut est similaire, la fonction persist() permet d’utiliser les autres niveaux de stockage que sont MEMORY_ONLY, MEMORY_ONLY_SER, MEMORY_AND_DISK_SER, DISK_ONLY, etc.

repartition() versus coalesce()

< A VENIR >

repartition(1) versus coalesce(1)

< A VENIR >

map() versus foreach()

< A VENIR >

from_timestamp versus unix_timestamp()

< A VENIR >

explode() versus split()

< A VENIR >

Connecter Power BI au SQL endpoint de Databricks

La stratégie “Lakehouse” de Databricks vise à fusionner les usages du Data Lake (stockage de fichiers) et du Data Warehouse (modélisation de données dans un but analytique). Il semble donc évident de trouver des connecteurs vers les principaux outils de Business Intelligence Self Service dans la partie “SQL” de l’espace de travail Databricks.

Nous allons détailler ici le processus de création de dataset et de mise à jour pour Power BI. Mais avant tout, résumons en quoi consiste la fonctionnalité “SQL” de Databricks.

Le constat est fait qu’aujourd’hui (comme depuis plus de 30 ans…), le langage SQL reste majoritairement utilisé pour travailler les données, en particulier pour effectuer des opérations de transformations, d’agrégats ou encore de fusions. Nous pouvions d’ores et déjà utiliser des notebooks Databricks s’appuyant sur l’API Spark SQL et définir des tables ou des vues dans le metastore Hive associé à un cluster (tables visibles uniquement lorsque le cluster est démarré).

Nous pouvons créer une table à partir de la syntaxe suivante, en s’appuyant sur des fichiers de type CSV, parquet ou encore Delta :

CREATE TABLE diamonds USING CSV LOCATION '/databricks-datasets/Rdatasets/data-001/csv/ggplot2/diamonds.csv' options (header = true, inferSchema = true)

La requête est exécutée par un “SQL endpoint” qui n’est autre qu’un cluster de machines virtuelles du fournisseur Cloud (ici le cloud Azure de Microsoft).

Différentes tailles prédéfinies de clusters sont disponibles et correspondent à des niveaux de facturation exprimés en Databricks Units (DBU).

A termes, nous espérons profiter d’une capacité “serverless” qui serait mise à disposition par l’éditeur, conjointement au fournisseur de cloud public.

La table précédemment créée est ensuite “requêtable” au moyen des syntaxes SQL traditionnelles.

Il serait alors possible de créer un dashboard dans l’interface Databricks mais nous allons profiter de la puissance d’un outil comme Power BI pour exploiter visuellement les données.

Nous récupérons donc les informations de connexion au SQL endpoint avant de lancer le client Power BI Desktop.

Databricks SQL endpoint comme source de données

Nous recherchons Azure Databricks dans les sources de données de Power BI Desktop.

Nous retrouvons ici les informations décrivant le SQL endpoint :

  • server hostname : l’URL (sans htpps) de l’espace de travail Databricks
  • HTTP path
  • le catalogue (par défaut hive_metastore)
  • le database (ou schéma)

A noter que les deux modes de connexion de Power BI sont disponibles : import et DirectQuery. Pour ce dernier, il faudra prendre en compte le nécessaire “réveil” du SQL endpoint lors de la première requête. Quelques minutes peuvent être nécessaires.

Précisions qu’il est possible de créer un catalogue autre que celui par défaut (hive_metastore) et il serait alors pertinent de rédiriger le stockage des métadonnées vers une partie du Data Lake ou vers un compte de stockage dédié, comme expliqué dans cet article.

Pour voir les données du metastore, il faudra choisir une méthode d’authentification parmi les trois disponibles.

Le jeton d’accès personnel (Personal Access Token) semble simplifier les choses mais attention, sa validité est lié au compte qui l’a généré. Un utilisateur retiré de l’espace Databricks disparaît avec ses jetons ! Le périmètre associé à un PAT est très large : utilisation du CLI et de l’API Databricks, accès aux secret scopes, etc.

Nous allons donc préférer la connexion au travers de l’annuaire Azure Active Directory.

Pour gagner du temps, nous pouvons télécharger un fichier au format .pbids qui est un fichier contenant déjà les informations de connexion renseignées.

La vue des tables disponibles s’ouvre alors directement si nous disposons déjà d’une authentification définie dans notre client Power BI Desktop.

C’est alors une connexion de type DirectQuery qui est réalisée. Il sera possible de basculer en mode import en cliquant sur le message en bas à droite du client desktop.

En ouvrant l’éditeur de requêtes Power Query, nous pouvons voir la syntaxe utilisée pour définir la connexion.

let
Source = Databricks.Catalogs("adb-xxx.x.azuredatabricks.net", "/sql/1.0/endpoints/xxx", []),
hive_metastore_Database = Source{[Name="hive_metastore",Kind="Database"]}[Data],
default_Schema = hive_metastore_Database{[Name="default",Kind="Schema"]}[Data],
diamonds_Table = default_Schema{[Name="diamonds",Kind="Table"]}[Data]
in
diamonds_Table

La fonction Databricks.Catalogs peut être remplacée par la fonction Databricks.Query qui permet alors de saisir directement une requête SQL dans l’éditeur.

Planifier une actualisation du dataset

Une fois le rapport publié sur l’espace de travail Databricks, nous allons pouvoir actualiser directement les données, sans passer par le client desktop. Nous nous rendons pour cela dans les paramètres du dataset, au niveau des informations d’identification.

Nous devons ici redonner les informations de connexion. Nous en profitons pour préciser le niveau de confidentialité des données.

Dans le cas d’une connexion de type DirectQuery, nous pouvons demander à ce que l’identité de l’AAD des personnes en consultation soit utilisée. Cela permet de sécuriser l’accès aux données mais à l’inverse, il sera nécessaire que ces personnes soient déclarées dans l’espace de travail Databricks.

En conclusion, le endpoint SQL de Databricks est une approche intéressante pour éviter le coût d’un serveur de base de données dans une architecture décisionnelle. Il sera pertinent de préparer au maximum les données dans le metastore Hive pour profiter pleinement de la puissance de l’API Spark SQL et du cluster de calcul. Le petit avantage du SQL endpoint sur la connexion directe à un cluster Databricks (voir cet article) consiste dans la séparation des tâches entre clusters :

  • interactive cluster pour les travaux quotidiens d’exploration et de modélisation des Data Scientists
  • job cluster pour les traitements planifiés, de type batch
  • SQL endpoint pour la mise à disposition des données vers un outil de visualisation

Orchestrer des jobs multi-tâches avec Databricks

Le cluster Spark managé que propose Azure Databricks permet l’exécution de code, présent dans des notebooks, dans des scripts Python ou bien packagés dans des artefacts JAR (Java) ou Wheel (Python). Si l’on imagine une architecture de production, il est nécessaire de disposer d’un ordonnanceur (scheduler) afin de démarrer et d’enchaîner les différentes tâches automatiquement.

Nous connaissions déjà les jobs Databricks, nous pouvons dorénavant enchaîner plusieurs tasks au sein d’un même job. Chaque task correspond à l’exécution d’un des éléments cités ci-dessus (notebook, script Python, artefact…). Comme il s’agit à ce jour (mars 2022) d’une fonctionnalité en préversion, il est nécessaire de l’activer dans les Workspace Settings de l’espace de travail.

Le principal intérêt d’enchainer des tasks à l’intérieur d’un même job est de conserver le même job cluster, qui se différencie des interactive clusters par… sa tarification ! En effet, celui-ci est environ deux fois moins cher qu’un cluster utilisé par exemple pour des explorations menés par les Data Scientists.

Le menu latéral nous permet de lancer la création d’un nouveau job (pensez à bien le nommer dans la case située en haut à gauche).

Pour réaliser nos premiers tests, nous utiliserons un job cluster de type “single node”, associée à une machine virtuelle relativement petite.

Le job se planifie à un moment donné, défini par une interface graphique ou une syntaxe classique de type CRON telle que : 31 45 20 * * ?

Syntaxe CRON : 31 45 20 * * ?

Nous pourrons retrouver tous les logs d’exécution dans l’interface “jobs” de l’espace de travail.

Passer des informations d’une tâche à une autre

L’enchainement de tâches nous pousse rapidement à imaginer des scénarios où une information pourrait être l’output d’une tâche et l’input de la suivante. Malheureusement, il n’existe pas à ce jour de fonctionnalité native pour répondre à ce besoin. Nous allons toutefois explorer ci-dessous deux possibilités. Il existe également quelques variables réservées pouvant être passées en paramètres de la tâche : job_id, run_id, start_date…

Passer un DataFrame à une autre tâche

Nous nous orienterons tout naturellement vers le stockage d’une table dans le metastore de l’espace de travail Databricks.

  • écriture d’un DataFrame en table : df = spark.read.parquet(‘dbfs:/databricks-datasets/credit-card-fraud/data/’)
  • lecture de la table : df = spark.table(“T_credit_fraud”)

Ceci ne peut être réalisé avec une vue temporaire, créée au moyen de la fonction createOrReplaceTempView(), celle-ci disparaissant en dehors de l’environnement créé pour son notebook d’origine.

Passer une valeur à une autre tâche

Nous profitons du fait que le job cluster soit commun aux différentes tâches pour utiliser le point de montage /databricks et y écrire un fichier. Nous pouvons ainsi exploiter les commandes dbutils et shell (magic command %sh) :

  • dbutils.fs.mkdirs() permet de créer un répertoire temporaire
  • dbutils.fs.put() écrit une chaîne de texte dans un fichier

Nous vérifions ici la bonne écriture de la valeur attendue mais la commande shell cat ne permet pas de stocker le résultat dans une variable. Nous utiliserons une commande Python open() en prenant soin de préfixer par /dbfs le chemin du fichier créé lorsla tâche précédente. Attention également à bien récupérer le premier élément de la liste lines avec [0] puis à convertir la valeur dans le type attendu.

Quand continuer à utiliser Azure Data Factory ?

Dans une architecture data classique sous Azure, et orientée autour des outils PaaS, c’est le service Azure Data Factory qui est généralement utilisé comme ordonnanceur.

Si l’enchainement des tâches peut se conditionner en utilisant le nom des tâches précédentes, celui-ci n’aura lieu qu’en cas de succès des autres tâches.

Il n’est donc pas possible de prévoir un scénario similaire à celui présenté ci-dessous, où l’enchainement se ferait sur échec d’une étape précédente.

Les déclenchements de jobs Databricks ne se font que sur une date / heure alors qu’il serait possible dans Azure Data Factory de détecter l’arrivée d’un fichier, par exemple dans un compte de stockage Azure, puis de déclencher un traitement par notebook Databricks.

Template “Transformation with Azure Databricks”

Côté fonctionnalité de Data Factory, nous attendons que les activités Databricks puissent exécuter également des packages Wheel, comme il est aujourd’hui possible de le faire pour les JAR. Une solution de contournement consistera à utiliser la nouvelle version de l’API job de Databricks (2.1) dans une activité de type Web.

En conclusion, il s’agit là d’une première avancée vers l’intégration dans Databricks d’un nouvel outil indispensable à une approche DataOps ou MLOps, même si celui-ci est pour l’instant incomplet. Les fonctionnalités de Delta Live Tables, encore en préversion également, viendront aussi compléter les tâches traditionnellement dévolues à un ETL.

Webhooks pour MLFlow Registry

Une nouvelle fonctionnalité est disponible dans Databricks, en public preview, depuis février 2022 : les webhooks liés aux événements MLFlow. Il s’agit de lancer une action sur un autre service, déclenchable par API REST, lors d’un changement intervenant sur un modèle stocké dans MLFlow Registry (changement d’état, commentaire, etc.).

Cette fonctionnalité n’a rien d’anecdotique car elle vient mettre le liant nécessaire entre différents outils qui permettront de parfaire une approche MLOps. Mais avant d’évoquer un scénario concret d’utilisation, revenons sur un point de l’interface MLFlow Registry qui peut poser question. Nous disposons dans cette interface d’un moyen de faire évoluer le “stage” d’un modèle entre différentes valeurs :

  • None
  • Staging
  • Production
  • Archived

Ces valeurs résument le cycle de vie d’un modèle de Machine Learning. Mais “dans la vraie vie”, les étapes de staging et de production se réalisent sur des environnements distincts. Les pratiques DevOps de CI/CD viennent assurer le passage d’un environnement à un autre.

MLFlow étant intégré à un workspace Azure Databricks, ce service se retrouve lié à un environnement. Bien sûr, il existe quelques pistes pour travailler sur plusieurs environnements :

  • utiliser un workspace Azure Databricks uniquement dédié aux interactions avec MLFlow (en définissant les propriétés tracking_uri et registry_uri)
  • exporter des experiments MLFlow Tracking puis enregistrer les modèles sur un autre environnement comme décrit dans cet article

Aucune de ces deux approches n’est réellement satisfaisante car manquant d’automatisation. C’est justement sur cet aspect que les webhooks vont être d’une grande utilité.

Déclencher un pipeline de release

Dans Azure DevOps, l’opération correspondant au déploiement sur un autre environnement se nomme “release pipeline“.

Pour simplifier la démonstration, nous définissions ici une pipeline contenant uniquement un tâche de type bash, réalisant un “hello world”.

Dans un cas d’utilisation plus réalise, nous pourrons par exemple déployer un service web prédictif contenant un modèle passé en production, grâce à une ressource comme Azure Machine Learning.

Nous vérifierons par la suite qu’une modification dans MLFlow Registry déclenche bien une nouvelle release de cette pipeline.

Précisons ici un point important : les webhooks Databricks attendent une URL dont l’appel sera fait par la méthode POST. Il existe une API REST d’Azure DevOps qui permettrait de piloter la release mais le paramétrage de celle-ci s’avère complexe et verbeux.

Nous passons donc un ordonnanceur intermédiaire : Azure Logic App. En effet, cette ressource Azure nous permettra d’imaginer des scénarios plus poussés et bénéficie également d’une très bonne intégration avec Azure DevOps. Nous définissons le workflow ci-dessous comprenant deux étapes :

  • When a HTTP request is received
  • Create a new release
Veillez à bien sélectionner la méthode POST.

Le workflow nous fournit alors une URL que nous utiliserons à l’intérieur du webhook. Il ne nous reste plus qu’à définir celui-ci dans Databricks. La documentation officielle des webhooks se trouve sur ce lien.

Il existe deux manières d’utiliser les webhooks : par l’API REST de Databricks ou bien par un package Python. Nous installons cette librairie databricks-registry-webhooks sur le cluster.

Depuis un nouveau notebook, nous pouvons créer le webhook, tout d’abord dans un statut “TEST_MODE”, en l’associant à un modèle déjà présent dans MLFlow Registry.

Ceci nous permet de tester un appel par la commande .test_webhook() qui attend en paramètre l’identifiant du webhook préalablement créé. Une réponse 202 nous indique que cette URL est acceptée (bannissez les réponses 305, 403, 404, etc.).

Le webhook peut se déclencher sur les événements suivants :

  • MODEL_VERSION_CREATED
  • MODEL_VERSION_TRANSITIONED_STAGE
  • TRANSITION_REQUEST_CREATED
  • COMMENT_CREATED
  • MODEL_VERSION_TAG_SET
  • REGISTERED_MODEL_CREATED

Le webhook peut ensuite être mis à jour au statut ACTIVE par la commande ci-dessous.

Il ne reste plus qu’à réaliser une des actions sélectionnées sur le modèle stocké dans MLFlow Registry et automatiquement, une nouvelle release Azure DevOps se déclenchera.

Maintenant, il n’y a plus qu’à laisser libre cours à vos scénarios les plus automatisés !

Ci-dessous, le schéma résumant les différentes interactions mises en œuvre.

Synchroniser un groupe Azure AD avec un groupe Databricks

Si vous travaillez à plusieurs sur Azure Databricks, vous vous êtes sûrement déjà confronté.e.s à des réflexions sur les droits à accorder à chacun.e, selon les profils : data analyst, data scientist, data engineer, ML engineer… Rappelons qu’une gestion fine des droits ne sera possible qu’en licence dite Premium.

Il est évident qu’un administrateur de l’espace de travail ne souhaitera pas gérer des autorisations nominatives et ainsi, devoir reprendre la gestion à droits à chaque arrivée ou départ sur un projet ! Nous souhaitons donc nous référer à des groupes et c’est justement une notion bien intégrée dans l’annuaire Azure Active Directory (AAD).

Databricks dispose également d’une gestion de groupes d’utilisateurs dans sa console d’administration.

Malheureusement, il n’est pas possible d’utiliser un alias de groupe AAD dans Databricks et ces deux notions de groupes ne sont pas synchronisées entre elles !

Nous pourrions aussi imaginer un déploiement Terraform qui viendrait décortiquer un groupe AD et ajouter chaque utilisateur au moyen d’une boucle (voir ce post Medium) mais cette démarche semble vraiment trop lourde à mettre en œuvre et à maintenir…

Un outil vient à notre secours : Azure Databricks SCIM Provisioning Connector et c’est mon collègue Hamza BACHAR qui me l’a soufflé.

Nous commençons par enregistrer cet outil en tant qu’enterprise application dans Azure AD.

Il s’agit d’un produit développé par la société Databricks Inc, ce qui est plutôt rassurant.

Il est recommandé de renommer l’application en intégrant le nom de la ressource Databricks car il faudra autant d’enregistrements d’applications que de workspaces.

L’application est maintenant enregistrée dans notre abonnement Azure et dispose de deux identifiants : application ID et object ID.

Nous cliquons maintenant sur le menu Provisioning qui va nous permettre d’associer l’application avec un workspace Databricks. Le mode de provisioning doit être basculé sur Automatic.

Nous devons fournir ici deux informations :

  • l’URL du workspace à laquelle sera ajoutée le point de terminaison de l’API SCIM
  • un Personal Access Token qui aura été généré depuis les user settings de Databricks

Nous testons la connexion puis clic sur “Save” pour conserver le paramétrage.

Nous laissons par défaut les autres options disponibles.

Le provisioning peut maintenant être démarré.

Il ne reste plus qu’à positionner des users ou groups directement dans cette application pour qu’ils soient automatiquement synchronisés avec la console d’administration Databricks ! Il est toutefois nécessaire d’attendre quelques minutes (cela peut aller jusqu’à 40 minutes). On privilégiera les “groupes de sécurité” plutôt que de type “M365” et les “nested groups” ne sont pas aujourd’hui supportés.

Il est aussi possible de “provisionner à la demande” un utilisateur.

Un écran vient ensuite confirmer que l’action a bien été réalisée et l’utilisateur est directement ajouté au workspace.

A noter que le nouvel utilisateur est restreint sur ses droits, par défaut.

Il n’est pas administrateur du workspace et se trouve restreint sur la création de nouveaux clusters.

EDIT

Il est nécessaire qu’une personne ayant le droit d’enregistrement d’applications réalise la première manipulation. Mais ensuite, d’autres personnes pourront avoir besoin d’utiliser cette interface.

Nous allons donc autoriser d’autres utilisateurs au “self-service” de cette application. Il faut tout d’abord activer le “single sign-on”, en mode “linked”.

Fournir pour cela l’URL de connexion.

On paramètre enfin le menu Self-service.

Si l’on souhaite plutôt ajouter des users ou des groups de manière programmatique, on pourra se pencher sur ce code PowerShell.

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.

Automated ML pour le forecasting de séries temporelles sous Databricks

Si vous parcourez régulièrement ce blog, vous avez déjà lu des articles par ici ou par évoquant l’approche “automated ML” (force brute élargissant le spectre plus traditionnel de l’hyperparameter tuning), en particulier pour les séries temporelles. J’ai également évoqué le package Prophet, mis à disposition par la recherche de Facebook dans un précédent billet. Ce sont tous ces éléments que nous allons retrouver dans une nouvelle fonctionnalité de Databricks, liée au Runtime 10 !

En upgradant le Runtime (version ML) de notre cluster, nous pouvons enfin accéder à la troisième entrée dans la menu déroulant “ML problem type“.

Aucune différence avec les deux autres approches, nous choisissons un jeu de données enregistré comme une table dans le metastore puis nous désignons les colonnes importantes de ce dataset :

  • une colonne cible (target)
  • une colonne de type date ou timestamp
  • d’éventuels “time series identifiers” si nous disposons d’index complémentaires à la date

Pour cette démonstration, nous partirons d’un jeu de données sur la qualité de l’air en 2004 et 2005, disponible sur ce lien.

Nous précisions l’horizon de prévision (pour rappel méthodologique, ne vous aventurez pas au-delà du tiers de votre historique, il faut savoir raison garder !). Celui-ci s’entend à partir de la dernière date du jeu de données.

La configuration avancée permet de choisir la métrique d’arrêt parmi des métriques adaptées à la problématique de forecasting.

C’est parti, laissons maintenant notre cluster travailler !

Voici le résultat de l’expérience enregistrée dans notre espace de travail Databricks.

Le premier run n’est pas encore un forecast puisqu’il s’agit d’une exploration des données (mais vous l’aviez réalisée au préalable, rassurez-moi !).

Nous y trouvons des informations importantes :

  • plage de dates
  • indicateurs de centrage et dispersion sur la variable cible
  • nombre de valeurs manquantes

Un diagramme en ligne termine cette rapide analyse.

Passons maintenant aux notebooks de forecasting.

Nous pouvons trier les runs selon leur performance sur les différentes métriques et observer les valeurs des paramètres utilisés. A ce jour (novembre 2021), seuls deux paramètres sont proposés pour un algorithme basé sur le modèle de décomposition de Prophet :

  • interval_width : seuil de confiance (1 – risque d’erreur) utilisé pour l’intervalle de prévision
  • holiday_country : intégration des jours fériés du pays comme composantes dans la décomposition de la série temporelle

Nous allons maintenant explorer en détail le notebook, généré automatiquement, qui a réalisé la meilleure performance.

Nous retrouvons les paramètres saisies dans l’interface graphique. Si nous souhaitons les modifier, autant le faire maintenant dans le notebook !

Les données sont tout d’abord chargées puis transformées pour correspondre au standard attendu par Prophet, à savoir une colonne date nommée “ds” et une cible nommée “y”.

Afin de n’avoir qu’une seule ligne par unité de temps, un agrégat est réalisé, en s’appuyant sur une fonction average. Si votre logique est différente, vous pouvez modifier cette fonction, puis relancer le notebook, ou bien préparer différemment les données en amont. Le Spark dataframe obtenu ainsi sera ensuite converti en pandas dataframe pour être soumis à Prophet.

Une première étape consiste à entrainer le modèle puis à l’évaluer par cross validation.

Il sera possible ici de modifier le code ISO du pays utilisé pour la définition des jours fériés.

La validation croisée utilise un découpage définis dans la variable cutoffs, se basant sur des multiples de l’horizon de prévision. C’est cette opération qui sera la plus longue.

Les hyperparamètres disponibles sont définis comme suit. Il s’agit principalement de tester différentes pondérations pour les différents éléments composant le modèle (changepoint, seasonality, holidays).

La recherche des meilleures valeurs des hyperparamètres sera assurée par hyperopt et sa classe SparkTrials. Le modèle final sera entrainé sur l’historique complet (ce qui permet de tenir compte des inflexions de tendance des derniers jours), avec les valeurs donnant la meilleure métrique d’évaluation.

La fin du notebook réalise les opérations de sauvegarde du modèle puis quelques représentations graphiques de l’horizon de prévision, réalisés avec Plotly, ce qui donne des options d’interaction avec le visuel.

L’exécution du notebook étant redirigée vers MLFlow, nous disposons d’une page dédiée au modèle où nous retrouverons paramètres, tags, métriques et surtout l’artefact au format binaire sérialisé pickle, accompagné des dépendances nécessaires (requirements).

En conclusion, rien de révolutionnaire puisque nous disposions déjà de toutes ces fonctionnalités (dès 2020 avec l’arrivée de Prophet) mais ce qui modifie considérablement les choses est d’aboutir à un premier résultat au bout de quelques dizaines de minutes de calcul, après “quelques clics” dans une interface graphique (sous réserve que vos données soient déjà préparées !). C’est l’approche dédiée à ce que l’on nomme communément “citizen data scientist” où une compétence de programmation n’est pas nécessaire. Pour autant, nous disposons bien de code à la fin de l’expérience et c’est là la principale force de ce que propose Databricks. Nous pouvons reprendre la main sur ce code et surtout l’intégrer dans une démarche d’industrialisation (versioning, tests, intégration et déploiement continus, etc.).

Nous gagnons donc quelques heures à quelques jours de développement de la part d’un.e data scientist, selon son niveau d’expérience. Nous allons rapidement prouver la valeur et le potentiel prédictif de nos données, ce qui permettra de définir plus sereinement les investissements et le ROI attendu.

Enfin, n’oublions pas qu’il n’y a pas que Prophet dans le monde des time series et du forecasting. Mais gageons que les équipes de Databricks feront évoluer la liste des méthodes comparées au sein de cette fonctionnalité d’automated ML. En attendant, allez jeter un coup d’œil sur kats, toujours issu des équipes de Facebook Research.

L’Automated ML est un accélérateur, ne le voyez surtout pas comme une baguette magique qui vous dispenserait de data scientists. Une fois un premier jet (très rapidement) obtenu, il sera nécessaire d’améliorer, simplifier, interpréter, monitorer, etc.

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).

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.

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.