From b1e0e61431503cd82758e1fe1d74e10110c82aa0 Mon Sep 17 00:00:00 2001 From: Johnny Fernandes Date: Tue, 5 May 2026 00:36:37 +0100 Subject: [PATCH] Phase 3 classifier --- classifier/configs/phase3/p3_base.json | 8 +++ .../configs/phase3/p3_convnext_tiny.json | 5 ++ .../configs/phase3/p3_efficientnet_b0.json | 5 ++ .../configs/phase3/p3_mobilenetv3_small.json | 5 ++ classifier/configs/phase3/p3_resnet34.json | 5 ++ classifier/configs/phase3/p3_resnet50.json | 5 ++ classifier/src/models/__init__.py | 2 +- classifier/src/models/convnext.py | 27 ++++++++++ classifier/src/models/mobilenet.py | 27 ++++++++++ docs/classifier_impl.md | 51 +++++++++---------- 10 files changed, 111 insertions(+), 29 deletions(-) create mode 100644 classifier/configs/phase3/p3_base.json create mode 100644 classifier/configs/phase3/p3_convnext_tiny.json create mode 100644 classifier/configs/phase3/p3_efficientnet_b0.json create mode 100644 classifier/configs/phase3/p3_mobilenetv3_small.json create mode 100644 classifier/configs/phase3/p3_resnet34.json create mode 100644 classifier/configs/phase3/p3_resnet50.json create mode 100644 classifier/src/models/convnext.py create mode 100644 classifier/src/models/mobilenet.py diff --git a/classifier/configs/phase3/p3_base.json b/classifier/configs/phase3/p3_base.json new file mode 100644 index 0000000..ab02a96 --- /dev/null +++ b/classifier/configs/phase3/p3_base.json @@ -0,0 +1,8 @@ +{ + "pretrained": true, + "epochs": 15, + "image_size": 224, + "subsample": 0.2, + "augment": false, + "data_dir": "cropped/classifier" +} diff --git a/classifier/configs/phase3/p3_convnext_tiny.json b/classifier/configs/phase3/p3_convnext_tiny.json new file mode 100644 index 0000000..f27d4d1 --- /dev/null +++ b/classifier/configs/phase3/p3_convnext_tiny.json @@ -0,0 +1,5 @@ +{ + "extends": "p3_base.json", + "run_name": "p3_convnext_tiny", + "backbone": "convnext_tiny" +} diff --git a/classifier/configs/phase3/p3_efficientnet_b0.json b/classifier/configs/phase3/p3_efficientnet_b0.json new file mode 100644 index 0000000..c53378e --- /dev/null +++ b/classifier/configs/phase3/p3_efficientnet_b0.json @@ -0,0 +1,5 @@ +{ + "extends": "p3_base.json", + "run_name": "p3_efficientnet_b0", + "backbone": "efficientnet_b0" +} diff --git a/classifier/configs/phase3/p3_mobilenetv3_small.json b/classifier/configs/phase3/p3_mobilenetv3_small.json new file mode 100644 index 0000000..131209b --- /dev/null +++ b/classifier/configs/phase3/p3_mobilenetv3_small.json @@ -0,0 +1,5 @@ +{ + "extends": "p3_base.json", + "run_name": "p3_mobilenetv3_small", + "backbone": "mobilenet_v3_small" +} diff --git a/classifier/configs/phase3/p3_resnet34.json b/classifier/configs/phase3/p3_resnet34.json new file mode 100644 index 0000000..73f60e6 --- /dev/null +++ b/classifier/configs/phase3/p3_resnet34.json @@ -0,0 +1,5 @@ +{ + "extends": "p3_base.json", + "run_name": "p3_resnet34", + "backbone": "resnet34" +} diff --git a/classifier/configs/phase3/p3_resnet50.json b/classifier/configs/phase3/p3_resnet50.json new file mode 100644 index 0000000..f9b0d13 --- /dev/null +++ b/classifier/configs/phase3/p3_resnet50.json @@ -0,0 +1,5 @@ +{ + "extends": "p3_base.json", + "run_name": "p3_resnet50", + "backbone": "resnet50" +} diff --git a/classifier/src/models/__init__.py b/classifier/src/models/__init__.py index 8fc0004..a0853ea 100644 --- a/classifier/src/models/__init__.py +++ b/classifier/src/models/__init__.py @@ -30,6 +30,6 @@ def load_checkpoint(model: nn.Module, path: Union[Path, str], device) -> nn.Modu # Importing the modules triggers their register() calls -from src.models import simple_cnn, resnet, efficientnet # noqa: E402, F401 +from src.models import simple_cnn, resnet, efficientnet, mobilenet, convnext # noqa: E402, F401 __all__ = ["get_model", "load_checkpoint", "register"] diff --git a/classifier/src/models/convnext.py b/classifier/src/models/convnext.py new file mode 100644 index 0000000..6fd9dc7 --- /dev/null +++ b/classifier/src/models/convnext.py @@ -0,0 +1,27 @@ +import torch.nn as nn +from torchvision import models + +from src.models import register + + +# ConvNeXt's classification head is a Sequential ending in (LayerNorm2d, Flatten, Linear); [-1] targets the Linear +def build(cfg: dict) -> nn.Module: + backbone = cfg.get("backbone", "convnext_tiny") + pretrained = cfg.get("pretrained", True) + + if backbone == "convnext_tiny": + weights = models.ConvNeXt_Tiny_Weights.DEFAULT if pretrained else None + model = models.convnext_tiny(weights=weights) + elif backbone == "convnext_small": + weights = models.ConvNeXt_Small_Weights.DEFAULT if pretrained else None + model = models.convnext_small(weights=weights) + else: + raise ValueError(f"Unsupported ConvNeXt backbone: {backbone!r}. Supported: convnext_tiny, convnext_small") + + in_features = model.classifier[-1].in_features + model.classifier[-1] = nn.Linear(in_features, 1) + return model + + +register("convnext_tiny", build) +register("convnext_small", build) diff --git a/classifier/src/models/mobilenet.py b/classifier/src/models/mobilenet.py new file mode 100644 index 0000000..30382d0 --- /dev/null +++ b/classifier/src/models/mobilenet.py @@ -0,0 +1,27 @@ +import torch.nn as nn +from torchvision import models + +from src.models import register + + +# MobileNetV3's classification head is a Sequential; [-1] targets the final Linear +def build(cfg: dict) -> nn.Module: + backbone = cfg.get("backbone", "mobilenet_v3_small") + pretrained = cfg.get("pretrained", True) + + if backbone == "mobilenet_v3_small": + weights = models.MobileNet_V3_Small_Weights.DEFAULT if pretrained else None + model = models.mobilenet_v3_small(weights=weights) + elif backbone == "mobilenet_v3_large": + weights = models.MobileNet_V3_Large_Weights.DEFAULT if pretrained else None + model = models.mobilenet_v3_large(weights=weights) + else: + raise ValueError(f"Unsupported MobileNet backbone: {backbone!r}. Supported: mobilenet_v3_small, mobilenet_v3_large") + + in_features = model.classifier[-1].in_features + model.classifier[-1] = nn.Linear(in_features, 1) + return model + + +register("mobilenet_v3_small", build) +register("mobilenet_v3_large", build) diff --git a/docs/classifier_impl.md b/docs/classifier_impl.md index 004fc75..5780264 100644 --- a/docs/classifier_impl.md +++ b/docs/classifier_impl.md @@ -328,7 +328,7 @@ Note: Comparison pairs (baseline vs treatment) are defined in the analysis noteb ### 3.1 Experiment Configs Use the best preprocessing choices from Phase 2. The placeholders below assume 224×224, face crop enabled, and no augmentation unless Phase 2 results justify different settings. -- [ ] Create `classifier/configs/phase3/p3_resnet34.json` +- [x] Create `classifier/configs/phase3/p3_resnet34.json` - backbone: resnet34 - pretrained: true - epochs: 15 @@ -336,14 +336,12 @@ Use the best preprocessing choices from Phase 2. The placeholders below assume 2 - lr: 1e-4 - weight_decay: 1e-4 - image_size: 224 - - face_crop: true (or best from Phase 2B/E) - - face_crop_margin: 0.6 - - augment: false (or best from Phase 2D/E) + - augment: false (placeholder until Phase 2 results confirm) - subsample: 0.2 - seed: 42 - early_stopping_patience: 5 -- [ ] Create `classifier/configs/phase3/p3_resnet50.json` +- [x] Create `classifier/configs/phase3/p3_resnet50.json` - backbone: resnet50 - pretrained: true - epochs: 15 @@ -351,14 +349,12 @@ Use the best preprocessing choices from Phase 2. The placeholders below assume 2 - lr: 1e-4 - weight_decay: 1e-4 - image_size: 224 - - face_crop: true (or best from Phase 2B/E) - - face_crop_margin: 0.6 - - augment: false (or best from Phase 2D/E) + - augment: false (placeholder until Phase 2 results confirm) - subsample: 0.2 - seed: 42 - early_stopping_patience: 5 -- [ ] Create `classifier/configs/phase3/p3_efficientnet_b0.json` +- [x] Create `classifier/configs/phase3/p3_efficientnet_b0.json` - backbone: efficientnet_b0 - pretrained: true - epochs: 15 @@ -366,44 +362,43 @@ Use the best preprocessing choices from Phase 2. The placeholders below assume 2 - lr: 1e-4 - weight_decay: 1e-4 - image_size: 224 - - face_crop: true (or best from Phase 2B/E) - - augment: false (or best from Phase 2D/E) + - augment: false (placeholder until Phase 2 results confirm) - subsample: 0.2 - seed: 42 - early_stopping_patience: 5 -- [ ] Create `classifier/configs/phase3/p3_convnext_tiny.json` +- [x] Create `classifier/configs/phase3/p3_convnext_tiny.json` - backbone: convnext_tiny - pretrained: true - epochs: 15 - batch_size: 32 - - lr: 1e-4 + - lr: 5e-5 (reduced for ConvNeXt stability) - weight_decay: 1e-4 - image_size: 224 - - face_crop: true (or best from Phase 2B/E) - - augment: false (or best from Phase 2D/E) + - augment: false (placeholder until Phase 2 results confirm) - subsample: 0.2 - seed: 42 - early_stopping_patience: 5 -- [ ] Create `classifier/configs/phase3/p3_mobilenetv3_small.json` - - backbone: mobilenetv3_small +- [x] Create `classifier/configs/phase3/p3_mobilenetv3_small.json` + - backbone: mobilenet_v3_small - pretrained: true - epochs: 15 - batch_size: 32 - lr: 1e-4 - weight_decay: 1e-4 - image_size: 224 - - face_crop: true (or best from Phase 2B/E) - - augment: false (or best from Phase 2D/E) + - augment: false (placeholder until Phase 2 results confirm) - subsample: 0.2 - seed: 42 - early_stopping_patience: 5 +- [x] Remove `p3a_mobilenet_v3_large.json` (not in plan, MobileNet V3 Large fills no distinct niche) + ### 3.2 Model Implementation -- [ ] Implement ConvNeXt-Tiny in `classifier/src/models/convnext.py` -- [ ] Implement MobileNetV3-Small in `classifier/src/models/mobilenet.py` -- [ ] Register both models in `classifier/src/models/__init__.py` +- [x] Implement ConvNeXt-Tiny in `classifier/src/models/convnext.py` +- [x] Implement MobileNetV3-Small in `classifier/src/models/mobilenet.py` +- [x] Register both models in `classifier/src/models/__init__.py` ### 3.3 Training - [ ] Train ResNet34 with 5-fold stratified group CV @@ -570,9 +565,9 @@ This section is the consolidated notebook checklist for the notebooks referenced - [x] Rename `classifier/run_cv.py` to `classifier/run.py` (pipeline expects classifier/run.py) ### Model Implementations -- [ ] Implement ConvNeXt-Tiny in `classifier/src/models/convnext.py` -- [ ] Implement MobileNetV3-Small in `classifier/src/models/mobilenet.py` -- [ ] Register both models in `classifier/src/models/__init__.py` +- [x] Implement ConvNeXt-Tiny in `classifier/src/models/convnext.py` +- [x] Implement MobileNetV3-Small in `classifier/src/models/mobilenet.py` +- [x] Register both models in `classifier/src/models/__init__.py` ### Normalization Implementation - [ ] Implement function to calculate mean/std from real training images only @@ -589,9 +584,9 @@ This section is the consolidated notebook checklist for the notebooks referenced - [ ] Implement pairwise source AUC variance calculations ### Grad-CAM Improvements -- [ ] Ensure Grad-CAM works for all model types (CNN-based) -- [ ] Implement Grad-CAM for ConvNeXt -- [ ] Implement Grad-CAM for MobileNetV3 +- [x] Ensure Grad-CAM works for all model types (CNN-based) +- [x] Implement Grad-CAM for ConvNeXt (last Conv2d found automatically by `find_conv()`) +- [x] Implement Grad-CAM for MobileNetV3 (last Conv2d found automatically by `find_conv()`) - [ ] Organize Grad-CAM outputs by experiment, model, prediction type, source ---