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