Source code for cuqi.sampler._laplace_approximation
import scipy as sp
import numpy as np
import cuqi
from cuqi.solver import CGLS
from cuqi.sampler import Sampler
[docs]
class UGLA(Sampler):
""" Unadjusted (Gaussian) Laplace Approximation sampler
Samples an approximate posterior where the prior is approximated
by a Gaussian distribution. The likelihood must be Gaussian.
Currently only works for LMRF priors.
The inner solver is Conjugate Gradient Least Squares (CGLS) solver.
For more details see: Uribe, Felipe, et al. A hybrid Gibbs sampler for edge-preserving
tomographic reconstruction with uncertain view angles. SIAM/ASA Journal on UQ,
https://doi.org/10.1137/21M1412268 (2022).
Parameters
----------
target : `cuqi.distribution.Posterior`
The target posterior distribution to sample.
initial_point : ndarray, *Optional*
Initial parameters.
If not provided, it defaults to zeros.
maxit : int
Maximum number of inner iterations for solver when generating one sample.
If not provided, it defaults to 50.
tol : float
Tolerance for inner solver.
The inner solvers will stop before maxit if convergence check reaches tol.
If not provided, it defaults to 1e-4.
beta : float
Smoothing parameter for the Gaussian approximation of the Laplace distribution.
A small value in the range of 1e-7 to 1e-3 is recommended, though values out of this
range might give better results in some cases. Generally, a larger beta value makes
sampling easier but results in a worse approximation. See details in Section 3.3 of the paper.
If not provided, it defaults to 1e-5.
callback : callable, optional
A function that will be called after each sampling step. It can be useful for monitoring the sampler during sampling.
The function should take three arguments: the sampler object, the index of the current sampling step, the total number of requested samples. The last two arguments are integers. An example of the callback function signature is: `callback(sampler, sample_index, num_of_samples)`.
"""
[docs]
def __init__(self, target=None, initial_point=None, maxit=50, tol=1e-4, beta=1e-5, **kwargs):
super().__init__(target=target, initial_point=initial_point, **kwargs)
# Parameters
self.maxit = maxit
self.tol = tol
self.beta = beta
def _initialize(self):
self._precompute()
@property
def prior(self):
return self.target.prior
@property
def likelihood(self):
return self.target.likelihood
@property
def model(self):
return self.target.model
@property
def _data(self):
return self.target.data - self.target.model._shift
def _precompute(self):
D = self.prior._diff_op
n = D.shape[0]
# Gaussian approximation of LMRF prior as function of x_k
def Lk_fun(x_k):
dd = 1/np.sqrt((D @ x_k)**2 + self.beta*np.ones(n))
W = sp.sparse.diags(dd)
return W.sqrt() @ D
self.Lk_fun = Lk_fun
self._m = len(self._data)
self._L1 = self.likelihood.distribution.sqrtprec
# If prior location is scalar, repeat it to match dimensions
if len(self.prior.location) == 1:
self._priorloc = np.repeat(self.prior.location, self.dim)
else:
self._priorloc = self.prior.location
# Initial Laplace approx
self._L2 = Lk_fun(self.initial_point)
self._L2mu = self._L2@self._priorloc
self._b_tild = np.hstack([self._L1@self._data, self._L2mu])
# Least squares form
def M(x, flag):
if flag == 1:
out1 = self._L1 @ self.model._forward_func_no_shift(x) # Use forward function which excludes shift
out2 = np.sqrt(1/self.prior.scale)*(self._L2 @ x)
out = np.hstack([out1, out2])
elif flag == 2:
idx = int(self._m)
out1 = self.model._adjoint_func_no_shift(self._L1.T@x[:idx])
out2 = np.sqrt(1/self.prior.scale)*(self._L2.T @ x[idx:])
out = out1 + out2
return out
self.M = M
[docs]
def step(self):
# Update Laplace approximation
self._L2 = self.Lk_fun(self.current_point)
self._L2mu = self._L2@self._priorloc
self._b_tild = np.hstack([self._L1@self._data, self._L2mu])
# Sample from approximate posterior
e = np.random.randn(len(self._b_tild))
y = self._b_tild + e # Perturb data
sim = CGLS(self.M, y, self.current_point, self.maxit, self.tol)
self.current_point, _ = sim.solve()
acc = 1
return acc
[docs]
def tune(self, skip_len, update_count):
pass
[docs]
def validate_target(self):
# Check target type
if not isinstance(self.target, cuqi.distribution.Posterior):
raise ValueError(f"To initialize an object of type {self.__class__}, 'target' need to be of type 'cuqi.distribution.Posterior'.")
# Check Affine model
if not isinstance(self.likelihood.model, cuqi.model.AffineModel):
raise TypeError("Model needs to be affine or linear")
# Check Gaussian likelihood
if not hasattr(self.likelihood.distribution, "sqrtprec"):
raise TypeError("Distribution in Likelihood must contain a sqrtprec attribute")
# Check that prior is LMRF
if not isinstance(self.prior, cuqi.distribution.LMRF):
raise ValueError('Unadjusted Gaussian Laplace approximation (UGLA) requires LMRF prior')
def _get_default_initial_point(self, dim):
""" Get the default initial point for the sampler. Defaults to an array of zeros. """
return np.zeros(dim)