CUQIpy 1.4.0.post0.dev13__py3-none-any.whl → 1.4.0.post0.dev41__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.dev41.dist-info}/METADATA +1 -1
- {cuqipy-1.4.0.post0.dev13.dist-info → cuqipy-1.4.0.post0.dev41.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.dev41.dist-info}/WHEEL +0 -0
- {cuqipy-1.4.0.post0.dev13.dist-info → cuqipy-1.4.0.post0.dev41.dist-info}/licenses/LICENSE +0 -0
- {cuqipy-1.4.0.post0.dev13.dist-info → cuqipy-1.4.0.post0.dev41.dist-info}/top_level.txt +0 -0
cuqi/__init__.py
CHANGED
cuqi/_version.py
CHANGED
|
@@ -8,11 +8,11 @@ import json
|
|
|
8
8
|
|
|
9
9
|
version_json = '''
|
|
10
10
|
{
|
|
11
|
-
"date": "2025-10-
|
|
11
|
+
"date": "2025-10-09T13:25:50+0200",
|
|
12
12
|
"dirty": false,
|
|
13
13
|
"error": null,
|
|
14
|
-
"full-revisionid": "
|
|
15
|
-
"version": "1.4.0.post0.
|
|
14
|
+
"full-revisionid": "92bb2e16f3828d8074c008d4ed6a08edcae0889d",
|
|
15
|
+
"version": "1.4.0.post0.dev41"
|
|
16
16
|
}
|
|
17
17
|
''' # END VERSION_JSON
|
|
18
18
|
|
cuqi/experimental/__init__.py
CHANGED
|
@@ -3,7 +3,7 @@ import inspect
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
|
|
5
5
|
# This import makes suggest_sampler easier to read
|
|
6
|
-
import cuqi.
|
|
6
|
+
import cuqi.sampler as samplers
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class SamplerRecommender(object):
|
|
@@ -15,7 +15,7 @@ class SamplerRecommender(object):
|
|
|
15
15
|
target: Density or JointDistribution
|
|
16
16
|
Distribution to get sampler recommendations for.
|
|
17
17
|
|
|
18
|
-
exceptions: list[cuqi.
|
|
18
|
+
exceptions: list[cuqi.sampler.Sampler], *optional*
|
|
19
19
|
Samplers not to be recommended.
|
|
20
20
|
|
|
21
21
|
Example
|
|
@@ -104,7 +104,7 @@ class SamplerRecommender(object):
|
|
|
104
104
|
|
|
105
105
|
"""
|
|
106
106
|
|
|
107
|
-
all_samplers = [(name, cls) for name, cls in inspect.getmembers(cuqi.
|
|
107
|
+
all_samplers = [(name, cls) for name, cls in inspect.getmembers(cuqi.sampler, inspect.isclass) if issubclass(cls, cuqi.sampler.Sampler)]
|
|
108
108
|
valid_samplers = []
|
|
109
109
|
|
|
110
110
|
for name, sampler in all_samplers:
|
|
@@ -116,7 +116,7 @@ class SamplerRecommender(object):
|
|
|
116
116
|
|
|
117
117
|
# Need a separate case for HybridGibbs
|
|
118
118
|
if self.valid_HybridGibbs_sampling_strategy() is not None:
|
|
119
|
-
valid_samplers += [cuqi.
|
|
119
|
+
valid_samplers += [cuqi.sampler.HybridGibbs.__name__ if as_string else cuqi.sampler.HybridGibbs]
|
|
120
120
|
|
|
121
121
|
return valid_samplers
|
|
122
122
|
|
cuqi/legacy/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from ._sampler import Sampler, ProposalBasedSampler
|
|
2
|
+
from ._conjugate import Conjugate
|
|
3
|
+
from ._conjugate_approx import ConjugateApprox
|
|
4
|
+
from ._cwmh import CWMH
|
|
5
|
+
from ._gibbs import Gibbs
|
|
6
|
+
from ._hmc import NUTS
|
|
7
|
+
from ._langevin_algorithm import ULA, MALA
|
|
8
|
+
from ._laplace_approximation import UGLA
|
|
9
|
+
from ._mh import MH
|
|
10
|
+
from ._pcn import pCN
|
|
11
|
+
from ._rto import LinearRTO, RegularizedLinearRTO
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from cuqi.distribution import Posterior, Gaussian, Gamma, GMRF
|
|
2
|
+
from cuqi.implicitprior import RegularizedGaussian, RegularizedGMRF
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
class Conjugate: # TODO: Subclass from Sampler once updated
|
|
6
|
+
""" Conjugate sampler
|
|
7
|
+
|
|
8
|
+
Sampler for sampling a posterior distribution where the likelihood and prior are conjugate.
|
|
9
|
+
|
|
10
|
+
Currently supported conjugate pairs are:
|
|
11
|
+
- (Gaussian, Gamma)
|
|
12
|
+
- (GMRF, Gamma)
|
|
13
|
+
- (RegularizedGaussian, Gamma) with nonnegativity constraints only
|
|
14
|
+
|
|
15
|
+
For more information on conjugate pairs, see https://en.wikipedia.org/wiki/Conjugate_prior.
|
|
16
|
+
|
|
17
|
+
For implicit regularized Gaussians see:
|
|
18
|
+
|
|
19
|
+
[1] Everink, Jasper M., Yiqiu Dong, and Martin S. Andersen. "Bayesian inference with projected densities." SIAM/ASA Journal on Uncertainty Quantification 11.3 (2023): 1025-1043.
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, target: Posterior):
|
|
24
|
+
if not isinstance(target.likelihood.distribution, (Gaussian, GMRF, RegularizedGaussian, RegularizedGMRF)):
|
|
25
|
+
raise ValueError("Conjugate sampler only works with a Gaussian-type likelihood function")
|
|
26
|
+
if not isinstance(target.prior, Gamma):
|
|
27
|
+
raise ValueError("Conjugate sampler only works with Gamma prior")
|
|
28
|
+
if not target.prior.dim == 1:
|
|
29
|
+
raise ValueError("Conjugate sampler only works with univariate Gamma prior")
|
|
30
|
+
|
|
31
|
+
if isinstance(target.likelihood.distribution, (RegularizedGaussian, RegularizedGMRF)) and (target.likelihood.distribution.preset["constraint"] not in ["nonnegativity"] or target.likelihood.distribution.preset["regularization"] is not None) :
|
|
32
|
+
raise ValueError("Conjugate sampler only works implicit regularized Gaussian likelihood with nonnegativity constraints")
|
|
33
|
+
|
|
34
|
+
self.target = target
|
|
35
|
+
|
|
36
|
+
def step(self, x=None):
|
|
37
|
+
# Extract variables
|
|
38
|
+
b = self.target.likelihood.data #mu
|
|
39
|
+
m = self._calc_m_for_Gaussians(b) #n
|
|
40
|
+
Ax = self.target.likelihood.distribution.mean #x_i
|
|
41
|
+
L = self.target.likelihood.distribution(np.array([1])).sqrtprec #L
|
|
42
|
+
alpha = self.target.prior.shape #alpha
|
|
43
|
+
beta = self.target.prior.rate #beta
|
|
44
|
+
|
|
45
|
+
# Create Gamma distribution and sample
|
|
46
|
+
dist = Gamma(shape=m/2+alpha,rate=.5*np.linalg.norm(L@(Ax-b))**2+beta)
|
|
47
|
+
|
|
48
|
+
return dist.sample()
|
|
49
|
+
|
|
50
|
+
def _calc_m_for_Gaussians(self, b):
|
|
51
|
+
""" Helper method to calculate m parameter for Gaussian-Gamma conjugate pair. """
|
|
52
|
+
if isinstance(self.target.likelihood.distribution, (Gaussian, GMRF)):
|
|
53
|
+
return len(b)
|
|
54
|
+
elif isinstance(self.target.likelihood.distribution, (RegularizedGaussian, RegularizedGMRF)):
|
|
55
|
+
return np.count_nonzero(b) # See
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from cuqi.distribution import Posterior, LMRF, Gamma
|
|
2
|
+
import numpy as np
|
|
3
|
+
import scipy as sp
|
|
4
|
+
|
|
5
|
+
class ConjugateApprox: # TODO: Subclass from Sampler once updated
|
|
6
|
+
""" Approximate Conjugate sampler
|
|
7
|
+
|
|
8
|
+
Sampler for sampling a posterior distribution where the likelihood and prior can be approximated
|
|
9
|
+
by a conjugate pair.
|
|
10
|
+
|
|
11
|
+
Currently supported pairs are:
|
|
12
|
+
- (LMRF, Gamma): Approximated by (Gaussian, Gamma)
|
|
13
|
+
|
|
14
|
+
For more information on conjugate pairs, see https://en.wikipedia.org/wiki/Conjugate_prior.
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def __init__(self, target: Posterior):
|
|
20
|
+
if not isinstance(target.likelihood.distribution, LMRF):
|
|
21
|
+
raise ValueError("Conjugate sampler only works with Laplace diff likelihood function")
|
|
22
|
+
if not isinstance(target.prior, Gamma):
|
|
23
|
+
raise ValueError("Conjugate sampler only works with Gamma prior")
|
|
24
|
+
self.target = target
|
|
25
|
+
|
|
26
|
+
def step(self, x=None):
|
|
27
|
+
# Extract variables
|
|
28
|
+
# Here we approximate the Laplace diff with a Gaussian
|
|
29
|
+
|
|
30
|
+
# Extract diff_op from target likelihood
|
|
31
|
+
D = self.target.likelihood.distribution._diff_op
|
|
32
|
+
n = D.shape[0]
|
|
33
|
+
|
|
34
|
+
# Gaussian approximation of LMRF prior as function of x_k
|
|
35
|
+
# See Uribe et al. (2022) for details
|
|
36
|
+
# Current has a zero mean assumption on likelihood! TODO
|
|
37
|
+
beta=1e-5
|
|
38
|
+
def Lk_fun(x_k):
|
|
39
|
+
dd = 1/np.sqrt((D @ x_k)**2 + beta*np.ones(n))
|
|
40
|
+
W = sp.sparse.diags(dd)
|
|
41
|
+
return W.sqrt() @ D
|
|
42
|
+
|
|
43
|
+
x = self.target.likelihood.data #x
|
|
44
|
+
d = len(x) #d
|
|
45
|
+
Lx = Lk_fun(x)@x #Lx
|
|
46
|
+
alpha = self.target.prior.shape #alpha
|
|
47
|
+
beta = self.target.prior.rate #beta
|
|
48
|
+
|
|
49
|
+
# Create Gamma distribution and sample
|
|
50
|
+
dist = Gamma(shape=d+alpha, rate=np.linalg.norm(Lx)**2+beta)
|
|
51
|
+
|
|
52
|
+
return dist.sample()
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import cuqi
|
|
3
|
+
from cuqi.legacy.sampler import ProposalBasedSampler
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CWMH(ProposalBasedSampler):
|
|
7
|
+
"""Component-wise Metropolis Hastings sampler.
|
|
8
|
+
|
|
9
|
+
Allows sampling of a target distribution by a component-wise 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 as inputs to UserDefinedDistribution)
|
|
48
|
+
target = cuqi.distribution.UserDefinedDistribution(dim=dim, logpdf_func=logpdf_func)
|
|
49
|
+
|
|
50
|
+
# Set up sampler
|
|
51
|
+
sampler = cuqi.legacy.sampler.CWMH(target, scale=1)
|
|
52
|
+
|
|
53
|
+
# Sample
|
|
54
|
+
samples = sampler.sample(2000)
|
|
55
|
+
|
|
56
|
+
"""
|
|
57
|
+
def __init__(self, target, proposal=None, scale=1, x0=None, dim = None, **kwargs):
|
|
58
|
+
super().__init__(target, proposal=proposal, scale=scale, x0=x0, dim=dim, **kwargs)
|
|
59
|
+
|
|
60
|
+
@ProposalBasedSampler.proposal.setter
|
|
61
|
+
def proposal(self, value):
|
|
62
|
+
fail_msg = "Proposal should be either None, cuqi.distribution.Distribution conditioned only on 'location' and 'scale', lambda function, or cuqi.distribution.Normal conditioned only on 'mean' and 'std'"
|
|
63
|
+
|
|
64
|
+
if value is None:
|
|
65
|
+
self._proposal = cuqi.distribution.Normal(mean = lambda location:location,std = lambda scale:scale, geometry=self.dim)
|
|
66
|
+
|
|
67
|
+
elif isinstance(value, cuqi.distribution.Distribution) and sorted(value.get_conditioning_variables())==['location','scale']:
|
|
68
|
+
self._proposal = value
|
|
69
|
+
|
|
70
|
+
elif isinstance(value, cuqi.distribution.Normal) and sorted(value.get_conditioning_variables())==['mean','std']:
|
|
71
|
+
self._proposal = value(mean = lambda location:location, std = lambda scale:scale)
|
|
72
|
+
|
|
73
|
+
elif not isinstance(value, cuqi.distribution.Distribution) and callable(value):
|
|
74
|
+
self._proposal = value
|
|
75
|
+
|
|
76
|
+
else:
|
|
77
|
+
raise ValueError(fail_msg)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _sample(self, N, Nb):
|
|
81
|
+
Ns = N+Nb # number of simulations
|
|
82
|
+
|
|
83
|
+
# allocation
|
|
84
|
+
samples = np.empty((self.dim, Ns))
|
|
85
|
+
target_eval = np.empty(Ns)
|
|
86
|
+
acc = np.zeros((self.dim, Ns), dtype=int)
|
|
87
|
+
|
|
88
|
+
# initial state
|
|
89
|
+
samples[:, 0] = self.x0
|
|
90
|
+
target_eval[0] = self.target.logd(self.x0)
|
|
91
|
+
acc[:, 0] = np.ones(self.dim)
|
|
92
|
+
|
|
93
|
+
# run MCMC
|
|
94
|
+
for s in range(Ns-1):
|
|
95
|
+
# run component by component
|
|
96
|
+
samples[:, s+1], target_eval[s+1], acc[:, s+1] = self.single_update(samples[:, s], target_eval[s])
|
|
97
|
+
|
|
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
|
+
acccomp = acc[:, Nb:].mean(axis=1)
|
|
105
|
+
print('\nAverage acceptance rate all components:', acccomp.mean(), '\n')
|
|
106
|
+
|
|
107
|
+
return samples, target_eval, acccomp
|
|
108
|
+
|
|
109
|
+
def _sample_adapt(self, N, Nb):
|
|
110
|
+
# this follows the vanishing adaptation Algorithm 4 in:
|
|
111
|
+
# Andrieu and Thoms (2008) - A tutorial on adaptive MCMC
|
|
112
|
+
Ns = N+Nb # number of simulations
|
|
113
|
+
|
|
114
|
+
# allocation
|
|
115
|
+
samples = np.empty((self.dim, Ns))
|
|
116
|
+
target_eval = np.empty(Ns)
|
|
117
|
+
acc = np.zeros((self.dim, Ns), dtype=int)
|
|
118
|
+
|
|
119
|
+
# initial state
|
|
120
|
+
samples[:, 0] = self.x0
|
|
121
|
+
target_eval[0] = self.target.logd(self.x0)
|
|
122
|
+
acc[:, 0] = np.ones(self.dim)
|
|
123
|
+
|
|
124
|
+
# initial adaptation params
|
|
125
|
+
Na = int(0.1*N) # iterations to adapt
|
|
126
|
+
hat_acc = np.empty((self.dim, int(np.floor(Ns/Na)))) # average acceptance rate of the chains
|
|
127
|
+
lambd = np.empty((self.dim, int(np.floor(Ns/Na)+1))) # scaling parameter \in (0,1)
|
|
128
|
+
lambd[:, 0] = self.scale
|
|
129
|
+
star_acc = 0.21/self.dim + 0.23 # target acceptance rate RW
|
|
130
|
+
i, idx = 0, 0
|
|
131
|
+
|
|
132
|
+
# run MCMC
|
|
133
|
+
for s in range(Ns-1):
|
|
134
|
+
# run component by component
|
|
135
|
+
samples[:, s+1], target_eval[s+1], acc[:, s+1] = self.single_update(samples[:, s], target_eval[s])
|
|
136
|
+
|
|
137
|
+
# adapt prop spread of each component using acc of past samples
|
|
138
|
+
if ((s+1) % Na == 0):
|
|
139
|
+
# evaluate average acceptance rate
|
|
140
|
+
hat_acc[:, i] = np.mean(acc[:, idx:idx+Na], axis=1)
|
|
141
|
+
|
|
142
|
+
# compute new scaling parameter
|
|
143
|
+
zeta = 1/np.sqrt(i+1) # ensures that the variation of lambda(i) vanishes
|
|
144
|
+
lambd[:, i+1] = np.exp(np.log(lambd[:, i]) + zeta*(hat_acc[:, i]-star_acc))
|
|
145
|
+
|
|
146
|
+
# update parameters
|
|
147
|
+
self.scale = np.minimum(lambd[:, i+1], np.ones(self.dim))
|
|
148
|
+
|
|
149
|
+
# update counters
|
|
150
|
+
i += 1
|
|
151
|
+
idx += Na
|
|
152
|
+
|
|
153
|
+
# display iterations
|
|
154
|
+
self._print_progress(s+2,Ns) #s+2 is the sample number, s+1 is index assuming x0 is the first sample
|
|
155
|
+
self._call_callback(samples[:, s+1], s+1)
|
|
156
|
+
|
|
157
|
+
# remove burn-in
|
|
158
|
+
samples = samples[:, Nb:]
|
|
159
|
+
target_eval = target_eval[Nb:]
|
|
160
|
+
acccomp = acc[:, Nb:].mean(axis=1)
|
|
161
|
+
print('\nAverage acceptance rate all components:', acccomp.mean(), '\n')
|
|
162
|
+
|
|
163
|
+
return samples, target_eval, acccomp
|
|
164
|
+
|
|
165
|
+
def single_update(self, x_t, target_eval_t):
|
|
166
|
+
if isinstance(self.proposal,cuqi.distribution.Distribution):
|
|
167
|
+
x_i_star = self.proposal(location= x_t, scale = self.scale).sample()
|
|
168
|
+
else:
|
|
169
|
+
x_i_star = self.proposal(x_t, self.scale)
|
|
170
|
+
x_star = x_t.copy()
|
|
171
|
+
acc = np.zeros(self.dim)
|
|
172
|
+
|
|
173
|
+
for j in range(self.dim):
|
|
174
|
+
# propose state
|
|
175
|
+
x_star[j] = x_i_star[j]
|
|
176
|
+
|
|
177
|
+
# evaluate target
|
|
178
|
+
target_eval_star = self.target.logd(x_star)
|
|
179
|
+
|
|
180
|
+
# ratio and acceptance probability
|
|
181
|
+
ratio = target_eval_star - target_eval_t # proposal is symmetric
|
|
182
|
+
alpha = min(0, ratio)
|
|
183
|
+
|
|
184
|
+
# accept/reject
|
|
185
|
+
u_theta = np.log(np.random.rand())
|
|
186
|
+
if (u_theta <= alpha):
|
|
187
|
+
x_t[j] = x_i_star[j]
|
|
188
|
+
target_eval_t = target_eval_star
|
|
189
|
+
acc[j] = 1
|
|
190
|
+
else:
|
|
191
|
+
pass
|
|
192
|
+
# x_t[j] = x_t[j]
|
|
193
|
+
# target_eval_t = target_eval_t
|
|
194
|
+
x_star = x_t.copy()
|
|
195
|
+
#
|
|
196
|
+
return x_t, target_eval_t, acc
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
from cuqi.distribution import JointDistribution
|
|
2
|
+
from cuqi.legacy.sampler import Sampler
|
|
3
|
+
from cuqi.samples import Samples
|
|
4
|
+
from typing import Dict, Union
|
|
5
|
+
import numpy as np
|
|
6
|
+
import sys
|
|
7
|
+
import warnings
|
|
8
|
+
|
|
9
|
+
class Gibbs:
|
|
10
|
+
"""
|
|
11
|
+
Gibbs sampler for sampling a joint distribution.
|
|
12
|
+
|
|
13
|
+
Gibbs sampling samples the variables of the distribution sequentially,
|
|
14
|
+
one variable at a time. When a variable represents a random vector, the
|
|
15
|
+
whole vector is sampled simultaneously.
|
|
16
|
+
|
|
17
|
+
The sampling of each variable is done by sampling from the conditional
|
|
18
|
+
distribution of that variable given the values of the other variables.
|
|
19
|
+
This is often a very efficient way of sampling from a joint distribution
|
|
20
|
+
if the conditional distributions are easy to sample from.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
target : cuqi.distribution.JointDistribution
|
|
25
|
+
Target distribution to sample from.
|
|
26
|
+
|
|
27
|
+
sampling_strategy : dict
|
|
28
|
+
Dictionary of sampling strategies for each parameter.
|
|
29
|
+
Keys are parameter names.
|
|
30
|
+
Values are sampler objects.
|
|
31
|
+
|
|
32
|
+
Example
|
|
33
|
+
-------
|
|
34
|
+
.. code-block:: python
|
|
35
|
+
|
|
36
|
+
import cuqi
|
|
37
|
+
import numpy as np
|
|
38
|
+
|
|
39
|
+
# Model and data
|
|
40
|
+
A, y_obs, probinfo = cuqi.testproblem.Deconvolution1D(phantom='square').get_components()
|
|
41
|
+
n = A.domain_dim
|
|
42
|
+
|
|
43
|
+
# Define distributions
|
|
44
|
+
d = cuqi.distribution.Gamma(1, 1e-4)
|
|
45
|
+
l = cuqi.distribution.Gamma(1, 1e-4)
|
|
46
|
+
x = cuqi.distribution.GMRF(np.zeros(n), lambda d: d)
|
|
47
|
+
y = cuqi.distribution.Gaussian(A, lambda l: 1/l)
|
|
48
|
+
|
|
49
|
+
# Combine into a joint distribution and create posterior
|
|
50
|
+
joint = cuqi.distribution.JointDistribution(d, l, x, y)
|
|
51
|
+
posterior = joint(y=y_obs)
|
|
52
|
+
|
|
53
|
+
# Define sampling strategy
|
|
54
|
+
sampling_strategy = {
|
|
55
|
+
'x': cuqi.legacy.sampler.LinearRTO,
|
|
56
|
+
('d', 'l'): cuqi.legacy.sampler.Conjugate,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Define Gibbs sampler
|
|
60
|
+
sampler = cuqi.legacy.sampler.Gibbs(posterior, sampling_strategy)
|
|
61
|
+
|
|
62
|
+
# Run sampler
|
|
63
|
+
samples = sampler.sample(Ns=1000, Nb=200)
|
|
64
|
+
|
|
65
|
+
# Plot results
|
|
66
|
+
samples['x'].plot_ci(exact=probinfo.exactSolution)
|
|
67
|
+
samples['d'].plot_trace(figsize=(8,2))
|
|
68
|
+
samples['l'].plot_trace(figsize=(8,2))
|
|
69
|
+
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(self, target: JointDistribution, sampling_strategy: Dict[Union[str,tuple], Sampler]):
|
|
73
|
+
|
|
74
|
+
warnings.warn(f"\nYou are using the legacy sampler '{self.__class__.__name__}'.\n"
|
|
75
|
+
f"This will be removed in a future release of CUQIpy.\n"
|
|
76
|
+
f"Please consider using the new samplers in the 'cuqi.sampler' module.\n", UserWarning, stacklevel=2)
|
|
77
|
+
|
|
78
|
+
# Store target and allow conditioning to reduce to a single density
|
|
79
|
+
self.target = target() # Create a copy of target distribution (to avoid modifying the original)
|
|
80
|
+
|
|
81
|
+
# Parse samplers and split any keys that are tuple into separate keys
|
|
82
|
+
self.samplers = {}
|
|
83
|
+
for par_name in sampling_strategy.keys():
|
|
84
|
+
if isinstance(par_name, tuple):
|
|
85
|
+
for par_name_ in par_name:
|
|
86
|
+
self.samplers[par_name_] = sampling_strategy[par_name]
|
|
87
|
+
else:
|
|
88
|
+
self.samplers[par_name] = sampling_strategy[par_name]
|
|
89
|
+
|
|
90
|
+
# Store parameter names
|
|
91
|
+
self.par_names = self.target.get_parameter_names()
|
|
92
|
+
|
|
93
|
+
# ------------ Public methods ------------
|
|
94
|
+
def sample(self, Ns, Nb=0):
|
|
95
|
+
""" Sample from target distribution """
|
|
96
|
+
|
|
97
|
+
# Initial points
|
|
98
|
+
current_samples = self._get_initial_points()
|
|
99
|
+
|
|
100
|
+
# Compute how many samples were already taken previously
|
|
101
|
+
at_Nb = self._Nb
|
|
102
|
+
at_Ns = self._Ns
|
|
103
|
+
|
|
104
|
+
# Allocate memory for samples
|
|
105
|
+
self._allocate_samples_warmup(Nb)
|
|
106
|
+
self._allocate_samples(Ns)
|
|
107
|
+
|
|
108
|
+
# Sample tuning phase
|
|
109
|
+
for i in range(at_Nb, at_Nb+Nb):
|
|
110
|
+
current_samples = self.step_tune(current_samples)
|
|
111
|
+
self._store_samples(self.samples_warmup, current_samples, i)
|
|
112
|
+
self._print_progress(i+1+at_Nb, at_Nb+Nb, 'Warmup')
|
|
113
|
+
|
|
114
|
+
# Sample phase
|
|
115
|
+
for i in range(at_Ns, at_Ns+Ns):
|
|
116
|
+
current_samples = self.step(current_samples)
|
|
117
|
+
self._store_samples(self.samples, current_samples, i)
|
|
118
|
+
self._print_progress(i+1, at_Ns+Ns, 'Sample')
|
|
119
|
+
|
|
120
|
+
# Convert to samples objects and return
|
|
121
|
+
return self._convert_to_Samples(self.samples)
|
|
122
|
+
|
|
123
|
+
def step(self, current_samples):
|
|
124
|
+
""" Sequentially go through all parameters and sample them conditionally on each other """
|
|
125
|
+
|
|
126
|
+
# Extract par names
|
|
127
|
+
par_names = self.par_names
|
|
128
|
+
|
|
129
|
+
# Sample from each conditional distribution
|
|
130
|
+
for par_name in par_names:
|
|
131
|
+
|
|
132
|
+
# Dict of all other parameters to condition on
|
|
133
|
+
other_params = {par_name_: current_samples[par_name_] for par_name_ in par_names if par_name_ != par_name}
|
|
134
|
+
|
|
135
|
+
# Set up sampler for current conditional distribution
|
|
136
|
+
sampler = self.samplers[par_name](self.target(**other_params))
|
|
137
|
+
|
|
138
|
+
# Take a MCMC step
|
|
139
|
+
current_samples[par_name] = sampler.step(current_samples[par_name])
|
|
140
|
+
|
|
141
|
+
# Ensure even 1-dimensional samples are 1D arrays
|
|
142
|
+
current_samples[par_name] = current_samples[par_name].reshape(-1)
|
|
143
|
+
|
|
144
|
+
return current_samples
|
|
145
|
+
|
|
146
|
+
def step_tune(self, current_samples):
|
|
147
|
+
""" Perform a single MCMC step for each parameter and tune the sampler """
|
|
148
|
+
# Not implemented. No tuning happening here yet. Requires samplers to be able to be modified after initialization.
|
|
149
|
+
return self.step(current_samples)
|
|
150
|
+
|
|
151
|
+
# ------------ Private methods ------------
|
|
152
|
+
def _allocate_samples(self, Ns):
|
|
153
|
+
""" Allocate memory for samples """
|
|
154
|
+
# Allocate memory for samples
|
|
155
|
+
samples = {}
|
|
156
|
+
for par_name in self.par_names:
|
|
157
|
+
samples[par_name] = np.zeros((self.target.get_density(par_name).dim, Ns))
|
|
158
|
+
|
|
159
|
+
# Store samples in self
|
|
160
|
+
if hasattr(self, 'samples'):
|
|
161
|
+
# Append to existing samples (This makes a copy)
|
|
162
|
+
for par_name in self.par_names:
|
|
163
|
+
samples[par_name] = np.hstack((self.samples[par_name], samples[par_name]))
|
|
164
|
+
self.samples = samples
|
|
165
|
+
|
|
166
|
+
def _allocate_samples_warmup(self, Nb):
|
|
167
|
+
""" Allocate memory for samples """
|
|
168
|
+
|
|
169
|
+
# If we already have warmup samples and more are requested raise error
|
|
170
|
+
if hasattr(self, 'samples_warmup') and Nb != 0:
|
|
171
|
+
raise ValueError('Sampler already has run warmup phase. Cannot run warmup phase again.')
|
|
172
|
+
|
|
173
|
+
# Allocate memory for samples
|
|
174
|
+
samples = {}
|
|
175
|
+
for par_name in self.par_names:
|
|
176
|
+
samples[par_name] = np.zeros((self.target.get_density(par_name).dim, Nb))
|
|
177
|
+
self.samples_warmup = samples
|
|
178
|
+
|
|
179
|
+
def _get_initial_points(self):
|
|
180
|
+
""" Get initial points for each parameter """
|
|
181
|
+
initial_points = {}
|
|
182
|
+
for par_name in self.par_names:
|
|
183
|
+
if hasattr(self, 'samples'):
|
|
184
|
+
initial_points[par_name] = self.samples[par_name][:, -1]
|
|
185
|
+
elif hasattr(self, 'samples_warmup'):
|
|
186
|
+
initial_points[par_name] = self.samples_warmup[par_name][:, -1]
|
|
187
|
+
elif hasattr(self.target.get_density(par_name), 'init_point'):
|
|
188
|
+
initial_points[par_name] = self.target.get_density(par_name).init_point
|
|
189
|
+
else:
|
|
190
|
+
initial_points[par_name] = np.ones(self.target.get_density(par_name).dim)
|
|
191
|
+
return initial_points
|
|
192
|
+
|
|
193
|
+
def _store_samples(self, samples, current_samples, i):
|
|
194
|
+
""" Store current samples at index i of samples dict """
|
|
195
|
+
for par_name in self.par_names:
|
|
196
|
+
samples[par_name][:, i] = current_samples[par_name]
|
|
197
|
+
|
|
198
|
+
def _convert_to_Samples(self, samples):
|
|
199
|
+
""" Convert each parameter in samples dict to cuqi.samples.Samples object with correct geometry """
|
|
200
|
+
samples_object = {}
|
|
201
|
+
for par_name in self.par_names:
|
|
202
|
+
samples_object[par_name] = Samples(samples[par_name], self.target.get_density(par_name).geometry)
|
|
203
|
+
return samples_object
|
|
204
|
+
|
|
205
|
+
def _print_progress(self, s, Ns, phase):
|
|
206
|
+
"""Prints sampling progress"""
|
|
207
|
+
if Ns < 2: # Don't print progress if only one sample
|
|
208
|
+
return
|
|
209
|
+
if (s % (max(Ns//100,1))) == 0:
|
|
210
|
+
msg = f'{phase} {s} / {Ns}'
|
|
211
|
+
sys.stdout.write('\r'+msg)
|
|
212
|
+
if s==Ns:
|
|
213
|
+
msg = f'{phase} {s} / {Ns}'
|
|
214
|
+
sys.stdout.write('\r'+msg+'\n')
|
|
215
|
+
|
|
216
|
+
# ------------ Private properties ------------
|
|
217
|
+
@property
|
|
218
|
+
def _Ns(self):
|
|
219
|
+
""" Number of samples already taken """
|
|
220
|
+
if hasattr(self, 'samples'):
|
|
221
|
+
return self.samples[self.par_names[0]].shape[-1]
|
|
222
|
+
else:
|
|
223
|
+
return 0
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def _Nb(self):
|
|
227
|
+
""" Number of samples already taken in warmup phase """
|
|
228
|
+
if hasattr(self, 'samples_warmup'):
|
|
229
|
+
return self.samples_warmup[self.par_names[0]].shape[-1]
|
|
230
|
+
else:
|
|
231
|
+
return 0
|