Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Non-Stationary GEVD: Neural ODE on μ(t)

Open In Colab

The previous two notebooks parameterised μ(t)\mu(t) as a function — first linear, then a B-spline basis expansion. This notebook takes the dynamical-systems route: μ(t)\mu(t) is the state of a learned ODE,

dμdt  =  fθ(μ(t),t),μ(0)=μ0,\frac{d\mu}{dt} \;=\; f_\theta\bigl(\mu(t), t\bigr), \qquad \mu(0) = \mu_0,

where fθf_\theta is a small multilayer perceptron (the neural-ODE setup of Chen, Rubanova, Bettencourt & Duvenaud, NeurIPS 2018). Solving the ODE on the data times yields μ(ti)\mu(t_i); gradient descent through diffrax.diffeqsolve trains θ\theta jointly with the stationary (σ,ξ)(\sigma, \xi) to maximise the GEVD log-likelihood.

Why bother with this when P-splines already work? Two reasons:

  1. Smoothness for free. A neural ODE produces C1C^1 trajectories without any hand-tuned smoothness penalty.

  2. Generalisation to coupled systems. The 1D scalar case here is the building block; the same recipe extends to multi-dimensional latent states (think: temperature + humidity coupled), to control inputs (forcings), and to stochastic ODEs.

What we cover

  1. Theory. The neural ODE; differentiable adaptive ODE solvers; the gradient through diffrax.

  2. Synthetic data with a non-monotone trend. A sinusoidal-plus-linear μ(t)\mu(t) that neither linear regression nor a 2-knot spline can capture cleanly.

  3. Model. A 1-state neural ODE with a small MLP vector field; diffrax.diffeqsolve integrating from t=0t = 0 to t=Tt = T and saving at the data times.

  4. Training. optax.adam on the negative GEVD log-likelihood.

  5. Diagnostics. Recovery of μ(t)\mu(t), comparison against the P-spline fit from the previous notebook, time-varying return-level plot.

Python implementation: CPython
Python version       : 3.12.3
IPython version      : 9.10.0

jax       : 0.10.1
diffrax   : 0.7.2
equinox   : 0.13.7
optax     : 0.2.8
numpy     : 2.4.4
matplotlib: 3.10.8
xtremax   : 0.0.0

Compiler    : GCC 13.3.0
OS          : Linux
Release     : 6.18.5
Machine     : x86_64
Processor   : x86_64
CPU cores   : 4
Architecture: 64bit

1. Neural ODEs in one screen

A neural ordinary differential equation (NODE) replaces the black-box function ff in

dzdt  =  f(z,t)\frac{dz}{dt} \;=\; f(z, t)

with a small neural network fθf_\theta. Given an initial state z0z_0 and a target time TT, the trajectory z(T)z(T) is obtained by numerical integration:

z(T)  =  z0  +  ∫0Tfθ(z(s),s) ds.z(T) \;=\; z_0 \;+\; \int_{0}^{T} f_\theta(z(s), s)\, ds.

For this to be useful in deep-learning training we need gradients with respect to θ\theta (and z0z_0) of any loss of the form L(z(T))L(z(T)). There are two routes:

  • Discretise then differentiate. Pick an explicit solver (Runge-Kutta, Tsit5), run it forward, store every intermediate state, and backpropagate through the resulting computation graph. This is what diffrax.RecursiveCheckpointAdjoint does and is what we use here.

  • Differentiate then discretise. Solve the adjoint ODE −dλ/dt=λ⊤∂f/∂z-d\lambda/dt = \lambda^\top \partial f / \partial z backwards in time. This is what the original Chen et al. paper used; it is memory-efficient but trickier with adaptive step-size solvers.

diffrax.diffeqsolve exposes both. For the dataset sizes in this notebook (80 evaluation points, simple 1D state) the recursive-checkpoint approach is fastest and easiest.

The full model. With a C1C^1 vector field fθ:R×R→Rf_\theta : \mathbb{R} \times \mathbb{R} \to \mathbb{R} given by a small MLP,

μ(t)  =  μ0+∫0tfθ(μ(s),s) ds,zi∣μ(ti),σ,ξ  ∼  GEVD(μ(ti),σ,ξ).\mu(t) \;=\; \mu_0 + \int_{0}^{t} f_\theta(\mu(s), s)\,ds, \qquad z_i \mid \mu(t_i), \sigma, \xi \;\sim\; \mathrm{GEVD}(\mu(t_i), \sigma, \xi).

Loss: negative log-likelihood,

L(θ,μ0,σ,ξ)  =  −∑i=1nlog⁡fGEVD(zi;μ(ti),σ,ξ).\mathcal{L}(\theta, \mu_0, \sigma, \xi) \;=\; -\sum_{i=1}^{n} \log f_{\mathrm{GEVD}}(z_i; \mu(t_i), \sigma, \xi).

We optimise everything jointly with Adam.

2. Synthetic data with a non-monotone trend

The previous notebook used a sigmoidal trend, which is monotone and well within reach of a 2-3 knot B-spline. To stress-test the neural ODE we plant something more wiggly:

μ(t)  =  30+2.5 sin⁡(t/8)+0.04 t.\mu(t) \;=\; 30 + 2.5\,\sin(t / 8) + 0.04\, t.

This has both a slow linear drift and a faster oscillation — neither a linear fit nor a low-resolution spline can catch both at once.

<Figure size 1000x400 with 1 Axes>

3. Model: a 1-state neural ODE for μ(t)

We build a tiny MLP for the vector field fθ(μ,t)f_\theta(\mu, t):

  • Input: the current state μ∈R\mu \in \mathbb{R} and the current time t∈Rt \in \mathbb{R} — concatenated to a 2-vector.

  • Hidden layers: two layers of 16 units each, tanh⁡\tanh activation.

  • Output: dμ/dt∈Rd\mu/dt \in \mathbb{R}.

The MLP parameter count is (2⋅16+16)+(16⋅16+16)+(16⋅1+1)=337(2 \cdot 16 + 16) + (16 \cdot 16 + 16) + (16 \cdot 1 + 1) = 337; adding the initial condition μ0\mu_0 and the stationary (log⁡σ,ξ)(\log\sigma, \xi) brings the total to 337+3=340337 + 3 = 340 — about as parsimonious as a neural ODE gets.

Number of parameters: 340
Initial μ_0 = 28.617, σ = 2.000, ξ = -0.050
Initial NLL: 330.36

4. Training loop with optax.adam

step  500  NLL=  185.38  σ=2.183  ξ=-0.013
step 1000  NLL=  178.68  σ=2.070  ξ=-0.103
step 1500  NLL=  167.92  σ=1.910  ξ=-0.155
step 2000  NLL=  165.24  σ=1.853  ξ=-0.168

Final: σ = 1.853 (true 1.50)
       ξ = -0.168 (true -0.10)
<Figure size 800x360 with 1 Axes>

5. Recovery of μ(t) from the trained ODE

Plot the integrated trajectory of the learned ODE on a dense time grid and overlay the truth.

<Figure size 1000x450 with 1 Axes>

6. Comparing against the linear and stationary fits

Compute the same MLE point estimates for the simpler models so we can put all three on one axis.

<Figure size 1000x480 with 1 Axes>

Reading this. The constant fit and the linear fit cannot represent the oscillation; the neural ODE picks up both the slow drift and the sinusoidal component. The drift alone over 80 years is 0.04×80=3.20.04 \times 80 = 3.2 — about 1σ1\sigma of the noise — so the linear fit gets it right on average but consistently mispredicts in any given decade.

7. Time-varying return-level curve

As before, with μ(t)\mu(t) in hand and stationary (σ,ξ)(\sigma, \xi), the year-TT return level is just μ(t)\mu(t) + a constant excess. We use the trained σ,ξ\sigma, \xi for a point-estimate band; a Bayesian version would put priors on θ,μ0,σ,ξ\theta, \mu_0, \sigma, \xi jointly and run NUTS through diffrax, but that’s a heavier lift than this notebook needs.

<Figure size 1000x450 with 1 Axes>

8. The vector field as a phase portrait

Because fθ(μ,t)f_\theta(\mu, t) takes both the state and time, the phase portrait is 2D: arrows on the (μ,t)(\mu, t) plane. Plotting it shows the structure the network has learned — where dμ/dtd\mu/dt is large vs small, positive vs negative.

<Figure size 1000x480 with 1 Axes>

Takeaways

  • Neural ODEs are a flexible, dynamical-systems-flavoured way to parameterise a non-stationary GEVD location. The state μ(t)\mu(t) is the integral of a learned vector field; gradients flow through diffrax.diffeqsolve.

  • Smoothness comes for free. A C1C^1 vector field gives a C1C^1 trajectory — no smoothness penalty needed, no knot placement.

  • The cost is identifiability and interpretability. A 337-parameter MLP is much harder to inspect than a 12-coefficient spline; with the small dataset here, many vector-field configurations are observationally equivalent.

  • Use cases. Multi-dimensional latent state (multiple coupled extremes), control inputs (forcings), or any setting where a causal mechanism is more appropriate than a basis expansion.

  • Bayesian extension. Replace optax.adam with NumPyro NUTS over the same parameters and you get a fully Bayesian neural-ODE-driven GEVD. The ODE solver is differentiable so HMC works through it; expect long compile times and many divergences without careful prior choice and re-parameterisation.

This concludes the three-notebook non-stationary mini-series:

  1. Parametric — linear μ(t)=β0+β1t\mu(t) = \beta_0 + \beta_1 t.

  2. Non-parametric basis expansion — P-splines.

  3. Dynamical — neural ODE.

Each successive notebook is more flexible and more parameter-hungry; each takes the same shape of “fit μ(⋅)\mu(\cdot), leave (σ,ξ)(\sigma, \xi) stationary”. The right choice for any particular dataset is a function of how much data you have and whether the domain motivates a mechanistic model.