| |
| """ |
| Main training script using modular components with improved robustness |
| """ |
| import argparse |
| import sys |
| from pathlib import Path |
| from datetime import datetime |
| import logging |
|
|
| |
| sys.path.append(str(Path(__file__).parent.parent)) |
|
|
| import torch |
| from torch.utils.data import DataLoader |
|
|
| |
| from src.models.model_factory import ModelFactory, create_optimizer, create_scheduler |
| from src.data.url_dataset import URLDataset, CachedDataset |
| from src.data.transforms import get_transforms_for_model |
| from src.training.trainer import Trainer, collate_multitask_fn |
| from src.training.metrics import calculate_class_weights |
| from src.utils.logger import ExperimentLogger |
| from src.utils.helpers import ( |
| set_seed, get_device, count_parameters, |
| create_experiment_structure, backup_code |
| ) |
| from src.utils.visualization import plot_training_curves |
| from src.data.data_utils import create_data_loaders |
|
|
| def parse_arguments(): |
| """Parse command line arguments with additional options""" |
| parser = argparse.ArgumentParser(description='Train decade classifier') |
|
|
| |
| parser.add_argument('--model_name', type=str, required=True, |
| choices=ModelFactory.list_available_models(), |
| help='Model architecture to use') |
| parser.add_argument('--pretrained', action='store_true', default=True, |
| help='Use pretrained weights') |
| parser.add_argument('--resume', type=str, default=None, |
| help='Path to checkpoint to resume from') |
|
|
| |
| parser.add_argument('--epochs', type=int, default=30, |
| help='Number of training epochs') |
| parser.add_argument('--batch_size', type=int, default=None, |
| help='Batch size (default: model-specific)') |
| parser.add_argument('--learning_rate', type=float, default=None, |
| help='Learning rate (default: model-specific)') |
| parser.add_argument('--weight_decay', type=float, default=None, |
| help='Weight decay (default: model-specific)') |
|
|
| |
| parser.add_argument('--data_dir', type=str, default='data', |
| help='Data directory') |
| parser.add_argument('--num_workers', type=int, default=4, |
| help='Number of data loading workers') |
| parser.add_argument('--use_cached', action='store_true', |
| help='Use pre-downloaded cached images') |
| |
| parser.add_argument('--use_subset', action='store_true', |
| help='Use a subset of data for quick testing') |
| parser.add_argument('--subset_fraction', type=float, default=0.1, |
| help='Fraction of data to use if use_subset is True') |
|
|
| |
| parser.add_argument('--optimizer', type=str, default='adamw', |
| choices=['adamw', 'adam', 'sgd'], |
| help='Optimizer to use') |
| parser.add_argument('--scheduler', type=str, default='cosine', |
| choices=['cosine', 'step', 'exponential', 'reduce_on_plateau'], |
| help='Learning rate scheduler') |
| parser.add_argument('--use_amp', action='store_true', default=True, |
| help='Use automatic mixed precision') |
| parser.add_argument('--gradient_clip', type=float, default=0.0, |
| help='Gradient clipping value') |
|
|
| |
| parser.add_argument('--loss', type=str, default='cross_entropy', |
| choices=['cross_entropy', 'label_smoothing', 'focal', 'weighted_ce'], |
| help='Loss function') |
| parser.add_argument('--label_smoothing', type=float, default=0.1, |
| help='Label smoothing factor') |
| parser.add_argument('--focal_gamma', type=float, default=2.0, |
| help='Gamma parameter for focal loss') |
| parser.add_argument('--focal_alpha', type=float, default=0.25, |
| help='Alpha parameter for focal loss') |
| parser.add_argument('--class_weights', action='store_true', |
| help='Use class weights for imbalanced data') |
|
|
| |
| parser.add_argument('--exp_name', type=str, default=None, |
| help='Experiment name') |
| parser.add_argument('--exp_dir', type=str, default='experiments', |
| help='Experiments directory') |
| parser.add_argument('--seed', type=int, default=42, |
| help='Random seed') |
| parser.add_argument('--use_wandb', action='store_true', |
| help='Use Weights & Biases logging') |
|
|
| |
| parser.add_argument('--early_stopping', type=int, default=5, |
| help='Early stopping patience (0 to disable)') |
| |
| parser.add_argument('--save_final', action='store_true', default=True, |
| help='Save final model state') |
| parser.add_argument('--gpu', type=int, default=None, |
| help='GPU ID to use') |
| |
| parser.add_argument('--multi_task', action='store_true', |
| help='Enable multi-task learning for decade, cluster, and device classification') |
| parser.add_argument('--decade_weight', type=float, default=1.0, |
| help='Weight for decade loss in multi-task learning') |
| parser.add_argument('--cluster_weight', type=float, default=1.0, |
| help='Weight for cluster loss in multi-task learning') |
| parser.add_argument('--device_weight', type=float, default=1.0, |
| help='Weight for device (phone/calculator) loss in multi-task learning') |
|
|
| return parser.parse_args() |
|
|
| def main(): |
| |
| args = parse_arguments() |
|
|
| |
| set_seed(args.seed) |
|
|
| print("111111") |
|
|
| |
| device = get_device(args.gpu) |
| if args.gpu is not None and not torch.cuda.is_available(): |
| print(f"Warning: GPU {args.gpu} requested but not available, falling back to CPU") |
| device = torch.device("cpu") |
| elif args.gpu is not None and args.gpu >= torch.cuda.device_count(): |
| print(f"Warning: GPU {args.gpu} invalid, falling back to CPU") |
| device = torch.device("cpu") |
| |
| |
| if torch.cuda.is_available() and device.type == 'cuda': |
| gpu_id = torch.cuda.current_device() |
| gpu_name = torch.cuda.get_device_name(gpu_id) |
| print(f"Using GPU {gpu_id}: {gpu_name}") |
|
|
| |
| |
| if not logging.getLogger().handlers: |
| logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') |
|
|
| |
| try: |
| config = ModelFactory.get_model_config(args.model_name) |
| except KeyError: |
| print(f"Error: Unknown model '{args.model_name}'. Available models:") |
| for model in ModelFactory.list_available_models(): |
| print(f" - {model}") |
| sys.exit(1) |
| except Exception as e: |
| print(f"Failed to load model config: {e}") |
| sys.exit(1) |
|
|
| |
| config.update({ |
| 'model_name': args.model_name, |
| 'pretrained': args.pretrained, |
| 'epochs': args.epochs, |
| 'num_workers': args.num_workers, |
| 'use_cached': args.use_cached, |
| 'use_subset': args.use_subset, |
| 'subset_fraction': args.subset_fraction, |
| 'optimizer': args.optimizer, |
| 'scheduler': args.scheduler, |
| 'use_amp': args.use_amp, |
| 'gradient_clip_val': args.gradient_clip, |
| 'num_classes': 3, |
| 'early_stopping': args.early_stopping, |
| 'seed': args.seed, |
| 'data_dir': args.data_dir, |
| 'use_class_weights': args.class_weights, |
| |
| 'use_weighted_sampling': False, |
| 'augmentation_level': 'medium', |
| 'max_download_retries': 3, |
| 'download_timeout': 10, |
| 'multi_task': args.multi_task, |
| 'decade_weight': args.decade_weight, |
| 'cluster_weight': args.cluster_weight, |
| 'device_weight': args.device_weight, |
| 'num_decade_classes': 5, |
| 'num_cluster_classes': 10, |
| 'num_device_classes': 2, |
| |
| 'monitor_metric': 'accuracy' if not args.multi_task else 'accuracy', |
| }) |
|
|
| |
| if args.batch_size: |
| config['batch_size'] = args.batch_size |
| if args.learning_rate: |
| config['learning_rate'] = args.learning_rate |
| if args.weight_decay: |
| config['weight_decay'] = args.weight_decay |
|
|
| |
| try: |
| if config['batch_size'] > 128: |
| print(f"Warning: Large batch size {config['batch_size']} may cause memory issues") |
| if not (1e-6 <= config['learning_rate'] <= 1): |
| print(f"Warning: Learning rate {config['learning_rate']} outside typical range [1e-6, 1]") |
| if not (0 <= config['weight_decay'] <= 1): |
| print(f"Warning: Weight decay {config['weight_decay']} outside typical range [0, 1]") |
| if args.use_subset and not (0 < args.subset_fraction <= 1): |
| print(f"Error: subset_fraction {args.subset_fraction} must be in (0, 1]") |
| sys.exit(1) |
| |
| |
| if args.multi_task: |
| if args.decade_weight <= 0: |
| print(f"Error: decade_weight {args.decade_weight} must be positive") |
| sys.exit(1) |
| if args.cluster_weight <= 0: |
| print(f"Error: cluster_weight {args.cluster_weight} must be positive") |
| sys.exit(1) |
| if args.device_weight <= 0: |
| print(f"Error: device_weight {args.device_weight} must be positive") |
| sys.exit(1) |
| if args.decade_weight + args.cluster_weight + args.device_weight == 0: |
| print(f"Error: At least one task weight must be non-zero") |
| sys.exit(1) |
| except KeyError as e: |
| print(f"Error: Missing required config parameter: {e}") |
| sys.exit(1) |
|
|
| |
| loss_config = {'name': args.loss, 'params': {}} |
| if args.loss == 'label_smoothing': |
| loss_config['params']['smoothing'] = args.label_smoothing |
| elif args.loss == 'focal': |
| loss_config['params']['gamma'] = args.focal_gamma |
| loss_config['params']['alpha'] = args.focal_alpha |
| config['loss'] = loss_config |
|
|
| |
| if args.exp_name: |
| exp_name = args.exp_name |
| else: |
| timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') |
| exp_name = f"{args.model_name}_{timestamp}" |
|
|
| |
| try: |
| exp_dirs = create_experiment_structure(Path(args.exp_dir), exp_name) |
| except Exception as e: |
| print(f"Failed to create experiment structure: {e}") |
| sys.exit(1) |
|
|
| |
| logger = ExperimentLogger( |
| experiment_name=exp_name, |
| project_name='decade-classifier', |
| log_dir=exp_dirs['logs'], |
| config=config, |
| use_wandb=args.use_wandb, |
| use_tensorboard=True |
| ) |
|
|
| logger.info(f"Starting experiment: {exp_name}") |
| logger.info(f"Using device: {device}") |
| logger.info(f"Command-line arguments: {vars(args)}") |
|
|
| |
| try: |
| backup_code( |
| src_dir=Path(__file__).parent.parent, |
| backup_dir=exp_dirs['configs'] / 'code_backup' |
| ) |
| except Exception as e: |
| logger.warning(f"Failed to backup code: {e}") |
|
|
| |
| if args.multi_task: |
| |
| |
| pass |
| |
| |
| try: |
| |
| data_dir_path = Path(args.data_dir) |
| |
| train_loader, val_loader, class_weights, class_names = create_data_loaders( |
| config, |
| data_dir=data_dir_path, |
| use_subset=args.use_subset, |
| subset_fraction=args.subset_fraction, |
| multi_task=args.multi_task |
| ) |
|
|
| |
| if args.multi_task: |
| logger.info(f"Decade class names: {class_names['decade']}") |
| logger.info(f"Cluster class names: {class_names['cluster']}") |
| |
| config['num_decade_classes'] = len(class_names['decade']) |
| config['num_cluster_classes'] = len(class_names['cluster']) |
| if 'device' in class_names: |
| logger.info(f"Device class names: {class_names['device']}") |
| config['num_device_classes'] = len(class_names['device']) |
| else: |
| logger.info(f"Class names: {class_names}") |
| config['num_classes'] = len(class_names) |
| except FileNotFoundError as e: |
| logger.error(f"Data files not found: {e}") |
| logger.error("Please ensure data/splits/ directory contains train.json, val.json, test.json") |
| sys.exit(1) |
| except Exception as e: |
| logger.error(f"Failed to create data loaders: {e}") |
| sys.exit(1) |
|
|
| logger.info(f"Train samples: {len(train_loader.dataset)}") |
| logger.info(f"Val samples: {len(val_loader.dataset)}") |
|
|
| |
| try: |
| if args.multi_task: |
| |
| num_classes = { |
| 'decade': config['num_decade_classes'], |
| 'cluster': config['num_cluster_classes'] |
| } |
| if 'num_device_classes' in config: |
| num_classes['device'] = config['num_device_classes'] |
| |
| model = ModelFactory.create_model( |
| config['model_name'], |
| num_classes=num_classes, |
| multi_task=True, |
| multitask_config={ |
| 'hidden_dim': config.get('multitask_hidden_dim', 512), |
| 'dropout_rate': config.get('multitask_dropout', 0.3), |
| 'num_device_classes': config.get('num_device_classes', 2) |
| }, |
| pretrained=config['pretrained'] |
| ) |
| else: |
| |
| model = ModelFactory.create_model( |
| config['model_name'], |
| num_classes=config['num_classes'], |
| pretrained=config['pretrained'] |
| ) |
| model = model.to(device) |
| except Exception as e: |
| logger.error(f"Failed to create model: {e}") |
| sys.exit(1) |
|
|
| |
| param_count = count_parameters(model) |
| logger.info(f"Model parameters: {param_count['total']:,} " |
| f"(Trainable: {param_count['trainable']:,})") |
|
|
| |
| if args.multi_task: |
| |
| loss_config = config.get('loss', {}) |
| loss_name = loss_config.get('name', 'cross_entropy') |
| loss_params = loss_config.get('params', {}) |
| |
| |
| if args.class_weights and class_weights is not None: |
| loss_params['class_weights'] = class_weights |
| logger.info(f"Using class weights for multi-task loss") |
| |
| criterion = ModelFactory.create_multitask_loss( |
| decade_weight=args.decade_weight, |
| cluster_weight=args.cluster_weight, |
| device_weight=args.device_weight, |
| loss_type=loss_name, |
| loss_params=loss_params |
| ) |
| else: |
| |
| loss_config = config.get('loss', {}) |
| loss_name = loss_config.get('name', 'cross_entropy') |
| loss_params = loss_config.get('params', {}) |
| |
| |
| if args.class_weights and class_weights is not None: |
| loss_params['class_weights'] = class_weights |
| logger.info(f"Using class weights: {class_weights.numpy()}") |
| |
| from src.training.losses import get_loss_function |
| criterion = get_loss_function(loss_name, **loss_params) |
|
|
| |
| try: |
| optimizer = create_optimizer(model, config) |
| scheduler = create_scheduler(optimizer, config) |
| except Exception as e: |
| logger.error(f"Failed to create optimizer or scheduler: {e}") |
| sys.exit(1) |
|
|
| |
| try: |
| trainer = Trainer( |
| model=model, |
| config=config, |
| device=device, |
| experiment_dir=exp_dirs['root'], |
| logger=logger, |
| multi_task=args.multi_task |
| ) |
| |
| |
| trainer.criterion = criterion |
| |
| except Exception as e: |
| logger.error(f"Failed to create trainer: {e}") |
| sys.exit(1) |
|
|
| |
| start_epoch = 0 |
| if args.resume: |
| try: |
| checkpoint = trainer.load_checkpoint(Path(args.resume)) |
| optimizer.load_state_dict(checkpoint['optimizer_state_dict']) |
| if scheduler and checkpoint.get('scheduler_state_dict'): |
| scheduler.load_state_dict(checkpoint['scheduler_state_dict']) |
| start_epoch = checkpoint['epoch'] |
| logger.info(f"Resumed from checkpoint: {args.resume} (epoch {start_epoch})") |
| except FileNotFoundError: |
| logger.error(f"Checkpoint file not found: {args.resume}") |
| sys.exit(1) |
| except Exception as e: |
| logger.error(f"Failed to load checkpoint: {e}") |
| sys.exit(1) |
|
|
| |
| try: |
| logger.info("Starting training...") |
| results = trainer.train( |
| train_loader=train_loader, |
| val_loader=val_loader, |
| optimizer=optimizer, |
| scheduler=scheduler, |
| start_epoch=start_epoch, |
| class_names=class_names |
| ) |
| except KeyboardInterrupt: |
| logger.info("Training interrupted by user") |
| sys.exit(0) |
| except Exception as e: |
| import traceback |
| logger.error(f"Training failed: {e}") |
| logger.error(f"Full traceback:\n{traceback.format_exc()}") |
| sys.exit(1) |
|
|
| |
| if args.save_final: |
| try: |
| final_path = exp_dirs['checkpoints'] / 'final_checkpoint.pth' |
| trainer.save_checkpoint( |
| optimizer=optimizer, |
| scheduler=scheduler, |
| epoch=config['epochs'], |
| val_metrics=results['metrics_history']['val'][-1] if results.get('metrics_history') and results['metrics_history']['val'] else {}, |
| is_best=False, |
| class_names=class_names |
| ) |
| logger.info(f"Final checkpoint saved to: {final_path}") |
| except Exception as e: |
| logger.error(f"Failed to save final checkpoint: {e}") |
|
|
| |
| logger.log_metrics( |
| { |
| 'final/best_accuracy': results['best_metric'], |
| 'final/best_epoch': results['best_epoch'], |
| 'final/total_time_minutes': results['total_time'] / 60 |
| }, |
| step=config['epochs'] |
| ) |
|
|
| |
| if results.get('metrics_history'): |
| try: |
| plot_training_curves( |
| results['metrics_history'], |
| save_path=exp_dirs['visualizations'] / 'training_curves.png', |
| show=False |
| ) |
| logger.info("Training curves saved") |
| except Exception as e: |
| logger.warning(f"Failed to plot training curves: {e}") |
| else: |
| logger.warning("No metrics history available to plot") |
|
|
| |
| try: |
| best_checkpoint_path = exp_dirs['checkpoints'] / 'best_checkpoint.pth' |
| if best_checkpoint_path.exists(): |
| logger.log_model(best_checkpoint_path, aliases=['best', f"acc_{results['best_metric']:.2f}"]) |
| except Exception as e: |
| logger.warning(f"Failed to log model: {e}") |
|
|
| |
| logger.finish() |
|
|
| print(f"\nTraining complete!") |
| print(f"Best accuracy: {results['best_metric']*100:.2f}% at epoch {results['best_epoch']}") |
| print(f"Results saved to: {exp_dirs['root']}") |
|
|
| if __name__ == '__main__': |
| main() |