import json import gradio as gr import spaces from backbone_utils import extract_all_features, get_cached_features from classical_ml_utils import train_classical_model from data_utils import dataset_overview, get_class_names, get_images_for_gallery from predict_utils import predict_uploaded_image, test_random_sample from train_utils import ( evaluate_saved_model, list_saved_models, model_meta_path, train_cnn, train_mlp, ) # --------------------------------------------------------------------------- # Tab 1 — Dataset # --------------------------------------------------------------------------- def load_dataset_callback(): try: summary, distribution_df = dataset_overview() class_names = ["Toutes les classes"] + get_class_names() return summary, distribution_df, gr.update(choices=class_names, value="Toutes les classes") except Exception as e: return {"Erreur": str(e)}, None, gr.update() def refresh_gallery_callback(split_name, class_name, max_images): try: return get_images_for_gallery(split_name, class_name, int(max_images)) except Exception as e: return [(None, f"Erreur : {e}")] # --------------------------------------------------------------------------- # Tab 2 — MLP (baseline) # --------------------------------------------------------------------------- @spaces.GPU(duration=300) def train_mlp_callback( num_layers, hidden_dim, dropout, learning_rate, weight_decay, batch_size, epochs, model_tag, ): try: result = train_mlp( num_layers=int(num_layers), hidden_dim=int(hidden_dim), dropout=float(dropout), learning_rate=float(learning_rate), weight_decay=float(weight_decay), batch_size=int(batch_size), epochs=int(epochs), model_tag=model_tag, ) models = list_saved_models() selected = result["model_name"] if result["model_name"] in models else None return ( result["logs"], result["history"], result["summary"], result["classification_report"], result["confusion_matrix"], result["confusion_matrix_path"], result["loss_curve_path"], gr.update(choices=models, value=selected), ) except Exception as e: return f"Échec de l'entraînement :\n{e}", None, None, None, None, None, None, gr.update() # --------------------------------------------------------------------------- # Tab 3 — SimpleCNN # --------------------------------------------------------------------------- @spaces.GPU(duration=300) def train_cnn_callback( num_conv_blocks, base_filters, kernel_size, use_batchnorm, dropout, fc_dim, learning_rate, weight_decay, batch_size, epochs, model_tag, ): try: result = train_cnn( num_conv_blocks=int(num_conv_blocks), base_filters=int(base_filters), kernel_size=int(kernel_size), use_batchnorm=bool(use_batchnorm), dropout=float(dropout), fc_dim=int(fc_dim), learning_rate=float(learning_rate), weight_decay=float(weight_decay), batch_size=int(batch_size), epochs=int(epochs), model_tag=model_tag, ) models = list_saved_models() selected = result["model_name"] if result["model_name"] in models else None return ( result["logs"], result["history"], result["summary"], result["classification_report"], result["confusion_matrix"], result["confusion_matrix_path"], result["loss_curve_path"], gr.update(choices=models, value=selected), ) except Exception as e: return f"Échec de l'entraînement :\n{e}", None, None, None, None, None, None, gr.update() # --------------------------------------------------------------------------- # Tab 4 — Backbone + ML classique # --------------------------------------------------------------------------- @spaces.GPU(duration=60) def extract_features_callback(): try: _, class_names, counts = extract_all_features() lines = [f"Extraction terminée — {len(class_names)} classes détectées"] for split, n in counts.items(): lines.append(f" • {split} : {n} images → {n} vecteurs de 512 dimensions") return "\n".join(lines) except Exception as e: return f"Erreur lors de l'extraction :\n{e}" def on_clf_type_change(clf_type): show = lambda t: gr.update(visible=(clf_type == t)) return show("SVM"), show("Régression logistique"), show("k-NN"), show("Forêt aléatoire") def train_classical_callback( clf_type, svm_c, logreg_c, knn_k, rf_n_estimators, use_cv, model_tag, ): try: features_cache = get_cached_features() if features_cache is None: return ( {"Erreur": "Veuillez d'abord extraire les caractéristiques (bouton ci-dessus)."}, None, None, None, gr.update(), ) params = {} if clf_type == "SVM": params = {"C": float(svm_c)} elif clf_type == "Régression logistique": params = {"C": float(logreg_c)} elif clf_type == "k-NN": params = {"n_neighbors": int(knn_k)} elif clf_type == "Forêt aléatoire": params = {"n_estimators": int(rf_n_estimators)} class_names = get_class_names() result = train_classical_model( clf_type, features_cache, class_names, model_tag, use_cv=bool(use_cv), **params ) models = list_saved_models() selected = result["model_name"] if result["model_name"] in models else None return ( result["summary"], result["classification_report"], result["confusion_matrix"], result["confusion_matrix_path"], gr.update(choices=models, value=selected), ) except Exception as e: return {"Erreur": str(e)}, None, None, None, gr.update() # --------------------------------------------------------------------------- # Tab 5 — Tester et analyser # --------------------------------------------------------------------------- def refresh_models_callback(): models = list_saved_models() return gr.update(choices=models, value=models[0] if models else None) def get_model_info_callback(model_name): if not model_name: return {"message": "Aucun modèle sélectionné."} try: with open(model_meta_path(model_name), "r", encoding="utf-8") as f: return json.load(f) except FileNotFoundError: return {"message": "Métadonnées introuvables."} @spaces.GPU(duration=120) def evaluate_callback(model_name): try: summary, report_df, cm_df, cm_path = evaluate_saved_model(model_name) return summary, report_df, cm_df, cm_path except Exception as e: return {"Erreur": str(e)}, None, None, None @spaces.GPU(duration=60) def predict_callback(model_name, image): try: return predict_uploaded_image(model_name, image) except Exception as e: return f"Échec :\n{e}", None @spaces.GPU(duration=60) def random_test_callback(model_name): try: return test_random_sample(model_name) except Exception as e: return None, f"Échec :\n{e}", None # --------------------------------------------------------------------------- # UI # --------------------------------------------------------------------------- initial_models = list_saved_models() with gr.Blocks(title="Classification d'images microscopiques") as demo: gr.Markdown("# Classification d'images microscopiques de charbons de bois") gr.Markdown( "Ce parcours pédagogique suit une progression en quatre étapes : " "**exploration des données**, **MLP de référence**, **CNN entraîné de zéro**, " "puis **exploitation d'un backbone préentraîné avec des algorithmes classiques**. " "L'objectif est de comprendre pourquoi la structure convolutive et l'apprentissage par " "transfert sont si puissants, surtout quand les données sont rares." ) with gr.Tabs(): # ------------------------------------------------------------------ # # Tab 1 — Explorer le dataset # ------------------------------------------------------------------ # with gr.Tab("1. Explorer le jeu de données"): gr.Markdown("## Comprendre le problème avant de modéliser") gr.Markdown( "Avant de choisir un modèle, il est essentiel de comprendre la structure du jeu de données. " "Combien de classes ? Combien d'images par classe ? Les classes sont-elles équilibrées ? " "Ces questions conditionnent directement les choix de modélisation." ) load_dataset_btn = gr.Button("Charger les informations du dataset", variant="primary") dataset_summary = gr.JSON(label="Résumé général") class_distribution = gr.Dataframe( label="Distribution des images par split et par classe", interactive=False ) gr.Markdown( "## Visualiser les images\n" "Parcourez des exemples d'images pour vous familiariser avec les données. " "Notez que les images microscopiques de charbons de bois peuvent être " "visuellement très similaires d'une espèce à l'autre — ce qui rend la tâche difficile." ) with gr.Row(): split_selector = gr.Dropdown( choices=["train", "validation", "test"], value="train", label="Split" ) class_selector = gr.Dropdown( choices=["Toutes les classes"], value="Toutes les classes", label="Classe" ) max_images = gr.Slider(minimum=4, maximum=48, value=24, step=4, label="Nombre d'images") refresh_gallery_btn = gr.Button("Afficher des exemples") image_gallery = gr.Gallery(label="Exemples d'images", columns=4, height=600) # ------------------------------------------------------------------ # # Tab 2 — MLP (modèle de référence) # ------------------------------------------------------------------ # with gr.Tab("2. MLP (modèle de référence)"): gr.Markdown("## Un premier réseau de neurones : le perceptron multicouche") gr.Markdown( "Avant d'introduire un CNN, commençons par le modèle le plus simple : un **MLP** " "(perceptron multicouche), entièrement connecté. Chaque image est aplatie en un long " "vecteur de pixels — le réseau ne sait donc rien de la structure spatiale de l'image " "(voisinage des pixels, formes, textures locales).\n\n" "**Contexte du problème :** notre jeu de données contient 39 espèces, " "avec seulement 8 images par espèce en moyenne, réparties en train / validation / test.\n\n" "**Ce que cet exercice doit montrer :** observez les courbes de perte train vs validation. " "À partir de quelle époque la courbe de validation cesse de s'améliorer (ou remonte) " "pendant que la perte d'entraînement continue de baisser ? C'est le signe du surapprentissage. " "Essayez de faire varier le nombre de couches et le nombre de neurones par couche : " "vous constaterez que le surapprentissage apparaît quel que soit le choix — " "un MLP n'exploite pas la structure de l'image et ne peut pas s'en affranchir. " "C'est cette limite qui motive le passage au CNN." ) with gr.Row(): with gr.Column(): gr.Markdown("#### Architecture du MLP") mlp_num_layers = gr.Slider( minimum=1, maximum=5, value=2, step=1, label="Nombre de couches cachées", ) mlp_hidden_dim = gr.Dropdown( choices=[64, 128, 256, 512, 1024], value=256, label="Neurones par couche cachée", ) gr.Markdown("#### Hyperparamètres d'entraînement") mlp_dropout = gr.Slider( minimum=0.0, maximum=0.8, value=0.4, step=0.05, label="Dropout", ) mlp_lr = gr.Number(value=1e-3, label="Taux d'apprentissage") mlp_wd = gr.Number(value=1e-4, label="Weight decay (régularisation L2)") mlp_bs = gr.Dropdown(choices=[8, 16, 32, 64], value=16, label="Taille du batch") mlp_epochs = gr.Slider( minimum=1, maximum=50, value=30, step=1, label="Nombre d'époques" ) mlp_tag = gr.Textbox( label="Nom du modèle", placeholder="ex. mlp_2couches_256" ) train_mlp_btn = gr.Button("Lancer l'entraînement", variant="primary") with gr.Column(): mlp_logs = gr.Textbox(label="Journal d'entraînement", lines=20) mlp_history = gr.JSON(label="Historique époque par époque") mlp_summary = gr.JSON(label="Résumé final") gr.Markdown("## Courbes de perte (train vs validation)") mlp_loss_curve = gr.Image(label="Perte par époque", type="filepath") gr.Markdown("## Résultats sur le jeu de test") mlp_report = gr.Dataframe(label="Rapport de classification", interactive=False) mlp_cm = gr.Dataframe(label="Matrice de confusion", interactive=False) mlp_cm_img = gr.Image(label="Matrice de confusion — figure", type="filepath") # ------------------------------------------------------------------ # # Tab 3 — SimpleCNN de zéro # ------------------------------------------------------------------ # with gr.Tab("3. CNN entraîné de zéro"): gr.Markdown("## Entraîner un réseau convolutif sans connaissances préalables") gr.Markdown( "Le MLP de l'onglet précédent surapprend quels que soient les hyperparamètres choisis : " "il ne peut pas exploiter la structure spatiale des images. Construisons maintenant un " "réseau de neurones convolutif (CNN), conçu pour capter des motifs locaux (contours, " "textures) grâce aux filtres de convolution, et entraînons-le directement sur nos données " "de charbons de bois. Ce réseau part de paramètres aléatoires : il ne sait rien des images " "au départ.\n\n" "**Contexte du problème :** notre jeu de données contient 39 espèces, " "avec seulement 8 images par espèce en moyenne. " "C'est extrêmement peu pour apprendre à distinguer 39 classes visuellement similaires.\n\n" "Jouez avec les paramètres d'architecture et d'entraînement pour observer leur effet " "sur les performances. Essayez notamment d'augmenter la complexité du réseau " "et observez ce qui se passe." ) with gr.Row(): with gr.Column(): gr.Markdown("#### Architecture du CNN") num_conv_blocks = gr.Slider( minimum=2, maximum=5, value=3, step=1, label="Blocs convolutionnels", info="Chaque bloc enchaîne Conv2d → BatchNorm → ReLU → MaxPool. Plus de blocs = réseau plus profond.", ) base_filters = gr.Dropdown( choices=[16, 32, 64, 128], value=32, label="Filtres du premier bloc", info="Le nombre de filtres double à chaque bloc. 32 → 64 → 128...", ) kernel_size = gr.Dropdown( choices=[3, 5], value=3, label="Taille du noyau de convolution", info="3×3 capte les détails fins, 5×5 capte des structures plus larges.", ) use_batchnorm = gr.Checkbox( value=True, label="Normalisation par lots (BatchNorm)", info="Stabilise l'entraînement et accélère la convergence.", ) gr.Markdown("#### Hyperparamètres d'entraînement") cnn_dropout = gr.Slider( minimum=0.0, maximum=0.8, value=0.4, step=0.05, label="Dropout", info="Désactive aléatoirement des neurones pour limiter le surapprentissage.", ) cnn_fc_dim = gr.Dropdown( choices=[64, 128, 256, 512], value=256, label="Dimension de la couche cachée", ) cnn_lr = gr.Number(value=1e-3, label="Taux d'apprentissage") cnn_wd = gr.Number(value=1e-4, label="Weight decay (régularisation L2)") cnn_bs = gr.Dropdown(choices=[8, 16, 32, 64], value=16, label="Taille du batch") cnn_epochs = gr.Slider( minimum=1, maximum=50, value=30, step=1, label="Nombre d'époques" ) cnn_tag = gr.Textbox( label="Nom du modèle", placeholder="ex. cnn_3blocs_32filtres" ) train_cnn_btn = gr.Button("Lancer l'entraînement", variant="primary") with gr.Column(): cnn_logs = gr.Textbox(label="Journal d'entraînement", lines=20) cnn_history = gr.JSON(label="Historique époque par époque") cnn_summary = gr.JSON(label="Résumé final") gr.Markdown("## Courbes de perte (train vs validation)") cnn_loss_curve = gr.Image(label="Perte par époque", type="filepath") gr.Markdown("## Résultats sur le jeu de test") cnn_report = gr.Dataframe(label="Rapport de classification", interactive=False) cnn_cm = gr.Dataframe(label="Matrice de confusion", interactive=False) cnn_cm_img = gr.Image(label="Matrice de confusion — figure", type="filepath") # ------------------------------------------------------------------ # # Tab 4 — Backbone préentraîné + ML classique # ------------------------------------------------------------------ # with gr.Tab("4. Backbone préentraîné + ML classique"): gr.Markdown("## Exploiter les connaissances d'un modèle préentraîné") gr.Markdown( "Face aux limites observées avec le MLP et le CNN de zéro (peu de données, beaucoup de " "classes), une stratégie radicalement différente consiste à réutiliser un réseau déjà " "entraîné sur d'autres images, et à s'appuyer sur les représentations qu'il a apprises.\n\n" "### Qu'est-ce qu'un backbone ?\n" "Un **backbone** est un réseau convolutif dont on retire la couche de classification finale. " "Il agit comme un extracteur de caractéristiques : pour chaque image en entrée, " "il produit un vecteur de nombres (ici **512 dimensions**) qui encode le contenu visuel " "de l'image de façon compacte et abstraite.\n\n" "### Quel backbone utilisons-nous ici ?\n" "Nous utilisons un **ResNet18 avec ses poids ImageNet d'origine, sans aucun fine-tuning** " "sur nos images de charbons de bois. Ce modèle a été préentraîné sur ImageNet " "(1,2 million d'images, 1 000 classes) — il n'a jamais vu une image de charbon de bois. " "L'objectif est d'observer si des représentations apprises sur des images naturelles " "génériques transfèrent malgré tout à un domaine très différent (microscopie).\n\n" "### Pourquoi des algorithmes classiques ensuite ?\n" "Une fois les images transformées en vecteurs de 512 dimensions, " "n'importe quel algorithme de classification classique peut être appliqué. " "Ces algorithmes (SVM, régression logistique, k-NN, forêt aléatoire) sont rapides à entraîner, " "interprétables, et ne nécessitent pas de GPU. " "Pour chaque algorithme, un seul hyperparamètre est ajustable — les autres réglages " "sont fixés pour rester comparables. Une option de validation croisée permet en plus " "d'estimer la stabilité du score sur le train set. " "Comparez leurs résultats avec ceux obtenus aux étapes précédentes (MLP, CNN)." ) gr.Markdown("## Étape 1 — Extraction des caractéristiques") gr.Markdown( "Passez toutes les images du jeu de données dans le backbone. " "Chaque image est convertie en un vecteur de 512 valeurs. " "Cette opération est réalisée une seule fois et mise en cache." ) extract_btn = gr.Button( "Extraire les caractéristiques (backbone gelé)", variant="primary" ) extract_status = gr.Textbox(label="Statut", lines=5, interactive=False) gr.Markdown("## Étape 2 — Entraîner un classifieur sur les caractéristiques") gr.Markdown( "Choisissez un algorithme et ajustez ses paramètres. " "L'entraînement est quasi-instantané car il opère sur des vecteurs, " "sans jamais manipuler les images brutes ni utiliser le GPU." ) with gr.Row(): with gr.Column(): clf_type = gr.Radio( choices=["SVM", "Régression logistique", "k-NN", "Forêt aléatoire"], value="SVM", label="Algorithme de classification", ) with gr.Column(visible=True) as svm_col: gr.Markdown("#### Paramètre SVM (noyau RBF, gamma='scale' fixés)") svm_c = gr.Number( value=1.0, label="C — force de régularisation", info="Une valeur faible regularise davantage (marges plus larges).", ) with gr.Column(visible=False) as logreg_col: gr.Markdown("#### Paramètre Régression logistique (max_iter=1000 fixé)") logreg_c = gr.Number(value=1.0, label="C — force de régularisation") with gr.Column(visible=False) as knn_col: gr.Markdown("#### Paramètre k-NN (distance euclidienne fixée)") knn_k = gr.Slider( minimum=1, maximum=20, value=5, step=1, label="k — nombre de voisins", info="k=1 mémorise les données, k élevé généralise davantage.", ) with gr.Column(visible=False) as rf_col: gr.Markdown("#### Paramètre Forêt aléatoire (profondeur illimitée fixée)") rf_n_estimators = gr.Slider( minimum=10, maximum=500, value=100, step=10, label="Nombre d'arbres" ) cv_checkbox = gr.Checkbox( value=False, label="Activer la validation croisée sur le train set", info="Ajoute un score F1 macro moyenné sur plusieurs folds, en plus du score sur le jeu de test.", ) ml_tag = gr.Textbox( label="Nom du modèle", placeholder="ex. svm_C1" ) train_classical_btn = gr.Button("Entraîner le classifieur", variant="primary") with gr.Column(): ml_summary = gr.JSON(label="Résumé des métriques") ml_report = gr.Dataframe(label="Rapport de classification", interactive=False) ml_cm = gr.Dataframe(label="Matrice de confusion", interactive=False) ml_cm_img = gr.Image(label="Matrice de confusion — figure", type="filepath") # ------------------------------------------------------------------ # # Tab 5 — Tester et analyser # ------------------------------------------------------------------ # with gr.Tab("5. Tester et analyser"): gr.Markdown("## Comparer et évaluer les modèles") gr.Markdown( "Tous les modèles entraînés dans les onglets précédents apparaissent ici — " "MLP, CNN de zéro comme classifieurs ML. " "Évaluez-les sur le jeu de test, prédisez la classe d'une image importée, " "et tirez vos conclusions sur l'apport du backbone préentraîné." ) with gr.Row(): with gr.Column(): model_selector = gr.Dropdown( choices=initial_models, value=initial_models[0] if initial_models else None, label="Modèle sauvegardé", ) refresh_btn = gr.Button("Actualiser la liste") load_info_btn = gr.Button("Afficher les informations du modèle") model_info = gr.JSON(label="Métadonnées du modèle") with gr.Column(): evaluate_btn = gr.Button("Évaluer sur le jeu de test", variant="primary") eval_summary = gr.JSON(label="Résumé des métriques") eval_report = gr.Dataframe(label="Rapport de classification", interactive=False) eval_cm = gr.Dataframe(label="Matrice de confusion", interactive=False) eval_cm_img = gr.Image(label="Matrice de confusion — figure", type="filepath") gr.Markdown("## Prédiction sur une image importée") gr.Markdown( "Importez une image microscopique de charbon de bois et observez " "comment les différents modèles la classifient." ) with gr.Row(): with gr.Column(): upload_image = gr.Image(type="pil", label="Image à classer") predict_btn = gr.Button("Prédire la classe", variant="primary") with gr.Column(): predict_text = gr.Textbox(label="Résultat de la prédiction", lines=7) predict_probs = gr.Label(label="Probabilités par classe") gr.Markdown("## Test sur un échantillon aléatoire du jeu de test") gr.Markdown( "Tirez une image au hasard dans le jeu de test et vérifiez si le modèle " "sélectionné la classe correctement." ) random_test_btn = gr.Button("Tirer un échantillon aléatoire") with gr.Row(): random_img = gr.Image(type="pil", label="Image tirée") random_text = gr.Textbox(label="Résultat", lines=7) random_probs = gr.Label(label="Probabilités par classe") # ---------------------------------------------------------------------- # # Event wiring # ---------------------------------------------------------------------- # load_dataset_btn.click( fn=load_dataset_callback, inputs=None, outputs=[dataset_summary, class_distribution, class_selector], ) refresh_gallery_btn.click( fn=refresh_gallery_callback, inputs=[split_selector, class_selector, max_images], outputs=image_gallery, ) train_mlp_btn.click( fn=train_mlp_callback, inputs=[ mlp_num_layers, mlp_hidden_dim, mlp_dropout, mlp_lr, mlp_wd, mlp_bs, mlp_epochs, mlp_tag, ], outputs=[ mlp_logs, mlp_history, mlp_summary, mlp_report, mlp_cm, mlp_cm_img, mlp_loss_curve, model_selector, ], ) train_cnn_btn.click( fn=train_cnn_callback, inputs=[ num_conv_blocks, base_filters, kernel_size, use_batchnorm, cnn_dropout, cnn_fc_dim, cnn_lr, cnn_wd, cnn_bs, cnn_epochs, cnn_tag, ], outputs=[ cnn_logs, cnn_history, cnn_summary, cnn_report, cnn_cm, cnn_cm_img, cnn_loss_curve, model_selector, ], ) extract_btn.click(fn=extract_features_callback, inputs=None, outputs=extract_status) clf_type.change( fn=on_clf_type_change, inputs=clf_type, outputs=[svm_col, logreg_col, knn_col, rf_col], ) train_classical_btn.click( fn=train_classical_callback, inputs=[ clf_type, svm_c, logreg_c, knn_k, rf_n_estimators, cv_checkbox, ml_tag, ], outputs=[ml_summary, ml_report, ml_cm, ml_cm_img, model_selector], ) refresh_btn.click(fn=refresh_models_callback, inputs=None, outputs=model_selector) load_info_btn.click( fn=get_model_info_callback, inputs=model_selector, outputs=model_info ) evaluate_btn.click( fn=evaluate_callback, inputs=model_selector, outputs=[eval_summary, eval_report, eval_cm, eval_cm_img], ) predict_btn.click( fn=predict_callback, inputs=[model_selector, upload_image], outputs=[predict_text, predict_probs], ) random_test_btn.click( fn=random_test_callback, inputs=model_selector, outputs=[random_img, random_text, random_probs], ) if __name__ == "__main__": demo.launch(ssr_mode=False)