ThoraxNet / models /classifier.py
Sowaiba01's picture
Upload models/classifier.py with huggingface_hub
0e50348 verified
Raw
History Blame Contribute Delete
3.63 kB
"""
Multi-label classification head with MC Dropout.
Architecture:
BioMedCLIP ViT-B/16 β†’ LayerNorm β†’ Dropout β†’ Linear(512, 256)
β†’ GELU β†’ Dropout β†’ Linear(256, 14)
The double-dropout design enables Monte Carlo uncertainty estimation at
inference time by keeping dropout active (model.train() mode).
"""
from __future__ import annotations
import torch
import torch.nn as nn
from models.backbone import BioMedCLIPVisionBackbone
from data.dataset import NUM_CLASSES
class ChestAIClassifier(nn.Module):
"""
Full model: backbone + classification head.
Args:
backbone_name: HF model ID for BioMedCLIP.
num_classes: Number of output labels (14 for NIH ChestX-ray14).
dropout_rate: Dropout probability β€” used in both training and MC inference.
freeze_backbone: Start with frozen backbone (set False after warm-up).
"""
def __init__(
self,
backbone_name: str = "microsoft/BiomedCLIP-PubMedBERT_256-vit_base_patch16_224",
num_classes: int = NUM_CLASSES,
dropout_rate: float = 0.3,
freeze_backbone: bool = True,
) -> None:
super().__init__()
self.backbone = BioMedCLIPVisionBackbone(
model_name=backbone_name,
pretrained=True,
freeze=freeze_backbone,
)
embed_dim = self.backbone.embed_dim
self.head = nn.Sequential(
nn.LayerNorm(embed_dim),
nn.Dropout(dropout_rate),
nn.Linear(embed_dim, 256),
nn.GELU(),
nn.Dropout(dropout_rate),
nn.Linear(256, num_classes),
)
self._dropout_rate = dropout_rate
self._init_head_weights()
def _init_head_weights(self) -> None:
"""Xavier uniform init for classification head linear layers."""
for m in self.head.modules():
if isinstance(m, nn.Linear):
nn.init.xavier_uniform_(m.weight)
nn.init.zeros_(m.bias)
# ------------------------------------------------------------------
# Forward
# ------------------------------------------------------------------
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""
Standard forward pass (logits, not probabilities).
Args:
x: (B, 3, H, W) image tensor.
Returns:
(B, num_classes) raw logits. Apply sigmoid for probabilities.
"""
features = self.backbone(x) # (B, embed_dim)
return self.head(features) # (B, num_classes)
# ------------------------------------------------------------------
# Backbone freeze / unfreeze API (called by trainer during warm-up)
# ------------------------------------------------------------------
def freeze_backbone(self) -> None:
self.backbone.freeze()
def unfreeze_backbone(self, last_n_blocks: int | None = None) -> None:
self.backbone.unfreeze(last_n_blocks)
# ------------------------------------------------------------------
# Utility
# ------------------------------------------------------------------
@property
def num_parameters(self) -> dict[str, int]:
total = sum(p.numel() for p in self.parameters())
trainable = sum(p.numel() for p in self.parameters() if p.requires_grad)
return {"total": total, "trainable": trainable}
def summary(self) -> str:
p = self.num_parameters
return (
f"ChestAIClassifier | "
f"total={p['total']:,} params | "
f"trainable={p['trainable']:,} params"
)