utils.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862
  1. """
  2. Utility classes for training progress monitoring, logging, and validation.
  3. This module provides comprehensive utilities for tracking training progress,
  4. professional logging, performance monitoring, and validation metrics.
  5. """
  6. import os
  7. import time
  8. import json
  9. import logging
  10. import threading
  11. from pathlib import Path
  12. from typing import Dict, List, Any, Optional, Callable, Union
  13. from dataclasses import dataclass, field
  14. from datetime import datetime, timedelta
  15. import sys
  16. import torch
  17. import torch.nn as nn
  18. import numpy as np
  19. from sklearn.metrics import (
  20. accuracy_score, precision_recall_fscore_support, confusion_matrix,
  21. classification_report, roc_auc_score, average_precision_score
  22. )
  23. try:
  24. import matplotlib.pyplot as plt
  25. import seaborn as sns
  26. PLOTTING_AVAILABLE = True
  27. except ImportError:
  28. PLOTTING_AVAILABLE = False
  29. try:
  30. from tensorboard import SummaryWriter
  31. TENSORBOARD_AVAILABLE = True
  32. except ImportError:
  33. TENSORBOARD_AVAILABLE = False
  34. SummaryWriter = None
  35. try:
  36. import wandb
  37. WANDB_AVAILABLE = True
  38. except ImportError:
  39. WANDB_AVAILABLE = False
  40. wandb = None
  41. def make_json_serializable(obj: Any) -> Any:
  42. """
  43. Convert numpy types and other non-JSON-serializable objects to JSON-safe types.
  44. Args:
  45. obj: Object to convert
  46. Returns:
  47. JSON-serializable object
  48. """
  49. # Handle numpy integer types
  50. if isinstance(obj, (np.integer,)):
  51. return int(obj)
  52. # Handle numpy floating point types
  53. elif isinstance(obj, (np.floating,)):
  54. return float(obj)
  55. # Handle numpy boolean types
  56. elif isinstance(obj, np.bool_):
  57. return bool(obj)
  58. elif isinstance(obj, np.ndarray):
  59. return obj.tolist()
  60. elif isinstance(obj, torch.Tensor):
  61. return obj.detach().cpu().numpy().tolist()
  62. elif isinstance(obj, dict):
  63. return {key: make_json_serializable(value) for key, value in obj.items()}
  64. elif isinstance(obj, (list, tuple)):
  65. return [make_json_serializable(item) for item in obj]
  66. elif hasattr(obj, '__dict__'):
  67. # Handle dataclass or custom objects
  68. if hasattr(obj, 'to_dict'):
  69. return make_json_serializable(obj.to_dict())
  70. else:
  71. return make_json_serializable(obj.__dict__)
  72. else:
  73. return obj
  74. class TrainerLogger:
  75. """
  76. Professional logging system for ML trainers.
  77. Provides structured logging with file output, console output,
  78. and optional integration with TensorBoard and Weights & Biases.
  79. """
  80. def __init__(self, name: str, log_dir: str = "./logs",
  81. log_level: str = "INFO",
  82. use_tensorboard: bool = True,
  83. use_wandb: bool = False,
  84. wandb_project: Optional[str] = None):
  85. """
  86. Initialize trainer logger.
  87. Args:
  88. name: Logger name
  89. log_dir: Directory for log files
  90. log_level: Logging level
  91. use_tensorboard: Whether to use TensorBoard logging
  92. use_wandb: Whether to use Weights & Biases logging
  93. wandb_project: W&B project name
  94. """
  95. self.name = name
  96. self.log_dir = Path(log_dir)
  97. self.log_dir.mkdir(parents=True, exist_ok=True)
  98. # Setup basic logger
  99. self.logger = self._setup_logger(log_level)
  100. # Setup TensorBoard
  101. self.tb_writer = None
  102. if use_tensorboard and TENSORBOARD_AVAILABLE:
  103. tb_log_dir = self.log_dir / "tensorboard" / name
  104. tb_log_dir.mkdir(parents=True, exist_ok=True)
  105. self.tb_writer = SummaryWriter(str(tb_log_dir))
  106. self.logger.info("TensorBoard logging enabled")
  107. elif use_tensorboard:
  108. self.logger.warning("TensorBoard not available - install with: pip install tensorboard")
  109. # Setup Weights & Biases
  110. self.use_wandb = False
  111. if use_wandb and WANDB_AVAILABLE:
  112. if wandb_project:
  113. wandb.init(project=wandb_project, name=name)
  114. self.use_wandb = True
  115. self.logger.info("Weights & Biases logging enabled")
  116. else:
  117. self.logger.warning("W&B project name required for W&B logging")
  118. elif use_wandb:
  119. self.logger.warning("Weights & Biases not available - install with: pip install wandb")
  120. # Training metrics storage
  121. self.metrics_history = []
  122. self.current_step = 0
  123. def _setup_logger(self, log_level: str) -> logging.Logger:
  124. """Setup the basic logger with file and console handlers."""
  125. logger = logging.getLogger(self.name)
  126. logger.setLevel(getattr(logging, log_level.upper()))
  127. # Clear existing handlers
  128. logger.handlers.clear()
  129. # Create formatters
  130. detailed_formatter = logging.Formatter(
  131. '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
  132. )
  133. simple_formatter = logging.Formatter(
  134. '%(asctime)s - %(levelname)s - %(message)s'
  135. )
  136. # File handler (detailed)
  137. log_file = self.log_dir / f"{self.name}.log"
  138. file_handler = logging.FileHandler(log_file)
  139. file_handler.setLevel(logging.DEBUG)
  140. file_handler.setFormatter(detailed_formatter)
  141. logger.addHandler(file_handler)
  142. # Console handler (simple)
  143. console_handler = logging.StreamHandler(sys.stdout)
  144. console_handler.setLevel(logging.INFO)
  145. console_handler.setFormatter(simple_formatter)
  146. logger.addHandler(console_handler)
  147. return logger
  148. def log_metrics(self, metrics: Dict[str, float], step: Optional[int] = None):
  149. """Log training metrics to all enabled loggers."""
  150. if step is None:
  151. step = self.current_step
  152. self.current_step += 1
  153. # Store metrics
  154. metric_entry = {"step": step, "timestamp": datetime.now().isoformat()}
  155. metric_entry.update(metrics)
  156. self.metrics_history.append(metric_entry)
  157. # Log to basic logger
  158. metric_str = ", ".join([f"{k}: {v:.6f}" for k, v in metrics.items()])
  159. self.logger.info(f"Step {step} - {metric_str}")
  160. # Log to TensorBoard
  161. if self.tb_writer:
  162. for key, value in metrics.items():
  163. self.tb_writer.add_scalar(key, value, step)
  164. self.tb_writer.flush()
  165. # Log to Weights & Biases
  166. if self.use_wandb:
  167. wandb.log(metrics, step=step)
  168. def log_model_graph(self, model: nn.Module, input_sample: torch.Tensor):
  169. """Log model graph to TensorBoard."""
  170. if self.tb_writer:
  171. try:
  172. self.tb_writer.add_graph(model, input_sample)
  173. self.logger.info("Model graph logged to TensorBoard")
  174. except Exception as e:
  175. self.logger.warning(f"Failed to log model graph: {str(e)}")
  176. def log_hyperparameters(self, hparams: Dict[str, Any]):
  177. """Log hyperparameters."""
  178. # Log to basic logger
  179. self.logger.info("Hyperparameters:")
  180. for key, value in hparams.items():
  181. self.logger.info(f" {key}: {value}")
  182. # Log to TensorBoard
  183. if self.tb_writer:
  184. # Convert non-scalar values to strings
  185. tb_hparams = {}
  186. for key, value in hparams.items():
  187. if isinstance(value, (int, float, str, bool)):
  188. tb_hparams[key] = value
  189. else:
  190. tb_hparams[key] = str(value)
  191. self.tb_writer.add_hparams(tb_hparams, {})
  192. # Log to Weights & Biases
  193. if self.use_wandb:
  194. wandb.config.update(hparams)
  195. def log_confusion_matrix(self, y_true: np.ndarray, y_pred: np.ndarray,
  196. class_names: List[str], step: int):
  197. """Log confusion matrix visualization."""
  198. cm = confusion_matrix(y_true, y_pred)
  199. if not PLOTTING_AVAILABLE:
  200. self.logger.warning("Matplotlib not available, skipping confusion matrix visualization")
  201. return
  202. # Create matplotlib figure
  203. fig, ax = plt.subplots(figsize=(10, 8))
  204. sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
  205. xticklabels=class_names, yticklabels=class_names, ax=ax)
  206. ax.set_xlabel('Predicted')
  207. ax.set_ylabel('Actual')
  208. ax.set_title('Confusion Matrix')
  209. # Log to TensorBoard
  210. if self.tb_writer:
  211. self.tb_writer.add_figure('confusion_matrix', fig, step)
  212. # Log to Weights & Biases
  213. if self.use_wandb:
  214. wandb.log({"confusion_matrix": wandb.Image(fig)}, step=step)
  215. plt.close(fig)
  216. def save_metrics_history(self, filepath: Optional[str] = None):
  217. """Save metrics history to JSON file."""
  218. if filepath is None:
  219. filepath = self.log_dir / f"{self.name}_metrics.json"
  220. with open(filepath, 'w') as f:
  221. json.dump(self.metrics_history, f, indent=2)
  222. self.logger.info(f"Metrics history saved to: {filepath}")
  223. def close(self):
  224. """Close all logging handlers."""
  225. if self.tb_writer:
  226. self.tb_writer.close()
  227. if self.use_wandb:
  228. wandb.finish()
  229. # Save metrics history
  230. self.save_metrics_history()
  231. class ProgressMonitor:
  232. """
  233. Real-time training progress monitor with ETA estimation.
  234. Provides progress tracking, ETA calculation, and performance monitoring
  235. during training with thread-safe updates.
  236. """
  237. def __init__(self, total_epochs: int, steps_per_epoch: int = None):
  238. """
  239. Initialize progress monitor.
  240. Args:
  241. total_epochs: Total number of training epochs
  242. steps_per_epoch: Number of steps per epoch (for step-level tracking)
  243. """
  244. self.total_epochs = total_epochs
  245. self.steps_per_epoch = steps_per_epoch
  246. self.total_steps = total_epochs * (steps_per_epoch or 1)
  247. # Progress tracking
  248. self.current_epoch = 0
  249. self.current_step = 0
  250. self.global_step = 0
  251. # Timing
  252. self.start_time = None
  253. self.epoch_start_time = None
  254. self.step_times = []
  255. self.epoch_times = []
  256. # Thread safety
  257. self.lock = threading.Lock()
  258. # Display settings
  259. self.update_interval = 1.0 # seconds
  260. self.last_update = 0
  261. def start_training(self):
  262. """Mark the start of training."""
  263. with self.lock:
  264. self.start_time = time.time()
  265. self.epoch_start_time = self.start_time
  266. def start_epoch(self, epoch: int):
  267. """Mark the start of an epoch."""
  268. with self.lock:
  269. self.current_epoch = epoch
  270. self.current_step = 0
  271. self.epoch_start_time = time.time()
  272. def update_step(self, step: int = None):
  273. """Update step progress."""
  274. with self.lock:
  275. if step is not None:
  276. self.current_step = step
  277. else:
  278. self.current_step += 1
  279. self.global_step += 1
  280. # Record step time
  281. current_time = time.time()
  282. if len(self.step_times) > 0:
  283. step_duration = current_time - self.step_times[-1]
  284. else:
  285. step_duration = current_time - self.epoch_start_time
  286. self.step_times.append(current_time)
  287. # Limit step times history
  288. if len(self.step_times) > 100:
  289. self.step_times = self.step_times[-100:]
  290. def end_epoch(self):
  291. """Mark the end of an epoch."""
  292. with self.lock:
  293. current_time = time.time()
  294. epoch_duration = current_time - self.epoch_start_time
  295. self.epoch_times.append(epoch_duration)
  296. # Limit epoch times history
  297. if len(self.epoch_times) > 50:
  298. self.epoch_times = self.epoch_times[-50:]
  299. def get_progress_info(self) -> Dict[str, Any]:
  300. """Get current progress information."""
  301. with self.lock:
  302. current_time = time.time()
  303. if self.start_time is None:
  304. return {"status": "not_started"}
  305. # Calculate progress
  306. epoch_progress = self.current_epoch / self.total_epochs
  307. if self.steps_per_epoch:
  308. step_progress = self.current_step / self.steps_per_epoch
  309. total_progress = (self.current_epoch + step_progress) / self.total_epochs
  310. else:
  311. total_progress = epoch_progress
  312. # Calculate timing
  313. elapsed_time = current_time - self.start_time
  314. # Estimate remaining time
  315. if total_progress > 0:
  316. eta = (elapsed_time / total_progress) * (1 - total_progress)
  317. else:
  318. eta = 0
  319. # Calculate average times
  320. avg_step_time = np.mean([
  321. self.step_times[i] - self.step_times[i-1]
  322. for i in range(1, len(self.step_times))
  323. ]) if len(self.step_times) > 1 else 0
  324. avg_epoch_time = np.mean(self.epoch_times) if self.epoch_times else 0
  325. return {
  326. "status": "running",
  327. "current_epoch": self.current_epoch,
  328. "total_epochs": self.total_epochs,
  329. "current_step": self.current_step,
  330. "steps_per_epoch": self.steps_per_epoch,
  331. "global_step": self.global_step,
  332. "total_steps": self.total_steps,
  333. "epoch_progress": epoch_progress,
  334. "total_progress": total_progress,
  335. "elapsed_time": elapsed_time,
  336. "eta": eta,
  337. "avg_step_time": avg_step_time,
  338. "avg_epoch_time": avg_epoch_time,
  339. "elapsed_str": str(timedelta(seconds=int(elapsed_time))),
  340. "eta_str": str(timedelta(seconds=int(eta))),
  341. }
  342. def print_progress(self, metrics: Dict[str, float] = None, force: bool = False):
  343. """Print progress information to console."""
  344. current_time = time.time()
  345. if not force and (current_time - self.last_update) < self.update_interval:
  346. return
  347. self.last_update = current_time
  348. info = self.get_progress_info()
  349. if info["status"] != "running":
  350. return
  351. # Format progress bar
  352. bar_length = 40
  353. filled_length = int(bar_length * info["total_progress"])
  354. bar = "█" * filled_length + "░" * (bar_length - filled_length)
  355. # Format output
  356. output = f"\rEpoch {info['current_epoch']}/{info['total_epochs']} "
  357. output += f"[{bar}] {info['total_progress']*100:.1f}% "
  358. output += f"- {info['elapsed_str']} / {info['eta_str']} "
  359. if metrics:
  360. metric_str = " - ".join([f"{k}: {v:.4f}" for k, v in metrics.items()])
  361. output += f"- {metric_str}"
  362. # Add step info if available
  363. if info["steps_per_epoch"]:
  364. output += f" - Step {info['current_step']}/{info['steps_per_epoch']}"
  365. print(output, end="", flush=True)
  366. class ValidationMetrics:
  367. """
  368. Comprehensive validation and testing metrics calculator.
  369. Provides calculation of various performance metrics for classification
  370. and regression tasks with support for multi-class and multi-label scenarios.
  371. """
  372. def __init__(self, task_type: str = "classification",
  373. num_classes: int = None,
  374. class_names: List[str] = None):
  375. """
  376. Initialize validation metrics calculator.
  377. Args:
  378. task_type: Type of task ('classification', 'regression', 'multilabel')
  379. num_classes: Number of classes for classification
  380. class_names: Names of classes
  381. """
  382. self.task_type = task_type
  383. self.num_classes = num_classes
  384. self.class_names = class_names or [f"Class_{i}" for i in range(num_classes or 2)]
  385. def calculate_classification_metrics(self, y_true: np.ndarray,
  386. y_pred: np.ndarray,
  387. y_prob: np.ndarray = None) -> Dict[str, Any]:
  388. """
  389. Calculate comprehensive classification metrics.
  390. Args:
  391. y_true: True labels
  392. y_pred: Predicted labels
  393. y_prob: Prediction probabilities (for AUC calculation)
  394. Returns:
  395. Dictionary containing various metrics
  396. """
  397. metrics = {}
  398. # Basic metrics
  399. metrics["accuracy"] = accuracy_score(y_true, y_pred)
  400. # Precision, recall, F1-score
  401. precision, recall, f1, support = precision_recall_fscore_support(
  402. y_true, y_pred, average=None, zero_division=0
  403. )
  404. # Per-class metrics
  405. for i, class_name in enumerate(self.class_names[:len(precision)]):
  406. metrics[f"precision_{class_name}"] = precision[i]
  407. metrics[f"recall_{class_name}"] = recall[i]
  408. metrics[f"f1_{class_name}"] = f1[i]
  409. metrics[f"support_{class_name}"] = support[i]
  410. # Averaged metrics
  411. metrics["precision_macro"] = np.mean(precision)
  412. metrics["recall_macro"] = np.mean(recall)
  413. metrics["f1_macro"] = np.mean(f1)
  414. # Weighted averages
  415. precision_weighted = np.average(precision, weights=support)
  416. recall_weighted = np.average(recall, weights=support)
  417. f1_weighted = np.average(f1, weights=support)
  418. metrics["precision_weighted"] = precision_weighted
  419. metrics["recall_weighted"] = recall_weighted
  420. metrics["f1_weighted"] = f1_weighted
  421. # Confusion matrix
  422. cm = confusion_matrix(y_true, y_pred)
  423. metrics["confusion_matrix"] = cm.tolist()
  424. # AUC metrics (if probabilities provided)
  425. if y_prob is not None:
  426. try:
  427. if len(np.unique(y_true)) == 2:
  428. # Binary classification
  429. metrics["auc_roc"] = roc_auc_score(y_true, y_prob[:, 1])
  430. metrics["auc_pr"] = average_precision_score(y_true, y_prob[:, 1])
  431. else:
  432. # Multi-class classification
  433. metrics["auc_roc_ovr"] = roc_auc_score(
  434. y_true, y_prob, multi_class='ovr', average='macro'
  435. )
  436. metrics["auc_roc_ovo"] = roc_auc_score(
  437. y_true, y_prob, multi_class='ovo', average='macro'
  438. )
  439. except Exception as e:
  440. # AUC calculation failed (e.g., only one class present)
  441. pass
  442. # Classification report
  443. report = classification_report(y_true, y_pred,
  444. target_names=self.class_names[:len(np.unique(y_true))],
  445. output_dict=True, zero_division=0)
  446. metrics["classification_report"] = report
  447. return metrics
  448. def calculate_regression_metrics(self, y_true: np.ndarray,
  449. y_pred: np.ndarray) -> Dict[str, float]:
  450. """
  451. Calculate regression metrics.
  452. Args:
  453. y_true: True values
  454. y_pred: Predicted values
  455. Returns:
  456. Dictionary containing regression metrics
  457. """
  458. from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
  459. metrics = {}
  460. # Basic metrics
  461. metrics["mse"] = mean_squared_error(y_true, y_pred)
  462. metrics["rmse"] = np.sqrt(metrics["mse"])
  463. metrics["mae"] = mean_absolute_error(y_true, y_pred)
  464. metrics["r2"] = r2_score(y_true, y_pred)
  465. # Additional metrics
  466. metrics["mean_error"] = np.mean(y_pred - y_true)
  467. metrics["std_error"] = np.std(y_pred - y_true)
  468. # Percentage errors
  469. mape = np.mean(np.abs((y_true - y_pred) / (y_true + 1e-8))) * 100
  470. metrics["mape"] = mape
  471. return metrics
  472. def calculate_embedding_metrics(self, embeddings: np.ndarray,
  473. labels: np.ndarray) -> Dict[str, float]:
  474. """
  475. Calculate metrics specific to embedding models (e.g., speaker recognition).
  476. Args:
  477. embeddings: Embedding vectors
  478. labels: True labels
  479. Returns:
  480. Dictionary containing embedding metrics
  481. """
  482. from sklearn.metrics.pairwise import cosine_similarity
  483. metrics = {}
  484. # Calculate pairwise similarities
  485. similarities = cosine_similarity(embeddings)
  486. # Intra-class and inter-class similarities
  487. intra_class_sims = []
  488. inter_class_sims = []
  489. unique_labels = np.unique(labels)
  490. for i in range(len(embeddings)):
  491. for j in range(i + 1, len(embeddings)):
  492. sim = similarities[i, j]
  493. if labels[i] == labels[j]:
  494. intra_class_sims.append(sim)
  495. else:
  496. inter_class_sims.append(sim)
  497. metrics["intra_class_similarity_mean"] = np.mean(intra_class_sims)
  498. metrics["intra_class_similarity_std"] = np.std(intra_class_sims)
  499. metrics["inter_class_similarity_mean"] = np.mean(inter_class_sims)
  500. metrics["inter_class_similarity_std"] = np.std(inter_class_sims)
  501. # Separation metric
  502. separation = (np.mean(intra_class_sims) - np.mean(inter_class_sims))
  503. metrics["class_separation"] = separation
  504. # Equal Error Rate (EER) calculation
  505. # Create pairs and labels for verification task
  506. pairs = []
  507. pair_labels = []
  508. for i in range(len(embeddings)):
  509. for j in range(i + 1, len(embeddings)):
  510. pairs.append((i, j))
  511. pair_labels.append(1 if labels[i] == labels[j] else 0)
  512. # Calculate similarities for pairs
  513. pair_similarities = [similarities[i, j] for i, j in pairs]
  514. # Find EER threshold
  515. thresholds = np.linspace(min(pair_similarities), max(pair_similarities), 1000)
  516. best_eer = 1.0
  517. best_threshold = 0.0
  518. for threshold in thresholds:
  519. predictions = (np.array(pair_similarities) >= threshold).astype(int)
  520. # Calculate FAR and FRR
  521. true_pairs = np.array(pair_labels) == 1
  522. false_pairs = np.array(pair_labels) == 0
  523. if np.sum(true_pairs) > 0 and np.sum(false_pairs) > 0:
  524. far = np.sum(predictions[false_pairs]) / np.sum(false_pairs) # False Accept Rate
  525. frr = 1 - np.sum(predictions[true_pairs]) / np.sum(true_pairs) # False Reject Rate
  526. if abs(far - frr) < abs(best_eer - best_eer):
  527. best_eer = (far + frr) / 2
  528. best_threshold = threshold
  529. metrics["eer"] = best_eer
  530. metrics["eer_threshold"] = best_threshold
  531. return metrics
  532. def calculate_all_metrics(self, y_true: np.ndarray, y_pred: np.ndarray,
  533. y_prob: np.ndarray = None,
  534. embeddings: np.ndarray = None) -> Dict[str, Any]:
  535. """
  536. Calculate all appropriate metrics based on task type.
  537. Args:
  538. y_true: True labels/values
  539. y_pred: Predicted labels/values
  540. y_prob: Prediction probabilities (for classification)
  541. embeddings: Embedding vectors (for embedding tasks)
  542. Returns:
  543. Dictionary containing all calculated metrics
  544. """
  545. if self.task_type == "classification":
  546. metrics = self.calculate_classification_metrics(y_true, y_pred, y_prob)
  547. # Add embedding metrics if available
  548. if embeddings is not None:
  549. embedding_metrics = self.calculate_embedding_metrics(embeddings, y_true)
  550. metrics.update(embedding_metrics)
  551. return metrics
  552. elif self.task_type == "regression":
  553. return self.calculate_regression_metrics(y_true, y_pred)
  554. else:
  555. raise ValueError(f"Unsupported task type: {self.task_type}")
  556. class ModelProfiler:
  557. """
  558. Model performance profiler for analyzing computational requirements.
  559. Provides profiling of model inference time, memory usage, FLOPs,
  560. and other performance characteristics.
  561. """
  562. def __init__(self, model: nn.Module, device: str = "cpu"):
  563. """
  564. Initialize model profiler.
  565. Args:
  566. model: PyTorch model to profile
  567. device: Device for profiling
  568. """
  569. self.model = model.to(device)
  570. self.device = device
  571. self.model.eval()
  572. def profile_inference(self, input_tensor: torch.Tensor,
  573. num_runs: int = 100,
  574. warmup_runs: int = 10) -> Dict[str, float]:
  575. """
  576. Profile model inference performance.
  577. Args:
  578. input_tensor: Sample input tensor
  579. num_runs: Number of inference runs for timing
  580. warmup_runs: Number of warmup runs
  581. Returns:
  582. Dictionary containing timing statistics
  583. """
  584. input_tensor = input_tensor.to(self.device)
  585. # Warmup runs
  586. with torch.no_grad():
  587. for _ in range(warmup_runs):
  588. _ = self.model(input_tensor)
  589. # Synchronize if using CUDA
  590. if self.device.startswith('cuda'):
  591. torch.cuda.synchronize()
  592. # Timing runs
  593. times = []
  594. with torch.no_grad():
  595. for _ in range(num_runs):
  596. start_time = time.perf_counter()
  597. output = self.model(input_tensor)
  598. if self.device.startswith('cuda'):
  599. torch.cuda.synchronize()
  600. end_time = time.perf_counter()
  601. times.append(end_time - start_time)
  602. return {
  603. "mean_inference_time": np.mean(times),
  604. "std_inference_time": np.std(times),
  605. "min_inference_time": np.min(times),
  606. "max_inference_time": np.max(times),
  607. "median_inference_time": np.median(times),
  608. "throughput_samples_per_second": input_tensor.shape[0] / np.mean(times)
  609. }
  610. def profile_memory(self, input_tensor: torch.Tensor) -> Dict[str, float]:
  611. """
  612. Profile memory usage during inference.
  613. Args:
  614. input_tensor: Sample input tensor
  615. Returns:
  616. Dictionary containing memory statistics
  617. """
  618. input_tensor = input_tensor.to(self.device)
  619. if not self.device.startswith('cuda'):
  620. return {"error": "Memory profiling only available for CUDA devices"}
  621. # Clear cache and reset peak memory stats
  622. torch.cuda.empty_cache()
  623. torch.cuda.reset_peak_memory_stats(self.device)
  624. # Record initial memory
  625. initial_memory = torch.cuda.memory_allocated(self.device)
  626. # Run inference
  627. with torch.no_grad():
  628. output = self.model(input_tensor)
  629. # Record peak memory
  630. peak_memory = torch.cuda.max_memory_allocated(self.device)
  631. final_memory = torch.cuda.memory_allocated(self.device)
  632. return {
  633. "initial_memory_mb": initial_memory / (1024 * 1024),
  634. "peak_memory_mb": peak_memory / (1024 * 1024),
  635. "final_memory_mb": final_memory / (1024 * 1024),
  636. "memory_increase_mb": (peak_memory - initial_memory) / (1024 * 1024)
  637. }
  638. def count_parameters(self) -> Dict[str, int]:
  639. """Count model parameters."""
  640. total_params = sum(p.numel() for p in self.model.parameters())
  641. trainable_params = sum(p.numel() for p in self.model.parameters() if p.requires_grad)
  642. return {
  643. "total_parameters": total_params,
  644. "trainable_parameters": trainable_params,
  645. "non_trainable_parameters": total_params - trainable_params
  646. }
  647. def estimate_model_size(self) -> Dict[str, float]:
  648. """Estimate model size in different formats."""
  649. # Parameter size
  650. param_size = sum(p.numel() * p.element_size() for p in self.model.parameters())
  651. buffer_size = sum(b.numel() * b.element_size() for b in self.model.buffers())
  652. total_size = param_size + buffer_size
  653. return {
  654. "total_size_bytes": total_size,
  655. "total_size_mb": total_size / (1024 * 1024),
  656. "parameter_size_mb": param_size / (1024 * 1024),
  657. "buffer_size_mb": buffer_size / (1024 * 1024)
  658. }
  659. def generate_profile_report(self, input_tensor: torch.Tensor) -> str:
  660. """Generate comprehensive profiling report."""
  661. inference_stats = self.profile_inference(input_tensor)
  662. memory_stats = self.profile_memory(input_tensor)
  663. param_stats = self.count_parameters()
  664. size_stats = self.estimate_model_size()
  665. report = "# Model Profiling Report\n\n"
  666. # Model info
  667. report += "## Model Information\n"
  668. report += f"- Model: {self.model.__class__.__name__}\n"
  669. report += f"- Device: {self.device}\n"
  670. report += f"- Input shape: {input_tensor.shape}\n\n"
  671. # Parameters
  672. report += "## Parameters\n"
  673. report += f"- Total parameters: {param_stats['total_parameters']:,}\n"
  674. report += f"- Trainable parameters: {param_stats['trainable_parameters']:,}\n"
  675. report += f"- Non-trainable parameters: {param_stats['non_trainable_parameters']:,}\n\n"
  676. # Model size
  677. report += "## Model Size\n"
  678. report += f"- Total size: {size_stats['total_size_mb']:.2f} MB\n"
  679. report += f"- Parameter size: {size_stats['parameter_size_mb']:.2f} MB\n"
  680. report += f"- Buffer size: {size_stats['buffer_size_mb']:.2f} MB\n\n"
  681. # Inference performance
  682. report += "## Inference Performance\n"
  683. report += f"- Mean inference time: {inference_stats['mean_inference_time']*1000:.2f} ms\n"
  684. report += f"- Std inference time: {inference_stats['std_inference_time']*1000:.2f} ms\n"
  685. report += f"- Throughput: {inference_stats['throughput_samples_per_second']:.1f} samples/sec\n\n"
  686. # Memory usage
  687. if "error" not in memory_stats:
  688. report += "## Memory Usage\n"
  689. report += f"- Peak memory: {memory_stats['peak_memory_mb']:.2f} MB\n"
  690. report += f"- Memory increase: {memory_stats['memory_increase_mb']:.2f} MB\n\n"
  691. return report