Sécuriser un modèle avec d’excellentes performances et être capable de l’expliquer est un jalon important de votre projet ML !
En revanche, il faut à un moment donné que vos clients ou votre entreprise commencent à s’en servir pour créer de la valeur. Autrement, votre modèle ne restera qu’une réussite factice !
Dans ce chapitre, nous aborderons les tenants et les aboutissants du déploiement d’un modèle de ML. Vous verrez qu’il ne suffira pas de copier-coller les résultats de votre code predict() dans un tableur Excel. C’est une pratique aux antipodes de ce que font les équipes Data aujourd’hui. ;)
De quoi parle-t-on quand on dit “déployer” un modèle ?
Aujourd’hui, votre modèle fonctionne uniquement dans votre ordinateur. Plus concrètement :
Il fonctionne, car vos packages et Python sont installés dans un environnement de code (idéalement un environnement virtuel, en savoir plus dans ce cours (lier vers le cours de Poetry) ) et un système d’exploitation sous-jacent.
Il fonctionne parce que votre ordinateur est doté d’une puissance de calcul suffisante, en RAM et CPU (voire GPU si vous utilisez un modèle les nécessitant, comme des Transformers ou autres réseaux de neurones lourds).
Il fonctionne également, car votre donnée est stockée dans votre disque dur ou SSD.
Avoir un modèle qui tourne dans le Cloud, revient à mettre ces trois composantes à disposition au modèle, ailleurs que sur votre ordinateur, de telle sorte à ce que d’autres personnes puissent communiquer avec votre modèle. Cela passe par l’achat, ou le plus souvent, la location à distance de machines physiques ou virtuelles :
L’environnement et la puissance de calcul doivent exister au sein de la même machine, où sera hébergé votre modèle ;
En revanche, on peut utiliser une autre machine pour le stockage des données historiques et des modèles. C’est la fameuse distinction entre Compute et Storage dans le Cloud, détaillée dans cet article (en anglais).
Très bien ! Donc déployer un modèle consiste à rendre notre modèle disponible dans un autre ordinateur pour que d’autres personnes puissent communiquer avec !
Mais alors comment des agents immobiliers vont-ils communiquer avec mon modèle pour obtenir des prédictions du prix de transaction ?
Il n’y a pas une méthode universelle pour mettre cette communication en place. On pourrait imaginer plusieurs solutions (par exemple, stocker les résultats de prédictions des nouvelles données dans une BDD accessible aux utilisateurs finaux du modèle) mais une des solutions les plus connues et les plus pratiques consiste à mettre à disposition le modèle via une API. Plus précisément :
Une API est un moyen de communication entre deux applications informatiques, comme un modèle de ML et une interface utilisateur.
Souvent, cette communication se fait via le Web, c’est-à-dire via des URLs. C’est ce que vous faites quand vous voulez interroger ChatGPT par exemple, vous envoyez une requête HTTP à l’API via l’interface de prompting.
Via cette même méthode de communication HTTP, notre modèle renvoie une réponse à l’utilisateur.
Chaque fonctionnalité du modèle, dont son consommateur a besoin, constitue un endpoint requêtable via HTTP. En fait, une requête HTTP envers un endpoint d’une API va exécuter sous le capot un script (dans notre cas un code Python) qui renvoie le résultat attendu.
L’URL pour communiquer avec l’API peut prendre une forme sophistiquée, comme un site web ou un tableau de bord (comme celui de ChatGPT), ou peut être utilisée de manière très rudimentaire, via une commande
curl
dans le Terminal/PowerShell.
Même si cela n’apporte rien à l’utilisateur final, on pourrait créer une API et l’utiliser uniquement au sein de notre ordinateur, sans déploiement. On parle de créer une API “en local”, cette expression signifie que notre ordinateur personnel devient le client ET le serveur dans la communication API. Nous passons souvent par cette étape quand on souhaite tester l’API avant de le déployer ! C’est ce que vous verrez dans le screencast de la section qui suit.
Si l’on souhaite manuellement déployer un modèle et l’exposer via une API, nous pouvons y passer beaucoup de temps. Effectivement, il faudrait écrire toute une batterie de lignes de codes pour définir la logique du service de notre API et ses endpoints, créer une documentation de notre API, s’assurer qu’elle fonctionne en local, puis conteneuriser avec Docker tout le code source ainsi que les packages dont on a besoin pour que le code fonctionne.
En tant que Data Scientist/ML Engineer, nous avons envie de passer le moins de temps possible à réinventer la roue et à automatiser le plus possible les tâches chronophages afin de se concentrer sur ce qui crée réellement de la valeur : Délivrer des prédictions à tous ceux qui ont en besoin.
Facilitez-vous la vie avec BentoML
Dans le vaste monde du MLOps, il existe plusieurs frameworks pour simplifier et automatiser les tâches précédant un déploiement d’un modèle ML (par exemple, tous les Cloud Providers en proposent). Dans ce cours, nous avons choisi de nous focaliser sur un outil en particulier : BentoML.
C’est un framework qui a l’avantage d’être open source, spécialisé en Machine Learning, très facile à prendre en main et doté d’une capacité native à exposer les modèles via API ! Il constitue alors un parfait candidat pour se familiariser avec les nombreux concepts que l’on doit mettre en musique pour préparer notre modèle à un déploiement !
Le concept central de BentoML est celui du Bento. C’est une archive qui va centraliser toutes les composantes nécessaires à la partie serving d’un déploiement :
Le modèle, bien sûr ;
La logique de l’API ;
Les dépendances (packages) dont le code à besoin ;
L’image Docker conteneurisant l’ensemble.
Ce point est très important à comprendre, il faut être précis quand on parle de déploiement ! On y distingue la partie Serving Runtime et la partie Serving Plateform :
Un Serving Runtime va conteneuriser le modèle de ML et tout le nécessaire (logique d’API, librairies etc.) pour qu’il soit prêt à l’emploi dans un environnement en production. Cette partie est également appelée le Model Serving.
Un Serving Plateform est un système qui va gérer et adapter l’infrastructure physique sous-jacente au modèle (nombre et puissance des machines) afin de l’adapter aux demandes du projet. Parfois source de confusion, cette partie est appelée “déploiement”, alors qu’on parle de quelque chose de moins vague.
Le package vient avec une boîte à outils prête à l’emploi pour facilement créer chacun des composants cités ci-dessus concernant le Runtime et les intégrer dans le fameux Bento !
Dans BentoML (ainsi que d’autres outils concurrents), la définition du service de l’API se fait souvent via une classe. C’est un concept de Programmation Orienté Objet (POO). Si vous n’avez jamais vu cette pratique, nous vous renvoyons vers ce chapitre du cours Python : Programmez en orienté objet.
Dans notre projet fil rouge, nous devons rendre la capacité d’inférence de nos deux modèles (régression et classification) accessible via une API, afin que des agents immobiliers puissent s’en servir. Regardons ensemble comment on définit la logique de notre API, tout en la testant “en local” avec BentoML.
Apportons certaines précisions par rapport à certains points évoqués lors du screencast :
L'interface Swagger que nous avons vue est automatiquement générée par BentoML. C'est une interface classique que vous verrez dans plusieurs projets se servant d'APIs. Vous pouvez requêter directement l'API via cette interface en insérant les test_values que nous avons utilisés dans le request body. Le Swagger peut également être personnalisé pour servir de documentation de votre API, où vous expliquez ce que chaque endpoint fait !
Dans notre fonction endpoint predict(), nous avons spécifié les formats attendus pour les inputs (nd.array) et les outputs (tuple de nd.array). Si vous requêtez votre API en utilisant un autre format que celui spécifié, BentoML rejettera automatiquement la requête. C'est en effet un mécanisme de sécurité contre la donnée hors format, pour éviter des problèmes de data quality. En réalité, BentoML utilise sous le capot un package nommé Pydantic. Nous vous recommandons d' y jeter un coup d'œil. ;)
Pour un agent immobilier, c'est un peu technique comme format, des numpy arrray non ? Et puis l'interface Swagger n'est pas la plus esthétique. C'est comme ça que font les équipes Data ?
C'est une bonne question ! En général, les interfaces Swagger sont plutôt utilisées par des profils techniques. Quand on doit exposer un modèle à des interlocuteurs métiers, il est d'usage d'utiliser un dashboard comme interface, connectée à l'API et donc au modèle. Entrer dans plus de détails sortirait du cadre de ce cours, mais vous pouvez aller regarder comme package Streamlit. Il s'agit d'une librairie Python qui permet de construire très rapidement des dashboards pour prototyper vos applications !
Conteneurisez votre modèle et votre service avec BentoML
Nos appels API fonctionnent en local ! Nous allons désormais conteneuriser le tout pour qu'on puisse reproduire notre environnement identique en production dans le Cloud.
En temps normal, il faudrait avoir des bases en Docker (ou équivalent) et écrire manuellement un script conteneurisant le modèle, les dépendances, l'API etc. Cependant, avec BentoML, ce processus devient beaucoup plus simple. On va passer par l'écriture d'un fichier bentofile.yaml.
C'est le fichier central qui définit notre Bento cité tout à l'heure. On va y déclarer tout le nécessaire pour notre environnement de production. Cette page de la documentation BentoML vous présente le template le plus basique d'un bentofile.yaml, qu'on a recopié ci-dessous.
service: 'service:Summarization'
labels:
owner: bentoml-team
project: gallery
include:
- '*.py'
python:
packages:
- torch
- transformers
En l'adaptant à notre cas d'usage, nous obtenons le fichier suivant :
service: 'service:TransactionPrediction’
labels:
owner: bentoml-team
project: gallery
include:
- 'settings.py'
- service.py'
python:
pip_args: "-e /home/bentoml/bento/src/."
En plus de remplacer le nom du service par celui que nous avons défini, nous avons réalisé deux modifications :
Nous avons uniquement précisé les scripts dont nous avons besoin. C'est une bonne pratique de programmation d'éviter les valeurs par défaut "*.py" qui vont venir surcharger votre conteneur avec des fichiers superflus. D'autant plus qu'un conteneur de ML "léger" prend souvent quelques Go de mémoire !
BentoML utilise par défaut Pip pour installer les packages déclarés explicitement. Dans le code associé au projet filé, nous utilisons Poetry, un package manager plus robuste que Pip. La ligne de code ci-dessous dans la section python permet d'installer les packages d'un fichier Poetry.lock déjà existant. Vous pouvez aller consulter le chapitre Installez Poetry et lancez votre premier environnement virtuel d'un autre cours afin de comprendre pourquoi Poetry est devenu le standard en package management, dans les projets en Python.
Tout ceci est bien, mais pour conteneuriser notre projet, il faut utiliser Docker ou un équivalent ! Eh bien, nous avons une ligne de commande qui permet de "traduire" notre bentofile.yaml en un Dockerfile, le croquis qui permet à Docker ou un équivalent de recréer à l'identique l'environnement dans lequel nous avons construit notre projet fil rouge. Autrement dit, notre fameux Bento ! Cette commande à exécuter dans votre Terminal/PowerShell est :
```(bash)
bentoml build
```
Une fois exécutée, vous pouvez regarder la liste de tous vos bentos en utilisant la commande bentoml list
:
À partir du Bento, nous pouvons créer enfin notre image Docker en utilisant la commande :
```(bash)
bentoml containerize transaction_prediction:inserez_ici_votre_numero_de_tag
```
Cette commande utilise Docker sous le capot (ou un autre outil de création de conteneurs de votre choix) et constitue l’étape finale de la préparation de votre modèle au déploiement. Autrement dit, nous avons fini la partie Serving Runtime !
On peut résumer tout ce qu’on a appris dans ce chapitre en complexifiant le schéma du déroulé d’une modélisation :
Dans l’image Docker, vous verrez une brique que l’on a nommée “Request Manager” (ce n’est pas un nommage conventionnel). Il s’agit de la fonctionnalité de l’API via BentoML qui va gérer intelligemment les requêtes HTTP entrantes et sortantes, dans le cas où on en aurait plusieurs par exemple. Nous n’en parlons pas plus pour ne pas nous éloigner du cœur du sujet. ;)
Afin de finaliser le déploiement, il suffit d’utiliser l’outil de conteneurisation du Serving Platform de votre choix et de choisir une machine qui va héberger votre conteneur.
Comme les étapes à suivre pour cela sont spécifiques à chaque Serving Platform, nous vous renvoyons vers leur documentation pour réaliser un Custom Deployment :
Voici un exemple de tutoriel pour déployer un modèle ML conteneurisé personnalisé sur Vertex AI (en anglais).
Voici un autre guide issu de la documentation officielle à date d’AWS pour déployer un modèle conteneurisé à la main avec SageMaker (en anglais).
Allez plus loin
Vous pouvez davantage améliorer votre service en parallélisant vos calculs via des Workers.
Ce concept de Worker n’a de sens que si vous utilisez une machine avec plusieurs CPUs et/ou des GPUs, ce qui est quasiment toujours le cas des machines orientées IA. Par exemple, le notebook par défaut proposé par Google Colab est doté de 2 vCPUs.
Pour faire simple, on peut dire qu’un Worker = un CPU (même si on pourrait faire autrement). On peut choisir de déployer notre API sur plusieurs Workers, ce qui va revenir à créer plusieurs instances de notre service, chacun déployé sur un Worker.
Concrètement, si nous recevons 100 requêtes en une seconde et que nous avons 10 workers, on pourrait imaginer que chaque Worker va recevoir 10 requêtes d’API à traiter.
Je dis bien “on pourrait imaginer”, car cette distribution des requêtes n’est pas toujours aussi simple. En réalité, il y a une brique de notre API qui est chargée de réaliser cette distribution de manière optimale, en fonction de la disponibilité des Workers et de l’état des ressources de la machine. C’est ce que l’on appelle un Load Balancer.
D’un point de vue code, il est très facile de configurer le nombre et le type de workers avec BentoML, il suffit d’ajouter un argument dans le décorateur de notre classe qui définit notre service, comme ceci :
@bentoml.service(workers=”cpu_count”) # Pour utiliser tous les CPUs de la machine
class TransactionPrediction:
Une petite mise à jour de notre schéma précédent s’impose :
Ce type d’optimisation commence à avoir de l’importance quand vous savez que vous allez faire face à un trafic important de requêtes API vers votre modèle, une fois déployé. Cela pourrait ne jamais être le cas en fonction de votre cas d’usage. A contrario, la mise en place de Workers pourrait ne pas suffire. Parmi les autres pistes à explorer (il y en a plusieurs), la plus simple et la plus répandue est de scaler vos ressources à la hausse.
A ce sujet, vous croiserez souvent la terminologie de scaling horizontal et de scaling vertical :
Le scaling vertical consiste simplement à augmenter la puissance de votre machine, en augmentant la RAM ou le nombre de CPUs/GPUs par exemple.
Le scaling horizontal consiste à augmenter le nombre de machines et de distribuer vos calculs sur ces machines-là. C'est le même principe qu’avoir plusieurs Workers, sauf qu’ici on parle de nœuds (ou nodes en anglais). En effet, Worker est un terme plus utilisé pour les CPUs d’une machine donnée. Un ensemble de noeuds forme ce que l’on appelle un Cluster, cela n’a rien à avoir avec le Clustering en apprentissage non-supervisé.
Comme lecture d’approfondissement, nous renvoyons vers quelques articles :
Une comparaison des différentes solutions de Serving Runtime et de Serving Platforms les plus connues (BentoML y compris).
La documentation de FastAPI, le framework le plus connu et le plus robust pour construire rapidement des APIs (au cas où vous avez des contraintes de serving qui vous empêchent d’utiliser BentoML).
À vous de jouer !
Vous avez vu comment mettre en place un service basique de prédiction des deux targets avec BentoML. Maintenant, à vous de le complexifier !
Créez un code de service qui analyse la région de provenance de la donnée et qui fait appel au modèle associé à cette région pour réaliser l'inférence des deux cibles ! Cela sous-entend que vous avez des modèles par région. ;)
Vous pouvez partir de ce notebook template.
Une fois que vous avez fini, vous avez ce corrigé à disposition.
En résumé
Déployer un modèle de ML consiste à rendre accessible ses prédictions et fonctionnalités à d’autres utilisateurs via un environnement distant, souvent par le biais d’une API. Cela implique de sortir le modèle de son environnement local pour le rendre accessible sur le Cloud ou une autre infrastructure.
BentoML simplifie le déploiement en centralisant le modèle, ses dépendances, et la logique API dans une archive appelée Bento, qui peut être facilement conteneurisée avec Docker pour être utilisée en production.
Le fichier bentofile.yaml permet de configurer précisément ce qui doit être inclus dans le Bento, facilitant ainsi la création d'une image Docker optimisée pour la production.
En production, il est possible d’optimiser les performances en répartissant les requêtes API sur plusieurs workers (CPU/GPU), et en mettant en place des mécanismes de scaling (vertical et horizontal) pour gérer un trafic élevé.
Utiliser des outils comme BentoML permet de rendre les modèles accessibles et faciles à utiliser, que ce soit pour des développeurs ou des interlocuteurs métiers, sans qu'ils aient à manipuler le code sous-jacent.
Votre modèle est en production, mais le travail ne s'arrête pas là ! Voyons ensemble comment détecter et gérer les dérives pour garder vos prédictions toujours fiables.