Déploiement de votre projet d'apprentissage automatique en tant que service

Par : Andres Solis Montero, Statistique Canada

La première étape du cycle de vie du développement logiciel (CVDL) d'un projet d'apprentissage automatique consiste à définir l'énoncé du problème et les objectifs. Ensuite, il s'agit de recueillir, d'analyser et de traiter les données. Le processus se poursuit avec de multiples itérations, parfois perpétuelles, de modélisation, d'entraînement, d'ajustement d'hyperparamètres, de tests et d'évaluation. Ces étapes sont essentielles à l'élaboration d'un modèle efficace et accaparent la majeure partie du temps et de l'attention consacrés au développement. Mais que se passe-t-il ensuite? Empaquetage et déploiement de logiciel? La plupart du temps, l'objectif final est de livrer un produit aux clients, de mettre le code à la disposition d'autres équipes ou d'utilisateurs aux fins de collaboration, ou simplement de communiquer le travail effectué et les résultats avec le reste du monde.

Le déploiement ne devrait pas être simplement la dernière étape du cycle de développement. L'intégration progressive de bonnes pratiques de génie logiciel et d'outils libres peut améliorer les compétences en développement ainsi que la capacité d'une organisation à fournir des applications et des services plus rapidement. Cette approche permet de créer un produit de A à Z qui peut être facilement partagé et déployé sans répercussions importantes sur le temps de modélisation et de développement.

Un modèle de projet conforme aux pratiques et aux outils mentionnés dans le présent article est accessible au public. Ce modèle peut constituer votre première étape dans l'élaboration de projets d'apprentissage automatique. N'hésitez pas à fourcher le projet et à étendre ses fonctionnalités. Une autre caractéristique intéressante de cette structure de projet est la séparation de la logique applicative pour le déploiement, ce qui lui permet de se conformer aux normes du gouvernement du Canada sur les API pour la prestation de services Web sécurisés par l'entremise du protocole HTTPS sans avoir à transformer votre code. Dans cet article, on a établi que Python est le langage de programmation utilisé. Cependant, les méthodologies et les solutions qui y sont présentées pourraient également être mises en œuvre en utilisant un autre langage de programmation.

Contrôle des versions

La première pratique pertinente à employer au moment du déploiement d'un projet d'apprentissage automatique en tant que service est le contrôle des versions. Le recours au contrôle des versions pour les projets d'analyse a été abordé dans un article précédent qui soulignait également son importance et sa valeur. Le présent article porte sur une structure de projet à utiliser dans votre système de contrôle des versions.

  • LICENSE [Détails de la licence]
  • README.md [Documentation d'utilisation rapide]
  • CONTRIBUTING.md
  • SECURITY.md
  • docs [Documentation]
    • Makefile
    • conf.py
    • index.rst
    • make.bat
    • markdown [Documents du manuel]
      • model.md
      • problem_statement.md
      • relevant.md
  • src [Code source]
    • mlapi
      • Dockerfile [Mise en conteneur]
      • requirements.txt
      • notebook.ipynb [Cahier de prototypage]
      • ml [Modules ML]
        • classifier.py
        • model.joblib
      • main.py [API REST]

Cette structure reflète le code prêt à la production dans la branche principale. D'autres branches reproduiront la même structure de dossiers, mais serviront à différentes étapes de développement, comme l'élaboration de versions différentes, la mise à l'essai, les nouvelles fonctions et l’expérimentation. L'objectif de la branche principale est qu'elle soit toujours prête à l'emploi, ce qui signifie que vous pouvez la déployer à tout moment. De plus, vous pouvez avoir plusieurs branches de la branche principale qui traitent des problèmes de production ou de développement.

Les flux de travail et l'utilisation de Git sont un vaste sujet et hors de la portée du présent article. Consultez la documentation publique pour obtenir plus de détails sur les directives d'utilisation de Git.

Documentation

La deuxième pratique dont il faut tenir compte est la documentation. La documentation du code est une étape importante pour vous assurer que votre projet d'apprentissage automatique est compréhensible et prêt à être déployé. La rédaction de la documentation peut être intimidante si vous essayez de la rassembler à la fin d'un projet. Grâce à quelques pratiques et outils raisonnables, le travail peut être plus facile à gérer.

Un projet bien documenté devrait cibler de multiples utilisateurs, depuis les développeurs et les spécialistes de la maintenance jusqu'aux utilisateurs, aux clients et aux intervenants. Le principal intérêt des développeurs et des spécialistes de la maintenance est de comprendre les détails de mise en œuvre et les interfaces de programmation d'applications (API) exposées. Les utilisateurs, les clients et les intervenants veulent savoir comment utiliser la solution, les sources de données, les pipelines d'extraction, de transformation et de chargement (ETL) et comprendre les expériences et les résultats.

Une bonne documentation de projet est élaborée au fur et à mesure que le projet avance, dès le début, et pas seulement lorsque le projet est terminé. Les outils de source libre tels que Sphinx peuvent générer automatiquement de la documentation à partir des commentaires Docstring. Documenter le code tout au long du cycle de développement de votre projet est un exercice qui devrait être encouragé et que votre équipe devrait suivre. Suivre le format des normes Docstring lors de la rédaction du code peut aider à créer une documentation exhaustive du code. Les chaînes de documentation (docstrings) sont un excellent moyen de générer de la documentation API lorsque vous rédigez du code en présentant vos modèles, paramètres, fonctions et modules. L'exemple de chaîne de documentation suivant montre la fonction mlapi.main.train.

async def train(gradient_boosting: bool = False) -> bool:
    """ 
    FastAPI POST route '/train' endpoint to train our model     

    Args:
         gradient_boosting: bool            
                A boolean flag to switch between a DTreeClassifier or GradientBoostClassifier

    Returns:
           bool:
  A boolean value identifying if training was successful.  
    """
    data = clf.dataset()
    return clf.train(data['X'], data['y'], gradient_boosting)

L'intégration de Sphinx avec des déclencheurs dans le système de contrôle des versions permet d'analyser la structure de notre projet à chaque validation, de rechercher les chaînes de documentation existantes et de générer automatiquement notre documentation. Dans notre exemple de projet, le fichier de configuration .gitlab.yaml intégrera nos validations à la branche principale avec Sphinx pour générer automatiquement la documentation API de notre code, comme indiqué ci-dessous.

async mlapi.main.train(gradient_boosting: bool = False) → bool
FastAPI POST route '/train' endpoint to train our model
Parameters: gradient_boosting – bool
A boolean flag to switch between a DTreeClassifier or GradientBoostClassifier
Returns: A boolean value identifying if training was successful.
Return type: bool

Par ailleurs, les utilisateurs, les clients et les intervenants peuvent profiter de nos descriptions de projet de haut niveau, comme les détails de la modélisation, les objectifs, les sources de données d'entrée, les pipelines ETL, les expériences et les résultats. Nous complétons la documentation du code en créant manuellement des fichiers dans le dossier docs/markdown/. Sphinx prend en charge les formats ReStructuredText (.rst) et Markdown (.md), ce qui simplifie la génération de documents HTML et PDF. Notre projet tire parti des formats de fichier .rst et .md, stockés dans le dossier docs/ et précisés dans le fichier index.rst.

L'envoi du code à notre branche principale déclenchera la génération automatique de documentation en inspectant toutes les chaînes de documentation du code dans le dossier source. Au cours du même processus, les Markdown indiqués dans l'index sont liés dans le site Web de la documentation finale. Il est également important de préciser un fichier README.md de haut niveau contenant un guide d'utilisation rapide avec des liens pertinents et un fichier LICENCE divulguant nos conditions d'utilisation pour les clients et les utilisateurs.

Interfaces API REST

La troisième pratique dont il faut tenir compte pour le déploiement des projets d'apprentissage automatique est l'utilisation des interfaces API REST. Le gouvernement du Canada a mis l'accent sur l'utilisation des API comme moyen de déploiement des interfaces API en tant que service Web client-serveur, suivant un style architectural de transfert d'état représentationnel (REST).

FastAPI est un cadre Web moderne et performant pour la création d'API REST. Cet outil de source libre de plus en plus populaire tire parti des annotations de type Python pour convertir automatiquement les objets Python en représentations JSON et vice versa.

Parlons un peu de la mise en œuvre du modèle dans notre projet avant de convertir son interface API en API Web REST. Sans perdre de vue la généralité, nous avons choisi un modèle de classification simple et supervisé. Le présent article ne porte pas sur l'entraînement des modèles, alors nous le garderons simple à des fins d'explication.

Dans le projet couplé, nous avons sélectionné l'ensemble de données Iris pour entraîner un modèle de classification. L'ensemble de données contient quatre caractéristiques (c.-à-d. longueur et largeur du sépale, et longueur et largeur du pétale). Ces caractéristiques sont utilisées pour classer chaque observation dans trois classes : Setosa, Versicolour et Virginica.

Nous entraînons notre modèle avec deux classificateurs simples, DecisionTreeClassifier et GradientBoosterClassifier, et nous les utilisons pour faire des prévisions. La description et la mise en œuvre de notre modèle IrisClassifier se trouvent sous src/mlapi/ml/classifier.py et contiennent cinq appels de méthode (c.-à-d. entraînement, téléchargement, chargement, enregistrement et prévision).

Voyons maintenant comment nous pouvons partager notre modèle en tant que service Web. Tout d'abord, nous créons une instance d'application FastAPI et un classificateur dans une application FastAPI. Le point d'entrée se trouve dans le fichier src/mlapi/main.py

app = FastAPI(title="MLAPI Template", description="API for ml model", version="1.0")
"""FastAPI app instance"""

clf = IrisClassifier.load()
"""Classifier instance"""

La méthode IrisClassifier.load() renvoie un classificateur déjà entraîné.

Ensuite, nous commençons par préciser nos chemins HTTP publics pour connecter notre service Web à l'interface API du classificateur.

@app.post("/train")
async def train(gradient_boosting: bool = False):
    """ Docstring """
    data = clf.dataset()
    clf.train(data['X'], data['y'], gradient_boosting)
    return True

Le chemin POST HTTP @app.post('/train') accepte un indicateur booléen pour basculer entre nos deux options de classificateurs mentionnées précédemment. Pour chaque demande de chemin à /train, notre service Web entraînera de nouveau le classificateur à l'aide de l'ensemble de données Iris et de l'indicateur gradient_boostring et mettra à jour l'instance du classificateur (c.-à-d. clf).

Ensuite, nous définissons le chemin qui prendra en charge nos demandes de prévisions; il s'agira d'une méthode « post » à /predict.

@app.post("/predict",response_model=IrisPredictionResponse)
async def predict(iris: IrisPredictionInput) :
    """ Docstring """
    return clf.predict(iris.data)

Cette méthode prend une IrisPredictionInput afin de s'assurer que le format des données de la demande est correct et retourne la classe IrisPredictionResponse avec les probabilités pour chaque catégorie. Une IrisPredictionInput contient un membre de données avec une liste de caractéristiques d'observation de taille quatre, comme indiqué dans notre ensemble de données Iris. FastAPI inspecte les annotations de type Python pour convertir la charge utile « post » JSON aux objets Python valides que nous avons déclarés dans le même fichier main.py

class IrisPredictionInput(BaseModel):
    """ Docstring """
    data: List[conlist(float, min_items=4, max_items=4)]

class IrisPredictionResponse(BaseModel):
    """ Docstring """
    prediction: List[int]
    probability: List[Any]

Enfin, lançons notre service Web

src/mlapi$ uvicorn main:app --reload --host 0.0.0.0 --port 8888

Ouvrez https://127.0.0.1:8888/docs dans votre navigateur Web. Comme nous avons suivi les pratiques exemplaires avec diligence, FastAPI a été en mesure de créer automatiquement une bonne application Web Swagger pour documenter et tester notre interface API. Bien que cela démontre à quel point il est facile d'utiliser ces pratiques de développement, il ne s'agit que d'un modeste exemple d'application. Enfin, votre certificat d'organisation et votre clé privée peuvent être transmis à uvicorn pendant le déploiement, fournissant une couche de communication HTTPS sécurisée pour votre interface API. Il n'est pas nécessaire de changer votre code ni de le modifier pour le sécuriser, car uvicorn intégrera le protocole de sécurité de la couche transport (TLS) simplement en lui disant où trouver le certificat. Notre structure de projet permet de séparer la logique applicative entre votre code et le déploiement facile du protocole TLS.

src/mlapi$ uvicorn main:app --host 0.0.0.0 --port 8888 –ssl-keyfile=./key.pem --ssl-certificate=./cert.pem

Si votre organisation dispose d'une solide infrastructure TLS grâce à d'autres systèmes, ceux-ci peuvent être associés au conteneur pour faciliter encore plus le processus. Il existe de nombreuses façons de mettre en œuvre le protocole TLS.

Mise en conteneur

La quatrième pratique à mettre en œuvre pendant le déploiement de votre projet d'apprentissage automatique est la mise en conteneur. La mise en conteneur est une forme de virtualisation du système d'exploitation où les applications s'exécutent dans des espaces utilisateurs isolés. Un conteneur est essentiellement un environnement informatique entièrement intégré qui contient tout ce dont une application a besoin d'exécuter (p. ex. le code et toutes ses dépendances). Le conteneur est extrait du système d'exploitation hôte, ce qui lui permet d'exécuter le même code dans n'importe quelle infrastructure sans avoir besoin que le code soit retravaillé (c.-à-d. tout système d'exploitation, machine virtuelle ou nuage).

L'avantage du codage de nos projets d'apprentissage automatique à l'aide d'un conteneur est de contrôler toutes nos dépendances logicielles et notre environnement. Par conséquent, nous nous assurons qu'il peut être partagé et exécuté comme prévu au départ. Qu'est-ce que cela signifie? Nous créons un fichier de description d'image Docker définissant nos dépendances et le processus en cours d'exécution. Cela n'a pas d'incidence sur notre modèle ni sur les mises en œuvre, mis à part la structure de dossiers proposée; cela reflète toutes les dépendances de notre code.

Il y a trois exigences de base dans notre modèle pour créer la description d'image Docker personnalisée (c.-à-d. Dockerfile) utilisée pour exécuter notre modèle en tant que service. Premièrement, les images Docker permettent l'héritage, ce qui signifie que nous pouvons tirer parti des images qui utilisent la plupart des mêmes bibliothèques et dépendances que notre projet. Par exemple, nous pourrions choisir d'étendre notre Dockerfile à partir d'une image à l'aide de scikit-learn, pytorch, tensorflow, Keras ou Caffe. Deuxièmement, nous ferons le suivi des dépendances des progiciels Python que nous utilisons dans notre projet à l'intérieur du fichier requirements.txt. Enfin, nous précisons le point d'entrée des commandes de notre conteneur pour exécuter notre application principale.

Dockerfile

FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7

WORKDIR /tmp
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt

COPY . /app
WORKDIR /app
CMD ["uvicorn", "main:app", "--reload", "--host", "0.0.0.0", "--port", "8888"]

Le fichier requirements.txt contient un seul nom de progiciel Python par ligne, précisant une dépendance Python nécessaire à notre projet.

requirements.txt

fastapi
uvicorn
pydantic
starlette
python-multipart
requests
scikit-learn
joblib

Nous pouvons maintenant transformer le fichier de définition en image Docker et exécuter le conteneur pointant vers notre service FastAPI

src/mlapi$ docker build -t mlapi .
src/mlapi$ docker run -d -p 8888:8888 --name ml-mlapi mlapi

Modularisation

La cinquième et dernière pratique soulignée dans le présent article est la modularisation. La modularisation est une technique utilisée pour diviser un système logiciel en plusieurs modules distincts et indépendants capables d'effectuer des tâches de façon indépendante. Ces modules sont considérés comme des concepts de base pour l'application.

Si nous voulons élaborer un code lisible et facile à maintenir, nous devons utiliser une conception modulaire. Il est essentiel de séparer notre code en blocs fonctionnels unitaires réutilisables. La division de notre code en différents blocs fonctionnels unitaires nous permet d'exécuter l'ensemble de la solution en les regroupant. Bien que ces derniers soient tous dans un scénario de cas par cas et dépendent du projet, les projets d'apprentissage automatique ont des blocs très déterminants comme les pipelines ETL de données, l'analyse, l'entraînement, les tests, les résultats et la production de rapports. La séparation de ces logiques dans différents modules de code rend notre code Python lisible et facile à maintenir tout en maintenant les coûts de production bas et en accélérant notre cycle de développement. Un code qui n'est pas modulaire prend plus de temps à passer à la production, et il est sujet à des erreurs et à des configurations défaillantes. Il devient difficile d'examiner les codes plusieurs fois avant le déploiement.

Les carnets Jupyter constituent l'un des outils les plus couramment utilisés pour le prototypage d'applications d'apprentissage automatique. Ils nous permettent d'exécuter des cellules de code et de les documenter au même endroit. Malheureusement, ils ne sont pas adaptés au déploiement d'un projet; nous devons traduire leur code en modules Python. Nous pourrions considérer les cellules des carnets comme des composantes de notre prototype. Une fois testées, une ou plusieurs cellules de code pourraient être intégrées à une fonction ou à un module Python sous un dossier src/mlapi/ml. Ensuite, nous pouvons les importer de nos cahiers et poursuivre le prototypage.

Pendant le prototypage de nos modèles, les carnets Jupyter doivent être sauvegardés dans le dossier src/mlapi/, à côté du point d'entrée main.py de l'API REST. Cela garantit que notre code de prototypage et de production maintient les mêmes importations de chemin de module absolu. De plus, de la même façon que nous présentons notre code, la documentation du projet pourrait suivre le même flux de travail. Les cellules Markdown de Jupyter contenant des renseignements importants sur l'application doivent être déplacées vers des documents.md docs/markdown/<document>, élargissant la documentation de notre projet. N'oubliez pas d'ajouter la référence à notre fichier Sphinx docs/index.rst. Ces pages de documentation peuvent encore être référencées à partir de notre carnet de prototypage en établissant un lien vers leur emplacement de publication final.

Une autre bonne pratique de modularisation consiste à limiter la quantité de valeurs de variables figées dans le code de notre application, à créer des fichiers de configuration qui font référence à ces valeurs, ou à en faire des arguments à une fonction. Utilisez la classe de base FastAPI BaseModel et les structures de données Python telles que les objets Enum, NamedTuple et DataClasses pour préciser les arguments à nos procédures et appels API. Il est également bon de rendre nos paramètres et hyperparamètres de modèle configurables et non figés dans le code, ce qui permet de définir différentes configurations chaque fois que nous entraînons ou exécutons notre modèle.

Dans les projets d'apprentissage automatique, l'entraînement de notre modèle dépend beaucoup de notre problème, des données d'entrée et du format. En raison des multiples itérations d'entraînement que nos modèles suivent, il est bon d'intégrer le code d'entraînement à une interface API qui pourrait être réutilisable. Par exemple, au lieu de simplement créer un code qui traite nos copies locales des fichiers d'entrée, nous pourrions traduire le même principe pour accepter une seule adresse URL pointant vers un fichier compressé contenant tous les ensembles de données, en suivant une structure particulière. Les autres ensembles de données pourraient suivre la même structure et être intégrés à notre entraînement en utilisant le même code. Avant de créer notre structure d'intégration des ensembles de données, il est préférable de chercher des ensembles de données publics pertinents à notre problème et de réutiliser leur format d'entrée dans la mesure du possible. La normalisation de nos ensembles de données est une autre façon positive de créer un code d'apprentissage automatique modulaire.

Pensez toujours à la façon dont nous aimerions utiliser la solution avant de la programmer. Lorsque vous créez des interfaces API ou des modules, pensez du point de vue de l'utilisateur et non du point de vue du développeur. Au fur et à mesure que la science des données progresse, on continue de produire des ressources sur la façon d'améliorer la modularité des codes Python et les compétences en génie.

Conclusion

cinq pratiques de génie logiciel qui nous permettent de déployer des projets d'apprentissage automatique

Description - Figure 1

Diagramme décrivant les cinq pratiques de génie logiciel qui nous permettent de déployer des projets d'apprentissage automatique en exécutant notre modèle en tant que service Web axé sur REST.

Pratique #1: Contrôle des versions; Le recours au contrôle des versions pour les projets d'analyse a été abordé dans un article précédent. Le présent article porte sur une structure de projet à utiliser dans votre système de contrôle des versions.

Pratique #2: Documentation; La documentation du code est une étape important pour vous assurer que votre projet d'apprentissage automatique est compréhensible et prêt à être déployé.

Pratique #3: Le gouvernement du Canada a mis l'accent sur l'utilisation des API comme moyen de déploiement des interfaces API en tant que service Web client-serveur, suivant un style architectural de transfert d'état représentationnel (REST).

Pratique #4: Mise en conteneur; La mise en conteneur est une forme de virtualisation du système d’exploitation où les applications s'exécutent dans des espaces utilisateurs isolés.

Pratique #5: La modularisation est une technique utilisée pour diviser un système logiciel en plusieurs modules distincts et indépendants capables d’effectuer des tâches de façon indépendante.

Dans cet article, nous avons présenté cinq pratiques de génie logiciel qui nous permettent de déployer des projets d'apprentissage automatique en exécutant notre modèle en tant que service Web axé sur REST. Nous abordons la pertinence du contrôle des versions des codes, de la documentation, des interfaces API REST, de la mise en conteneur et de la modularisation des codes comme étapes fondamentales à suivre dans votre CVDL. L'application de bonnes pratiques de développement logiciel et les outils mentionnés dans cet article amélioreront votre projet, votre collaboration en matière de code et le déploiement. Ce ne sont pas les seules bonnes pratiques sur lesquelles nous devrions nous concentrer, mais il s'agit d'un bon point de départ. Pour cet article, nous avons créé un modèle de projet de base suivant les pratiques mentionnées dans le présent article. N'hésitez pas à fourcher et réutiliser le modèle pour vos projets d'apprentissage automatique.

Date de modification :