pyzag.stochastic
Tools for converting deterministc models implemented in pytorch to stochastic models
Besides the model-conversion machinery (MapNormal,
HierarchicalStatisticalModel), this module hosts the Pyro side of
Gauss-Newton preconditioning: PyroGaussNewtonOptim and
PreconditionedSVI let pyzag.preconditioning drive an SVI fit.
They live here rather than next to the curvature engine because they must
subclass Pyro types at class-definition time; pyzag.preconditioning
itself stays importable without Pyro.
- class pyzag.stochastic.MapNormal(cov: float, loc_suffix: str = '_loc', scale_suffix: str = '_scale')
Bases:
objectA map between a deterministic torch parameter and a two-scale normal distribution
- Parameters:
cov – coefficient of variation used to define the scale priors
- Keyword Arguments:
sep (str) – seperator character in names
loc_suffix – suffix to add to parameter name to give the upper-level distribution for the scale
scale_suffix – suffix to add to the parameter name to give the lower-level distribution for the scale
- class pyzag.stochastic.HierarchicalStatisticalModel(base: Module, parameter_mapper: MapNormal, noise_prior: Tensor, update_mask: bool = False)
Bases:
PyroModuleConverts a torch model over to being a Pyro-based hierarchical statistical model
- Parameters:
base (torch.nn.Module) – base torch module
parameter_mapper (MapParameter) – mapper class describing how to convert from Parameter to Distribution
noise_prior (float) – scale prior for white noise
- Keyword Arguments:
update_mask (bool) – if True, update the mask to remove samples that are not valid
- forward(*args: Tensor, results: Tensor | None = None, weights: Tensor | None = None, **kwargs) Tensor
Call the base forward with the appropriate args.
- Parameters:
*args – arguments forwarded to the underlying model. At least one must be a tensor so the batch shape can be inferred.
- Keyword Arguments:
results (torch.tensor or None) – results to condition on.
weights (torch.tensor or None) – weights on the results; defaults to ones.
- training: bool
- class pyzag.stochastic.GaussianResidual(flat, blocks, plates, obs=None)
Bases:
objectThe MAP residual of a Gaussian-family Pyro model, with its site structure.
For a
Deltaguide the negative log-joint is0.5 * ||r||^2 + sum(log scale), so Gauss-Newton applies exactly to the0.5 * ||r||^2part. This object carries thatrplus enough structure for a curvature estimator to exploit the model’s plates.- flat
the flat residual, differentiable w.r.t. the guide’s parameters.
- Type:
Tensor
- blocks
site name ->
sliceintoflat.- Type:
dict
- plates
site name -> tuple of enclosing plate names.
- Type:
dict
- obs_name
name of the observed site, if any.
- Type:
str or None
- obs_shape
unflattened shape of the observed block.
- Type:
torch.Size
- obs_scale
the observed site’s scale (the noise level).
- Type:
Tensor or None
- obs_plate_axis(plate_name)
Positive axis of
plate_namewithin the observed batch shape.
- property prior_names
Names of the latent (unobserved) blocks, in trace order.
- prior_flat()
The latent blocks concatenated, differentiably.
- pyzag.stochastic.gaussian_map_residual(model, guide, *args, **kwargs)
Assemble the MAP residual of a Gaussian-family Pyro model from a trace.
Runs the guide, replays the model under it, and turns every Gaussian-family sample site into whitened residual rows –
(value - loc) / scaleforNormal,value / scaleforHalfNormal– including the observed site. Any poutinescale/maskon a site is folded into its rows, so the resulting least-squares objective matches the weighting the ELBO uses.Gauss-Newton deliberately ignores the
sum(log scale)log-normalizer terms of the log-joint: they are not of least-squares form, and dropping them is the standard Gauss-Newton approximation, not an oversight. The residual must include the prior rows, though – the likelihood alone has exactly zero gradient w.r.t. hierarchical hyper-parameters, so a likelihood-only curvature would silently leave them unpreconditioned.- Parameters:
model – the Pyro model (e.g. a
HierarchicalStatisticalModel).guide – the guide, typically
AutoDelta– a point-mass guide is what makes the objective a deterministic least-squares problem.*args – forwarded to both model and guide.
**kwargs – forwarded to both model and guide.
- Returns:
GaussianResidual
- pyzag.stochastic.hierarchical_gn_diagonal(curvature, gres, param_names, *, plate_name='samples', nsub=8, generator=None, validate=True)
Structure-aware
diag(J^T J)for a hierarchical MAP problem.A hierarchical model makes the generic row-subsampling estimator useless: with one latent block per plate member, drawing
nsubrandom rows out ofntime * nmembertouches at mostnsubmembers and leaves every other member with zero curvature, while theN / nsubrescaling badly distorts the ones it does hit. This estimator instead splits the residual and uses the method each part deserves:prior rows carry no forward model, so their whole Jacobian comes back from a single batched reverse pass – exact, and effectively free;
observed rows, plate-local parameters use one cotangent per slice across the plate, which is exact per member at
nmembertimes fewer sweeps (see_obs_group_cotangents());observed rows, the shared noise scale are analytic: the residual is
(y - f) / eps, sod r_k / d eps = -r_k / epsand the whole column norm follows fromralone, with no sweep at all.
Grouped cotangents would double-count a parameter shared across the plate, so shared parameters are excluded from the swept estimate and handled by the analytic term. Hierarchical hyper-parameters are shared but touch the likelihood only through the prior, so the prior block already covers them;
validatechecks that on the first call rather than assuming it.- Parameters:
curvature (GaussNewtonCurvature) – supplies the parameters and the VJP.
gres (GaussianResidual) – from
gaussian_map_residual().param_names (list of str) – param-store names, in
curvature.parametersorder, used to map parameters onto model sites.
- Keyword Arguments:
plate_name (str) – the plate whose members own private latents.
nsub (int) – number of plate-slices to sample from the observed block.
generator (torch.Generator, optional) – RNG for that subsample.
validate (bool) – check the shared-parameter assumption (one extra sweep).
- Returns:
(diag, nsweeps)– the length-pcurvature diagonal and the number of reverse-mode sweeps it cost.- Return type:
tuple
- class pyzag.stochastic.PyroGaussNewtonOptim(optim_constructor, optim_args, residual_closure, *, plate_name='samples', nsub=8, rho=0.25, lam=0.01, lam_adapt=True, min_refresh_interval=1, generator=None, on_refresh=None, curvature_fn=None, check_optimizer=True)
Bases:
PyroOptimGauss-Newton preconditioning for Pyro SVI.
A stock
pyro.optim.PyroOptimbuilds one torch optimizer per parameter, so it can never see the cross-parameter structure a preconditioner is made of. This subclass instead builds a single optimizer over every parameter at once, and rescales the gradient SVI just computed by the Gauss-Newton curvature before handing it over.SVIaccepts it because it only checksisinstance(optim, PyroOptim).Two details of SVI shape the design. Parameters are created lazily, on the first step, so the optimizer and the curvature engine are built on first call rather than in
__init__. AndSVI.stephas already runbackwardand dropped the graph by the time the optimizer is invoked, so a refresh cannot reuse it –residual_closurere-evaluates the model to get a differentiable residual. That cost is paid only on refresh steps.Because pyro hands over an unordered
setof parameters, they are sorted by param-store name to give the packed vector a stable, reproducible layout.- Parameters:
optim_constructor – a torch optimizer class or factory, as for
pyro.optim.PyroOptim.optim_args (dict) – its keyword arguments. Unlike the base class this must be a plain dict – a per-parameter callable cannot describe a single optimizer covering all parameters.
residual_closure (callable) – returns the residual at the current parameters, differentiable w.r.t. them. Returning a
GaussianResidual(fromgaussian_map_residual()) selects the structure-aware hierarchical estimator; a plain tensor falls back to generic row subsampling.
- Keyword Arguments:
plate_name (str) – plate whose members own private latents.
nsub (int) – plate-slices (or rows, generically) sampled per refresh.
rho (float or None) – gain-ratio refresh threshold;
Nonecomputes the curvature once and reuses it.lam (float) – initial Marquardt damping factor.
lam_adapt (bool) – Levenberg-Marquardt damping adaptation; see
pyzag.preconditioning.GaussNewtonCurvature.adapt_damping().min_refresh_interval (int) – minimum steps between refreshes.
generator (torch.Generator, optional) – RNG for subsampling.
on_refresh (callable, optional) – refresh callback, see
pyzag.preconditioning.GaussNewtonCurvature.curvature_fn (callable, optional) – full override, called as
curvature_fn(curvature, residual, param_names)and returning(H, nsweeps).check_optimizer (bool) – reject a base optimizer that is invariant to gradient preconditioning. SVI’s usual choice,
ClippedAdam, is one of those – seepyzag.preconditioning.check_optimizer_respects_gradient_scale().
- record_loss(loss)
Tell the optimizer the objective value at the current parameters.
The gain-ratio staleness test needs the loss, and
SVI.stepdoes not pass it to the optimizer.PreconditionedSVIcalls this automatically; with a plainSVIcall it yourself before eachsvi.step, or the curvature is computed once and never refreshed.
- recover_from_failed_evaluation()
Back out the last step after the model failed to evaluate.
Called by
PreconditionedSVIwhen the ELBO itself raises: with SVI the failure happens insideloss_and_grads, before the optimizer is ever invoked, so recovery has to be driven from the training loop.
- property recorded_loss
The most recent loss passed to
record_loss(), orNone.
- get_state()
Serializable state of the single inner optimizer.
- set_state(state_dict)
Stage state to be loaded when the inner optimizer is built.
- class pyzag.stochastic.PreconditionedSVI(model, guide, optim, loss, loss_and_grads=None, num_samples=0, num_steps=0, **kwargs)
Bases:
SVISVIthat reports each step’s loss to its optimizer.Identical to the base class except that the loss is handed to the optimizer before the update.
PyroGaussNewtonOptimneeds the objective value at the current parameters to run its gain-ratio staleness test, and the stockSVI.stepcomputes exactly that but returns it to the caller instead of passing it down. Optimizers without arecord_lossmethod are unaffected.- step(*args, **kwargs)
Take one SVI step, recording the pre-update loss on the optimizer.
A model driven outside its valid region raises here, inside
loss_and_grads– before the optimizer runs, so the optimizer cannot react on its own. If it can back the last step out and damp harder, do that and report the step asnanrather than letting the whole fit die on one bad point.