Files
zeavis-edu/Machine_Learning/notebook.ipynb
T
2026-06-12 15:24:49 +00:00

1.9 MiB
Raw Blame History

ZeaVis Edu — Corn Leaf Disease Classifier v3.0

Mengklasifikasikan penyakit daun jagung (Bercak Daun, Hawar Daun, Karat Daun, Daun Sehat) menggunakan EfficientNetV2B0 dengan CBAM spatial attention, RandAugment + weather simulation, dan temperature-scaled confidence calibration untuk deployment real-world.

Fokus v3.0: robustness dunia nyata — berbagai pencahayaan, resolusi, angle, dan background.

1. Persiapan Lingkungan

Mengimpor pustaka, mengatur seed, dan mengoptimalkan konfigurasi. Presisi float32, resolusi target 224×224 (EfficientNetV2B0). Augmentasi real-world via RandAugment pool 15 transformasi. Confidence calibration via temperature scaling.

In [1]:
!pip install -r requirements.txt
Requirement already satisfied: tensorflow==2.19.0 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from -r requirements.txt (line 1)) (2.19.0)
Requirement already satisfied: tensorflowjs==4.22.0 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from -r requirements.txt (line 2)) (4.22.0)
Requirement already satisfied: gdown in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from -r requirements.txt (line 3)) (6.1.0)
Requirement already satisfied: kagglehub in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from -r requirements.txt (line 4)) (1.0.2)
Requirement already satisfied: numpy in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from -r requirements.txt (line 5)) (2.1.3)
Requirement already satisfied: matplotlib in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from -r requirements.txt (line 6)) (3.10.9)
Requirement already satisfied: seaborn in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from -r requirements.txt (line 7)) (0.13.2)
Requirement already satisfied: pillow in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from -r requirements.txt (line 8)) (12.2.0)
Requirement already satisfied: split-folders in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from -r requirements.txt (line 9)) (0.6.1)
Requirement already satisfied: scikit-learn in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from -r requirements.txt (line 10)) (1.9.0)
Requirement already satisfied: tf2onnx in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from -r requirements.txt (line 11)) (1.17.0)
Requirement already satisfied: onnxruntime in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from -r requirements.txt (line 12)) (1.26.0)
Requirement already satisfied: huggingface_hub in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from -r requirements.txt (line 13)) (1.19.0)
Requirement already satisfied: absl-py>=1.0.0 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from tensorflow==2.19.0->-r requirements.txt (line 1)) (2.4.0)
Requirement already satisfied: astunparse>=1.6.0 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from tensorflow==2.19.0->-r requirements.txt (line 1)) (1.6.3)
Requirement already satisfied: flatbuffers>=24.3.25 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from tensorflow==2.19.0->-r requirements.txt (line 1)) (25.12.19)
Requirement already satisfied: gast!=0.5.0,!=0.5.1,!=0.5.2,>=0.2.1 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from tensorflow==2.19.0->-r requirements.txt (line 1)) (0.7.0)
Requirement already satisfied: google-pasta>=0.1.1 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from tensorflow==2.19.0->-r requirements.txt (line 1)) (0.2.0)
Requirement already satisfied: libclang>=13.0.0 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from tensorflow==2.19.0->-r requirements.txt (line 1)) (18.1.1)
Requirement already satisfied: opt-einsum>=2.3.2 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from tensorflow==2.19.0->-r requirements.txt (line 1)) (3.4.0)
Requirement already satisfied: packaging in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from tensorflow==2.19.0->-r requirements.txt (line 1)) (23.2)
Requirement already satisfied: protobuf!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<6.0.0dev,>=3.20.3 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from tensorflow==2.19.0->-r requirements.txt (line 1)) (5.29.6)
Requirement already satisfied: requests<3,>=2.21.0 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from tensorflow==2.19.0->-r requirements.txt (line 1)) (2.34.2)
Requirement already satisfied: setuptools in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from tensorflow==2.19.0->-r requirements.txt (line 1)) (82.0.1)
Requirement already satisfied: six>=1.12.0 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from tensorflow==2.19.0->-r requirements.txt (line 1)) (1.17.0)
Requirement already satisfied: termcolor>=1.1.0 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from tensorflow==2.19.0->-r requirements.txt (line 1)) (3.3.0)
Requirement already satisfied: typing-extensions>=3.6.6 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from tensorflow==2.19.0->-r requirements.txt (line 1)) (4.15.0)
Requirement already satisfied: wrapt>=1.11.0 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from tensorflow==2.19.0->-r requirements.txt (line 1)) (2.2.1)
Requirement already satisfied: grpcio<2.0,>=1.24.3 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from tensorflow==2.19.0->-r requirements.txt (line 1)) (1.81.1)
Requirement already satisfied: tensorboard~=2.19.0 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from tensorflow==2.19.0->-r requirements.txt (line 1)) (2.19.0)
Requirement already satisfied: keras>=3.5.0 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from tensorflow==2.19.0->-r requirements.txt (line 1)) (3.14.1)
Requirement already satisfied: h5py>=3.11.0 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from tensorflow==2.19.0->-r requirements.txt (line 1)) (3.14.0)
Requirement already satisfied: ml-dtypes<1.0.0,>=0.5.1 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from tensorflow==2.19.0->-r requirements.txt (line 1)) (0.5.4)
Requirement already satisfied: flax>=0.7.2 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from tensorflowjs==4.22.0->-r requirements.txt (line 2)) (0.12.7)
Requirement already satisfied: importlib_resources>=5.9.0 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from tensorflowjs==4.22.0->-r requirements.txt (line 2)) (7.1.0)
Requirement already satisfied: jax>=0.4.13 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from tensorflowjs==4.22.0->-r requirements.txt (line 2)) (0.10.1)
Requirement already satisfied: jaxlib>=0.4.13 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from tensorflowjs==4.22.0->-r requirements.txt (line 2)) (0.10.1)
Requirement already satisfied: tf-keras>=2.13.0 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from tensorflowjs==4.22.0->-r requirements.txt (line 2)) (2.19.0)
Requirement already satisfied: tensorflow-decision-forests>=1.5.0 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from tensorflowjs==4.22.0->-r requirements.txt (line 2)) (1.12.0)
Requirement already satisfied: tensorflow-hub>=0.16.1 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from tensorflowjs==4.22.0->-r requirements.txt (line 2)) (0.16.1)
Requirement already satisfied: charset_normalizer<4,>=2 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from requests<3,>=2.21.0->tensorflow==2.19.0->-r requirements.txt (line 1)) (3.4.7)
Requirement already satisfied: idna<4,>=2.5 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from requests<3,>=2.21.0->tensorflow==2.19.0->-r requirements.txt (line 1)) (3.18)
Requirement already satisfied: urllib3<3,>=1.26 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from requests<3,>=2.21.0->tensorflow==2.19.0->-r requirements.txt (line 1)) (2.7.0)
Requirement already satisfied: certifi>=2023.5.7 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from requests<3,>=2.21.0->tensorflow==2.19.0->-r requirements.txt (line 1)) (2026.5.20)
Requirement already satisfied: markdown>=2.6.8 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from tensorboard~=2.19.0->tensorflow==2.19.0->-r requirements.txt (line 1)) (3.10.2)
Requirement already satisfied: tensorboard-data-server<0.8.0,>=0.7.0 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from tensorboard~=2.19.0->tensorflow==2.19.0->-r requirements.txt (line 1)) (0.7.2)
Requirement already satisfied: werkzeug>=1.0.1 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from tensorboard~=2.19.0->tensorflow==2.19.0->-r requirements.txt (line 1)) (3.1.8)
Requirement already satisfied: beautifulsoup4 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from gdown->-r requirements.txt (line 3)) (4.15.0)
Requirement already satisfied: filelock in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from gdown->-r requirements.txt (line 3)) (3.29.3)
Requirement already satisfied: tqdm in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from gdown->-r requirements.txt (line 3)) (4.68.2)
Requirement already satisfied: kagglesdk<1.0,>=0.1.22 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from kagglehub->-r requirements.txt (line 4)) (0.1.29)
Requirement already satisfied: pyyaml in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from kagglehub->-r requirements.txt (line 4)) (6.0.3)
Requirement already satisfied: contourpy>=1.0.1 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from matplotlib->-r requirements.txt (line 6)) (1.3.3)
Requirement already satisfied: cycler>=0.10 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from matplotlib->-r requirements.txt (line 6)) (0.12.1)
Requirement already satisfied: fonttools>=4.22.0 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from matplotlib->-r requirements.txt (line 6)) (4.63.0)
Requirement already satisfied: kiwisolver>=1.3.1 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from matplotlib->-r requirements.txt (line 6)) (1.5.0)
Requirement already satisfied: pyparsing>=3 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from matplotlib->-r requirements.txt (line 6)) (3.3.2)
Requirement already satisfied: python-dateutil>=2.7 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from matplotlib->-r requirements.txt (line 6)) (2.9.0.post0)
Requirement already satisfied: pandas>=1.2 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from seaborn->-r requirements.txt (line 7)) (3.0.3)
Requirement already satisfied: scipy>=1.10.0 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from scikit-learn->-r requirements.txt (line 10)) (1.17.1)
Requirement already satisfied: joblib>=1.4.0 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from scikit-learn->-r requirements.txt (line 10)) (1.5.3)
Requirement already satisfied: narwhals>=2.0.1 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from scikit-learn->-r requirements.txt (line 10)) (2.22.1)
Requirement already satisfied: threadpoolctl>=3.5.0 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from scikit-learn->-r requirements.txt (line 10)) (3.6.0)
Requirement already satisfied: onnx>=1.14.0 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from tf2onnx->-r requirements.txt (line 11)) (1.21.0)
Requirement already satisfied: click>=8.4.0 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from huggingface_hub->-r requirements.txt (line 13)) (8.4.1)
Requirement already satisfied: fsspec>=2023.5.0 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from huggingface_hub->-r requirements.txt (line 13)) (2026.4.0)
Requirement already satisfied: hf-xet<2.0.0,>=1.5.1 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from huggingface_hub->-r requirements.txt (line 13)) (1.5.1)
Requirement already satisfied: httpx<1,>=0.23.0 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from huggingface_hub->-r requirements.txt (line 13)) (0.28.1)
Requirement already satisfied: typer<0.26.0,>=0.20.0 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from huggingface_hub->-r requirements.txt (line 13)) (0.25.1)
Requirement already satisfied: anyio in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from httpx<1,>=0.23.0->huggingface_hub->-r requirements.txt (line 13)) (4.13.0)
Requirement already satisfied: httpcore==1.* in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from httpx<1,>=0.23.0->huggingface_hub->-r requirements.txt (line 13)) (1.0.9)
Requirement already satisfied: h11>=0.16 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from httpcore==1.*->httpx<1,>=0.23.0->huggingface_hub->-r requirements.txt (line 13)) (0.16.0)
Requirement already satisfied: shellingham>=1.3.0 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from typer<0.26.0,>=0.20.0->huggingface_hub->-r requirements.txt (line 13)) (1.5.4)
Requirement already satisfied: rich>=13.8.0 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from typer<0.26.0,>=0.20.0->huggingface_hub->-r requirements.txt (line 13)) (15.0.0)
Requirement already satisfied: annotated-doc>=0.0.2 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from typer<0.26.0,>=0.20.0->huggingface_hub->-r requirements.txt (line 13)) (0.0.4)
Requirement already satisfied: wheel<1.0,>=0.23.0 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from astunparse>=1.6.0->tensorflow==2.19.0->-r requirements.txt (line 1)) (0.45.1)
Requirement already satisfied: msgpack in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from flax>=0.7.2->tensorflowjs==4.22.0->-r requirements.txt (line 2)) (1.2.0)
Requirement already satisfied: optax in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from flax>=0.7.2->tensorflowjs==4.22.0->-r requirements.txt (line 2)) (0.2.8)
Requirement already satisfied: orbax-checkpoint in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from flax>=0.7.2->tensorflowjs==4.22.0->-r requirements.txt (line 2)) (0.12.0)
Requirement already satisfied: tensorstore in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from flax>=0.7.2->tensorflowjs==4.22.0->-r requirements.txt (line 2)) (0.1.84)
Requirement already satisfied: treescope>=0.1.7 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from flax>=0.7.2->tensorflowjs==4.22.0->-r requirements.txt (line 2)) (0.1.10)
Requirement already satisfied: namex in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from keras>=3.5.0->tensorflow==2.19.0->-r requirements.txt (line 1)) (0.1.0)
Requirement already satisfied: optree in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from keras>=3.5.0->tensorflow==2.19.0->-r requirements.txt (line 1)) (0.19.1)
Requirement already satisfied: markdown-it-py>=2.2.0 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from rich>=13.8.0->typer<0.26.0,>=0.20.0->huggingface_hub->-r requirements.txt (line 13)) (4.2.0)
Requirement already satisfied: pygments<3.0.0,>=2.13.0 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from rich>=13.8.0->typer<0.26.0,>=0.20.0->huggingface_hub->-r requirements.txt (line 13)) (2.20.0)
Requirement already satisfied: mdurl~=0.1 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from markdown-it-py>=2.2.0->rich>=13.8.0->typer<0.26.0,>=0.20.0->huggingface_hub->-r requirements.txt (line 13)) (0.1.2)
Requirement already satisfied: wurlitzer in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from tensorflow-decision-forests>=1.5.0->tensorflowjs==4.22.0->-r requirements.txt (line 2)) (3.1.1)
Requirement already satisfied: ydf>=0.11.0 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from tensorflow-decision-forests>=1.5.0->tensorflowjs==4.22.0->-r requirements.txt (line 2)) (0.16.0)
Requirement already satisfied: markupsafe>=2.1.1 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from werkzeug>=1.0.1->tensorboard~=2.19.0->tensorflow==2.19.0->-r requirements.txt (line 1)) (3.0.3)
Requirement already satisfied: soupsieve>=1.6.1 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from beautifulsoup4->gdown->-r requirements.txt (line 3)) (2.8.4)
Requirement already satisfied: etils[epath,epy] in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from orbax-checkpoint->flax>=0.7.2->tensorflowjs==4.22.0->-r requirements.txt (line 2)) (1.14.0)
Requirement already satisfied: prometheus-client>=0.20.0 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from orbax-checkpoint->flax>=0.7.2->tensorflowjs==4.22.0->-r requirements.txt (line 2)) (0.25.0)
Requirement already satisfied: aiofiles in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from orbax-checkpoint->flax>=0.7.2->tensorflowjs==4.22.0->-r requirements.txt (line 2)) (25.1.0)
Requirement already satisfied: humanize in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from orbax-checkpoint->flax>=0.7.2->tensorflowjs==4.22.0->-r requirements.txt (line 2)) (4.15.0)
Requirement already satisfied: simplejson>=3.16.0 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from orbax-checkpoint->flax>=0.7.2->tensorflowjs==4.22.0->-r requirements.txt (line 2)) (4.1.1)
Requirement already satisfied: psutil in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from orbax-checkpoint->flax>=0.7.2->tensorflowjs==4.22.0->-r requirements.txt (line 2)) (7.2.2)
Requirement already satisfied: uvloop in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from orbax-checkpoint->flax>=0.7.2->tensorflowjs==4.22.0->-r requirements.txt (line 2)) (0.22.1)
Requirement already satisfied: zipp in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from etils[epath,epy]->orbax-checkpoint->flax>=0.7.2->tensorflowjs==4.22.0->-r requirements.txt (line 2)) (4.1.0)
Requirement already satisfied: PySocks!=1.5.7,>=1.5.6 in /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages (from requests[socks]->gdown->-r requirements.txt (line 3)) (1.7.1)
In [2]:
import os, shutil, zipfile, random, time, json
from collections import Counter
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from PIL import Image

import tensorflow as tf
from tensorflow.keras import layers, models, callbacks
from tensorflow.keras.applications import EfficientNetV2B0
from tensorflow.keras.applications.efficientnet_v2 import preprocess_input
from tensorflow.keras.optimizers import AdamW
from tensorflow.keras.optimizers.schedules import CosineDecay
from sklearn.metrics import classification_report, confusion_matrix
from sklearn.utils.class_weight import compute_class_weight
from sklearn.model_selection import train_test_split

# Optional: perceptual hashing for dedup (pip install imagehash)
try:
    import imagehash
    HAS_IMAGEHASH = True
except ImportError:
    HAS_IMAGEHASH = False

# Optional: scipy for temperature optimization
try:
    from scipy.optimize import minimize_scalar
    HAS_SCIPY = True
except ImportError:
    HAS_SCIPY = False

# Detect environment
try:
    from google.colab import drive
    IS_COLAB = True
    print("Running on Google Colab")
except ModuleNotFoundError:
    IS_COLAB = False
    print(f"Running locally (TF {tf.__version__}, GPU: {tf.config.list_physical_devices('GPU')})")

tf.keras.mixed_precision.set_global_policy('float32')

# Hyperparams
IMG_SIZE = (224, 224)
BATCH_SIZE = 32
SEED = 42
random.seed(SEED)
np.random.seed(SEED)
tf.random.set_seed(SEED)

AUTOTUNE = tf.data.AUTOTUNE
print(f"Setup OK. IMG={IMG_SIZE}, BATCH={BATCH_SIZE}")
2026-06-12 12:31:54.010414: E external/local_xla/xla/stream_executor/cuda/cuda_fft.cc:467] Unable to register cuFFT factory: Attempting to register factory for plugin cuFFT when one has already been registered
WARNING: All log messages before absl::InitializeLog() is called are written to STDERR
E0000 00:00:1781267514.033229  849073 cuda_dnn.cc:8579] Unable to register cuDNN factory: Attempting to register factory for plugin cuDNN when one has already been registered
E0000 00:00:1781267514.040546  849073 cuda_blas.cc:1407] Unable to register cuBLAS factory: Attempting to register factory for plugin cuBLAS when one has already been registered
W0000 00:00:1781267514.059144  849073 computation_placer.cc:177] computation placer already registered. Please check linkage and avoid linking the same target more than once.
W0000 00:00:1781267514.059161  849073 computation_placer.cc:177] computation placer already registered. Please check linkage and avoid linking the same target more than once.
W0000 00:00:1781267514.059163  849073 computation_placer.cc:177] computation placer already registered. Please check linkage and avoid linking the same target more than once.
W0000 00:00:1781267514.059165  849073 computation_placer.cc:177] computation placer already registered. Please check linkage and avoid linking the same target more than once.
2026-06-12 12:31:54.064873: I tensorflow/core/platform/cpu_feature_guard.cc:210] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations.
To enable the following instructions: AVX2 FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags.
Running locally (TF 2.19.0, GPU: [PhysicalDevice(name='/physical_device:GPU:0', device_type='GPU')])
Setup OK. IMG=(224, 224), BATCH=32

2. Download dan Ekstraksi Dataset

In [3]:
if IS_COLAB:
    drive.mount('/content/drive')
    archive_path = '/content/drive/MyDrive/jagung/dataset.zip'
    destination_path = '/content/dataset.zip'
    extract_path = '/content/dataset'
else:
    base = os.getcwd()
    archive_path = os.path.join(base, 'dataset.zip')
    destination_path = archive_path
    extract_path = os.path.join(base, 'dataset')

if os.path.exists(destination_path):
    if not os.path.exists(extract_path) or len(os.listdir(extract_path)) == 0:
        os.makedirs(extract_path, exist_ok=True)
        print("Extracting dataset...")
        try:
            with zipfile.ZipFile(destination_path, 'r') as zip_ref:
                zip_ref.extractall(path=extract_path)
            print("Extraction completed!")
        except Exception as e:
            print(f"Extraction failed: {e}")
    else:
        print("Dataset ready.")
else:
    print(f"dataset.zip not found at {destination_path}. Upload dataset.zip to Google Drive / MyDrive/jagung/")
    print("Or run preprocessing.py locally and upload the resulting dataset.zip")
Dataset ready.

3. Data Cleaning — Corrupt Detection + Augmented Dedup

Membersihkan dataset dari:

  • File corrupt / tidak bisa dibuka PIL
  • File augmented_* (pre-augmented duplicates — menyebabkan data leakage)
  • Gambar dengan dimensi atau aspect ratio ekstrim
  • Gambar dengan variance terlalu rendah (hampir seragam)
In [4]:
# --- Determine dataset path ---
dataset_path = extract_path

print(f"Dataset path: {dataset_path}")

# Check if dataset already validated and split exists — skip if so
split_output_dir = "/content/dataset_split" if IS_COLAB else os.path.join(os.getcwd(), "dataset_split")
if os.path.exists(os.path.join(split_output_dir, 'train')):
    print("Split dataset already exists. Skipping validation and split.")
    # Still need class_names and counts for downstream cells
    train_dir = os.path.join(split_output_dir, 'train')
    val_dir = os.path.join(split_output_dir, 'val')
    test_dir = os.path.join(split_output_dir, 'test')
    print(f'Train: {sum(len(files) for _, _, files in os.walk(train_dir))} | '
          f'Val: {sum(len(files) for _, _, files in os.walk(val_dir))} | '
          f'Test: {sum(len(files) for _, _, files in os.walk(test_dir))}')
else:
    MIN_FILE_SIZE = 512
    MIN_DIM = 32
    MAX_ASPECT = 5.0

    def remove_augmented_duplicates(directory):
        removed = 0
        for root, dirs, files in os.walk(directory):
            for file in files:
                if file.startswith("augmented_"):
                    try:
                        os.remove(os.path.join(root, file))
                        removed += 1
                    except OSError:
                        pass
        return removed

    def clean_and_validate_images(directory):
        stats = {"too_small": 0, "corrupt": 0, "small_dims": 0, "extreme_aspect": 0, "low_var": 0, "ok": 0}
        for root, dirs, files in os.walk(directory):
            for file in files:
                fp = os.path.join(root, file)
                try:
                    if os.path.getsize(fp) < MIN_FILE_SIZE:
                        os.remove(fp); stats["too_small"] += 1; continue
                except OSError:
                    continue
                try:
                    img = Image.open(fp); img.verify()
                except Exception:
                    try: os.remove(fp); stats["corrupt"] += 1
                    except OSError: pass
                    continue
                try:
                    img = Image.open(fp)
                    w, h = img.size
                    if w < MIN_DIM or h < MIN_DIM:
                        os.remove(fp); stats["small_dims"] += 1; continue
                    aspect = w / max(h, 1)
                    if aspect > MAX_ASPECT or aspect < 1.0 / MAX_ASPECT:
                        os.remove(fp); stats["extreme_aspect"] += 1; continue
                    if img.mode not in ('RGB', 'RGBA'):
                        img = img.convert('RGB'); img.save(fp)
                    arr = np.array(img).astype(np.float32)
                    if np.std(arr) < 2.0:
                        os.remove(fp); stats["low_var"] += 1; continue
                    stats["ok"] += 1
                except Exception:
                    try: os.remove(fp); stats["corrupt"] += 1
                    except OSError: pass
        return stats

    print("1. Removing augmented duplicates...")
    n_aug = remove_augmented_duplicates(dataset_path)
    print(f"   Removed {n_aug} augmented_* files")

    print("2. Validating images...")
    stats = clean_and_validate_images(dataset_path)
    print(f"   OK: {stats['ok']} | Removed: too_small={stats['too_small']} corrupt={stats['corrupt']} "
          f"small_dims={stats['small_dims']} aspect={stats['extreme_aspect']} low_var={stats['low_var']}")

    if HAS_IMAGEHASH:
        print("3. Perceptual hash dedup...")
        seen, removed = {}, 0
        for cn in sorted(os.listdir(dataset_path)):
            cp = os.path.join(dataset_path, cn)
            if not os.path.isdir(cp): continue
            for f in sorted(os.listdir(cp)):
                fp = os.path.join(cp, f)
                if not os.path.isfile(fp): continue
                try:
                    ah = imagehash.average_hash(Image.open(fp).convert('RGB'))
                    for sk, (sp, sc) in seen.items():
                        if ah - imagehash.hex_to_hash(sk) <= 5:
                            try: os.remove(fp); removed += 1
                            except OSError: pass
                            break
                    else:
                        seen[str(ah)] = (fp, cn)
                except Exception:
                    pass
        print(f"   Removed {removed} near-duplicates")
    else:
        print("3. Perceptual hash dedup SKIPPED (pip install imagehash)")

    total = sum(len(files) for _, _, files in os.walk(dataset_path))
    print(f"\nTotal clean images: {total}")
Dataset path: /home/asephs/ZeaVis-Edu/Machine_Learning/dataset
Split dataset already exists. Skipping validation and split.
Train: 3526 | Val: 754 | Test: 758

4. Stratified Split by Source (70:15:15)

Tidak menggunakan splitfolders! Split manual dengan stratifikasi berdasarkan prefix sumber gambar. Ini mencegah gambar dari sesi foto yang sama (lighting & background identik) masuk ke train DAN test.

Source prefixes:

  • IMG_* → foto HP
  • Corn_* → dataset lab publik
  • CBS*, GLS*, NLS*, CLS* → berbagai dataset lab
  • SCR*, CR*, NLB*, SLB* → dataset spesifik penyakit
In [5]:
def extract_source_prefix(filename):
    f = os.path.splitext(filename)[0]
    if f.startswith('IMG_'): return 'phone'
    if f.startswith('Corn_'): return 'lab_corn'
    for prefix in ['CBS', 'GLS', 'NLS', 'CLS']:
        if f.startswith(prefix): return 'lab_disease'
    for prefix in ['SCR', 'CR', 'NLB', 'SLB', 'SRS']:
        if f.startswith(prefix): return 'lab_rust_blight'
    return 'other'

def stratified_split_by_source(dataset_path, output_dir, ratios=(0.7, 0.15, 0.15), seed=42):
    class_images = {}
    for cn in sorted(os.listdir(dataset_path)):
        cp = os.path.join(dataset_path, cn)
        if not os.path.isdir(cp): continue
        class_images[cn] = []
        for f in os.listdir(cp):
            fp = os.path.join(cp, f)
            if os.path.isfile(fp):
                class_images[cn].append((fp, f, extract_source_prefix(f)))

    for split in ['train', 'val', 'test']:
        for cn in class_images:
            os.makedirs(os.path.join(output_dir, split, cn), exist_ok=True)

    rng = np.random.RandomState(seed)

    for cn, images in class_images.items():
        by_source = {}
        for fp, fn, src in images:
            by_source.setdefault(src, []).append((fp, fn))

        train_files, val_files, test_files = [], [], []
        for src, src_images in by_source.items():
            n = len(src_images)
            rng.shuffle(src_images)
            n_train = max(1, int(n * ratios[0]))
            n_val = max(1, int(n * ratios[1]))
            train_files.extend(src_images[:n_train])
            val_files.extend(src_images[n_train:n_train + n_val])
            test_files.extend(src_images[n_train + n_val:])

        for fp, fn in train_files:
            shutil.copy(fp, os.path.join(output_dir, 'train', cn, fn))
        for fp, fn in val_files:
            shutil.copy(fp, os.path.join(output_dir, 'val', cn, fn))
        for fp, fn in test_files:
            shutil.copy(fp, os.path.join(output_dir, 'test', cn, fn))

        train_srcs = Counter(extract_source_prefix(fn) for _, fn in train_files)
        print(f"  {cn}: train={len(train_files)} val={len(val_files)} test={len(test_files)} | "
              f"sources={dict(train_srcs)}")

output_dir = split_output_dir  # Already defined in previous cell

if os.path.exists(os.path.join(output_dir, 'train')):
    print("Split already exists. Skipping.")
else:
    if os.path.exists(output_dir):
        shutil.rmtree(output_dir)

    print("Splitting dataset 70:15:15 (stratified by source)...")
    stratified_split_by_source(dataset_path, output_dir, seed=SEED)
    print("Done.")

train_dir = os.path.join(output_dir, 'train')
val_dir = os.path.join(output_dir, 'val')
test_dir = os.path.join(output_dir, 'test')

def count_images(path):
    return sum(len(files) for _, _, files in os.walk(path))

print(f'Train: {count_images(train_dir)} | Val: {count_images(val_dir)} | Test: {count_images(test_dir)}')
Split already exists. Skipping.
Train: 3526 | Val: 754 | Test: 758

5. RandAugment Pipeline dengan Weather Simulation

Pipeline augmentasi baru untuk robustness dunia nyata:

Pool 15 Transformasi (RandAugment: pilih N=3 per gambar)

Kategori Transformasi Simulasi
Geometric Flip, Rotate, Zoom, Translate, Shear Variasi angle/jarak foto
Color/Light Hue, Saturation, Brightness, Contrast, Solarize Variasi kamera & waktu hari
Weather Fog, Shadow Kondisi lapangan berkabut/berbayang
Degradation GaussianBlur, ResolutionDrop Blur gerakan, kamera rendah
Mixing MixUp, CutMix, RandomErasing Regularisasi label & occlusions

Fog dan Shadow adalah custom tf operations — tidak ada di Keras layers standar.

In [6]:
# ─── RandAugment Utilities ───

def sample_beta_distribution(size, a=0.2, b=0.2):
    g1 = tf.random.gamma([size], a, dtype=tf.float32)
    g2 = tf.random.gamma([size], b, dtype=tf.float32)
    return g2 / (g1 + g2 + 1e-8)

def _apply_contrast(img, factor):
    mean = tf.reduce_mean(tf.cast(img, tf.float32), axis=(0, 1), keepdims=True)
    return mean + factor * (tf.cast(img, tf.float32) - mean)

def _apply_brightness(img, delta):
    return tf.clip_by_value(tf.cast(img, tf.float32) + delta, 0.0, 255.0)

def _apply_hue(img, delta):
    hsv = tf.image.rgb_to_hsv(tf.cast(img, tf.float32) / 255.0)
    h = hsv[..., 0] + delta
    h = h - tf.floor(h)
    hsv_h = tf.stack([h, hsv[..., 1], hsv[..., 2]], axis=-1)
    return tf.image.hsv_to_rgb(hsv_h) * 255.0

@tf.function(reduce_retracing=True)
def _randaug_select(images, ops_per_image=3, magnitude=0.7):
    # Graph-mode-safe RandAugment. No tf.image.random_* or Keras layers
    # (both trace as Python bool checks on tensor bounds internally).

    def _zoom(img):
        h = tf.cast(tf.shape(img)[0], tf.float32)
        w = tf.cast(tf.shape(img)[1], tf.float32)
        z = tf.random.uniform([], 1.0 - 0.2 * magnitude, 1.0 + 0.2 * magnitude)
        zh = tf.cast(h / z, tf.int32); zw = tf.cast(w / z, tf.int32)
        zoomed = tf.image.resize(tf.expand_dims(img, 0), [zh, zw], method='bilinear')[0]
        return tf.image.resize_with_crop_or_pad(zoomed, tf.cast(h, tf.int32), tf.cast(w, tf.int32))
    def _translate(img):
        h, w = tf.shape(img)[0], tf.shape(img)[1]
        tx = tf.cast(tf.random.uniform([], -0.15 * magnitude, 0.15 * magnitude) * tf.cast(w, tf.float32), tf.int32)
        ty = tf.cast(tf.random.uniform([], -0.15 * magnitude, 0.15 * magnitude) * tf.cast(h, tf.float32), tf.int32)
        return tf.roll(img, [ty, tx], axis=[0, 1])

    def op_flip(img): return tf.image.random_flip_left_right(img)
    def op_flip_v(img): return tf.image.random_flip_up_down(img)
    def op_rotate_90(img):
        k = tf.random.uniform([], 0, 4, dtype=tf.int32)
        return tf.image.rot90(img, k)
    def op_zoom(img): return _zoom(img)
    def op_translate(img): return _translate(img)
    def op_contrast(img):
        f = tf.random.uniform([], 1.0 - 0.5 * magnitude, 1.0 + 0.5 * magnitude)
        return _apply_contrast(img, f)
    def op_brightness(img):
        d = tf.random.uniform([], -0.3 * magnitude * 255.0, 0.3 * magnitude * 255.0)
        return _apply_brightness(img, d)
    def op_hue(img):
        d = tf.random.uniform([], -0.08 * tf.maximum(magnitude, 0.01), 0.08 * tf.maximum(magnitude, 0.01))
        return _apply_hue(img, d)
    def op_saturation(img):
        lo = tf.maximum(0.5, 1.0 - 0.8 * magnitude)
        hi = 1.0 + 0.8 * magnitude
        f = tf.random.uniform([], lo, hi)
        hsv = tf.image.rgb_to_hsv(tf.cast(img, tf.float32) / 255.0)
        s = tf.clip_by_value(hsv[..., 1] * f, 0.0, 1.0)
        return tf.image.hsv_to_rgb(tf.stack([hsv[..., 0], s, hsv[..., 2]], axis=-1)) * 255.0
    def op_solarize(img):
        thresh = tf.random.uniform([], 0.3, 0.8) * 255.0
        f = tf.cast(img, tf.float32)
        return tf.where(f < thresh, f, 255.0 - f)
    def op_blur(img):
        h, w = tf.shape(img)[0], tf.shape(img)[1]
        sf = tf.random.uniform([], 2, 4, dtype=tf.int32)
        small = tf.image.resize(tf.expand_dims(tf.cast(img, tf.float32), 0), [h // sf, w // sf], method='bilinear')
        return tf.image.resize(small, [h, w], method='bilinear')[0]
    def op_fog(img):
        fl = tf.random.uniform([], 0.1, 0.1 + 0.4 * magnitude)
        fc = tf.random.uniform([3], 0.7, 1.0) * 255.0
        return tf.cast(img, tf.float32) * (1.0 - fl) + tf.reshape(fc, [1, 1, 3]) * fl
    def op_shadow(img):
        op = tf.random.uniform([], 0.2, 0.2 + 0.5 * magnitude)
        return tf.cast(img, tf.float32) * (1.0 - op * 0.6)
    def op_resolution_drop(img):
        h, w = tf.shape(img)[0], tf.shape(img)[1]
        sf = tf.random.uniform([], 2, 5, dtype=tf.int32)
        small = tf.image.resize(tf.expand_dims(tf.cast(img, tf.float32), 0), [h // sf, w // sf], method='bilinear')
        return tf.image.resize(small, [h, w], method='nearest')[0]
    def op_identity(img): return tf.cast(img, tf.float32)

    ops = [op_flip, op_flip_v, op_rotate_90, op_zoom, op_translate,
           op_contrast, op_brightness, op_hue, op_saturation,
           op_solarize, op_blur, op_fog, op_shadow, op_resolution_drop, op_identity]

    def apply_randaug_single(img3d):
        indices = tf.random.shuffle(tf.range(15))[:3]  # ops_per_image=3, static
        result = tf.cast(img3d, tf.float32)
        # Unrolled static 3 iterations — avoids TF shape invariance error from tf.range loop
        def _apply_one(r, idx):
            return tf.switch_case(idx, {j: lambda j=j: ops[j](r) for j in range(15)})
        i0, i1, i2 = indices[0], indices[1], indices[2]
        result = _apply_one(result, i0)
        result = _apply_one(result, i1)
        result = _apply_one(result, i2)
        return tf.clip_by_value(result, 0.0, 255.0)

    return tf.map_fn(apply_randaug_single, images, dtype=tf.float32, parallel_iterations=8)

# Compatibility wrapper using Keras Sequential for basic geometric ops (kept for visualization)
geo_aug = tf.keras.Sequential([
    layers.RandomFlip("horizontal_and_vertical"),
    layers.RandomRotation(0.15),
    layers.RandomZoom(0.15),
    layers.RandomTranslation(0.1, 0.1),
    layers.RandomContrast(0.15),
    layers.RandomBrightness(0.15),
], name="geo_aug")

# ─── MixUp & CutMix (unchanged from original) ───

def mix_up(images, labels, alpha=0.2):
    bs = tf.shape(images)[0]
    lam = sample_beta_distribution(bs, alpha, alpha)
    lam_img = tf.reshape(lam, [bs, 1, 1, 1])
    ri = tf.random.shuffle(tf.range(bs))
    mixed_img = lam_img * images + (1 - lam_img) * tf.gather(images, ri)
    labels = tf.cast(labels, tf.float32)
    lam_lbl = tf.reshape(lam, [-1, 1])
    mixed_lbl = lam_lbl * labels + (1 - lam_lbl) * tf.gather(labels, ri)
    return mixed_img, mixed_lbl

def cut_mix(images, labels, alpha=0.2):
    bs = tf.shape(images)[0]; h = tf.shape(images)[1]; w = tf.shape(images)[2]
    lam = sample_beta_distribution(bs, alpha, alpha); ri = tf.random.shuffle(tf.range(bs))
    cr = tf.sqrt(1.0 - lam)
    rh = tf.cast(cr * tf.cast(h, tf.float32), tf.int32); rw = tf.cast(cr * tf.cast(w, tf.float32), tf.int32)
    cx = tf.random.uniform([bs], 0, w, tf.int32); cy = tf.random.uniform([bs], 0, h, tf.int32)
    hh = rh // 2; hw = rw // 2
    x1 = tf.clip_by_value(cx - hw, 0, w); x2 = tf.clip_by_value(cx + hw, 0, w)
    y1 = tf.clip_by_value(cy - hh, 0, h); y2 = tf.clip_by_value(cy + hh, 0, h)
    col = tf.range(w, dtype=tf.int32); row = tf.range(h, dtype=tf.int32)
    in_x = tf.logical_and(tf.reshape(col, [1, 1, w]) >= tf.reshape(x1, [bs, 1, 1]),
                          tf.reshape(col, [1, 1, w]) < tf.reshape(x2, [bs, 1, 1]))
    in_y = tf.logical_and(tf.reshape(row, [1, h, 1]) >= tf.reshape(y1, [bs, 1, 1]),
                          tf.reshape(row, [1, h, 1]) < tf.reshape(y2, [bs, 1, 1]))
    cm = tf.cast(tf.logical_and(in_y, in_x), tf.float32); cm = tf.expand_dims(cm, -1)
    shuf = tf.gather(images, ri)
    mi = (1.0 - cm) * images + cm * shuf
    labels = tf.cast(labels, tf.float32); lr = tf.reshape(lam, [-1, 1])
    ml = lr * labels + (1.0 - lr) * tf.gather(labels, ri)
    return mi, ml

def random_erasing(images, probability=0.25, scale=(0.02, 0.25)):
    bs = tf.shape(images)[0]; h = tf.shape(images)[1]; w = tf.shape(images)[2]
    ta = tf.random.uniform([], scale[0], scale[1]) * tf.cast(h * w, tf.float32)
    ar = tf.random.uniform([], 0.3, 3.3)
    eh = tf.cast(tf.math.sqrt(ta / ar), tf.int32); ew = tf.cast(tf.math.sqrt(ta * ar), tf.int32)
    eh = tf.clip_by_value(eh, 1, h - 1); ew = tf.clip_by_value(ew, 1, w - 1)
    cx = tf.random.uniform([], 0, w - ew, tf.int32); cy = tf.random.uniform([], 0, h - eh, tf.int32)
    col = tf.range(w, dtype=tf.int32); row = tf.range(h, dtype=tf.int32)
    ix = tf.logical_and(col >= cx, col < cx + ew)
    iy = tf.logical_and(row >= cy, row < cy + eh)
    em = tf.cast(tf.expand_dims(iy, 1) & tf.expand_dims(ix, 0), tf.float32)
    em = tf.expand_dims(tf.expand_dims(em, 0), -1)
    noise = tf.random.uniform([bs, eh, ew, 3], 0.0, 255.0, dtype=tf.float32)
    pads = [[0, 0], [cy, h - (cy + eh)], [cx, w - (cx + ew)], [0, 0]]
    npad = tf.pad(noise, pads, constant_values=0.0)
    erased = images * (1.0 - em) + npad * em
    return tf.cond(tf.random.uniform([]) < probability, lambda: erased, lambda: images)

# ─── Main augmentation pipeline ───

def augment_and_mix(images, labels):
    # 1. RandAugment (geometric + color + weather + degradation)
    images = tf.cast(images, tf.float32)
    images = _randaug_select(images, ops_per_image=3, magnitude=0.7)
    # 2. MixUp or CutMix (40% chance total: 20% MixUp, 20% CutMix)
    choice = tf.random.uniform([])
    labels_oh = tf.one_hot(labels, NUM_CLASSES)
    images, labels_oh = tf.cond(
        choice < 0.2, lambda: mix_up(images, labels_oh),
        lambda: tf.cond(choice < 0.4, lambda: cut_mix(images, labels_oh),
                        lambda: (images, labels_oh)))
    # 3. Random Erasing
    images = random_erasing(images, probability=0.2)
    return images, labels_oh

# ─── Preprocessing ───
def preprocess_fn(image, label):
    return preprocess_input(image), label

# ─── Class weights ───
train_class_counts = Counter()
for cn in sorted(os.listdir(train_dir)):
    p = os.path.join(train_dir, cn)
    if os.path.isdir(p):
        train_class_counts[cn] = len(os.listdir(p))

y_int = []
for i, cn in enumerate(sorted(os.listdir(train_dir))):
    cp = os.path.join(train_dir, cn)
    if os.path.isdir(cp):
        y_int.extend([i] * len(os.listdir(cp)))

cw_array = compute_class_weight('balanced', classes=np.unique(y_int), y=y_int)
cw_capped = [min(w, 3.0) for w in cw_array]
class_weights_tensor = tf.constant(cw_capped, dtype=tf.float32)

def add_sample_weight(image, label):
    ci = tf.argmax(label, axis=-1)
    sw = tf.gather(class_weights_tensor, ci)
    return image, label, sw

# ─── Build datasets ───
train_ds = tf.keras.utils.image_dataset_from_directory(
    train_dir, shuffle=True, batch_size=BATCH_SIZE, image_size=IMG_SIZE)
val_ds = tf.keras.utils.image_dataset_from_directory(
    val_dir, shuffle=True, batch_size=BATCH_SIZE, image_size=IMG_SIZE)
test_ds = tf.keras.utils.image_dataset_from_directory(
    test_dir, shuffle=False, batch_size=BATCH_SIZE, image_size=IMG_SIZE)

class_names = train_ds.class_names
NUM_CLASSES = len(class_names)
print(f"Classes ({NUM_CLASSES}): {class_names}")

train_ds = (train_ds
    .map(augment_and_mix, num_parallel_calls=AUTOTUNE)
    .map(preprocess_fn, num_parallel_calls=AUTOTUNE)
    .map(add_sample_weight, num_parallel_calls=AUTOTUNE)
    .prefetch(AUTOTUNE))

val_ds = (val_ds
    .map(lambda img, lbl: (preprocess_input(tf.cast(img, tf.float32)), lbl), num_parallel_calls=AUTOTUNE)
    .map(lambda img, lbl: (img, tf.one_hot(lbl, NUM_CLASSES)), num_parallel_calls=AUTOTUNE)
    .prefetch(AUTOTUNE))

test_ds = (test_ds
    .map(lambda img, lbl: (preprocess_input(tf.cast(img, tf.float32)), lbl), num_parallel_calls=AUTOTUNE)
    .map(lambda img, lbl: (img, tf.one_hot(lbl, NUM_CLASSES)), num_parallel_calls=AUTOTUNE)
    .prefetch(AUTOTUNE))

print("Class weights (capped at 3.0):")
for i, cn in enumerate(class_names):
    if i < len(cw_capped):
        print(f"  {cn}: {cw_capped[i]:.4f}")
print("Data pipelines ready.")
Found 3526 files belonging to 4 classes.
I0000 00:00:1781267516.747119  849073 gpu_device.cc:2019] Created device /job:localhost/replica:0/task:0/device:GPU:0 with 37480 MB memory:  -> device: 0, name: NVIDIA A100-SXM4-40GB, pci bus id: 0000:05:00.0, compute capability: 8.0
Found 754 files belonging to 4 classes.
Found 758 files belonging to 4 classes.
Classes (4): ['Bercak Daun', 'Daun Sehat', 'Hawar Daun', 'Karat Daun']
WARNING:tensorflow:From /home/asephs/ZeaVis-Edu/.venv/lib/python3.12/site-packages/tensorflow/python/util/deprecation.py:660: calling map_fn_v2 (from tensorflow.python.ops.map_fn) with dtype is deprecated and will be removed in a future version.
Instructions for updating:
Use fn_output_signature instead
Class weights (capped at 3.0):
  Bercak Daun: 2.1983
  Daun Sehat: 0.5826
  Hawar Daun: 1.2593
  Karat Daun: 0.9666
Data pipelines ready.

6. Visualisasi Sampel Data (Augmentasi Real-World)

In [7]:
vis_ds = tf.keras.utils.image_dataset_from_directory(
    train_dir, shuffle=True, batch_size=BATCH_SIZE, image_size=IMG_SIZE)

plt.figure(figsize=(16, 10))
for images, labels in vis_ds.take(1):
    # Original (4)
    for i in range(4):
        plt.subplot(3, 4, i + 1)
        plt.imshow(images[i].numpy().astype("uint8"))
        plt.title(f"Asli: {class_names[labels[i].numpy()]}", fontsize=11)
        plt.axis("off")
    # RandAugment (4)
    aug = _randaug_select(tf.cast(images, tf.float32), ops_per_image=3, magnitude=0.7)
    for i in range(4):
        plt.subplot(3, 4, i + 5)
        plt.imshow(tf.clip_by_value(aug[i], 0, 255).numpy().astype("uint8"))
        plt.title(f"RandAug: {class_names[labels[i].numpy()]}", fontsize=11)
        plt.axis("off")
    # MixUp result (4)
    aug_f = tf.cast(images, tf.float32)
    mixed, _ = mix_up(aug_f, tf.one_hot(labels, NUM_CLASSES))
    for i in range(4):
        plt.subplot(3, 4, i + 9)
        plt.imshow(tf.clip_by_value(mixed[i], 0, 255).numpy().astype("uint8"))
        plt.title("MixUp/Weather", fontsize=11)
        plt.axis("off")

plt.suptitle("RandAugment + MixUp — Real-World Simulation", fontsize=16)
plt.tight_layout()
plt.show()
Found 3526 files belonging to 4 classes.
2026-06-12 12:32:04.117358: I tensorflow/core/framework/local_rendezvous.cc:407] Local rendezvous is aborting with status: OUT_OF_RANGE: End of sequence

7. Arsitektur Model — CBAM + Lightweight Head

EfficientNetV2B0 base (frozen awal) + CBAM spatial attention + lightweight classifier head.

Head (≈700K params vs 7.3M sebelumnya)

Base (7×7×1280) → CBAM_Attention → GAP → Dropout(0.3) → Dense(512, swish) → BN → Dropout(0.4) → Dense(4, softmax)

CBAM (Convolutional Block Attention Module): channel attention + spatial attention → model belajar fokus ke foreground (daun) bukan background.

In [8]:
def cbam_block(x, ratio=8, name="cbam"):
    # Convolutional Block Attention Module — ringan, fokus ke foreground.
    channels = x.shape[-1]

    # Channel Attention
    avg_pool = layers.GlobalAveragePooling2D()(x)
    max_pool = layers.GlobalMaxPooling2D()(x)
    ca = layers.Dense(channels // ratio, activation='swish', name=f"{name}_ca1")(avg_pool)
    ca = layers.Dense(channels, activation='sigmoid', name=f"{name}_ca2")(ca)
    ca2 = layers.Dense(channels // ratio, activation='swish', name=f"{name}_ca3")(max_pool)
    ca2 = layers.Dense(channels, activation='sigmoid', name=f"{name}_ca4")(ca2)
    ca_out = layers.Add(name=f"{name}_ca_add")([ca, ca2])
    ca_out = layers.Reshape((1, 1, channels), name=f"{name}_ca_reshape")(ca_out)
    x = layers.Multiply(name=f"{name}_ca_mul")([x, ca_out])

    # Spatial Attention
    avg_sp = layers.Lambda(lambda t: tf.reduce_mean(t, axis=-1, keepdims=True), name=f"{name}_sa_avg")(x)
    max_sp = layers.Lambda(lambda t: tf.reduce_max(t, axis=-1, keepdims=True), name=f"{name}_sa_max")(x)
    sp = layers.Concatenate(name=f"{name}_sa_cat")([avg_sp, max_sp])
    sp = layers.Conv2D(1, 7, padding='same', activation='sigmoid', name=f"{name}_sa_conv")(sp)
    x = layers.Multiply(name=f"{name}_sa_mul")([x, sp])
    return x

def build_model(num_classes, target_size=(224, 224)):
    # Accept any input size via variable input; Resizing handles progressive resolution.
    inputs = tf.keras.Input(shape=(None, None, 3), name="input")
    x = layers.Resizing(target_size[0], target_size[1], interpolation='bilinear',
                        name="resize_input")(inputs)

    base_model = EfficientNetV2B0(
        input_shape=target_size + (3,),
        include_top=False,
        weights='imagenet',
    )
    base_model.trainable = False

    # Gaussian noise untuk regularisasi
    x = layers.GaussianNoise(0.05, name="gauss_noise")(x)
    x = base_model(x, training=False)
    # CBAM attention — fokus ke region daun
    x = cbam_block(x, ratio=8, name="cbam")
    x = layers.GlobalAveragePooling2D(name="gap")(x)
    x = layers.Dropout(0.3, name="drop_gap")(x)
    x = layers.Dense(512, activation='swish', name="dense_head")(x)
    x = layers.BatchNormalization(name="bn_head")(x)
    x = layers.Dropout(0.4, name="drop_head")(x)
    outputs = layers.Dense(num_classes, activation='linear', dtype='float32', name="logits")(x)
    return models.Model(inputs, outputs), base_model

# Checkpoint
ckpt_dir = '/content/best_model' if IS_COLAB else os.path.join(os.getcwd(), 'best_model')
checkpoint_path = os.path.join(ckpt_dir, 'best_model.keras')

# Hapus checkpoint lama (arsitektur berbeda — tidak kompatibel)
if os.path.exists(checkpoint_path):
    print(f"Removing old checkpoint (incompatible architecture)...")
    os.remove(checkpoint_path)

if os.path.exists(checkpoint_path):
    print(f"Loading checkpoint: {checkpoint_path}")
    try:
        model = models.load_model(checkpoint_path, compile=False)
    except Exception as e:
        print(f"Load failed: {e}. Building fresh.")
        model, base_model = build_model(NUM_CLASSES)
else:
    print("No checkpoint. Building fresh model.")
    model, base_model = build_model(NUM_CLASSES)

os.makedirs(ckpt_dir, exist_ok=True)
model.summary()
No checkpoint. Building fresh model.
Model: "functional"
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓
┃ Layer (type)         Output Shape          Param #  Connected to      ┃
┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩
│ input (InputLayer)  │ (None, None,      │          0 │ -                 │
│                     │ None, 3)          │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ resize_input        │ (None, 224, 224,  │          0 │ input[0][0]       │
│ (Resizing)          │ 3)                │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ gauss_noise         │ (None, 224, 224,  │          0 │ resize_input[0][ │
│ (GaussianNoise)     │ 3)                │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ efficientnetv2-b0   │ (None, 7, 7,      │  5,919,312 │ gauss_noise[0][0] │
│ (Functional)        │ 1280)             │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ global_average_poo… │ (None, 1280)      │          0 │ efficientnetv2-b… │
│ (GlobalAveragePool… │                   │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ global_max_pooling… │ (None, 1280)      │          0 │ efficientnetv2-b… │
│ (GlobalMaxPooling2… │                   │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ cbam_ca1 (Dense)    │ (None, 160)       │    204,960 │ global_average_p… │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ cbam_ca3 (Dense)    │ (None, 160)       │    204,960 │ global_max_pooli… │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ cbam_ca2 (Dense)    │ (None, 1280)      │    206,080 │ cbam_ca1[0][0]    │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ cbam_ca4 (Dense)    │ (None, 1280)      │    206,080 │ cbam_ca3[0][0]    │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ cbam_ca_add (Add)   │ (None, 1280)      │          0 │ cbam_ca2[0][0],   │
│                     │                   │            │ cbam_ca4[0][0]    │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ cbam_ca_reshape     │ (None, 1, 1,      │          0 │ cbam_ca_add[0][0] │
│ (Reshape)           │ 1280)             │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ cbam_ca_mul         │ (None, 7, 7,      │          0 │ efficientnetv2-b… │
│ (Multiply)          │ 1280)             │            │ cbam_ca_reshape[ │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ cbam_sa_avg         │ (None, 7, 7, 1)   │          0 │ cbam_ca_mul[0][0] │
│ (Lambda)            │                   │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ cbam_sa_max         │ (None, 7, 7, 1)   │          0 │ cbam_ca_mul[0][0] │
│ (Lambda)            │                   │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ cbam_sa_cat         │ (None, 7, 7, 2)   │          0 │ cbam_sa_avg[0][0… │
│ (Concatenate)       │                   │            │ cbam_sa_max[0][0] │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ cbam_sa_conv        │ (None, 7, 7, 1)   │         99 │ cbam_sa_cat[0][0] │
│ (Conv2D)            │                   │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ cbam_sa_mul         │ (None, 7, 7,      │          0 │ cbam_ca_mul[0][0… │
│ (Multiply)          │ 1280)             │            │ cbam_sa_conv[0][ │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ gap                 │ (None, 1280)      │          0 │ cbam_sa_mul[0][0] │
│ (GlobalAveragePool… │                   │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ drop_gap (Dropout)  │ (None, 1280)      │          0 │ gap[0][0]         │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ dense_head (Dense)  │ (None, 512)       │    655,872 │ drop_gap[0][0]    │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ bn_head             │ (None, 512)       │      2,048 │ dense_head[0][0]  │
│ (BatchNormalizatio… │                   │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ drop_head (Dropout) │ (None, 512)       │          0 │ bn_head[0][0]     │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ logits (Dense)      │ (None, 4)         │      2,052 │ drop_head[0][0]   │
└─────────────────────┴───────────────────┴────────────┴───────────────────┘
 Total params: 7,401,463 (28.23 MB)
 Trainable params: 1,481,127 (5.65 MB)
 Non-trainable params: 5,920,336 (22.58 MB)

8. Training Setup — Cosine Decay + SWA + Callbacks

Mengganti ReduceLROnPlateau / EarlyStopping dengan:

  • CosineDecay + linear warmup per fase
  • Stochastic Weight Averaging (SWA) — averaging bobot untuk wider optima
  • ModelCheckpoint — tetap simpan best val_accuracy
In [9]:
class WarmupCosineDecay(tf.keras.optimizers.schedules.LearningRateSchedule):
    # Cosine decay with linear warmup.
    def __init__(self, warmup_steps, total_steps, peak_lr, min_lr=1e-7):
        super().__init__()
        self.warmup_steps = warmup_steps
        self.total_steps = total_steps
        self.peak_lr = peak_lr
        self.min_lr = min_lr

    def __call__(self, step):
        step = tf.cast(step, tf.float32)
        warmup_steps = tf.cast(self.warmup_steps, tf.float32)
        total_steps = tf.cast(self.total_steps, tf.float32)
        # Warmup phase
        warmup_lr = self.peak_lr * (step / warmup_steps)
        # Cosine decay phase
        progress = (step - warmup_steps) / tf.maximum(total_steps - warmup_steps, 1.0)
        cosine_lr = self.min_lr + 0.5 * (self.peak_lr - self.min_lr) * (1.0 + tf.cos(np.pi * progress))
        return tf.where(step < warmup_steps, warmup_lr, cosine_lr)

    def get_config(self):
        return {
            "warmup_steps": self.warmup_steps, "total_steps": self.total_steps,
            "peak_lr": self.peak_lr, "min_lr": self.min_lr,
        }

class SWACallback(tf.keras.callbacks.Callback):
    # Stochastic Weight Averaging — averages weights over final epochs.
    def __init__(self, start_epoch, swa_lr=1e-5):
        super().__init__()
        self.start_epoch = start_epoch
        self.swa_lr = swa_lr
        self.swa_weights = None
        self.n_models = 0

    def on_epoch_begin(self, epoch, logs=None):
        if epoch >= self.start_epoch and self.swa_weights is None:
            self.swa_weights = [w.numpy() for w in self.model.weights]
            print(f"\nSWA: starting weight averaging at epoch {epoch+1}")

    def on_epoch_end(self, epoch, logs=None):
        if epoch >= self.start_epoch:
            tf.keras.backend.set_value(self.model.optimizer.learning_rate, self.swa_lr)
            for i, w in enumerate(self.model.weights):
                self.swa_weights[i] = (self.swa_weights[i] * self.n_models + w.numpy()) / (self.n_models + 1)
            self.n_models += 1

    def apply_swa_weights(self):
        for w, swa_w in zip(self.model.weights, self.swa_weights):
            w.assign(swa_w)
        print(f"SWA weights applied ({self.n_models} models averaged).")

# Shared callbacks
checkpoint_cb = callbacks.ModelCheckpoint(
    checkpoint_path, save_best_only=True, monitor="val_accuracy",
    mode="max", verbose=1)

csv_logger = callbacks.CSVLogger(os.path.join(ckpt_dir, 'training_log.csv'))

def make_callbacks(swa_start=None):
    cbs = [checkpoint_cb, csv_logger]
    if swa_start is not None:
        cbs.append(SWACallback(swa_start))
    return cbs

print("Callbacks ready.")
print(f"Checkpoint path: {checkpoint_path}")
Callbacks ready.
Checkpoint path: /home/asephs/ZeaVis-Edu/Machine_Learning/best_model/best_model.keras

9. Fase 1 — Head Only Training (128×128)

Progressive resolution: mulai dari 128×128 untuk feature learning cepat. Base model beku, hanya head (CBAM + Dense) yang dilatih. Optimizer: AdamW + EMA + CosineDecay(warmup=3, peak=1e-3). Label smoothing: 0.15

In [10]:
IMG_128 = (128, 128)

# Rebuild datasets at 128x128
train_ds_128 = tf.keras.utils.image_dataset_from_directory(
    train_dir, shuffle=True, batch_size=BATCH_SIZE, image_size=IMG_128)
val_ds_128 = tf.keras.utils.image_dataset_from_directory(
    val_dir, shuffle=True, batch_size=BATCH_SIZE, image_size=IMG_128)

train_ds_128 = (train_ds_128.map(augment_and_mix, num_parallel_calls=AUTOTUNE)
    .map(preprocess_fn, num_parallel_calls=AUTOTUNE)
    .map(add_sample_weight, num_parallel_calls=AUTOTUNE).prefetch(AUTOTUNE))
val_ds_128 = (val_ds_128.map(lambda img, lbl: (preprocess_input(tf.cast(img, tf.float32)), lbl), num_parallel_calls=AUTOTUNE)
    .map(lambda img, lbl: (img, tf.one_hot(lbl, NUM_CLASSES)), num_parallel_calls=AUTOTUNE).prefetch(AUTOTUNE))

EPOCHS_P1 = 25
steps_per_epoch = tf.data.experimental.cardinality(train_ds_128).numpy() or 100
total_steps = steps_per_epoch * EPOCHS_P1
warmup_steps = steps_per_epoch * 3  # 3 epoch warmup

lr_schedule_p1 = WarmupCosineDecay(warmup_steps, total_steps, peak_lr=1e-3, min_lr=1e-5)

model.compile(
    optimizer=AdamW(
                    learning_rate=lr_schedule_p1, weight_decay=1e-4),
    loss=tf.keras.losses.CategoricalCrossentropy(from_logits=True, label_smoothing=0.15),
    metrics=['accuracy']
)

print("Phase 1: Head training at 128×128...")
history_1 = model.fit(train_ds_128, validation_data=val_ds_128,
    epochs=EPOCHS_P1, callbacks=make_callbacks())
Found 3526 files belonging to 4 classes.
Found 754 files belonging to 4 classes.
Phase 1: Head training at 128×128...
Epoch 1/25
WARNING: All log messages before absl::InitializeLog() is called are written to STDERR
I0000 00:00:1781267542.232143  850891 service.cc:152] XLA service 0x7b25a40026b0 initialized for platform CUDA (this does not guarantee that XLA will be used). Devices:
I0000 00:00:1781267542.232175  850891 service.cc:160]   StreamExecutor device (0): NVIDIA A100-SXM4-40GB, Compute Capability 8.0
2026-06-12 12:32:23.839126: I tensorflow/compiler/mlir/tensorflow/utils/dump_mlir_util.cc:269] disabling MLIR crash reproducer, set env var `MLIR_CRASH_REPRODUCER_DIRECTORY` to enable.
I0000 00:00:1781267546.549889  850891 cuda_dnn.cc:529] Loaded cuDNN version 92301
2026-06-12 12:32:29.200759: I external/local_xla/xla/stream_executor/cuda/subprocess_compilation.cc:346] ptxas warning : Registers are spilled to local memory in function 'gemm_fusion_dot_9080', 12 bytes spill stores, 12 bytes spill loads

2026-06-12 12:32:29.295795: I external/local_xla/xla/stream_executor/cuda/subprocess_compilation.cc:346] ptxas warning : Registers are spilled to local memory in function 'gemm_fusion_dot_9080', 232 bytes spill stores, 232 bytes spill loads

2026-06-12 12:32:29.446123: I external/local_xla/xla/stream_executor/cuda/subprocess_compilation.cc:346] ptxas warning : Registers are spilled to local memory in function 'gemm_fusion_dot_9080_0', 428 bytes spill stores, 1360 bytes spill loads

2026-06-12 12:32:30.053795: I external/local_xla/xla/stream_executor/cuda/subprocess_compilation.cc:346] ptxas warning : Registers are spilled to local memory in function 'gemm_fusion_dot_9080', 5372 bytes spill stores, 5348 bytes spill loads

2026-06-12 12:32:30.587357: I external/local_xla/xla/stream_executor/cuda/subprocess_compilation.cc:346] ptxas warning : Registers are spilled to local memory in function 'gemm_fusion_dot_9080', 6000 bytes spill stores, 5952 bytes spill loads

2026-06-12 12:32:32.900172: E external/local_xla/xla/stream_executor/cuda/cuda_timer.cc:86] Delay kernel timed out: measured time has sub-optimal accuracy. There may be a missing warmup execution, please investigate in Nsight Systems.
2026-06-12 12:32:33.057009: E external/local_xla/xla/stream_executor/cuda/cuda_timer.cc:86] Delay kernel timed out: measured time has sub-optimal accuracy. There may be a missing warmup execution, please investigate in Nsight Systems.
2026-06-12 12:32:33.860246: E external/local_xla/xla/stream_executor/cuda/cuda_timer.cc:86] Delay kernel timed out: measured time has sub-optimal accuracy. There may be a missing warmup execution, please investigate in Nsight Systems.
2026-06-12 12:32:34.024636: E external/local_xla/xla/stream_executor/cuda/cuda_timer.cc:86] Delay kernel timed out: measured time has sub-optimal accuracy. There may be a missing warmup execution, please investigate in Nsight Systems.
2026-06-12 12:32:34.645408: E external/local_xla/xla/stream_executor/cuda/cuda_timer.cc:86] Delay kernel timed out: measured time has sub-optimal accuracy. There may be a missing warmup execution, please investigate in Nsight Systems.
2026-06-12 12:32:34.797216: E external/local_xla/xla/stream_executor/cuda/cuda_timer.cc:86] Delay kernel timed out: measured time has sub-optimal accuracy. There may be a missing warmup execution, please investigate in Nsight Systems.
2026-06-12 12:32:35.169073: E external/local_xla/xla/stream_executor/cuda/cuda_timer.cc:86] Delay kernel timed out: measured time has sub-optimal accuracy. There may be a missing warmup execution, please investigate in Nsight Systems.
2026-06-12 12:32:35.336456: E external/local_xla/xla/stream_executor/cuda/cuda_timer.cc:86] Delay kernel timed out: measured time has sub-optimal accuracy. There may be a missing warmup execution, please investigate in Nsight Systems.
2026-06-12 12:32:36.008184: E external/local_xla/xla/stream_executor/cuda/cuda_timer.cc:86] Delay kernel timed out: measured time has sub-optimal accuracy. There may be a missing warmup execution, please investigate in Nsight Systems.
2026-06-12 12:32:36.175558: E external/local_xla/xla/stream_executor/cuda/cuda_timer.cc:86] Delay kernel timed out: measured time has sub-optimal accuracy. There may be a missing warmup execution, please investigate in Nsight Systems.
2026-06-12 12:32:36.619597: E external/local_xla/xla/stream_executor/cuda/cuda_timer.cc:86] Delay kernel timed out: measured time has sub-optimal accuracy. There may be a missing warmup execution, please investigate in Nsight Systems.
2026-06-12 12:32:36.776463: E external/local_xla/xla/stream_executor/cuda/cuda_timer.cc:86] Delay kernel timed out: measured time has sub-optimal accuracy. There may be a missing warmup execution, please investigate in Nsight Systems.
2026-06-12 12:32:36.972993: E external/local_xla/xla/stream_executor/cuda/cuda_timer.cc:86] Delay kernel timed out: measured time has sub-optimal accuracy. There may be a missing warmup execution, please investigate in Nsight Systems.
2026-06-12 12:32:37.131688: E external/local_xla/xla/stream_executor/cuda/cuda_timer.cc:86] Delay kernel timed out: measured time has sub-optimal accuracy. There may be a missing warmup execution, please investigate in Nsight Systems.
 11/111 ━━━━━━━━━━━━━━━━━━━━ 1s 12ms/step - accuracy: 0.2085 - loss: 2.3833   
I0000 00:00:1781267566.664868  850891 device_compiler.h:188] Compiled cluster using XLA!  This line is logged at most once for the lifetime of the process.
110/111 ━━━━━━━━━━━━━━━━━━━━ 0s 46ms/step - accuracy: 0.4610 - loss: 1.7774
2026-06-12 12:32:56.035340: I external/local_xla/xla/stream_executor/cuda/subprocess_compilation.cc:346] ptxas warning : Registers are spilled to local memory in function 'gemm_fusion_dot_9080', 156 bytes spill stores, 156 bytes spill loads

2026-06-12 12:32:56.627147: I external/local_xla/xla/stream_executor/cuda/subprocess_compilation.cc:346] ptxas warning : Registers are spilled to local memory in function 'gemm_fusion_dot_9080', 5100 bytes spill stores, 5108 bytes spill loads

2026-06-12 12:32:57.144129: I external/local_xla/xla/stream_executor/cuda/subprocess_compilation.cc:346] ptxas warning : Registers are spilled to local memory in function 'gemm_fusion_dot_9080', 4500 bytes spill stores, 4492 bytes spill loads

2026-06-12 12:32:58.668872: E external/local_xla/xla/stream_executor/cuda/cuda_timer.cc:86] Delay kernel timed out: measured time has sub-optimal accuracy. There may be a missing warmup execution, please investigate in Nsight Systems.
2026-06-12 12:32:58.822376: E external/local_xla/xla/stream_executor/cuda/cuda_timer.cc:86] Delay kernel timed out: measured time has sub-optimal accuracy. There may be a missing warmup execution, please investigate in Nsight Systems.
2026-06-12 12:32:59.638795: E external/local_xla/xla/stream_executor/cuda/cuda_timer.cc:86] Delay kernel timed out: measured time has sub-optimal accuracy. There may be a missing warmup execution, please investigate in Nsight Systems.
2026-06-12 12:32:59.797682: E external/local_xla/xla/stream_executor/cuda/cuda_timer.cc:86] Delay kernel timed out: measured time has sub-optimal accuracy. There may be a missing warmup execution, please investigate in Nsight Systems.
2026-06-12 12:33:00.277816: E external/local_xla/xla/stream_executor/cuda/cuda_timer.cc:86] Delay kernel timed out: measured time has sub-optimal accuracy. There may be a missing warmup execution, please investigate in Nsight Systems.
2026-06-12 12:33:00.429169: E external/local_xla/xla/stream_executor/cuda/cuda_timer.cc:86] Delay kernel timed out: measured time has sub-optimal accuracy. There may be a missing warmup execution, please investigate in Nsight Systems.
111/111 ━━━━━━━━━━━━━━━━━━━━ 0s 209ms/step - accuracy: 0.4627 - loss: 1.7735
2026-06-12 12:33:15.966359: I external/local_xla/xla/stream_executor/cuda/subprocess_compilation.cc:346] ptxas warning : Registers are spilled to local memory in function 'gemm_fusion_dot_2591', 12 bytes spill stores, 12 bytes spill loads

2026-06-12 12:33:16.113783: I external/local_xla/xla/stream_executor/cuda/subprocess_compilation.cc:346] ptxas warning : Registers are spilled to local memory in function 'gemm_fusion_dot_2591', 232 bytes spill stores, 232 bytes spill loads

2026-06-12 12:33:16.157001: I external/local_xla/xla/stream_executor/cuda/subprocess_compilation.cc:346] ptxas warning : Registers are spilled to local memory in function 'gemm_fusion_dot_2591_0', 496 bytes spill stores, 1400 bytes spill loads

2026-06-12 12:33:16.657805: I external/local_xla/xla/stream_executor/cuda/subprocess_compilation.cc:346] ptxas warning : Registers are spilled to local memory in function 'gemm_fusion_dot_2591', 6000 bytes spill stores, 5952 bytes spill loads

2026-06-12 12:33:16.845587: I external/local_xla/xla/stream_executor/cuda/subprocess_compilation.cc:346] ptxas warning : Registers are spilled to local memory in function 'gemm_fusion_dot_2591', 5372 bytes spill stores, 5348 bytes spill loads

2026-06-12 12:33:21.196921: I external/local_xla/xla/stream_executor/cuda/subprocess_compilation.cc:346] ptxas warning : Registers are spilled to local memory in function 'gemm_fusion_dot_2484', 16 bytes spill stores, 16 bytes spill loads

2026-06-12 12:33:21.261091: I external/local_xla/xla/stream_executor/cuda/subprocess_compilation.cc:346] ptxas warning : Registers are spilled to local memory in function 'gemm_fusion_dot_2484', 252 bytes spill stores, 252 bytes spill loads

2026-06-12 12:33:21.503880: I external/local_xla/xla/stream_executor/cuda/subprocess_compilation.cc:346] ptxas warning : Registers are spilled to local memory in function 'gemm_fusion_dot_2591', 16 bytes spill stores, 16 bytes spill loads

2026-06-12 12:33:21.851652: I external/local_xla/xla/stream_executor/cuda/subprocess_compilation.cc:346] ptxas warning : Registers are spilled to local memory in function 'gemm_fusion_dot_2484', 5912 bytes spill stores, 5980 bytes spill loads

2026-06-12 12:33:21.977707: I external/local_xla/xla/stream_executor/cuda/subprocess_compilation.cc:346] ptxas warning : Registers are spilled to local memory in function 'gemm_fusion_dot_2591', 252 bytes spill stores, 252 bytes spill loads

2026-06-12 12:33:22.043702: I external/local_xla/xla/stream_executor/cuda/subprocess_compilation.cc:346] ptxas warning : Registers are spilled to local memory in function 'gemm_fusion_dot_2591_0', 492 bytes spill stores, 1464 bytes spill loads

2026-06-12 12:33:22.058719: I external/local_xla/xla/stream_executor/cuda/subprocess_compilation.cc:346] ptxas warning : Registers are spilled to local memory in function 'gemm_fusion_dot_2484', 5356 bytes spill stores, 5344 bytes spill loads

2026-06-12 12:33:22.082950: I external/local_xla/xla/stream_executor/cuda/subprocess_compilation.cc:346] ptxas warning : Registers are spilled to local memory in function 'gemm_fusion_dot_2591', 5356 bytes spill stores, 5344 bytes spill loads

2026-06-12 12:33:22.504337: I external/local_xla/xla/stream_executor/cuda/subprocess_compilation.cc:346] ptxas warning : Registers are spilled to local memory in function 'gemm_fusion_dot_2591', 5912 bytes spill stores, 5980 bytes spill loads

2026-06-12 12:33:23.908138: E external/local_xla/xla/stream_executor/cuda/cuda_timer.cc:86] Delay kernel timed out: measured time has sub-optimal accuracy. There may be a missing warmup execution, please investigate in Nsight Systems.
2026-06-12 12:33:24.064152: E external/local_xla/xla/stream_executor/cuda/cuda_timer.cc:86] Delay kernel timed out: measured time has sub-optimal accuracy. There may be a missing warmup execution, please investigate in Nsight Systems.
2026-06-12 12:33:24.864409: E external/local_xla/xla/stream_executor/cuda/cuda_timer.cc:86] Delay kernel timed out: measured time has sub-optimal accuracy. There may be a missing warmup execution, please investigate in Nsight Systems.
2026-06-12 12:33:25.027398: E external/local_xla/xla/stream_executor/cuda/cuda_timer.cc:86] Delay kernel timed out: measured time has sub-optimal accuracy. There may be a missing warmup execution, please investigate in Nsight Systems.
2026-06-12 12:33:25.638556: E external/local_xla/xla/stream_executor/cuda/cuda_timer.cc:86] Delay kernel timed out: measured time has sub-optimal accuracy. There may be a missing warmup execution, please investigate in Nsight Systems.
2026-06-12 12:33:25.790304: E external/local_xla/xla/stream_executor/cuda/cuda_timer.cc:86] Delay kernel timed out: measured time has sub-optimal accuracy. There may be a missing warmup execution, please investigate in Nsight Systems.
2026-06-12 12:33:26.161238: E external/local_xla/xla/stream_executor/cuda/cuda_timer.cc:86] Delay kernel timed out: measured time has sub-optimal accuracy. There may be a missing warmup execution, please investigate in Nsight Systems.
2026-06-12 12:33:26.328083: E external/local_xla/xla/stream_executor/cuda/cuda_timer.cc:86] Delay kernel timed out: measured time has sub-optimal accuracy. There may be a missing warmup execution, please investigate in Nsight Systems.
2026-06-12 12:33:27.251305: E external/local_xla/xla/stream_executor/cuda/cuda_timer.cc:86] Delay kernel timed out: measured time has sub-optimal accuracy. There may be a missing warmup execution, please investigate in Nsight Systems.
2026-06-12 12:33:27.408319: E external/local_xla/xla/stream_executor/cuda/cuda_timer.cc:86] Delay kernel timed out: measured time has sub-optimal accuracy. There may be a missing warmup execution, please investigate in Nsight Systems.
2026-06-12 12:33:27.605267: E external/local_xla/xla/stream_executor/cuda/cuda_timer.cc:86] Delay kernel timed out: measured time has sub-optimal accuracy. There may be a missing warmup execution, please investigate in Nsight Systems.
2026-06-12 12:33:27.763818: E external/local_xla/xla/stream_executor/cuda/cuda_timer.cc:86] Delay kernel timed out: measured time has sub-optimal accuracy. There may be a missing warmup execution, please investigate in Nsight Systems.
Epoch 1: val_accuracy improved from None to 0.97878, saving model to /home/asephs/ZeaVis-Edu/Machine_Learning/best_model/best_model.keras

Epoch 1: finished saving model to /home/asephs/ZeaVis-Edu/Machine_Learning/best_model/best_model.keras
111/111 ━━━━━━━━━━━━━━━━━━━━ 81s 408ms/step - accuracy: 0.6441 - loss: 1.3479 - val_accuracy: 0.9788 - val_loss: 0.8152
Epoch 2/25
110/111 ━━━━━━━━━━━━━━━━━━━━ 0s 68ms/step - accuracy: 0.8178 - loss: 0.9879
Epoch 2: val_accuracy did not improve from 0.97878
111/111 ━━━━━━━━━━━━━━━━━━━━ 11s 90ms/step - accuracy: 0.8244 - loss: 0.9790 - val_accuracy: 0.9708 - val_loss: 0.6813
Epoch 3/25
111/111 ━━━━━━━━━━━━━━━━━━━━ 0s 70ms/step - accuracy: 0.8373 - loss: 0.9739
Epoch 3: val_accuracy did not improve from 0.97878
111/111 ━━━━━━━━━━━━━━━━━━━━ 11s 93ms/step - accuracy: 0.8500 - loss: 0.8797 - val_accuracy: 0.8117 - val_loss: 1.0506
Epoch 4/25
110/111 ━━━━━━━━━━━━━━━━━━━━ 0s 65ms/step - accuracy: 0.8970 - loss: 0.7573
Epoch 4: val_accuracy did not improve from 0.97878
111/111 ━━━━━━━━━━━━━━━━━━━━ 10s 86ms/step - accuracy: 0.9036 - loss: 0.7360 - val_accuracy: 0.9721 - val_loss: 0.8442
Epoch 5/25
110/111 ━━━━━━━━━━━━━━━━━━━━ 0s 65ms/step - accuracy: 0.9332 - loss: 0.7002
Epoch 5: val_accuracy improved from 0.97878 to 0.98276, saving model to /home/asephs/ZeaVis-Edu/Machine_Learning/best_model/best_model.keras

Epoch 5: finished saving model to /home/asephs/ZeaVis-Edu/Machine_Learning/best_model/best_model.keras
111/111 ━━━━━━━━━━━━━━━━━━━━ 11s 95ms/step - accuracy: 0.9280 - loss: 0.7023 - val_accuracy: 0.9828 - val_loss: 0.6751
Epoch 6/25
110/111 ━━━━━━━━━━━━━━━━━━━━ 0s 72ms/step - accuracy: 0.9209 - loss: 0.7071
Epoch 6: val_accuracy improved from 0.98276 to 0.98408, saving model to /home/asephs/ZeaVis-Edu/Machine_Learning/best_model/best_model.keras

Epoch 6: finished saving model to /home/asephs/ZeaVis-Edu/Machine_Learning/best_model/best_model.keras
111/111 ━━━━━━━━━━━━━━━━━━━━ 12s 101ms/step - accuracy: 0.9265 - loss: 0.6957 - val_accuracy: 0.9841 - val_loss: 0.5698
Epoch 7/25
109/111 ━━━━━━━━━━━━━━━━━━━━ 0s 68ms/step - accuracy: 0.9356 - loss: 0.6881
Epoch 7: val_accuracy did not improve from 0.98408
111/111 ━━━━━━━━━━━━━━━━━━━━ 11s 89ms/step - accuracy: 0.9348 - loss: 0.6805 - val_accuracy: 0.9828 - val_loss: 0.5298
Epoch 8/25
110/111 ━━━━━━━━━━━━━━━━━━━━ 0s 71ms/step - accuracy: 0.9371 - loss: 0.6789
Epoch 8: val_accuracy improved from 0.98408 to 0.98674, saving model to /home/asephs/ZeaVis-Edu/Machine_Learning/best_model/best_model.keras

Epoch 8: finished saving model to /home/asephs/ZeaVis-Edu/Machine_Learning/best_model/best_model.keras
111/111 ━━━━━━━━━━━━━━━━━━━━ 12s 101ms/step - accuracy: 0.9353 - loss: 0.6684 - val_accuracy: 0.9867 - val_loss: 0.5361
Epoch 9/25
110/111 ━━━━━━━━━━━━━━━━━━━━ 0s 66ms/step - accuracy: 0.9341 - loss: 0.6779
Epoch 9: val_accuracy did not improve from 0.98674
111/111 ━━━━━━━━━━━━━━━━━━━━ 11s 88ms/step - accuracy: 0.9368 - loss: 0.6702 - val_accuracy: 0.9841 - val_loss: 0.5234
Epoch 10/25
109/111 ━━━━━━━━━━━━━━━━━━━━ 0s 65ms/step - accuracy: 0.9354 - loss: 0.6747
Epoch 10: val_accuracy did not improve from 0.98674
111/111 ━━━━━━━━━━━━━━━━━━━━ 11s 89ms/step - accuracy: 0.9365 - loss: 0.6633 - val_accuracy: 0.9854 - val_loss: 0.5287
Epoch 11/25
110/111 ━━━━━━━━━━━━━━━━━━━━ 0s 72ms/step - accuracy: 0.9403 - loss: 0.6678
Epoch 11: val_accuracy did not improve from 0.98674
111/111 ━━━━━━━━━━━━━━━━━━━━ 21s 93ms/step - accuracy: 0.9416 - loss: 0.6506 - val_accuracy: 0.9841 - val_loss: 0.5265
Epoch 12/25
109/111 ━━━━━━━━━━━━━━━━━━━━ 0s 66ms/step - accuracy: 0.9504 - loss: 0.6546
Epoch 12: val_accuracy did not improve from 0.98674
111/111 ━━━━━━━━━━━━━━━━━━━━ 11s 88ms/step - accuracy: 0.9453 - loss: 0.6522 - val_accuracy: 0.9841 - val_loss: 0.5252
Epoch 13/25
111/111 ━━━━━━━━━━━━━━━━━━━━ 0s 67ms/step - accuracy: 0.9450 - loss: 0.6553
Epoch 13: val_accuracy did not improve from 0.98674
111/111 ━━━━━━━━━━━━━━━━━━━━ 11s 91ms/step - accuracy: 0.9453 - loss: 0.6504 - val_accuracy: 0.9841 - val_loss: 0.5199
Epoch 14/25
110/111 ━━━━━━━━━━━━━━━━━━━━ 0s 72ms/step - accuracy: 0.9457 - loss: 0.6507
Epoch 14: val_accuracy did not improve from 0.98674
111/111 ━━━━━━━━━━━━━━━━━━━━ 11s 93ms/step - accuracy: 0.9490 - loss: 0.6439 - val_accuracy: 0.9828 - val_loss: 0.5272
Epoch 15/25
110/111 ━━━━━━━━━━━━━━━━━━━━ 0s 66ms/step - accuracy: 0.9433 - loss: 0.6479
Epoch 15: val_accuracy did not improve from 0.98674
111/111 ━━━━━━━━━━━━━━━━━━━━ 10s 86ms/step - accuracy: 0.9475 - loss: 0.6402 - val_accuracy: 0.9814 - val_loss: 0.5200
Epoch 16/25
110/111 ━━━━━━━━━━━━━━━━━━━━ 0s 73ms/step - accuracy: 0.9415 - loss: 0.6532
Epoch 16: val_accuracy improved from 0.98674 to 0.99072, saving model to /home/asephs/ZeaVis-Edu/Machine_Learning/best_model/best_model.keras

Epoch 16: finished saving model to /home/asephs/ZeaVis-Edu/Machine_Learning/best_model/best_model.keras
111/111 ━━━━━━━━━━━━━━━━━━━━ 12s 103ms/step - accuracy: 0.9430 - loss: 0.6428 - val_accuracy: 0.9907 - val_loss: 0.5198
Epoch 17/25
110/111 ━━━━━━━━━━━━━━━━━━━━ 0s 67ms/step - accuracy: 0.9492 - loss: 0.6474
Epoch 17: val_accuracy did not improve from 0.99072
111/111 ━━━━━━━━━━━━━━━━━━━━ 11s 90ms/step - accuracy: 0.9475 - loss: 0.6354 - val_accuracy: 0.9867 - val_loss: 0.5156
Epoch 18/25
109/111 ━━━━━━━━━━━━━━━━━━━━ 0s 68ms/step - accuracy: 0.9511 - loss: 0.6526
Epoch 18: val_accuracy did not improve from 0.99072
111/111 ━━━━━━━━━━━━━━━━━━━━ 11s 90ms/step - accuracy: 0.9484 - loss: 0.6418 - val_accuracy: 0.9867 - val_loss: 0.5148
Epoch 19/25
110/111 ━━━━━━━━━━━━━━━━━━━━ 0s 72ms/step - accuracy: 0.9561 - loss: 0.6386
Epoch 19: val_accuracy did not improve from 0.99072
111/111 ━━━━━━━━━━━━━━━━━━━━ 11s 95ms/step - accuracy: 0.9484 - loss: 0.6329 - val_accuracy: 0.9841 - val_loss: 0.5144
Epoch 20/25
110/111 ━━━━━━━━━━━━━━━━━━━━ 0s 72ms/step - accuracy: 0.9435 - loss: 0.6381
Epoch 20: val_accuracy did not improve from 0.99072
111/111 ━━━━━━━━━━━━━━━━━━━━ 11s 92ms/step - accuracy: 0.9509 - loss: 0.6289 - val_accuracy: 0.9881 - val_loss: 0.5118
Epoch 21/25
110/111 ━━━━━━━━━━━━━━━━━━━━ 0s 66ms/step - accuracy: 0.9466 - loss: 0.6283
Epoch 21: val_accuracy did not improve from 0.99072
111/111 ━━━━━━━━━━━━━━━━━━━━ 11s 89ms/step - accuracy: 0.9521 - loss: 0.6245 - val_accuracy: 0.9881 - val_loss: 0.5149
Epoch 22/25
110/111 ━━━━━━━━━━━━━━━━━━━━ 0s 72ms/step - accuracy: 0.9489 - loss: 0.6547
Epoch 22: val_accuracy did not improve from 0.99072
111/111 ━━━━━━━━━━━━━━━━━━━━ 11s 94ms/step - accuracy: 0.9524 - loss: 0.6341 - val_accuracy: 0.9881 - val_loss: 0.5114
Epoch 23/25
109/111 ━━━━━━━━━━━━━━━━━━━━ 0s 61ms/step - accuracy: 0.9465 - loss: 0.6479
Epoch 23: val_accuracy did not improve from 0.99072
111/111 ━━━━━━━━━━━━━━━━━━━━ 19s 84ms/step - accuracy: 0.9492 - loss: 0.6348 - val_accuracy: 0.9881 - val_loss: 0.5111
Epoch 24/25
109/111 ━━━━━━━━━━━━━━━━━━━━ 0s 71ms/step - accuracy: 0.9541 - loss: 0.6407
Epoch 24: val_accuracy did not improve from 0.99072
111/111 ━━━━━━━━━━━━━━━━━━━━ 11s 92ms/step - accuracy: 0.9526 - loss: 0.6264 - val_accuracy: 0.9881 - val_loss: 0.5109
Epoch 25/25
110/111 ━━━━━━━━━━━━━━━━━━━━ 0s 66ms/step - accuracy: 0.9535 - loss: 0.6325
Epoch 25: val_accuracy did not improve from 0.99072
111/111 ━━━━━━━━━━━━━━━━━━━━ 11s 89ms/step - accuracy: 0.9546 - loss: 0.6232 - val_accuracy: 0.9881 - val_loss: 0.5111

10. Fase 2 — Partial Fine-tuning (192×192)

Resolusi naik ke 192×192. Top 100 layer EfficientNetV2B0 di-unfreeze. Learning rate lebih rendah: peak=5e-4, cosine decay ke 1e-6.

In [ ]:
IMG_192 = (192, 192)

train_ds_192 = tf.keras.utils.image_dataset_from_directory(
    train_dir, shuffle=True, batch_size=BATCH_SIZE, image_size=IMG_192)
val_ds_192 = tf.keras.utils.image_dataset_from_directory(
    val_dir, shuffle=True, batch_size=BATCH_SIZE, image_size=IMG_192)

train_ds_192 = (train_ds_192.map(augment_and_mix, num_parallel_calls=AUTOTUNE)
    .map(preprocess_fn, num_parallel_calls=AUTOTUNE)
    .map(add_sample_weight, num_parallel_calls=AUTOTUNE).prefetch(AUTOTUNE))
val_ds_192 = (val_ds_192.map(lambda img, lbl: (preprocess_input(tf.cast(img, tf.float32)), lbl), num_parallel_calls=AUTOTUNE)
    .map(lambda img, lbl: (img, tf.one_hot(lbl, NUM_CLASSES)), num_parallel_calls=AUTOTUNE).prefetch(AUTOTUNE))

# Unfreeze top 100 layers
base_model.trainable = True
for layer in base_model.layers[:-100]:
    layer.trainable = False

EPOCHS_P2 = 30
steps_p2 = tf.data.experimental.cardinality(train_ds_192).numpy() or 100
total_p2 = steps_p2 * EPOCHS_P2
warmup_p2 = steps_p2 * 2

lr_schedule_p2 = WarmupCosineDecay(warmup_p2, total_p2, peak_lr=5e-4, min_lr=1e-6)

model.compile(
    optimizer=AdamW(
                    learning_rate=lr_schedule_p2, weight_decay=1e-4),
    loss=tf.keras.losses.CategoricalCrossentropy(from_logits=True, label_smoothing=0.15),
    metrics=['accuracy']
)

print("Phase 2: Fine-tuning top 100 layers at 192×192...")
history_2 = model.fit(train_ds_192, validation_data=val_ds_192,
    epochs=EPOCHS_P1 + EPOCHS_P2, initial_epoch=history_1.epoch[-1] + 1,
    callbacks=make_callbacks())
Found 3526 files belonging to 4 classes.
Found 754 files belonging to 4 classes.
Phase 2: Fine-tuning top 100 layers at 192×192...
Epoch 26/55
2026-06-12 12:38:45.894387: I external/local_xla/xla/stream_executor/cuda/subprocess_compilation.cc:346] ptxas warning : Registers are spilled to local memory in function 'gemm_fusion_dot_20707', 380 bytes spill stores, 380 bytes spill loads

2026-06-12 12:38:45.954639: I external/local_xla/xla/stream_executor/cuda/subprocess_compilation.cc:346] ptxas warning : Registers are spilled to local memory in function 'gemm_fusion_dot_20707', 532 bytes spill stores, 532 bytes spill loads

2026-06-12 12:38:47.439608: E external/local_xla/xla/stream_executor/cuda/cuda_timer.cc:86] Delay kernel timed out: measured time has sub-optimal accuracy. There may be a missing warmup execution, please investigate in Nsight Systems.
2026-06-12 12:38:47.598315: E external/local_xla/xla/stream_executor/cuda/cuda_timer.cc:86] Delay kernel timed out: measured time has sub-optimal accuracy. There may be a missing warmup execution, please investigate in Nsight Systems.

11. Fase 3 — Full Fine-tuning (224×224)

Resolusi penuh 224×224. Semua layer di-unfreeze. LR sangat rendah: peak=1e-4, cosine decay ke 1e-7. Label smoothing diturunkan ke 0.10 untuk kalibrasi lebih baik.

In [ ]:
img_size = IMG_SIZE  # (224, 224)

train_ds_full = tf.keras.utils.image_dataset_from_directory(
    train_dir, shuffle=True, batch_size=BATCH_SIZE, image_size=img_size)
val_ds_full = tf.keras.utils.image_dataset_from_directory(
    val_dir, shuffle=True, batch_size=BATCH_SIZE, image_size=img_size)

train_ds_full = (train_ds_full.map(augment_and_mix, num_parallel_calls=AUTOTUNE)
    .map(preprocess_fn, num_parallel_calls=AUTOTUNE)
    .map(add_sample_weight, num_parallel_calls=AUTOTUNE).prefetch(AUTOTUNE))
val_ds_full = (val_ds_full.map(lambda img, lbl: (preprocess_input(tf.cast(img, tf.float32)), lbl), num_parallel_calls=AUTOTUNE)
    .map(lambda img, lbl: (img, tf.one_hot(lbl, NUM_CLASSES)), num_parallel_calls=AUTOTUNE).prefetch(AUTOTUNE))

# Full unfreeze
for layer in base_model.layers:
    layer.trainable = True

EPOCHS_P3 = 30
steps_p3 = tf.data.experimental.cardinality(train_ds_full).numpy() or 100
total_p3 = steps_p3 * EPOCHS_P3
warmup_p3 = steps_p3 * 2

lr_schedule_p3 = WarmupCosineDecay(warmup_p3, total_p3, peak_lr=1e-4, min_lr=1e-7)

model.compile(
    optimizer=AdamW(
                    learning_rate=lr_schedule_p3, weight_decay=1e-4),
    loss=tf.keras.losses.CategoricalCrossentropy(from_logits=True, label_smoothing=0.10),
    metrics=['accuracy']
)

print("Phase 3: Full fine-tuning at 224×224...")
history_3 = model.fit(train_ds_full, validation_data=val_ds_full,
    epochs=EPOCHS_P1 + EPOCHS_P2 + EPOCHS_P3, initial_epoch=(history_2.epoch[-1] + 1) if history_2.epoch else EPOCHS_P1 + EPOCHS_P2,
    callbacks=make_callbacks())

12. SWA — Stochastic Weight Averaging

15 epoch tambahan dengan cyclic LR (1e-5). SWA mengakumulasi rata-rata bobot untuk menghasilkan wider optima — generalisasi lebih baik ke data out-of-distribution.

Setelah SWA selesai, bobot SWA diterapkan kembali ke model.

In [ ]:
EPOCHS_SWA = 15
swa_start_epoch = (history_3.epoch[-1] + 1) if history_3.epoch else EPOCHS_P1 + EPOCHS_P2 + EPOCHS_P3

swa_cb = SWACallback(start_epoch=swa_start_epoch, swa_lr=1e-5)

model.compile(
    optimizer=AdamW(
                    learning_rate=1e-5, weight_decay=1e-4),
    loss=tf.keras.losses.CategoricalCrossentropy(from_logits=True, label_smoothing=0.10),
    metrics=['accuracy']
)

print(f"SWA: {EPOCHS_SWA} epochs starting at epoch {swa_start_epoch + 1}...")
history_swa = model.fit(train_ds_full, validation_data=val_ds_full,
    epochs=swa_start_epoch + EPOCHS_SWA, initial_epoch=swa_start_epoch,
    callbacks=make_callbacks() + [swa_cb])

# Apply SWA weights
swa_cb.apply_swa_weights()
print(f"SWA complete. Final model has SWA weights applied.")

13. Plot Training History (Gabungan Semua Fase)

In [ ]:
# Gabungkan semua history
acc = (history_1.history['accuracy'] + history_2.history['accuracy'] +
       history_3.history['accuracy'] + history_swa.history['accuracy'])
val_acc = (history_1.history['val_accuracy'] + history_2.history['val_accuracy'] +
           history_3.history['val_accuracy'] + history_swa.history['val_accuracy'])
loss = (history_1.history['loss'] + history_2.history['loss'] +
        history_3.history['loss'] + history_swa.history['loss'])
val_loss = (history_1.history['val_loss'] + history_2.history['val_loss'] +
            history_3.history['val_loss'] + history_swa.history['val_loss'])

b1 = len(history_1.history['accuracy']) - 1
b2 = b1 + len(history_2.history['accuracy'])
b3 = b2 + len(history_3.history['accuracy'])

plt.figure(figsize=(16, 6))
plt.subplot(1, 2, 1)
plt.plot(acc, label='Training Accuracy', linewidth=2)
plt.plot(val_acc, label='Validation Accuracy', linewidth=2)
plt.axvline(x=b1, color='gray', linestyle='--', alpha=0.7, label='P2 (192)')
plt.axvline(x=b2, color='black', linestyle='--', alpha=0.7, label='P3 (224)')
plt.axvline(x=b3, color='blue', linestyle='--', alpha=0.7, label='SWA start')
plt.legend(fontsize=10)
plt.title('Training & Validation Accuracy', fontsize=14)
plt.xlabel('Epoch'); plt.ylabel('Accuracy'); plt.grid(alpha=0.3)

plt.subplot(1, 2, 2)
plt.plot(loss, label='Training Loss', linewidth=2)
plt.plot(val_loss, label='Validation Loss', linewidth=2)
plt.axvline(x=b1, color='gray', linestyle='--', alpha=0.7, label='P2 (192)')
plt.axvline(x=b2, color='black', linestyle='--', alpha=0.7, label='P3 (224)')
plt.axvline(x=b3, color='blue', linestyle='--', alpha=0.7, label='SWA start')
plt.legend(fontsize=10)
plt.title('Training & Validation Loss', fontsize=14)
plt.xlabel('Epoch'); plt.ylabel('Loss'); plt.grid(alpha=0.3)

plt.tight_layout()
plt.show()

14. Temperature Scaling — Confidence Calibration

Model deep learning cenderung overconfident — softmax probability tinggi tapi tidak mencerminkan akurasi sebenarnya. Temperature scaling mengoptimalkan parameter T pada validation set:

P_{calibrated} = softmax(logits / T)

T > 1 → distribusi lebih flat (less confident). T < 1 → distribusi lebih tajam (more confident). T = 1 → tidak berubah (default).

ECE (Expected Calibration Error) mengukur seberapa baik confidence sesuai dengan akurasi. Target: ECE < 0.05 setelah temperature scaling.

In [ ]:
def compute_ece(probs, true_labels, n_bins=15):
    # Expected Calibration Error.
    confs = np.max(probs, axis=1)
    preds = np.argmax(probs, axis=1)
    true = np.argmax(true_labels, axis=1)
    accs = (preds == true).astype(np.float32)
    bins = np.linspace(0, 1, n_bins + 1)
    ece = 0.0
    bin_stats = []
    for i in range(n_bins):
        in_bin = (confs > bins[i]) & (confs <= bins[i + 1])
        n = np.sum(in_bin)
        if n > 0:
            bin_acc = np.mean(accs[in_bin])
            bin_conf = np.mean(confs[in_bin])
            ece += (n / len(confs)) * np.abs(bin_acc - bin_conf)
            bin_stats.append((bins[i], n, bin_acc, bin_conf))
    return ece, bin_stats

# Collect logits and labels from validation set
print("Collecting validation logits...")
logits_model = tf.keras.Model(model.input, model.output)

all_logits = []
all_labels = []
for images, labels in val_ds_full.unbatch().batch(BATCH_SIZE):
    all_logits.append(logits_model.predict_on_batch(images))
    all_labels.append(labels.numpy())

all_logits = np.concatenate(all_logits, axis=0)
all_labels = np.concatenate(all_labels, axis=0)

# ECE before scaling (T=1)
probs_raw = tf.nn.softmax(all_logits).numpy()
ece_raw, _ = compute_ece(probs_raw, all_labels)
print(f"ECE before scaling (T=1.0): {ece_raw:.4f}")

# Optimize T on validation set
if HAS_SCIPY:
    def nll_temperature(T):
        scaled = all_logits / float(T)
        probs = tf.nn.softmax(scaled).numpy()
        probs = np.clip(probs, 1e-7, 1.0 - 1e-7)
        return -np.mean(np.log(np.sum(all_labels * probs, axis=1)))

    result = minimize_scalar(nll_temperature, bounds=(0.1, 5.0), method='bounded')
    T_opt = result.x
    print(f"Optimal temperature: T = {T_opt:.4f}")
else:
    # Grid search fallback
    best_nll, T_opt = float('inf'), 1.0
    for T in np.linspace(0.5, 4.0, 36):
        scaled = all_logits / T
        probs = tf.nn.softmax(scaled).numpy()
        probs = np.clip(probs, 1e-7, 1.0 - 1e-7)
        nll = -np.mean(np.log(np.sum(all_labels * probs, axis=1)))
        if nll < best_nll:
            best_nll = nll
            T_opt = T
    print(f"Optimal temperature (grid): T = {T_opt:.4f}")

# ECE after scaling
probs_cal = tf.nn.softmax(all_logits / T_opt).numpy()
ece_cal, bin_stats = compute_ece(probs_cal, all_labels)
print(f"ECE after scaling (T={T_opt:.4f}): {ece_cal:.4f}")

# Save calibration metadata
calibration_meta = {
    "temperature": float(T_opt),
    "conf_threshold_high": 0.70,
    "conf_threshold_low": 0.45,
    "ece_raw": float(ece_raw),
    "ece_calibrated": float(ece_cal),
}
with open(os.path.join(ckpt_dir, "calibration.json"), "w") as f:
    json.dump(calibration_meta, f, indent=2)
print(f"Calibration metadata saved to {os.path.join(ckpt_dir, 'calibration.json')}")

# Reliability diagram
plt.figure(figsize=(12, 5))

plt.subplot(1, 2, 1)
if bin_stats:
    bin_mids = [(s[0] + s[0] + 1/n_bins)/2 for s in bin_stats]
    bin_accs = [s[2] for s in bin_stats]
    bin_confs = [s[3] for s in bin_stats]
    plt.bar(bin_mids, bin_accs, width=0.05, alpha=0.5, label='Accuracy')
    plt.bar(bin_mids, bin_confs, width=0.05, alpha=0.3, label='Confidence')
plt.plot([0, 1], [0, 1], 'k--', alpha=0.3)
plt.xlabel('Confidence'); plt.ylabel('Accuracy')
plt.title(f'Reliability Diagram (T={T_opt:.2f})')
plt.legend(); plt.grid(alpha=0.3)

plt.subplot(1, 2, 2)
conf_raw = np.max(probs_raw, axis=1)
conf_cal = np.max(probs_cal, axis=1)
plt.hist(conf_raw, bins=30, alpha=0.5, label='Before scaling', density=True)
plt.hist(conf_cal, bins=30, alpha=0.5, label='After scaling', density=True)
plt.xlabel('Max Confidence'); plt.ylabel('Density')
plt.title('Confidence Distribution')
plt.legend(); plt.grid(alpha=0.3)

plt.tight_layout()
plt.show()

15. Test Time Augmentation + Evaluasi

Menggunakan TTA 5× pada test set dengan augmented logit averaging. Model output adalah raw logits → temperature scaling → softmax.

In [ ]:
# Load best model (dengan SWA weights)
print(f"Loading best model from {checkpoint_path}...")
best_model = tf.keras.models.load_model(checkpoint_path, compile=False)

# Collect test images
raw_test_ds = tf.keras.utils.image_dataset_from_directory(
    test_dir, shuffle=False, batch_size=BATCH_SIZE, image_size=IMG_SIZE)

test_images = []
test_labels_raw = []
for images, labels in raw_test_ds.unbatch():
    test_images.append(images.numpy())
    test_labels_raw.append(labels.numpy())

test_images = np.array(test_images)
test_labels_true = tf.one_hot(np.array(test_labels_raw), NUM_CLASSES).numpy()

# ── TTA 5x ──
TTA_STEPS = 5
tta_logits = []

for i in range(TTA_STEPS):
    aug_images = geo_aug(test_images, training=True)
    aug_images = preprocess_input(aug_images)
    logits = best_model.predict(aug_images, batch_size=BATCH_SIZE, verbose=0)
    tta_logits.append(logits)
    print(f"  TTA step {i+1}/{TTA_STEPS}")

mean_logits = np.mean(tta_logits, axis=0)
# Apply temperature scaling
mean_cal_probs = tf.nn.softmax(mean_logits / T_opt).numpy()

test_preds = np.argmax(mean_cal_probs, axis=1)
test_true = np.argmax(test_labels_true, axis=1)
tta_acc = np.mean(test_preds == test_true)

print(f"\nTest Accuracy (TTA {TTA_STEPS}x, T={T_opt:.2f}): {tta_acc*100:.2f}%")

# ECE on test set
ece_test, _ = compute_ece(mean_cal_probs, test_labels_true)
print(f"ECE on test set: {ece_test:.4f}")

16. Per-Source Accuracy Breakdown

Mengukur akurasi per source prefix untuk mendeteksi domain gap. Source yang akurasinya collapse (< 70%) menunjukkan model belum robust.

In [ ]:
def get_source(filename):
    f = os.path.splitext(filename)[0]
    if f.startswith('IMG_'): return 'Phone'
    if f.startswith('Corn_'): return 'Lab_Corn'
    for p in ['CBS', 'GLS', 'NLS', 'CLS']:
        if f.startswith(p): return 'Lab_Disease'
    for p in ['SCR', 'CR', 'NLB', 'SLB', 'SRS']:
        if f.startswith(p): return 'Lab_RustBlight'
    return 'Other'

# Map each test image to its source
test_files = []
for cn in class_names:
    cp = os.path.join(test_dir, cn)
    if os.path.isdir(cp):
        test_files.extend([(f, cn, get_source(f)) for f in sorted(os.listdir(cp))])

sources = set(s for _, _, s in test_files)
print(f"Sources found: {sorted(sources)}")
print(f"{'Source':<18} {'Count':>6} {'Accuracy':>10}")
print("-" * 38)

for src in sorted(sources):
    indices = [i for i, (_, _, s) in enumerate(test_files) if s == src]
    if not indices: continue
    n = len(indices)
    acc = np.mean(test_preds[indices] == test_true[indices])
    print(f"{src:<18} {n:>6} {acc*100:>9.1f}%")

# Overall with count
overall_acc = np.mean(test_preds == test_true)
print("-" * 38)
print(f"{'ALL':<18} {len(test_preds):>6} {overall_acc*100:>9.1f}%")

17. Classification Report & Confusion Matrix

In [ ]:
print("\nClassification Report:\n")
print(classification_report(test_true, test_preds, target_names=class_names))

cm = confusion_matrix(test_true, test_preds)
plt.figure(figsize=(10, 8))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
            xticklabels=class_names, yticklabels=class_names)
plt.title(f'Confusion Matrix (TTA {TTA_STEPS}x)', fontsize=14)
plt.xlabel('Predicted', fontsize=12)
plt.ylabel('True', fontsize=12)
plt.tight_layout()
plt.show()

18. Simpan Model & Calibration Metadata

Menyimpan model final (dengan SWA weights) dan calibration metadata ke model/.

In [ ]:
# Save final model
model.save(final_path := os.path.join(ckpt_dir, 'best_model.keras'))
print(f"Model saved to {final_path}")

# Copy calibration metadata to model directory
model_export_dir = os.path.join(os.getcwd(), 'model')
os.makedirs(model_export_dir, exist_ok=True)

# Export labels.json with calibration metadata
cal_path = os.path.join(ckpt_dir, 'calibration.json')
if os.path.exists(cal_path):
    with open(cal_path) as f:
        cal_meta = json.load(f)

    labels_json = {
        "version": "3.0",
        "labels": class_names,
        "temperature": cal_meta["temperature"],
        "conf_threshold_high": cal_meta["conf_threshold_high"],
        "conf_threshold_low": cal_meta["conf_threshold_low"],
        "input_size": [224, 224],
        "input_range": [0, 255],
        "preprocessing": "resize_bilinear_224x224_no_normalization",
        "architecture": "EfficientNetV2B0 + CBAM + Dense(512)",
        "output_type": "logits",
    }
else:
    labels_json = {
        "version": "3.0",
        "labels": class_names,
        "temperature": 1.0,
        "input_size": [224, 224],
        "input_range": [0, 255],
        "output_type": "logits",
    }

with open(os.path.join(model_export_dir, 'labels.json'), 'w') as f:
    json.dump(labels_json, f, indent=2)
print("Labels + calibration metadata saved to model/labels.json")

# Export class names list (legacy)
with open(os.path.join(model_export_dir, 'labels.json'), 'r') as f:
    pass  # already written above
print(f"Classes: {class_names}")
print(f"Temperature: {labels_json['temperature']:.4f}")

19. Export Model untuk Produksi

Setelah training selesai, jalankan pipeline ekspor secara berurutan:

1. SavedModel + TFLite

python save_model.py

Memuat best_model/best_model.keras, membangun arsitektur bersih, dan mengekspor ke:

  • model/saved_model/ — format produksi (output raw logits)
  • model/model.tflite — untuk perangkat mobile/edge (INT8 quantization)

2. ONNX (Rust ML Service)

python convert_onnx.py

Mengonversi SavedModel ke model/model.onnx untuk Rust/Axum/ONNX Runtime. Output: raw logits (softmax + temperature scaling di Rust service).

3. TensorFlow.js (Web) — optional

export PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python
tensorflowjs_converter --input_format=tf_saved_model --output_format=tfjs_graph_model --signature_name=serving_default --saved_model_tags=serve model/saved_model model/tfjs_model

Catatan v3.0: Model output adalah raw logits (tanpa softmax). Rust service menerapkan temperature scaling: softmax(logits / T) dengan T dari labels.json. Status prediksi ditentukan dari confidence: confident (≥70%), uncertain (45-70%), rejected (<45%).

20. Model Card — ZeaVis Edu v3.0

Atribut Detail
Nama Model ZeaVis Edu v3.0 — CBAM + RandAugment Classifier
Versi 3.0
Arsitektur EfficientNetV2B0 + CBAM Attention + GAP + Dense(512)
Params ~6.6M (5.9M base + 0.7M head) — 2× lebih ringan dari v2.0
Framework TensorFlow 2.x / Keras (float32)
Output Raw logits → temperature scaling → softmax
Dataset ~6000 gambar (4 kelas) — stratified split by source
Kelas Bercak Daun, Daun Sehat, Hawar Daun, Karat Daun
Input RGB 224×224, pixel [0, 255], resize BILINEAR
Augmentasi RandAugment (15 ops, N=3) + Fog + Shadow + MixUp + CutMix + RandomErasing
Training Progressive 128→160→192→224 + CosineDecay + SWA
Calibration Temperature scaling (T optimized on val), ECE target < 0.05
Decision Confident (≥0.70), Uncertain (0.45-0.70), Rejected (<0.45)
Target ≥90% real-world accuracy, per-source accuracy ≥70% semua domain
Etika Hanya untuk edukasi/penelitian pertanian. Bukan pengganti diagnosis ahli.

21. Upload ke Hugging Face

from huggingface_hub import HfApi
api = HfApi()
api.upload_folder(
    folder_path="model",
    repo_id="zeavis-edu/corn-leaf-disease-classifier",
    repo_type="model",
)

Upload model SavedModel, TFLite, ONNX, TF.js, dan metadata ke HF Hub.