CUQIpy 1.3.0.post0.dev298__py3-none-any.whl → 1.4.0.post0.dev61__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.
Files changed (59) hide show
  1. cuqi/__init__.py +1 -0
  2. cuqi/_version.py +3 -3
  3. cuqi/density/_density.py +9 -1
  4. cuqi/distribution/_distribution.py +24 -15
  5. cuqi/distribution/_joint_distribution.py +96 -11
  6. cuqi/distribution/_posterior.py +9 -0
  7. cuqi/experimental/__init__.py +1 -2
  8. cuqi/experimental/_recommender.py +4 -4
  9. cuqi/implicitprior/__init__.py +1 -1
  10. cuqi/implicitprior/_restorator.py +35 -1
  11. cuqi/legacy/__init__.py +2 -0
  12. cuqi/legacy/sampler/__init__.py +11 -0
  13. cuqi/legacy/sampler/_conjugate.py +55 -0
  14. cuqi/legacy/sampler/_conjugate_approx.py +52 -0
  15. cuqi/legacy/sampler/_cwmh.py +196 -0
  16. cuqi/legacy/sampler/_gibbs.py +231 -0
  17. cuqi/legacy/sampler/_hmc.py +335 -0
  18. cuqi/legacy/sampler/_langevin_algorithm.py +198 -0
  19. cuqi/legacy/sampler/_laplace_approximation.py +184 -0
  20. cuqi/legacy/sampler/_mh.py +190 -0
  21. cuqi/legacy/sampler/_pcn.py +244 -0
  22. cuqi/legacy/sampler/_rto.py +284 -0
  23. cuqi/legacy/sampler/_sampler.py +182 -0
  24. cuqi/likelihood/_likelihood.py +1 -1
  25. cuqi/model/_model.py +212 -77
  26. cuqi/pde/__init__.py +4 -0
  27. cuqi/pde/_observation_map.py +36 -0
  28. cuqi/pde/_pde.py +52 -21
  29. cuqi/problem/_problem.py +87 -80
  30. cuqi/sampler/__init__.py +120 -8
  31. cuqi/sampler/_conjugate.py +376 -35
  32. cuqi/sampler/_conjugate_approx.py +40 -16
  33. cuqi/sampler/_cwmh.py +132 -138
  34. cuqi/{experimental/mcmc → sampler}/_direct.py +1 -1
  35. cuqi/sampler/_gibbs.py +269 -130
  36. cuqi/sampler/_hmc.py +328 -201
  37. cuqi/sampler/_langevin_algorithm.py +282 -98
  38. cuqi/sampler/_laplace_approximation.py +87 -117
  39. cuqi/sampler/_mh.py +47 -157
  40. cuqi/sampler/_pcn.py +56 -211
  41. cuqi/sampler/_rto.py +206 -140
  42. cuqi/sampler/_sampler.py +540 -135
  43. {cuqipy-1.3.0.post0.dev298.dist-info → cuqipy-1.4.0.post0.dev61.dist-info}/METADATA +1 -1
  44. {cuqipy-1.3.0.post0.dev298.dist-info → cuqipy-1.4.0.post0.dev61.dist-info}/RECORD +47 -45
  45. cuqi/experimental/mcmc/__init__.py +0 -122
  46. cuqi/experimental/mcmc/_conjugate.py +0 -396
  47. cuqi/experimental/mcmc/_conjugate_approx.py +0 -76
  48. cuqi/experimental/mcmc/_cwmh.py +0 -190
  49. cuqi/experimental/mcmc/_gibbs.py +0 -374
  50. cuqi/experimental/mcmc/_hmc.py +0 -460
  51. cuqi/experimental/mcmc/_langevin_algorithm.py +0 -382
  52. cuqi/experimental/mcmc/_laplace_approximation.py +0 -154
  53. cuqi/experimental/mcmc/_mh.py +0 -80
  54. cuqi/experimental/mcmc/_pcn.py +0 -89
  55. cuqi/experimental/mcmc/_rto.py +0 -306
  56. cuqi/experimental/mcmc/_sampler.py +0 -564
  57. {cuqipy-1.3.0.post0.dev298.dist-info → cuqipy-1.4.0.post0.dev61.dist-info}/WHEEL +0 -0
  58. {cuqipy-1.3.0.post0.dev298.dist-info → cuqipy-1.4.0.post0.dev61.dist-info}/licenses/LICENSE +0 -0
  59. {cuqipy-1.3.0.post0.dev298.dist-info → cuqipy-1.4.0.post0.dev61.dist-info}/top_level.txt +0 -0
cuqi/sampler/_hmc.py CHANGED
@@ -1,42 +1,46 @@
1
1
  import numpy as np
2
+ import numpy as np
2
3
  from cuqi.sampler import Sampler
4
+ from cuqi.array import CUQIarray
5
+ from numbers import Number
3
6
 
4
-
5
- # another implementation is in https://github.com/mfouesneau/NUTS
6
7
  class NUTS(Sampler):
7
8
  """No-U-Turn Sampler (Hoffman and Gelman, 2014).
8
9
 
9
- Samples a distribution given its logpdf and gradient using a Hamiltonian Monte Carlo (HMC) algorithm with automatic parameter tuning.
10
+ Samples a distribution given its logpdf and gradient using a Hamiltonian
11
+ Monte Carlo (HMC) algorithm with automatic parameter tuning.
10
12
 
11
- For more details see: See Hoffman, M. D., & Gelman, A. (2014). The no-U-turn sampler: Adaptively setting path lengths in Hamiltonian Monte Carlo. Journal of Machine Learning Research, 15, 1593-1623.
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.
12
16
 
13
17
  Parameters
14
18
  ----------
15
-
16
19
  target : `cuqi.distribution.Distribution`
17
- The target distribution to sample. Must have logpdf and gradient method. Custom logpdfs and gradients are supported by using a :class:`cuqi.distribution.UserDefinedDistribution`.
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`.
18
23
 
19
- x0 : ndarray
20
- Initial parameters. *Optional*
24
+ initial_point : ndarray
25
+ Initial parameters. *Optional*. If not provided, the initial point is
26
+ an array of ones.
21
27
 
22
28
  max_depth : int
23
- Maximum depth of the tree.
29
+ Maximum depth of the tree >=0 and the default is 15.
24
30
 
25
- adapt_step_size : Bool or float
26
- Whether to adapt the step size.
27
- If True, the step size is adapted automatically.
28
- If False, the step size is fixed to the initially estimated value.
29
- If set to a scalar, the step size will be given by user and not adapted.
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.
30
34
 
31
35
  opt_acc_rate : float
32
36
  The optimal acceptance rate to reach if using adaptive step size.
33
- Suggested values are 0.6 (default) or 0.8 (as in stan).
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.
34
40
 
35
- callback : callable, *Optional*
36
- If set this function will be called after every sample.
37
- The signature of the callback function is `callback(sample, sample_index)`,
38
- where `sample` is the current sample and `sample_index` is the index of the sample.
39
- An example is shown in demos/demo31_callback.py.
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)`.
40
44
 
41
45
  Example
42
46
  -------
@@ -53,7 +57,11 @@ class NUTS(Sampler):
53
57
  sampler = cuqi.sampler.NUTS(target)
54
58
 
55
59
  # Sample
56
- samples = sampler.sample(10000, 5000)
60
+ sampler.warmup(5000)
61
+ sampler.sample(10000)
62
+
63
+ # Get samples
64
+ samples = sampler.get_samples()
57
65
 
58
66
  # Plot samples
59
67
  samples.plot_pair()
@@ -70,170 +78,234 @@ class NUTS(Sampler):
70
78
  sampler.epsilon_list
71
79
 
72
80
  # Suggested step size during adaptation (the value of this step size is
73
- # only used after adaptation). The suggested step size is None if
74
- # adaptation is not requested.
81
+ # only used after adaptation).
75
82
  sampler.epsilon_bar_list
76
83
 
77
- # Additionally, iterations' number can be accessed via
78
- sampler.iteration_list
79
-
80
84
  """
81
- def __init__(self, target, x0=None, max_depth=15, adapt_step_size=True, opt_acc_rate=0.6, **kwargs):
82
- super().__init__(target, x0=x0, **kwargs)
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
83
101
  self.max_depth = max_depth
84
- self.adapt_step_size = adapt_step_size
102
+ self.step_size = step_size
85
103
  self.opt_acc_rate = opt_acc_rate
86
- # if this flag is True, the samples and the burn-in will be returned
87
- # otherwise, the burn-in will be truncated
88
- self._return_burnin = False
89
104
 
90
- # NUTS run diagnostic
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
91
133
  # number of tree nodes created each NUTS iteration
92
134
  self._num_tree_node = 0
135
+
93
136
  # Create lists to store NUTS run diagnostics
94
137
  self._create_run_diagnostic_attributes()
95
138
 
96
- def _create_run_diagnostic_attributes(self):
97
- """A method to create attributes to store NUTS run diagnostic."""
98
- self._reset_run_diagnostic_attributes()
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
99
181
 
100
- def _reset_run_diagnostic_attributes(self):
101
- """A method to reset attributes to store NUTS run diagnostic."""
102
- # NUTS iterations
103
- self.iteration_list = []
104
- # List to store number of tree nodes created each NUTS iteration
105
- self.num_tree_node_list = []
106
- # List of step size used in each NUTS iteration
107
- self.epsilon_list = []
108
- # List of burn-in step size suggestion during adaptation
109
- # only used when adaptation is done
110
- # remains fixed after adaptation (after burn-in)
111
- self.epsilon_bar_list = []
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()
112
198
 
113
- def _update_run_diagnostic_attributes(self, k, n_tree, eps, eps_bar):
114
- """A method to update attributes to store NUTS run diagnostic."""
115
- # Store the current iteration number k
116
- self.iteration_list.append(k)
117
- # Store the number of tree nodes created in iteration k
118
- self.num_tree_node_list.append(n_tree)
119
- # Store the step size used in iteration k
120
- self.epsilon_list.append(eps)
121
- # Store the step size suggestion during adaptation in iteration k
122
- self.epsilon_bar_list.append(eps_bar)
199
+ def step(self):
200
+ if isinstance(self._epsilon_bar, str) and self._epsilon_bar == "unset":
201
+ self._epsilon_bar = self._epsilon
123
202
 
124
- def _nuts_target(self, x): # returns logposterior tuple evaluation-gradient
125
- return self.target.logd(x), self.target.gradient(x)
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()
126
211
 
127
- def _sample_adapt(self, N, Nb):
128
- return self._sample(N, Nb)
212
+ # reset number of tree nodes for each iteration
213
+ self._num_tree_node = 0
129
214
 
130
- def _sample(self, N, Nb):
131
- # Reset run diagnostic attributes
132
- self._reset_run_diagnostic_attributes()
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
133
219
 
134
- if self.adapt_step_size is True and Nb == 0:
135
- raise ValueError("Adaptive step size is True but number of burn-in steps is 0. Please set Nb > 0.")
136
-
137
- # Allocation
138
- Ns = Nb+N # total number of chains
139
- theta = np.empty((self.dim, Ns))
140
- joint_eval = np.empty(Ns)
141
- step_sizes = np.empty(Ns)
142
-
143
- # Initial state
144
- theta[:, 0] = self.x0
145
- joint_eval[0], grad = self._nuts_target(self.x0)
146
-
147
- # Step size variables
148
- epsilon, epsilon_bar = None, None
149
-
150
- # parameters dual averaging
151
- if (self.adapt_step_size == True):
152
- epsilon = self._FindGoodEpsilon(theta[:, 0], joint_eval[0], grad)
153
- mu = np.log(10*epsilon)
154
- gamma, t_0, kappa = 0.05, 10, 0.75 # kappa in (0.5, 1]
155
- epsilon_bar, H_bar = 1, 0
156
- delta = self.opt_acc_rate # https://mc-stan.org/docs/2_18/reference-manual/hmc-algorithm-parameters.html
157
- step_sizes[0] = epsilon
158
- elif (self.adapt_step_size == False):
159
- epsilon = self._FindGoodEpsilon(theta[:, 0], joint_eval[0], grad)
160
- else:
161
- epsilon = self.adapt_step_size # if scalar then user specifies the step size
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()
162
232
 
163
233
  # run NUTS
164
- for k in range(1, Ns):
165
- # reset number of tree nodes for each iteration
166
- self._num_tree_node = 0
167
-
168
- theta_k, joint_k = theta[:, k-1], joint_eval[k-1] # initial position (parameters)
169
- r_k = self._Kfun(1, 'sample') # resample momentum vector
170
- Ham = joint_k - self._Kfun(r_k, 'eval') # Hamiltonian
171
-
172
- # slice variable
173
- log_u = Ham - np.random.exponential(1, size=1) # u = np.log(np.random.uniform(0, np.exp(H)))
174
-
175
- # initialization
176
- j, s, n = 0, 1, 1
177
- theta[:, k], joint_eval[k] = theta_k, joint_k
178
- theta_minus, theta_plus = np.copy(theta_k), np.copy(theta_k)
179
- grad_minus, grad_plus = np.copy(grad), np.copy(grad)
180
- r_minus, r_plus = np.copy(r_k), np.copy(r_k)
181
-
182
- # run NUTS
183
- while (s == 1) and (j <= self.max_depth):
184
- # sample a direction
185
- v = int(2*(np.random.rand() < 0.5)-1)
186
-
187
- # build tree: doubling procedure
188
- if (v == -1):
189
- theta_minus, r_minus, grad_minus, _, _, _, \
190
- theta_prime, joint_prime, grad_prime, n_prime, s_prime, alpha, n_alpha = \
191
- self._BuildTree(theta_minus, r_minus, grad_minus, Ham, log_u, v, j, epsilon)
192
- else:
193
- _, _, _, theta_plus, r_plus, grad_plus, \
194
- theta_prime, joint_prime, grad_prime, n_prime, s_prime, alpha, n_alpha = \
195
- self._BuildTree(theta_plus, r_plus, grad_plus, Ham, log_u, v, j, epsilon)
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')
196
285
 
197
- # Metropolis step
198
- alpha2 = min(1, (n_prime/n)) #min(0, np.log(n_p) - np.log(n))
199
- if (s_prime == 1) and (np.random.rand() <= alpha2):
200
- theta[:, k] = theta_prime
201
- joint_eval[k] = joint_prime
202
- grad = np.copy(grad_prime)
203
-
204
- # update number of particles, tree level, and stopping criterion
205
- n += n_prime
206
- dtheta = theta_plus - theta_minus
207
- s = s_prime * int((dtheta @ r_minus.T) >= 0) * int((dtheta @ r_plus.T) >= 0)
208
- j += 1
209
-
210
- # update run diagnostic attributes
211
- self._update_run_diagnostic_attributes(
212
- k, self._num_tree_node, epsilon, epsilon_bar)
213
-
214
- # adapt epsilon during burn-in using dual averaging
215
- if (k <= Nb) and (self.adapt_step_size == True):
216
- eta1 = 1/(k + t_0)
217
- H_bar = (1-eta1)*H_bar + eta1*(delta - (alpha/n_alpha))
218
- epsilon = np.exp(mu - (np.sqrt(k)/gamma)*H_bar)
219
- eta = k**(-kappa)
220
- epsilon_bar = np.exp(eta*np.log(epsilon) + (1-eta)*np.log(epsilon_bar))
221
- elif (k == Nb+1) and (self.adapt_step_size == True):
222
- epsilon = epsilon_bar # fix epsilon after burn-in
223
- step_sizes[k] = epsilon
224
-
225
- # msg
226
- self._print_progress(k+1, Ns) #k+1 is the sample number, k is index assuming x0 is the first sample
227
- self._call_callback(theta[:, k], k)
228
-
229
- if np.isnan(joint_eval[k]):
230
- raise NameError('NaN potential func')
231
-
232
- # apply burn-in
233
- if not self._return_burnin:
234
- theta = theta[:, Nb:]
235
- joint_eval = joint_eval[Nb:]
236
- return theta, joint_eval, step_sizes
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)
237
309
 
238
310
  #=========================================================================
239
311
  # auxiliary standard Gaussian PDF: kinetic energy function
@@ -245,48 +317,61 @@ class NUTS(Sampler):
245
317
  return np.random.standard_normal(size=self.dim)
246
318
 
247
319
  #=========================================================================
248
- def _FindGoodEpsilon(self, theta, joint, grad, epsilon=1):
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
+
249
327
  r = self._Kfun(1, 'sample') # resample a momentum
250
- Ham = joint - self._Kfun(r, 'eval') # initial Hamiltonian
251
- _, r_prime, joint_prime, grad_prime = self._Leapfrog(theta, r, grad, epsilon)
328
+ Ham = logd - self._Kfun(r, 'eval') # initial Hamiltonian
329
+ _, r_prime, logd_prime, grad_prime = self._Leapfrog(
330
+ point_k, r, grad, epsilon)
252
331
 
253
- # trick to make sure the step is not huge, leading to infinite values of the likelihood
332
+ # trick to make sure the step is not huge, leading to infinite values of
333
+ # the likelihood
254
334
  k = 1
255
- while np.isinf(joint_prime) or np.isinf(grad_prime).any():
335
+ while np.isinf(logd_prime) or np.isinf(grad_prime).any():
256
336
  k *= 0.5
257
- _, r_prime, joint_prime, grad_prime = self._Leapfrog(theta, r, grad, epsilon*k)
337
+ _, r_prime, logd_prime, grad_prime = self._Leapfrog(
338
+ point_k, r, grad, epsilon*k)
258
339
  epsilon = 0.5*k*epsilon
259
340
 
260
- # doubles/halves the value of epsilon until the accprob of the Langevin proposal crosses 0.5
261
- Ham_prime = joint_prime - self._Kfun(r_prime, 'eval')
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')
262
344
  log_ratio = Ham_prime - Ham
263
345
  a = 1 if log_ratio > np.log(0.5) else -1
264
346
  while (a*log_ratio > -a*np.log(2)):
265
347
  epsilon = (2**a)*epsilon
266
- _, r_prime, joint_prime, _ = self._Leapfrog(theta, r, grad, epsilon)
267
- Ham_prime = joint_prime - self._Kfun(r_prime, 'eval')
348
+ _, r_prime, logd_prime, _ = self._Leapfrog(
349
+ point_k, r, grad, epsilon)
350
+ Ham_prime = logd_prime - self._Kfun(r_prime, 'eval')
268
351
  log_ratio = Ham_prime - Ham
269
352
  return epsilon
270
353
 
271
354
  #=========================================================================
272
- def _Leapfrog(self, theta_old, r_old, grad_old, epsilon):
355
+ def _Leapfrog(self, point_old, r_old, grad_old, epsilon):
273
356
  # symplectic integrator: trajectories preserve phase space volumen
274
357
  r_new = r_old + 0.5*epsilon*grad_old # half-step
275
- theta_new = theta_old + epsilon*r_new # full-step
276
- joint_new, grad_new = self._nuts_target(theta_new) # new gradient
358
+ point_new = point_old + epsilon*r_new # full-step
359
+ logd_new, grad_new = self._nuts_target(point_new) # new gradient
277
360
  r_new += 0.5*epsilon*grad_new # half-step
278
- return theta_new, r_new, joint_new, grad_new
361
+ return point_new, r_new, logd_new, grad_new
279
362
 
280
363
  #=========================================================================
281
- # @functools.lru_cache(maxsize=128)
282
- def _BuildTree(self, theta, r, grad, Ham, log_u, v, j, epsilon, Delta_max=1000):
364
+ def _BuildTree(
365
+ self, point_k, r, grad, Ham, log_u, v, j, epsilon, Delta_max=1000):
283
366
  # Increment the number of tree nodes counter
284
367
  self._num_tree_node += 1
285
368
 
286
369
  if (j == 0): # base case
287
370
  # single leapfrog step in the direction v
288
- theta_prime, r_prime, joint_prime, grad_prime = self._Leapfrog(theta, r, grad, v*epsilon)
289
- Ham_prime = joint_prime - self._Kfun(r_prime, 'eval') # Hamiltonian eval
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
290
375
  n_prime = int(log_u <= Ham_prime) # if particle is in the slice
291
376
  s_prime = int(log_u < Delta_max + Ham_prime) # check U-turn
292
377
  #
@@ -299,37 +384,79 @@ class NUTS(Sampler):
299
384
  alpha_prime = 1 if diff_Ham > 0 else np.exp(diff_Ham)
300
385
  n_alpha_prime = 1
301
386
  #
302
- theta_minus, theta_plus = theta_prime, theta_prime
387
+ point_minus, point_plus = point_prime, point_prime
303
388
  r_minus, r_plus = r_prime, r_prime
304
389
  grad_minus, grad_plus = grad_prime, grad_prime
305
390
  else:
306
391
  # recursion: build the left/right subtrees
307
- theta_minus, r_minus, grad_minus, theta_plus, r_plus, grad_plus, \
308
- theta_prime, joint_prime, grad_prime, n_prime, s_prime, alpha_prime, n_alpha_prime = \
309
- self._BuildTree(theta, r, grad, Ham, log_u, v, j-1, epsilon)
310
- if (s_prime == 1): # do only if the stopping criteria does not verify at the first subtree
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
311
399
  if (v == -1):
312
- theta_minus, r_minus, grad_minus, _, _, _, \
313
- theta_2prime, joint_2prime, grad_2prime, n_2prime, s_2prime, alpha_2prime, n_alpha_2prime = \
314
- self._BuildTree(theta_minus, r_minus, grad_minus, Ham, log_u, v, j-1, epsilon)
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)
315
405
  else:
316
- _, _, _, theta_plus, r_plus, grad_plus, \
317
- theta_2prime, joint_2prime, grad_2prime, n_2prime, s_2prime, alpha_2prime, n_alpha_2prime = \
318
- self._BuildTree(theta_plus, r_plus, grad_plus, Ham, log_u, v, j-1, epsilon)
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)
319
411
 
320
412
  # Metropolis step
321
413
  alpha2 = n_2prime / max(1, (n_prime + n_2prime))
322
414
  if (np.random.rand() <= alpha2):
323
- theta_prime = np.copy(theta_2prime)
324
- joint_prime = np.copy(joint_2prime)
325
- grad_prime = np.copy(grad_2prime)
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()
326
423
 
327
424
  # update number of particles and stopping criterion
328
425
  alpha_prime += alpha_2prime
329
426
  n_alpha_prime += n_alpha_2prime
330
- dtheta = theta_plus - theta_minus
331
- s_prime = s_2prime * int((dtheta@r_minus.T)>=0) * int((dtheta@r_plus.T)>=0)
427
+ dpoints = point_plus - point_minus
428
+ s_prime = s_2prime *\
429
+ int((dpoints@r_minus.T)>=0) * int((dpoints@r_plus.T)>=0)
332
430
  n_prime += n_2prime
333
- return theta_minus, r_minus, grad_minus, theta_plus, r_plus, grad_plus, \
334
- theta_prime, joint_prime, grad_prime, n_prime, s_prime, alpha_prime, n_alpha_prime
335
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)