Skip to content

Commit ce9e98d

Browse files
authored
Merge pull request #3285 from lucianopaz/iss3271
Fix for #3271
2 parents b400299 + da5b66e commit ce9e98d

File tree

3 files changed

+101
-28
lines changed

3 files changed

+101
-28
lines changed

RELEASE-NOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
- Refactor SMC and properly compute marginal likelihood (#3124)
2929
- Removed use of deprecated `ymin` keyword in matplotlib's `Axes.set_ylim` (#3279)
3030
- Fix for #3210. Now `distribution.draw_values(params)`, will draw the `params` values from their joint probability distribution and not from combinations of their marginals (Refer to PR #3273).
31+
- Rewrote `Multinomial._random` method to better handle shape broadcasting (#3271)
3132

3233
### Deprecations
3334

pymc3/distributions/multivariate.py

Lines changed: 84 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -535,37 +535,93 @@ def _random(self, n, p, size=None):
535535
# Set float type to float64 for numpy. This change is related to numpy issue #8317 (https://github.com/numpy/numpy/issues/8317)
536536
p = p.astype('float64')
537537
# Now, re-normalize all of the values in float64 precision. This is done inside the conditionals
538-
if size == p.shape:
539-
size = None
540-
elif size[-len(p.shape):] == p.shape:
541-
size = size[:len(size) - len(p.shape)]
542538

543-
n_dim = n.squeeze().ndim
544-
545-
if (n_dim == 0) and (p.ndim == 1):
546-
p = p / p.sum()
547-
randnum = np.random.multinomial(n, p.squeeze(), size=size)
548-
elif (n_dim == 0) and (p.ndim > 1):
549-
p = p / p.sum(axis=1, keepdims=True)
550-
randnum = np.asarray([
551-
np.random.multinomial(n.squeeze(), pp, size=size)
552-
for pp in p
553-
])
554-
randnum = np.moveaxis(randnum, 1, 0)
555-
elif (n_dim > 0) and (p.ndim == 1):
539+
# np.random.multinomial needs `n` to be a scalar int and `p` a
540+
# sequence
541+
if p.ndim == 1 and (n.ndim == 0 or (n.ndim == 1 and n.shape[0] == 1)):
542+
# If `n` is already a scalar and `p` is a sequence, then just
543+
# return np.multinomial with some size handling
556544
p = p / p.sum()
557-
randnum = np.asarray([
558-
np.random.multinomial(nn, p.squeeze(), size=size)
559-
for nn in n
560-
])
561-
randnum = np.moveaxis(randnum, 1, 0)
545+
if size is not None:
546+
if size == p.shape:
547+
size = None
548+
elif size[-len(p.shape):] == p.shape:
549+
size = size[:len(size) - len(p.shape)]
550+
randnum = np.random.multinomial(n, p, size=size)
551+
return randnum.astype(original_dtype)
552+
# The shapes of `p` and `n` must be broadcasted by hand depending on
553+
# their ndim. We will assume that the last axis of the `p` array will
554+
# be the sequence to feed into np.random.multinomial. The other axis
555+
# will only have to be iterated over.
556+
if n.ndim == p.ndim:
557+
# p and n have the same ndim, so n.shape[-1] must be 1
558+
if n.shape[-1] != 1:
559+
raise ValueError('If n and p have the same number of '
560+
'dimensions, the last axis of n must be '
561+
'have len 1. Got {} instead.\n'
562+
'n.shape = {}\n'
563+
'p.shape = {}.'.format(n.shape[-1],
564+
n.shape,
565+
p.shape))
566+
n_p_shape = np.broadcast(np.empty(p.shape[:-1]),
567+
np.empty(n.shape[:-1])).shape
568+
p = np.broadcast_to(p, n_p_shape + (p.shape[-1],))
569+
n = np.broadcast_to(n, n_p_shape + (1,))
570+
elif n.ndim == p.ndim - 1:
571+
# n has the number of dimensions of p for the iteration, these must
572+
# broadcast together
573+
n_p_shape = np.broadcast(np.empty(p.shape[:-1]),
574+
n).shape
575+
p = np.broadcast_to(p, n_p_shape + (p.shape[-1],))
576+
n = np.broadcast_to(n, n_p_shape + (1,))
577+
elif p.ndim == 1:
578+
# p only has the sequence array. We extend it with the dimensions
579+
# of n
580+
n_p_shape = n.shape
581+
p = np.broadcast_to(p, n_p_shape + (p.shape[-1],))
582+
n = np.broadcast_to(n, n_p_shape + (1,))
583+
elif n.ndim == 0 or (n.dim == 1 and n.shape[0] == 1):
584+
# n is a scalar. We extend it with the dimensions of p
585+
n_p_shape = p.shape[:-1]
586+
n = np.broadcast_to(n, n_p_shape + (1,))
587+
else:
588+
# There is no clear rule to broadcast p and n so we raise an error
589+
raise ValueError('Incompatible shapes of n and p.\n'
590+
'n.shape = {}\n'
591+
'p.shape = {}'.format(n.shape, p.shape))
592+
593+
# Check what happens with size
594+
if size is not None:
595+
if size == p.shape:
596+
size = None
597+
_size = 1
598+
elif size[-len(p.shape):] == p.shape:
599+
size = size[:len(size) - len(p.shape)]
600+
_size = np.prod(size)
601+
else:
602+
_size = np.prod(size)
603+
else:
604+
_size = 1
605+
606+
# We now flatten p and n up to the last dimension
607+
p_shape = p.shape
608+
p = np.reshape(p, (np.prod(n_p_shape), -1))
609+
n = np.reshape(n, (np.prod(n_p_shape), -1))
610+
# We renormalize p
611+
p = p / p.sum(axis=1, keepdims=True)
612+
# We iterate calls to np.random.multinomial
613+
randnum = np.asarray([
614+
np.random.multinomial(nn, pp, size=_size)
615+
for (nn, pp) in zip(n, p)
616+
])
617+
# We swap the iteration axis with the _size axis
618+
randnum = np.moveaxis(randnum, 1, 0)
619+
# We reshape the random numbers to the corresponding size + p_shape
620+
if size is None:
621+
randnum = np.reshape(randnum, p_shape)
562622
else:
563-
p = p / p.sum(axis=1, keepdims=True)
564-
randnum = np.asarray([
565-
np.random.multinomial(nn, pp, size=size)
566-
for (nn, pp) in zip(n, p)
567-
])
568-
randnum = np.moveaxis(randnum, 1, 0)
623+
randnum = np.reshape(randnum, size + p_shape)
624+
# We cast back to the original dtype
569625
return randnum.astype(original_dtype)
570626

571627
def random(self, point=None, size=None):

pymc3/tests/test_sampling.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,22 @@ def test_multivariate(self):
370370
assert m.random(size=10).shape == (10, 4)
371371
assert trace['m'].shape == (10, 4)
372372

373+
def test_multivariate2(self):
374+
# Added test for issue #3271
375+
mn_data = np.random.multinomial(n=100, pvals=[1/6.]*6, size=10)
376+
with pm.Model() as dm_model:
377+
probs = pm.Dirichlet('probs', a=np.ones(6), shape=6)
378+
obs = pm.Multinomial('obs', n=100, p=probs, observed=mn_data)
379+
burned_trace = pm.sample(20, tune=10, cores=1)
380+
sim_priors = pm.sample_prior_predictive(samples=20,
381+
model=dm_model)
382+
sim_ppc = pm.sample_posterior_predictive(burned_trace,
383+
samples=20,
384+
model=dm_model)
385+
assert sim_priors['probs'].shape == (20, 6)
386+
assert sim_priors['obs'].shape == (20, 6)
387+
assert sim_ppc['obs'].shape == (20,) + obs.distribution.shape
388+
373389
def test_layers(self):
374390
with pm.Model() as model:
375391
a = pm.Uniform('a', lower=0, upper=1, shape=10)

0 commit comments

Comments
 (0)