functionNormally commited on
Commit
c7f3e02
·
1 Parent(s): 27c7e24

Restructurer le parcours pédagogique : CNN de zéro puis backbone + ML classique

Browse files
Files changed (1) hide show
  1. app.py +251 -175
app.py CHANGED
@@ -12,7 +12,6 @@ from train_utils import (
12
  list_saved_models,
13
  model_meta_path,
14
  train_cnn,
15
- train_fc_head,
16
  )
17
 
18
  # ---------------------------------------------------------------------------
@@ -36,16 +35,56 @@ def refresh_gallery_callback(split_name, class_name, max_images):
36
 
37
 
38
  # ---------------------------------------------------------------------------
39
- # Tab 2 — ML classique
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  # ---------------------------------------------------------------------------
41
 
42
  @spaces.GPU(duration=60)
43
  def extract_features_callback():
44
  try:
45
  _, class_names, counts = extract_all_features()
46
- lines = [f"Extraction terminée ({len(class_names)} classes)"]
47
  for split, n in counts.items():
48
- lines.append(f" {split} : {n} images")
49
  return "\n".join(lines)
50
  except Exception as e:
51
  return f"Erreur lors de l'extraction :\n{e}"
@@ -68,7 +107,10 @@ def train_classical_callback(
68
  try:
69
  features_cache = get_cached_features()
70
  if features_cache is None:
71
- return {"Erreur": "Veuillez d'abord extraire les caractéristiques (bouton ci-dessus)."}, None, None, None, gr.update()
 
 
 
72
 
73
  params = {}
74
  if clf_type == "SVM":
@@ -88,7 +130,6 @@ def train_classical_callback(
88
 
89
  models = list_saved_models()
90
  selected = result["model_name"] if result["model_name"] in models else None
91
-
92
  return (
93
  result["summary"],
94
  result["classification_report"],
@@ -101,67 +142,7 @@ def train_classical_callback(
101
 
102
 
103
  # ---------------------------------------------------------------------------
104
- # Tab 3Modèles neuronaux
105
- # ---------------------------------------------------------------------------
106
-
107
- def on_neural_type_change(model_type):
108
- is_cnn = (model_type == "CNN de zéro")
109
- default_lr = 1e-3 if is_cnn else 1e-4
110
- return gr.update(visible=is_cnn), gr.update(value=default_lr)
111
-
112
-
113
- @spaces.GPU(duration=300)
114
- def train_neural_callback(
115
- model_type,
116
- num_conv_blocks, base_filters, kernel_size, use_batchnorm,
117
- dropout, fc_dim,
118
- learning_rate, weight_decay, batch_size, epochs,
119
- model_tag,
120
- ):
121
- try:
122
- if model_type == "FC sur backbone préentraîné":
123
- result = train_fc_head(
124
- dropout=float(dropout),
125
- fc_dim=int(fc_dim),
126
- learning_rate=float(learning_rate),
127
- weight_decay=float(weight_decay),
128
- batch_size=int(batch_size),
129
- epochs=int(epochs),
130
- model_tag=model_tag,
131
- )
132
- else:
133
- result = train_cnn(
134
- num_conv_blocks=int(num_conv_blocks),
135
- base_filters=int(base_filters),
136
- kernel_size=int(kernel_size),
137
- use_batchnorm=bool(use_batchnorm),
138
- dropout=float(dropout),
139
- fc_dim=int(fc_dim),
140
- learning_rate=float(learning_rate),
141
- weight_decay=float(weight_decay),
142
- batch_size=int(batch_size),
143
- epochs=int(epochs),
144
- model_tag=model_tag,
145
- )
146
-
147
- models = list_saved_models()
148
- selected = result["model_name"] if result["model_name"] in models else None
149
-
150
- return (
151
- result["logs"],
152
- result["history"],
153
- result["summary"],
154
- result["classification_report"],
155
- result["confusion_matrix"],
156
- result["confusion_matrix_path"],
157
- gr.update(choices=models, value=selected),
158
- )
159
- except Exception as e:
160
- return f"Échec de l'entraînement :\n{e}", None, None, None, None, None, gr.update()
161
-
162
-
163
- # ---------------------------------------------------------------------------
164
- # Tab 4 — Tester et prédire
165
  # ---------------------------------------------------------------------------
166
 
167
  def refresh_models_callback():
@@ -211,28 +192,41 @@ def random_test_callback(model_name):
211
  initial_models = list_saved_models()
212
 
213
  with gr.Blocks(title="Classification d'images microscopiques") as demo:
 
214
  gr.Markdown("# Classification d'images microscopiques de charbons de bois")
215
  gr.Markdown(
216
- "Application pédagogique : explorez le jeu de données, entraînez des classifieurs "
217
- "traditionnels ou neuronaux sur les caractéristiques extraites par un backbone "
218
- "ResNet18 préentraîné, puis analysez et comparez les résultats."
 
 
219
  )
220
 
221
  with gr.Tabs():
222
 
223
  # ------------------------------------------------------------------ #
224
- # Tab 1
225
  # ------------------------------------------------------------------ #
226
  with gr.Tab("1. Explorer le jeu de données"):
227
- gr.Markdown("## Comprendre le jeu de données avant l'entraînement")
 
 
 
 
 
228
 
229
  load_dataset_btn = gr.Button("Charger les informations du dataset", variant="primary")
230
  dataset_summary = gr.JSON(label="Résumé général")
231
  class_distribution = gr.Dataframe(
232
- label="Distribution par split et par classe", interactive=False
233
  )
234
 
235
- gr.Markdown("## Visualisation des images")
 
 
 
 
 
236
  with gr.Row():
237
  split_selector = gr.Dropdown(
238
  choices=["train", "validation", "test"], value="train", label="Split"
@@ -246,57 +240,179 @@ with gr.Blocks(title="Classification d'images microscopiques") as demo:
246
  image_gallery = gr.Gallery(label="Exemples d'images", columns=4, height=600)
247
 
248
  # ------------------------------------------------------------------ #
249
- # Tab 2
250
  # ------------------------------------------------------------------ #
251
- with gr.Tab("2. ML classique sur caractéristiques"):
 
252
  gr.Markdown(
253
- "## Étape 1 Extraction des caractéristiques\n"
254
- "Le backbone ResNet18 préentraîné sur les charbons extrait un vecteur de "
255
- "512 dimensions par image. Cette étape s'exécute sur CPU et ne nécessite "
256
- "aucun GPU."
 
 
 
 
 
257
  )
258
 
259
- extract_btn = gr.Button("Extraire les caractéristiques (backbone gelé)", variant="primary")
260
- extract_status = gr.Textbox(label="Statut de l'extraction", lines=4, interactive=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
 
262
- gr.Markdown("## Étape 2 — Entraîner un classifieur")
 
 
 
 
 
263
 
264
  with gr.Row():
265
  with gr.Column():
266
  clf_type = gr.Radio(
267
  choices=["SVM", "Régression logistique", "k-NN", "Forêt aléatoire", "LDA"],
268
  value="SVM",
269
- label="Algorithme",
270
  )
271
 
272
  with gr.Column(visible=True) as svm_col:
273
  gr.Markdown("#### Paramètres SVM")
274
- svm_c = gr.Number(value=1.0, label="C (régularisation)")
275
- svm_kernel = gr.Dropdown(choices=["rbf", "linear", "poly"], value="rbf", label="Noyau")
276
- svm_gamma = gr.Dropdown(choices=["scale", "auto"], value="scale", label="Gamma")
 
 
 
 
 
 
 
 
 
277
 
278
  with gr.Column(visible=False) as logreg_col:
279
  gr.Markdown("#### Paramètres Régression logistique")
280
- logreg_c = gr.Number(value=1.0, label="C (régularisation)")
281
  logreg_max_iter = gr.Number(value=1000, label="Itérations max")
282
 
283
  with gr.Column(visible=False) as knn_col:
284
  gr.Markdown("#### Paramètres k-NN")
285
- knn_k = gr.Slider(minimum=1, maximum=20, value=5, step=1, label="k (voisins)")
 
 
 
 
286
  knn_metric = gr.Dropdown(
287
- choices=["euclidean", "cosine", "manhattan"], value="euclidean", label="Métrique"
 
288
  )
289
 
290
  with gr.Column(visible=False) as rf_col:
291
  gr.Markdown("#### Paramètres Forêt aléatoire")
292
- rf_n_estimators = gr.Slider(minimum=10, maximum=500, value=100, step=10, label="Nombre d'arbres")
293
- rf_max_depth = gr.Number(value=0, label="Profondeur max (0 = illimitée)")
 
 
 
 
294
 
295
  with gr.Column(visible=False) as lda_col:
296
  gr.Markdown("#### Paramètres LDA")
297
- lda_solver = gr.Dropdown(choices=["svd", "lsqr", "eigen"], value="svd", label="Solveur")
 
 
298
 
299
- ml_model_tag = gr.Textbox(label="Nom court du modèle", placeholder="ex. svm_rbf")
 
 
300
  train_classical_btn = gr.Button("Entraîner le classifieur", variant="primary")
301
 
302
  with gr.Column():
@@ -307,58 +423,15 @@ with gr.Blocks(title="Classification d'images microscopiques") as demo:
307
  ml_cm_img = gr.Image(label="Matrice de confusion — figure", type="filepath")
308
 
309
  # ------------------------------------------------------------------ #
310
- # Tab 3
311
- # ------------------------------------------------------------------ #
312
- with gr.Tab("3. Modèles neuronaux"):
313
- gr.Markdown("## Architecture")
314
-
315
- with gr.Row():
316
- with gr.Column():
317
- neural_type = gr.Radio(
318
- choices=["FC sur backbone préentraîné", "CNN de zéro"],
319
- value="FC sur backbone préentraîné",
320
- label="Type de modèle",
321
- info=(
322
- "FC sur backbone : backbone gelé, seule la tête FC est entraînée — rapide, peu de GPU. "
323
- "CNN de zéro : réseau convolutif entraîné entièrement depuis rien — référence sans transfert."
324
- ),
325
- )
326
-
327
- with gr.Column(visible=False) as cnn_arch_col:
328
- gr.Markdown("#### Architecture CNN")
329
- num_conv_blocks = gr.Slider(minimum=2, maximum=5, value=3, step=1, label="Blocs convolutionnels")
330
- base_filters = gr.Dropdown(choices=[16, 32, 64, 128], value=32, label="Filtres du premier bloc")
331
- kernel_size = gr.Dropdown(choices=[3, 5], value=3, label="Taille du noyau")
332
- use_batchnorm = gr.Checkbox(value=True, label="BatchNorm")
333
-
334
- gr.Markdown("#### Hyperparamètres d'entraînement")
335
- n_dropout = gr.Slider(minimum=0.0, maximum=0.8, value=0.4, step=0.05, label="Dropout")
336
- n_fc_dim = gr.Dropdown(choices=[64, 128, 256, 512], value=256, label="Dimension couche cachée")
337
- n_lr = gr.Number(value=1e-4, label="Taux d'apprentissage")
338
- n_wd = gr.Number(value=1e-4, label="Weight decay")
339
- n_bs = gr.Dropdown(choices=[8, 16, 32, 64], value=16, label="Taille du batch")
340
- n_epochs = gr.Slider(minimum=1, maximum=50, value=20, step=1, label="Époques")
341
- n_tag = gr.Textbox(label="Nom court du modèle", placeholder="ex. fc_head_v1")
342
-
343
- train_neural_btn = gr.Button("Lancer l'entraînement", variant="primary")
344
-
345
- with gr.Column():
346
- neural_logs = gr.Textbox(label="Journal d'entraînement", lines=20)
347
- neural_history = gr.JSON(label="Historique")
348
- neural_summary = gr.JSON(label="Résumé final")
349
-
350
- gr.Markdown("## Résultats sur le test set")
351
- neural_report = gr.Dataframe(label="Rapport de classification", interactive=False)
352
- neural_cm = gr.Dataframe(label="Matrice de confusion", interactive=False)
353
- neural_cm_img = gr.Image(label="Matrice de confusion — figure", type="filepath")
354
-
355
- # ------------------------------------------------------------------ #
356
- # Tab 4
357
  # ------------------------------------------------------------------ #
358
  with gr.Tab("4. Tester et analyser"):
359
- gr.Markdown("## Sélectionner un modèle sauvegardé")
360
  gr.Markdown(
361
- "_Tous les types de modèles apparaissent ici : classifieurs ML, têtes FC et CNN._"
 
 
 
362
  )
363
 
364
  with gr.Row():
@@ -366,14 +439,14 @@ with gr.Blocks(title="Classification d'images microscopiques") as demo:
366
  model_selector = gr.Dropdown(
367
  choices=initial_models,
368
  value=initial_models[0] if initial_models else None,
369
- label="Modèle",
370
  )
371
  refresh_btn = gr.Button("Actualiser la liste")
372
  load_info_btn = gr.Button("Afficher les informations du modèle")
373
- model_info = gr.JSON(label="Métadonnées")
374
 
375
  with gr.Column():
376
- evaluate_btn = gr.Button("Évaluer sur le test set", variant="primary")
377
  eval_summary = gr.JSON(label="Résumé des métriques")
378
 
379
  eval_report = gr.Dataframe(label="Rapport de classification", interactive=False)
@@ -381,18 +454,26 @@ with gr.Blocks(title="Classification d'images microscopiques") as demo:
381
  eval_cm_img = gr.Image(label="Matrice de confusion — figure", type="filepath")
382
 
383
  gr.Markdown("## Prédiction sur une image importée")
 
 
 
 
384
  with gr.Row():
385
  with gr.Column():
386
- upload_image = gr.Image(type="pil", label="Importer une image")
387
  predict_btn = gr.Button("Prédire la classe", variant="primary")
388
  with gr.Column():
389
- predict_text = gr.Textbox(label="Résultat", lines=7)
390
  predict_probs = gr.Label(label="Probabilités par classe")
391
 
392
- gr.Markdown("## Test sur un échantillon aléatoire du test set")
393
- random_test_btn = gr.Button("Tester un échantillon aléatoire")
 
 
 
 
394
  with gr.Row():
395
- random_img = gr.Image(type="pil", label="Image test")
396
  random_text = gr.Textbox(label="Résultat", lines=7)
397
  random_probs = gr.Label(label="Probabilités par classe")
398
 
@@ -412,6 +493,21 @@ with gr.Blocks(title="Classification d'images microscopiques") as demo:
412
  outputs=image_gallery,
413
  )
414
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415
  extract_btn.click(fn=extract_features_callback, inputs=None, outputs=extract_status)
416
 
417
  clf_type.change(
@@ -429,36 +525,16 @@ with gr.Blocks(title="Classification d'images microscopiques") as demo:
429
  knn_k, knn_metric,
430
  rf_n_estimators, rf_max_depth,
431
  lda_solver,
432
- ml_model_tag,
433
  ],
434
  outputs=[ml_summary, ml_report, ml_cm, ml_cm_img, model_selector],
435
  )
436
 
437
- neural_type.change(
438
- fn=on_neural_type_change,
439
- inputs=neural_type,
440
- outputs=[cnn_arch_col, n_lr],
441
- )
442
-
443
- train_neural_btn.click(
444
- fn=train_neural_callback,
445
- inputs=[
446
- neural_type,
447
- num_conv_blocks, base_filters, kernel_size, use_batchnorm,
448
- n_dropout, n_fc_dim,
449
- n_lr, n_wd, n_bs, n_epochs,
450
- n_tag,
451
- ],
452
- outputs=[
453
- neural_logs, neural_history, neural_summary,
454
- neural_report, neural_cm, neural_cm_img,
455
- model_selector,
456
- ],
457
- )
458
-
459
  refresh_btn.click(fn=refresh_models_callback, inputs=None, outputs=model_selector)
460
 
461
- load_info_btn.click(fn=get_model_info_callback, inputs=model_selector, outputs=model_info)
 
 
462
 
463
  evaluate_btn.click(
464
  fn=evaluate_callback,
 
12
  list_saved_models,
13
  model_meta_path,
14
  train_cnn,
 
15
  )
16
 
17
  # ---------------------------------------------------------------------------
 
35
 
36
 
37
  # ---------------------------------------------------------------------------
38
+ # Tab 2 — SimpleCNN
39
+ # ---------------------------------------------------------------------------
40
+
41
+ @spaces.GPU(duration=300)
42
+ def train_cnn_callback(
43
+ num_conv_blocks, base_filters, kernel_size, use_batchnorm,
44
+ dropout, fc_dim,
45
+ learning_rate, weight_decay, batch_size, epochs,
46
+ model_tag,
47
+ ):
48
+ try:
49
+ result = train_cnn(
50
+ num_conv_blocks=int(num_conv_blocks),
51
+ base_filters=int(base_filters),
52
+ kernel_size=int(kernel_size),
53
+ use_batchnorm=bool(use_batchnorm),
54
+ dropout=float(dropout),
55
+ fc_dim=int(fc_dim),
56
+ learning_rate=float(learning_rate),
57
+ weight_decay=float(weight_decay),
58
+ batch_size=int(batch_size),
59
+ epochs=int(epochs),
60
+ model_tag=model_tag,
61
+ )
62
+ models = list_saved_models()
63
+ selected = result["model_name"] if result["model_name"] in models else None
64
+ return (
65
+ result["logs"],
66
+ result["history"],
67
+ result["summary"],
68
+ result["classification_report"],
69
+ result["confusion_matrix"],
70
+ result["confusion_matrix_path"],
71
+ gr.update(choices=models, value=selected),
72
+ )
73
+ except Exception as e:
74
+ return f"Échec de l'entraînement :\n{e}", None, None, None, None, None, gr.update()
75
+
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # Tab 3 — Backbone + ML classique
79
  # ---------------------------------------------------------------------------
80
 
81
  @spaces.GPU(duration=60)
82
  def extract_features_callback():
83
  try:
84
  _, class_names, counts = extract_all_features()
85
+ lines = [f"Extraction terminée{len(class_names)} classes détectées"]
86
  for split, n in counts.items():
87
+ lines.append(f" {split} : {n} images → {n} vecteurs de 512 dimensions")
88
  return "\n".join(lines)
89
  except Exception as e:
90
  return f"Erreur lors de l'extraction :\n{e}"
 
107
  try:
108
  features_cache = get_cached_features()
109
  if features_cache is None:
110
+ return (
111
+ {"Erreur": "Veuillez d'abord extraire les caractéristiques (bouton ci-dessus)."},
112
+ None, None, None, gr.update(),
113
+ )
114
 
115
  params = {}
116
  if clf_type == "SVM":
 
130
 
131
  models = list_saved_models()
132
  selected = result["model_name"] if result["model_name"] in models else None
 
133
  return (
134
  result["summary"],
135
  result["classification_report"],
 
142
 
143
 
144
  # ---------------------------------------------------------------------------
145
+ # Tab 4Tester et analyser
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  # ---------------------------------------------------------------------------
147
 
148
  def refresh_models_callback():
 
192
  initial_models = list_saved_models()
193
 
194
  with gr.Blocks(title="Classification d'images microscopiques") as demo:
195
+
196
  gr.Markdown("# Classification d'images microscopiques de charbons de bois")
197
  gr.Markdown(
198
+ "Ce parcours pédagogique suit une progression en trois étapes : "
199
+ "**exploration des données**, **entraînement d'un CNN de zéro**, "
200
+ "puis **exploitation d'un backbone préentraîné avec des algorithmes classiques**. "
201
+ "L'objectif est de comprendre pourquoi l'apprentissage par transfert est si puissant, "
202
+ "surtout quand les données sont rares."
203
  )
204
 
205
  with gr.Tabs():
206
 
207
  # ------------------------------------------------------------------ #
208
+ # Tab 1 — Explorer le dataset
209
  # ------------------------------------------------------------------ #
210
  with gr.Tab("1. Explorer le jeu de données"):
211
+ gr.Markdown("## Comprendre le problème avant de modéliser")
212
+ gr.Markdown(
213
+ "Avant de choisir un modèle, il est essentiel de comprendre la structure du jeu de données. "
214
+ "Combien de classes ? Combien d'images par classe ? Les classes sont-elles équilibrées ? "
215
+ "Ces questions conditionnent directement les choix de modélisation."
216
+ )
217
 
218
  load_dataset_btn = gr.Button("Charger les informations du dataset", variant="primary")
219
  dataset_summary = gr.JSON(label="Résumé général")
220
  class_distribution = gr.Dataframe(
221
+ label="Distribution des images par split et par classe", interactive=False
222
  )
223
 
224
+ gr.Markdown(
225
+ "## Visualiser les images\n"
226
+ "Parcourez des exemples d'images pour vous familiariser avec les données. "
227
+ "Notez que les images microscopiques de charbons de bois peuvent être "
228
+ "visuellement très similaires d'une espèce à l'autre — ce qui rend la tâche difficile."
229
+ )
230
  with gr.Row():
231
  split_selector = gr.Dropdown(
232
  choices=["train", "validation", "test"], value="train", label="Split"
 
240
  image_gallery = gr.Gallery(label="Exemples d'images", columns=4, height=600)
241
 
242
  # ------------------------------------------------------------------ #
243
+ # Tab 2 — SimpleCNN de zéro
244
  # ------------------------------------------------------------------ #
245
+ with gr.Tab("2. CNN entraîné de zéro"):
246
+ gr.Markdown("## Entraîner un réseau convolutif sans connaissances préalables")
247
  gr.Markdown(
248
+ "La première approche naturelle est de construire un réseau de neurones convolutif (CNN) "
249
+ "et de l'entraîner directement sur nos données de charbons de bois. "
250
+ "Ce réseau part de paramètres aléatoires : il ne sait rien des images au départ.\n\n"
251
+ "**Contexte du problème :** notre jeu de données contient 39 espèces, "
252
+ "avec seulement 8 images par espèce en moyenne. "
253
+ "C'est extrêmement peu pour apprendre à distinguer 39 classes visuellement similaires.\n\n"
254
+ "Jouez avec les paramètres d'architecture et d'entraînement pour observer leur effet "
255
+ "sur les performances. Essayez notamment d'augmenter la complexité du réseau "
256
+ "et observez ce qui se passe."
257
  )
258
 
259
+ with gr.Row():
260
+ with gr.Column():
261
+ gr.Markdown("#### Architecture du CNN")
262
+ num_conv_blocks = gr.Slider(
263
+ minimum=2, maximum=5, value=3, step=1,
264
+ label="Blocs convolutionnels",
265
+ info="Chaque bloc enchaîne Conv2d → BatchNorm → ReLU → MaxPool. Plus de blocs = réseau plus profond.",
266
+ )
267
+ base_filters = gr.Dropdown(
268
+ choices=[16, 32, 64, 128], value=32,
269
+ label="Filtres du premier bloc",
270
+ info="Le nombre de filtres double à chaque bloc. 32 → 64 → 128...",
271
+ )
272
+ kernel_size = gr.Dropdown(
273
+ choices=[3, 5], value=3,
274
+ label="Taille du noyau de convolution",
275
+ info="3×3 capte les détails fins, 5×5 capte des structures plus larges.",
276
+ )
277
+ use_batchnorm = gr.Checkbox(
278
+ value=True, label="Normalisation par lots (BatchNorm)",
279
+ info="Stabilise l'entraînement et accélère la convergence.",
280
+ )
281
+
282
+ gr.Markdown("#### Hyperparamètres d'entraînement")
283
+ cnn_dropout = gr.Slider(
284
+ minimum=0.0, maximum=0.8, value=0.4, step=0.05,
285
+ label="Dropout",
286
+ info="Désactive aléatoirement des neurones pour limiter le surapprentissage.",
287
+ )
288
+ cnn_fc_dim = gr.Dropdown(
289
+ choices=[64, 128, 256, 512], value=256,
290
+ label="Dimension de la couche cachée",
291
+ )
292
+ cnn_lr = gr.Number(value=1e-3, label="Taux d'apprentissage")
293
+ cnn_wd = gr.Number(value=1e-4, label="Weight decay (régularisation L2)")
294
+ cnn_bs = gr.Dropdown(choices=[8, 16, 32, 64], value=16, label="Taille du batch")
295
+ cnn_epochs = gr.Slider(
296
+ minimum=1, maximum=50, value=30, step=1, label="Nombre d'époques"
297
+ )
298
+ cnn_tag = gr.Textbox(
299
+ label="Nom du modèle", placeholder="ex. cnn_3blocs_32filtres"
300
+ )
301
+ train_cnn_btn = gr.Button("Lancer l'entraînement", variant="primary")
302
+
303
+ with gr.Column():
304
+ cnn_logs = gr.Textbox(label="Journal d'entraînement", lines=20)
305
+ cnn_history = gr.JSON(label="Historique époque par époque")
306
+ cnn_summary = gr.JSON(label="Résumé final")
307
+
308
+ gr.Markdown("## Résultats sur le jeu de test")
309
+ cnn_report = gr.Dataframe(label="Rapport de classification", interactive=False)
310
+ cnn_cm = gr.Dataframe(label="Matrice de confusion", interactive=False)
311
+ cnn_cm_img = gr.Image(label="Matrice de confusion — figure", type="filepath")
312
+
313
+ # ------------------------------------------------------------------ #
314
+ # Tab 3 — Backbone préentraîné + ML classique
315
+ # ------------------------------------------------------------------ #
316
+ with gr.Tab("3. Backbone préentraîné + ML classique"):
317
+ gr.Markdown("## Exploiter les connaissances d'un modèle préentraîné")
318
+ gr.Markdown(
319
+ "Face aux limites observées avec le CNN de zéro, une stratégie radicalement différente "
320
+ "consiste à réutiliser un réseau déjà entraîné, et à s'appuyer sur les représentations "
321
+ "qu'il a apprises.\n\n"
322
+ "### Qu'est-ce qu'un backbone ?\n"
323
+ "Un **backbone** est un réseau convolutif dont on retire la couche de classification finale. "
324
+ "Il agit comme un extracteur de caractéristiques : pour chaque image en entrée, "
325
+ "il produit un vecteur de nombres (ici **512 dimensions**) qui encode le contenu visuel "
326
+ "de l'image de façon compacte et abstraite.\n\n"
327
+ "### Quel backbone utilisons-nous ici ?\n"
328
+ "Nous utilisons un **ResNet18 affiné sur notre jeu de données de charbons de bois**. "
329
+ "Ce modèle a d'abord été préentraîné sur ImageNet (1,2 million d'images, 1 000 classes), "
330
+ "puis ses poids ont été ajustés sur nos images microscopiques. "
331
+ "Il a donc appris à reconnaître les structures visuelles propres aux charbons de bois.\n\n"
332
+ "### Pourquoi des algorithmes classiques ensuite ?\n"
333
+ "Une fois les images transformées en vecteurs de 512 dimensions, "
334
+ "n'importe quel algorithme de classification classique peut être appliqué. "
335
+ "Ces algorithmes (SVM, régression logistique, k-NN…) sont rapides à entraîner, "
336
+ "interprétables, et ne nécessitent pas de GPU. "
337
+ "Comparez leurs résultats avec ceux obtenus à l'étape précédente."
338
+ )
339
+
340
+ gr.Markdown("## Étape 1 — Extraction des caractéristiques")
341
+ gr.Markdown(
342
+ "Passez toutes les images du jeu de données dans le backbone. "
343
+ "Chaque image est convertie en un vecteur de 512 valeurs. "
344
+ "Cette opération est réalisée une seule fois et mise en cache."
345
+ )
346
+ extract_btn = gr.Button(
347
+ "Extraire les caractéristiques (backbone gelé)", variant="primary"
348
+ )
349
+ extract_status = gr.Textbox(label="Statut", lines=5, interactive=False)
350
 
351
+ gr.Markdown("## Étape 2 — Entraîner un classifieur sur les caractéristiques")
352
+ gr.Markdown(
353
+ "Choisissez un algorithme et ajustez ses paramètres. "
354
+ "L'entraînement est quasi-instantané car il opère sur des vecteurs, "
355
+ "sans jamais manipuler les images brutes ni utiliser le GPU."
356
+ )
357
 
358
  with gr.Row():
359
  with gr.Column():
360
  clf_type = gr.Radio(
361
  choices=["SVM", "Régression logistique", "k-NN", "Forêt aléatoire", "LDA"],
362
  value="SVM",
363
+ label="Algorithme de classification",
364
  )
365
 
366
  with gr.Column(visible=True) as svm_col:
367
  gr.Markdown("#### Paramètres SVM")
368
+ svm_c = gr.Number(
369
+ value=1.0, label="C force de régularisation",
370
+ info="Une valeur faible regularise davantage (marges plus larges).",
371
+ )
372
+ svm_kernel = gr.Dropdown(
373
+ choices=["rbf", "linear", "poly"], value="rbf",
374
+ label="Noyau",
375
+ info="RBF est généralement le meilleur point de départ.",
376
+ )
377
+ svm_gamma = gr.Dropdown(
378
+ choices=["scale", "auto"], value="scale", label="Gamma"
379
+ )
380
 
381
  with gr.Column(visible=False) as logreg_col:
382
  gr.Markdown("#### Paramètres Régression logistique")
383
+ logreg_c = gr.Number(value=1.0, label="C — force de régularisation")
384
  logreg_max_iter = gr.Number(value=1000, label="Itérations max")
385
 
386
  with gr.Column(visible=False) as knn_col:
387
  gr.Markdown("#### Paramètres k-NN")
388
+ knn_k = gr.Slider(
389
+ minimum=1, maximum=20, value=5, step=1,
390
+ label="k — nombre de voisins",
391
+ info="k=1 mémorise les données, k élevé généralise davantage.",
392
+ )
393
  knn_metric = gr.Dropdown(
394
+ choices=["euclidean", "cosine", "manhattan"],
395
+ value="euclidean", label="Métrique de distance",
396
  )
397
 
398
  with gr.Column(visible=False) as rf_col:
399
  gr.Markdown("#### Paramètres Forêt aléatoire")
400
+ rf_n_estimators = gr.Slider(
401
+ minimum=10, maximum=500, value=100, step=10, label="Nombre d'arbres"
402
+ )
403
+ rf_max_depth = gr.Number(
404
+ value=0, label="Profondeur max (0 = illimitée)"
405
+ )
406
 
407
  with gr.Column(visible=False) as lda_col:
408
  gr.Markdown("#### Paramètres LDA")
409
+ lda_solver = gr.Dropdown(
410
+ choices=["svd", "lsqr", "eigen"], value="svd", label="Solveur"
411
+ )
412
 
413
+ ml_tag = gr.Textbox(
414
+ label="Nom du modèle", placeholder="ex. svm_rbf_C1"
415
+ )
416
  train_classical_btn = gr.Button("Entraîner le classifieur", variant="primary")
417
 
418
  with gr.Column():
 
423
  ml_cm_img = gr.Image(label="Matrice de confusion — figure", type="filepath")
424
 
425
  # ------------------------------------------------------------------ #
426
+ # Tab 4 — Tester et analyser
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
427
  # ------------------------------------------------------------------ #
428
  with gr.Tab("4. Tester et analyser"):
429
+ gr.Markdown("## Comparer et évaluer les modèles")
430
  gr.Markdown(
431
+ "Tous les modèles entraînés dans les onglets précédents apparaissent ici "
432
+ "CNN de zéro comme classifieurs ML. "
433
+ "Évaluez-les sur le jeu de test, prédisez la classe d'une image importée, "
434
+ "et tirez vos conclusions sur l'apport du backbone préentraîné."
435
  )
436
 
437
  with gr.Row():
 
439
  model_selector = gr.Dropdown(
440
  choices=initial_models,
441
  value=initial_models[0] if initial_models else None,
442
+ label="Modèle sauvegardé",
443
  )
444
  refresh_btn = gr.Button("Actualiser la liste")
445
  load_info_btn = gr.Button("Afficher les informations du modèle")
446
+ model_info = gr.JSON(label="Métadonnées du modèle")
447
 
448
  with gr.Column():
449
+ evaluate_btn = gr.Button("Évaluer sur le jeu de test", variant="primary")
450
  eval_summary = gr.JSON(label="Résumé des métriques")
451
 
452
  eval_report = gr.Dataframe(label="Rapport de classification", interactive=False)
 
454
  eval_cm_img = gr.Image(label="Matrice de confusion — figure", type="filepath")
455
 
456
  gr.Markdown("## Prédiction sur une image importée")
457
+ gr.Markdown(
458
+ "Importez une image microscopique de charbon de bois et observez "
459
+ "comment les différents modèles la classifient."
460
+ )
461
  with gr.Row():
462
  with gr.Column():
463
+ upload_image = gr.Image(type="pil", label="Image à classer")
464
  predict_btn = gr.Button("Prédire la classe", variant="primary")
465
  with gr.Column():
466
+ predict_text = gr.Textbox(label="Résultat de la prédiction", lines=7)
467
  predict_probs = gr.Label(label="Probabilités par classe")
468
 
469
+ gr.Markdown("## Test sur un échantillon aléatoire du jeu de test")
470
+ gr.Markdown(
471
+ "Tirez une image au hasard dans le jeu de test et vérifiez si le modèle "
472
+ "sélectionné la classe correctement."
473
+ )
474
+ random_test_btn = gr.Button("Tirer un échantillon aléatoire")
475
  with gr.Row():
476
+ random_img = gr.Image(type="pil", label="Image tirée")
477
  random_text = gr.Textbox(label="Résultat", lines=7)
478
  random_probs = gr.Label(label="Probabilités par classe")
479
 
 
493
  outputs=image_gallery,
494
  )
495
 
496
+ train_cnn_btn.click(
497
+ fn=train_cnn_callback,
498
+ inputs=[
499
+ num_conv_blocks, base_filters, kernel_size, use_batchnorm,
500
+ cnn_dropout, cnn_fc_dim,
501
+ cnn_lr, cnn_wd, cnn_bs, cnn_epochs,
502
+ cnn_tag,
503
+ ],
504
+ outputs=[
505
+ cnn_logs, cnn_history, cnn_summary,
506
+ cnn_report, cnn_cm, cnn_cm_img,
507
+ model_selector,
508
+ ],
509
+ )
510
+
511
  extract_btn.click(fn=extract_features_callback, inputs=None, outputs=extract_status)
512
 
513
  clf_type.change(
 
525
  knn_k, knn_metric,
526
  rf_n_estimators, rf_max_depth,
527
  lda_solver,
528
+ ml_tag,
529
  ],
530
  outputs=[ml_summary, ml_report, ml_cm, ml_cm_img, model_selector],
531
  )
532
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
533
  refresh_btn.click(fn=refresh_models_callback, inputs=None, outputs=model_selector)
534
 
535
+ load_info_btn.click(
536
+ fn=get_model_info_callback, inputs=model_selector, outputs=model_info
537
+ )
538
 
539
  evaluate_btn.click(
540
  fn=evaluate_callback,