Spaces:
Sleeping
Sleeping
File size: 6,479 Bytes
8ccabdf | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | """
models/level2_router.py
Level 2 Disease Router β EfficientNet-B2 pretrained on ImageNet.
Task: Route ABNORMAL scans into 5 disease families
Input: 224Γ224 RGB tensors (same resolution as L1 for pipeline consistency)
Output: 5-class logits
0 β Macular_Degeneration (CNV + DRUSEN + AMD β 47,107 images, 79%)
1 β Diabetic_Complications (DME + DR β 11,602 images)
2 β Vascular_Occlusions (MH + RVO + RAO β 225 images) β aggregated
3 β Fluid_Accumulation (CSR β 102 images)
4 β Structural_Issues (ERM + VID β 231 images)
Design Notes:
- EfficientNet-B2 is chosen over ResNet-50 for L2 because it achieves
higher accuracy with fewer parameters (compound scaling), which matters
when the training signal from minority families is weak.
- Label smoothing (Ξ΅=0.1) is applied to mitigate annotation ambiguity
from merging three heterogeneous source datasets.
- FocalLoss (Ξ³=2) is the primary imbalance mitigation at the loss level.
- Backbone progressive unfreezing follows the same 2-phase protocol as L1.
"""
import logging
from typing import Dict, List
import torch
import torch.nn as nn
from torchvision import models
from torchvision.models import EfficientNet_B2_Weights
logger = logging.getLogger(__name__)
class DiseaseRouterModel(nn.Module):
"""
EfficientNet-B2 multi-class disease family router.
Args:
num_classes: Number of disease families (5).
dropout_rate: Dropout in classifier head (0.4 recommended for B2).
pretrained: Load IMAGENET1K_V1 weights if True.
freeze_backbone: Start with backbone frozen.
"""
def __init__(
self,
num_classes: int = 5,
dropout_rate: float = 0.4,
pretrained: bool = True,
freeze_backbone: bool = True,
) -> None:
super().__init__()
weights = EfficientNet_B2_Weights.IMAGENET1K_V1 if pretrained else None
backbone = models.efficientnet_b2(weights=weights)
# EfficientNet anatomy: features β avgpool β classifier
self.features = backbone.features # MBConv blocks
self.avgpool = backbone.avgpool # AdaptiveAvgPool2d(1, 1)
# EfficientNet-B2 produces 1408 channels after avgpool
in_features = backbone.classifier[-1].in_features # 1408
self.classifier = nn.Sequential(
nn.Dropout(p=dropout_rate),
nn.Linear(in_features, num_classes),
)
if freeze_backbone:
self.freeze_backbone()
logger.info(
"DiseaseRouterModel ready | backbone=EfficientNet-B2 | "
"in_features=%d | num_classes=%d | frozen=%s",
in_features, num_classes, freeze_backbone,
)
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Freeze / Unfreeze API
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def freeze_backbone(self) -> None:
"""Freeze MBConv feature layers β head-only warm-up."""
for param in self.features.parameters():
param.requires_grad = False
logger.info("Router backbone FROZEN.")
def unfreeze_backbone(self) -> None:
"""Unfreeze for full fine-tuning."""
for param in self.features.parameters():
param.requires_grad = True
logger.info("Router backbone UNFROZEN.")
def get_param_groups(
self,
backbone_lr: float = 5e-5,
head_lr: float = 5e-4,
) -> List[Dict]:
"""
Differential LR groups for Phase 2 AdamW.
EfficientNet-B2 uses a lower backbone LR than ResNet-50 because
compound scaling makes its feature extraction more specialised β
larger perturbations risk destroying learned representations.
"""
return [
{"params": self.features.parameters(), "lr": backbone_lr},
{"params": self.classifier.parameters(), "lr": head_lr},
]
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Forward
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""
Args:
x: Float tensor, shape ``(B, 3, 224, 224)``.
Returns:
Logits tensor, shape ``(B, num_classes=5)``.
"""
x = self.features(x) # (B, 1408, H', W')
x = self.avgpool(x) # (B, 1408, 1, 1)
x = torch.flatten(x, 1) # (B, 1408)
x = self.classifier(x) # (B, 5)
return x
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Factory
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def build_router(
num_classes: int = 5,
dropout_rate: float = 0.4,
pretrained: bool = True,
freeze_backbone: bool = True,
) -> DiseaseRouterModel:
"""
Factory function for the Level 2 Disease Router.
Args:
num_classes: 5 disease families.
dropout_rate: Head dropout (0.4 default for EfficientNet-B2).
pretrained: Use ImageNet pretrained weights.
freeze_backbone: Start with frozen backbone.
Returns:
Configured :class:`DiseaseRouterModel`.
"""
return DiseaseRouterModel(
num_classes=num_classes,
dropout_rate=dropout_rate,
pretrained=pretrained,
freeze_backbone=freeze_backbone,
)
|