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/sampler/_cwmh.py
CHANGED
|
@@ -1,41 +1,49 @@
|
|
|
1
1
|
import numpy as np
|
|
2
2
|
import cuqi
|
|
3
3
|
from cuqi.sampler import ProposalBasedSampler
|
|
4
|
-
|
|
4
|
+
from cuqi.array import CUQIarray
|
|
5
|
+
from numbers import Number
|
|
5
6
|
|
|
6
7
|
class CWMH(ProposalBasedSampler):
|
|
7
8
|
"""Component-wise Metropolis Hastings sampler.
|
|
8
9
|
|
|
9
|
-
Allows sampling of a target distribution by a component-wise random-walk
|
|
10
|
+
Allows sampling of a target distribution by a component-wise random-walk
|
|
11
|
+
sampling of a proposal distribution along with an accept/reject step.
|
|
10
12
|
|
|
11
13
|
Parameters
|
|
12
14
|
----------
|
|
13
15
|
|
|
14
16
|
target : `cuqi.distribution.Distribution` or lambda function
|
|
15
|
-
The target distribution to sample. Custom logpdfs are supported by using
|
|
17
|
+
The target distribution to sample. Custom logpdfs are supported by using
|
|
18
|
+
a :class:`cuqi.distribution.UserDefinedDistribution`.
|
|
16
19
|
|
|
17
20
|
proposal : `cuqi.distribution.Distribution` or callable method
|
|
18
|
-
The proposal to sample from. If a callable method it should provide a
|
|
21
|
+
The proposal to sample from. If a callable method it should provide a
|
|
22
|
+
single independent sample from proposal distribution. Defaults to a
|
|
23
|
+
Gaussian proposal. *Optional*.
|
|
19
24
|
|
|
20
|
-
scale : float
|
|
21
|
-
Scale parameter used to define correlation between previous and proposed
|
|
25
|
+
scale : float or ndarray
|
|
26
|
+
Scale parameter used to define correlation between previous and proposed
|
|
27
|
+
sample in random-walk. *Optional*. If float, the same scale is used for
|
|
28
|
+
all dimensions. If ndarray, a (possibly) different scale is used for
|
|
29
|
+
each dimension.
|
|
22
30
|
|
|
23
|
-
|
|
31
|
+
initial_point : ndarray
|
|
24
32
|
Initial parameters. *Optional*
|
|
25
33
|
|
|
26
|
-
|
|
27
|
-
|
|
34
|
+
callback : callable, optional
|
|
35
|
+
A function that will be called after each sampling step. It can be useful for monitoring the sampler during sampling.
|
|
36
|
+
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)`.
|
|
28
37
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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.
|
|
38
|
+
kwargs : dict
|
|
39
|
+
Additional keyword arguments to be passed to the base class
|
|
40
|
+
:class:`ProposalBasedSampler`.
|
|
34
41
|
|
|
35
42
|
Example
|
|
36
43
|
-------
|
|
37
44
|
.. code-block:: python
|
|
38
|
-
|
|
45
|
+
import numpy as np
|
|
46
|
+
import cuqi
|
|
39
47
|
# Parameters
|
|
40
48
|
dim = 5 # Dimension of distribution
|
|
41
49
|
mu = np.arange(dim) # Mean of Gaussian
|
|
@@ -44,153 +52,139 @@ class CWMH(ProposalBasedSampler):
|
|
|
44
52
|
# Logpdf function
|
|
45
53
|
logpdf_func = lambda x: -1/(std**2)*np.sum((x-mu)**2)
|
|
46
54
|
|
|
47
|
-
# Define distribution from logpdf as UserDefinedDistribution (sample
|
|
48
|
-
|
|
55
|
+
# Define distribution from logpdf as UserDefinedDistribution (sample
|
|
56
|
+
# and gradients also supported as inputs to UserDefinedDistribution)
|
|
57
|
+
target = cuqi.distribution.UserDefinedDistribution(
|
|
58
|
+
dim=dim, logpdf_func=logpdf_func)
|
|
49
59
|
|
|
50
60
|
# Set up sampler
|
|
51
61
|
sampler = cuqi.sampler.CWMH(target, scale=1)
|
|
52
62
|
|
|
53
63
|
# Sample
|
|
54
|
-
samples = sampler.sample(2000)
|
|
64
|
+
samples = sampler.sample(2000).get_samples()
|
|
55
65
|
|
|
56
66
|
"""
|
|
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
67
|
|
|
98
|
-
|
|
99
|
-
self._call_callback(samples[:, s+1], s+1)
|
|
68
|
+
_STATE_KEYS = ProposalBasedSampler._STATE_KEYS.union(['_scale_temp'])
|
|
100
69
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
print('\nAverage acceptance rate all components:', acccomp.mean(), '\n')
|
|
70
|
+
def __init__(self, target:cuqi.density.Density=None, proposal=None, scale=1,
|
|
71
|
+
initial_point=None, **kwargs):
|
|
72
|
+
super().__init__(target, proposal=proposal, scale=scale,
|
|
73
|
+
initial_point=initial_point, **kwargs)
|
|
106
74
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
#
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
#
|
|
133
|
-
|
|
134
|
-
|
|
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')
|
|
75
|
+
def _initialize(self):
|
|
76
|
+
if isinstance(self.scale, Number):
|
|
77
|
+
self.scale = np.ones(self.dim)*self.scale
|
|
78
|
+
self._acc = [np.ones((self.dim))] # Overwrite acc from ProposalBasedSampler with list of arrays
|
|
79
|
+
|
|
80
|
+
# Handling of temporary scale parameter due to possible bug in old CWMH
|
|
81
|
+
self._scale_temp = self.scale.copy()
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def scale(self):
|
|
85
|
+
""" Get the scale parameter. """
|
|
86
|
+
return self._scale
|
|
87
|
+
|
|
88
|
+
@scale.setter
|
|
89
|
+
def scale(self, value):
|
|
90
|
+
""" Set the scale parameter. """
|
|
91
|
+
if self._is_initialized and isinstance(value, Number):
|
|
92
|
+
value = np.ones(self.dim)*value
|
|
93
|
+
self._scale = value
|
|
94
|
+
|
|
95
|
+
def validate_target(self):
|
|
96
|
+
if not isinstance(self.target, cuqi.density.Density):
|
|
97
|
+
raise ValueError(
|
|
98
|
+
"Target should be an instance of "+\
|
|
99
|
+
f"{cuqi.density.Density.__class__.__name__}")
|
|
100
|
+
# Fail when there is no log density, which is currently assumed to be the case in case NaN is returned.
|
|
101
|
+
if np.isnan(self.target.logd(self._get_default_initial_point(self.dim))):
|
|
102
|
+
raise ValueError("Target does not have valid logd")
|
|
162
103
|
|
|
163
|
-
|
|
104
|
+
def validate_proposal(self):
|
|
105
|
+
if not isinstance(self.proposal, cuqi.distribution.Distribution):
|
|
106
|
+
raise ValueError("Proposal must be a cuqi.distribution.Distribution object")
|
|
107
|
+
if not self.proposal.is_symmetric:
|
|
108
|
+
raise ValueError("Proposal must be symmetric")
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def proposal(self):
|
|
112
|
+
if self._proposal is None:
|
|
113
|
+
self._proposal = cuqi.distribution.Normal(
|
|
114
|
+
mean=lambda location: location,
|
|
115
|
+
std=lambda scale: scale,
|
|
116
|
+
geometry=self.dim,
|
|
117
|
+
)
|
|
118
|
+
return self._proposal
|
|
119
|
+
|
|
120
|
+
@proposal.setter
|
|
121
|
+
def proposal(self, value):
|
|
122
|
+
self._proposal = value
|
|
123
|
+
|
|
124
|
+
def step(self):
|
|
125
|
+
# Initialize x_t which is used to store the current CWMH sample
|
|
126
|
+
x_t = self.current_point.copy()
|
|
164
127
|
|
|
165
|
-
|
|
128
|
+
# Initialize x_star which is used to store the proposed sample by
|
|
129
|
+
# updating the current sample component-by-component
|
|
130
|
+
x_star = self.current_point.copy()
|
|
131
|
+
|
|
132
|
+
# Propose a sample x_all_components from the proposal distribution
|
|
133
|
+
# for all the components
|
|
134
|
+
target_eval_t = self.current_target_logd
|
|
166
135
|
if isinstance(self.proposal,cuqi.distribution.Distribution):
|
|
167
|
-
|
|
136
|
+
x_all_components = self.proposal(
|
|
137
|
+
location= self.current_point, scale=self.scale).sample()
|
|
168
138
|
else:
|
|
169
|
-
|
|
170
|
-
|
|
139
|
+
x_all_components = self.proposal(self.current_point, self.scale)
|
|
140
|
+
|
|
141
|
+
# Initialize acceptance rate
|
|
171
142
|
acc = np.zeros(self.dim)
|
|
172
143
|
|
|
144
|
+
# Loop over all the components of the sample and accept/reject
|
|
145
|
+
# each component update.
|
|
173
146
|
for j in range(self.dim):
|
|
174
|
-
# propose state
|
|
175
|
-
x_star[j] =
|
|
147
|
+
# propose state x_star by updating the j-th component
|
|
148
|
+
x_star[j] = x_all_components[j]
|
|
176
149
|
|
|
177
150
|
# evaluate target
|
|
178
151
|
target_eval_star = self.target.logd(x_star)
|
|
179
152
|
|
|
180
|
-
#
|
|
181
|
-
|
|
182
|
-
alpha = min(0, ratio)
|
|
153
|
+
# compute Metropolis acceptance ratio
|
|
154
|
+
alpha = min(0, target_eval_star - target_eval_t)
|
|
183
155
|
|
|
184
156
|
# accept/reject
|
|
185
157
|
u_theta = np.log(np.random.rand())
|
|
186
|
-
if (u_theta <= alpha)
|
|
187
|
-
|
|
158
|
+
if (u_theta <= alpha) and \
|
|
159
|
+
(not np.isnan(target_eval_star)) and \
|
|
160
|
+
(not np.isinf(target_eval_star)):
|
|
161
|
+
x_t[j] = x_all_components[j]
|
|
188
162
|
target_eval_t = target_eval_star
|
|
189
163
|
acc[j] = 1
|
|
190
|
-
|
|
191
|
-
pass
|
|
192
|
-
# x_t[j] = x_t[j]
|
|
193
|
-
# target_eval_t = target_eval_t
|
|
164
|
+
|
|
194
165
|
x_star = x_t.copy()
|
|
195
|
-
|
|
196
|
-
|
|
166
|
+
|
|
167
|
+
self.current_target_logd = target_eval_t
|
|
168
|
+
self.current_point = x_t
|
|
169
|
+
|
|
170
|
+
return acc
|
|
171
|
+
|
|
172
|
+
def tune(self, skip_len, update_count):
|
|
173
|
+
# Store update_count in variable i for readability
|
|
174
|
+
i = update_count
|
|
175
|
+
|
|
176
|
+
# Optimal acceptance rate for CWMH
|
|
177
|
+
star_acc = 0.21/self.dim + 0.23
|
|
178
|
+
|
|
179
|
+
# Mean of acceptance rate over the last skip_len samples
|
|
180
|
+
hat_acc = np.mean(self._acc[i*skip_len:(i+1)*skip_len], axis=0)
|
|
181
|
+
|
|
182
|
+
# Compute new intermediate scaling parameter scale_temp
|
|
183
|
+
# Factor zeta ensures that the variation of the scale update vanishes
|
|
184
|
+
zeta = 1/np.sqrt(update_count+1)
|
|
185
|
+
scale_temp = np.exp(
|
|
186
|
+
np.log(self._scale_temp) + zeta*(hat_acc-star_acc))
|
|
187
|
+
|
|
188
|
+
# Update the scale parameter
|
|
189
|
+
self.scale = np.minimum(scale_temp, np.ones(self.dim))
|
|
190
|
+
self._scale_temp = scale_temp
|