Diagnosis everywhere - lightweight multimodal AI for scans and records on limited hospital hardware
Reference implementation for Diagnosis Everywhere: Lightweight AI That Detects Disease from Scans and Records on Limited Hospital Hardware — Frontiers in Computer Science and Artificial Intelligence 2(2), 82-94, 2023.
The constraint that shaped this model is a deployment constraint, not an accuracy one. A district hospital has a CPU, a few gigabytes of RAM, and no reliable link to an inference server. A model that needs a GPU is not a slightly worse option there - it is not an option.
The second observation is that these hospitals already hold structured patient data. Age, vitals, and basic labs sit in a register. That signal is nearly free at inference time and most imaging models throw it away.
Ghost convolutions. Roughly half the feature maps produced by a normal convolution are close to linear transformations of the other half. Generating them explicitly with cheap depthwise operations, rather than paying full convolution cost for all of them:
primary = conv(x) # half the maps, full cost
cheap = depthwise_conv(primary) # the other half, near-free
out = concat(primary, cheap)
The result, measured by scripts/benchmark.py on a single CPU thread: 350K
parameters and a 39 ms median latency for the image-only build, 510K
parameters for the multimodal one.
A note on quantisation, because the naive estimate is wrong. Dividing the fp32
size by four predicts ~0.4 MB at int8. The measured figure is 1.30 MB, because
PyTorch dynamic quantisation converts Linear layers only, and this backbone is
almost entirely convolutional. The honest summary is that dynamic int8 buys
about 13% here; getting the rest requires static quantisation with a calibration
set, which a deployment site often cannot provide.
A missing-value mask on the tabular branch. Records in these settings are
partial. Feeding a missing lab result as 0.0 makes it indistinguishable from a
measured zero, so values and an explicit mask are concatenated:
input = concat(values * mask, mask)
Gated fusion that degrades gracefully. The gate learns how much to trust the record for each patient. When no record exists the branch is bypassed entirely rather than fed zeros, so an image-only deployment is not silently penalised by a fusion layer expecting input that never comes.
- Three call signatures, all valid: image only, image plus
record, and image plus partial record. The smoke test in
src/model.pyexercises all three. image_only_forward()is explicit. The deployment path where no record exists is a named method, not an implicitNonedefault, so it cannot be entered by accident.- Batch size 64 in the default config. Most medical imaging configs cannot afford this. This one can, and larger batches stabilise BatchNorm.
- Latency is measured by median, not mean.
measure_latency()insrc/utils.pyreports median and p95 - one slow first pass should not define the number that goes in a paper. - The benchmark pins one CPU thread. A district-hospital machine is not a 16-core server, and a latency figure measured on all cores would not transfer to the setting the model was designed for.
Image (3, 224, 224) Record (N features + mask)
│ │
├─ Ghost stem 3x3 s2 ├─ values * mask ⊕ mask
├─ GhostBottleneck ×8 ├─ Linear 128 + LayerNorm
│ 16->24->40->80->160 ├─ Linear 64
├─ 1x1 proj -> 256ch │
├─ Global average pool │
│ │
└──────────┬───────────────────┘
│
Gated fusion
gate = sigmoid(W[image ⊕ proj(record)])
fused = image + gate * proj(record)
(record branch bypassed entirely when absent)
│
Dropout + Linear ── num_classes
Ghost conv: primary = conv(x) half the maps
cheap = dwconv(primary) the other half
Measured on one CPU thread (scripts/benchmark.py):
image only 350K params · 1.49 MB fp32 · 1.30 MB int8 · 39 ms median
multimodal 510K params · 2.10 MB fp32 · 1.46 MB int8 · 38 ms median
git clone https://github.com/abedur/edgecare.git
cd edgecare
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txtRequires Python 3.10+ and PyTorch 2.1+. A GPU is recommended for training but not required — every module in this repository runs on CPU.
No clinical imagery is redistributed here. Place a dataset in this layout:
data/
train/<class_name>/*.png
val/<class_name>/*.png
test/<class_name>/*.png
Class order is derived by sorting directory names and is stored in the
checkpoint, so evaluation cannot silently run against a different label order —
src/evaluate.py raises rather than reporting corrupted metrics.
Verify the model builds and the shapes are right:
python src/model.pyTrain:
python -m src.train --config configs/default.yaml --data-root data/Evaluate a checkpoint on the held-out test split:
python -m src.evaluate --checkpoint runs/latest/best.pth --data-root data/Override any config value from the command line:
python -m src.train --epochs 40 --batch-size 32 --lr 1e-4 --output runs/exp2Measure the deployment numbers that actually matter:
from src.model import build_model
from src.utils import count_parameters, measure_latency
model = build_model(num_classes=2, num_record_features=12)
print(count_parameters(model))
print(measure_latency(model, input_size=(1, 3, 224, 224), device="cpu"))Multimodal inference with a partially missing record:
import torch
records = torch.randn(1, 12)
mask = torch.ones(1, 12)
mask[0, 5:] = 0 # labs 5-11 were never taken
logits = model(images, records, mask)Reported results for this method are in the published paper cited below. This repository contains the implementation and the evaluation harness that produces those metrics; it does not ship precomputed numbers, so that anything reported from it is reproducible from a run you can inspect.
src/evaluate.py writes a metrics.json containing accuracy, macro precision /
recall / F1, weighted F1, Cohen's kappa, AUROC, the full confusion matrix, and
per-class figures. Pass --positive-class <name> to add sensitivity,
specificity, PPV and NPV.
edgecare/
├── src/
│ ├── model.py ghost convolutions and gated multimodal fusion
│ ├── dataset.py dataset, transforms, class weighting
│ ├── train.py training loop, early stopping, checkpointing
│ ├── evaluate.py held-out evaluation and metric export
│ └── utils.py seeding, metrics, latency and parameter accounting
├── configs/
│ └── default.yaml the configuration used for the reported runs
├── tests/ shape and invariant checks
└── requirements.txt
@article{rahman2023edgecare,
title = {Diagnosis Everywhere: Lightweight AI That Detects Disease from Scans and Records on Limited Hospital Hardware},
author = {M. S. Hossain and K. C. Shekhor and M. A. Hossain and M. A. Rahman},
journal = {Frontiers in Computer Science and Artificial Intelligence 2(2)},
year = {2023}
}Md Abedur Rahman — Co-author on this work. GitHub · ORCID · LinkedIn
MIT — see LICENSE.
Released for research and educational use. This is not a medical device and has not been evaluated by any regulatory body. It must not be used to make clinical decisions about real patients.