Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| from pathlib import Path | |
| from typing import Any, Mapping | |
| import numpy as np | |
| import torch | |
| from PIL import Image | |
| from torch import nn | |
| from torchvision import transforms | |
| from torchvision.models import EfficientNet_B0_Weights, efficientnet_b0 | |
| EFFICIENTNET_VERSION = "efficientnet-b0-ft-v2" | |
| IMAGE_SIZE = 224 | |
| IMAGENET_MEAN = [0.485, 0.456, 0.406] | |
| IMAGENET_STD = [0.229, 0.224, 0.225] | |
| EFFICIENTNET_ARCHITECTURE_CURRENT = "gelu-head" | |
| EFFICIENTNET_ARCHITECTURE_LEGACY = "legacy-spatial-attention" | |
| SUPPORTED_EFFICIENTNET_ARCHITECTURES = ( | |
| EFFICIENTNET_ARCHITECTURE_CURRENT, | |
| EFFICIENTNET_ARCHITECTURE_LEGACY, | |
| ) | |
| def clamp(value: float, lower: float = 0.0, upper: float = 1.0) -> float: | |
| return max(lower, min(upper, value)) | |
| class SpatialAttention(nn.Module): | |
| """Legacy spatial attention block used by the shipped checkpoint.""" | |
| def __init__(self, kernel_size: int = 7) -> None: | |
| super().__init__() | |
| self.conv = nn.Conv2d( | |
| 2, | |
| 1, | |
| kernel_size=kernel_size, | |
| padding=kernel_size // 2, | |
| bias=False, | |
| ) | |
| self.sigmoid = nn.Sigmoid() | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| avg_out = torch.mean(x, dim=1, keepdim=True) | |
| max_out, _ = torch.max(x, dim=1, keepdim=True) | |
| attention = torch.cat([avg_out, max_out], dim=1) | |
| scale = self.sigmoid(self.conv(attention)) | |
| return x * scale | |
| def build_efficientnet_model( | |
| *, | |
| pretrained: bool = True, | |
| architecture: str = EFFICIENTNET_ARCHITECTURE_CURRENT, | |
| ) -> nn.Module: | |
| weights = EfficientNet_B0_Weights.IMAGENET1K_V1 if pretrained else None | |
| model = efficientnet_b0(weights=weights) | |
| if architecture == EFFICIENTNET_ARCHITECTURE_LEGACY: | |
| model.features.add_module("spatial_attention", SpatialAttention()) | |
| model.classifier = nn.Sequential( | |
| nn.Dropout(0.35), | |
| nn.Linear(1280, 512), | |
| nn.GELU(), | |
| nn.BatchNorm1d(512), | |
| nn.Dropout(0.25), | |
| nn.Linear(512, 128), | |
| nn.GELU(), | |
| nn.BatchNorm1d(128), | |
| nn.Dropout(0.15), | |
| nn.Linear(128, 2), | |
| ) | |
| elif architecture == EFFICIENTNET_ARCHITECTURE_CURRENT: | |
| model.classifier = nn.Sequential( | |
| nn.Dropout(0.35), | |
| nn.Linear(1280, 512), | |
| nn.GELU(), | |
| nn.Dropout(0.25), | |
| nn.Linear(512, 128), | |
| nn.GELU(), | |
| nn.Dropout(0.15), | |
| nn.Linear(128, 2), | |
| ) | |
| else: | |
| raise ValueError( | |
| f"Unsupported EfficientNet architecture {architecture!r}. " | |
| f"Supported values: {SUPPORTED_EFFICIENTNET_ARCHITECTURES!r}" | |
| ) | |
| for param in model.features.parameters(): | |
| param.requires_grad = False | |
| for name, param in model.features.named_parameters(): | |
| if architecture == EFFICIENTNET_ARCHITECTURE_LEGACY: | |
| if name.startswith(("4", "5", "6", "7", "8", "spatial_attention")): | |
| param.requires_grad = True | |
| elif name.startswith(("4", "5", "6", "7", "8")): | |
| param.requires_grad = True | |
| for param in model.classifier.parameters(): | |
| param.requires_grad = True | |
| return model | |
| def build_train_transform() -> transforms.Compose: | |
| return transforms.Compose( | |
| [ | |
| transforms.RandomHorizontalFlip(), | |
| transforms.RandomVerticalFlip(p=0.15), | |
| transforms.ColorJitter( | |
| brightness=0.4, | |
| contrast=0.4, | |
| saturation=0.3, | |
| hue=0.05, | |
| ), | |
| transforms.RandomRotation(20), | |
| transforms.RandomAffine( | |
| degrees=0, | |
| translate=(0.12, 0.12), | |
| scale=(0.88, 1.12), | |
| ), | |
| transforms.RandomPerspective(distortion_scale=0.15, p=0.3), | |
| transforms.Resize((IMAGE_SIZE, IMAGE_SIZE)), | |
| transforms.ToTensor(), | |
| transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD), | |
| transforms.RandomErasing( | |
| p=0.25, | |
| scale=(0.02, 0.12), | |
| ratio=(0.3, 3.3), | |
| ), | |
| ] | |
| ) | |
| def build_val_transform() -> transforms.Compose: | |
| return transforms.Compose( | |
| [ | |
| transforms.Resize((IMAGE_SIZE, IMAGE_SIZE)), | |
| transforms.ToTensor(), | |
| transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD), | |
| ] | |
| ) | |
| def load_efficientnet_checkpoint( | |
| path: str | Path, | |
| *, | |
| map_location: str | torch.device = "cpu", | |
| ) -> dict[str, Any]: | |
| checkpoint = torch.load(path, map_location=map_location) | |
| state_dict = checkpoint["state_dict"] if "state_dict" in checkpoint else checkpoint | |
| architecture_hint = checkpoint.get("architecture") | |
| architecture = _normalize_architecture_hint( | |
| architecture_hint, | |
| state_dict=state_dict, | |
| ) | |
| model, resolved_architecture = _load_compatible_model( | |
| state_dict, | |
| architecture_hint=architecture, | |
| ) | |
| device = torch.device(map_location) | |
| model.to(device) | |
| model.eval() | |
| return { | |
| "version": checkpoint.get("version", EFFICIENTNET_VERSION), | |
| "architecture": resolved_architecture, | |
| "created_at": checkpoint.get("created_at"), | |
| "decision_threshold": float(checkpoint.get("decision_threshold", 0.5)), | |
| "hb_mean": float(checkpoint.get("hb_mean", 0.0)), | |
| "hb_std": float(checkpoint.get("hb_std", 1.0)), | |
| "val_metrics": checkpoint.get("val_metrics"), | |
| "model": model, | |
| "device": device, | |
| "transform": build_val_transform(), | |
| } | |
| def predict_with_efficientnet_model( | |
| bundle: dict[str, Any], | |
| image: Image.Image, | |
| *, | |
| mc_passes: int = 10, | |
| ) -> dict[str, float]: | |
| model: nn.Module = bundle["model"] | |
| device: torch.device = bundle["device"] | |
| transform = bundle["transform"] | |
| hb_mean = float(bundle.get("hb_mean", 0.0)) | |
| hb_std_scale = max(float(bundle.get("hb_std", 1.0)), 1e-6) | |
| rgb = image.convert("RGB") | |
| tta_images = [ | |
| rgb, | |
| rgb.transpose(Image.FLIP_LEFT_RIGHT), | |
| ] | |
| probabilities: list[float] = [] | |
| hemoglobin_values: list[float] = [] | |
| with torch.no_grad(): | |
| for tta_img in tta_images: | |
| tensor = transform(tta_img).unsqueeze(0).to(device) | |
| for _ in range(max(mc_passes, 1)): | |
| model.eval() | |
| if mc_passes > 1: | |
| _enable_dropout(model) | |
| output = model(tensor) | |
| probabilities.append(float(torch.sigmoid(output[:, 0]).item())) | |
| hemoglobin_values.append( | |
| float((output[:, 1].item() * hb_std_scale) + hb_mean) | |
| ) | |
| mean_probability = float(np.mean(probabilities)) | |
| mean_hemoglobin = float(np.mean(hemoglobin_values)) | |
| probability_std = float(np.std(probabilities)) | |
| hemoglobin_std = float(np.std(hemoglobin_values)) | |
| margin_uncertainty = 1.0 - min(1.0, abs(mean_probability - 0.5) * 2.5) | |
| uncertainty = clamp( | |
| (probability_std * 2.2) | |
| + (min(hemoglobin_std / 2.0, 1.0) * 0.30) | |
| + (margin_uncertainty * 0.18), | |
| 0.04, | |
| 0.95, | |
| ) | |
| model.eval() | |
| return { | |
| "anemia_risk": mean_probability, | |
| "predicted_hemoglobin": mean_hemoglobin, | |
| "uncertainty": uncertainty, | |
| "decision_threshold": float(bundle.get("decision_threshold", 0.5)), | |
| "probability_std": probability_std, | |
| "hemoglobin_std": hemoglobin_std, | |
| } | |
| def _normalize_architecture_hint( | |
| architecture_hint: object, | |
| *, | |
| state_dict: Mapping[str, Any], | |
| ) -> str: | |
| hint = str(architecture_hint).strip().lower() if architecture_hint else "" | |
| aliases = { | |
| EFFICIENTNET_ARCHITECTURE_CURRENT: EFFICIENTNET_ARCHITECTURE_CURRENT, | |
| "current": EFFICIENTNET_ARCHITECTURE_CURRENT, | |
| "gelu": EFFICIENTNET_ARCHITECTURE_CURRENT, | |
| "gelu-head": EFFICIENTNET_ARCHITECTURE_CURRENT, | |
| EFFICIENTNET_ARCHITECTURE_LEGACY: EFFICIENTNET_ARCHITECTURE_LEGACY, | |
| "legacy": EFFICIENTNET_ARCHITECTURE_LEGACY, | |
| "legacy-spatial-attention": EFFICIENTNET_ARCHITECTURE_LEGACY, | |
| "spatial-attention": EFFICIENTNET_ARCHITECTURE_LEGACY, | |
| "spatial_attention": EFFICIENTNET_ARCHITECTURE_LEGACY, | |
| } | |
| if hint in aliases: | |
| return aliases[hint] | |
| return _detect_checkpoint_architecture(state_dict) | |
| def _detect_checkpoint_architecture(state_dict: Mapping[str, Any]) -> str: | |
| keys = set(state_dict.keys()) | |
| if ( | |
| "features.spatial_attention.conv.weight" in keys | |
| or "classifier.3.running_mean" in keys | |
| or "classifier.7.running_mean" in keys | |
| or "classifier.9.weight" in keys | |
| ): | |
| return EFFICIENTNET_ARCHITECTURE_LEGACY | |
| return EFFICIENTNET_ARCHITECTURE_CURRENT | |
| def _load_compatible_model( | |
| state_dict: Mapping[str, Any], | |
| *, | |
| architecture_hint: str, | |
| ) -> tuple[nn.Module, str]: | |
| candidate_architectures = [architecture_hint] + [ | |
| architecture | |
| for architecture in SUPPORTED_EFFICIENTNET_ARCHITECTURES | |
| if architecture != architecture_hint | |
| ] | |
| errors: dict[str, str] = {} | |
| for architecture in candidate_architectures: | |
| model = build_efficientnet_model( | |
| pretrained=False, | |
| architecture=architecture, | |
| ) | |
| try: | |
| model.load_state_dict(state_dict, strict=True) | |
| return model, architecture | |
| except RuntimeError as exc: | |
| errors[architecture] = str(exc) | |
| error_summary = " | ".join( | |
| f"{architecture}: {message}" | |
| for architecture, message in errors.items() | |
| ) | |
| raise RuntimeError( | |
| "EfficientNet checkpoint does not match any supported architecture. " | |
| f"Tried {candidate_architectures!r}. Errors: {error_summary}" | |
| ) | |
| def _enable_dropout(model: nn.Module) -> None: | |
| for module in model.modules(): | |
| if isinstance(module, nn.Dropout): | |
| module.train() | |