therayelab's picture
Load inference libraries on demand for faster app startup
7db9f93 verified
Raw History Blame Contribute Delete
3.59 kB
"""Pinned model inference and official test split; no training or remote code."""
import hashlib
import os
from functools import lru_cache
from pathlib import Path
from urllib.request import urlopen
ROOT = Path(__file__).resolve().parent
CACHE = Path(os.environ.get("APP_CACHE", ROOT / ".cache"))
os.environ.setdefault("HF_HOME", str(CACHE / "huggingface"))
os.environ.setdefault("HF_HUB_DISABLE_XET", "1")
import numpy as np
import torch
from PIL import Image, ImageOps
MODEL_ID = "therayelab/pneumoniamnist-resnet18"
REVISION = "a5e0ebebfcf20bee6701e6d528e33c5bf7a33b94"
DATA_URL = "https://zenodo.org/records/10519652/files/pneumoniamnist.npz?download=1"
DATA_MD5 = "28209eda62fecd6e6a2d98b1501bb15f"
LABELS = ["Normal", "Pneumonia"]
torch.set_num_threads(min(4, os.cpu_count() or 1))
@lru_cache(maxsize=1)
def model_bundle():
from transformers import AutoImageProcessor, AutoModelForImageClassification
processor = AutoImageProcessor.from_pretrained(MODEL_ID, revision=REVISION, use_fast=False)
model = AutoModelForImageClassification.from_pretrained(
MODEL_ID, revision=REVISION, use_safetensors=True, trust_remote_code=False
).eval()
if model.config.id2label != {0: "0", 1: "1"}:
raise ValueError("Unexpected model label mapping; review before using this demo.")
return processor, model
def clean_image(image):
if image is None:
raise ValueError("Upload an image first.")
if image.width * image.height > 20_000_000:
raise ValueError("Please use an image smaller than 20 megapixels.")
return ImageOps.exif_transpose(image).convert("RGB")
def predict(images):
processor, model = model_bundle()
inputs = processor(images=[clean_image(i) for i in images], return_tensors="pt")
with torch.inference_mode():
probabilities = model(**inputs).logits.softmax(dim=-1).cpu().numpy()
return probabilities
@lru_cache(maxsize=1)
def test_data():
CACHE.mkdir(parents=True, exist_ok=True)
path = CACHE / "pneumoniamnist.npz"
if not path.exists():
with urlopen(DATA_URL, timeout=120) as response:
data = response.read(20_000_000)
if hashlib.md5(data).hexdigest() != DATA_MD5:
raise ValueError("Dataset checksum failed; please retry later.")
path.write_bytes(data)
if hashlib.md5(path.read_bytes()).hexdigest() != DATA_MD5:
raise ValueError("Dataset checksum failed; remove the cached dataset and retry.")
with np.load(path, allow_pickle=False) as data:
images = data["test_images"].copy()
labels = data["test_labels"].reshape(-1).copy()
assert images.shape == (624, 28, 28) and set(labels) == {0, 1}
return images, labels
@lru_cache(maxsize=1)
def test_predictions():
images, _ = test_data()
return np.concatenate([predict([Image.fromarray(i) for i in images[s:s+16]])
for s in range(0, len(images), 16)])
def metrics(labels, probabilities, threshold):
predicted = (np.asarray(probabilities)[:, 1] >= threshold).astype(int)
labels = np.asarray(labels)
tn = int(np.sum((labels == 0) & (predicted == 0)))
fp = int(np.sum((labels == 0) & (predicted == 1)))
fn = int(np.sum((labels == 1) & (predicted == 0)))
tp = int(np.sum((labels == 1) & (predicted == 1)))
divide = lambda a, b: a / b if b else None
return {"n": len(labels), "accuracy": divide(tp+tn, len(labels)),
"sensitivity": divide(tp, tp+fn), "specificity": divide(tn, tn+fp),
"precision": divide(tp, tp+fp), "tn": tn, "fp": fp, "fn": fn, "tp": tp}