Source code for cuqi.legacy.sampler._laplace_approximation

import scipy as sp
import numpy as np
import cuqi
from cuqi.distribution import Normal
from cuqi.solver import CGLS
from cuqi.legacy.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." arXiv preprint arXiv:2104.06919 (2021). Parameters ---------- target : `cuqi.distribution.Posterior` The target posterior distribution to sample. x0 : ndarray Initial parameters. *Optional* maxit : int Maximum number of inner iterations for solver when generating one sample. tol : float Tolerance for inner solver. Will stop before maxit if the inner solvers convergence check reaches tol. beta : float Smoothing parameter for the Gaussian approximation of the Laplace distribution. Larger beta is easier to sample but is a worse approximation. rng : np.random.RandomState Random number generator used for sampling. *Optional* callback : callable, *Optional* If set this function will be called after every sample. The signature of the callback function is `callback(sample, sample_index)`, where `sample` is the current sample and `sample_index` is the index of the sample. An example is shown in demos/demo31_callback.py. Returns ------- cuqi.samples.Samples Samples from the posterior distribution. """
[docs] def __init__(self, target, x0=None, maxit=50, tol=1e-4, beta=1e-5, rng=None, **kwargs): super().__init__(target, x0=x0, **kwargs) # 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.target.likelihood.model, cuqi.model.AffineModel): raise TypeError("Model needs to be affine or linear") # Check Gaussian likelihood if not hasattr(self.target.likelihood.distribution, "sqrtprec"): raise TypeError("Distribution in Likelihood must contain a sqrtprec attribute") # Check that prior is LMRF if not isinstance(self.target.prior, cuqi.distribution.LMRF): raise ValueError('Unadjusted Gaussian Laplace approximation (UGLA) requires LMRF prior') # Modify initial guess since Sampler sets it to ones. if x0 is not None: self.x0 = x0 else: self.x0 = np.zeros(self.target.prior.dim) # Store internal parameters self.maxit = maxit self.tol = tol self.beta = beta self.rng = rng
def _sample_adapt(self, Ns, Nb): return self._sample(Ns, Nb) def _sample(self, Ns, Nb): """ Sample from the approximate posterior. Parameters ---------- Ns : int Number of samples to draw. Nb : int Number of burn-in samples to discard. Returns ------- samples : ndarray Samples from the approximate posterior. target_eval : ndarray Log-likelihood of each sample. acc : ndarray Acceptance rate of each sample. """ # Extract diff_op from target prior D = self.target.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 # Now prepare "LinearRTO" type sampler. TODO: Use LinearRTO for this instead self._shift = 0 # Pre-computations self._model = self.target.likelihood.model self._data = self.target.likelihood.data - self.target.model._shift self._m = len(self._data) self._L1 = self.target.likelihood.distribution.sqrtprec # If prior location is scalar, repeat it to match dimensions if len(self.target.prior.location) == 1: self._priorloc = np.repeat(self.target.prior.location, self.dim) else: self._priorloc = self.target.prior.location # Initial Laplace approx self._L2 = Lk_fun(self.x0) self._L2mu = self._L2@self._priorloc self._b_tild = np.hstack([self._L1@self._data, self._L2mu]) #self.n = len(self.x0) # 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.target.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.target.prior.scale)*(self._L2.T @ x[idx:]) out = out1 + out2 return out # Initialize samples N = Ns+Nb # number of simulations samples = np.empty((self.target.dim, N)) # initial state samples[:, 0] = self.x0 for s in range(N-1): # Update Laplace approximation self._L2 = Lk_fun(samples[:, s]) self._L2mu = self._L2@self._priorloc self._b_tild = np.hstack([self._L1@self._data, self._L2mu]) # Sample from approximate posterior e = Normal(mean=np.zeros(len(self._b_tild)), std=1).sample(rng=self.rng) y = self._b_tild + e # Perturb data sim = CGLS(M, y, samples[:, s], self.maxit, self.tol, self._shift) samples[:, s+1], _ = sim.solve() self._print_progress(s+2,N) #s+2 is the sample number, s+1 is index assuming x0 is the first sample self._call_callback(samples[:, s+1], s+1) # remove burn-in samples = samples[:, Nb:] return samples, None, None