bamojax 0.4.2__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.
- bamojax/__init__.py +2 -0
- bamojax/_version.py +21 -0
- bamojax/base.py +632 -0
- bamojax/examples/__init__.py +0 -0
- bamojax/gwishart/__init__.py +6 -0
- bamojax/gwishart/gwishart.py +361 -0
- bamojax/inference.py +794 -0
- bamojax/marginal_likelihoods/__init__.py +0 -0
- bamojax/marginal_likelihoods/bayesian_quadrature.py +55 -0
- bamojax/marginal_likelihoods/bridge_sampling.py +180 -0
- bamojax/marginal_likelihoods/importance_sampling.py +82 -0
- bamojax/marginal_likelihoods/laplace.py +40 -0
- bamojax/marginal_likelihoods/naive_monte_carlo.py +55 -0
- bamojax/marginal_likelihoods/thames.py +122 -0
- bamojax/marginal_likelihoods/utility.py +54 -0
- bamojax/model_search/__init__.py +1 -0
- bamojax/model_search/anchored_rjsmc.py +236 -0
- bamojax/modified_blackjax/__init__.py +0 -0
- bamojax/modified_blackjax/modified_elliptical_slice_nd.py +323 -0
- bamojax/modified_blackjax/more_proposals.py +66 -0
- bamojax/more_distributions.py +556 -0
- bamojax/samplers.py +435 -0
- bamojax-0.4.2.dist-info/METADATA +264 -0
- bamojax-0.4.2.dist-info/RECORD +27 -0
- bamojax-0.4.2.dist-info/WHEEL +5 -0
- bamojax-0.4.2.dist-info/licenses/LICENSE +21 -0
- bamojax-0.4.2.dist-info/top_level.txt +1 -0
bamojax/__init__.py
ADDED
bamojax/_version.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
|
|
2
|
+
# This file was generated by 'versioneer.py' (0.29) from
|
|
3
|
+
# revision-control system data, or from the parent directory name of an
|
|
4
|
+
# unpacked source archive. Distribution tarballs contain a pre-generated copy
|
|
5
|
+
# of this file.
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
version_json = '''
|
|
10
|
+
{
|
|
11
|
+
"date": "2026-10-26T21:35:56+0100",
|
|
12
|
+
"dirty": false,
|
|
13
|
+
"error": null,
|
|
14
|
+
"full-revisionid": "7b6576685f711fc94c568bb32813d45e67e2a861",
|
|
15
|
+
"version": "0.4.2"
|
|
16
|
+
}
|
|
17
|
+
''' # END VERSION_JSON
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_versions():
|
|
21
|
+
return json.loads(version_json)
|
bamojax/base.py
ADDED
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
from jaxtyping import Array, Union
|
|
2
|
+
import jax
|
|
3
|
+
import jax.numpy as jnp
|
|
4
|
+
|
|
5
|
+
import jax.random as jrnd
|
|
6
|
+
from typing import Tuple, Callable
|
|
7
|
+
|
|
8
|
+
from numpyro.distributions import Distribution, TransformedDistribution
|
|
9
|
+
from numpyro.distributions.transforms import Transform as Bijector
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Node:
|
|
14
|
+
r""" The essential element of any Bayesian model is the variable, represented by a node in a DAG.
|
|
15
|
+
|
|
16
|
+
Nodes can consist of stochastic or deterministic variables, and can be observed or latent.
|
|
17
|
+
|
|
18
|
+
Hyperparameters of a model are implicitly observed, deterministic nodes.
|
|
19
|
+
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, name: str = 'root',
|
|
23
|
+
observations: Array = None,
|
|
24
|
+
distribution: Union[Distribution, Bijector] = None,
|
|
25
|
+
parents = None,
|
|
26
|
+
link_fn: Callable = None,
|
|
27
|
+
shape: Union[Tuple, int] = None,
|
|
28
|
+
bijector: Bijector = None):
|
|
29
|
+
self.name = name
|
|
30
|
+
|
|
31
|
+
if shape is None:
|
|
32
|
+
shape = ( )
|
|
33
|
+
self.shape = shape
|
|
34
|
+
if bijector is not None:
|
|
35
|
+
self.bijector = bijector
|
|
36
|
+
if observations is not None:
|
|
37
|
+
observations = jnp.asarray(observations) if jnp.isscalar(observations) else observations
|
|
38
|
+
self.observations = observations
|
|
39
|
+
if distribution is not None:
|
|
40
|
+
self.distribution = distribution
|
|
41
|
+
self.parents = {}
|
|
42
|
+
if parents is not None:
|
|
43
|
+
for param, parent in parents.items():
|
|
44
|
+
self.add_parent(param, parent)
|
|
45
|
+
if link_fn is None:
|
|
46
|
+
def identity(**kwargs):
|
|
47
|
+
return kwargs
|
|
48
|
+
link_fn = identity
|
|
49
|
+
self.link_fn = link_fn
|
|
50
|
+
|
|
51
|
+
#
|
|
52
|
+
def is_observed(self) -> bool:
|
|
53
|
+
""" Check if a node is an observed variable.
|
|
54
|
+
|
|
55
|
+
"""
|
|
56
|
+
return hasattr(self, 'observations')
|
|
57
|
+
|
|
58
|
+
#
|
|
59
|
+
def is_stochastic(self) -> bool:
|
|
60
|
+
""" Check whether a node is stochastic or deterministic.
|
|
61
|
+
|
|
62
|
+
"""
|
|
63
|
+
return hasattr(self, 'distribution')
|
|
64
|
+
|
|
65
|
+
#
|
|
66
|
+
def is_root(self) -> bool:
|
|
67
|
+
""" Check whether a node is a root node.
|
|
68
|
+
|
|
69
|
+
"""
|
|
70
|
+
return not hasattr(self, 'parents') or len(self.parents) == 0
|
|
71
|
+
|
|
72
|
+
#
|
|
73
|
+
def add_parent(self, param, node):
|
|
74
|
+
""" Add a parent node.
|
|
75
|
+
|
|
76
|
+
"""
|
|
77
|
+
assert isinstance(node, Node)
|
|
78
|
+
self.parents[param] = node
|
|
79
|
+
|
|
80
|
+
#
|
|
81
|
+
def set_step_fn(self, step_fn):
|
|
82
|
+
self.step_fn = step_fn
|
|
83
|
+
|
|
84
|
+
#
|
|
85
|
+
def set_step_fn_parameter(self, step_fn_params):
|
|
86
|
+
self.step_fn_params = step_fn_params
|
|
87
|
+
|
|
88
|
+
#
|
|
89
|
+
def is_leaf(self):
|
|
90
|
+
""" Check whether a node is observed and has parents.
|
|
91
|
+
|
|
92
|
+
"""
|
|
93
|
+
return hasattr(self, 'parents') and self.is_observed()
|
|
94
|
+
|
|
95
|
+
#
|
|
96
|
+
def get_distribution(self, state: dict = None, minibatch: dict = None) -> Distribution:
|
|
97
|
+
r""" Derives the parametrized distribution p(node | Parents=x), where x is derived from the state object.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
state: Current assignment of (parent) values.
|
|
101
|
+
minibatch: A additional set of assigned variables, useful for out-of-sample predictions.
|
|
102
|
+
Returns:
|
|
103
|
+
An instantiated distrax distribution object.
|
|
104
|
+
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
# Root-level nodes can be defined as instantiated numpyro distributions.
|
|
108
|
+
if isinstance(self.distribution, Distribution):
|
|
109
|
+
return self.distribution
|
|
110
|
+
|
|
111
|
+
if minibatch is None:
|
|
112
|
+
minibatch = {}
|
|
113
|
+
|
|
114
|
+
# Otherwise the distribution is instantiated from the state.
|
|
115
|
+
parent_values = {}
|
|
116
|
+
for parent_name, parent_node in self.parents.items():
|
|
117
|
+
if parent_node in state:
|
|
118
|
+
parent_values[parent_name] = state[parent_node]
|
|
119
|
+
else:
|
|
120
|
+
if parent_name in minibatch:
|
|
121
|
+
parent_values[parent_name] = minibatch[parent_name]
|
|
122
|
+
else:
|
|
123
|
+
parent_values[parent_name] = self.parents[parent_name].observations
|
|
124
|
+
|
|
125
|
+
transformed_parents = self.link_fn(**parent_values)
|
|
126
|
+
if hasattr(self, 'bijector'):
|
|
127
|
+
return TransformedDistribution(self.distribution(**transformed_parents), self.bijector)
|
|
128
|
+
else:
|
|
129
|
+
return self.distribution(**transformed_parents)
|
|
130
|
+
|
|
131
|
+
#
|
|
132
|
+
def __repr__(self) -> str:
|
|
133
|
+
return f'{self.name}'
|
|
134
|
+
|
|
135
|
+
#
|
|
136
|
+
def __hash__(self):
|
|
137
|
+
return hash((self.name))
|
|
138
|
+
|
|
139
|
+
#
|
|
140
|
+
def __eq__(self, other):
|
|
141
|
+
# Nodes compare equal to other nodes, and to their own name. Because __hash__ is the
|
|
142
|
+
# hash of the name, this lets a Node look up entries in dicts keyed by variable name.
|
|
143
|
+
if isinstance(other, Node):
|
|
144
|
+
return self.name == other.name
|
|
145
|
+
if isinstance(other, str):
|
|
146
|
+
return self.name == other
|
|
147
|
+
raise NotImplementedError
|
|
148
|
+
|
|
149
|
+
#
|
|
150
|
+
|
|
151
|
+
#
|
|
152
|
+
class Model:
|
|
153
|
+
r""" A Bayesian model is represented as a directed acyclic graph, in which nodes are associated with random variables.
|
|
154
|
+
|
|
155
|
+
Typical use:
|
|
156
|
+
|
|
157
|
+
model = Model('model name')
|
|
158
|
+
_ = model.add_node('x', observations=...)
|
|
159
|
+
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
def __init__(self, name='Bayesian model', verbose=False):
|
|
163
|
+
self.name = name
|
|
164
|
+
self.nodes = {}
|
|
165
|
+
self.root_nodes = list()
|
|
166
|
+
self.leaf_nodes = list()
|
|
167
|
+
self.children = dict()
|
|
168
|
+
self.parents = dict()
|
|
169
|
+
self.verbose = verbose
|
|
170
|
+
|
|
171
|
+
#
|
|
172
|
+
def add_node(self,
|
|
173
|
+
name: str = 'root',
|
|
174
|
+
distribution: Union[Distribution, Bijector] = None,
|
|
175
|
+
observations: Array = None,
|
|
176
|
+
parents: dict = None,
|
|
177
|
+
link_fn: Callable = None,
|
|
178
|
+
shape: Union[Tuple, int] = None,
|
|
179
|
+
bijector: Bijector = None) -> Node:
|
|
180
|
+
r""" Adds a node to the Bayesian model DAG
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
name: The name of the variable.
|
|
184
|
+
distribution: The distrax distribution of the variable given its (transformed) parents.
|
|
185
|
+
observations: If the node is observed; the actual observations.
|
|
186
|
+
parents: The nodes that this node depends on.
|
|
187
|
+
link_fn: A link function combining the inputs to form the input to the corresponding distrax distribution.
|
|
188
|
+
shape: The dimensions of the variable.
|
|
189
|
+
bijector: A bijector can be passed to transform variables.
|
|
190
|
+
Returns:
|
|
191
|
+
New node
|
|
192
|
+
|
|
193
|
+
"""
|
|
194
|
+
if parents is not None:
|
|
195
|
+
new_parents = {}
|
|
196
|
+
for parent_name, parent in parents.items():
|
|
197
|
+
if not isinstance(parent, Node):
|
|
198
|
+
# Parent is numeric
|
|
199
|
+
parent_node = self.add_node(name=f'{parent_name}_{name}', observations=parent)
|
|
200
|
+
new_parents[parent_name] = parent_node
|
|
201
|
+
else:
|
|
202
|
+
new_parents[parent_name] = parent
|
|
203
|
+
parents = new_parents
|
|
204
|
+
new_node = Node(name=name, distribution=distribution, observations=observations, parents=parents, link_fn=link_fn, shape=shape, bijector=bijector)
|
|
205
|
+
self.nodes[name] = new_node
|
|
206
|
+
if self.verbose: print(f'Adding node ({name})')
|
|
207
|
+
if parents is not None:
|
|
208
|
+
for parent in parents.values():
|
|
209
|
+
self.add_edge(parent, new_node)
|
|
210
|
+
if new_node.is_root():
|
|
211
|
+
self.root_nodes.append(new_node)
|
|
212
|
+
if new_node.is_leaf():
|
|
213
|
+
self.leaf_nodes.append(new_node)
|
|
214
|
+
return new_node
|
|
215
|
+
|
|
216
|
+
#
|
|
217
|
+
def add_edge(self, from_node, to_node):
|
|
218
|
+
r""" Store the dependency between two nodes.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
from_node: source node
|
|
222
|
+
to_node: target node
|
|
223
|
+
|
|
224
|
+
"""
|
|
225
|
+
if self.verbose: print(f'Add edge ({from_node}) -> ({to_node})')
|
|
226
|
+
if not from_node in self.children:
|
|
227
|
+
self.children[from_node] = set()
|
|
228
|
+
self.children[from_node].add(to_node)
|
|
229
|
+
if not to_node in self.parents:
|
|
230
|
+
self.parents[to_node] = set()
|
|
231
|
+
self.parents[to_node].add(from_node)
|
|
232
|
+
|
|
233
|
+
#
|
|
234
|
+
def get_children(self, node):
|
|
235
|
+
""" Returns the children of a node.
|
|
236
|
+
|
|
237
|
+
"""
|
|
238
|
+
if node in self.children:
|
|
239
|
+
return self.children[node]
|
|
240
|
+
return []
|
|
241
|
+
|
|
242
|
+
#
|
|
243
|
+
def get_parents(self, node):
|
|
244
|
+
""" Returns the parents of a node.
|
|
245
|
+
|
|
246
|
+
"""
|
|
247
|
+
if node in self.parents:
|
|
248
|
+
return self.parents[node]
|
|
249
|
+
return []
|
|
250
|
+
|
|
251
|
+
#
|
|
252
|
+
def get_root_nodes(self):
|
|
253
|
+
""" Return all nodes that are roots.
|
|
254
|
+
|
|
255
|
+
"""
|
|
256
|
+
return self.root_nodes
|
|
257
|
+
|
|
258
|
+
#
|
|
259
|
+
def get_leaf_nodes(self):
|
|
260
|
+
""" Returns all nodes that are leaves.
|
|
261
|
+
|
|
262
|
+
"""
|
|
263
|
+
return {self.nodes[k]: self.nodes[k] for k in self.nodes.keys() - self.children.keys()}
|
|
264
|
+
|
|
265
|
+
#
|
|
266
|
+
def get_stochastic_nodes(self):
|
|
267
|
+
""" Returns all stochastic nodes.
|
|
268
|
+
|
|
269
|
+
"""
|
|
270
|
+
return {k: v for k, v in self.nodes.items() if v.is_stochastic()}
|
|
271
|
+
|
|
272
|
+
#
|
|
273
|
+
def get_latent_nodes(self):
|
|
274
|
+
""" Returns all latent nodes.
|
|
275
|
+
|
|
276
|
+
"""
|
|
277
|
+
return {k: v for k, v in self.nodes.items() if v.is_stochastic() and not v.is_observed()}
|
|
278
|
+
|
|
279
|
+
#
|
|
280
|
+
def logprior_fn(self) -> Callable:
|
|
281
|
+
r""" Returns a callable function that provides the log prior of the model given the current state of assigned variables.
|
|
282
|
+
|
|
283
|
+
"""
|
|
284
|
+
|
|
285
|
+
def logprior_fn_(state) -> float:
|
|
286
|
+
sorted_free_variables = [node for node in self.get_node_order() if node.is_stochastic() and not node.is_observed()]
|
|
287
|
+
logprob = 0.0
|
|
288
|
+
for node in sorted_free_variables:
|
|
289
|
+
if node.is_root():
|
|
290
|
+
logprob += jnp.sum(node.get_distribution().log_prob(state[node.name]))
|
|
291
|
+
else:
|
|
292
|
+
logprob += jnp.sum(node.get_distribution(state).log_prob(state[node.name]))
|
|
293
|
+
return logprob
|
|
294
|
+
|
|
295
|
+
#
|
|
296
|
+
return logprior_fn_
|
|
297
|
+
|
|
298
|
+
#
|
|
299
|
+
def loglikelihood_fn(self) -> Callable:
|
|
300
|
+
r""" Returns a callable function that provides the log likelihood of the model given the current state of assigned variables.
|
|
301
|
+
|
|
302
|
+
"""
|
|
303
|
+
|
|
304
|
+
def loglikelihood_fn_(state) -> float:
|
|
305
|
+
logprob = 0.0
|
|
306
|
+
for node in self.get_leaf_nodes():
|
|
307
|
+
if '__mask' in state:
|
|
308
|
+
element_wise_logp = node.get_distribution(state).log_prob(value=node.observations)
|
|
309
|
+
logprob += jnp.sum(jnp.nan_to_num(element_wise_logp, nan=0.0)*state['__mask'])
|
|
310
|
+
else:
|
|
311
|
+
logprob += jnp.sum(node.get_distribution(state).log_prob(value=node.observations))
|
|
312
|
+
return logprob
|
|
313
|
+
|
|
314
|
+
#
|
|
315
|
+
return loglikelihood_fn_
|
|
316
|
+
|
|
317
|
+
#
|
|
318
|
+
def batched_loglikelihood_fn(self) -> Callable:
|
|
319
|
+
r""" Batched loglikelihood function for stochastic-gradient methods.
|
|
320
|
+
|
|
321
|
+
Assumes `minibatch` is a dictionary containing a subset of observations for each observed leaf node.
|
|
322
|
+
|
|
323
|
+
"""
|
|
324
|
+
|
|
325
|
+
def loglikelihood_fn_(state, minibatch) -> float:
|
|
326
|
+
logprob = 0.0
|
|
327
|
+
for node in self.get_leaf_nodes():
|
|
328
|
+
logprob += jnp.sum(node.get_distribution(state, minibatch=minibatch).log_prob(value=minibatch[node]))
|
|
329
|
+
return logprob
|
|
330
|
+
|
|
331
|
+
#
|
|
332
|
+
return loglikelihood_fn_
|
|
333
|
+
|
|
334
|
+
#
|
|
335
|
+
def get_model_size(self) -> int:
|
|
336
|
+
r""" Returns the total dimensions of the model.
|
|
337
|
+
|
|
338
|
+
As node.distribution can be abstract, we create a concrete instantiation by drawing a sample from the prior and deriving the shape from this sample.
|
|
339
|
+
|
|
340
|
+
"""
|
|
341
|
+
|
|
342
|
+
draw = self.sample_prior(key=jrnd.PRNGKey(0))
|
|
343
|
+
size = jnp.sum(jnp.array([jnp.size(v) for v in draw.values()]))
|
|
344
|
+
return size
|
|
345
|
+
|
|
346
|
+
#
|
|
347
|
+
def get_node_order(self):
|
|
348
|
+
r""" Returns the latent variables in topological order; child nodes are always listed after their parents.
|
|
349
|
+
|
|
350
|
+
"""
|
|
351
|
+
if not hasattr(self, 'node_order'):
|
|
352
|
+
self.node_order = self.__get_topological_order()
|
|
353
|
+
return self.node_order
|
|
354
|
+
|
|
355
|
+
#
|
|
356
|
+
def __get_topological_order(self) -> list:
|
|
357
|
+
r""" Traverses the directed acyclic graph that defines the Bayesian model and returns its nodes in topological order
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
A list of sorted nodes.
|
|
361
|
+
|
|
362
|
+
"""
|
|
363
|
+
def traverse_dag_backwards(node: Node, visited_: dict, order: list):
|
|
364
|
+
if node in visited_:
|
|
365
|
+
return
|
|
366
|
+
visited_[node] = 1
|
|
367
|
+
|
|
368
|
+
for parent_node in node.parents.values():
|
|
369
|
+
if parent_node.is_stochastic():
|
|
370
|
+
traverse_dag_backwards(parent_node, visited_, order)
|
|
371
|
+
|
|
372
|
+
order.append(node)
|
|
373
|
+
|
|
374
|
+
#
|
|
375
|
+
order = []
|
|
376
|
+
visited = {}
|
|
377
|
+
leaves = self.get_leaf_nodes()
|
|
378
|
+
for leaf in leaves:
|
|
379
|
+
traverse_dag_backwards(leaf, visited, order)
|
|
380
|
+
return order
|
|
381
|
+
|
|
382
|
+
#
|
|
383
|
+
def sample_prior(self, key) -> dict:
|
|
384
|
+
r""" Samples from the (hierarchical) prior distribution of the model.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
key: Random seed
|
|
388
|
+
Returns:
|
|
389
|
+
A state dictionary with one random value for each node.
|
|
390
|
+
|
|
391
|
+
"""
|
|
392
|
+
state = dict()
|
|
393
|
+
sorted_free_variables = [node for node in self.get_node_order() if node.is_stochastic() and not node.is_observed()]
|
|
394
|
+
for node in sorted_free_variables:
|
|
395
|
+
key, subkey = jrnd.split(key)
|
|
396
|
+
if node.is_root():
|
|
397
|
+
dist = node.get_distribution()
|
|
398
|
+
else:
|
|
399
|
+
dist = node.get_distribution(state)
|
|
400
|
+
state[node.name] = dist.sample(key=subkey, sample_shape=node.shape)
|
|
401
|
+
return state
|
|
402
|
+
|
|
403
|
+
#
|
|
404
|
+
def sample_predictive(self, key, state: dict, input_variables: dict = None) -> dict:
|
|
405
|
+
r""" Sample stochastic observed nodes
|
|
406
|
+
|
|
407
|
+
Args:
|
|
408
|
+
key: PRNGKey
|
|
409
|
+
state: a draw from either $p(x)$ or $p(x \mid \cdot)$
|
|
410
|
+
input_variables: a dictionary with values for observed non-stochastic nodes
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
A dictionary which is the same as 'state' but appended with sampled values.
|
|
414
|
+
|
|
415
|
+
"""
|
|
416
|
+
state = dict(state) # do not modify the caller's dictionary in place
|
|
417
|
+
for node in self.get_leaf_nodes():
|
|
418
|
+
key, key_obs = jrnd.split(key)
|
|
419
|
+
distribution = node.get_distribution(state, minibatch=input_variables)
|
|
420
|
+
sample_shape = self._predictive_sample_shape(node, distribution, input_variables)
|
|
421
|
+
state[node.name] = distribution.sample(key=key_obs, sample_shape=sample_shape)
|
|
422
|
+
return state
|
|
423
|
+
|
|
424
|
+
#
|
|
425
|
+
@staticmethod
|
|
426
|
+
def _predictive_sample_shape(node, distribution, input_variables=None) -> tuple:
|
|
427
|
+
r""" Determine the sample_shape that makes a predictive draw look like the observed data.
|
|
428
|
+
|
|
429
|
+
If the user gave the node an explicit `shape`, that is used. If new input variables are
|
|
430
|
+
supplied, the shape follows from those inputs (via the distribution's batch shape). Otherwise,
|
|
431
|
+
i.i.d. observations from a distribution with scalar parameters need extra leading sample
|
|
432
|
+
dimensions so that the draw has the same shape as `node.observations`.
|
|
433
|
+
|
|
434
|
+
"""
|
|
435
|
+
if node.shape != ():
|
|
436
|
+
return node.shape
|
|
437
|
+
if input_variables or not node.is_observed():
|
|
438
|
+
return ()
|
|
439
|
+
try:
|
|
440
|
+
dist_shape = tuple(distribution.batch_shape) + tuple(distribution.event_shape)
|
|
441
|
+
except (AttributeError, NotImplementedError, ValueError):
|
|
442
|
+
return ()
|
|
443
|
+
obs_shape = tuple(jnp.shape(node.observations))
|
|
444
|
+
num_extra = len(obs_shape) - len(dist_shape)
|
|
445
|
+
return obs_shape[:num_extra] if num_extra > 0 else ()
|
|
446
|
+
|
|
447
|
+
#
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def sample_prior_predictive(self, key, prediction_options: dict = None) -> dict:
|
|
452
|
+
r""" Sample from the (hierarchical) prior predictive distribution of the model.
|
|
453
|
+
|
|
454
|
+
Args:
|
|
455
|
+
key: Random seed
|
|
456
|
+
prediction_options: A dictionary of options which can include minibatched input variables
|
|
457
|
+
|
|
458
|
+
Returns:
|
|
459
|
+
A dictionary with a random value for all stochastic observed nodes.
|
|
460
|
+
|
|
461
|
+
"""
|
|
462
|
+
key, key_latent = jrnd.split(key)
|
|
463
|
+
state = self.sample_prior(key_latent)
|
|
464
|
+
return self.sample_predictive(key, state, prediction_options)
|
|
465
|
+
|
|
466
|
+
#
|
|
467
|
+
def sample_posterior_predictive(self, key, state: dict, input_variables: dict = None) -> dict:
|
|
468
|
+
r""" Sample from the posterior predictive
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
key: Random key
|
|
472
|
+
state: A draw from the posterior
|
|
473
|
+
input_variables: Potential predictors and other non-stochastic observations
|
|
474
|
+
|
|
475
|
+
Returns:
|
|
476
|
+
A dictionary containing values for all stochastic observed nodes, conditioned on the observations.
|
|
477
|
+
|
|
478
|
+
"""
|
|
479
|
+
|
|
480
|
+
return self.sample_predictive(key, state, input_variables)
|
|
481
|
+
|
|
482
|
+
#
|
|
483
|
+
def print_gibbs(self):
|
|
484
|
+
r""" Print the structure of conditional distributions.
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
"""
|
|
488
|
+
|
|
489
|
+
print('Gibbs structure:')
|
|
490
|
+
sorted_free_variables = [node for node in self.get_node_order() if node.is_stochastic() and not node.is_observed()]
|
|
491
|
+
for node in sorted_free_variables:
|
|
492
|
+
# get prior density
|
|
493
|
+
if node.is_root():
|
|
494
|
+
prior = f'p({node})'
|
|
495
|
+
else:
|
|
496
|
+
parents = {p for p in self.get_parents(node)}
|
|
497
|
+
prior = f'p({node} | {", ".join([p.name for p in parents])})'
|
|
498
|
+
|
|
499
|
+
# get conditional density
|
|
500
|
+
conditionals = []
|
|
501
|
+
children = [c for c in self.get_children(node)]
|
|
502
|
+
for child in children:
|
|
503
|
+
co_parents = set()
|
|
504
|
+
for parent in self.get_parents(child):
|
|
505
|
+
if not parent == node or parent in co_parents:
|
|
506
|
+
co_parents.add(parent)
|
|
507
|
+
co_parents.add(node)
|
|
508
|
+
conditional = f'p({child} | {", ".join([str(p) for p in co_parents])})'
|
|
509
|
+
conditionals.append(conditional)
|
|
510
|
+
|
|
511
|
+
print(f'{str(node):20s}: {" ".join(conditionals)} {prior}')
|
|
512
|
+
|
|
513
|
+
#
|
|
514
|
+
|
|
515
|
+
#
|
|
516
|
+
class MetaModel():
|
|
517
|
+
r""" A meta-model is a collection of Bayesian models, which can be used for reversible jump MCMC.
|
|
518
|
+
"""
|
|
519
|
+
|
|
520
|
+
def __init__(self,
|
|
521
|
+
model_list):
|
|
522
|
+
self.model_list = model_list
|
|
523
|
+
self.M = len(model_list)
|
|
524
|
+
self.model_sizes = [model.get_model_size() for model in model_list]
|
|
525
|
+
self.indiv_latent_nodes = [set(model.get_latent_nodes().keys()) for model in model_list]
|
|
526
|
+
|
|
527
|
+
# find the number of auxiliary variables needed, as the largest difference in latent node sets between any two models
|
|
528
|
+
self.num_auxiliary = 0
|
|
529
|
+
for i in range(self.M):
|
|
530
|
+
for j in range(i+1, self.M):
|
|
531
|
+
diff_left = set.difference(self.indiv_latent_nodes[i], self.indiv_latent_nodes[j])
|
|
532
|
+
diff_right = set.difference(self.indiv_latent_nodes[j], self.indiv_latent_nodes[i])
|
|
533
|
+
max_diff = jnp.max(jnp.array([len(diff_left), len(diff_right)]))
|
|
534
|
+
if max_diff > self.num_auxiliary:
|
|
535
|
+
self.num_auxiliary = max_diff
|
|
536
|
+
|
|
537
|
+
self.latent_variables = set.union(*self.indiv_latent_nodes)
|
|
538
|
+
self.auxiliary_variables = [f'u_{i}' for i in range(self.num_auxiliary)]
|
|
539
|
+
self.meta_state = self.latent_variables.union(set(self.auxiliary_variables))
|
|
540
|
+
|
|
541
|
+
def make_model_sample_prior_fn(model_index):
|
|
542
|
+
"""Creates a function that samples from the prior of the specified model index."""
|
|
543
|
+
def fn(key):
|
|
544
|
+
sample = self.model_list[model_index].sample_prior(key)
|
|
545
|
+
all_latents = {k: jnp.nan for k in self.latent_variables}
|
|
546
|
+
return {**all_latents, **sample}
|
|
547
|
+
|
|
548
|
+
#
|
|
549
|
+
return fn
|
|
550
|
+
#
|
|
551
|
+
self.model_sample_prior_fns = [make_model_sample_prior_fn(i) for i in range(self.M)]
|
|
552
|
+
|
|
553
|
+
def make_model_sample_predictive_fn(model_index):
|
|
554
|
+
"""Creates a function that samples from the predictive distribution of the specified model index."""
|
|
555
|
+
def fn(input):
|
|
556
|
+
key, state, input_variables = input
|
|
557
|
+
return self.model_list[model_index].sample_predictive(key, state, input_variables)
|
|
558
|
+
|
|
559
|
+
#
|
|
560
|
+
return fn
|
|
561
|
+
|
|
562
|
+
#
|
|
563
|
+
self.model_sample_predictive_fns = [make_model_sample_predictive_fn(i) for i in range(self.M)]
|
|
564
|
+
|
|
565
|
+
def make_logprior_fn(model_index):
|
|
566
|
+
"""Creates a function that computes the log prior of the specified model index."""
|
|
567
|
+
def fn(state):
|
|
568
|
+
return self.model_list[model_index].logprior_fn()(state)
|
|
569
|
+
|
|
570
|
+
#
|
|
571
|
+
return fn
|
|
572
|
+
|
|
573
|
+
#
|
|
574
|
+
self.model_logprior_fns = [make_logprior_fn(i) for i in range(self.M)]
|
|
575
|
+
|
|
576
|
+
def make_loglikelihood_fn(model_index):
|
|
577
|
+
"""Creates a function that computes the log likelihood of the specified model index."""
|
|
578
|
+
def fn(state):
|
|
579
|
+
return self.model_list[model_index].loglikelihood_fn()(state)
|
|
580
|
+
|
|
581
|
+
#
|
|
582
|
+
return fn
|
|
583
|
+
|
|
584
|
+
#
|
|
585
|
+
self.model_loglikelihood_fns = [make_loglikelihood_fn(i) for i in range(self.M)]
|
|
586
|
+
|
|
587
|
+
#
|
|
588
|
+
def sample_prior(self, key) -> dict:
|
|
589
|
+
key_model, key_sample = jrnd.split(key)
|
|
590
|
+
model_index = jrnd.randint(key_model, shape=(), minval=0, maxval=self.M)
|
|
591
|
+
sample = jax.lax.switch(model_index, self.model_sample_prior_fns, operand=key_sample)
|
|
592
|
+
auxiliary_values = {f'u_{i}': jnp.nan for i in range(self.num_auxiliary)} # to keep the same pytree structure across models in reversible jump MCMC
|
|
593
|
+
return {'model_index': model_index, **sample, **auxiliary_values}
|
|
594
|
+
|
|
595
|
+
#
|
|
596
|
+
def sample_predictive(self, key, state: dict, input_variables: dict = None) -> dict:
|
|
597
|
+
return jax.lax.switch(state['model_index'], self.model_sample_predictive_fns,
|
|
598
|
+
operand=[key, state, input_variables])
|
|
599
|
+
|
|
600
|
+
#
|
|
601
|
+
def sample_prior_predictive(self, key, **input_variables) -> dict:
|
|
602
|
+
key_prior, key_predictive = jrnd.split(key)
|
|
603
|
+
prior_sample = self.sample_prior(key_prior)
|
|
604
|
+
return self.sample_predictive(key_predictive, prior_sample, input_variables=input_variables)
|
|
605
|
+
|
|
606
|
+
#
|
|
607
|
+
def sample_posterior_predictive(self, key, state: dict, input_variables: dict = None) -> dict:
|
|
608
|
+
return self.sample_predictive(key, state, input_variables=input_variables)
|
|
609
|
+
|
|
610
|
+
#
|
|
611
|
+
def logprior_fn(self) -> Callable:
|
|
612
|
+
|
|
613
|
+
def fn(state: dict) -> float:
|
|
614
|
+
"""Computes the log prior of the state."""
|
|
615
|
+
model_index = state['model_index']
|
|
616
|
+
return jax.lax.switch(model_index, self.model_logprior_fns, operand=state)
|
|
617
|
+
|
|
618
|
+
#
|
|
619
|
+
return fn
|
|
620
|
+
|
|
621
|
+
#
|
|
622
|
+
def loglikelihood_fn(self) -> Callable:
|
|
623
|
+
|
|
624
|
+
def fn(state: dict) -> float:
|
|
625
|
+
"""Computes the log likelihood of the state."""
|
|
626
|
+
model_index = state['model_index']
|
|
627
|
+
return jax.lax.switch(model_index, self.model_loglikelihood_fns, operand=state)
|
|
628
|
+
#
|
|
629
|
+
return fn
|
|
630
|
+
|
|
631
|
+
#
|
|
632
|
+
#
|
|
File without changes
|