CUQIpy 1.4.0.post0.dev13__py3-none-any.whl → 1.4.0.post0.dev45__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 +1 -0
- cuqi/_version.py +3 -3
- cuqi/experimental/__init__.py +1 -2
- cuqi/experimental/_recommender.py +4 -4
- cuqi/legacy/__init__.py +2 -0
- cuqi/legacy/sampler/__init__.py +11 -0
- 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/legacy/sampler/_langevin_algorithm.py +198 -0
- cuqi/legacy/sampler/_laplace_approximation.py +184 -0
- cuqi/legacy/sampler/_mh.py +190 -0
- cuqi/legacy/sampler/_pcn.py +244 -0
- cuqi/legacy/sampler/_rto.py +284 -0
- cuqi/legacy/sampler/_sampler.py +182 -0
- cuqi/problem/_problem.py +87 -80
- 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 +269 -130
- cuqi/sampler/_hmc.py +328 -201
- cuqi/sampler/_langevin_algorithm.py +282 -98
- cuqi/sampler/_laplace_approximation.py +87 -117
- cuqi/sampler/_mh.py +47 -157
- cuqi/sampler/_pcn.py +56 -211
- cuqi/sampler/_rto.py +206 -140
- cuqi/sampler/_sampler.py +540 -135
- {cuqipy-1.4.0.post0.dev13.dist-info → cuqipy-1.4.0.post0.dev45.dist-info}/METADATA +1 -1
- {cuqipy-1.4.0.post0.dev13.dist-info → cuqipy-1.4.0.post0.dev45.dist-info}/RECORD +36 -35
- cuqi/experimental/mcmc/__init__.py +0 -122
- cuqi/experimental/mcmc/_conjugate.py +0 -396
- cuqi/experimental/mcmc/_conjugate_approx.py +0 -76
- cuqi/experimental/mcmc/_cwmh.py +0 -190
- cuqi/experimental/mcmc/_gibbs.py +0 -366
- cuqi/experimental/mcmc/_hmc.py +0 -462
- cuqi/experimental/mcmc/_langevin_algorithm.py +0 -382
- cuqi/experimental/mcmc/_laplace_approximation.py +0 -154
- cuqi/experimental/mcmc/_mh.py +0 -80
- cuqi/experimental/mcmc/_pcn.py +0 -89
- cuqi/experimental/mcmc/_rto.py +0 -350
- cuqi/experimental/mcmc/_sampler.py +0 -582
- {cuqipy-1.4.0.post0.dev13.dist-info → cuqipy-1.4.0.post0.dev45.dist-info}/WHEEL +0 -0
- {cuqipy-1.4.0.post0.dev13.dist-info → cuqipy-1.4.0.post0.dev45.dist-info}/licenses/LICENSE +0 -0
- {cuqipy-1.4.0.post0.dev13.dist-info → cuqipy-1.4.0.post0.dev45.dist-info}/top_level.txt +0 -0
|
@@ -1,382 +0,0 @@
|
|
|
1
|
-
import numpy as np
|
|
2
|
-
import cuqi
|
|
3
|
-
from cuqi.experimental.mcmc import Sampler
|
|
4
|
-
from cuqi.implicitprior import RestorationPrior, MoreauYoshidaPrior
|
|
5
|
-
from cuqi.array import CUQIarray
|
|
6
|
-
from copy import copy
|
|
7
|
-
|
|
8
|
-
class ULA(Sampler): # Refactor to Proposal-based sampler?
|
|
9
|
-
"""Unadjusted Langevin algorithm (ULA) (Roberts and Tweedie, 1996)
|
|
10
|
-
|
|
11
|
-
It approximately samples a distribution given its logpdf gradient based on
|
|
12
|
-
the Langevin diffusion dL_t = dW_t + 1/2*Nabla target.logd(L_t)dt, where
|
|
13
|
-
W_t is the `dim`-dimensional standard Brownian motion.
|
|
14
|
-
ULA results from the Euler-Maruyama discretization of this Langevin stochastic
|
|
15
|
-
differential equation (SDE).
|
|
16
|
-
|
|
17
|
-
For more details see: Roberts, G. O., & Tweedie, R. L. (1996). Exponential convergence
|
|
18
|
-
of Langevin distributions and their discrete approximations. Bernoulli, 341-363.
|
|
19
|
-
|
|
20
|
-
Parameters
|
|
21
|
-
----------
|
|
22
|
-
|
|
23
|
-
target : `cuqi.distribution.Distribution`
|
|
24
|
-
The target distribution to sample. Must have logd and gradient method. Custom logpdfs
|
|
25
|
-
and gradients are supported by using a :class:`cuqi.distribution.UserDefinedDistribution`.
|
|
26
|
-
|
|
27
|
-
initial_point : ndarray
|
|
28
|
-
Initial parameters. *Optional*
|
|
29
|
-
|
|
30
|
-
scale : float
|
|
31
|
-
The Langevin diffusion discretization time step (In practice, scale must
|
|
32
|
-
be smaller than 1/L, where L is the Lipschitz of the gradient of the log
|
|
33
|
-
target density, logd).
|
|
34
|
-
|
|
35
|
-
callback : callable, optional
|
|
36
|
-
A function that will be called after each sampling step. It can be useful for monitoring the sampler during sampling.
|
|
37
|
-
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)`.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
Example
|
|
41
|
-
-------
|
|
42
|
-
.. code-block:: python
|
|
43
|
-
|
|
44
|
-
# Parameters
|
|
45
|
-
dim = 5 # Dimension of distribution
|
|
46
|
-
mu = np.arange(dim) # Mean of Gaussian
|
|
47
|
-
std = 1 # standard deviation of Gaussian
|
|
48
|
-
|
|
49
|
-
# Logpdf function
|
|
50
|
-
logpdf_func = lambda x: -1/(std**2)*np.sum((x-mu)**2)
|
|
51
|
-
gradient_func = lambda x: -2/(std**2)*(x - mu)
|
|
52
|
-
|
|
53
|
-
# Define distribution from logpdf and gradient as UserDefinedDistribution
|
|
54
|
-
target = cuqi.distribution.UserDefinedDistribution(dim=dim, logpdf_func=logpdf_func,
|
|
55
|
-
gradient_func=gradient_func)
|
|
56
|
-
|
|
57
|
-
# Set up sampler
|
|
58
|
-
sampler = cuqi.experimental.mcmc.ULA(target, scale=1/dim**2)
|
|
59
|
-
|
|
60
|
-
# Sample
|
|
61
|
-
sampler.sample(2000)
|
|
62
|
-
|
|
63
|
-
A Deblur example can be found in demos/demo27_ULA.py
|
|
64
|
-
# TODO: update demo once sampler merged
|
|
65
|
-
"""
|
|
66
|
-
|
|
67
|
-
_STATE_KEYS = Sampler._STATE_KEYS.union({'scale', 'current_target_grad'})
|
|
68
|
-
|
|
69
|
-
def __init__(self, target=None, scale=1.0, **kwargs):
|
|
70
|
-
|
|
71
|
-
super().__init__(target, **kwargs)
|
|
72
|
-
self.initial_scale = scale
|
|
73
|
-
|
|
74
|
-
def _initialize(self):
|
|
75
|
-
self.scale = self.initial_scale
|
|
76
|
-
self.current_target_grad = self._eval_target_grad(self.current_point)
|
|
77
|
-
|
|
78
|
-
def validate_target(self):
|
|
79
|
-
try:
|
|
80
|
-
self._eval_target_grad(np.ones(self.dim))
|
|
81
|
-
pass
|
|
82
|
-
except (NotImplementedError, AttributeError):
|
|
83
|
-
raise ValueError("The target needs to have a gradient method")
|
|
84
|
-
|
|
85
|
-
def _eval_target_logd(self, x):
|
|
86
|
-
return None
|
|
87
|
-
|
|
88
|
-
def _eval_target_grad(self, x):
|
|
89
|
-
return self.target.gradient(x)
|
|
90
|
-
|
|
91
|
-
def _accept_or_reject(self, x_star, target_eval_star, target_grad_star):
|
|
92
|
-
"""
|
|
93
|
-
Accepts the proposed state and updates the sampler's state accordingly, i.e.,
|
|
94
|
-
current_point, current_target_eval, and current_target_grad_eval.
|
|
95
|
-
|
|
96
|
-
Parameters
|
|
97
|
-
----------
|
|
98
|
-
x_star :
|
|
99
|
-
The proposed state
|
|
100
|
-
|
|
101
|
-
target_eval_star:
|
|
102
|
-
The log likelihood evaluated at x_star
|
|
103
|
-
|
|
104
|
-
target_grad_star:
|
|
105
|
-
The gradient of log likelihood evaluated at x_star
|
|
106
|
-
|
|
107
|
-
Returns
|
|
108
|
-
-------
|
|
109
|
-
scalar
|
|
110
|
-
1 (accepted)
|
|
111
|
-
"""
|
|
112
|
-
|
|
113
|
-
self.current_point = x_star
|
|
114
|
-
self.current_target_grad = target_grad_star
|
|
115
|
-
acc = 1
|
|
116
|
-
|
|
117
|
-
return acc
|
|
118
|
-
|
|
119
|
-
def step(self):
|
|
120
|
-
# propose state
|
|
121
|
-
xi = cuqi.distribution.Normal(mean=np.zeros(self.dim), std=np.sqrt(self.scale)).sample()
|
|
122
|
-
x_star = self.current_point + 0.5*self.scale*self.current_target_grad + xi
|
|
123
|
-
|
|
124
|
-
# evaluate target
|
|
125
|
-
target_eval_star = self._eval_target_logd(x_star)
|
|
126
|
-
target_grad_star = self._eval_target_grad(x_star)
|
|
127
|
-
|
|
128
|
-
# accept or reject proposal
|
|
129
|
-
acc = self._accept_or_reject(x_star, target_eval_star, target_grad_star)
|
|
130
|
-
|
|
131
|
-
return acc
|
|
132
|
-
|
|
133
|
-
def tune(self, skip_len, update_count):
|
|
134
|
-
pass
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
class MALA(ULA): # Refactor to Proposal-based sampler?
|
|
138
|
-
""" Metropolis-adjusted Langevin algorithm (MALA) (Roberts and Tweedie, 1996)
|
|
139
|
-
|
|
140
|
-
Samples a distribution given its logd and gradient (up to a constant) based on
|
|
141
|
-
Langevin diffusion dL_t = dW_t + 1/2*Nabla target.logd(L_t)dt,
|
|
142
|
-
W_t is the `dim`-dimensional standard Brownian motion.
|
|
143
|
-
A sample is firstly proposed by ULA and is then accepted or rejected according
|
|
144
|
-
to a Metropolis–Hastings step.
|
|
145
|
-
This accept-reject step allows us to remove the asymptotic bias of ULA.
|
|
146
|
-
|
|
147
|
-
For more details see: Roberts, G. O., & Tweedie, R. L. (1996). Exponential convergence
|
|
148
|
-
of Langevin distributions and their discrete approximations. Bernoulli, 341-363.
|
|
149
|
-
|
|
150
|
-
Parameters
|
|
151
|
-
----------
|
|
152
|
-
|
|
153
|
-
target : `cuqi.distribution.Distribution`
|
|
154
|
-
The target distribution to sample. Must have logpdf and gradient method. Custom logpdfs
|
|
155
|
-
and gradients are supported by using a :class:`cuqi.distribution.UserDefinedDistribution`.
|
|
156
|
-
|
|
157
|
-
initial_point : ndarray
|
|
158
|
-
Initial parameters. *Optional*
|
|
159
|
-
|
|
160
|
-
scale : float
|
|
161
|
-
The Langevin diffusion discretization time step (In practice, scale must
|
|
162
|
-
be smaller than 1/L, where L is the Lipschitz of the gradient of the log
|
|
163
|
-
target density, logd).
|
|
164
|
-
|
|
165
|
-
callback : callable, optional
|
|
166
|
-
A function that will be called after each sampling step. It can be useful for monitoring the sampler during sampling.
|
|
167
|
-
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)`.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
Example
|
|
171
|
-
-------
|
|
172
|
-
.. code-block:: python
|
|
173
|
-
|
|
174
|
-
# Parameters
|
|
175
|
-
dim = 5 # Dimension of distribution
|
|
176
|
-
mu = np.arange(dim) # Mean of Gaussian
|
|
177
|
-
std = 1 # standard deviation of Gaussian
|
|
178
|
-
|
|
179
|
-
# Logpdf function
|
|
180
|
-
logpdf_func = lambda x: -1/(std**2)*np.sum((x-mu)**2)
|
|
181
|
-
gradient_func = lambda x: -2/(std**2)*(x-mu)
|
|
182
|
-
|
|
183
|
-
# Define distribution from logpdf as UserDefinedDistribution (sample and gradients also supported)
|
|
184
|
-
target = cuqi.distribution.UserDefinedDistribution(dim=dim, logpdf_func=logpdf_func,
|
|
185
|
-
gradient_func=gradient_func)
|
|
186
|
-
|
|
187
|
-
# Set up sampler
|
|
188
|
-
sampler = cuqi.experimental.mcmc.MALA(target, scale=1/5**2)
|
|
189
|
-
|
|
190
|
-
# Sample
|
|
191
|
-
sampler.sample(2000)
|
|
192
|
-
|
|
193
|
-
A Deblur example can be found in demos/demo28_MALA.py
|
|
194
|
-
# TODO: update demo once sampler merged
|
|
195
|
-
"""
|
|
196
|
-
|
|
197
|
-
_STATE_KEYS = ULA._STATE_KEYS.union({'current_target_logd'})
|
|
198
|
-
|
|
199
|
-
def _initialize(self):
|
|
200
|
-
super()._initialize()
|
|
201
|
-
self.current_target_logd = self.target.logd(self.current_point)
|
|
202
|
-
|
|
203
|
-
def _eval_target_logd(self, x):
|
|
204
|
-
return self.target.logd(x)
|
|
205
|
-
|
|
206
|
-
def _accept_or_reject(self, x_star, target_eval_star, target_grad_star):
|
|
207
|
-
"""
|
|
208
|
-
Accepts the proposed state according to a Metropolis step and updates
|
|
209
|
-
the sampler's state accordingly, i.e., current_point, current_target_eval,
|
|
210
|
-
and current_target_grad_eval.
|
|
211
|
-
|
|
212
|
-
Parameters
|
|
213
|
-
----------
|
|
214
|
-
x_star :
|
|
215
|
-
The proposed state
|
|
216
|
-
|
|
217
|
-
target_eval_star:
|
|
218
|
-
The log likelihood evaluated at x_star
|
|
219
|
-
|
|
220
|
-
target_grad_star:
|
|
221
|
-
The gradient of log likelihood evaluated at x_star
|
|
222
|
-
|
|
223
|
-
Returns
|
|
224
|
-
-------
|
|
225
|
-
scaler
|
|
226
|
-
1 if accepted, 0 otherwise
|
|
227
|
-
"""
|
|
228
|
-
log_target_ratio = target_eval_star - self.current_target_logd
|
|
229
|
-
log_prop_ratio = self._log_proposal(self.current_point, x_star, target_grad_star) \
|
|
230
|
-
- self._log_proposal(x_star, self.current_point, self.current_target_grad)
|
|
231
|
-
log_alpha = min(0, log_target_ratio + log_prop_ratio)
|
|
232
|
-
|
|
233
|
-
# accept/reject with Metropolis
|
|
234
|
-
acc = 0
|
|
235
|
-
log_u = np.log(np.random.rand())
|
|
236
|
-
if (log_u <= log_alpha) and \
|
|
237
|
-
(not np.isnan(target_eval_star)) and \
|
|
238
|
-
(not np.isinf(target_eval_star)):
|
|
239
|
-
self.current_point = x_star
|
|
240
|
-
self.current_target_logd = target_eval_star
|
|
241
|
-
self.current_target_grad = target_grad_star
|
|
242
|
-
acc = 1
|
|
243
|
-
return acc
|
|
244
|
-
|
|
245
|
-
def tune(self, skip_len, update_count):
|
|
246
|
-
pass
|
|
247
|
-
|
|
248
|
-
def _log_proposal(self, theta_star, theta_k, g_logpi_k):
|
|
249
|
-
mu = theta_k + ((self.scale)/2)*g_logpi_k
|
|
250
|
-
misfit = theta_star - mu
|
|
251
|
-
return -0.5*((1/(self.scale))*(misfit.T @ misfit))
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
class MYULA(ULA):
|
|
255
|
-
"""Moreau-Yoshida Unadjusted Langevin algorithm (MYUULA) (Durmus et al., 2018)
|
|
256
|
-
|
|
257
|
-
Samples a smoothed target distribution given its smoothed logpdf gradient.
|
|
258
|
-
It is based on the Langevin diffusion dL_t = dW_t + 1/2*Nabla target.logd(L_t)dt,
|
|
259
|
-
where W_t is a `dim`-dimensional standard Brownian motion.
|
|
260
|
-
It targets a differentiable density (partially) smoothed by the Moreau-Yoshida
|
|
261
|
-
envelope. The smoothed target density can be made arbitrarily closed to the
|
|
262
|
-
true unsmoothed target density.
|
|
263
|
-
|
|
264
|
-
For more details see: Durmus, Alain, Eric Moulines, and Marcelo Pereyra.
|
|
265
|
-
"Efficient Bayesian
|
|
266
|
-
computation by proximal Markov chain Monte Carlo: when Langevin meets Moreau."
|
|
267
|
-
SIAM Journal on Imaging Sciences 11.1 (2018): 473-506.
|
|
268
|
-
|
|
269
|
-
Parameters
|
|
270
|
-
----------
|
|
271
|
-
|
|
272
|
-
target : `cuqi.distribution.Distribution`
|
|
273
|
-
The target distribution to sample from. The target distribution results from
|
|
274
|
-
a differentiable likelihood and prior of type RestorationPrior.
|
|
275
|
-
|
|
276
|
-
initial_point : ndarray
|
|
277
|
-
Initial parameters. *Optional*
|
|
278
|
-
|
|
279
|
-
scale : float
|
|
280
|
-
The Langevin diffusion discretization time step (In practice, scale must
|
|
281
|
-
be smaller than 1/L, where L is the Lipschitz of the gradient of the log
|
|
282
|
-
target density, logd).
|
|
283
|
-
|
|
284
|
-
smoothing_strength : float
|
|
285
|
-
This parameter controls the smoothing strength of MYULA.
|
|
286
|
-
|
|
287
|
-
callback : callable, optional
|
|
288
|
-
A function that will be called after each sampling step. It can be useful for monitoring the sampler during sampling.
|
|
289
|
-
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)`.
|
|
290
|
-
|
|
291
|
-
A Deblur example can be found in demos/howtos/myula.py
|
|
292
|
-
# TODO: update demo once sampler merged
|
|
293
|
-
"""
|
|
294
|
-
def __init__(self, target=None, scale=1.0, smoothing_strength=0.1, **kwargs):
|
|
295
|
-
self.smoothing_strength = smoothing_strength
|
|
296
|
-
super().__init__(target=target, scale=scale, **kwargs)
|
|
297
|
-
|
|
298
|
-
@Sampler.target.setter
|
|
299
|
-
def target(self, value):
|
|
300
|
-
""" Set the target density. Runs validation of the target. """
|
|
301
|
-
self._target = value
|
|
302
|
-
|
|
303
|
-
if self._target is not None:
|
|
304
|
-
# Create a smoothed target
|
|
305
|
-
self._smoothed_target = self._create_smoothed_target(value)
|
|
306
|
-
|
|
307
|
-
# Validate the target
|
|
308
|
-
self.validate_target()
|
|
309
|
-
|
|
310
|
-
def _create_smoothed_target(self, value):
|
|
311
|
-
""" Create a smoothed target using a Moreau-Yoshida envelope. """
|
|
312
|
-
copied_value = copy(value)
|
|
313
|
-
if isinstance(copied_value.prior, RestorationPrior):
|
|
314
|
-
# Acceess the prior name
|
|
315
|
-
name = value.prior.name
|
|
316
|
-
copied_value.prior = MoreauYoshidaPrior(
|
|
317
|
-
copied_value.prior,
|
|
318
|
-
self.smoothing_strength,
|
|
319
|
-
name=name)
|
|
320
|
-
return copied_value
|
|
321
|
-
|
|
322
|
-
def validate_target(self):
|
|
323
|
-
# Call ULA target validation
|
|
324
|
-
super().validate_target()
|
|
325
|
-
|
|
326
|
-
# Additional validation for MYULA target
|
|
327
|
-
if isinstance(self.target.prior, MoreauYoshidaPrior):
|
|
328
|
-
raise ValueError(("The prior is already smoothed, apply"
|
|
329
|
-
" ULA when using a MoreauYoshidaPrior."))
|
|
330
|
-
if not hasattr(self.target.prior, "restore"):
|
|
331
|
-
raise NotImplementedError(
|
|
332
|
-
("Using MYULA with a prior that does not have a restore method"
|
|
333
|
-
" is not supported.")
|
|
334
|
-
)
|
|
335
|
-
|
|
336
|
-
def _eval_target_grad(self, x):
|
|
337
|
-
return self._smoothed_target.gradient(x)
|
|
338
|
-
|
|
339
|
-
class PnPULA(MYULA):
|
|
340
|
-
"""Plug-and-Play Unadjusted Langevin algorithm (PnP-ULA)
|
|
341
|
-
(Laumont et al., 2022)
|
|
342
|
-
|
|
343
|
-
Samples a smoothed target distribution given its smoothed logpdf gradient based on
|
|
344
|
-
Langevin diffusion dL_t = dW_t + 1/2*Nabla target.logd(L_t)dt, where W_t is
|
|
345
|
-
a `dim`-dimensional standard Brownian motion.
|
|
346
|
-
It targets a differentiable density (partially) smoothed by a convolution
|
|
347
|
-
with Gaussian kernel with zero mean and smoothing_strength variance. The
|
|
348
|
-
smoothed target density can be made arbitrarily closed to the
|
|
349
|
-
true unsmoothed target density.
|
|
350
|
-
|
|
351
|
-
For more details see: Laumont, R., Bortoli, V. D., Almansa, A., Delon, J.,
|
|
352
|
-
Durmus, A., & Pereyra, M. (2022). Bayesian imaging using plug & play priors:
|
|
353
|
-
when Langevin meets Tweedie. SIAM Journal on Imaging Sciences, 15(2), 701-737.
|
|
354
|
-
|
|
355
|
-
Parameters
|
|
356
|
-
----------
|
|
357
|
-
|
|
358
|
-
target : `cuqi.distribution.Distribution`
|
|
359
|
-
The target distribution to sample. The target distribution result from
|
|
360
|
-
a differentiable likelihood and prior of type RestorationPrior.
|
|
361
|
-
|
|
362
|
-
initial_point : ndarray
|
|
363
|
-
Initial parameters. *Optional*
|
|
364
|
-
|
|
365
|
-
scale : float
|
|
366
|
-
The Langevin diffusion discretization time step (In practice, a scale of
|
|
367
|
-
1/L, where L is the Lipschitz of the gradient of the log target density
|
|
368
|
-
is recommended but not guaranteed to be the optimal choice).
|
|
369
|
-
|
|
370
|
-
smoothing_strength : float
|
|
371
|
-
This parameter controls the smoothing strength of PnP-ULA.
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
callback : callable, optional
|
|
375
|
-
A function that will be called after each sampling step. It can be useful for monitoring the sampler during sampling.
|
|
376
|
-
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)`.
|
|
377
|
-
|
|
378
|
-
# TODO: update demo once sampler merged
|
|
379
|
-
"""
|
|
380
|
-
def __init__ (self, target=None, scale=1.0, smoothing_strength=0.1, **kwargs):
|
|
381
|
-
super().__init__(target=target, scale=scale,
|
|
382
|
-
smoothing_strength=smoothing_strength, **kwargs)
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import scipy as sp
|
|
2
|
-
import numpy as np
|
|
3
|
-
import cuqi
|
|
4
|
-
from cuqi.solver import CGLS
|
|
5
|
-
from cuqi.experimental.mcmc import Sampler
|
|
6
|
-
|
|
7
|
-
class UGLA(Sampler):
|
|
8
|
-
""" Unadjusted (Gaussian) Laplace Approximation sampler
|
|
9
|
-
|
|
10
|
-
Samples an approximate posterior where the prior is approximated
|
|
11
|
-
by a Gaussian distribution. The likelihood must be Gaussian.
|
|
12
|
-
|
|
13
|
-
Currently only works for LMRF priors.
|
|
14
|
-
|
|
15
|
-
The inner solver is Conjugate Gradient Least Squares (CGLS) solver.
|
|
16
|
-
|
|
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).
|
|
20
|
-
|
|
21
|
-
Parameters
|
|
22
|
-
----------
|
|
23
|
-
target : `cuqi.distribution.Posterior`
|
|
24
|
-
The target posterior distribution to sample.
|
|
25
|
-
|
|
26
|
-
initial_point : ndarray, *Optional*
|
|
27
|
-
Initial parameters.
|
|
28
|
-
If not provided, it defaults to zeros.
|
|
29
|
-
|
|
30
|
-
maxit : int
|
|
31
|
-
Maximum number of inner iterations for solver when generating one sample.
|
|
32
|
-
If not provided, it defaults to 50.
|
|
33
|
-
|
|
34
|
-
tol : float
|
|
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.
|
|
38
|
-
|
|
39
|
-
beta : float
|
|
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)`.
|
|
49
|
-
"""
|
|
50
|
-
def __init__(self, target=None, initial_point=None, maxit=50, tol=1e-4, beta=1e-5, **kwargs):
|
|
51
|
-
|
|
52
|
-
super().__init__(target=target, initial_point=initial_point, **kwargs)
|
|
53
|
-
|
|
54
|
-
# Parameters
|
|
55
|
-
self.maxit = maxit
|
|
56
|
-
self.tol = tol
|
|
57
|
-
self.beta = beta
|
|
58
|
-
|
|
59
|
-
def _initialize(self):
|
|
60
|
-
self._precompute()
|
|
61
|
-
|
|
62
|
-
@property
|
|
63
|
-
def prior(self):
|
|
64
|
-
return self.target.prior
|
|
65
|
-
|
|
66
|
-
@property
|
|
67
|
-
def likelihood(self):
|
|
68
|
-
return self.target.likelihood
|
|
69
|
-
|
|
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
|
|
77
|
-
|
|
78
|
-
def _precompute(self):
|
|
79
|
-
|
|
80
|
-
D = self.prior._diff_op
|
|
81
|
-
n = D.shape[0]
|
|
82
|
-
|
|
83
|
-
# Gaussian approximation of LMRF prior as function of x_k
|
|
84
|
-
def Lk_fun(x_k):
|
|
85
|
-
dd = 1/np.sqrt((D @ x_k)**2 + self.beta*np.ones(n))
|
|
86
|
-
W = sp.sparse.diags(dd)
|
|
87
|
-
return W.sqrt() @ D
|
|
88
|
-
self.Lk_fun = Lk_fun
|
|
89
|
-
|
|
90
|
-
self._m = len(self._data)
|
|
91
|
-
self._L1 = self.likelihood.distribution.sqrtprec
|
|
92
|
-
|
|
93
|
-
# If prior location is scalar, repeat it to match dimensions
|
|
94
|
-
if len(self.prior.location) == 1:
|
|
95
|
-
self._priorloc = np.repeat(self.prior.location, self.dim)
|
|
96
|
-
else:
|
|
97
|
-
self._priorloc = self.prior.location
|
|
98
|
-
|
|
99
|
-
# Initial Laplace approx
|
|
100
|
-
self._L2 = Lk_fun(self.initial_point)
|
|
101
|
-
self._L2mu = self._L2@self._priorloc
|
|
102
|
-
self._b_tild = np.hstack([self._L1@self._data, self._L2mu])
|
|
103
|
-
|
|
104
|
-
# Least squares form
|
|
105
|
-
def M(x, flag):
|
|
106
|
-
if flag == 1:
|
|
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)
|
|
109
|
-
out = np.hstack([out1, out2])
|
|
110
|
-
elif flag == 2:
|
|
111
|
-
idx = int(self._m)
|
|
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:])
|
|
114
|
-
out = out1 + out2
|
|
115
|
-
return out
|
|
116
|
-
self.M = M
|
|
117
|
-
|
|
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")
|
|
143
|
-
|
|
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')
|
|
151
|
-
|
|
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/experimental/mcmc/_mh.py
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import numpy as np
|
|
2
|
-
import cuqi
|
|
3
|
-
from cuqi.experimental.mcmc import ProposalBasedSampler
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class MH(ProposalBasedSampler):
|
|
7
|
-
""" Metropolis-Hastings (MH) sampler.
|
|
8
|
-
|
|
9
|
-
Parameters
|
|
10
|
-
----------
|
|
11
|
-
target : cuqi.density.Density
|
|
12
|
-
Target density or distribution.
|
|
13
|
-
|
|
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).
|
|
16
|
-
|
|
17
|
-
scale : float
|
|
18
|
-
Scaling parameter for the proposal distribution.
|
|
19
|
-
|
|
20
|
-
kwargs : dict
|
|
21
|
-
Additional keyword arguments to be passed to the base class :class:`ProposalBasedSampler`.
|
|
22
|
-
|
|
23
|
-
"""
|
|
24
|
-
|
|
25
|
-
_STATE_KEYS = ProposalBasedSampler._STATE_KEYS.union({'scale', '_scale_temp'})
|
|
26
|
-
|
|
27
|
-
def __init__(self, target=None, proposal=None, scale=1, **kwargs):
|
|
28
|
-
super().__init__(target, proposal=proposal, scale=scale, **kwargs)
|
|
29
|
-
|
|
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
|
|
33
|
-
|
|
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")
|
|
38
|
-
|
|
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")
|
|
44
|
-
|
|
45
|
-
def step(self):
|
|
46
|
-
# propose state
|
|
47
|
-
xi = self.proposal.sample(1) # sample from the proposal
|
|
48
|
-
x_star = self.current_point + self.scale*xi.flatten() # MH proposal
|
|
49
|
-
|
|
50
|
-
# evaluate target
|
|
51
|
-
target_eval_star = self.target.logd(x_star)
|
|
52
|
-
|
|
53
|
-
# ratio and acceptance probability
|
|
54
|
-
ratio = target_eval_star - self.current_target_logd # proposal is symmetric
|
|
55
|
-
alpha = min(0, ratio)
|
|
56
|
-
|
|
57
|
-
# accept/reject
|
|
58
|
-
u_theta = np.log(np.random.rand())
|
|
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
|
|
65
|
-
acc = 1
|
|
66
|
-
|
|
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)
|