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

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

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

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

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

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

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

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

C’est parti, laissons maintenant notre cluster travailler !

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

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

Nous y trouvons des informations importantes :

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

Un diagramme en ligne termine cette rapide analyse.

Passons maintenant aux notebooks de forecasting.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Du rôle des pipelines dans Azure ML et son SDK

Si vous êtes habitués à manipuler le package Python scikit learn, la notion de pipeline vous est sans doute familière. Cet objet permet d’enchainer des étapes (“steps“) comme la préparation de données (normalisation, réduction de dimensions, etc.), l’entrainement puis l’évaluation d’un modèle. La documentation officielle du package donne ainsi cet exemple.

>>> from sklearn.svm import SVC
>>> from sklearn.preprocessing import StandardScaler
>>> from sklearn.datasets import make_classification
>>> from sklearn.model_selection import train_test_split
>>> from sklearn.pipeline import Pipeline
>>> X, y = make_classification(random_state=0)
>>> X_train, X_test, y_train, y_test = train_test_split(X, y,
...                                                     random_state=0)
>>> pipe = Pipeline([('scaler', StandardScaler()), ('svc', SVC())])
>>> # The pipeline can be used as any other estimator
>>> # and avoids leaking the test set into the train set
>>> pipe.fit(X_train, y_train)
Pipeline(steps=[('scaler', StandardScaler()), ('svc', SVC())])
>>> pipe.score(X_test, y_test)
0.88

Le SDK Python azureml que nous avons déjà évoqué sur ce blog dispose également d’un concept de pipeline. Celui-ci ne doit pas être confondu avec les pipelines pouvant être définis dans l’interface visuelle du concepteur (“designer“). Les pipelines en tant que tels sont visibles dans un menu dédié.

(NDA : on serait en droit d’espérer à termes une fusion de ces éléments, c’est-à-dire d’observer le pipeline dans l’interface du concepteur, voire de retrouver le pipeline créé par le code dans les objets manipulables visuellement).

Les avantages des pipelines

La manipulation des pipelines, en particulier des entrées-sorties, ne sera pas triviale, alors nous allons insister sur tous les bénéfices d’un tel objet. Au delà de la représentation visuelle toujours plus parlante, le principal avantage sera la planification du pipeline. Celle-ci peut se faire de trois manières. Le code présenté ici est disponible dans ce dépôt GitHub.

Objet Scheduler

L’interface du studio Azure Machine Learning ne présente pas d’ordonnanceur visuel des notebooks. Pour autant, cette notion d’ordonnancement existe bien et se manipule au travers du SDK. Une fois publié, un pipeline dispose d’un identifiant. A partir de cet id, un objet Schedule peut être défini, et se déclenchera selon une récurrence déclarée au moyen de l’objet ScheduleRecurrence.

A sa création, l’ordonnancement est activé. Il sera possible de le désactiver à partir de son identifiant (à ne pas confondre avec l’identifiant du pipeline).

Les points négatifs de cet approche sont le manque de visibilité sur les ordonnancements définis (il est nécessaire de lancer la commande Schedule.list) et le fait que d’autres activités non définies dans des scripts présents sur dans l’espace de travail Azure Machine Learning.

Pipeline Azure DevOps

Encore un pipeline à ne pas confondre avec le pipeline du SDK azureml ! Nous parlons ici des pipelines de release d’Azure DevOps. En recherchant le terme “azureml” dans l’assistant (volet de droite), nous trouvons trois tâches, dont une permettant de lancer un pipeline Azure ML désigné à nouveau par son identifiant.

Un pipeline de release peut ensuite être ordonnancé au moyen des standards d’écriture du fichier YAML.

Activité Azure Data Factory

Nous disposons de trois activités distinctes dans le groupe “Machine Learning”. Les deux premières concernent l’ancien Azure Machine Learning studio, aujourd’hui déprécié. Concentrons-nous sur la troisième activité qui permet d’exécuter un pipeline Azure ML.

Pour remplir les différents paramètres, nous devons tout d’abord fournir un service lié (linked service) de type Azure Machine Learning (catégorie “compute“).

Nous privilégions l’authentification par identité managée, celle-ci se voyant attribuer un rôle de contributeur sur la ressource Azure Machine Learning.

Seconde information obligatoire : l’ID du pipeline publié. Un menu déroulant nous permettra de le choisir. Nous constatons ici que cette information sera particulièrement sensible lorsque nous devrons re-publier le pipeline. Il faudra donc limiter au maximum cette action, car elle engendrera une modification dans les paramètres de l’activité Data Factory. Une problématique similaire se posera dans le cas d’un déploiement automatique entre environnements (par exemple, de dev à prod) avec des ID de pipelines différents.

Et maintenant, comment réaliser un pipeline

Nous allons maintenant plonger dans le SDK Python azureml pour décortiquer les étapes de création d’un pipeline.

Il nous faut pour base un script Python qui contiendra le code à exécuter par une étape (step) du pipeline. Nous ne travaillerons ici qu’avec des étapes de PythonStepScript mais il en existe d’autres types, référencés dans la documentation officielle. Nous prendrons la bonne habitude de faire figurer dans ces scripts les lignes de code suivantes :

from azureml.core.run import Run

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

Celles-ci permettront de retrouver les éléments du “niveau supérieur”, c’est-à-dire l’expérience, son exécution ainsi que l’espace de travail.

Ensuite, nous pourrons travailler sur les entrées et sorties de chaque étape. Cette gestion des entrées-sorties nécessitera un article à part entière sur ce blog.

Exemple de “graph” de sortie du pipeline (experiment)

Nous recréons ainsi, par le code (et au prix de nombreux efforts !), un objet similaire au pipeline obtenu dans le Designer.

from azureml.pipeline.core import Pipeline
from azureml.pipeline.steps import PythonScriptStep

step1 = PythonScriptStep(
    script_name="step1.py",
    compute_target=compute_target,
    source_directory="scripts",
    allow_reuse=True
)


step2 = PythonScriptStep(
    script_name="step1.py",
    compute_target=compute_target,
    source_directory="scripts",
    allow_reuse=True
)

steps = [ step1, step2 ]

pipeline = Pipeline(workspace=ws, steps=steps)
pipeline.validate()

Nous pouvons maintenant soumettre le pipeline, dont la logique des étapes aura préalablement été validée par l’instruction .validate().

pipeline_run = experiment.submit(pipeline)
pipeline_run.wait_for_completion()

Mais cela ne nous permettrait pas de réutiliser ce pipeline dans les scénarios évoqués ci-dessus, nous allons donc le publier avec l’instruction .publish().

published_pipeline = pipeline.publish(
name=published_pipeline_name,
description="ceci est un pipeline à deux étapes"
)
published_pipeline

Ceci nous permet de connaître l’identifiant du pipeline (masqué dans la copie d’écran ci-dessous).

Il ne reste plus qu’à soumettre le pipeline pour l’exécuter même si nous utiliserons vraisemblablement d’autres méthodes comme l’appel par Azure Data Factory ou l’emploi de l’API REST.

published_pipeline.submit(ws, published_pipeline_name)

Les logs de l’exécution seront accessibles dans le menu Pipelines du portail, qui nous redirigera vers le menu Experiments. N’oubliez pas d’activer l’affichage des “child runs” pour visualiser les traces de l’exécution de chacune des étapes.

Voici enfin un exemple de code qui permettra de lancer ce pipeline par requête HTTP.

from azureml.core.authentication import InteractiveLoginAuthentication
import requests

auth = InteractiveLoginAuthentication()
aad_token = auth.get_authentication_header()

rest_endpoint1 = published_pipeline.endpoint

print("You can perform HTTP POST on URL {} to trigger this pipeline".format(rest_endpoint1))

response = requests.post(rest_endpoint1, 
                         headers=aad_token, 
                         json={"ExperimentName": "My_Pipeline_batch",
                               "RunSource": "SDK"}
                        )

try:
    response.raise_for_status()
except Exception:    
    raise Exception('Received bad response from the endpoint: {}\n'
                    'Response Code: {}\n'
                    'Headers: {}\n'
                    'Content: {}'.format(rest_endpoint, response.status_code, response.headers, response.content))

run_id = response.json().get('Id')
print('Submitted pipeline run: ', run_id)

Attention, une réponse “correcte” à cet appel HTTP sera de confirmer le bon lancement du pipeline. Mais rien ne vous garantit que son exécution se fera avec succès jusqu’au bout ! Il faudra pour cela se pencher sur la remontée de logs (et d’alertes !) à l’aide d’un outil comme Azure Monitor.

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 des séries temporelles avec la librairie fbprophet

En 2017, les équipes de recherche de Facebook publiaient ce papier qui introduira la librairie fbprophet, disponible en R et en Python (pas de jaloux !). Cet outil peut être rangé dans la catégorie des modèles additifs généraux car il décompose une série temporelle de la sorte :

y(t) = g(t) + s(t) + h(t) + e(t)

avec respectivement :

  • g(t) : la tendance (linéaire ou logistique)
  • s(s) : une ou plusieurs composantes saisonnières (annuelle, hebdomadaire ou quotidienne)
  • h(t) : l’effet des vacances ou de jours spécifiques qui pourront être paramétrés
  • e(t) : l’erreur, bruit aléatoire qui mesure l’écart entre le modèle et les données réelles

Si l’on retient le terme “additif”, il est bien sûr possible de modifier le modèle pour le rendre “multiplicatif” (observez les crêtes de votre série temporelles, si celles-ci forment un cône, le modèle est sans doute multiplicatif).

La grande force de ce type de modèle tient dans sa capacité à être interprété ainsi que dans la clarté des représentations graphiques de la décomposition. Il sera particulièrement adapté à des phénomènes comme… la fréquentation d’un réseau social mais aussi des mesures économiques fortement soumises à des saisonnalités et aux périodes de vacances d’un pays. Il est si simple à mettre en œuvre qu’il gagnera à être comparé à des modèles plus classiques comme SARIMA (nous en reparlerons en toute fin d’article).

Installation de la librairie

Pour ne pas “polluer” notre installation locale avec de nouveaux packages et leurs dépendances, nous créons au préalable un environnement virtuel, à l’aide du package pyenv pour Windows (voir ce GitHub). Ne pas oublier d’ajouter les variables d’environnement et de redémarrer votre terminal ou IDE pour terminer l’installation.

pyenv install 3.8.10

pyenv shell 3.8.10

Nous pouvons alors installer les librairies suivantes :

pip install pystan==2.19.1.1

pip install fbprophet

Pystan est une librairie pour l’inférence bayésienne. Si cette installation ne fonctionne pas (les dépendances sont parfois capricieuses…), vous pouvez utiliser un prompt Anaconda et tenter la commande suivante :

conda install -c conda-forge fbprophet

Vérifiez enfin la bonne installation en choisissant l’interpréteur voulu (ici, conda) dans Visual Studio Code.

Lancez ensuite Python dans le terminal et testez un import de la librairie.

Mise en pratique

Déroulons maintenant un exemple simple, de bout en bout, en réalisant quelques tentatives d’optimisation du modèle. RTE France met à disposition les données énergétiques “eco2mix” dont la profondeur d’historique et le niveau de détail sera intéressant pour évaluer notre outil. J’ai pris connaissance de ce jeu de données dans cet excellent article du blog de Publicis Sapient.

Une seule contrainte dans la façon dont les données doivent être soumises à Prophet : les colonnes de temps et de mesure quantitative doivent être respectivement nommées “ds” et “y”.

Une granularité plus fine que le jour peut être utilisée dans le champ datetime. C’est alors qu’il sera pertinent d’activer le daily effect qui sera visualisable graphiquement.

La régularité est un élément fondamental pour la modélisation d’une série temporelle, que viennent perturber les années bissextiles. Nous pouvons décider de supprimer les 29 février, par exemple avec le code ci-dessous :

df= df.loc[~(df['ds'].dt.month.eq(2) & df['ds'].dt.day.eq(29))]

Attention, nous dégradons alors inévitablement la régularité des semaines…

Création du modèle et évaluation

Lançons tout d’abord un modèle simple, sur l’intégralité de l’historique. Dans l’extrait de code ci-dessous, df représente un pandas dataframe regroupant les deux colonnes nommées ds et y.

De nombreux paramètres sont définis par défaut : additivité, pas de saisonnalité journalière, détection automatique des change points (changement de tendance), etc.

m = Prophet(daily_seasonality=False)
m.add_country_holidays(country_name='FR')
m.fit(df)

Ici, nous ajoutons au modèle les jours fériés français qui deviendront autant d’indicateurs dans notre modèle additif.

m.train_holiday_names

Comme l’indique cette discussion, il ne semble pas possible d’ajouter plusieurs pays à l’aide de cette méthode.

Nous pouvons également ajouter nos propres dates si nous considérons que des événements (répétitifs sur la saisonnalité attendue) ont un impact sur le phénomène observé. Nous pourrions par exemple identifier les journées les plus froides de l’année qui engendrent certainement une hausse de consommation électrique. Mais serons-nous en capacité de les prédire par la suite ? Mieux vaut se limiter à des événements connus à l’avance tels que des compétitions sportives (lors de la finale de la Coupe du Monde, tout le monde allume la télé !).

Les séries temporelles seraient si simples si toutes les tendances étaient linéaires ! Mais dans la vraie vie (et encore plus en période de pandémie…), les tendances fluctuent et il est indispensable que notre modèle les comprenne. Prophet identifie automatiquement les “change points” et on les visualise ainsi, en rouge, à l’aide du code ci-dessous.

from prophet.plot import add_changepoints_to_plot

fig = m.plot(forecast)
a = add_changepoints_to_plot(fig.gca(), m, forecast)

Nous pouvons aussi les définir arbitrairement. Pourquoi pas en donnant les dates des changements de saison (été et hiver) ?

m = Prophet(daily_seasonality=False,
changepoints=['2013-12-21', '2014-06-21', '2014-12-21', '2015-06-21', '2015-12-21', '2016-06-21', '2016-12-21', '2017-06-21'],
changepoint_prior_scale=1)

Le paramètre changepoint_prior_scale indique à quel point le modèle doit respecter notre liste (ou sa détection automatique). C’est une valeur entre 0 et 1. Avec 1, nos change points sont bien retenus, comme l’atteste ce graphique.

Forecast

Pour calculer une prévision, nous allons avoir besoin d’un nouveau dataframe contenant une colonne de type datetime, toujours nommée “ds”, que nous soumettrons à la méthode .predict(), appliquée au modèle.

future = pd.date_range(start="2020-01-01",end="2021-12-31")
future = pd.DataFrame(future, columns=['ds'])

Tout comme pour la méthode .fit(), la syntaxe est ainsi tout à fait similaire à celle du package scikit-learn.

forecast = m.predict(future)

Une autre méthode pour créer la plage de forecast consiste à utiliser la fonction ci-dessous.

future = m.make_future_dataframe(periods=365)

Cette fonction génère automatiquement une plage de dates couvrant l’historique complet auquel s’ajoute la période définie en paramètre. Ceci a pour avantage de nous permettre de comparer les prévisions avec les données réelles qui ont été utilisées pour le modèle, ce que nous allons observer dans les sorties graphiques.

Sorties graphiques

Pour afficher la superposition des historiques et des prévisions, un simple plot suffit !

m.plot(forecast)

Nous pouvons ensuite obtenir les différents graphiques de décomposition.

m.plot_components(forecast)

D’autres graphiques interactifs sont disponibles conjointement avec la librairie plotly.

Cross validation

Nous allons maintenant évaluer la qualité de la prévision à l’aide des métriques d’évaluation traditionnelles que sont MSE, RMSE, MAE, MAPE, etc. et d’une méthode de validation croisée. Ici encore, tout est intégré dans une fonction ! Analysons le code ci-dessous.

df_cv = cross_validation(m, initial='730 days', period='365 days', horizon='365 days')
df_p = performance_metrics(df_cv, rolling_window=1)

Nous retrouvons le modèle préalablement entraîné (m). Un seul paramètre est obligatoire, celui de l’horizon de prévision. Mais nous pouvons également préciser la période initiale d’entrainement (par défaut, 3 horizons, et en effet avec ces méthodes dites “à court termes”, ne vous aventurez pas à prédire au delà d’un tiers de votre historique !). Enfin, le paramètre period indique la taille des “découpes” faites dans le jeu de données pour établir de nouvelles prévisions (par défaut, un demi horizon). Voici un exemple de sortie de la commande lancée sur un cluster Databricks.

A partir d’un entrainement sur les années 2013 et 2014, la validation croisée a effectué 4 prévisions sur les années 2015 à 2018.


Nous obtenons alors les métriques d’évaluation, en moyenne sur le nombre de “folds“.

L’argument rolling_window indique le pourcentage de données considérées dans la prévision (1 équivaut à 100%).

Nous pouvons enfin rechercher les meilleurs hyperparamètres pour notre modèle.

Utiliser une approche par hyperparameter tuning implique de lancer un nombre important d’entrainements et d’évaluations. C’est ici que nous tirerons profit d’un cluster de machines, en précisant le paramètre parallel=’processes’ dans la fonction cross_validation(). L’exemple de code donné sur le site officiel sera simple à adapter.

Et maintenant, Kats !

La R&D de Facebook ne s’est pas arrêtée là ! En 2021, une nouvelle librairie est mise à disposition : Kats. Celle-ci a pour vocation de simplifier les tâches des Data Scientists autour de l’analyse et de la modélisation des séries temporelles.

Ici, pas de nommage particulier des colonnes, mais nous transformerons le dataframe en objet spécifique à Kats : TimeSeriesData().

Une première fonctionnalité consiste à déterminer automatiquement 65 features de la série temporelle, c’est-à-dire des caractéristiques de cette série (moyenne, variance, entropie, etc.) qui pourront être par la suite intégrées à des modèles de Machine Learning ou à une approche par régression de la modélisation de la série temporelle.

De nombreuses fonctionnalités s’orientent autour de la détection : seasonalities, outlier, change point, and slow trend changes

Enfin, Kats intègre un grand nombre de modèles (ARIMA, HW, stlf… et l’inévitable Prophet !). Bref, c’est le couteau suisse rêvé de tout.e Data Scientist qui s’attaque à un sujet de séries temporelles.

Repos Databricks & GitHub actions, better together !

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

name: CI

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Utiliser les services cognitifs Azure au travers des images Docker

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

http://localhost:5000/

http://localhost:5000/ready

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

http://localhost:5000/status

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

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

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

Nous visualisons le résultat suivant :

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

Forecasting par automated ML (UI)

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

  • régression
  • classification
  • forecasting

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • normalized RMSE
  • R2
  • normalized MAE

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

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

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

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

Trois paramètres additionnels de configuration sont ensuite disponibles.

Ils correspondent respectivement à :

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Réviser la certification AI-102

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

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

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

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

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

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

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

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

Nous travaillerons autour des quatre familles principales de services cognitifs :

  • vision
  • langage
  • speech
  • décision

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

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

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

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

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

Plan and Manage an Azure Cognitive Services Solution

Nous allons nous concentrer ici sur trois points du programme :

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

Implement Computer Vision Solutions

Les services cognitifs concernés par ce chapitre sont :

  • Computer Vision
  • Custom Vision
  • Face

Implement Natural Language Processing Solutions

Les services cognitifs concernés par ce chapitre sont :

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

Implement Knowledge Mining Solutions

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

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

Implement Conversational AI Solutions

Les services cognitifs concernés par ce chapitre sont :

  • QnA Maker
  • Bot Framework

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

En conclusion

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

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

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

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

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

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

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

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

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

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

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

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

Nous disposons de trois types pour ces décorateurs :

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

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

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

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

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

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

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

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

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

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

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

Modification de la méthode de prévision

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

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

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

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

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

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

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

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

[[0.27303021717776266, 0.7269697828222373]]

En conclusion

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

Le notebook complet sera disponible dans ce repository.

Découverte de l’autoML experiment sous Azure Databricks

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Nous pouvons visualiser ce pipeline graphiquement.

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

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

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

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

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

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

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