Spaces:
Sleeping
Sleeping
functionNormally
Restructurer le parcours pédagogique : CNN de zéro puis backbone + ML classique
c7f3e02 | 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, | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # 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 — SimpleCNN | |
| # --------------------------------------------------------------------------- | |
| 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"], | |
| gr.update(choices=models, value=selected), | |
| ) | |
| except Exception as e: | |
| return f"Échec de l'entraînement :\n{e}", None, None, None, None, None, gr.update() | |
| # --------------------------------------------------------------------------- | |
| # Tab 3 — Backbone + ML classique | |
| # --------------------------------------------------------------------------- | |
| 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"), show("LDA") | |
| def train_classical_callback( | |
| clf_type, | |
| svm_c, svm_kernel, svm_gamma, | |
| logreg_c, logreg_max_iter, | |
| knn_k, knn_metric, | |
| rf_n_estimators, rf_max_depth, | |
| lda_solver, | |
| 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), "kernel": svm_kernel, "gamma": svm_gamma} | |
| elif clf_type == "Régression logistique": | |
| params = {"C": float(logreg_c), "max_iter": int(logreg_max_iter)} | |
| elif clf_type == "k-NN": | |
| params = {"n_neighbors": int(knn_k), "metric": knn_metric} | |
| elif clf_type == "Forêt aléatoire": | |
| depth = int(rf_max_depth) if rf_max_depth and int(rf_max_depth) > 0 else None | |
| params = {"n_estimators": int(rf_n_estimators), "max_depth": depth} | |
| elif clf_type == "LDA": | |
| params = {"solver": lda_solver} | |
| class_names = get_class_names() | |
| result = train_classical_model(clf_type, features_cache, class_names, model_tag, **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 4 — 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."} | |
| 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 | |
| def predict_callback(model_name, image): | |
| try: | |
| return predict_uploaded_image(model_name, image) | |
| except Exception as e: | |
| return f"Échec :\n{e}", None | |
| 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 trois étapes : " | |
| "**exploration des données**, **entraînement d'un CNN de zéro**, " | |
| "puis **exploitation d'un backbone préentraîné avec des algorithmes classiques**. " | |
| "L'objectif est de comprendre pourquoi l'apprentissage par transfert est si puissant, " | |
| "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 — SimpleCNN de zéro | |
| # ------------------------------------------------------------------ # | |
| with gr.Tab("2. CNN entraîné de zéro"): | |
| gr.Markdown("## Entraîner un réseau convolutif sans connaissances préalables") | |
| gr.Markdown( | |
| "La première approche naturelle est de construire un réseau de neurones convolutif (CNN) " | |
| "et de l'entraîner 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("## 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 3 — Backbone préentraîné + ML classique | |
| # ------------------------------------------------------------------ # | |
| with gr.Tab("3. 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 CNN de zéro, une stratégie radicalement différente " | |
| "consiste à réutiliser un réseau déjà entraîné, 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 affiné sur notre jeu de données de charbons de bois**. " | |
| "Ce modèle a d'abord été préentraîné sur ImageNet (1,2 million d'images, 1 000 classes), " | |
| "puis ses poids ont été ajustés sur nos images microscopiques. " | |
| "Il a donc appris à reconnaître les structures visuelles propres aux charbons de bois.\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…) sont rapides à entraîner, " | |
| "interprétables, et ne nécessitent pas de GPU. " | |
| "Comparez leurs résultats avec ceux obtenus à l'étape précédente." | |
| ) | |
| 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", "LDA"], | |
| value="SVM", | |
| label="Algorithme de classification", | |
| ) | |
| with gr.Column(visible=True) as svm_col: | |
| gr.Markdown("#### Paramètres SVM") | |
| svm_c = gr.Number( | |
| value=1.0, label="C — force de régularisation", | |
| info="Une valeur faible regularise davantage (marges plus larges).", | |
| ) | |
| svm_kernel = gr.Dropdown( | |
| choices=["rbf", "linear", "poly"], value="rbf", | |
| label="Noyau", | |
| info="RBF est généralement le meilleur point de départ.", | |
| ) | |
| svm_gamma = gr.Dropdown( | |
| choices=["scale", "auto"], value="scale", label="Gamma" | |
| ) | |
| with gr.Column(visible=False) as logreg_col: | |
| gr.Markdown("#### Paramètres Régression logistique") | |
| logreg_c = gr.Number(value=1.0, label="C — force de régularisation") | |
| logreg_max_iter = gr.Number(value=1000, label="Itérations max") | |
| with gr.Column(visible=False) as knn_col: | |
| gr.Markdown("#### Paramètres k-NN") | |
| 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.", | |
| ) | |
| knn_metric = gr.Dropdown( | |
| choices=["euclidean", "cosine", "manhattan"], | |
| value="euclidean", label="Métrique de distance", | |
| ) | |
| with gr.Column(visible=False) as rf_col: | |
| gr.Markdown("#### Paramètres Forêt aléatoire") | |
| rf_n_estimators = gr.Slider( | |
| minimum=10, maximum=500, value=100, step=10, label="Nombre d'arbres" | |
| ) | |
| rf_max_depth = gr.Number( | |
| value=0, label="Profondeur max (0 = illimitée)" | |
| ) | |
| with gr.Column(visible=False) as lda_col: | |
| gr.Markdown("#### Paramètres LDA") | |
| lda_solver = gr.Dropdown( | |
| choices=["svd", "lsqr", "eigen"], value="svd", label="Solveur" | |
| ) | |
| ml_tag = gr.Textbox( | |
| label="Nom du modèle", placeholder="ex. svm_rbf_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 4 — Tester et analyser | |
| # ------------------------------------------------------------------ # | |
| with gr.Tab("4. 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 — " | |
| "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_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, | |
| 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, lda_col], | |
| ) | |
| train_classical_btn.click( | |
| fn=train_classical_callback, | |
| inputs=[ | |
| clf_type, | |
| svm_c, svm_kernel, svm_gamma, | |
| logreg_c, logreg_max_iter, | |
| knn_k, knn_metric, | |
| rf_n_estimators, rf_max_depth, | |
| lda_solver, | |
| 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) | |