Spaces:
Sleeping
Calibrer les durees ZeroGPU sur des mesures reelles et exposer le telechargement des modeles
Browse filesLe duration= des decorateurs @spaces.GPU etait un plafond fixe (300s) quel
que soit l'entrainement demande, alors que ZeroGPU reserve la duree
declaree (pas le temps reellement mesure) sur le quota journalier de la
personne qui clique, pas sur celui du proprietaire du Space. Pour un
compte gratuit (300s/jour), un seul entrainement MLP ou CNN epuisait donc
tout le quota du jour.
MLP et CNN utilisent maintenant une duree dynamique calibree sur des
mesures reelles (66.6s / 140.8s pour le MLP, 65.3s / 153.9s pour le CNN
selon les hyperparametres), plafonnee a 180s pour rester nettement sous
le quota journalier gratuit. Les curseurs d'architecture les plus extremes
(hidden_dim=1024, 5 couches/blocs, filtres=128) sont retires de l'UI :
les mesures montrent que le nombre de parametres surestime tres largement
le cout reel sur ce jeu de donnees minuscule, dont le temps est domine par
un overhead fixe plutot que par les FLOPs du modele.
Ajout d'un composant de telechargement (poids .pt ou pipeline .joblib)
pour le modele selectionne dans l'onglet 5, absent jusqu'ici alors que
c'etait la seule facon de recuperer un modele entraine hors du Space.
- app.py +65 -6
- train_utils.py +11 -0
|
@@ -11,6 +11,7 @@ from train_utils import (
|
|
| 11 |
evaluate_saved_model,
|
| 12 |
list_saved_models,
|
| 13 |
model_meta_path,
|
|
|
|
| 14 |
train_cnn,
|
| 15 |
train_mlp,
|
| 16 |
)
|
|
@@ -39,7 +40,27 @@ def refresh_gallery_callback(split_name, class_name, max_images):
|
|
| 39 |
# Tab 2 — MLP (baseline)
|
| 40 |
# ---------------------------------------------------------------------------
|
| 41 |
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
def train_mlp_callback(
|
| 44 |
num_layers, hidden_dim, dropout,
|
| 45 |
learning_rate, weight_decay, batch_size, epochs,
|
|
@@ -79,7 +100,31 @@ def train_mlp_callback(
|
|
| 79 |
# Tab 3 — SimpleCNN
|
| 80 |
# ---------------------------------------------------------------------------
|
| 81 |
|
| 82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
def train_cnn_callback(
|
| 84 |
num_conv_blocks, base_filters, kernel_size, use_batchnorm,
|
| 85 |
dropout, fc_dim,
|
|
@@ -207,6 +252,15 @@ def get_model_info_callback(model_name, request: gr.Request):
|
|
| 207 |
return {"message": "Métadonnées introuvables."}
|
| 208 |
|
| 209 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
@spaces.GPU(duration=120)
|
| 211 |
def evaluate_callback(model_name, request: gr.Request):
|
| 212 |
try:
|
|
@@ -309,11 +363,11 @@ with gr.Blocks(title="Classification d'images microscopiques") as demo:
|
|
| 309 |
with gr.Column():
|
| 310 |
gr.Markdown("#### Architecture du MLP")
|
| 311 |
mlp_num_layers = gr.Slider(
|
| 312 |
-
minimum=1, maximum=
|
| 313 |
label="Nombre de couches cachées",
|
| 314 |
)
|
| 315 |
mlp_hidden_dim = gr.Dropdown(
|
| 316 |
-
choices=[64, 128, 256, 512
|
| 317 |
label="Neurones par couche cachée",
|
| 318 |
)
|
| 319 |
|
|
@@ -370,12 +424,12 @@ with gr.Blocks(title="Classification d'images microscopiques") as demo:
|
|
| 370 |
with gr.Column():
|
| 371 |
gr.Markdown("#### Architecture du CNN")
|
| 372 |
num_conv_blocks = gr.Slider(
|
| 373 |
-
minimum=2, maximum=
|
| 374 |
label="Blocs convolutionnels",
|
| 375 |
info="Chaque bloc enchaîne Conv2d → BatchNorm → ReLU → MaxPool. Plus de blocs = réseau plus profond.",
|
| 376 |
)
|
| 377 |
base_filters = gr.Dropdown(
|
| 378 |
-
choices=[16, 32, 64
|
| 379 |
label="Filtres du premier bloc",
|
| 380 |
info="Le nombre de filtres double à chaque bloc. 32 → 64 → 128...",
|
| 381 |
)
|
|
@@ -546,6 +600,7 @@ with gr.Blocks(title="Classification d'images microscopiques") as demo:
|
|
| 546 |
refresh_btn = gr.Button("Actualiser la liste")
|
| 547 |
load_info_btn = gr.Button("Afficher les informations du modèle")
|
| 548 |
model_info = gr.JSON(label="Métadonnées du modèle")
|
|
|
|
| 549 |
|
| 550 |
with gr.Column():
|
| 551 |
evaluate_btn = gr.Button("Évaluer sur le jeu de test", variant="primary")
|
|
@@ -652,6 +707,10 @@ with gr.Blocks(title="Classification d'images microscopiques") as demo:
|
|
| 652 |
fn=get_model_info_callback, inputs=model_selector, outputs=model_info
|
| 653 |
)
|
| 654 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 655 |
evaluate_btn.click(
|
| 656 |
fn=evaluate_callback,
|
| 657 |
inputs=model_selector,
|
|
|
|
| 11 |
evaluate_saved_model,
|
| 12 |
list_saved_models,
|
| 13 |
model_meta_path,
|
| 14 |
+
saved_model_file_path,
|
| 15 |
train_cnn,
|
| 16 |
train_mlp,
|
| 17 |
)
|
|
|
|
| 40 |
# Tab 2 — MLP (baseline)
|
| 41 |
# ---------------------------------------------------------------------------
|
| 42 |
|
| 43 |
+
def mlp_gpu_duration(
|
| 44 |
+
num_layers, hidden_dim, dropout,
|
| 45 |
+
learning_rate, weight_decay, batch_size, epochs,
|
| 46 |
+
model_tag,
|
| 47 |
+
request: gr.Request,
|
| 48 |
+
):
|
| 49 |
+
# Calibré sur deux exécutions réelles :
|
| 50 |
+
# 2 couches, hidden_dim=256, epochs=30 -> 66.6s (2.22 s/époque)
|
| 51 |
+
# 5 couches, hidden_dim=1024, epochs=50 -> 140.8s (2.82 s/époque)
|
| 52 |
+
# Le jeu de données est minuscule (peu de pas par époque) : le temps est
|
| 53 |
+
# dominé par un overhead fixe, hidden_dim ne le fait varier que doucement
|
| 54 |
+
# (x4 sur hidden_dim -> seulement +27% par époque). On plafonne à 180s :
|
| 55 |
+
# nettement sous le quota journalier d'un compte gratuit (300s), pour
|
| 56 |
+
# qu'un seul entraînement au pire réglage ne consomme pas tout le quota
|
| 57 |
+
# du jour d'un·e étudiant·e.
|
| 58 |
+
per_epoch = 2.22 + 0.0008 * max(0, int(hidden_dim) - 256)
|
| 59 |
+
estimated = 15 + per_epoch * int(epochs)
|
| 60 |
+
return min(180, max(45, int(estimated * 1.4)))
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
@spaces.GPU(duration=mlp_gpu_duration)
|
| 64 |
def train_mlp_callback(
|
| 65 |
num_layers, hidden_dim, dropout,
|
| 66 |
learning_rate, weight_decay, batch_size, epochs,
|
|
|
|
| 100 |
# Tab 3 — SimpleCNN
|
| 101 |
# ---------------------------------------------------------------------------
|
| 102 |
|
| 103 |
+
def cnn_gpu_duration(
|
| 104 |
+
num_conv_blocks, base_filters, kernel_size, use_batchnorm,
|
| 105 |
+
dropout, fc_dim,
|
| 106 |
+
learning_rate, weight_decay, batch_size, epochs,
|
| 107 |
+
model_tag,
|
| 108 |
+
request: gr.Request,
|
| 109 |
+
):
|
| 110 |
+
# Calibré sur deux exécutions réelles :
|
| 111 |
+
# 3 blocs, filtres=32, noyau=3, epochs=30 -> 65.3s (2.18 s/époque)
|
| 112 |
+
# 5 blocs, filtres=128, noyau=5, epochs=50 -> 153.9s (3.08 s/époque)
|
| 113 |
+
# Le nombre de paramètres varie de ~130x entre ces deux essais mais le temps
|
| 114 |
+
# par époque seulement de 40% : sur ce jeu de données minuscule, le coût est
|
| 115 |
+
# dominé par un overhead fixe (chargement/augmentation), pas par les FLOPs
|
| 116 |
+
# du réseau — le nombre de paramètres surestimerait donc très largement.
|
| 117 |
+
# On interpole plutôt sur un score d'architecture simple. Plafond 180s :
|
| 118 |
+
# nettement sous le quota journalier d'un compte gratuit (300s).
|
| 119 |
+
score = int(num_conv_blocks) * int(base_filters) * (int(kernel_size) / 3)
|
| 120 |
+
baseline_score, worst_score = 96.0, 1066.7
|
| 121 |
+
frac = max(0.0, min(1.0, (score - baseline_score) / (worst_score - baseline_score)))
|
| 122 |
+
per_epoch = 2.18 + 0.9 * frac
|
| 123 |
+
estimated = 15 + per_epoch * int(epochs)
|
| 124 |
+
return min(180, max(45, int(estimated * 1.4)))
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
@spaces.GPU(duration=cnn_gpu_duration)
|
| 128 |
def train_cnn_callback(
|
| 129 |
num_conv_blocks, base_filters, kernel_size, use_batchnorm,
|
| 130 |
dropout, fc_dim,
|
|
|
|
| 252 |
return {"message": "Métadonnées introuvables."}
|
| 253 |
|
| 254 |
|
| 255 |
+
def download_model_callback(model_name, request: gr.Request):
|
| 256 |
+
if not model_name:
|
| 257 |
+
return gr.update(value=None, visible=False)
|
| 258 |
+
try:
|
| 259 |
+
return gr.update(value=saved_model_file_path(model_name, request.session_hash), visible=True)
|
| 260 |
+
except FileNotFoundError:
|
| 261 |
+
return gr.update(value=None, visible=False)
|
| 262 |
+
|
| 263 |
+
|
| 264 |
@spaces.GPU(duration=120)
|
| 265 |
def evaluate_callback(model_name, request: gr.Request):
|
| 266 |
try:
|
|
|
|
| 363 |
with gr.Column():
|
| 364 |
gr.Markdown("#### Architecture du MLP")
|
| 365 |
mlp_num_layers = gr.Slider(
|
| 366 |
+
minimum=1, maximum=4, value=2, step=1,
|
| 367 |
label="Nombre de couches cachées",
|
| 368 |
)
|
| 369 |
mlp_hidden_dim = gr.Dropdown(
|
| 370 |
+
choices=[64, 128, 256, 512], value=256,
|
| 371 |
label="Neurones par couche cachée",
|
| 372 |
)
|
| 373 |
|
|
|
|
| 424 |
with gr.Column():
|
| 425 |
gr.Markdown("#### Architecture du CNN")
|
| 426 |
num_conv_blocks = gr.Slider(
|
| 427 |
+
minimum=2, maximum=4, value=3, step=1,
|
| 428 |
label="Blocs convolutionnels",
|
| 429 |
info="Chaque bloc enchaîne Conv2d → BatchNorm → ReLU → MaxPool. Plus de blocs = réseau plus profond.",
|
| 430 |
)
|
| 431 |
base_filters = gr.Dropdown(
|
| 432 |
+
choices=[16, 32, 64], value=32,
|
| 433 |
label="Filtres du premier bloc",
|
| 434 |
info="Le nombre de filtres double à chaque bloc. 32 → 64 → 128...",
|
| 435 |
)
|
|
|
|
| 600 |
refresh_btn = gr.Button("Actualiser la liste")
|
| 601 |
load_info_btn = gr.Button("Afficher les informations du modèle")
|
| 602 |
model_info = gr.JSON(label="Métadonnées du modèle")
|
| 603 |
+
model_download = gr.File(label="Télécharger le modèle sélectionné", visible=False)
|
| 604 |
|
| 605 |
with gr.Column():
|
| 606 |
evaluate_btn = gr.Button("Évaluer sur le jeu de test", variant="primary")
|
|
|
|
| 707 |
fn=get_model_info_callback, inputs=model_selector, outputs=model_info
|
| 708 |
)
|
| 709 |
|
| 710 |
+
model_selector.change(
|
| 711 |
+
fn=download_model_callback, inputs=model_selector, outputs=model_download
|
| 712 |
+
)
|
| 713 |
+
|
| 714 |
evaluate_btn.click(
|
| 715 |
fn=evaluate_callback,
|
| 716 |
inputs=model_selector,
|
|
@@ -8,6 +8,7 @@ import torch
|
|
| 8 |
import torch.nn as nn
|
| 9 |
import torch.optim as optim
|
| 10 |
|
|
|
|
| 11 |
from config import DATASET_DISPLAY_NAME, CLASSICAL_MODEL_TYPES, IMAGE_SIZE, session_model_dir, session_meta_dir
|
| 12 |
from data_utils import make_loaders
|
| 13 |
from metrics_utils import compute_classification_metrics, save_confusion_matrix_figure, save_loss_curve_figure
|
|
@@ -41,6 +42,16 @@ def get_runtime_device() -> torch.device:
|
|
| 41 |
return torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 42 |
|
| 43 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
# ---------------------------------------------------------------------------
|
| 45 |
# Save / load
|
| 46 |
# ---------------------------------------------------------------------------
|
|
|
|
| 8 |
import torch.nn as nn
|
| 9 |
import torch.optim as optim
|
| 10 |
|
| 11 |
+
from classical_ml_utils import classifier_path
|
| 12 |
from config import DATASET_DISPLAY_NAME, CLASSICAL_MODEL_TYPES, IMAGE_SIZE, session_model_dir, session_meta_dir
|
| 13 |
from data_utils import make_loaders
|
| 14 |
from metrics_utils import compute_classification_metrics, save_confusion_matrix_figure, save_loss_curve_figure
|
|
|
|
| 42 |
return torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 43 |
|
| 44 |
|
| 45 |
+
def saved_model_file_path(model_name: str, session_id: str) -> str:
|
| 46 |
+
"""Chemin du fichier de poids téléchargeable — .joblib pour les
|
| 47 |
+
classifieurs ML classiques, .pt pour les modèles neuronaux."""
|
| 48 |
+
with open(model_meta_path(model_name, session_id), "r", encoding="utf-8") as f:
|
| 49 |
+
model_type = json.load(f)["config"]["model_type"]
|
| 50 |
+
if model_type in CLASSICAL_MODEL_TYPES:
|
| 51 |
+
return classifier_path(model_name, session_id)
|
| 52 |
+
return model_weight_path(model_name, session_id)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
# ---------------------------------------------------------------------------
|
| 56 |
# Save / load
|
| 57 |
# ---------------------------------------------------------------------------
|