CUQIpy 1.1.1.post0.dev36__py3-none-any.whl → 1.4.1.post0.dev124__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of CUQIpy might be problematic. Click here for more details.
- cuqi/__init__.py +2 -0
- cuqi/_version.py +3 -3
- cuqi/algebra/__init__.py +2 -0
- cuqi/algebra/_abstract_syntax_tree.py +358 -0
- cuqi/algebra/_ordered_set.py +82 -0
- cuqi/algebra/_random_variable.py +457 -0
- cuqi/array/_array.py +4 -13
- cuqi/config.py +7 -0
- cuqi/density/_density.py +9 -1
- cuqi/distribution/__init__.py +3 -2
- cuqi/distribution/_beta.py +7 -11
- cuqi/distribution/_cauchy.py +2 -2
- cuqi/distribution/_custom.py +0 -6
- cuqi/distribution/_distribution.py +31 -45
- cuqi/distribution/_gamma.py +7 -3
- cuqi/distribution/_gaussian.py +2 -12
- cuqi/distribution/_inverse_gamma.py +4 -10
- cuqi/distribution/_joint_distribution.py +112 -15
- cuqi/distribution/_lognormal.py +0 -7
- cuqi/distribution/{_modifiedhalfnormal.py → _modified_half_normal.py} +23 -23
- cuqi/distribution/_normal.py +34 -7
- cuqi/distribution/_posterior.py +9 -0
- cuqi/distribution/_truncated_normal.py +129 -0
- cuqi/distribution/_uniform.py +47 -1
- cuqi/experimental/__init__.py +2 -2
- cuqi/experimental/_recommender.py +216 -0
- cuqi/geometry/__init__.py +2 -0
- cuqi/geometry/_geometry.py +15 -1
- cuqi/geometry/_product_geometry.py +181 -0
- cuqi/implicitprior/__init__.py +5 -3
- cuqi/implicitprior/_regularized_gaussian.py +483 -0
- cuqi/implicitprior/{_regularizedGMRF.py → _regularized_gmrf.py} +4 -2
- cuqi/implicitprior/{_regularizedUnboundedUniform.py → _regularized_unbounded_uniform.py} +3 -2
- cuqi/implicitprior/_restorator.py +269 -0
- cuqi/legacy/__init__.py +2 -0
- cuqi/{experimental/mcmc → legacy/sampler}/__init__.py +7 -11
- cuqi/legacy/sampler/_conjugate.py +55 -0
- cuqi/legacy/sampler/_conjugate_approx.py +52 -0
- cuqi/legacy/sampler/_cwmh.py +196 -0
- cuqi/legacy/sampler/_gibbs.py +231 -0
- cuqi/legacy/sampler/_hmc.py +335 -0
- cuqi/{experimental/mcmc → legacy/sampler}/_langevin_algorithm.py +82 -111
- cuqi/legacy/sampler/_laplace_approximation.py +184 -0
- cuqi/legacy/sampler/_mh.py +190 -0
- cuqi/legacy/sampler/_pcn.py +244 -0
- cuqi/{experimental/mcmc → legacy/sampler}/_rto.py +132 -90
- cuqi/legacy/sampler/_sampler.py +182 -0
- cuqi/likelihood/_likelihood.py +9 -1
- cuqi/model/__init__.py +1 -1
- cuqi/model/_model.py +1361 -359
- cuqi/pde/__init__.py +4 -0
- cuqi/pde/_observation_map.py +36 -0
- cuqi/pde/_pde.py +134 -33
- cuqi/problem/_problem.py +93 -87
- cuqi/sampler/__init__.py +120 -8
- cuqi/sampler/_conjugate.py +376 -35
- cuqi/sampler/_conjugate_approx.py +40 -16
- cuqi/sampler/_cwmh.py +132 -138
- cuqi/{experimental/mcmc → sampler}/_direct.py +1 -1
- cuqi/sampler/_gibbs.py +288 -130
- cuqi/sampler/_hmc.py +328 -201
- cuqi/sampler/_langevin_algorithm.py +284 -100
- cuqi/sampler/_laplace_approximation.py +87 -117
- cuqi/sampler/_mh.py +47 -157
- cuqi/sampler/_pcn.py +65 -213
- cuqi/sampler/_rto.py +211 -142
- cuqi/sampler/_sampler.py +553 -136
- cuqi/samples/__init__.py +1 -1
- cuqi/samples/_samples.py +24 -18
- cuqi/solver/__init__.py +6 -4
- cuqi/solver/_solver.py +230 -26
- cuqi/testproblem/_testproblem.py +2 -3
- cuqi/utilities/__init__.py +6 -1
- cuqi/utilities/_get_python_variable_name.py +2 -2
- cuqi/utilities/_utilities.py +182 -2
- {CUQIpy-1.1.1.post0.dev36.dist-info → cuqipy-1.4.1.post0.dev124.dist-info}/METADATA +10 -6
- cuqipy-1.4.1.post0.dev124.dist-info/RECORD +101 -0
- {CUQIpy-1.1.1.post0.dev36.dist-info → cuqipy-1.4.1.post0.dev124.dist-info}/WHEEL +1 -1
- CUQIpy-1.1.1.post0.dev36.dist-info/RECORD +0 -92
- cuqi/experimental/mcmc/_conjugate.py +0 -197
- cuqi/experimental/mcmc/_conjugate_approx.py +0 -81
- cuqi/experimental/mcmc/_cwmh.py +0 -191
- cuqi/experimental/mcmc/_gibbs.py +0 -268
- cuqi/experimental/mcmc/_hmc.py +0 -470
- cuqi/experimental/mcmc/_laplace_approximation.py +0 -156
- cuqi/experimental/mcmc/_mh.py +0 -78
- cuqi/experimental/mcmc/_pcn.py +0 -89
- cuqi/experimental/mcmc/_sampler.py +0 -561
- cuqi/experimental/mcmc/_utilities.py +0 -17
- cuqi/implicitprior/_regularizedGaussian.py +0 -323
- {CUQIpy-1.1.1.post0.dev36.dist-info → cuqipy-1.4.1.post0.dev124.dist-info/licenses}/LICENSE +0 -0
- {CUQIpy-1.1.1.post0.dev36.dist-info → cuqipy-1.4.1.post0.dev124.dist-info}/top_level.txt +0 -0
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import scipy as sp
|
|
2
2
|
import numpy as np
|
|
3
3
|
import cuqi
|
|
4
|
-
from cuqi.distribution import Normal
|
|
5
4
|
from cuqi.solver import CGLS
|
|
6
5
|
from cuqi.sampler import Sampler
|
|
7
6
|
|
|
8
|
-
|
|
9
7
|
class UGLA(Sampler):
|
|
10
8
|
""" Unadjusted (Gaussian) Laplace Approximation sampler
|
|
11
9
|
|
|
@@ -16,103 +14,70 @@ class UGLA(Sampler):
|
|
|
16
14
|
|
|
17
15
|
The inner solver is Conjugate Gradient Least Squares (CGLS) solver.
|
|
18
16
|
|
|
19
|
-
For more details see: Uribe, Felipe, et al.
|
|
20
|
-
tomographic reconstruction with uncertain view angles.
|
|
17
|
+
For more details see: Uribe, Felipe, et al. A hybrid Gibbs sampler for edge-preserving
|
|
18
|
+
tomographic reconstruction with uncertain view angles. SIAM/ASA Journal on UQ,
|
|
19
|
+
https://doi.org/10.1137/21M1412268 (2022).
|
|
21
20
|
|
|
22
21
|
Parameters
|
|
23
22
|
----------
|
|
24
23
|
target : `cuqi.distribution.Posterior`
|
|
25
24
|
The target posterior distribution to sample.
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
Initial parameters.
|
|
26
|
+
initial_point : ndarray, *Optional*
|
|
27
|
+
Initial parameters.
|
|
28
|
+
If not provided, it defaults to zeros.
|
|
29
29
|
|
|
30
30
|
maxit : int
|
|
31
31
|
Maximum number of inner iterations for solver when generating one sample.
|
|
32
|
+
If not provided, it defaults to 50.
|
|
32
33
|
|
|
33
34
|
tol : float
|
|
34
|
-
Tolerance for inner solver.
|
|
35
|
+
Tolerance for inner solver.
|
|
36
|
+
The inner solvers will stop before maxit if convergence check reaches tol.
|
|
37
|
+
If not provided, it defaults to 1e-4.
|
|
35
38
|
|
|
36
39
|
beta : float
|
|
37
|
-
Smoothing parameter for the Gaussian approximation of the Laplace distribution.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
An example is shown in demos/demo31_callback.py.
|
|
47
|
-
|
|
48
|
-
Returns
|
|
49
|
-
-------
|
|
50
|
-
cuqi.samples.Samples
|
|
51
|
-
Samples from the posterior distribution.
|
|
52
|
-
|
|
40
|
+
Smoothing parameter for the Gaussian approximation of the Laplace distribution.
|
|
41
|
+
A small value in the range of 1e-7 to 1e-3 is recommended, though values out of this
|
|
42
|
+
range might give better results in some cases. Generally, a larger beta value makes
|
|
43
|
+
sampling easier but results in a worse approximation. See details in Section 3.3 of the paper.
|
|
44
|
+
If not provided, it defaults to 1e-5.
|
|
45
|
+
|
|
46
|
+
callback : callable, optional
|
|
47
|
+
A function that will be called after each sampling step. It can be useful for monitoring the sampler during sampling.
|
|
48
|
+
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)`.
|
|
53
49
|
"""
|
|
50
|
+
def __init__(self, target=None, initial_point=None, maxit=50, tol=1e-4, beta=1e-5, **kwargs):
|
|
54
51
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
super().__init__(target, x0=x0, **kwargs)
|
|
58
|
-
|
|
59
|
-
# Check target type
|
|
60
|
-
if not isinstance(self.target, cuqi.distribution.Posterior):
|
|
61
|
-
raise ValueError(f"To initialize an object of type {self.__class__}, 'target' need to be of type 'cuqi.distribution.Posterior'.")
|
|
62
|
-
|
|
63
|
-
# Check Linear model
|
|
64
|
-
if not isinstance(self.target.likelihood.model, cuqi.model.LinearModel):
|
|
65
|
-
raise TypeError("Model needs to be linear")
|
|
52
|
+
super().__init__(target=target, initial_point=initial_point, **kwargs)
|
|
66
53
|
|
|
67
|
-
#
|
|
68
|
-
if not hasattr(self.target.likelihood.distribution, "sqrtprec"):
|
|
69
|
-
raise TypeError("Distribution in Likelihood must contain a sqrtprec attribute")
|
|
70
|
-
|
|
71
|
-
# Check that prior is LMRF
|
|
72
|
-
if not isinstance(self.target.prior, cuqi.distribution.LMRF):
|
|
73
|
-
raise ValueError('Unadjusted Gaussian Laplace approximation (UGLA) requires LMRF prior')
|
|
74
|
-
|
|
75
|
-
# Modify initial guess since Sampler sets it to ones.
|
|
76
|
-
if x0 is not None:
|
|
77
|
-
self.x0 = x0
|
|
78
|
-
else:
|
|
79
|
-
self.x0 = np.zeros(self.target.prior.dim)
|
|
80
|
-
|
|
81
|
-
# Store internal parameters
|
|
54
|
+
# Parameters
|
|
82
55
|
self.maxit = maxit
|
|
83
56
|
self.tol = tol
|
|
84
57
|
self.beta = beta
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
return self._sample(Ns, Nb)
|
|
89
|
-
|
|
90
|
-
def _sample(self, Ns, Nb):
|
|
91
|
-
""" Sample from the approximate posterior.
|
|
92
|
-
|
|
93
|
-
Parameters
|
|
94
|
-
----------
|
|
95
|
-
Ns : int
|
|
96
|
-
Number of samples to draw.
|
|
97
|
-
|
|
98
|
-
Nb : int
|
|
99
|
-
Number of burn-in samples to discard.
|
|
58
|
+
|
|
59
|
+
def _initialize(self):
|
|
60
|
+
self._precompute()
|
|
100
61
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
Samples from the approximate posterior.
|
|
62
|
+
@property
|
|
63
|
+
def prior(self):
|
|
64
|
+
return self.target.prior
|
|
105
65
|
|
|
106
|
-
|
|
107
|
-
|
|
66
|
+
@property
|
|
67
|
+
def likelihood(self):
|
|
68
|
+
return self.target.likelihood
|
|
108
69
|
|
|
109
|
-
|
|
110
|
-
|
|
70
|
+
@property
|
|
71
|
+
def model(self):
|
|
72
|
+
return self.target.model
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def _data(self):
|
|
76
|
+
return self.target.data - self.target.model._shift
|
|
111
77
|
|
|
112
|
-
|
|
78
|
+
def _precompute(self):
|
|
113
79
|
|
|
114
|
-
|
|
115
|
-
D = self.target.prior._diff_op
|
|
80
|
+
D = self.prior._diff_op
|
|
116
81
|
n = D.shape[0]
|
|
117
82
|
|
|
118
83
|
# Gaussian approximation of LMRF prior as function of x_k
|
|
@@ -120,65 +85,70 @@ class UGLA(Sampler):
|
|
|
120
85
|
dd = 1/np.sqrt((D @ x_k)**2 + self.beta*np.ones(n))
|
|
121
86
|
W = sp.sparse.diags(dd)
|
|
122
87
|
return W.sqrt() @ D
|
|
88
|
+
self.Lk_fun = Lk_fun
|
|
123
89
|
|
|
124
|
-
# Now prepare "LinearRTO" type sampler. TODO: Use LinearRTO for this instead
|
|
125
|
-
self._shift = 0
|
|
126
|
-
|
|
127
|
-
# Pre-computations
|
|
128
|
-
self._model = self.target.likelihood.model
|
|
129
|
-
self._data = self.target.likelihood.data
|
|
130
90
|
self._m = len(self._data)
|
|
131
|
-
self._L1 = self.
|
|
91
|
+
self._L1 = self.likelihood.distribution.sqrtprec
|
|
132
92
|
|
|
133
93
|
# If prior location is scalar, repeat it to match dimensions
|
|
134
|
-
if len(self.
|
|
135
|
-
self._priorloc = np.repeat(self.
|
|
94
|
+
if len(self.prior.location) == 1:
|
|
95
|
+
self._priorloc = np.repeat(self.prior.location, self.dim)
|
|
136
96
|
else:
|
|
137
|
-
self._priorloc = self.
|
|
97
|
+
self._priorloc = self.prior.location
|
|
138
98
|
|
|
139
99
|
# Initial Laplace approx
|
|
140
|
-
self._L2 = Lk_fun(self.
|
|
100
|
+
self._L2 = Lk_fun(self.initial_point)
|
|
141
101
|
self._L2mu = self._L2@self._priorloc
|
|
142
102
|
self._b_tild = np.hstack([self._L1@self._data, self._L2mu])
|
|
143
103
|
|
|
144
|
-
#self.n = len(self.x0)
|
|
145
|
-
|
|
146
104
|
# Least squares form
|
|
147
105
|
def M(x, flag):
|
|
148
106
|
if flag == 1:
|
|
149
|
-
out1 = self._L1 @ self.
|
|
150
|
-
out2 = np.sqrt(1/self.
|
|
107
|
+
out1 = self._L1 @ self.model._forward_func_no_shift(x) # Use forward function which excludes shift
|
|
108
|
+
out2 = np.sqrt(1/self.prior.scale)*(self._L2 @ x)
|
|
151
109
|
out = np.hstack([out1, out2])
|
|
152
110
|
elif flag == 2:
|
|
153
111
|
idx = int(self._m)
|
|
154
|
-
out1 = self.
|
|
155
|
-
out2 = np.sqrt(1/self.
|
|
112
|
+
out1 = self.model._adjoint_func_no_shift(self._L1.T@x[:idx])
|
|
113
|
+
out2 = np.sqrt(1/self.prior.scale)*(self._L2.T @ x[idx:])
|
|
156
114
|
out = out1 + out2
|
|
157
|
-
return out
|
|
158
|
-
|
|
159
|
-
# Initialize samples
|
|
160
|
-
N = Ns+Nb # number of simulations
|
|
161
|
-
samples = np.empty((self.target.dim, N))
|
|
162
|
-
|
|
163
|
-
# initial state
|
|
164
|
-
samples[:, 0] = self.x0
|
|
165
|
-
for s in range(N-1):
|
|
166
|
-
|
|
167
|
-
# Update Laplace approximation
|
|
168
|
-
self._L2 = Lk_fun(samples[:, s])
|
|
169
|
-
self._L2mu = self._L2@self._priorloc
|
|
170
|
-
self._b_tild = np.hstack([self._L1@self._data, self._L2mu])
|
|
171
|
-
|
|
172
|
-
# Sample from approximate posterior
|
|
173
|
-
e = Normal(mean=np.zeros(len(self._b_tild)), std=1).sample(rng=self.rng)
|
|
174
|
-
y = self._b_tild + e # Perturb data
|
|
175
|
-
sim = CGLS(M, y, samples[:, s], self.maxit, self.tol, self._shift)
|
|
176
|
-
samples[:, s+1], _ = sim.solve()
|
|
115
|
+
return out
|
|
116
|
+
self.M = M
|
|
177
117
|
|
|
178
|
-
|
|
179
|
-
|
|
118
|
+
def step(self):
|
|
119
|
+
# Update Laplace approximation
|
|
120
|
+
self._L2 = self.Lk_fun(self.current_point)
|
|
121
|
+
self._L2mu = self._L2@self._priorloc
|
|
122
|
+
self._b_tild = np.hstack([self._L1@self._data, self._L2mu])
|
|
123
|
+
|
|
124
|
+
# Sample from approximate posterior
|
|
125
|
+
e = np.random.randn(len(self._b_tild))
|
|
126
|
+
y = self._b_tild + e # Perturb data
|
|
127
|
+
sim = CGLS(self.M, y, self.current_point, self.maxit, self.tol)
|
|
128
|
+
self.current_point, _ = sim.solve()
|
|
129
|
+
acc = 1
|
|
130
|
+
return acc
|
|
131
|
+
|
|
132
|
+
def tune(self, skip_len, update_count):
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
def validate_target(self):
|
|
136
|
+
# Check target type
|
|
137
|
+
if not isinstance(self.target, cuqi.distribution.Posterior):
|
|
138
|
+
raise ValueError(f"To initialize an object of type {self.__class__}, 'target' need to be of type 'cuqi.distribution.Posterior'.")
|
|
139
|
+
|
|
140
|
+
# Check Affine model
|
|
141
|
+
if not isinstance(self.likelihood.model, cuqi.model.AffineModel):
|
|
142
|
+
raise TypeError("Model needs to be affine or linear")
|
|
180
143
|
|
|
181
|
-
#
|
|
182
|
-
|
|
144
|
+
# Check Gaussian likelihood
|
|
145
|
+
if not hasattr(self.likelihood.distribution, "sqrtprec"):
|
|
146
|
+
raise TypeError("Distribution in Likelihood must contain a sqrtprec attribute")
|
|
147
|
+
|
|
148
|
+
# Check that prior is LMRF
|
|
149
|
+
if not isinstance(self.prior, cuqi.distribution.LMRF):
|
|
150
|
+
raise ValueError('Unadjusted Gaussian Laplace approximation (UGLA) requires LMRF prior')
|
|
183
151
|
|
|
184
|
-
|
|
152
|
+
def _get_default_initial_point(self, dim):
|
|
153
|
+
""" Get the default initial point for the sampler. Defaults to an array of zeros. """
|
|
154
|
+
return np.zeros(dim)
|
cuqi/sampler/_mh.py
CHANGED
|
@@ -4,187 +4,77 @@ from cuqi.sampler import ProposalBasedSampler
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class MH(ProposalBasedSampler):
|
|
7
|
-
"""Metropolis
|
|
8
|
-
|
|
9
|
-
Allows sampling of a target distribution by random-walk sampling of a proposal distribution along with an accept/reject step.
|
|
7
|
+
""" Metropolis-Hastings (MH) sampler.
|
|
10
8
|
|
|
11
9
|
Parameters
|
|
12
10
|
----------
|
|
11
|
+
target : cuqi.density.Density
|
|
12
|
+
Target density or distribution.
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
proposal : `cuqi.distribution.Distribution` or callable method
|
|
18
|
-
The proposal to sample from. If a callable method it should provide a single independent sample from proposal distribution. Defaults to a Gaussian proposal. *Optional*.
|
|
14
|
+
proposal : cuqi.distribution.Distribution or callable
|
|
15
|
+
Proposal distribution. If None, a random walk MH is used (i.e., Gaussian proposal with identity covariance).
|
|
19
16
|
|
|
20
17
|
scale : float
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
x0 : ndarray
|
|
24
|
-
Initial parameters. *Optional*
|
|
18
|
+
Scaling parameter for the proposal distribution.
|
|
25
19
|
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
kwargs : dict
|
|
21
|
+
Additional keyword arguments to be passed to the base class :class:`ProposalBasedSampler`.
|
|
28
22
|
|
|
29
|
-
|
|
30
|
-
If set this function will be called after every sample.
|
|
31
|
-
The signature of the callback function is `callback(sample, sample_index)`,
|
|
32
|
-
where `sample` is the current sample and `sample_index` is the index of the sample.
|
|
33
|
-
An example is shown in demos/demo31_callback.py.
|
|
34
|
-
|
|
35
|
-
Example
|
|
36
|
-
-------
|
|
37
|
-
.. code-block:: python
|
|
23
|
+
"""
|
|
38
24
|
|
|
39
|
-
|
|
40
|
-
dim = 5 # Dimension of distribution
|
|
41
|
-
mu = np.arange(dim) # Mean of Gaussian
|
|
42
|
-
std = 1 # standard deviation of Gaussian
|
|
25
|
+
_STATE_KEYS = ProposalBasedSampler._STATE_KEYS.union({'scale', '_scale_temp'})
|
|
43
26
|
|
|
44
|
-
|
|
45
|
-
|
|
27
|
+
def __init__(self, target=None, proposal=None, scale=1, **kwargs):
|
|
28
|
+
super().__init__(target, proposal=proposal, scale=scale, **kwargs)
|
|
46
29
|
|
|
47
|
-
|
|
48
|
-
|
|
30
|
+
def _initialize(self):
|
|
31
|
+
# Due to a bug? in old MH, we must keep track of this extra variable to match behavior.
|
|
32
|
+
self._scale_temp = self.scale
|
|
49
33
|
|
|
50
|
-
|
|
51
|
-
|
|
34
|
+
def validate_target(self):
|
|
35
|
+
# Fail only when there is no log density, which is currently assumed to be the case in case NaN is returned.
|
|
36
|
+
if np.isnan(self.target.logd(self._get_default_initial_point(self.dim))):
|
|
37
|
+
raise ValueError("Target does not have valid logd")
|
|
52
38
|
|
|
53
|
-
|
|
54
|
-
|
|
39
|
+
def validate_proposal(self):
|
|
40
|
+
if not isinstance(self.proposal, cuqi.distribution.Distribution):
|
|
41
|
+
raise ValueError("Proposal must be a cuqi.distribution.Distribution object")
|
|
42
|
+
if not self.proposal.is_symmetric:
|
|
43
|
+
raise ValueError("Proposal must be symmetric")
|
|
55
44
|
|
|
56
|
-
|
|
57
|
-
#target, proposal=None, scale=1, x0=None, dim=None
|
|
58
|
-
# super().__init__(target, proposal=proposal, scale=scale, x0=x0, dim=dim)
|
|
59
|
-
def __init__(self, target, proposal=None, scale=None, x0=None, dim=None, **kwargs):
|
|
60
|
-
""" Metropolis-Hastings (MH) sampler. Default (if proposal is None) is random walk MH with proposal that is Gaussian with identity covariance"""
|
|
61
|
-
super().__init__(target, proposal=proposal, scale=scale, x0=x0, dim=dim, **kwargs)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
@ProposalBasedSampler.proposal.setter
|
|
65
|
-
def proposal(self, value):
|
|
66
|
-
fail_msg = "Proposal should be either None, symmetric cuqi.distribution.Distribution or a lambda function."
|
|
67
|
-
|
|
68
|
-
if value is None:
|
|
69
|
-
self._proposal = cuqi.distribution.Gaussian(np.zeros(self.dim), 1)
|
|
70
|
-
elif not isinstance(value, cuqi.distribution.Distribution) and callable(value):
|
|
71
|
-
raise NotImplementedError(fail_msg)
|
|
72
|
-
elif isinstance(value, cuqi.distribution.Distribution) and value.is_symmetric:
|
|
73
|
-
self._proposal = value
|
|
74
|
-
else:
|
|
75
|
-
raise ValueError(fail_msg)
|
|
76
|
-
self._proposal.geometry = self.target.geometry
|
|
77
|
-
|
|
78
|
-
def _sample(self, N, Nb):
|
|
79
|
-
if self.scale is None:
|
|
80
|
-
raise ValueError("Scale must be set to sample without adaptation. Consider using sample_adapt instead.")
|
|
81
|
-
|
|
82
|
-
Ns = N+Nb # number of simulations
|
|
83
|
-
|
|
84
|
-
# allocation
|
|
85
|
-
samples = np.empty((self.dim, Ns))
|
|
86
|
-
target_eval = np.empty(Ns)
|
|
87
|
-
acc = np.zeros(Ns, dtype=int)
|
|
88
|
-
|
|
89
|
-
# initial state
|
|
90
|
-
samples[:, 0] = self.x0
|
|
91
|
-
target_eval[0] = self.target.logd(self.x0)
|
|
92
|
-
acc[0] = 1
|
|
93
|
-
|
|
94
|
-
# run MCMC
|
|
95
|
-
for s in range(Ns-1):
|
|
96
|
-
# run component by component
|
|
97
|
-
samples[:, s+1], target_eval[s+1], acc[s+1] = self.single_update(samples[:, s], target_eval[s])
|
|
98
|
-
self._print_progress(s+2,Ns) #s+2 is the sample number, s+1 is index assuming x0 is the first sample
|
|
99
|
-
self._call_callback(samples[:, s+1], s+1)
|
|
100
|
-
|
|
101
|
-
# remove burn-in
|
|
102
|
-
samples = samples[:, Nb:]
|
|
103
|
-
target_eval = target_eval[Nb:]
|
|
104
|
-
accave = acc[Nb:].mean()
|
|
105
|
-
print('\nAverage acceptance rate:', accave, '\n')
|
|
106
|
-
#
|
|
107
|
-
return samples, target_eval, accave
|
|
108
|
-
|
|
109
|
-
def _sample_adapt(self, N, Nb):
|
|
110
|
-
# Set intial scale if not set
|
|
111
|
-
if self.scale is None:
|
|
112
|
-
self.scale = 0.1
|
|
113
|
-
|
|
114
|
-
Ns = N+Nb # number of simulations
|
|
115
|
-
|
|
116
|
-
# allocation
|
|
117
|
-
samples = np.empty((self.dim, Ns))
|
|
118
|
-
target_eval = np.empty(Ns)
|
|
119
|
-
acc = np.zeros(Ns)
|
|
120
|
-
|
|
121
|
-
# initial state
|
|
122
|
-
samples[:, 0] = self.x0
|
|
123
|
-
target_eval[0] = self.target.logd(self.x0)
|
|
124
|
-
acc[0] = 1
|
|
125
|
-
|
|
126
|
-
# initial adaptation params
|
|
127
|
-
Na = int(0.1*N) # iterations to adapt
|
|
128
|
-
hat_acc = np.empty(int(np.floor(Ns/Na))) # average acceptance rate of the chains
|
|
129
|
-
lambd = self.scale
|
|
130
|
-
star_acc = 0.234 # target acceptance rate RW
|
|
131
|
-
i, idx = 0, 0
|
|
132
|
-
|
|
133
|
-
# run MCMC
|
|
134
|
-
for s in range(Ns-1):
|
|
135
|
-
# run component by component
|
|
136
|
-
samples[:, s+1], target_eval[s+1], acc[s+1] = self.single_update(samples[:, s], target_eval[s])
|
|
137
|
-
|
|
138
|
-
# adapt prop spread using acc of past samples
|
|
139
|
-
if ((s+1) % Na == 0):
|
|
140
|
-
# evaluate average acceptance rate
|
|
141
|
-
hat_acc[i] = np.mean(acc[idx:idx+Na])
|
|
142
|
-
|
|
143
|
-
# d. compute new scaling parameter
|
|
144
|
-
zeta = 1/np.sqrt(i+1) # ensures that the variation of lambda(i) vanishes
|
|
145
|
-
lambd = np.exp(np.log(lambd) + zeta*(hat_acc[i]-star_acc))
|
|
146
|
-
|
|
147
|
-
# update parameters
|
|
148
|
-
self.scale = min(lambd, 1)
|
|
149
|
-
|
|
150
|
-
# update counters
|
|
151
|
-
i += 1
|
|
152
|
-
idx += Na
|
|
153
|
-
|
|
154
|
-
# display iterations
|
|
155
|
-
self._print_progress(s+2,Ns) #s+2 is the sample number, s+1 is index assuming x0 is the first sample
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
# remove burn-in
|
|
159
|
-
samples = samples[:, Nb:]
|
|
160
|
-
target_eval = target_eval[Nb:]
|
|
161
|
-
accave = acc[Nb:].mean()
|
|
162
|
-
print('\nAverage acceptance rate:', accave, 'MCMC scale:', self.scale, '\n')
|
|
163
|
-
|
|
164
|
-
return samples, target_eval, accave
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
def single_update(self, x_t, target_eval_t):
|
|
45
|
+
def step(self):
|
|
168
46
|
# propose state
|
|
169
47
|
xi = self.proposal.sample(1) # sample from the proposal
|
|
170
|
-
x_star =
|
|
48
|
+
x_star = self.current_point + self.scale*xi.flatten() # MH proposal
|
|
171
49
|
|
|
172
50
|
# evaluate target
|
|
173
51
|
target_eval_star = self.target.logd(x_star)
|
|
174
52
|
|
|
175
53
|
# ratio and acceptance probability
|
|
176
|
-
ratio = target_eval_star -
|
|
54
|
+
ratio = target_eval_star - self.current_target_logd # proposal is symmetric
|
|
177
55
|
alpha = min(0, ratio)
|
|
178
56
|
|
|
179
57
|
# accept/reject
|
|
180
58
|
u_theta = np.log(np.random.rand())
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
59
|
+
acc = 0
|
|
60
|
+
if (u_theta <= alpha) and \
|
|
61
|
+
(not np.isnan(target_eval_star)) and \
|
|
62
|
+
(not np.isinf(target_eval_star)):
|
|
63
|
+
self.current_point = x_star
|
|
64
|
+
self.current_target_logd = target_eval_star
|
|
184
65
|
acc = 1
|
|
185
|
-
else:
|
|
186
|
-
x_next = x_t
|
|
187
|
-
target_eval_next = target_eval_t
|
|
188
|
-
acc = 0
|
|
189
66
|
|
|
190
|
-
return
|
|
67
|
+
return acc
|
|
68
|
+
|
|
69
|
+
def tune(self, skip_len, update_count):
|
|
70
|
+
hat_acc = np.mean(self._acc[-skip_len:])
|
|
71
|
+
|
|
72
|
+
# d. compute new scaling parameter
|
|
73
|
+
zeta = 1/np.sqrt(update_count+1) # ensures that the variation of lambda(i) vanishes
|
|
74
|
+
|
|
75
|
+
# We use self._scale_temp here instead of self.scale in update. This might be a bug,
|
|
76
|
+
# but is equivalent to old MH
|
|
77
|
+
self._scale_temp = np.exp(np.log(self._scale_temp) + zeta*(hat_acc-0.234))
|
|
78
|
+
|
|
79
|
+
# update parameters
|
|
80
|
+
self.scale = min(self._scale_temp, 1)
|