-
-
Notifications
You must be signed in to change notification settings - Fork 11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add relation for the Beta-Binomial observation model #29
Conversation
This seems to be a more general issue, or a misunderstanding from my part. The following minimal example reproduces the error: import aesara.tensor as at
from unification import var, unify, reify
from etuples import etuplize, etuple
srng = at.random.RandomStream(0)
Y = srng.beta(1., 1.)
y_srng_lv, y_size_lv, y_type_idx_lv = var(), var(), var()
alpha_lv, beta_lv = var(), var()
y_lv = etuple(etuplize(at.random.beta), y_srng_lv, y_size_lv, y_type_idx_lv, alpha_lv, beta_lv)
s = unify(Y, y_lv)
reify(y_lv, s).eval_obj
# AttributeError: 'RandomGeneratorType' object has no attribute 'ndim'. Did you mean: 'evaled_obj'? |
@rlouf could it be that the variables are in the wrong order? It looks to me like a numpy array is expected but an RV object is supplied instead. |
The issue is subtler than that. The signature of def __call__(self, *args, rng=None, size=None, type=None) So that when
The solution is to pass p_posterior_lv = etuple(
etuplize(at.random.beta),
new_alpha_lv,
new_beta_lv,
rng=p_srng_lv,
size=p_size_lv,
dtype=p_type_idx_lv,
) |
Codecov Report
@@ Coverage Diff @@
## main #29 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 2 3 +1
Lines 185 206 +21
Branches 11 11
=========================================
+ Hits 185 206 +21
Continue to review full report at Codecov.
|
bb82ca7
to
200a569
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to be safe, we should add kanren
, logical-unification
, and etuples
as direct requires. We're currently getting those from Aesara, but that's too indirect.
Otherwise, this looks great!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't forget that this can be made into an immediately applicable Aesara rewrite using KanrenRelationSub
(see here for more information).
ea0825b
to
4d30768
Compare
Added the missing imports. I am not sure how I am going to orchestrate the sampler building yet but I'll remember |
tests/test_conjugates.py
Outdated
def test_beta_binomial_conjugate(): | ||
"""Test that we can produce the closed-form posterior for | ||
the binomial observation model with a beta prior. | ||
|
||
""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it work the other way: i.e. can it expand a beta into a binomial and a beta?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great point, I'm still used to thinking one way. I'll expand the test.
Update: No, it doesn't work the other way. I am trying to understand why.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't worry, it doesn't need to right now, but it's good to understand why.
I forgot to mention this old Symbolic PyMC example for the beta-binomial: Symbolic-PyMC Beta-Binomial Conjugate Example. |
We want the material in this repository to be more immediately useful, and we could easily accomplish that by creating an The changes/additions necessary to accomplish this are fairly minimal, so they could be reasonably included in this PR. |
Ok I'll give it a try! |
I updated the code. The relation does not work the other way, and it is related to the issue I was mentioning above. It all boils down to def beta_binomial_conjugateo(model_expr, observation_expr, posterior_expr):
# Beta-binomial observation model
alpha_lv, beta_lv = var(), var()
p_rng_lv = var()
p_size_lv = var()
p_type_idx_lv = var()
p_et = etuple(
etuplize(at.random.beta), p_rng_lv, p_size_lv, p_type_idx_lv, alpha_lv, beta_lv
)
n_lv = var()
Y_et = etuple(etuplize(at.random.binomial), var(), var(), var(), n_lv, p_et)
y_lv = var() # observation
# Posterior distribution for p
new_alpha_et = etuple(etuplize(at.add), alpha_lv, y_lv)
new_beta_et = etuple(
etuplize(at.sub), etuple(etuplize(at.add), beta_lv, n_lv), y_lv
)
p_posterior_et = etuple(
etuplize(at.random.beta),
new_alpha_et,
new_beta_et,
rng=p_rng_lv,
size=p_size_lv,
dtype=p_type_idx_lv,
)
return lall(
eq(model_expr, Y_et),
eq(observation_expr, y_lv),
eq(posterior_expr, p_posterior_et),
) And to be able to expand the contracted posterior we would need to use the following relation: def beta_binomial_conjugateo(model_expr, observation_expr, posterior_expr):
# Beta-binomial observation model
alpha_lv, beta_lv = var(), var()
p_rng_lv = var()
p_size_lv = var()
p_type_idx_lv = var()
p_et = etuple(
etuplize(at.random.beta),
alpha_lv,
beta_lv,
rng=p_rng_lv,
size=p_size_lv,
dtype=p_type_idx_lv,
)
n_lv = var()
Y_et = etuple(etuplize(at.random.binomial), var(), var(), var(), n_lv, p_et)
y_lv = var() # observation
# Posterior distribution for p
new_alpha_et = etuple(etuplize(at.add), alpha_lv, y_lv)
new_beta_et = etuple(
etuplize(at.sub), etuple(etuplize(at.add), beta_lv, n_lv), y_lv
)
p_posterior_et = etuple(
etuplize(at.random.beta),
p_rng_lv,
p_size_lv,
p_type_idx_lv,
new_alpha_et,
new_beta_et,
)
return lall(
eq(model_expr, Y_et),
eq(observation_expr, y_lv),
eq(posterior_expr, p_posterior_et),
) Can we somehow change the behavior of the |
|
I was not clear enough. To see the issue, consider the following example: import aesara.tensor as at
from aesara.graph.unify import eval_if_etuple
from unification import var, unify, reify
from etuples import etuple, etuplize
from kanren import run, lall, eq
def normaleo(in_expr, out_expr):
mu_lv, std_lv = var(), var()
rng_lv, size_lv, dtype_lv = var(), var(), var()
norm_in_lv = etuple(etuplize(at.random.normal), rng_lv, size_lv, dtype_lv, mu_lv, std_lv)
norm_out_lv = etuple(etuplize(at.random.normal), rng_lv, size_lv, dtype_lv, mu_lv, std_lv)
return lall(eq(in_expr, norm_in_lv), eq(out_expr, norm_out_lv))
srng = at.random.RandomStream(0)
Y_rv = srng.normal(1, 1)
a_lv = var()
(a_expr,) = run(1, a_lv, normaleo(Y_rv, a_lv))
print(eval_if_etuple(a_expr))
# TypeError: too many positional arguments
import aesara.tensor as at
from aesara.graph.unify import eval_if_etuple
from unification import var, unify, reify
from etuples import etuple, etuplize
from kanren import run, lall, eq
def normaleo(in_expr, out_expr):
mu_lv, std_lv = var(), var()
rng_lv, size_lv, dtype_lv = var(), var(), var()
norm_in_lv = etuple(etuplize(at.random.normal), rng_lv, size_lv, dtype_lv, mu_lv, std_lv)
norm_out_kwd_lv = etuple(etuplize(at.random.normal), mu_lv, std_lv, rng=rng_lv, size=size_lv, dtype=dtype_lv)
return lall(eq(in_expr, norm_in_lv), eq(out_expr, norm_out_kwd_lv))
srng = at.random.RandomStream(0)
Y_rv = srng.normal(1, 1)
a_lv = var()
(a_expr,) = run(1, a_lv, normaleo(Y_rv, a_lv))
print(eval_if_etuple(a_expr))
# normal_rv{0, (0, 0), floatX, False}.out But now this does not work the other way: b_lv = var()
(b_expr,) = run(1, b_lv, normaleo(b_lv, Y_rv))
# ValueError: not enough values to unpack (expected 1, got 0) We would not need the kwargs in the def __call__(self, rng, size, dtype, *args) Is there an easy workaround for this? |
The problem appears to be at the In your first example, the goal succeeds and returns the following: a_expr
# e(
# e(aesara.tensor.random.basic.NormalRV, 'normal', 0, (0, 0), 'floatX', False),
# RandomGeneratorSharedVariable(<Generator(PCG64) at 0x7FB710030BA0>),
# TensorConstant{[]},
# TensorConstant{11},
# TensorConstant{1},
# TensorConstant{1})
If the last step was a_expr[0].evaled_obj.make_node(*a_expr[1:])
# normal_rv{0, (0, 0), floatX, False}(RandomGeneratorSharedVariable(<Generator(PCG64) at 0x7FB710030BA0>), TensorConstant{[]}, TensorConstant{11}, TensorConstant{1}, TensorConstant{1}) Normally, We could change those Another option is to not use from dataclasses import dataclass
from aesara.graph import Op, Variable
from cons.core import ConsError, _car, _cdr
@dataclass
class MakeNodeOp:
op: Op
def __call__(self, *args):
return self.op.make_node(*args)
def car_Variable(x):
if x.owner:
return MakeNodeOp(x.owner.op)
else:
raise ConsError("Not a cons pair.")
_car.add((Variable,), car_Variable)
def car_MakeNodeOp(x):
return type(x)
_car.add((MakeNodeOp,), car_MakeNodeOp)
def cdr_MakeNodeOp(x):
x_e = etuple(_car(x), x.op, evaled_obj=x)
return x_e[1:]
_cdr.add((MakeNodeOp,), cdr_MakeNodeOp)
y_et = etuplize(Y_rv)
y_et
# e(
# e(
# __main__.MakeNodeOp,
# e(
# aesara.tensor.random.basic.NormalRV,
# 'normal',
# 0,
# (0, 0),
# 'floatX',
# False)),
# RandomGeneratorSharedVariable(<Generator(PCG64) at 0x7F8FA11129E0>),
# TensorConstant{[]},
# TensorConstant{11},
# TensorConstant{1},
# TensorConstant{1})
y_et.evaled_obj
# normal_rv{0, (0, 0), floatX, False}.out There are a few other ways this could be done, but those should illustrate the basics of any viable approach. |
I see, so I can add this logic in Aesara and add a |
aemcmc/conjugates.py
Outdated
from unification import var | ||
|
||
|
||
def beta_binomial_conjugateo(model_expr, observation_expr, posterior_expr): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The underlying relation isn't clear from these arguments. For example, why is there an observation_expr
? It seems like this should be implementing an equality with only two terms.
Yes, but add a |
7961626
to
b4c71d7
Compare
aemcmc/conjugates.py
Outdated
conjugatesdb = OptimizationDatabase() | ||
|
||
|
||
def beta_binomial_conjugateo(observation_model_expr, value_expr, posterior_expr): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just like my previous comment, it's not clear why this relation should have three inputs instead of two. We need our relations to directly represent equalities, so that they can be applied generally.
In this PR we add a relation that represents the application of Bayes theorem to a binomial observation model and a beta prior. The relation is between the beta-binomial model, the observed value, and the closed-form posterior for the parameter. This will allow
aemcmc
to sample from closed-form posteriors whenever applying the Bayes theorem returns a known distribution. I have often seen people using pyMC3's sampler for such simple models, so there is no doubt this could be useful.Similarly to Symbolic PyMC we also introduce a "conjugate" kanren
Relation
object that will store this goal and similar ones.Related to #4