S’inscrire au service OpenAI sous Azure

Est-il besoin de présenter la société Open AI dont le modèle GPT3 connaît une renommée planétaire, suite à la mise en service de ChatGPT ?

Au delà du buzz, des exemples humoristiques ou de la recherche des erreurs (souvent dans des cas d’utilisation pour lesquels il n’a pas été entrainé), nous disposons dorénavant d’un accès professionnel aux modèles d’Open AI sous Azure, et ce sous le statut de general availability (GA), c’est-à-dire avec tout le support et garantis de service (SLA) attendus.

Une recherche de “openAI” dans la barre du portail Azure nous donne accès à la création de notre première ressource Azure OpenAI. Il faut remarquer ici que ce service est catégorisé comme un service cognitif, services qui représentent l’intelligence artificielle “appliquée” au sein des services Azure.

Un descriptif du service est donné, citant ses principales fonctionnalités (résumé, génération de contenu ou de code) :

Enable new business solutions with OpenAI’s language generation capabilities powered by GPT-3 models. These models have been pretrained with trillions of words and can easily adapt to your scenario with a few short examples provided at inference. Apply them to numerous scenarios, from summarization to content and code generation.

Azure portal

A ce jour, le modèle GPT est disponible ainsi que CODEX qui s’exprime au travers de GitHub Copilot. La génération d’images grâce au modèle DALL-E est encore en préversion (preview) sous Azure.

Avant de pouvoir réellement accéder à la création du service, un avertissement est donné :

Azure OpenAI Service is currently available to customers via an application process. Request access to Azure OpenAI Service.

Un formulaire sera nécessaire pour obtenir le droit de créer une ressource Azure OpenAI. Au bout d’un délai de quelques jours, vous serez informés de l’approbation ou du rejet de votre demande. Nous allons ici détailler quelques-unes des 35 questions posées afin de bien comprendre les cas d’usage autorisés et les garde-fous posés par Microsoft.


Description des cas d’usage

Please explain how you will use Azure OpenAI Service in your application.

  • Please explain the data you will use,
  • how you plan to use the models,
  • how people will consume or interact with the outputs,
  • and more details about the domain or industry in which you will use the application.

PLEASE PROVIDE AT LEAST 5+ SENTENCES. IF YOUR USE CASE IS TOO SHORT OR TOO VAGUE, YOU WILL BE DENIED.

Il s’agit tout d’abord de décrire l’usage qui sera fait du service Azure OpenAI, sur un principe de “bout en bout” : données en entrée, modèle(s) utilisé(s) et interactions avec l’utilisateur. Le cas d’usage doit être suffisamment détaillé et il convient de préciser le domaine ou le secteur d’activités concerné, même si ce dernier point fera l’objet de la question suivante.

Ce paragraphe est particulièrement important et vous devez démontrer qu’une réflexion a déjà été élaborée autour de l’application que vous souhaitez développer. Lorsque vous achetez des outils dans un magasin de bricolage, vous avez sans doute déjà une idée de ce pour quoi vous allez les utiliser !

Domaine(s) d’utilisation

Applications in these domains may require additional mitigations and will be approved only if the customer demonstrates that the risks associated with the application are well-managed and outweighed by the beneficial uses.

Le terme à retenir ici est celui de mitigation (atténuation) que l’on emploie dans l’expression “bias mitigation” pour éviter la correction des biais possibles d’un modèle d’apprentissage. Outre la détection des biais, des actions devront être entreprises pour éviter l’effet néfaste qu’ils pourraient avoir sur les utilisateurs. Des librairies spécifiques existent pour cela comme le produit Open Source FairLearn, développé par Microsoft.

Les différents domaines “à risque” ou dits encore “à enjeux élevés” sont :

  • Law enforcement, legal, and criminal justice
  • Healthcare and medicine Government and civil services, such as essential private and public services Politics
  • Financial services and banking Social media
  • Management and operation of critical infrastructure
  • Pollution and emission management and control
  • Migration, asylum, and border control management
  • Education, vocational training, hiring, and employment, such as applications in consequential decision making that impacts one’s opportunities
  • Therapy, wellness, relationship coaching or forecasting, such as relationship advice or bots for companionship, emotional support, or romance
  • Military or intelligence
  • Other scenario that could have a consequential impact on legal position, life opportunities, or result in physical or psychological injury to an individual if misused
  • None of the above. The domain, industry, or scenario do not have the potential to have a consequential impact on legal position, life opportunities, or result in physical or psychological injury to an individual if misused

Il conviendra de cocher “None of the above” si aucun de ces domaines n’est concerné.

Fonctionnalités attendues

Il serait tentant de tout cocher dans cette question 26 ! En effet, vous avez sûrement beaucoup d’idées d’utilisation des services d’OpenAI mais il faut ici se limiter à ceux qui seront réellement utiles à votre cas d’usage décrit ci-dessus. Il est peu probable qu’un agent conversationnel (chatbot), dans un scénario d’entreprise, propose des images générées par DALL-E ! Soyez donc raisonnables sur les fonctionnalités demandées et si besoin, remplissez plusieurs formulaires, en isolant les applications.

Fonctionnalités spécifiques de l’agent conversationnel

Si vous avez coché la case “Conversational AI” à la question 26, vous devez préciser les fonctionnalités attendues pour l’agent conversationnel.

Attention à nouveau si vous prévoyez de déployer ce bot dans un domaine “à enjeux élevés”.

Acceptation des conditions d’utilisation

Enfin, il sera nécessaire d’approuver explicitement les conditions d’utilisation (“Yes, I agree“) énoncées dans les questions 29 à 35. C’est tout particulièrement sur l’usage en production que vous allez devoir vous engager.

Question 29

29. I understand that mitigations should be considered early in development and must be implemented prior to production.

N’attendez pas d’être en production pour atténuer les biais !

Question 30

30.My application will ensure human oversight prior to production.

This includes never automatically posting generated outputs and never automatically executing generated code. This may also include clearly disclosing AI’s role, communicating relevant limitations to stakeholders (including developers and end users), making sure people (e.g., end users) have a role in decision-making, highlighting inaccuracies in generated outputs, and letting people edit generated outputs.

Ce point nous alerte sur des chaines de CI/CD trop automatisées : un contrôle humain est nécessaire. (Si vous me connaissez bien, vous m’avez déjà entendu pester contre le Continuous Training :))

Question 31

31.My application will implement strong technical limits on inputs from end users and outputs from the system prior to production.

This increases the likelihood your application will perform as expected and decreases the likelihood it can be misused beyond its intended purpose. This may include limiting the length of inputs and outputs, exposing the service to end users through a front end, requiring that inputs and outputs follow a specific structure, returning outputs only from validated source materials, implementing blocklists or content filtering, and implementing rate limits.

En production, un contrôle fort sur les entrées et les sorties sera essentiel. Il s’agit par exemple d’éviter tout détournement de l’usage intial prévu. Ainsi, au démarrage de ChatGPT, il était possible de contourner certaines de ses limites en lui demandant de jouer un rôle.

Question 32

32.I will test my application thoroughly prior to production to ensure it responds in a way that is fit for the application’s purpose.

This includes conducting adversarial testing where trusted testers attempt to find system failures, undesirable behaviors such as producing offensive content, and ways that application can be misused by malicious actors beyond its intended purpose.

Non, tester n’est pas douter ! Ici, il s’agira même d’essayer de “hacker” votre propre application.

Question 33

33.My application will establish feedback channels for users and impacted groups prior to production.

This includes providing ways to report problematic content and misuse such as building feedback features into the user experience and providing an easy to remember email address for feedback submission. 

A minima, votre application devra donner un contact simple, par exemple par email, aux utilisateurs qui souhaiteraient faire part de leur réaction. Au mieux, vous pourrez penser une vraie boucle de feedback (human feedback loop), qui vous servira à termes à améliorer le modèle et l’expérience utilisateur.

Question 34

34.My application will follow the Microsoft guidelines for responsible development of conversational AI systems prior to production.

Prenez connaissance des principes pour une IA responsable, donnés par Microsoft.

Question 35

35.I will resubmit this form for a production review before going into production.

Avant le passage en production, et surtout si des changements sont apparus par rapport à l’expression du cas d’usage intial, il sera nécessaire de soumettre à nouveau le formulaire.

Maintenant que vous connaissez les conditions à remplir, vous voilà prêts à décider si l’expérience Azure OpenAI est une opportunité pour vous et votre organisation !

Exploiter le pool Spark d’Azure Synapse Analytics

Nous avons vu dans cet article d’introduction la dualité de Synapse Analytics entre SQL et Spark. Nous développons ici les aspects liés à Apache Spark, framework Open Source de calcul distribué, et recommandé pour le traitement de la Big Data (volume mais aussi vélocité et variété).

Apache Spark pool

L’exploration des données dans un notebook va requérir l’utilisation d’un pool Apache Spark pour exécuter les commandes, comme l’annonce le message d’alerte ci-dessous.

Please select a Spark pool to attach before running cell!

Nous naviguons dans le menu Manage pour créer un nouveau pool ou retrouver les pools existants.

Un pool correspond à un cluster (grappe) de plusieurs nodes (nœuds) pour lequel nous définissons le nombre de nœuds et leur “taille” (size) correspondant aux propriétés RAM, CPU des machines virtuelles.

La facturation de ce service dépend bien évidemment de ces deux paramètres, qui pourront être modifiés une fois le pool créé.

Je vous recommande de n’activer que l’autoscaling que si les charges de travail sont très variables, dans un sens comme dans l’autre. Débutez sur un petit nombre de nœuds que vous augmenterez au fur et à mesure, en vous assurant que le code écrit est bien en capacité de se distribuer sur les différents nœuds.

Dans le paramétrage additionnel, nous allons trouver la version principale du framework Spark et les différentes versions des langages associés.

Sur cette boîte de dialogues, nous retrouvons aussi la configuration de pause automatique, enclenchée par défaut et paramétrée pour un arrêt au bout d’une inactivité (aucun job en exécution) de 15 minutes.

Langages disponibles

Dans le notebook, nous pourrons utiliser plusieurs langages.

Il est même possible de changer de langage selon les cellules du notebook, à l’aide des commandes magiques comme %%sql, %%csharp, etc. mais cela ne doit pas être une pratique à pérenniser dans le cadre de travaux de production.

Au lancement de la première commande Spark, une nouvelle session débute et cela peut prendre quelques minutes.

La librairie mssparkutils

Un des premiers besoins va être de communiquer avec les services du workspace comme les bases SQL et les systèmes de fichiers de type data lake. Pour accéder à des services liés comme un data lake, nous allons nous appuyer sur une librairie pré-installée : mssparkutils pour Microsoft Spark Utilities. La documentation officielle est disponible sur ce lien. Cet outil est très proche de dbutils de Databricks.

Nous allons nous concentrer en particulier sur les commandes liées au file system (fs) : list, copy, move, rm, put…

Nous commençons par “monter” le data lake par défaut afin d’accéder aux fichiers. Nous utilisons pour cela le modèle de code ci-dessous.

mssparkutils.fs.mount( 
    "abfss://<containername>@<accountname>.dfs.core.windows.net", 
    "</mountname>", 
    {"linkedService":<linkedServiceName>} 
)

A noter que le nom attribué au point de montage doit débuter par un caractère /.

La commande mounts() permet de vérifier les points de montage existants et si besoin, unmount(<mountname>) s’utiliserait pour “démonter” ce point.

Attention, si nous utilisons la syntaxe seule du point de montage pour faire une lecture, nous obtenons une erreur de type PathNotFound. En effet, une première différence apparait ici pour les utilisateurs habitués au dbutils de Databricks.

Nous devons constituer un chemin débutant par le mot clé synfs, suivant de l’identifiant du job, qui se trouve être une variable d’environnement.

Prenez soin de variabiliser ces éléments car le job id changera à chaque démarrage d’une nouvelle session !

Les points de montage ne sont malheureusement pas pérennes ! A chaque nouvelle session, il sera nécessaire de relance la commande mount.

Le résultat de la commande fs est une liste et les différentes propriétés des fichiers peuvent être isolées par du code :

for file in files:
    print(file.name, file.isDir, file.isFile, file.path, file.size)

Les commandes Python de la librairie os peuvent également utiliser ce point de montage. Attention, la syntaxe du path change légèrement avec la disparition du symbole : et l’ajout d’un / avant synfs.

Réalisons maintenant quelques tâches courantes.

Lister et compter tous les fichiers des sous-répertoires

Une simple recherche Google nous montre que la première problématique avec cette librairie mssparkutils est la recherche récursive dans les répertoires !

Heureusement, la recherche nous amène rapidement à découvrir cet excellent repository GitHub : Recursively listing Data Lake files with `display` implemented · GitHub (donnez-lui une étoile :)).

L’exécution imbriquée des deux fonctions deep_ls et convertfiles2df permet d’obtenir un dataframe Pandas avec la liste des fichiers, leur path et la taille en octets.

Nous affichons une synthèse à l’aide du print ci-dessous.

print(f"{df.shape[0]} files for a total size of {df['size'].sum()} bytes")
Afficher les premières lignes d’un fichier .csv ou .parquet

La commande mssparkutils.fs.head() répond à ce besoin.

Toutefois, il est difficile d’interpréter les sauts de ligne et de faire par exemple la différence entre les noms de colonne et les valeurs.

Nous allons nous faire aider ici par la génération automatique de syntaxe, en clic droit sur un nom de fichier ou de dossier.

Nous savons que la première ligne constitue les entêtes du fichier.

La commande df.printSchema() va nous montrer les types de colonnes. Venant d’un fichier .csv, il n’est pas étonnant de ne retrouver que des chaines de caractères (string).

En appliquant l’option inferSchema = True, nous pouvons demander au moteur de déterminer le type le plus probable de chaque colonne.

Il est important de rappeler que le format de fichier .parquet conserve les types de colonnes, nous privilégierons donc ce format pour le stockage de données dite “raffinées”.

Utiliser des packages supplémentaires

Les packages disponibles par défaut ne sont généralement pas suffisants pour traiter tous les aspects d’un projet de Data Engineering ou de Data Science. Nous allons donc charger de nouvelles librairies au moyen d’un fichier local requirements.txt.

Le fichier liste tout simplement les packages, avec, idéalement, leur numéro de version.

great-expectations==0.15.44
xgboost==1.7.3

Par défaut, l’installation de packages pour une session n’est pas activée.

Difficile de réaliser une vérification de l’installation autrement qu’en réalisant une commande d’import du package voulu.

Convertir le contenu d’une table SQL en un dataframe

Nous pouvons établir une connexion JDBC à toute base de données mais ici, nous allons de nouveau profiter, pour un scénario en lecture seule, générer le code Spark dans un notebook.

La méthode spark.read.synapsesql() est mise à profit pour créer un Spark Dataframe en mémoire. Attention, il s’agit d’une commande Scala.

Cette ressource de formation donne également un exemple d’écriture d’un dataframe vers la base de données.

Pour utiliser au mieux Spark et l’API pyspark, je vous recommande la certification Apache Spark developer associate, décrite dans cet article.

Voici un schéma récapitulatif des commandes disponibles avec la librairie MSSparkUtilities.

Découvrir toutes les fonctionnalités d’Azure Synapse Analytics

Azure Synapse Analytics (ASA) a remplacé depuis quelques temps Azure SQL Data Warehouse. Mais au delà d’une base SQL de type MPP (“massive parallel processing“), nous avons maintenant accès à des fonctionnalités offrant une vision “bout en bout” des projets data.

Une fois la ressource Azure provisionnée, nous pouvons lancer le Synapse Studio, dans une fenêtre de navigateur Web.

Le studio présente un menu latéral qui permettra de naviguer entre les différents outils composant Azure Synapse Analytics.

Dans ce premier article introductif, nous allons nous focaliser sur les trois menus Data, Integrate et Develop. Nous traiterons par la suite les aspects de Management et de Monitoring. Nous terminerons cette série sur les bonnes pratiques collaboratives : versioning et intégration / déploiement continus.

Data: le stockage

Si les bases de données (BDD) sont présentes dans notre quotidien depuis plusieurs décennies, le Data Lake (qui reste fondamentalement un système de fichiers…) reste plus récent mais est porté par la dynamique autour des formats de fichiers optimisés pour l’analytique (Parquet, Delta, Iceberg…) et autorisant les transactions (insert, update, delete), opérations qui restaient jusque-là la prérogative des BDD.

Ces deux approches vont se retrouver au sein des services internes à ASA ou en lien (“linked“).

Workspace

De manière “intégrée” dans l’espace de travail (workspace) Synapse, nous retrouvons les bases de données SQL MPP (ex SQL DWH).

Tous les objets classiques d’une BDD sont disponibles (tables, vues, procédures stockées…) et nous ferons un focus sur la notion de tables externes.

La ressource SQL dite “dédiée” apparaît également dans le groupe de ressources Azure. C’est une ressource dont la performance et l’espace de stockage disponible dépendront du niveau choisi (entre DW100c et DW30000c) et qui pourra être arrêtée ou démarrée. Sa facturation sera fonction de ces éléments.

Les tables externes : un fichier “vu” en SQL

Grâce aux tables externes, nous allons pouvoir interroger un système de fichier (Blob Storage, Data Lake) à l’aide d’instructions SQL. Le langage SQL étant si répandu dans le monde professionnel de la data, il s’agit là d’une fonctionnalité particulièrement intéressante.

Création d’une table externe (script automatiquement généré)

Un article dédié sera nécessaire pour décrire les cas d’usage des tables externes, différencier tables externes et vues, ainsi que noter les différences entre les deux pools SQL (dédié et serverless).

La nouveauté (2022) : le lake database

Nous avons vu qu’il est désormais facile d’interroger un fichier comme si l’on utilisait une table (plutôt une vue non matérialisée) d’une base de données. Une difficulté va tout de même se présenter lorsqu’il s’agira de bien appréhender le dictionnaire des données (tous les champs disponibles, associés à leur type respectif) ainsi que les relations entre les différents fichiers (au sens des cardinalités : un à plusieurs, plusieurs à plusieurs, etc.).

Le Lake Database vient proposer une solution à cette problématique, en proposant de mieux visualiser toutes les métadonnées, tout en conservant un stockage de type fichier. De manière sous-jacente, ce sont les ressources SQL Pool serverless ou Spark Pool qui seront utilisées.

Nous créons pour cela une base de type lake, en définissant le container de stockage des données (input folder) ainsi que le format à utiliser (.csv ou .parquet, Delta à venir).

Nous pouvons maintenant utiliser l’interface visuelle pour définir des tables:

  • personnalisées (custom), c’est-à-dire en renseignant chaque champ manuellement
  • depuis des modèles (template) proposés
  • depuis des fichiers du Data Lake

Visitons la galerie de modèles, ceux-ci couvrent de nombreux domaines fonctionnels.

Par domaine, nous disposons d’un grand nombre de schémas de tables, pour lesquelles les clés primaires et étrangères sont déjà déterminées. Nous obtenons donc déjà des relations dans le modèle en cours de création.

Les tables (vides pour l’instant) sont maintenant visibles dans le menu latéral de navigation.

Il sera alors nécessaire de remplir les tables, soit par des instructions SQL INSERT, soit au moyen de l’outil de mapping présenté dans la documentation officielle.

Un bon cas d’usage consiste à mapper les champs du Dataverse utilisé par les produits de la Power Platform. En effet, nous découvrirons ci-dessous la fonctionnalité Synapse Link qui permettra une synchronisation des données en quasi temps réel.

Linked

Les ressources liées sont par définition extérieures à la ressource Synapse Analytics. On utilisera ici principalement un ou plusieurs Data Lake Azure (de génération 2 uniquement) dont les différents file systems seront visibles.

Les notions bronze, silver et god correspondent à l’approche dite “medallion”.

La liste des services externes est présentée ci-dessous.

Il n’est étonnant de retrouver le stockage Blob car le Data Lake de génération 2 n’est autre qu’un Blob Storage augmenté par un “hierarchical namespace“. Nous retrouvons également deux APIs de la base Cosmos DB : l’API native pour le NoSQL et l’API pour MongoDB. Nous écartons donc les APIs PostgreSQL, Cassandra ou Gremlin.

Les integration datasets listeront toutes les sources ou destinations (sink) des pipelines de données qui seront abordés dans le prochain paragraphe. Ainsi, pour une source de type file system, nous disposerons des formats de fichiers suivants:

Integrate : les pipelines “no code”

L’intégration de données consiste à faire entrer dans un environnement de destination des données issues d’une ou plusieurs sources, et ce, à des intervalles de temps réguliers ou sur la détection d’événements (par l’exemple, l’arrivée de fichiers dans un data lake déclenche leur intégration en base de données).

Pipeline

Nous voici dans un univers bien connu des utilisateur.ices d’Azure Data Factory, même si la parité des fonctions n’est pas encore totalement établie (janvier 2023).

Un pipeline est composé d’une ou plusieurs activités, s’exécutant en parallèle ou bien s’enchaînant selon des règles définies.

L’activité de base est la copie de données (copy data) et nous retrouvons également trois activités permettant d’ordonnancer d’autres éléments propres à ASA : les jobs Spark et les procédures stockées SQL.

Synapse Link Connection

Azure Synapse Link est un outil permettant un transfert rapide de données vers une base SQL dédiée (SQL dedicated pool) depuis des sources comme :

  • Azure SQL DB
  • SQL Server 2022 (on premises)
  • Azure Cosmos DB (APIs NoSQL, Cosmos DB & Gremlin)
  • Dataverse

L’objectif est ici de réaliser, “near real time“, des travaux analytiques depuis les données enregistrées dans des bases dont l’usage est plutôt transactionnel. Il n’est plus nécessaire ici de déployer des pipelines ou jobs d’intégration.

Copy Data Tool

Il s’agit sans doute de la manière la plus simple pour copier des données d’une source à une destination (sink).

Ici, il n’est pas envisageable d’ajouter des transformations plus poussées lors de la copie. Nous retrouvons toutefois les propriétés de déclenchement :

  • unique
  • planifié
  • sur des fenêtres glissantes (tumbling windows)

Develop : l’approche “full code”

Les différents scripts compatibles avec les éléments de ASA sont présentés ci-dessous.

Les data flows pourraient sembler être un intrus dans cette liste car c’est bien une interface de type “no code” qui va permettre de les développer. La justification tient sans doute dans le fait que les data flows sont convertis en langage Spark lors de leur exécution.

De même, les jobs Apache Spark fonctionnent à partir de fichiers .JAR qui ne seront pas développés directement dans le studio ASA.

Les scripts SQL travaillent sur les bases de données de type SQL Pools (serverless ou dedicated).

Les scripts KQL (KustoQL) travaillent uniquement sur les bases de données Azure Data Explorer.

Les notebooks s’exécutent grâce à un Pool Spark et peuvent interagir avec les données, qu’elles soient sous forme de fichiers ou intégrées dans les bases de données de type SQL (et non KQL), à l’aide d’un connecteur JDBC.

Nous devons tout d’abord instancier un pool Apache Spark, tout comme nous avons dû créer un pool SQL dédié. Seul le pool SQL serverless est présent par défaut dans le workspace ASA.

Le pool est une grappe (cluster) de plusieurs machines virtuelles (nodes), puisque Spark est un framework distribué.

La version de Spark est modifiable, elle conditionne les versions des langages sous-jacents (Python, Scala, Java, .NET, R…)

La propriété “automatic pausing” est intéressante afin de réduire les coûts d’utilisation mais nous verrons, dans un prochain article, qu’elle implique une gestion fine des sessions Spark.

Premier exemple “de bout en bout”

Prenons l’exemple d’un fichier .csv “brut” qui a été uploadé dans le container Bronze du Data Lake.

Notre premier objectif sera de stoker le fichier dans un format optimisé comme Parquet dans le container Silver. Nous avons pour cela plusieurs possibilités.

Lire avec OPENROWSET

Un clic droit sur un fichier ou un répertoire propose les options suivantes.

Voici le code automatiquement généré par un “Select TOP 100 rows”.

Il est nécessaire d’ajouter l’option HEADER_ROW = TRUE afin de considérer la première ligne du fichier comme l’en-tête. Les autres bulk options sont données dans cette documentation officielle.

Pipeline de copie de CSV vers Parquet

Il s’agit ici de réaliser une copie conforme de la source mais nous allons utiliser pour cela l’outil “copy data tool” car l’objectif est de changer de format de fichier. Nous définissons donc une activité de copie dans un pipeline de type Azure Data Factory.

Le mapping permet de définir le type de données contenues dans chaque champ. C’est en effet une propriété des fichiers .parquet que de pouvoir stocker cette information.

Pipeline de copie du fichier Parquet dans une table

Posons l’objectif de matérialiser les données au sein de la BDD. Nous allons donc utiliser le pool SQL dédié. En effet, dans sa version serverless, nous ne faisons que créer des métadonnées facilitant l’interprétation des fichiers comme des vues.

Nous faisons de nouveau appel à une activité de copie dans un pipeline de type Azure Data Factory. Nous allons laisser le soin à l’activité de copie de créer directement le schéma de table attendu. Pour cela, nous choisissons le mode “Bulk insert” et l’option “Auto create table”.

Nous pouvons maintenant visualiser la table de destination dans le pool dédié et l’interroger au moyen du langage SQL.

En guise de conclusion, et avant de rentrer plus en détails dans certains aspects de cette ressource Azure, nous avons montré ici la polyvalence de l’outil pour les scénarios analytiques :

  • à partir de fichiers de données ou de tables contenues dans une base de données
  • avec la simplicité de la syntaxe SQL directement sur le contenu des fichiers
  • dans une démarche no code ou full code, selon les compétences des utilisateurs.

Automatiser le CLI v2 d’Azure Machine Learning

Partons à la découverte des commandes en ligne (CLI) en version 2 pour Azure Machine Learning ! En effet, cette manière de travailler devient incontournable dans toute démarche MLOps, c’est-à-dire avec un objectif d’industrialisation des projets de Machine Learning.

Nous allons ainsi suivre les premières étapes permettant de lancer un job Azure ML dans un workspace. Nous pourrons tout d’abord le faire depuis un laptop, puis ensuite, au sein d’un pipeline Azure DevOps.

Le repository GitHub officiel de Microsoft donne des très bons exemples pour débuter dans l’utilisation du CLI. Nous clonons donc ce répertoire localement afin de commencer la mise en pratique.

Installer le CLI Azure ML v2

Depuis une fenêtre de commande (par exemple, un terminal Bash dans Visual Studio Code), nous lançons la commande de mise à jour du CLI Azure.

az upgrade

Nous pouvons ensuite réaliser l’installation de la nouvelle extension ml, en prenant soin de désinstaller au préalable le CLI dans sa première version.

az extension remove -n azure-cli-ml
az extension remove -n ml
az extension add -n ml -y

Afin de vérifier que l’extension est bien fonctionnelle, nous pouvons demander l’affichage de l’aide.

az ml -h

Si l’installation a échouée, le message d’erreur suivant apparaîtra.

az ml : 'ml' is misspelled or not recognized by the system

Voici un résumé des versions utilisées pour cet article.

Nous pouvons maintenant voir le workspace déjà existant dans un resource group.

az ml workspace list -g rg-azureml-demo

Dans ce workspace, nous pouvons provisionner, toujours par le CLI, une ressource de calcul de type cluster.

az ml compute create -n cpu-cluster --type amlcompute --min-instances 0 --max-instances 4 --size Standard_DS3_V2 -g rg-azureml-demo --workspace-name azuremldemo

Enfin, nous lançons un job (ce terme regroupe les experiments et leurs runs) défini par un fichier YAML.

az ml job create -f jobs/basics/hello-world.yml --web -g rg-azureml-demo --workspace azuremldemo

Nous allons regarder de plus près la structure de ce fichier YAML.

$schema: https://azuremlschemas.azureedge.net/latest/commandJob.schema.json
command: echo "hello world"
environment:  
  image: library/python:latest
compute: azureml:cpu-cluster

Il s’agit donc de lancer une commande dans un environnement, spécifié par une image Docker, sur une ressource de calcul (compute). Pour des actions nécessitant des entrées et sorties, les deux termes inputs et outputs s’ajouteront dans le fichier.

Le lancement du job s’effectue correctement.

Nous pouvons maintenant suivre son exécution dans le portail Azure ML.

Les user_logs indiquent le résultat du job : hello, world !

Il faut repérer ici le nom du job : eager_arch_nc8hdvj6kp

Pour ne pas avoir à préciser le groupe de ressources ainsi que le nom du workspace, nous pouvons configurer des valeurs par défaut. Le paramètre location correspond à la région Azure d’installation du workspace. Vérifiez l’orthographe exacte dans le menu Overview de la ressource, sur le portail Azure.

az configure --defaults group=$GROUP workspace=$WORKSPACE location=$LOCATION

Vérifiez alors les valeurs par défaut à l’aide de la commande suivante.

az configure -l -o table

La commande d’archivage du job, appelé par son nom, fait disparaître celui-ci de l’interface graphique.

az ml job archive -n eager_arch_nc8hdvj6kp

Pour un job plus proche des travaux de Machine Learning, je vous recommande d’exécuter la commande suivante, toujours liée aux fichiers du repo cloné initialement.

az ml job create -f jobs/pipelines/nyc-taxi/pipeline.yml --web --resource-group rg-azureml-demo --workspace-name azuremldemo

Exécuter le CLI v2 depuis un pipeline Azure DevOps

A partir d’un nouveau pipeline, nous allons nous appuyer sur le code présenté dans la première partie de cet article.

Il est important de comprendre que l’environnement d’exécution sera un agent, c’est-à-dire une machine virtuelle dans Azure, avec, par défaut, une distribution Linux Ubuntu. Cet agent ne connaît donc pas le CLI Azure, ni son extension pour le Machine Learning.

Dans une logique d’automatisation, nous souhaitons maintenant lancer ces commandes au travers d’un pipeline Azure DevOps. Nous avons au préalable poussé (“push“) tout le code vers le repository d’un nouveau projet Azure DevOps.

Nous choisissons ensuite un starter pipeline qui nous permettra de coller les commandes CLI v2 testées dans la première partie de cet article.

La tâche (task) Azure CLI va permettre d’obtenir la syntaxe YAML adaptée dans le fichier.

Nous choisissons les paramètres suivants pour chaque tâche.

Un service connection est nécessaire.

Au commit sur la branche main, le pipeline s’exécute.

Succès ! Le code s’est correctement exécuté et nous pourrons le vérifier dans le workspace Azure Machine Learning.

Ce cours proposé par Microsoft Learn vous permettra d’approfondir l’utilisation des pipelines avec le CLI v2 d’Azure ML.

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 >

NLP avec automated ML dans Azure Machine Learning

Jusqu’ici réservée aux domaines supervisés (régression & classification) ainsi qu’aux séries temporelles, l’automated ML de Microsoft s’ouvre maintenant aux données de type texte ou image pour entrainer des modèles de NLP ou de Computer Vision destinés à des tâches de classification, et ceci au travers de l’interface utilisateur d’Azure Machine Learning.

Nous allons ici réaliser un premier entrainement pour une problématique simple de classification de spams / non spams (ou hams), à partir d’un dataset connu pour débuter sur cette problématique.

Ce jeu de données aura été préalablement déclaré en tant que dataset, au sens d’Azure ML et nous prendrons soin de le découper en amont de l’expérience d’automated ML. Il nous faut donc deux datasets enregistrés : le jeu d’entrainement et le jeu de test. En effet, l’interface graphique ne nous proposera pas (encore ?) de séparer aléatoirement les données soumises. Notons enfin que ces données doivent être enregistrées au format tabulaire. Nous devons donc a minima disposer de deux colonnes : un label (spam / ham) et le texte en lui-même.

L’entrainement va nécessiter une machine virtuel (compute instance ou compute cluster) disposant d’un GPU. Attention, le coût de ces VMs est naturellement plus élevé que celui de VMs équipées de CPU.

Le premier écran de l’interface se configure de la sorte.

Nous choisissons ensuite une tâche de type “Multi-class classification”.

Si celle-ci est unique, il est recommandé de préciser la langue du texte contenu dans le dataset.

Attention au temps maximum de recherche du meilleur modèle, celui-ci est par défaut de 24h ! Et nous savons que le GPU coûte cher…

Nous finissions le paramétrage en indiquant le dataset de test.

Un seul modèle a été ici évalué : un modèle de type BERT.

En observant les outputs de ce modèle, nous retrouvons le binaire sérialisé (.pkl) ainsi que des fichiers définissant l’environnement d’entrainement et les dépendances de librairies nécessaires. C’est ici un standard MLFlow qui est respecté.

Toujours au moyen de l’interface graphique, nous pouvons maintenant déployer ce modèle sous forme de point de terminaison prédictif.

Nous allons opter ici pour un Managed Online Endpoint (MOE), qui offre un niveau de management des ressources plus fort que le service Kubernetes d’Azure.

Ce Management Online Endpoint s’appuie sur des ressources de calcul qui sont simplement des VMs Azure. A noter qu’une ressource spécifique Azure sera bien visible au travers du portail, dans le groupe de ressources contenant le service Azure ML.

Il est maintenant possible d’interroger ce point de terminaison !

Voici en quelques clics dans l’interface d’Azure Machine Learning comment nous avons pu parvenir à un service Web prédictif. Bien sûr, la préparation de données sera indispensable dans un cas réel d’utilisation de l’automated ML pour une tâche de NLP.

Enfin, sachez que le SDK Python d’Azure ML dispose de la classe permettant d’effectuer cette tâche par du code (v1 et v2).

Utiliser AKS comme attached compute pour Azure ML

Azure Machine Learning donne la possibilité de supporter les calculs de Machine Learning par des ressources extérieures et différentes des machines virtuelles classiques de l’on retrouve dans les compute instances ou compute clusters.

Nous allons voir dans cet article comment mettre à disposition un attached compute de type Kubernetes. Pour une ressource Azure Kubernetes Service déjà existante dans la souscription Azure, nous allons avoir besoin d’ajouter une extension Azure Machine Learning sur le cluster k8s. Cette installation ne se fait que par l’intermédiaire des commandes en ligne (az cli).

Depuis le portail Azure, il est possible de lancer une fenêtre Cloud Shell déjà connectée, sous le login de l’utilisateur. Vérifions les versions installées.

Si k8s-extension n’est pas présent ou n’est pas à jour (version minimale : 1.2.3, il faut l’installer par la commande ci-dessous.

az extension update --name k8s-extension

Nous allons avoir besoin d’ajouter une identité managée sur la ressource Azure Kubernetes Service, ce qui se fait par la commande suivante.

az aks update --enable-managed-identity --name <aks-name> --resource-group <rg-name>

Comme indiqué dans la documentation, il faut tout d’abord inscrire les différents fournisseurs.

Nous pouvons surveiller le bon déroulement des inscriptions, tous les statuts doivent passer sur “Registered”.

Nous pouvons maintenant passer à l’ajout de l’extension.

Nous choisissons dans la documentation disponible sur ce lien la syntaxe adaptée à AKS (manageClusters) pour un test rapide (à ne pas utiliser pour un environnement de production).

az k8s-extension create --name azureml --extension-type Microsoft.AzureML.Kubernetes --config enableTraining=True enableInference=True inferenceRouterServiceType=LoadBalancer allowInsecureConnections=True inferenceLoadBalancerHA=False --cluster-type managedClusters --cluster-name <aks-name> --resource-group <rg-name> --scope cluster

Vérifions la bonne installation de l’extension avec la commande suivante.

az k8s-extension show --name azureml --cluster-type managedClusters --cluster-name centralizedaks --resource-group rg-centralized-registry

Attention à bien lire la sortie dans le détail, l’extension pourrait être créée mais sans que l’installation n’ait correctement abouti.

Il est maintenant possible d’attacher le service AKS dans l’interface graphique d’Azure Machine Learning.

Les namespaces disponibles sont visibles depuis le portail Azure.

Vous pouvez maintenant vous pencher sur les différents cas d’usage qu’offre cet attached compute, présentés dans cet article publié par Microsoft :

Realizing Machine Learning anywhere with Azure Kubernetes Service and Arc-enabled Machine Learning – Microsoft Tech Community

Dataïku DSS et Azure Synapse Analytics : connexion à la base de données

Nous avons présenté dans un précédent article la plateforme de Data Science de Dataïku et son installation sur une VM dans Azure. Nous avons également déjà abordé la connexion à un compte de stockage Azure, afin de lire des fichiers de données ou d’écrire les différents résultats obtenus.

Dans un Système d’Information non centré autour du Data Lake, certaines données particulièrement intéressantes pour le Machine Learning peuvent être stockées dans les tables d’une base de données, souvent orientée “analytique”. Le Data Warehouse d’Azure, Synapse Analytics, est disponible dans les connexions prévues par Dataïku DSS. Nous allons détailler ici les actions à réaliser pour définir la connexion à la base de données. Un grand merci à mon collègue Benjamin BENITO qui a réalisé les opérations techniques décrites ci-dessous.

Dans les paramètres de Dataïku DSS, nous pouvons trouver la liste des différentes bases de données SQL compatibles avec le Studio de Data Science.

Nous choisissons Azure Synapse. Il faut noter, que dans une exploitation plus large des capacités de Synapse Analytics, il sera aussi possible d’utiliser cette ressource comme moteur de calcul Spark. Nous verrons ceci dans un prochain article.

Dedicated SQL Pool

Une base de données de type “dedicated SQL pool” aura été préalablement créée du côté de Synapse Analytics.

Pour la démonstration, nous disposons d’une table à une colonne, contenant une valeur.

Nous pouvons maintenant renseigner les différents éléments attendus pour la définition de la connexion. Commençons par cocher la case “Use custom JDBC URL”.

L’URL JDBC se trouve sur le portail Azure, à la page de la ressource Dedicated SQL Pool, onglet “connection strings“. Nous utiliserons ici la première version, utilisant la connexion SQL.

L’URL JDBC se détaille comme ci-dessous.

jdbc:sqlserver://<synapse-name>.sql.azuresynapse.net:1433;database=<db-name>;user=<admin-login>@<synapse-name>;password={your_password_here};encrypt=true;trustServerCertificate=false;hostNameInCertificate=*.sql.azuresynapse.net;loginTimeout=30;

Il faudra remplacer ici le texte {your_password_here} par le mot de passe du compte administrateur.

Ces informations, ainsi que le port ouvert (1433) pourront également être précisées dans les cases disponibles. Il ne reste plus alors qu’à tester la connexion à l’aide du bouton situé en bas à gauche de l’écran. Je vous recommande de créer la connexion (bouton “create”) au préalable afin de sauvegarder les informations saisies.

Il est alors fort probable que vous soyez confronté.e.s à l’erreur ci-dessous.

Cette erreur est due à l’absence de driver SQL Server sur la VM où est installé Dataïku DSS. L’URL suivante détaille cette erreur.

ERR_SQL_CANNOT_LOAD_DRIVER: Failed to load database driver — Dataiku DSS 9.0 documentation

Nous allons donc nous connecter à cette machine virtuelle Linux pour effectuer manuellement l’installation du driver attendu. Nous devons disposer pour cela d’un login / password ou d’une clé SSH.

Voici les différentes commandes à jouer successivement.

wget -O /tmp/sqljdbc_10.2.1.0_enu.zip https://download.microsoft.com/download/4/d/5/4d5a79be-35f8-48d4-a984-473747362f99/sqljdbc_10.2.1.0_enu.zip
unzip /tmp/sqljdbc_10.2.1.0_enu.zip -d /tmp/
sudo cp /tmp/sqljdbc_10.2\enu/mssql-jdbc-10.2.1.jre8.jar /home/dataiku/dss/lib/jdbc/

sudo su dataiku
dataiku/dss/bin/dss stop
dataiku/dss/bin/dss start

Les premières commandes réalisent le téléchargement du binaire nécessaire, le dézippe puis le copie à l’endroit attendu. Un restart de DSS est ensuite nécessaire.

La connexion est maintenant établie !

Nous pouvons utiliser le lien “Import tables to datasets” pour obtenir des données stockées dans le Data Warehouse.

Serverless SQL Pool

Pour utiliser le mode serverless de Synapse Analytics, nous devrions nous orienter vers un autre type d’authentification. En effet, ce mode correspond à la création de méta-données sur des fichiers présents dans un Data Lake mais sans matérialisation dans le Data Warehouse. Des droits sous-jacents (sur les fichiers du Data Lake) sont donc nécessaires et il n’est pas possible d’attribuer de tels droits au user connu uniquement de la base de données.

Le message d’erreur rencontré sera alors :

Failed to read data from table, caused by: SQLServerException: External table 'dbo.<external_table>' is not accessible because location does not exist or it is used by another process.

La documentation officielle de Dataïku donne toutes les étapes permettant d’utiliser l’authentification au travers d’Azure Active Directory.

Nous avons besoin d’un Principal de Service, c’est-à-dire d’une identité dans Azure. Nous relevons tout d’abord l’application ID (ou client ID) qu’il sera nécessaire de renseigner dans Dataïku DSS.

Dans le menu “Endpoints”, nous notons l’information de l’OAuth 2.0 token endpoint (v1).

Dans le menu “API permisssions”, nous devons ajouter la permission Azure SQL Database.

Choisir ensuite “Delegated permissions” puis cocher la case user_impersonation.

Nous terminons le paramétrage de cette identité en lui associant un secret. Notez ici bien la valeur du secret, que vous ne pourrez plus retrouver par la suite.

Toujours dans le portail Azure, il faut donner les droits de lecture sur le Data Lake au principal de service. Cela se fait au moyen de l’ajout d’un RBAC de type “Blob storage data reader”.

Sinon, une erreur comme ci-dessous se produira lors de l’aperçu de la table ou de la vue :

Failed to read data from table, caused by: SQLServerException: File 'https://adlsdataiku.dfs.core.windows.net/dataiku/input/diamonds.csv' cannot be opened because it does not exist or it is used by another process.

Nous reprenons maintenant la définition d’une nouvelle connexion Synapse Analytics depuis le Data Science Studio.

En cochant la case “Use custom JDBC URL”, il faut donner une Connection URL de la forme suivante :

jdbc:sqlserver://<synapse-name>-ondemand.sql.azuresynapse.net:1433;database=master;encrypt=true;trustServerCertificate=false;hostNameInCertificate=*.sql.azuresynapse.net;loginTimeout=30

Nous précisons le port (1433 par défaut) et le nom de la Database.

Pour la partie liée à l’Azure AD, il faut cocher la case “Login with Azure OAuth” puis coller les informations préalablement collectées :

  • STS URL (OAuth 2.0 token endpoint (v1))
  • Client id (application id)
  • Client secret (la valeur du secret et non le secret id)

Deux modes de “credentials” sont possibles : Global ou Per user. Ce second mode signifie que c’est le login utilisé dans DSS qui est repris pour s’authentifier auprès de la base Synapse. Il ne sera alors pas nécessaire de renseigner le client secret.

Depuis Synapse Analytics, dans une nouvelle requête SQL, nous allons déclarer le principal de service (SPN) comme un utilisateur de la base de données. Veillez à bien être connecté à “Built-in”, c’est-à-dire la base serverless.

Voici le code à exécuter, où le nom du SPN doit être mis à jour. Il est nécessaire d’attribuer des droits de type “BULK” puis “SELECT” sur les objets qui seront autorisés à Dataïku DSS.

CREATE USER [dataikuspn] FROM  EXTERNAL PROVIDER  WITH DEFAULT_SCHEMA=[dbo];
GO
GRANT ADMINISTER DATABASE BULK OPERATIONS TO dataikuspn;
GO
GRANT SELECT ON OBJECT::dbo.V_diamonds TO [dataikuspn];
GO

Il est maintenant possible d’ouvrir la liste des tables à importer depuis l’interface de DSS.

Ce second scénario s’avère intéressant pour limiter les coûts d’utilisation de Synapse Analytics (coût à la quantité de données lues) et facilite l’exploitation des données du Data Lake qui auront pu être réorganisées sous forme de vues SQL plus lisibles des utilisateurs de Dataïku.

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

Connecter Dataïku DSS et Azure Blob Storage

Suite à l’article initial sur l’installation de la sandbox Dataïku DSS, nous allons maintenant voir comment faire interagir le studio avec un compte de stockage Azure Blob Storage. Nous souhaiterons bien sûr lire des fichiers de données mais aussi écrire des données dite “raffinées” suite aux opérations de nettoyage et de transformation pouvant être réalisées par la plateforme de Dataïku.

Les tests relatés ci-dessous ont été réalisés avec ma collègue Sulan LIU.

Création d’un compte de stockage blob

Nous disposons d’un compte de stockage créé sur le portail Azure, dans lequel nous pourrons définir plusieurs conteneurs (containers). Nous verrons ci-dessous l’intérêt de disposer de plusieurs de ces répertoires physiques.

A partir du menu DSS settings puis Connections, nous pouvons cliquer sur le bouton “+NEW CONNECTION” et choisir le cloud storage Azure Blob Storage.

Nous allons maintenant détailler les principales options disponibles pour définir cette connexion.

Connection

La première information attendue est le nom que portera cette connexion du sein du Data Science Studio.

Nous pouvons ensuite choisir entre deux modes d’authentification vis à vis de la ressource Azure.

Le premier mode donne un accès total au compte de stockage au moyen du nom du compte de stockage et de la clé d’accès du compte de stockage. Selon les données contenues sur ce stockage, ce mode de connexion ne sera sans doute pas recommandé.

Nous pourrons préférer le mode de connexion “OAuth from App” correspondant à un principal de service déclaré dans l’annuaire Azure Active Directory.

Après la création du principal de service (menu “app registration”), il faut créer un secret associé.

Il sera bien sûr nécessaire de donner à cette identité des droits “Storage Blob Data Contributor” sur la ressource de stockage Azure.

Path restriction and default container

Une connexion peut être définie vers la totalité des containers ou avec une restriction sur l’un d’eux. Une bonne pratique me semble être de définir une connexion par conteneur, afin de bien isoler les rôles (données brutes en entrée, données préparées intermédiaires, prévisions en batch…).

Si l’on ne définit pas de conteneur dans la connexion, il est impératif qu’un conteneur par défaut existe. Sans modification de la valeur par défaut, celui-ci doit être nommé dataiku (sans tréma sur le i).

Security settings

Les paramètres de sécurité vont enfin permettre de donner des droits d’utilisation à tous les utilisateurs “analysts” ou bien seulement à des groupes.

Une bonne pratique consistera à ne donner l’autorisation “Details readable” qu’à un groupe administrateur de la plateforme. En effet, ce droit donne accès à la configuration de la connexion et donc par exemple à l’App ID (client ID) du principal de service.

Lecture et écriture de jeux de données

Nous pouvons maintenant démarrer un nouveau projet au sein du Data Science Studio et nous commençons logiquement par la définition d’un premier dataset.

Nous vérifions que la connexion précédemment définie apparaît bien dans la première liste déroulante.

Il sera ensuite nécessaire de cliquer sur le bouton FETCH LIST pour voir apparaître les containers autorisés.

Le bouton BROWSE permet alors de choisir le fichier voulu.

Un message nous informe du bon parsing (séparation des colonnes) du fichier.

Un aperçu du jeu de données (PREVIEW) est alors disponible et nous pouvons voir que les colonnes ont été automatiquement typées. Un rapide contrôle visuel sera toutefois souhaitable. Nous pouvons par exemple rencontrer une colonne dont les premières valeurs (celles de l’aperçu) seraient vides.

Nous pouvons maintenant construire un premier flow basé sur ce jeu de données.

Nous ajoutons quelques opérations de transformation au sein d’une “recipe“.

A la fin de la préparation des données, nous définissions l’output dataset. Dans la liste déroulante “Store into”, nous retrouvons naturellement la connexion définie au compte de stockage.

Vous pourriez, à cette étape, rencontrer le message d’erreur ci-dessous.

Dataset error – Failed to check if the new dataset name is safe, there could be a problem with the database: Failed to get information for location '', caused by: NoSuchElementException: An error occurred while enumerating the result, check the original exception for details., caused by: StorageException: The specified container does not exist.

Ce message nous alerte sur le fait que le container nommé “dataiku” n’a pas été créé. Veuillez vérifier ce point, présenté en tout début d’article.

Nous vérifions que le dataset s’est bien enregistré dans le container du compte de stockage.

Nous remarquons que le dataset, enregistré au format CSV et compressé (.gz), est partitionné automatiquement. Nous disposons de trois choix de format, le format Avro étant un format dit orienté colonne et également compressé.

Dans la version sandbox de Dataïku DSS, nous ne pourrons en revanche pas utiliser une ressource de type Azure Synapse Analytics pour lire et écrire des données. En effet, un driver JDBC est nécessaire mais les répertoires de la machine virtuelle ne sont pas accessibles. Nous aborderons donc la version Enterprise de Dataïku DSS dans de prochaines articles.