| """ |
| model/model.py |
| ββββββββββββββ |
| DenseNet-121 backbone fine-tuned for binary classification of |
| breast cancer histopathology images (benign vs. malignant). |
| |
| Architecture |
| ββββββββββββ |
| DenseNet-121 (pretrained on ImageNet) |
| ββ Adaptive Average Pool β flatten |
| ββ Classifier head |
| ββ BatchNorm1d(1024) |
| ββ Dropout(p=0.4) |
| ββ Linear(1024 β 256) + ReLU |
| ββ BatchNorm1d(256) |
| ββ Dropout(p=0.3) |
| ββ Linear(256 β 2) β raw logits [benign, malignant] |
| |
| Outputs |
| ββββββββ |
| logits : Tensor[1, 2] β raw scores (pre-softmax) |
| probs : Tensor[1, 2] β calibrated probabilities via softmax |
| """ |
|
|
| import torch |
| import torch.nn as nn |
| from torchvision import models |
|
|
|
|
| class BreastCancerClassifier(nn.Module): |
| """ |
| DenseNet-121 backbone with a custom two-class head. |
| |
| Parameters |
| ---------- |
| pretrained : bool |
| Load ImageNet weights into the DenseNet-121 backbone (default True). |
| freeze_backbone : bool |
| Freeze all DenseNet layers except the classifier head (default False). |
| Set True for pure feature-extraction / fast fine-tuning scenarios. |
| dropout_rate : float |
| Dropout probability applied in the classifier head (default 0.4). |
| """ |
|
|
| def __init__( |
| self, |
| pretrained: bool = True, |
| freeze_backbone: bool = False, |
| dropout_rate: float = 0.4, |
| ) -> None: |
| super().__init__() |
|
|
| |
| weights = models.DenseNet121_Weights.IMAGENET1K_V1 if pretrained else None |
| densenet = models.densenet121(weights=weights) |
|
|
| |
| self.features = densenet.features |
| self.pool = nn.AdaptiveAvgPool2d((1, 1)) |
|
|
| in_features = densenet.classifier.in_features |
|
|
| |
| self.classifier = nn.Sequential( |
| nn.BatchNorm1d(in_features), |
| nn.Dropout(p=dropout_rate), |
| nn.Linear(in_features, 256), |
| nn.ReLU(inplace=True), |
| nn.BatchNorm1d(256), |
| nn.Dropout(p=dropout_rate * 0.75), |
| nn.Linear(256, 2), |
| ) |
|
|
| |
| if freeze_backbone: |
| for param in self.features.parameters(): |
| param.requires_grad = False |
|
|
| self._init_classifier_weights() |
|
|
| |
| def _init_classifier_weights(self) -> None: |
| """Kaiming / Xavier initialisation for the custom head.""" |
| for module in self.classifier.modules(): |
| if isinstance(module, nn.Linear): |
| nn.init.kaiming_normal_(module.weight, nonlinearity="relu") |
| if module.bias is not None: |
| nn.init.zeros_(module.bias) |
| elif isinstance(module, nn.BatchNorm1d): |
| nn.init.ones_(module.weight) |
| nn.init.zeros_(module.bias) |
|
|
| |
| def forward(self, x: torch.Tensor) -> dict: |
| """ |
| Forward pass. |
| |
| Parameters |
| ---------- |
| x : torch.Tensor |
| Normalised image tensor of shape (B, 3, 224, 224). |
| |
| Returns |
| ------- |
| dict with keys |
| "logits" : Tensor[B, 2] β raw model outputs |
| "probs" : Tensor[B, 2] β softmax probabilities |
| """ |
| features = self.features(x) |
| pooled = self.pool(features) |
| flat = torch.flatten(pooled, 1) |
| logits = self.classifier(flat) |
| probs = torch.softmax(logits, dim=1) |
|
|
| return {"logits": logits, "probs": probs} |
|
|
| |
| def get_feature_maps(self, x: torch.Tensor) -> torch.Tensor: |
| """ |
| Return the final DenseNet feature maps before pooling. |
| Used by Grad-CAM and other spatial explainability modules. |
| |
| Returns |
| ------- |
| Tensor[B, 1024, 7, 7] |
| """ |
| return self.features(x) |
|
|