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,9 +1,8 @@
|
|
|
1
1
|
import numpy as np
|
|
2
2
|
import cuqi
|
|
3
|
-
from cuqi.
|
|
4
|
-
from cuqi.array import CUQIarray
|
|
3
|
+
from cuqi.legacy.sampler import Sampler
|
|
5
4
|
|
|
6
|
-
class ULA(Sampler):
|
|
5
|
+
class ULA(Sampler):
|
|
7
6
|
"""Unadjusted Langevin algorithm (ULA) (Roberts and Tweedie, 1996)
|
|
8
7
|
|
|
9
8
|
Samples a distribution given its logpdf and gradient (up to a constant) based on
|
|
@@ -20,13 +19,17 @@ class ULA(Sampler): # Refactor to Proposal-based sampler?
|
|
|
20
19
|
The target distribution to sample. Must have logd and gradient method. Custom logpdfs
|
|
21
20
|
and gradients are supported by using a :class:`cuqi.distribution.UserDefinedDistribution`.
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
x0 : ndarray
|
|
24
23
|
Initial parameters. *Optional*
|
|
25
24
|
|
|
26
25
|
scale : int
|
|
27
26
|
The Langevin diffusion discretization time step (In practice, a scale of 1/dim**2 is
|
|
28
27
|
recommended but not guaranteed to be the optimal choice).
|
|
29
28
|
|
|
29
|
+
dim : int
|
|
30
|
+
Dimension of parameter space. Required if target logpdf and gradient are callable
|
|
31
|
+
functions. *Optional*.
|
|
32
|
+
|
|
30
33
|
callback : callable, *Optional*
|
|
31
34
|
If set this function will be called after every sample.
|
|
32
35
|
The signature of the callback function is `callback(sample, sample_index)`,
|
|
@@ -52,80 +55,61 @@ class ULA(Sampler): # Refactor to Proposal-based sampler?
|
|
|
52
55
|
gradient_func=gradient_func)
|
|
53
56
|
|
|
54
57
|
# Set up sampler
|
|
55
|
-
sampler = cuqi.
|
|
58
|
+
sampler = cuqi.legacy.sampler.ULA(target, scale=1/dim**2)
|
|
56
59
|
|
|
57
60
|
# Sample
|
|
58
|
-
sampler.sample(2000)
|
|
61
|
+
samples = sampler.sample(2000)
|
|
59
62
|
|
|
60
|
-
A Deblur example can be found in demos/
|
|
61
|
-
# TODO: update demo once sampler merged
|
|
63
|
+
A Deblur example can be found in demos/demo27_ula.py
|
|
62
64
|
"""
|
|
65
|
+
def __init__(self, target, scale, x0=None, dim=None, rng=None, **kwargs):
|
|
66
|
+
super().__init__(target, x0=x0, dim=dim, **kwargs)
|
|
67
|
+
self.scale = scale
|
|
68
|
+
self.rng = rng
|
|
69
|
+
|
|
70
|
+
def _sample_adapt(self, N, Nb):
|
|
71
|
+
return self._sample(N, Nb)
|
|
72
|
+
|
|
73
|
+
def _sample(self, N, Nb):
|
|
74
|
+
# allocation
|
|
75
|
+
Ns = Nb+N
|
|
76
|
+
samples = np.empty((self.dim, Ns))
|
|
77
|
+
target_eval = np.empty(Ns)
|
|
78
|
+
g_target_eval = np.empty((self.dim, Ns))
|
|
79
|
+
acc = np.zeros(Ns)
|
|
80
|
+
|
|
81
|
+
# initial state
|
|
82
|
+
samples[:, 0] = self.x0
|
|
83
|
+
target_eval[0], g_target_eval[:,0] = self.target.logd(self.x0), self.target.gradient(self.x0)
|
|
84
|
+
acc[0] = 1
|
|
85
|
+
|
|
86
|
+
# ULA
|
|
87
|
+
for s in range(Ns-1):
|
|
88
|
+
samples[:, s+1], target_eval[s+1], g_target_eval[:,s+1], acc[s+1] = \
|
|
89
|
+
self.single_update(samples[:, s], target_eval[s], g_target_eval[:,s])
|
|
90
|
+
self._print_progress(s+2,Ns) #s+2 is the sample number, s+1 is index assuming x0 is the first sample
|
|
91
|
+
self._call_callback(samples[:, s+1], s+1)
|
|
92
|
+
|
|
93
|
+
# apply burn-in
|
|
94
|
+
samples = samples[:, Nb:]
|
|
95
|
+
target_eval = target_eval[Nb:]
|
|
96
|
+
acc = acc[Nb:]
|
|
97
|
+
return samples, target_eval, np.mean(acc)
|
|
63
98
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
self.initial_scale = scale
|
|
71
|
-
|
|
72
|
-
def _initialize(self):
|
|
73
|
-
self.scale = self.initial_scale
|
|
74
|
-
self.current_target_logd = self.target.logd(self.current_point)
|
|
75
|
-
self.current_target_grad = self.target.gradient(self.current_point)
|
|
76
|
-
|
|
77
|
-
def validate_target(self):
|
|
78
|
-
try:
|
|
79
|
-
self.target.gradient(np.ones(self.dim))
|
|
80
|
-
pass
|
|
81
|
-
except (NotImplementedError, AttributeError):
|
|
82
|
-
raise ValueError("The target needs to have a gradient method")
|
|
83
|
-
|
|
84
|
-
def _accept_or_reject(self, x_star, target_eval_star, target_grad_star):
|
|
85
|
-
"""
|
|
86
|
-
Accepts the proposed state and updates the sampler's state accordingly, i.e.,
|
|
87
|
-
current_point, current_target_eval, and current_target_grad_eval.
|
|
88
|
-
|
|
89
|
-
Parameters
|
|
90
|
-
----------
|
|
91
|
-
x_star :
|
|
92
|
-
The proposed state
|
|
93
|
-
|
|
94
|
-
target_eval_star:
|
|
95
|
-
The log likelihood evaluated at x_star
|
|
96
|
-
|
|
97
|
-
target_grad_star:
|
|
98
|
-
The gradient of log likelihood evaluated at x_star
|
|
99
|
-
|
|
100
|
-
Returns
|
|
101
|
-
-------
|
|
102
|
-
scalar
|
|
103
|
-
1 (accepted)
|
|
104
|
-
"""
|
|
105
|
-
self.current_point = x_star
|
|
106
|
-
self.current_target_logd = target_eval_star
|
|
107
|
-
self.current_target_grad = target_grad_star
|
|
108
|
-
acc = 1
|
|
109
|
-
return acc
|
|
110
|
-
|
|
111
|
-
def step(self):
|
|
112
|
-
# propose state
|
|
113
|
-
xi = cuqi.distribution.Normal(mean=np.zeros(self.dim), std=np.sqrt(self.scale)).sample()
|
|
114
|
-
x_star = self.current_point + 0.5*self.scale*self.current_target_grad + xi
|
|
115
|
-
|
|
116
|
-
# evaluate target
|
|
117
|
-
target_eval_star, target_grad_star = self.target.logd(x_star), self.target.gradient(x_star)
|
|
118
|
-
|
|
119
|
-
# accept or reject proposal
|
|
120
|
-
acc = self._accept_or_reject(x_star, target_eval_star, target_grad_star)
|
|
99
|
+
def single_update(self, x_t, target_eval_t, g_target_eval_t):
|
|
100
|
+
# approximate Langevin diffusion
|
|
101
|
+
xi = cuqi.distribution.Normal(mean=np.zeros(self.dim), std=np.sqrt(self.scale)).sample(rng=self.rng)
|
|
102
|
+
x_star = x_t + 0.5*self.scale*g_target_eval_t + xi
|
|
103
|
+
logpi_eval_star, g_logpi_star = self.target.logd(x_star), self.target.gradient(x_star)
|
|
121
104
|
|
|
122
|
-
|
|
105
|
+
# msg
|
|
106
|
+
if np.isnan(logpi_eval_star):
|
|
107
|
+
raise NameError('NaN potential func. Consider using smaller scale parameter')
|
|
123
108
|
|
|
124
|
-
|
|
125
|
-
pass
|
|
109
|
+
return x_star, logpi_eval_star, g_logpi_star, 1 # sample always accepted without Metropolis correction
|
|
126
110
|
|
|
127
111
|
|
|
128
|
-
class MALA(ULA):
|
|
112
|
+
class MALA(ULA):
|
|
129
113
|
""" Metropolis-adjusted Langevin algorithm (MALA) (Roberts and Tweedie, 1996)
|
|
130
114
|
|
|
131
115
|
Samples a distribution given its logd and gradient (up to a constant) based on
|
|
@@ -143,12 +127,16 @@ class MALA(ULA): # Refactor to Proposal-based sampler?
|
|
|
143
127
|
The target distribution to sample. Must have logpdf and gradient method. Custom logpdfs
|
|
144
128
|
and gradients are supported by using a :class:`cuqi.distribution.UserDefinedDistribution`.
|
|
145
129
|
|
|
146
|
-
|
|
130
|
+
x0 : ndarray
|
|
147
131
|
Initial parameters. *Optional*
|
|
148
132
|
|
|
149
133
|
scale : int
|
|
150
134
|
The Langevin diffusion discretization time step.
|
|
151
135
|
|
|
136
|
+
dim : int
|
|
137
|
+
Dimension of parameter space. Required if target logpdf and gradient are callable
|
|
138
|
+
functions. *Optional*.
|
|
139
|
+
|
|
152
140
|
callback : callable, *Optional*
|
|
153
141
|
If set this function will be called after every sample.
|
|
154
142
|
The signature of the callback function is `callback(sample, sample_index)`,
|
|
@@ -174,54 +162,37 @@ class MALA(ULA): # Refactor to Proposal-based sampler?
|
|
|
174
162
|
gradient_func=gradient_func)
|
|
175
163
|
|
|
176
164
|
# Set up sampler
|
|
177
|
-
sampler = cuqi.
|
|
165
|
+
sampler = cuqi.legacy.sampler.MALA(target, scale=1/5**2)
|
|
178
166
|
|
|
179
167
|
# Sample
|
|
180
|
-
sampler.sample(2000)
|
|
168
|
+
samples = sampler.sample(2000)
|
|
181
169
|
|
|
182
|
-
A Deblur example can be found in demos/
|
|
183
|
-
# TODO: update demo once sampler merged
|
|
170
|
+
A Deblur example can be found in demos/demo28_mala.py
|
|
184
171
|
"""
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
x_star
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
target_grad_star:
|
|
199
|
-
The gradient of log likelihood evaluated at x_star
|
|
200
|
-
|
|
201
|
-
Returns
|
|
202
|
-
-------
|
|
203
|
-
scaler
|
|
204
|
-
1 if accepted, 0 otherwise
|
|
205
|
-
"""
|
|
206
|
-
log_target_ratio = target_eval_star - self.current_target_logd
|
|
207
|
-
log_prop_ratio = self._log_proposal(self.current_point, x_star, target_grad_star) \
|
|
208
|
-
- self._log_proposal(x_star, self.current_point, self.current_target_grad)
|
|
172
|
+
def __init__(self, target, scale, x0=None, dim=None, rng=None, **kwargs):
|
|
173
|
+
super().__init__(target, scale, x0=x0, dim=dim, rng=rng, **kwargs)
|
|
174
|
+
|
|
175
|
+
def single_update(self, x_t, target_eval_t, g_target_eval_t):
|
|
176
|
+
# approximate Langevin diffusion
|
|
177
|
+
xi = cuqi.distribution.Normal(mean=np.zeros(self.dim), std=np.sqrt(self.scale)).sample(rng=self.rng)
|
|
178
|
+
x_star = x_t + (self.scale/2)*g_target_eval_t + xi
|
|
179
|
+
logpi_eval_star, g_logpi_star = self.target.logd(x_star), self.target.gradient(x_star)
|
|
180
|
+
|
|
181
|
+
# Metropolis step
|
|
182
|
+
log_target_ratio = logpi_eval_star - target_eval_t
|
|
183
|
+
log_prop_ratio = self.log_proposal(x_t, x_star, g_logpi_star) \
|
|
184
|
+
- self.log_proposal(x_star, x_t, g_target_eval_t)
|
|
209
185
|
log_alpha = min(0, log_target_ratio + log_prop_ratio)
|
|
210
186
|
|
|
211
|
-
# accept/reject
|
|
212
|
-
|
|
213
|
-
log_u
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
self.current_target_grad = target_grad_star
|
|
218
|
-
acc = 1
|
|
219
|
-
return acc
|
|
187
|
+
# accept/reject
|
|
188
|
+
log_u = np.log(cuqi.distribution.Uniform(low=0, high=1).sample(rng=self.rng))
|
|
189
|
+
if (log_u <= log_alpha) and (np.isnan(logpi_eval_star) == False):
|
|
190
|
+
return x_star, logpi_eval_star, g_logpi_star, 1
|
|
191
|
+
else:
|
|
192
|
+
return x_t.copy(), target_eval_t, g_target_eval_t.copy(), 0
|
|
220
193
|
|
|
221
|
-
def
|
|
222
|
-
pass
|
|
223
|
-
|
|
224
|
-
def _log_proposal(self, theta_star, theta_k, g_logpi_k):
|
|
194
|
+
def log_proposal(self, theta_star, theta_k, g_logpi_k):
|
|
225
195
|
mu = theta_k + ((self.scale)/2)*g_logpi_k
|
|
226
196
|
misfit = theta_star - mu
|
|
227
197
|
return -0.5*((1/(self.scale))*(misfit.T @ misfit))
|
|
198
|
+
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import scipy as sp
|
|
2
|
+
import numpy as np
|
|
3
|
+
import cuqi
|
|
4
|
+
from cuqi.distribution import Normal
|
|
5
|
+
from cuqi.solver import CGLS
|
|
6
|
+
from cuqi.legacy.sampler import Sampler
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class UGLA(Sampler):
|
|
10
|
+
""" Unadjusted (Gaussian) Laplace Approximation sampler
|
|
11
|
+
|
|
12
|
+
Samples an approximate posterior where the prior is approximated
|
|
13
|
+
by a Gaussian distribution. The likelihood must be Gaussian.
|
|
14
|
+
|
|
15
|
+
Currently only works for LMRF priors.
|
|
16
|
+
|
|
17
|
+
The inner solver is Conjugate Gradient Least Squares (CGLS) solver.
|
|
18
|
+
|
|
19
|
+
For more details see: Uribe, Felipe, et al. "A hybrid Gibbs sampler for edge-preserving
|
|
20
|
+
tomographic reconstruction with uncertain view angles." arXiv preprint arXiv:2104.06919 (2021).
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
target : `cuqi.distribution.Posterior`
|
|
25
|
+
The target posterior distribution to sample.
|
|
26
|
+
|
|
27
|
+
x0 : ndarray
|
|
28
|
+
Initial parameters. *Optional*
|
|
29
|
+
|
|
30
|
+
maxit : int
|
|
31
|
+
Maximum number of inner iterations for solver when generating one sample.
|
|
32
|
+
|
|
33
|
+
tol : float
|
|
34
|
+
Tolerance for inner solver. Will stop before maxit if the inner solvers convergence check reaches tol.
|
|
35
|
+
|
|
36
|
+
beta : float
|
|
37
|
+
Smoothing parameter for the Gaussian approximation of the Laplace distribution. Larger beta is easier to sample but is a worse approximation.
|
|
38
|
+
|
|
39
|
+
rng : np.random.RandomState
|
|
40
|
+
Random number generator used for sampling. *Optional*
|
|
41
|
+
|
|
42
|
+
callback : callable, *Optional*
|
|
43
|
+
If set this function will be called after every sample.
|
|
44
|
+
The signature of the callback function is `callback(sample, sample_index)`,
|
|
45
|
+
where `sample` is the current sample and `sample_index` is the index of the sample.
|
|
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
|
+
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, target, x0=None, maxit=50, tol=1e-4, beta=1e-5, rng=None, **kwargs):
|
|
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 Affine model
|
|
64
|
+
if not isinstance(self.target.likelihood.model, cuqi.model.AffineModel):
|
|
65
|
+
raise TypeError("Model needs to be affine or linear")
|
|
66
|
+
|
|
67
|
+
# Check Gaussian likelihood
|
|
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
|
|
82
|
+
self.maxit = maxit
|
|
83
|
+
self.tol = tol
|
|
84
|
+
self.beta = beta
|
|
85
|
+
self.rng = rng
|
|
86
|
+
|
|
87
|
+
def _sample_adapt(self, Ns, Nb):
|
|
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.
|
|
100
|
+
|
|
101
|
+
Returns
|
|
102
|
+
-------
|
|
103
|
+
samples : ndarray
|
|
104
|
+
Samples from the approximate posterior.
|
|
105
|
+
|
|
106
|
+
target_eval : ndarray
|
|
107
|
+
Log-likelihood of each sample.
|
|
108
|
+
|
|
109
|
+
acc : ndarray
|
|
110
|
+
Acceptance rate of each sample.
|
|
111
|
+
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
# Extract diff_op from target prior
|
|
115
|
+
D = self.target.prior._diff_op
|
|
116
|
+
n = D.shape[0]
|
|
117
|
+
|
|
118
|
+
# Gaussian approximation of LMRF prior as function of x_k
|
|
119
|
+
def Lk_fun(x_k):
|
|
120
|
+
dd = 1/np.sqrt((D @ x_k)**2 + self.beta*np.ones(n))
|
|
121
|
+
W = sp.sparse.diags(dd)
|
|
122
|
+
return W.sqrt() @ D
|
|
123
|
+
|
|
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 - self.target.model._shift
|
|
130
|
+
self._m = len(self._data)
|
|
131
|
+
self._L1 = self.target.likelihood.distribution.sqrtprec
|
|
132
|
+
|
|
133
|
+
# If prior location is scalar, repeat it to match dimensions
|
|
134
|
+
if len(self.target.prior.location) == 1:
|
|
135
|
+
self._priorloc = np.repeat(self.target.prior.location, self.dim)
|
|
136
|
+
else:
|
|
137
|
+
self._priorloc = self.target.prior.location
|
|
138
|
+
|
|
139
|
+
# Initial Laplace approx
|
|
140
|
+
self._L2 = Lk_fun(self.x0)
|
|
141
|
+
self._L2mu = self._L2@self._priorloc
|
|
142
|
+
self._b_tild = np.hstack([self._L1@self._data, self._L2mu])
|
|
143
|
+
|
|
144
|
+
#self.n = len(self.x0)
|
|
145
|
+
|
|
146
|
+
# Least squares form
|
|
147
|
+
def M(x, flag):
|
|
148
|
+
if flag == 1:
|
|
149
|
+
out1 = self._L1 @ self._model._forward_func_no_shift(x) # Use forward function which excludes shift
|
|
150
|
+
out2 = np.sqrt(1/self.target.prior.scale)*(self._L2 @ x)
|
|
151
|
+
out = np.hstack([out1, out2])
|
|
152
|
+
elif flag == 2:
|
|
153
|
+
idx = int(self._m)
|
|
154
|
+
out1 = self._model._adjoint_func_no_shift(self._L1.T@x[:idx])
|
|
155
|
+
out2 = np.sqrt(1/self.target.prior.scale)*(self._L2.T @ x[idx:])
|
|
156
|
+
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()
|
|
177
|
+
|
|
178
|
+
self._print_progress(s+2,N) #s+2 is the sample number, s+1 is index assuming x0 is the first sample
|
|
179
|
+
self._call_callback(samples[:, s+1], s+1)
|
|
180
|
+
|
|
181
|
+
# remove burn-in
|
|
182
|
+
samples = samples[:, Nb:]
|
|
183
|
+
|
|
184
|
+
return samples, None, None
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import cuqi
|
|
3
|
+
from cuqi.legacy.sampler import ProposalBasedSampler
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MH(ProposalBasedSampler):
|
|
7
|
+
"""Metropolis Hastings sampler.
|
|
8
|
+
|
|
9
|
+
Allows sampling of a target distribution by random-walk sampling of a proposal distribution along with an accept/reject step.
|
|
10
|
+
|
|
11
|
+
Parameters
|
|
12
|
+
----------
|
|
13
|
+
|
|
14
|
+
target : `cuqi.distribution.Distribution` or lambda function
|
|
15
|
+
The target distribution to sample. Custom logpdfs are supported by using a :class:`cuqi.distribution.UserDefinedDistribution`.
|
|
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*.
|
|
19
|
+
|
|
20
|
+
scale : float
|
|
21
|
+
Scale parameter used to define correlation between previous and proposed sample in random-walk. *Optional*.
|
|
22
|
+
|
|
23
|
+
x0 : ndarray
|
|
24
|
+
Initial parameters. *Optional*
|
|
25
|
+
|
|
26
|
+
dim : int
|
|
27
|
+
Dimension of parameter space. Required if target and proposal are callable functions. *Optional*.
|
|
28
|
+
|
|
29
|
+
callback : callable, *Optional*
|
|
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
|
|
38
|
+
|
|
39
|
+
# Parameters
|
|
40
|
+
dim = 5 # Dimension of distribution
|
|
41
|
+
mu = np.arange(dim) # Mean of Gaussian
|
|
42
|
+
std = 1 # standard deviation of Gaussian
|
|
43
|
+
|
|
44
|
+
# Logpdf function
|
|
45
|
+
logpdf_func = lambda x: -1/(std**2)*np.sum((x-mu)**2)
|
|
46
|
+
|
|
47
|
+
# Define distribution from logpdf as UserDefinedDistribution (sample and gradients also supported)
|
|
48
|
+
target = cuqi.distribution.UserDefinedDistribution(dim=dim, logpdf_func=logpdf_func)
|
|
49
|
+
|
|
50
|
+
# Set up sampler
|
|
51
|
+
sampler = cuqi.legacy.sampler.MH(target, scale=1)
|
|
52
|
+
|
|
53
|
+
# Sample
|
|
54
|
+
samples = sampler.sample(2000)
|
|
55
|
+
|
|
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):
|
|
168
|
+
# propose state
|
|
169
|
+
xi = self.proposal.sample(1) # sample from the proposal
|
|
170
|
+
x_star = x_t + self.scale*xi.flatten() # MH proposal
|
|
171
|
+
|
|
172
|
+
# evaluate target
|
|
173
|
+
target_eval_star = self.target.logd(x_star)
|
|
174
|
+
|
|
175
|
+
# ratio and acceptance probability
|
|
176
|
+
ratio = target_eval_star - target_eval_t # proposal is symmetric
|
|
177
|
+
alpha = min(0, ratio)
|
|
178
|
+
|
|
179
|
+
# accept/reject
|
|
180
|
+
u_theta = np.log(np.random.rand())
|
|
181
|
+
if (u_theta <= alpha):
|
|
182
|
+
x_next = x_star
|
|
183
|
+
target_eval_next = target_eval_star
|
|
184
|
+
acc = 1
|
|
185
|
+
else:
|
|
186
|
+
x_next = x_t
|
|
187
|
+
target_eval_next = target_eval_t
|
|
188
|
+
acc = 0
|
|
189
|
+
|
|
190
|
+
return x_next, target_eval_next, acc
|