Spaces:
Running on Zero
Running on Zero
| """Model architectures, loss functions, and metric helpers.""" | |
| from typing import Optional | |
| import torch | |
| import torch.nn as nn | |
| import torchvision.models as models | |
| import segmentation_models_pytorch as smp | |
| def get_device() -> torch.device: | |
| """Auto-detect CUDA or MPS device, falling back to CPU. | |
| Returns: | |
| torch.device object set to the best available accelerator. | |
| """ | |
| if torch.cuda.is_available(): | |
| return torch.device("cuda") | |
| if torch.backends.mps.is_available(): | |
| return torch.device("mps") | |
| return torch.device("cpu") | |
| def get_classifier( | |
| num_classes: int = 4, dropout: float = 0.4, freeze_backbone: bool = True, model_name: str = "efficientnet_b3" | |
| ) -> nn.Module: | |
| """Build an EfficientNet classifier with a custom head. | |
| Uses ImageNet-pretrained weights and replaces the final classification | |
| layer with Dropout + Linear(num_classes). | |
| Args: | |
| num_classes: Number of output classes. | |
| dropout: Dropout probability before the final linear layer. | |
| freeze_backbone: If True, freeze all backbone parameters initially. | |
| model_name: EfficientNet variant ('efficientnet_b0' to 'efficientnet_b7'). | |
| Returns: | |
| PyTorch EfficientNet model. | |
| """ | |
| weight_map = { | |
| "efficientnet_b0": models.EfficientNet_B0_Weights.IMAGENET1K_V1, | |
| "efficientnet_b1": models.EfficientNet_B1_Weights.IMAGENET1K_V1, | |
| "efficientnet_b2": models.EfficientNet_B2_Weights.IMAGENET1K_V1, | |
| "efficientnet_b3": models.EfficientNet_B3_Weights.IMAGENET1K_V1, | |
| "efficientnet_b4": models.EfficientNet_B4_Weights.IMAGENET1K_V1, | |
| "efficientnet_b5": models.EfficientNet_B5_Weights.IMAGENET1K_V1, | |
| "efficientnet_b6": models.EfficientNet_B6_Weights.IMAGENET1K_V1, | |
| "efficientnet_b7": models.EfficientNet_B7_Weights.IMAGENET1K_V1, | |
| } | |
| weights = weight_map.get(model_name, models.EfficientNet_B3_Weights.IMAGENET1K_V1) | |
| model = getattr(models, model_name)( | |
| weights=weights | |
| ) | |
| if freeze_backbone: | |
| for param in model.parameters(): | |
| param.requires_grad = False | |
| in_features = model.classifier[1].in_features | |
| model.classifier = nn.Sequential( | |
| nn.Dropout(p=dropout), | |
| nn.Linear(in_features, num_classes), | |
| ) | |
| return model | |
| def unfreeze_top_layers(model: nn.Module, num_layers: int = 30) -> None: | |
| """Unfreeze the top N layers of an EfficientNet backbone for fine-tuning. | |
| Args: | |
| model: EfficientNet model whose backbone layers will be unfrozen. | |
| num_layers: Number of top layers to unfreeze. | |
| """ | |
| children = list(model.features.children()) | |
| for layer in children[-num_layers:]: | |
| for param in layer.parameters(): | |
| param.requires_grad = True | |
| def get_segmenter( | |
| encoder: str = "resnet34", | |
| encoder_weights: Optional[str] = "imagenet", | |
| in_channels: int = 3, | |
| classes: int = 1, | |
| activation: str = "sigmoid", | |
| ) -> nn.Module: | |
| """Build a U-Net with a ResNet34 encoder for binary segmentation. | |
| Args: | |
| encoder: Encoder backbone name (e.g., 'resnet34'). | |
| encoder_weights: Pretrained weights to load ('imagenet' or None). | |
| in_channels: Number of input channels (3 for RGB/grayscale-as-RGB). | |
| classes: Number of output classes (1 for binary segmentation). | |
| activation: Output activation ('sigmoid' or 'softmax2d'). | |
| Returns: | |
| PyTorch U-Net model from segmentation_models_pytorch. | |
| """ | |
| model = smp.Unet( | |
| encoder_name=encoder, | |
| encoder_weights=encoder_weights, | |
| in_channels=in_channels, | |
| classes=classes, | |
| activation=activation, | |
| ) | |
| return model | |
| class BCEDiceLoss(nn.Module): | |
| """Combined Binary Cross-Entropy and Dice loss. | |
| Expects model outputs to already be in probability space (sigmoid applied). | |
| """ | |
| def __init__( | |
| self, | |
| bce_weight: float = 0.5, | |
| dice_weight: float = 0.5, | |
| smooth: float = 1e-6, | |
| ): | |
| """Initialize the combined loss. | |
| Args: | |
| bce_weight: Weight for BCE term. | |
| dice_weight: Weight for Dice term. | |
| smooth: Smoothing constant to avoid division by zero. | |
| """ | |
| super().__init__() | |
| self.bce_weight = bce_weight | |
| self.dice_weight = dice_weight | |
| self.smooth = smooth | |
| self.bce = nn.BCELoss() | |
| def forward(self, probs: torch.Tensor, targets: torch.Tensor) -> torch.Tensor: | |
| """Compute the combined BCE + Dice loss. | |
| Args: | |
| probs: Predicted probability tensor from sigmoid output. | |
| targets: Ground truth binary mask tensor. | |
| Returns: | |
| Scalar loss value. | |
| """ | |
| probs = torch.clamp(probs, 1e-7, 1 - 1e-7) | |
| bce_loss = self.bce(probs, targets) | |
| intersection = (probs * targets).sum() | |
| dice = (2.0 * intersection + self.smooth) / ( | |
| probs.sum() + targets.sum() + self.smooth | |
| ) | |
| dice_loss = 1.0 - dice | |
| return self.bce_weight * bce_loss + self.dice_weight * dice_loss | |
| def dice_coefficient( | |
| pred: torch.Tensor, target: torch.Tensor, smooth: float = 1e-6 | |
| ) -> torch.Tensor: | |
| """Compute the Dice coefficient for a single prediction-target pair. | |
| Args: | |
| pred: Predicted tensor (logits or probabilities). | |
| target: Ground truth binary mask tensor. | |
| smooth: Smoothing constant. | |
| Returns: | |
| Dice coefficient scalar. | |
| """ | |
| if pred.min() < 0 or pred.max() > 1: | |
| pred = torch.sigmoid(pred) | |
| pred = pred.view(-1) | |
| target = target.view(-1) | |
| intersection = (pred * target).sum() | |
| return (2.0 * intersection + smooth) / (pred.sum() + target.sum() + smooth) | |
| def iou_score( | |
| pred: torch.Tensor, target: torch.Tensor, smooth: float = 1e-6 | |
| ) -> torch.Tensor: | |
| """Compute the Intersection over Union (Jaccard Index). | |
| Args: | |
| pred: Predicted tensor (logits or probabilities). | |
| target: Ground truth binary mask tensor. | |
| smooth: Smoothing constant. | |
| Returns: | |
| IoU scalar. | |
| """ | |
| if pred.min() < 0 or pred.max() > 1: | |
| pred = torch.sigmoid(pred) | |
| pred = pred.view(-1) | |
| target = target.view(-1) | |
| intersection = (pred * target).sum() | |
| union = pred.sum() + target.sum() - intersection | |
| return (intersection + smooth) / (union + smooth) | |