11. Building and training
The theory chapters introduced every part. This one is the assembly manual: the order things must happen in, the knobs that matter, and the habits that make a model reproducible and recoverable. It is short on new ideas and long on the things that cost an afternoon when learned by surprise.
11.1 The order of construction
One rule outranks the others: seed first, then build. Parameter initialization draws from the package generator at construction, and a model’s options draw their own seed, for training’s Monte Carlo and the simulation stream, from the same generator when they are created. One call before any object exists makes the whole pipeline replayable.
import os
import geoml
import numpy as np
os.makedirs("figures", exist_ok=True)
geoml.set_seed(1234) # before ANY model object is built
walker, walker_grid = geoml.datasets.walker()
Then the assembly, in the order the pieces depend on each other: inducing points, input node, GP node, warping, likelihood, model. Each one gets its own name, which costs a line and buys a network you can inspect a piece at a time.
experts = geoml.data.inducing.grid_experts(walker_grid, 10.0, block=8)
root = geoml.latent.BasicInput(
experts,
transform=geoml.transform.Isotropic(50))
gp = geoml.latent.BasicGP(
root,
size=1,
kernel=geoml.kernels.Spherical())
warping = geoml.warping.ChainedWarping(
geoml.warping.Scale(1),
geoml.warping.Softplus(1),
geoml.warping.ZScore(1),
geoml.warping.Spline(1, knots_per_arm=4))
likelihood = geoml.likelihood.Gaussian(warping)
model = geoml.models.VGPNetwork(
walker, "V",
likelihood,
gp,
options=geoml.models.GPOptions(verbose=False))
print(model)
The repr is the model document: every node, every parameter, its bounds
and whether it is fixed. Read it before training the way a variogram model
is read before kriging. Most “why is it doing that” questions are answered
in it.
Multi-variable models pass lists in matching order (["Elements", "Landuse"] with a likelihood each). Two places are worth a moment’s
thought before training starts: the initial transform range, and the
warping chain. Initialization is not fitting, but a range wrong by an
order of magnitude starts the climb in a bad valley, and a chain without a
positivity link will happily predict a negative grade (chapter 4).
11.2 Training, watching, adjusting
train_full(max_iter=...) for datasets that fit in a batch, train_svi
past that (chapter 3). Adam runs underneath, and set_learning_rate is
there for when the default is too bold for a delicate warping. Two habits
matter.
model.train_full(max_iter=500)
figure = geoml.plots.Explorer(walker, continuous="V",
model=model).training_curve()
figure.savefig("figures/11-training-curve.png", dpi=150,
bbox_inches="tight")

The first habit is to watch the running mean of the ELBO until it is flat. An unsettled fit gives seed-dependent answers, which is a reproducibility problem wearing a modelling problem’s clothes. The curve above is worth looking at closely: it rises steeply for the first fifty iterations and then keeps creeping for hundreds more. The steep part is the model finding the right scale, and the creep is the warping and the variational state settling against each other. Stopping at the elbow is the common mistake, and the flat tail is what the eye should be waiting for.
The second is to train in rounds. Another train_full call continues
where the last one stopped, so predict, look and resume is the intended
rhythm rather than a trick.
Two GPOptions are worth knowing beyond the defaults. jit_predict=True
compiles prediction with XLA, measured at 3–5× on big grids, and is off by
default because XLA refuses what it cannot compile instead of falling
back. training_samples sets the Monte Carlo width of the ELBO estimate,
and the default serves. Raise it only when the curve is too noisy to read.
11.3 Saving, loading, resuming
A trained model persists whole: structure, parameters, training data, even the seed it drew, so a reloaded model replays its simulations exactly.
import tempfile, os
os.makedirs("figures", exist_ok=True)
path = os.path.join(tempfile.mkdtemp(), "walker_model")
geoml.persistence.save_model(model, path)
restored = geoml.persistence.load_model(path)
fresh_grid = geoml.data.Grid2D(start=[1, 1], n=[50, 50], step=[5.2, 5.6])
restored.predict(fresh_grid, n_sim=8)
print("the reloaded model predicts:", "V" in fresh_grid.variables)
print("lowest value:",
round(float(np.min(fresh_grid.values("V/prediction"))), 1))
The value is positive, as the Scale → Softplus → ZScore → Spline chain
guarantees, and it is the same number the original model would have given.
The mechanism is worth understanding, because two workflows ride on it.
Saving records the constructor calls that built every object, plus the
parameter values they arrived at, and loading replays them. One
consequence is that a class recorded in a save must keep its import path
forever, which the package maintains shims for. The useful consequence is
that load_model(path, data=another_container) rebuilds the same trained
model around different data. That gives you the fitted structure as a
starting point on a new drilling campaign, and it is the mechanism chapter
13’s cross-validation uses to build its fold models.
In the code.
models.VGPNetwork,models.GPOptions,persistence.save_modelandload_model, andstats/random.pyfor the one seed knob.
Further reading
Chapter 3 for what training optimizes, chapter 13 for judging the result, and the case studies for three full assemblies with their reasoning written down.