Spaces:
Sleeping
Sleeping
| from pathlib import Path | |
| import torch | |
| import torch.nn as nn | |
| import torchvision.models as models | |
| def load_resnet18(model_path: str | None): | |
| """ | |
| Load a ResNet-18 model with optional checkpoint. | |
| Args: | |
| model_path: Path to checkpoint file, or None for random init. | |
| Returns: | |
| Tuple of (model, feature_module). | |
| """ | |
| model = models.resnet18(weights=None) | |
| model.fc = nn.Linear(model.fc.in_features, 10) | |
| feature_module = model.avgpool | |
| # try to load a checkpoint if provided | |
| if model_path and Path(model_path).exists(): | |
| model, feature_module = detect_and_build(model_path, arch_hint="resnet18", num_classes=10) | |
| else: | |
| print(f"[loader] checkpoint not found at '{model_path}'. Using randomly initialized model (ok for pipeline tests).") | |
| model.eval() | |
| return model, feature_module | |
| def get_feature_module(model): | |
| """ | |
| Returns the penultimate feature module for a given model architecture. | |
| Args: | |
| model: PyTorch model instance. | |
| Returns: | |
| The feature extraction module (e.g., model.avgpool for ResNet). | |
| Raises: | |
| NotImplementedError: If architecture is not supported. | |
| """ | |
| arch = model.__class__.__name__ | |
| if arch == 'ResNet': | |
| return model.avgpool | |
| else: | |
| raise NotImplementedError(f"Feature module not defined for architecture: {arch}") | |
| def _build_resnet18_cifar(num_classes: int = 10): | |
| """ | |
| Build a CIFAR-adapted ResNet-18. | |
| Many backdoor research pipelines (BackdoorBench, TrojanZoo, etc.) use a | |
| modified ResNet-18 for small images (32x32) with: | |
| - conv1: 3x3 kernel, stride=1, padding=1 (instead of 7x7, stride=2, padding=3) | |
| - No maxpool layer after conv1 | |
| - avgpool adjusted with AdaptiveAvgPool2d(1) (same as standard) | |
| This matches the architecture used in most CIFAR-10 backdoor papers. | |
| """ | |
| m = models.resnet18(weights=None) | |
| # Replace the ImageNet-style conv1 (7x7) with CIFAR-style (3x3) | |
| m.conv1 = nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False) | |
| # Remove maxpool — CIFAR images are too small for it | |
| m.maxpool = nn.Identity() | |
| # Adjust final classifier | |
| m.fc = nn.Linear(m.fc.in_features, num_classes) | |
| return m | |
| def detect_and_build(ckpt_path: str, arch_hint: str = "resnet18", num_classes: int = 10): | |
| """ | |
| Auto-detect the architecture variant from checkpoint weights and build | |
| the matching model. | |
| This inspects the checkpoint's conv1.weight shape to determine if the | |
| model was trained with a standard ImageNet architecture (7x7 kernel) or | |
| a CIFAR-adapted architecture (3x3 kernel), then builds and loads the | |
| correct variant. | |
| Args: | |
| ckpt_path: Path to the checkpoint file. | |
| arch_hint: Base architecture family (e.g. "resnet18"). Used as a | |
| fallback when auto-detection can't determine the variant. | |
| num_classes: Number of output classes. | |
| Returns: | |
| Tuple of (model, feature_module) with weights loaded. | |
| """ | |
| # Load and unwrap the checkpoint to inspect weight shapes | |
| ckpt = torch.load(ckpt_path, map_location="cpu", weights_only=False) | |
| sd = _unwrap_state_dict(ckpt) | |
| # Auto-detect the variant from weight shapes | |
| detected_arch = _detect_resnet_variant(sd) | |
| if detected_arch != arch_hint: | |
| print(f"[loader] auto-detected architecture variant: '{detected_arch}' " | |
| f"(hint was '{arch_hint}')") | |
| # Build the correct model | |
| model, feature_module = build_model(detected_arch, num_classes) | |
| # Load weights into the correctly-shaped model | |
| missing, unexpected = model.load_state_dict(sd, strict=False) | |
| total_params = len(list(model.state_dict().keys())) | |
| loaded_params = total_params - len(missing) | |
| if loaded_params == 0: | |
| raise RuntimeError( | |
| f"No weights were loaded from '{ckpt_path}' even after " | |
| f"auto-detecting architecture '{detected_arch}'. " | |
| f"The checkpoint may be incompatible." | |
| ) | |
| if missing: | |
| print(f"[warn] detect_and_build: {len(missing)} missing keys (partial load)") | |
| if unexpected: | |
| print(f"[warn] detect_and_build: {len(unexpected)} unexpected keys ignored") | |
| print(f"[loader] loaded {loaded_params}/{total_params} parameter tensors " | |
| f"from '{ckpt_path}' into '{detected_arch}'") | |
| return model, feature_module | |
| def _detect_resnet_variant(state_dict: dict) -> str: | |
| """ | |
| Inspect checkpoint weights to determine if this is a standard ImageNet | |
| ResNet or a CIFAR-adapted variant. | |
| Returns: | |
| "resnet18" — standard ImageNet variant (conv1 is 7x7) | |
| "resnet18_cifar" — CIFAR-adapted variant (conv1 is 3x3) | |
| """ | |
| conv1_key = "conv1.weight" | |
| if conv1_key not in state_dict: | |
| # Can't determine — fall back to standard | |
| return "resnet18" | |
| shape = state_dict[conv1_key].shape | |
| # Standard ImageNet ResNet-18 conv1: (64, 3, 7, 7) | |
| # CIFAR-adapted ResNet-18 conv1: (64, 3, 3, 3) | |
| kernel_size = shape[-1] | |
| if kernel_size == 3: | |
| return "resnet18_cifar" | |
| else: | |
| return "resnet18" | |
| def build_model(arch: str = "resnet18", num_classes: int = 10): | |
| """ | |
| Build a model with the specified architecture. | |
| Supported: | |
| - resnet18 | |
| - resnet18_cifar | |
| - resnet34 | |
| - hf_resnet50 | |
| """ | |
| arch_lower = arch.lower() | |
| if arch_lower == "resnet18": | |
| from torchvision.models import resnet18 | |
| m = resnet18(weights=None) | |
| m.fc = torch.nn.Linear(m.fc.in_features, num_classes) | |
| return m, get_feature_module(m) | |
| elif arch_lower == "resnet18_cifar": | |
| m = _build_resnet18_cifar(num_classes) | |
| return m, get_feature_module(m) | |
| elif arch_lower == "resnet34": | |
| from torchvision.models import resnet34 | |
| m = resnet34(weights=None) | |
| m.fc = torch.nn.Linear(m.fc.in_features, num_classes) | |
| return m, get_feature_module(m) | |
| elif arch_lower == "hf_resnet50": | |
| from mithridatium.loader_hf import HFImageClassifier | |
| m = HFImageClassifier("microsoft/resnet-50") | |
| return m, None | |
| else: | |
| raise NotImplementedError(f"Architecture '{arch}' not yet supported") | |
| def _unwrap_state_dict(ckpt: dict) -> dict: | |
| """ | |
| Extract the raw state dict from a checkpoint that may be wrapped | |
| in a training checkpoint dict. | |
| Handles formats like: | |
| {'model_state_dict': {...}, 'epoch': 50, 'args': ...} | |
| {'state_dict': {...}, ...} | |
| {'model': {...}, ...} | |
| {'net': {...}, ...} | |
| Or a raw state dict with layer keys directly. | |
| """ | |
| state_dict_keys = ["model_state_dict", "state_dict", "model", "net"] | |
| if isinstance(ckpt, dict): | |
| for key in state_dict_keys: | |
| if key in ckpt: | |
| print(f"[loader] found weights under '{key}' key, unwrapping") | |
| return ckpt[key] | |
| return ckpt | |
| def validate_model(model: torch.nn.Module, arch: str, input_size): | |
| """ | |
| Basic model validation: | |
| - Verify input_size looks correct | |
| - Run a dry forward pass | |
| - Verify output is [batch, num_classes] | |
| """ | |
| if not isinstance(input_size, (tuple, list)) or len(input_size) != 3: | |
| raise ValueError(f"Invalid input_size for validation: {input_size} (expected (C, H, W))") | |
| C, H, W = input_size | |
| model_cpu = model.cpu().eval() | |
| dummy = torch.randn(1, C, H, W) | |
| with torch.no_grad(): | |
| try: | |
| out = model_cpu(dummy) | |
| except Exception as ex: | |
| raise RuntimeError( | |
| "Dry forward pass failed — model architecture or weights " | |
| f"are incompatible with input size {input_size}.\nReason: {ex}" | |
| ) | |
| if not isinstance(out, torch.Tensor): | |
| raise RuntimeError( | |
| f"Model forward must return a torch.Tensor of logits, got {type(out)}" | |
| ) | |
| if out.ndim != 2: | |
| raise RuntimeError( | |
| f"Model forward must return logits of shape [batch, num_classes], got shape {tuple(out.shape)}" | |
| ) | |
| if out.shape[0] != 1: | |
| raise RuntimeError( | |
| f"Validation forward pass expected batch dimension 1, got output shape {tuple(out.shape)}" | |
| ) | |
| return True | |