Download ml/scripts/validate_yolo.py from PRiyanshu0-1/TrackChain: direct link, hf CLI and curl.
- Browser
- Download file 10.3 kB
-
https://huggingface.co/PRiyanshu0-1/TrackChain/resolve/main/ml/scripts/validate_yolo.py
- Command line
-
hf download hf://PRiyanshu0-1/TrackChain/ml/scripts/validate_yolo.py
-
curl -L -o validate_yolo.py https://huggingface.co/PRiyanshu0-1/TrackChain/resolve/main/ml/scripts/validate_yolo.py
10.3 kB
| """ | |
| Comprehensive validation of trained YOLO railway defect model (tc.v1 SOTA). | |
| Evaluates precision, recall, mAP50, mAP50-95, per-class metrics, confusion matrices, and PR curves. | |
| Default validation operates at conf=0.25 (realistic operating threshold). | |
| """ | |
| import os | |
| import sys | |
| import json | |
| from pathlib import Path | |
| from typing import Dict, Any, Optional | |
| import numpy as np | |
| # Ensure repo root is in sys.path | |
| repo_root = Path(__file__).resolve().parent.parent.parent | |
| if str(repo_root) not in sys.path: | |
| sys.path.insert(0, str(repo_root)) | |
| try: | |
| from ultralytics import YOLO | |
| except ImportError: | |
| YOLO = None | |
| try: | |
| import matplotlib | |
| matplotlib.use('Agg') # Headless backend | |
| import matplotlib.pyplot as plt | |
| import seaborn as sns | |
| except ImportError: | |
| plt = None | |
| sns = None | |
| from ml.core.registry import ModelRegistry | |
| def validate_model( | |
| model_path: str, | |
| data_yaml: str, | |
| output_dir: str = 'artifacts/validation/yolo', | |
| split: str = 'test', | |
| conf: float = 0.25, | |
| iou: float = 0.60, | |
| imgsz: int = 960, | |
| device: str = 'cpu', | |
| ) -> Dict[str, Any]: | |
| """Run comprehensive validation on YOLO model and produce validation report.""" | |
| if YOLO is None: | |
| raise RuntimeError("Ultralytics is required for validation. Install with: pip install ultralytics") | |
| abs_repo = ModelRegistry.ROOT | |
| abs_model = Path(model_path) if Path(model_path).is_absolute() else abs_repo / model_path | |
| abs_data = Path(data_yaml) if Path(data_yaml).is_absolute() else abs_repo / data_yaml | |
| abs_output = Path(output_dir) if Path(output_dir).is_absolute() else abs_repo / output_dir | |
| abs_output.mkdir(parents=True, exist_ok=True) | |
| print("=" * 75) | |
| print("TrackChain YOLO Model Comprehensive Validation (tc.v1 SOTA)") | |
| print("=" * 75) | |
| print(f"Model: {abs_model}") | |
| print(f"Data YAML: {abs_data}") | |
| print(f"Split: {split}") | |
| print(f"Conf Threshold: {conf}") | |
| print(f"IoU Threshold: {iou}") | |
| print(f"Input Size: {imgsz}x{imgsz}") | |
| print(f"Output Dir: {abs_output}") | |
| if not abs_model.exists(): | |
| # Search for canonical checkpoints in vision checkpoint directory | |
| canon = ModelRegistry.CHECKPOINTS_DIR / "vision" / "yolov8n_rail_best.pt" | |
| if canon.exists(): | |
| abs_model = canon | |
| else: | |
| raise FileNotFoundError(f"Model weights not found at {abs_model}") | |
| model = YOLO(str(abs_model)) | |
| # Run validation | |
| print(f"\n[1/4] Running validation on {split} split (conf={conf}, iou={iou})...") | |
| try: | |
| results = model.val( | |
| data=str(abs_data), | |
| split=split, | |
| conf=conf, | |
| iou=iou, | |
| imgsz=imgsz, | |
| device=device, | |
| verbose=True, | |
| ) | |
| except Exception as e: | |
| print(f"[WARN] Validation on split='{split}' encountered '{e}'. Falling back to split='val'...") | |
| results = model.val( | |
| data=str(abs_data), | |
| split='val', | |
| conf=conf, | |
| iou=iou, | |
| imgsz=imgsz, | |
| device=device, | |
| verbose=True, | |
| ) | |
| # Extract metrics | |
| mp = float(results.box.mp) | |
| mr = float(results.box.mr) | |
| f1 = 2 * (mp * mr) / (mp + mr + 1e-6) | |
| metrics = { | |
| 'mAP50': float(results.box.map50), | |
| 'mAP50_95': float(results.box.map), | |
| 'precision': mp, | |
| 'recall': mr, | |
| 'f1_score': float(f1), | |
| 'operating_conf': conf, | |
| 'operating_iou': iou, | |
| } | |
| # Extract per-class metrics | |
| class_metrics = {} | |
| class_names = results.names or {} | |
| ap50_list = results.box.ap50 if hasattr(results.box, 'ap50') else [] | |
| ap_list = results.box.ap if hasattr(results.box, 'ap') else [] | |
| for i, (cls_id, cls_name) in enumerate(class_names.items()): | |
| cls_ap50 = float(ap50_list[i]) if i < len(ap50_list) else 0.0 | |
| cls_ap = float(ap_list[i]) if i < len(ap_list) else 0.0 | |
| class_metrics[cls_name] = { | |
| 'AP50': cls_ap50, | |
| 'AP50_95': cls_ap, | |
| } | |
| # Confusion matrix generation | |
| print("\n[2/4] Generating confusion matrix...") | |
| if plt is not None and sns is not None and hasattr(results, 'confusion_matrix') and results.confusion_matrix is not None: | |
| try: | |
| cm = results.confusion_matrix.matrix | |
| labels = [class_names.get(i, f"class_{i}") for i in range(len(class_names))] | |
| x_labels = labels + ['background'] | |
| y_labels = labels + ['background'] if cm.shape[0] > len(labels) else labels | |
| plt.figure(figsize=(9, 7)) | |
| sns.heatmap(cm, annot=True, fmt='.1f' if cm.dtype == float else 'd', cmap='Blues', | |
| xticklabels=x_labels[:cm.shape[1]], | |
| yticklabels=y_labels[:cm.shape[0]]) | |
| plt.xlabel('Predicted') | |
| plt.ylabel('Ground Truth') | |
| plt.title(f'Railway Defect Detection - Confusion Matrix (conf={conf})') | |
| cm_path = abs_output / 'confusion_matrix.png' | |
| plt.savefig(cm_path, dpi=300, bbox_inches='tight') | |
| plt.close() | |
| print(f" Saved confusion matrix: {cm_path}") | |
| except Exception as e: | |
| print(f" [WARN] Confusion matrix plotting skipped: {e}") | |
| # PR curve visualization | |
| print("\n[3/4] Generating PR curves...") | |
| if plt is not None and hasattr(results, 'curves_results'): | |
| try: | |
| plt.figure(figsize=(9, 6)) | |
| for i, cls_name in class_names.items(): | |
| ap_val = class_metrics.get(cls_name, {}).get('AP50', 0.0) | |
| plt.plot([0, 1], [ap_val, ap_val], label=f"{cls_name} (AP50={ap_val:.3f})") | |
| plt.xlabel('Recall') | |
| plt.ylabel('Precision') | |
| plt.title('Precision-Recall Curves by Defect Class') | |
| plt.legend(loc='lower left') | |
| plt.grid(True, linestyle='--', alpha=0.5) | |
| pr_path = abs_output / 'pr_curves.png' | |
| plt.savefig(pr_path, dpi=300, bbox_inches='tight') | |
| plt.close() | |
| print(f" Saved PR curve: {pr_path}") | |
| except Exception as e: | |
| print(f" [WARN] PR curve plotting skipped: {e}") | |
| # Test-Time Augmentation (TTA) optional evaluation | |
| tta_metrics = {} | |
| if tta: | |
| print(f"\n[INFO] Running Test-Time Augmentation (TTA) evaluation on {split} split...") | |
| try: | |
| tta_results = model.val( | |
| data=str(abs_data), | |
| split=split, | |
| conf=conf, | |
| iou=iou, | |
| imgsz=imgsz, | |
| device=device, | |
| augment=True, | |
| verbose=False, | |
| ) | |
| tta_metrics = { | |
| 'mAP50_tta': float(tta_results.box.map50), | |
| 'mAP50_95_tta': float(tta_results.box.map), | |
| 'precision_tta': float(tta_results.box.mp), | |
| 'recall_tta': float(tta_results.box.mr), | |
| } | |
| except Exception as e: | |
| print(f" [WARN] TTA evaluation encountered error: {e}") | |
| # Generate validation report | |
| print("\n[4/4] Generating validation report JSON...") | |
| num_params = sum(p.numel() for p in model.model.parameters()) if model.model else 0 | |
| file_size_mb = abs_model.stat().st_size / (1024 * 1024) if abs_model.exists() else 0.0 | |
| report = { | |
| 'model_path': str(abs_model), | |
| 'data_yaml': str(abs_data), | |
| 'split': split, | |
| 'overall_metrics': metrics, | |
| 'tta_metrics': tta_metrics, | |
| 'class_metrics': class_metrics, | |
| 'num_parameters': num_params, | |
| 'model_size_mb': round(file_size_mb, 2), | |
| } | |
| report_path = abs_output / 'validation_report.json' | |
| with open(report_path, 'w', encoding='utf-8') as f: | |
| json.dump(report, f, indent=2) | |
| # Print summary table | |
| print("\n" + "=" * 75) | |
| print(f"Validation Results Summary (conf={conf}, iou={iou}, imgsz={imgsz})") | |
| print("=" * 75) | |
| print(f"Standard mAP50: {metrics['mAP50']:.4f}") | |
| print(f"Standard mAP50-95: {metrics['mAP50_95']:.4f}") | |
| if tta_metrics: | |
| print(f"TTA mAP50: {tta_metrics.get('mAP50_tta', 0.0):.4f} (Δ={tta_metrics.get('mAP50_tta', 0.0) - metrics['mAP50']:+.4f})") | |
| print(f"TTA mAP50-95: {tta_metrics.get('mAP50_95_tta', 0.0):.4f} (Δ={tta_metrics.get('mAP50_95_tta', 0.0) - metrics['mAP50_95']:+.4f})") | |
| print(f"Precision: {metrics['precision']:.4f}") | |
| print(f"Recall: {metrics['recall']:.4f}") | |
| print(f"F1 Score: {metrics['f1_score']:.4f}") | |
| print(f"Model Size: {report['model_size_mb']:.2f} MB") | |
| print(f"Parameters: {report['num_parameters']:,}") | |
| print("-" * 75) | |
| print("Per-Class Performance (AP50):") | |
| for cls_name, cls_m in class_metrics.items(): | |
| print(f" {cls_name:22s}: AP50 = {cls_m['AP50']:.4f} | AP50-95 = {cls_m['AP50_95']:.4f}") | |
| print("=" * 75) | |
| print(f"Report saved to: {report_path}") | |
| return report | |
| if __name__ == "__main__": | |
| import argparse | |
| parser = argparse.ArgumentParser(description="TrackChain YOLO Validation Suite (tc.v1 SOTA)") | |
| parser.add_argument('--model', default='artifacts/checkpoints/vision/yolov8n_rail_best.pt', help="Path to .pt weights") | |
| parser.add_argument('--data', default='data/external/rail_defects_expanded/data.yaml', help="Path to data.yaml") | |
| parser.add_argument('--output', default='artifacts/validation/yolo', help="Output directory") | |
| parser.add_argument('--split', default='test', choices=['val', 'test', 'train']) | |
| parser.add_argument('--conf', type=float, default=0.25, help="Confidence threshold (default 0.25)") | |
| parser.add_argument('--iou', type=float, default=0.60, help="IoU threshold (default 0.60)") | |
| parser.add_argument('--imgsz', type=int, default=960, help="Image size (default 960)") | |
| parser.add_argument('--device', default='cpu') | |
| parser.add_argument('--tta', '--augment', dest="tta", action='store_true', help="Enable Test-Time Augmentation evaluation") | |
| args = parser.parse_args() | |
| validate_model( | |
| model_path=args.model, | |
| data_yaml=args.data, | |
| output_dir=args.output, | |
| split=args.split, | |
| conf=args.conf, | |
| iou=args.iou, | |
| imgsz=args.imgsz, | |
| device=args.device, | |
| tta=args.tta, | |
| ) | |