File size: 21,475 Bytes
18a82fb | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 | #!/usr/bin/env python
"""
Main training script using modular components with improved robustness
"""
import argparse
import sys
from pathlib import Path
from datetime import datetime
import logging
# FIXED: Ensure proper path handling
sys.path.append(str(Path(__file__).parent.parent))
import torch
from torch.utils.data import DataLoader # ADDED: Missing import
# Import modular components
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')
# Model arguments
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')
# Training arguments
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)')
# Data arguments - FIXED: Correct default path
parser.add_argument('--data_dir', type=str, default='data', # FIXED: Removed ../
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')
# REMOVED: refresh_cache (not implemented in data_utils)
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')
# Optimization arguments
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')
# Loss arguments
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')
# Experiment arguments
parser.add_argument('--exp_name', type=str, default=None,
help='Experiment name')
parser.add_argument('--exp_dir', type=str, default='experiments', # FIXED: Removed ../
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')
# Other arguments
parser.add_argument('--early_stopping', type=int, default=5,
help='Early stopping patience (0 to disable)')
# REMOVED: save_every (implement in Trainer if needed)
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():
# Parse arguments
args = parse_arguments()
# Set random seed
set_seed(args.seed)
print("111111")
# Get device with improved handling
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")
# Log GPU details
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}")
# Initialize basic logging to ensure create_data_loaders can log
# Only set up basic logging if no handlers exist to avoid duplication
if not logging.getLogger().handlers:
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# Create config - FIXED: Better error handling
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)
# Override with command line arguments - FIXED: Match data_utils expectations
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, # 3 decades (1980s, 1990s, 2000s)
'early_stopping': args.early_stopping,
'seed': args.seed,
'data_dir': args.data_dir,
'use_class_weights': args.class_weights,
# ADDED: Missing parameters that data_utils expects
'use_weighted_sampling': False, # Default to False
'augmentation_level': 'medium', # Default augmentation
'max_download_retries': 3, # Default retries
'download_timeout': 10, # Default timeout
'multi_task': args.multi_task,
'decade_weight': args.decade_weight,
'cluster_weight': args.cluster_weight,
'device_weight': args.device_weight,
'num_decade_classes': 5, # Number of decade classes - will be updated from data
'num_cluster_classes': 10, # Default number of cluster classes - will be updated from data
'num_device_classes': 2, # phone vs calculator
# Set appropriate monitor metric for multi-task
'monitor_metric': 'accuracy' if not args.multi_task else 'accuracy', # Uses combined accuracy for multi-task
})
# Override specific parameters if provided
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
# IMPROVED: Better validation with specific error messages
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)
# Validate multi-task specific parameters
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 configuration
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
# Create experiment name with timestamp for uniqueness
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}"
# Create experiment structure with error handling
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)
# Initialize experiment logger
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)}")
# Backup code
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}")
# First, get the actual number of classes from the data
if args.multi_task:
# For multi-task, we need to know the actual number of clusters
# We'll get this after creating the data loaders
pass
# Create data loaders first to get the actual class counts
try:
# Convert string path to Path object as expected by data_utils
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
)
# Log class names
if args.multi_task:
logger.info(f"Decade class names: {class_names['decade']}")
logger.info(f"Cluster class names: {class_names['cluster']}")
# Update config with actual number of classes
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)}")
# Create model with correct number of classes
try:
if args.multi_task:
# Multi-task model - pass num_classes as a dictionary
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:
# Single-task model
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)
# Log model info
param_count = count_parameters(model)
logger.info(f"Model parameters: {param_count['total']:,} "
f"(Trainable: {param_count['trainable']:,})")
# Create loss function
if args.multi_task:
# Multi-task loss
loss_config = config.get('loss', {})
loss_name = loss_config.get('name', 'cross_entropy')
loss_params = loss_config.get('params', {})
# Update loss params with class weights if requested
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:
# Single-task loss
loss_config = config.get('loss', {})
loss_name = loss_config.get('name', 'cross_entropy')
loss_params = loss_config.get('params', {})
# Update loss config with class weights
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)
# Create optimizer and scheduler
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)
# Create trainer
try:
trainer = Trainer(
model=model,
config=config,
device=device,
experiment_dir=exp_dirs['root'],
logger=logger,
multi_task=args.multi_task # Pass multi_task flag
)
# Set the criterion
trainer.criterion = criterion
except Exception as e:
logger.error(f"Failed to create trainer: {e}")
sys.exit(1)
# Resume from checkpoint if specified
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)
# Train model
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)
# Save final model state if specified
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}")
# Save final results
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']
)
# Create visualizations with safety check
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")
# Log best model
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}")
# Finish logging
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() |