Evaluate model states#

A position is a dictionary of selected variable or node names and values. A model state also contains the cached values and update flags of its computational nodes. Use model from Build your first model below.

Try a position#

import jax
import jax.numpy as jnp
import numpy as np
import tensorflow_probability.substrates.jax.distributions as tfd

import liesel.model as lsl
position = model.extract_position(["beta", "variance"])
position["beta"] = jnp.array([0.5, 1.5])

model.update()
candidate = model.update_state(position, model.state)
model.extract_position(["beta"], candidate)
{'beta': Array([0.5, 1.5], dtype=float32)}
model.extract_position(["beta"])
{'beta': Array([1., 2.], dtype=float32)}

The first result contains the candidate coefficients; the second contains the model’s existing coefficients. update_state leaves the model unchanged by default. Its input state must already be up to date: it only triggers updates through the supplied position. Call update() first if automatic updates have been disabled. Weak variables normally remain calculated, read-only values.

To apply the candidate explicitly:

model.state = candidate

This is also how you can apply a position returned by an optimizer. See Choose optimizers for a fit-to-fit example and update_state() for the in-place option.

Calculate gradients#

log_prob = lsl.LogProb(model)
position = model.extract_position(["beta", "variance"])
round(float(log_prob(position)), 3)
-102.532
gradient = log_prob.grad(position)
{name: np.asarray(value).round(3) for name, value in gradient.items()}
{'beta': array([40.275,  8.956], dtype=float32),
 'variance': np.float32(-16.079)}

The gradient has the same dictionary keys and parameter shapes as position. It describes the local change in the unnormalized log density, not a fitted parameter value. Use component="log_lik" or "log_prior" to inspect one contribution, and FlatLogProb for an interface that accepts a flat parameter array.

Write JAX calculations#

Pass every changing dependency into the calculation:

def linear_mean(x, beta):
    return beta[0] + beta[1] * x


predictor = lsl.Var.new_calc(
    linear_mean,
    model.vars["x"],
    model.vars["beta"],
    name="prediction_check",
)
np.asarray(predictor.value[:3]).round(3)
array([-1.   , -0.962, -0.924], dtype=float32)

Use JAX array operations in calculations that will be differentiated or compiled. Avoid reading mutable globals or capturing beta.value in a closure: those values would not be explicit graph inputs. A pure function depends only on its inputs and has no side effects.

Python control flow over static values can work. Branching on a traced array value needs JAX control flow such as jax.lax.cond or an elementwise jnp.where. Keep data preparation outside the graph when it need not be re-evaluated, and test any calculation that must also run on new prediction data.

By default, calculator variables cache their results and recompute when their inputs change. new_calc(..., cache=False) avoids storing the result, but repeats the calculation on access. Consider it for a cheap reshaping operation, not an expensive matrix decomposition. See new_calc() for details.