2D CNN classifier over MRI slices for Alzheimer's/CDR-related classes, with
Grad-CAM interpretability checks. See docs/methodology.md for the full
write-up, including an honestly-reported interpretability limitation on
one class.
Everything below is transcribed from the actual notebook and cross-checked
against alzheimer_results.zip (metrics, training history, figures,
best_model.pth). src/models/cnn2d.py is verified to load the real
checkpoint with strict=True. Nothing in this repo is a placeholder or a
guess anymore — see docs/methodology.md for the full write-up.
- 3-class problem: Non Demented / Very mild Dementia / Mild or worse Dementia, from the 4-class OASIS Kaggle dataset with Mild+Moderate merged, subject-level split (63 held-out test subjects).
- Accuracy 0.712, but macro-F1 is only 0.590 — the model is strong on
Non Demented (F1 0.857) and notably weaker on Very Mild (F1 0.455) and
Mild-or-worse (F1 0.457). Train/test class proportions shift
substantially, so accuracy alone overstates performance — see
docs/methodology.md§1 and §6. - Training: AdamW, lr=3e-5 (deliberately lowered after an earlier value caused divergence), cosine LR schedule, batch size 32, dampened inverse-frequency class weights, early stopping (patience 5). Loss falls 0.724 → 0.332 over 15 of a possible 20 epochs while val accuracy stays in a 0.709–0.724 band with a slight upward trend, peaking at epoch 9 (checkpointed) — overfitting on loss without a val-accuracy collapse.
- Grad-CAM on Non Demented and Mild-or-worse: anatomically plausible, centered near the ventricles.
- Grad-CAM on Very Mild Dementia: consistent limitation, and now
explained — activation bleeds into background/border regions in all
3 checked examples, and the raw sample-slice figure shows Very Mild
images are cropped/oriented differently from the other two classes in
the source data. This looks like a real framing confound the model may
be exploiting, not just a modeling artifact — see
docs/methodology.md§7.
These paths are relative to this README (repo root), pointing at
results/figures/ — GitHub renders them automatically once the actual
PNGs from alzheimer_results.zip are committed there. If an image looks
broken on GitHub, it almost always means the file wasn't actually added
(check git status/.gitignore) or the filename doesn't match exactly
(case-sensitive on GitHub even if your OS isn't).
Training curves — loss and validation accuracy over the run:
Confusion matrix — test set, unseen subjects:
Class distribution shift — why the dataset's own train/test split
isn't used as-is (see data/README.md):
Sample slice per class — also the figure that revealed the Very Mild framing confound:
Grad-CAM, one example per class:
Grad-CAM, Very Mild consistency check (3 examples):
alzheimer-mri-cdr-classification/
├── README.md
├── LICENSE
├── requirements.txt
├── .gitignore
├── data/
│ └── README.md # how to get the OASIS-derived data (not committed)
├── notebooks/
│ └── 01_alzheimer_cdr_pipeline.ipynb
├── src/
│ ├── __init__.py
│ ├── config.py # DONE — constants, transcribed from notebook
│ ├── preprocessing.py # DONE — scan/merge/subject-split logic, transcribed
│ ├── dataset.py # DONE — PyTorch Dataset, transforms, class weights
│ ├── models/
│ │ ├── __init__.py
│ │ ├── cnn2d.py # DONE — transcribed verbatim, loads best_model.pth strict=True
│ │ └── resnet3d.py # legacy/unused for final results — final model is 2D only
│ ├── train.py # DONE — training loop, transcribed
│ ├── evaluate.py # DONE — test-set eval + figures, transcribed
│ └── gradcam.py # DONE — transcribed from notebook Grad-CAM cells
├── results/
│ ├── figures/ # drop in the 6 PNGs from alzheimer_results.zip — embedded in this README, must be committed (not gitignored)
│ ├── metrics/ # drop in metrics.json, train_history.json
│ └── checkpoints/ # drop in best_model.pth
└── docs/
└── methodology.md
Drop the contents of alzheimer_results.zip straight into results/ —
figures, metrics, and checkpoints all landed exactly where the notebook
already saves them (/kaggle/working/results/...), so no renaming needed.
Every src/ module is now a direct transcription of the notebook, not a
guess — including one honest correction along the way: an earlier pass
reconstructed cnn2d.py from the checkpoint's state_dict alone and got
the residual blocks right but missed a stem MaxPool2d and two
Dropout/Dropout2d layers (neither has learnable weights, so that
version still loaded strict=True while being architecturally
incomplete). The current version is transcribed from the real
model-definition cell and re-verified.





