Machine learning / Regularization overfitting and optimization
Early Stopping Preventing Overfitting in Machine Learning
Contents
- Understanding Early Stopping Core Concepts
- Basic Implementation with PyTorch
- Implementing Learning Curves
- Mathematical Foundation of Early Stopping
- Custom Early Stopping Criteria
- Real-world Implementation with Neural Network
- Source Code for Real-world Implementation
- Performance Metrics Implementation
- Adaptive Early Stopping
- Cross-Validation Integration with Early Stopping
- Visualization of Training Dynamics
- Hyperparameter Optimization for Early Stopping
- Results Analysis and Model Selection
- Additional Resources
Understanding Early Stopping Core Concepts
Early stopping serves as a regularization technique that monitors model performance during training by evaluating a validation set after each epoch. When the validation error starts to increase while training error continues to decrease, it indicates potential overfitting.
class EarlyStop:
def __init__(self, patience=5, min_delta=0.001):
self.patience = patience
self.min_delta = min_delta
self.counter = 0
self.best_loss = None
self.should_stop = False
def __call__(self, validation_loss):
if self.best_loss is None:
self.best_loss = validation_loss
elif validation_loss > self.best_loss - self.min_delta:
self.counter += 1
if self.counter >= self.patience:
self.should_stop = True
else:
self.best_loss = validation_loss
self.counter = 0
Basic Implementation with PyTorch
Early stopping requires tracking validation metrics across epochs and implementing a callback mechanism to halt training when validation performance deteriorates consistently over a specified period.
import torch
import torch.nn as nn
import numpy as np
def train_with_early_stopping(model, train_loader, val_loader, epochs=100):
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters())
early_stop = EarlyStop(patience=5)
for epoch in range(epochs):
model.train()
train_loss = 0
for X, y in train_loader:
optimizer.zero_grad()
output = model(X)
loss = criterion(output, y)
loss.backward()
optimizer.step()
train_loss += loss.item()
# Validation phase
model.eval()
val_loss = 0
with torch.no_grad():
for X, y in val_loader:
output = model(X)
val_loss += criterion(output, y).item()
early_stop(val_loss)
if early_stop.should_stop:
print(f"Early stopping triggered at epoch {epoch}")
break
Implementing Learning Curves
Learning curves provide visual feedback on model training progress and help identify the optimal stopping point by plotting training and validation losses over time.
import matplotlib.pyplot as plt
def plot_learning_curves(train_losses, val_losses):
plt.figure(figsize=(10, 6))
plt.plot(train_losses, label='Training Loss')
plt.plot(val_losses, label='Validation Loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.title('Learning Curves')
plt.legend()
# Add stopping point annotation
stop_epoch = np.argmin(val_losses)
plt.axvline(x=stop_epoch, color='r', linestyle='--')
plt.text(stop_epoch+1, plt.ylim()[0], f'Optimal Stop: {stop_epoch}')
plt.show()
Mathematical Foundation of Early Stopping
The theoretical basis for early stopping relates to the bias-variance tradeoff and can be expressed through mathematical formulations that demonstrate how training time affects model complexity.
"""
Key Mathematical Concepts in Early Stopping:
Generalization Error Decomposition:
$$E(x) = E_{bias}(x) + E_{var}(x) + \epsilon$$
Effective Number of Parameters:
$$P_{eff}(t) = P_{\infty} (1 - e^{-\alpha t})$$
where:
- t is training time
- P_∞ is asymptotic number of parameters
- α is the learning rate
"""
def calculate_effective_params(t, p_inf, alpha):
return p_inf * (1 - np.exp(-alpha * t))
Custom Early Stopping Criteria
Advanced early stopping implementations often require custom stopping criteria based on multiple metrics or complex conditions specific to the problem domain.
class CustomEarlyStopping:
def __init__(self, patience=5, min_delta=0.001, monitor_metrics=['loss', 'accuracy']):
self.patience = patience
self.min_delta = min_delta
self.monitor_metrics = monitor_metrics
self.best_metrics = {metric: None for metric in monitor_metrics}
self.counters = {metric: 0 for metric in monitor_metrics}
def check_stopping(self, metrics_dict):
should_stop = []
for metric in self.monitor_metrics:
current_value = metrics_dict[metric]
if self.best_metrics[metric] is None:
self.best_metrics[metric] = current_value
elif self._is_worse(current_value, self.best_metrics[metric], metric):
self.counters[metric] += 1
should_stop.append(self.counters[metric] >= self.patience)
else:
self.best_metrics[metric] = current_value
self.counters[metric] = 0
return all(should_stop)
def _is_worse(self, current, best, metric):
if metric == 'loss':
return current > best - self.min_delta
return current < best + self.min_delta
Real-world Implementation with Neural Network
This implementation demonstrates a complete neural network training pipeline with early stopping, including data preprocessing, model architecture, and training loop for a regression problem.
import torch.nn as nn
import torch.optim as optim
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
import numpy as np
class RegressionModel(nn.Module):
def __init__(self, input_dim):
super().__init__()
self.layers = nn.Sequential(
nn.Linear(input_dim, 64),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(64, 32),
nn.ReLU(),
nn.Linear(32, 1)
)
def forward(self, x):
return self.layers(x)
# Data preparation and training
X = np.random.randn(1000, 10) # Example dataset
y = np.sum(X, axis=1) + np.random.randn(1000) * 0.1
# Preprocessing
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
X_train, X_val, y_train, y_val = train_test_split(X_scaled, y, test_size=0.2)
# Convert to tensors
X_train = torch.FloatTensor(X_train)
y_train = torch.FloatTensor(y_train).reshape(-1, 1)
X_val = torch.FloatTensor(X_val)
y_val = torch.FloatTensor(y_val).reshape(-1, 1)
Source Code for Real-world Implementation
def train_model(X_train, y_train, X_val, y_val, model, epochs=1000):
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
early_stopping = EarlyStop(patience=10, min_delta=1e-4)
train_losses = []
val_losses = []
for epoch in range(epochs):
# Training phase
model.train()
optimizer.zero_grad()
outputs = model(X_train)
loss = criterion(outputs, y_train)
loss.backward()
optimizer.step()
train_losses.append(loss.item())
# Validation phase
model.eval()
with torch.no_grad():
val_outputs = model(X_val)
val_loss = criterion(val_outputs, y_val)
val_losses.append(val_loss.item())
# Early stopping check
early_stopping(val_loss.item())
if early_stopping.should_stop:
print(f"Training stopped at epoch {epoch}")
break
return train_losses, val_losses
# Initialize and train model
model = RegressionModel(input_dim=10)
train_losses, val_losses = train_model(X_train, y_train, X_val, y_val, model)
Performance Metrics Implementation
The evaluation of early stopping effectiveness requires comprehensive metrics tracking across different aspects of model performance during training.
class PerformanceTracker:
def __init__(self):
self.metrics = {
'train_loss': [],
'val_loss': [],
'val_r2': [],
'generalization_gap': []
}
def update(self, train_loss, val_loss, y_true, y_pred):
from sklearn.metrics import r2_score
r2 = r2_score(y_true.detach().numpy(), y_pred.detach().numpy())
gen_gap = abs(train_loss - val_loss)
self.metrics['train_loss'].append(train_loss)
self.metrics['val_loss'].append(val_loss)
self.metrics['val_r2'].append(r2)
self.metrics['generalization_gap'].append(gen_gap)
def plot_metrics(self):
fig, axes = plt.subplots(2, 1, figsize=(10, 12))
# Loss curves
axes[0].plot(self.metrics['train_loss'], label='Training Loss')
axes[0].plot(self.metrics['val_loss'], label='Validation Loss')
axes[0].set_title('Loss Curves')
axes[0].legend()
# Generalization gap
axes[1].plot(self.metrics['generalization_gap'], label='Generalization Gap')
axes[1].set_title('Generalization Gap Over Time')
axes[1].legend()
plt.tight_layout()
plt.show()
Adaptive Early Stopping
This advanced implementation adjusts stopping criteria based on the training dynamics and model complexity, providing more flexible control over the stopping decision.
class AdaptiveEarlyStopping:
def __init__(self, initial_patience=5, max_patience=15):
self.initial_patience = initial_patience
self.max_patience = max_patience
self.current_patience = initial_patience
self.best_loss = float('inf')
self.counter = 0
self.slope_window = []
def calculate_trend(self, validation_losses, window_size=5):
if len(validation_losses) < window_size:
return 0
recent_losses = validation_losses[-window_size:]
x = np.arange(window_size)
slope, _ = np.polyfit(x, recent_losses, 1)
return slope
def __call__(self, val_loss, validation_losses):
trend = self.calculate_trend(validation_losses)
# Adjust patience based on loss trend
if abs(trend) < 0.001:
self.current_patience = min(self.current_patience + 1, self.max_patience)
else:
self.current_patience = max(self.initial_patience, self.current_patience - 1)
if val_loss < self.best_loss:
self.best_loss = val_loss
self.counter = 0
else:
self.counter += 1
return self.counter >= self.current_patience
Cross-Validation Integration with Early Stopping
Early stopping becomes more robust when integrated with k-fold cross-validation, providing a more reliable estimate of the optimal stopping point across different data splits.
from sklearn.model_selection import KFold
import numpy as np
class CrossValidatedEarlyStopping:
def __init__(self, n_splits=5):
self.n_splits = n_splits
self.kf = KFold(n_splits=n_splits, shuffle=True)
self.fold_stopping_epochs = []
def find_optimal_epochs(self, X, y, model_class, max_epochs=1000):
X, y = np.array(X), np.array(y)
for fold, (train_idx, val_idx) in enumerate(self.kf.split(X)):
X_train, X_val = X[train_idx], X[val_idx]
y_train, y_val = y[train_idx], y[val_idx]
model = model_class()
early_stop = EarlyStop(patience=5)
for epoch in range(max_epochs):
train_loss = self._train_epoch(model, X_train, y_train)
val_loss = self._validate_epoch(model, X_val, y_val)
early_stop(val_loss)
if early_stop.should_stop:
self.fold_stopping_epochs.append(epoch)
break
return int(np.median(self.fold_stopping_epochs))
Visualization of Training Dynamics
Advanced visualization techniques help understand the relationship between early stopping and model behavior, including loss landscapes and parameter trajectories.
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
class TrainingVisualizer:
def __init__(self):
self.history = {
'train_loss': [],
'val_loss': [],
'param_trajectory': [],
'gradients': []
}
def record_state(self, model, train_loss, val_loss):
params = torch.cat([p.data.view(-1) for p in model.parameters()])
grads = torch.cat([p.grad.view(-1) for p in model.parameters()])
self.history['train_loss'].append(train_loss)
self.history['val_loss'].append(val_loss)
self.history['param_trajectory'].append(params.clone().cpu().numpy())
self.history['gradients'].append(grads.clone().cpu().numpy())
def plot_loss_landscape(self):
fig = plt.figure(figsize=(12, 8))
ax = fig.add_subplot(111, projection='3d')
epochs = np.arange(len(self.history['train_loss']))
param_norm = [np.linalg.norm(p) for p in self.history['param_trajectory']]
ax.plot3D(epochs, param_norm, self.history['train_loss'], 'b-', label='Training Loss')
ax.plot3D(epochs, param_norm, self.history['val_loss'], 'r-', label='Validation Loss')
ax.set_xlabel('Epochs')
ax.set_ylabel('Parameter Norm')
ax.set_zlabel('Loss')
ax.legend()
plt.show()
Hyperparameter Optimization for Early Stopping
Implementing a systematic approach to optimize early stopping hyperparameters using Bayesian optimization ensures optimal stopping criteria for specific problems.
from sklearn.gaussian_process import GaussianProcessRegressor
from sklearn.gaussian_process.kernels import Matern
class EarlyStoppingOptimizer:
def __init__(self, param_space, n_iterations=50):
self.param_space = param_space
self.n_iterations = n_iterations
self.gp = GaussianProcessRegressor(
kernel=Matern(nu=2.5),
n_restarts_optimizer=10,
random_state=42
)
self.X_observed = []
self.y_observed = []
def optimize(self, objective_function):
for i in range(self.n_iterations):
# Sample parameters using Thompson sampling
if len(self.X_observed) > 0:
next_point = self._thompson_sampling()
else:
next_point = self._random_sample()
# Evaluate parameters
score = objective_function(next_point)
# Update observations
self.X_observed.append(next_point)
self.y_observed.append(score)
# Update Gaussian Process
self.gp.fit(np.array(self.X_observed), np.array(self.y_observed))
best_idx = np.argmin(self.y_observed)
return self.X_observed[best_idx]
def _thompson_sampling(self):
candidates = self._generate_candidates(100)
samples = self.gp.sample_y(candidates, n_samples=1)
best_idx = np.argmin(samples)
return candidates[best_idx]
Results Analysis and Model Selection
A comprehensive framework for analyzing early stopping results and selecting the best model based on multiple criteria ensures optimal model selection.
class ModelSelector:
def __init__(self, metrics_weights={'val_loss': 0.4,
'stability': 0.3,
'generalization': 0.3}):
self.metrics_weights = metrics_weights
self.results = []
def evaluate_model(self, model, train_history, val_history):
stability_score = self._calculate_stability(val_history)
generalization_score = self._calculate_generalization_gap(
train_history[-10:], val_history[-10:]
)
final_score = (
self.metrics_weights['val_loss'] * val_history[-1] +
self.metrics_weights['stability'] * stability_score +
self.metrics_weights['generalization'] * generalization_score
)
self.results.append({
'model': model,
'final_score': final_score,
'val_loss': val_history[-1],
'stability': stability_score,
'generalization': generalization_score
})
def get_best_model(self):
best_idx = np.argmin([r['final_score'] for r in self.results])
return self.results[best_idx]['model']
def _calculate_stability(self, val_history):
return np.std(val_history[-10:])
def _calculate_generalization_gap(self, train_hist, val_hist):
return np.mean(np.abs(np.array(train_hist) - np.array(val_hist)))
Additional Resources
- https://arxiv.org/abs/1812.05162 - "A Theoretical Framework for Early Stopping in Deep Neural Networks"
- https://arxiv.org/abs/2007.15191 - "Understanding Early Stopping and its Effect on Model Generalization"
- https://arxiv.org/abs/1903.08848 - "Revisiting Early Stopping in the Era of Deep Learning"
- https://arxiv.org/abs/2012.07175 - "Adaptive Early Stopping for Deep Neural Networks"
- https://arxiv.org/abs/1908.04928 - "On the Convergence Properties of Early Stopping in Neural Networks"