Views
No views yet
HeatmapUNet3D): finds nodule centres in a full
CT volume (3D sliding window → centre-probability heatmap → peaks).Student2p5D): for each candidate, from a single
64³ patch (its 7 central axial slices) it jointly predicts
Linear(8 concepts → 2), every malignancy
prediction is fully attributable to the 8 clinical concepts — you can read off
exactly which concept (e.g. spiculation) drove the decision.⚠️ Research use only. Pulmo is not a medical device and must not be used for clinical diagnosis.
UNet3D trunk (CNN-only) with concept-bottleneck multi-task
heads — was trained on LUNA16/LIDC with focal loss, MixUp and aggressive
augmentation. Teacher test: det 0.998 / mal 0.986 / Dice 0.857.loss = 0.5·hard + 0.5·soft, temperature 3.0) to imitate the frozen teacher,
for ~5–10× faster inference at a fraction of the size, with the multi-task
metrics preserved (see table).HeatmapUNet3D, base=16) trained
with a CenterNet-style penalty-reduced focal loss to output a nodule-centre
heatmap, then peak-picked + clustered into 3D candidate coordinates. It is kept
at full 3D (already lightweight at ~23 MB); distilling it gave no useful
size/speed win, so the 3D detector is shipped as-is.| Task | Metric | Pulmo (2.5D student) | Teacher (3D) |
|---|---|---|---|
| Detection | AUC | 0.997 | 0.998 |
| Malignancy | AUC | 0.986 | 0.986 |
| Segmentation | Dice | 0.859 | 0.857 |
| Metric | Value |
|---|---|
| CPM (mean sensitivity @ 1/8…8 FP/scan) | 0.629 |
| Sensitivity @ 16 FP/scan | 0.956 |
| Mean centre distance | 1.85 mm |
1import numpy as np
2from analyze_scan import load_pipeline, analyze_scan
3
4stage1, stage2, device = load_pipeline()
5
6# volume: (Z, Y, X) raw HU; spacing: (sz, sy, sx) mm in [z, y, x] order
7findings = analyze_scan(volume, spacing, stage1, stage2, device=device)
8
9for f in findings:
10 z, y, x = f["location_voxel"]
11 print(z, y, x, f["malignancy_prob"], f["prediction"], f["top_reasons"])analyze_scan.py.candidates.csv), you can run Stage 2 alone — see inference_example.py.1import torch
2from huggingface_hub import hf_hub_download
3from modeling import load_stage2, crop_stage2_input, explain_malignancy
4
5model = load_stage2(hf_hub_download("ariyul/Pulmo", "student_2p5d_best.pth"))
6x = crop_stage2_input(patch_3d, (32, 32, 32)) # 64^3 raw-HU patch -> (1, 7, 64, 64)
7with torch.no_grad():
8 out = model(x)
9mal_p = torch.softmax(out["malignancy"][0], 0)[1].item()
10print(explain_malignancy(model, out)) # concept-level explanation[-1000, 1000], then normalize to [0, 1] (identical for both stages).(Z, Y, X) HU volume; processed as sliding-window 3D patches of [64, 128, 128] at native resolution.(B, 7, 64, 64).(sz, sy, sx) in mm, [z, y, x] order.stage1_detector_v2.pth — Stage-1 detector weights (HeatmapUNet3D)student_2p5d_best.pth — Stage-2 characteriser weights (Student2p5D)modeling.py — both model definitions + find_candidates, crop_stage2_input, explain_malignancyanalyze_scan.py — end-to-end pipeline (raw volume → findings)inference_example.py — single-patch (Stage-2-only) exampleconfig.json — architecture and preprocessing parameterspeak_thresh);
the pipeline relies on Stage 2 to reject Stage-1 false positives.