Hi, are there any examples of how to implement the random()
function needed to support sample_ppc
for a custom distribution? It’s briefly mentioned here, but I’m not sure where to start on this.
Thx!
We dont have a good example, but have a look at the test case for some inspiration:
I am trying to implement the random method for DensityDist
, but I do not understand how the function should be implemented. I would like to get a template to follow.
For example, for the logp
function this information (from this post) is very useful (we should include it in the documentation):
# Log likelihood function
def logp_function(parameters):
def logp(observed):
_logp = some_function(parameters, observed)
return _logp
return logp # returns a function
Is there anything similar for random
?
The example you provided in the post above uses normal_dist.random
. The random
method is:
def random(self, point=None, size=None):
"""
Draw random values from Normal distribution.
Parameters
----------
point : dict, optional
Dict of variable values on which random values are to be
conditioned (uses default point if not specified).
size : int, optional
Desired size of random sample (returns one sample if not
specified).
Returns
-------
array
"""
mu, tau, _ = draw_values([self.mu, self.tau, self.sd],
point=point, size=size)
return generate_samples(stats.norm.rvs, loc=mu, scale=tau**-0.5,
dist_shape=self.shape,
size=size)
but I have difficulties to create a standalone function from the method in the class. For example, how do I pass the parameters for my function? Do I need to use draw_values
and generate_samples
? If so, how to pass the parameter point
and shape
?
You do need draw_values
and generate_samples
: draw_values
fetch a numpy value from theano tensor, and generate_sample is a function wrapper that turns an “elementary” random generator to a one outputs high-dimensional arrayes
Maybe you can have a look at my PR for adding random method to LKJCorr:
I tried to implement the method for a normal distribution (for sake of example), but I get the error 'numpy.ndarray' object is not callable
. I created a notebook here. Briefly, my random
method is:
def normal_rng(mu, sd, point=None, size=None):
# draw a numerical value for the parameters
mu_, sd_ = draw_values([mu, sd], point=point)
size = 1 if size is None else size
return generate_samples(scipy.stats.norm.rvs, loc=mu_, scale=sd_, size=size, broadcast_shape=(size,))
and it is called in DensityDist
as:
likelihood = pm.DensityDist('likelihood', normal_logp(mu, sd), observed=y, random=normal_rng(mu, sd))
Still has shape problem but this is starting to run:
with pm.Model() as model:
mu = pm.Normal('mu', mu=0, sd=10)
sd = pm.HalfNormal('sd', sd=10)
def normal_rng(point=None, size=None):
# draw a numerical value for the parameters
mu_, sd_ = draw_values([mu, sd], point=point)
size = 1 if size is None else size
return generate_samples(scipy.stats.norm.rvs, loc=mu_, scale=sd_, size=size, broadcast_shape=(size,))
likelihood = pm.DensityDist('likelihood', normal_logp(mu, sd), observed=y, random=normal_rng)
trace = pm.sample()
ppc = pm.sample_posterior_predictive(trace)
Strange it does not work if the function is outside the context. Do you think it is some sort of bug?
I implemented the function as you suggested and it seems that works (see updated notebook). Note that I removed broadcast_shape=
because it seems it does not change the results. What do you mean with
If I run ppc = pm.sample_posterior_predictive(trace, samples=250, size=100)
I get an (250x100) array, which I think is correct.
I feel like there should be a way to write the function outside of the context, but I havent figure out yet
Actually, you should get a (250x100) array by doing just ppc = pm.sample_posterior_predictive(trace, samples=250)
, as the size
should be inferred from the shape of the observed - that’s how RV defined using regular distribution in pymc3 does.
Let’s see if I understand. If I run ppc = pm.sample_posterior_predictive(trace)
, then I get a (trace_length, 1) array. Instead I should get a (trace_length, observed_size) array?
EDIT: I feel like we need to declare dist_shape
somehow (which is always defined in the class method). A dirty hack would be generate_samples(scipy.stats.norm.rvs, loc=mu_, scale=sd_, size=size, dist_shape=observed.shape)
Yes.
I changed this behavior in Improve sample_ppc by junpenglao · Pull Request #3053 · pymc-devs/pymc · GitHub. I think it makes more sense, see discussion here: Sample_ppc shape - #2 by junpenglao
Do you think I should open a Github issue to track down the problem here?
Also, I cannot use more than 1 core with DensityDist
. Given the model:
with pm.Model() as model:
mu = pm.Normal('mu', mu=0, sd=10)
sd = pm.HalfNormal('sd', sd=10)
def normal_logp(value):
return (1/2)*(-((value - mu)**2/sd**2) - tt.log(2*np.pi) - 2*tt.log(sd))
def normal_rng(point=None, size=None):
# draw a numerical value for the parameters
mu_, sd_ = draw_values([mu, sd], point=point)
print(shape)
size = 1 if size is None else size
return generate_samples(scipy.stats.norm.rvs, loc=mu_, scale=sd_, size=size, dist_shape=y.shape)
likelihood = pm.DensityDist('likelihood', normal_logp, observed=y, random=normal_rng)
if I use cores
>1, then I get a broken pipe
error (I am using Win10, and I do not know if the problem exists with Unix also):
Auto-assigning NUTS sampler...
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (2 chains in 2 jobs)
NUTS: [sd, mu]
---------------------------------------------------------------------------
BrokenPipeError Traceback (most recent call last)
<ipython-input-27-d9190de005b7> in <module>()
2 CORES = 2
3 with model:
----> 4 trace = pm.sample(draws=1000, tune=1000, chains=CHAINS, cores=CORES, random_seed=[23+ i for i in np.arange(CHAINS)])
C:\Miniconda3\envs\intro_to_pymc3\lib\site-packages\pymc3\sampling.py in sample(draws, step, init, n_init, start, trace, chain_idx, chains, cores, tune, nuts_kwargs, step_kwargs, progressbar, model, random_seed, live_plot, discard_tuned_samples, live_plot_kwargs, compute_convergence_checks, use_mmap, **kwargs)
438 _print_step_hierarchy(step)
439 try:
--> 440 trace = _mp_sample(**sample_args)
441 except pickle.PickleError:
442 _log.warning("Could not pickle model, sampling singlethreaded.")
C:\Miniconda3\envs\intro_to_pymc3\lib\site-packages\pymc3\sampling.py in _mp_sample(draws, tune, step, chains, cores, chain, random_seed, start, progressbar, trace, model, use_mmap, **kwargs)
985 sampler = ps.ParallelSampler(
986 draws, tune, chains, cores, random_seed, start, step,
--> 987 chain, progressbar)
988 try:
989 with sampler:
C:\Miniconda3\envs\intro_to_pymc3\lib\site-packages\pymc3\parallel_sampling.py in __init__(self, draws, tune, chains, cores, seeds, start_points, step_method, start_chain_num, progressbar)
273 ProcessAdapter(draws, tune, step_method,
274 chain + start_chain_num, seed, start)
--> 275 for chain, seed, start in zip(range(chains), seeds, start_points)
276 ]
277
C:\Miniconda3\envs\intro_to_pymc3\lib\site-packages\pymc3\parallel_sampling.py in <listcomp>(.0)
273 ProcessAdapter(draws, tune, step_method,
274 chain + start_chain_num, seed, start)
--> 275 for chain, seed, start in zip(range(chains), seeds, start_points)
276 ]
277
C:\Miniconda3\envs\intro_to_pymc3\lib\site-packages\pymc3\parallel_sampling.py in __init__(self, draws, tune, step_method, chain, seed, start)
180 draws, tune, seed)
181 # We fork right away, so that the main process can start tqdm threads
--> 182 self._process.start()
183
184 @property
C:\Miniconda3\envs\intro_to_pymc3\lib\multiprocessing\process.py in start(self)
103 'daemonic processes are not allowed to have children'
104 _cleanup()
--> 105 self._popen = self._Popen(self)
106 self._sentinel = self._popen.sentinel
107 # Avoid a refcycle if the target function holds an indirect
C:\Miniconda3\envs\intro_to_pymc3\lib\multiprocessing\context.py in _Popen(process_obj)
221 @staticmethod
222 def _Popen(process_obj):
--> 223 return _default_context.get_context().Process._Popen(process_obj)
224
225 class DefaultContext(BaseContext):
C:\Miniconda3\envs\intro_to_pymc3\lib\multiprocessing\context.py in _Popen(process_obj)
320 def _Popen(process_obj):
321 from .popen_spawn_win32 import Popen
--> 322 return Popen(process_obj)
323
324 class SpawnContext(BaseContext):
C:\Miniconda3\envs\intro_to_pymc3\lib\multiprocessing\popen_spawn_win32.py in __init__(self, process_obj)
63 try:
64 reduction.dump(prep_data, to_child)
---> 65 reduction.dump(process_obj, to_child)
66 finally:
67 set_spawning_popen(None)
C:\Miniconda3\envs\intro_to_pymc3\lib\multiprocessing\reduction.py in dump(obj, file, protocol)
58 def dump(obj, file, protocol=None):
59 '''Replacement for pickle.dump() using ForkingPickler.'''
---> 60 ForkingPickler(file, protocol).dump(obj)
61
62 #
BrokenPipeError: [Errno 32] Broken pipe
I think there is some kind of pickling issue (I thought this is not a problem any more but apparently it is at least under Windows). Try declaring the logp function outside of the model context:
def normal_logp(value):
return (1/2)*(-((value - mu)**2/sd**2) - tt.log(2*np.pi) - 2*tt.log(sd))
with pm.Model() as ...
Feel free to open an issue on Github - I think the random method would need some more work to make it more user friendly (documentation, example, etc)
I’m reviving an old discussion here, but it might be better than opening a new one. I’ve implemented my own distribution, which is basicall the lognormal distribution from scipy. Sampling the log likelihood is fine, but generating samples for the posterior predictive isn’t working, despite following the suggestions from this thread.
Help is much appreciated.
Attached is my code and the error message:
import pymc as pm
import scipy as sp
import numpy as np
import arviz as az
import matplotlib.pyplot as plt
def logp_lognorm(value, shape, loc, scale):
return -np.log((value-loc)/scale)**2 / (2*shape**2) - np.log(shape*((value-loc)/scale)*np.sqrt(2*np.pi))
def rand_lognorm(point=None, size=None):
# draw a numerical value for the parameters
shape_, loc_, scale_ = draw_values([shape, loc, scale], point=point)
size = 1 if size is None else size
return generate_samples(sp.stats.lognorm.rvs, shape=shape_, loc=loc_, scale=scale_, size=size, broadcast_shape=(size,))
with pm.Model() as model:
shape = pm.Uniform('shape', lower=1, upper=3)
loc = pm.Uniform('loc', lower=4000, upper=5000)
scale = pm.Uniform('scale', lower=8000, upper=9000)
lognorm = pm.DensityDist('likelihood', shape, loc, scale, logp=logp_lognorm, observed=data, random=rand_lognorm)
idata = pm.sample(4000, tune=2000, return_inferencedata=True, chains=1)
idata.extend(pm.sample_posterior_predictive(idata))
TypeError: rand_lognorm() got an unexpected keyword argument ‘rng’
Apply node that caused the error: DensityDist_likelihood_rv{0, (0, 0, 0), floatX, True}(RandomGeneratorSharedVariable(<Generator(PCG64) at 0x7F384431F120>), TensorConstant{[2172 1]}, TensorConstant{11}, shape, loc, scale)
Toposort index: 0
Inputs types: [RandomGeneratorType, TensorType(int64, (2,)), TensorType(int64, ()), TensorType(float64, ()), TensorType(float64, ()), TensorType(float64, ())]
Inputs shapes: [‘No shapes’, (2,), (), (), (), ()]
Inputs strides: [‘No strides’, (8,), (), (), (), ()]
Inputs values: [Generator(PCG64) at 0x7F384431F120, array([2172, 1]), array(11), array(2.37989727), array(4986.3850034), array(8990.54453988)]
Outputs clients: [[‘output’], [‘output’]]
I don’t know which version of PyMC you’re using. We have examples of random
function for the latest version in the docstrings: pymc.CustomDist — PyMC dev documentation
The signature has changed, and should now be def random(*params, rng, size):
DensityDist is just an alias for CustomDist now
Thanks, it was indeed an issue with the PyMC version. After upgrading to the latest release on git, the following is working:
import pymc as pm
import numpy as np
import scipy as sp
import arviz as az
import matplotlib.pyplot as plt
def logp_lognormal(value, shape, loc, scale):
return -np.log((value-loc)/scale)**2 / (2*shape**2) - np.log(shape*((value-loc)/scale)*np.sqrt(2*np.pi))
def rand_lognormal(shape, loc, scale, rng=None, size=None):
return sp.stats.lognorm.rvs(shape, loc=loc, scale=scale)
def logp_genpareto(value, shape, loc, scale):
return -(shape+1)*np.log1p(shape*(data-loc)/scale)/shape
def rand_genpareto(shape, loc, scale, rng=None, size=None):
return sp.stats.genpareto.rvs(shape, loc=loc, scale=scale)
with pm.Model() as model:
shape = pm.Uniform('shape', lower=1, upper=3)
loc = pm.Uniform('loc', lower=4000, upper=5000)
scale = pm.Uniform('scale', lower=6000, upper=10000)
lognorm = pm.CustomDist('likelihood', shape, loc, scale, logp=logp_genpareto, observed=data, random=rand_genpareto)
idata = pm.sample(4000, tune=2000, return_inferencedata=True, chains=1)
idata.extend(pm.sample_posterior_predictive(idata,random_seed=42))
You should make use of the rng. Scipy rvs methods accept it under the kwarg random_state
IIRC.
You can also pass size to the rvs method, always nice even if you plan to only sample scalar distributions
Great. Thanks Ricardo.
Hello, I’m trying to implement skew student-t in pymc by following your example. But I ran into scipy.stats.rv_generic._argcheck
error of TypeError: Variables do not support boolean operations.
Then I try print out the args of logp, and it isn’t a number but a pytensor.tensor.var.TensorVariable
. I am not familiar with pytensor
. So please advised
code
import pymc as pm
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats, interpolate
class Helper:
@staticmethod
def get_bounds(
pdf_func,
*args,
step_sz: float = 0.1,
eps: float = 1e-4,
ref_x: float = 0,
**kwargs,
):
lo_n, hi_n = 0, 0
while pdf_func(-1 * lo_n * step_sz + ref_x, *args, **kwargs) >= eps:
lo_n += 1
while pdf_func(hi_n * step_sz + ref_x, *args, **kwargs) >= eps:
hi_n += 1
return -1 * lo_n * step_sz + ref_x, hi_n * step_sz + ref_x
@staticmethod
def get_est_cdf_func(
pdf_func,
*args,
n_samples: int = 10000,
**kwargs
):
lo, hi = Helper.get_bounds(pdf_func, *args, **kwargs)
xs = np.linspace(lo, hi, n_samples + 1)
xk = (xs[1:] + xs[:-1])/2
qvals = pdf_func(xs, *args, **kwargs)
qvals = (qvals[1:] + qvals[:-1])/2
qvals = np.cumsum(qvals * (hi - lo)/n_samples)
def _cdf_factory(values, *a, **kw):
return np.where(
values < lo, 0,
np.where(
values > hi, 1,
qvals[np.argmin(np.abs(xk - values.reshape(-1, 1)), axis=1)]
)
)
return _cdf_factory
@staticmethod
def inversion_sampling(
pdf_func,
*args,
size=None,
rng=None,
n_samples: int = 10000,
**kwargs
):
if rng is not None:
np.random.seed(rng)
lo, hi = Helper.get_bounds(pdf_func, *args, **kwargs)
cdf_func = Helper.get_est_cdf_func(pdf_func, *args, n_samples=n_samples, **kwargs)
xs = np.linspace(lo, hi, n_samples + 1)
inverse_cdf = interpolate.interp1d(cdf_func(xs), xs, fill_value="extrapolate")
uniform_samples = np.random.uniform(size=size)
return np.where(
uniform_samples < lo, 0,
np.where(
uniform_samples > hi, 1,
inverse_cdf(uniform_samples)
)
)
def logp_skewt(value, nu, mu, sigma, alpha, *args, **kwargs):
print(nu, mu, sigma, alpha)
print(type(nu), type(mu), type(sigma), type(alpha))
return np.log(2) + stats.t.logpdf((value - mu)/sigma, nu) + stats.t.logcdf(alpha * (value - mu)/sigma, nu) - np.log(sigma)
def rand_genskewt(nu, mu, sigma, alpha, size=None, rng=None):
p_skewt = lambda value, nu, mu, sigma, alpha, *args, **kwargs: np.exp(logp_skewt(value, nu, mu, sigma, alpha, *args, **kwargs))
return Helper.inversion_sampling(p_skewt, nu, mu, sigma, alpha, size=size, rng=rng)
with pm.Model() as model:
nu = pm.Uniform('shape', lower=1, upper=10)
mu = pm.Uniform('loc', lower=-1, upper=1)
sigma = pm.Uniform('scale', lower=1, upper=10)
alpha = pm.Uniform('skew', lower=-10, upper=10)
log_skewt = pm.CustomDist('likelihood', nu, mu, sigma, alpha, logp=logp_skewt, random=rand_genskewt)
model_trace = pm.sample(
nuts_sampler="numpyro",
draws=100,
chains=1,
idata_kwargs={"log_likelihood": True},
)
The logp function must be defined with Pytensor operations instead of calls to Numpy or Scipy
You can use pm.logp(pm.StudentT.dist(...), value)
and pm.logcdf
to obtain PyTensor expressions of the logp and logcdf for use in your logp function.
Fix the code, now work as expected. Thanks!
code
import pymc as pm
import matplotlib.pyplot as plt
def logp_skewt(value, nu, mu, sigma, alpha, *args, **kwargs):
return (
pm.math.log(2) +
pm.logp(pm.StudentT.dist(nu, mu=mu, sigma=sigma), value) +
pm.logcdf(pm.StudentT.dist(nu, mu=mu, sigma=sigma), alpha*value) -
pm.math.log(sigma)
)
with pm.Model() as model:
skewt = pm.CustomDist('likelihood', 1, 0, 3, -10, logp=logp_skewt)
model_trace = pm.sample(
nuts_sampler="numpyro",
draws=10_000,
chains=1,
)
samples = model_trace.posterior.likelihood.to_numpy()
plt.hist(samples[(samples < 30) & (samples > -100)], bins=100, density=True)
plt.show()