Skip to content

Hyperparameter tuning with Hyperopt.jl

1. Setup and Data Loading

Load package and synthetic dataset

julia
using EasyHybrid
using CairoMakie
using Hyperopt
julia
ds = load_timeseries_netcdf("https://github.com/bask0/q10hybrid/raw/master/data/Synthetic4BookChap.nc")
ds = ds[1:20000, :]  # Use subset for faster execution
first(ds, 5)
5×6 DataFrame
Rowtimesw_potdsw_pottarecorb
DateTimeFloat64?Float64?Float64?Float64?Float64?
12003-01-01T00:15:00109.817115.5952.10.8447411.42522
22003-01-01T00:45:00109.817115.5951.980.8406411.42522
32003-01-01T01:15:00109.817115.5951.890.8375791.42522
42003-01-01T01:45:00109.817115.5952.060.8433721.42522
52003-01-01T02:15:00109.817115.5952.090.8443991.42522

2. Define the Process-based Model

RbQ10 model: Respiration model with Q10 temperature sensitivity

julia
function RbQ10(;ta, Q10, rb, tref = 15.0f0)
    reco = rb .* Q10 .^ (0.1f0 .* (ta .- tref))
    return (; reco, Q10, rb)
end
RbQ10 (generic function with 1 method)

3. Configure Model Parameters

Parameter specification: (default, lower_bound, upper_bound)

julia
parameters = (
    rb  = (3.0f0, 0.0f0, 13.0f0),  # Basal respiration [μmol/m²/s]
    Q10 = (2.0f0, 1.0f0, 4.0f0),   # Temperature sensitivity - describes factor by which respiration is increased for 10 K increase in temperature [-]
)
(rb = (3.0f0, 0.0f0, 13.0f0),
 Q10 = (2.0f0, 1.0f0, 4.0f0),)

4. Construct the Hybrid Model

Define input variables

julia
forcing = [:ta]                    # Forcing variables (temperature)
predictors = [:sw_pot, :dsw_pot]   # Predictor variables (solar radiation)
target = [:reco]                   # Target variable (respiration)
1-element Vector{Symbol}:
 :reco

Parameter classification as global, neural or fixed (difference between global and neural)

julia
global_param_names = [:Q10]        # Global parameters (same for all samples)
neural_param_names = [:rb]         # Neural network predicted parameters
1-element Vector{Symbol}:
 :rb

Construct hybrid model

julia
hybrid_model = constructHybridModel(
    predictors,               # Input features
    forcing,                  # Forcing variables
    target,                   # Target variables
    RbQ10,                    # Process-based model function
    parameters,               # Parameter definitions
    neural_param_names,       # NN-predicted parameters
    global_param_names,       # Global parameters
    hidden_layers = [16, 16], # Neural network architecture
    activation = relu,       # Activation function
    scale_nn_outputs = true,  # Scale neural network outputs
    input_batchnorm = false    # Apply batch normalization to inputs
)
Neural Network:
    Chain(
        layer_1 = WrappedFunction(identity),
        layer_2 = Dense(2 => 16, relu),               # 48 parameters
        layer_3 = Dense(16 => 16, relu),              # 272 parameters
        layer_4 = Dense(16 => 1),                     # 17 parameters
    )         # Total: 337 parameters,
              #        plus 0 states.
Predictors: [:sw_pot, :dsw_pot]
Forcing: [:ta]
Neural parameters: [:rb]
Global parameters: [:Q10]
Fixed parameters: Symbol[]
Scale NN outputs: true
Parameter defaults and bounds:
    HybridParams{typeof(Main.RbQ10)}(
    ┌─────┬─────────┬───────┬───────┐
    │     │ default │ lower │ upper │
    ├─────┼─────────┼───────┼───────┤
    │  rb │     3.0 │   0.0 │  13.0 │
    │ Q10 │     2.0 │   1.0 │   4.0 │
    └─────┴─────────┴───────┴───────┘
    )

5. Train the Model

julia
out = train(
    hybrid_model,
    ds,
    ();
    nepochs = 100,               # Number of training epochs
    batchsize = 512,             # Batch size for training
    opt = AdamW(0.001),        # Optimizer and learning rate
    monitor_names = [:rb, :Q10], # Parameters to monitor during training
    yscale = identity,           # Scaling for outputs
    patience = 30,               # Early stopping patience
    show_progress=false,
    hybrid_name="before"
)
  train_history: (31, 2)
    mse  (reco, sum)
    r2   (reco, sum)
  val_history: (31, 2)
    mse  (reco, sum)
    r2   (reco, sum)
  ps_history: (31, 2)
    ϕ        ()
    monitor  (train, val)
  train_obs_pred: 16000×3 DataFrame
    reco, index, reco_pred
  val_obs_pred: 4000×3 DataFrame
    reco, index, reco_pred
  train_diffs: 
    Q10         (1,)
    rb          (16000,)
    parameters  (rb, Q10)
  val_diffs: 
    Q10         (1,)
    rb          (4000,)
    parameters  (rb, Q10)
  ps: 
    ps   (layer_1, layer_2, layer_3, layer_4)
    Q10  (1,)
  st: 
    st     (layer_1, layer_2, layer_3, layer_4)
    fixed  ()
  best_epoch: 
  best_loss: 

6. Check Results

Evolution of train and validation loss

julia
EasyHybrid.plot_loss(out, yscale = identity)

Check results - what do you think - is it the true Q10 used to generate the synthetic dataset?

julia
out.train_diffs.Q10
1-element Vector{Float32}:
 2.0

Quick scatterplot - dispatches on the output of train

julia
EasyHybrid.poplot(out)

Hyperparameter Tuning

EasyHybrid provides built-in hyperparameter tuning capabilities to optimize your model configuration. This is especially useful for finding the best neural network architecture, optimizer settings, and other hyperparameters.

Basic Hyperparameter Tuning

You can use the tune function to automatically search for optimal hyperparameters. Check Hyperopt.jl for details on algorithms.

julia
# Create empty model specification for tuning
mspempty = ModelSpec()

# Define hyperparameter search space
nhyper = 4
ho = @thyperopt for i=nhyper,
    opt = [AdamW(0.01), AdamW(0.1), RMSProp(0.001), RMSProp(0.01)],
    input_batchnorm = [true, false]

    hyper_parameters = (;opt, input_batchnorm)
    println("Hyperparameter run: ", i, " of ", nhyper, " with hyperparameters: ", hyper_parameters)

    # Run tuning with current hyperparameters
    out = EasyHybrid.tune(
        hybrid_model,
        ds,
        mspempty;
        hyper_parameters...,
        nepochs = 10,
        plotting = false,
        show_progress = false,
        file_name = "test$i.jld2"
    )

    out.best_loss
end

# Get the best hyperparameters
ho.minimizer
printmin(ho)

# Train the model with the best hyperparameters
best_hyperp = best_hyperparams(ho)
(opt = AdamW(eta=0.01, beta=(0.9, 0.999), lambda=0.0, epsilon=1.0e-8, couple=true),
 input_batchnorm = true,)

Train model with the best hyperparameters

julia
# Run tuning with specific hyperparameters
out_tuned = EasyHybrid.tune(
    hybrid_model,
    ds,
    mspempty;
    best_hyperp...,
    nepochs = 100,
    monitor_names = [:rb, :Q10],
    hybrid_name="after"
)

# Check the tuned model performance
out_tuned.best_loss
0.0030114756f0

Key Hyperparameters to Tune

When tuning your hybrid model, consider these important hyperparameters:

  • Optimizer and Learning Rate: Try different optimizers (AdamW, RMSProp, Adam) with various learning rates

  • Neural Network Architecture: Experiment with different hidden_layers configurations

  • Activation Functions: Test different activation functions (relu, sigmoid, tanh)

  • Batch Normalization: Enable/disable input_batchnorm and other normalization options

  • Batch Size: Adjust batchsize for optimal training performance

Tips for Hyperparameter Tuning

  • Start with a small search space to get a baseline understanding

  • Monitor for overfitting by tracking validation loss

  • Consider computational cost - more hyperparameters and epochs increase training time

More Examples

Check out the projects/ directory for additional examples and use cases. Each project demonstrates different aspects of hybrid modeling with EasyHybrid.