-
-
Notifications
You must be signed in to change notification settings - Fork 59
Genextreme #84
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
Genextreme #84
Conversation
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.
Thanks for coming back to this. Looks great.
The tests in the main repo have been recently refactored to follow the same path as the code. I suggest you update it so it's already pymc compatible. Otherwise I only left a small suggestion for a direct test of the scipy
kwarg and a small logp/logcdf tweak.
# bnd = mu - sigma/xi | ||
return check_parameters( | ||
logp_expression, | ||
1 + xi * (value - mu) / sigma > 0, |
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.
This constraint depends on mu so it should probably be in a switch(constraint, logp, -inf)
. Usually value-dependent constraints are treated elemwise while parameter based constraints are treated blockwise with check_parameters
(if one fails, everything fails), and we raise an error outside of sampling.
1 + xi * (value - mu) / sigma > 0, | |
1 + xi * (value - mu) / sigma > 0, |
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.
I've implemented this now, but not precisely sure the purpose: check_parameters
is value-based, while switch(constraint...
is tensor-based, right? Anyway, please have a look.
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 point is that the logp is usually defined for every possible value
(although it can be zero for some combinations of value parameters, such as a halfnormal evaluated at -1
), so it should never "error" out. Logps can however be undefined for some parameter values, such as a halfnormal with negative sigma in which case we error out. We do that distinction in PyMC by using a switch
for 0
probability values and check_parameters
for parameter constraints
import pymc as pm
print(pm.logp(pm.HalfNormal.dist(), -1.0).eval()) # -np.inf
pm.logp(pm.HalfNormal.dist(sigma=-5), 1).eval() # ValueError: sigma must be positive
Scipy is similar, but yields nan
import scipy.stats as st
print(st.halfnorm().logpdf(-1.0)) # -inf
print(st.halfnorm(scale=-5).logpdf(1.0)) # nan
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.
Thanks @ricardoV94 . I can see this alright. Nevertheless, the tests are failing only in the pymc test environment. Here are two cases:
Allowing xi to be in the R domain (mathematically true, but not in practice). The npt.assert_almost_equal
is an absolute test of equality defaulting to 6 decimal places, and so we end up with silly outcomes like below, where the relative error is at the 61 - 47 = 15th order of magnitude down! This surely should be accepted?
AssertionError:
Arrays are not almost equal to 6 decimals
{'mu': array(1.), 'sigma': array(0.01), 'xi': array(-0.01), 'value': array(-2.1)}
Mismatched elements: 1 / 1 (100%)
Max absolute difference: 2.39777612e+47
Max relative difference: 1.26305703e-14
x: array(-1.898391e+61)
y: array(-1.898391e+61)
You can see the check from numpy here. It's worth noting that the numpy docs don't recommend using assert_almost_equal
. For example, assert_allclose
would allow a relative tolerance to be specified, rather than an absolute one (in fact both or either).
Restricting the domain of xi to the practical range of [-1,+1]. In this case running check_logp
gives:
File ~/anaconda3/envs/pymc4-dev/lib/python3.10/site-packages/_pytest/outcomes.py:
196, in fail(reason, pytrace, msg)
194 __tracebackhide__ = True
195 reason = _resolve_msg_to_reason("fail", reason, msg)
--> 196 raise Failed(msg=reason, pytrace=pytrace)
Failed: test_params={'mu': array(-2.1), 'sigma': array(0.01), 'xi': TensorConstan
t{-2.0}}, valid_value=-2.1
where it is checking for an invalid value for xi (-2). But note that it passes when called directly:
In [15]: μ,σ,ξ,x = -2.1,0.01,-2.0,-2.1 # fails check_parameters with xi = R Doma
...: in
...: logp = pm.logp(GenExtreme.dist(mu=μ,sigma=σ,xi=ξ), x).eval()
...: print(logp)
3.605170185988091
and calling check_parameters
passes
In [16]: mu = at.as_tensor_variable(floatX(μ))
...: sigma = at.as_tensor_variable(floatX(σ))
...: xi = at.as_tensor_variable(floatX(ξ))
In [17]: sigma > 0
Out[17]: Elemwise{gt,no_inplace}.0
In [18]: 1 + xi * (x-mu)/sigma > 0,
Out[18]: (Elemwise{gt,no_inplace}.0,)
In [19]: check_parameters(logp,
...: sigma > 0,
...: 1 + xi * (x-mu)/sigma > 0,
...: msg="sigma <= 0 or 1+xi*(x-mu)/sigma <= 0")
Out[19]: Check{sigma <= 0 or 1+xi*(x-mu)/sigma <= 0}.0
I did have a look at adding a -1 < xi < 1
check in check-parameters
but I couldn't get it tow work with either
xi > -1 & xi < 1
as a single check due to the tensors (is there an elementwise and? I couldn't find it in aesara docs), or- two checks,
...,x > -1, x<1,...
It would be great to have your input on this, since everything seems to be working fine until it enters the pymc test environment. Hopefully it is just a tweak to one of the conditions in the logp/logc functions perhaps?
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.
You can increase the test tolerange in check_logp/ check_locdf
, with the kwarg decimal
. Do note that you may only be checking a subset of parameter combinations when testing locally (the default is a random subset of up to 100 combinations), so you can temporarily set n_samples=-1
to make sure all parameter combinations are tested
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.
@ricardoV94 good news: Using at.and_
in check_parameters
did the trick for the restricted (but practical) domain of
I can expand to the full domain of
In any case, thank you so much for your help on this. I think it's been 11 months in the pipeline, so hopefully we can get it merged?!
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.
A relative check like the one I mentioned would compare the ratios of the two numbers to a relative accuracy of 1e-6 instead, which makes more sense.
I have thought about this sometime ago, but then faced issues. logps vary widely (and non-linearly) between 1e-700xs and 1e+x. More annoying, it's not uncommon to have logps around 0, in which cases relative comparisons or, on the other hand, more loose absolute comparisons become pretty meaningless.
If you have a more flexible solution that works in general that would be great. Certainly there are more principled ways of doing these accuracy checks.
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.
@ricardoV94 npt.assert_allcose
here allows for both a relative, rtol
and absolute atol
tolerance to be set. Example:
>>> x = np.pi*np.array([1e2,1e-1,1e-6,1e-10,1e-100,1e-700])
>>> y = (1 + 1e-6)*x
>>> npt.assert_allclose(x,y) # will assert
AssertionError:
Not equal to tolerance rtol=1e-07, atol=0
Mismatched elements: 5 / 6 (83.3%)
Max absolute difference: 0.00031416
Max relative difference: 9.99999e-07
x: array([3.141593e+002, 3.141593e-001, 3.141593e-006, 3.141593e-010,
3.141593e-100, 0.000000e+000])
y: array([3.141596e+002, 3.141596e-001, 3.141596e-006, 3.141596e-010,
3.141596e-100, 0.000000e+000])
but this does not assert:
>>> npt.assert_allclose(x,y,rtol=1e-6,atol=1e-10)
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.
Do want to try to open a PR on the main repo to change the logic then?
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.
Done: pymc-devs/pymc#6159
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.
Two more small things
@ricardoV94 it fails the tests for float32 due to differences between scipy and pymc underflows. Shall I add:
to the tests for logp and logc as per other distributions? |
Sure |
The test on the windows machine is failing with message:
You can see why I reckon relative tolerance would be good! pymc-devs/pymc#6159 Can this be accepted without this test passing as is? |
Let's just extend the |
Those are also very extreme values (pun not intended), tweaking the grid of |
@ricardoV94 that's a fantastic idea - thank you. I've just implemented that now. |
Looks good, I pushed a commit that separates the logp and logcdf tests and adds an xfail only for float32. I tested with |
Wonderful to get this finally done @ricardoV94 ; the first PR on this was a year ago! The corresponding example was also just merged, so many thanks for all your help indeed! |
Hopefully the next one will be smoother. Thanks for sticking around |
No worries - a big learning curve with long gaps between efforts! Thanks for your patience! |
This is a follow-on from #4 which after rebasing closed the PR. It would be good to merge and close this out as it has been a long time. @ricardoV94 please advise if anything outstanding.