Aller au contenu principal

Module 7 — Chaînes d'intégration et de livraison continues

L'équipe d'Alice fait maintenant 12 commits par semaine sur le dépôt du modèle. Sans chaîne d'intégration, chaque commit est une invitation à casser silencieusement le pipeline. Ce module met en place les tests qui comptent pour un projet d'apprentissage, puis les assemble dans une chaîne GitHub Actions concrète.

Ce qu'il faut tester dans un projet MLOps

Les tests unitaires classiques (pytest) restent utiles : chaque fonction de transformation de variables, chaque adaptateur de base doit avoir son test. Mais deux familles de tests spécifiques à l'apprentissage distinguent une chaîne MLOps d'une chaîne logicielle ordinaire.

Les tests de données. Ils protègent le pipeline de l'arrivée d'un jeu qui ne ressemble plus à ce qu'on attend :

import pandera as pa
from pandera.typing import Series

class SchemaAbonne(pa.DataFrameModel):
id_abonne: Series[str] = pa.Field(unique=True)
age: Series[int] = pa.Field(ge=18, le=110)
anciennete_mois: Series[int] = pa.Field(ge=0, le=600)
forfait: Series[str] = pa.Field(isin=["A", "B", "C", "D"])
taux_utilisation: Series[float] = pa.Field(ge=0.0, le=1.0)

Ce schéma refuse un âge de 250, un forfait inconnu, un identifiant en double, un taux d'utilisation à 3,7. Ces valeurs sont rares mais elles tombent, et sans schéma elles empoisonnent l'entraînement en silence.

Les tests de modèle avec seuils. Ils protègent la production d'une régression sournoise :

def test_auc_sur_validation_figee():
modele = charger("candidat")
x, y = charger_validation_figee()
auc = roc_auc_score(y, modele.predict_proba(x)[:, 1])
assert auc >= 0.80, f"AUC {auc:.3f} sous le seuil de 0.80"

def test_pas_de_regression_par_tranche():
modele = charger("candidat")
for segment in ["pro", "prepaye", "nouveau"]:
auc = auc_sur_segment(modele, segment)
assert auc >= AUC_CHAMPION[segment] - 0.02, (
f"regression de {AUC_CHAMPION[segment] - auc:.3f} sur {segment}"
)

Ces tests ne remplacent pas l'expérimentation : ils garantissent qu'aucun commit ne dégrade tacitement le modèle en dessous de seuils sur lesquels l'équipe s'est engagée.

Une chaîne GitHub Actions concrète

Un fichier .github/workflows/mlops.yml orchestre les étapes :

name: mlops

on:
push:
branches: [main]
pull_request:

jobs:
qualite:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v3
- run: uv sync --frozen
- run: uv run ruff check .
- run: uv run pytest tests/unitaires -q

donnees_et_modele:
needs: qualite
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: iterative/setup-dvc@v1
- run: dvc pull data/validation_figee.parquet
- run: uv run pytest tests/donnees -q
- run: uv run pytest tests/modele -q

image:
needs: donnees_et_modele
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
with:
push: true
tags: ghcr.io/mon-org/churn:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
- run: trivy image --exit-code 1 --severity HIGH,CRITICAL ghcr.io/mon-org/churn:${{ github.sha }}

deployer_preprod:
needs: image
runs-on: ubuntu-latest
environment: preproduction
steps:
- run: ./deploiement/appliquer.sh preprod ${{ github.sha }}

deployer_prod:
needs: deployer_preprod
runs-on: ubuntu-latest
environment:
name: production
url: https://churn.exemple.com
steps:
- run: ./deploiement/appliquer.sh prod ${{ github.sha }}

L'ordre importe : la qualité (linting, tests unitaires) est peu coûteuse et attrape 80 % des erreurs ; on ne fait pas tourner les tests de modèle si le linting est cassé. La construction d'image n'a lieu que sur main et déclenche un scan trivy qui échoue en cas de faille critique.

L'approbation manuelle qui protège la production

Le déclencheur environment: production de GitHub Actions porte une règle : « ce job ne démarre qu'après approbation d'une personne autorisée ». Cette pause obligée n'est pas un frein bureaucratique ; c'est la barrière qui empêche qu'une chaîne verte pousse en production un modèle dont personne n'a réellement lu la fiche.

L'approbateur consulte :

  • Le résumé des métriques de la nouvelle version dans MLflow.
  • La comparaison par tranches vs. la version champion actuelle.
  • Le journal du scan de sécurité.
  • La note de version rédigée par l'auteur du commit.

S'il approuve, l'alias champion bascule automatiquement (module 5) et le service redémarre.

Cache, parallélisation et coût

Trois techniques rendent la chaîne rapide sans la dénaturer :

  • Cache du gestionnaire de paquets (actions/setup-python avec cache: pip ou setup-uv). Passe l'installation de 2 minutes à 15 secondes.
  • Cache Docker Buildx (cache-from: type=gha). Une image dont seule la couche du modèle change se reconstruit en 20 secondes.
  • Jobs parallèles quand ils sont indépendants (qualite et donnees_et_modele peuvent s'exécuter en parallèle).

Une chaîne qui prend une heure sera contournée par l'équipe (« je rebasculerai plus tard »). Une chaîne qui prend cinq minutes devient une seconde nature.

Les tests de modèle ne sont pas les tests unitaires

Un test de modèle est stochastique : il compare une métrique à un seuil, il ne compare pas une sortie à une valeur attendue. Il faut donc fixer les graines (module 2) pour qu'un même commit donne toujours le même verdict, et laisser une marge (AUC_CHAMPION - 0.02) pour absorber le bruit résiduel. Un seuil trop serré crée une chaîne rouge intermittente que l'équipe apprend à ignorer.

En résumé

  • Testez les données avec un schéma (pandera), le modèle avec des seuils par tranche, et gardez les tests unitaires classiques du code.
  • Structurez la chaîne en jobs dépendants : qualité, données et modèle, image scannée, préproduction, production.
  • Exigez une approbation humaine avant la production ; elle protège contre les cas que le code ne peut pas juger.
  • Cachez ce qui peut l'être ; une chaîne rapide est une chaîne suivie.

Le module 8 choisit entre trois manières de servir le modèle : par lots, en ligne ou en flux, selon la latence exigée par le cas d'usage.