Application

Une application fil rouge pour illustrer l’intérêt d’appliquer graduellement les bonnes pratiques dans une optique de mise en production d’une application de data science.

Dérouler les slides ci-dessous ou cliquer ici pour afficher les slides en plein écran.

L’objectif de cette mise en application est d’illustrer les différentes étapes qui séparent la phase de développement d’un projet de celle de la mise en production. Elle permettra de mettre en pratique les différents concepts présentés tout au long du cours.

Celle-ci est un tutoriel pas à pas pour avoir un projet reproductible et disponible sous plusieurs livrables. Toutes les étapes ne sont pas indispensables à tous les projets de data science.

Nous nous plaçons dans une situation initiale correspondant à la fin de la phase de développement d’un projet de data science. On a un notebook un peu monolithique, qui réalise les étapes classiques d’un pipeline de machine learning :

L’objectif est d’améliorer le projet de manière incrémentale jusqu’à pouvoir le mettre en production, en le valorisant sous une forme adaptée.

Illustration de notre point de départ
Illustration de l’horizon vers lequel on se dirige
Important

Il est important de bien lire les consignes et d’y aller progressivement. Certaines étapes peuvent être rapides, d’autres plus fastidieuses ; certaines être assez guidées, d’autres vous laisser plus de liberté. Si vous n’effectuez pas une étape, vous risquez de ne pas pouvoir passer à l’étape suivante qui en dépend.

Bien que l’exercice soit applicable sur toute configuration bien faite, nous recommandons de privilégier l’utilisation du SSP Cloud, où tous les outils nécessaires sont pré-installés et pré-configurés. Le service VSCode ne sera en effet que le point d’entrée pour l’utilisation d’outils plus exigeants sur le plan de l’infrastructure: Argo, MLFLow, etc.

Partie 0 : initialisation du projet

Application préliminaire: forker le dépôt d’exemple

Les premières étapes consistent à mettre en place son environnement de travail sur Github:

  • Générer un jeton d’accès (token) sur GitHub afin de permettre l’authentification en ligne de commande à votre compte. La procédure est décrite ici. Vous ne voyez ce jeton qu’une fois, ne fermez pas la page de suite.

  • Mettez de côté ce jeton en l’enregistrant dans un gestionnaire de mot de passe ou dans l’espace “Mon compte” du SSP Cloud.

  • Forker le dépôt Github : https://github.com/ensae-reproductibilite/application-correction en faisant attention à deux choses:

    • Décocher la case “Copy the main branch only” afin de copier également les tags Git qui nous permettront de faire les checkpoint
Ce que vous devriez voir sur la page de création du fork

Il est maintenant possible de ce lancer dans la création de l’environnement de travail:

  • Ouvrir un service VSCode sur le SSP Cloud. Vous pouvez aller dans la page My Services et cliquer sur New service. Sinon, vous pouvez initialiser la création du service en cliquant directement ici. Modifier les options suivantes:
    • Dans l’onglet Kubernetes, sélectionner le rôle Admin ;
    • Dans l’onglet Networking, cliquer sur “Enable a custom service port” et laisser la valeur par défaut 5000 pour le numéro du port
  • Clôner votre dépôt Github en utilisant le terminal depuis Visual Studio (Terminal > New Terminal) et en passant directement le token dans l’URL selon cette structure:
terminal
$ git clone https://$TOKEN@github.com/$USERNAME/application-correction.git

$TOKEN et $USERNAME sont à remplacer, respectivement, par le jeton que vous avez généré précédemment et votre nom d’utilisateur.

  • Se placer avec le terminal dans le dossier en question :
terminal
$ cd application-correction
  • Se placer sur une branche de travail en faisant:
terminal
$ git checkout -b dev

Partie 1 : qualité du script

Cette première partie vise à rendre le projet conforme aux bonnes pratiques présentées dans le cours.

Elle fait intervenir les notions suivantes :

Le plan de la partie est le suivant :

  1. S’assurer que le script fonctionne ;
  2. Nettoyer le code des scories formelles avec un linter et un formatter ;
  3. Paramétrisation du script ;
  4. Utilisation de fonctions.

Étape 1 : s’assurer que le script s’exécute correctement

On va partir du fichier notebook.py qui reprend le contenu du notebook1 mais dans un script classique. Le travail de nettoyage en sera facilité.

La première étape est simple, mais souvent oubliée : vérifier que le code fonctionne correctement. Pour cela, nous recommandons de faire un aller-retour entre le script ouvert dans VSCode et un terminal pour le lancer.

Application 1: corriger les erreurs
  • Ouvrir dans VSCode le script titanic.py ;
  • Exécuter le script en ligne de commande (python titanic.py)2 pour détecter les erreurs ;
  • Corriger les deux erreurs qui empêchent la bonne exécution ;
  • Vérifier le fonctionnement du script en utilisant la ligne de commande:
terminal
$ python titanic.py

Le code devrait afficher des sorties.

Aide sur les erreurs rencontrées

La première erreur rencontrée est une alerte FileNotFoundError, la seconde est liée à un package.

Il est maintenant temps de commit les changements effectués avec Git3 :

terminal
$ git add titanic.py
$ git commit -m "Corrige l'erreur qui empêchait l'exécution"
$ git push
terminal
1$ git stash
$ git checkout appli1
1
Pour annuler les modifications depuis le dernier commit

Étape 2: utiliser un linter puis un formatter

On va maintenant améliorer la qualité de notre code en appliquant les standards communautaires. Pour cela, on va utiliser le linter classique PyLint et le formatter Black. Si vous désirez un outil deux en un, il est possible d’utiliser Ruff en complément ou substitut.

Ce nettoyage automatique du code permettra, au passage, de restructurer notre script de manière plus naturelle.

Important

PyLint et Black sont des packages Python qui s’utilisent principalement en ligne de commande.

Si vous avez une erreur qui suggère que votre terminal ne connait pas PyLint ou Black, n’oubliez pas d’exécuter la commande pip install pylint ou pip install black.

Le linter renvoie alors une série d’irrégularités, en précisant à chaque fois la ligne de l’erreur et le message d’erreur associé (ex : mauvaise identation). Il renvoie finalement une note sur 10, qui estime la qualité du code à l’aune des standards communautaires évoqués dans la partie Qualité du code.

Application 2: rendre lisible le script
  • Diagnostiquer et évaluer la qualité de titanic.py avec PyLint. Regarder la note obtenue.
  • Utiliser black titanic.py --diff --color pour observer les changements de forme que va induire l’utilisation du formatter Black. Cette étape n’applique pas les modifications, elle ne fait que vous les montrer.
  • Appliquer le formatter Black
  • Réutiliser PyLint pour diagnostiquer l’amélioration de la qualité du script et le travail qui reste à faire.
  • Comme la majorité du travail restant est à consacrer aux imports:
    • Mettre tous les imports ensemble en début de script
    • Retirer les imports redondants en s’aidant des diagnostics de votre éditeur
    • Réordonner les imports si PyLint vous indique de le faire
    • Corriger les dernières fautes formelles suggérées par PyLint
  • Délimiter des parties dans votre code pour rendre sa structure plus lisible. Si des parties vous semblent être dans le désordre, vous pouvez réordonner le script (mais n’oubliez pas de le tester)
terminal
1$ git stash
$ git checkout appli2
1
Pour annuler les modifications depuis le dernier commit

Le code est maintenant lisible, il obtient à ce stade une note formelle proche de 10. Mais il n’est pas encore totalement intelligible ou fiable. Il y a notamment quelques redondances de code auxquelles nous allons nous attaquer par la suite. Néanmoins, avant cela, occupons-nous de mieux gérer certains paramètres du script: jetons d’API et chemin des fichiers.

Étape 3: gestion des paramètres

L’exécution du code et les résultats obtenus dépendent de certains paramètres définis dans le code. L’étude de résultats alternatifs, en jouant sur des variantes des (hyper)paramètres, est à ce stade compliquée car il est nécessaire de parcourir le code pour trouver ces paramètres. De plus, certains paramètres personnels comme des jetons d’API ou des mots de passe n’ont pas vocation à être présents dans le code.

Il est plus judicieux de considérer ces paramètres comme des variables d’entrée du script. Cela peut être fait de deux manières:

  1. Avec des arguments optionnels appelés depuis la ligne de commande (Application 3a). Cela peut être pratique pour mettre en oeuvre des tests automatisés mais n’est pas forcément pertinent pour toutes les variables. Nous allons montrer cet usage avec le nombre d’arbres de notre random forest ;
  2. En utilisant un fichier de configuration dont les valeurs sont importées dans le script principal (Application 3b).
Un exemple de définition d’un argument pour l’utilisation en ligne de commande
prenom.py
import argparse
parser = argparse.ArgumentParser(description="Qui êtes-vous?")
parser.add_argument(
    "--prenom", type=str, default="Toto", help="Un prénom à afficher"
)
args = parser.parse_args()
print(args.prenom)

Exemples d’utilisations en ligne de commande

terminal
$ python prenom.py
$ python prenom.py --prenom "Zinedine"
Application 3a: Paramétrisation du script
  1. En s’inspirant de l’exemple ci-dessus 👆️, créer une variable n_trees qui peut éventuellement être paramétrée en ligne de commande et dont la valeur par défaut est 20 ;
  2. Tester cette paramétrisation en ligne de commande avec la valeur par défaut puis 2, 10 et 50 arbres.

L’exercice suivant permet de mettre en application le fait de paramétriser un script en utilisant des variables définies dans un fichier YAML.

Application 3b: La configuration dans un fichier YAML

Nous allons mettre 4 paramètres dans notre YAML. Celui-ci prendra la forme suivante:

config.yaml
jeton_api: ####
data_path: ####

Avec #### des valeurs à remplacer.

  1. Créer à la racine du projet un fichier config.yaml à partir du modèle 👆️ ;
  2. Repérer les valeurs dans le code associé et compléter.

Maintenant, nous allons exploiter ce fichier:

  1. Pour éviter d’avoir à le faire plus tard, créer une fonction import_yaml_config qui prend en argument le chemin d’un fichier YAML et renvoie le contenu de celui-ci en output. Vous pouvez suivre le conseil du chapitre sur la Qualité du code en adoptant le type hinting ;
Indice si vous ne trouvez pas comment lire un fichier YAML

Si le fichier s’appelle toto.yaml, vous pouvez l’importer de cette manière:

with open("toto.yaml", "r", encoding="utf-8") as stream:
    dict_config = yaml.safe_load(stream)
  1. Dans la fonction import_yaml_config, créer une condition logique pour tenir compte du fait que le YAML de configuration peut ne pas exister4 ;
Indice si vous ne savez comment conditionner la création de la configuration à l’existence du fichier

Voici la ligne qui peut vous aider. L’idéal est d’insérer ceci dans import_yaml_config:

CONFIG_PATH = 'config.yaml'
config = {}
if os.path.exists(CONFIG_PATH):
    # lecture du fichier
  1. Utiliser le canevas de code suivant pour créer les variables adéquates
API_TOKEN = config.get("jeton_api")
TRAIN_PATH = config.get("train_path", "train.csv")
TEST_PATH = config.get("test_path", "test.csv")
TEST_FRACTION = config.get("test_fraction", .1)

et remplacer dans le code ;

  1. Tester en ligne de commande que l’exécution du fichier est toujours sans erreur et sinon corriger ;
  2. Refaire un diagnostic avec PyLint et corriger les éventuels messages ;
  3. Créer un fichier .gitignore (cf. Chapitre Git). Ajouter dans ce fichier config.yaml car il ne faut pas committer ce fichier. Au passage ajouter __pycache__/ au .gitignore5, cela évitera d’avoir à le faire ultérieurement ;
  4. Créer un fichier README.md où vous indiquez qu’il faut créer un fichier config.yaml pour pouvoir utiliser l’API.
terminal
1$ git stash
$ git checkout appli3
1
Pour annuler les modifications depuis le dernier commit

Étape 4 : Privilégier la programmation fonctionnelle

Nous allons mettre en fonctions les parties importantes de l’analyse. Ceci facilitera l’étape ultérieure de modularisation de notre projet.

Cet exercice étant chronophage, il n’est pas obligatoire de le réaliser en entier. L’important est de comprendre la démarche et d’adopter fréquemment une approche fonctionnelle6. Pour obtenir une chaine entièrement fonctionnalisée, vous pouvez reprendre le checkpoint.

Application 4: adoption des standards de programmation fonctionnelle

Cette application peut être chronophage, vous pouvez aller plus ou moins loin dans la fonctionalisation de votre script en fonction du temps dont vous disposez.

  • Créer une fonction générique pour réduire la redondance de code dans l’étape d’exploration des données où on utilise split ;
  • Créer une fonction qui réalise le split train/test en fonction d’un paramètre représentant la proportion de l’échantillon de test et d’arguments optionnels sur les chemins d’écriture des deux échantillons en csv.
  • Créer une fonction qui intègre les différentes étapes du pipeline (preprocessing et définition du modèle). Cette fonction prend en paramètre le nombre d’arbres (argument obligatoire) et des arguments optionnels supplémentaires (les colonnes sur lesquelles s’appliquent les différentes étapes du pipeline, max_depth et max_features).
  • Créer une fonction d’évaluation renvoyant le score obtenu et la matrice de confusion, à l’issue d’une estimation (mais cette estimation est faite en amont de la fonction, pas au sein de celle-ci)
  • Déplacer toutes les fonctions ensemble, en début de script.
Important

Le fait d’appliquer des fonctions a déjà amélioré la fiabilité du processus en réduisant le nombre d’erreurs de copier-coller. Néanmoins, pour vraiment fiabiliser le processus, il faudrait utiliser un pipeline de transformations de données.

Ceci n’est pas encore au programme du cours mais le sera dans une prochaine version.

terminal
1$ git stash
$ git checkout appli4
1
Pour annuler les modifications depuis le dernier commit

Cela ne se remarque pas encore vraiment car nous avons de nombreuses définitions de fonctions mais notre chaine de production est beaucoup plus concise (le script fait environ 300 lignes dont 250 de définitions de fonctions génériques). Cette auto-discipline facilitera grandement les étapes ultérieures. Cela aurait été néanmoins beaucoup moins coûteux en temps d’adopter ces bons gestes de manière plus précoce.

Partie 2 : adoption d’une structure modulaire

Dans la partie précédente, on a appliqué de manière incrémentale de nombreuses bonnes pratiques vues tout au long du cours. Ce faisant, on s’est déjà considérablement rapprochés d’un possible partage du code : celui-ci est lisible et intelligible. Le code est proprement versionné sur un dépôt GitHub. Cependant, le projet est encore perfectible: il est encore difficile de rentrer dedans si on ne sait pas exactement ce qu’on recherche. L’objectif de cette partie est d’isoler les différentes étapes de notre pipeline. Outre le gain de clarté pour notre projet, nous économiserons beaucoup de peines pour la mise en production ultérieure de notre modèle.

Illustration de l’état actuel du projet

Dans cette partie nous allons continuer les améliorations incrémentales de notre projet avec les étapes suivantes:

  1. Modularisation du code Python pour séparer les différentes étapes de notre pipeline ;
  2. Adopter une structure standardisée pour notre projet afin d’autodocumenter l’organisation de celui-ci ;
  3. Documenter les packages indispensables à l’exécution du code ;
  4. Stocker les données dans un environnement adéquat afin de continuer la démarche de séparer conceptuellement les données du code en de la configuration.

Étape 1 : modularisation

Nous allons profiter de la modularisation pour adopter une structure applicative pour notre code. Celui-ci n’étant en effet plus lancé que depuis la ligne de commande, on peut considérer qu’on construit une application générique où un script principal (main.py) encapsule des éléments issus d’autres scripts Python.

Application 5: modularisation
  • Déplacer les fonctions dans une série de fichiers dédiés:
    • import_data.py: fonctions d’import et d’exploration de données
    • build_features.py: fonctions regroupant la définition des échantillons d’apprentissage et de test ainsi que le pipeline
    • train_evaluate.py: fonctions d’évaluation du modèle
  • Spécifier les dépendances (i.e. les packages à importer) dans les modules pour que ceux-ci puissent s’exécuter indépendamment ;
  • Renommer titanic.py en main.py pour suivre la convention de nommage des projets Python ;
  • Importer les fonctions nécessaires à partir des modules.
  • Vérifier que tout fonctionne bien en exécutant le script main à partir de la ligne de commande :
terminal
$ python main.py
terminal
1$ git stash
$ git checkout appli5
1
Pour annuler les modifications depuis le dernier commit

Étape 2 : adopter une architecture standardisée de projet

On dispose maintenant d’une application Python fonctionnelle. Néanmoins, le projet est certes plus fiable mais sa structuration laisse à désirer et il serait difficile de rentrer à nouveau dans le projet dans quelques temps.

Etat actuel du projet 🙈
├── .gitignore
├── data.csv
├── train.csv
├── test.csv
├── README.md
├── config.yaml
├── import_data.py
├── build_features.py
├── train_evaluate.py
├── titanic.ipynb
└── main.py

Comme cela est expliqué dans la partie Structure des projets, on va adopter une structure certes arbitraire mais qui va faciliter l’autodocumentation de notre projet. De plus, une telle structure va faciliter des évolutions optionnelles comme la packagisation du projet. Passer d’une structure modulaire bien faite à un package est quasi-immédiat en Python.

On va donc modifier l’architecture de notre projet pour la rendre plus standardisée. Pour cela, on va s’inspirer des structures cookiecutter qui génèrent des templates de projet. En l’occurrence notre source d’inspiration sera le template datascience issu d’un effort communautaire.

Note

L’idée de cookiecutter est de proposer des templates que l’on utilise pour initialiser un projet, afin de bâtir à l’avance une structure évolutive. La syntaxe à utiliser dans ce cas est la suivante :

terminal
$ pip install cookiecutter
$ cookiecutter https://github.com/drivendata/cookiecutter-data-science

Ici, on a déjà un projet, on va donc faire les choses dans l’autre sens : on va s’inspirer de la structure proposée afin de réorganiser celle de notre projet selon les standards communautaires.

En s’inspirant du cookiecutter data science on va adopter la structure suivante:

Structure recommandée
application
├── main.py
├── README.md
├── data
│   ├── raw
│   │   └── data.csv
│   └── derived
│       ├── test.csv
│       └── train.csv
├── configuration
│   └── config.yaml
├── notebooks
│   └── titanic.ipynb
└── src
    ├── data
    │   └── import_data.py
    ├── pipeline
    │   └── build_pipeline.py
    └── models
        └── train_evaluate.py
Application 6: adopter une structure lisible
  • (optionnel) Analyser et comprendre la structure de projet proposée par le template ;
  • Modifier l’arborescence du projet selon le modèle ;
  • Mettre à jour l’import des dépendances, le fichier de configuration et main.py avec les nouveaux chemins ;
terminal
1$ git stash
$ git checkout appli6
1
Pour annuler les modifications depuis le dernier commit

Étape 3: indiquer l’environnement minimal de reproductibilité

Le script main.py nécessite un certain nombre de packages pour être fonctionnel. Chez vous les packages nécessaires sont bien sûr installés mais êtes-vous assuré que c’est le cas chez la personne qui testera votre code ?

Afin de favoriser la portabilité du projet, il est d’usage de “fixer l’environnement”, c’est-à-dire d’indiquer dans un fichier toutes les dépendances utilisées ainsi que leurs version. Nous proposons de créer un fichier requirements.txt minimal, sur lequel nous reviendrons dans la partie consacrée aux environnements reproductibles.

Le fichier requirements.txt est conventionnellement localisé à la racine du projet. Ici on ne va pas fixer les versions, on raffinera ce fichier ultérieurement.

Application 7: création du requirements.txt
  • Créer un fichier requirements.txt avec la liste des packages nécessaires
  • Ajouter une indication dans README.md sur l’installation des packages grâce au fichier requirements.txt
terminal
1$ git stash
$ git checkout appli7
1
Pour annuler les modifications depuis le dernier commit

Étape 4 : stocker les données de manière externe

Pour mettre en oeuvre cette étape, il peut être utile de comprendre un peu comme fonctionne le SSP Cloud. Vous devrez suivre la documentation du SSP Cloud pour la réaliser. Une aide-mémoire est également disponible dans le cours de 2e année de l’ENSAE Python pour la data science.

Le chapitre sur la structure des projets développe l’idée qu’il est recommandé de converger vers un modèle où environnements d’exécution, de stockage du code et des données sont conceptuellement séparés. Ce haut niveau d’exigence est un gain de temps important lors de la mise en production car au cours de cette dernière, le projet est amené à être exécuté sur une infrastructure informatique dédiée qu’il est bon d’anticiper.

A l’heure actuelle, les données sont stockées dans le dépôt. C’est une mauvaise pratique. En premier lieu, Git n’est techniquement pas bien adapté au stockage de données. Ici ce n’est pas très grave car il ne s’agit pas de données volumineuses et ces dernières ne sont pas modifiées au cours de notre chaine de traitement. La raison principale est que les données traitées par les data scientists sont généralement soumises à des clauses de confidentialités (RGPD, secret statistique…). Mettre ces données sous contrôle de version c’est prendre le risque de les divulguer à un public non habilité. Il est donc recommandé de privilégier des outils techniques adaptés au stockage de données.

L’idéal, dans notre cas, est d’utiliser une solution de stockage externe. On va utiliser pour cela MinIO, la solution de stockage de type S3 offerte par le SSP Cloud. Cela nous permettra de supprimer les données de Github tout en maintenant la reproductibilité de notre projet 7.

Application 8: utilisation d’un système de stockage distant

A partir de la ligne de commande, utiliser l’utilitaire MinIO pour copier les données data/raw/data.csv vers votre bucket personnel. Les données intermédiaires peuvent être laissées en local mais doivent être ajoutées au .gitignore.

Indice

Structure à adopter:

terminal
$ mc cp data/raw/data.csv s3/$BUCKET_PERSONNEL/ensae-reproductibilite/data/raw/data.csv
en modifiant $BUCKET_PERSONNEL, l’emplacement de votre bucket personnel

Pour se simplifier la vie, on va utiliser des URL de téléchargement des fichiers (comme si ceux-ci étaient sur n’importe quel espace de stockage) plutôt que d’utiliser une librairie S3 compatible comme boto3 ou s3fs. Par défaut, le contenu de votre bucket est privé, seul vous y avez accès. Néanmoins, vous pouvez rendre accessible à tous en lecture le contenu de votre bucket en faisant lui donnant des droits anonymes. Pour cela, en ligne de commande, faire:

terminal
$ mc anonymous set download s3/$BUCKET_PERSONNEL/ensae-reproductibilite/data/raw/

en modifiant $BUCKET_PERSONNEL. Les URL de téléchargement seront de la forme https://minio.lab.sspcloud.fr/$BUCKET_PERSONNEL/ensae-reproductibilite/data/raw/data.csv

  • Modifier configuration/config.yaml pour utiliser directement les URL dans l’import ;
  • Modifier les valeurs par défaut dans votre code ;
  • Ajouter le dossier data/ au .gitignore
  • Supprimer le dossier data de votre projet et faites git rm --cached -r data
  • Vérifier le bon fonctionnement de votre application.

Maintenant qu’on a arrangé la structure de notre projet, c’est l’occasion de supprimer le code qui n’est plus nécessaire au bon fonctionnement de notre projet (cela réduit la charge de maintenance8).

Pour vous aider, vous pouvez utiliser vulture de manière itérative pour vous assister dans le nettoyage de votre code.

terminal
$ vulture main.py src/
Exemple de sortie
terminal
$ vulture main.py src/
main.py:21: unused variable 'jeton_api' (60% confidence)
main.py:36: unused variable 'ticket_count' (60% confidence)
main.py:37: unused variable 'name_count' (60% confidence)
terminal
1$ git stash
$ git checkout appli8
1
Pour annuler les modifications depuis le dernier commit

Partie 2bis: packagisation de son projet (optionnel)

Cette série d’actions n’est pas forcément pertinente pour tous les projets. Elle fait un peu la transition entre la modularité et la portabilité.

Étape 1 : proposer des tests unitaires (optionnel)

Notre code comporte un certain nombre de fonctions génériques. On peut vouloir tester leur usage sur des données standardisées, différentes de celles du Titanic.

Même si la notion de tests unitaires prend plus de sens dans un package, nous pouvons proposer dans le projet des exemples d’utilisation de la fonction, ceci peut être pédagogique.

Nous allons utiliser unittest pour effectuer des tests unitaires. Cette approche nécessite quelques notions de programmation orientée objet ou une bonne discussion avec ChatGPT.

Application 9: test unitaire (optionnel)

Dans le dossier tests/, créer avec l’aide de ChatGPT ou de Copilot un test pour la fonction split_and_count.

  • Effectuer le test unitaire en ligne de commande avec unittest (python -m unittest tests/test_split.py). Corriger le test unitaire en cas d’erreur.
  • Si le temps le permet, proposer des variantes ou d’autres tests.
terminal
1$ git stash
$ git checkout appli9
1
Pour annuler les modifications depuis le dernier commit

Note

Lorsqu’on effectue des tests unitaires, on cherche généralement à tester le plus de lignes possibles de son code. On parle de taux de couverture (coverage rate) pour désigner la statistique mesurant cela.

Cela peut s’effectuer de la manière suivante avec le package coverage:

terminal
$ coverage run -m unittest tests/test_create_variable_title.py
$ coverage report -m
Name                                  Stmts   Miss  Cover   Missing
-------------------------------------------------------------------
src/features/build_features.py           34     21    38%   35-36, 48-58, 71-74, 85-89, 99-101, 111-113
tests/test_create_variable_title.py      21      1    95%   54
-------------------------------------------------------------------
TOTAL                                    55     22    60%

Le taux de couverture est souvent mis en avant par les gros projets comme indicateur de leur qualité. Il existe d’ailleurs des badges Github dédiés.

Étape 2 : transformer son projet en package (optionnel)

Notre projet est modulaire, ce qui le rend assez simple à transformer en package, en s’inspirant de la structure du cookiecutter adapté, issu de cet ouvrage.

On va créer un package nommé titanicml qui encapsule tout notre code et qui sera appelé par notre script main.py. La structure attendue est la suivante:

Structure visée
ensae-reproductibilite-application
├── docs                                    ┐ 
│   ├── main.py                             │ 
│   └── notebooks                           │ Package documentation and examples
│       └── titanic.ipynb                   │ 
├── configuration                           ┐ Configuration (pas à partager avec Git)
│   └── config.yaml                         ┘ 
├── README.md                                
├── pyproject.toml                          ┐ 
├── requirements.txt                        │
├── titanicml                               │                
│   ├── __init__.py                         │ Package source code, metadata
│   ├── data                                │ and build instructions 
│   │   ├── import_data.py                  │  
│   │   └── test_create_variable_title.py   │   
│   ├── features                            │
│   │   └── build_features.py               │
│   └── models                              │
│       └── train_evaluate.py               ┘
└── tests                                   ┐
    └── test_create_variable_title.py       ┘ Package tests
Rappel: structure actuelle
ensae-reproductibilite-application
├── notebooks                                 
│   └── titanic.ipynb                  
├── configuration                                 
│   └── config.yaml                  
├── main.py                              
├── README.md                 
├── requirements.txt                      
└── src 
    ├── data                                
    │   ├── import_data.py                    
    │   └── test_create_variable_title.py      
    ├── features                           
    │   └── build_features.py      
    └── models                          
        └── train_evaluate.py              

Il existe plusieurs frameworks pour construire un package. Nous allons privilégier Poetry à Setuptools.

Note

Pour créer la structure minimale d’un package, le plus simple est d’utiliser le cookiecutter adapté, issu de cet ouvrage.

Comme on a déjà une structure très modulaire, on va plutôt recréer cette structure dans notre projet déjà existant. En fait, il ne manque qu’un fichier essentiel, le principal distinguant un projet classique d’un package : pyproject.toml.

terminal
$ cookiecutter https://github.com/py-pkgs/py-pkgs-cookiecutter.git
Dérouler pour voir les choix possibles
author_name [Monty Python]: Daffy Duck
package_name [mypkg]: titanicml
package_short_description []: Impressive Titanic survival analysis
package_version [0.1.0]: 
python_version [3.9]: 
Select open_source_license:
1 - MIT
2 - Apache License 2.0
3 - GNU General Public License v3.0
4 - Creative Commons Attribution 4.0
5 - BSD 3-Clause
6 - Proprietary
7 - None
Choose from 1, 2, 3, 4, 5, 6 [1]: 
Select include_github_actions:
1 - no
2 - ci
3 - ci+cd
Choose from 1, 2, 3 [1]:
Application 10: packagisation (optionnel)
  • Renommer le dossier titanicml pour respecter la nouvelle arborescence ;
  • Créer un fichier pyproject.toml sur cette base ;
pyproject.toml
[tool.poetry]
name = "titanicml"
version = "0.0.1"
description = "Awesome Machine Learning project"
authors = ["Daffy Duck <daffy.duck@fauxmail.fr>", "Mickey Mouse"]
license = "MIT"
readme = "README.md"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

[tool.pytest.ini_options]
log_cli = true
log_cli_level = "WARNING"
log_cli_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)"
log_cli_date_format = "%Y-%m-%d %H:%M:%S"
  • Créer le dossier docs et mettre les fichiers indiqués dedans
  • Dans titanicml/, créer un fichier __init__.py9
__init__.py
from .import_data import (
    import_data, import_yaml_config
)
from .build_features import (
    create_variable_title,
    fill_na_titanic,
    label_encoder_titanic,
    check_has_cabin,
    ticket_length
)
from .train_evaluate import random_forest_titanic

__all__ = [
    "import_data", "import_yaml_config",
    "create_variable_title",
    "fill_na_titanic",
    "label_encoder_titanic",
    "check_has_cabin",
    "ticket_length",
    "random_forest_titanic"
]
  • Installer le package en local avec pip install -e .
  • Modifier le contenu de docs/main.py pour importer les fonctions de notre package titanicml et tester en ligne de commande notre fichier main.py
terminal
1$ git stash
$ git checkout appli10
1
Pour annuler les modifications depuis le dernier commit

Partie 3 : construction d’un projet portable et reproductible

Dans la partie précédente, on a appliqué de manière incrémentale de nombreuses bonnes pratiques vues dans les chapitres Qualité du code et Structure des projets tout au long du cours.

Ce faisant, on s’est déjà considérablement rapprochés d’une possible mise en production : le code est lisible, la structure du projet est normalisée et évolutive, et le code est proprement versionné sur un dépôt GitHub .

Illustration de l’état actuel du projet

A présent, nous avons une version du projet qui est largement partageable. Du moins en théorie, car la pratique est souvent plus compliquée : il y a fort à parier que si vous essayez d’exécuter votre projet sur un autre environnement (typiquement, votre ordinateur personnel), les choses ne se passent pas du tout comme attendu. Cela signifie qu’en l’état, le projet n’est pas portable : il n’est pas possible, sans modifications coûteuses, de l’exécuter dans un environnement différent de celui dans lequel il a été développé.

Dans cette troisème partie de notre travail vers la mise en production, nous allons voir comment normaliser l’environnement d’exécution afin de produire un projet portable. Autrement dit, nous n’allons plus nous contenter de modularité mais allons rechercher la portabilité. On sera alors tout proche de pouvoir mettre le projet en production.

On progressera dans l’échelle de la reproductibilité de la manière suivante:

  1. Environnements virtuels ;
  2. Créer un script shell qui permet, depuis un environnement minimal, de construire l’application de A à Z ;
  3. Images et conteneurs Docker.

Nous allons repartir de l’application 8, c’est-à-dire d’un projet modulaire mais qui n’est pas, à strictement parler, un package (objet des applications optionnelles suivantes 9 et 10).

Pour se replacer dans l’état du projet à ce niveau, il est possible d’utiliser le tag ad hoc.

terminal
$ git checkout appli8

Étape 1 : un environnement pour rendre le projet portable

Pour qu’un projet soit portable, il doit remplir deux conditions:

  • Ne pas nécessiter de dépendance qui ne soient pas renseignées quelque part ;
  • Ne pas proposer des dépendances inutiles, qui ne sont pas utilisées dans le cadre du projet.

Le prochain exercice vise à mettre ceci en oeuvre. Comme expliqué dans le chapitre portabilité, le choix du gestionnaire d’environnement est laissé libre. Il est recommandé de privilégier venv si vous découvrez la problématique de la portabilité.

L’approche la plus légère est l’environnement virtuel. Nous avons en fait implicitement déjà commencé à aller vers cette direction en créant un fichier requirements.txt.

Application 11a: environnement virtuel venv
  1. Exécuter pip freeze en ligne de commande et observer la (très) longue liste de package
  2. Créer l’environnement virtuel titanic en s’inspirant de la documentation officielle10 ou du chapitre dédié
  3. Utiliser ls pour observer et comprendre le contenu du dossier titanic/bin installé
  4. Activer l’environnement et vérifier l’installation de Python maintenant utilisée par votre machine
  5. Vérifier directement depuis la ligne de commande que Python exécute bien une commande11 avec:
terminal
$ python -c "print('Hello')"
  1. Faire la même chose mais avec import pandas as pd
  2. Installer les packages à partir du requirements.txt. Tester à nouveau import pandas as pd pour comprendre la différence.
  3. Exécuter pip freeze et comprendre la différence avec la situation précédente.
  4. Vérifier que le script main.py fonctionne bien. Sinon ajouter les packages manquants dans le requirements.txt et reprendre de manière itérative à partir de la question 7.
  5. Ajouter le dossier titanic/ au .gitignore pour ne pas ajouter ce dossier à Git.
Aide pour la question 4

Après l’activation, vous pouvez vérifier quel python est utilisé de cette manière

terminal
(titanic) $ which python
terminal
1$ git stash
$ git checkout appli11a
1
Pour annuler les modifications depuis le dernier commit

Les environnements conda sont plus lourds à mettre en oeuvre que les environnements virtuels mais peuvent permettre un contrôle plus formel des dépendances.

Application 11b: environnement conda
  1. Exécuter conda env export en ligne de commande et observer la (très) longue liste de package
  2. Créer un environnement titanic avec conda create
  3. Activer l’environnement et vérifier l’installation de Python maintenant utilisée par votre machine
  4. Vérifier directement depuis la ligne de commande que Python exécute bien une commande12 avec:
terminal
$ python -c "print('Hello')"
  1. Faire la même chose mais avec import pandas as pd
  2. Installer les packages qu’on avait listé dans le requirements.txt précédemment. Ne pas faire un pip install -r requirements.txt afin de privilégier conda install
  3. Exécuter à nouveau conda env export et comprendre la différence avec la situation précédente13.
  4. Vérifier que le script main.py fonctionne bien. Sinon installer les packages manquants et reprndre de manière itérative à partir de la question 7.
  5. Quand main.py fonctionne, faire conda env export > environment.yml pour figer l’environnement de travail.
terminal
1$ git stash
$ git checkout appli11b
1
Pour annuler les modifications depuis le dernier commit

Étape 2: construire l’environnement de notre application via un script shell

Les environnements virtuels permettent de mieux spécifier les dépendances de notre projet, mais ne permettent pas de garantir une portabilité optimale. Pour cela, il faut recourir à la technologie des conteneurs. L’idée est de construire une machine, en partant d’une base quasi-vierge, qui permette de construire étape par étape l’environnement nécessaire au bon fonctionnement de notre projet. C’est le principe des conteneurs Docker .

Leur méthode de construction étant un peu difficile à prendre en main au début, nous allons passer par une étape intermédiaire afin de bien comprendre le processus de production.

  • Nous allons d’abord créer un script shell, c’est à dire une suite de commandes Linux permettant de construire l’environnement à partir d’une machine vierge ;
  • Nous transformerons celui-ci en Dockerfile dans un deuxième temps. C’est l’objet de l’étape suivante.
Application 12a : créer un fichier d’installation de A à Z
  1. Créer un service ubuntu sur le SSP Cloud
  2. Ouvrir un terminal
  3. Cloner le dépôt
  4. Se placer dans le dossier du projet avec cd
  5. Se placer au niveau du checkpoint 11a avec git checkout appli11a
  6. Via l’explorateur de fichiers, créer le fichier install.sh à la racine du projet avec le contenu suivant:
Script à créer sous le nom install.sh
install.sh
#!/bin/bash
# Install Python
apt-get -y update
apt-get install -y python3-pip python3-venv
# Create empty virtual environment
python3 -m venv titanic
source titanic/bin/activate
# Install project dependencies
pip install -r requirements.txt
  1. Changer les permissions sur le script pour le rendre exécutable
terminal
$ chmod +x install.sh
  1. Exécuter le script depuis la ligne de commande avec des droits de super-utilisateur (nécessaires pour installer des packages via apt)
terminal
$ sudo ./install.sh
  1. Vérifier que le script main.py fonctionne correctement dans l’environnement virtuel créé
terminal
$ source titanic/bin/activate
$ python3 main.py
terminal
1$ git stash
$ git checkout appli12a
1
Pour annuler les modifications depuis le dernier commit

Application 12b : créer un fichier d’installation de A à Z
  1. Créer un service ubuntu sur le SSP Cloud
  2. Ouvrir un terminal
  3. Cloner le dépôt
  4. Se placer dans le dossier du projet avec cd
  5. Se placer au niveau du checkpoint 11b avec git checkout appli11b
  6. Via l’explorateur de fichiers, créer le fichier install.sh à la racine du projet avec le contenu suivant:
Script à créer sous le nom install.sh
install.sh
apt-get -y update && apt-get -y install wget
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh && \
    bash Miniconda3-latest-Linux-x86_64.sh -b -p /miniconda && \
    rm -f Miniconda3-latest-Linux-x86_64.sh
PATH="/miniconda/bin:${PATH}"
# Create environment
conda create -n titanic pandas PyYAML scikit-learn -c conda-forge
conda activate titanic
PATH="/miniconda/envs/titanic/bin:${PATH}"
python main.py
  1. Changer les permissions sur le script pour le rendre exécutable
terminal
$ chmod +x install.sh
  1. Exécuter le script depuis la ligne de commande avec des droits de super-utilisateur (nécessaires pour installer des packages via apt)
terminal
$ sudo ./install.sh
  1. Vérifier que le script main.py fonctionne correctement dans l’environnement virtuel créé
terminal
$ conda activate titanic
$ python3 main.py
terminal
1$ git stash
$ git checkout appli12b
1
Pour annuler les modifications depuis le dernier commit

Étape 3: conteneuriser l’application avec Docker

Note

Cette application nécessite l’accès à une version interactive de Docker. Il n’y a pas beaucoup d’instances en ligne disponibles.

Nous proposons deux solutions:

Sinon, elle peut être réalisée en essai-erreur par le biais des services d’intégration continue de Github ou Gitlab . Néanmoins, nous présenterons l’utilisation de ces services plus tard, dans la prochaine partie.

Maintenant qu’on sait que ce script préparatoire fonctionne, on va le transformer en Dockerfile pour anticiper la mise en production. Comme la syntaxe Docker est légèrement différente de la syntaxe Linux classique (voir le chapitre portabilité), il va être nécessaire de changer quelques instructions mais ceci sera très léger.

On va tester le Dockerfile dans un environnement bac à sable pour ensuite pouvoir plus facilement automatiser la construction de l’image Docker.

Application 13: création de l’image Docker

Se placer dans un environnement avec Docker, par exemple Play with Docker

Création du Dockerfile

  • Dans le terminal Linux, cloner votre dépôt Github
  • Repartir de la dernière version à disposition. Par exemple, si vous avez privilégié l’environnement virtuel venv, ce sera:
terminal
1$ git stash
$ git checkout appli12a
1
Pour annuler les modifications depuis le dernier commit
  • Créer via la ligne de commande un fichier texte vierge nommé Dockerfile (la majuscule au début du mot est importante)
Commande pour créer un Dockerfile vierge depuis la ligne de commande
terminal
$ touch Dockerfile
  • Ouvrir ce fichier via un éditeur de texte et copier le contenu suivant dedans:
Premier Dockerfile
terminal
FROM ubuntu:22.04
WORKDIR ${HOME}/titanic
# Install Python
RUN apt-get -y update && \
    apt-get install -y python3-pip
# Install project dependencies
COPY requirements.txt .
RUN pip install -r requirements.txt
CMD ["python3", "main.py"]

Construire (build) l’image

  • Utiliser docker build pour créer une image avec le tag my-python-app
terminal
$ docker build . -t my-python-app
  • Vérifier les images dont vous disposez. Vous devriez avoir un résultat proche de celui-ci :
terminal
$ docker images
REPOSITORY      TAG       IMAGE ID       CREATED              SIZE
my-python-app   latest    188957e16594   About a minute ago   879MB

Tester l’image: découverte du cache

L’étape de build a fonctionné: une image a été construite.

Mais fait-elle effectivement ce que l’on attend d’elle ?

Pour le savoir, il faut passer à l’étape suivante, l’étape de run.

terminal
$ docker run -it my-python-app
python3: can't open file '/~/titanic/main.py': [Errno 2] No such file or directory

Le message d’erreur est clair : Docker ne sait pas où trouver le fichier main.py. D’ailleurs, il ne connait pas non plus les autres fichiers de notre application qui sont nécessaires pour faire tourner le code, par exemple le dossier src.

  • Avant l’étape CMD, copier les fichiers nécessaires sur l’image afin que l’application dispose de tous les éléments nécessaires pour être en mesure de fonctionner.
Nouveau Dockerfile
terminal
FROM ubuntu:22.04
WORKDIR ${HOME}/titanic
# Install Python
RUN apt-get -y update && \
    apt-get install -y python3-pip
# Install project dependencies
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY main.py .
COPY src ./src
CMD ["python3", "main.py"]
  • Refaire tourner l’étape de build

  • Refaire tourner l’étape de run. A ce stade, la matrice de confusion doit fonctionner 🎉. Vous avez créé votre première application reproductible !

Note

Ici, le cache permet d’économiser beaucoup de temps. Par besoin de refaire tourner toutes les étapes, Docker agit de manière intelligente en faisant tourner uniquement les étapes qui ont changé.

terminal
1$ git stash
$ git checkout appli13
1
Pour annuler les modifications depuis le dernier commit

Partie 4 : automatisation avec l’intégration continue

Imaginez que vous êtes au restaurant et qu’on ne vous serve pas le plat mais seulement la recette et que, de plus, on vous demande de préparer le plat chez vous avec les ingrédients dans votre frigo. Vous seriez quelque peu déçu. En revanche, si vous avez goûté au plat, que vous êtes un réel cordon bleu et qu’on vous donne la recette pour refaire ce plat ultérieurement, peut-être que vous appréciriez plus.

Cette analogie illustre l’enjeu de définir le public cible et ses attentes afin de fournir un livrable adapté. Une image Docker est un livrable qui n’est pas forcément intéressant pour tous les publics. Certains préféreront avoir un plat bien préparé qu’une recette ; certains apprécieront avoir une image Docker mais d’autres ne seront pas en mesure de construire celle-ci ou ne sauront pas la faire fonctionner. Une image Docker est plus souvent un moyen pour faciliter la mise en service d’une production qu’une fin en soi.

Nous allons donc proposer plusieurs types de livrables plus classiques par la suite. Ceux-ci correspondront mieux aux attendus des publics utilisateurs de services construits à partir de techniques de data science. Docker est néanmoins un passage obligé car l’ensemble des types de livrables que nous allons explorer reposent sur la standardisation permise par les conteneurs.

Cette approche nous permettra de quitter le domaine de l’artisanat pour s’approcher d’une industrialisation de la mise à disposition de notre projet. Ceci va notamment nous amener à mettre en oeuvre l’approche pragmatique du DevOps qui consiste à intégrer dès la phase de développement d’un projet les contraintes liées à sa mise à disposition au public cible (cette approche est détaillée plus amplement dans le chapitre sur la mise en production).

L’automatisation et la mise à disposition automatisée de nos productions sera faite progressivement, au cours des prochaines parties. Tous les projets n’ont pas vocation à aller aussi loin dans ce domaine. L’opportunité doit être comparée aux coûts humains et financiers de leur mise en oeuvre et de leur cycle de vie. Avant de faire une production en série de nos modèles, nous allons déjà commencer par automatiser quelques tests de conformité de notre code. On va ici utiliser l’intégration continue pour deux objectifs distincts:

  • la mise à disposition de l’image Docker ;
  • la mise en place de tests automatisés de la qualité du code sur le modèle de notre linter précédent.

Nous allons utiliser Github Actions pour cela. Il s’agit de serveurs standardisés mis à disposition gratuitement par Github . Gitlab , l’autre principal acteur du domaine, propose des services similaires. L’implémentation est légèrement différente mais les principes sont identiques.

terminal
$ git checkout appli13

Étape 1: mise en place de tests automatisés

Avant d’essayer de mettre en oeuvre la création de notre image Docker de manière automatisée, nous allons présenter la logique de l’intégration continue en testant de manière automatisée notre script main.py.

Pour cela, nous allons partir de la structure proposée dans l’action officielle. La documentation associée est ici. Des éléments succincts de présentation de la logique déclarative des actions Github sont disponibles dans le chapitre sur la mise en production. Néanmoins, la meilleure école pour comprendre le fonctionnement de celles-ci est de parcourir la documentation du service et d’observer les actions Github mises en oeuvre par vos projets favoris, celles-ci seront fort instructives !

Application 14: premier script d’intégration continue

A partir de l’exemple présent dans la documentation officielle de Github , on a déjà une base de départ qui peut être modifiée. Les questions suivantes permettront d’automatiser les tests et le diagnostic qualité de notre code14

  1. Créer un fichier .github/workflows/test.yaml avec le contenu de l’exemple de la documentation
  2. Avec l’aide de la documentation, introduire une étape d’installation des dépendances. Utiliser le fichier requirements.txt pour installer les dépendances.
  3. Utiliser pylint pour vérifier la qualité du code. Ajouter l’argument --fail-under=6 pour renvoyer une erreur en cas de note trop basse15
  4. Utiliser une étape appelant notre application en ligne de commande (python main.py) pour tester que la matrice de confusion s’affiche bien.
  5. Aller voir votre test automatisé dans l’onglet Actions de votre dépôt sur Github
terminal
1$ git stash
$ git checkout appli14
1
Pour annuler les modifications depuis le dernier commit

Maintenant, nous pouvons observer que l’onglet Actions s’est enrichi. Chaque commit va entraîner une série d’actions automatisées.

Si l’une des étapes échoue, ou si la note de notre projet est mauvaise, nous aurons une croix rouge (et nous recevrons un mail). On pourra ainsi détecter, en développant son projet, les moments où on dégrade la qualité du script afin de la rétablir immédiatemment.

Étape 2: Automatisation de la livraison de l’image Docker

Maintenant, nous allons automatiser la mise à disposition de notre image sur DockerHub (le lieu de partage des images Docker). Cela facilitera sa réutilisation mais aussi des valorisations ultérieures.

Là encore, nous allons utiliser une série d’actions pré-configurées.

Pour que Github puisse s’authentifier auprès de DockerHub, il va falloir d’abord interfacer les deux plateformes. Pour cela, nous allons utiliser un jeton (token) DockerHub que nous allons mettre dans un espace sécurisé associé à votre dépôt Github.

Application 15a: configuration
  • Se rendre sur https://hub.docker.com/ et créer un compte. Il est recommandé d’associer ce compte à votre compte Github.
  • Créer un dépôt public application-correction
  • Aller dans les paramètres de votre compte et cliquer, à gauche, sur Security
  • Créer un jeton personnel d’accès, ne fermez pas l’onglet en question, vous ne pouvez voir sa valeur qu’une fois.
  • Dans le dépôt Github de votre projet, cliquer sur l’onglet Settings et cliquer, à gauche, sur Secrets and variables puis dans le menu déroulant en dessous sur Actions. Sur la page qui s’affiche, aller dans la section Repository secrets
  • Créer un jeton DOCKERHUB_TOKEN à partir du jeton que vous aviez créé sur Dockerhub. Valider
  • Créer un deuxième secret nommé DOCKERHUB_USERNAME ayant comme valeur le nom d’utilisateur que vous avez créé sur Dockerhub
Etape optionnelle supplémentaire si on met en production un site web
  • Dans le dépôt Github de votre projet, cliquer sur l’onglet Settings et cliquer, à gauche, sur Actions. Donner les droits d’écriture à vos actions sur le dépôt du projet (ce sera nécessaire pour Github Pages)

A ce stade, nous avons donné les moyens à Github de s’authentifier avec notre identité sur Dockerhub. Il nous reste à mettre en oeuvre l’action en s’inspirant de la documentation officielle. On ne va modifier que trois éléments dans ce fichier. Effectuer les actions suivantes:

Application 15b: automatisation de l’image Docker
  • En s’inspirant de ce template, créer le fichier .github/workflows/prod.yml qui va build et push l’image sur le DockerHub. Il va être nécessaire de changer légèrement ce modèle :
    • Retirer la condition restrictive sur les commits pour lesquels sont lancés cette automatisation. Pour cela, remplacer le contenu de on de sorte à avoir on: push: branches: - main - dev
    • Changer le tag à la fin pour mettre username/application-correction:latestusername est le nom d’utilisateur sur DockerHub;
    • Optionnel: changer le nom de l’action
  • Faire un commit et un push de ces fichiers

Comme on est fier de notre travail, on va afficher ça avec un badge sur le README (partie optionnelle).

  • Se rendre dans l’onglet Actions et cliquer sur une des actions listées.
  • En haut à droite, cliquer sur ...
  • Sélectionner Create status badge
  • Récupérer le code Markdown proposé
  • Copier dans votre README.md le code markdown proposé
Créer le badge

Maintenant, il nous reste à tester notre application dans l’espace bac à sable ou en local, si Docker est installé.

Application 15b (partie optionnelle): Tester l’application
  • Se rendre sur l’environnement bac à sable Play with Docker ou dans votre environnement Docker de prédilection.
  • Récupérer et lancer l’image :
terminal
$ docker run -it username/application-correction:latest

🎉 La matrice de confusion doit s’afficher ! Vous avez grandement facilité la réutilisation de votre image.

terminal
1$ git stash
$ git checkout appli15
1
Pour annuler les modifications depuis le dernier commit

Partie 5: expérimenter en local des valorisations puis automatiser leur production

Nous avons automatisé les étapes intermédiaires de notre projet. Néanmoins nous n’avons pas encore réfléchi à la valorisation à mettre en oeuvre pour notre projet. On va supposer que notre projet s’adresse à des data scientists mais aussi à une audience moins technique. Pour ces premiers, nous pourrions nous contenter de valorisations techniques, comme des API, mais pour ces derniers il est conseillé de privilégier des formats plus user friendly.

terminal
$ git checkout appli15

Afin de faire le parallèle avec les parcours possibles pour l’évaluation, nous allons proposer trois valorisations16:

  • Une API facilitant la réutilisation du modèle en “production” ;
  • Un site web statique exploitant cette API pour exposer les prédictions à une audience moins technique.

La solution que nous allons proposer pour les sites statiques, Quarto associé à Github Pages, peut être utilisée dans le cadre des parcours “rapport reproductible” ou “dashboard / application interactive”.

Pour ce dernier parcours, d’autres approches techniques sont néanmoins possibles, comme Streamlit. Celles-ci sont plus exigeantes sur le plan technique puisqu’elles nécessitent de mettre en production sur des serveurs conteuneurisés (comme la mise en production de l’API) là où le site statique ne nécessite qu’un serveur web, mis à disposition gratuitement par Github.

La distinction principale entre ces deux approches est qu’elles s’appuient sur des serveurs différents. Un site statique repose sur un serveur web là où Streamlit s’appuie sur serveur classique en backend. La différence principale entre ces deux types de serveurs réside principalement dans leur fonction et leur utilisation:

  • Un serveur web est spécifiquement conçu pour stocker, traiter et livrer des pages web aux clients. Cela inclut des fichiers HTML, CSS, JavaScript, images, etc. Les serveurs web écoutent les requêtes HTTP/HTTPS provenant des navigateurs des utilisateurs et y répondent en envoyant les données demandées.
  • Un serveur backend classique est conçu pour effectuer des opérations en réponse à un front, en l’occurrence une page web. Dans le contexte d’une application Streamlit, il s’agit d’un serveur avec l’environnement Python ad hoc pour exécuter le code nécessaire à répondre à toute action d’un utilisateur de l’appliacation.

Étape 1: développer une API en local

Le premier livrable devenu classique dans un projet impliquant du machine learning est la mise à disposition d’un modèle par le biais d’une API (voir chapitre sur la mise en production). Le framework FastAPI va permettre de rapidement transformer notre application Python en une API fonctionnelle.

terminal
$ git checkout appli15

Application 16: Mise à disposition sous forme d’API locale
  • Installer fastAPI et uvicorn puis les ajouter au requirements.txt
  • Renommer le fichier main.py en train.py. Dans ce script, ajouter une sauvegarde du modèle après l’avoir entraîné, sous le format joblib.
  • Faire tourner
terminal
$ python train.py

pour enregistrer en local votre modèle de production.

  • Modifier les appels à main.py dans votre Dockerfile et vos actions Github sous peine d’essuyer des échecs lors de vos actions Github après le prochain push.

  • Ajouter model.joblib au .gitignore car Git n’est pas fait pour ce type de fichiers.

Nous allons maintenant passer au développement de l’API. Comme découvrir FastAPI n’est pas l’objet de cet enseignement, nous donnons directement le modèle pour créer l’API. Si vous désirez tester de vous-mêmes, vous pouvez créer votre fichier sans vous référer à l’exemple

  • Créer le fichier api.py permettant d’initialiser l’API:
Fichier api.py
src/models/train_evaluation.py
"""A simple API to expose our trained RandomForest model for Tutanic survival."""
from fastapi import FastAPI
from joblib import load

import pandas as pd

model = load('model.joblib')

app = FastAPI(
    title="Prédiction de survie sur le Titanic",
    description=
    "Application de prédiction de survie sur le Titanic 🚢 <br>Une version par API pour faciliter la réutilisation du modèle 🚀" +\
        "<br><br><img src=\"https://media.vogue.fr/photos/5faac06d39c5194ff9752ec9/1:1/w_2404,h_2404,c_limit/076_CHL_126884.jpg\" width=\"200\">"
    )


@app.get("/", tags=["Welcome"])
def show_welcome_page():
    """
    Show welcome page with model name and version.
    """

    return {
        "Message": "API de prédiction de survie sur le Titanic",
        "Model_name": 'Titanic ML',
        "Model_version": "0.1",
    }


@app.get("/predict", tags=["Predict"])
async def predict(
    sex: str = "female",
    age: float = 29.0,
    fare: float = 16.5,
    embarked: str = "S"
) -> str:
    """
    """

    df = pd.DataFrame(
        {
            "Sex": [sex],
            "Age": [age],
            "Fare": [fare],
            "Embarked": [embarked],
        }
    )

    prediction = "Survived 🎉" if int(model.predict(df)) == 1 else "Dead ⚰️"

    return prediction
  • Déployer en local l’API avec la commande
terminal
$ uvicorn api:app --reload --host "0.0.0.0" --port 5000
  • A partir du README du service, se rendre sur l’URL de déploiement, ajouter /docs/ à celui-ci et observer la documentation de l’API
  • Se servir de la documentation pour tester les requêtes /predict
  • Récupérer l’URL d’une des requêtes proposées. La tester dans le navigateur et depuis Python avec requests :
import request
requests.get(url).json()
  • Une fois que vous avez testé, vous pouvez tuer l’application en faisant CTRL+C. Retester votre bout de code Python et comprendre l’origine du problème.
terminal
1$ git stash
$ git checkout appli17
1
Pour annuler les modifications depuis le dernier commit

Étape 2: déployer l’API de manière manuelle

terminal
$ git checkout appli18

A ce stade, nous avons déployé l’API seulement localement, dans le cadre d’un terminal qui tourne en arrière-plan. C’est une mise en production manuelle, pas franchement pérenne. Ce mode de déploiement est très pratique pour la phase de développement, afin de s’assurer que l’API fonctionne comme attendu. Pour pérenniser la mise en production, on va éliminer l’aspect artisanal de celle-ci.

Il est temps de passer à l’étape de déploiement, qui permettra à notre API d’être accessible via une URL sur le web et d’avoir un serveur, en arrière plan, qui effectuera les opérations pour répondre à une requête. Pour se faire, on va utiliser les possibilités offertes par Kubernetes, sur lequel est basé le SSP Cloud.

Application 18a: Dockeriser l’API (intégration continue)
  • Pour rendre la structure du projet plus lisible, déplacer api.py -> api/main.py

  • Créer un script api/run.sh à la racine du projet qui lance le script train.py puis déploie localement l’API

Fichier run.sh
api/run.sh
#/bin/bash
python3 train.py
uvicorn api.main:app --reload --host "0.0.0.0" --port 5000
  • Donner au script api/run.sh des permissions d’exécution : chmod +x api/run.sh

  • Changer l’instruction CMD du Dockerfile pour exécuter le script api/run.sh au lancement du conteneur (CMD ["bash", "-c", "./api/run.sh"])

  • Commit et push les changements

  • Une fois le CI terminé, récupérer la nouvelle image dans votre environnement de test de Docker et vérifier que l’API se déploie correctement

Nous avons préparé la mise à disposition de notre API mais à l’heure actuelle elle n’est pas disponible de manière aisée car il est nécessaire de lancer manuellement une image Docker pour pouvoir y accéder. Ce type de travail est la spécialité de Kubernetes que nous allons utiliser pour gérer la mise à disposition de notre API.

Application 18b: Mettre à disposition l’API (déploiement manuel)

Cette partie nécessite d’avoir à disposition une infrastructure cloud.

  • Créer un dossier deployment à la racine du projet qui va contenir les fichiers de configuration nécessaires pour déployer sur un cluster Kubernetes

  • En vous inspirant de la documentation, y ajouter un premier fichier deployment.yaml qui va spécifier la configuration du Pod à lancer sur le cluster

Fichier deployment/deployment.yaml
deployment/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: titanic-deployment
  labels:
    app: titanic
spec:
  replicas: 1
  selector:
    matchLabels:
      app: titanic
  template:
    metadata:
      labels:
        app: titanic
    spec:
      containers:
      - name: titanic
        image: linogaliana/application-correction:latest
        ports:
        - containerPort: 5000
  • En vous inspirant de la documentation, y ajouter un second fichier service.yaml qui va créer une ressource Service permettant de donner une identité fixe au Pod précédemment créé au sein du cluster
Fichier deployment/service.yaml
deployment/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: titanic-service
spec:
  selector:
    app: titanic
  ports:
    - protocol: TCP
      port: 80
      targetPort: 5000
  • En vous inspirant de la documentation, y ajouter un troisième fichier ingress.yaml qui va créer une ressource Ingress permettant d’exposer le service via une URL en dehors du cluster
Fichier deployment/ingress.yaml
deployment/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: titanic-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  tls:
  - hosts:
1    - # METTRE URL ICI
  rules:
2  - host: # METTRE URL ICI
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: titanic-service
            port:
              number: 80
1
Mettez l’URL auquel vous voulez exposer votre service. Sur le modèle de titanic.kub.sspcloud.fr (mais ne tentez pas celui-là, il est déjà pris 😃)
2
Mettre ici aussi
  • Appliquer ces fichiers de configuration sur le cluster : kubectl apply -f deployment/

  • Si tout a correctement fonctionné, vous devriez pouvoir accéder depuis votre navigateur à l’API à l’URL spécifiée dans le fichier deployment/ingress.yaml. Par exemple https://toto.kub.sspcloud.fr/ si vous avez mis celui-ci plus tôt (et https://toto.kub.sspcloud.fr/docs pour la documentation).

terminal
1$ git stash
$ git checkout appli18
1
Pour annuler les modifications depuis le dernier commit

On peut remarquer quelques voies d’amélioration de notre approche qui seront ultérieurement traitées:

  • L’entraînement du modèle est ré-effectué à chaque lancement d’un nouveau conteneur. On relance donc autant de fois un entraînement qu’on déploie de conteneurs pour répondre à nos utilisateurs. Ce sera l’objet de la partie MLOps de fiabiliser et optimiser cette partie du pipeline.
  • il est nécessaire de (re)lancer manuellement kubectl apply -f deployment/ à chaque changement de notre code. Autrement dit, lors de cette application, on a amélioré la fiabilité du lancement de notre API mais un lancement manuel est encore indispensable. Comme dans le reste de ce cours, on va essayer d’éviter un geste manuel pouvant être source d’erreur en privilégiant l’automatisation et l’archivage dans des scripts. C’est l’objet de la prochaine étape.

Etape 3: automatiser le déploiement (déploiement en continu)

Clarification sur la branche de travail

A partir de maintenant, il est nécessaire de clarifier la branche principale sur laquelle nous travaillons. De manière traditionnelle, on utilise la branche main. Néanmoins, pour être cohérent avec les instructions du début, qui étaient de créer une branche dev, tous les exemples ultérieures partiront de cette hypothèse.

Si vous avez fait les applications les unes après les autres, et que vous vous situez toujours sur dev, vous pouvez passer aux applications suivantes. Si vous avez changé de branche, vous pouvez continuer mais en tenir compte dans les exemples ultérieurs.

Si vous avez utilisé un tag pour sauter une ou plusieurs étapes, il va être nécessaire de se placer sur une branche car vous êtes en head detached. Pour cela, après avoir committé les fichiers que vous désirez garder

terminal
1$ git branch -D dev
2$ git push origin -d dev
3$ git checkout -b dev
4$ git push --set-upstream origin dev
1
Supprime la branche dev locale (si elle existe).
2
Supprime la branche dev remote (si elle existe).
3
Crée une nouvelle branche dev locale et on se place sur cette branche.
4
Pousse la branche dev et active la synchronisation entre la branche locale et la branche remote.

Qu’est-ce qui peut déclencher une évolution nécessitant de mettre à jour l’ensemble de notre processus de production ?

Regardons à nouveau notre pipeline:

Les inputs de notre pipeline sont donc:

  • La configuration. Ici, on peut considérer que notre YAML de configuration relève de cette catégorie ;
  • Les données. Nos données sont statiques et n’ont pas vocation à évoluer. Si c’était le cas, il faudrait en tenir compte dans notre automatisation. ;
  • Le code. C’est l’élément principal qui évolue chez nous. Idéalement, on veut automatiser le processus au maximum en faisant en sorte qu’à chaque mise à jour de notre code (un push sur Github), les étapes ultérieures (production de l’image Docker, etc.) se lancent. Néanmoins, on veut aussi éviter qu’une erreur puisse donner lieu à une mise en production non-fonctionnelle, on va donc maintenir une action manuelle minimale comme garde-fou.

Pour automatiser au maximum la mise en production, on va utiliser un nouvel outil : ArgoCD. Ainsi, au lieu de devoir appliquer manuellement la commande kubectl apply à chaque modification des fichiers de déploiement (présents dans le dossier kubernetes/), c’est l’opérateur ArgoCD, déployé sur le cluster, qui va détecter les changements de configuration du déploiement et les appliquer automatiquement. C’est l’approche dite GitOps : le dépôt Git du déploiement fait office de source de vérité unique de l’état voulu de l’application, tout changement sur ce dernier doit donc se répercuter immédiatement sur le déploiement effectif.

Application 19a: Automatiser la mise à disposition de l’API (déploiement continu)
  • Lancer un service ArgoCD sur le SSPCloud depuis la page Mes services (catalogue Automation). Laisser les configurations par défaut.
  • Sur GitHub, créer un dépôt application-deployment qui va servir de dépôt GitOps, c’est à dire un dépôt qui spécifie le paramétrage du déploiement de votre application.
  • Ajouter un dossier deployment à votre dépôt GitOps, dans lequel on mettra les trois fichiers de déploiement qui permettent de déployer notre application sur Kubernetes (deployment.yaml, service.yaml, ingress.yaml).
  • A la racine de votre dépôt GitOps, créez un fichier application.yml avec le contenu suivant, en prenant bien soin de modifier les lignes surlignées avec les informations pertinentes :
application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: ensae-mlops
spec:
  project: default
  source:
1    repoURL: https://github.com/<your_github_username>/application-deployment.git
2    targetRevision: main
3    path: deployment
  destination:
    server: https://kubernetes.default.svc
4    namespace: user-<your_sspcloud_username>
  syncPolicy:
    automated:
      selfHeal: true
1
L’URL de votre dépôt Github
2
La branche à partir de laquelle vous déployez
3
Le nom du dossier contenant vos fichiers de déploiement Kubernetes
4
Votre namespace Kubernetes. Sur le SSPCloud, cela prend la forme user-${username}
  • Dans ArgoCD, cliquez sur New App puis Edit as a YAML. Copiez-collez le contenu de application.yml et cliquez sur Create.
  • Observez dans l’interface d’ArgoCD le déploiement progressif des ressources nécessaires à votre application sur le cluster. Joli non ?
  • Vérifiez que votre API est bien déployée en utilisant l’URL définie dans le fichier ingress.yml.
terminal
1$ git stash
$ git checkout appli19a
1
Pour annuler les modifications depuis le dernier commit

A présent, nous avons tous les outils à notre disposition pour construire un vrai pipeline de CI/CD, automatisé de bout en bout. Il va nous suffire pour cela de mettre à bout les composants :

  • dans la partie 4 de l’application, nous avons construit un pipeline de CI : on a donc seulement à faire un commit sur le dépôt de l’application pour lancer l’étape de build et de mise à disposition de la nouvelle image sur le DockerHub ;

  • dans l’application précédente, nous avons construit un pipeline de CD : ArgoCD suit en permanence l’état du dépôt GitOps, tout commit sur ce dernier lancera donc automatiquement un redéploiement de l’application.

Il y a donc un élément qui fait la liaison entre ces deux pipelines et qui nous sert de garde-fou en cas d’erreur : la version de l’application.

Application 19b : Mettre à jour la version en production

Jusqu’à maintenant, on a utilisé le tag latest pour définir la version de notre application. En pratique, lorsqu’on passe de la phase de développement à celle de production, on a plutôt envie de versionner proprement les versions de l’application afin de savoir ce qui est déployé. On va pour cela utiliser les tags avec Git, qui vont se propager au nommage de l’image Docker.

  • Modifier le fichier de CI prod.yml pour assurer la propagation des tags.

Fichier .github/workflows/prod.yml

.github/workflows/prod.yml
name: Construction image Docker

on: 
  push:
    branches:
      - main
      - dev
    tags:

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      -
        name: Set up QEMU
        uses: docker/setup-qemu-action@v3
      -
        name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      -
        name: Docker meta
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: linogaliana/application-correction

      -
        name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      -
        name: Build and push
        uses: docker/build-push-action@v5
        with:
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
  • Dans le dépôt de l’application, mettre à jour le code dans api/main.py pour changer un élément de l’interface de votre documentation. Par exemple, mettre en gras un titre.
app = FastAPI(
    title="Prédiction de survie sur le Titanic",
    description=
    "<b>Application de prédiction de survie sur le Titanic</b> 🚢 <br>Une version par API pour faciliter la réutilisation du modèle 🚀" +\
        "<br><br><img src=\"https://media.vogue.fr/photos/5faac06d39c5194ff9752ec9/1:1/w_2404,h_2404,c_limit/076_CHL_126884.jpg\" width=\"200\">"
    )
  • Commit et push les changements.

  • Tagger le commit effectué précédemment et push le nouveau tag :

terminal
$ git tag v1.0.0
$ git push --tags
  • Vérifier sur le dépôt GitHub de l’application que ce commit lance bien un pipeline de CI associé au tag v1.0.0. Une fois terminé, vérifier sur le DockerHub que le tag v1.0.0 existe bien parmi les tags disponibles de l’image.

La partie CI a correctement fonctionné. Intéressons-nous à présent à la partie CD.

  • Sur le dépôt GitOps de l’application, mettre à jour la version de l’image à déployer en production dans le fichier deployment/deployment.yaml

Fichier deployment/deployment.yaml

deployment/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: titanic-deployment
  labels:
    app: titanic
spec:
  replicas: 1
  selector:
    matchLabels:
      app: titanic
  template:
    metadata:
      labels:
        app: titanic
    spec:
      containers:
      - name: titanic
        image: linogaliana/application-correction:v1.0.0
        ports:
        - containerPort: 5000
  1. Remplacer username par la valeur adéquate
  • Après avoir committé et pushé, observer dans ArgoCD le statut de votre application. Normalement, l’opérateur devrait avoir automatiquement identifié le changement, et mettre à jour le déploiement pour en tenir compte.

  • Vérifier que l’API a bien été mise à jour.
terminal
1$ git stash
$ git checkout appli19b
1
Pour annuler les modifications depuis le dernier commit

Gérer le CORS

Notre API est accessible sans problème depuis Python ou notre navigateur mais si on désire utiliser JavaScript pour créer une application interactive, on va essuyer un refus à cause du CORS.

Permettre à n’importe quel client de se connecter à notre API permettra de faire un site web exploitant notre API. Comme c’est un point technique qui ne concerne pas les compétences liées à ce cours, nous donnons directement les mises à jour nécessaires du projet:

deployment/ingress.yaml
deployment/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: titanic-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    # Enable CORS by adding these annotations
    nginx.ingress.kubernetes.io/enable-cors: "true"
    nginx.ingress.kubernetes.io/cors-allow-methods: "PUT, GET, POST, DELETE, PATCH, OPTIONS"
    nginx.ingress.kubernetes.io/cors-allow-credentials: "true"
    nginx.ingress.kubernetes.io/cors-allow-headers: "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization"
    nginx.ingress.kubernetes.io/cors-allow-origin: "*"
spec:
  ingressClassName: nginx
  tls:
  - hosts:
1    - #VOTRE URL ICI
  rules:
2  - host: #VOTRE URL ICI
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: titanic-service
            port:
              number: 80
1
Mettre votre url ici
2
Ici aussi
.github/worflows/prod.yaml
.github/worflows/prod.yaml
name: Construction image Docker

on:
  push:
    branches:
      - main
      - dev

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      -
        name: Set up QEMU
        uses: docker/setup-qemu-action@v3
      -
        name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      -
        name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      -
        name: Build and push
        uses: docker/build-push-action@v5
        with:
          push: true
1          tags: username/application-correction:v0.0.2
1
Remplacer username par la valeur adéquate
deployment/deployment.yaml
deployment/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: titanic-deployment
  labels:
    app: titanic
spec:
  replicas: 1
  selector:
    matchLabels:
      app: titanic
  template:
    metadata:
      labels:
        app: titanic
    spec:
      containers:
      - name: titanic
1        image: username/application-correction:v0.0.2
        ports:
        - containerPort: 5000
1
Remplacer username par votre nom d’utilisateur

Etape 4: construire un site web

terminal
$ git checkout appli19
$ git checkout -b dev
$ git push origin dev

On va proposer un nouveau livrable pour parler à un public plus large. Pour faire ce site web, on va utiliser Quarto et déployer sur Github Pages.

Application 20: Création d’un site web pour valoriser le projet
terminal
$ quarto create project website mysite
  • Faire remonter d’un niveau _quarto.yml
  • Supprimer about.qmd, déplacer index.qmd vers la racine de notre projet.
  • Remplacer le contenu de index.qmd par celui-ci et retirer about.qmd des fichiers à compiler.
  • Déplacer styles.css à la racine du projet
  • Mettre à jour le .gitignore avec les instructions suivantes
/.quarto/
*.html
*_files
_site/
  • En ligne de commande, faire quarto preview (ajouter les arguments --port 5000 --host 0.0.0.0 si vous passez par le SSPCloud)
  • Observer le site web généré en local

Enfin, on va construire et déployer automatiquement ce site web grâce au combo Github Actions et Github Pages:

  • Créer une branche gh-pages à partir du contenu de cette page
  • Revenir à votre branche
  • Créer un fichier .github/workflows/website.yaml avec le contenu de ce fichier
  • Modifier le README pour indiquer l’URL de votre site web et de votre API
terminal
1$ git stash
$ git checkout appli20
1
Pour annuler les modifications depuis le dernier commit

Partie 6: adopter une approche MLOps pour améliorer notre modèle

terminal
$ git checkout appli20
$ git checkout -b dev
$ git push origin dev

Nous allons dans cette partie faire de la validation croisée. Pour éviter le problème du data leakage, nous proposons de revoir notre pipeline pour exclure la variable Title dont certaines modalités rares posent problème dans les découpages multiples d’échantillons lors de la validation croisée.

Restructurer le pipeline pour fluidifier la mise en production

Application 21 (optionnelle): restructuration de la chaîne
  1. Faire les modifications suivantes pour restructurer notre pipeline afin de mieux distinguer les étapes d’estimation et d’évaluation
Modification de src/models/train_evaluation.py à effectuer
src/models/train_evaluation.py
def build_pipeline(
    n_trees: int = 20,
    numeric_features=["Age", "Fare"],
    categorical_features=["Title", "Embarked", "Sex"]):
    """Random forest model for Titanic survival

    Args:
        n_trees (int, optional): _description_. Defaults to 20.

    Returns:
        _type_: _description_
    """

    numeric_transformer = Pipeline(
        steps=[
            ("imputer", SimpleImputer(strategy="median")),
            ("scaler", MinMaxScaler()),
        ]
    )

    categorical_transformer = Pipeline(
        steps=[
            ("imputer", SimpleImputer(strategy="most_frequent")),
            ("onehot", OneHotEncoder()),
        ]
    )

    preprocessor = ColumnTransformer(
        transformers=[
            ("Preprocessing numerical", numeric_transformer, numeric_features),
            (
                "Preprocessing categorical",
                categorical_transformer,
                categorical_features,
            ),
        ]
    )

    pipe = Pipeline(
        [
            ("preprocessor", preprocessor),
            ("classifier", RandomForestClassifier(n_estimators=n_trees)),
        ]
    )

    return pipe
Modification de src/features/build_features.py pour enchaîner les étapes de feature engineering
src/features/build_features.py
def feature_engineering(data: pd.DataFrame) -> pd.DataFrame:
    """Applying our feature engineering pipeline

    Args:
        data (pd.DataFrame): Initial dataframe

    Returns:
        pd.DataFrame: Dataframe with feature engineering being handled
    """
    data_training = create_variable_title(data)
    data_training = check_has_cabin(data_training)
    data_training = ticket_length(data_training)
    return data_training
Modification de train.py pour faire une grid search
train.py
"""
Prediction de la survie d'un individu sur le Titanic
"""

# GESTION ENVIRONNEMENT --------------------------------

from pathlib import Path
import argparse
from joblib import dump
from sklearn.model_selection import GridSearchCV

import src.data.import_data as imp
import src.features.build_features as bf
import src.models.train_evaluate as te

# PARAMETRES -------------------------------

# Paramètres ligne de commande
parser = argparse.ArgumentParser(description="Paramètres du random forest")
parser.add_argument("--n_trees", type=int, default=20, help="Nombre d'arbres")
parser.add_argument("--appli", type=str, default="appli21", help="Application number")
args = parser.parse_args()

# Paramètres YAML
config = imp.import_yaml_config("configuration/config.yaml")
base_url = (
    "https://minio.lab.sspcloud.fr/projet-formation/ensae-reproductibilite/data/raw"
)
API_TOKEN = config.get("jeton_api")
LOCATION_TRAIN = config.get("train_path", f"{base_url}/train.csv")
LOCATION_TEST = config.get("test_path", f"{base_url}/test.csv")
TEST_FRACTION = config.get("test_fraction", 0.1)
N_TREES = args.n_trees
APPLI_ID = args.appli
EXPERIMENT_NAME = "titanicml"

# FEATURE ENGINEERING --------------------------------

titanic_raw = imp.import_data(LOCATION_TRAIN)

# Create a 'Title' variable
titanic_intermediate = bf.feature_engineering(titanic_raw)


train, test = te.split_train_test_titanic(
    titanic_intermediate, fraction_test=TEST_FRACTION
)
X_train, y_train = train.drop("Survived", axis="columns"), train["Survived"]
X_test, y_test = test.drop("Survived", axis="columns"), test["Survived"]


def log_local_data(data, filename):
    data.to_csv(f"data/intermediate/{filename}.csv", index=False)


output_dir = Path("data/intermediate")
output_dir.mkdir(parents=True, exist_ok=True)

log_local_data(X_train, "X_train")
log_local_data(X_test, "X_test")
log_local_data(y_train, "y_train")
log_local_data(y_test, "y_test")


# MODELISATION: RANDOM FOREST ----------------------------

pipe = te.build_pipeline(n_trees=N_TREES, categorical_features=["Embarked", "Sex"])

param_grid = {
    "classifier__n_estimators": [10, 20, 50],
    "classifier__max_leaf_nodes": [5, 10, 50],
}


pipe_cross_validation = GridSearchCV(
    pipe,
    param_grid=param_grid,
    scoring=["accuracy", "precision", "recall", "f1"],
    refit="f1",
    cv=5,
    n_jobs=5,
    verbose=1,
)


pipe_cross_validation.fit(X_train, y_train)
pipe = pipe_cross_validation.best_estimator_

dump(pipe, "model.joblib")
Fichier eval.py pour évaluer la meilleure validation croisée
eval.py
import pandas as pd
from sklearn.metrics import confusion_matrix, accuracy_score
from joblib import load

# EVALUATE ----------------------------

loaded_model = load("model.joblib")

# Predict on a Pandas DataFrame.
X_test = pd.read_csv("data/intermediate/X_test.csv")
y_test = pd.read_csv("data/intermediate/y_test.csv")
y_test_predict = loaded_model.predict(X_test)

# EVALUATE ----------------------------

matrix = confusion_matrix(y_test, y_test_predict)

print("Accuracy:")
print(f"{accuracy_score(y_test, y_test_predict):.0%}")
print("Matrice de confusion:")
print(matrix)
Modification de api/main.py à effectuer
api/main.py
"""A simple API to expose our trained RandomForest model for Tutanic survival."""
import requests
from fastapi import FastAPI
from joblib import load

import pandas as pd

# GET PRODUCTION MODEL -------------

username_sspcloud = "lgaliana"
url = f"https://minio.lab.sspcloud.fr/{username_sspcloud}/ensae-reproductibilite/model/model.joblib"
local_filename = "model.joblib"

with open(local_filename, mode = "wb") as file:
    file.write(requests.get(url).content)


model = load(local_filename)


# USE PRODUCTION MODEL IN APP ----------

app = FastAPI(
    title="Prédiction de survie sur le Titanic",
    description=
    "<b>Application de prédiction de survie sur le Titanic</b> 🚢 <br>Une version par API pour faciliter la réutilisation du modèle 🚀" +\
        "<br><br><img src=\"https://media.vogue.fr/photos/5faac06d39c5194ff9752ec9/1:1/w_2404,h_2404,c_limit/076_CHL_126884.jpg\" width=\"200\">"
    )


@app.get("/", tags=["Welcome"])
def show_welcome_page():
    """
    Show welcome page with model name and version.
    """

    return {
        "Message": "API de prédiction de survie sur le Titanic",
        "Model_name": 'Titanic ML',
        "Model_version": "0.1",
    }


@app.get("/predict", tags=["Predict"])
async def predict(
    pclass: int = 3,
    sex: str = "female",
    age: float = 29.0,
    sib_sp: int = 1,
    parch: int = 1,
    fare: float = 16.5,
    embarked: str = "S",
    has_cabin: int = 1,
    ticket_len: int = 7
) -> str:
    """
    """

    df = pd.DataFrame(
        {
            "Pclass": [pclass],
            "Sex": [sex],
            "Age": [age],
            "SibSp": [sib_sp],
            "parch": [parch],
            "Fare": [fare],
            "Embarked": [embarked],
            "hasCabin": [has_cabin],
            "Ticket_Len": [ticket_len] 
        }
    )

    prediction = "Survived 🎉" if int(model.predict(df)) == 1 else "Dead ⚰️"

    return prediction
  1. Tester votre script train.py et uploader le meilleur modèle sur S3 de la manière suivante:
terminal
1mc cp model.joblib s3/<BUCKET_PERSONNEL>/ensae-reproductibilite/model/model.joblib
2mc anonymous set download s3/<BUCKET_PERSONNEL>/ensae-reproductibilite/
1
Uploader sur S3
2
Ouvrir les droits en lecture de ce fichier pour simplifier la récupération17

<BUCKET_PERSONNEL> est à remplacer par votre nom d’utilisateur sur le SSPCloud

  1. Tester en local api/main.py pour vérifier le caractère fonctionnel de l’API. Les deux modifications principales de celle-ci sont les suivantes:

    • On n’utilise plus la variable Title puisque celle-ci a été retirée des données en entrée du modèle
    • On récupère la version de “production” sur le système de stockage S3, on ne fait plus l’entraînement à chaque initialisation d’un conteneur
  2. Modifier deployment/deployment.yaml et .github/workflows/prod.yaml pour définir et utiliser le tag v0.0.3 de l’image Docker

  3. Retirer la ligne python train.py du fichier run.sh

  4. Dans index.qmd, remplacer la ligne suivante:

```{ojs}
prediction = `https://titanic.kub.sspcloud.fr/predict?pclass=${class_boat}&sex=${gender}&age=${age}&sib_sp=1&parch=1&fare=16.5&embarked=S&has_cabin=1&ticket_len=7`
```

par celle-ci

```{ojs}
prediction = `https://titanic.kub.sspcloud.fr/predict?pclass=${class_boat}&sex=${gender}&age=${age}&sib_sp=1&parch=1&fare=16.5&embarked=S&has_cabin=1&ticket_len=7`
```

Faire un commit et un push des modifications

terminal
1$ git stash
$ git checkout appli21
1
Pour annuler les modifications depuis le dernier commit

Garder une trace des entraînements de notre modèle grâce au register de MLFlow

terminal
$ git checkout appli21
$ git checkout -b dev
$ git push origin dev

Application 22 : archiver nos entraînements avec MLFlow
  1. Lancer MLFlow depuis l’onflet Mes services du SSPCloud. Attendre que le service soit bien lancé. Cela créera un service dont l’URL est de la forme https://user-{username}-{pod_id}.user.lab.sspcloud.fr, où pod_id est un identifiant aléatoire. Ce service MLFlow communiquera avec les VSCode que vous ouvrirez ultérieurement à partir de cet URL ainsi qu’avec le système de stockage S318.

  2. Regarder la page Experiments. Elle est vide à ce stade, c’est normal

  1. Une fois le service MLFlow fonctionnel, lancer un nouveau VSCode pour bénéficier de la configuration automatisée

  2. Clôner votre projet, vous situer sur la branche de travail (nous supposerons qu’il s’agit de dev).

  3. Depuis un terminal Python, lancer les commandes suivantes:

import mlflow
mlflow_experiment_name = "titanicml"
mlflow.set_experiment(experiment_name=mlflow_experiment_name)

Retourner sur l’UI et observer la différence, à gauche.

  1. Créer un fichier src/models/log.py
Contenu du fichier src/models/log.py
src/models/log.py
import mlflow
import os


def log_gsvc_to_mlflow(
    gscv, mlflow_experiment_name, application_number: str = "appli21"
):
    """Log a scikit-learn trained GridSearchCV object as an MLflow experiment."""
    # Set up MLFlow context
    mlflow.set_experiment(experiment_name=mlflow_experiment_name)

    for run_idx in range(len(gscv.cv_results_["params"])):
        # For each hyperparameter combination we trained the model with, we log a run in MLflow
        run_name = f"run {run_idx}"
        with mlflow.start_run(run_name=run_name):
            # Log hyperparameters
            params = gscv.cv_results_["params"][run_idx]
            for param in params:
                mlflow.log_param(param, params[param])

            # Log fit metrics
            scores = [
                score
                for score in gscv.cv_results_
                if "mean_test" in score or "std_test" in score
            ]
            for score in scores:
                mlflow.log_metric(score, gscv.cv_results_[score][run_idx])

            # Log model as an artifact
            mlflow.sklearn.log_model(gscv, "gscv_model")
            # Log training data URL
            mlflow.log_param("appli", application_number)
  1. Modifier le fichier train.py pour ajouter la ligne
mlog.log_gsvc_to_mlflow(pipe_cross_validation, EXPERIMENT_NAME, APPLI_ID)

avec

import src.models.log as mlog
  1. Faire tourner avec le paramètre --appli appli22:
terminal
$ python train.py --appli appli22
  1. Observer l’évolution de la page Experiments. Cliquer sur un des run. Observer toutes les métadonnées archivées (hyperparamètres, métriques d’évaluation, requirements.txt dont MLFlow a fait l’inférence, etc.)

  2. Observer le code proposé par MLFlow pour récupérer le run en question. Modifier le fichier eval.py à partir de cet exemple et du modèle suivant pour utiliser un des modèles archivés dans MLFlow

  3. Retourner à la liste des runs en cliquant à nouveau sur “titanicml” dans les expérimentations

  4. Dans l’onglet Table, sélectionner plusieurs expérimentations, cliquer sur Columns et ajouter mean_test_f1. Ajuster la taille des colonnes pour la voir et classer les modèles par score décroissants

  5. Cliquer sur Compare après en avoir sélectionné plusieurs. Afficher un scatterplot des performances en fonction du nombre d’estimateurs. Conclure.

terminal
1$ git stash
$ git checkout appli22
1
Pour annuler les modifications depuis le dernier commit

Cette appplication illustre l’un des premiers apports de MLFlow: on garde une trace de nos expérimentations et on peut déjà mieux comprendre la manière dont certains paramètres de notre modèle peuvent influencer la qualité de nos prédictions.

Néanmoins, persistent un certain nombre de voies d’amélioration:

  • On entraîne le modèle en local, de manière séquentielle, et en lançant nous-mêmes le script train.py
  • On n’archive pas les jeux de données associés à ces modèles (les jeux d’entraînement et de test). On doit alors le faire manuellement si on désire évaluer les performances ex post, ce qui est pénible.
  • On récupère manuellement les modèles ce qui n’est pas très pérenne.
  • Notre API n’utilise pas encore l’un des modèles archivé sur MLFlow.

Les prochaines applications permettront d’améliorer ceci.

Mise en production d’un modèle

Application 23a : réutiliser un modèle archivé sur MLFlow
  1. A partir du tableau de performance précédent, choisir le modèle avec le F1 score maximal. Accéder à celui-ci.

Créer un script dans mlflow/predict.py pour illustrer l’utilisation d’un modèle depuis MLFlow. Nous allons progressivement l’améliorer.

  1. Copier-coller le contenu ci-dessous afin de se simplifier la création de données en entrée de notre code
mlflow/predict.py
import pandas as pd

def create_data(
    pclass: int = 3,
    sex: str = "female",
    age: float = 29.0,
    sib_sp: int = 1,
    parch: int = 1,
    fare: float = 16.5,
    embarked: str = "S",
    has_cabin: int = 1,
    ticket_len: int = 7
) -> str:
    """
    """

    df = pd.DataFrame(
        {
            "Pclass": [pclass],
            "Sex": [sex],
            "Age": [age],
            "SibSp": [sib_sp],
            "parch": [parch],
            "Fare": [fare],
            "Embarked": [embarked],
            "hasCabin": [has_cabin],
            "Ticket_Len": [ticket_len] 
        }
    )

    return df

data = pd.concat([
    create_data(),
    create_data(sex="male")
])
  1. Cliquer sur votre meilleur modèle et introduire dans mlflow/predict.py le morceau de code suggéré par MLFlow, du type de celui-ci:
import mlflow
1logged_model = #A CHANGER

# Load model as a PyFuncModel.
loaded_model = mlflow.pyfunc.load_model(logged_model)

# Predict on a Pandas DataFrame.
import pandas as pd
loaded_model.predict(pd.DataFrame(data))
1
Hash du modèle

Lancer depuis la ligne de commande ce script et observer l’application obtenue.

A ce stade, nous avons amélioré la fiabilité de notre modèle car nous utilisons le meilleur. Néanmoins, celui-ci n’est pas forcément pratique à récupérer car nous utilisons un hash qui certes identifie de manière unique notre modèle mais présente l’inconvénient d’être peu intelligible. Nous allons passer de l’expérimentation à la mise en production en sélectionnant explicitement notre meilleur modèle.

Application 23b : passer en production un modèle
  1. Dans la page du modèle en question sur MLFlow, cliquer sur Register model et le nommer titanic.
  2. Aller dans l’onglet Models et observer le changement par rapport à précédemment.
  3. Mettre à jour le code dans mlflow/predict.py pour utiliser la version en production :

{.python filename = "mlflow/predict.py"} model_name = "titanic" model_version = 1 loaded_model = mlflow.pyfunc.load_model( model_uri=f"models:/{model_name}/{model_version}" )

  1. Tester cette application. Si celle-ci fonctionne, modifier la récupération du modèle dans votre script d’API.

  2. Tester en local cette API mise à jour

terminal
uvicorn api.main:app --reload --host "0.0.0.0" --port 5000
  1. Ajouter mlflow au requirements.txt

  2. Mettre à jour les fichiers .github/worflows/prod.yaml et kubernetes/deployment.yaml pour produire et utiliser le tag v0.0.5

.github/worflows/prod.yaml
name: Construction image Docker

on:
  push:
    branches:
      - main
      - dev

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      -
        name: Set up QEMU
        uses: docker/setup-qemu-action@v3
      -
        name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      -
        name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      -
        name: Build and push
        uses: docker/build-push-action@v5
        with:
          push: true
1          tags: linogaliana/application-correction:v0.0.7
1
Modifier l’image ici
# Creating MLflow deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: titanicml
spec:
  replicas: 1
  selector:
    matchLabels:
      app: titanicml
  template:
    metadata:
      labels:
        app: titanicml
    spec:
      containers:
        - name: api
1          image: linogaliana/application-correction:v0.0.7
          imagePullPolicy: Always
          env:
            - name: MLFLOW_TRACKING_URI
2              value: https://user-{USERNAME}-mlflow.user.lab.sspcloud.fr
            - name: MLFLOW_MODEL_NAME
              value: titanic
            - name: MLFLOW_MODEL_VERSION
              value: "1"
          resources:
            limits:
              memory: "2Gi"
              cpu: "1000m"
1
Modifier l’image Docker
2
Modifier l’URL de MLFlow
terminal
1$ git stash
$ git checkout appli23
1
Pour annuler les modifications depuis le dernier commit

Industrialiser les entraînements de nos modèles

Pour industrialiser nos entraînements, nous allons créer des processus parallèles indépendants pour chaque combinaison de nos hyperparamètres. Pour cela, l’outil pratique sur le SSPCloud est Argo workflows. Chaque combinaison d’hyperparamètres sera un processus isolé à l’issue duquel sera loggué le résultat dans MLFlow. Ces entraînements auront lieu en parallèle.

  1. Lancer un service Argo Workflows
  2. Dans mlflow/training.yaml
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: titanic-training-workflow-
spec:
  entrypoint: main
  arguments:
    parameters:
      # The MLflow tracking server is responsible to log the hyper-parameter and model metrics.
      - name: mlflow-tracking-uri
1        value: https://user-lgaliana-argo-workflows.user.lab.sspcloud.fr
      - name: mlflow-experiment-name
2        value: titanicml
      - name: model-training-conf-list
        value: |
          [
            { "dim": 25, "lr": 0.1 },
            { "dim": 100, "lr": 0.2 },
            { "dim": 150, "lr": 0.3 }
          ]
  templates:
    # Entrypoint DAG template
    - name: main
      dag:
        tasks:
          # Task 0: Start pipeline
          - name: start-pipeline
            template: start-pipeline-wt
          # Task 1: Train model with given params
          - name: train-model-with-params
            dependencies: [ start-pipeline ]
            template: run-model-training-wt
            arguments:
              parameters:
                - name: dim
                  value: "{{item.dim}}"
                - name: lr
                  value: "{{item.lr}}"
            # Pass the inputs to the task using "withParam"
            withParam: "{{workflow.parameters.model-training-conf-list}}"

    # Now task container templates are defined
    # Worker template for task 0 : start-pipeline
    - name: start-pipeline-wt
      inputs:
      container:
        image: busybox
        command: [ sh, -c ]
        args: [ "echo Starting pipeline" ]

    # Worker template for task-1 : train model with params
    - name: run-model-training-wt
      inputs:
        parameters:
          - name: dim
          - name: lr
      container:
        image: inseefrlab/formation-mlops:main
        imagePullPolicy: Always
        command: [sh, -c]
        args: ["mlflow run .
                --env-manager=local
                -P remote_server_uri=$MLFLOW_TRACKING_URI
                -P experiment_name=$MLFLOW_EXPERIMENT_NAME
                -P dim={{inputs.parameters.dim}}
                -P lr={{inputs.parameters.lr}}"]
        env:
          - name: MLFLOW_TRACKING_URI
            value: "{{workflow.parameters.mlflow-tracking-uri}}"
          - name: MLFLOW_EXPERIMENT_NAME
            value: "{{workflow.parameters.mlflow-experiment-name}}"
1
Changer
2
titanicml

max_depth

max_features “sqrt”, “log2”

Pour aller plus loin

Créer un service label studio pour évaluer la qualité du modèle

Footnotes

  1. L’export dans un script .py a été fait directement depuis VSCode. Comme cela n’est pas vraiment l’objet du cours, nous passons cette étape et fournissons directement le script expurgé du texte intermédiaire. Mais n’oubliez pas que cette démarche, fréquente quand on a démarré sur un notebook et qu’on désire consolider en faisant la transition vers des scripts, nécessite d’être attentif pour ne pas risquer de faire une erreur.↩︎

  2. Il est également possible avec VSCode d’exécuter le script ligne à ligne de manière interactive ligne à ligne (MAJ+ENTER). Néanmoins, cela nécessite de s’assurer que le working directory de votre console interactive est le bon. Celle-ci se lance selon les paramètres préconfigurés de VSCode et les votres ne sont peut-être pas les mêmes que les notres. Vous pouvez changer le working directory dans le script en utilisant le package os mais peut-être allez vous découvrir ultérieurement qu’il y a de meilleures pratiques…↩︎

  3. Essayez de commit vos changements à chaque étape de l’exercice, c’est une bonne habitude à prendre.↩︎

  4. Ici, le jeton d’API n’est pas indispensable pour que le code fonctionne. Afin d’éviter une erreur non nécessaire lorsqu’on automatisera le processus, on peut créer une condition qui vérifie la présence ou non de ce fichier. Le script reste donc reproductible même pour un utilisateur n’ayant pas le fichier secrets.yaml.↩︎

  5. Il est normal d’avoir des dossiers __pycache__ qui traînent en local : ils se créent automatiquement à l’exécution d’un script en Python. Néanmoins, il ne faut pas associer ces fichiers à Git, voilà pourquoi on les ajoute au .gitignore.↩︎

  6. Nous proposons ici d’adopter le principe de la programmation fonctionnelle. Pour encore fiabiliser un processus, il serait possible d’adopter le paradigme de la programmation orientée objet (POO). Celle-ci est plus rebutante et demande plus de temps au développeur. L’arbitrage coût-avantage est négatif pour notre exemple, nous proposons donc de nous en passer. Néanmoins, pour une mise en production réelle d’un modèle, il est recommandé de l’adopter. C’est d’ailleurs obligatoire avec des pipelines scikit.↩︎

  7. Attention, les données ont été committées au moins une fois. Les supprimer du dépôt ne les efface pas de l’historique. Si cette erreur arrive, le mieux est de supprimer le dépôt en ligne, créer un nouvel historique Git et partir de celui-ci pour des publications ultérieures sur Github. Néanmoins l’idéal serait de ne pas s’exposer à cela. C’est justement l’objet des bonnes pratiques de ce cours: un .gitignore bien construit et une séparation des environnements de stockage du code et des données seront bien plus efficaces pour vous éviter ces problèmes que tout les conseils de vigilance que vous pourrez trouver ailleurs.↩︎

  8. Lorsqu’on développe du code qui finalement ne s’avère plus nécessaire, on a souvent un cas de conscience à le supprimer et on préfère le mettre de côté. Au final, ce syndrôme de Diogène est mauvais pour la pérennité du projet : on se retrouve à devoir maintenir une base de code qui n’est, en pratique, pas utilisée. Ce n’est pas un problème de supprimer un code ; si finalement celui-ci s’avère utile, on peut le retrouver grâce à l’historique Git et les outils de recherche sur Github. Le package vulture est très pratique pour diagnostiquer les morceaux de code inutiles dans un projet.↩︎

  9. Le fichier __init__.py indique à Python que le dossier est un package. Il permet de proposer certaines configurations lors de l’import du package. Il permet également de contrôler les objets exportés (c’est-à-dire mis à disposition de l’utilisateur) par le package par rapport aux objets internes au package. En le laissant vide, nous allons utiliser ce fichier pour importer l’ensemble des fonctions de nos sous-modules. Ce n’est pas la meilleure pratique mais un contrôle plus fin des objets exportés demanderait un investissement qui ne vaut, ici, pas le coût.↩︎

  10. Si vous désirez aussi contrôler la version de Python, ce qui peut être important dans une perspective de portabilité, vous pouvez ajouter une option, par exemple -p python3.10. Néanmoins nous n’allons pas nous embarasser de cette nuance pour la suite car nous pourrons contrôler la version de Python plus finement par le biais de Docker.↩︎

  11. L’option -c passée après la commande python permet d’indiquer à Python que la commande ne se trouve pas dans un fichier mais sera dans le texte qu’on va directement lui fournir.↩︎

  12. L’option -c passée après la commande python permet d’indiquer à Python que la commande ne se trouve pas dans un fichier mais sera dans le texte qu’on va directement lui fournir.↩︎

  13. Pour comparer les deux listes, vous pouvez utiliser la fonctionnalité de split du terminal sur VSCode pour comparer les outputs de conda env export en les mettant en face à face.↩︎

  14. Il est tout à fait normal de ne pas parvenir à créer une action fonctionnelle du premier coup. N’hésitez pas à pusher votre code après chaque question pour vérifier que vous parvenez bien à réaliser chaque étape. Sinon vous risquez de devoir corriger bout par bout un fichier plus conséquent.↩︎

  15. Il existe une approche alternative pour faire des tests réguliers: les hooks Git. Il s’agit de règles qui doivent être satisfaites pour que le fichier puisse être committé. Cela assure que chaque commit remplisse des critères de qualité afin d’éviter le problème de la procrastination.

    La documentation de pylint offre des explications supplémentaires. Ici, nous allons adopter une approche moins ambitieuse en demandant à notre action de faire ce travail d’évaluation de la qualité de notre code↩︎

  16. Vous n’êtes pas obligés pour l’évaluation de mettre en oeuvre les jalons de plusieurs parcours. Néanmoins, vous découvrirez que chaque nouveau pas en avant est moins coûteux que le précédent si vous avez mis en oeuvre les réflexes des bonnes pratiques.↩︎

  17. Ce n’est pas indispensable si vous avez une manière cohérente de gérer vos jetons d’accès aux données dans votre API, par exemple par le biais de service account. Néanmoins, pour se faciliter la tâche, on ne va pas se poser de question sur les droits d’accès au modèle.↩︎

  18. Par conséquent, MLFLow bénéficie de l’injection automatique des tokens pour pouvoir lire/écrire sur S3. Ces jetons ont la même durée avant expiration que ceux de vos services interactifs VSCode. Il faut donc supprimer et rouvrir un service MLFLow régulièrement. La manière d’éviter cela est de créer des service account sur https://minio-console.lab.sspcloud.fr/ et de les renseigner sur la page.↩︎