D'un algorithme AI à une API pleinement fonctionnelle

Ismail Mebsout
23 octobre 2024
10 min
Sommaire

En tant que data scientist, lorsque vous travaillez sur un projet complexe avec d'autres développeurs, vous avez très souvent besoin d'empaqueter votre algorithme d'AI dans ce que l'on appelle une API qui peut être appelée par le backend afin de coordonner votre app. L'utilisation d'une API présente plusieurs avantages, rendant vos prédictions plus efficaces et moins chronophages.

Dans cet article, nous passerons en revue la définition d'une API en général avec un focus sur les APIs RESTful, puis nous créerons une API en python via les modules Flask et FastAPI. Enfin, nous verrons comment communiquer avec elle en utilisant un protocole HTTP via Curls ou via le logiciel Postman.

Le sommaire est le suivant :

  1. API & RESTful API
  2. Protocole HTTP & CURL & Postman
  3. Modèle de Data Science
  4. Flask
  5. FastAPI

API & RESTful API

Une API, pour Application Programming Interface, est un outil informatique qui permet d'empaqueter votre code en un service simple et efficace pour communiquer.
Cela peut être vu comme une étape de transformation de vos développements en une boîte noire avec des codes de communication prédéfinis vous permettant, en tant que provider, de l'exposer facilement aux clients ou consumers qui peuvent être des développeurs Front & Back-end de votre équipe.

Concept d'une API

Il existe de nombreuses APIs gratuites (Météo, Recherche de vols, Football, ...) qui peuvent être explorées sur RapidAPI.
Il existe plusieurs types d'APIs :

  • APIs Public ou Open : sans restrictions
  • APIs Private ou Internal : utilisées au sein d'une même entreprise
  • APIs Partner : nécessitent une licence pour y accéder

Si les APIs permettent à deux applications de communiquer, les Web Service APIs, quant à elles, permettent l'interaction entre deux machines sur un réseau donné. C'est un système qui utilise une url sur le World Wide Web pour fournir l'accès à ses services.
Il existe plusieurs types de web service APIs tels que SOAP, JSON-RPC, XML-RPC, etc. Dans cet article, nous nous concentrerons principalement sur les APIs REST qui sont d'un autre type.
Contrairement aux autres web service APIs qui sont des protocoles, REST, pour REpresentational State Transfer, est un ensemble de 5 principes architecturaux majeurs qui rendent le RESTful web service léger, efficace, scalable et simple à utiliser :

  • Client-server architecture : lorsque le client communique avec le serveur, il doit soit accepter sa requête et envoyer une réponse, soit la rejeter et l'en notifier
  • Statelessness : consiste à ne stocker aucune information sur le serveur. Cela implique également que le client doit s'assurer que toutes les données nécessaires sont dans la requête
  • Cacheability : est implémenté côté client afin de renvoyer une réponse plus rapide lors de l'envoi d'une ancienne requête
  • Layered system : est la capacité de superposer des couches additionnelles, par exemple une couche de sécurité ou un load balancer, sans affecter les échanges client-server
  • Uniform interface : en termes simples, est l'utilisation des URIs, pour Uniform Resource Identifiers, pour exposer la structure du repository. Combinée aux méthodes HTTP, elle permet des échanges XML ou JSON efficaces avec le serveur.
Concept d'API détaillé

Protocole HTTP & CURL & Postman

• Protocole HTTP

Une fois que vous avez créé votre web service API, vous devrez communiquer avec elle et c'est là qu'HTTP entre en jeu.
HTTP, pour HyperText Transfer Protocol, est un protocole de communication réseau utilisé pour échanger des données sur le web. Il a été conçu pour faciliter la communication entre les serveurs web et les navigateurs web tels que Google Chrome et Safari. C'est un protocole stateless qui suit l'architecture client-server ce qui le rend facile à intégrer aux APIs RESTful.

Protocole HTTP

Ci-dessous, certaines des méthodes les plus utilisées du protocole :

  • POST : crée une ressource sur le serveur
  • GET : accède à une ressource sur le serveur
  • PUT : met à jour une ressource sur le serveur
  • DELETE : supprime une ressource sur le serveur

• CURL

Très souvent, lorsque vous travaillez sur une machine virtuelle, vous n'avez accès qu'à une command line interface puisqu'il n'y a pas de graphical interface et donc pas de navigateur.
CURL, pour Client URL Request Library et précédemment nommé httpget, est un outil en ligne de commande utilisé pour obtenir et envoyer des ressources à un serveur connecté à un certain réseau. Il prend en charge de nombreux protocoles dont HTTP.
Il existe de nombreuses options de curl, mais dans cet article, nous nous concentrerons sur une spécifique pour notre RESTful API :

  • -X : détermine la méthode HTTP utilisée lors de la communication avec le serveur

• Postman

Postman est un logiciel ou une plateforme qui simplifie le développement et le test des APIs.
Grâce à son interface conviviale, il permet d'envoyer des requêtes très simplement en :

  • Choisissant la méthode HTTP
  • Entrant l'URL et le port sur lesquels l'API écoute
  • Sélectionnant les arguments, qui mettront automatiquement à jour la requête HTTP
  • Envoyant la requête à l'API

La réponse peut être visualisée en bas de la page dans la section body.

App Postman

Modèle de Data Science

À titre d'illustration, nous considérerons le dataset Iris dont la tâche ML consiste à classer les iris en trois classes (Setosa, Versicolour et Virginica) en utilisant quatre variables (Sepal Length, Sepal Width, Petal Length et Petal Width).

Le dataset iris sera téléchargé depuis Sklearn et nous utiliserons un random forest classifier pour l'entraînement

import numpy as np
import pandas as pd

from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix
from sklearn.metrics import accuracy_score
from sklearn.datasets import load_iris
from matplotlib import pyplot as plt
import joblib

%matplotlib inline
WEIGHTS_DIR="weights/"

iris = load_iris()

df=pd.DataFrame(iris.data, columns=iris.feature_names)
df["species"]=iris.target

X = df[iris.feature_names]
y = df['species']

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.20, random_state=42)

clf = RandomForestClassifier(max_depth=2, random_state=42)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)

print("Confusion matrix: \n", confusion_matrix(y_test, y_pred))
print("Accuracy score: ", accuracy_score(y_test, y_pred, normalize=True) )

joblib.dump(clf, WEIGHTS_DIR+"clf_iris.joblib")

Dans les paragraphes suivants, nous ferons la prédiction sur le dataset suivant :

Dataset de prédiction

Flask

Flask est un module python développé pour créer des APIs et exposer leurs services sur un réseau donné. Il peut être installé via la command-line suivante :

pip install flask

Dans le code suivant, je vais créer une flask API qui utilise le modèle entraîné précédemment pour prédire la classe des iris à partir des quatre variables en entrée.

• Initialisation de l'API

#%%
from flask import Flask, request, jsonify
import pandas as pd
import joblib
import json
WEIGHTS_DIR = "weights/"
FLASK_API = Flask(__name__)

• Chargement du modèle

def get_iris_model():
    loaded_clf = joblib.load(WEIGHTS_DIR + "clf_iris.joblib")
    return loaded_clf
loaded_clf = get_iris_model()
def str_to_float_list(arg):
    arg = arg.split(",")
    arg = [float(x) for x in arg]
    return arg

• Routing

Lors de la création d'une API, les Routes sont utilisées pour exposer ses fonctions et services. Dans flask, elles sont ajoutées à l'aide de décorateurs.

- predict_class_postman
Nous créons la route à travers laquelle nous ferons la prédiction. Cette route renvoie une réponse json avec la classe correspondante pour chaque ensemble de variables. Lors de l'utilisation de Postman, nous extrayons les variables via le paramètre args de la request.

#%%Postman
def get_params_postman(request):
    sep_length = str_to_float_list(request.args.get("sepLen"))
    sep_width = str_to_float_list(request.args.get("sepWid"))
    pet_length = str_to_float_list(request.args.get("petLen"))
    pet_width = str_to_float_list(request.args.get("petWid"))
    return (sep_length, sep_width, pet_length, pet_width)
@FLASK_API.route("/predict_class_postman", methods=["GET", "POST"])
def predict_class_postman():
    (sep_length, sep_width, pet_length, pet_width) = get_params_postman(request)
    new_row = pd.DataFrame(
        {
            "sepal length (cm)": [float(x) for x in sep_length],
            "sepal width (cm)": [float(x) for x in sep_width],
            "petal length (cm)": [float(x) for x in pet_length],
            "petal width (cm)": [float(x) for x in pet_width],
        }
    )
    y_pred = list(loaded_clf.predict(new_row))
    y_pred = [str(x) for x in y_pred]
    response = {"y_pred": ",".join(y_pred)}
    return jsonify(response)

- predict_class_curl
Nous créons une autre route cette fois pour communiquer avec la commande CURL. Nous extrayons les variables de la command-line en utilisant la méthode form.get de la request envoyée.

#%%CURL
def get_params_curl(request):
    request_input = request.form.get("input")
    request_input = json.loads(request_input)
    sep_length = str_to_float_list(request_input["sepLen"])
    sep_width = str_to_float_list(request_input["sepWid"])
    pet_length = str_to_float_list(request_input["petLen"])
    pet_width = str_to_float_list(request_input["petWid"])
    return (sep_length, sep_width, pet_length, pet_width)
@FLASK_API.route("/predict_class_curl", methods=["GET", "POST"])
def predict_class_curl():
    (sep_length, sep_width, pet_length, pet_width) = get_params_curl(request)
    new_row = pd.DataFrame(
        {
            "sepal length (cm)": [float(x) for x in sep_length],
            "sepal width (cm)": [float(x) for x in sep_width],
            "petal length (cm)": [float(x) for x in pet_length],
            "petal width (cm)": [float(x) for x in pet_width],
        }
    )
    y_pred = list(loaded_clf.predict(new_row))
    y_pred = [str(x) for x in y_pred]
    response = {"y_pred": ",".join(y_pred)}
    return jsonify(response)

• Démarrer le service

Une fois tous les éléments ci-dessus définis, nous démarrons le service de l'API en ajoutant le code suivant :

#%%
if __name__ == "__main__":
    FLASK_API.debug = True
    FLASK_API.run(host="0.0.0.0", port="8080")
  • Le debug mode peut être utile pour visualiser instantanément les changements
  • Nous pouvons choisir l'URL et le port sur lesquels l'API est exposée :

Pour lancer l'API, tapez :

python flask_api.py

flask_api.py est le fichier contenant tout le code développé ci-dessus.

Nous obtenons la réponse suivante :

>>> * Serving Flask app "flask_api" (lazy loading)
>>> * Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
>>> * Debug mode: on
>>> * Running on http://0.0.0.0:8080/ (Press CTRL+C to quit)
>>> * Restarting with fsevents reloader
>>> * Debugger is active!
>>> * Debugger PIN: 514-546-929

• Request & Response

+ Postman
Étant donné la HTTP request sur Postman

localhost:8080/predict_class_postman?sepLen=1,5&sepWid=2,6&petLen=3,7&petWid=4,8

La réponse est la suivante :

{
  "y_pred": "1,2"
}
Appel d'API via Postman

+ CURL
Nous lançons la command-line suivante en utilisant un curl pour communiquer avec l'API :

curl -F "input={\"sepLen\":\"1,5\",\"sepWid\":\"2,6\",\"petLen\":\"3,7\",\"petWid\":\"4,8\"}" -X POST "http://0.0.0.0:8080/predict_class_curl"

Comme attendu, nous obtenons les mêmes résultats :

Image par l'auteur

Codes HTTP : si la requête est correcte, l'API renvoie le code HTTP 200. Il existe d'autres codes tels que 4xx pour les erreurs client et 5xx pour les erreurs serveur.

Image par l'auteur

Vous pouvez trouver le code de la flask API dans mon repository GitHub.

FastAPI

FastAPI est un autre module python qui permet le développement d'APIs.
Il peut être installé via la command-line :

pip install fastapi

Il est très similaire à Flask, mais plus rapide, avec des modifications mineures :

  • Les Query parameters de postman sont extraits via request.query_params
  • Les Form parameters dans les curls sont obtenus via eval(input)input: str = Form(...)
#%%
import pandas as pd
import joblib
import json
from fastapi import FastAPI, Form, Request
import uvicorn

WEIGHTS_DIR = "weights/"
FASTAPI_API = FastAPI()

#%%
def get_iris_model():
    loaded_clf = joblib.load(WEIGHTS_DIR + "clf_iris.joblib")
    return loaded_clf


def str_to_float_list(arg):
    arg = arg.split(",")
    arg = [float(x) for x in arg]
    return arg


loaded_clf = get_iris_model()

#%%Postman
def get_params_postman(query_params):
    sep_length = str_to_float_list(query_params["sepLen"])
    sep_width = str_to_float_list(query_params["sepWid"])
    pet_length = str_to_float_list(query_params["petLen"])
    pet_width = str_to_float_list(query_params["petWid"])
    return (sep_length, sep_width, pet_length, pet_width)


@FASTAPI_API.post("/predict_class_postman")
def predict_class_postman(request: Request):
    query_params = dict(request.query_params)
    (sep_length, sep_width, pet_length, pet_width) = get_params_postman(query_params)
    new_row = pd.DataFrame(
        {
            "sepal length (cm)": [float(x) for x in sep_length],
            "sepal width (cm)": [float(x) for x in sep_width],
            "petal length (cm)": [float(x) for x in pet_length],
            "petal width (cm)": [float(x) for x in pet_width],
        }
    )
    y_pred = list(loaded_clf.predict(new_row))
    y_pred = [str(x) for x in y_pred]

    response = {"y_pred": ",".join(y_pred)}
    return response


#%%CURL
def get_params_curls(input_var):
    sep_length = str_to_float_list(input_var["sepLen"])
    sep_width = str_to_float_list(input_var["sepWid"])
    pet_length = str_to_float_list(input_var["petLen"])
    pet_width = str_to_float_list(input_var["petWid"])
    return (sep_length, sep_width, pet_length, pet_width)


@FASTAPI_API.post("/predict_class_curl")
def predict_class_curl(input: str = Form(...)):
    input_var = eval(input)
    (sep_length, sep_width, pet_length, pet_width) = get_params_curls(input_var)
    new_row = pd.DataFrame(
        {
            "sepal length (cm)": [float(x) for x in sep_length],
            "sepal width (cm)": [float(x) for x in sep_width],
            "petal length (cm)": [float(x) for x in pet_length],
            "petal width (cm)": [float(x) for x in pet_width],
        }
    )
    y_pred = list(loaded_clf.predict(new_row))
    y_pred = [str(x) for x in y_pred]

    response = {"y_pred": ",".join(y_pred)}
    return response


#%%
if __name__ == "__main__":
    uvicorn.run(FASTAPI_API, host="0.0.0.0", port=8080)

FastAPI est exécuté à l'aide de Uvicorn. C'est une implémentation de serveur ASGI ultra-rapide, basée sur uvloop et httptools où uvloop est un remplacement basé sur Cython pour l'event loop d'asyncio permettant d'être 2 à 4 fois plus rapide que l'event loop par défaut.
Uvicorn peut être installé via la command-line suivante :

pip install uvicorn

Pour lancer l'API, tapez :

python fastapi_api.py

fastapi_api.py est le fichier contenant tout le code développé ci-dessus.

Nous obtenons la réponse suivante :

>>> INFO:     Started server process [50003]
>>> INFO:     Waiting for application startup.
>>> INFO:     Application startup complete.
>>> INFO:     Uvicorn running on http://0.0.0.0:8080 (Press CTRL+C to quit)

• Request & Response

+ Postman
Étant donné la requête HTTP sur Postman :

localhost:8080/predict_class_postman?sepLen=1,5&sepWid=2,6&petLen=3,7&petWid=4,8

La réponse est la suivante :

{  "y_pred": "1,2"}
Image par l'auteur

+ CURL
Nous lançons la command-line suivante en utilisant un curl pour communiquer avec l'API :

curl -F "input={\"sepLen\":\"1,5\",\"sepWid\":\"2,6\",\"petLen\":\"3,7\",\"petWid\":\"4,8\"}" -X POST "http://0.0.0.0:8080/predict_class_curl"

Comme attendu, nous obtenons les mêmes résultats avec un code HTTP 200 :

{
"y_pred": "1,2"
}

Vous pouvez trouver le code de l'API FastAPI dans mon repository GitHub.

Conclusion

Les APIs sont des outils très puissants qui vous permettent d'exposer votre travail à des services et de faciliter la communication avec celui-ci. Lorsque vous travaillez dans une équipe de développeurs, maîtriser ces technologies devient crucial pour la progression du projet.

Restons en contact

Vous avez une question ? Nous serions ravis d'échanger avec vous.