Déployez vos modèles de Machine Learning avec Amazon SageMaker

Rédigé par Mohamed Reda LARBI YOUCEF le 21/10/2024
Temps de lecture: 12 minutes

1. Introduction

Dans un article précédent, nous avons vu et expliqué de manière théorique ce qu’était un workflow de machine learning. Nous avons vu les différentes étapes, de la collecte de données au déploiement et monitoring des modèles (Si vous n’avez pas encore lu l’article, c’est par ici). Aujourd’hui, nous allons voir ensemble comment mettre en place ce workflow dans le cloud, précisément avec AWS SageMaker.

2. Présentation d’Amazon SageMaker

2.1 Fonctionnalités

SageMaker est un service end-to-end pour le Machine Learning. Il permet le développement, l’entraînement et le déploiement des modèles. Il s’appuie sur l’infrastructure AWS pour fonctionner. Il dispose d’un grand nombre de fonctionnalités, parmi lesquelles :

  • Training Job : Comme son nom l’indique, celle-ci exécute la fonction train d’un modèle dans un environnement fourni par SageMaker. Une fois le job fini, le modèle produit est poussé dans S3. L’entraînement peut se faire en mode CPU ou en mode GPU selon le type d’instance que nous choisissons.

  • Inférence : SageMaker permet de faire de l’inférence de modèles de plusieurs types. Nous retrouvons l’inférence de modèle à travers une URL, appelée Endpoint. Nous pouvons également déployer les modèles en mode batch pour traiter des données et produire un résultat, à travers les batch transform jobs.

  • Processing jobs: SageMaker met également à disposition la possibilité de faire des traitements sur des données. Nous pouvons y faire du preprocessing, ou encore du feature engineering. Les processing jobs permettent aussi d’évaluer un modèle.

  • Inférence recommander : Une fonctionnalité sympathique permettant d’estimer la puissance nécessaire pour l’inférence d’un modèle, ce qui permet d’optimiser les coûts, tout en offrant des temps de réponse adaptés à nos préférences et contraintes.

  • Shadow tests : Cette fonctionnalité permet de tester une nouvelle version d’un modèle sans impacter les utilisatrices et utilisateurs.

Un des atouts majeurs de SageMaker est le SageMaker Studio. Celui-ci embarque une suite complète d’outils et de fonctionnalités pour produire un modèle, de son développement jusqu’à son déploiement. Il s’appuie d’une part sur les mécanismes que nous avons cité ci-dessus pour le preprocessing, l’entraînement et le déploiement. D’autre part, il embarque un Jupyter Notebook, un IDE VScode, de l’experiment tracking avec MLFlow, une registry de modèles, des pipelines, et bien d’autres encore. L’idée du SageMaker Studio est d’avoir tous les outils en un seul endroit.

2.2 Fonctionnement

Pour fonctionner, SageMaker s’appuie essentiellement sur la conteneurisation. L’idée est de fournir des images contenant toutes les dépendances nécessaires pour effectuer les tâches de processing, d’entraînement et d’inférence. Par dépendance, Nous retrouvons bien sûr le langage utilisé (Python dans la majorité des cas), la librairie ML telle que PyTorch ou TensorFlow, et éventuellement les drivers CUDA pour l’utilisation d’un GPU Nvidia. Plusieurs fonctionnalités de SageMaker consistent essentiellement en l’instanciation de conteneurs depuis ces images. AWS fournit des images prêtes à l’emploi. Nous pouvons également utiliser nos propres images.

Pour le stockage, SageMaker utilise S3 pour les données et les modèles. En effet, la phase d’entraînement a besoin d’un jeu de données. Celui-ci n’est pas embarqué dans les images. Il peut être stocké dans S3, puis téléchargé au démarrage du conteneur d’entraînement. De même, les modèles produits par la phase d’entraînement, ou encore les modèles utilisés lors de la phase d’inférence, sont également stockés sur S3 (Il existe tout de même une possibilité pour SageMaker de les récupérer autrement que sur S3, notamment sur Hugging Face, qui est, entre autres, une registry publique de modèles). Les modèles ne sont donc pas embarqués dans les images utilisées par SageMaker. Voici une illustration du process :

Architecture de SageMaker
Architecture de SageMaker

2.3 Facturation de SageMaker

Concernant la facturation, nous payons essentiellement les ressources utilisées par les fonctionnalités de SageMaker. Pour le Jupyter Notebook dans le SageMaker Studio par exemple, nous payons selon le type d’instance choisi et le temps passé sur le notebook. Pour les training jobs, nous payons selon le type d’instance et la durée du job. Pour la partie inférence, il existe 2 modes : le mode provisioned dans lequel nous payons à la demande selon le type d’instance et l’uptime de l’endpoint, et le mode Serverless dans lequel nous payons uniquement lorsque l’endpoint est consommé, modulo la quantité de RAM choisie. Il faut tout de même noter que plusieurs fonctionnalités possibles dans le mode provisioned sont indisponibles en Serverless. C’est le cas notamment de l’utilisation d’un GPU.

3. Cas pratique

Pour comprendre et tester SageMaker, démarrons d’un cas pratique. Pour notre besoin, nous souhaitons entraîner un modèle qui devra reconnaître les chiffres écrits manuellement dans des images. Nous allons utiliser Python ainsi que PyTorch, qui est une librairie ML parmi d’autres. Nous allons nous appuyer sur un jeu de données existant, à savoir Mnist, qui est un ensemble d’images écrites à la main. Nous allons aussi partir sur un code existant disponible dans les exemples de la librairie PyTorch. Celui-ci définit un modèle sous forme de réseau de neurones pour notre besoin.

Coté SageMaker, nous avons uniquement besoin d’entraîner le modèle et le déployer. Nous aurons donc besoin des training jobs. Pour la partie inférence, nous avons besoin de réponses immédiates du modèle. Nous souhaitons aussi disposer d’une URL pour interroger le modèle. Nous allons donc faire de l’inférence en Real-Time avec un Endpoint. Concernant le mode, nous allons utiliser le mode provisioned.

3.1 Entraînement d’un modèle

Nous allons naturellement commencer par l’entraînement du modèle. Commençons par examiner le code :

1class Net(nn.Module):
2   ...

Cette partie, sans rentrer dans les détails, contient la définition d’un réseau de neurones. Nous avons également la partie suivante :

1def train(args, modèle, device, train_loader, optimizer, epoch):
2   ...
3
4def test(model, device, test_loader):
5   ...

Ces deux fonctions sont les fonctions train et test qui permettent respectivement d’entraîner et d’évaluer le modèle. Concernant le jeu de données, il est téléchargé par les instructions suivantes dans la fonction main du script :

1dataset1 = datasets.MNIST('../data', train=True, download=True, transform=transform)
2dataset2 = datasets.MNIST('../data', train=False, transform=transform)

Il contient des images compressées de chiffres écrits à la main. Il est divisé en 2 : Le jeu de données d’entraînement, et le jeu de données d’évaluation. Il est important d’évaluer le modèle avec des données autres que les données d’entraînement. À noter que dans la pratique, la récolte des données ne se fait pas directement dans le code du modèle. Cependant pour simplifier les choses, nous allons le garder tel quel.

Nous allons maintenant produire un script qui va créer un training job sur SageMaker via Python et la librairie sagemaker :

 1def sagemaker_train():
 2   pyversion = "py311"
 3   framework_version = "2.3.0"
 4   hyperparameters = {"epochs": 1}
 5   instance_type = "ml.c5.2xlarge"
 6
 7
 8   estimator = PyTorch(
 9       entry_point="mnist.py",
10       py_version=pyversion,
11       framework_version=framework_version,
12       role=config.role,
13       instance_count=1,
14       instance_type=instance_type,
15       hyperparameters=hyperparameters
16   )
17
18
19   estimator.fit({"training": config.s3_data_url}, job_name=config.training_job_name)

Tout d’abord, nous avons noté précédemment que SageMaker fournit des images prêtes à l’emploi. Nous souhaitons pour notre besoin une image Python avec PyTorch. Celle-ci peut être automatiquement déterminée avec la version de Python et de PyTorch. Nous pouvons aussi la spécifier explicitement avec le champ image_uri. Nous remarquons également que nous pouvons choisir un type d’instance. Ceci doit être adapté à notre besoin (pour un usage de GPU, il faut bien s’assurer que le type d’instance contient bien un GPU, et que l’image est bien une image GPU contenant les drivers CUDA).

De plus, nous remarquons la présence des hyperparamètres. Ceux-ci nous permettent de personnaliser notre phase d’entraînement. Pour les renseigner, il faut fournir un dictionnaire Python. Celui-ci sera traduit sous la forme --key value. Dans notre cas, l’option --epochs 1 sera donc fournie au script d’entraînement. Il faut donc bien veiller à ce que le script d’entraînement puisse lire ces paramètres. Nous pouvons le voir dans le script mnist.py fournit par PyTorch :

1def main():
2   parser = argparse.ArgumentParser(description='PyTorch MNIST Example')
3   parser.add_argument('--batch-size', type=int, default=64, metavar='N',
4                       help='input batch size for training (default: 64)')
5   parser.add_argument('--test-batch-size', type=int, default=1000, metavar='N',
6                       help='input batch size for testing (default: 1000)')
7   parser.add_argument('--epochs', type=int, default=14, metavar='N',
8                       help='number of epochs to train (default: 14)')
9...

Enfin, la fonction fit de l’objet estimator permet de lancer l’entraînement. Avant de la lancer, nous devons traiter quelques détails. En effet, SageMaker fournit également des paramètres supplémentaires de son côté. Parmi ces variables, nous avons uniquement besoin de la variable SM_MODEL_DIR (sa valeur est /opt/ml/model), qui contient le chemin vers lequel le modèle doit être sauvegardé, permettant par la suite à SageMaker de l’uploader sur S3. Nous devons modifier le script mnist.py pour qu’il puisse la prendre en compte, et sauvegarder le modèle dans le dossier spécifié dans la variable :

1...
2
3def main():
4   ...
5   parser.add_argument("--model-dir", type=str, default=os.environ["SM_MODEL_DIR"])
6   ...
7
8   path = os.path.join(args.model_dir, "model.pth")
9   torch.save(model.cpu().state_dict(), path)

Une dernière chose à faire, nous devons ajouter les fonctions suivantes au script mnist.py :

 1def model_fn(model_dir):
 2   device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
 3   model = Net()
 4   with open(os.path.join(model_dir, "model.pth"), "rb") as f:
 5       model.load_state_dict(torch.load(f))
 6   return model.to(device)
 7
 8def predict_fn(input_object, model):
 9   with torch.no_grad():
10       prediction = model(input_object)
11   return prediction

Ces fonctions servent respectivement à dire à SageMaker comment charger un modèle sauvegardé dans un fichier, et comment exécuter un modèle sur un input. Elles serviront plus tard dans la partie déploiement. Nous pouvons les séparer dans un autre fichier mais j’ai préféré faire au plus simple pour cet article.

Nous sommes donc prêts à lancer la phase d’entraînement :

 1$ python sageamaker_train.py
 2...
 3INFO:sagemaker.image_uris:image_uri is not presented, retrieving image_uri based on instance_type, framework etc.
 4INFO:sagemaker:Creating training-job with name: cockpitio-mnist-14-33-05-25-08-2024
 52024-08-25 12:33:09 Starting - Starting the training job...
 62024-08-25 12:33:42 Downloading - Downloading input data...
 72024-08-25 12:33:52 Downloading - Downloading the training image......
 82024-08-25 12:34:48 Training - Training image download completed.
 9...
10Test set: Average loss: 0.0465, Accuracy: 9845/10000 (98%)
11...
12
132024-08-25 12:36:01 Uploading - Uploading generated training model
142024-08-25 12:36:01 Completed - Training job completed
15Training seconds: 139
16Billable seconds: 139
17Model data: s3://sagemaker-eu-west-1-XXXXXXXXXXXX/cockpitio-mnist-14-33-05-25-08-2024/output/model.tar.gz
18
19Sourcedir: s3://sagemaker-eu-west-1-XXXXXXXXXXXXX/cockpitio-mnist-14-33-05-25-08-2024/source/sourcedir.tar.gz

Et voilà ! Notre modèle est poussé sur S3 sous forme de fichier tar.gz. Le code du modèle utilisé est également poussé dans S3. Nous pouvons le voir dans l’interface de S3 :

Modèle dans S3
Modèle dans S3

Code dans S3
Code dans S3

Nous pouvons aussi voir que l’accuracy est d’environ 98%, et la loss moyenne de 0.0465, ce qui est déjà un bon premier indicateur sur la performance de notre modèle. Dans la pratique, il faudrait faire plusieurs lancements en variant les hyperparamètres. Ceci est possible dans le SageMaker Studio avec MLFlow.

3.2 Déploiement avec SageMaker

Une fois notre modèle produit et poussé dans S3, nous pouvons le déployer. Nous allons utiliser l’inférence de modèle via une URL HTTP. Dans SageMaker, cela se traduit en 3 étapes :

  • Création du modèle au sens SageMaker : Dans cette étape, nous indiquons dans SageMaker où se trouve le modèle. Nous choisissons également l’image Docker qui sera utilisée par le conteneur qui servira le modèle. (Dans notre cas, elle est déterminée automatiquement avec la version de Python et de PyTorch)
  • Création d’un endpoint configuration : Ici, nous choisissons un modèle (au sens SageMaker) à déployer, ainsi que le type d’endpoint que nous souhaitons, Provisioned ou Serverless. Dans le cas provisioned, nous choisissons essentiellement un type d’instance, tandis que dans le cas Serverless, nous choisissons une quantité de RAM.
  • Création d’un endpoint : Cette dernière étape déploie concrètement un conteneur qui sert le modèle. Elle nécessite un endpoint configuration, et fournit une URL avec laquelle on peut interroger le modèle. Petite subtilité, l’url est accessible uniquement si nous sommes authentifiés à AWS. Il est donc nécessaire de mettre un service en frontal pour servir le modèle publiquement. (Comme une API Gateway ou un Load Balancer).

Nous allons donc effectuer ces 3 étapes à l’aide d’un script Python :

 1...
 2
 3def sagemaker_deploy(model_data, sourcedir_data):
 4   model = PyTorchModel(
 5       model_data=model_data,
 6       source_dir=sourcedir_data,
 7       role=config.role,
 8       framework_version="2.3.0",
 9       py_version="py311",
10       entry_point="mnist.py"
11   )
12
13   predictor = model.deploy(initial_instance_count=1, instance_type="ml.c5.2xlarge", endpoint_name=config.endpoint_name)
14...

Nous déclarons notre modèle sous forme d’un objet PyTorchModel, dans lequel nous indiquons le chemin dans S3 vers l’artifact du modèle, ainsi que le chemin du code source utilisé. La librairie SageMaker va fusionner les deux fichiers en un seul tarball. De cette manière, lorsque le conteneur d’inférence télécharge le modèle, il pourra récupérer l’artifact du modèle, ainsi que le code contenant les deux fonctions model_fn et predict_fn que nous avons déclaré plus tôt.

Très bien, nous pouvons donc lancer notre déploiement :

1$ python sagemaker_deploy.py s3://sagemaker-eu-west-1-XXXXXXXXXXX/cockpitio-mnist-12-24-28-23-08-2024/output/model.tar.gz s3://sagemaker-eu-west-1-XXXXXXXXXXX/cockpitio-mnist-15-58-42-23-08-2024/source/sourcedir.tar.gz

Et voilà ! Notre modèle est déployé, nous pouvons le vérifier dans SageMaker :

Modèle dans SageMaker
Modèle dans SageMaker

Endpoint configuration dans SageMaker
Endpoint configuration dans SageMaker

Endpoint dans SageMaker
Endpoint dans SageMaker

C’est très bien, nous devons maintenant le tester. Pour cela, nous avons prévu un script Python :

 1...
 2def download_data():
 3   transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])
 4
 5   datasets.MNIST('data', train=True, download=True, transform=transform)
 6   datasets.MNIST('data', train=False, download=True, transform=transform)
 7
 8def get_random_number():
 9   ...
10
11   return payload
12
13def parse_response(response):
14   response_parsed = json.loads(str(response).replace("b", "").replace("'", ""))
15   labeled_predictions = list(zip(range(10), response_parsed[0]))
16   labeled_predictions.sort(key=lambda label_and_prob: 1.0 - label_and_prob[1])
17   print("Most likely answer: {}".format(labeled_predictions[0][0]))
18
19
20def main():
21   ...
22   download_data()
23   payload = get_random_number()
24
25   predictor = sagemaker.predictor.Predictor(endpoint_name=endpoint_name)
26   response = predictor.predict(payload, initial_args={'ContentType': 'application/json'})
27
28   parse_response(response)

Ce script télécharge le jeu de données en local, prend aléatoirement une image du jeu de données de test, l’affiche, et effectue une requête sur l’endpoint que nous avons déployé, à l’aide d’un objet Predictor, pour que celui-ci détermine un nombre à partir d’une image. Voyons cela :

1$ python sagemaker_test.py cockpitio-mnist-16-08-16-23-08-2024
2...

Nombre à détecter par le modèle
Nombre à détecter par le modèle

1Most likely answer: 8

Parfait ! À noter que pour cette démonstration, j’ai déployé manuellement. Bien entendu, dans un environnement de production, l’idéal est de le faire avec processus automatisé, utilisant un pipeline de CI/CD avec Terraform ou autre. La mise en place des pipelines et des processus pour le déploiement de modèles est un vaste sujet qui mérite un article à part entière.

4. Conclusion

Dans cet article, nous avons vu ensemble comment entraîner et déployer un modèle dans SageMaker. Ceci était bien évidemment une introduction. Il existe encore plusieurs fonctionnalités dans SageMaker qui peuvent être très intéressantes, notamment le SageMaker Studio. De même, des outils supplémentaires peuvent être ajoutés afin d’apporter plus d’automatisation au processus. Enfin, SageMaker est une plateforme parmi d’autres, il serait très intéressant de comparer avec des plateformes concurrentes. Vous l’aurez compris, le monde de l’IA et de la Data est très vaste qui n’attend qu’à être exploré !

Par ailleurs, l’ensemble du code utilisé par cet article se trouve sur ce repository github.