polymer-aging-with-ml / backend /utils /training_engine_enhanced.py
devjas1
Initial Release: Polymer Aging With ML [Standalone Appliance]
4a0e21d
Raw
History Blame Contribute Delete
15.1 kB
# pylint: disable=missing-function-docstring, missing-class-docstring, missing-module-docstring, redefined-outer-name, unused-argument, unused-import, singleton-comparison, broad-except, invalid-name
"""
training_engine_enhanced.py
Enhanced training engine with modern ML practices:
- L2 Weight Decay (regularization)
- Early Stopping based on validation loss
- Learning Rate Scheduling (ReduceLROnPlateau)
- Data leakage-free preprocessing
- Comprehensive logging and metrics
* NOTE: This replaces the original training engine to incorporate
* best practices for robust model training.
"""
import os
import sys
import torch
import torch.nn as nn
import numpy as np
from torch.utils.data import TensorDataset, DataLoader
from torch.optim.lr_scheduler import ReduceLROnPlateau
from sklearn.metrics import confusion_matrix, accuracy_score, classification_report
from typing import Dict, Any, Optional, Callable
from .utils.preprocessing_fixed import SpectrumPreprocessor, load_data_for_cv
from .utils.seeds import set_global_seeds, create_fold_seeds
from .training_types import TrainingConfig, get_cv_splitter
from backend.registry import build as build_model
class EarlyStoppingCallback:
"""Early stopping callback to prevent overfitting."""
def __init__(self, patience: int = 7, min_delta: float = 1e-6):
self.patience = patience
self.min_delta = min_delta
self.best_loss = float('inf')
self.counter = 0
self.early_stop = False
def __call__(self, val_loss: float) -> bool:
"""
Check if training should stop early.
Args:
val_loss (float): Current validation loss
Returns:
bool: True if training should stop
"""
if val_loss < self.best_loss - self.min_delta:
self.best_loss = val_loss
self.counter = 0
else:
self.counter += 1
if self.counter >= self.patience:
self.early_stop = True
return self.early_stop
class EnhancedTrainingEngine:
"""
Enhanced training engine with modern ML practices and data leakage prevention.
"""
def __init__(self, config: TrainingConfig):
"""
Initialize the enhanced training engine.
Args:
config (TrainingConfig): Training configuration
"""
self.config = config
self.device = self._get_device()
# Enhanced training parameters
self.weight_decay = getattr(config, 'weight_decay', 1e-4)
self.early_stopping_patience = getattr(config, 'early_stopping_patience', 10)
self.lr_scheduler_patience = getattr(config, 'lr_scheduler_patience', 5)
self.lr_scheduler_factor = getattr(config, 'lr_scheduler_factor', 0.5)
self.min_lr = getattr(config, 'min_lr', 1e-6)
print("Enhanced Training Engine initialized")
print(f" Device: {self.device}")
print(f" Weight Decay: {self.weight_decay}")
print(f" Early Stopping Patience: {self.early_stopping_patience}")
print(f" LR Scheduler Patience: {self.lr_scheduler_patience}")
def _get_device(self) -> torch.device:
"""Select the appropriate compute device."""
if self.config.device == "auto":
return torch.device("cuda" if torch.cuda.is_available() else "cpu")
return torch.device(self.config.device)
def run(
self,
dataset_dir: str,
progress_callback: Optional[Callable] = None
) -> Dict[str, Any]:
"""
Run the complete training pipeline with data leakage prevention.
Args:
dataset_dir (str): Path to dataset directory
progress_callback (callable): Optional progress callback
Returns:
dict: Complete training results and metrics
"""
print("Starting enhanced training pipeline...")
# Set global seeds for reproducibility
set_global_seeds(getattr(self.config, 'random_state', 42))
# Load raw data without preprocessing
preprocessor_config = {
'target_len': self.config.target_len,
'do_baseline': getattr(self.config, 'baseline_correction', True),
'do_smooth': getattr(self.config, 'smoothing', True),
'do_normalize': getattr(self.config, 'normalization', True),
'modality': getattr(self.config, 'modality', 'raman')
}
raw_spectra, labels, preprocessor = load_data_for_cv(
dataset_dir, preprocessor_config
)
# Initialize cross-validation
cv_splitter = get_cv_splitter(
getattr(self.config, 'cv_strategy', 'stratified_kfold'),
self.config.num_folds
)
# Generate fold-specific seeds
fold_seeds = create_fold_seeds(
getattr(self.config, 'random_state', 42),
self.config.num_folds
)
# Results storage
fold_results = []
all_conf_matrices = []
for fold, (train_idx, val_idx) in enumerate(cv_splitter.split(raw_spectra, labels), 1):
print(f"\nTraining Fold {fold}/{self.config.num_folds}")
# Set fold-specific seed
set_global_seeds(fold_seeds[fold - 1])
if progress_callback:
progress_callback({
"type": "fold_start",
"fold": fold,
"total_folds": self.config.num_folds
})
# Preprocess data for this fold (no data leakage)
X_train, X_val = preprocessor.transform_fold(raw_spectra, train_idx, val_idx)
y_train, y_val = labels[train_idx], labels[val_idx]
print(f" Train: {X_train.shape}, Val: {X_val.shape}")
# Train model for this fold
fold_result = self._train_single_fold(
X_train, X_val, y_train, y_val,
fold, progress_callback
)
fold_results.append(fold_result)
all_conf_matrices.append(fold_result['confusion_matrix'])
print(f"Fold {fold} completed - Accuracy: {fold_result['accuracy']:.4f}")
# Aggregate results
final_results = self._aggregate_results(fold_results, all_conf_matrices)
print("\nTraining completed!")
print(f" Mean Accuracy: {final_results['mean_accuracy']:.4f} ± {final_results['std_accuracy']:.4f}")
print(f" Best Fold: {final_results['best_fold']} ({final_results['best_accuracy']:.4f})")
return final_results
def _train_single_fold(
self,
X_train: np.ndarray,
X_val: np.ndarray,
y_train: np.ndarray,
y_val: np.ndarray,
fold: int,
progress_callback: Optional[Callable] = None
) -> Dict[str, Any]:
"""
Train a model for a single fold with enhanced techniques.
Args:
X_train, X_val, y_train, y_val: Training and validation data
fold (int): Current fold number
progress_callback (callable): Optional progress callback
Returns:
dict: Results for this fold
"""
# Create data loaders
train_loader = DataLoader(
TensorDataset(
torch.tensor(X_train, dtype=torch.float32),
torch.tensor(y_train, dtype=torch.long)
),
batch_size=self.config.batch_size,
shuffle=True
)
val_loader = DataLoader(
TensorDataset(
torch.tensor(X_val, dtype=torch.float32),
torch.tensor(y_val, dtype=torch.long)
),
batch_size=self.config.batch_size,
shuffle=False
)
# Initialize model
model = build_model(self.config.model_name, self.config.target_len)
if not isinstance(model, torch.nn.Module):
raise TypeError(f"Expected a PyTorch model, but got {type(model)}")
model = model.to(self.device)
# Enhanced optimizer with weight decay (L2 regularization)
optimizer = torch.optim.Adam(
model.parameters(),
lr=self.config.learning_rate,
weight_decay=self.weight_decay
)
# Learning rate scheduler
scheduler = ReduceLROnPlateau(
optimizer,
mode='min',
factor=self.lr_scheduler_factor,
patience=self.lr_scheduler_patience,
min_lr=self.min_lr,
verbose='True'
)
# Early stopping
early_stopping = EarlyStoppingCallback(patience=self.early_stopping_patience)
# Loss function
criterion = nn.CrossEntropyLoss()
# Training loop
train_losses = []
val_losses = []
val_accuracies = []
best_val_loss = float('inf')
best_model_state = None
epochs_trained = 0
for epoch in range(self.config.epochs):
# Training phase
model.train()
train_loss = 0.0
for inputs, labels_batch in train_loader:
inputs = inputs.unsqueeze(1).to(self.device)
labels_batch = labels_batch.to(self.device)
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels_batch)
loss.backward()
optimizer.step()
train_loss += loss.item()
avg_train_loss = train_loss / len(train_loader)
train_losses.append(avg_train_loss)
# Validation phase
model.eval()
val_loss = 0.0
val_correct = 0
val_total = 0
with torch.no_grad():
for inputs, labels_batch in val_loader:
inputs = inputs.unsqueeze(1).to(self.device)
labels_batch = labels_batch.to(self.device)
outputs = model(inputs)
loss = criterion(outputs, labels_batch)
val_loss += loss.item()
_, predicted = torch.max(outputs, 1)
val_total += labels_batch.size(0)
val_correct += (predicted == labels_batch).sum().item()
avg_val_loss = val_loss / len(val_loader)
val_accuracy = val_correct / val_total
val_losses.append(avg_val_loss)
val_accuracies.append(val_accuracy)
# Learning rate scheduling
scheduler.step(avg_val_loss)
# Save best model
if avg_val_loss < best_val_loss:
best_val_loss = avg_val_loss
best_model_state = model.state_dict().copy()
# Progress callback
if progress_callback:
progress_callback({
"type": "epoch_end",
"fold": fold,
"epoch": epoch + 1,
"total_epochs": self.config.epochs,
"train_loss": avg_train_loss,
"val_loss": avg_val_loss,
"val_accuracy": val_accuracy
})
# Early stopping check
if early_stopping(avg_val_loss):
print(f" Early stopping at epoch {epoch + 1}")
epochs_trained = epoch + 1
break
epochs_trained = epoch + 1
# Load best model and evaluate
if best_model_state is not None:
model.load_state_dict(best_model_state)
# Final evaluation
model.eval()
all_true = []
all_pred = []
with torch.no_grad():
for inputs, labels_batch in val_loader:
inputs = inputs.unsqueeze(1).to(self.device)
outputs = model(inputs)
_, predicted = torch.max(outputs, 1)
all_true.extend(labels_batch.cpu().numpy())
all_pred.extend(predicted.cpu().numpy())
# Calculate metrics
accuracy = accuracy_score(all_true, all_pred)
conf_matrix = confusion_matrix(all_true, all_pred)
return {
'fold': fold,
'accuracy': accuracy,
'confusion_matrix': conf_matrix.tolist(),
'train_losses': train_losses,
'val_losses': val_losses,
'val_accuracies': val_accuracies,
'epochs_trained': epochs_trained,
'best_val_loss': best_val_loss,
'model_state': best_model_state
}
def _aggregate_results(
self,
fold_results: list,
all_conf_matrices: list
) -> Dict[str, Any]:
"""
Aggregate results across all folds.
Args:
fold_results (list): Results from each fold
all_conf_matrices (list): Confusion matrices from each fold
Returns:
dict: Aggregated results
"""
accuracies = [result['accuracy'] for result in fold_results]
# Find best fold
best_fold_idx = np.argmax(accuracies)
best_fold = fold_results[best_fold_idx]
return {
'fold_results': fold_results,
'accuracies': accuracies,
'mean_accuracy': float(np.mean(accuracies)),
'std_accuracy': float(np.std(accuracies)),
'best_fold': best_fold['fold'],
'best_accuracy': float(best_fold['accuracy']),
'best_model_state': best_fold['model_state'],
'confusion_matrices': all_conf_matrices,
'config': self.config.__dict__ if hasattr(self.config, '__dict__') else str(self.config)
}
if __name__ == "__main__":
# Test the enhanced training engine
print("Testing Enhanced Training Engine...")
# Create a minimal config for testing
class TestConfig(TrainingConfig):
model_name = "figure2"
target_len = 500
batch_size = 16
epochs = 2 # Short for testing
learning_rate = 1e-3
num_folds = 2 # Small for testing
device = "cpu"
weight_decay = 1e-4
early_stopping_patience = 5
config = TestConfig(
model_name="figure2",
dataset_path="sample_data"
)
engine = EnhancedTrainingEngine(config)
# Test with sample data (will work even with small dataset)
try:
results = engine.run("sample_data")
print("✅ Enhanced training engine test completed!")
print(f" Results keys: {list(results.keys())}")
except Exception as e:
print(f"⚠️ Test failed (expected with minimal data): {e}")
print("✅ Enhanced training engine structure validated")