You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/source/contributing/implementing_distribution.md
+26-24
Original file line number
Diff line number
Diff line change
@@ -1,11 +1,11 @@
1
1
(implementing-a-distribution)=
2
2
# Implementing a RandomVariable Distribution
3
3
4
-
This guide provides an overview on how to implement a distribution for PyMC version `>=4.0.0`.
4
+
This guide provides an overview on how to implement a distribution for PyMC.
5
5
It is designed for developers who wish to add a new distribution to the library.
6
-
Users will not be aware of all this complexity and should instead make use of helper methods such as `~pymc.DensityDist`.
6
+
Users will not be aware of all this complexity and should instead make use of helper methods such as `~pymc.CustomDist`.
7
7
8
-
PyMC {class}`~pymc.Distribution` builds on top of PyTensor's {class}`~pytensor.tensor.random.op.RandomVariable`, and implements `logp`, `logcdf` and `moment` methods as well as other initialization and validation helpers.
8
+
PyMC {class}`~pymc.Distribution` builds on top of PyTensor's {class}`~pytensor.tensor.random.op.RandomVariable`, and implements `logp`, `logcdf`, `icdf` and `moment` methods as well as other initialization and validation helpers.
9
9
Most notably `shape/dims/observed` kwargs, alternative parametrizations, and default `transform`.
10
10
11
11
Here is a summary check-list of the steps needed to implement a new distribution.
@@ -14,14 +14,14 @@ Each section will be expanded below:
14
14
1. Creating a new `RandomVariable``Op`
15
15
1. Implementing the corresponding `Distribution` class
16
16
1. Adding tests for the new `RandomVariable`
17
-
1. Adding tests for `logp` / `logcdf` and `moment` methods
17
+
1. Adding tests for `logp` / `logcdf`/ `icdf`and `moment` methods
18
18
1. Documenting the new `Distribution`.
19
19
20
20
This guide does not attempt to explain the rationale behind the `Distributions` current implementation, and details are provided only insofar as they help to implement new "standard" distributions.
21
21
22
22
## 1. Creating a new `RandomVariable``Op`
23
23
24
-
{class}`~pytensor.tensor.random.op.RandomVariable` are responsible for implementing the random sampling methods, which in version 3 of PyMC used to be one of the standard `Distribution` methods, alongside `logp` and `logcdf`.
24
+
{class}`~pytensor.tensor.random.op.RandomVariable` are responsible for implementing the random sampling methods.
25
25
The `RandomVariable` is also responsible for parameter broadcasting and shape inference.
26
26
27
27
Before creating a new `RandomVariable` make sure that it is not already offered in the {mod}`NumPy library <numpy.random>`.
@@ -68,8 +68,7 @@ class BlahRV(RandomVariable):
68
68
# This is the Python code that produces samples. Its signature will always
69
69
# start with a NumPy `RandomState` object, then the distribution
70
70
# parameters, and, finally, the size.
71
-
#
72
-
# This is effectively the PyMC >=4.0 replacement for `Distribution.random`.
71
+
73
72
@classmethod
74
73
defrng_fn(
75
74
cls,
@@ -87,19 +86,20 @@ blah = BlahRV()
87
86
88
87
Some important things to keep in mind:
89
88
90
-
1. Everything inside the `rng_fn` method is pure Python code (as are the inputs) and should not make use of other `PyTensor` symbolic ops. The random method should make use of the `rng` which is a NumPy {class}`~numpy.random.RandomState`, so that samples are reproducible.
89
+
1. Everything inside the `rng_fn` method is pure Python code (as are the inputs) and should __not__ make use of other `PyTensor` symbolic ops. The random method should make use of the `rng` which is a NumPy {class}`~numpy.random.RandomGenerator`, so that samples are reproducible.
91
90
1. Non-default `RandomVariable` dimensions will end up in the `rng_fn` via the `size` kwarg. The `rng_fn` will have to take this into consideration for correct output. `size` is the specification used by NumPy and SciPy and works like PyMC `shape` for univariate distributions, but is different for multivariate distributions. For multivariate distributions the __`size` excludes the `ndim_supp` support dimensions__, whereas the __`shape` of the resulting `TensorVariable` or `ndarray` includes the support dimensions__. For more context check {ref}`The dimensionality notebook <dimensionality>`.
92
-
1.`PyTensor`tries to infer the output shape of the`RandomVariable` (given a user-specified size) by introspection of the `ndim_supp` and `ndim_params` attributes. However, the default method may not work for more complex distributions. In that case, custom `_supp_shape_from_params`(and less probably, `_infer_shape`) should also be implemented in the new `RandomVariable` class. One simple example is seen in the {class}`~pymc.DirichletMultinomialRV` where it was necessary to specify the `rep_param_idx` so that the `default_supp_shape_from_params` helper method can do its job. In more complex cases, it may not suffice to use this default helper. This could happen for instance if the argument values determined the support shape of the distribution, as happens in the `~pymc.distributions.multivarite._LKJCholeskyCovRV`.
91
+
1.`PyTensor`can automatically infer the output shape of univariate`RandomVariable`s (`ndim_supp=0`). For multivariate distributions (`ndim_supp>=1`), the method `_supp_shape_from_params`must be implemented in the new `RandomVariable` class. This method returns the support dimensionality of an RV given its parameters. In some cases this can be derived from the shape of one of its parameters, in which case the helper {func}`pytensor.tensor.random.utils.supp_shape_from_ref_param_shape` cand be used as is in {class}`~pymc.DirichletMultinomialRV`. In other cases the argument values (and not their shapes) may determine the support shape of the distribution, as happens in the `~pymc.distributions.multivarite._LKJCholeskyCovRV`. In simpler cases they may be constant.
93
92
1. It's okay to use the `rng_fn``classmethods` of other PyTensor and PyMC `RandomVariables` inside the new `rng_fn`. For example if you are implementing a negative HalfNormal `RandomVariable`, your `rng_fn` can simply return `- halfnormal.rng_fn(rng, scale, size)`.
94
93
95
94
*Note: In addition to `size`, the PyMC API also provides `shape`, `dims` and `observed` as alternatives to define a distribution dimensionality, but this is taken care of by {class}`~pymc.Distribution`, and should not require any extra changes.*
96
95
97
-
For a quick test that your new `RandomVariable``Op` is working, you can call the `Op` with the necessary parameters and then call `eval()` on the returned object:
96
+
For a quick test that your new `RandomVariable``Op` is working, you can call the `Op` with the necessary parameters and then call {class}`~pymc.draw` on the returned object:
98
97
99
98
```python
100
99
101
100
# blah = pytensor.tensor.random.uniform in this example
102
-
blah([0, 0], [1, 2], size=(10, 2)).eval()
101
+
# multiple calls with the same seed should return the same values
## 2. Inheriting from a PyMC base `Distribution` class
118
118
119
119
After implementing the new `RandomVariable``Op`, it's time to make use of it in a new PyMC {class}`~pymc.Distribution`.
120
-
PyMC `>=4.0.0`works in a very {term}`functional <Functional Programming>` way, and the `distribution` classes are there mostly to facilitate porting the `PyMC3` v3.x code to PyMC `>=4.0.0`, add PyMC API features and keep related methods organized together.
120
+
PyMC works in a very {term}`functional <Functional Programming>` way, and the `distribution` classes are there mostly to add PyMC API features and keep related methods organized together.
121
121
In practice, they take care of:
122
122
123
-
1. Linking ({term}`Dispatching`) an rv_op class with the corresponding `moment`, `logp`and `logcdf` methods.
123
+
1. Linking ({term}`Dispatching`) an `rv_op` class with the corresponding `moment`, `logp`, `logcdf`and `icdf` methods.
124
124
1. Defining a standard transformation (for continuous distributions) that converts a bounded variable domain (e.g., positive line) to an unbounded domain (i.e., the real line), which many samplers prefer.
125
125
1. Validating the parametrization of a distribution and converting non-symbolic inputs (i.e., numeric literals or NumPy arrays) to symbolic variables.
126
126
1. Converting multiple alternative parametrizations to the standard parametrization that the `RandomVariable` is defined in terms of.
@@ -130,7 +130,6 @@ Here is how the example continues:
130
130
```python
131
131
132
132
import pytensor.tensor as pt
133
-
from pymc.pytensorf import floatX, intX
134
133
from pymc.distributions.continuous import PositiveContinuous
135
134
from pymc.distributions.dist_math import check_parameters
136
135
from pymc.distributions.shape_utils import rv_size_is_none
@@ -146,12 +145,12 @@ class Blah(PositiveContinuous):
146
145
# We pass the standard parametrizations to super().dist
raiseValueError("Only one of param2 and alt_param2 is allowed.")
152
151
if alt_param2 isnotNone:
153
152
param2 =1/ alt_param2
154
-
param2 = pt.as_tensor_variable(floatX(param2))
153
+
param2 = pt.as_tensor_variable(param2)
155
154
156
155
# The first value-only argument should be a list of the parameters that
157
156
# the rv_op needs in order to be instantiated
@@ -183,7 +182,7 @@ class Blah(PositiveContinuous):
183
182
# Whenever a bound is invalidated, the returned expression raises an error
184
183
# with the message defined in the optional `msg` keyword argument.
185
184
return check_parameters(
186
-
logp_expression,
185
+
bounded_logp_expression,
187
186
param2 >=0,
188
187
msg="param2 >= 0",
189
188
)
@@ -193,15 +192,18 @@ class Blah(PositiveContinuous):
193
192
deflogcdf(value, param1, param2):
194
193
...
195
194
195
+
deficdf(value, param1, param2):
196
+
...
197
+
196
198
```
197
199
198
200
Some notes:
199
201
200
202
1. A distribution should at the very least inherit from {class}`~pymc.Discrete` or {class}`~pymc.Continuous`. For the latter, more specific subclasses exist: `PositiveContinuous`, `UnitContinuous`, `BoundedContinuous`, `CircularContinuous`, `SimplexContinuous`, which specify default transformations for the variables. If you need to specify a one-time custom transform you can also create a `_default_transform` dispatch function as is done for the {class}`~pymc.distributions.multivariate.LKJCholeskyCov`.
201
203
1. If a distribution does not have a corresponding `rng_fn` implementation, a `RandomVariable` should still be created to raise a `NotImplementedError`. This is, for example, the case in {class}`~pymc.distributions.continuous.Flat`. In this case it will be necessary to provide a `moment` method, because without a `rng_fn`, PyMC can't fall back to a random draw to use as an initial point for MCMC.
202
-
1. As mentioned above, PyMC `>=4.0.0`works in a very {term}`functional <Functional Programming>` way, and all the information that is needed in the `logp`and `logcdf` methods is expected to be "carried" via the `RandomVariable` inputs. You may pass numerical arguments that are not strictly needed for the `rng_fn` method but are used in the `logp` and `logcdf`methods. Just keep in mind whether this affects the correct shape inference behavior of the `RandomVariable`. If specialized non-numeric information is needed you might need to define your custom`_logp` and `_logcdf` {term}`Dispatching` functions, but this should be done as a last resort.
203
-
1. The `logcdf` method is not a requirement, but it's a nice plus!
204
-
1. Currently, only one moment is supported in the `moment` method, and probably the "higher-order" one is the most useful (that is `mean` > `median` > `mode`)... You might need to truncate the moment if you are dealing with a discrete distribution.
204
+
1. As mentioned above, PyMC works in a very {term}`functional <Functional Programming>` way, and all the information that is needed in the `logp`, `logcdf`, `icdf`and `moment` methods is expected to be "carried" via the `RandomVariable` inputs. You may pass numerical arguments that are not strictly needed for the `rng_fn` method but are used in the those methods. Just keep in mind whether this affects the correct shape inference behavior of the `RandomVariable`.
205
+
1. The `logcdf`, and `icdf` methods is not a requirement, but it's a nice plus!
206
+
1. Currently, only one moment is supported in the `moment` method, and probably the "higher-order" one is the most useful (that is `mean` > `median` > `mode`)... You might need to truncate the moment if you are dealing with a discrete distribution.`moment` should return a valid point for the random variable (i.e., it always has non-zero probability when evaluated at that point)
205
207
1. When creating the `moment` method, be careful with `size != None` and broadcast properly also based on parameters that are not necessarily used to calculate the moment. For example, the `sigma` in `pm.Normal.dist(mu=0, sigma=np.arange(1, 6))` is irrelevant for the moment, but may nevertheless inform about the shape. In this case, the `moment` should return `[mu, mu, mu, mu, mu]`.
206
208
207
209
For a quick check that things are working you can try the following:
@@ -215,7 +217,7 @@ from pymc.distributions.distribution import moment
215
217
blah = pm.blah.dist(mu=0, sigma=1)
216
218
217
219
# Test that the returned blah_op is still working fine
218
-
blah.eval()
220
+
pm.draw(blah, random_seed=1)
219
221
# array(-1.01397228)
220
222
221
223
# Test the moment method
@@ -306,10 +308,10 @@ Finally, when your `rng_fn` is doing something more than just calling a NumPy or
306
308
You can find an example in {class}`~tests.distributions.test_continuous.TestWeibull`, whose `rng_fn` returns `beta * np.random.weibull(alpha, size=size)`.
307
309
308
310
309
-
## 4. Adding tests for the `logp` / `logcdf` methods
311
+
## 4. Adding tests for the `logp` / `logcdf`/ `icdf`methods
310
312
311
-
Tests for the `logp`and `logcdf` mostly make use of the helpers `check_logp`, `check_logcdf`, and
312
-
`check_selfconsistency_discrete_logcdf` implemented in `~tests.distributions.util`
313
+
Tests for the `logp`, `logcdf`and `icdf` mostly make use of the helpers `check_logp`, `check_logcdf`,`check_icdf` and
314
+
`check_selfconsistency_discrete_logcdf` implemented in `~testing`
0 commit comments