Downloads · 30 days
0
Pranaytg/pulmonet-ai
pulmonet-ai is a machine learning model from Pranaytg. Use it for the machine learning task on the model card, and read the license before you ship it in a product.
Multi-label thoracic pathology classification on the NIH ChestX-ray14 dataset, with Grad-CAM explanations and a containerised inference API.
Downloads · 30 days
0
Access
Public
Updated Aug 8, 2026
Repo size
240 MB
Likes
0
Public
Click a slice to open those files.
.pt212 MB · 100%
From the Hugging Face model README
Multi-label thoracic pathology classification on the NIH ChestX-ray14 dataset, with Grad-CAM explanations and a containerised inference API.
Research and educational use only. PulmoNet AI is not a medical device, has not been clinically validated, and must not be used for diagnosis or treatment decisions.
An end-to-end pipeline over all 112,120 chest X-rays from 30,805 patients:
| Live demo | https://pranaytg-pulmonet-ai-demo.static.hf.space |
| Inference API | https://rcjvt4hup24qsu7nq5kukoonci0itnpb.lambda-url.ap-south-1.on.aws/health |
| Model + code | https://huggingface.co/Pranaytg/pulmonet-ai |
| Run it yourself | RUNNING_LOCALLY.md |
The demo is a free static Space; the API is an AWS Lambda container serving both classification and Grad-CAM. First request after idle takes ~15 s (cold start), then ~200 ms.
pulmonet/ config, data, model, losses, gradcam, engine, inference
scripts/ ingest, train, evaluate, export_onnx
app/ FastAPI app, Lambda handler, local demo UI
space/ static frontend deployed to Hugging Face Spaces
notebooks/ kaggle_train.ipynb - same pipeline on a free cloud GPU
docker/ Dockerfile (generic) and Dockerfile.lambda (AWS)
tests/ 33 tests: data contract, Grad-CAM, API, and backend equivalence
reports/ metrics.json and auroc_table.md (generated, never hand-edited)
See reports/metrics.json and
reports/auroc_table.md. Those files are written by
scripts/evaluate.py and are the only source of truth for numbers quoted about this project.
Evaluation is on the official NIH test split — 25,596 images from 2,797 patients, with no patient appearing in both training and test.
<!-- RESULTS -->Model: nih-densenet121 at 224px.
| Pathology | AUROC | Wang et al. 2017 | Delta |
|---|---|---|---|
| Atelectasis | 0.662 | 0.716 | -0.054 |
| Cardiomegaly | 0.807 | 0.807 | +0.000 |
| Effusion | 0.746 | 0.784 | -0.038 |
| Infiltration | 0.632 | 0.609 | +0.023 |
| Mass | 0.709 | 0.706 | +0.003 |
| Nodule | 0.626 | 0.671 | -0.045 |
| Pneumonia | 0.620 | 0.633 | -0.013 |
| Pneumothorax | 0.600 | 0.806 | -0.206 |
| Consolidation | 0.663 | 0.708 | -0.045 |
| Edema | 0.735 | 0.835 | -0.100 |
| Emphysema | 0.514 | 0.815 | -0.301 |
| Fibrosis | 0.671 | 0.769 | -0.098 |
| Pleural_Thickening | 0.681 | 0.708 | -0.027 |
| Hernia | 0.735 | 0.767 | -0.032 |
| Mean | 0.671 | 0.738 | -0.067 |
pip install -r requirements-train.txt
# install the torch build matching your CUDA runtime, e.g.
pip install torch --index-url https://download.pytorch.org/whl/cu118
python scripts/ingest.py --limit 200 # dry run: verifies the pipeline in ~30 s
python scripts/ingest.py # full ingest, ~15 min, ~1.7 GB on disk
python scripts/train.py --smoke # 50 steps, verifies the training loop
python scripts/train.py # full run
python scripts/evaluate.py # writes reports/
uvicorn app.main:app --port 7860 # http://localhost:7860
pytest
Set PULMONET_DATA to control where images and checkpoints go (default C:\pulmonet_data).
Keep it outside any cloud-synced folder.
notebooks/kaggle_train.ipynb runs the same pipeline on
Kaggle's free P100 (16 GB, 30 GPU-hours/week). Two reasons it is the better option than a
4 GB laptop GPU here:
--source kaggle reads it in place — no
download at all.Roughly 3–4× faster end to end. scripts/ingest.py --source kaggle is the only piece that
differs; training, evaluation, and reporting are byte-identical.
Deployment: see DEPLOYMENT.md.
These are the choices worth understanding; each one is a place where the obvious approach is wrong.
A patient contributes multiple X-rays. Splitting randomly by image puts near-duplicate views of the same chest on both sides of the train/test boundary, which inflates AUROC by several points and measures memorisation rather than generalisation.
This project uses the official train_val_list.txt / test_list.txt, which partition by
patient — verified empirically: 28,008 training patients and 2,797 test patients, zero
overlap. The validation set is then carved out of the training patients, again by patient ID
(pulmonet/data.py:split_manifest), and tests/test_data.py asserts all three splits are
mutually disjoint.
One X-ray can show Effusion and Infiltration and Atelectasis simultaneously. So the model
emits 14 independent sigmoids, not a softmax over 14 classes, and trains with
BCEWithLogitsLoss. Probabilities do not sum to 1, and they are not supposed to.
"No Finding" is deliberately not a 15th class — it is the absence of all 14 labels, so an all-zero prediction vector already expresses it. Modelling it separately would double-count.
ChestX-ray14 prevalence ranges from Infiltration at ~17.7% to Hernia at ~0.2%. Unweighted BCE
scores 99.8% "accuracy" on Hernia by always predicting negative, which is worthless — hence
pos_weight = n_neg / n_pos per class.
But Hernia's raw ratio is ~500×, and feeding that in lets a single positive sample dominate the
batch gradient; the loss diverges within a few hundred steps. Clipping at 10× keeps rare classes
meaningfully up-weighted while bounding any one class's contribution
(pulmonet/losses.py:compute_pos_weight).
Horizontal flip, ±10° rotation, mild brightness/contrast, and RandomResizedCrop at 0.85–1.0.
Deliberately no vertical flip and no aggressive rotation: thoracic anatomy has a fixed
orientation and the heart sits on one side, so an upside-down chest X-ray is not a sample the
model will ever see at inference. Augmenting toward it spends capacity on an impossible input.
With Hernia at 0.2% prevalence, accuracy is uninformative. AUROC is threshold-free and
prevalence-insensitive, and it is the metric the ChestX-ray14 literature reports, so results
here are directly comparable to Wang et al. (2017), which evaluate.py prints alongside.
Classes with no positive samples in a split report NaN rather than a fabricated 0.5.
B4's native resolution is 380px, which OOMs on an RTX 3050 Laptop at any useful batch size. The working configuration:
| Lever | Setting | Effect |
|---|---|---|
| Resolution | 224 | fits; source images are stored at 300px so crops have room |
| Batch | 8 | see below — 16 fits only on an idle GPU |
| Gradient accumulation | ×4 | effective batch 32 without the memory |
AMP (torch.amp) | on | ~40% memory saved, ~1.7× faster |
channels_last | on | better conv kernel selection |
Batch 16 measures at 2.3 GB reserved and looks fine on an idle card — but a running browser
holds ~2 GB of VRAM for compositing, leaving under 2 GB. Training then dies partway through
an epoch with CUDA out of memory (or, more confusingly, CUDNN_STATUS_INTERNAL_ERROR, or
CUBLAS_STATUS_NOT_INITIALIZED — cuDNN and cuBLAS both report allocation failures as opaque
init errors). Batch 8 peaks near 1.25 GB and trains reliably on a machine in use.
Checkpoint selection is on validation mean AUROC, not validation loss — the pos-weighted loss is dominated by rare classes and can improve while ranking quality is flat.
pulmonet/gradcam.py is ~60 lines using a forward hook plus a tensor gradient hook on
conv_head. No pytorch-grad-cam dependency, so every line is explicable.
Two non-obvious details it gets right:
__exit__. A leaked hook accumulates on every API request.torch.enable_grad() internally. The API wraps inference in no_grad, under which
a naive implementation silently returns an all-zero map that still looks like a heatmap.What Grad-CAM is for here: confirming the network attends to lung fields rather than to text burn-ins, scanner artifacts, or image borders — a documented shortcut-learning failure mode in chest X-ray models. It is not clinical localisation and not evidence of diagnostic validity.
The 300px source mirror ships (image, labels) but drops the original filenames, so patient IDs
and official split membership are not directly available. Its rows are in official split-list
order and its counts match exactly (86,524 / 25,596), so position i of the train split should
be train_val_list.txt[i].
That is an assumption, and a wrong one would silently corrupt every metric in the project. So
scripts/ingest.py re-derives each row's expected findings from Data_Entry_2017.csv and
asserts they match the labels the parquet actually carries — for all 112,120 rows. Any mismatch
aborts the ingest. --source nih-box fetches the original 1024px archives instead, streaming
one at a time so peak disk stays ~13 GB rather than ~90 GB.
Wang X., Peng Y., Lu L., Lu Z., Bagheri M., Summers R.M. ChestX-ray8: Hospital-scale Chest X-ray Database and Benchmarks on Weakly-Supervised Classification and Localization of Common Thorax Diseases. CVPR 2017.
Dataset released by the NIH Clinical Center: https://nihcc.app.box.com/v/ChestXray-NIHCC