import argparse from pathlib import Path from typing import Optional import matplotlib.pyplot as plt import numpy as np import seaborn as sns import torch import yaml from sklearn.metrics import classification_report, confusion_matrix from tqdm import tqdm def evaluate(config_path: str = "configs/config.yaml", checkpoint: str = "checkpoints/best.pt"): with open(config_path) as f: config = yaml.safe_load(f) device = "cuda" if torch.cuda.is_available() else "cpu" from src.dataset import build_dataloaders from src.model import build_model _, val_loader, classes = build_dataloaders(config) config["model"]["num_classes"] = len(classes) model = build_model(config).to(device) ckpt = torch.load(checkpoint, map_location=device, weights_only=False) model.load_state_dict(ckpt["model_state_dict"]) model.eval() all_preds, all_labels = [], [] with torch.no_grad(): for images, labels in tqdm(val_loader, desc="Evaluating"): preds = model(images.to(device)).argmax(1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) all_preds, all_labels = np.array(all_preds), np.array(all_labels) print(classification_report(all_labels, all_preds, target_names=classes, zero_division=0)) out_dir = Path(config["paths"]["outputs"]) out_dir.mkdir(parents=True, exist_ok=True) n = len(classes) cm = confusion_matrix(all_labels, all_preds) fig, ax = plt.subplots(figsize=(max(10, n // 2), max(8, n // 2))) sns.heatmap(cm, annot=n <= 30, fmt="d", xticklabels=classes, yticklabels=classes, ax=ax, cmap="Blues") ax.set_xlabel("Predicted") ax.set_ylabel("True") ax.set_title("Confusion Matrix") plt.tight_layout() cm_path = out_dir / "confusion_matrix.png" plt.savefig(cm_path, dpi=150) print(f"Confusion matrix → {cm_path}") plt.close() def gradcam_heatmap( model: torch.nn.Module, image_tensor: torch.Tensor, target_class: Optional[int] = None, ) -> np.ndarray: """ Grad-CAM heatmap for a single image tensor (1, C, H, W). Works for EfficientNet/ResNet backbones; ViT needs a different target layer. """ from pytorch_grad_cam import GradCAM from pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget children = list(model.backbone.children()) target_layers = [children[-1]] targets = [ClassifierOutputTarget(target_class)] if target_class is not None else None with GradCAM(model=model, target_layers=target_layers) as cam: return cam(input_tensor=image_tensor, targets=targets)[0] if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--config", default="configs/config.yaml") parser.add_argument("--checkpoint", default="checkpoints/best.pt") args = parser.parse_args() evaluate(args.config, args.checkpoint)