In this notebook, we calibrate the learning rate for the gradient descent optimiser on a parameter identification problem. The gradient descent learning rate is taken as the sigma0
value passed to the pybop.Optimisation
class, or via problem.sigma0
or cost.sigma0
if it is passed earlier in the workflow.
Before we begin, we need to ensure that we have all the necessary tools. We will install PyBOP and upgrade dependencies:
%pip install --upgrade pip ipywidgets -q
%pip install pybop -q
/Users/engs2510/Documents/Git/Second_PyBOP/.nox/notebooks-overwrite/bin/python3: No module named pip Note: you may need to restart the kernel to use updated packages. /Users/engs2510/Documents/Git/Second_PyBOP/.nox/notebooks-overwrite/bin/python3: No module named pip Note: you may need to restart the kernel to use updated packages.
With the environment set up, we can now import PyBOP alongside other libraries we will need:
import numpy as np
import pybop
pybop.plot.PlotlyManager().pio.renderers.default = "notebook_connected"
Let's fix the random seed in order to generate consistent output during development, although this does not need to be done in practice.
np.random.seed(8)
To demonstrate parameter estimation, we first need some data. We will generate synthetic data using a forward model, which requires defining a parameter set and the model itself.
We start by creating an example parameter set, constructing the single-particle model (SPM) and generating the synthetic data.
parameter_set = pybop.ParameterSet.pybamm("Chen2020")
parameter_set.update(
{
"Negative electrode active material volume fraction": 0.65,
"Positive electrode active material volume fraction": 0.51,
}
)
model = pybop.lithium_ion.SPM(parameter_set=parameter_set)
initial_state = {"Initial SoC": 0.4}
experiment = pybop.Experiment(
[
(
"Discharge at 0.5C for 6 minutes (4 second period)",
"Charge at 0.5C for 6 minutes (4 second period)",
),
]
* 2
)
values = model.predict(initial_state=initial_state, experiment=experiment)
To make the parameter estimation more realistic, we add Gaussian noise to the data.
sigma = 0.002
corrupt_values = values["Voltage [V]"].data + np.random.normal(
0, sigma, len(values["Voltage [V]"].data)
)
We will now set up the parameter estimation process by defining the datasets for optimisation and selecting the model parameters we wish to estimate.
The dataset for optimisation is composed of time, current, and the noisy voltage data:
dataset = pybop.Dataset(
{
"Time [s]": values["Time [s]"].data,
"Current function [A]": values["Current [A]"].data,
"Voltage [V]": corrupt_values,
}
)
We select the parameters for estimation and set up their prior distributions and bounds:
parameters = pybop.Parameters(
pybop.Parameter(
"Negative electrode active material volume fraction",
prior=pybop.Uniform(0.45, 0.7),
bounds=[0.4, 0.8],
true_value=0.65,
),
pybop.Parameter(
"Positive electrode active material volume fraction",
prior=pybop.Uniform(0.45, 0.7),
bounds=[0.4, 0.8],
true_value=0.51,
),
)
With the datasets and parameters defined, we can set up the optimisation problem, its cost function, and the optimiser. For gradient descent, the sigma0
value corresponds to the learning rate. Let's set this hyperparmeter incorrectly to view how we calibrate it. In this example, let's start with sigma0=0.2
.
problem = pybop.FittingProblem(model, parameters, dataset)
cost = pybop.SumSquaredError(problem)
optim = pybop.GradientDescent(cost, sigma0=0.2, max_iterations=100)
NOTE: Boundaries ignored by <class 'pints._optimisers._gradient_descent.GradientDescent'>
We proceed to run the optimisation algorithm to estimate the parameters with the updated learning rate (sigma0
).
results = optim.run()
Halt: No significant change for 15 iterations. OptimisationResult: Initial parameters: [0.46478836 0.56896942] Optimised parameters: [0.64605501 0.51469905] Final cost: 0.0013514039920203883 Optimisation time: 2.287174940109253 seconds Number of iterations: 52 SciPy result available: No
After the optimisation, we can examine the estimated parameter values. In this case, the optimiser misses the optimal solution by a large amount.
results.x # This will output the estimated parameters
array([0.64605501, 0.51469905])
Let's plot the time-series prediction for the given solution. As we suspected, the optimiser found a very poor solution.
pybop.plot.quick(problem, problem_inputs=results.x, title="Optimised Comparison");
Now that we've seen how poor an incorrect sigma0
value is for this optimisation problem, let's calibrate this value to find the optimal solution in the lowest number of iterations.
sigmas = np.linspace(0.001, 0.08, 8) # Change this to a smaller range for a quicker run
xs = []
optims = []
for sigma in sigmas:
print(sigma)
problem = pybop.FittingProblem(model, parameters, dataset)
cost = pybop.SumSquaredError(problem)
optim = pybop.GradientDescent(cost, sigma0=sigma, max_iterations=100)
results = optim.run()
optims.append(optim)
xs.append(results.x)
0.001 NOTE: Boundaries ignored by <class 'pints._optimisers._gradient_descent.GradientDescent'> Halt: Maximum number of iterations (100) reached. OptimisationResult: Initial parameters: [0.46478836 0.56896942] Optimised parameters: [0.53090948 0.59339862] Final cost: 0.014783332157851634 Optimisation time: 4.26934027671814 seconds Number of iterations: 100 SciPy result available: No 0.012285714285714285 NOTE: Boundaries ignored by <class 'pints._optimisers._gradient_descent.GradientDescent'> Halt: Maximum number of iterations (100) reached. OptimisationResult: Initial parameters: [0.46478836 0.56896942] Optimised parameters: [0.60345692 0.56598814] Final cost: 0.002790217328868725 Optimisation time: 4.251996040344238 seconds Number of iterations: 100 SciPy result available: No 0.023571428571428573 NOTE: Boundaries ignored by <class 'pints._optimisers._gradient_descent.GradientDescent'> Halt: Maximum number of iterations (100) reached. OptimisationResult: Initial parameters: [0.46478836 0.56896942] Optimised parameters: [0.62409599 0.5385165 ] Final cost: 0.0017177540172860569 Optimisation time: 4.208575963973999 seconds Number of iterations: 100 SciPy result available: No 0.03485714285714286 NOTE: Boundaries ignored by <class 'pints._optimisers._gradient_descent.GradientDescent'> Halt: Maximum number of iterations (100) reached. OptimisationResult: Initial parameters: [0.46478836 0.56896942] Optimised parameters: [0.63571019 0.52533286] Final cost: 0.0014363025957628353 Optimisation time: 4.245105028152466 seconds Number of iterations: 100 SciPy result available: No 0.046142857142857145 NOTE: Boundaries ignored by <class 'pints._optimisers._gradient_descent.GradientDescent'> Halt: Maximum number of iterations (100) reached. OptimisationResult: Initial parameters: [0.46478836 0.56896942] Optimised parameters: [0.641494 0.51930683] Final cost: 0.0013701538891978493 Optimisation time: 4.226551055908203 seconds Number of iterations: 100 SciPy result available: No 0.05742857142857143 NOTE: Boundaries ignored by <class 'pints._optimisers._gradient_descent.GradientDescent'> Halt: Maximum number of iterations (100) reached. OptimisationResult: Initial parameters: [0.46478836 0.56896942] Optimised parameters: [0.64414678 0.5165881 ] Final cost: 0.0013574904717929912 Optimisation time: 4.241044044494629 seconds Number of iterations: 100 SciPy result available: No 0.06871428571428571 NOTE: Boundaries ignored by <class 'pints._optimisers._gradient_descent.GradientDescent'> Halt: No significant change for 15 iterations. OptimisationResult: Initial parameters: [0.46478836 0.56896942] Optimised parameters: [0.64460623 0.51615892] Final cost: 0.0013563752974500437 Optimisation time: 3.6841771602630615 seconds Number of iterations: 87 SciPy result available: No 0.08 NOTE: Boundaries ignored by <class 'pints._optimisers._gradient_descent.GradientDescent'> Halt: No significant change for 15 iterations. OptimisationResult: Initial parameters: [0.46478836 0.56896942] Optimised parameters: [0.64463942 0.51613016] Final cost: 0.001356018556480287 Optimisation time: 3.1757869720458984 seconds Number of iterations: 75 SciPy result available: No
for optim, sigma in zip(optims, sigmas):
print(
f"| Sigma: {sigma} | Num Iterations: {optim.result.n_iterations} | Best Cost: {optim.optimiser.f_best()} | Results: {optim.optimiser.x_best()} |"
)
| Sigma: 0.001 | Num Iterations: 100 | Best Cost: 0.014783332157851634 | Results: [0.53090948 0.59339862] | | Sigma: 0.012285714285714285 | Num Iterations: 100 | Best Cost: 0.002790217328868725 | Results: [0.60345692 0.56598814] | | Sigma: 0.023571428571428573 | Num Iterations: 100 | Best Cost: 0.0017177540172860569 | Results: [0.62409599 0.5385165 ] | | Sigma: 0.03485714285714286 | Num Iterations: 100 | Best Cost: 0.0014363025957628353 | Results: [0.63571019 0.52533286] | | Sigma: 0.046142857142857145 | Num Iterations: 100 | Best Cost: 0.0013701538891978493 | Results: [0.641494 0.51930683] | | Sigma: 0.05742857142857143 | Num Iterations: 100 | Best Cost: 0.0013574904717929912 | Results: [0.64414678 0.5165881 ] | | Sigma: 0.06871428571428571 | Num Iterations: 87 | Best Cost: 0.0013563752974500437 | Results: [0.64460623 0.51615892] | | Sigma: 0.08 | Num Iterations: 75 | Best Cost: 0.001356018556480287 | Results: [0.64463942 0.51613016] |
Perhaps a better way to view this information is to plot the optimiser convergences,
for optim, sigma in zip(optims, sigmas):
pybop.plot.convergence(optim, title=f"Sigma: {sigma}")
pybop.plot.parameters(optim)
Finally, we can visualise the cost landscape and the path taken by the optimiser:
# Plot the cost landscape with optimisation path and updated bounds
bounds = np.array([[0.4, 0.8], [0.4, 0.8]])
for optim, sigma in zip(optims, sigmas):
pybop.plot.surface(optim, bounds=bounds, title=f"Sigma: {sigma}")
Let's take sigma0 = 0.08
as the best learning rate for this problem and look at the time-series trajectories.
optim = pybop.Optimisation(cost, optimiser=pybop.GradientDescent, sigma0=0.08)
results = optim.run()
pybop.plot.quick(problem, problem_inputs=results.x, title="Optimised Comparison");
NOTE: Boundaries ignored by <class 'pints._optimisers._gradient_descent.GradientDescent'> Halt: No significant change for 15 iterations. OptimisationResult: Initial parameters: [0.46478836 0.56896942] Optimised parameters: [0.64463942 0.51613016] Final cost: 0.001356018556480287 Optimisation time: 3.1555721759796143 seconds Number of iterations: 75 SciPy result available: No
This notebook covers how to calibrate the learning rate for the gradient descent optimiser. This provides an introduction into hyper-parameter tuning that will be discussed in further notebooks.