Spaces:
Sleeping
Sleeping
functionNormally commited on
Commit ·
c72538b
1
Parent(s): e7db87b
Isoler le stockage des modeles par session et corriger le role de la CV
Browse filesChaque session de navigateur (session_hash Gradio) a maintenant son
propre sous-repertoire dans saved_models/ et saved_models_meta/, pour
qu'un(e) etudiant(e) ne voie plus les modeles entraines par un(e) autre
sur le meme Space partage.
Pour les classifieurs ML classiques, la validation croisee effectue
desormais une vraie recherche par grille (GridSearchCV) sur
l'hyperparametre expose dans l'UI au lieu de se contenter de noter la
valeur choisie a la main : elle selectionne le meilleur candidat, et
c'est ce modele-la qui est sauvegarde et evalue.
- .gitignore +1 -0
- app.py +30 -18
- classical_ml_utils.py +75 -22
- config.py +23 -0
- predict_utils.py +8 -8
- train_utils.py +35 -33
.gitignore
CHANGED
|
@@ -6,3 +6,4 @@ saved_figures/
|
|
| 6 |
__pycache__/
|
| 7 |
*.pyc
|
| 8 |
.DS_Store
|
|
|
|
|
|
| 6 |
__pycache__/
|
| 7 |
*.pyc
|
| 8 |
.DS_Store
|
| 9 |
+
test_api.py
|
app.py
CHANGED
|
@@ -44,9 +44,12 @@ def train_mlp_callback(
|
|
| 44 |
num_layers, hidden_dim, dropout,
|
| 45 |
learning_rate, weight_decay, batch_size, epochs,
|
| 46 |
model_tag,
|
|
|
|
| 47 |
):
|
| 48 |
try:
|
|
|
|
| 49 |
result = train_mlp(
|
|
|
|
| 50 |
num_layers=int(num_layers),
|
| 51 |
hidden_dim=int(hidden_dim),
|
| 52 |
dropout=float(dropout),
|
|
@@ -56,7 +59,7 @@ def train_mlp_callback(
|
|
| 56 |
epochs=int(epochs),
|
| 57 |
model_tag=model_tag,
|
| 58 |
)
|
| 59 |
-
models = list_saved_models()
|
| 60 |
selected = result["model_name"] if result["model_name"] in models else None
|
| 61 |
return (
|
| 62 |
result["logs"],
|
|
@@ -82,9 +85,12 @@ def train_cnn_callback(
|
|
| 82 |
dropout, fc_dim,
|
| 83 |
learning_rate, weight_decay, batch_size, epochs,
|
| 84 |
model_tag,
|
|
|
|
| 85 |
):
|
| 86 |
try:
|
|
|
|
| 87 |
result = train_cnn(
|
|
|
|
| 88 |
num_conv_blocks=int(num_conv_blocks),
|
| 89 |
base_filters=int(base_filters),
|
| 90 |
kernel_size=int(kernel_size),
|
|
@@ -97,7 +103,7 @@ def train_cnn_callback(
|
|
| 97 |
epochs=int(epochs),
|
| 98 |
model_tag=model_tag,
|
| 99 |
)
|
| 100 |
-
models = list_saved_models()
|
| 101 |
selected = result["model_name"] if result["model_name"] in models else None
|
| 102 |
return (
|
| 103 |
result["logs"],
|
|
@@ -142,8 +148,10 @@ def train_classical_callback(
|
|
| 142 |
rf_n_estimators,
|
| 143 |
use_cv,
|
| 144 |
model_tag,
|
|
|
|
| 145 |
):
|
| 146 |
try:
|
|
|
|
| 147 |
features_cache = get_cached_features()
|
| 148 |
if features_cache is None:
|
| 149 |
return (
|
|
@@ -163,10 +171,11 @@ def train_classical_callback(
|
|
| 163 |
|
| 164 |
class_names = get_class_names()
|
| 165 |
result = train_classical_model(
|
| 166 |
-
clf_type, features_cache, class_names,
|
|
|
|
| 167 |
)
|
| 168 |
|
| 169 |
-
models = list_saved_models()
|
| 170 |
selected = result["model_name"] if result["model_name"] in models else None
|
| 171 |
return (
|
| 172 |
result["summary"],
|
|
@@ -183,42 +192,42 @@ def train_classical_callback(
|
|
| 183 |
# Tab 5 — Tester et analyser
|
| 184 |
# ---------------------------------------------------------------------------
|
| 185 |
|
| 186 |
-
def refresh_models_callback():
|
| 187 |
-
models = list_saved_models()
|
| 188 |
return gr.update(choices=models, value=models[0] if models else None)
|
| 189 |
|
| 190 |
|
| 191 |
-
def get_model_info_callback(model_name):
|
| 192 |
if not model_name:
|
| 193 |
return {"message": "Aucun modèle sélectionné."}
|
| 194 |
try:
|
| 195 |
-
with open(model_meta_path(model_name), "r", encoding="utf-8") as f:
|
| 196 |
return json.load(f)
|
| 197 |
except FileNotFoundError:
|
| 198 |
return {"message": "Métadonnées introuvables."}
|
| 199 |
|
| 200 |
|
| 201 |
@spaces.GPU(duration=120)
|
| 202 |
-
def evaluate_callback(model_name):
|
| 203 |
try:
|
| 204 |
-
summary, report_df, cm_df, cm_path = evaluate_saved_model(model_name)
|
| 205 |
return summary, report_df, cm_df, cm_path
|
| 206 |
except Exception as e:
|
| 207 |
return {"Erreur": str(e)}, None, None, None
|
| 208 |
|
| 209 |
|
| 210 |
@spaces.GPU(duration=60)
|
| 211 |
-
def predict_callback(model_name, image):
|
| 212 |
try:
|
| 213 |
-
return predict_uploaded_image(model_name, image)
|
| 214 |
except Exception as e:
|
| 215 |
return f"Échec :\n{e}", None
|
| 216 |
|
| 217 |
|
| 218 |
@spaces.GPU(duration=60)
|
| 219 |
-
def random_test_callback(model_name):
|
| 220 |
try:
|
| 221 |
-
return test_random_sample(model_name)
|
| 222 |
except Exception as e:
|
| 223 |
return None, f"Échec :\n{e}", None
|
| 224 |
|
|
@@ -227,8 +236,6 @@ def random_test_callback(model_name):
|
|
| 227 |
# UI
|
| 228 |
# ---------------------------------------------------------------------------
|
| 229 |
|
| 230 |
-
initial_models = list_saved_models()
|
| 231 |
-
|
| 232 |
with gr.Blocks(title="Classification d'images microscopiques") as demo:
|
| 233 |
|
| 234 |
gr.Markdown("# Classification d'images microscopiques de charbons de bois")
|
|
@@ -531,9 +538,10 @@ with gr.Blocks(title="Classification d'images microscopiques") as demo:
|
|
| 531 |
with gr.Row():
|
| 532 |
with gr.Column():
|
| 533 |
model_selector = gr.Dropdown(
|
| 534 |
-
choices=
|
| 535 |
-
value=
|
| 536 |
label="Modèle sauvegardé",
|
|
|
|
| 537 |
)
|
| 538 |
refresh_btn = gr.Button("Actualiser la liste")
|
| 539 |
load_info_btn = gr.Button("Afficher les informations du modèle")
|
|
@@ -662,6 +670,10 @@ with gr.Blocks(title="Classification d'images microscopiques") as demo:
|
|
| 662 |
outputs=[random_img, random_text, random_probs],
|
| 663 |
)
|
| 664 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 665 |
|
| 666 |
if __name__ == "__main__":
|
| 667 |
demo.launch(ssr_mode=False)
|
|
|
|
| 44 |
num_layers, hidden_dim, dropout,
|
| 45 |
learning_rate, weight_decay, batch_size, epochs,
|
| 46 |
model_tag,
|
| 47 |
+
request: gr.Request,
|
| 48 |
):
|
| 49 |
try:
|
| 50 |
+
session_id = request.session_hash
|
| 51 |
result = train_mlp(
|
| 52 |
+
session_id=session_id,
|
| 53 |
num_layers=int(num_layers),
|
| 54 |
hidden_dim=int(hidden_dim),
|
| 55 |
dropout=float(dropout),
|
|
|
|
| 59 |
epochs=int(epochs),
|
| 60 |
model_tag=model_tag,
|
| 61 |
)
|
| 62 |
+
models = list_saved_models(session_id)
|
| 63 |
selected = result["model_name"] if result["model_name"] in models else None
|
| 64 |
return (
|
| 65 |
result["logs"],
|
|
|
|
| 85 |
dropout, fc_dim,
|
| 86 |
learning_rate, weight_decay, batch_size, epochs,
|
| 87 |
model_tag,
|
| 88 |
+
request: gr.Request,
|
| 89 |
):
|
| 90 |
try:
|
| 91 |
+
session_id = request.session_hash
|
| 92 |
result = train_cnn(
|
| 93 |
+
session_id=session_id,
|
| 94 |
num_conv_blocks=int(num_conv_blocks),
|
| 95 |
base_filters=int(base_filters),
|
| 96 |
kernel_size=int(kernel_size),
|
|
|
|
| 103 |
epochs=int(epochs),
|
| 104 |
model_tag=model_tag,
|
| 105 |
)
|
| 106 |
+
models = list_saved_models(session_id)
|
| 107 |
selected = result["model_name"] if result["model_name"] in models else None
|
| 108 |
return (
|
| 109 |
result["logs"],
|
|
|
|
| 148 |
rf_n_estimators,
|
| 149 |
use_cv,
|
| 150 |
model_tag,
|
| 151 |
+
request: gr.Request,
|
| 152 |
):
|
| 153 |
try:
|
| 154 |
+
session_id = request.session_hash
|
| 155 |
features_cache = get_cached_features()
|
| 156 |
if features_cache is None:
|
| 157 |
return (
|
|
|
|
| 171 |
|
| 172 |
class_names = get_class_names()
|
| 173 |
result = train_classical_model(
|
| 174 |
+
clf_type, features_cache, class_names, session_id,
|
| 175 |
+
model_tag=model_tag, use_cv=bool(use_cv), **params
|
| 176 |
)
|
| 177 |
|
| 178 |
+
models = list_saved_models(session_id)
|
| 179 |
selected = result["model_name"] if result["model_name"] in models else None
|
| 180 |
return (
|
| 181 |
result["summary"],
|
|
|
|
| 192 |
# Tab 5 — Tester et analyser
|
| 193 |
# ---------------------------------------------------------------------------
|
| 194 |
|
| 195 |
+
def refresh_models_callback(request: gr.Request):
|
| 196 |
+
models = list_saved_models(request.session_hash)
|
| 197 |
return gr.update(choices=models, value=models[0] if models else None)
|
| 198 |
|
| 199 |
|
| 200 |
+
def get_model_info_callback(model_name, request: gr.Request):
|
| 201 |
if not model_name:
|
| 202 |
return {"message": "Aucun modèle sélectionné."}
|
| 203 |
try:
|
| 204 |
+
with open(model_meta_path(model_name, request.session_hash), "r", encoding="utf-8") as f:
|
| 205 |
return json.load(f)
|
| 206 |
except FileNotFoundError:
|
| 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:
|
| 213 |
+
summary, report_df, cm_df, cm_path = evaluate_saved_model(model_name, request.session_hash)
|
| 214 |
return summary, report_df, cm_df, cm_path
|
| 215 |
except Exception as e:
|
| 216 |
return {"Erreur": str(e)}, None, None, None
|
| 217 |
|
| 218 |
|
| 219 |
@spaces.GPU(duration=60)
|
| 220 |
+
def predict_callback(model_name, image, request: gr.Request):
|
| 221 |
try:
|
| 222 |
+
return predict_uploaded_image(model_name, image, request.session_hash)
|
| 223 |
except Exception as e:
|
| 224 |
return f"Échec :\n{e}", None
|
| 225 |
|
| 226 |
|
| 227 |
@spaces.GPU(duration=60)
|
| 228 |
+
def random_test_callback(model_name, request: gr.Request):
|
| 229 |
try:
|
| 230 |
+
return test_random_sample(model_name, request.session_hash)
|
| 231 |
except Exception as e:
|
| 232 |
return None, f"Échec :\n{e}", None
|
| 233 |
|
|
|
|
| 236 |
# UI
|
| 237 |
# ---------------------------------------------------------------------------
|
| 238 |
|
|
|
|
|
|
|
| 239 |
with gr.Blocks(title="Classification d'images microscopiques") as demo:
|
| 240 |
|
| 241 |
gr.Markdown("# Classification d'images microscopiques de charbons de bois")
|
|
|
|
| 538 |
with gr.Row():
|
| 539 |
with gr.Column():
|
| 540 |
model_selector = gr.Dropdown(
|
| 541 |
+
choices=[],
|
| 542 |
+
value=None,
|
| 543 |
label="Modèle sauvegardé",
|
| 544 |
+
info="Liste propre à votre session — les modèles des autres étudiant·e·s ne sont pas visibles ici.",
|
| 545 |
)
|
| 546 |
refresh_btn = gr.Button("Actualiser la liste")
|
| 547 |
load_info_btn = gr.Button("Afficher les informations du modèle")
|
|
|
|
| 670 |
outputs=[random_img, random_text, random_probs],
|
| 671 |
)
|
| 672 |
|
| 673 |
+
# Peuple la liste des modèles au chargement de la page, à partir de la
|
| 674 |
+
# session du navigateur qui vient de se connecter (cf. refresh_models_callback).
|
| 675 |
+
demo.load(fn=refresh_models_callback, inputs=None, outputs=model_selector)
|
| 676 |
+
|
| 677 |
|
| 678 |
if __name__ == "__main__":
|
| 679 |
demo.launch(ssr_mode=False)
|
classical_ml_utils.py
CHANGED
|
@@ -7,13 +7,13 @@ from typing import List
|
|
| 7 |
import joblib
|
| 8 |
from sklearn.ensemble import RandomForestClassifier
|
| 9 |
from sklearn.linear_model import LogisticRegression
|
| 10 |
-
from sklearn.model_selection import
|
| 11 |
from sklearn.neighbors import KNeighborsClassifier
|
| 12 |
from sklearn.pipeline import Pipeline
|
| 13 |
from sklearn.preprocessing import StandardScaler
|
| 14 |
from sklearn.svm import SVC
|
| 15 |
|
| 16 |
-
from config import
|
| 17 |
from metrics_utils import compute_classification_metrics, save_confusion_matrix_figure
|
| 18 |
|
| 19 |
CLF_TYPE_MAP = {
|
|
@@ -23,13 +23,25 @@ CLF_TYPE_MAP = {
|
|
| 23 |
"Forêt aléatoire": "rf",
|
| 24 |
}
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
-
def classifier_path(model_name: str) -> str:
|
| 28 |
-
return os.path.join(
|
| 29 |
|
| 30 |
|
| 31 |
-
def meta_path(model_name: str) -> str:
|
| 32 |
-
return os.path.join(
|
| 33 |
|
| 34 |
|
| 35 |
def build_pipeline(clf_type: str, **params) -> Pipeline:
|
|
@@ -67,27 +79,58 @@ def build_pipeline(clf_type: str, **params) -> Pipeline:
|
|
| 67 |
return Pipeline([("scaler", StandardScaler()), ("clf", clf)])
|
| 68 |
|
| 69 |
|
| 70 |
-
def
|
| 71 |
-
"""
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
min_class_count = min(Counter(y_train.tolist()).values())
|
| 74 |
folds = max(2, min(CV_FOLDS, min_class_count))
|
| 75 |
-
|
| 76 |
-
pipeline = build_pipeline(clf_type, **params)
|
| 77 |
skf = StratifiedKFold(n_splits=folds, shuffle=True, random_state=42)
|
| 78 |
-
scores = cross_val_score(pipeline, X_train, y_train, cv=skf, scoring="f1_macro")
|
| 79 |
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
"cv_folds": folds,
|
| 82 |
-
"
|
| 83 |
-
"
|
|
|
|
|
|
|
|
|
|
| 84 |
}
|
|
|
|
| 85 |
|
| 86 |
|
| 87 |
def train_classical_model(
|
| 88 |
clf_type: str,
|
| 89 |
features_cache: dict,
|
| 90 |
class_names: List[str],
|
|
|
|
| 91 |
model_tag: str = "",
|
| 92 |
use_cv: bool = False,
|
| 93 |
**params,
|
|
@@ -97,10 +140,20 @@ def train_classical_model(
|
|
| 97 |
X_test = features_cache["test"]["X"]
|
| 98 |
y_test = features_cache["test"]["y"]
|
| 99 |
|
| 100 |
-
|
| 101 |
|
| 102 |
-
|
| 103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
|
| 105 |
y_pred = pipeline.predict(X_test)
|
| 106 |
metrics = compute_classification_metrics(y_test.tolist(), y_pred.tolist(), class_names)
|
|
@@ -109,7 +162,7 @@ def train_classical_model(
|
|
| 109 |
safe_tag = model_tag.strip().replace(" ", "_") if model_tag.strip() else CLF_TYPE_MAP.get(clf_type, "clf")
|
| 110 |
model_name = f"{safe_tag}_{timestamp}"
|
| 111 |
|
| 112 |
-
joblib.dump(pipeline, classifier_path(model_name))
|
| 113 |
cm_path = save_confusion_matrix_figure(metrics["confusion_matrix"], model_name)
|
| 114 |
|
| 115 |
config_dict = {
|
|
@@ -129,7 +182,7 @@ def train_classical_model(
|
|
| 129 |
**(cv_metrics or {}),
|
| 130 |
}
|
| 131 |
|
| 132 |
-
with open(meta_path(model_name), "w", encoding="utf-8") as f:
|
| 133 |
json.dump(
|
| 134 |
{
|
| 135 |
"model_name": model_name,
|
|
@@ -151,8 +204,8 @@ def train_classical_model(
|
|
| 151 |
}
|
| 152 |
|
| 153 |
|
| 154 |
-
def load_classical_pipeline(model_name: str) -> Pipeline:
|
| 155 |
-
path = classifier_path(model_name)
|
| 156 |
if not os.path.exists(path):
|
| 157 |
raise FileNotFoundError(f"Classifieur introuvable : {model_name}")
|
| 158 |
return joblib.load(path)
|
|
|
|
| 7 |
import joblib
|
| 8 |
from sklearn.ensemble import RandomForestClassifier
|
| 9 |
from sklearn.linear_model import LogisticRegression
|
| 10 |
+
from sklearn.model_selection import GridSearchCV, StratifiedKFold
|
| 11 |
from sklearn.neighbors import KNeighborsClassifier
|
| 12 |
from sklearn.pipeline import Pipeline
|
| 13 |
from sklearn.preprocessing import StandardScaler
|
| 14 |
from sklearn.svm import SVC
|
| 15 |
|
| 16 |
+
from config import CV_FOLDS, session_model_dir, session_meta_dir
|
| 17 |
from metrics_utils import compute_classification_metrics, save_confusion_matrix_figure
|
| 18 |
|
| 19 |
CLF_TYPE_MAP = {
|
|
|
|
| 23 |
"Forêt aléatoire": "rf",
|
| 24 |
}
|
| 25 |
|
| 26 |
+
# Grille de recherche par algorithme : le SEUL hyperparamètre exposé dans
|
| 27 |
+
# l'UI est aussi celui balayé par la validation croisée — c'est ce balayage
|
| 28 |
+
# (et non le score de stabilité d'une valeur choisie à la main) qui constitue
|
| 29 |
+
# le véritable rôle pédagogique de la CV : sélectionner le meilleur
|
| 30 |
+
# hyperparamètre plutôt que de simplement le deviner.
|
| 31 |
+
PARAM_GRIDS = {
|
| 32 |
+
"svm": ("C", [0.01, 0.1, 1.0, 10.0, 100.0]),
|
| 33 |
+
"logreg": ("C", [0.01, 0.1, 1.0, 10.0, 100.0]),
|
| 34 |
+
"knn": ("n_neighbors", [1, 3, 5, 7, 9, 15]),
|
| 35 |
+
"rf": ("n_estimators", [50, 100, 200, 300]),
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
|
| 39 |
+
def classifier_path(model_name: str, session_id: str) -> str:
|
| 40 |
+
return os.path.join(session_model_dir(session_id), f"{model_name}.joblib")
|
| 41 |
|
| 42 |
|
| 43 |
+
def meta_path(model_name: str, session_id: str) -> str:
|
| 44 |
+
return os.path.join(session_meta_dir(session_id), f"{model_name}.json")
|
| 45 |
|
| 46 |
|
| 47 |
def build_pipeline(clf_type: str, **params) -> Pipeline:
|
|
|
|
| 79 |
return Pipeline([("scaler", StandardScaler()), ("clf", clf)])
|
| 80 |
|
| 81 |
|
| 82 |
+
def _grid_search_cv(clf_type: str, X_train, y_train, manual_value, **other_params):
|
| 83 |
+
"""Recherche par grille en validation croisée : le véritable rôle de la CV
|
| 84 |
+
n'est pas de noter la valeur choisie à la main, mais de comparer plusieurs
|
| 85 |
+
valeurs candidates et de sélectionner celle qui généralise le mieux.
|
| 86 |
+
La valeur choisie manuellement par l'étudiant·e est incluse dans la
|
| 87 |
+
grille pour qu'il/elle puisse comparer son choix au choix retenu par CV.
|
| 88 |
+
"""
|
| 89 |
+
key = CLF_TYPE_MAP.get(clf_type, clf_type)
|
| 90 |
+
hp_name, default_grid = PARAM_GRIDS[key]
|
| 91 |
+
candidates = sorted(set(default_grid) | ({manual_value} if manual_value is not None else set()))
|
| 92 |
+
|
| 93 |
min_class_count = min(Counter(y_train.tolist()).values())
|
| 94 |
folds = max(2, min(CV_FOLDS, min_class_count))
|
|
|
|
|
|
|
| 95 |
skf = StratifiedKFold(n_splits=folds, shuffle=True, random_state=42)
|
|
|
|
| 96 |
|
| 97 |
+
pipeline = build_pipeline(clf_type, **{hp_name: candidates[0], **other_params})
|
| 98 |
+
param_grid = {f"clf__{hp_name}": candidates}
|
| 99 |
+
|
| 100 |
+
search = GridSearchCV(
|
| 101 |
+
pipeline, param_grid=param_grid, cv=skf, scoring="f1_macro", refit=True, n_jobs=-1
|
| 102 |
+
)
|
| 103 |
+
search.fit(X_train, y_train)
|
| 104 |
+
|
| 105 |
+
grid_results = []
|
| 106 |
+
for candidate_params, mean_score, std_score in zip(
|
| 107 |
+
search.cv_results_["params"],
|
| 108 |
+
search.cv_results_["mean_test_score"],
|
| 109 |
+
search.cv_results_["std_test_score"],
|
| 110 |
+
):
|
| 111 |
+
grid_results.append({
|
| 112 |
+
hp_name: candidate_params[f"clf__{hp_name}"],
|
| 113 |
+
"cv_f1_macro_mean": round(float(mean_score), 4),
|
| 114 |
+
"cv_f1_macro_std": round(float(std_score), 4),
|
| 115 |
+
})
|
| 116 |
+
grid_results.sort(key=lambda r: r[hp_name])
|
| 117 |
+
|
| 118 |
+
cv_metrics = {
|
| 119 |
"cv_folds": folds,
|
| 120 |
+
"hyperparameter_name": hp_name,
|
| 121 |
+
"manual_value": manual_value,
|
| 122 |
+
"cv_best_value": search.best_params_[f"clf__{hp_name}"],
|
| 123 |
+
"cv_best_f1_macro": round(float(search.best_score_), 4),
|
| 124 |
+
"cv_grid_results": grid_results,
|
| 125 |
}
|
| 126 |
+
return cv_metrics, search.best_estimator_
|
| 127 |
|
| 128 |
|
| 129 |
def train_classical_model(
|
| 130 |
clf_type: str,
|
| 131 |
features_cache: dict,
|
| 132 |
class_names: List[str],
|
| 133 |
+
session_id: str,
|
| 134 |
model_tag: str = "",
|
| 135 |
use_cv: bool = False,
|
| 136 |
**params,
|
|
|
|
| 140 |
X_test = features_cache["test"]["X"]
|
| 141 |
y_test = features_cache["test"]["y"]
|
| 142 |
|
| 143 |
+
key = CLF_TYPE_MAP.get(clf_type, clf_type)
|
| 144 |
|
| 145 |
+
if use_cv:
|
| 146 |
+
hp_name, _ = PARAM_GRIDS[key]
|
| 147 |
+
manual_value = params.get(hp_name)
|
| 148 |
+
other_params = {k: v for k, v in params.items() if k != hp_name}
|
| 149 |
+
cv_metrics, pipeline = _grid_search_cv(clf_type, X_train, y_train, manual_value, **other_params)
|
| 150 |
+
# Le modèle sauvegardé utilise l'hyperparamètre sélectionné par CV,
|
| 151 |
+
# pas nécessairement celui choisi à la main dans l'UI.
|
| 152 |
+
params = {**params, hp_name: cv_metrics["cv_best_value"]}
|
| 153 |
+
else:
|
| 154 |
+
cv_metrics = None
|
| 155 |
+
pipeline = build_pipeline(clf_type, **params)
|
| 156 |
+
pipeline.fit(X_train, y_train)
|
| 157 |
|
| 158 |
y_pred = pipeline.predict(X_test)
|
| 159 |
metrics = compute_classification_metrics(y_test.tolist(), y_pred.tolist(), class_names)
|
|
|
|
| 162 |
safe_tag = model_tag.strip().replace(" ", "_") if model_tag.strip() else CLF_TYPE_MAP.get(clf_type, "clf")
|
| 163 |
model_name = f"{safe_tag}_{timestamp}"
|
| 164 |
|
| 165 |
+
joblib.dump(pipeline, classifier_path(model_name, session_id))
|
| 166 |
cm_path = save_confusion_matrix_figure(metrics["confusion_matrix"], model_name)
|
| 167 |
|
| 168 |
config_dict = {
|
|
|
|
| 182 |
**(cv_metrics or {}),
|
| 183 |
}
|
| 184 |
|
| 185 |
+
with open(meta_path(model_name, session_id), "w", encoding="utf-8") as f:
|
| 186 |
json.dump(
|
| 187 |
{
|
| 188 |
"model_name": model_name,
|
|
|
|
| 204 |
}
|
| 205 |
|
| 206 |
|
| 207 |
+
def load_classical_pipeline(model_name: str, session_id: str) -> Pipeline:
|
| 208 |
+
path = classifier_path(model_name, session_id)
|
| 209 |
if not os.path.exists(path):
|
| 210 |
raise FileNotFoundError(f"Classifieur introuvable : {model_name}")
|
| 211 |
return joblib.load(path)
|
config.py
CHANGED
|
@@ -13,6 +13,29 @@ os.makedirs(FIGURE_DIR, exist_ok=True)
|
|
| 13 |
HF_DATASET_REPO = os.environ.get("HF_DATASET_REPO", "CircleStar/charcoal-microscopy")
|
| 14 |
HF_TOKEN = os.environ.get("HF_TOKEN")
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
IMAGE_SIZE = 224
|
| 17 |
RANDOM_SEED = 42
|
| 18 |
|
|
|
|
| 13 |
HF_DATASET_REPO = os.environ.get("HF_DATASET_REPO", "CircleStar/charcoal-microscopy")
|
| 14 |
HF_TOKEN = os.environ.get("HF_TOKEN")
|
| 15 |
|
| 16 |
+
|
| 17 |
+
def _sanitize_session_id(session_id: str) -> str:
|
| 18 |
+
"""Un session_hash Gradio est déjà un identifiant sûr, mais on filtre par
|
| 19 |
+
prudence pour ne jamais laisser un caractère de traversée de chemin
|
| 20 |
+
(../) atteindre os.path.join."""
|
| 21 |
+
safe = "".join(c for c in (session_id or "") if c.isalnum() or c in "-_")
|
| 22 |
+
return safe or "default"
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def session_model_dir(session_id: str) -> str:
|
| 26 |
+
"""Répertoire des poids/pipelines pour UNE session de navigateur — isole
|
| 27 |
+
les modèles entraînés par un·e étudiant·e de ceux des autres, qui
|
| 28 |
+
partagent pourtant le même Space Gradio."""
|
| 29 |
+
d = os.path.join(MODEL_DIR, _sanitize_session_id(session_id))
|
| 30 |
+
os.makedirs(d, exist_ok=True)
|
| 31 |
+
return d
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def session_meta_dir(session_id: str) -> str:
|
| 35 |
+
d = os.path.join(META_DIR, _sanitize_session_id(session_id))
|
| 36 |
+
os.makedirs(d, exist_ok=True)
|
| 37 |
+
return d
|
| 38 |
+
|
| 39 |
IMAGE_SIZE = 224
|
| 40 |
RANDOM_SEED = 42
|
| 41 |
|
predict_utils.py
CHANGED
|
@@ -19,25 +19,25 @@ def _extract_feature(image: Image.Image, device: torch.device) -> np.ndarray:
|
|
| 19 |
return feat.cpu().numpy()
|
| 20 |
|
| 21 |
|
| 22 |
-
def predict_uploaded_image(model_name: str, image: Image.Image):
|
| 23 |
if not model_name:
|
| 24 |
return "Veuillez sélectionner un modèle.", None
|
| 25 |
if image is None:
|
| 26 |
return "Veuillez importer une image.", None
|
| 27 |
|
| 28 |
-
meta = _load_meta(model_name)
|
| 29 |
model_type = meta["config"].get("model_type", "cnn")
|
| 30 |
class_names = meta["config"]["class_names"]
|
| 31 |
device = get_runtime_device()
|
| 32 |
|
| 33 |
if model_type in CLASSICAL_MODEL_TYPES:
|
| 34 |
from classical_ml_utils import load_classical_pipeline
|
| 35 |
-
pipeline = load_classical_pipeline(model_name)
|
| 36 |
feat = _extract_feature(image, device)
|
| 37 |
probs = pipeline.predict_proba(feat)[0].tolist()
|
| 38 |
pred_idx = int(np.argmax(probs))
|
| 39 |
else:
|
| 40 |
-
model, _ = load_model(model_name, device)
|
| 41 |
tensor = get_eval_transform()(image.convert("RGB")).unsqueeze(0).to(device)
|
| 42 |
with torch.no_grad():
|
| 43 |
logits = model(tensor)
|
|
@@ -55,11 +55,11 @@ def predict_uploaded_image(model_name: str, image: Image.Image):
|
|
| 55 |
return result_text, prob_dict
|
| 56 |
|
| 57 |
|
| 58 |
-
def test_random_sample(model_name: str):
|
| 59 |
if not model_name:
|
| 60 |
return None, "Veuillez sélectionner un modèle.", None
|
| 61 |
|
| 62 |
-
meta = _load_meta(model_name)
|
| 63 |
model_type = meta["config"].get("model_type", "cnn")
|
| 64 |
class_names = get_class_names()
|
| 65 |
device = get_runtime_device()
|
|
@@ -77,12 +77,12 @@ def test_random_sample(model_name: str):
|
|
| 77 |
|
| 78 |
if model_type in CLASSICAL_MODEL_TYPES:
|
| 79 |
from classical_ml_utils import load_classical_pipeline
|
| 80 |
-
pipeline = load_classical_pipeline(model_name)
|
| 81 |
feat = _extract_feature(image, device)
|
| 82 |
probs = pipeline.predict_proba(feat)[0].tolist()
|
| 83 |
pred_idx = int(np.argmax(probs))
|
| 84 |
else:
|
| 85 |
-
model, _ = load_model(model_name, device)
|
| 86 |
tensor = get_eval_transform()(image).unsqueeze(0).to(device)
|
| 87 |
with torch.no_grad():
|
| 88 |
logits = model(tensor)
|
|
|
|
| 19 |
return feat.cpu().numpy()
|
| 20 |
|
| 21 |
|
| 22 |
+
def predict_uploaded_image(model_name: str, image: Image.Image, session_id: str):
|
| 23 |
if not model_name:
|
| 24 |
return "Veuillez sélectionner un modèle.", None
|
| 25 |
if image is None:
|
| 26 |
return "Veuillez importer une image.", None
|
| 27 |
|
| 28 |
+
meta = _load_meta(model_name, session_id)
|
| 29 |
model_type = meta["config"].get("model_type", "cnn")
|
| 30 |
class_names = meta["config"]["class_names"]
|
| 31 |
device = get_runtime_device()
|
| 32 |
|
| 33 |
if model_type in CLASSICAL_MODEL_TYPES:
|
| 34 |
from classical_ml_utils import load_classical_pipeline
|
| 35 |
+
pipeline = load_classical_pipeline(model_name, session_id)
|
| 36 |
feat = _extract_feature(image, device)
|
| 37 |
probs = pipeline.predict_proba(feat)[0].tolist()
|
| 38 |
pred_idx = int(np.argmax(probs))
|
| 39 |
else:
|
| 40 |
+
model, _ = load_model(model_name, device, session_id)
|
| 41 |
tensor = get_eval_transform()(image.convert("RGB")).unsqueeze(0).to(device)
|
| 42 |
with torch.no_grad():
|
| 43 |
logits = model(tensor)
|
|
|
|
| 55 |
return result_text, prob_dict
|
| 56 |
|
| 57 |
|
| 58 |
+
def test_random_sample(model_name: str, session_id: str):
|
| 59 |
if not model_name:
|
| 60 |
return None, "Veuillez sélectionner un modèle.", None
|
| 61 |
|
| 62 |
+
meta = _load_meta(model_name, session_id)
|
| 63 |
model_type = meta["config"].get("model_type", "cnn")
|
| 64 |
class_names = get_class_names()
|
| 65 |
device = get_runtime_device()
|
|
|
|
| 77 |
|
| 78 |
if model_type in CLASSICAL_MODEL_TYPES:
|
| 79 |
from classical_ml_utils import load_classical_pipeline
|
| 80 |
+
pipeline = load_classical_pipeline(model_name, session_id)
|
| 81 |
feat = _extract_feature(image, device)
|
| 82 |
probs = pipeline.predict_proba(feat)[0].tolist()
|
| 83 |
pred_idx = int(np.argmax(probs))
|
| 84 |
else:
|
| 85 |
+
model, _ = load_model(model_name, device, session_id)
|
| 86 |
tensor = get_eval_transform()(image).unsqueeze(0).to(device)
|
| 87 |
with torch.no_grad():
|
| 88 |
logits = model(tensor)
|
train_utils.py
CHANGED
|
@@ -8,31 +8,30 @@ import torch
|
|
| 8 |
import torch.nn as nn
|
| 9 |
import torch.optim as optim
|
| 10 |
|
| 11 |
-
from config import
|
| 12 |
from data_utils import make_loaders
|
| 13 |
from metrics_utils import compute_classification_metrics, save_confusion_matrix_figure, save_loss_curve_figure
|
| 14 |
from model import SimpleCNN, BackboneWithFC, MLP
|
| 15 |
|
| 16 |
|
| 17 |
# ---------------------------------------------------------------------------
|
| 18 |
-
# Path helpers
|
|
|
|
|
|
|
| 19 |
# ---------------------------------------------------------------------------
|
| 20 |
|
| 21 |
-
def model_weight_path(model_name: str) -> str:
|
| 22 |
-
return os.path.join(
|
| 23 |
|
| 24 |
|
| 25 |
-
def
|
| 26 |
-
return os.path.join(
|
| 27 |
|
| 28 |
|
| 29 |
-
def
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
def list_saved_models() -> List[str]:
|
| 34 |
names = []
|
| 35 |
-
for fn in os.listdir(
|
| 36 |
if fn.endswith(".json"):
|
| 37 |
names.append(fn[:-5])
|
| 38 |
return sorted(names, reverse=True)
|
|
@@ -46,15 +45,15 @@ def get_runtime_device() -> torch.device:
|
|
| 46 |
# Save / load
|
| 47 |
# ---------------------------------------------------------------------------
|
| 48 |
|
| 49 |
-
def save_model(model: nn.Module, model_name: str, config: dict, training_summary: dict):
|
| 50 |
if config["model_type"] == "fc_head":
|
| 51 |
state_dict = {k: v.detach().cpu() for k, v in model.classifier.state_dict().items()}
|
| 52 |
else:
|
| 53 |
state_dict = {k: v.detach().cpu() for k, v in model.state_dict().items()}
|
| 54 |
|
| 55 |
-
torch.save(state_dict, model_weight_path(model_name))
|
| 56 |
|
| 57 |
-
with open(model_meta_path(model_name), "w", encoding="utf-8") as f:
|
| 58 |
json.dump(
|
| 59 |
{
|
| 60 |
"model_name": model_name,
|
|
@@ -68,16 +67,16 @@ def save_model(model: nn.Module, model_name: str, config: dict, training_summary
|
|
| 68 |
)
|
| 69 |
|
| 70 |
|
| 71 |
-
def _load_meta(model_name: str) -> dict:
|
| 72 |
-
path = model_meta_path(model_name)
|
| 73 |
if not os.path.exists(path):
|
| 74 |
raise FileNotFoundError(f"Métadonnées introuvables : {model_name}")
|
| 75 |
with open(path, "r", encoding="utf-8") as f:
|
| 76 |
return json.load(f)
|
| 77 |
|
| 78 |
|
| 79 |
-
def load_model(model_name: str, device: torch.device) -> Tuple[nn.Module, dict]:
|
| 80 |
-
meta = _load_meta(model_name)
|
| 81 |
cfg = meta["config"]
|
| 82 |
model_type = cfg.get("model_type", "cnn")
|
| 83 |
|
|
@@ -86,7 +85,7 @@ def load_model(model_name: str, device: torch.device) -> Tuple[nn.Module, dict]:
|
|
| 86 |
backbone = load_backbone(device)
|
| 87 |
model = BackboneWithFC(backbone, cfg["num_classes"], cfg.get("dropout", 0.4), cfg.get("fc_dim", 256))
|
| 88 |
model.classifier.load_state_dict(
|
| 89 |
-
torch.load(model_weight_path(model_name), map_location="cpu")
|
| 90 |
)
|
| 91 |
|
| 92 |
elif model_type == "cnn":
|
|
@@ -99,7 +98,7 @@ def load_model(model_name: str, device: torch.device) -> Tuple[nn.Module, dict]:
|
|
| 99 |
dropout=cfg.get("dropout", 0.4),
|
| 100 |
fc_dim=cfg.get("fc_dim", 256),
|
| 101 |
)
|
| 102 |
-
model.load_state_dict(torch.load(model_weight_path(model_name), map_location="cpu"))
|
| 103 |
|
| 104 |
elif model_type == "mlp":
|
| 105 |
model = MLP(
|
|
@@ -109,7 +108,7 @@ def load_model(model_name: str, device: torch.device) -> Tuple[nn.Module, dict]:
|
|
| 109 |
hidden_dim=cfg.get("hidden_dim", 256),
|
| 110 |
dropout=cfg.get("dropout", 0.4),
|
| 111 |
)
|
| 112 |
-
model.load_state_dict(torch.load(model_weight_path(model_name), map_location="cpu"))
|
| 113 |
|
| 114 |
else:
|
| 115 |
raise ValueError(f"load_model n'accepte pas le type '{model_type}'. Utilisez load_classical_pipeline pour les modèles ML classiques.")
|
|
@@ -207,6 +206,7 @@ def _training_loop(model, train_loader, val_loader, criterion, optimizer, schedu
|
|
| 207 |
# ---------------------------------------------------------------------------
|
| 208 |
|
| 209 |
def train_fc_head(
|
|
|
|
| 210 |
dropout: float = 0.4,
|
| 211 |
fc_dim: int = 256,
|
| 212 |
learning_rate: float = 1e-4,
|
|
@@ -281,7 +281,7 @@ def train_fc_head(
|
|
| 281 |
"trainable_params": trainable_params,
|
| 282 |
}
|
| 283 |
|
| 284 |
-
save_model(model, model_name, config, training_summary)
|
| 285 |
|
| 286 |
logs += [
|
| 287 |
"",
|
|
@@ -310,6 +310,7 @@ def train_fc_head(
|
|
| 310 |
# ---------------------------------------------------------------------------
|
| 311 |
|
| 312 |
def train_cnn(
|
|
|
|
| 313 |
num_conv_blocks: int = 3,
|
| 314 |
base_filters: int = 32,
|
| 315 |
kernel_size: int = 3,
|
|
@@ -399,7 +400,7 @@ def train_cnn(
|
|
| 399 |
"trainable_params": trainable_params,
|
| 400 |
}
|
| 401 |
|
| 402 |
-
save_model(model, model_name, config, training_summary)
|
| 403 |
|
| 404 |
logs += [
|
| 405 |
"",
|
|
@@ -429,6 +430,7 @@ def train_cnn(
|
|
| 429 |
# ---------------------------------------------------------------------------
|
| 430 |
|
| 431 |
def train_mlp(
|
|
|
|
| 432 |
num_layers: int = 2,
|
| 433 |
hidden_dim: int = 256,
|
| 434 |
dropout: float = 0.4,
|
|
@@ -512,7 +514,7 @@ def train_mlp(
|
|
| 512 |
"trainable_params": trainable_params,
|
| 513 |
}
|
| 514 |
|
| 515 |
-
save_model(model, model_name, config, training_summary)
|
| 516 |
|
| 517 |
logs += [
|
| 518 |
"",
|
|
@@ -541,22 +543,22 @@ def train_mlp(
|
|
| 541 |
# Evaluate any saved model
|
| 542 |
# ---------------------------------------------------------------------------
|
| 543 |
|
| 544 |
-
def evaluate_saved_model(model_name: str):
|
| 545 |
if not model_name:
|
| 546 |
raise ValueError("Aucun modèle sélectionné.")
|
| 547 |
|
| 548 |
-
meta = _load_meta(model_name)
|
| 549 |
model_type = meta["config"].get("model_type", "cnn")
|
| 550 |
|
| 551 |
if model_type in CLASSICAL_MODEL_TYPES:
|
| 552 |
-
return _evaluate_classical(model_name, meta)
|
| 553 |
else:
|
| 554 |
-
return _evaluate_neural(model_name, meta)
|
| 555 |
|
| 556 |
|
| 557 |
-
def _evaluate_neural(model_name: str, meta: dict):
|
| 558 |
device = get_runtime_device()
|
| 559 |
-
model, meta = load_model(model_name, device)
|
| 560 |
|
| 561 |
batch_size = int(meta["config"].get("batch_size", 16))
|
| 562 |
_, _, test_loader, class_names = make_loaders(batch_size)
|
|
@@ -582,7 +584,7 @@ def _evaluate_neural(model_name: str, meta: dict):
|
|
| 582 |
)
|
| 583 |
|
| 584 |
|
| 585 |
-
def _evaluate_classical(model_name: str, meta: dict):
|
| 586 |
from backbone_utils import get_cached_features, extract_all_features
|
| 587 |
from classical_ml_utils import load_classical_pipeline
|
| 588 |
|
|
@@ -591,7 +593,7 @@ def _evaluate_classical(model_name: str, meta: dict):
|
|
| 591 |
features_cache, _, _ = extract_all_features()
|
| 592 |
|
| 593 |
class_names = meta["config"]["class_names"]
|
| 594 |
-
pipeline = load_classical_pipeline(model_name)
|
| 595 |
|
| 596 |
X_test = features_cache["test"]["X"]
|
| 597 |
y_test = features_cache["test"]["y"]
|
|
|
|
| 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
|
| 14 |
from model import SimpleCNN, BackboneWithFC, MLP
|
| 15 |
|
| 16 |
|
| 17 |
# ---------------------------------------------------------------------------
|
| 18 |
+
# Path helpers — tout est scopé par session_id (= session_hash Gradio, un par
|
| 19 |
+
# navigateur/onglet) pour qu'un·e étudiant·e ne voie jamais les modèles d'un·e
|
| 20 |
+
# autre alors que tous partagent le même Space.
|
| 21 |
# ---------------------------------------------------------------------------
|
| 22 |
|
| 23 |
+
def model_weight_path(model_name: str, session_id: str) -> str:
|
| 24 |
+
return os.path.join(session_model_dir(session_id), f"{model_name}.pt")
|
| 25 |
|
| 26 |
|
| 27 |
+
def model_meta_path(model_name: str, session_id: str) -> str:
|
| 28 |
+
return os.path.join(session_meta_dir(session_id), f"{model_name}.json")
|
| 29 |
|
| 30 |
|
| 31 |
+
def list_saved_models(session_id: str) -> List[str]:
|
| 32 |
+
meta_dir = session_meta_dir(session_id)
|
|
|
|
|
|
|
|
|
|
| 33 |
names = []
|
| 34 |
+
for fn in os.listdir(meta_dir):
|
| 35 |
if fn.endswith(".json"):
|
| 36 |
names.append(fn[:-5])
|
| 37 |
return sorted(names, reverse=True)
|
|
|
|
| 45 |
# Save / load
|
| 46 |
# ---------------------------------------------------------------------------
|
| 47 |
|
| 48 |
+
def save_model(model: nn.Module, model_name: str, config: dict, training_summary: dict, session_id: str):
|
| 49 |
if config["model_type"] == "fc_head":
|
| 50 |
state_dict = {k: v.detach().cpu() for k, v in model.classifier.state_dict().items()}
|
| 51 |
else:
|
| 52 |
state_dict = {k: v.detach().cpu() for k, v in model.state_dict().items()}
|
| 53 |
|
| 54 |
+
torch.save(state_dict, model_weight_path(model_name, session_id))
|
| 55 |
|
| 56 |
+
with open(model_meta_path(model_name, session_id), "w", encoding="utf-8") as f:
|
| 57 |
json.dump(
|
| 58 |
{
|
| 59 |
"model_name": model_name,
|
|
|
|
| 67 |
)
|
| 68 |
|
| 69 |
|
| 70 |
+
def _load_meta(model_name: str, session_id: str) -> dict:
|
| 71 |
+
path = model_meta_path(model_name, session_id)
|
| 72 |
if not os.path.exists(path):
|
| 73 |
raise FileNotFoundError(f"Métadonnées introuvables : {model_name}")
|
| 74 |
with open(path, "r", encoding="utf-8") as f:
|
| 75 |
return json.load(f)
|
| 76 |
|
| 77 |
|
| 78 |
+
def load_model(model_name: str, device: torch.device, session_id: str) -> Tuple[nn.Module, dict]:
|
| 79 |
+
meta = _load_meta(model_name, session_id)
|
| 80 |
cfg = meta["config"]
|
| 81 |
model_type = cfg.get("model_type", "cnn")
|
| 82 |
|
|
|
|
| 85 |
backbone = load_backbone(device)
|
| 86 |
model = BackboneWithFC(backbone, cfg["num_classes"], cfg.get("dropout", 0.4), cfg.get("fc_dim", 256))
|
| 87 |
model.classifier.load_state_dict(
|
| 88 |
+
torch.load(model_weight_path(model_name, session_id), map_location="cpu")
|
| 89 |
)
|
| 90 |
|
| 91 |
elif model_type == "cnn":
|
|
|
|
| 98 |
dropout=cfg.get("dropout", 0.4),
|
| 99 |
fc_dim=cfg.get("fc_dim", 256),
|
| 100 |
)
|
| 101 |
+
model.load_state_dict(torch.load(model_weight_path(model_name, session_id), map_location="cpu"))
|
| 102 |
|
| 103 |
elif model_type == "mlp":
|
| 104 |
model = MLP(
|
|
|
|
| 108 |
hidden_dim=cfg.get("hidden_dim", 256),
|
| 109 |
dropout=cfg.get("dropout", 0.4),
|
| 110 |
)
|
| 111 |
+
model.load_state_dict(torch.load(model_weight_path(model_name, session_id), map_location="cpu"))
|
| 112 |
|
| 113 |
else:
|
| 114 |
raise ValueError(f"load_model n'accepte pas le type '{model_type}'. Utilisez load_classical_pipeline pour les modèles ML classiques.")
|
|
|
|
| 206 |
# ---------------------------------------------------------------------------
|
| 207 |
|
| 208 |
def train_fc_head(
|
| 209 |
+
session_id: str,
|
| 210 |
dropout: float = 0.4,
|
| 211 |
fc_dim: int = 256,
|
| 212 |
learning_rate: float = 1e-4,
|
|
|
|
| 281 |
"trainable_params": trainable_params,
|
| 282 |
}
|
| 283 |
|
| 284 |
+
save_model(model, model_name, config, training_summary, session_id)
|
| 285 |
|
| 286 |
logs += [
|
| 287 |
"",
|
|
|
|
| 310 |
# ---------------------------------------------------------------------------
|
| 311 |
|
| 312 |
def train_cnn(
|
| 313 |
+
session_id: str,
|
| 314 |
num_conv_blocks: int = 3,
|
| 315 |
base_filters: int = 32,
|
| 316 |
kernel_size: int = 3,
|
|
|
|
| 400 |
"trainable_params": trainable_params,
|
| 401 |
}
|
| 402 |
|
| 403 |
+
save_model(model, model_name, config, training_summary, session_id)
|
| 404 |
|
| 405 |
logs += [
|
| 406 |
"",
|
|
|
|
| 430 |
# ---------------------------------------------------------------------------
|
| 431 |
|
| 432 |
def train_mlp(
|
| 433 |
+
session_id: str,
|
| 434 |
num_layers: int = 2,
|
| 435 |
hidden_dim: int = 256,
|
| 436 |
dropout: float = 0.4,
|
|
|
|
| 514 |
"trainable_params": trainable_params,
|
| 515 |
}
|
| 516 |
|
| 517 |
+
save_model(model, model_name, config, training_summary, session_id)
|
| 518 |
|
| 519 |
logs += [
|
| 520 |
"",
|
|
|
|
| 543 |
# Evaluate any saved model
|
| 544 |
# ---------------------------------------------------------------------------
|
| 545 |
|
| 546 |
+
def evaluate_saved_model(model_name: str, session_id: str):
|
| 547 |
if not model_name:
|
| 548 |
raise ValueError("Aucun modèle sélectionné.")
|
| 549 |
|
| 550 |
+
meta = _load_meta(model_name, session_id)
|
| 551 |
model_type = meta["config"].get("model_type", "cnn")
|
| 552 |
|
| 553 |
if model_type in CLASSICAL_MODEL_TYPES:
|
| 554 |
+
return _evaluate_classical(model_name, meta, session_id)
|
| 555 |
else:
|
| 556 |
+
return _evaluate_neural(model_name, meta, session_id)
|
| 557 |
|
| 558 |
|
| 559 |
+
def _evaluate_neural(model_name: str, meta: dict, session_id: str):
|
| 560 |
device = get_runtime_device()
|
| 561 |
+
model, meta = load_model(model_name, device, session_id)
|
| 562 |
|
| 563 |
batch_size = int(meta["config"].get("batch_size", 16))
|
| 564 |
_, _, test_loader, class_names = make_loaders(batch_size)
|
|
|
|
| 584 |
)
|
| 585 |
|
| 586 |
|
| 587 |
+
def _evaluate_classical(model_name: str, meta: dict, session_id: str):
|
| 588 |
from backbone_utils import get_cached_features, extract_all_features
|
| 589 |
from classical_ml_utils import load_classical_pipeline
|
| 590 |
|
|
|
|
| 593 |
features_cache, _, _ = extract_all_features()
|
| 594 |
|
| 595 |
class_names = meta["config"]["class_names"]
|
| 596 |
+
pipeline = load_classical_pipeline(model_name, session_id)
|
| 597 |
|
| 598 |
X_test = features_cache["test"]["X"]
|
| 599 |
y_test = features_cache["test"]["y"]
|