Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

EdgeCare

Diagnosis everywhere - lightweight multimodal AI for scans and records on limited hospital hardware

Python PyTorch License Domain

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 problem

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.

Approach

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.

Design notes

  • Three call signatures, all valid: image only, image plus record, and image plus partial record. The smoke test in src/model.py exercises all three.
  • image_only_forward() is explicit. The deployment path where no record exists is a named method, not an implicit None default, 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() in src/utils.py reports 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.

Architecture

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

Installation

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.txt

Requires Python 3.10+ and PyTorch 2.1+. A GPU is recommended for training but not required — every module in this repository runs on CPU.

Data

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.

Usage

Verify the model builds and the shapes are right:

python src/model.py

Train:

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/exp2

Measure 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)

Results

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.

Repository layout

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

Citation

@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}
}

Author

Md Abedur Rahman — Co-author on this work. GitHub · ORCID · LinkedIn

License

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.

About

EdgeCare - Diagnosis everywhere - lightweight multimodal AI for scans and records on limited hospital hardware

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages