diff options
| author | Vineet Kumar <git@vineetk.net> | 2026-04-20 10:03:17 -0400 |
|---|---|---|
| committer | Vineet Kumar <git@vineetk.net> | 2026-04-20 10:03:17 -0400 |
| commit | 9dd8b3007e79f0634f868d405f5e7c48917305fd (patch) | |
| tree | b2dd74419a78b28190664f482b30263879742d7a /utils.py | |
| parent | 8f571ee51854540d186aafc01d25908bba5ea09e (diff) | |
improve training for class-imbalanced DR classification
- WeightedRandomSampler for balanced class sampling during training
- FocalLoss (gamma=2) for hard-example mining, no alpha (sampler handles balance)
- Stronger augmentation: RandomResizedCrop, ColorJitter, GaussianBlur, 30deg rotation
- Discriminative LR: backbone at 0.1x, head at 1x
- 3-epoch linear warmup + cosine decay scheduler
- Early stopping on macro F1 instead of weighted F1
- Updated defaults: batch_size=64, epochs=50, patience=10
- Comparison chart now shows both macro and weighted F1
- DataParallel checkpoint handling (strip module. prefix)
- Enable cudnn.benchmark for training speed
Diffstat (limited to 'utils.py')
| -rw-r--r-- | utils.py | 30 |
1 files changed, 28 insertions, 2 deletions
| @@ -1,6 +1,8 @@ | |||
| 1 | import random | 1 | import random |
| 2 | import numpy as np | 2 | import numpy as np |
| 3 | import torch | 3 | import torch |
| 4 | import torch.nn as nn | ||
| 5 | import torch.nn.functional as F | ||
| 4 | 6 | ||
| 5 | CLASS_NAMES = [ | 7 | CLASS_NAMES = [ |
| 6 | 'Healthy (0)', | 8 | 'Healthy (0)', |
| @@ -18,8 +20,7 @@ def set_seed(seed: int = 42) -> None: | |||
| 18 | np.random.seed(seed) | 20 | np.random.seed(seed) |
| 19 | torch.manual_seed(seed) | 21 | torch.manual_seed(seed) |
| 20 | torch.cuda.manual_seed_all(seed) | 22 | torch.cuda.manual_seed_all(seed) |
| 21 | torch.backends.cudnn.deterministic = True | 23 | torch.backends.cudnn.benchmark = True |
| 22 | torch.backends.cudnn.benchmark = False | ||
| 23 | 24 | ||
| 24 | 25 | ||
| 25 | def compute_class_weights(labels) -> torch.FloatTensor: | 26 | def compute_class_weights(labels) -> torch.FloatTensor: |
| @@ -35,3 +36,28 @@ def get_device() -> torch.device: | |||
| 35 | if torch.cuda.is_available(): | 36 | if torch.cuda.is_available(): |
| 36 | return torch.device('cuda') | 37 | return torch.device('cuda') |
| 37 | return torch.device('cpu') | 38 | return torch.device('cpu') |
| 39 | |||
| 40 | |||
| 41 | class FocalLoss(nn.Module): | ||
| 42 | """Focal loss with optional per-class alpha weighting. | ||
| 43 | |||
| 44 | Focuses learning on hard, misclassified examples by down-weighting | ||
| 45 | easy examples (high pt). Good for class imbalance in medical imaging. | ||
| 46 | |||
| 47 | Args: | ||
| 48 | alpha: Per-class weight tensor (same shape as class weights for CE). | ||
| 49 | If None, no per-class weighting is applied. | ||
| 50 | gamma: Focusing parameter. gamma=0 reduces to standard CE. | ||
| 51 | gamma=2 is the standard value from the RetinaNet paper. | ||
| 52 | """ | ||
| 53 | |||
| 54 | def __init__(self, alpha=None, gamma: float = 2.0): | ||
| 55 | super().__init__() | ||
| 56 | self.alpha = alpha | ||
| 57 | self.gamma = gamma | ||
| 58 | |||
| 59 | def forward(self, inputs: torch.Tensor, targets: torch.Tensor) -> torch.Tensor: | ||
| 60 | ce_loss = F.cross_entropy(inputs, targets, weight=self.alpha, reduction='none') | ||
| 61 | pt = torch.exp(-ce_loss) | ||
| 62 | focal_loss = ((1.0 - pt) ** self.gamma) * ce_loss | ||
| 63 | return focal_loss.mean() | ||
