Spaces:
Running
Running
| from __future__ import annotations | |
| import math | |
| from pathlib import Path | |
| import timm | |
| import torch | |
| import torch.nn as nn | |
| from torchvision import transforms | |
| class ViTBackbone(nn.Module): | |
| def __init__(self) -> None: | |
| super().__init__() | |
| self.model = timm.create_model( | |
| "vit_base_patch14_dinov2", | |
| pretrained=False, | |
| num_classes=0, | |
| img_size=518, | |
| ) | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| return self.model.forward_features(x) # (B, 1+N, D) | |
| class ProjectionHead(nn.Module): | |
| def __init__(self, in_dim: int, out_dim: int) -> None: | |
| super().__init__() | |
| self.net = nn.Sequential( | |
| nn.Linear(in_dim, out_dim), | |
| nn.BatchNorm1d(out_dim), | |
| ) | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| return self.net(x) | |
| class SpatialAttention(nn.Module): | |
| def __init__(self) -> None: | |
| super().__init__() | |
| self.alpha = nn.Parameter(torch.ones(())) | |
| self.proj = nn.Sequential( | |
| nn.Conv2d(1, 8, kernel_size=3, padding=1, bias=False), | |
| nn.ReLU(inplace=True), | |
| nn.Conv2d(8, 1, kernel_size=1), | |
| ) | |
| def forward(self, patch_tokens: torch.Tensor) -> torch.Tensor: | |
| B, N, _ = patch_tokens.shape | |
| H = W = int(math.isqrt(N)) | |
| scores = patch_tokens.norm(dim=-1).view(B, 1, H, W) | |
| attn = self.proj(scores).view(B, N) | |
| return torch.softmax(self.alpha * attn, dim=-1) | |
| class FusionGate(nn.Module): | |
| def __init__(self, in_dim: int = 1024, out_dim: int = 512) -> None: | |
| super().__init__() | |
| self.gate = nn.Sequential( | |
| nn.Linear(in_dim, out_dim), | |
| nn.GELU(), | |
| nn.Dropout(p=0.1), | |
| nn.Linear(out_dim, out_dim), | |
| ) | |
| def forward(self, global_feat: torch.Tensor, local_feat: torch.Tensor) -> torch.Tensor: | |
| return self.gate(torch.cat([global_feat, local_feat], dim=-1)) | |
| class DINOv2Classifier(nn.Module): | |
| def __init__(self, num_classes: int = 13) -> None: | |
| super().__init__() | |
| self.backbone = ViTBackbone() | |
| self.global_proj = ProjectionHead(768, 512) | |
| self.local_proj = ProjectionHead(768, 512) | |
| self.attention = SpatialAttention() | |
| self.fusion = FusionGate(1024, 512) | |
| self.embedding_head = nn.Sequential(nn.Linear(512, 256)) # Sequential required: key is embedding_head.0.* | |
| self.classifier = nn.Linear(256, num_classes) | |
| self.global_classifier = nn.Linear(512, num_classes) # aux — loaded for state-dict compat, unused at inference | |
| self.local_classifier = nn.Linear(512, num_classes) # aux — loaded for state-dict compat, unused at inference | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| tokens = self.backbone(x) | |
| cls, patches = tokens[:, 0], tokens[:, 1:] | |
| global_feat = self.global_proj(cls) | |
| attn_weights = self.attention(patches) | |
| attended = (patches * attn_weights.unsqueeze(-1)).sum(1) | |
| local_feat = self.local_proj(attended) | |
| fused = self.fusion(global_feat, local_feat) | |
| return self.classifier(self.embedding_head(fused)) | |
| def build_transform(image_size: int = 518) -> transforms.Compose: | |
| return transforms.Compose([ | |
| transforms.Resize(image_size, interpolation=transforms.InterpolationMode.BICUBIC), | |
| transforms.CenterCrop(image_size), | |
| transforms.ToTensor(), | |
| transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), | |
| ]) | |
| def _resolve_checkpoint(path: Path, hf_repo_id: str, hf_filename: str) -> Path: | |
| if path.exists(): | |
| return path | |
| if hf_repo_id: | |
| from huggingface_hub import hf_hub_download | |
| return Path(hf_hub_download(repo_id=hf_repo_id, filename=hf_filename)) | |
| raise FileNotFoundError( | |
| f"Checkpoint not found at '{path}'. Set APP_HF_REPO_ID to download from HuggingFace Hub." | |
| ) | |
| def load_model( | |
| checkpoint_path: Path, | |
| num_classes: int, | |
| device: str, | |
| hf_repo_id: str = "", | |
| hf_filename: str = "best_mcc.pt", | |
| ) -> tuple[DINOv2Classifier, dict]: | |
| resolved = _resolve_checkpoint(checkpoint_path, hf_repo_id, hf_filename) | |
| ckpt = torch.load(resolved, map_location=device, weights_only=False) | |
| model = DINOv2Classifier(num_classes=num_classes) | |
| model.load_state_dict(ckpt["model"]) | |
| model.to(device) | |
| model.eval() | |
| meta = { | |
| "epoch": ckpt.get("epoch", -1), | |
| "metric": ckpt.get("metric", ""), | |
| "value": ckpt.get("value", 0.0), | |
| } | |
| return model, meta | |