vimdhayak commited on
Commit
292b6c2
·
verified ·
1 Parent(s): 804dc72

Upload 7 files

Browse files
README.md CHANGED
@@ -1,13 +1,39 @@
1
  ---
2
- title: LCVC Ensemble
3
- emoji: 🐠
4
- colorFrom: yellow
5
- colorTo: pink
6
  sdk: gradio
7
- sdk_version: 6.14.0
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: LCVC-Ensemble Brain Tumor MRI Classifier
 
 
 
3
  sdk: gradio
 
 
4
  app_file: app.py
5
  pinned: false
6
+ license: mit
7
  ---
8
 
9
+ # LCVC-Ensemble Brain Tumor MRI Classifier
10
+
11
+ This Space contains a research demonstration of **LCVC-Ensemble**
12
+ (**Leakage-Controlled Validation-Calibrated Multi-Backbone Ensemble**) for four-class brain tumor MRI classification.
13
+
14
+ ## Classes
15
+
16
+ glioma, meningioma, notumor, pituitary
17
+
18
+ ## Test metrics from the leakage-aware evaluation protocol
19
+
20
+ - Accuracy: 0.9887640449438202
21
+ - Macro-F1: 0.9887621278421996
22
+ - Balanced accuracy: 0.9887638076313365
23
+ - Macro-AUC OVR: 0.9997131229291261
24
+
25
+ ## Ensemble
26
+
27
+ The app loads `5` selected checkpoints and performs weighted averaging of temperature-scaled softmax probabilities.
28
+
29
+ ## Intended use
30
+
31
+ Research and educational demonstration only.
32
+
33
+ ## Medical disclaimer
34
+
35
+ This is **not a medical device**. It must not be used for diagnosis, treatment, triage, or clinical decision-making.
36
+
37
+ ## Reproducibility note
38
+
39
+ The ensemble was selected using validation Macro-F1 only. Test labels were used only for final reporting after model selection.
app.py ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import json
3
+ from pathlib import Path
4
+
5
+ import gradio as gr
6
+ import numpy as np
7
+ import torch
8
+ import torch.nn as nn
9
+ import torch.nn.functional as F
10
+ from PIL import Image
11
+ from torchvision import models, transforms
12
+
13
+
14
+ ROOT = Path(__file__).resolve().parent
15
+ CONFIG_PATH = ROOT / "ensemble_config.json"
16
+
17
+ with open(CONFIG_PATH, "r") as f:
18
+ CFG = json.load(f)
19
+
20
+ CLASS_NAMES = CFG["classes"]
21
+ NUM_CLASSES = int(CFG["num_classes"])
22
+ IMAGE_SIZE = int(CFG["image_size"])
23
+ DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
24
+
25
+
26
+ def replace_classifier(model, model_name, num_classes):
27
+ if model_name == "vgg16_bn":
28
+ in_features = model.classifier[-1].in_features
29
+ model.classifier[-1] = nn.Linear(in_features, num_classes)
30
+
31
+ elif model_name == "densenet121":
32
+ in_features = model.classifier.in_features
33
+ model.classifier = nn.Linear(in_features, num_classes)
34
+
35
+ elif model_name == "efficientnet_b0":
36
+ in_features = model.classifier[-1].in_features
37
+ model.classifier[-1] = nn.Linear(in_features, num_classes)
38
+
39
+ elif model_name == "mobilenet_v3_small":
40
+ in_features = model.classifier[-1].in_features
41
+ model.classifier[-1] = nn.Linear(in_features, num_classes)
42
+
43
+ elif model_name == "convnext_tiny":
44
+ in_features = model.classifier[-1].in_features
45
+ model.classifier[-1] = nn.Linear(in_features, num_classes)
46
+
47
+ else:
48
+ raise ValueError(f"Unknown model name: {model_name}")
49
+
50
+ return model
51
+
52
+
53
+ def build_model(model_name, num_classes):
54
+ if model_name == "vgg16_bn":
55
+ model = models.vgg16_bn(weights=None)
56
+
57
+ elif model_name == "densenet121":
58
+ model = models.densenet121(weights=None)
59
+
60
+ elif model_name == "efficientnet_b0":
61
+ model = models.efficientnet_b0(weights=None)
62
+
63
+ elif model_name == "mobilenet_v3_small":
64
+ model = models.mobilenet_v3_small(weights=None)
65
+
66
+ elif model_name == "convnext_tiny":
67
+ model = models.convnext_tiny(weights=None)
68
+
69
+ else:
70
+ raise ValueError(f"Unknown model name: {model_name}")
71
+
72
+ return replace_classifier(model, model_name, num_classes)
73
+
74
+
75
+ def load_state_dict_safely(path):
76
+ try:
77
+ ckpt = torch.load(path, map_location="cpu", weights_only=True)
78
+ except TypeError:
79
+ ckpt = torch.load(path, map_location="cpu")
80
+
81
+ if isinstance(ckpt, dict):
82
+ for key in ["model_state_dict", "state_dict", "model"]:
83
+ if key in ckpt and isinstance(ckpt[key], dict):
84
+ ckpt = ckpt[key]
85
+ break
86
+
87
+ cleaned = {}
88
+
89
+ for k, v in ckpt.items():
90
+ nk = k[7:] if str(k).startswith("module.") else k
91
+ cleaned[nk] = v
92
+
93
+ return cleaned
94
+
95
+
96
+ def load_ensemble():
97
+ loaded = []
98
+
99
+ for member in CFG["members"]:
100
+ model_name = member["model"]
101
+ ckpt_path = ROOT / member["checkpoint_file"]
102
+
103
+ model = build_model(model_name, NUM_CLASSES)
104
+ state = load_state_dict_safely(ckpt_path)
105
+ model.load_state_dict(state, strict=True)
106
+ model.to(DEVICE)
107
+ model.eval()
108
+
109
+ loaded.append({
110
+ "model": model,
111
+ "display_name": member.get("display_name", model_name),
112
+ "seed": member.get("seed"),
113
+ "weight": float(member["weight"]),
114
+ "temperature": max(float(member["temperature"]), 1e-8),
115
+ })
116
+
117
+ weight_sum = sum(m["weight"] for m in loaded)
118
+
119
+ if weight_sum <= 0:
120
+ for m in loaded:
121
+ m["weight"] = 1.0 / len(loaded)
122
+ else:
123
+ for m in loaded:
124
+ m["weight"] /= weight_sum
125
+
126
+ return loaded
127
+
128
+
129
+ ENSEMBLE = load_ensemble()
130
+
131
+ PREPROCESS = transforms.Compose([
132
+ transforms.Resize((IMAGE_SIZE, IMAGE_SIZE)),
133
+ transforms.ToTensor(),
134
+ transforms.Normalize(
135
+ mean=CFG["preprocessing"]["normalization_mean"],
136
+ std=CFG["preprocessing"]["normalization_std"],
137
+ ),
138
+ ])
139
+
140
+
141
+ @torch.no_grad()
142
+ def predict(image):
143
+ if image is None:
144
+ return None, "Please upload an MRI image."
145
+
146
+ if not isinstance(image, Image.Image):
147
+ image = Image.fromarray(image)
148
+
149
+ image = image.convert("RGB")
150
+ x = PREPROCESS(image).unsqueeze(0).to(DEVICE)
151
+
152
+ final_probs = torch.zeros((1, NUM_CLASSES), dtype=torch.float32, device=DEVICE)
153
+ member_lines = []
154
+
155
+ for member in ENSEMBLE:
156
+ logits = member["model"](x)
157
+ probs = F.softmax(logits / member["temperature"], dim=1)
158
+ final_probs += member["weight"] * probs
159
+
160
+ top_prob, top_idx = torch.max(probs, dim=1)
161
+
162
+ member_lines.append(
163
+ f"{member['display_name']} seed {member['seed']}: "
164
+ f"{CLASS_NAMES[int(top_idx.item())]} ({float(top_prob.item()):.4f})"
165
+ )
166
+
167
+ final_probs_np = final_probs.squeeze(0).detach().cpu().numpy()
168
+ pred_idx = int(np.argmax(final_probs_np))
169
+ pred_class = CLASS_NAMES[pred_idx]
170
+ pred_conf = float(final_probs_np[pred_idx])
171
+
172
+ label_scores = {
173
+ CLASS_NAMES[i]: float(final_probs_np[i])
174
+ for i in range(NUM_CLASSES)
175
+ }
176
+
177
+ details = (
178
+ f"Predicted class: {pred_class}\n"
179
+ f"Calibrated ensemble confidence: {pred_conf:.4f}\n\n"
180
+ "Member predictions:\n"
181
+ + "\n".join(member_lines)
182
+ + "\n\nDisclaimer: This tool is for research and educational demonstration only. "
183
+ "It is not a medical device and must not be used for diagnosis or treatment."
184
+ )
185
+
186
+ return label_scores, details
187
+
188
+
189
+ demo = gr.Interface(
190
+ fn=predict,
191
+ inputs=gr.Image(type="pil", label="Upload MRI image"),
192
+ outputs=[
193
+ gr.Label(num_top_classes=NUM_CLASSES, label="Calibrated ensemble probabilities"),
194
+ gr.Textbox(label="Prediction details"),
195
+ ],
196
+ title="LCVC-Ensemble Brain Tumor MRI Classifier",
197
+ description=(
198
+ "Leakage-Controlled Validation-Calibrated Multi-Backbone Ensemble. "
199
+ "Research demonstration only; not for clinical use."
200
+ ),
201
+ flagging_mode="never",
202
+ )
203
+
204
+ if __name__ == "__main__":
205
+ demo.launch()
ensemble_config.json ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "proposed_model": "LCVC-Ensemble",
3
+ "full_name": "Leakage-Controlled Validation-Calibrated Multi-Backbone Ensemble",
4
+ "task": "Brain tumor MRI four-class classification",
5
+ "classes": [
6
+ "glioma",
7
+ "meningioma",
8
+ "notumor",
9
+ "pituitary"
10
+ ],
11
+ "num_classes": 4,
12
+ "image_size": 224,
13
+ "preprocessing": {
14
+ "input_mode": "RGB",
15
+ "resize": [
16
+ 224,
17
+ 224
18
+ ],
19
+ "normalization_mean": [
20
+ 0.485,
21
+ 0.456,
22
+ 0.406
23
+ ],
24
+ "normalization_std": [
25
+ 0.229,
26
+ 0.224,
27
+ 0.225
28
+ ]
29
+ },
30
+ "ensemble_rule": "weighted_average_of_temperature_scaled_softmax_probabilities",
31
+ "selection_rule": "highest validation Macro-F1 among predeclared ensembles; no test labels used for selection",
32
+ "strategy": "top5_by_checkpoint_val_macro_f1",
33
+ "calibrated_probs": true,
34
+ "weight_mode": "uniform",
35
+ "num_members": 5,
36
+ "members": [
37
+ {
38
+ "member_order": 0,
39
+ "model": "convnext_tiny",
40
+ "display_name": "ConvNeXt-Tiny",
41
+ "seed": 123,
42
+ "weight": 0.2,
43
+ "temperature": 1.3677136307807327,
44
+ "checkpoint_file": "checkpoints/best_convnext_tiny_seed123.pt",
45
+ "checkpoint_val_macro_f1": 0.9870486143666763,
46
+ "test_cal_macro_f1": 0.9747367912288595
47
+ },
48
+ {
49
+ "member_order": 1,
50
+ "model": "efficientnet_b0",
51
+ "display_name": "EfficientNet-B0",
52
+ "seed": 123,
53
+ "weight": 0.2,
54
+ "temperature": 1.3258544405019956,
55
+ "checkpoint_file": "checkpoints/best_efficientnet_b0_seed123.pt",
56
+ "checkpoint_val_macro_f1": 0.9842404596100688,
57
+ "test_cal_macro_f1": 0.984111907683999
58
+ },
59
+ {
60
+ "member_order": 2,
61
+ "model": "efficientnet_b0",
62
+ "display_name": "EfficientNet-B0",
63
+ "seed": 2026,
64
+ "weight": 0.2,
65
+ "temperature": 1.2086706764615134,
66
+ "checkpoint_file": "checkpoints/best_efficientnet_b0_seed2026.pt",
67
+ "checkpoint_val_macro_f1": 0.9842174297345077,
68
+ "test_cal_macro_f1": 0.9802870622455948
69
+ },
70
+ {
71
+ "member_order": 3,
72
+ "model": "efficientnet_b0",
73
+ "display_name": "EfficientNet-B0",
74
+ "seed": 42,
75
+ "weight": 0.2,
76
+ "temperature": 1.4470591581130952,
77
+ "checkpoint_file": "checkpoints/best_efficientnet_b0_seed42.pt",
78
+ "checkpoint_val_macro_f1": 0.9835034785528444,
79
+ "test_cal_macro_f1": 0.9859513817770005
80
+ },
81
+ {
82
+ "member_order": 4,
83
+ "model": "convnext_tiny",
84
+ "display_name": "ConvNeXt-Tiny",
85
+ "seed": 42,
86
+ "weight": 0.2,
87
+ "temperature": 1.0611162444290951,
88
+ "checkpoint_file": "checkpoints/best_convnext_tiny_seed42.pt",
89
+ "checkpoint_val_macro_f1": 0.9824939017733962,
90
+ "test_cal_macro_f1": 0.9831484695235262
91
+ }
92
+ ],
93
+ "validation_metrics": {
94
+ "accuracy": 0.9916201117318436,
95
+ "macro_f1": 0.9917075605938603,
96
+ "balanced_accuracy": 0.9916862142328199,
97
+ "macro_auc_ovr": 0.9997306138031021
98
+ },
99
+ "test_metrics": {
100
+ "accuracy": 0.9887640449438202,
101
+ "macro_f1": 0.9887621278421996,
102
+ "balanced_accuracy": 0.9887638076313365,
103
+ "macro_auc_ovr": 0.9997131229291261
104
+ },
105
+ "medical_disclaimer": "This app is for research and educational demonstration only. It is not a medical device and must not be used for diagnosis, treatment, or clinical decision-making."
106
+ }
hf_bundle_manifest.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "bundle_type": "huggingface_space_upload",
3
+ "contains_checkpoints": true,
4
+ "num_checkpoints": 5,
5
+ "files": [
6
+ "app.py",
7
+ "requirements.txt",
8
+ "README.md",
9
+ "ensemble_config.json",
10
+ "selected_lcvc_ensemble_members.csv",
11
+ "proposed_lcvc_ensemble_summary.json",
12
+ "checkpoints/*.pt"
13
+ ]
14
+ }
proposed_lcvc_ensemble_summary.json ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "proposed_model": "LCVC_Ensemble",
3
+ "selection_rule": "highest validation Macro-F1 among predeclared ensembles; no test labels used for selection",
4
+ "strategy": "top5_by_checkpoint_val_macro_f1",
5
+ "calibrated_probs": true,
6
+ "weight_mode": "uniform",
7
+ "num_members": 5,
8
+ "class_names": [
9
+ "glioma",
10
+ "meningioma",
11
+ "notumor",
12
+ "pituitary"
13
+ ],
14
+ "validation_metrics": {
15
+ "accuracy": 0.9916201117318436,
16
+ "macro_f1": 0.9917075605938603,
17
+ "balanced_accuracy": 0.9916862142328199,
18
+ "macro_auc_ovr": 0.9997306138031021
19
+ },
20
+ "test_metrics": {
21
+ "accuracy": 0.9887640449438202,
22
+ "macro_f1": 0.9887621278421996,
23
+ "balanced_accuracy": 0.9887638076313365,
24
+ "macro_auc_ovr": 0.9997131229291261
25
+ },
26
+ "cm_csv_path": "/kaggle/working/lcvc_ensemble_outputs/cm_LCVC_Ensemble.csv",
27
+ "report_path": "/kaggle/working/lcvc_ensemble_outputs/classification_report_LCVC_Ensemble.json",
28
+ "selected_members_path": "/kaggle/working/lcvc_ensemble_outputs/selected_lcvc_ensemble_members.csv",
29
+ "auc_fix": {
30
+ "fixed": true,
31
+ "method": "manual one-vs-rest macro-AUC from saved probabilities",
32
+ "note": "Model selection remains validation-only. Test labels are used only for final reporting."
33
+ }
34
+ }
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ torch
2
+ torchvision
3
+ numpy
4
+ pandas
5
+ Pillow
6
+ gradio>=5.0
selected_lcvc_ensemble_members.csv ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ member_order,model,display_name,seed,weight,temperature,checkpoint_path,checkpoint_val_macro_f1,test_cal_macro_f1
2
+ 0,convnext_tiny,ConvNeXt-Tiny,123,0.2,1.3677136307807327,/kaggle/input/datasets/sayemahmedshayeed/baseline-cnn-result/mri_backbone_baselines_outputs/best_convnext_tiny_seed123.pt,0.9870486143666763,0.9747367912288595
3
+ 1,efficientnet_b0,EfficientNet-B0,123,0.2,1.3258544405019956,/kaggle/input/datasets/sayemahmedshayeed/baseline-cnn-result/mri_backbone_baselines_outputs/best_efficientnet_b0_seed123.pt,0.9842404596100688,0.984111907683999
4
+ 2,efficientnet_b0,EfficientNet-B0,2026,0.2,1.2086706764615134,/kaggle/input/datasets/sayemahmedshayeed/baseline-cnn-result/mri_backbone_baselines_outputs/best_efficientnet_b0_seed2026.pt,0.9842174297345077,0.9802870622455948
5
+ 3,efficientnet_b0,EfficientNet-B0,42,0.2,1.4470591581130952,/kaggle/input/datasets/sayemahmedshayeed/baseline-cnn-result/mri_backbone_baselines_outputs/best_efficientnet_b0_seed42.pt,0.9835034785528444,0.9859513817770005
6
+ 4,convnext_tiny,ConvNeXt-Tiny,42,0.2,1.0611162444290951,/kaggle/input/datasets/sayemahmedshayeed/baseline-cnn-result/mri_backbone_baselines_outputs/best_convnext_tiny_seed42.pt,0.9824939017733962,0.9831484695235262