1. Learning Objectives

By the end of this lesson, you will be able to:

  • Implement and train neural networks in PyTorch with proper training loops.

  • Apply advanced regularisation techniques (dropout, batch normalisation, weight decay, data augmentation).

  • Implement learning rate schedules (Step, Cosine, ReduceLROnPlateau) for financial models.

  • Use advanced optimisers (Adam, AdamW, RMSprop, SGD with momentum) effectively.

  • Implement early stopping and model checkpointing for robust training.

  • Apply data augmentation techniques for financial time series (time warping, jittering, scaling).

  • Understand the importance of hyperparameter tuning for neural networks.

  • Avoid common pitfalls in training financial neural networks.


2. The Complete Training Pipeline

2.1 Data Preparation

text
import torch
from torch.utils.data import DataLoader, TensorDataset
from sklearn.preprocessing import StandardScaler

def prepare_financial_data(X_train, y_train, X_val, y_val, X_test, y_test, batch_size=128):
    """
    Prepare data for neural network training.
    """
    # Convert to tensors
    X_train_t = torch.FloatTensor(X_train.values)
    y_train_t = torch.FloatTensor(y_train.values)
    X_val_t = torch.FloatTensor(X_val.values)
    y_val_t = torch.FloatTensor(y_val.values)
    X_test_t = torch.FloatTensor(X_test.values)
    y_test_t = torch.FloatTensor(y_test.values)

    # Create datasets
    train_dataset = TensorDataset(X_train_t, y_train_t)
    val_dataset = TensorDataset(X_val_t, y_val_t)
    test_dataset = TensorDataset(X_test_t, y_test_t)

    # Create dataloaders
    train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, pin_memory=True)
    val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False, pin_memory=True)
    test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False, pin_memory=True)

    return train_loader, val_loader, test_loader

2.2 Model Definition

text
import torch.nn as nn
import torch.nn.functional as F

class FinancialNN(nn.Module):
    def __init__(self, input_dim, hidden_dims, output_dim, dropout_rate=0.3, use_batch_norm=True):
        super(FinancialNN, self).__init__()

        layers = []
        prev_dim = input_dim

        for hidden_dim in hidden_dims:
            layers.append(nn.Linear(prev_dim, hidden_dim))
            if use_batch_norm:
                layers.append(nn.BatchNorm1d(hidden_dim))
            layers.append(nn.ReLU())
            layers.append(nn.Dropout(dropout_rate))
            prev_dim = hidden_dim

        layers.append(nn.Linear(prev_dim, output_dim))

        self.network = nn.Sequential(*layers)

        # Weight initialisation
        self._init_weights()

    def _init_weights(self):
        for m in self.modules():
            if isinstance(m, nn.Linear):
                nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='relu')
                if m.bias is not None:
                    nn.init.constant_(m.bias, 0)

    def forward(self, x):
        return self.network(x)

2.3 The Complete Training Loop

text
import torch.optim as optim
from torch.optim.lr_scheduler import ReduceLROnPlateau, CosineAnnealingLR, StepLR

def train_neural_network(model, train_loader, val_loader, criterion, epochs=100,
                         lr=0.001, weight_decay=1e-5, patience=20,
                         scheduler_type='plateau', checkpoint_path='best_model.pth'):
    """
    Complete training loop with early stopping and checkpointing.
    """
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    model.to(device)

    # Optimiser
    optimizer = optim.Adam(model.parameters(), lr=lr, weight_decay=weight_decay, betas=(0.9, 0.999))

    # Learning rate scheduler
    if scheduler_type == 'plateau':
        scheduler = ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=10, verbose=True)
    elif scheduler_type == 'cosine':
        scheduler = CosineAnnealingLR(optimizer, T_max=epochs, eta_min=1e-6)
    elif scheduler_type == 'step':
        scheduler = StepLR(optimizer, step_size=30, gamma=0.1)
    else:
        scheduler = None

    # Training history
    history = {'train_loss': [], 'val_loss': [], 'lr': []}

    best_val_loss = float('inf')
    patience_counter = 0

    for epoch in range(epochs):
        # Training phase
        model.train()
        train_loss = 0.0
        for X_batch, y_batch in train_loader:
            X_batch = X_batch.to(device)
            y_batch = y_batch.to(device)

            # Forward pass
            y_pred = model(X_batch)
            loss = criterion(y_pred.squeeze(), y_batch)

            # Backward pass
            optimizer.zero_grad()
            loss.backward()

            # Gradient clipping
            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

            optimizer.step()

            train_loss += loss.item() * X_batch.size(0)

        train_loss /= len(train_loader.dataset)
        history['train_loss'].append(train_loss)

        # Validation phase
        model.eval()
        val_loss = 0.0
        with torch.no_grad():
            for X_batch, y_batch in val_loader:
                X_batch = X_batch.to(device)
                y_batch = y_batch.to(device)

                y_pred = model(X_batch)
                loss = criterion(y_pred.squeeze(), y_batch)
                val_loss += loss.item() * X_batch.size(0)

        val_loss /= len(val_loader.dataset)
        history['val_loss'].append(val_loss)

        # Learning rate scheduler
        if scheduler_type == 'plateau':
            scheduler.step(val_loss)
        elif scheduler is not None:
            scheduler.step()

        # Record learning rate
        history['lr'].append(optimizer.param_groups[0]['lr'])

        # Early stopping and checkpointing
        if val_loss < best_val_loss:
            best_val_loss = val_loss
            patience_counter = 0
            torch.save({
                'epoch': epoch,
                'model_state_dict': model.state_dict(),
                'optimizer_state_dict': optimizer.state_dict(),
                'val_loss': val_loss,
                'history': history
            }, checkpoint_path)
        else:
            patience_counter += 1
            if patience_counter >= patience:
                print(f"Early stopping at epoch {epoch}")
                break

        if epoch % 10 == 0:
            print(f"Epoch {epoch}: train_loss={train_loss:.6f}, val_loss={val_loss:.6f}, lr={optimizer.param_groups[0]['lr']:.6f}")

    # Load best model
    checkpoint = torch.load(checkpoint_path)
    model.load_state_dict(checkpoint['model_state_dict'])

    return model, history

3. Regularisation Techniques for Financial Neural Networks

3.1 L1 and L2 Regularisation (Weight Decay)

text
# L2 regularisation (built into optimiser)
optimizer = optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-5)

# L1 regularisation (manual)
def l1_regularisation(model, lambda_l1=1e-5):
    l1_norm = sum(p.abs().sum() for p in model.parameters())
    return lambda_l1 * l1_norm

# In training loop:
loss = criterion(y_pred, y_batch) + l1_regularisation(model)

3.2 Dropout – Preventing Co-Adaptation

text
class DropoutNN(nn.Module):
    def __init__(self, input_dim, hidden_dims, output_dim, dropout_rate=0.5):
        super(DropoutNN, self).__init__()
        layers = []
        prev_dim = input_dim
        for hidden_dim in hidden_dims:
            layers.append(nn.Linear(prev_dim, hidden_dim))
            layers.append(nn.ReLU())
            layers.append(nn.Dropout(dropout_rate))
            prev_dim = hidden_dim
        layers.append(nn.Linear(prev_dim, output_dim))
        self.network = nn.Sequential(*layers)

    def forward(self, x):
        return self.network(x)

3.3 Batch Normalisation – Stabilising Training

text
class BatchNormNN(nn.Module):
    def __init__(self, input_dim, hidden_dims, output_dim):
        super(BatchNormNN, self).__init__()
        layers = []
        prev_dim = input_dim
        for hidden_dim in hidden_dims:
            layers.append(nn.Linear(prev_dim, hidden_dim))
            layers.append(nn.BatchNorm1d(hidden_dim))
            layers.append(nn.ReLU())
            prev_dim = hidden_dim
        layers.append(nn.Linear(prev_dim, output_dim))
        self.network = nn.Sequential(*layers)

    def forward(self, x):
        return self.network(x)

3.4 Data Augmentation for Financial Time Series

Data augmentation creates synthetic training samples to improve generalisation.

text
def augment_financial_time_series(X, y, augmentation_methods=['jitter', 'scale', 'time_warp']):
    """
    Augment financial time series data.
    """
    X_aug = []
    y_aug = []

    for X_i, y_i in zip(X, y):
        # Original
        X_aug.append(X_i)
        y_aug.append(y_i)

        if 'jitter' in augmentation_methods:
            # Add Gaussian noise
            noise = np.random.normal(0, 0.01, X_i.shape)
            X_aug.append(X_i + noise)
            y_aug.append(y_i)

        if 'scale' in augmentation_methods:
            # Scale by random factor
            scale = np.random.uniform(0.9, 1.1)
            X_aug.append(X_i * scale)
            y_aug.append(y_i)

        if 'time_warp' in augmentation_methods:
            # Time warping (simplified)
            if len(X_i) > 10:
                warp_factor = np.random.uniform(0.9, 1.1)
                indices = np.arange(len(X_i))
                warped_indices = np.linspace(0, len(X_i)-1, int(len(X_i) * warp_factor))
                warped_indices = np.clip(warped_indices, 0, len(X_i)-1).astype(int)
                X_aug.append(X_i[warped_indices])
                y_aug.append(y_i)

    return np.array(X_aug), np.array(y_aug)

4. Learning Rate Schedulers – Optimising Convergence

4.1 StepLR – Step Decay
Reduces learning rate by gamma every step_size epochs.
lr = lr * gamma^{floor(epoch / step_size)}

text
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1)

4.2 ReduceLROnPlateau – Adaptive Decay
Reduces learning rate when a metric has stopped improving.

text
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
    optimizer, mode='min', factor=0.5, patience=10, threshold=1e-4, min_lr=1e-6
)

4.3 Cosine Annealing – Cyclical Learning
lr_t = lr_min + (lr_max - lr_min) * (1 + cos(π * t / T_max)) / 2

text
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
    optimizer, T_max=50, eta_min=1e-6
)

4.4 OneCycleLR – Fast Convergence
Increases learning rate then decreases it.

text
scheduler = torch.optim.lr_scheduler.OneCycleLR(
    optimizer, max_lr=0.01, steps_per_epoch=len(train_loader), epochs=epochs
)

5. Advanced Optimisers – Beyond Standard SGD

5.1 SGD with Momentum

text
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9, weight_decay=1e-5)

5.2 RMSprop

text
optimizer = optim.RMSprop(model.parameters(), lr=0.001, alpha=0.99, eps=1e-8, weight_decay=1e-5)

5.3 Adam (Most Common)

text
optimizer = optim.Adam(model.parameters(), lr=0.001, betas=(0.9, 0.999), eps=1e-8, weight_decay=1e-5)

5.4 AdamW (Adam with Decoupled Weight Decay)

text
optimizer = optim.AdamW(model.parameters(), lr=0.001, betas=(0.9, 0.999), eps=1e-8, weight_decay=1e-5)

5.5 Optimiser Comparison for Finance

 
 
Optimiser Pros Cons Financial Use
SGD + Momentum Simple, generalises well Requires careful tuning Small datasets
Adam Fast, robust, default May not generalise as well Default for most tasks
AdamW Same as Adam, better regularisation Slightly slower Recommended default
RMSprop Good for non-stationary Can be unstable Volatility forecasting

6. Hyperparameter Tuning for Neural Networks

6.1 Key Hyperparameters

 
 
Hyperparameter Range Financial Recommendation
Learning Rate 1e-5 to 1e-1 Start with 1e-3 (Adam) or 1e-2 (SGD)
Batch Size 16 to 512 128 (balance speed and convergence)
Hidden Layers 1 to 5 2-3 for most financial tasks
Hidden Units 32 to 1024 128-256 for moderate datasets
Dropout Rate 0.1 to 0.5 0.3 for noisy financial data
Weight Decay 0 to 1e-2 1e-5 (light regularisation)
Number of Epochs 50 to 500 Use early stopping

6.2 Hyperparameter Tuning with Optuna

text
import optuna

def objective(trial, X_train, y_train, X_val, y_val):
    # Define hyperparameters
    lr = trial.suggest_float('lr', 1e-5, 1e-1, log=True)
    hidden_units = trial.suggest_int('hidden_units', 32, 512)
    num_layers = trial.suggest_int('num_layers', 1, 4)
    dropout = trial.suggest_float('dropout', 0.1, 0.5)
    weight_decay = trial.suggest_float('weight_decay', 1e-6, 1e-2, log=True)
    batch_size = trial.suggest_categorical('batch_size', [32, 64, 128, 256])

    # Build model
    hidden_dims = [hidden_units] * num_layers
    model = FinancialNN(X_train.shape[1], hidden_dims, 1, dropout)

    # Train and evaluate
    train_loader, val_loader, _ = prepare_financial_data(
        X_train, y_train, X_val, y_val, None, None, batch_size
    )

    model, history = train_neural_network(
        model, train_loader, val_loader, nn.MSELoss(),
        epochs=50, lr=lr, weight_decay=weight_decay, patience=10
    )

    return min(history['val_loss'])

study = optuna.create_study(direction='minimize')
study.optimize(lambda trial: objective(trial, X_train, y_train, X_val, y_val), n_trials=100)

7. Common Pitfalls and Solutions

 
 
Pitfall Solution
Overfitting Increase dropout, reduce model size, add weight decay, use early stopping.
Underfitting Increase model size, reduce regularisation, increase training time.
Vanishing Gradients Use ReLU, batch normalisation, residual connections.
Exploding Gradients Use gradient clipping, smaller learning rate, weight initialisation.
Non-Convergence Adjust learning rate, use learning rate scheduler, try different optimiser.
Data Leakage Use time series cross-validation, never use future data.
Non-Stationarity Use rolling windows, online learning, retrain periodically.

8. Summary for the AI Practitioner

  1. Training pipeline: Data preparation → Model definition → Training loop → Validation → Early stopping → Checkpointing.

  2. Regularisation is mandatory for financial neural networks. Use dropout (0.3), weight decay (1e-5), and batch normalisation.

  3. Adam/AdamW are the default optimisers. Use OneCycleLR for fast convergence.

  4. Early stopping prevents overfitting. Save the best model based on validation loss.

  5. Hyperparameter tuning (Optuna) significantly improves performance. Start with learning rate, hidden units, and dropout.

  6. Gradient clipping prevents exploding gradients. Use max_norm=1.0.

  7. Data augmentation (jittering, scaling) can improve generalisation for small datasets.

  8. Monitor training – training loss should decrease, validation loss should not increase significantly.