-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Generalized Periodic Kernel #6722
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
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 @jahall for the PR! It looks very nice. I added a couple of nitpicky comments above, but I do have one bigger question. Do you want GeneralizedPeriodic
to take an instance or a class as base_kernel
? I think that the current implementation assumes the former, but I think that the latter might be better. We would have to create the base_kernel
instance during initialization, but that doesn't seem like a bad choice to me. That would allow us to keep a consistent __init__
signature across subclasses and also ensure that the base_kernel
has consistent input_dim
and active_dims
pymc/gp/cov.py
Outdated
original inputs through the transformation u = (cos(x), sin(x)). | ||
""" | ||
|
||
def __init__(self, base_kernel: IsotropicStationary, period): |
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 would try to mimic the initialization signature that the Periodic
kernel uses.
def __init__(self, base_kernel: IsotropicStationary, period): | |
def __init__(self, input_dim, base_kernel: IsotropicStationary, period, ls=None, ls_inv=None, active_dims=None): |
I'm not sure if we want to keep the ls
, ls_inv
, etc or just use a **kwargs
unpacking though.
By the way, is your idea to pass in an instance of an IsotropicSationary
kernel, or the class? In the former case, you would need to make sure that the input_dim
, and other parameters match with the base_kernel
. In the latter case, you can construct the base_kernel
inside of __init__
, and make sure that you get consistent active_dims
and input_dim
.
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 like where you're going with this. If we went with this approach could we then simply "enhance" the existing Periodic
kernel instead of creating a new one? e.g.
def __init__(self, input_dim, period, ls=None, ls_inv=None, active_dims=None, base_kernel_class: Type[IsotropicKernel] = ExpQuad):
super().__init__(input_dim, ls, ls_inv, active_dims)
self.base_kernel_class = base_kernel_class
self.period = period
@property
def base_kernel(self) -> IsotropicKernel:
return self.base_kernel_class(self.input_dim, ls=self.ls, ls_inv=self.ls_inv, active_dims=self.active_dims)
...where the instance is instantiated on-demand so we're not storing input_dim
, ls
, ls_inv
in two places? Also means if someone changes any of the attributes on the class, they will be sure to be reflected in the base kernel. But I don't have super strong feelings..
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.
@lucianopaz - re-worked the change such that the original Periodic
kernel is now extended in a backward compatible way. And the base kernel is passed as a class.
pymc/gp/cov.py
Outdated
f1 = X.dimshuffle(0, "x", 1) | ||
f2 = Xs.dimshuffle("x", 0, 1) |
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 really dislike dimshuffle
! Could we switch to numpy syntax here?
f1 = X.dimshuffle(0, "x", 1) | |
f2 = Xs.dimshuffle("x", 0, 1) | |
f1 = X[..., :, None, :] | |
f2 = Xs[..., None, :, :] |
you can also use numpy.newaxis
if you find that more readable
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.
There's also pytensor.tensor.expand_dims
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.
Yes absolutely, I had just copied and pasted what was currently in Periodic
pymc/gp/cov.py
Outdated
f2 = Xs.dimshuffle("x", 0, 1) | ||
r = np.pi * (f1 - f2) / self.period | ||
if hasattr(self.base_kernel, "full_r"): | ||
r = pt.sum(pt.abs(pt.sin(r) / self.ls), 2) |
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.
Should we keep the multiplying 4 or not? I see that GPflow chose to leave it out and have it absorbed by the ls
.
My ruff derivation of the argument that goes into r
is
From looking at Periodic
it seems as though we had dropped it long ago, so it might be irrelevant
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'd prob just go with what is consistent with current implementation.
… creating a new one; and implemented such that the base kernel is passed by class rather than by instance
… creating a new one; and implemented such that the base kernel is passed by class rather than by instance
This looks interesting! Sorry for the nit-picking, but I just want to point out those kernels technically aren't isotropic, as the length scale |
This allows proper seeding in CustomDists with Scans
@dehorsley This is a great point - let me try and roll this functionality into the |
How different is this from this example here? https://www.pymc.io/projects/examples/en/latest/gaussian_processes/GP-MeansAndCovs.html#constructing-periodic-using-warpedinput |
Also, btw, thanks for adding the typing! long overdue |
pymc/gp/cov.py
Outdated
""" | ||
|
||
def __init__(self, factor_list): | ||
def __init__(self, factor_list) -> None: |
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.
Nitpick, but python never allows __init__
to return anything, so it must always be None
.
I've never seen __init__
return type annotated
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.
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.
According to this comment in that thread you linked, mypy assumes None by default if there are any other typed arguments in __init__
: https://stackoverflow.com/a/67693892
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.
Seems it assumes None
provided one or more of the args are type-annotated - otherwise its ignored from type-checking. The PEP advises
Note that the return type of init ought to be annotated with -> None. [...]
However, I don't feel too strongly about it, so happy to remove these annotations.
Gotcha @jahall. Regardless of implementation, I'm all for this functionality being easier to find/use for people. I think it'd be nice to double check that the efficiency claim is true though. Maybe it's cleaner to actually implement There are other uses where it'd be great to have the One thought too if you don't mind, is if you split out the typing improvements into a separate PR I think we could pretty much get that merged in immediately. |
… creating a new one; and implemented such that the base kernel is passed by class rather than by instance
… creating a new one; and implemented such that the base kernel is passed by class rather than by instance
@bwengals I've separated the type-hint changes out into this PR which should be ready for review. I agree that the ideal signature would be inline with Also agree that |
…all/pymc into feature/generalized-periodic
… creating a new one; and implemented such that the base kernel is passed by class rather than by instance
… creating a new one; and implemented such that the base kernel is passed by class rather than by instance
This PR is to introduce a generalized form of the periodic kernel. There is no need for us to be stuck with only the periodic form of the squared exponential when we can easily extend to any isotropic stationary kernel e.g. the materns and rational quadratic.
I implemented a similar change in GPFlow a couple of years ago so the changes in this PR very much reflect that. See e.g. periodic.py and stationaries.py from that repo.
NOTE: This is currently in draft mode as I want to get input on whether we should try to extend/adapt the current Periodic kernel (as was done in GPFlow) rather than having a new kernel? And input on the implementation itself...which could almost certainly be improved.
Major / Breaking Changes
New features
Stationary
kernel now allows the specification offull_r2
(orfull_r
) instead of having to overridefull
directlyPeriodic
kernel can now accept abase_kernel_class
kwarg (defaulting toExpQuad
) and optionalbase_kernel_kwargs
parametersBugfixes
Documentation
Maintenance
📚 Documentation preview 📚: https://pymc--6722.org.readthedocs.build/en/6722/