YELY_AI_Module / docs /MONITORING.md
danielxdata's picture
Ajoute le suivi des echecs et la boucle de correction pompiste
3020394
|
Raw
History Blame Contribute Delete
13.2 kB

Monitoring et apprentissage continu (monitoring/)

Deux besoins distincts couverts ici : savoir comment le modèle se comporte en production (suivi des performances), et transformer les erreurs observées en nouvelles données d'entraînement (apprentissage continu). Aucun des deux ne modifie le comportement de /analyze — ce sont des composants qui lisent/complètent les traces déjà produites par l'API.

1. Suivi des performances

Chaque appel à POST /analyze écrit déjà une ligne JSON dans logs/api.log (requête sans l'image + réponse complète + model_version). monitoring/metrics.py parse ce fichier et calcule, sans état en mémoire ni base de données :

  • total_requests : nombre d'appels journalisés.
  • success_rate : proportion de réponses success=true.
  • avg_confidence_score : moyenne des confidence_score renvoyés.
  • blocking_causes : histogramme des message pour les réponses bloquées (utile pour voir si le facteur limitant en démo est le flou, l'incohérence, ou la confiance — voir docs/LIMITATIONS.md).
  • image_quality_distribution : histogramme des image_quality observés.

Deux façons de le lire :

# en ligne de commande, pour un rapport texte
cd yely_ai_module
python -m monitoring.metrics

# via l'API elle-même, pendant la démo
curl http://127.0.0.1:8000/metrics

GET /metrics renvoie le même dictionnaire que compute_metrics(), sans paramètre — il relit logs/api.log à chaque appel, donc reflète l'état courant sans redémarrer le serveur.

web/stats.html est un tableau de bord minimal pour la soutenance : il appelle GET /metrics (URL de l'API dérivée de celle configurée dans web/index.html, stockée dans le même localStorage) et affiche requêtes totales, taux de succès, confiance moyenne, répartition qualité image et causes de blocage, avec auto-rafraîchissement toutes les 15s. Nécessite que allow_methods inclue GET dans le middleware CORS de app/main.py (sinon le navigateur bloque la lecture de la réponse, même si l'appel aboutit côté serveur — piège déjà rencontré avec /analyze en POST).

model_version (variable d'environnement MODEL_VERSION, valeur par défaut crnn_v2_70.2pct) est ajouté à chaque entrée de log — pas à la réponse publique de /analyze, pour ne pas polluer le contrat API §13 avec un champ technique. Il permet de comparer logs/api.log avant/après un ré-entraînement en filtrant par version, une fois plusieurs modèles utilisés en séquence (voir §3).

2. Boucle d'apprentissage continu (monitoring/feedback.py)

Principe

Chaque photo reçue par /analyze est déjà sauvegardée dans photos/<transaction_id>.jpg et référencée dans la réponse (photo_reference). Cette boucle réutilise cette photo pour transformer une lecture erronée, corrigée a posteriori par un pompiste ou un contrôle YELY, en nouvelle donnée d'entraînement — sans jamais toucher au dataset pendant qu'un entraînement est en cours (leçon retenue d'un crash réel, voir docs/TRAINING.md).

Étape 1 — collecte : POST /feedback

curl -X POST http://127.0.0.1:8000/feedback \
  -F "photo_reference=3f2a1c9e-....jpg" \
  -F "corrected_prix=10000" \
  -F "corrected_volume=14.28" \
  -F "corrected_prix_litre=700" \
  -F "corrected_by=pompiste-7"
  • photo_reference (requis) : valeur renvoyée par /analyze dans la réponse d'origine — l'API vérifie que la photo existe encore dans photos/ avant d'accepter la correction (404 sinon).
  • Au moins un des trois champs corrected_prix / corrected_volume / corrected_prix_litre est requis ; les champs non fournis restent inconnus (pas déduits automatiquement).
  • Chaque correction est ajoutée en une ligne à monitoring/feedback.jsonl (append-only, jamais réécrit sur cet endpoint) via monitoring.feedback.record_feedback.

En pratique (démo/soutenance), pas besoin de curl : web/stats.html liste les transactions bloquées (GET /failures, 20 dernières, avec la photo servie par GET /photos/{photo_reference}) avec un bouton « Corriger » par entrée, qui ouvre web/feedback.html?ref=<photo_reference> — la référence est déjà pré-remplie, la photo s'affiche automatiquement, il ne reste qu'à saisir les valeurs correctes et valider. Cette page reste aussi utilisable seule (référence saisie à la main) si besoin.

Étape 2 — conversion : monitoring/feedback.py

cd yely_ai_module
python -m monitoring.feedback

Pour chaque entrée de feedback.jsonl pas encore convertie :

  1. Relit la photo dans photos/.
  2. Relance detect_screen_region (même fonction que l'API) pour localiser l'écran et produire un crop, sauvegardé dans monitoring/feedback_crops/.
  3. Ajoute une entrée à annotator/annotations/annotations.json, au même format que l'annotation manuelle (lcd_bbox, lcd_crop, fields), en utilisant les valeurs corrigées comme champs.
  4. Marque l'entrée converted=true dans feedback.jsonl pour ne pas la reconvertir au prochain passage.

Point important : la détection d'écran automatique n'est pas fiable à 100% (voir docs/LIMITATIONS.md, point 2 — 86% des images d'origine sont tombées sur un repli approximatif). Les entrées générées ici sont donc marquées "status": "pending_review" (pas "annotated") : une relecture humaine rapide via l'outil annotator/ (vérifier que lcd_bbox cadre bien l'écran) reste nécessaire avant de les inclure dans un ré-entraînement. Ce choix est délibéré — préférer une étape manuelle courte à l'injection silencieuse de crops mal cadrés dans le dataset, qui dégraderait la précision plutôt que de l'améliorer (cf. la cause n°2 de la limitation à 70.2%).

Relire les entrées pending_review (annotator/annotate.py --review-pending)

annotate.py a un mode dédié qui ne parcourt pas un dossier d'images mais relit directement les entrées status="pending_review" d'annotations.json — une par une, avec le rectangle et les valeurs déjà pré-remplis (à confirmer ou corriger) au lieu de repartir de zéro :

cd annotator
python annotate.py --review-pending

Marche à suivre pour chaque image affichée :

  1. Le rectangle vert affiché est le lcd_bbox détecté automatiquement au moment de la conversion — regarde s'il cadre bien l'écran LCD.
  2. S'il est correct : appuie sur S ou Entrée directement, pas besoin de redessiner.
  3. S'il est mal cadré (coupe un chiffre, déborde sur le boîtier...) : appuie sur R pour l'effacer, puis dessine un nouveau rectangle (clic + glisser) avant S/Entrée.
  4. Le terminal demande ensuite de confirmer les valeurs (prix, volume, prix/litre...) — elles sont déjà pré-remplies avec la correction envoyée par le pompiste (visibles entre crochets [...]) : appuie sur Entrée pour chaque champ correct, ou retape la valeur si besoin de l'ajuster.
  5. N pour ignorer une entrée douteuse (photo illisible, correction qui ne semble pas fiable) — elle passe en status="skipped" et ne sera plus proposée ni utilisée à l'entraînement.
  6. Q/Esc pour arrêter la session en cours ; la progression déjà validée est sauvegardée, il suffit de relancer --review-pending plus tard pour reprendre (seules les entrées encore pending_review sont reproposées).

Chaque entrée confirmée passe automatiquement en status="annotated" — la suite (§3) la traite alors exactement comme une annotation manuelle classique. python annotate.py --review (sans --review-pending) affiche à tout moment un résumé indiquant combien d'entrées restent [à relire].

Étape 3 — ré-entraînement périodique

Une fois un nombre suffisant d'entrées pending_review relues et passées à "annotated" (ex. tous les 30-50 nouvelles corrections, à ajuster selon le volume réel observé en production) :

cd annotator
python split_lines.py          # nettoie et régénère train/val (fix leak inclus)
cd ../train
python prepare_doctr_dataset.py
python finetune_doctr.py --epochs 60

Voir docs/TRAINING.md pour le détail de chaque étape et les pièges déjà rencontrés (ne pas modifier le dataset pendant l'entraînement, toujours repartir d'un dossier de sortie nettoyé).

Étape 4 — versioning du modèle actif

Chaque nouveau modèle entraîné est écrit dans son propre dossier (train/models/v<N>/), jamais en écrasant le précédent — l'incident de ce projet où un entraînement de test a écrasé le meilleur checkpoint (55.3%) avec un résultat plus faible (44.7%) a motivé ce choix. Pour déployer une nouvelle version :

  1. Copier le .pt retenu vers yely_ai_module/models/crnn_fuel_pump_best.pt (écrase le fichier servi par l'API, mais l'ancien reste dans train/models/v<N>/ si un retour arrière est nécessaire).
  2. Mettre à jour MODEL_VERSION (variable d'environnement du déploiement, ex. Hugging Face Spaces → Settings → Variables) pour que les nouvelles entrées de logs/api.log soient distinguables des précédentes dans monitoring/metrics.py.
  3. Redémarrer le Space (ou le processus uvicorn) — le modèle est chargé une seule fois au démarrage (singleton _get_model()), un nouveau fichier sur disque n'est pas repris à chaud.

3. Stockage persistant (obligatoire pour que tout ceci survive en production)

Par défaut, un Hugging Face Space Docker écrit sur le disque éphémère du conteneur : photos/, logs/api.log et monitoring/feedback.jsonl disparaissent à chaque redémarrage/veille du Space (les Spaces gratuits se mettent en veille après une période d'inactivité). Pour une démo tenue en une seule session continue ce n'est pas gênant, mais la boucle de feedback et l'historique /metrics//failures n'ont de sens que si ces fichiers survivent — c'est là qu'intervient le stockage persistant.

Ce que fait le code

app/main.py, monitoring/metrics.py et monitoring/feedback.py lisent tous la même variable d'environnement YELY_DATA_DIR : si elle est définie, photos/, logs/ et monitoring/feedback.jsonl sont placés sous ce répertoire au lieu du dossier de l'application. Par défaut (variable absente), tout reste sous yely_ai_module/ comme avant — donc rien ne casse en local ou sur un Space sans stockage persistant.

Activer le stockage persistant sur le Space

  1. Ouvre ton Space → Settings → section Persistent storage. C'est une option payante (facturée au mois, plusieurs tailles proposées) — vérifie le tarif affiché à cet instant sur la page, il peut avoir changé. Pour une démo, la plus petite taille suffit largement (quelques photos + logs texte).
  2. Active-la et choisis une taille. Hugging Face monte un disque dans le conteneur — le chemin de montage est indiqué dans l'interface au moment de l'activation (généralement /data).
  3. Toujours dans Settings → Variables and secrets → New variable (pas secret, cette valeur n'a rien de sensible) :
    • Nom : YELY_DATA_DIR
    • Valeur : le chemin de montage indiqué à l'étape 2 (ex. /data)
  4. Redémarre le Space (Settings → Restart this Space, ou un simple push suffit à le redéployer) pour que la variable soit prise en compte.
  5. Vérifier que ça persiste vraiment : envoie une image via /analyze (ou le formulaire web/index.html), consulte GET /metrics (total_requests ≥ 1), puis redémarre le Space depuis Settings et rappelle GET /metrics — si total_requests n'est pas retombé à 0, c'est branché correctement.

Si tu ne veux pas payer pour la démo

Pas bloquant : sans stockage persistant, tout fonctionne normalement tant que le Space ne redémarre pas pendant la session de démonstration (soutenance en continu). C'est seulement l'historique qui ne survit pas d'une session à l'autre — acceptable pour une démo ponctuelle, à mentionner comme limitation connue si le sujet est posé (docs/LIMITATIONS.md).

4. Ce qui est réellement implémenté vs conçu

Fonctionnel et testé (tests/test_monitoring.py) : POST /feedback, GET /metrics, GET /failures, GET /photos/{photo_reference}, monitoring/metrics.py, monitoring/feedback.py::record_feedback et ::convert_feedback_to_annotations. Le mode annotate.py --review-pending n'a pas de test automatisé (outil interactif OpenCV, nécessite un affichage) — vérifié manuellement.

Conçu mais volontairement manuel (pas automatisé, par choix — voir §2 ci-dessus) : la relecture des entrées pending_review et le déclenchement du ré-entraînement lui-même, qui restent des actions humaines délibérées plutôt qu'un cron — le volume de données actuel (quelques centaines d'images) ne justifie pas encore une automatisation complète, et une relecture humaine reste la meilleure garde-fou contre la dégradation du dataset tant que le découpage en lignes n'est pas plus robuste.