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,
    )