vimdhayak commited on
Commit
f6ab35f
·
verified ·
1 Parent(s): 3c38093

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -1,35 +1,4 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
  *.pt filter=lfs diff=lfs merge=lfs -text
23
  *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
  *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  *.pt filter=lfs diff=lfs merge=lfs -text
2
  *.pth filter=lfs diff=lfs merge=lfs -text
3
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
4
  *.safetensors filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ .env
5
+ .DS_Store
6
+ .ipynb_checkpoints/
DEPLOYMENT_INSTRUCTIONS.md ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Deployment instructions: Hugging Face Space MRI ensemble
2
+
3
+ This is a fresh Hugging Face Space bundle. It is not a patch of the earlier bundle.
4
+
5
+ The app expects all model checkpoints to live in exactly one folder:
6
+
7
+ ```text
8
+ models/
9
+ ```
10
+
11
+ ## 1. Required checkpoint files
12
+
13
+ Place these three files in the `models/` folder:
14
+
15
+ ```text
16
+ models/best_efficientnet_b0_seed123.pt
17
+ models/best_efficientnet_b0_seed2026.pt
18
+ models/best_mobilenet_v3_small_seed42.pt
19
+ ```
20
+
21
+ These correspond to the non-zero-weight members of the selected ensemble:
22
+
23
+ | Member | Ensemble weight |
24
+ |---|---:|
25
+ | EfficientNet-B0 seed 123 | `0.49513684` |
26
+ | EfficientNet-B0 seed 2026 | `0.35077890` |
27
+ | MobileNetV3-Small seed 42 | `0.15408426` |
28
+
29
+ Do not add the zero-weight selected members unless you modify the app. They do not affect inference.
30
+
31
+ ## 2. Folder structure to upload
32
+
33
+ Your final Space repo should look like this:
34
+
35
+ ```text
36
+ .
37
+ ├── app.py
38
+ ├── README.md
39
+ ├── requirements.txt
40
+ ├── DEPLOYMENT_INSTRUCTIONS.md
41
+ ├── validate_bundle.py
42
+ ├── .gitattributes
43
+ ├── src/
44
+ │ ├── __init__.py
45
+ │ ├── config.py
46
+ │ └── modeling.py
47
+ ├── results/
48
+ │ ├── selected_ensemble.json
49
+ │ ├── deployed_members.csv
50
+ │ ├── selected_metrics.csv
51
+ │ ├── cm_ensemble_lightweight_effnet_mobilenet_optimized_val_ce_weighted_soft.csv
52
+ │ ├── classification_report_ensemble_lightweight_effnet_mobilenet_optimized_val_ce_weighted_soft.json
53
+ │ ├── ensemble_fusion_ablation_results.csv
54
+ │ └── single_checkpoint_results.csv
55
+ └── models/
56
+ ├── README.md
57
+ ├── best_efficientnet_b0_seed123.pt
58
+ ├── best_efficientnet_b0_seed2026.pt
59
+ └── best_mobilenet_v3_small_seed42.pt
60
+ ```
61
+
62
+ ## 3. Local test before upload
63
+
64
+ From the bundle folder:
65
+
66
+ ```bash
67
+ python -m venv .venv
68
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
69
+ pip install --upgrade pip
70
+ pip install -r requirements.txt
71
+ python validate_bundle.py
72
+ python app.py
73
+ ```
74
+
75
+ Open the local Gradio URL shown in the terminal. Go to **Model status** and click **Test-load ensemble**.
76
+
77
+ If checkpoint files are not present yet, `validate_bundle.py` will warn you but still validate the source files. The Space will not run real inference until the checkpoints are present.
78
+
79
+ ## 4. Create the Hugging Face Space
80
+
81
+ 1. Go to Hugging Face.
82
+ 2. Create a new Space.
83
+ 3. Select **Gradio** as the SDK.
84
+ 4. Use the files in this bundle as the root of the Space repo.
85
+ 5. Keep `README.md` at the repo root because the Space config is in the YAML block at the top of that file.
86
+
87
+ ## 5. Upload using Git
88
+
89
+ Install Git LFS once:
90
+
91
+ ```bash
92
+ git lfs install
93
+ ```
94
+
95
+ Clone your Space:
96
+
97
+ ```bash
98
+ git clone https://huggingface.co/spaces/<your-username>/<your-space-name>
99
+ cd <your-space-name>
100
+ ```
101
+
102
+ Copy the bundle into the repo. Then copy the three checkpoint files into `models/`.
103
+
104
+ Track large checkpoints with LFS:
105
+
106
+ ```bash
107
+ git lfs track "*.pt"
108
+ git lfs track "*.pth"
109
+ git lfs track "*.ckpt"
110
+ git lfs track "*.safetensors"
111
+ ```
112
+
113
+ Commit and push:
114
+
115
+ ```bash
116
+ git add .gitattributes .
117
+ git commit -m "Deploy MRI ensemble Space"
118
+ git push
119
+ ```
120
+
121
+ ## 6. Upload using the web interface
122
+
123
+ If you prefer the browser UI:
124
+
125
+ 1. Upload all code files and folders from this bundle.
126
+ 2. Create or open the `models/` folder.
127
+ 3. Upload the three `.pt` files into `models/`.
128
+ 4. Confirm the filenames match exactly.
129
+ 5. Wait for the Space to rebuild.
130
+ 6. Open the **Model status** tab and click **Test-load ensemble**.
131
+
132
+ For large files, Git + Git LFS is usually more reliable than the browser uploader.
133
+
134
+ ## 7. After deployment: checks to run
135
+
136
+ Open the Space and run these checks:
137
+
138
+ 1. **Model status** tab shows all three checkpoints as found.
139
+ 2. **Test-load ensemble** returns a success message.
140
+ 3. Upload a sample MRI image.
141
+ 4. Run prediction with heatmap **off** first.
142
+ 5. Then try heatmap **on**. Heatmap generation can be slow on CPU.
143
+
144
+ ## 8. Common errors
145
+
146
+ ### Error: missing checkpoint
147
+
148
+ Make sure the files are in `models/`, not the repo root or another folder.
149
+
150
+ Correct:
151
+
152
+ ```text
153
+ models/best_efficientnet_b0_seed123.pt
154
+ ```
155
+
156
+ Incorrect:
157
+
158
+ ```text
159
+ best_efficientnet_b0_seed123.pt
160
+ checkpoints/best_efficientnet_b0_seed123.pt
161
+ model/best_efficientnet_b0_seed123.pt
162
+ ```
163
+
164
+ ### Error: filename mismatch
165
+
166
+ The app expects exact filenames. Rename your files if necessary.
167
+
168
+ ### Error: size mismatch during model loading
169
+
170
+ This usually means the checkpoint does not match the deployed architecture or class count. The app expects:
171
+
172
+ ```text
173
+ EfficientNet-B0 with 4 output classes
174
+ EfficientNet-B0 with 4 output classes
175
+ MobileNetV3-Small with 4 output classes
176
+ ```
177
+
178
+ Class order:
179
+
180
+ ```text
181
+ glioma, meningioma, notumor, pituitary
182
+ ```
183
+
184
+ ### Error: Space runs out of memory
185
+
186
+ Use CPU Upgrade or a small GPU hardware tier. The app loads two EfficientNet-B0 models and one MobileNetV3-Small model. CPU Basic may work, but startup and heatmaps can be slow.
187
+
188
+ ### Error: heatmap fails but prediction works
189
+
190
+ The heatmap is optional interpretability support. Leave the heatmap checkbox off if CPU performance is poor. Prediction does not depend on heatmap generation.
191
+
192
+ ## 9. Customization
193
+
194
+ ### Change checkpoint names
195
+
196
+ Edit `src/config.py`:
197
+
198
+ ```python
199
+ ENSEMBLE_MEMBERS = [...]
200
+ ```
201
+
202
+ Update `checkpoint_file` values only if your filenames are different.
203
+
204
+ ### Disable strict checkpoint loading
205
+
206
+ By default, checkpoint loading is strict. To loosen loading, set this Space environment variable:
207
+
208
+ ```text
209
+ STRICT_CHECKPOINT_LOADING=false
210
+ ```
211
+
212
+ Use this only for debugging. For research reproducibility, strict loading is better.
213
+
214
+ ## 10. Important research caution
215
+
216
+ The reported metrics come from the provided experiment outputs. They do not prove clinical readiness. External validation, calibration review, scanner/protocol shift testing, patient-level leakage checks, bias checks, and clinical governance are still required before any medical use.
README.md CHANGED
@@ -1,13 +1,67 @@
1
  ---
2
- title: LCVC DeepFuse
3
- emoji: 🦀
4
- colorFrom: purple
5
- colorTo: purple
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: Brain MRI Ensemble Classifier
3
+ emoji: 🧠
4
+ colorFrom: indigo
5
+ colorTo: blue
6
  sdk: gradio
7
+ sdk_version: 5.49.1
8
+ python_version: 3.10
9
  app_file: app.py
10
  pinned: false
11
+ short_description: Weighted-soft EfficientNet/MobileNet MRI ensemble demo
12
+ suggested_hardware: cpu-upgrade
13
+ tags:
14
+ - medical-imaging
15
+ - mri
16
+ - image-classification
17
+ - gradio
18
+ - pytorch
19
  ---
20
 
21
+ # Brain MRI Ensemble Classifier
22
+
23
+ This Hugging Face Space deploys the selected ensemble from the MRI backbone/ensemble research notebooks.
24
+
25
+ ## Selected deployment ensemble
26
+
27
+ **Pool:** `lightweight_effnet_mobilenet`
28
+ **Fusion:** `optimized_val_ce_weighted_soft`
29
+ **Classes:** `glioma`, `meningioma`, `notumor`, `pituitary`
30
+
31
+ Only non-zero-weight members are deployed:
32
+
33
+ | Member | Weight | Checkpoint required in `models/` |
34
+ |---|---:|---|
35
+ | EfficientNet-B0 seed 123 | `0.49513684` | `best_efficientnet_b0_seed123.pt` |
36
+ | EfficientNet-B0 seed 2026 | `0.35077890` | `best_efficientnet_b0_seed2026.pt` |
37
+ | MobileNetV3-Small seed 42 | `0.15408426` | `best_mobilenet_v3_small_seed42.pt` |
38
+
39
+ Zero-weight members from the optimization result are intentionally omitted because they do not change weighted-soft inference.
40
+
41
+ ## Reported research metrics
42
+
43
+ | Metric | Value |
44
+ |---|---:|
45
+ | Validation Macro-F1 | `0.994487` |
46
+ | Test accuracy | `0.990637` |
47
+ | Test Macro-F1 | `0.990633` |
48
+ | Test balanced accuracy | `0.990640` |
49
+ | Test macro AUC OVR | `0.999339` |
50
+ | Test ECE | `0.008194` |
51
+
52
+ ## Checkpoint placement
53
+
54
+ Put all required checkpoint files in:
55
+
56
+ ```text
57
+ models/
58
+ ├── best_efficientnet_b0_seed123.pt
59
+ ├── best_efficientnet_b0_seed2026.pt
60
+ └── best_mobilenet_v3_small_seed42.pt
61
+ ```
62
+
63
+ The app intentionally looks in `models/` only, so deployment remains simple and reproducible.
64
+
65
+ ## Medical disclaimer
66
+
67
+ This Space is a research prototype and is not a medical device. It must not be used for diagnosis, treatment, or patient triage.
app.py ADDED
@@ -0,0 +1,337 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import traceback
4
+
5
+ import gradio as gr
6
+ import matplotlib.pyplot as plt
7
+ import pandas as pd
8
+ from PIL import Image
9
+
10
+ from src.config import CLASS_DISPLAY_NAMES, CLASS_NAMES, ENSEMBLE_MEMBERS
11
+ from src.modeling import diagnose_checkpoints, load_ensemble, predict, weighted_ensemble_cam
12
+
13
+
14
+ DISCLAIMER = """
15
+ <div class="notice">
16
+ <b>Research prototype only.</b> This Space is for demonstrating a trained MRI image classifier from the submitted research workflow. It is not a medical device and must not be used for diagnosis, treatment, or patient triage.
17
+ </div>
18
+ """
19
+
20
+
21
+ CUSTOM_CSS = """
22
+ :root {
23
+ --radius-xl: 26px;
24
+ --radius-lg: 18px;
25
+ --glass: rgba(255,255,255,0.80);
26
+ --stroke: rgba(148,163,184,0.28);
27
+ --shadow: 0 18px 60px rgba(15, 23, 42, .16);
28
+ }
29
+ .gradio-container {
30
+ max-width: 1220px !important;
31
+ margin: auto !important;
32
+ background:
33
+ radial-gradient(circle at 10% 8%, rgba(99,102,241,.18), transparent 30%),
34
+ radial-gradient(circle at 90% 0%, rgba(14,165,233,.17), transparent 28%),
35
+ linear-gradient(180deg, #f8fafc 0%, #eef2ff 52%, #f8fafc 100%) !important;
36
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
37
+ }
38
+ .hero {
39
+ padding: 34px 36px;
40
+ border-radius: var(--radius-xl);
41
+ background: linear-gradient(135deg, rgba(15,23,42,.96), rgba(30,64,175,.88) 52%, rgba(2,132,199,.84));
42
+ color: white;
43
+ box-shadow: var(--shadow);
44
+ border: 1px solid rgba(255,255,255,.16);
45
+ margin-bottom: 20px;
46
+ }
47
+ .hero h1 {
48
+ margin: 0 0 10px 0 !important;
49
+ font-size: clamp(2rem, 5vw, 3.8rem) !important;
50
+ line-height: 1.02 !important;
51
+ letter-spacing: -0.05em !important;
52
+ }
53
+ .hero p {
54
+ font-size: 1.02rem;
55
+ color: rgba(255,255,255,.86);
56
+ max-width: 880px;
57
+ margin: 0;
58
+ }
59
+ .badges {
60
+ display: flex;
61
+ flex-wrap: wrap;
62
+ gap: 10px;
63
+ margin-top: 20px;
64
+ }
65
+ .badge {
66
+ border: 1px solid rgba(255,255,255,.22);
67
+ background: rgba(255,255,255,.12);
68
+ color: #fff;
69
+ padding: 8px 12px;
70
+ border-radius: 999px;
71
+ font-weight: 650;
72
+ font-size: .88rem;
73
+ }
74
+ .notice {
75
+ padding: 14px 16px;
76
+ border-radius: 16px;
77
+ background: rgba(254, 243, 199, .82);
78
+ border: 1px solid rgba(245, 158, 11, .34);
79
+ color: #78350f;
80
+ margin: 10px 0 18px 0;
81
+ }
82
+ .result-card {
83
+ padding: 22px;
84
+ border-radius: var(--radius-xl);
85
+ background: var(--glass);
86
+ border: 1px solid var(--stroke);
87
+ box-shadow: 0 14px 44px rgba(15,23,42,.11);
88
+ backdrop-filter: blur(12px);
89
+ }
90
+ .pred-title {
91
+ font-size: 1.05rem;
92
+ text-transform: uppercase;
93
+ letter-spacing: .08em;
94
+ color: #475569;
95
+ font-weight: 800;
96
+ margin-bottom: 4px;
97
+ }
98
+ .pred-label {
99
+ font-size: clamp(2rem, 4vw, 3.4rem);
100
+ line-height: 1;
101
+ font-weight: 900;
102
+ letter-spacing: -0.045em;
103
+ color: #0f172a;
104
+ }
105
+ .pred-sub {
106
+ margin-top: 12px;
107
+ color: #475569;
108
+ font-size: 1rem;
109
+ }
110
+ .metric-grid {
111
+ display: grid;
112
+ grid-template-columns: repeat(3, minmax(0, 1fr));
113
+ gap: 12px;
114
+ margin-top: 18px;
115
+ }
116
+ .metric {
117
+ padding: 14px;
118
+ border-radius: 16px;
119
+ background: rgba(255,255,255,.76);
120
+ border: 1px solid rgba(148,163,184,.28);
121
+ }
122
+ .metric .k { color: #64748b; font-size: .8rem; font-weight: 800; text-transform: uppercase; letter-spacing: .07em; }
123
+ .metric .v { color: #0f172a; font-size: 1.35rem; font-weight: 900; margin-top: 4px; }
124
+ .status-good { color: #166534; font-weight: 800; }
125
+ .status-bad { color: #991b1b; font-weight: 800; }
126
+ .footer-note { color: #64748b; font-size: .92rem; }
127
+ button.primary, .primary button {
128
+ border-radius: 16px !important;
129
+ font-weight: 850 !important;
130
+ }
131
+ .block, .form, .panel, .tabitem, .gr-box {
132
+ border-radius: 18px !important;
133
+ }
134
+ @media (max-width: 800px) {
135
+ .hero { padding: 24px 20px; }
136
+ .metric-grid { grid-template-columns: 1fr; }
137
+ }
138
+ """
139
+
140
+
141
+ HERO_HTML = """
142
+ <div class="hero">
143
+ <h1>Brain MRI Ensemble Classifier</h1>
144
+ <p>Upload a brain MRI image and run the selected lightweight weighted-soft ensemble from the research notebook: EfficientNet-B0 + MobileNetV3-Small.</p>
145
+ <div class="badges">
146
+ <span class="badge">4 classes</span>
147
+ <span class="badge">Weighted soft voting</span>
148
+ <span class="badge">EfficientNet-B0</span>
149
+ <span class="badge">MobileNetV3-Small</span>
150
+ <span class="badge">Optional heatmap</span>
151
+ </div>
152
+ </div>
153
+ """
154
+
155
+
156
+ def _status_markdown() -> str:
157
+ ok, _df, message = diagnose_checkpoints()
158
+ cls = "status-good" if ok else "status-bad"
159
+ return f"<div class='{cls}'>{message}</div>"
160
+
161
+
162
+ def _model_table() -> pd.DataFrame:
163
+ _ok, df, _message = diagnose_checkpoints()
164
+ return df
165
+
166
+
167
+ def _research_metrics_table() -> pd.DataFrame:
168
+ return pd.DataFrame(
169
+ [
170
+ {"metric": "Validation Macro-F1", "value": "0.994487"},
171
+ {"metric": "Test Accuracy", "value": "0.990637"},
172
+ {"metric": "Test Macro-F1", "value": "0.990633"},
173
+ {"metric": "Test Balanced Accuracy", "value": "0.990640"},
174
+ {"metric": "Test Macro AUC OVR", "value": "0.999339"},
175
+ {"metric": "Test ECE", "value": "0.008194"},
176
+ ]
177
+ )
178
+
179
+
180
+ def _deployed_members_table() -> pd.DataFrame:
181
+ rows = []
182
+ for m in ENSEMBLE_MEMBERS:
183
+ rows.append(
184
+ {
185
+ "member": m["display_name"],
186
+ "weight": f"{m['weight']:.8f}",
187
+ "checkpoint": f"models/{m['checkpoint_file']}",
188
+ }
189
+ )
190
+ return pd.DataFrame(rows)
191
+
192
+
193
+ def _probability_plot(prob_df: pd.DataFrame):
194
+ fig, ax = plt.subplots(figsize=(7.5, 4.2))
195
+ labels = prob_df["class"].tolist()[::-1]
196
+ values = prob_df["probability"].tolist()[::-1]
197
+ ax.barh(labels, values)
198
+ ax.set_xlim(0, 1)
199
+ ax.set_xlabel("Probability")
200
+ ax.set_title("Ensemble class probabilities")
201
+ ax.grid(axis="x", alpha=0.28)
202
+ for idx, value in enumerate(values):
203
+ ax.text(min(value + 0.015, 0.98), idx, f"{value*100:.1f}%", va="center", fontweight="bold")
204
+ fig.tight_layout()
205
+ return fig
206
+
207
+
208
+ def _prediction_card(label: str, confidence: float, image: Image.Image, heatmap_enabled: bool) -> str:
209
+ width, height = image.size if image is not None else (0, 0)
210
+ heatmap_text = "Generated" if heatmap_enabled else "Off"
211
+ return f"""
212
+ <div class="result-card">
213
+ <div class="pred-title">Top prediction</div>
214
+ <div class="pred-label">{label}</div>
215
+ <div class="pred-sub">Weighted-soft ensemble confidence: <b>{confidence*100:.2f}%</b></div>
216
+ <div class="metric-grid">
217
+ <div class="metric"><div class="k">Input size</div><div class="v">{width}×{height}</div></div>
218
+ <div class="metric"><div class="k">Model votes</div><div class="v">3</div></div>
219
+ <div class="metric"><div class="k">Heatmap</div><div class="v">{heatmap_text}</div></div>
220
+ </div>
221
+ </div>
222
+ """
223
+
224
+
225
+ def run_prediction(image: Image.Image, make_heatmap: bool):
226
+ if image is None:
227
+ raise gr.Error("Upload an MRI image first.")
228
+
229
+ try:
230
+ result = predict(image)
231
+ prob_df = result.probability_df.copy()
232
+ prob_df["probability"] = prob_df["probability"].map(lambda x: round(float(x), 6))
233
+ plot = _probability_plot(result.probability_df)
234
+ heatmap = None
235
+ if make_heatmap:
236
+ heatmap = weighted_ensemble_cam(image, result.predicted_class)
237
+ card = _prediction_card(result.predicted_display, result.confidence, image, make_heatmap and heatmap is not None)
238
+ return card, prob_df, result.member_df, plot, heatmap
239
+ except FileNotFoundError as exc:
240
+ raise gr.Error(str(exc)) from exc
241
+ except Exception as exc:
242
+ detail = traceback.format_exc(limit=3)
243
+ raise gr.Error(f"Prediction failed: {exc}\n\n{detail}") from exc
244
+
245
+
246
+ def warmup_status() -> str:
247
+ ok, _df, message = diagnose_checkpoints()
248
+ if not ok:
249
+ return message
250
+ try:
251
+ # Load once so the first user prediction is faster.
252
+ load_ensemble()
253
+ return "✅ Checkpoints found and ensemble loaded successfully."
254
+ except Exception as exc:
255
+ return f"❌ Checkpoints were found, but model loading failed: {exc}"
256
+
257
+
258
+ with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Soft(primary_hue="indigo", secondary_hue="sky"), title="MRI Ensemble Classifier") as demo:
259
+ gr.HTML(HERO_HTML)
260
+ gr.HTML(DISCLAIMER)
261
+
262
+ with gr.Tabs():
263
+ with gr.Tab("Predict"):
264
+ with gr.Row(equal_height=False):
265
+ with gr.Column(scale=5):
266
+ image_input = gr.Image(
267
+ label="Upload MRI image",
268
+ type="pil",
269
+ height=390,
270
+ sources=["upload", "clipboard"],
271
+ )
272
+ with gr.Row():
273
+ heatmap_toggle = gr.Checkbox(
274
+ value=False,
275
+ label="Generate ensemble heatmap",
276
+ info="Slower on CPU. Use after the basic prediction works.",
277
+ )
278
+ run_button = gr.Button("Run ensemble prediction", variant="primary", elem_classes="primary")
279
+ with gr.Column(scale=7):
280
+ prediction_html = gr.HTML(
281
+ "<div class='result-card'><div class='pred-title'>Waiting for image</div><div class='pred-label'>—</div><div class='pred-sub'>Upload an MRI image and run the selected ensemble.</div></div>",
282
+ label="Prediction",
283
+ )
284
+ probabilities_output = gr.Dataframe(
285
+ label="Class probabilities",
286
+ headers=["class", "probability", "percent"],
287
+ interactive=False,
288
+ )
289
+ with gr.Row():
290
+ probability_plot = gr.Plot(label="Probability chart")
291
+ heatmap_output = gr.Image(label="Optional ensemble heatmap", type="pil")
292
+ member_output = gr.Dataframe(label="Per-member predictions", interactive=False)
293
+
294
+ run_button.click(
295
+ fn=run_prediction,
296
+ inputs=[image_input, heatmap_toggle],
297
+ outputs=[prediction_html, probabilities_output, member_output, probability_plot, heatmap_output],
298
+ )
299
+
300
+ with gr.Tab("Model status"):
301
+ gr.Markdown("### Checkpoint status")
302
+ status_md = gr.Markdown(_status_markdown())
303
+ status_table = gr.Dataframe(value=_model_table(), interactive=False, label="Required files")
304
+ refresh_btn = gr.Button("Refresh status")
305
+ load_btn = gr.Button("Test-load ensemble", variant="secondary")
306
+ load_status = gr.Textbox(label="Load result", interactive=False)
307
+ refresh_btn.click(fn=_status_markdown, inputs=None, outputs=status_md)
308
+ refresh_btn.click(fn=_model_table, inputs=None, outputs=status_table)
309
+ load_btn.click(fn=warmup_status, inputs=None, outputs=load_status)
310
+
311
+ with gr.Tab("Research summary"):
312
+ gr.Markdown(
313
+ """
314
+ ### Selected ensemble
315
+
316
+ The deployed model is the selected **`lightweight_effnet_mobilenet | optimized_val_ce_weighted_soft`** ensemble. It uses only the non-zero-weight members from the ablation result. The zero-weight EfficientNet/MobileNet members are not loaded because they do not affect weighted-soft inference.
317
+
318
+ Class order used at inference: **glioma, meningioma, notumor, pituitary**.
319
+ """
320
+ )
321
+ gr.Dataframe(value=_deployed_members_table(), label="Deployed members", interactive=False)
322
+ gr.Dataframe(value=_research_metrics_table(), label="Reported evaluation metrics", interactive=False)
323
+ gr.Markdown(
324
+ """
325
+ ### Practical interpretation
326
+
327
+ High validation/test scores from the research split do not make this a clinical diagnostic tool. Before any real-world medical use, the model would need independent external validation, bias checks, clinical review, calibration review, privacy/security review, and regulatory evaluation.
328
+ """
329
+ )
330
+
331
+ gr.Markdown(
332
+ "<div class='footer-note'>Built for Hugging Face Spaces with Gradio. Put all required checkpoint files inside the repository's <code>models/</code> folder.</div>"
333
+ )
334
+
335
+
336
+ if __name__ == "__main__":
337
+ demo.queue(max_size=16).launch()
models/.gitkeep ADDED
File without changes
models/README.md ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Model checkpoint folder
2
+
3
+ Place the three required checkpoint files in this folder before running the Space:
4
+
5
+ ```text
6
+ models/best_efficientnet_b0_seed123.pt
7
+ models/best_efficientnet_b0_seed2026.pt
8
+ models/best_mobilenet_v3_small_seed42.pt
9
+ ```
10
+
11
+ These are the non-zero-weight members of the selected ensemble:
12
+
13
+ ```text
14
+ lightweight_effnet_mobilenet | optimized_val_ce_weighted_soft
15
+ ```
16
+
17
+ The app does not search other folders by design. This prevents accidentally loading the wrong checkpoint when multiple experiments are present.
models/best_efficientnet_b0_seed123.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:035063f01fb1f2fead59f5141c5e7d6517e48f054d6fe1f75e25a9760ac0da8a
3
+ size 16357045
models/best_efficientnet_b0_seed2026.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:19388352509559d7f41979a17545b8c2eb7debd9fca1c0e9a8cd5f7be66cde32
3
+ size 16357411
models/best_mobilenet_v3_small_seed42.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:28a1a35b5e45ae269ef3150a75ac3214fd783b7d3447416d25608a88750b5ed2
3
+ size 6227533
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ gradio>=5.0,<6
2
+ torch==2.5.1
3
+ torchvision==0.20.1
4
+ numpy
5
+ pandas
6
+ pillow
7
+ matplotlib
results/classification_report_ensemble_lightweight_effnet_mobilenet_optimized_val_ce_weighted_soft.json ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "glioma": {
3
+ "precision": 0.9962121212121212,
4
+ "recall": 0.9813432835820896,
5
+ "f1-score": 0.9887218045112782,
6
+ "support": 268.0
7
+ },
8
+ "meningioma": {
9
+ "precision": 0.9739776951672863,
10
+ "recall": 0.9886792452830189,
11
+ "f1-score": 0.9812734082397003,
12
+ "support": 265.0
13
+ },
14
+ "notumor": {
15
+ "precision": 1.0,
16
+ "recall": 1.0,
17
+ "f1-score": 1.0,
18
+ "support": 267.0
19
+ },
20
+ "pituitary": {
21
+ "precision": 0.9925373134328358,
22
+ "recall": 0.9925373134328358,
23
+ "f1-score": 0.9925373134328358,
24
+ "support": 268.0
25
+ },
26
+ "accuracy": 0.9906367041198502,
27
+ "macro avg": {
28
+ "precision": 0.9906817824530608,
29
+ "recall": 0.990639960574486,
30
+ "f1-score": 0.9906331315459536,
31
+ "support": 1068.0
32
+ },
33
+ "weighted avg": {
34
+ "precision": 0.9907199791237635,
35
+ "recall": 0.9906367041198502,
36
+ "f1-score": 0.9906506524274749,
37
+ "support": 1068.0
38
+ }
39
+ }
results/cm_ensemble_lightweight_effnet_mobilenet_optimized_val_ce_weighted_soft.csv ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ ,glioma,meningioma,notumor,pituitary
2
+ glioma,263,5,0,0
3
+ meningioma,1,262,0,2
4
+ notumor,0,0,267,0
5
+ pituitary,0,2,0,266
results/deployed_members.csv ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ member,model_name,seed,weight,checkpoint_file
2
+ efficientnet_b0__seed123,efficientnet_b0,123,0.49513684,best_efficientnet_b0_seed123.pt
3
+ efficientnet_b0__seed2026,efficientnet_b0,2026,0.3507789,best_efficientnet_b0_seed2026.pt
4
+ mobilenet_v3_small__seed42,mobilenet_v3_small,42,0.15408426,best_mobilenet_v3_small_seed42.pt
results/ensemble_fusion_ablation_results.csv ADDED
The diff for this file is too large to render. See raw diff
 
results/selected_ensemble.json ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "ensemble_name": "lightweight_effnet_mobilenet__optimized_val_ce_weighted_soft",
3
+ "pool": "lightweight_effnet_mobilenet",
4
+ "fusion_method": "optimized_val_ce_weighted_soft",
5
+ "classes": [
6
+ "glioma",
7
+ "meningioma",
8
+ "notumor",
9
+ "pituitary"
10
+ ],
11
+ "image_size": 224,
12
+ "normalization": {
13
+ "mean": [
14
+ 0.485,
15
+ 0.456,
16
+ 0.406
17
+ ],
18
+ "std": [
19
+ 0.229,
20
+ 0.224,
21
+ 0.225
22
+ ]
23
+ },
24
+ "members_all_from_result": [
25
+ {
26
+ "member": "efficientnet_b0__seed123",
27
+ "weight": 0.49513684
28
+ },
29
+ {
30
+ "member": "efficientnet_b0__seed2026",
31
+ "weight": 0.3507789
32
+ },
33
+ {
34
+ "member": "efficientnet_b0__seed42",
35
+ "weight": 0.0
36
+ },
37
+ {
38
+ "member": "mobilenet_v3_small__seed42",
39
+ "weight": 0.15408426
40
+ },
41
+ {
42
+ "member": "mobilenet_v3_small__seed123",
43
+ "weight": 0.0
44
+ },
45
+ {
46
+ "member": "mobilenet_v3_small__seed2026",
47
+ "weight": 0.0
48
+ }
49
+ ],
50
+ "members_deployed_nonzero": [
51
+ {
52
+ "member": "efficientnet_b0__seed123",
53
+ "model_name": "efficientnet_b0",
54
+ "seed": 123,
55
+ "weight": 0.49513684,
56
+ "checkpoint_file": "best_efficientnet_b0_seed123.pt"
57
+ },
58
+ {
59
+ "member": "efficientnet_b0__seed2026",
60
+ "model_name": "efficientnet_b0",
61
+ "seed": 2026,
62
+ "weight": 0.3507789,
63
+ "checkpoint_file": "best_efficientnet_b0_seed2026.pt"
64
+ },
65
+ {
66
+ "member": "mobilenet_v3_small__seed42",
67
+ "model_name": "mobilenet_v3_small",
68
+ "seed": 42,
69
+ "weight": 0.15408426,
70
+ "checkpoint_file": "best_mobilenet_v3_small_seed42.pt"
71
+ }
72
+ ],
73
+ "metrics": {
74
+ "val_loss": 0.0351625656474978,
75
+ "val_accuracy": 0.994413407821229,
76
+ "val_macro_f1": 0.9944868552693548,
77
+ "val_weighted_f1": 0.9944201163864824,
78
+ "val_balanced_accuracy": 0.9944473211762224,
79
+ "val_macro_auc_ovr": 0.9997045067264196,
80
+ "test_loss": 0.0411555544777738,
81
+ "test_accuracy": 0.9906367041198502,
82
+ "test_macro_f1": 0.9906331315459536,
83
+ "test_weighted_f1": 0.9906506524274749,
84
+ "test_balanced_accuracy": 0.990639960574486,
85
+ "test_macro_auc_ovr": 0.9993387485257516,
86
+ "test_ece": 0.0081940678915398
87
+ },
88
+ "notes": "Zero-weight selected ensemble members are intentionally omitted from deployment; they do not affect weighted-soft inference."
89
+ }
results/selected_metrics.csv ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ val_loss,val_accuracy,val_macro_f1,val_weighted_f1,val_balanced_accuracy,val_macro_auc_ovr,test_loss,test_accuracy,test_macro_f1,test_weighted_f1,test_balanced_accuracy,test_macro_auc_ovr,test_ece
2
+ 0.0351625656474978,0.994413407821229,0.9944868552693548,0.9944201163864824,0.9944473211762224,0.9997045067264196,0.0411555544777738,0.9906367041198502,0.9906331315459536,0.9906506524274749,0.990639960574486,0.9993387485257516,0.0081940678915398
results/single_checkpoint_results.csv ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ run_key,model,display_name,seed,params,val_loss,val_accuracy,val_macro_f1,val_weighted_f1,val_balanced_accuracy,val_macro_auc_ovr,test_loss,test_accuracy,test_macro_f1,test_weighted_f1,test_balanced_accuracy,test_macro_auc_ovr,test_ece,cm_path,cm_csv_path,report_path,reliability_path,checkpoint_path
2
+ convnext_tiny__seed123,convnext_tiny,ConvNeXt-Tiny,123,27823204,0.05633521461468096,0.9869646182495344,0.9870486143666763,0.9869666913812417,0.9869601256474765,0.9995339866054144,0.08842497824530703,0.9747191011235955,0.9747367912288595,0.9747705564029551,0.9747363326467092,0.9987674614827884,0.013772038010398967,/kaggle/working/mri_ensemble_fusion_outputs/confusion_matrices/cm_single_convnext_tiny_seed123.png,/kaggle/working/mri_ensemble_fusion_outputs/confusion_matrices/cm_single_convnext_tiny_seed123.csv,/kaggle/working/mri_ensemble_fusion_outputs/classification_reports/classification_report_single_convnext_tiny_seed123.json,/kaggle/working/mri_ensemble_fusion_outputs/plots/reliability_single_convnext_tiny_seed123.png,/kaggle/input/datasets/sayemahmedshayeed/baseline-cnn-result/mri_backbone_baselines_outputs/best_convnext_tiny_seed123.pt
3
+ efficientnet_b0__seed123,efficientnet_b0,EfficientNet-B0,123,4012672,0.05216454092915397,0.984171322160149,0.9842404596100689,0.9841449541023992,0.9843385749686048,0.9994274057336936,0.050461610398573026,0.9840823970037453,0.984111907683999,0.9841518869848078,0.9840750139487004,0.9993316443503547,0.007627396994315781,/kaggle/working/mri_ensemble_fusion_outputs/confusion_matrices/cm_single_efficientnet_b0_seed123.png,/kaggle/working/mri_ensemble_fusion_outputs/confusion_matrices/cm_single_efficientnet_b0_seed123.csv,/kaggle/working/mri_ensemble_fusion_outputs/classification_reports/classification_report_single_efficientnet_b0_seed123.json,/kaggle/working/mri_ensemble_fusion_outputs/plots/reliability_single_efficientnet_b0_seed123.png,/kaggle/input/datasets/sayemahmedshayeed/baseline-cnn-result/mri_backbone_baselines_outputs/best_efficientnet_b0_seed123.pt
4
+ efficientnet_b0__seed2026,efficientnet_b0,EfficientNet-B0,2026,4012672,0.05301791995071825,0.984171322160149,0.9842174297345075,0.984133994553996,0.9842908942132239,0.999159752653795,0.05500029845628876,0.9803370786516854,0.9802870622455948,0.9803219434421269,0.9803224707499181,0.999261557309636,0.009121747889768784,/kaggle/working/mri_ensemble_fusion_outputs/confusion_matrices/cm_single_efficientnet_b0_seed2026.png,/kaggle/working/mri_ensemble_fusion_outputs/confusion_matrices/cm_single_efficientnet_b0_seed2026.csv,/kaggle/working/mri_ensemble_fusion_outputs/classification_reports/classification_report_single_efficientnet_b0_seed2026.json,/kaggle/working/mri_ensemble_fusion_outputs/plots/reliability_single_efficientnet_b0_seed2026.png,/kaggle/input/datasets/sayemahmedshayeed/baseline-cnn-result/mri_backbone_baselines_outputs/best_efficientnet_b0_seed2026.pt
5
+ efficientnet_b0__seed42,efficientnet_b0,EfficientNet-B0,42,4012672,0.0588622792676823,0.9832402234636871,0.9835034785528445,0.9832479292355641,0.9835550767396486,0.9991128183741339,0.05389325926188666,0.9859550561797753,0.9859513817770005,0.9859744427022225,0.9859406064863303,0.9993916755816631,0.006639772698227371,/kaggle/working/mri_ensemble_fusion_outputs/confusion_matrices/cm_single_efficientnet_b0_seed42.png,/kaggle/working/mri_ensemble_fusion_outputs/confusion_matrices/cm_single_efficientnet_b0_seed42.csv,/kaggle/working/mri_ensemble_fusion_outputs/classification_reports/classification_report_single_efficientnet_b0_seed42.json,/kaggle/working/mri_ensemble_fusion_outputs/plots/reliability_single_efficientnet_b0_seed42.png,/kaggle/input/datasets/sayemahmedshayeed/baseline-cnn-result/mri_backbone_baselines_outputs/best_efficientnet_b0_seed42.pt
6
+ convnext_tiny__seed42,convnext_tiny,ConvNeXt-Tiny,42,27823204,0.06510456716608627,0.9823091247672253,0.9824939017733962,0.9822945948953601,0.9826165494285591,0.9992531872698879,0.0726724916066551,0.9831460674157303,0.9831484695235262,0.983153026842635,0.9831104969112461,0.9983606331386008,0.008378916688626179,/kaggle/working/mri_ensemble_fusion_outputs/confusion_matrices/cm_single_convnext_tiny_seed42.png,/kaggle/working/mri_ensemble_fusion_outputs/confusion_matrices/cm_single_convnext_tiny_seed42.csv,/kaggle/working/mri_ensemble_fusion_outputs/classification_reports/classification_report_single_convnext_tiny_seed42.json,/kaggle/working/mri_ensemble_fusion_outputs/plots/reliability_single_convnext_tiny_seed42.png,/kaggle/input/datasets/sayemahmedshayeed/baseline-cnn-result/mri_backbone_baselines_outputs/best_convnext_tiny_seed42.pt
7
+ densenet121__seed42,densenet121,DenseNet-121,42,6957956,0.04785946883591831,0.9823091247672253,0.9823141380049392,0.9822805558528293,0.9824195310812389,0.9994077142903257,0.047856924889672484,0.9878277153558053,0.9878113582281064,0.9878187496215687,0.9878133447665214,0.9992095550961899,0.005454951569382119,/kaggle/working/mri_ensemble_fusion_outputs/confusion_matrices/cm_single_densenet121_seed42.png,/kaggle/working/mri_ensemble_fusion_outputs/confusion_matrices/cm_single_densenet121_seed42.csv,/kaggle/working/mri_ensemble_fusion_outputs/classification_reports/classification_report_single_densenet121_seed42.json,/kaggle/working/mri_ensemble_fusion_outputs/plots/reliability_single_densenet121_seed42.png,/kaggle/input/datasets/sayemahmedshayeed/baseline-cnn-result/mri_backbone_baselines_outputs/best_densenet121_seed42.pt
8
+ mobilenet_v3_small__seed42,mobilenet_v3_small,MobileNetV3-Small,42,1521956,0.06709872207855672,0.9813780260707635,0.9815436769897888,0.981328149769024,0.9816721892794491,0.9989308126793339,0.07185961680883995,0.9850187265917603,0.9849757853063128,0.9849918327374848,0.9850077706654348,0.9986725683477449,0.007273516683989229,/kaggle/working/mri_ensemble_fusion_outputs/confusion_matrices/cm_single_mobilenet_v3_small_seed42.png,/kaggle/working/mri_ensemble_fusion_outputs/confusion_matrices/cm_single_mobilenet_v3_small_seed42.csv,/kaggle/working/mri_ensemble_fusion_outputs/classification_reports/classification_report_single_mobilenet_v3_small_seed42.json,/kaggle/working/mri_ensemble_fusion_outputs/plots/reliability_single_mobilenet_v3_small_seed42.png,/kaggle/input/datasets/sayemahmedshayeed/baseline-cnn-result/mri_backbone_baselines_outputs/best_mobilenet_v3_small_seed42.pt
9
+ vgg16_bn__seed123,vgg16_bn,VGG16-BN,123,134285380,0.0873618445690808,0.9776536312849162,0.9779303695977464,0.9776897022973287,0.9778162818507063,0.9980755053946028,0.11785322495562217,0.9644194756554307,0.9645044856653757,0.964527977005436,0.9643694554575015,0.9975909112925884,0.013353234429037995,/kaggle/working/mri_ensemble_fusion_outputs/confusion_matrices/cm_single_vgg16_bn_seed123.png,/kaggle/working/mri_ensemble_fusion_outputs/confusion_matrices/cm_single_vgg16_bn_seed123.csv,/kaggle/working/mri_ensemble_fusion_outputs/classification_reports/classification_report_single_vgg16_bn_seed123.json,/kaggle/working/mri_ensemble_fusion_outputs/plots/reliability_single_vgg16_bn_seed123.png,/kaggle/input/datasets/sayemahmedshayeed/baseline-cnn-result/mri_backbone_baselines_outputs/best_vgg16_bn_seed123.pt
10
+ mobilenet_v3_small__seed123,mobilenet_v3_small,MobileNetV3-Small,123,1521956,0.07689923900253745,0.9776536312849162,0.9778842773264018,0.9776990699982956,0.9777087921761276,0.9990071749048224,0.07556553708152419,0.9803370786516854,0.9803202327111422,0.9803418056756279,0.9803120685527208,0.9986498759354855,0.010024301038029503,/kaggle/working/mri_ensemble_fusion_outputs/confusion_matrices/cm_single_mobilenet_v3_small_seed123.png,/kaggle/working/mri_ensemble_fusion_outputs/confusion_matrices/cm_single_mobilenet_v3_small_seed123.csv,/kaggle/working/mri_ensemble_fusion_outputs/classification_reports/classification_report_single_mobilenet_v3_small_seed123.json,/kaggle/working/mri_ensemble_fusion_outputs/plots/reliability_single_mobilenet_v3_small_seed123.png,/kaggle/input/datasets/sayemahmedshayeed/baseline-cnn-result/mri_backbone_baselines_outputs/best_mobilenet_v3_small_seed123.pt
11
+ convnext_tiny__seed2026,convnext_tiny,ConvNeXt-Tiny,2026,27823204,0.07302775405488651,0.9776536312849162,0.977484034968861,0.9775049475166269,0.977749161577879,0.9993729225664927,0.07790437014985867,0.9822097378277154,0.9821713003235568,0.9821711563628671,0.9822304631179485,0.9988261503310832,0.013854810695969623,/kaggle/working/mri_ensemble_fusion_outputs/confusion_matrices/cm_single_convnext_tiny_seed2026.png,/kaggle/working/mri_ensemble_fusion_outputs/confusion_matrices/cm_single_convnext_tiny_seed2026.csv,/kaggle/working/mri_ensemble_fusion_outputs/classification_reports/classification_report_single_convnext_tiny_seed2026.json,/kaggle/working/mri_ensemble_fusion_outputs/plots/reliability_single_convnext_tiny_seed2026.png,/kaggle/input/datasets/sayemahmedshayeed/baseline-cnn-result/mri_backbone_baselines_outputs/best_convnext_tiny_seed2026.pt
12
+ densenet121__seed2026,densenet121,DenseNet-121,2026,6957956,0.09570076874671694,0.9757914338919925,0.9759267930906603,0.9757787884069115,0.9759707949097911,0.9977856943941891,0.08383405938199204,0.9644194756554307,0.9643978889368212,0.9644189685879881,0.9644186055096575,0.9988792690733579,0.019289190598418174,/kaggle/working/mri_ensemble_fusion_outputs/confusion_matrices/cm_single_densenet121_seed2026.png,/kaggle/working/mri_ensemble_fusion_outputs/confusion_matrices/cm_single_densenet121_seed2026.csv,/kaggle/working/mri_ensemble_fusion_outputs/classification_reports/classification_report_single_densenet121_seed2026.json,/kaggle/working/mri_ensemble_fusion_outputs/plots/reliability_single_densenet121_seed2026.png,/kaggle/input/datasets/sayemahmedshayeed/baseline-cnn-result/mri_backbone_baselines_outputs/best_densenet121_seed2026.pt
13
+ densenet121__seed123,densenet121,DenseNet-121,123,6957956,0.09968911743004086,0.9739292364990689,0.9739673854664407,0.9739159889765808,0.9735980467564134,0.9976868268064609,0.07860011383005662,0.9747191011235955,0.9747641445122848,0.9748104625930375,0.9747115598602282,0.9988868932999658,0.01756444989183869,/kaggle/working/mri_ensemble_fusion_outputs/confusion_matrices/cm_single_densenet121_seed123.png,/kaggle/working/mri_ensemble_fusion_outputs/confusion_matrices/cm_single_densenet121_seed123.csv,/kaggle/working/mri_ensemble_fusion_outputs/classification_reports/classification_report_single_densenet121_seed123.json,/kaggle/working/mri_ensemble_fusion_outputs/plots/reliability_single_densenet121_seed123.png,/kaggle/input/datasets/sayemahmedshayeed/baseline-cnn-result/mri_backbone_baselines_outputs/best_densenet121_seed123.pt
14
+ vgg16_bn__seed2026,vgg16_bn,VGG16-BN,2026,134285380,0.10408583767801201,0.9720670391061452,0.9722151046438083,0.9720761879541201,0.972068450817785,0.9984540600625993,0.08444612967529652,0.9765917602996255,0.9765918352506224,0.9766090798263743,0.9765983523130584,0.9990747676874356,0.013427661097005068,/kaggle/working/mri_ensemble_fusion_outputs/confusion_matrices/cm_single_vgg16_bn_seed2026.png,/kaggle/working/mri_ensemble_fusion_outputs/confusion_matrices/cm_single_vgg16_bn_seed2026.csv,/kaggle/working/mri_ensemble_fusion_outputs/classification_reports/classification_report_single_vgg16_bn_seed2026.json,/kaggle/working/mri_ensemble_fusion_outputs/plots/reliability_single_vgg16_bn_seed2026.png,/kaggle/input/datasets/sayemahmedshayeed/baseline-cnn-result/mri_backbone_baselines_outputs/best_vgg16_bn_seed2026.pt
15
+ mobilenet_v3_small__seed2026,mobilenet_v3_small,MobileNetV3-Small,2026,1521956,0.10379444774017793,0.9720670391061452,0.972096982838462,0.9719829130460705,0.9722590142810259,0.9984988748854673,0.07063478876050787,0.9803370786516854,0.9803169820469883,0.9803391301476101,0.9803577248377573,0.9991091203815945,0.010550924901212212,/kaggle/working/mri_ensemble_fusion_outputs/confusion_matrices/cm_single_mobilenet_v3_small_seed2026.png,/kaggle/working/mri_ensemble_fusion_outputs/confusion_matrices/cm_single_mobilenet_v3_small_seed2026.csv,/kaggle/working/mri_ensemble_fusion_outputs/classification_reports/classification_report_single_mobilenet_v3_small_seed2026.json,/kaggle/working/mri_ensemble_fusion_outputs/plots/reliability_single_mobilenet_v3_small_seed2026.png,/kaggle/input/datasets/sayemahmedshayeed/baseline-cnn-result/mri_backbone_baselines_outputs/best_mobilenet_v3_small_seed2026.pt
16
+ vgg16_bn__seed42,vgg16_bn,VGG16-BN,42,134285380,0.10840335874189626,0.9683426443202979,0.9685667625901326,0.9683206603405599,0.9686558442815867,0.997278133381573,0.09195423337944612,0.9700374531835206,0.9701289544321485,0.9701563944371983,0.9700085537966306,0.9978532311113247,0.008399970717867699,/kaggle/working/mri_ensemble_fusion_outputs/confusion_matrices/cm_single_vgg16_bn_seed42.png,/kaggle/working/mri_ensemble_fusion_outputs/confusion_matrices/cm_single_vgg16_bn_seed42.csv,/kaggle/working/mri_ensemble_fusion_outputs/classification_reports/classification_report_single_vgg16_bn_seed42.json,/kaggle/working/mri_ensemble_fusion_outputs/plots/reliability_single_vgg16_bn_seed42.png,/kaggle/input/datasets/sayemahmedshayeed/baseline-cnn-result/mri_backbone_baselines_outputs/best_vgg16_bn_seed42.pt
src/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """MRI ensemble Hugging Face Space package."""
src/config.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ APP_ROOT = Path(__file__).resolve().parents[1]
6
+ MODELS_DIR = APP_ROOT / "models"
7
+ RESULTS_DIR = APP_ROOT / "results"
8
+ SELECTED_ENSEMBLE_PATH = RESULTS_DIR / "selected_ensemble.json"
9
+
10
+ CLASS_NAMES = ["glioma", "meningioma", "notumor", "pituitary"]
11
+ CLASS_DISPLAY_NAMES = {
12
+ "glioma": "Glioma",
13
+ "meningioma": "Meningioma",
14
+ "notumor": "No tumor",
15
+ "pituitary": "Pituitary",
16
+ }
17
+
18
+ IMAGE_SIZE = 224
19
+ NORMALIZE_MEAN = [0.485, 0.456, 0.406]
20
+ NORMALIZE_STD = [0.229, 0.224, 0.225]
21
+
22
+ # The selected deployment ensemble from the ablation notebook.
23
+ # Zero-weight members are intentionally omitted.
24
+ ENSEMBLE_MEMBERS = [
25
+ {
26
+ "member": "efficientnet_b0__seed123",
27
+ "model_name": "efficientnet_b0",
28
+ "seed": 123,
29
+ "weight": 0.49513684,
30
+ "checkpoint_file": "best_efficientnet_b0_seed123.pt",
31
+ "display_name": "EfficientNet-B0 · seed 123",
32
+ },
33
+ {
34
+ "member": "efficientnet_b0__seed2026",
35
+ "model_name": "efficientnet_b0",
36
+ "seed": 2026,
37
+ "weight": 0.35077890,
38
+ "checkpoint_file": "best_efficientnet_b0_seed2026.pt",
39
+ "display_name": "EfficientNet-B0 · seed 2026",
40
+ },
41
+ {
42
+ "member": "mobilenet_v3_small__seed42",
43
+ "model_name": "mobilenet_v3_small",
44
+ "seed": 42,
45
+ "weight": 0.15408426,
46
+ "checkpoint_file": "best_mobilenet_v3_small_seed42.pt",
47
+ "display_name": "MobileNetV3-Small · seed 42",
48
+ },
49
+ ]
src/modeling.py ADDED
@@ -0,0 +1,344 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from dataclasses import dataclass
6
+ from functools import lru_cache
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import numpy as np
11
+ import pandas as pd
12
+ import torch
13
+ import torch.nn as nn
14
+ import torch.nn.functional as F
15
+ from PIL import Image
16
+ from torchvision import models, transforms
17
+
18
+ from .config import (
19
+ CLASS_DISPLAY_NAMES,
20
+ CLASS_NAMES,
21
+ ENSEMBLE_MEMBERS,
22
+ IMAGE_SIZE,
23
+ MODELS_DIR,
24
+ NORMALIZE_MEAN,
25
+ NORMALIZE_STD,
26
+ SELECTED_ENSEMBLE_PATH,
27
+ )
28
+
29
+
30
+ def _env_flag(name: str, default: bool = True) -> bool:
31
+ raw = os.getenv(name)
32
+ if raw is None:
33
+ return default
34
+ return raw.strip().lower() not in {"0", "false", "no", "off"}
35
+
36
+
37
+ STRICT_CHECKPOINT_LOADING = _env_flag("STRICT_CHECKPOINT_LOADING", True)
38
+
39
+
40
+ @dataclass
41
+ class LoadedMember:
42
+ member: str
43
+ display_name: str
44
+ model_name: str
45
+ seed: int
46
+ weight: float
47
+ checkpoint_file: str
48
+ checkpoint_path: Path
49
+ model: nn.Module
50
+
51
+
52
+ @dataclass
53
+ class PredictionResult:
54
+ predicted_class: str
55
+ predicted_display: str
56
+ confidence: float
57
+ probabilities: dict[str, float]
58
+ probability_df: pd.DataFrame
59
+ member_df: pd.DataFrame
60
+ ensemble_logits: torch.Tensor
61
+ input_tensor: torch.Tensor
62
+
63
+
64
+ _preprocess = transforms.Compose(
65
+ [
66
+ transforms.Resize((IMAGE_SIZE, IMAGE_SIZE)),
67
+ transforms.ToTensor(),
68
+ transforms.Normalize(mean=NORMALIZE_MEAN, std=NORMALIZE_STD),
69
+ ]
70
+ )
71
+
72
+
73
+ def preprocess_image(image: Image.Image) -> torch.Tensor:
74
+ if image is None:
75
+ raise ValueError("Please upload an MRI image first.")
76
+ return _preprocess(image.convert("RGB")).unsqueeze(0)
77
+
78
+
79
+ def build_model(model_name: str, num_classes: int = len(CLASS_NAMES)) -> nn.Module:
80
+ constructors = {
81
+ "efficientnet_b0": models.efficientnet_b0,
82
+ "mobilenet_v3_small": models.mobilenet_v3_small,
83
+ }
84
+ if model_name not in constructors:
85
+ raise ValueError(f"Unsupported deployment backbone: {model_name}")
86
+
87
+ # Do not request torchvision pretrained weights at Space startup. The fine-tuned
88
+ # checkpoint is expected to contain the trained weights.
89
+ model = constructors[model_name](weights=None)
90
+
91
+ if model_name in {"efficientnet_b0", "mobilenet_v3_small"}:
92
+ in_features = model.classifier[-1].in_features
93
+ model.classifier[-1] = nn.Linear(in_features, num_classes)
94
+ else: # Defensive; guarded above.
95
+ raise ValueError(f"No classifier replacement rule for {model_name}")
96
+ return model
97
+
98
+
99
+ def _torch_load(path: Path) -> Any:
100
+ """Load a PyTorch checkpoint across torch versions.
101
+
102
+ Newer PyTorch versions may support weights_only. We first try the safer path,
103
+ then fall back for older checkpoints that store a richer dictionary.
104
+ """
105
+ try:
106
+ return torch.load(path, map_location="cpu", weights_only=True)
107
+ except TypeError:
108
+ return torch.load(path, map_location="cpu")
109
+ except Exception:
110
+ # Only use this fallback for your own trusted checkpoints.
111
+ return torch.load(path, map_location="cpu", weights_only=False)
112
+
113
+
114
+ def clean_state_dict(checkpoint: Any) -> dict[str, torch.Tensor]:
115
+ if isinstance(checkpoint, nn.Module):
116
+ checkpoint = checkpoint.state_dict()
117
+
118
+ if isinstance(checkpoint, dict):
119
+ for key in ("model_state_dict", "state_dict", "model", "net", "weights"):
120
+ value = checkpoint.get(key)
121
+ if isinstance(value, dict):
122
+ checkpoint = value
123
+ break
124
+
125
+ if not isinstance(checkpoint, dict):
126
+ raise TypeError("Checkpoint does not contain a PyTorch state_dict-like object.")
127
+
128
+ cleaned: dict[str, torch.Tensor] = {}
129
+ for key, value in checkpoint.items():
130
+ if not torch.is_tensor(value):
131
+ continue
132
+ new_key = str(key)
133
+ for prefix in ("module.", "model."):
134
+ if new_key.startswith(prefix):
135
+ new_key = new_key[len(prefix) :]
136
+ cleaned[new_key] = value
137
+
138
+ if not cleaned:
139
+ raise ValueError("No tensor weights were found in the checkpoint.")
140
+ return cleaned
141
+
142
+
143
+ def expected_checkpoint_paths() -> dict[str, Path]:
144
+ return {m["checkpoint_file"]: MODELS_DIR / m["checkpoint_file"] for m in ENSEMBLE_MEMBERS}
145
+
146
+
147
+ def diagnose_checkpoints() -> tuple[bool, pd.DataFrame, str]:
148
+ rows = []
149
+ all_present = True
150
+ for m in ENSEMBLE_MEMBERS:
151
+ path = MODELS_DIR / m["checkpoint_file"]
152
+ exists = path.exists()
153
+ all_present = all_present and exists
154
+ rows.append(
155
+ {
156
+ "member": m["display_name"],
157
+ "weight": round(float(m["weight"]), 8),
158
+ "expected file": f"models/{m['checkpoint_file']}",
159
+ "status": "✅ found" if exists else "❌ missing",
160
+ }
161
+ )
162
+ df = pd.DataFrame(rows)
163
+ if all_present:
164
+ message = "✅ All required checkpoint files were found in `models/`."
165
+ else:
166
+ missing = [r["expected file"] for r in rows if r["status"].startswith("❌")]
167
+ message = "❌ Missing checkpoint file(s):\n" + "\n".join(f"- `{m}`" for m in missing)
168
+ return all_present, df, message
169
+
170
+
171
+ def _load_selected_metadata() -> dict[str, Any]:
172
+ if SELECTED_ENSEMBLE_PATH.exists():
173
+ return json.loads(SELECTED_ENSEMBLE_PATH.read_text(encoding="utf-8"))
174
+ return {}
175
+
176
+
177
+ @lru_cache(maxsize=1)
178
+ def load_ensemble() -> tuple[list[LoadedMember], torch.device, dict[str, Any]]:
179
+ all_present, _df, message = diagnose_checkpoints()
180
+ if not all_present:
181
+ raise FileNotFoundError(message)
182
+
183
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
184
+ loaded: list[LoadedMember] = []
185
+ for m in ENSEMBLE_MEMBERS:
186
+ checkpoint_path = MODELS_DIR / m["checkpoint_file"]
187
+ model = build_model(m["model_name"], len(CLASS_NAMES))
188
+ state_dict = clean_state_dict(_torch_load(checkpoint_path))
189
+ model.load_state_dict(state_dict, strict=STRICT_CHECKPOINT_LOADING)
190
+ model.eval().to(device)
191
+ loaded.append(
192
+ LoadedMember(
193
+ member=m["member"],
194
+ display_name=m["display_name"],
195
+ model_name=m["model_name"],
196
+ seed=int(m["seed"]),
197
+ weight=float(m["weight"]),
198
+ checkpoint_file=m["checkpoint_file"],
199
+ checkpoint_path=checkpoint_path,
200
+ model=model,
201
+ )
202
+ )
203
+ return loaded, device, _load_selected_metadata()
204
+
205
+
206
+ def predict(image: Image.Image) -> PredictionResult:
207
+ members, device, _metadata = load_ensemble()
208
+ x_cpu = preprocess_image(image)
209
+ x = x_cpu.to(device)
210
+
211
+ ensemble_probs = None
212
+ rows = []
213
+ with torch.inference_mode():
214
+ for m in members:
215
+ logits = m.model(x)
216
+ probs = F.softmax(logits, dim=1)
217
+ weighted_probs = probs * m.weight
218
+ ensemble_probs = weighted_probs if ensemble_probs is None else ensemble_probs + weighted_probs
219
+
220
+ probs_np = probs.squeeze(0).detach().cpu().numpy()
221
+ idx = int(np.argmax(probs_np))
222
+ rows.append(
223
+ {
224
+ "member": m.display_name,
225
+ "weight": round(m.weight, 8),
226
+ "member prediction": CLASS_DISPLAY_NAMES[CLASS_NAMES[idx]],
227
+ "member confidence": round(float(probs_np[idx]), 6),
228
+ }
229
+ )
230
+
231
+ if ensemble_probs is None:
232
+ raise RuntimeError("No ensemble members were loaded.")
233
+
234
+ probs_np = ensemble_probs.squeeze(0).detach().cpu().numpy()
235
+ # The weights are normalized from the optimization result, but normalize defensively.
236
+ probs_np = probs_np / max(float(probs_np.sum()), 1e-12)
237
+ top_idx = int(np.argmax(probs_np))
238
+ predicted_class = CLASS_NAMES[top_idx]
239
+
240
+ prob_rows = []
241
+ for label, probability in zip(CLASS_NAMES, probs_np):
242
+ prob_rows.append(
243
+ {
244
+ "class": CLASS_DISPLAY_NAMES[label],
245
+ "probability": float(probability),
246
+ "percent": f"{100.0 * float(probability):.2f}%",
247
+ }
248
+ )
249
+ prob_df = pd.DataFrame(prob_rows).sort_values("probability", ascending=False).reset_index(drop=True)
250
+
251
+ return PredictionResult(
252
+ predicted_class=predicted_class,
253
+ predicted_display=CLASS_DISPLAY_NAMES[predicted_class],
254
+ confidence=float(probs_np[top_idx]),
255
+ probabilities={label: float(prob) for label, prob in zip(CLASS_NAMES, probs_np)},
256
+ probability_df=prob_df,
257
+ member_df=pd.DataFrame(rows),
258
+ ensemble_logits=torch.from_numpy(np.log(np.maximum(probs_np, 1e-12))).unsqueeze(0),
259
+ input_tensor=x_cpu,
260
+ )
261
+
262
+
263
+ def get_target_layer(model: nn.Module, model_name: str) -> nn.Module:
264
+ # Last convolutional feature block for each deployed torchvision architecture.
265
+ if model_name == "efficientnet_b0":
266
+ return model.features[-1]
267
+ if model_name == "mobilenet_v3_small":
268
+ return model.features[-1]
269
+ raise ValueError(f"No Grad-CAM layer configured for {model_name}")
270
+
271
+
272
+ def gradcam_for_member(member: LoadedMember, x_cpu: torch.Tensor, target_index: int, output_size: tuple[int, int]) -> np.ndarray:
273
+ device = next(member.model.parameters()).device
274
+ x = x_cpu.to(device)
275
+ activations: list[torch.Tensor] = []
276
+ gradients: list[torch.Tensor] = []
277
+
278
+ target_layer = get_target_layer(member.model, member.model_name)
279
+
280
+ def forward_hook(_module, _inputs, output):
281
+ activations.append(output.detach())
282
+
283
+ def backward_hook(_module, _grad_input, grad_output):
284
+ gradients.append(grad_output[0].detach())
285
+
286
+ handle_fwd = target_layer.register_forward_hook(forward_hook)
287
+ handle_bwd = target_layer.register_full_backward_hook(backward_hook)
288
+ try:
289
+ member.model.zero_grad(set_to_none=True)
290
+ logits = member.model(x)
291
+ score = logits[0, target_index]
292
+ score.backward()
293
+ finally:
294
+ handle_fwd.remove()
295
+ handle_bwd.remove()
296
+
297
+ if not activations or not gradients:
298
+ raise RuntimeError(f"Could not collect gradients for {member.display_name}.")
299
+
300
+ acts = activations[-1]
301
+ grads = gradients[-1]
302
+ weights = grads.mean(dim=(2, 3), keepdim=True)
303
+ cam = torch.relu((weights * acts).sum(dim=1, keepdim=True))
304
+ cam = F.interpolate(cam, size=output_size, mode="bilinear", align_corners=False)
305
+ cam_np = cam.squeeze().detach().cpu().numpy()
306
+ cam_np = cam_np - cam_np.min()
307
+ denom = cam_np.max()
308
+ if denom > 1e-8:
309
+ cam_np = cam_np / denom
310
+ return cam_np.astype(np.float32)
311
+
312
+
313
+ def weighted_ensemble_cam(image: Image.Image, target_class: str) -> Image.Image:
314
+ members, _device, _metadata = load_ensemble()
315
+ rgb = image.convert("RGB")
316
+ x_cpu = preprocess_image(rgb)
317
+ target_index = CLASS_NAMES.index(target_class)
318
+ width, height = rgb.size
319
+
320
+ combined = np.zeros((height, width), dtype=np.float32)
321
+ total_weight = 0.0
322
+ for member in members:
323
+ try:
324
+ cam = gradcam_for_member(member, x_cpu, target_index, output_size=(height, width))
325
+ combined += cam * float(member.weight)
326
+ total_weight += float(member.weight)
327
+ except Exception:
328
+ # Heatmap is interpretability assistance, not the core prediction. Keep
329
+ # going if one hook fails; deployment prediction remains unaffected.
330
+ continue
331
+
332
+ if total_weight <= 0:
333
+ raise RuntimeError("Could not generate Grad-CAM for any ensemble member.")
334
+ combined = combined / total_weight
335
+ combined = combined - combined.min()
336
+ if combined.max() > 1e-8:
337
+ combined = combined / combined.max()
338
+
339
+ import matplotlib.cm as cm
340
+
341
+ base = np.asarray(rgb).astype(np.float32) / 255.0
342
+ heat = cm.get_cmap("magma")(combined)[..., :3].astype(np.float32)
343
+ overlay = np.clip(0.58 * base + 0.42 * heat, 0, 1)
344
+ return Image.fromarray((overlay * 255).astype(np.uint8))
validate_bundle.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from pathlib import Path
5
+
6
+ ROOT = Path(__file__).resolve().parent
7
+ REQUIRED_FILES = [
8
+ "app.py",
9
+ "README.md",
10
+ "requirements.txt",
11
+ "src/__init__.py",
12
+ "src/config.py",
13
+ "src/modeling.py",
14
+ "results/selected_ensemble.json",
15
+ "models/README.md",
16
+ ]
17
+ REQUIRED_CHECKPOINTS = [
18
+ "models/best_efficientnet_b0_seed123.pt",
19
+ "models/best_efficientnet_b0_seed2026.pt",
20
+ "models/best_mobilenet_v3_small_seed42.pt",
21
+ ]
22
+ PY_FILES = ["app.py", "src/config.py", "src/modeling.py", "validate_bundle.py"]
23
+
24
+
25
+ def check_exists() -> bool:
26
+ ok = True
27
+ for rel in REQUIRED_FILES:
28
+ path = ROOT / rel
29
+ if not path.exists():
30
+ print(f"ERROR missing required file: {rel}")
31
+ ok = False
32
+ return ok
33
+
34
+
35
+ def check_syntax() -> bool:
36
+ ok = True
37
+ for rel in PY_FILES:
38
+ path = ROOT / rel
39
+ if not path.exists():
40
+ continue
41
+ try:
42
+ ast.parse(path.read_text(encoding="utf-8"), filename=rel)
43
+ print(f"OK syntax: {rel}")
44
+ except SyntaxError as exc:
45
+ print(f"ERROR syntax in {rel}: {exc}")
46
+ ok = False
47
+ return ok
48
+
49
+
50
+ def check_checkpoints() -> bool:
51
+ ok = True
52
+ print("\nCheckpoint check:")
53
+ for rel in REQUIRED_CHECKPOINTS:
54
+ path = ROOT / rel
55
+ if path.exists():
56
+ print(f"OK found: {rel}")
57
+ else:
58
+ print(f"WARNING missing: {rel}")
59
+ ok = False
60
+ return ok
61
+
62
+
63
+ if __name__ == "__main__":
64
+ files_ok = check_exists()
65
+ syntax_ok = check_syntax()
66
+ ckpt_ok = check_checkpoints()
67
+ print("\nSummary:")
68
+ print(f"Required files: {'OK' if files_ok else 'ERROR'}")
69
+ print(f"Python syntax: {'OK' if syntax_ok else 'ERROR'}")
70
+ print(f"Checkpoints: {'OK' if ckpt_ok else 'MISSING - add them before live inference'}")
71
+ raise SystemExit(0 if files_ok and syntax_ok else 1)