File size: 5,178 Bytes
565aecf | 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 | """
model.py
--------
EfficientNetB3-based leaf disease classifier.
- Pretrained on ImageNet for strong generalization
- Custom classification head
- Supports feature extraction + fine-tuning phases
"""
import torch
import torch.nn as nn
import timm
class LeafDiseaseModel(nn.Module):
"""
EfficientNetB3 with custom classification head for leaf disease detection.
Architecture:
EfficientNetB3 backbone (ImageNet pretrained)
β Global Average Pooling
β BatchNorm β Dense(512) β GELU β Dropout(0.4)
β BatchNorm β Dense(256) β GELU β Dropout(0.3)
β Dense(num_classes) β Softmax
"""
def __init__(self, num_classes: int, pretrained: bool = True, dropout: float = 0.4):
super().__init__()
self.num_classes = num_classes
# ββ Backbone ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
self.backbone = timm.create_model(
"efficientnet_b3",
pretrained=pretrained,
num_classes=0, # remove default head
global_pool="avg",
)
backbone_out = self.backbone.num_features # 1536 for B3
# ββ Custom Head βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
self.head = nn.Sequential(
nn.BatchNorm1d(backbone_out),
nn.Linear(backbone_out, 512),
nn.GELU(),
nn.Dropout(dropout),
nn.BatchNorm1d(512),
nn.Linear(512, 256),
nn.GELU(),
nn.Dropout(dropout * 0.75),
nn.Linear(256, num_classes),
)
# Weight initialisation for the head
for m in self.head.modules():
if isinstance(m, nn.Linear):
nn.init.xavier_uniform_(m.weight)
if m.bias is not None:
nn.init.zeros_(m.bias)
# ββ Phase control βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def freeze_backbone(self):
"""Freeze backbone β only train the head (Phase 1)."""
for p in self.backbone.parameters():
p.requires_grad = False
print(" Backbone frozen β training head only.")
def unfreeze_backbone(self, unfreeze_layers: int = 30):
"""
Unfreeze the last N backbone layers for fine-tuning (Phase 2).
EfficientNetB3 has ~360 parameters groups; last 30 covers blocks 5-7.
"""
all_params = list(self.backbone.parameters())
# First, freeze everything
for p in all_params:
p.requires_grad = False
# Then unfreeze last N
for p in all_params[-unfreeze_layers:]:
p.requires_grad = True
trainable = sum(p.numel() for p in self.backbone.parameters() if p.requires_grad)
print(f" Unfrozen last {unfreeze_layers} backbone param groups "
f"({trainable:,} params now trainable).")
def unfreeze_all(self):
"""Fully unfreeze everything (Phase 3)."""
for p in self.parameters():
p.requires_grad = True
print(" All layers unfrozen.")
# ββ Forward βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def forward(self, x: torch.Tensor) -> torch.Tensor:
features = self.backbone(x) # (B, 1536)
logits = self.head(features) # (B, num_classes)
return logits
def get_probabilities(self, x: torch.Tensor) -> torch.Tensor:
"""Return softmax probabilities."""
return torch.softmax(self.forward(x), dim=-1)
def predict(self, x: torch.Tensor):
"""Return (class_idx, confidence) tuple."""
probs = self.get_probabilities(x)
conf, idx = torch.max(probs, dim=-1)
return idx, conf
# ββ Utilities βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def count_parameters(self):
total = sum(p.numel() for p in self.parameters())
trainable = sum(p.numel() for p in self.parameters() if p.requires_grad)
print(f" Total parameters: {total:>12,}")
print(f" Trainable parameters: {trainable:>12,}")
return total, trainable
def build_model(num_classes: int, pretrained: bool = True) -> LeafDiseaseModel:
"""Factory function β builds and returns the model."""
model = LeafDiseaseModel(num_classes=num_classes, pretrained=pretrained)
return model
if __name__ == "__main__":
m = build_model(39)
m.count_parameters()
x = torch.randn(4, 3, 300, 300)
out = m(x)
print(f" Output shape: {out.shape}") # (4, 39)
|