| """ |
| Model building utilities for crop disease classification. |
| """ |
| from tensorflow import keras |
| from tensorflow.keras import layers, applications |
| from typing import Optional |
|
|
| from ml.config import MODEL_CONFIG, CROPS |
|
|
|
|
| def build_model( |
| num_classes: int, |
| crop: str, |
| *, |
| from_scratch: bool = False, |
| architecture: str = "EfficientNetB0", |
| ) -> keras.Model: |
| """ |
| Build a transfer learning model for crop disease classification. |
| |
| Args: |
| num_classes: Number of disease classes (including healthy) |
| crop: Crop name for logging |
| from_scratch: If True, ImageNet weights are not loaded. |
| architecture: Backbone name β any keras.applications class, e.g. |
| 'EfficientNetB0', 'MobileNetV2', 'ResNet50V2'. |
| |
| Returns: |
| Compiled Keras model |
| """ |
| config = MODEL_CONFIG |
|
|
| weights: Optional[str] = None if from_scratch else config["weights"] |
|
|
| |
| if not hasattr(applications, architecture): |
| raise ValueError(f"Unknown architecture '{architecture}'. " |
| f"Must be a keras.applications class name.") |
| base_model = getattr(applications, architecture)( |
| include_top=config["include_top"], |
| weights=weights, |
| input_shape=config["input_shape"] |
| ) |
|
|
| if from_scratch or weights is None: |
| |
| |
| base_model.trainable = True |
| else: |
| |
| base_model.trainable = False |
| |
| |
| inputs = keras.Input(shape=config["input_shape"]) |
|
|
| |
| |
| |
| |
| |
| if architecture.lower().startswith("efficientnet"): |
| |
| x = layers.Rescaling(scale=255.0, offset=0.0, name="input_rescaling")(inputs) |
| else: |
| |
| x = layers.Rescaling(scale=2.0, offset=-1.0, name="input_rescaling")(inputs) |
| |
| |
| if base_model.trainable: |
| x = base_model(x) |
| else: |
| x = base_model(x, training=False) |
| |
| |
| x = layers.GlobalAveragePooling2D()(x) |
|
|
| l2 = keras.regularizers.l2(0.0001) |
|
|
| |
| |
| |
| x = layers.Dense(256, activation='relu', kernel_regularizer=l2)(x) |
| x = layers.Dropout(0.4)(x) |
|
|
| |
| outputs = layers.Dense(num_classes, activation='softmax')(x) |
| |
| model = keras.Model(inputs, outputs, name=f"{crop}_disease_classifier") |
| |
| |
| model.compile( |
| optimizer=keras.optimizers.Adam(learning_rate=0.0001), |
| loss=keras.losses.CategoricalCrossentropy(label_smoothing=0.1), |
| metrics=['accuracy'] |
| ) |
|
|
| return model |
|
|
|
|
| def unfreeze_model(model: keras.Model, fine_tune_at: int = 50, lr: float = 1e-4): |
| """ |
| Unfreeze top layers of base model for fine-tuning. |
| |
| Args: |
| model: Keras model |
| fine_tune_at: Number of layers from top to unfreeze |
| lr: Learning rate for the fine-tuning optimizer |
| """ |
| base_model = model.layers[2] |
|
|
| |
| base_model.trainable = True |
| for layer in base_model.layers[:-fine_tune_at]: |
| layer.trainable = False |
|
|
| |
| model.compile( |
| optimizer=keras.optimizers.Adam(learning_rate=lr), |
| loss=keras.losses.CategoricalCrossentropy(label_smoothing=0.1), |
| metrics=['accuracy'] |
| ) |
|
|
| return model |
|
|