Job d’entrainement de modèle : nouvelle expérience guidée

Avec un peu de pratique des services Azure Machine Learning, vous vous apercevrez qu’un mode de développement idéal peut se dérouler comme suit :

  • travailler dans un IDE (par exemple, Visual Studio Code) depuis votre poste
  • exécuter et tester le code localement
  • ajouter des interactions au service Azure ML : charger un dataset, enregistrer un modèle, logguer des métriques d’évaluation…
  • exécuter le code à distance et à l’échelle des données complètes sur une ressource de calcul (compute cluster)
  • éventuellement, intégrer ce code au sein d’un objet pipeline qui sera planifiable (scheduling)

Nous pouvons alors écrire un canevas global, basé sur le SDK azureml, qui prendra en entrée le script Python réalisant des traitements ou l’entrainement d’un modèle.

Dans une optique de simplification de ce processus, nous découvrons (septembre 2021) une fonctionnalité dans la home du portail Azure Machine Learning : “train a model“.

Vous pouvez d’ailleurs vous tenir informé.e.s des nouveautés d’Azure Machine Learning sur ce lien officiel.

Nous allons retrouver cette nouveauté dans le menu déroulant “Create new“.

Cliquons sur “Job (preview)” pour ouvrir la fenêtre ci-dessous.

Comme pour toute autre tâche, nous devons choisir une ressource de calcul : compute cluster, compute instance ou Kubernetes.

A la deuxième étape, nous choisissons un environnement d’exécution parmi les environnements prédéfinis, un environnement custom créé préalablement ou bien une image Docker stockée dans un Container Registry.

Nous choisissons ici un environnement disposant du SDK azureml, de la librairie Scikit-Learn et quelques packages supplémentaires.

Troisième étape, nous allons soumettre un script Python contenant l’entrainement d’un modèle. Nous choisissons comme exemple un script présent dans les “samples” de code, au niveau du menu notebooks.

Pour soumettre le fichier .py, nous pouvons réaliser un upload depuis le poste local ou pointer vers un compte de stockage Azure.

Pensez à donner un nouveau nom d’expérience à la place de “Default”.

Une ligne de commande va lancer le script sur l’environnement défini. Nous écrivons tout simplement l’instruction python suivie du nom du fichier .py.

Nous pourrions nous arrêter ici pour faire marcher cette démonstration mais jetons tout de même un œil sur les options disponibles au bas de cet écran.

Il est possible d’ajouter un ou plusieurs inputs au script :

  • un dataset enregistré sur Azure ML
  • un autre fichier local (par exemple, un autre script Python contenant des dépendances)
  • un chemin vers le blob storage ou le file share par défaut

Nous retrouvons donc des actions qui demanderaient l’usage du SDK azureml à l’intérieur du script initial.

L’écran Review résume enfin les paramètres choisis. Attention, le nom du job ne pourra plus être utilisé !

*

Une fois le job lancé, nous pourrons suivre son exécution dans le menu Experiment. Les logs indiquent ci-dessous les sorties prévues dans le code et des modèles au format pickle sont disponibles dans le répertoire outputs.

En synthèse, Microsoft semble nous mettre sur la voie d’une utilisation plus poussée de l’UI (interface utilisateur) au détriment du SDK azureml qui représente une marche supplémentaire dans l’industrialisation du Machine Learning.

Tout l’intérêt de cette fonctionnalité résidera dans le fait de pointer sur un dataset important (déclaré dans Azure ML) et de réaliser les calculs avec une ressource puissance (plus puissante que notre laptop !).

Il manque peut-être à ce jour une capacité à planifier voire ordonnancer les jobs mais des nouveautés seront sans doute bientôt annoncées.

Dans une optique d’industrialisation, il sera toujours plus intéressant de disposer du code “de bout en bout” afin d’en gérer le versioning, la répétabilité ou encore le déploiement entre workspaces mais nous gagnerons ici un temps précieux dans la phase cruciale de preuve de valeur des algorithmes.

Forecasting par automated ML (UI)

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

  • régression
  • classification
  • forecasting

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • normalized RMSE
  • R2
  • normalized MAE

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

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

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

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

Trois paramètres additionnels de configuration sont ensuite disponibles.

Ils correspondent respectivement à :

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Nous disposons de trois types pour ces décorateurs :

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

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

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

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

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

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

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

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

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

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

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

Modification de la méthode de prévision

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

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

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

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

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

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

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

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

[[0.27303021717776266, 0.7269697828222373]]

En conclusion

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

Le notebook complet sera disponible dans ce repository.

Créer une ressource Azure Machine Learning

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

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

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

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

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

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

Décrivons rapidement ces services :

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Associer Azure Databricks et Azure Machine Learning

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

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

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

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

Le SDK azureml sous Databricks

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

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

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

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

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

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

from azureml.core import Workspace

ws = Workspace.from_config()

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

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

from azureml.core import Workspace

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

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

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

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

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

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

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

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

Databricks comme attached compute

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

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

La ressource est maintenant correctement déclarée.

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

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

DatabricksCompute(workspace, name)

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

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

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

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

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

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

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

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

En conclusion

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

Différencier Azure Databricks et Azure Machine Learning

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

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

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

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

Les sources de données

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

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

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

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

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

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

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

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

Les scripts et leur exécution (calcul)

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

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

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

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

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

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

Le versionning des modèles

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

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

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

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

Vous risquez aussi le message d’erreur suivant :

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

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

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

from azureml.core.model import Model

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

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

L’exposition des modèles

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

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

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

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

Ordonnancement

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

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

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

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

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

Versionning du code et cycle CI/CD

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

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

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

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

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

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

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

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

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

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

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

Coût et FinOps

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

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

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

Quelques points inclassables

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

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

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

Alors, lequel choisir ?

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

Interagir avec Azure ML depuis Visual Studio Code

Travailler avec une ressource Azure Machine Learning peut se faire de plusieurs façons : approche “no code” depuis le Concepteur, Automated ML ou encore par scripts R ou Python exploitant le SDK azureml qui permet d’interagir avec tous les composants du studio.

C’est bien sûr cette dernière approche “full code” que nous allons privilégier. Il est possible de lancer les notebooks natifs du studio mais ceux-ci ne donnent pas à ce jour (avril 2021) la même expérience que les notebooks Jupyter. Nous pouvons alors créer une instance de calcul qui se déploiera avec un serveur JupyterLab (ainsi que RStudio qu’il n’est pas impossible dans l’absolu d’utiliser… avec Python !).

Plusieurs limites apparaissent alors pour un usage professionnel, c’est-à-dire vérifiant un niveau de sécurité au travers de l’isolement des développements, de leur versionning et de leur déploiement en production. Si les instances de calcul sont personnelles (attribuées à un et un seul utilisateur, nominativement), les scripts enregistrés sont visibles par toutes les personnes disposant d’une ressource. De plus, il n’y a pas (à ce jour) de possibilité de lier les notebooks et autres fichiers à un dépôt (repository) de type Git. Enfin, si l’on pousse la réflexion sur le passage à l’échelle, il faudra envisager d’exécuter les scripts sur un cluster de calcul plutôt que sur l’instance de calcul elle-même.

Nous allons répondre à toutes ces problématiques par l’utilisation de l’IDE de Microsoft : Visual Studio Code. Au préalable, il faudra installer les extensions suivantes :

  • Python
  • Jupyter
  • Azure Account
  • Azure Machine Learning

En appuyant sur F1, nous pouvons nous authentifier sur Azure.

L’icône Azure donne alors la visibilité des ressources Azure Machine Learning et de tous les éléments qui les composent.

Nous souhaiterons en particulier utiliser un “compute cluster” pour exécuter un script Python. Nous allons pour cela créer un notebook d’interaction avec Azure Machine Learning qui pilotera l’exécution du script.

Pour assurer la gestion “Git” de nos fichiers, nous avons maintenant tous les outils à disposition. Il suffira d’utiliser les fonctions intégrées, par le menu ou en lignes de code, ce que nous développerons dans un autre article.

Les notebooks sont “suspects” pour VSC et il faudra déclarer qu’ils sont fiables (“trust“) à chaque ouverture. Pour simplifier ce processus, il est possible de modifier les paramètres pour accepter par défaut tous les notebooks.

Nous allons utiliser un serveur local Jupyter pour exécuter le code Python (et c’est une source d’économies !).

Les librairies spécifiques à Azure Machine Learning doivent bien sûr être installées dans cet environnement local.

!python -m pip install azureml-sdk --upgrade --user
!python -m pip install azureml-core --upgrade --user

Le package azureml-core évolue souvent, pensez à le mettre régulièrement à jour car les composants du studio Azure ML évolueront automatiquement.

Il faut maintenant nous authentifier vis à vis de ce service. Les informations nécessaires sont accessibles dans le fichier “config.json” téléchargeable depuis le portail Azure.

Nous donnons ces informations dans le code ci-dessous.

A l’exécution du code, une fenêtre va s’ouvrir dans un navigateur demandant de s’authentifier avec le compte Azure autorisé sur la ressource Azure ML. Une fois cette opération réalisée, la cellule du notebook est validée.

Ceci ne sera bien sûr pas envisageable dans une scénario tout automatisé et nous privilégierons alors l’authentification au travers d’un principal de service.

Nous pouvons vérifier la bonne connexion à la ressource avec l’instruction suivante :

print(ws.name, ws.location, ws.resource_group, ws.location, sep='\t')

Nous allons maintenant mettre en place les éléments qui permettront d’exécuter un script Python.

Ce schéma décrit le fonctionnement global :

  • VSC lance l’exécution (run) d’un script Python
  • exécuté sur un cluster de calcul
  • accompagné d’un environnement (les dépendances de packages)
  • loggé dans une expérience

Les éléments de script ci-dessous ont été piochés dans différents tutoriels disponibles sur le Web. Nous commençons par la cellule qui déclarer le cluster de calcul ou bien le crée à la volée s’il n’existe pas de cluster nommé de la sorte.

Les packages nécessaires au script Python sont déclarés dans un environnement.

Notez la dernière commande qui permet de sauvegarder cet environnement dans un fichier YAML.

env.python.conda_dependencies.save_to_file(".", "my_remote_env.yml")

Il sera alors possible de réutiliser directement ce fichier pour définir de nouveaux environnements. Voici son contenu.

Enfin, l’expérience s’instancie tout simplement, en relation avec l’espace de travail Azure ML.

Le script Python peut être écrit dans une cellule du notebook. Il pourra alors être donné en paramètre de l’objet ScriptRunConfig.

A l’intérieur du script Python, il sera certainement utile de retrouver la référence à l’exécution réalisée au sein de l’espace de travail. Ceci se fait en enchainant ces trois lignes de code, précédées de l’import de la librairie azureml-core.

from azureml.core.run import Run


run = Run.get_context()
exp = run.experiment
ws = run.experiment.workspace

La soumission du script au sein de l’expérience se fait alors par la commande ci-dessous.

run = exp.submit(config=src, tags=runtags)

Nous pouvons dès lors suivre l’évolution dans le studio Azure ML.

A noter que les widgets qui permettent de suivre l’exécution dans un notebook semblent ne pas être compatibles avec VSC.

Mais ce n’est là qu’un détail au regard de ce que nous gagnons à utiliser VSC : versionning du code, approche DevOps, diminution des coûts de développement.

Le notebook utilisé dans cet article est disponible sur ce repository.

Créer un custom role pour sécuriser les accès au portail Azure Machine Learning

Le service Azure Machine Learning offre de nombreuses fonctionnalités au travers de son portail accessible par l’URL https://ml.azure.com:

  • Notebooks Jupyter
  • Interface d’automated Machine Learning
  • Interface de conception visuelle de pipelines de Machine Learning
  • Gestion de ressources de calcul
  • Gestion des sources et des jeux de données
  • etc.

Dans une optique d’utilisation en entreprise, et donc en équipe, tout le monde ne bénéficiera pas de toutes ces fonctionnalités. En effet, il faut poser a minima des garde-fous concernant la gestion des ressources de calcul, car la facturation du service en dépend directement !

On pourra également souhaiter que seuls certains administrateurs soient en capacité de créer ou supprimer des sources de données. Bref, il faudra établir des profils types (“personae“) des utilisateurs et leur accorder le bon périmètre d’actions autorisées.

La gestion dite “Role Based Access Control” (RBAC) des ressources Azure permet de répondre à ce besoin. Dans le cas d’Azure Machine Learning, nous disposons de trois rôles prédéfinis, décrits dans la documentation officielle.

Extrait de la documentation Microsoft https://docs.microsoft.com/fr-fr/azure/machine-learning/how-to-assign-roles#default-roles

L’affectation d’un rôle se fait dans le menu “Access control (IAM)” de la ressource, depuis le portail Azure.

Mais prenons par exemple le scénario suivant. Une organisation fait travailler des Data Scientists dans un espace de travail Azure Machine Learning et souhaite qu’ils puissent :

  • Créer des jeux de données à partir de sources de données définies (et ne pas supprimer ces dernières)
  • Créer des expériences de Machine Learning par le biais du Concepteur, de l’Automated ML ou de notebooks
  • Solliciter des ressources de calculs de type “compute instance” ou “compute cluster” mais ne pas en créer de nouvelles
  • Publier des pipelines d’inférence sous la forme de points de terminaison (endpoint) temps réel ou par lot (batch)

Cette description ne correspond à aucun rôle prédéfini, nous allons donc passer par la création d’un rôle personnalisé ou “custom role“.

Ce rôle ne peut être créé que par le propriétaire de la ressource Azure Machine Learning et se fait au niveau de l’espace de travail contenant cette ressource.

Pour débuter plus simplement, nous commençons par cloner un des trois rôles existants :

Le menu suivant permet d’ajouter ou de retirer des permissions. La liste de celles-ci s’obtient en recherchant le terme “machine learning”.

Les (très nombreuses !) actions sont alors détaillées et souvent déclinées sous le forme : read / write / delete / other.

Veillez à bien préciser le périmètre d’application du rôle en notant l’identifiant de souscription suivi du nom du groupe de ressources où se trouve le service Azure Machine Learning.

Nous allons ensuite utiliser le langage JSON pour définir précisément chaque action.

{
"properties": {
"roleName": "Data Scientist",
"description": "Create and run experiments, publish endpoints, can't create or delete compute ressources and data sources",
"assignableScopes": [
"/subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX/resourceGroups/rg-sandbox"
],
"permissions": [
{
"Actions": [
            "Microsoft.MachineLearningServices/workspaces/*/read",
           ],
"NotActions": [
            "Microsoft.MachineLearningServices/workspaces/*/write",
            "Microsoft.MachineLearningServices/workspaces/*/delete",
              ],
"dataActions": [],
"notDataActions": []
}
]
}
}

Il faut comprendre que certaines actions sont “imbriquées” et il sera possible d’autoriser un niveau supérieur pour sous-entendre l’autorisation sur les niveaux inférieurs. C’est ainsi le rôle du caractère “*” dans la ligne ci-dessous.

 "Microsoft.MachineLearningServices/workspaces/*/read"

Voici le JSON complet répondant à notre scénario.

Ainsi, dans la partie Clusters de calcul, le bouton “Nouveau” a disparu mais l’utilisateur pourra toujours affecter un cluster à une expérience lancée par le Concepteur ou par le biais de l’Automated ML.

Mais nous verrons que ce rôle interdit l’utilisation d’instance de calcul, c’est-à-dire des machines virtuelles qui sont le support de l’usage de notebooks. Autant dire que les Data Scientists n’iront pas très loin sans cela ! (Sauf peut-être à travailler depuis Visual Studio Code et nous en reparlerons dans un prochain article…).

Nous cherchons donc à ajouter ces ressources de calcul, à l’aide des permissions cochées dans l’image ci-dessous.

Malheureusement il n’existe pas de droits différenciant les instances de calcul des clusters. Nous ne pouvons donc pas aboutir à un scénario où l’utilisateur pourrait créer son instance personnelle sans pour autant créer un grappe d’une dizaine de machines en GPU ! Une instance de calcul est dédiée à un et un seul utilisateur mais il est possible pour un administrateur de la ressource (profil Owner) de créer des VMs et de les affecter à des utilisateurs. Ce processus se fait au travers du déploiement d’un template ARM et est documenté sur ce lien.

Attention, même si l’utilisateur ne peut pas accéder à la VM, il pourrait tout de même avec ces autorisations la démarrer… et oublier de l’éteindre (pas d’arrêt planifié)… et donc faire augmenter la facturation !

EDIT : Les “notebook VM” ont été remplacées par les instances de calcul mais les droits associés figurent toujours dans la liste des permissions.

Text Analytics avec le concepteur (Designer) dans Azure Machine Learning

Les citizen data scientists disposent d’un outil graphique dans le portail Azure Machine Learning nommé Concepteur (ou Designer en anglais) permettant de réaliser des pipelines de Machine Learning, pour l’apprentissage comme pour l’inférence (c’est-à-dire le fonctionnement prédictif).

Depuis la première version de l’outil, anciennement nommé Azure Machine Learning Studio, et dit maintenant “classique”, des modules pour l’analyse textuelle étaient déjà présents mais l’offre s’est aujourd’hui, en ce début 2021, agrandie.

L’objectif de cet article est de faire un survol des méthodes disponibles, dans cette approche “low code“, pour un jeu de données contenant un champ de “texte libre”, et dans le but de réaliser une méthode d’apprentissage supervisé, et plus précisément de classification.

En prérequis, rappelons les notions indispensables avant de s’attaquer à notre problématique :

– disposer d’une ressource de calcul dite “compute cluster” qui servira à exécuter les différentes itérations du pipeline (succession de modules)

– créer une nouvelle expérience, qui permettra de suivre le résultat des exécutions ou runs (outputs, logs, artefacts, métriques…)

– connaître la structure classique d’un pipeline d’entrainement supervisé (s’inspirer des exemples disponibles, en particulier “Text Classification – Wikipedia SP 500 Dataset” qui est décrit sur ce site GitHub.

Vue d’ensemble des fonctionnalités de Microsoft Machine Learning Studio (classique)
Image reprise de la documentation officielle de Microsoft et téléchargeable ici : https://download.microsoft.com/download/C/4/6/C4606116-522F-428A-BE04-B6D3213E9E52/ml_studio_overview_v1.1.pdf

Nous allons nous concentrer ici sur deux phases qui auront leurs spécificités pour le traitement de données textuelles :

– le preprocessing

– le choix du modèle d’apprentissage pour une classification

Preprocessing des données textuelles

Nous allons utiliser ici le module Preprocess text qui réalisera de nombreuses actions de nettoyage et de préparation de nos données textuelles, en une seule étape.

Ce module est très puissant et basé sur la librairie de référence SpaCy mais ne supporte pour l’instant malheureusement que la langue anglaise. Nous pourrons contourner cela par un script R ou Python pour le cas de corpus en français. Mais à l’exception de la lemmatisation, les opérations pourront toutefois s’appliquer à un corpus d’une autre langue que l’anglais.

La case à cocher “Expand verb contractions” est toutefois spécifique à des formulations anglaises comme don’t, isn’t, I’ve, you’ll, etc.

Détaillons maintenant les autres options, avec quelques exemples simples si nécessaire.

– Retrait des mots outils (stop words) : a, about, all, any, etc.

Cette opération est paramétrable au moyen de la seconde entrée du module. Nous pouvons donc utiliser un fichier, d’une colonne, contenant les mots en français. Ce site propose une liste de mots outils dans de nombreuses langues.

Lemmatisation (remplacement du mot par sa forme canonique qui est “l’entrée du dictionnaire”, c’est-à-dire, l’infinitif ou le masculin singulier lorsqu’il existe) : la phrase “The babies are walking on their feet” donnera “The baby be walk on their foot” puis sans mots outils “baby walk foot“.

– Détection des phrases (séparées alors par |||, à choisir si l’on souhaite un traitement spécifique par phrase plutôt que par ligne du jeu de données)

– Normalisation en minuscules (sinon les mots sont considérés comme différents)

Suppression des nombres, caractères spéciaux (non alphanumériques), caractères dupliqués plus de deux fois, adresses email de la forme <string>@<string>, URLs reconnues par les préfixes httphttpsftpwww

– Remplacement des backslashes \ en slashes / : l’usage ne sera pas fréquent, on passera plutôt par la suppression de ces caractères spéciaux.

Séparation des tokens (ici, les mots) sur la base de caractères spéciaux comme & (pour l’esperluette, on préfèrera à nouveau la citer dans les caractères spéciaux à supprimer) ou - : un “pense-bête” deviendra “penser bête” suite à séparation et lemmatisation en français.

En pratique, il est important de maîtriser l’ordre dans lequel ces opérations s’appliquent. Ainsi, on réalisera plutôt les opérations de nettoyage (suppressions, minuscules, mots outils…) avant les opérations de lemmatisation.

Importer la librairie NLTK et les mots outils en français

Nous disposons d’un module Python permettant d’exécuter un script, sur un ou deux dataframes en entrée de ce module. Le code suivant nous permettra d’importer la librairie NLTK et d’utiliser la liste des mots outils ou bien la lemmatisation en français.

import pandas as pd

def azureml_main(dataframe1 = None, dataframe2 = None):
    import importlib.util
    package_name = 'nltk'
    spec = importlib.util.find_spec(package_name)
    logging.debug(spec)
    if spec is None:
        import os
        os.system(f"pip install nltk")

    import nltk
    from nltk.corpus import stopwords
    stopwords = set(stopwords.words('french'))

    def preprocess(text):
        clean_data = []
        for x in (text[:]):
            print(x)
            new_text = re.sub('<.*?>', '', x)   # remove HTML tags
            new_text = re.sub(r'[^\w\s]', '', new_text) # remove punc.
            new_text = re.sub(r'\d+','', new_text)# remove numbers
            new_text = new_text.lower() # lower case, .upper() for upper          
            if new_text != '':
                clean_data.append(new_text)
        return clean_data    

    dataframe1['preprocess'] = preprocess(dataframe1['Text'])

    dataframe1['preprocess_without_stop'] = dataframe1['preprocess'].apply(lambda x: [word for word in x if word not in stopwords])

    #On peut observer la liste des mots outils de NLTK dans la seconde sortie
    dataframe2 = pd.DataFrame(list(stopwords))

    return dataframe1, dataframe2,

Apprentissage supervisé sur données textuelles

Dans le groupe Text Analytics, nous disposons des modules ci-dessous.

Vowpall Wabbit

Nous écartons d’emblée l’approche Vowpal Wabbit, framework de Machine / Online Learning initialement développé par la société Yahoo !. Celui-ci nécessite en effet d’obtenir en entrée des données préprocessées mais surtout présentées dans un format spécifique. Vous pouvez toutefois vous référer à ce blog si vous souhaitez préparer vos données en ce sens. Il faudra ensuite utiliser des lignes de commandes au sein du module Train Vowpal Wabbit Model.

Toutefois, nous allons pouvoir remplacer ce framework par une approche relativement similaire réalisée par le module Feature Hashing.

Feature Hashing

Cette méthode consiste à créer une table de hash, c’est-à-dire de valeurs numériques, construites à partir du dictionnaire des mots.

Nous prendrons comme entrée le texte préprocessé. Nous avons alors deux paramètres à renseigner dans la boîte de dialogue du module.

Une taille de 10 bits correspondra à 2^10, soit 1024, nouvelles colonnes dans le jeu de données en sortie. Ces colonnes contiendront le poids de la feature dans le texte. Une valeur de 10 se montre généralement suffisante et attention à l’explosion de la taille du jeu de données (2^20 correspondrait à 1048576 colonnes !).

Un N-gram est une suite de n mots consécutifs, considérée comme une seule unité. Dans la phrase “le ciel est bleu”, nous avons quatre unigrams, trois bigrams (“le ciel”, “ciel est”, “est bleu”) et deux trigrams (“le ciel est”, “ciel est bleu”). Le paramètre attendu est la valeur maximale, ainsi si nous choisissons la valeur 3, nous prenons en compte les unigrams, bigrams et trigrams.

Nous obtenons le résultat suivant.

Seules les colonnes numériques seront ensuite soumises à l’entrainement du modèle.

Extract N-gram feature

Nous retrouvons ici la notion de N-gram (suite de n mots consécutifs, considérée comme une seule unité). Les paramètres disponibles nous permettent de spécifier :

– la taille maximale des N-grams considérés dans le dictionnaire. Ce paramètre conditionnera la taille du jeu de données, qui peut devenir très grand.

– les longueurs minimale et maximale des mots, en nombre de lettres

– la fréquence minimale de présence d’un N-gram dans le document (ensemble des lignes du jeu de données)

– le pourcentage maximum de présence d’un N-gram par ligne sur l’ensemble des lignes. La valeur 1 (100%) correspond à retirer un N-gram présent dans chaque ligne, et donc considéré comme un bruit qu’il faut supprimer.

– la métrique de pondération calculée par ligne, pour chaque feature créée.

La métrique TF-IDF est un standard de la discipline est correspond au ratio de la fréquence d’un N-gram dans la ligne (TF) par le log d’un autre ratio dit”inversé” : le nombre de lignes du jeu de données sur la fréquence du terme dans le jeu de données. Les valeurs seront normalisées selon une norme L2 si la case correspondante est cochée dans la boîte de dialogue.

En créant le vocabulaire, nous obtenons un jeu de données en sortie tel que représenté ci-dessous. Celui-ci contient la liste des N-grams, ainsi que les métriques DF et IDF associées.

Un pronom est ici identifié dans le bigram.

C’est ce vocabulaire établi sur le jeu d’entrainement qui devra saisir à la phase de validation sur les nouvelles données. Il ne faudrait pas créer un nouveau vocabulaire sur la seconde partie des données ! Mais les métriques doivent être calculées. Nous utilisons donc la combinaison ci-dessous des modules.

Le paramètre “Vocabulary mode” est alors positionné sur la valeur ReadOnly.

Convert word to vector

Nous retrouvons ici une méthode dite de plongement lexical (word embedding) qui a connu un fort engouement ces dernières années, et plus communément appelée word2vec. Il s’agit d’utiliser des modèles pré-entrainés sur des milliards de documents, issus par exemple de Wikipedia ou de Google News.

A ce jour (janvier 2021), il ne semble pas y avoir de module permettant de tirer parti du vocabulaire obtenu en sortie. Nous privilégierons donc une approche par script Python, au moyen de librairies comme Gensim (voir le site officiel).

Latent Dirichlet Allocation

Nous sortons ici du cadre supervisé pour obtenir une méthode non supervisée et donc dédiée à rassembler des textes similaires, dans un nombre prédéfini (“Number of topics to model”) de catégories, sans que celles-ci ne soient explicitement définies.

Comme nous disposons déjà des catégories, cette méthode ne se prête pas à l’objectif mais pourrait être intéressante en amont, dans un but exploratoire (nos catégories a priori ne sont peut-être pas si pertinentes…).

Un mode d’options avancées donnera la main sur tous les hyperparamètres du module.

Chaque ligne est évaluée sur les nombre prédéfini de sujets (topics).

A partir de la sortie, il sera nécessaire de réaliser une lecture et une interprétation “humaine” sur les sur les groupes afin de les nommer. Dans la feature matrix topic, nous pouvons visualiser les N-grams les plus contributeurs de chaque sujet.

Publier le pipeline d’entrainement

Publier un pipeline permet de bénéficier de l’ordonnancement de celui-ci, par exemple avec un outil comme Azure Data Factory. Nous obtiendrons ainsi un identifiant (ID) qui devra être renseignée dans l’activité.

Les différents paramètres sélectionnés dans les modules peuvent être ajoutés comme paramètres du pipeline et ainsi renseignés lors de nouveaux lancements.

Déploiement du pipeline d’inférence

Une fois le pipeline d’entrainement soumis et correctement exécuté (tous les modules sont en vert), nous pouvons choisir entre un service Web prédictif, soit en real-time (prévision par valeur), soit en batch (prévision par lot).

Si nous avons comparé plusieurs modèles au sein du pipeline d’entrainement, il faut cliquer sur le module “Train model” correspond à celui que l’on veut déployer (en pratique, celui ayant les meilleures métriques d’évaluation).

Le pipeline graphique se simplifie alors et se voit enrichi d’une entrée et d’une sortie pour le service Web qui sera créé.

Attention, pour aller plus loin, il est nécessaire de supprimer le module d’évaluation ou sinon, vous rencontrerez le message d’erreur suivant.

Un nouveau run du pipeline doit être soumis avant de pouvoir cliquer sur le bouton de déploiement du service web.

Le déploiement se fait au choix, sur une ressource ACI (plutôt pour le test) ou AKS (plutôt pour la production).

En conclusion, même s’il paraît difficile d’aller jusqu’en production avec cet outil, il reste néanmoins très rapide pour mettre en œuvre différentes méthodes reconnues dans le Traitement Automatique du Langage (TAL) et pourra apporter des résultats pertinents dans des cas relativement simples de classification.