Build and Deploy / build (map[dockerfile:apps/api/Dockerfile name:api]) (push) Failing after 3m54s
Build and Deploy / build (map[dockerfile:apps/ml-service/Dockerfile name:ml]) (push) Failing after 41s
Build and Deploy / build (map[dockerfile:apps/web/Dockerfile name:web]) (push) Failing after 21s
Build and Deploy / deploy (push) Skipped
update code & comment
216 lines
6.7 KiB
Python
216 lines
6.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Validate parity between Keras and ONNX models on test images."""
|
|
|
|
import argparse
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import onnxruntime as ort
|
|
import tensorflow as tf
|
|
from PIL import Image, UnidentifiedImageError
|
|
|
|
# Definisi label kelas sesuai urutan output model klasifikasi
|
|
LABELS = ["Bercak Daun", "Daun Sehat", "Karat Daun", "Hawar Daun"]
|
|
|
|
# Kelas eksepsi kustom untuk menangani ketidaksesuaian akurasi prediksi
|
|
class ParityError(RuntimeError):
|
|
"""Raised when Keras and ONNX predictions do not match."""
|
|
pass
|
|
|
|
|
|
def preprocess_image(image_path, input_size):
|
|
"""
|
|
Load and preprocess an image for model inference.
|
|
|
|
Args:
|
|
image_path: Path to the image file.
|
|
input_size: Target size (int) for square resize.
|
|
|
|
Returns:
|
|
Preprocessed image as float32 NHWC batch (1, input_size, input_size, 3).
|
|
|
|
Raises:
|
|
ParityError: If image cannot be loaded or processed.
|
|
"""
|
|
# Penanganan error secara aman saat memuat gambar ke format RGB
|
|
try:
|
|
img = Image.open(image_path).convert("RGB")
|
|
except (FileNotFoundError, UnidentifiedImageError, OSError) as e:
|
|
raise ParityError(f"Failed to load image {image_path}: {e}")
|
|
|
|
# Penyesuaian resolusi gambar menggunakan metode interpolasi Bilinear
|
|
try:
|
|
img = img.resize((input_size, input_size), Image.Resampling.BILINEAR)
|
|
except Exception as e:
|
|
raise ParityError(f"Failed to resize image {image_path}: {e}")
|
|
|
|
# Konversi ke matriks float32 dan penambahan dimensi batch (1, H, W, C)
|
|
img_array = np.array(img, dtype=np.float32)
|
|
img_batch = np.expand_dims(img_array, axis=0)
|
|
|
|
return img_batch
|
|
|
|
|
|
def predict_keras(model, image_batch):
|
|
"""
|
|
Run inference on Keras model.
|
|
|
|
Args:
|
|
model: Loaded Keras model.
|
|
image_batch: Preprocessed image batch (1, H, W, 3).
|
|
|
|
Returns:
|
|
Predictions array (1, num_classes).
|
|
"""
|
|
# Eksekusi inferensi pada model TensorFlow/Keras tanpa log proses
|
|
predictions = model.predict(image_batch, verbose=0)
|
|
return predictions
|
|
|
|
|
|
def predict_onnx(session, image_batch):
|
|
"""
|
|
Run inference on ONNX model.
|
|
|
|
Args:
|
|
session: ONNX Runtime InferenceSession.
|
|
image_batch: Preprocessed image batch (1, H, W, 3).
|
|
|
|
Returns:
|
|
Predictions array (1, num_classes).
|
|
"""
|
|
# Eksekusi inferensi secara dinamis pada model ONNX menggunakan sesi runtime
|
|
input_name = session.get_inputs()[0].name
|
|
predictions = session.run(None, {input_name: image_batch})
|
|
return predictions[0]
|
|
|
|
|
|
def validate_image(image_path, keras_model, onnx_session, input_size, atol):
|
|
"""
|
|
Validate that Keras and ONNX predictions match for a single image.
|
|
|
|
Args:
|
|
image_path: Path to the test image.
|
|
keras_model: Loaded Keras model.
|
|
onnx_session: ONNX Runtime InferenceSession.
|
|
input_size: Input size for preprocessing.
|
|
atol: Absolute tolerance for np.allclose comparison.
|
|
|
|
Raises:
|
|
ParityError: If predictions do not match or image cannot be processed.
|
|
"""
|
|
# Menyiapkan tensor gambar untuk pengujian
|
|
img_batch = preprocess_image(image_path, input_size)
|
|
|
|
# Mengekstrak matriks probabilitas dari kedua format model
|
|
keras_pred = predict_keras(keras_model, img_batch)
|
|
onnx_pred = predict_onnx(onnx_session, img_batch)
|
|
|
|
# Mendapatkan indeks kelas dengan probabilitas tertinggi (Top-1)
|
|
keras_label_idx = np.argmax(keras_pred[0])
|
|
onnx_label_idx = np.argmax(onnx_pred[0])
|
|
|
|
# Validasi keselarasan keputusan klasifikasi utama
|
|
if keras_label_idx != onnx_label_idx:
|
|
keras_label = LABELS[keras_label_idx]
|
|
onnx_label = LABELS[onnx_label_idx]
|
|
raise ParityError(
|
|
f"Top-1 label mismatch for {image_path}: "
|
|
f"Keras={keras_label}, ONNX={onnx_label}"
|
|
)
|
|
|
|
# Validasi selisih nilai desimal probabilitas menggunakan toleransi absolut
|
|
if not np.allclose(keras_pred, onnx_pred, atol=atol):
|
|
max_diff = np.max(np.abs(keras_pred - onnx_pred))
|
|
raise ParityError(
|
|
f"Predictions diverge for {image_path}: "
|
|
f"max difference={max_diff:.6e} (atol={atol})"
|
|
)
|
|
|
|
# Pencatatan log sistem jika kedua model presisi 100%
|
|
label = LABELS[keras_label_idx]
|
|
logging.info(f"PASS: {image_path} -> {label}")
|
|
|
|
|
|
def main():
|
|
"""Validate parity between Keras and ONNX models."""
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
|
|
|
# Inisialisasi parser argumen untuk antarmuka CLI (Command Line Interface)
|
|
parser = argparse.ArgumentParser(
|
|
description="Validate parity between Keras and ONNX models"
|
|
)
|
|
parser.add_argument(
|
|
"images",
|
|
nargs="+",
|
|
type=Path,
|
|
help="Image files to validate",
|
|
)
|
|
parser.add_argument(
|
|
"--keras-model",
|
|
type=Path,
|
|
default=Path("best_model/best_model.keras"),
|
|
help="Path to the Keras model",
|
|
)
|
|
parser.add_argument(
|
|
"--onnx-model",
|
|
type=Path,
|
|
default=Path("model/model.onnx"),
|
|
help="Path to the ONNX model",
|
|
)
|
|
parser.add_argument(
|
|
"--input-size",
|
|
type=int,
|
|
default=224,
|
|
help="Input image size (square)",
|
|
)
|
|
parser.add_argument(
|
|
"--atol",
|
|
type=float,
|
|
default=1e-4,
|
|
help="Absolute tolerance for prediction comparison",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Pengecekan eksistensi berkas model sebelum memuat memori
|
|
if not args.keras_model.exists():
|
|
msg = f"Keras model not found at {args.keras_model}"
|
|
logging.error(msg)
|
|
raise FileNotFoundError(msg)
|
|
|
|
if not args.onnx_model.exists():
|
|
msg = f"ONNX model not found at {args.onnx_model}"
|
|
logging.error(msg)
|
|
raise FileNotFoundError(msg)
|
|
|
|
# Memuat model Keras (tanpa kompilasi agar lebih hemat beban komputasi)
|
|
logging.info(f"Loading Keras model from {args.keras_model}...")
|
|
keras_model = tf.keras.models.load_model(args.keras_model, compile=False)
|
|
|
|
# Memuat sesi ONNX dengan penyedia eksekusi CPU murni
|
|
logging.info(f"Loading ONNX model from {args.onnx_model}...")
|
|
onnx_session = ort.InferenceSession(
|
|
str(args.onnx_model),
|
|
providers=["CPUExecutionProvider"],
|
|
)
|
|
|
|
# Iterasi pengujian paritas (kesetaraan performa) untuk setiap gambar
|
|
logging.info(f"Validating {len(args.images)} image(s)...")
|
|
for image_path in args.images:
|
|
try:
|
|
validate_image(
|
|
image_path,
|
|
keras_model,
|
|
onnx_session,
|
|
args.input_size,
|
|
args.atol,
|
|
)
|
|
except ParityError as e:
|
|
logging.error(str(e))
|
|
raise
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|