Spaces:
Sleeping
Sleeping
File size: 13,168 Bytes
3020394 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 | # 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 :
```bash
# 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`
```bash
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`
```bash
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 :
```bash
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) :
```bash
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.
|