import torch import numpy as np import random import os from pathlib import Path import json import yaml from typing import Dict, Any, Optional, List import hashlib import shutil from datetime import datetime def set_seed(seed: int = 42): """ Set random seed for reproducibility Args: seed: Random seed value """ random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False os.environ['PYTHONHASHSEED'] = str(seed) def get_device(gpu_id: Optional[int] = None) -> torch.device: """ Get torch device Args: gpu_id: Specific GPU ID to use Returns: torch.device """ if torch.cuda.is_available(): if gpu_id is not None: device = torch.device(f'cuda:{gpu_id}') else: device = torch.device('cuda') else: device = torch.device('cpu') return device def count_parameters(model: torch.nn.Module) -> Dict[str, int]: """ Count model parameters Args: model: PyTorch model Returns: Dictionary with parameter counts """ total_params = sum(p.numel() for p in model.parameters()) trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad) non_trainable_params = total_params - trainable_params return { 'total': total_params, 'trainable': trainable_params, 'non_trainable': non_trainable_params, 'total_mb': total_params * 4 / 1024 / 1024, # Assuming float32 'trainable_mb': trainable_params * 4 / 1024 / 1024 } def save_config(config: Dict[str, Any], save_path: Path, format: str = 'json'): """ Save configuration to file Args: config: Configuration dictionary save_path: Path to save file format: File format ('json' or 'yaml') """ save_path = Path(save_path) save_path.parent.mkdir(parents=True, exist_ok=True) if format == 'json': with open(save_path, 'w') as f: json.dump(config, f, indent=2) elif format == 'yaml': with open(save_path, 'w') as f: yaml.dump(config, f, default_flow_style=False) else: raise ValueError(f"Unknown format: {format}") def load_config(config_path: Path) -> Dict[str, Any]: """ Load configuration from file Args: config_path: Path to config file Returns: Configuration dictionary """ config_path = Path(config_path) if config_path.suffix == '.json': with open(config_path, 'r') as f: config = json.load(f) elif config_path.suffix in ['.yaml', '.yml']: with open(config_path, 'r') as f: config = yaml.safe_load(f) else: raise ValueError(f"Unknown config format: {config_path.suffix}") return config def merge_configs(base_config: Dict, update_config: Dict) -> Dict: """ Recursively merge two configuration dictionaries Args: base_config: Base configuration update_config: Configuration with updates Returns: Merged configuration """ merged = base_config.copy() for key, value in update_config.items(): if key in merged and isinstance(merged[key], dict) and isinstance(value, dict): merged[key] = merge_configs(merged[key], value) else: merged[key] = value return merged def get_timestamp() -> str: """Get current timestamp string""" return datetime.now().strftime('%Y%m%d_%H%M%S') def get_experiment_name( model_name: str, dataset_name: str = 'decade', timestamp: bool = True ) -> str: """ Generate experiment name Args: model_name: Name of the model dataset_name: Name of the dataset timestamp: Whether to include timestamp Returns: Experiment name """ name_parts = [model_name, dataset_name] if timestamp: name_parts.append(get_timestamp()) return '_'.join(name_parts) def compute_file_hash(file_path: Path, hash_algo: str = 'md5') -> str: """ Compute hash of a file Args: file_path: Path to file hash_algo: Hash algorithm to use Returns: Hex digest of file hash """ hash_func = getattr(hashlib, hash_algo)() with open(file_path, 'rb') as f: for chunk in iter(lambda: f.read(4096), b''): hash_func.update(chunk) return hash_func.hexdigest() def create_experiment_structure(base_dir: Path, experiment_name: str) -> Dict[str, Path]: """ Create directory structure for an experiment Args: base_dir: Base directory for experiments experiment_name: Name of the experiment Returns: Dictionary mapping directory names to paths """ exp_dir = base_dir / experiment_name dirs = { 'root': exp_dir, 'checkpoints': exp_dir / 'checkpoints', 'logs': exp_dir / 'logs', 'visualizations': exp_dir / 'visualizations', 'predictions': exp_dir / 'predictions', 'configs': exp_dir / 'configs' } for dir_path in dirs.values(): dir_path.mkdir(parents=True, exist_ok=True) return dirs def backup_code(src_dir: Path, backup_dir: Path, extensions: List[str] = None): """ Backup source code to experiment directory Args: src_dir: Source directory backup_dir: Backup destination extensions: List of file extensions to backup """ if extensions is None: extensions = ['.py', '.yaml', '.yml', '.json', '.txt', '.md'] backup_dir = Path(backup_dir) backup_dir.mkdir(parents=True, exist_ok=True) for file_path in src_dir.rglob('*'): if file_path.is_file() and file_path.suffix in extensions: relative_path = file_path.relative_to(src_dir) backup_path = backup_dir / relative_path backup_path.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(file_path, backup_path) def format_time(seconds: float) -> str: """ Format time in seconds to human-readable string Args: seconds: Time in seconds Returns: Formatted time string """ hours = int(seconds // 3600) minutes = int((seconds % 3600) // 60) seconds = int(seconds % 60) if hours > 0: return f"{hours}h {minutes}m {seconds}s" elif minutes > 0: return f"{minutes}m {seconds}s" else: return f"{seconds}s" def get_gpu_memory_usage() -> Dict[str, float]: """ Get GPU memory usage Returns: Dictionary with memory usage in MB """ if not torch.cuda.is_available(): return {'allocated': 0, 'reserved': 0} return { 'allocated': torch.cuda.memory_allocated() / 1024 / 1024, 'reserved': torch.cuda.memory_reserved() / 1024 / 1024 } def clean_checkpoint(checkpoint_path: Path, keep_keys: List[str] = None): """ Clean checkpoint file by keeping only specified keys Args: checkpoint_path: Path to checkpoint keep_keys: Keys to keep (default: model_state_dict only) """ if keep_keys is None: keep_keys = ['model_state_dict'] checkpoint = torch.load(checkpoint_path, map_location='cpu') cleaned_checkpoint = {k: v for k, v in checkpoint.items() if k in keep_keys} # Save cleaned checkpoint output_path = checkpoint_path.parent / f"{checkpoint_path.stem}_cleaned.pth" torch.save(cleaned_checkpoint, output_path) # Print size reduction original_size = checkpoint_path.stat().st_size / 1024 / 1024 new_size = output_path.stat().st_size / 1024 / 1024 print(f"Cleaned checkpoint: {original_size:.1f}MB → {new_size:.1f}MB") return output_path class AverageMeter: """Computes and stores the average and current value""" def __init__(self): self.reset() def reset(self): self.val = 0 self.avg = 0 self.sum = 0 self.count = 0 def update(self, val, n=1): self.val = val self.sum += val * n self.count += n self.avg = self.sum / self.count class EarlyStopping: """Early stopping to stop training when validation loss doesn't improve""" def __init__(self, patience=7, verbose=False, delta=0): self.patience = patience self.verbose = verbose self.counter = 0 self.best_score = None self.early_stop = False self.val_loss_min = np.Inf self.delta = delta def __call__(self, val_loss, model=None): score = -val_loss if self.best_score is None: self.best_score = score self.save_checkpoint(val_loss, model) elif score < self.best_score + self.delta: self.counter += 1 if self.verbose: print(f'EarlyStopping counter: {self.counter} out of {self.patience}') if self.counter >= self.patience: self.early_stop = True else: self.best_score = score self.save_checkpoint(val_loss, model) self.counter = 0 def save_checkpoint(self, val_loss, model): """Saves model when validation loss decrease""" if self.verbose: print(f'Validation loss decreased ({self.val_loss_min:.6f} --> {val_loss:.6f})') self.val_loss_min = val_loss