Data Gouvernance
05/01/2021

Comment créer et déployer une API de Machine Learning avec FastAPI ?


Auteur : Victor Bigand
Temps de lecture : 12 minutes
Quantmetry.com : Comment créer et déployer une API de Machine Learning avec FastAPI ?

        La modélisation scientifique constitue le cœur du métier de data scientist, du cadrage à la création du modèle en passant par la préparation des données. D’après Venturebeat, 87% des projets IA en entreprise ne dépassent pas le stade du POC. Parmi les raisons de ces échecs, on trouve la séparation en silos des compétences, notamment les Data Scientists qui développent les modèles et les équipes en charge du déploiement de ces modèles. De ce constat est né le profil de Machine Learning Engineer, avec un rôle centré sur le déploiement de modèles en production. Ce tutoriel vous guide à travers un exemple simple de partage, au sein de votre équipe ou d’un groupe métier par exemple, d’un modèle de machine learning en s’appuyant en particulier sur la brique applicative FastAPI.

Il existe plusieurs frameworks pour créer une API en Python, les plus connus étant Django et Flask. FastAPI est un framework plus récent, inspiré de Flask, qui reprend la plupart de ses fonctionnalités et présente quelques nouveautés, notamment le fait de pouvoir gérer les appels asynchrones, valider les données d’entrée ou de sortie, sérializer les JSON par défaut ou encore une rédaction automatique de documentation standardisée.

Si vous n’êtes pas familiers avec ces notions, ne vous en faites pas, nous les reverrons plus loin dans cet article.

Voici le schéma représentant ce que nous allons mettre en place le long de ce tutoriel :

Architecture API

Nous allons donc développer une API avec FastAPI comprenant un modèle de Machine Learning (grâce à Tensorflow) et Uvicorn comme serveur web. Le tout sera packagé au sein d’une image Docker et déployé sur le cloud grâce à un service nommé Render, rendant l’API accessible à n’importe qui disposant d’une connexion web.

 Voici les différentes parties que nous allons aborder:

 Avant de débuter, nous devons choisir quel est le but de l’API et comment elle sera manipulée par nos utilisateurs. Pour ce tutoriel, nous allons utiliser un modèle pré-entraîné de classification d’images. Les utilisateurs pourront envoyer une requête à l’API avec une image en paramètre, et recevront en retour une liste des deux classes les plus probables détectées par notre modèle.

Spécifications de l’API et tests associés

        La première étape consiste à créer un squelette de code qui va initialiser l’API et contiendra une méthode de prédiction. Pour ce faire, nous allons créer un projet python avec votre éditeur de code préféré, ainsi qu’un environnement virtuel python associé. Ensuite, nous y ajoutons le fichier requirements.txt suivant :

fastapi==0.61.1
uvicorn==0.11.8
tensorflow==2.3.1
pillow==8.0.1
python-multipart==0.0.5
pytest==6.1.2

On y trouve:

  • Uvicorn, un serveur ASGI  (Asynchronous Server Gateway Interface) qui va permettre à notre application de communiquer de manière asynchrone avec le serveur que l’on va déployer. C’est une vraie différence par rapport à Flask, qui utilise WSGI, son prédécesseur, qui ne permettait nativement que l’exécution de requêtes synchrones. Dans le cas où une API effectue des tâches qui prennent du temps (dans notre cas, la lecture d’image n’est pas aussi rapide que l’envoi d’un nombre comme paramètre), l’asynchrone permet de traiter plusieurs requêtes en même temps en dissociant le traitement de la tâche du thread principal.
  • Tensorflow, librairie phare de Deep Learning, pour charger notre modèle de classification d’images et effectuer des prédictions.
  • Pillow, pour effectuer des manipulations sur les images (notamment les lire).
  • Python-multipart pour que FastAPI puisse recevoir des images en paramètre.

Une fois le fichier créé, on peut installer ces dépendances avec la commande  pip install -r requirements.txt.

Les librairies étant installées, nous allons définir un test vérifiant que l’API n’accepte que des images en entrée. Le fait de tester les données d’entrée, de sortie, ou le bon fonctionnement de l’API en amont nous permet d’éviter de tomber sur une erreur inattendue une fois l’API développée. Pour ce tutoriel, nous testerons uniquement le type des données reçues.

Pour cela, nous allons créer un dossier tests à la racine du projet, avec un fichier test_predict.py.

Nous allons ensuite ajouter un dossier files au sein de tests, contenant un fichier image et un fichier texte que vous pouvez ajouter manuellement (ex: un fichier .jpg et un fichier .txt)

Au sein du fichier test_predict, nous allons ajouter deux fonctions, qui vont chacune tester un appel de l’API avec un des fichiers.

from fastapi.testclient import TestClient

from app.main import app

client = TestClient(app)


def test_predict_image():

    filepath = "tests/files/picture.jpg"

    response = client.post(
        "/predict", files={"file": ("filename", open(filepath, "rb"), "image/jpeg")}
    )

    assert response.status_code == 200


def test_predict_text():

    filepath = "tests/files/text.txt"

    response = client.post(
        "/predict", files={"file": ("filename", open(filepath, "rb"), "text/plain")}
    )

    assert response.status_code == 400
    assert response.json() == {"detail": "File provided is not an image."}

On importe le TestClient, qui nous permet de requêter l’API au sein de tests, ainsi que notre API qui n’a pas encore été créée.

Chacun des tests effectue une requête à l’API avec un des fichiers, et vérifie la réponse. Les requêtes sont envoyées sur la route /predictUne réponse 200 signifie que la requête s’est bien déroulée, 400 que le fichier envoyé n’est pas au bon format.

Il nous reste maintenant à créer l’API. Pour cela, on va ajouter un fichier main.py au sein d’un nouveau dossier app situé à la racine du projet.

from fastapi import FastAPI, File, HTTPException, UploadFile


app = FastAPI()


@app.post("/predict")
def prediction(file: UploadFile = File(...)):

    # Initialize the data dictionnary that will be returned
    response = {"success": False}

    # Ensure that the file is an image
    if not file.content_type.startswith("image/"):
        raise HTTPException(status_code=400, detail="File provided is not an image.")

    return response

Dans ce fichier, on retrouve la création de l’API à la ligne 4, ensuite on va définir notre méthode POST, accessible au endpoint /predict, qui nous permet d’envoyer des données au serveur (contrairement à la méthode GET). Un simple décorateur @app.post suivi du endpoint nous permet d’associer la fonction prediction à l’API.

Pour l’instant, on ne fait que spécifier le paramètre file qui indique que l’on attend un fichier, et on vérifie que ce fichier est bien une image, sinon on renvoie un code d’erreur 400 et un message associé. 

Avant de lancer les tests, il faut ajouter pytest, la librairie qui nous permet de lancer les tests, aux requirements. Pytest étant utilisée pour la phase de développement, on peut l’ajouter dans un autre fichier appelé requirements_dev.txt à la racine du projet, puis lancer la commande pip install -r requirements_dev.txt.

-r requirements.txt
pytest==6.1.2

On peut à présent lancer les tests avec la commande python -m pytest et vous devriez voir que nos deux tests s’exécutent correctement.

 Voici l’arborescence du projet final, afin que vous puissiez correctement situer les différents fichiers que nous allons à présent rajouter: