vperies@bebop ← Retour au portfolio

Machine learning & computer vision · Simplon × Microsoft

WildAIprint : Classification d'empreintes animales par réseau convolutif (ResNet50) & API Flask

Projet d'équipe à quatre, en formation Simplon × Microsoft : reconnaître 13 espèces à partir de photos d'empreintes. Mon meilleur résultat : 48 % de bonnes réponses sur 104 photos de test jamais vues (hasard : 8 %), avec le modèle que j'ai repris en novembre 2026. Le ResNet50 de l'équipe, celui de Yasmine Sayad, était annoncé à 83 %. Une fois les photos séparées par photo d'origine, il fait 26 %.

Espèces
13
Photos d'origine dans le jeu agrandi (250 au départ)
715
83 % annoncés · 26 % réévalués · 48 % avec le modèle repris, sur 104 photos de test (hasard : 8 %)
26 → 48 %
F1 macro du modèle repris, même test
0,48

Conclusion · En trois lignes

Ce qu'il faut retenir

83 % annoncés, 26 % mesurés. Les variantes d'une même photo étaient à la fois en entraînement et en validation. Séparé par photo d'origine, le ResNet50 fait 26 %. Avec 715 photos d'origine au lieu de 250, il fait toujours 26 % (104 photos de test).

48 % avec le modèle repris. Des réseaux pré-entraînés gelés et une régression logistique par-dessus : DINOv2-Large donne 48 % (38 à 58 %) sur les mêmes 104 photos de test, contre 8 % au hasard.

Limites. Un seul entraînement, 8 photos de test par espèce, et l'application Flask utilise encore l'ancien modèle.

En bref · Computer vision

Le modèle et son score

Moteur de l'app
ResNet50 · Keras / TF
Modèle repris
DINOv2-Large gelé · régression logistique
Premier CNN
From scratch, suivi MLflow · 18 %
Test, 38 photos
0,26 (26 %) avec le ResNet50
Test, 104 photos
26 % avec le ResNet50, 48 % avec le modèle repris

Volet 01 · Pipeline & données

Le défi de terrain & préparation du dataset

Reconnaître une espèce à partir d'une photo d'empreinte est un problème de vision par ordinateur. L'équipe disposait de 250 photos pour 13 espèces.

Photos d'origine par espèce : Castor 16, Chat 18, Chien 24, Coyote 21, Écureuil 19, Lapin 11, Loup 18, Lynx 24, Ours 22, Puma 22, Rat 10, Raton laveur 23, Renard 22. Les images corrompues sont retirées, et les copies exactes aussi (empreinte MD5).

Une augmentation hors ligne (luminosité, miroir, bruit, rotation, zoom) crée des variantes de chaque photo : 1 500 images au total. À l'entraînement, une augmentation à la volée s'ajoute (miroir, rotation, zoom, translation, luminosité, bruit gaussien). Images en 256×256, lots de 16.

  • 250 photos, 1 500 après augmentation
  • 13 espèces
  • Déduplication MD5
  • Augmentation hors ligne et à la volée

Volet 02 · Modèle & évaluation

ResNet50 en deux phases, puis une évaluation refaite

Un premier CNN entraîné from scratch (3 blocs de convolution, suivi dans MLflow, ReduceLROnPlateau) plafonnait à 18 %. L'équipe a retenu un ResNet50 pré-entraîné sur ImageNet, sans sa tête. La tête ajoutée : GlobalAveragePooling2D, Dropout(0.5), Dense(128, relu), Dropout(0.3) et Dense(13, softmax).

Deux phases d'entraînement. Phase 1 : backbone gelé, Adam à 1e-3, 10 époques. Phase 2 : les 30 dernières couches dégelées, Adam à 1e-5, 20 époques, EarlyStopping (patience 10).

Ma part. En équipe : nettoyage et déduplication MD5 des images, premier CNN from scratch suivi dans MLflow, export du modèle (model.keras, class_names.json) et intégration dans l'API Flask. Seul, en octobre et novembre 2026 : l'évaluation refaite, le jeu agrandi, puis le modèle repris, racontés ci-dessous.

Première évaluation : 83 %. Les 1 500 images ont été séparées en 80 % d'entraînement et 20 % de validation, soit 300 images. Résultat : 83 % de bonnes réponses (exactitude, ou accuracy) et un F1 macro de 0,83. Mais les variantes d'une même photo se retrouvaient des deux côtés : les mêmes photos servaient à entraîner et à vérifier. Le score était donc optimiste.

Évaluation refaite : 26 %. Même architecture et même recette, modèle réentraîné ; une seule différence : la séparation. Chaque variante est rattachée à sa photo d'origine, et les photos presque identiques sont regroupées (empreinte visuelle dHash). Espèce par espèce, avant l'augmentation : 70 % entraînement, 15 % validation, 15 % test. Les variantes ne vont qu'en entraînement. Le test ne sert qu'une fois, à la fin.

Sur 38 photos de test jamais vues : 26 % de bonnes réponses (intervalle à 95 % : 13 % à 39 %) et un F1 macro de 0,26 (0,11 à 0,33). Au hasard, sur 13 espèces, on aurait 8 %. Aucun quasi-doublon trouvé entre photos d'origine (dHash) : la fuite venait de l'augmentation faite avant la séparation. L'intervalle ne couvre qu'un entraînement, avec une seule graine. Notebook : split_par_photo.ipynb.

Les courbes le confirment. L'exactitude d'entraînement monte vers 70 %, la validation ne dépasse jamais 35 %. Le modèle retient les photos, pas les empreintes. Après séparation, chaque espèce n'a que 6 à 17 photos d'origine pour l'entraînement.

Piste testée : preprocess_input. Le modèle reçoit des pixels bruts (0 à 255), alors que ResNet50 attend des images centrées sur la moyenne d'ImageNet. L'essai a été annoncé avant d'être lancé : une seule ligne de plus, le même test. Résultat : 24 % (intervalle à 95 % : 11 % à 37 %), F1 macro 0,19 (0,07 à 0,26). Aucun gain mesurable. Notebook : split_par_photo_preprocess.ipynb.

Jeu agrandi : 715 photos, aucun gain visible. Pour tester l'idée du manque de photos, j'ai porté le jeu à 715 photos d'origine, 55 par espèce, contre 250. Je garde les 250 du projet et j'ajoute 465 photos d'empreintes prises sur iNaturalist, marquées « empreinte », sous licence libre (CC0 : 9, CC BY : 50, CC BY-NC : 406) et triées à la main. Chaque photo garde son auteur et sa licence dans inat_attributions.csv. Même séparation, même architecture, même recette, mêmes cinq variantes. Modèle réentraîné une fois (graine 12), sur carte graphique.

Sur 104 photos de test jamais vues (8 par espèce) : 26 % de bonnes réponses (intervalle à 95 % : 18 % à 35 %) et un F1 macro de 0,24 (0,16 à 0,31). Par source : 25 % sur les 48 photos d'origine du projet (13 % à 38 %), 27 % sur les 56 photos iNaturalist (16 % à 39 %) ; ces deux scores ne se distinguent pas. Les deux tests (38 puis 104 photos) ne sont pas les mêmes et leurs intervalles se recouvrent : presque trois fois plus de photos n'apportent aucun gain visible, au plus quelques points. La mesure est seulement plus précise : 18-35 % au lieu de 13-39 %. Le contrôle des quasi-doublons (dHash) a signalé deux paires de photos d'espèces différentes ; vérifiées à l'œil, ce sont des photos différentes, peu détaillées (neige, sol uniforme). Notebook : split_par_photo_v2.ipynb.

Ajouter des photos n'a pas suffi, le nombre de photos n'était pas le seul frein. Une limite de la recette, la même qu'à l'origine : l'arrêt anticipé garde les poids de la plus faible perte de validation, ici ceux de la première passe de fine-tuning (validation à 20 %, alors que la justesse montait ensuite vers 30 %). Le modèle évalué est donc peu entraîné. J'ai ensuite changé de méthode, racontée plus bas. Chaque mesure du ResNet50 vient d'un seul entraînement : l'écart d'un entraînement à l'autre n'est pas mesuré. Les scores par espèce du premier test ne sont pas publiés : 2 à 4 photos de test par espèce, c'est trop peu.

  1. 01Input (256×256)Pixels bruts 0 à 255 & augmentation à la volée
  2. 02ResNet50 backboneTransfer learning ImageNet
  3. 03Tête custom & DropoutGlobalAvgPool + Dense 128
  4. 04Softmax 13 classesExport model.keras
  • ResNet50 backbone
  • Fine-tuning 2 phases
  • Séparation par photo d'origine
  • 26 % sur 38 photos de test

Novembre 2026 · Modèle repris

Le modèle repris : 48 %

Après le jeu agrandi, j'ai changé de méthode. Au lieu d'ajuster le ResNet50 sur des pixels bruts, j'extrais les caractéristiques de réseaux pré-entraînés gelés, chacun avec son propre prétraitement. J'entraîne ensuite dessus une régression logistique : une « sonde linéaire ».

Le reste ne change pas : même jeu, même séparation par photo d'origine, même graine. Je choisis le réseau et le réglage sur la validation seulement. Le test ne sert qu'une fois par tour.

Tour 1. Cinq candidats : ResNet50, ConvNeXt-Base, DINOv2-Base, CLIP ViT-B/16 et SigLIP-Base. DINOv2-Base arrive en tête sur la validation (40 %). Sur le test : 37,5 % de bonnes réponses (28 à 47 %), F1 macro 0,38. Notebook : sondes_lineaires.ipynb.

Tour 2. DINOv2-Large, et DINOv2 avec la moyenne des zones de l'image, avec ou sans les variantes d'entraînement. DINOv2-Large arrive en tête sur la validation (41,9 %). Plusieurs réglages sont à égalité : je prends le premier, dans l'ordre fixé d'avance. Sur le test : 48 % de bonnes réponses (38 à 58 %), F1 macro 0,48 (0,38 à 0,56), contre 8 % au hasard. Par source : 48 % sur les 48 photos du projet (33 à 63 %), 48 % sur les 56 photos iNaturalist (36 à 61 %). Notebook : sondes_tour2.ipynb.

Les trois scores (26 % pour le ResNet50, 37,5 % et 48 % pour le modèle repris) sont mesurés sur les mêmes 104 photos de test : la comparaison tient. Le test a servi deux fois, une fois par tour. Je publie les deux résultats.

Limites. Un seul entraînement. Le test reste petit : 8 photos par espèce. L'application Flask utilise encore l'ancien modèle (model.keras), pas le nouveau. Prochaines pistes : recadrer l'empreinte avant de la classer, brancher le nouveau modèle dans l'application.

  • Réseaux gelés + régression logistique
  • DINOv2-Large retenu sur la validation
  • 48 % sur 104 photos de test
  • PyTorch, transformers, scikit-learn

Volet 03 · Application & Docker

Application Flask, front séparé & Docker Compose

Le modèle exporté (model.keras pour les poids, class_names.json pour l'ordre des classes) est servi par une API Flask (route /predict). Le front est un projet séparé. Les deux tournent dans deux conteneurs, lancés par Docker Compose.

Une base SQLite garde l'historique des relevés : photo, lieu, date, espèce. L'API a 7 tests pytest, lancés par GitHub Actions et par un pipeline GitLab CI qui construit et pousse les images ; seul GitLab récupère le vrai modèle (Git LFS) pour le septième test. Le fichier model.keras est versionné avec Git LFS. Les fiches espèces sont dans infos_especes.csv.

  • API Flask, route /predict
  • Docker Compose, 2 conteneurs
  • SQLite, tests pytest
  • GitLab CI, Git LFS

Guide naturaliste · Pas une sortie du modèle

Guide des 13 espèces : à quoi ressemble chaque empreinte

01 / 13
Puma / Cougar
Classe 01 / 13

Félin · Empreinte d'auteur

Puma / Cougar

Puma concolor · Famille des Félidés

Griffes
Rétractiles · Zéro marque visible au sol
Coussinet
4 pelotes · Base trilobée asymétrique
Démarche
Pas direct feutré · Trace nette en ligne
Substrats
Neige, boue, terre meuble

Grand félin discret. L'absence systématique de griffes au sol et la forme asymétrique du coussinet plantaire (trois lobes nets à l'arrière) caractérisent son empreinte.

Sélecteur rapide · Cliquez pour basculer d'espèce

Collaboration & dépôts publics

Projet d'équipe mené dans le cadre de la formation Simplon x Microsoft avec Yasmine Sayad, Valentin Phan, Brunel et Valentin Périès. Chacun a travaillé sur toutes les étapes. L'équipe a retenu le modèle le plus performant, celui de Yasmine Sayad.

Le dépôt du modèle contient les trois notebooks de l'évaluation refaite, split_par_photo.ipynb, split_par_photo_preprocess.ipynb et split_par_photo_v2.ipynb, et les deux du modèle repris, sondes_lineaires.ipynb et sondes_tour2.ipynb, avec resultats_sondes.json, resultats_sondes_tour2.json et requirements-sondes.txt (PyTorch, torchvision, transformers, scikit-learn). Le dossier dataset/ n'y est plus distribué : les photos sont retirées de l'historique, leurs droits sont inconnus. Pour le jeu agrandi, fetch_inat.py retélécharge les photos iNaturalist et build_dataset.py reconstruit le jeu. Auteurs et licences : inat_attributions.csv.