akoulapure's picture
Initial Deployment: Best ViT Model
228add1 verified
Raw
History Blame Contribute Delete
12.6 kB
"""
Unified Trainer class that orchestrates the full training pipeline.
Takes model, dataloaders, config → trains, evaluates, saves artifacts.
"""
import sys
import time
from pathlib import Path
from typing import Optional
import torch
import torch.nn as nn
PROJECT_ROOT = Path(__file__).resolve().parents[3]
sys.path.insert(0, str(PROJECT_ROOT))
from ml.src.training.engine import train_one_epoch, validate, measure_inference_latency
from ml.src.training.losses import create_loss
from ml.src.training.optimizer_factory import create_optimizer
from ml.src.training.scheduler_factory import create_scheduler
from ml.src.training.callbacks import EarlyStopping, ModelCheckpoint, MetricLogger
from ml.src.evaluation.metrics import compute_metrics
from ml.src.evaluation.plots import (
plot_training_curves,
plot_confusion_matrix,
plot_per_class_f1,
)
from ml.src.utils.io import save_json, ensure_dirs
from ml.src.utils.seed import set_seed
class Trainer:
"""
Unified training orchestrator.
Usage:
trainer = Trainer(model, config, dataloaders, class_names, device)
trainer.train()
trainer.evaluate()
trainer.save_artifacts()
"""
def __init__(
self,
model: nn.Module,
config: dict,
dataloaders: dict,
class_names: list[str],
device: torch.device,
model_name: Optional[str] = None,
):
self.model = model.to(device)
self.config = config
self.dataloaders = dataloaders
self.class_names = class_names
self.device = device
self.model_name = model_name or config.get('model', {}).get('name', 'model')
# Training config
train_cfg = config.get('training', {})
self.num_epochs = train_cfg.get('num_epochs', 30)
self.gradient_clip = train_cfg.get('gradient_clip_max_norm', 1.0)
# Setup artifact directories
artifacts_cfg = config.get('artifacts', {})
self.checkpoint_dir = PROJECT_ROOT / artifacts_cfg.get('checkpoints_dir', 'ml/artifacts/checkpoints')
self.figures_dir = PROJECT_ROOT / artifacts_cfg.get('figures_dir', 'ml/artifacts/figures')
self.logs_dir = PROJECT_ROOT / artifacts_cfg.get('logs_dir', 'ml/artifacts/logs')
self.reports_dir = PROJECT_ROOT / artifacts_cfg.get('reports_dir', 'ml/artifacts/reports')
ensure_dirs(self.checkpoint_dir, self.figures_dir, self.logs_dir, self.reports_dir)
# Create loss
label_smoothing = train_cfg.get('label_smoothing', 0.0)
self.criterion = create_loss(label_smoothing=label_smoothing)
# Create optimizer
self.optimizer = create_optimizer(
self.model,
optimizer_name=train_cfg.get('optimizer', 'adam'),
learning_rate=train_cfg.get('learning_rate', 0.001),
weight_decay=train_cfg.get('weight_decay', 0.0001),
)
# Create scheduler
self.scheduler = create_scheduler(
self.optimizer,
scheduler_name=train_cfg.get('scheduler', 'cosine'),
num_epochs=self.num_epochs,
step_size=train_cfg.get('scheduler_step_size', 10),
gamma=train_cfg.get('scheduler_gamma', 0.1),
warmup_epochs=train_cfg.get('warmup_epochs', 0),
)
# Callbacks
self.early_stopping = EarlyStopping(
patience=train_cfg.get('early_stopping_patience', 7),
mode='min',
)
self.checkpoint = ModelCheckpoint(
save_dir=self.checkpoint_dir,
model_name=self.model_name,
mode='min',
)
self.logger = MetricLogger(
log_dir=self.logs_dir,
model_name=self.model_name,
)
# Results storage
self.test_results = None
self.training_summary = None
def train(self) -> dict:
"""Run the full training loop."""
print(f"\n{'='*60}")
print(f"Training: {self.model_name}")
print(f"Device: {self.device}")
print(f"Epochs: {self.num_epochs}")
print(f"{'='*60}\n")
set_seed(self.config.get('data', {}).get('seed', 42))
for epoch in range(1, self.num_epochs + 1):
# Train
train_metrics = train_one_epoch(
self.model, self.dataloaders['train'], self.criterion,
self.optimizer, self.device, self.gradient_clip,
)
# Validate
val_metrics = validate(
self.model, self.dataloaders['val'], self.criterion, self.device,
)
# Get current LR
current_lr = self.optimizer.param_groups[0]['lr']
# Log
self.logger.log_epoch(epoch, train_metrics, val_metrics, current_lr)
# Checkpoint
self.checkpoint(
val_metrics['loss'], self.model, self.optimizer, epoch,
extra_info={'config': self.config, 'class_names': self.class_names},
)
# Step scheduler
if self.scheduler is not None:
if isinstance(self.scheduler, torch.optim.lr_scheduler.ReduceLROnPlateau):
self.scheduler.step(val_metrics['loss'])
else:
self.scheduler.step()
# Early stopping
if self.early_stopping(val_metrics['loss']):
print(f"\nEarly stopping at epoch {epoch}")
break
# Save training history
self.logger.save()
self.training_summary = self.logger.get_summary()
print(f"\nTraining complete! Best val loss: {self.checkpoint.best_value:.4f}")
return self.training_summary
def evaluate(self, split: str = 'test') -> dict:
"""
Evaluate the best model on test set.
Loads the best checkpoint and runs full evaluation.
"""
print(f"\n{'='*60}")
print(f"Evaluating: {self.model_name} on {split}")
print(f"{'='*60}\n")
# Load best checkpoint
if self.checkpoint.best_path and self.checkpoint.best_path.exists():
ckpt = torch.load(self.checkpoint.best_path, map_location=self.device, weights_only=False)
self.model.load_state_dict(ckpt['model_state_dict'])
print(f"Loaded best checkpoint from epoch {ckpt.get('epoch', '?')}")
# Run evaluation
results = validate(
self.model, self.dataloaders[split], self.criterion, self.device,
)
# Compute detailed metrics
metrics = compute_metrics(
results['labels'], results['predictions'], self.class_names,
)
# Measure inference latency
latency = measure_inference_latency(
self.model, self.device,
img_size=self.config.get('image', {}).get('size', 224),
)
# Calculate model size
model_size_mb = sum(
p.numel() * p.element_size() for p in self.model.parameters()
) / (1024 * 1024)
self.test_results = {
'model_name': self.model_name,
'split': split,
'loss': results['loss'],
'metrics': metrics,
'latency': latency,
'model_size_mb': round(model_size_mb, 2),
'num_parameters': sum(p.numel() for p in self.model.parameters()),
}
# Print summary
print(f"\n{split.upper()} Results:")
print(f" Loss: {results['loss']:.4f}")
print(f" Accuracy: {metrics['accuracy']:.4f}")
print(f" Macro F1: {metrics['macro_f1']:.4f}")
print(f" Macro Prec: {metrics['macro_precision']:.4f}")
print(f" Macro Recall: {metrics['macro_recall']:.4f}")
print(f" Avg Latency: {latency['avg_ms']:.2f} ms")
print(f" Model Size: {model_size_mb:.2f} MB")
return self.test_results
def save_artifacts(self) -> None:
"""Save all evaluation artifacts: plots, reports."""
print(f"\nSaving artifacts for {self.model_name}...")
model_fig_dir = self.figures_dir / self.model_name
model_fig_dir.mkdir(parents=True, exist_ok=True)
# Training curves
plot_training_curves(
self.logger.history,
save_path=model_fig_dir / 'training_curves.png',
title=f'{self.model_name} - Training Curves',
)
# Confusion matrix (from test results)
if self.test_results:
labels = self.test_results.get('_raw_labels', None)
preds = self.test_results.get('_raw_predictions', None)
# Load from last validation run if needed
if labels is None:
val_results = validate(
self.model, self.dataloaders.get('test', self.dataloaders['val']),
self.criterion, self.device,
)
labels = val_results['labels']
preds = val_results['predictions']
plot_confusion_matrix(
labels, preds, self.class_names,
save_path=model_fig_dir / 'confusion_matrix.png',
title=f'{self.model_name} - Confusion Matrix',
)
# Per-class F1
if self.test_results and 'metrics' in self.test_results:
per_class = self.test_results['metrics'].get('per_class', {})
if per_class:
plot_per_class_f1(
per_class, self.class_names,
save_path=model_fig_dir / 'per_class_f1.png',
title=f'{self.model_name} - Per-Class F1 Score',
)
# Save detailed report JSON
if self.test_results:
report = {**self.test_results}
report.pop('_raw_labels', None)
report.pop('_raw_predictions', None)
save_json(report, self.reports_dir / f'{self.model_name}_report.json')
# Save training summary
if self.training_summary:
save_json(
self.training_summary,
self.reports_dir / f'{self.model_name}_training_summary.json',
)
print(f" Artifacts saved to {self.figures_dir / self.model_name}")
def train_with_phase_switch(
self,
phase1_epochs: int = 10,
phase2_lr: float = 0.0001,
unfreeze_layers: list[str] = None,
) -> dict:
"""
Two-phase training for transfer learning models.
Phase 1: Frozen backbone, train head only.
Phase 2: Unfreeze specified layers, use lower LR.
"""
print(f"\n{'='*60}")
print(f"Phase 1: Frozen backbone training ({phase1_epochs} epochs)")
print(f"{'='*60}\n")
# Phase 1
original_epochs = self.num_epochs
self.num_epochs = phase1_epochs
self.train()
# Phase 2: Unfreeze
remaining_epochs = original_epochs - phase1_epochs
if remaining_epochs > 0 and hasattr(self.model, 'unfreeze_layers'):
print(f"\n{'='*60}")
print(f"Phase 2: Fine-tuning ({remaining_epochs} epochs)")
print(f"{'='*60}\n")
self.model.unfreeze_layers(unfreeze_layers)
# Use param groups if available
if hasattr(self.model, 'get_param_groups'):
base_lr = self.config.get('training', {}).get('learning_rate', 0.001)
param_groups = self.model.get_param_groups(base_lr, phase2_lr)
self.optimizer = create_optimizer(
param_groups,
optimizer_name=self.config.get('training', {}).get('optimizer', 'adamw'),
learning_rate=base_lr,
weight_decay=self.config.get('training', {}).get('weight_decay', 0.0001),
)
else:
for pg in self.optimizer.param_groups:
pg['lr'] = phase2_lr
# Reset scheduler and early stopping
self.scheduler = create_scheduler(
self.optimizer,
scheduler_name=self.config.get('training', {}).get('scheduler', 'cosine'),
num_epochs=remaining_epochs,
)
self.early_stopping = EarlyStopping(
patience=self.config.get('training', {}).get('early_stopping_patience', 7),
)
self.num_epochs = remaining_epochs
self.train()
self.num_epochs = original_epochs
return self.logger.get_summary()