SVBMC#
Note
The SVBMC class implements Stacking Variational Bayesian Monte Carlo
(S-VBMC): it combines the variational posteriors of several completed
VBMC runs on the same problem into one stacked posterior by optimizing
the stacked ELBO with respect to the mixture weights only. The runs’
components and their stored expected log-joint statistics stay fixed, so
stacking needs no further evaluations of the model. Use it when repeated
VBMC runs on the same model and data disagree, or each captures part of a
multimodal posterior.
S-VBMC needs the optional torch extra (see Installation);
constructing an SVBMC object without it raises an ImportError.
Importing PyVBMC does not import torch.
Basic usage#
Run VBMC several times on the same target, collect the returned posteriors, and stack them:
from pyvbmc import VBMC, SVBMC
vps = []
for seed in range(5):
vbmc = VBMC(log_joint, x0[seed], lb, ub, plb, pub, seed=seed)
vp, results = vbmc.optimize()
vps.append(vp)
stacked = SVBMC(vps, seed=0)
stacked.optimize()
samples = stacked.sample(10000) # (10000, D), original space
print(stacked.elbo) # headline stacked ELBO
print(stacked.elbo_sd) # uncertainty of the raw estimate
print(stacked.elbo_details["raw"])
print(stacked.elbo_details["shrunk_two_level"])
PyVBMC Example 7: Stacking the posteriors of several runs (S-VBMC) walks through this on a bimodal target.
ELBO reporting#
stacked.elbo is the headline estimate of the stacked ELBO, chosen by the
noise status of the stack:
For a stack treated as noiseless, it is
raw, the ELBO from the fresh final evaluation at the selected weights.If any retained run is treated as noisy, it is
shrunk_two_level, the two-level shrinkage estimate at the same weights and with the same final entropy, whatever theversionof the optimization. Where that calculation is numerically undefined, the headline iscapped_I_medianinstead, andoptimize()warns.
The choice affects the reported value alone: the weights, and so the stacked posterior and its samples, come from the same optimization whatever the headline.
The weights maximize the weighted sum of the components’ expected log-joints, each estimated by its run’s GP, plus the entropy of the mixture. On a noisy target, this optimization favors components whose estimates came out high and adds an upward bias to the raw ELBO. Here bias is measured against the stack’s ELBO computed with the exact log joint, which is itself a lower bound on the log evidence.
The two-level shrinkage estimate targets this selection bias. It first shrinks component expected log-joints toward the mean within each run, using their GP uncertainty, and then shrinks the runs’ levels toward their common mean. It can also remove some bias inherited from the input VBMC runs, but it does not make the headline unbiased: miscalibrated GP uncertainty and different run-level biases can leave an error in either direction. The raw ELBO can likewise fall below the stack’s exact ELBO when the input runs are pessimistic.
On our benchmarks with every component weight optimized, the remaining
stacking optimism and the inherited bias removed by shrinkage roughly
balanced, so noisy-stack headlines were about as biased as the input runs.
With version="ns", stacking adds no weight-selection bias; shrinkage
still applies and can move the headline below the raw value.
stacked.elbo_details contains the complete report:
Key |
Meaning |
|---|---|
|
ELBO from the fresh evaluation at the selected weights; the headline of a noiseless stack. |
|
ELBO with the expected log-joint capped at the component median; the headline of a noisy stack whose shrinkage is unavailable. |
|
ELBO with the expected log-joint capped at the retained-run median. |
|
Baseline that gives every retained run equal total weight and keeps that run’s original internal component weights. It inherits errors in the input runs and is not a bound on the optimized ELBO. |
|
Empirical-Bayes estimate that first shrinks component expected
log-joints within each retained run using their full GP covariance,
then shifts each run’s components by the change that shrinkage makes
to the run’s level; the headline of a noisy stack. It uses the
selected stacking weights and the same final entropy as |
|
Diagnostic ratio of estimated noise to component spread, averaged over
runs using their selected mixture masses. It can exceed one and is
|
|
The key of the headline: |
|
Reduction that the median cap applied to the headline, |
|
Monte Carlo contribution to the uncertainty of |
|
GP quadrature contribution, treating retained runs as independent. |
|
Combined raw-estimate uncertainty, equal to |
|
Whether the stack is treated as noisy. |
|
Per-retained-run provenance: |
stacked.elbo_sd combines the stratified entropy estimator’s Monte Carlo
variance with the GP quadrature uncertainty at the selected weights. It
describes the raw value. It excludes selection bias and does not
propagate the shrinkage or the median cap, so it must not be interpreted as
defining a calibrated confidence interval for the headline of a noisy stack.
A composite posterior#
Each VBMC run works in its own transformed parameter space (bounded
parameters, and possibly a rotation and rescaling learned during the run).
The components of different runs therefore live in incompatible spaces and
are only comparable after mapping back to the original space. The stacked
posterior keeps the input posteriors as they are and draws from each through
its own transform. Do not combine the component means and covariances of
different runs directly, and do not read them as a single mixture: use
sample() for every estimate and plot.
Helpers#
pyvbmc.svbmc.utils holds two helpers from the S-VBMC workflow:
overlay_corner_plot draws several sample sets on one corner plot, to
compare the individual runs with the stacked posterior, and
find_init_bounds returns the box to draw VBMC starting points from
given the bounds and plausible bounds of a problem.
Differences from the standalone svbmc package#
The implementation derives from the standalone S-VBMC package
(acerbilab/svbmc, version 0.1.1), which is deprecated in its favor: use
pyvbmc.SVBMC for new and existing code. The package’s BSD 3-Clause
license notice ships with the subpackage, pyvbmc.svbmc. The method and
the optimizer of the weights are the same, and on our benchmark the two
reach the same weights. What differs:
It is faster. The work that the components of one run share, its parameter transform above all, is done once per run rather than once per component.
The reported ELBO is more precise. The corrections that bring the expected log-joint of each run to the original parameter space are computed once, when the object is created, exactly or by numerical quadrature, where the standalone package estimated them by Monte Carlo at every step. After the optimization the ELBO is evaluated again with more draws (
optimize(n_samples_final=...)), and it comes with an uncertainty,elbo_sd.elbois a number, the headline described under ELBO reporting, which for a noisy stack is a shrinkage estimate that the standalone package does not compute. The entries of the standalone package’selbodictionary are inelbo_details, under the names in the table below.seedtakes the place oftesting: every random draw comes from the generator of theSVBMCobject, and the input posteriors are neither modified nor advanced.sample(n)returns exactlyndraws. The standalone package rounded the share of each run separately and could return a few more or fewer.Results are float64, problems with one variable work, and malformed inputs raise errors.
Code written for the standalone package’s dictionary-valued elbo can be
migrated as follows:
Standalone |
|
|---|---|
|
|
|
|
|
|
Citation#
Silvestrin, F., Li, C., & Acerbi, L. (2025). Stacking Variational Bayesian Monte Carlo. Transactions on Machine Learning Research. arXiv:2504.05004, TMLR. Please cite it together with the VBMC and PyVBMC papers when you use S-VBMC.
- class pyvbmc.svbmc.SVBMC(vp_list: list, s_max: float = np.float64(2.23606797749979), M_min: float | int = 0.6666666666666666, seed=None, *, show_tips: bool = True, noisy: bool | None = None)[source]#
Stacking Variational Bayesian Monte Carlo (S-VBMC).
Combines the variational posteriors of several completed VBMC runs on the same problem into one stacked mixture by optimizing the stacked ELBO with respect to the mixture weights only. Component locations and scales, and the expected log-joint statistics VBMC stored with each posterior, stay fixed, so no further evaluations of the model are needed.
Construct the object with the fitted posteriors, then call
optimize(). Construction discards runs that VBMC did not mark as converged (stats["stable"]) and runs whose expected log-joint carries excessive uncertainty (s_max), and raises when fewer thanM_minruns remain.The stacked posterior is a composite over the input posteriors: each VBMC run has its own parameter transform, so components of different runs live in different transformed spaces and are only comparable in the original space. Use
sample()for estimates and plots; do not combine the component means and covariances of different runs directly.Requires the optional
torchextra (pip install --upgrade "pyvbmc[torch]"); construction raisesImportErrorwithout it. An object saved withsave()can be loaded, sampled and plotted without it.- Parameters:
- vp_listlist of VariationalPosterior
Posteriors returned by completed VBMC runs on the same model, data and original parameter space. Construction verifies the dimension, the mixture shapes, the statistics stacking reads, and that every run carries the same original-space hard bounds; the runs are free to map that space differently. That they were run on the same model and data does not follow from the posteriors and is not verified.
- s_maxfloat, optional
Tolerance on the standard deviation of the expected log-joint of any single component,
sqrt(max(stats["J_sjk"])). A run at or above it is discarded as poorly converged. Defaultsqrt(5).- M_minint or float, optional
Minimum number of runs that must survive the filters: a count when greater than 1, a proportion of
len(vp_list)when at most 1 (so exactly 1 means every run). Default2/3.- seedNone, int, array_like[int], SeedSequence, BitGenerator or Generator, optional
Seed of the object’s random generator, which drives the Monte Carlo entropy estimate and
sample().Nonederives the generator from NumPy’s global state, sonp.random.seedbefore construction still fixes a run. DefaultNone.- show_tipsbool, optional
Show occasional guidance when INFO logging is enabled. Set to
Falseto suppress tips while keeping the messages on progress and on the headline ELBO. DefaultTrue.- noisybool or None, optional
Override the noise status of the whole stack. With
None(the default), read each retained run’suncertainty_handling_levelstatistic; when absent, infer noise fromelbo_sd > 0.1. This inference can misclassify old posteriors. Any noisy retained run makes the headlineelbothe two-level shrinkage estimate, where it is defined.
- Attributes:
- vp_listlist of VariationalPosterior
The retained posteriors, in input order. They are read, never modified: sampling works on copies that use this object’s generator.
- Dint
Dimension of the problem.
- Mint
Number of retained runs.
- Klist of int
Number of components of each retained run.
- wnp.ndarray, shape (1, K_total)
Weights of all components, concatenated in run order and normalized.
optimize()replaces them with the optimized ones.- elbofloat or None
After
optimize(): the headline estimate of the stacked ELBO at the returned weights, evaluated with fresh draws. For a noiseless stack it is the raw ELBO; for a noisy stack, the two-level shrinkage estimateelbo_details["shrunk_two_level"], or the component-median capped ELBO where the shrinkage is numerically undefined. Choosing the weights on noisy estimates adds an upward bias to the raw ELBO of a noisy stack, which the shrinkage targets. The shrinkage can also remove part of the bias that each run’s ELBO carries from VBMC, and residual bias can remain. The raw ELBO is not an upper bound on the stack’s true ELBO.- elbo_sdfloat or None
Estimated uncertainty of the raw evaluation at the returned weights: entropy Monte Carlo error and GP quadrature uncertainty, treating runs as independent. It excludes selection bias and the uncertainty of the shrinkage or of the cap; it does not define a calibrated confidence interval for the headline of a noisy stack.
- elbo_detailsdict or None
After
optimize():raw,capped_I_median,capped_E_median,naiveandshrunk_two_levelELBO estimates;shrinkage_noise_share;headline_method(raw,shrunk_two_levelorcapped_I_median);cap_amount(the reduction the median cap applied to the headline, zero unless the headline iscapped_I_median);entropy_sd,gp_sdandraw_sd(equal toelbo_sd);noisyandnoise_status_source. Shrinkage adjusts component estimates within each run and then shifts run levels, at the selected weights and with the same final entropy asraw.shrunk_two_levelisNonewhen its numerical calculation is undefined, in which case optimization still succeeds with aRuntimeWarning.shrinkage_noise_shareis the estimated noise-to-spread ratio averaged over runs using their selected masses; it can exceed one and can beNoneindependently. Naive stacking equally weights retained runs with their original internal weights. Its estimate inherits the runs’ errors and is a diagnostic baseline, not a bound on the optimized ELBO.- noisybool
Whether the stack is treated as noisy.
- noise_status_sourcetuple of str
How each retained run’s noise status was obtained:
recorded,inferredoroverride, in retained-run order.- entropyfloat or None
After
optimize(): the entropy estimate of the stacked posterior at the optimized weights.- Inp.ndarray, shape (1, K_total)
Expected log-joint of every component (the mean over the run’s GP hyperparameter samples of
stats["I_sk"]), in the run’s own transformed space.- individual_elboslist of float
The ELBO of each retained run, used to initialize the weights.
- I_corrected, E_correctednp.ndarray
The expected log-joints corrected to the original space, per component (shape
(1, K_total)) and per run (shape(M,)), computed deterministically at construction. The capped ELBO variants limit the expected log-joint to their medians.- rngnp.random.Generator
The object’s random generator.
- loggerlogging.Logger
Progress goes to the
"SVBMC"logger atINFOlevel; raise its level (toWARNING, say) to quieten a run.
- classmethod load(file)[source]#
Load a stacked posterior from a file written by
save().Loading needs no torch:
sample()andplot()of the loaded object work without it, whileoptimize()and the methods that estimate the stacked ELBO or its entropy raiseImportErroras construction does. The logging level is not saved: progress goes to the"SVBMC"logger atINFOlevel unless a level has been set on it, as for a newly constructed object.- Parameters:
- filepath-like
The file name or path to read from. Default file extension .pkl will be added if no extension is specified.
- Returns:
- stackedSVBMC
The loaded object.
- Raises:
- TypeError
If the file holds another kind of object, such as a
VariationalPosterior.- OSError
If the file cannot be found, or cannot be opened for other reasons.
- maximize_ELBO(n_samples: int = 20, lr: float = 0.1, max_steps: int = 500, version: str = 'all-weights')[source]#
Maximize the stacked ELBO over the weights with Adam.
The weights are the softmax of unconstrained logits, initialized from the runs’ own weights and ELBOs so that better runs start heavier. That starting point belongs to the stack, so every call departs from it whatever
wcurrently holds. Every step draws fresh entropy samples. The optimization stops when the ELBO, rounded to five decimals, has not improved for five consecutive steps, or aftermax_steps; the best iterate is returned.- Parameters:
- n_samplesint, optional
Draws per component for the entropy estimate. Default 20.
- lrfloat, optional
Adam learning rate. Default 0.1.
- max_stepsint, optional
Maximum number of steps. Default 500.
- version{“all-weights”, “posterior-only”, “ns”}, optional
"all-weights"optimizes the weight of every component;"posterior-only"optimizes one weight per run, keeping the relative weights within a run;"ns"(naive stacking) performs no optimization and returns the normalized input weights. Default"all-weights".
- Returns:
- w_finaltorch.Tensor, shape (K_total,)
The optimized weights (float64).
- elbo_besttorch.Tensor
Optimization-time Monte Carlo ELBO from the selected iteration (float64 scalar).
optimize()re-evaluates the returned weights with fresh draws for the reported headline.- entropy_besttorch.Tensor
The entropy estimate at the returned weights (float64 scalar).
- optimize(n_samples: int = 20, lr: float = 0.1, max_steps: int = 500, version: str = 'all-weights', *, n_samples_final: int = 100)[source]#
Optimize the stacked posterior in place.
Runs
maximize_ELBO(), stores the optimized weights inw(NumPy float64), the entropy estimate inentropyand a fresh final evaluation inelbo,elbo_sdandelbo_details. The report includes a deterministic two-level shrinkage estimate at those weights, computed with the same final entropy and without random draws; where it is defined, it is the headline of a noisy stack. ReturnsNone.- Parameters:
- n_samplesint, optional
Draws per component for the entropy estimate. Default 20.
- lrfloat, optional
Adam learning rate. Default 0.1.
- max_stepsint, optional
Maximum number of steps. Default 500.
- version{“all-weights”, “posterior-only”, “ns”}, optional
Optimization mode; see
maximize_ELBO(). Default"all-weights".- n_samples_finalint, optional
Fresh entropy draws per component at the returned weights and at the naive-stacking weights. At least 2, to estimate sampling variance. Default 100. Naive mode reuses one final evaluation.
- plot(n_samples: int = 10000, color: str = 'black', figsize: tuple | None = None, smooth: float | None = None, **corner_kwargs)[source]#
Draw a corner plot of the stacked posterior from fresh samples.
- Parameters:
- n_samplesint, optional
Draws taken with
sample(). Default 10000.- colorstr, optional
Matplotlib color of the contours and histograms. Default
"black".- figsize(float, float) or None, optional
Figure size in inches;
Noneuses 2.5 inches per dimension.- smoothfloat or None, optional
Gaussian kernel smoothing applied by
corner.- **corner_kwargs
Passed to
corner.corner().
- Returns:
- matplotlib.figure.Figure
The figure.
- sample(n_samples: int, balance_flag: bool = False)[source]#
Draw samples from the stacked posterior in the original space.
The stacked posterior is a mixture over the retained runs, so a draw picks a run with probability equal to that run’s total weight and then a point from that run’s reweighted posterior through its own transform. By default the run counts are multinomial, the component choice within a run is random (see
VariationalPosterior.sample()) and the rows are shuffled, so the result is an independent sample of exactlyn_samplesrows. Withbalance_flag=Truethe draws are stratified instead: the number taken from a run is within one draw of that run’s exact share of the weights, and within a run they are spread over its components by the balanced allocation ofVariationalPosterior.sample(), which follows the component weights up to the random placement of its remainder and so holds no exact bound per component. Stratification lowers the variance of expectations; it does not give an independent sample. All randomness comes from this object’s generator; the input posteriors are not touched.- Parameters:
- n_samplesint
Number of draws.
- balance_flagbool, optional
Stratify the draws by run and component. Default
False.
- Returns:
- Xnp.ndarray, shape (n_samples, D)
Draws in the original parameter space.
- save(file, overwrite=False)[source]#
Save the stacked posterior to a file.
The file holds the whole object: the retained posteriors, the weights, the ELBO report and the state of the generator, so the object that
load()returns draws what this one would draw next. It holds no Python bytecode, so it can be loaded, used and saved again under another minor version of Python than the one that wrote it.- Parameters:
- filepath-like
The file name or path to write to. Default file extension .pkl will be added if no extension is specified.
- overwritebool
Whether to allow overwriting existing files. Default False.
- Raises:
- FileExistsError
If the file already exists and
overwriteis False.- OSError
If the file cannot be opened for other reasons (e.g., the directory is not found, the disk is full, etc.).
- stacked_ELBO(w, n_samples: int = 20)[source]#
Estimate the stacked ELBO at given weights.
The expected log-joint is the weighted sum of the components’ stored expected log-joints (from the runs’ GP surrogates), corrected to the original space with the Jacobian terms of
stacked_entropy(); the entropy is estimated by Monte Carlo. The expected log-joints are corrected deterministically during construction.- Parameters:
- wtorch.Tensor, np.ndarray or list
Weights of the stacked posterior (
K_totalvalues; normalized internally). NumPy or list input is converted to a float64 tensor.- n_samplesint, optional
Draws per component for the entropy estimate. Default 20.
- Returns:
- ELBOtorch.Tensor
The estimated stacked ELBO (float64 scalar).
- Htorch.Tensor
The estimated entropy (float64 scalar).
- stacked_entropy(w, n_samples: int = 20)[source]#
Monte Carlo estimate of the entropy of the stacked posterior.
Differentiable with respect to the mixture weights. Every component contributes
n_samplesdraws, taken in its own run’s transformed space and mapped to the original space; the log density of the stacked mixture at every draw is then evaluated by mapping the draws into each run’s space with the matching Jacobian correction. The draws come from this object’s generator.- Parameters:
- wtorch.Tensor
Weights of the stacked posterior (any shape holding
K_totalvalues; normalized internally). Converted to float64.- n_samplesint, optional
Draws per component. Default 20.
- Returns:
- Htorch.Tensor
The entropy estimate (a float64 scalar).
- J_correctionsnp.ndarray, shape (K_total,)
Per-component expected log-Jacobian, computed deterministically at construction. Subtracting it from the stored expected log-joints expresses them in the original space, which
I_correctedalready holds andstacked_ELBO()reads; this copy is for inspection.
Helpers for comparing VBMC runs and choosing their starting points.
- pyvbmc.svbmc.utils.find_init_bounds(LB: ndarray = None, UB: ndarray = None, PLB: ndarray = None, PUB: ndarray = None)[source]#
Find the bounds to sample uniformly from when choosing starting points for VBMC.
If plausible lower bounds (PLB) are specified, it uses those as lower sampling bounds (sample_LB), otherwise it uses lower bounds (LB). Upper sampling bounds (sample_UB) are determined in the same way with plausible upper bounds (PUB) or upper bounds (UB).
At least one of LB, UB, PLB and PUB must be specified as an array with the same dimensionality of the inference problem. Each bound may be a scalar (broadcast to every dimension) or an array holding D values in any layout, including the
(1, D)rows PyVBMC uses; the returned bounds are flat(D,)arrays.- Parameters:
- LB, UBnp.ndarray, optional
Inputs for VBMC. They represent lower (LB) and upper (UB) bounds for the coordinate vector, x, so that the posterior has support on LB < x < UB. If scalars, the bound is replicated in each dimension. Use
Nonefor LB and UB if no bounds exist. Set LB [d] = -inf and UB [d] = inf if the d-th coordinate is unbounded (while other coordinates may be bounded). Note that if LB and UB contain unbounded variables, the respective values of PLB and PUB need to be specified (see below). If PLB and PUB are not specified (see below), the lower and upper sampling bounds (sample_LB and sample_UB, respectively) will be determined by LB and UB. Both are by default None.- PLB, PUBnp.ndarray, optional
Inputs for VBMC. They represent a set of plausible lower (PLB) and upper (PUB) bounds such that LB < PLB < PUB < UB. Both PLB and PUB need to be finite. PLB and PUB represent a “plausible” range, which should denote a region of high posterior probability mass. Among other things, the plausible box is used by VBMC to draw initial samples and to set priors over hyperparameters of the algorithm. If PLB and PUB are specified, they determine the lower and upper sampling bounds (sample_LB and sample_UB, respectively). If they are not, the sampling bounds will be determined by LB and UB. Both are by default None.
- Returns:
- sample_LB, sample_UBnp.ndarray
Lower (sample_LB) and upper (sample_UB) bounds to sample uniformly from when initializing VBMC.
- pyvbmc.svbmc.utils.overlay_corner_plot(samples: List[ndarray], labels: List[str] | None = None, colors: List[str] | None = None, figsize: Tuple[float, float] | None = None, smooth: float | None = None, base: float = 2.5, **corner_kwargs)[source]#
Overlay a corner plot for multiple sample sets.
- Parameters:
- sampleslist[np.ndarray]
List of arrays, each of shape (N_i,`D`) where D is the dimensionality.
- labelslist[str], optional
Legend labels corresponding to each item in samples. Defaults to “Run 1”, “Run 2”, …
- colorslist[str], optional
Matplotlib colors for each sample set. Defaults to the rcParams color cycle.
- figsize(float, float) or None, optional
Figure size in inches. If None, uses base`*`D on each side.
- smoothfloat or None, optional
Gaussian kernel smoothing (px) applied by
corner.- basefloat, default 2.5
Inches per variable when auto-sizing.
- **corner_kwargs
Extra keywords forwarded to
corner.corner()(e.g.bins=40,levels=(0.68, 0.95)).
- Returns:
- matplotlib.figure.Figure
The resulting corner-plot figure.