Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- .gitattributes +1 -32
- .gitignore +6 -0
- DEPLOYMENT_INSTRUCTIONS.md +216 -0
- README.md +61 -7
- app.py +337 -0
- models/.gitkeep +0 -0
- models/README.md +17 -0
- models/best_efficientnet_b0_seed123.pt +3 -0
- models/best_efficientnet_b0_seed2026.pt +3 -0
- models/best_mobilenet_v3_small_seed42.pt +3 -0
- requirements.txt +7 -0
- results/classification_report_ensemble_lightweight_effnet_mobilenet_optimized_val_ce_weighted_soft.json +39 -0
- results/cm_ensemble_lightweight_effnet_mobilenet_optimized_val_ce_weighted_soft.csv +5 -0
- results/deployed_members.csv +4 -0
- results/ensemble_fusion_ablation_results.csv +0 -0
- results/selected_ensemble.json +89 -0
- results/selected_metrics.csv +2 -0
- results/single_checkpoint_results.csv +16 -0
- src/__init__.py +1 -0
- src/config.py +49 -0
- src/modeling.py +344 -0
- validate_bundle.py +71 -0
.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 |
-
*.
|
| 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:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
-
python_version:
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)
|