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.

Files changed (48) hide show
  1. cuqi/__init__.py +1 -0
  2. cuqi/_version.py +3 -3
  3. cuqi/experimental/__init__.py +1 -2
  4. cuqi/experimental/_recommender.py +4 -4
  5. cuqi/legacy/__init__.py +2 -0
  6. cuqi/legacy/sampler/__init__.py +11 -0
  7. cuqi/legacy/sampler/_conjugate.py +55 -0
  8. cuqi/legacy/sampler/_conjugate_approx.py +52 -0
  9. cuqi/legacy/sampler/_cwmh.py +196 -0
  10. cuqi/legacy/sampler/_gibbs.py +231 -0
  11. cuqi/legacy/sampler/_hmc.py +335 -0
  12. cuqi/legacy/sampler/_langevin_algorithm.py +198 -0
  13. cuqi/legacy/sampler/_laplace_approximation.py +184 -0
  14. cuqi/legacy/sampler/_mh.py +190 -0
  15. cuqi/legacy/sampler/_pcn.py +244 -0
  16. cuqi/legacy/sampler/_rto.py +284 -0
  17. cuqi/legacy/sampler/_sampler.py +182 -0
  18. cuqi/problem/_problem.py +87 -80
  19. cuqi/sampler/__init__.py +120 -8
  20. cuqi/sampler/_conjugate.py +376 -35
  21. cuqi/sampler/_conjugate_approx.py +40 -16
  22. cuqi/sampler/_cwmh.py +132 -138
  23. cuqi/{experimental/mcmc → sampler}/_direct.py +1 -1
  24. cuqi/sampler/_gibbs.py +269 -130
  25. cuqi/sampler/_hmc.py +328 -201
  26. cuqi/sampler/_langevin_algorithm.py +282 -98
  27. cuqi/sampler/_laplace_approximation.py +87 -117
  28. cuqi/sampler/_mh.py +47 -157
  29. cuqi/sampler/_pcn.py +56 -211
  30. cuqi/sampler/_rto.py +206 -140
  31. cuqi/sampler/_sampler.py +540 -135
  32. {cuqipy-1.4.0.post0.dev13.dist-info → cuqipy-1.4.0.post0.dev45.dist-info}/METADATA +1 -1
  33. {cuqipy-1.4.0.post0.dev13.dist-info → cuqipy-1.4.0.post0.dev45.dist-info}/RECORD +36 -35
  34. cuqi/experimental/mcmc/__init__.py +0 -122
  35. cuqi/experimental/mcmc/_conjugate.py +0 -396
  36. cuqi/experimental/mcmc/_conjugate_approx.py +0 -76
  37. cuqi/experimental/mcmc/_cwmh.py +0 -190
  38. cuqi/experimental/mcmc/_gibbs.py +0 -366
  39. cuqi/experimental/mcmc/_hmc.py +0 -462
  40. cuqi/experimental/mcmc/_langevin_algorithm.py +0 -382
  41. cuqi/experimental/mcmc/_laplace_approximation.py +0 -154
  42. cuqi/experimental/mcmc/_mh.py +0 -80
  43. cuqi/experimental/mcmc/_pcn.py +0 -89
  44. cuqi/experimental/mcmc/_rto.py +0 -350
  45. cuqi/experimental/mcmc/_sampler.py +0 -582
  46. {cuqipy-1.4.0.post0.dev13.dist-info → cuqipy-1.4.0.post0.dev45.dist-info}/WHEEL +0 -0
  47. {cuqipy-1.4.0.post0.dev13.dist-info → cuqipy-1.4.0.post0.dev45.dist-info}/licenses/LICENSE +0 -0
  48. {cuqipy-1.4.0.post0.dev13.dist-info → cuqipy-1.4.0.post0.dev45.dist-info}/top_level.txt +0 -0
@@ -1,462 +0,0 @@
1
- import numpy as np
2
- import numpy as np
3
- from cuqi.experimental.mcmc import Sampler
4
- from cuqi.array import CUQIarray
5
- from numbers import Number
6
-
7
- class NUTS(Sampler):
8
- """No-U-Turn Sampler (Hoffman and Gelman, 2014).
9
-
10
- Samples a distribution given its logpdf and gradient using a Hamiltonian
11
- Monte Carlo (HMC) algorithm with automatic parameter tuning.
12
-
13
- For more details see: See Hoffman, M. D., & Gelman, A. (2014). The no-U-turn
14
- sampler: Adaptively setting path lengths in Hamiltonian Monte Carlo. Journal
15
- of Machine Learning Research, 15, 1593-1623.
16
-
17
- Parameters
18
- ----------
19
- target : `cuqi.distribution.Distribution`
20
- The target distribution to sample. Must have logpdf and gradient method.
21
- Custom logpdfs and gradients are supported by using a
22
- :class:`cuqi.distribution.UserDefinedDistribution`.
23
-
24
- initial_point : ndarray
25
- Initial parameters. *Optional*. If not provided, the initial point is
26
- an array of ones.
27
-
28
- max_depth : int
29
- Maximum depth of the tree >=0 and the default is 15.
30
-
31
- step_size : None or float
32
- If step_size is provided (as positive float), it will be used as initial
33
- step size. If None, the step size will be estimated by the sampler.
34
-
35
- opt_acc_rate : float
36
- The optimal acceptance rate to reach if using adaptive step size.
37
- Suggested values are 0.6 (default) or 0.8 (as in stan). In principle,
38
- opt_acc_rate should be in (0, 1), however, choosing a value that is very
39
- close to 1 or 0 might lead to poor performance of the sampler.
40
-
41
- callback : callable, optional
42
- A function that will be called after each sampling step. It can be useful for monitoring the sampler during sampling.
43
- 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)`.
44
-
45
- Example
46
- -------
47
- .. code-block:: python
48
-
49
- # Import cuqi
50
- import cuqi
51
-
52
- # Define a target distribution
53
- tp = cuqi.testproblem.WangCubic()
54
- target = tp.posterior
55
-
56
- # Set up sampler
57
- sampler = cuqi.experimental.mcmc.NUTS(target)
58
-
59
- # Sample
60
- sampler.warmup(5000)
61
- sampler.sample(10000)
62
-
63
- # Get samples
64
- samples = sampler.get_samples()
65
-
66
- # Plot samples
67
- samples.plot_pair()
68
-
69
- After running the NUTS sampler, run diagnostics can be accessed via the
70
- following attributes:
71
-
72
- .. code-block:: python
73
-
74
- # Number of tree nodes created each NUTS iteration
75
- sampler.num_tree_node_list
76
-
77
- # Step size used in each NUTS iteration
78
- sampler.epsilon_list
79
-
80
- # Suggested step size during adaptation (the value of this step size is
81
- # only used after adaptation).
82
- sampler.epsilon_bar_list
83
-
84
- """
85
-
86
- _STATE_KEYS = Sampler._STATE_KEYS.union({'_epsilon', '_epsilon_bar',
87
- '_H_bar',
88
- 'current_target_logd',
89
- 'current_target_grad',
90
- 'max_depth'})
91
-
92
- _HISTORY_KEYS = Sampler._HISTORY_KEYS.union({'num_tree_node_list',
93
- 'epsilon_list',
94
- 'epsilon_bar_list'})
95
-
96
- def __init__(self, target=None, initial_point=None, max_depth=None,
97
- step_size=None, opt_acc_rate=0.6, **kwargs):
98
- super().__init__(target, initial_point=initial_point, **kwargs)
99
-
100
- # Assign parameters as attributes
101
- self.max_depth = max_depth
102
- self.step_size = step_size
103
- self.opt_acc_rate = opt_acc_rate
104
-
105
-
106
- def _initialize(self):
107
-
108
- self._current_alpha_ratio = np.nan # Current alpha ratio will be set to some
109
- # value (other than np.nan) before
110
- # being used
111
-
112
- self.current_target_logd, self.current_target_grad = self._nuts_target(self.current_point)
113
-
114
- # Parameters dual averaging
115
- # Initialize epsilon and epsilon_bar
116
- # epsilon is the step size used in the current iteration
117
- # after warm up and one sampling step, epsilon is updated
118
- # to epsilon_bar for the remaining sampling steps.
119
- if self.step_size is None:
120
- self._epsilon = self._FindGoodEpsilon()
121
- self.step_size = self._epsilon
122
- else:
123
- self._epsilon = self.step_size
124
-
125
- self._epsilon_bar = "unset"
126
-
127
- # Parameter mu, does not change during the run
128
- self._mu = np.log(10*self._epsilon)
129
-
130
- self._H_bar = 0
131
-
132
- # NUTS run diagnostics
133
- # number of tree nodes created each NUTS iteration
134
- self._num_tree_node = 0
135
-
136
- # Create lists to store NUTS run diagnostics
137
- self._create_run_diagnostic_attributes()
138
-
139
- #=========================================================================
140
- #============================== Properties ===============================
141
- #=========================================================================
142
- @property
143
- def max_depth(self):
144
- return self._max_depth
145
-
146
- @max_depth.setter
147
- def max_depth(self, value):
148
- if value is None:
149
- value = 15 # default value
150
- if not isinstance(value, int):
151
- raise TypeError('max_depth must be an integer.')
152
- if value < 0:
153
- raise ValueError('max_depth must be >= 0.')
154
- self._max_depth = value
155
-
156
- @property
157
- def step_size(self):
158
- return self._step_size
159
-
160
- @step_size.setter
161
- def step_size(self, value):
162
- if value is None:
163
- pass # NUTS will adapt the step size
164
-
165
- # step_size must be a positive float, raise error otherwise
166
- elif isinstance(value, bool)\
167
- or not isinstance(value, Number)\
168
- or value <= 0:
169
- raise TypeError('step_size must be a positive float or None.')
170
- self._step_size = value
171
-
172
- @property
173
- def opt_acc_rate(self):
174
- return self._opt_acc_rate
175
-
176
- @opt_acc_rate.setter
177
- def opt_acc_rate(self, value):
178
- if not isinstance(value, Number) or value <= 0 or value >= 1:
179
- raise ValueError('opt_acc_rate must be a float in (0, 1).')
180
- self._opt_acc_rate = value
181
-
182
- #=========================================================================
183
- #================== Implement methods required by Sampler =============
184
- #=========================================================================
185
- def validate_target(self):
186
- # Check if the target has logd and gradient methods
187
- try:
188
- current_target_logd, current_target_grad =\
189
- self._nuts_target(np.ones(self.dim))
190
- except:
191
- raise ValueError('Target must have logd and gradient methods.')
192
-
193
- def reinitialize(self):
194
- # Call the parent reset method
195
- super().reinitialize()
196
- # Reset NUTS run diagnostic attributes
197
- self._reset_run_diagnostic_attributes()
198
-
199
- def step(self):
200
- if isinstance(self._epsilon_bar, str) and self._epsilon_bar == "unset":
201
- self._epsilon_bar = self._epsilon
202
-
203
- # Convert current_point, logd, and grad to numpy arrays
204
- # if they are CUQIarray objects
205
- if isinstance(self.current_point, CUQIarray):
206
- self.current_point = self.current_point.to_numpy()
207
- if isinstance(self.current_target_logd, CUQIarray):
208
- self.current_target_logd = self.current_target_logd.to_numpy()
209
- if isinstance(self.current_target_grad, CUQIarray):
210
- self.current_target_grad = self.current_target_grad.to_numpy()
211
-
212
- # reset number of tree nodes for each iteration
213
- self._num_tree_node = 0
214
-
215
- # copy current point, logd, and grad in local variables
216
- point_k = self.current_point # initial position (parameters)
217
- logd_k = self.current_target_logd
218
- grad_k = self.current_target_grad # initial gradient
219
-
220
- # compute r_k and Hamiltonian
221
- r_k = self._Kfun(1, 'sample') # resample momentum vector
222
- Ham = logd_k - self._Kfun(r_k, 'eval') # Hamiltonian
223
-
224
- # slice variable
225
- log_u = Ham - np.random.exponential(1, size=1)
226
-
227
- # initialization
228
- j, s, n = 0, 1, 1
229
- point_minus, point_plus = point_k.copy(), point_k.copy()
230
- grad_minus, grad_plus = grad_k.copy(), grad_k.copy()
231
- r_minus, r_plus = r_k.copy(), r_k.copy()
232
-
233
- # run NUTS
234
- acc = 0
235
- while (s == 1) and (j <= self.max_depth):
236
- # sample a direction
237
- v = int(2*(np.random.rand() < 0.5)-1)
238
-
239
- # build tree: doubling procedure
240
- if (v == -1):
241
- point_minus, r_minus, grad_minus, _, _, _, \
242
- point_prime, logd_prime, grad_prime,\
243
- n_prime, s_prime, alpha, n_alpha = \
244
- self._BuildTree(point_minus, r_minus, grad_minus,
245
- Ham, log_u, v, j, self._epsilon)
246
- else:
247
- _, _, _, point_plus, r_plus, grad_plus, \
248
- point_prime, logd_prime, grad_prime,\
249
- n_prime, s_prime, alpha, n_alpha = \
250
- self._BuildTree(point_plus, r_plus, grad_plus,
251
- Ham, log_u, v, j, self._epsilon)
252
-
253
- # Metropolis step
254
- alpha2 = min(1, (n_prime/n)) #min(0, np.log(n_p) - np.log(n))
255
- if (s_prime == 1) and \
256
- (np.random.rand() <= alpha2) and \
257
- (not np.isnan(logd_prime)) and \
258
- (not np.isinf(logd_prime)):
259
- self.current_point = point_prime.copy()
260
- # copy if array, else assign if scalar
261
- self.current_target_logd = (
262
- logd_prime.copy()
263
- if isinstance(logd_prime, np.ndarray)
264
- else logd_prime
265
- )
266
- self.current_target_grad = grad_prime.copy()
267
- acc = 1
268
-
269
-
270
- # update number of particles, tree level, and stopping criterion
271
- n += n_prime
272
- dpoints = point_plus - point_minus
273
- s = s_prime *\
274
- int((dpoints @ r_minus.T) >= 0) * int((dpoints @ r_plus.T) >= 0)
275
- j += 1
276
- self._current_alpha_ratio = alpha/n_alpha
277
-
278
- # update run diagnostic attributes
279
- self._update_run_diagnostic_attributes(
280
- self._num_tree_node, self._epsilon, self._epsilon_bar)
281
-
282
- self._epsilon = self._epsilon_bar
283
- if np.isnan(self.current_target_logd):
284
- raise NameError('NaN potential func')
285
-
286
- return acc
287
-
288
- def tune(self, skip_len, update_count):
289
- """ adapt epsilon during burn-in using dual averaging"""
290
- if isinstance(self._epsilon_bar, str) and self._epsilon_bar == "unset":
291
- self._epsilon_bar = 1
292
-
293
- k = update_count+1
294
-
295
- # Fixed parameters that do not change during the run
296
- gamma, t_0, kappa = 0.05, 10, 0.75 # kappa in (0.5, 1]
297
-
298
- eta1 = 1/(k + t_0)
299
- self._H_bar = (1-eta1)*self._H_bar +\
300
- eta1*(self.opt_acc_rate - (self._current_alpha_ratio))
301
- self._epsilon = np.exp(self._mu - (np.sqrt(k)/gamma)*self._H_bar)
302
- eta = k**(-kappa)
303
- self._epsilon_bar =\
304
- np.exp(eta*np.log(self._epsilon) +(1-eta)*np.log(self._epsilon_bar))
305
-
306
- #=========================================================================
307
- def _nuts_target(self, x): # returns logposterior tuple evaluation-gradient
308
- return self.target.logd(x), self.target.gradient(x)
309
-
310
- #=========================================================================
311
- # auxiliary standard Gaussian PDF: kinetic energy function
312
- # d_log_2pi = d*np.log(2*np.pi)
313
- def _Kfun(self, r, flag):
314
- if flag == 'eval': # evaluate
315
- return 0.5*(r.T @ r) #+ d_log_2pi
316
- if flag == 'sample': # sample
317
- return np.random.standard_normal(size=self.dim)
318
-
319
- #=========================================================================
320
- def _FindGoodEpsilon(self, epsilon=1):
321
- point_k = self.current_point
322
- self.current_target_logd, self.current_target_grad = self._nuts_target(
323
- point_k)
324
- logd = self.current_target_logd
325
- grad = self.current_target_grad
326
-
327
- r = self._Kfun(1, 'sample') # resample a momentum
328
- Ham = logd - self._Kfun(r, 'eval') # initial Hamiltonian
329
- _, r_prime, logd_prime, grad_prime = self._Leapfrog(
330
- point_k, r, grad, epsilon)
331
-
332
- # trick to make sure the step is not huge, leading to infinite values of
333
- # the likelihood
334
- k = 1
335
- while np.isinf(logd_prime) or np.isinf(grad_prime).any():
336
- k *= 0.5
337
- _, r_prime, logd_prime, grad_prime = self._Leapfrog(
338
- point_k, r, grad, epsilon*k)
339
- epsilon = 0.5*k*epsilon
340
-
341
- # doubles/halves the value of epsilon until the accprob of the Langevin
342
- # proposal crosses 0.5
343
- Ham_prime = logd_prime - self._Kfun(r_prime, 'eval')
344
- log_ratio = Ham_prime - Ham
345
- a = 1 if log_ratio > np.log(0.5) else -1
346
- while (a*log_ratio > -a*np.log(2)):
347
- epsilon = (2**a)*epsilon
348
- _, r_prime, logd_prime, _ = self._Leapfrog(
349
- point_k, r, grad, epsilon)
350
- Ham_prime = logd_prime - self._Kfun(r_prime, 'eval')
351
- log_ratio = Ham_prime - Ham
352
- return epsilon
353
-
354
- #=========================================================================
355
- def _Leapfrog(self, point_old, r_old, grad_old, epsilon):
356
- # symplectic integrator: trajectories preserve phase space volumen
357
- r_new = r_old + 0.5*epsilon*grad_old # half-step
358
- point_new = point_old + epsilon*r_new # full-step
359
- logd_new, grad_new = self._nuts_target(point_new) # new gradient
360
- r_new += 0.5*epsilon*grad_new # half-step
361
- return point_new, r_new, logd_new, grad_new
362
-
363
- #=========================================================================
364
- def _BuildTree(
365
- self, point_k, r, grad, Ham, log_u, v, j, epsilon, Delta_max=1000):
366
- # Increment the number of tree nodes counter
367
- self._num_tree_node += 1
368
-
369
- if (j == 0): # base case
370
- # single leapfrog step in the direction v
371
- point_prime, r_prime, logd_prime, grad_prime = self._Leapfrog(
372
- point_k, r, grad, v*epsilon)
373
- Ham_prime = logd_prime - self._Kfun(r_prime, 'eval') # Hamiltonian
374
- # eval
375
- n_prime = int(log_u <= Ham_prime) # if particle is in the slice
376
- s_prime = int(log_u < Delta_max + Ham_prime) # check U-turn
377
- #
378
- diff_Ham = Ham_prime - Ham
379
-
380
- # Compute the acceptance probability
381
- # alpha_prime = min(1, np.exp(diff_Ham))
382
- # written in a stable way to avoid overflow when computing
383
- # exp(diff_Ham) for large values of diff_Ham
384
- alpha_prime = 1 if diff_Ham > 0 else np.exp(diff_Ham)
385
- n_alpha_prime = 1
386
- #
387
- point_minus, point_plus = point_prime, point_prime
388
- r_minus, r_plus = r_prime, r_prime
389
- grad_minus, grad_plus = grad_prime, grad_prime
390
- else:
391
- # recursion: build the left/right subtrees
392
- point_minus, r_minus, grad_minus, point_plus, r_plus, grad_plus, \
393
- point_prime, logd_prime, grad_prime,\
394
- n_prime, s_prime, alpha_prime, n_alpha_prime = \
395
- self._BuildTree(point_k, r, grad,
396
- Ham, log_u, v, j-1, epsilon)
397
- if (s_prime == 1): # do only if the stopping criteria does not
398
- # verify at the first subtree
399
- if (v == -1):
400
- point_minus, r_minus, grad_minus, _, _, _, \
401
- point_2prime, logd_2prime, grad_2prime,\
402
- n_2prime, s_2prime, alpha_2prime, n_alpha_2prime = \
403
- self._BuildTree(point_minus, r_minus, grad_minus,
404
- Ham, log_u, v, j-1, epsilon)
405
- else:
406
- _, _, _, point_plus, r_plus, grad_plus, \
407
- point_2prime, logd_2prime, grad_2prime,\
408
- n_2prime, s_2prime, alpha_2prime, n_alpha_2prime = \
409
- self._BuildTree(point_plus, r_plus, grad_plus,
410
- Ham, log_u, v, j-1, epsilon)
411
-
412
- # Metropolis step
413
- alpha2 = n_2prime / max(1, (n_prime + n_2prime))
414
- if (np.random.rand() <= alpha2):
415
- point_prime = point_2prime.copy()
416
- # copy if array, else assign if scalar
417
- logd_prime = (
418
- logd_2prime.copy()
419
- if isinstance(logd_2prime, np.ndarray)
420
- else logd_2prime
421
- )
422
- grad_prime = grad_2prime.copy()
423
-
424
- # update number of particles and stopping criterion
425
- alpha_prime += alpha_2prime
426
- n_alpha_prime += n_alpha_2prime
427
- dpoints = point_plus - point_minus
428
- s_prime = s_2prime *\
429
- int((dpoints@r_minus.T)>=0) * int((dpoints@r_plus.T)>=0)
430
- n_prime += n_2prime
431
-
432
- return point_minus, r_minus, grad_minus, point_plus, r_plus, grad_plus,\
433
- point_prime, logd_prime, grad_prime,\
434
- n_prime, s_prime, alpha_prime, n_alpha_prime
435
-
436
- #=========================================================================
437
- #======================== Diagnostic methods =============================
438
- #=========================================================================
439
-
440
- def _create_run_diagnostic_attributes(self):
441
- """A method to create attributes to store NUTS run diagnostic."""
442
- self._reset_run_diagnostic_attributes()
443
-
444
- def _reset_run_diagnostic_attributes(self):
445
- """A method to reset attributes to store NUTS run diagnostic."""
446
- # List to store number of tree nodes created each NUTS iteration
447
- self.num_tree_node_list = []
448
- # List of step size used in each NUTS iteration
449
- self.epsilon_list = []
450
- # List of burn-in step size suggestion during adaptation
451
- # only used when adaptation is done
452
- # remains fixed after adaptation (after burn-in)
453
- self.epsilon_bar_list = []
454
-
455
- def _update_run_diagnostic_attributes(self, n_tree, eps, eps_bar):
456
- """A method to update attributes to store NUTS run diagnostic."""
457
- # Store the number of tree nodes created in iteration k
458
- self.num_tree_node_list.append(n_tree)
459
- # Store the step size used in iteration k
460
- self.epsilon_list.append(eps)
461
- # Store the step size suggestion during adaptation in iteration k
462
- self.epsilon_bar_list.append(eps_bar)