Application

Appliquer pas à pas les concepts étudiés à un projet de data science

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.

Author

Romain Avouac et Lino Galiana

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.

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.

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

Partie 0 : initialisation du projet

Etape 1 : forker le dépôt d’exemple et créer une branche 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 lancer le service en cliquant directement ici.

  • 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. Garder le jeton généré de côté.

  • Forker le dépôt Github : https://github.com/ensae-reproductibilite/application-correction

  • Clôner votre dépôt Github en utilisant le terminal depuis Visual Studio (Terminal > New Terminal) :

$ git clone https://<TOKEN>@github.com/<USERNAME>/ensae-reproductibilite-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 :
$ cd ensae-reproductibilite-application-correction

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 :

Nous allons partir de ce Notebook Jupyter, que vous pouvez prévisualiser voire tester en cliquant sur l’un des liens suivants:

Onyxia Open In Colab

Etape 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.

La première étape est simple, mais souvent oubliée : vérifier que le code fonctionne correctement.

Application 1: corriger les erreurs
  • Ouvrir dans VSCode le script titanic.py ;
  • Exécuter le script ligne à ligne 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
python titanic.py

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

$ git add titanic.py
$ git commit -m "Corrige l'erreur qui empêchait l'exécution"
$ git push
git checkout appli1

ou

Script checkpoint

Etape 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.

Note

N’hésitez pas à taper un code d’erreur sur un moteur de recherche pour obtenir plus d’informations si jamais le message n’est pas clair !

Pour appliquer le linter à un script .py, la syntaxe à entrer dans le terminal est la suivante :

$ pylint mon_script.py
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
  • 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

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 beaucoup de redondance 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.

git checkout appli2

ou

titanic.py

Etape 3: gestion des paramètres

L’exécution du code et les résultats obtenus dépendent de certains paramètres. L’étude de résultats alternatifs, en jouant sur des variantes des 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. Cela peut être pratique pour mettre en oeuvre des tests automatisés3 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. Nous allons le mettre en oeuvre pour deux types de fichiers: les éléments de configuration à partager et ceux à conserver pour soi mais pouvant servir.
Application 3: Paramétrisation du script
  1. En s’inspirant de cette réponse, 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
  3. Repérer le jeton d’API dans le code. Retirer le jeton d’API du code et créer à la racine du projet un fichier YAML nommé secrets.yaml où vous écrivez ce secret sous la forme key: value
  4. 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.
  5. Créer la variable API_TOKEN ayant la valeur stockée dans secrets.yaml4.
  6. Tester en ligne de commande que l’exécution du fichier est toujours sans erreur
  7. Refaire un diagnostic avec PyLint et corriger les éventuels messages.
  8. Créer un fichier config.yaml stockant trois informations: le chemin des données d’entraînement, des données de test et la répartition train/test utilisée dans le code. Créer les variables correspondantes dans le code après avoir utilisé import_yaml_config
  9. Créer un fichier .gitignore. Ajouter dans ce fichier secrets.yaml car il ne faut pas committer ce fichier.
  10. Créer un fichier README.md où vous indiquez qu’il faut créer un fichier secrets.yaml pour pouvoir utiliser l’API.
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)

Etape 4 : Adopter la programmation fonctionnelle

Nous allons mettre en fonctions les parties importantes de l’analyse, et les mettre dans un module afin de pouvoir les importer directement depuis le notebook.

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 fonctionnelle5. Pour obtenir une chaine entièrement fonctionnalisée, vous pouvez reprendre le checkpoint.

Application 4: adoption des standards de programmation fonctionnelle
  • Créer une fonction qui importe les données d’entraînement (train.csv) et de test (test.csv) et renvoie des DataFrames Pandas ;
  • En fonction du temps disponible, créer plusieurs fonctions pour réaliser les étapes de feature engineering:
    • La création de la variable “Title” peut être automatisée en vertu du principe “do not repeat yourself”6.
    • Regrouper ensemble les fillna et essayer de créer une fonction généralisant l’opération.
    • Les label encoders peuvent être transformés en deux fonctions: une première pour encoder une colonne puis une seconde qui utilise la première de manière répétée pour encoder plusieurs colonnes. Remarquez les erreurs de copier-coller que cela corrige
    • Finaliser les dernières transformations avec des fonctions
  • Créer une fonction qui réalise le split train/test de validation en fonction d’un paramètre représentant la proportion de l’échantillon de test.
  • Créer une fonction qui entraîne et évalue un classifieur RandomForest, et qui prend en paramètre le nombre d’arbres (n_estimators). La fonction doit imprimer à la fin la performance obtenue et la matrice de confusion.
  • 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.

git checkout appli4

ou

Les autres fichiers inchangés:

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.

Illustration de l’état actuel du projet

Néanmoins, la structure du projet n’est pas encore normalisée. De plus, l’adoption d’une structure plus modulaire facilitera la compréhension de la chaine de traitement.

Etape 1 : modularisation

Fini le temps de l’expérimentation : on va maintenant essayer de se passer complètement du notebook. Pour cela, on va utiliser un main script, c’est à dire un script qui reproduit l’analyse en important et en exécutant les différentes fonctions dans l’ordre attendu.

Application 5: modularisation
  • Déplacer les fonctions dans une série de fichiers dédiés:
    • import_data.py: fonctions d’import de données
    • build_features.py: fonctions regroupant les étapes de feature engineering
    • train_evaluate.py: fonctions d’entrainement et 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. ⚠️ Ne pas utiliser from XXX import *, ce n’est pas une bonne pratique !
  • Vérifier que tout fonctionne bien en exécutant le script main à partir de la ligne de commande :
$ python main.py

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 🙈
├── README.md
├── train.csv
├── test.csv
├── .gitignore
├── config.yaml
├── secrets.yaml
├── import_data.py
├── build_features.py
├── train_evaluate.py
└──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.

Etape 2 : adopter une architecture standardisée de projet

On va maintenant 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.

On va s’inspirer de la structure du template datascience développé par la communauté.

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 :

$ 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:

ensae-reproductibilite-application
├── main.py
├── README.md
├── data
│   └── raw
│       ├── test.csv
│       └── train.csv
├── configuration
│   ├── secrets.yaml
│   └── config.yaml
├── notebooks
│   └── titanic.ipynb
└── src
    ├── data
    │   └── import_data.py
    ├── features
    │   └── build_features.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
  • Adapter les scripts et les fichiers de configuration à la nouvelle arborescence
  • Ajouter le dossier pycache au .gitignore7 et le dossier data
git checkout appli6

ou

Les autres fichiers sont inchangés, à l’exception de leur emplacement.

Etape 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 plus tard.

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
git checkout appli7

ou

Etape 3 : stocker les données de manière externe

Warning

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

Comme on l’a vu dans le cours (partie structure des projets), les données ne sont pas censées être versionnées sur un projet Git.

L’idéal pour éviter cela tout en maintenant la reproductibilité 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.

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/train.csv et data/raw/test.csv vers votre bucket personnel, respectivement dans les dossiers ensae-reproductibilite/data/raw/train.csv et ensae-reproductibilite/data/raw/test.csv.

Indice

Structure à adopter:

$ mc cp data/raw/train.csv s3/<BUCKET_PERSONNEL>/ensae-reproductibilite/data/raw/train.csv
$ mc cp data/raw/test.csv s3/<BUCKET_PERSONNEL>/ensae-reproductibilite/data/raw/test.csv
en modifiant 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. Pour cela, en ligne de commande, faire:
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/test.csv et https://minio.lab.sspcloud.fr/<BUCKET_PERSONNEL>/ensae-reproductibilite/data/raw/train.csv

  • Modifier configuration.yaml pour utiliser directement les URL dans l’import
  • Supprimer les fichiers .csv du dossier data de votre projet, on n’en a plus besoin vu qu’on les importe de l’extérieur
  • Vérifier le bon fonctionnement de votre application
git checkout appli8

ou

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é.

Etape 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 une maîtrise de la programmation orientée objet.

Application 9: test unitaire (optionnel)

Dans le dossier src/data/, créer un fichier test_create_variable_title.py8.

En s’inspirant de l’exemple de base, créer une classe TestCreateVariableTitle qui effectue les opérations suivantes:

  • Création d’une fonction test_create_variable_title_default_variable_name qui permet de comparer les objets suivants:

    • Création d’un DataFrame de test :
    df = pd.DataFrame({
                'Name': ['Braund, Mr. Owen Harris', 'Cumings, Mrs. John Bradley (Florence Briggs Thayer)',
                        'Heikkinen, Miss. Laina', 'Futrelle, Mrs. Jacques Heath (Lily May Peel)',
                        'Allen, Mr. William Henry', 'Moran, Mr. James',
                        'McCarthy, Mr. Timothy J', 'Palsson, Master. Gosta Leonard',
                        'Johnson, Mrs. Oscar W (Elisabeth Vilhelmina Berg)',
                        'Nasser, Mrs. Nicholas (Adele Achem)'],
                'Age': [22, 38, 26, 35, 35, 27, 54, 2, 27, 14],
                'Survived': [0, 1, 1, 1, 0, 0, 0, 0, 1, 1]
            })
    • Utilisation de la fonction create_variable_title sur ce DataFrame
    • Comparaison au DataFrame attendu:
    expected_result = pd.DataFrame({
                'Title': ['Mr.', 'Mrs.', 'Miss.', 'Mrs.', 'Mr.', 'Mr.', 'Mr.', 'Master.', 'Mrs.', 'Mrs.'],
                'Age': [22, 38, 26, 35, 35, 27, 54, 2, 27, 14],
                'Survived': [0, 1, 1, 1, 0, 0, 0, 0, 1, 1]
            })
  • Effectuer le test unitaire en ligne de commande avec unittest. Corriger le test unitaire en cas d’erreur.

  • Si le temps le permet, proposer des variantes pour tenir compte de paramètres (comme la variable variable_name) ou d’exceptions (comme la gestion du cas “Dona”)

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:

$ coverage run -m pytest test_create_variable_title.py
$ coverage report -m

Name                            Stmts   Miss  Cover   Missing
-------------------------------------------------------------
import_data.py                     15      6    60%   16-19, 31-34
test_create_variable_title.py      21      1    95%   54
-------------------------------------------------------------
TOTAL                              36      7    81%

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.

git checkout appli9

ou

Les autres fichiers sont inchangés.

Etape 2 : transformer son projet en package (optionnel)

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

Structure visée
ensae-reproductibilite-application
├── docs                                    ┐ 
│   ├── main.py                             │ 
│   └── notebooks                           │ Package documentation and examples
│       ├── titanic.ipynb                   │ 
├── README.md                               ┘ 
├── pyproject.toml                          ┐ 
├── requirements.txt                        │
├── src                                     │
│   └── titanicml                           │ Package source code, metadata,
│       ├── __init__.py                     │ and build instructions 
│       ├── config.yaml                     │
│       ├── import_data.py                  │
│       ├── build_features.py               │ 
│       └── 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              
Application 10: packagisation (optionnel)
  • Déplacer les fichiers dans le dossier src pour respecter la nouvelle arborescence ;
  • Dans src/titanicml, créer un fichier vide __init__.py9 ;
  • Déplacer le fichier de configuration dans le package (nécessaire à la reproductibilité) ;
  • Créer le dossier docs et mettre les fichiers indiqués dedans
  • Modifier src/titanicml/import_data.py :
    • Ajouter la variable config_file = os.path.join(os.path.dirname(__file__), "config.yaml"). Cela permettra d’utiliser directement le fichier ;
    • Proposer un argument par défaut à la fonction import_config_yaml égal à config_file
  • Créer un fichier pyproject.toml à partir du contenu de ce modèle de pyproject10
  • Installer le package en local avec pip install .
  • 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
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.

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]:
git checkout appli10

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 seconde partie, 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:

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.

git checkout appli8

Etape 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.

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 officielle11

  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 commande avec:

    python -c "print('Hello')"
  6. Faire la même chose mais avec import pandas as pd

  7. Installer les packages à partir du requirements.txt. Tester à nouveau import pandas as pd pour comprendre la différence.

  8. Exécuter pip freeze et comprendre la différence avec la situation précédente.

  9. 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

  10. Ajouter le dossier titanic/ au .gitignore pour ne pas ajouter ce dossier à Git

git checkout appli11a

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

conda est à la fois un gestionnaire de packages (alternative à pip) et d’environnements virtuels. L’inconvénient de l’utilisation de conda pour gérer les environnements virtuels est que cet outil est assez lent car l’algorithme de vérification des conflits de version n’est pas extrêmement rapide.

Pour cette raison, nous allons utiliser mamba, un utilitaire de gestion des environnements conda qui est plus rapide.

Application 11b: environnement conda
  1. Exécuter conda env export en ligne de commande et observer la (très) longue liste de package

  2. Tester l’utilisation d’un package qu’on n’utilise pas dans notre chaine de production, par exemple seaborn:

    python -c "import seaborn as sns"
  3. Créer un environnement titanic avec mamba create en listant les packages que vous aviez mis dans le requirements.txt et en ajoutant l’option -c conda-forge à la fin pour utiliser la conda forge

  4. Activer l’environnement et vérifier l’installation de Python maintenant utilisée par votre machine

  5. (optionnel) Utiliser ls dans le dossier parent de Python pour observer et comprendre le contenu de celui-ci

  6. Vérifier que cette fois seaborn n’est pas installé dans l’environnement :

    python -c "import seaborn as sns"
  7. Exécuter à nouveau conda env export et comprendre la différence avec la situation précédente12.

  8. Vérifier que le script main.py fonctionne bien. Sinon utiliser mamba install avec les packages manquants jusqu’à ce que la chaine de production fonctionne

  9. Créer le fichier environment.yaml à partir de conda env export:

    conda env export > environment.yaml
  10. Ajouter le dossier titanic/ au .gitignore pour ne pas ajouter ce dossier à Git

git checkout appli11b

Etape 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
#!/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
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)
sudo ./install.sh
  1. Vérifier que le script main.py fonctionne correctement dans l’environnement virtuel créé
source titanic/bin/activate
python3 main.py
git checkout appli12a
Application 12b : créer un fichier d’installation de A à Z
  1. Créer un service ubuntu sur le SSP Cloud en cliquant sur ce lien
  2. Cloner le dépôt et se placer au niveau du checkpoint 11b avec git checkout appli11b
  3. Se placer dans le dossier du projet avec cd
  4. On va se placer en super-utilisateur dans la ligne de commande en tapant
sudo bash
  1. Créer le fichier install.sh avec le contenu suivant:
Script à créer sous le nom 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 install mamba -c conda-forge
mamba create -n titanic pandas PyYAML scikit-learn -c conda-forge
mamba activate titanic

PATH="/miniconda/envs/titanic/bin:${PATH}"

python main.py
  1. Changer les permissions sur le fichier
chmod u+x ./install.sh
  1. Exécuter le script depuis la ligne de commande
git checkout appli12b

Etape 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:

Maintenant qu’on sait que ce script préparatoire fonctionne, on va le transformer en Dockerfile (la syntaxe Docker est légèrement différente de la syntaxe Linux classique). Puis on va le tester dans un environnement bac à sable (pour ensuite pouvoir plus facilement automatiser la construction de l’image Docker par la suite).

Application 13: création de l’image Docker

Se placer dans un environnement avec Docker

  • Dans le terminal Linux, cloner votre dépôt Github
  • 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
touch Dockerfile
  • Ouvrir ce fichier via un éditeur de texte et copier le contenu suivant dedans:
Premier Dockerfile
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 l’image

Le Dockerfile est la recette de construction de l’image. La construction effective de l’image à partir de cette recette s’appelle l’étape de build.

  • Utiliser docker build pour créer une image avec le tag my-python-app
docker build . -t my-python-app
  • Vérifier les images dont vous disposez. Vous devriez avoir un résultat proche de celui-ci :
$ docker images

REPOSITORY      TAG       IMAGE ID       CREATED         SIZE
my-python-app   latest    c0dfa42d8520   6 minutes ago   836MB
ubuntu          25.04     825d55fb6340   6 days ago      77.8MB

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.

$ 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: config.yaml et 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
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
COPY configuration ./configuration
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é.

git checkout appli13

Partie 4 : automatisation avec l’intégration continue

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. Nous allons donc proposer d’aller plus loin en proposant plusieurs types de livrables.

Cela va nous amener à découvrir les outils du CI/CD (Continuous Integration / Continuous Delivery) qui sont au coeur de l’approche DevOps.

Notre approche appliquée au machine learning va nous entraîner plutôt du côté du MLOps qui devient une approche de plus en plus fréquente dans l’industrie de la data science.

Nous allons améliorer notre approche de trois manières:

  • Automatisation de la création de l’image Docker et tests automatisés de la qualité du code ;
  • Production d’un site web automatisé permettant de documenter et valoriser le modèle de Machine Learning ;
  • Mise à disposition du modèle entraîné par le biais d’une API pour ne pas le ré-entraîner à chaque fois et faciliter sa réutilisation ;

A chaque fois, nous allons d’abord tester en local notre travail puis essayer d’automatiser cela avec les outils de Github.

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.

Etape 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

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:

  1. Créer un fichier .github/workflows/ci.yaml avec le contenu de l’exemple de la documentation
  2. Retirer la strategy matrix et ne tester qu’avec la version 3.10 de Python
  3. Utiliser le fichier requirements.txt pour installer les dépendances.
  4. Remplacer russ par pylint pour vérifier la qualité du code. Ajouter l’argument --fail-under=6 pour renvoyer une erreur en cas de note trop basse13
  5. Plutôt que pytest, utiliser python main.py pour tester que la matrice de confusion s’affiche bien.
Fichier .github/workflows/ci.yaml obtenu
name: Build, test and push

on: [push]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.10'
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install pylint
          pip install -r requirements.txt
      - name: Lint
        run: |
          pylint src --fail-under=6
      - name: Test workflow
        run: |
          python main.py
git checkout appli14

Maintenant, nous pouvons observer que l’onglet Actions s’est enrichi. Chaque commit va entraîner une action pour tester nos scripts.

Si la note 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.

Etape 2: Automatisation de la livraison de l’image Docker

Maintenant, nous allons automatiser la mise à disposition de notre image sur DockerHub. 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.
  • Créer un dépôt public application-correction
  • Aller dans les paramètres (https://hub.docker.com/settings/general) 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 votre dépôt Github, cliquer sur l’onglet Settings et cliquer, à gauche, sur Actions. Sur la page qui s’affiche, cliquer sur New repository secret
  • Donner le nom DOCKERHUB_TOKEN à ce jeton et copier la valeur. Valider
  • Créer un deuxième secret nommé DOCKERHUB_USERNAME ayant comme valeur le nom d’utilisateur que vous avez créé sur Dockerhub

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 https://github.com/docker/build-push-action/#usage. 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, ajouter un nouveau job docker dans le fichier ci.yaml qui va build et push l’image sur le DockerHub
  • Définir le job test comme prérequis du job docker en vous référant à cette documentation
  • Changer le tag à la fin pour mettre <username>/application-correction:latestusername est le nom d’utilisateur sur DockerHub;
Fichier .github/workflows/ci.yaml obtenu
name: Build, test and push

on: [push]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.10'
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install pylint
          pip install -r requirements.txt
      - name: Lint
        run: |
          pylint src --fail-under=6
      - name: Test workflow
        run: |
          python main.py
  docker:
    runs-on: ubuntu-latest
    needs: test
    steps:
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v2
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2
      - name: Login to Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      - name: Build and push
        uses: docker/build-push-action@v4
        with:
          push: true
          tags: linogaliana/application-correction:latest
  • 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.

Application 15c: Afficher un badge dans le README
  • Se rendre dans l’onglet Actions et cliquer sur un des scripts en train de tourner.
  • En haut à droite, cliquer sur ...
  • Sélectionner Create status badge
  • Récupérer le code Markdown proposé
  • Copier dans le README depuis VSCode
  • Faire de même pour l’autre workflow

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

Application 15d: Tester l’application
  • Se rendre sur l’environnement bac à sable Play with Docker
  • Récupérer l’image :
docker pull <username_dockerhub>/application-correction:latest
  • Tester le bon fonctionnement de l’image
docker run -it <username_dockerhub>/application-correction:latest

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

git checkout appli15

Partie 5: mise en production d’une API servant un modèle de machine learning

Etape 1: création d’un pipeline scikit

Notre code respecte des bonnes pratiques formelles. Cependant, la mise en production nécessite d’être exigeant sur la mise en oeuvre opérationnelle de notre pipeline.

Quand on utilise scikit, la bonne pratique est d’utiliser les pipelines qui sécurisent les étapes de feature engineering avant la mise en oeuvre d’un modèle (que ce soit pour l’entraînement ou le test sur un nouveau jeu de données).

On va donc devoir refactoriser notre application pour utiliser un pipeline scikit. Les raisons sont expliquées ici. Cela aura aussi l’avantage de rendre les étapes plus lisibles.

Application 16: Un pipeline de machine learning
  • Refactoriser le code de random_forest_titanic pour créer un vrai pipeline de preprocessing avant la modélisation

  • Simplifier la fonction split_train_test_titanic en la réduisant au découpage train/test

  • Modifier main.py pour que ce soit à ce niveau qu’a lieu le découpage en train/test, l’entrainement et l’évaluation du modèle

git checkout appli16

Etape 2: développer une API en local

Application 17: Mise à disposition sous forme d’API locale
  • Créer un nouveau service SSPCloud en paramétrant dans l’onglet Networking le port 5000 ;
  • Cloner le dépôt et se placer au niveau de l’application précédente (git checkout appli16)
  • Installer fastAPI et uvicorn puis les ajouter au requirements.txt
  • Renommer le fichier main.py en train.py et insérer le contenu suivant dedans :
Fichier train.py Récupérer le contenu sur cette page
  • Créer le fichier api.py permettant d’initialiser l’API:
Fichier api.py Récupérer le contenu sur cette page
  • Exécuter train.py pour stocker en local le modèle entraîné
  • Ajouter model.joblib au .gitignore
  • Déployer en local l’API avec la commande
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 (requests.get(url).json())
git checkout appli17

Etape 3: déployer l’API

A ce stade, nous avons déployé l’API seulement localement, dans le cadre d’un service. 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. A présent, il est temps de passer à l’étape de déploiement, qui permettra à notre API d’être accessible via une URL sur le web, et donc aux utilisateurs potentiels de la requêter. Pour se faire, on va utiliser les possibilités offertes par Kubernetes, sur lequel est basé le SSP Cloud.

Application 18: Dockeriser l’API
  • Modifier le Dockerfile pour tenir compte des changements dans les noms de fichier effecutés dans l’application précédente

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

Fichier run.sh
#/bin/bash

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

  • Changer l’instruction CMD du Dockerfile pour exécuter le script run.sh au lancement du conteneur

  • Commit et push les changements

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

git checkout appli18
Application 19: Déployer l’API
  • 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
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
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
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: titanic-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - titanic.kub.sspcloud.fr
  rules:
  - host: titanic.kub.sspcloud.fr
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: titanic-service
            port:
              number: 80
  • Appliquer ces fichiers de configuration sur le cluster : kubectl apply -f deployement/

  • Si tout a correctement fonctionné, vous devriez pouvoir accéder à l’API à l’URL spécifiée dans le fichier deployment/ingress.yaml

git checkout appli19

Partie 6: un workflow complet de MLOps

Ce sera l’an prochain, désolé !

Partie 7: livrer un site web de manière automatisée

On va proposer un nouveau livrable pour parler à un public plus large. Pour cela, on va déployer un site web statique qui permet de visualiser rapidement les résultats du modèle.

On propose de créer un site web qui permet de comprendre, avec l’appui des valeurs de Shapley, les facteurs qui auraient pu nous mettre la puce à l’oreille sur les destins de Jake et de Rose.

Pour faire ce site web, on va utiliser Quarto et déployer sur Github Pages. Des étapes préliminaires sont réalisées en Python puis l’affichage interactif sera contrôlé par du JavaScript grâce à des blocs Observable.

Application 19: Déploiement automatisé d’un site web

Dans un premier temps, on va créer un projet Quarto au sein de notre dépôt:

  • Installer Quarto dans votre environnement local (s’il n’est pas déjà disponible) ;
  • Dans le projet, utiliser la commande quarto create-project pour initialiser le projet Quarto ;
  • Supprimer le fichier automatiquement généré avec l’extension .qmd ;
  • Récupérer le contenu du modèle de fichier Quarto Markdown cette page. Celui-ci permet de générer la page d’accueil de notre site. Enregistrer dans un fichier nommé index.qmd

On teste ensuite la compilation en local du fichier:

  • Modifier le fichier train.py à partir de cette page pour être en mesure de compiler le fichier
  • Exécuter le fichier train.py
  • 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
  • Créer un fichier .github/workflows/website.yaml avec le contenu de ce fichier
Note

On doit dans cette application modifier le fichier train.py pour enregistrer en local une duplication du modèle de machine learning et de l’ensemble d’entraînement car pour ces deux éléments on n’est pas allé au bout de la démarche MLOps d’enregistrement dans un model registry et un feature store.

Dans la prochaine version de ce cours, qui intègrera MLFlow, on aura une démarche plus propre car on utilisera bien le modèle de production et le jeu d’entrainement associé.

git checkout appli20

Footnotes

  1. L’export dans un script .py a été fait avec Jupytext. Comme cela n’est pas vraiment l’objet du cours, nous passons cette étape et fournissons directement le script. 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. Essayez de commit vos changements à chaque étape de l’exercice, c’est une bonne habitude à prendre.↩︎

  3. Nous le verrons lorsque nous mettrons en oeuvre l’intégration continue.↩︎

  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.

    Cela peut être fait avec la fonction os.path.exists :

    if os.path.exists('secrets.yaml'):
        secrets = import_yaml_config("secrets.yaml")

    La variable secrets n’existera que dans le cas où un fichier secrets.yaml existe. Le script reste donc reproductible même pour un utilisateur n’ayant pas le fichier secrets.yaml.↩︎

  5. 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.↩︎

  6. Au passage vous pouvez noter que mauvaises pratiques discutables, peuvent être corrigées, notamment l’utilisation excessive de apply là où il serait possible d’utiliser des méthodes embarquées par Pandas. Cela est plutôt de l’ordre du bon style de programmation que de la qualité formelle du script. Ce n’est donc pas obligatoire mais c’est mieux.↩︎

  7. Il est normal d’avoir des dossiers __pycache__ qui traînent : ils se créent automatiquement à l’exécution d’un script en Python.↩︎

  8. L’emplacement de ce fichier est amené à évoluer dans le cadre d’une packagisation. Dans un package, ces tests seront dans un dossier spécifique /tests car Python sait gérer de manière plus formelle les imports de fonctions depuis des modules. Ici, on est dans une situation transitoire, raison pour laquelle les tests sont dans les mêmes dossiers que les fonctions.↩︎

  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. Ce pyproject.toml est un modèle qui utilise setuptools pour build le package. C’est l’outil classique. Néanmoins, pour des usages plus raffinés, il peut être utile d’utiliser poetry qui propose des fonctionnalités plus complètes.↩︎

  11. 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.9.↩︎

  12. 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.↩︎

  13. 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 assurera 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.↩︎