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.
Introduction
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.
L’objectif pédagogique principal de cette application est d’adopter un point de vue pragmatique en choisissant des outils et des méthodes de travail qui permettent de réaliser des objectifs ambitieux de valorisation de données. Python
sera le trait d’union entre les différentes technologies ou infrastructures que nous utiliserons.
Cette application 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 et il existe des outils alternatifs à ceux présentés. Néanmoins, les outils présentés ont l’avantage d’être très bien intégrés à Python
, bien configurés si vous utilisez le SSPCloud
comme nous le recommandons, tout en étant agnostiques sur le reste des outils que vous utilisez ; de sorte à ne pas être bloquants si on remplace l’une des briques logicielles par une autre.
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 :
- Import de données ;
- Statistiques descriptives et visualisations ;
- Feature engineering ;
- Entraînement d’un modèle ;
- Evaluation du modèle.
Objectif
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 et en adoptant une méthode de travail fluidifiant les évolutions futures.
La Figure 1 montre que notre point de départ initial, à savoir un notebook, mélange tout. Ceci rend très complexe la mise à jour de notre modèle ou l’exploitation de notre modèle sur de nouvelles données, ce qui est pourtant la raison d’être du machine learning qui est pensé pour l’extrapolation. Si on vous demande de valoriser votre modèle sur de nouvelles données, vous risquez de devoir refaire tourner tout votre notebook, avec le risque de ne pas retrouver les mêmes résultats que dans la version précédente.
La Figure 2 illustre l’horizon auquel nous aboutirons à la fin de cette application. Nous désynchronisons les étapes d’entraînement et de prédiction, en identifiant mieux les pré-requis de chacune et en adoptant des briques technologiques adaptées à celles-ci. Les noms présents sur cette figure sont encore obscurs, c’est normal, mais ils vous deviendrons familiers si vous adoptez une infrastructure et une méthode de travail à l’état de l’art.


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.
Ce que cette application ne couvre pas (pour le moment)
A l’heure actuelle, cette application se concentre sur la mise en oeuvre fiable de l’entraînement de modèles de machine learning. Comme vous pouvez le voir, quand on part d’aussi loin qu’un projet monolithique dans un notebook, c’est un travail conséquent d’en arriver à un pipeline pensé pour la production. Cette application vise à vous sensibiliser au fait qu’avoir la Figure 2 en tête et adopter une organisation de travail et faire des choix techniques adéquats, vous fera économiser des dizaines voire centaines d’heures lorsque votre modèle aura vocation à passer en production.
A l’heure actuelle, cette application ne se concentre que sur une partie du cycle de vie d’un projet data ; il y a déjà fort à faire. Nous nous concentrons sur l’entraînement et la mise à disposition d’un modèle à des fins opérationnelles. C’est la première partie du cycle de vie d’un modèle. Dans une approche MLOps, il faut également penser la maintenance de ce modèle et les enjeux que représentent l’arrivée continue de nouvelles données, ou le besoin d’en collecter de nouvelles à travers des annotations, sur la qualité prédictive d’un modèle. Toute entreprise qui ne pense pas cet après est vouée à se faire doubler par un nouveau venu. Une prochaine version de cette application permettra certainement d’illustrer certains des enjeux afférants à la vie en production d’un modèle (supervision, annotations…) sur notre cas d’usage.
Il convient aussi de noter que nous ne faisons que parcourir la surface des sujets que nous évoquons. Ce cours, déjà dense, deviendrait indigeste si nous devions présenter chaque outil dans le détail. Nous laissons donc les curieux approfondir chacun des outils que nous présentons pour découvrir comment en tirer le maximum (et si vous avez l’impression que nous oublions des éléments cruciaux, les issues et pull requests sont bienvenues).
Partie 0 : initialisation du projet
Nous allons prendre comme point de départ un projet livré exclusivement avec un notebook, à la manière d’un challenge Kaggle. Vous pourrez ainsi voir à quel point ce type de livrable est très loin d’être satisfaisant si on veut que le projet soit réutilisable.
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 en faisant attention à une option :- Décocher la case “Copy the
main
branch only” afin de copier également les tagsGit
qui nous permettront de faire les checkpoint.
- Décocher la case “Copy the
Ce que vous devriez voir sur la page de création du fork.
Nous recommandons d’utiliser, tout au long de ce projet, l’environnement de développement VSCode
. En plus d’être très bien construit, les nombreuses extensions disponibles rendent celui-ci adaptable à tous nos besoins. Comme il s’agit de l’outil sur lequel vous passerez votre quotidien, n’hésitez pas à personnaliser celui-ci grâce aux nombreuses ressources disponibles en ligne1.
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 pageMy Services
et cliquer surNew service
. Sinon, vous pouvez initialiser la création du service en cliquant directement ici. Modifier les options suivantes:- Dans l’onglet
Role
, sélectionner le rôleAdmin
; - 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 - (optionnel) Pour préinstaller quelques extensions supplémentaires à celles disponibles par défaut, dans l’onglet
Init
, dans le champPersonalInit
, renseigner l’adresse https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/init.sh
- Dans l’onglet
- Clôner votre dépôt
Github
en utilisant le terminal depuisVisual 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.git
où $TOKEN
et $USERNAME
sont à remplacer, respectivement, par le jeton que vous avez généré précédemment et votre nom d’utilisateur.
Le script d’initialisation proposé
Ce script initialise quelques extensions intéressantes pour le développement de projets utilisant Python
ou des fichiers textes type Markdown
ou YAML
: diagnostic, mise en forme automatisée, etc.
#!/bin/bash
# Define the configuration directory for VS Code
="$HOME/.local/share/code-server/User"
VSCODE_CONFIG_DIR
# Create the configuration directory if necessary
-p "$VSCODE_CONFIG_DIR"
mkdir
# User settings file
="$VSCODE_CONFIG_DIR/settings.json"
SETTINGS_FILE
-server --install-extension yzhang.markdown-all-in-one
code-server --install-extension oderwat.indent-rainbow
code-server --install-extension tamasfe.even-better-toml
code-server --install-extension aaron-bond.better-comments
code-server --install-extension github.vscode-github-actions
code
# Replace default flake8 linter with project-preconfigured ruff
-server --uninstall-extension ms-python.flake8
code-server --install-extension charliermarsh.ruff
code
'. + {
jq "workbench.colorTheme": "Default Dark Modern", # Set the theme
"editor.rulers": [80, 100, 120], # Add specific vertical rulers
"files.trimTrailingWhitespace": true, # Automatically trim trailing whitespace
"files.insertFinalNewline": true, # Ensure files end with a newline
"flake8.args": [
"--max-line-length=100" # Max line length for Python linting
]
' "$SETTINGS_FILE" > "$SETTINGS_FILE.tmp" && mv "$SETTINGS_FILE.tmp" "$SETTINGS_FILE" }
Si, dans quelques jours, vous désirez relancer un service avec cette configuration, vous pouvez cliquer sur ce lien:
En cliquant sur l’onglet Git
, vous pouvez renseigner directement votre URL de la forme https://github.com/username/depot.git
: cela clônera votre dépôt dans le service et injectera le token pour vous économiser l’authentification à venir lors de la phase de push
.
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 :
- Utilisation du terminal (voir Linux 101) ;
- Qualité du code (voir Qualité du code) ;
- Architecture de projets (voir Architecture des projets) ;
- Contrôle de version avec
Git
(voir RappelsGit
) ; - Travail collaboratif avec
Git
etGitHub
(voir RappelsGit
).
Le plan de la partie est le suivant :
- S’assurer que le script fonctionne ;
- Nettoyer le code des scories formelles avec un linter et un formatter ;
- Paramétrisation du script ;
- 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 notebook2 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.
- Ouvrir dans
VSCode
le scripttitanic.py
; - Exécuter le script en ligne de commande (
python titanic.py
)3 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 Git
4 :
terminal
git add titanic.py
git commit -m "Corrige l'erreur qui empêchait l'exécution"
git push
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli12
rm -f update.sh
- 1
- Récupérer le script de checkpoint
- 2
- Avancer à l’état à l’issue de l’application appli1
- 3
- Nettoyer derrière nous
É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.
Le linter PyLint
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.
- Diagnostiquer et évaluer la qualité de
titanic.py
avecPyLint
. Regarder la note obtenue. - Utiliser
black titanic.py --diff --color
pour observer les changements de forme que va induire l’utilisation du formatterBlack
. 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:
- 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
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli22
rm -f update.sh
- 1
- Récupérer le script de checkpoint
- 2
- Avancer à l’état à l’issue de l’application appli2
- 3
- Nettoyer derrière nous
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
VSCode
actif avec la configuration proposée dans l'application préliminaire, vous pouvez repartir de ce service:Et ensuite, après avoir clôné le dépôt
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli22
rm -f update.sh
- 1
- Récupérer le script de checkpoint
- 2
- Avancer à l’état à l’issue de l’application appli2
- 3
- Nettoyer derrière nous
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:
- 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 ;
- 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
= argparse.ArgumentParser(description="Qui êtes-vous?")
parser
parser.add_argument("--prenom", type=str, default="Toto", help="Un prénom à afficher"
)= parser.parse_args()
args print(args.prenom)
Exemples d’utilisations en ligne de commande
terminal
python prenom.py
python prenom.py --prenom "Zinedine"
- 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 ; - 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.
- Installer le package
python-dotenv
que nous allons utiliser pour charger notre jeton d’API à partir d’une variable d’environnement. - A partir de l’exemple de la documentation, utiliser la fonction
load_dotenv
pour charger dansPython
nos variables d’environnement à partir d’un fichier (vous pouvez le créer mais ne pas le remplir encore avec les valeurs voulues, ce sera fait ensuite) - Créer la variable et vérifier la sortie de
Python
en faisant tournertitanic.py
en ligne de commande
titanic.py
= os.environ.get("JETON_API", "")
jeton_api
if jeton_api.startswith("$"):
print("API token has been configured properly")
else:
print("API token has not been configured")
- Maintenant introduire la valeur voulue pour le jeton d’API dans le fichier d’environnement lu par
dotenv
- S’il n’existe pas déjà, créer un fichier
.gitignore
(cf. ChapitreGit
). Ajouter dans ce fichier.env
car il ne faut pas committer ce fichier. Au passage ajouter__pycache__/
au.gitignore
5, cela évitera d’avoir à le faire ultérieurement ; - Créer un fichier
README.md
où vous indiquez qu’il faut créer un fichier.env
pour pouvoir utiliser l’API.
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli32
rm -f update.sh
- 1
- Récupérer le script de checkpoint
- 2
- Avancer à l’état à l’issue de l’application appli3
- 3
- Nettoyer derrière nous
É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. Comme cela est évoqué dans les éléments magistraux de ce cours, l’utilisation de fonctions va rendre notre code plus concis, plus traçable, mieux documenté.
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.
Pour commencer, cet exercice fait un petit pas de côté pour faire comprendre la manière dont les pipelines scikit sont un outil au service des bonnes pratiques.
Scikit
?
- Le pipeline
Scikit
d’estimation et d’évaluation vous a été donné tel quel. Regardez, ci-dessous, le code équivalent sans utiliser de pipelineScikit
:
Le code équivalent sans pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import MinMaxScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import confusion_matrix
import pandas as pd
import numpy as np
# Définition des variables
= ["Age", "Fare"]
numeric_features = ["Embarked", "Sex"]
categorical_features
# PREPROCESSING ----------------------------
# Handling missing values for numerical features
= SimpleImputer(strategy="median")
num_imputer = num_imputer.fit_transform(X_train[numeric_features])
X_train[numeric_features] = num_imputer.transform(X_test[numeric_features])
X_test[numeric_features]
# Scaling numerical features
= MinMaxScaler()
scaler = scaler.fit_transform(X_train[numeric_features])
X_train[numeric_features] = scaler.transform(X_test[numeric_features])
X_test[numeric_features]
# Handling missing values for categorical features
= SimpleImputer(strategy="most_frequent")
cat_imputer = cat_imputer.fit_transform(X_train[categorical_features])
X_train[categorical_features] = cat_imputer.transform(X_test[categorical_features])
X_test[categorical_features]
# One-hot encoding categorical features
= OneHotEncoder(handle_unknown='ignore', sparse_output=False)
encoder = encoder.fit_transform(X_train[categorical_features])
X_train_encoded = encoder.transform(X_test[categorical_features])
X_test_encoded
# Convert encoded features into a DataFrame
= pd.DataFrame(X_train_encoded, columns=encoder.get_feature_names_out(categorical_features), index=X_train.index)
X_train_encoded = pd.DataFrame(X_test_encoded, columns=encoder.get_feature_names_out(categorical_features), index=X_test.index)
X_test_encoded
# Drop original categorical columns and concatenate encoded ones
= X_train.drop(columns=categorical_features).join(X_train_encoded)
X_train = X_test.drop(columns=categorical_features).join(X_test_encoded)
X_test
# MODEL TRAINING ----------------------------
# Defining the model
= RandomForestClassifier(n_estimators=n_trees)
model
# Fitting the model
model.fit(X_train, y_train)
# EVALUATION ----------------------------
# Scoring
= model.score(X_test, y_test)
rdmf_score print(f"{rdmf_score:.1%} de bonnes réponses sur les données de test pour validation")
# Confusion matrix
print(20 * "-")
print("matrice de confusion")
print(confusion_matrix(y_test, model.predict(X_test)))
Voyez-vous l’intérêt de l’approche par pipeline en termes de lisibilité, évolutivité et fiabilité ?
Créer un notebook qui servira de brouillon. Y introduire le code suivant:
Le code à copier-coller dans un notebook
import pandas as pd
from sklearn.preprocessing import OneHotEncoder, MinMaxScaler
from sklearn.ensemble import RandomForestClassifier
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
= pd.read_csv("train.csv")
train = pd.read_csv("test.csv")
test = train.drop("Survived", axis="columns"), train["Survived"]
X_train, y_train = test.drop("Survived", axis="columns"), train["Survived"]
X_test, y_test
= None
MAX_DEPTH = "sqrt"
MAX_FEATURES =20
n_trees
= ["Age", "Fare"]
numeric_features = ["Embarked", "Sex"]
categorical_features
# Variables numériques
= Pipeline(
numeric_transformer =[
steps"imputer", SimpleImputer(strategy="median")),
("scaler", MinMaxScaler()),
(
]
)
# Variables catégorielles
= Pipeline(
categorical_transformer =[
steps"imputer", SimpleImputer(strategy="most_frequent")),
("onehot", OneHotEncoder()),
(
]
)
# Preprocessing
= ColumnTransformer(
preprocessor =[
transformers"Preprocessing numerical", numeric_transformer, numeric_features),
(
("Preprocessing categorical",
categorical_transformer,
categorical_features,
),
]
)
# Pipeline
= Pipeline(
pipe
["preprocessor", preprocessor),
("classifier", RandomForestClassifier(
(=n_trees,
n_estimators=MAX_DEPTH,
max_depth=MAX_FEATURES
max_features
)),
]
)
pipe.fit(X_train, y_train)
Afficher ce pipeline dans une cellule de votre notebook. Cela vous aide-t-il mieux à comprendre les différentes étapes du pipeline de modélisation ?
Comment pouvez-vous accéder aux étapes de preprocessing ?
- Comment pouvez-vous faire pour appliquer le pipeline de preprocessing des variables numériques (et uniquement celui-ci) à ce DataFrame ?
Le DataFrame à créer pour appliquer un bout de notre pipeline
import numpy as np
= {
new_data "Age": [22, np.nan, 35, 28, np.nan],
"Fare": [7.25, 8.05, np.nan, 13.00, 15.50]
}
= pd.DataFrame(new_data) new_data
- Normalement ce code ne devrait pas prendre plus d’une demie-douzaine de lignes. Sans pipeline le code équivalent, beaucoup plus verbeux et moins fiable, ressemble à celui-ci
Le code équivalent, sans pipeline
import pandas as pd
import numpy as np
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import MinMaxScaler
# Définition des nouvelles données
= pd.DataFrame({
new_data "Age": [25, np.nan, 40, 33, np.nan],
"Fare": [10.50, 7.85, np.nan, 22.00, 12.75]
})
# Définition des transformations (même que dans le pipeline)
= SimpleImputer(strategy="median")
num_imputer = MinMaxScaler()
scaler
# Apprentissage des transformations sur X_train (assumant que vous l'avez déjà)
= X_train[["Age", "Fare"]] # Supposons que X_train existe
X_train_numeric
num_imputer.fit(X_train_numeric)
scaler.fit(num_imputer.transform(X_train_numeric))
# Transformation des nouvelles données
= num_imputer.transform(new_data)
new_data_imputed = scaler.transform(new_data_imputed)
new_data_scaled
# Création du DataFrame final
= pd.DataFrame(
new_data_preprocessed
new_data_scaled,=["Age_scaled", "Fare_scaled"] # Générer des noms de colonnes adaptés
columns
)
# Affichage du DataFrame
print(new_data_preprocessed)
- Imaginons que vous ayez déjà des données préprocessées:
Créer des données préprocessées
import numpy as np
import pandas as pd
= pd.DataFrame({
new_data "Age": [25, np.nan, 40, 33, np.nan],
"Fare": [10.50, 7.85, np.nan, 22.00, 12.75],
"Embarked": ["S", "C", np.nan, "Q", "S"],
"Sex": ["male", "female", "male", np.nan, "female"]
})= np.random.randint(0, 2, size=len(new_data))
new_y
= pd.DataFrame(
preprocessed_data -1].transform(new_data),
pipe[:= preprocessor_numeric.get_feature_names_out()
columns
) preprocessed_data
- Déterminer le score en prédiction sur ces données
Maintenant, revenons à notre chaine de production et appliquons des fonctions pour la rendre plus lisible, plus fiable et plus modulaire.
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 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
etmax_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. Si besoin, ajouter des paramètres à votre fichier d’environnement pour créer de nouvelles variables comme les chemins des données.
- En profiter pour supprimer le code zombie qu’on a gardé jusqu’à présent mais qui ne correspond pas vraiment à des opérations utiles à notre chaine de production
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli42
rm -f update.sh
- 1
- Récupérer le script de checkpoint
- 2
- Avancer à l’état à l’issue de l’application appli4
- 3
- Nettoyer derrière nous
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 150 lignes dont une centaine issues 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.
Dans cette partie nous allons continuer les améliorations incrémentales de notre projet avec les étapes suivantes:
- Modularisation du code
Python
pour séparer les différentes étapes de notre pipeline ; - Adopter une structure standardisée pour notre projet afin d’autodocumenter l’organisation de celui-ci ;
- Documenter les packages indispensables à l’exécution du code ;
- 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
.
- Déplacer les fonctions dans une série de fichiers dédiés:
build_pipeline.py
: script avec la définition du pipelinetrain_evaluate.py
: script avec les fonctions d’évaluation du projet
- 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
enmain.py
pour suivre la convention de nommage des projetsPython
; - 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
- Optionnel: profitez en pour mettre un petit coup de formatter à votre projet, si vous ne l’avez pas fait régulièrement.
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli52
rm -f update.sh
- 1
- Récupérer le script de checkpoint
- 2
- Avancer à l’état à l’issue de l’application appli5
- 3
- Nettoyer derrière nous
É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
├── .env
├── data.csv
├── train.csv
├── test.csv
├── README.md
├── build_pipeline.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.
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
├── .env
├── README.md
├── data
│ ├── raw
│ │ └── data.csv
│ └── derived
│ ├── test.csv
│ └── train.csv
├── notebooks
│ └── titanic.ipynb
└── src
├── pipeline
│ └── build_pipeline.py
└── models
└── train_evaluate.py
- (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
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli62
rm -f update.sh
- 1
- Récupérer le script de checkpoint
- 2
- Avancer à l’état à l’issue de l’application appli6
- 3
- Nettoyer derrière nous
Étape 3: mieux tracer notre chaine de production
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.
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 fichierrequirements.txt
Tracer notre chaîne
Quand votre projet passera en production, vous aurez un accès limité à celui-ci. Il est donc important de faire remonter, par le biais du logging des informations critiques sur votre projet qui vous permettront de savoir où il en est (si vous avez accès à la console où il tourne) ou là où il s’est arrêté.
L’utilisation de print
montre rapidement ses limites pour cela. Les informations enregistrées ne persistent pas après la session et sont quelques peu rudimentaires.
Pour faire du logging, la librairie consacrée depuis longtemps en Python
est… logging
. Il existe aussi une librairie nommée loguru
qui est un peu plus simple à configurer (l’instanciation du logger est plus aisée) et plus agréable grâce à ses messages en couleurs qui permettent de visuellement trier les informations.
L’exercice suivant peut être fait avec les deux librairies, cela ne change pas grand chose. Les prochaines applications repartiront de la version utilisant la librairie standard logging
.
- Aller sur la documentation de la librairie ici et sur ce tutoriel pour trouver des sources d’inspiration sur la configuration et l’utilisation de
logging
. - Pour afficher les messages dans la console et dans un fichier de log, s’inspirer de cette réponse sur stack overflow.
- Tester en ligne de commande votre code et observer le fichier de log
- Installer
loguru
et l’ajouter aurequirements.txt
- En s’aidant du
README
du projet surGithub
, remplacer nosprint
par différents types de messages (info, success, etc.). - Tester l’exécution du script en ligne de commande et observer vos sorties
- Mettre à jour le logger pour enregistrer dans un fichier de log. Ajouter celui-ci au
.gitignore
puis tester en ligne de commande votre script. Ouvrir le fichier en question, refaites tourner le script et regardez son évolutoin. - Il est possible avec
loguru
de capturer les erreurs des fonctions grâce au système de cache décrit ici. Introduire une erreur dans une des fonctions (par exemple danscreate_pipeline
) avec un code du typeraise ValueError("Problème ici")
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli72
rm -f update.sh
- 1
- Récupérer le script de checkpoint
- 2
- Avancer à l’état à l’issue de l’application appli7
- 3
- Nettoyer derrière nous
Étape 4 : stocker les données de manière externe
Pour cette partie, il faut avoir un service VSCode
dont les jetons d’authentification à S3
sont valides. Pour cela, si vous êtes sur le SSPCloud
, le plus simple est de recréer un nouveau service avec le bouton suivant
et remplir l’onglet Git
comme ça votre VSCode
sera pré à l’emploi (cf. application 0).
Une fois que vous avez un VSCode
fonctionnel, il est possible de reprendre cette application fil rouge depuis le checkpoint précédent.
VSCode
actif avec la configuration proposée dans l'application préliminaire, vous pouvez repartir de ce service:Et ensuite, après avoir clôné le dépôt
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli72
rm -f update.sh
- 1
- Récupérer le script de checkpoint
- 2
- Avancer à l’état à l’issue de l’application appli7
- 3
- Nettoyer derrière nous
Enfin, il vous suffira d’ouvrir un terminal et faire pip install -r requirements.txt && python main.py
pour pouvoir démarrer l’application.
L’étape précédente nous a permis d’isoler la configuration. Nous avons conceptuellement isolé les données du code lors des applications précédentes. Cependant, nous n’avons pas été au bout du chemin car le stockage des données reste conjoint à celui du code. Nous allons maintenant dissocier ces deux éléments.
S3
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.
Parquet
L’objectif de cette application est de montrer comment utiliser le format Parquet
dans une chaîne production ; un objectif somme toute modeste.
Si vous voulez aller plus loin dans la découverte du format Parquet
, vous pouvez consulter cette ressource R
très similaire à ce cours (oui elle est faite par les mêmes auteurs…) et essayer de faire les exercices avec votre librairie Python
de prédilection (PyArrow
ou DuckDB
)
SSPCloud
? (une idée saugrenue mais sait-on jamais)
Les exemples à venir peuvent très bien être répliqués sur n’importe quel cloud provider qui propose une solution de type S3
, qu’il s’agisse d’un cloud provider privé (AWS, GCP, Azure, etc.) ou d’une réinstanciation ad hoc du projet Onyxia
, le logiciel derrière le SSPCloud
.
Pour un système de stockage S3
, il suffit de changer les paramètres de connexion de s3fs
(endpoint, region, etc.). Pour les stockages sur GCP
, les codes sont presque équivalents, il suffit de remplacer la librairie s3fs
par gcfs
; ces deux librairies sont en fait des briques d’un standard plus général de gestion de systèmes de fichiers en Python
ffspec
.
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. Schématiquement, nous visons la structure de projet suivante:
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.
Plus concrètement, nous allons adopter le pipeline suivant pour notre projet:
Le scénario type est que nous avons une source brute, reçue sous forme de CSV, dont on ne peut changer le format. Il aurait été idéal d’avoir un format plus adapté au traitement de données pour ce fichier mais ce n’était pas de notre ressort. Notre chaine va aller chercher ce fichier, travailler dessus jusqu’à valoriser celui-ci sous la forme de notre matrice de confusion. Si on imagine que notre chaine prend un certain temps, il n’est pas inutile d’écrire des données intermédiaires. Pour faire cela, puisque nous avons la main, autant choisir un format adapté, à savoir le format Parquet
.
Cette application va se dérouler en trois temps:
- Upload de notre source brute (CSV) sur S3
- Illustration de l’usage des librairies cloud native pour lire celle-ci
- Partage public de cette donnée pour la rendre accessible de manière plus simple à nos futures applications.
S3
Pour commencer, à 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
="nom_utilisateur_sspcloud"
BUCKET_PERSONNEL/raw/data.csv s3/${BUCKET_PERSONNEL}/ensae-reproductibilite/data/raw/data.csv mc cp data
BUCKET_PERSONNEL
, l’emplacement de votre bucket personnel
Pour se simplifier la vie, dans les prochaines applications, 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
.
Néanmoins, il est utile de les utiliser une fois pour comprendre la logique. Pour aller plus loin sur ces librairies, vous pouvez consulter cette page du cours de 2A de Python pour la data science.
Pour commencer, on va lister les fichiers se trouvant dans un bucket. En ligne de commande, sur notre poste local, on ferait ls
(cf. Linux 101). Cela ne va pas beaucoup différer avec les librairies cloud native:
Dans un notebook, copier-coller ce code, le modifier et exécuter:
import s3fs
= s3fs.S3FileSystem(client_kwargs={"endpoint_url": "https://minio.lab.sspcloud.fr"})
fs
1= "mon_nom_utilisateur_sspcloud"
MY_BUCKET 2= "ensae-reproductibilite/data/raw"
CHEMIN f"s3://{MY_BUCKET}/{CHEMIN}") fs.ls(
- 1
- Changer avec le bucket
- 2
- Changer en fonction du chemin voulu
On va maintenant lire directement une donnée stockée sur S3
. Pour illustrer le fait que cela change peu notre code d’être sur un système cloud avec les librairies adaptées, on va lire directement un fichier CSV
stocké sur le SSPCloud
, sans passer par un fichier en local8.
S3
Pour illustrer la cohérence avec un système de fichier local, voici trois solutions pour lire le fichier que vous venez de mettre sur S3
. Attention, il faut avoir des jetons de connexion à S3
à jour. Si vous avez cette erreur
A client error (InvalidAccessKeyId) occurred when calling the ListBuckets operation: The AWS Access Key Id you provided does not exist in our records.
c’est que vos identifiants de connexion ne sont plus à jour (pour des raisons de sécurité, ils sont régulièrement renouvelés). Dans ce cas, recréez un service VSCode
avec le bouton proposé plus haut.
Dans un notebook, copier-coller et mettre à jour ces deux variables qui seront utilisées dans différents exemples:
1= "mon_nom_utilisateur_sspcloud"
MY_BUCKET 2= "ensae-reproductibilite/data/raw/data.csv" CHEMIN_FICHIER
- 1
- Changer avec le bucket
- 2
- Changer en fonction du chemin voulu
import s3fs
import pandas as pd
= s3fs.S3FileSystem(client_kwargs={"endpoint_url": "https://minio.lab.sspcloud.fr"})
fs
with fs.open(f"s3://{MY_BUCKET}/{CHEMIN_FICHIER}") as f:
= pd.read_csv(f)
df
df
import s3fs
from pyarrow import csv
= s3fs.S3FileSystem(client_kwargs={"endpoint_url": "https://minio.lab.sspcloud.fr"})
fs
with fs.open(f"s3://{MY_BUCKET}/{CHEMIN_FICHIER}") as f:
= csv.read_csv(f)
df
df
import os
import duckdb
= duckdb.connect(database=":memory:")
con
con.execute(f"""
CREATE SECRET secret (
TYPE S3,
KEY_ID '{os.environ["AWS_ACCESS_KEY_ID"]}',
SECRET '{os.environ["AWS_SECRET_ACCESS_KEY"]}',
ENDPOINT 'minio.lab.sspcloud.fr',
SESSION_TOKEN '{os.environ["AWS_SESSION_TOKEN"]}',
REGION 'us-east-1',
URL_STYLE 'path',
SCOPE 's3://{MY_BUCKET}/'
);
"""
)
= f"SELECT * FROM read_csv('s3://{MY_BUCKET}/{CHEMIN_FICHIER}')"
query_definition = con.sql(query_definition)
df
df
Pour illustrer le fonctionnement encore plus simple de S3 avec les fichiers Parquet
, on propose de copier un Parquet
mis à disposition dans un bucket collectiv vers votre bucket personnel:
1="nom_utilisateur_sspcloud"
BUCKET_PERSONNEL
2-o rp.parquet "https://minio.lab.sspcloud.fr/projet-formation/bonnes-pratiques/data/REGION=11/part-0.parquet"
curl
/${BUCKET_PERSONNEL}/ensae-reproductibilite/data/example/rp.parquet
mc cp rp.parquet s3
rm rp.parquet
- 1
- Remplacer par le nom de votre bucket.
- 2
-
Télécharger le fichier
Parquet
mis à dispositoin
Pour lire ceux-ci, tester les exemples de code suivants:
1= "mon_nom_utilisateur_sspcloud"
MY_BUCKET = "ensae-reproductibilite/data/example/rp.parquet" CHEMIN_FICHIER
- 1
- Remplacer ici par la valeur appropriée
import s3fs
import pandas as pd
= s3fs.S3FileSystem(client_kwargs={"endpoint_url": "https://minio.lab.sspcloud.fr"})
fs
= pd.read_parquet(f"s3://{MY_BUCKET}/{CHEMIN_FICHIER}", filesystem=fs)
df
df
import pyarrow as pa
import pyarrow.parquet as pq
= pa.fs.S3FileSystem(endpoint_override ="https://minio.lab.sspcloud.fr")
s3
= pq.read_table(f"{MY_BUCKET}/{CHEMIN_FICHIER}", filesystem=s3)
df
df
import os
import duckdb
= duckdb.connect(database=":memory:")
con
con.execute(f"""
CREATE SECRET secret (
TYPE S3,
KEY_ID '{os.environ["AWS_ACCESS_KEY_ID"]}',
SECRET '{os.environ["AWS_SECRET_ACCESS_KEY"]}',
ENDPOINT 'minio.lab.sspcloud.fr',
SESSION_TOKEN '{os.environ["AWS_SESSION_TOKEN"]}',
REGION 'us-east-1',
URL_STYLE 'path',
SCOPE 's3://{MY_BUCKET}/'
);
"""
)
= f"SELECT * FROM read_parquet('s3://{MY_BUCKET}/{CHEMIN_FICHIER}')"
query_definition = con.sql(query_definition)
df
df
Pour aller plus loin sur le format Parquet
, notamment découvrir comment importer des données partitionnées, vous pouvez traduire en Python
les exemples issus de la formation aux bonnes pratiques avec R
de l’Insee.
Parquet
dans notre chaîne
Dans main.py
, remplacer le format csv initialement prévu par un format parquet:
= os.environ.get("train_path", "data/derived/train.parquet")
data_train_path = os.environ.get("test_path", "data/derived/test.parquet") data_test_path
Et modifier l’écriture des données pour utiliser to_parquet
plutôt que to_csv
pour écrire les fichiers intermédiaires:
main.py
= 1).to_parquet(data_train_path)
pd.concat([X_train, y_train], axis = 1).to_parquet(data_test_path) pd.concat([X_test, y_test], axis
S3
Par défaut, le contenu de votre bucket est privé, seul vous y avez accès. Pour pouvoir lire votre donnée, vos applications externes devront utiliser des jetons vous identifiant. Ici, comme nous utilisons une donnée publique, vous pouvez rendre accessible celle-ci à tous en lecture. Dans le jargon S3, cela signifie donner un accès anonyme à votre donnée.
Le modèle de commande à utiliser dans le terminal est le suivant:
terminal
1BUCKET_PERSONNEL="nom_utilisateur_sspcloud"
mc anonymous set download s3/${BUCKET_PERSONNEL}/ensae-reproductibilite/data/raw/
- 1
- Remplacer par le nom de votre bucket.
Les URL de téléchargement seront de la forme https://minio.lab.sspcloud.fr/<BUCKET_PERSONNEL>/ensae-reproductibilite/data/raw/data.csv
- Remplacer la définition de
data_path
pour utiliser, par défaut, directement l’URL dans l’import. Modifier, si cela est pertinent, aussi votre fichier.env
.
1= ""
URL_RAW = os.environ.get("data_path", URL_RAW) data_path
- 1
-
Modifier avec
URL_RAW
un lien de la forme"https://minio.lab.sspcloud.fr/${BUCKET_PERSONNEL}/ensae-reproductibilite/data/raw/data.csv"
(ne laissez pas${BUCKET_PERSONNEL}
, remplacez par la vraie valeur!).
Ajouter le dossier
data/
au.gitignore
ainsi que les fichiers*.parquet
Supprimer le dossier
data
de votre projet et faitesgit 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 maintenance9).
Pour vous aider, vous pouvez utiliser vulture
de manière itérative pour vous assister dans le nettoyage de votre code.
terminal
pip install vulture vulture .
Exemple de sortie
terminal
vulture .
/data/import_data.py:3: unused function 'split_and_count' (60% confidence)
src/pipeline/build_pipeline.py:12: unused function 'split_train_test' (60% confidence) src
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli82
rm -f update.sh
- 1
- Récupérer le script de checkpoint
- 2
- Avancer à l’état à l’issue de l’application appli8
- 3
- Nettoyer derrière nous
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
.
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
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli92
rm -f update.sh
- 1
- Récupérer le script de checkpoint
- 2
- Avancer à l’état à l’issue de l’application appli9
- 3
- Nettoyer derrière nous
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-------------------------------------------------------------------
/features/build_features.py 34 21 38% 35-36, 48-58, 71-74, 85-89, 99-101, 111-113
src/test_create_variable_title.py 21 1 95% 54
tests-------------------------------------------------------------------
55 22 60% TOTAL
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
.
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 analysis0.1.0]:
package_version [3.9]:
python_version [
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
from 1, 2, 3, 4, 5, 6 [1]:
Choose
Select include_github_actions:1 - no
2 - ci
3 - ci+cd
from 1, 2, 3 [1]: Choose
- Renommer le dossier
titanicml
pour respecter la nouvelle arborescence ; - Créer un fichier
pyproject.toml
sur cette base ;
#| code-summary: "pyproject.toml"
#| filename: "pyproject.toml"
[tool.poetry]= "titanicml"
name = "0.0.1"
version = "Awesome Machine Learning project"
description = ["Daffy Duck <daffy.duck@fauxmail.fr>", "Mickey Mouse"]
authors = "MIT"
license = "README.md"
readme
-system]
[build= ["poetry-core"]
requires -backend = "poetry.core.masonry.api"
build
[tool.pytest.ini_options]= true
log_cli = "WARNING"
log_cli_level = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)"
log_cli_format = "%Y-%m-%d %H:%M:%S" log_cli_date_format
- Créer le dossier
docs
et mettre les fichiers indiqués dedans - Dans
titanicml/
, créer un fichier__init__.py
10
#| code-summary: "__init__.py"
#| filename: "__init__.py"
from .data.import_data import (
split_and_count
)from .pipeline.build_pipeline import (
split_train_test,
create_pipeline
)from .models.train_evaluate import (
evaluate_model
)= [
__all__ "split_and_count",
"split_train_test",
"create_pipeline",
"evaluate_model"
]
- Installer le package en local avec
pip install -e .
- Modifier le contenu de
docs/main.py
pour importer les fonctions de notre packagetitanicml
et tester en ligne de commande notre fichiermain.py
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli102
rm -f update.sh
- 1
- Récupérer le script de checkpoint
- 2
- Avancer à l’état à l’issue de l’application appli10
- 3
- Nettoyer derrière nous
Partie 3 : construction d’un projet portable et reproductible
VSCode
actif avec la configuration proposée dans l'application préliminaire, vous pouvez repartir de ce service:Et ensuite, après avoir clôné le dépôt
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli82
rm -f update.sh
- 1
- Récupérer le script de checkpoint
- 2
- Avancer à l’état à l’issue de l’application appli8
- 3
- Nettoyer derrière nous
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:
- Environnements virtuels ;
- Créer un script shell qui permet, depuis un environnement minimal, de construire l’application de A à Z ;
- 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).
VSCode
actif avec la configuration proposée dans l'application préliminaire, vous pouvez repartir de ce service:Et ensuite, après avoir clôné le dépôt
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli82
rm -f update.sh
- 1
- Récupérer le script de checkpoint
- 2
- Avancer à l’état à l’issue de l’application appli8
- 3
- Nettoyer derrière nous
É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
.
venv
- Exécuter
pip freeze
en ligne de commande et observer la (très) longue liste de package - Créer l’environnement virtuel
titanic
en s’inspirant de la documentation officielle11 ou du chapitre dédié - Utiliser
ls
pour observer et comprendre le contenu du dossiertitanic/bin
installé - Le SSPCloud, par défaut, fonctionne sur un environnement
conda
. Le désactiver en faisantconda deactivate
. - Activer l’environnement et vérifier l’installation de
Python
maintenant utilisée par votre machine - Vérifier directement depuis la ligne de commande que
Python
exécute bien une commande12 avec:
terminal
python -c "print('Hello')"
- Faire la même chose mais avec
import pandas as pd
- Installer les packages à partir du
requirements.txt
. Tester à nouveauimport pandas as pd
pour comprendre la différence. - Exécuter
pip freeze
et comprendre la différence avec la situation précédente. - Vérifier que le script
main.py
fonctionne bien. Sinon ajouter les packages manquants dans lerequirements.txt
et reprendre de manière itérative à partir de la question 7. - 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
which python
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli11a2
rm -f update.sh
- 1
- Récupérer le script de checkpoint
- 2
- Avancer à l’état à l’issue de l’application appli11a
- 3
- Nettoyer derrière nous
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
- Exécuter
conda env export
en ligne de commande et observer la (très) longue liste de package - Créer un environnement
titanic
avecconda create
- Activer l’environnement et vérifier l’installation de
Python
maintenant utilisée par votre machine - Vérifier directement depuis la ligne de commande que
Python
exécute bien une commande13 avec:
terminal
python -c "print('Hello')"
- Faire la même chose mais avec
import pandas as pd
- Installer les packages qu’on avait listé dans le
requirements.txt
précédemment. Ne pas faire unpip install -r requirements.txt
afin de privilégierconda install
- Exécuter à nouveau
conda env export
et comprendre la différence avec la situation précédente14. - 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. - Quand
main.py
fonctionne, faireconda env export > environment.yml
pour figer l’environnement de travail.
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli11b2
rm -f update.sh
- 1
- Récupérer le script de checkpoint
- 2
- Avancer à l’état à l’issue de l’application appli11b
- 3
- Nettoyer derrière nous
uv
est le new kid in the game pour gérer les environnements virtuels avec Python
.
venv
(via uv
)
- Après avoir installé
uv
, exécuteruv init .
et supprimer le fichierhello.py
généré. Ouvrir lepyproject.toml
et observer sa structure. - Exécuter
uv pip freeze
en ligne de commande et observer la (très) longue liste de package - Créer un environnement virtuel
titanic
par le biais d’uv
(documentation) sous le nomtitanic
- Utiliser
ls
pour observer et comprendre le contenu du dossiertitanic/bin
installé - Activer l’environnement et vérifier l’installation de
Python
maintenant utilisée par votre machine - Vérifier directement depuis la ligne de commande que
Python
exécute bien une commande15 avec:
terminal
python -c "print('Hello')"
- Faire la même chose mais avec
import pandas as pd
. Maintenant, essayeruv run main.py
en ligne de commande: comprenez-vous ce qu’il se passe ? - Installer de manière itérative les packages à partir d’
uv add
(documentation) et en testant avecuv run main.py
: avez-vous remarqué la vitesse à laquelle cela a été quand vous avez faituv add pandas
? - Observer votre
pyproject.toml
. Regarder le lockfileuv.lock
. Générer automatiquement lerequirements.txt
en faisantpip compile
et regarder celui-ci. - Ajouter le dossier
titanic/
au.gitignore
pour ne pas ajouter ce dossier àGit
.
Aide pour la question 5
Après l’activation, vous pouvez vérifier quel python
est utilisé de cette manière
terminal
which python
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli11c2
rm -f update.sh
- 1
- Récupérer le script de checkpoint
- 2
- Avancer à l’état à l’issue de l’application appli11c
- 3
- Nettoyer derrière nous
É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 commandesLinux
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.
- Créer un service
ubuntu
sur le SSP Cloud - Ouvrir un terminal
- Cloner le dépôt
- Se placer dans le dossier du projet avec
cd
- Se placer au niveau du checkpoint 11a avec
git checkout appli11a
- 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
- Changer les permissions sur le script pour le rendre exécutable
terminal
chmod +x install.sh
- 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
- Vérifier que le script
main.py
fonctionne correctement dans l’environnement virtuel créé
terminal
source titanic/bin/activate
python3 main.py
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli12a2
rm -f update.sh
- 1
- Récupérer le script de checkpoint
- 2
- Avancer à l’état à l’issue de l’application appli12a
- 3
- Nettoyer derrière nous
- Créer un service
ubuntu
sur le SSP Cloud - Ouvrir un terminal
- Cloner le dépôt
- Se placer dans le dossier du projet avec
cd
- Se placer au niveau du checkpoint 11b avec
git checkout appli11b
- 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
- Changer les permissions sur le script pour le rendre exécutable
terminal
chmod +x install.sh
- 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
- Vérifier que le script
main.py
fonctionne correctement dans l’environnement virtuel créé
terminal
conda activate titanic
python3 main.py
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli12b2
rm -f update.sh
- 1
- Récupérer le script de checkpoint
- 2
- Avancer à l’état à l’issue de l’application appli12b
- 3
- Nettoyer derrière nous
Étape 3: conteneuriser l’application avec Docker
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:
- Installer
Docker
sur sa machine ; - Se rendre sur l’environnement bac à sable Play with Docker
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
.
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ôtGithub
- Repartir de la dernière version à disposition. Par exemple, si vous avez privilégié l’environnement virtuel
venv
, ce sera:
terminal
1git 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
# 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 tagmy-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-python-app latest 188957e16594 About a minute ago 879MB my
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
't open file '/~/titanic/main.py': [Errno 2] No such file or directory python3: can
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
# 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 !
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
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli132
rm -f update.sh
- 1
- Récupérer le script de checkpoint
- 2
- Avancer à l’état à l’issue de l’application appli13
- 3
- Nettoyer derrière nous
Partie 4 : automatisation avec l’intégration continue
VSCode
actif avec la configuration proposée dans l'application préliminaire, vous pouvez repartir de ce service:Et ensuite, après avoir clôné le dépôt
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli132
rm -f update.sh
- 1
- Récupérer le script de checkpoint
- 2
- Avancer à l’état à l’issue de l’application appli13
- 3
- Nettoyer derrière nous
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.
VSCode
actif avec la configuration proposée dans l'application préliminaire, vous pouvez repartir de ce service:Et ensuite, après avoir clôné le dépôt
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli132
rm -f update.sh
- 1
- Récupérer le script de checkpoint
- 2
- Avancer à l’état à l’issue de l’application appli13
- 3
- Nettoyer derrière nous
É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 !
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 code16
- Créer un fichier
.github/workflows/test.yaml
avec le contenu de l’exemple de la documentation - Avec l’aide de la documentation, introduire une étape d’installation des dépendances. Utiliser le fichier
requirements.txt
pour installer les dépendances. - Utiliser
pylint
pour vérifier la qualité du code. Ajouter l’argument--fail-under=6
pour renvoyer une erreur en cas de note trop basse17 - Utiliser une étape appelant notre application en ligne de commande (
python main.py
) pour tester que la matrice de confusion s’affiche bien. - Créer un secret stockant une valeur du
JETON_API
. Ne le faites pas commencer par un “$” comme ça vous pourrez regarder la log ultérieurement - Aller voir votre test automatisé dans l’onglet
Actions
de votre dépôt surGithub
- (optionnel): Créer un artefact à partir du fichier de log que vous créez dans
main.py
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli142
rm -f update.sh
- 1
- Récupérer le script de checkpoint
- 2
- Avancer à l’état à l’issue de l’application appli14
- 3
- Nettoyer derrière nous
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
.
- 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
- 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’ongletSettings
et cliquer, à gauche, surSecrets and variables
puis dans le menu déroulant en dessous surActions
. Sur la page qui s’affiche, aller dans la sectionRepository secrets
- Créer un jeton
DOCKERHUB_TOKEN
à partir du jeton que vous aviez créé surDockerhub
. Valider - Créer un deuxième secret nommé
DOCKERHUB_USERNAME
ayant comme valeur le nom d’utilisateur que vous avez créé surDockerhub
Etape optionnelle supplémentaire si on met en production un site web
- Dans le dépôt
Github
de votre projet, cliquer sur l’ongletSettings
et cliquer, à gauche, surActions
. Donner les droits d’écriture à vos actions sur le dépôt du projet (ce sera nécessaire pourGithub 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:
Docker
- En s’inspirant de ce template, créer le fichier
.github/workflows/prod.yml
qui va build et push l’image sur leDockerHub
. 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:latest
oùusername
est le nom d’utilisateur surDockerHub
; - Optionnel: changer le nom de l’action
- Retirer la condition restrictive sur les commits pour lesquels sont lancés cette automatisation. Pour cela, remplacer le contenu de
- Faire un
commit
et unpush
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é.
- 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:latest
🎉 La matrice de confusion doit s’afficher ! Vous avez grandement facilité la réutilisation de votre image.
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli152
rm -f update.sh
- 1
- Récupérer le script de checkpoint
- 2
- Avancer à l’état à l’issue de l’application appli15
- 3
- Nettoyer derrière nous
Partie 5: déployer une application en 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.
Afin de faire le parallèle avec les parcours possibles pour l’évaluation, nous allons proposer deux valorisations :
- 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’environnementPython
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.
- Installer
fastAPI
etuvicorn
puis les ajouter aurequirements.txt
- Renommer le fichier
main.py
entrain.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 votreDockerfile
et vos actionsGithub
sous peine d’essuyer des échecs lors de vos actionsGithub
après le prochain push.Ajouter
model.joblib
au.gitignore
carGit
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
app/api.py
permettant d’initialiser l’API:
Fichier app/api.py
app/api.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
= load('model.joblib')
model
= FastAPI(
app ="Prédiction de survie sur le Titanic",
title=
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(
str = "female",
sex: float = 29.0,
age: float = 16.5,
fare: str = "S"
embarked: -> str:
) """
"""
= pd.DataFrame(
df
{"Sex": [sex],
"Age": [age],
"Fare": [fare],
"Embarked": [embarked],
}
)
= "Survived 🎉" if int(model.predict(df)) == 1 else "Dead ⚰️"
prediction
return prediction
- Déployer l’API en local avec la commande suivante.
terminal
uvicorn app.api:app
- Observer l’output dans la console. Notre API est désormais déployée en local, plus précisément sur le localhost, un serveur web local déployé à l’adresse
http://127.0.0.1
. L’API est déployée sur le port par défaut utilisé paruvicorn
, soit le port8000
. - Sans fermer le terminal précédent, ouvrir un nouveau terminal. Tester le bon déploiement de l’API en requêtant son endpoint. Pour cela, on envoie une simple requête
GET
sur le endpoint via l’utilitairecurl
.
terminal
curl "http://127.0.0.1:8000"
Si tout s’est bien passé, on devrait avoir récupéré une réponse (au format
JSON
) affichant le message d’accueil de notre API. Dans ce cas, on va pouvoir requêter notre modèle via l’API.En vous inspirant du code qui définit le endpoint
/predict
dans le code de l’API (app/api.py
), effectuer sur le même modèle que la requête précédente une requête qui calcule la survie d’une femme de32
ans qui aurait payé son billet16
dollars et aurait embarqué au portS
.
Solution
terminal
curl "http://127.0.0.1:8000/predict?sex=female&age=32&fare=16&embarked=S"
- Toujours sans fermer le terminal qui déploie l’API, ouvrir une session
Python
et tester une requête avec des paramètres différents, avec la librairierequests
:
Solution
import requests
= "http://127.0.0.1:8000/predict?sex=male&age=25&fare=80&embarked=S"
URL requests.get(URL).json()
- Une fois que l’API a été testée, vous pouvez fermer l’application en effectuant CTRL+C depuis le terminal où elle est lancée.
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli162
rm -f update.sh
- 1
- Récupérer le script de checkpoint
- 2
- Avancer à l’état à l’issue de l’application appli16
- 3
- Nettoyer derrière nous
Étape 2: déployer l’API de manière manuelle
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 attendue. 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, à tout moment, 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
, technologie sur laquelle est basée l’infrastructure SSP Cloud
.
SSPCloud
? (une idée saugrenue mais sait-on jamais)
Les exemples à venir peuvent très bien être répliqués sur n’importe quel cloud provider qui propose une solution d’ordonnancement type Kubernetes
. Il existe également des fournisseurs de services dédiés, généralement associés à une implémentation, par exemple pour Streamlit
. Ces services sont pratiques si on n’a pas le choix mais il faut garder à l’esprit qu’ils peuvent constituer un mur de la production car vous ne contrôlez pas l’environnement en question, qui peut se distinguer de votre environnement de développement.
Et si jamais vous voulez avoir un SSPCloud
dans votre entreprise c’est possible: le logiciel Onyxia
sur lequel repose cette infrastructure est open source et est, déjà, réimplémenté par de nombreux acteurs. Pour bénéficier d’un accompagnement dans la création d’une telle infrastructure, rdv sur le Slack
du projet Onyxia
:
- Créer un script
app/run.sh
à la racine du projet qui lance le scripttrain.py
puis déploie localement l’API. Attention, quand on se place dans le monde des conteneurs et plus généralement des infrastructures cloud, on ne va plus déployer sur le localhost mais sur “l’ensemble des interfaces réseaux”. Lorsqu’on déploie une application web dans un conteneur, on va donc toujours devoir spécifier un host valant0.0.0.0
(et non plus localhost ou, de manière équivalente,http://127.0.0.1
).
Fichier run.sh
api/run.sh
#/bin/bash
python3 train.py
uvicorn app.api:app --host "0.0.0.0"
Donner au script
api/run.sh
des permissions d’exécution :chmod +x api/run.sh
Ajouter
COPY app ./app
pour avoir les fichiers nécessaires au lancement dans l’API dans l’imageModifier
COPY train.py .
pour tenir compte du nouveau nom du fichierChanger l’instruction
CMD
duDockerfile
pour exécuter le scriptapi/run.sh
au lancement du conteneur (CMD ["bash", "-c", "./app/run.sh"]
)Commit et push les changements
Une fois le CI terminé, vérifier que le nouveau tag
latest
a été pushé sur le DockerHub. Récupérer la nouvelle image dans votre environnement de test deDocker
et vérifier que l’API se déploie correctement.
Tester l’image sur le SSP Cloud
Lancer dans un terminal la commande suivante pour pull l’application depuis le DockerHub et la déployer en local :
terminal
kubectl run -it api-ml --image=votre_compte_docker_hub/application:latest
- Si tout se passe correctement, vous devriez observer dans la console un output similaire au déploiement en local de la partie précédente. Cette fois, l’application est déployée à l’adresse
http://0.0.0.0:8000
. On ne peut néanmoins pas directement l’exploiter à ce stade : si le conteneur de l’API est déployé, il manque un ensemble de ressources Kubernetes qui permettent de déployer proprement l’API à tout utilisateur. C’est l’objet de l’application suivante !
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli172
rm -f update.sh
- 1
- Récupérer le script de checkpoint
- 2
- Avancer à l’état à l’issue de l’application appli17
- 3
- Nettoyer derrière nous
Nous avons préparé la mise à disposition de notre API mais à l’heure actuelle elle n’est pas accessible 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.
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 clusterKubernetes
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
#| filename: "deployment/deployment.yaml"
/v1
apiVersion: apps
kind: Deployment
metadata:-deployment
name: titanic
labels:
app: titanic
spec:1
replicas:
selector:
matchLabels:
app: titanic
template:
metadata:
labels:
app: titanic
spec:
containers:- name: titanic
/application:latest
image: votre_compte_docker_hub
ports:- containerPort: 8000
- En vous inspirant de la documentation, y ajouter un second fichier
service.yaml
qui va créer une ressourceService
permettant de donner une identité fixe auPod
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: 8000
- En vous inspirant de la documentation, y ajouter un troisième fichier
ingress.yaml
qui va créer une ressourceIngress
permettant d’exposer le service via une URL en dehors du cluster
Fichier deployment/ingress.yaml
#| filename: "deployment/ingress.yaml"
/v1
apiVersion: networking.k8s.io
kind: Ingress
metadata:-ingress
name: titanic
spec:
ingressClassName: nginx
tls:- hosts:
- votre_nom_d_application.lab.sspcloud.fr
rules:- host: votre_nom_d_application.lab.sspcloud.fr
http:
paths:- path: /
pathType: Prefix
backend:
service:-service
name: titanic
port:80 number:
- Mettez l’URL auquel vous voulez exposer votre service. Sur le modèle de
titanic.lab.sspcloud.fr
(mais ne tentez pas celui-là, il est déjà pris 😃) - Mettre cette même URL ici aussi
Appliquer ces fichiers de configuration sur le cluster :
kubectl apply -f deployment/
Vérifier le bon déploiement de l’application (c’est à dire du
Pod
qui encapsule le conteneur) à l’aide de la commandekubectl get pods
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 exemplehttps://api-titanic-test.lab.sspcloud.fr/
si vous avez mis celui-ci plus tôtExplorer le swagger de votre API à l’adresse
https://api-titanic-test.lab.sspcloud.fr/docs
. Il s’agit d’une page de documentation standard à la plupart des APIs, bien utiles pour tester des requêtes de manière interactive.
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli182
rm -f update.sh
- 1
- Récupérer le script de checkpoint
- 2
- Avancer à l’état à l’issue de l’application appli18
- 3
- Nettoyer derrière nous
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)
Docker
utilisée
A partir de maintenant, il est nécessaire de clarifier la branche principale sur laquelle nous travaillons. Toutes les prochaines applications supposeront que vous travaillez depuis la branche main
. Si vous avez changé de branche, vous pouvez fusionner celle-ci à main
.
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. Si vous avez utilisé les scripts automatisés de checkpoint, cette gymnastique a été faite pour vous.
Les prochaines applications vont également nécessiter d’utiliser une image Docker
. Si vous avez suivi de manière linéaire cette application, votre image Docker
devrait exister depuis l’application 15 si vous avez pushé votre dépôt à ce moment là.
Néanmoins, si vous n’avez pas fait cette application, vous pouvez utiliser le checkpoint de l’application 18 et faire un git push origin main --force
(à ne pas reproduire sur vos projets!) qui devrait déclencher les opérations côté Github
pour construire et livrer votre image Docker
. Cela nécessite quelques opérations de votre côté, notamment la création d’un token Dockerhub
à renseigner en secret Github
. Pour vous refraîchir la mémoire sur le sujet, vous pouvez retourner consulter l’application 15.
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
.env
de configuration, les secrets renseignés àGithub
ou encore lerequirements.txt
relèvent 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 (Note 1). ;
- 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’imageDocker
, 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.
Ici, nous nous plaçons dans le cas simple où les données brutes reçues sont figées. Ce qui peut changer est la manière dont on constitue nos échantillons train/test. Il sera donc utile de logguer les données en question par le biais de MLFlow
. Mais il n’est pas nécessaire de versionner les données brutes.
Si celles-ci évoluaient, il pourrait être utile de versionner les données, à la manière dont on le fait pour le code. Git
n’est pas l’outil approprié pour cela. Parmi les outils populaires de versionning de données, bien intégrés avec S3
, il y a, sur le SSPCloud
, lakefs
.
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.
Lancer un service
ArgoCD
sur leSSPCloud
depuis la pageMes services
(catalogueAutomation
). Laisser les configurations par défaut.Sur
GitHub
, créer un dépôtapplication-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ôtGitOps
, dans lequel on mettra les trois fichiers de déploiement qui permettent de déployer notre application surKubernetes
(deployment.yaml
,service.yaml
,ingress.yaml
).A la racine de votre dépôt
GitOps
, créez un fichierapplication.yml
avec le contenu suivant, en prenant bien soin de modifier les lignes annotées avec des informations pertinentes :application.yaml
/v1alpha1 apiVersion: argoproj.io kind: Application metadata:-mlops name: ensae spec: project: default source:1//github.com/<your_github_username>/application-deployment.git repoURL: https:2 targetRevision: main3 path: deployment destination://kubernetes.default.svc server: https:4-<your_sspcloud_username> namespace: user syncPolicy: automated: selfHeal: true
- 1
-
L’URL de votre dépôt
Github
faisant office de dépôtGitOps
. - 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 formeuser-${username}
.
Pousser sur
Github
le dépôtGitOps
.Dans
ArgoCD
, cliquez surNew App
puisEdit as a YAML
. Copiez-collez le contenu deapplication.yml
et cliquez surCreate
.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
.Supprimer du code applicatif le dossier
deployment
puisque c’est maintenant votre dépôt de déploiement qui le contrôle.Indiquer dans le
README.md
que le déploiement de votre application (dont vous pouvez mettre l’URL dans le README) est contrôlé par un autre dépôt.
Si cela a fonctionné, vous devriez maintenant voir votre application dans votre tableau de bord ArgoCD
:
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli19a2
rm -f update.sh
- 1
- Récupérer le script de checkpoint
- 2
- Avancer à l’état à l’issue de l’application appli19a
- 3
- Nettoyer derrière nous
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ôtGitOps
, 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.
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
.github/workflows/prod.yml
name: Construction image Docker
on:
push:
branches:
- main
- dev
tags:
- 'v*.*.*'
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:
1 images: linogaliana/application
-
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 }}
- 1
- Modifier ici !
Dans le dépôt de l’application, mettre à jour le code dans
app/main.py
pour changer un élément de l’interface de votre documentation. Par exemple, mettre en gras un titre.app/main.py
= FastAPI( app ="Démonstration du modèle de prédiction de survie sur le Titanic", title= 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
.0.1 git tag v0--tags git push
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 leDockerHub
que le tagv0.0.1
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
, mettre à jour la version de l’image à déployer en production dans le fichierdeployment/deployment.yaml
Fichier deployment/deployment.yaml
deployment/deployment.yaml
deployment/deployment.yaml
/v1
apiVersion: apps
kind: Deployment
metadata:-deployment
name: titanic
labels:
app: titanic
spec:1
replicas:
selector:
matchLabels:
app: titanic
template:
metadata:
labels:
app: titanic
spec:
containers:- name: titanic
1/application:v0.0.1
image: linogaliana
ports:- containerPort: 8000
- 1
- Remplacer ici par le dépôt applicatif adéquat
- 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
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli19b2
rm -f update.sh
- 1
- Récupérer le script de checkpoint
- 2
- Avancer à l’état à l’issue de l’application appli19b
- 3
- Nettoyer derrière nous
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
.
quarto create project website mysite
- Faire remonter d’un niveau
_quarto.yml
- Supprimer
about.qmd
, déplacerindex.qmd
vers la racine de notre projet. - Remplacer le contenu de
index.qmd
par celui-ci et retirerabout.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
- 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 des lignes suivantes
terminal
--orphan gh-pages
git checkout --hard # make sure all changes are committed before running this!
git reset --allow-empty -m "Initialising gh-pages branch"
git commit -pages git push origin gh
- Revenir à votre branche principale (
main
normalement) - 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
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli202
rm -f update.sh
- 1
- Récupérer le script de checkpoint
- 2
- Avancer à l’état à l’issue de l’application appli20
- 3
- Nettoyer derrière nous
Partie 6: adopter une approche MLOps pour améliorer notre modèle
VSCode
actif avec la configuration proposée dans l'application préliminaire, vous pouvez repartir de ce service:Et ensuite, après avoir clôné le dépôt
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli202
rm -f update.sh
- 1
- Récupérer le script de checkpoint
- 2
- Avancer à l’état à l’issue de l’application appli20
- 3
- Nettoyer derrière nous
Maintenant que nous avons tout préparé pour mettre à disposition rapidement un modèle, nous pouvons revenir en arrière pour améliorer ce modèle. Pour cela, nous allons mettre en oeuvre une validation croisée.
Le problème que nous allons rencontrer va être que nous voudrions facilement tracer les évolutions de notre modèle, la qualité prédictive de celui-ci dans différentes situations. Il s’agira d’à nouveau mettre en place du logging mais, cette fois, de suivre la qualité du modèle et pas seulement s’il fonctionne. L’outil MLFlow
va répondre à ce problème et va, au passage, fluidifier la mise à disposition du modèle de production, c’est-à-dire de celui qu’on désire mettre à disposition du public.
Revenir sur le code d’entraînement du modèle pour faire de la validation croisée
Pour pouvoir faire ceci, il va falloir changer un tout petit peu notre code applicatif dans sa phase d’entraînement.
- Faire les modifications suivantes pour restructurer notre pipeline afin de mieux distinguer les étapes d’estimation et d’évaluation
Modification de train.py
pour faire une grid search
train.py
"""
Prediction de la survie d'un individu sur le Titanic
"""
import os
from dotenv import load_dotenv
import argparse
from loguru import logger
from joblib import dump
import pathlib
import pandas as pd
from sklearn.model_selection import train_test_split, GridSearchCV
from src.pipeline.build_pipeline import create_pipeline
from src.models.train_evaluate import evaluate_model
# ENVIRONMENT CONFIGURATION ---------------------------
"recording.log", rotation="500 MB")
logger.add(
load_dotenv()
= argparse.ArgumentParser(description="Paramètres du random forest")
parser
parser.add_argument("--n_trees", type=int, default=20, help="Nombre d'arbres"
)= parser.parse_args()
args
= "https://minio.lab.sspcloud.fr/lgaliana/ensae-reproductibilite/data/raw/data.csv"
URL_RAW
= args.n_trees
n_trees = os.environ.get("JETON_API", "")
jeton_api = os.environ.get("data_path", URL_RAW)
data_path = os.environ.get("train_path", "data/derived/train.parquet")
data_train_path = os.environ.get("test_path", "data/derived/test.parquet")
data_test_path = None
MAX_DEPTH = "sqrt"
MAX_FEATURES
if jeton_api.startswith("$"):
"API token has been configured properly")
logger.info(else:
"API token has not been configured")
logger.warning(
# IMPORT ET STRUCTURATION DONNEES --------------------------------
= pathlib.Path("data/derived/")
p =True, exist_ok=True)
p.mkdir(parents
= pd.read_csv(data_path)
TrainingData
= TrainingData["Survived"]
y = TrainingData.drop("Survived", axis="columns")
X
= train_test_split(
X_train, X_test, y_train, y_test =0.1
X, y, test_size
)= 1).to_parquet(data_train_path)
pd.concat([X_train, y_train], axis = 1).to_parquet(data_test_path)
pd.concat([X_test, y_test], axis
# PIPELINE ----------------------------
# Create the pipeline
= create_pipeline(
pipe =MAX_DEPTH, max_features=MAX_FEATURES
n_trees, max_depth
)
= {
param_grid "classifier__n_estimators": [10, 20, 50],
"classifier__max_leaf_nodes": [5, 10, 50],
}
= GridSearchCV(
pipe_cross_validation
pipe,=param_grid,
param_grid=["accuracy", "precision", "recall", "f1"],
scoring="f1",
refit=5,
cv=5,
n_jobs=1,
verbose
)
pipe_cross_validation.fit(X_train, y_train)
= pipe_cross_validation.best_estimator_
pipe
# ESTIMATION ET EVALUATION ----------------------
pipe.fit(X_train, y_train)
with open("model.joblib", "wb") as f:
dump(pipe, f)
# Evaluate the model
= evaluate_model(pipe, X_test, y_test)
score, matrix
f"{score:.1%} de bonnes réponses sur les données de test pour validation")
logger.success(20 * "-")
logger.debug("Matrice de confusion")
logger.info( logger.debug(matrix)
- Dans le code de l’API (
app/api.py
), changer la version du modèle mis en oeuvre en “0.2” (dans la fonctionshow_welcome_page
) - Après avoir committé cette nouvelle version du code applicatif, tagguer ce dépôt avec le tag
v0.0.2
- Modifier
deployment/deployment.yaml
dans le codeGitOps
pour utiliser ce tag.
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli212
rm -f update.sh
- 1
- Récupérer le script de checkpoint
- 2
- Avancer à l’état à l’issue de l’application appli21
- 3
- Nettoyer derrière nous
Garder une trace des entraînements de notre modèle grâce au register de MLFlow
VSCode
actif avec la configuration proposée dans l'application préliminaire, vous pouvez repartir de ce service:Et ensuite, après avoir clôné le dépôt
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli212
rm -f update.sh
- 1
- Récupérer le script de checkpoint
- 2
- Avancer à l’état à l’issue de l’application appli21
- 3
- Nettoyer derrière nous
Enregistrer nos premiers entraînements
MLFlow
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 formehttps://user-{username}.user.lab.sspcloud.fr
. Ce serviceMLFlow
communiquera avec lesVSCode
que vous ouvrirez ultérieurement à partir de cet URL ainsi qu’avec le système de stockageS3
18.Regarder la page
Experiments
. Elle ne contient queDefault
à ce stade, c’est normal.
Une fois le service
MLFlow
fonctionnel, lancer un nouveauVSCode
pour bénéficier de la connexion automatique entre les services interactifs du SSPCloud et les services d’automatisation commeMLFlow
.Clôner votre projet, vous situer sur la branche de travail.
Dans la section de passage des paramètres de notre ligne de commande, introduire ce morceau de code:
= argparse.ArgumentParser(description="Paramètres du random forest")
parser
parser.add_argument("--n_trees", type=int, default=20, help="Nombre d'arbres"
)
parser.add_argument("--experiment_name", type=str, default="titanicml", help="MLFlow experiment name"
)= parser.parse_args() args
- A la fin du script
train.py
, ajouter le code suivant
Code à ajouter
fin de train.py
# LOGGING IN MLFLOW -----------------
= os.getenv("MLFLOW_TRACKING_URI")
mlflow_server
f"Saving experiment in {mlflow_server}")
logger.info(
mlflow.set_tracking_uri(mlflow_server)
mlflow.set_experiment(args.experiment_name)
= mlflow.data.from_pandas(
input_data_mlflow =data_path, name="Raw dataset"
TrainingData, source
)= mlflow.data.from_pandas(
training_data_mlflow =1), source=data_path, name="Training data"
pd.concat([X_train, y_train], axis
)
with mlflow.start_run():
# Log datasets
="raw")
mlflow.log_input(input_data_mlflow, context="raw")
mlflow.log_input(training_data_mlflow, context
# Log parameters
"n_trees", n_trees)
mlflow.log_param("max_depth", MAX_DEPTH)
mlflow.log_param("max_features", MAX_FEATURES)
mlflow.log_param(
# Log best hyperparameters from GridSearchCV
= pipe_cross_validation.best_params_
best_params for param, value in best_params.items():
mlflow.log_param(param, value)
# Log metrics
"accuracy", score)
mlflow.log_metric(
# Log confusion matrix as an artifact
= "confusion_matrix.txt"
matrix_path with open(matrix_path, "w") as f:
str(matrix))
f.write(
mlflow.log_artifact(matrix_path)
# Log model
"model") mlflow.sklearn.log_model(pipe,
Ajouter
mlruns/*
dans.gitignore
Tester
train.py
en ligne de commandeObserver 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
dontMLFlow
a fait l’inférence, etc.)Observer le code proposé par
MLFlow
pour récupérer le run en question. Tester celui-ci dans un notebook sur le fichier intermédiaire de test au formatParquet
En ligne de commande, faites tourner pour une autre valeur de
n_trees
. Retourner à la liste des runs en cliquant à nouveau sur “titanicml” dans les expérimentationsDans l’onglet
Table
, sélectionner plusieurs expérimentations, cliquer surColumns
et ajouter la statistique d’accuracy. Ajuster la taille des colonnes pour la voir et classer les modèles par score décroissantsCliquer sur
Compare
après en avoir sélectionné plusieurs. Afficher un scatterplot des performances en fonction du nombre d’estimateurs. Conclure.Ajouter
mlflow
aurequirements.txt
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli222
rm -f update.sh
- 1
- Récupérer le script de checkpoint
- 2
- Avancer à l’état à l’issue de l’application appli22
- 3
- Nettoyer derrière nous
Cette appplication illustre l’un des premiers apports de MLFlow
: on garde une trace de nos expérimentations: le modèle est archivé avec les paramètres et des métriques de performance. On peut donc retrouver de plusieurs manières un modèle qui nous avait tapé dans l’oeil.
Néanmoins, persistent un certain nombre de voies d’amélioration dans notre pipeline.
- On entraîne le modèle en local, de manière séquentielle, et en lançant nous-mêmes le script
train.py
. - Pis encore, à l’heure actuelle, cette étape d’estimation n’est pas séparée de la mise à disposition du modèle par le biais de notre API. On archive des modèles mais on les utilise pas ultérieurement.
Les prochaines applications permettront d’améliorer ceci.
Consommation d’un modèle archivé sur MLFlow
A l’heure actuelle, notre pipeline est linéaire:
Ceci nous gêne pour faire évoluer notre modèle: on ne dissocie pas ce qui relève de l’entraînement du modèle de son utilisation. Un pipeline plus cyclique permettra de mieux dissocier l’expérimentation de la production:
Si vous avez entraîné plusieurs modèles avec des
n_trees
différents, utiliser l’interface deMLFlow
pour sélectionner le “meilleur”. Cliquer sur le modèle en question et faire l’action “Register Model”. L’enregistrer comme le modèle de “production”Rendez-vous sur l’onglet
Models
et observez cet entrepôt de modèles. Cliquez sur le modèle de production. Vous pourrez par ce biais suivre ses différentes versions.Ouvrir un notebook temporaire et observer le résultat.
Exemple de code à tester
import mlflow
import pandas as pd
= "production"
model_name = "latest"
model_version
# Load the model from the Model Registry
= f"models:/{model_name}/{model_version}"
model_uri = mlflow.sklearn.load_model(model_uri)
logged_model
# GENERATE PREDICTION DATA ---------------------
def create_data(
str = "female",
sex: float = 29.0,
age: float = 16.5,
fare: str = "S",
embarked: -> str:
) """
"""
= pd.DataFrame(
df
{"Sex": [sex],
"Age": [age],
"Fare": [fare],
"Embarked": [embarked],
}
)
return df
= pd.concat(
data =40), create_data(sex="male")]
[create_data(age
)
# PREDICTION ---------------------
logged_model.predict(pd.DataFrame(data))
- On va adapter le code applicatif de notre API pour tenir compte de ce modèle de production.
Voir le script app/api.py
proposé
"""A simple API to expose our trained RandomForest model for Tutanic survival."""
from fastapi import FastAPI
import mlflow
import pandas as pd
# Preload model -------------------
= "production"
model_name = "latest"
model_version
# Load the model from the Model Registry
= f"models:/{model_name}/{model_version}"
model_uri = mlflow.sklearn.load_model(model_uri)
model
# Define app -------------------------
= FastAPI(
app ="Prédiction de survie sur le Titanic",
title=
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.3",
}
@app.get("/predict", tags=["Predict"])
async def predict(
str = "female",
sex: float = 29.0,
age: float = 16.5,
fare: str = "S"
embarked: -> str:
) """
"""
= pd.DataFrame(
df
{"Sex": [sex],
"Age": [age],
"Fare": [fare],
"Embarked": [embarked],
}
)
= "Survived 🎉" if int(model.predict(df)) == 1 else "Dead ⚰️"
prediction
return prediction
Les changements principaux de ce code sont:
- on va chercher le modèle de production
- on met à jour la version de notre API pour signaler à nos clients que celle-ci a évolué
- On va retirer l’entraînement de la séquence d’opération du
api/run.sh
. En supprimant la ligne relative à l’entraînement du modèle, vous devriez avoir
#/bin/bash
--host "0.0.0.0" uvicorn app.api:app
Mettons en production cette nouvelle version. Cela implique de faire les gestes suivants:
Commit de ce changement dans
main
Publier un tag
v0.0.3
pour le code applicatifMettre à jour notre manifeste dans le dépôt
GitOps
.- En premier lieu, il faut changer la version de référence pour utiliser le tag
v0.0.3
. - De plus, il faut déclarer la variable d’environnement
MLFLOW_TRACKING_URI
qui indique àPython
l’entrepôt de modèles où aller chercher celui en production. La bonne pratique est de définir ceci hors du code, dans un fichier de configuration donc, ce qui est l’objet de notre manifestedeployment.yaml
. On peut donc changer de cette manière ce fichier:
- En premier lieu, il faut changer la version de référence pour utiliser le tag
Le modèle deployment.yaml
proposé
/v1
apiVersion: apps
kind: Deployment
metadata:-deployment
name: titanic
labels:
app: titanic
spec:1
replicas:
selector:
matchLabels:
app: titanic
template:
metadata:
labels:
app: titanic
spec:
containers:- name: titanic
1/application:v0.0.3
image: linogaliana
ports:- containerPort: 8000
env:- name: MLFLOW_TRACKING_URI
2//user-${USERNAME}-mlflow.user.lab.sspcloud.fr
value: https:
resources:
limits:"2Gi"
memory: "1000m" cpu:
- 1
- Le tag de notre code applicatif
- 2
-
La variable d’environnement à adapter en fonction de l’adresse du dépôt demodèles utilisé. Remplacer par votre URL
MLFlow
.
- Pour s’assurer que l’application fonctionne bien, on peut aller voir les logs de la machine qui fait tourner notre code. Pour ça, faire
kubectl get pods
et, en supposant que votre service soit nommétitanic
dans vos fichiers YAML de configuration, récupérer le nom commençant partitanic-deployment-*
et fairekubectl logs titanic-deployment-*
terminal
curl -sSL https://raw.githubusercontent.com/ensae-reproductibilite/website/refs/heads/main/chapters/applications/overwrite.sh -o update.sh && chmod +x update.sh
./update.sh appli232
rm -f update.sh
- 1
- Récupérer le script de checkpoint
- 2
- Avancer à l’état à l’issue de l’application appli23
- 3
- Nettoyer derrière nous
A ce stade, nous avons amélioré la fiabilité de notre application car nous utilisons le meilleur modèle. Néanmoins, nos entraînements sont encore manuels. Là encore il y a des gains possibles car cela paraît pénible à la longue de devoir systématiquement relancer des entraînements manuellement pour tester des variations de tel ou tel paramètre. Heureusement, nous allons pouvoir automatiser ceci également.
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.
Ce travail nous amène de l’approche pipeline à mi chemin entre data science et data engineering. Il existe plusieurs outils pour faire ceci, généralement issus de la sphère du data engineering. L’outil le plus complet sur le SSPCloud
, bien intégré à l’écosystème Kubernetes
, est Argo Workflows
19.
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.
Nous allons construire, dans les deux prochaines applications, un pipeline simple prenant cette forme20:

Argo Workflows

Github Actions
L’outil permettant une intégration native de notre pipeline dans l’infrastructure cloud (SSPCloud
) que nous avons utilisée jusqu’à présent est Argo Workflows
. Néanmoins, pour illustrer la modularité de notre chaîne, permise par l’adoption de Docker
, nous allons montrer que les serveurs d’intégration continue de Github
peuvent très bien servir d’environnement d’exécution, sans rien perdre de ce que nous avons mis en oeuvre précédemment (logging des modèles dans MLFlow
, récupération de données depuis S3
, etc.)
Argo Workflow
A l’heure actuelle, notre entraînement ne dépend que d’un hyperparamètre fixé à partir de la ligne de commande: n_trees
. Nous allons commencer par ajouter un argument à notre chaine de production (code applicatif):
- Dans
train.py
, dans la section relative au parsing de nos arguments, ajouter ce bout de code
parser.add_argument("--max_features",
type=str, default="sqrt",
=['sqrt', 'log2'],
choiceshelp="Number of features to consider when looking for the best split"
)
et remplacer la définition de MAX_FEATURES
par l’argument fourni en ligne de commande:
= args.max_features MAX_FEATURES
Faire un commit, taguer cette version (
v0.0.4
) et pusher le tagMaintenant, dans le dépôt
GitOps
, créer un fichierargo-workflow/manifest.yaml
Le modèle proposé
/v1alpha1
apiVersion: argoproj.io
kind: Workflow
metadata:-training-workflow-
generateName: titanic-lgaliana
namespace: user
spec:
entrypoint: main
serviceAccountName: workflow
arguments:
parameters:# The MLflow tracking server is responsible to log the hyper-parameter and model metrics.
- name: mlflow-tracking-uri
1//user-${USERNAME}-mlflow.user.lab.sspcloud.fr/
value: https:- name: mlflow-experiment-name
2
value: titanicml- name: model-training-conf-list
|
value:
["n_trees": 10, "max_features": "log2" },
{ "n_trees": 20, "max_features": "sqrt" },
{ "n_trees": 20, "max_features": "log2" },
{ "n_trees": 50, "max_features": "sqrt" }
{
]
templates:# Entrypoint DAG template
- name: main
dag:
tasks:# Task 0: Start pipeline
- name: start-pipeline
-pipeline-wt
template: start# Task 1: Train model with given params
- name: train-model-with-params
-pipeline ]
dependencies: [ start-model-training-wt
template: run
arguments:
parameters:- name: max_features
"{{item.max_features}}"
value: - name: n_trees
"{{item.n_trees}}"
value: # Pass the inputs to the task using "withParam"
"{{workflow.parameters.model-training-conf-list}}"
withParam: # Now task container templates are defined
# Worker template for task 0 : start-pipeline
- name: start-pipeline-wt
inputs:
container:
image: busybox-c ]
command: [ sh, "echo Starting pipeline" ]
args: [ # Worker template for task-1 : train model with params
- name: run-model-training-wt
inputs:
parameters:- name: n_trees
- name: max_features
container:3****/application:v0.0.4
image:
imagePullPolicy: Always-c]
command: [sh,
args: ["python3 train.py --n_trees={{inputs.parameters.n_trees}} --max_features={{inputs.parameters.max_features}}"
]
env:- name: MLFLOW_TRACKING_URI
"{{workflow.parameters.mlflow-tracking-uri}}"
value: - name: MLFLOW_EXPERIMENT_NAME
"{{workflow.parameters.mlflow-experiment-name}}"
value: - name: AWS_DEFAULT_REGION
-east-1
value: us- name: AWS_S3_ENDPOINT
value: minio.lab.sspcloud.fr
- 1
- Changer pour votre entrepot de modèle
- 2
-
Le nom de l’expérimentation
MLFLow
dont nous allons avoir besoin (on propose de continuer surtitanicml
) - 3
-
Changer l’image
Docker
ici
- Observer l’UI d’
Argo Workflow
dans vos services ouverts du SSPCloud. Vous devriez retrouver Figure 3 (a) dans celle-ci.
Nous pouvons maintenant passer à la version Github
. Celle-ci est optionnelle car elle vient surtout démontrer l’intérêt d’avoir une chaine modulaire et la dissociation que cela permet entre l’environnement d’exécution et les autres environnements nécessaires à notre chaine (notamment le stockage code et le logging).
Github Actions
comme ordonnanceur
Pour que Github
sache où aller chercher MLFlow
et S3
et comment s’y identifier, il va falloir lui donner un certain de variables d’environnement. Il est hors de question de mettre celles-ci dans le code. Heureusement, Github
propose la possibilité de renseigner des secrets: nous allons utiliser ceux-ci.
Aller dans les paramètres de votre projet
GitOps
et dans la sectionSecrets and variables
Vous allez avoir besoin de créer les secrets suivants:
MLFLOW_TRACKING_PASSWORD
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
AWS_SESSION_TOKEN
Les valeurs à renseigner sont à récupérer à différents endroits:
- Pour les secrets liés à
S3
(AWS_*
), ceux-ci sont dans l’espace Mon compte duSSPCloud
. Ils ont une durée de validité limitée: si vous devez refaire tourner le code dans quelques jours, il faudra les mettre à jour (ou passer par un compte de service comme indiqué précédemment) - Le mot de passe de
MLFlow
est dans leREADME
de votre service, qui s’affiche quand vous cliquez sur le boutonOuvrir
depuis la page Mes services
- Reprendre ce modèle d’action à mettre dans votre dépôt
GitOps
(.github/workflows/train.yaml
par exemple).
Modèle d’action Github
name: Titanic Model Training
on:
push:
branches:- main
workflow_dispatch:
jobs:-pipeline:
start-on: ubuntu-latest
runs
steps:- name: Start Pipeline
"Starting pipeline"
run: echo
-model:
train-pipeline
needs: start-on: ubuntu-latest
runs
strategy:
matrix:-config:
model- { n_trees: 10, max_features: "log2" }
- { n_trees: 20, max_features: "sqrt" }
- { n_trees: 20, max_features: "log2" }
- { n_trees: 50, max_features: "sqrt" }
container:1***/application:v0.0.4
image:
env:"https://user-lgaliana-mlflow.user.lab.sspcloud.fr/"
MLFLOW_TRACKING_URI: "titanicml"
MLFLOW_EXPERIMENT_NAME: "${{ secrets.MLFLOW_TRACKING_PASSWORD }}"
MLFLOW_TRACKING_PASSWORD: "us-east-1"
AWS_DEFAULT_REGION: "minio.lab.sspcloud.fr"
AWS_S3_ENDPOINT: "${{ secrets.AWS_ACCESS_KEY_ID }}"
AWS_ACCESS_KEY_ID: "${{ secrets.AWS_SECRET_ACCESS_KEY }}"
AWS_SECRET_ACCESS_KEY: "${{ secrets.AWS_SESSION_TOKEN }}"
AWS_SESSION_TOKEN:
steps:- name: Checkout Repository
/checkout@v4
uses: actionswith:
2'ensae-reproductibilite/application'
repository:
ref: appli24
- name: Train Model
|
run: --n_trees=${{ matrix.model-config.n_trees }} --max_features=${{ matrix.model-config.max_features }} python3 train.py
- 1
-
Mettre votre image ici. Si vous n’en avez pas, vous pouvez mettre
linogaliana/application:v0.0.4
- 2
- On reprend le code applicatif de l’application précédente. Vous pouvez remplacer par votre dépôt et une référence adaptée si vous préférez
- Pusher et observer l’UI de
Github
depuis l’ongletActions
. Vous devriez retrouver Figure 3 (b) dans celle-ci.
Pour aller plus loin
Nous n’avons géré qu’une partie du cycle de vie d’un projet data, à savoir l’entraînement et la mise à disposition d’un modèle. Comme nous partions de très long, ce sprint avait plutôt l’allure d’un marathon. Néanmoins, nous avons maintenant un projet très flexible qui pourrait permettre d’aller plus loin et d’intégrer d’autres aspects du cycle de vie d’un projet data.
En premier lieu, pour suivre la vie d’un modèle en production (enjeu de l’observabilité), il faut collecter de nouvelles données annotées. Si cette collecte n’est pas naturelle (de nouveaux enregistrement du label sont automatiques), il faut généralement mettre en oeuvre de l’annotation humaine ou des feedbacks. Parmi les outils disponibles sur le SSPCloud
pour cela, il existe Label Studio
.
En second lieu, on a encore une chaîne de production où peu de profils non tech peuvent s’insérer. On pourrait vouloir la rendre plus accessible: soit en améliorant notre output site web, soit en mettant en oeuvre des dashboards plus orientés BI, pensés pour des profils moins techniques mais ayant une place dans la chaîne de valeur data comme les data analysts. Pour cela, il existe des outils sur le SSPCloud
comme Apache Superset
.
Gardons aussi à l’esprit que notre chaine dépend de données tabulaires simples avec un objectif assez modeste: mettre à disposition un modèle de classification dans un cadre supervisé. Si on avait un use case différent ou des données plus complexes - par exemple des remontées de données en temps réel ou encore des données textuelles - nous adopterions probablement un pipeline ou des briques technologiques différents à certaines étapes. Néanmoins, la philosophie serait probablement la même : avoir une chaine modulaire, avec les outils technologiques les plus efficaces à chaque maillon, et Python
et Kubernetes
pour, dans les ténèbres, les lier.
Footnotes
Il y a quelques différences entre le VSCode server mis à disposition sur le SSPCloud et la version desktop sur laquelle s’appuient beaucoup de ressources. A quelques extensions prêts (Data Wrangler, Copilot), les différences sont néanmoins minimes.↩︎
L’export dans un script
.py
a été fait directement depuisVSCode
. 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.↩︎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 deVSCode
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 packageos
mais peut-être allez vous découvrir ultérieurement qu’il y a de meilleures pratiques…↩︎Essayez de commit vos changements à chaque étape de l’exercice, c’est une bonne habitude à prendre.↩︎
Il est normal d’avoir des dossiers
__pycache__
qui traînent en local : ils se créent automatiquement à l’exécution d’un script enPython
. Néanmoins, il ne faut pas associer ces fichiers àGit
, voilà pourquoi on les ajoute au.gitignore
.↩︎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 peut être utle de l’adopter car certains frameworks, à commencer par les pipelines
scikit
, exigeront certaines classes et méthodes si vous désirez brancher des objets ad hoc à ceux-ci.↩︎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 surGithub
. 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.↩︎Alors oui, c’est vrai,
s3
se distingue d’un système de fichiers classiques comme on peut le lire dans certains posts énervés sur la question (par exemple sur Reddit). Mais du point de vue de l’utilisateurPython
plutôt que de l’architecte cloud, on va avoir assez peu de différence avec un système de fichier local. C’est pour le mieux, cela réduit la difficulté à rentrer dans cette technologie.↩︎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 surGithub
. Le packagevulture
est très pratique pour diagnostiquer les morceaux de code inutiles dans un projet.↩︎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.↩︎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 dePython
plus finement par le biais deDocker
.↩︎L’option
-c
passée après la commandepython
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.↩︎L’option
-c
passée après la commandepython
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.↩︎Pour comparer les deux listes, vous pouvez utiliser la fonctionnalité de split du terminal sur
VSCode
pour comparer les outputs deconda env export
en les mettant en face à face.↩︎L’option
-c
passée après la commandepython
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.↩︎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.↩︎
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 chaquecommit
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↩︎
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 interactifsVSCode
. Il faut donc, par défaut, supprimer et rouvrir un serviceMLFLow
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.↩︎Il existe d’autres outils d’ordonnancement de pipelines très utilisés dans l’industrie, notamment
Airflow
.Ce dernier est plus utilisé, en pratique, qu’
Argo Workflow
mais, même s’il est disponible sur leSSPCloud
aussi, est moins pensé autour deKubernetes
que l’estArgo
.Pour mieux comprendre la différence entre
↩︎Argo
etAirflow
, la philosphie différente de ces deux outils et leurs avantages comparatifs, cette courte vidéo est intéressante:Il serait bien sûr possible d’aller beaucoup plus loin dans la définition du pipeline.
Par exemple, il est possible, si le framework utilisé pour la modélisation n’intègre pas la notion de pipeline au niveau de
Python
de faire ceci au niveau d’Argo
. Cela donnerait un pipeline prenant cette forme:Néanmoins, ici, nous utilisons
Scikit
qui permet d’intégrer le preprocessing comme une étape de modélisation. Nous n’avons donc pas d’intérêt à définir ceci comme une tâche autonome, raison pour laquelle notre pipeline apparaît plus simple.↩︎
Comment gérer les checkpoints ?
Pour simplifier la reprise en cours de ce fil rouge, nous proposons un système de checkpoints qui s’appuient sur des tags
Git
. Ces tags figent le projet tel qu’il est à l’issue d’un exercice donné.Si vous faites évoluer votre projet de manière expérimentale mais désirez tout de même utiliser à un moment ces checkpoints, il va falloir faire quelques acrobaties
Git
. Pour cela, nous mettons à disposition un script qui permet de sauvegarder votre avancée dans un tag donné (au cas où, à un moment, vous vouliez revenir dessus) et écraser la branchemain
avec le tag en question. Par exemple, si vous désirez reprendre après l’exercice 9, vous devrez faire tourner le code dans cette boite :Celui-ci sauvegarde votre avancée dans un tag nommé
dev_before_appli9
, le pousse sur votre dépôtGithub
puis force votre branche à adopter l’état du tagappli9
.