Compound models initial work#3231
Conversation
|
Thanks for posting this. I just gave it a skim through, which makes me think I probably won't have much intelligent to say until the user docs are posted. I'll probably come back and read through again at that point. |
|
That's the thing--it's such a big PR that it's hard to know what to comment on. I spent a while looking into ways I could maybe break this up into a few smaller changes, but it just wasn't practical, ultimately. |
|
If any part of this should have been separated out, it's f456a8a; this PR very much depends on it, but that change could have been made first. It was originally part of another branch where there's more work being done to simplify how parameters are handled (in such a way that will also, finally, support Quantity parameters). But I haven't had time to get that 100% working. The part pulled out in f456a8a was very helpful to have here though, more than anything else. |
|
@embray I've noticed that now a sliced model does not have the correct parameters (perhaps a regression?) Evaluation of |
|
That's odd--I'm sure that used to work, so it might be a regression, though I'm not sure where it was introduced. Probably when I reworked how compound models of model instances are handled. |
|
I haven't quite gotten to the bottom of this yet but I think it's a minor error somewhere. I have to head out for most of the rest of the day, but I think I'll be able to fix this tomorrow. |
|
Another interesting thing that doesn't work with this example is: sm = m[:'xscale']in other words, a slice using every part of that model except for the final (Shift & Shift) | Rotation2D | Scalewhere the result of
(Shift & Shift) | Rotation2D | (Scale & Identity(1))So the x output from |
|
Yeah, I was also thinking how to handle these cases. Another one is |
|
Yes--currently when combining two models in an expression there actually is no explicit checking on input/output compatibility which obviously should be. That's one of the items on my TODO list still. Since that's pretty easy I'll probably put it in before merging--should just take a few minutes. I agree that explicit is better than implicit, though at the same time I think putting in an Identity is fairly straightforward as well and might seem natural to some. I'm not sure though--if someone else thinks that's a good idea it can be changed later. |
|
Is this for 1.0 or later? |
|
It should be for 1.0. |
|
@cdeil - this is one of the high priority features for 1.0 (see https://trello.com/b/XzwPX49N/brainstorming-for-1-0). It would be nice if it could be included, if the current issues can be solved. |
|
@embray - I see you are working on the documentation now. I can review and test this tomorrow morning with several examples once the docs are up. |
|
Weird...it seems like some of the doctests ran on Python 3? I thought we didn't enable that yet... I don't have those failures locally. |
|
Tests in docstrings do seem to be running in Python 3 by default (saw this earlier this evening on a different PR, locally). Tests in the docs don't run yet. |
|
I've tracked down the main bug in slicing. It's extremely subtle. Updating coming soon. |
|
Meanwhile it seems like there's a bug with my |
|
There's still the And the sphinx build fails because of these warnings (see https://travis-ci.org/astropy/astropy/jobs/44530890): This is huge ... thanks @embray! I will try this out today. |
|
Looks like composite models always make copies of their parts. Assume you have a simple signal model with a few parameter and a complex background model with dozens of parameters: signal = SignalModel()
background = BackgroundModel()
compound = signal + background
# fitting
compound._tree.left.value # want to access signal model only.Is there a way to do this without accessing the private |
There was a problem hiding this comment.
Is there a reason to prefer this composition operator order (f | g)(x) = g(f(x)), e.g. easier or more efficient implementation?
When I tried this I first got my example wrong because I assumed the other order: (f | g)(x) = f(g(x)).
I find that syntax and the advice to read "|" as "after" as described here more intuitive.
There was a problem hiding this comment.
I think the ideal here would be if Python allowed custom operator syntax, and it were possible to define a composition operator that works in the sense you described, which I agree is familiar. As it is, the most natural choice for this among Python's built-in operators was |, and although this may be subjective I think the tendency, when using | as a composition operator, is to read it as first | last as in shell piping. So I think this should be read as building a pipeline of transformations, and not quite the same as the function composition operator a la f ∘ g = f(g(x)).
|
One thing I'd be very interested in is how to write a composite model in a library (e.g. in Astropy itself or an affiliated package) and have the parameter names and docstring turn out OK? I'll try to do this tonight, but if you could put an example in the docs how to do this, that would be great! |
Yeah, I will fix that too. I was surprised that those doctests were running on Python 3 though--IIRC we hadn't enabled that yet, but I could be mistaken (maybe it was just the .rst doctests we didn't enable yet). Anyways, that one is failing because it uses I'll fix the other doc issues as I work on updating the docs for this.
That's correct. If needed, we can later look into ways to maybe get around this, but for now it's the safest default choice.
That is a good question, and one I've brought up myself a few times (see for example here) There are a few possibilities right now. First of all, I'll note that when you make a compound model out of classes, the class >>> Compound = SignalModel + BackgroundModel
>>> Compound
<class '__main__.CompoundModel53'>
Name: CompoundModel53
Inputs: ('x',)
Outputs: ('y',)
Fittable parameters: ('param_0_0', 'param_1_0', 'param_0_1', 'param_1_1')
Expression: [0] + [1]
Components:
[0]: <class 'SignalModel'>
Name: SignalModel
Inputs: ('x',)
Outputs: ('y',)
Fittable parameters: ('param_0', 'param_1')
[1]: <class 'BackgroundModel'>
Name: BackgroundModel
Inputs: ('x',)
Outputs: ('y',)
Fittable parameters: ('param_0', 'param_1')You can see a few things going on here--I think most of it is clear, but I am definitely open to better suggestions for how to format this output. Right now it's just what was useful enough for debugging purposes, but I think there are all kinds of possibilities for something slicker (especially in the IPython Notebook). You can also see that the compound model class has a pretty uninteresting generated name. We might be able to make more interesting default names, though that could also get a bit out of hand for complicated expressions. However, you can also give your compound model a custom name like so: SignalPlusNoise = (SignalModel + BackgroundModel).rename('SignalPlusNoise')or another, I think sort of nice syntax is: class SignalPlusNoise(SignalModel + BackgroundModel):
"""Add a docstring too, why not?"""As for navigating, once you've printed out the class and see the expression and its components you can access the individual components by their index: >>> Compound[1]
<class 'BackgroundModel'>
Name: BackgroundModel
Inputs: ('x',)
Outputs: ('y',)
Fittable parameters: ('param_0', 'param_1')or if the components have custom names, you can use the names instead of the numerical index, which is of course usually preferable and a good reason to give custom names (or sometimes I refer to them as labels). All of the above applies when combining model instances like in your example: >>> signal = SignalModel()
>>> background = BackgroundModel()
>>> compound = signal + backgroundThe only problem with that right now is that the >>> type(compound)but I tend to agree that compound model instances should display more useful information in their own right. As for navigating the expression tree, as I wrote in the comment I linked to, I'd be open to ideas for how to better expose that (I think for most cases looking at the |
Right now there's not enough in the way of controlling parameter names. That's another item that's high on the TODO list, but was a bit too tricky to resolve for the first version. There are other ideas floating around associated with this. @ChrisBeaumont had the idea, which I like, of also allowing semantic hints on parameters. For example, many of the models have an "amplitude"-like scaling factor--if multiplying multiple models together it might make sense to combine their amplitudes into a single parameter, and such semantic hints would allow that. Other categories of parameters he identified included "width", "offset" and "slope", and probably others. I think it's an idea worth pursuing, though the details are still a bit hazy. In the meantime I think providing custom labels for parameters on compound models is not far out of reach. I'm open to ideas for that. Also, in the meantime, when you combine multiple models it generates some parameter names based on the names of the parameters in the sub-models, along with an index attached to it. This is a bit of a "good enough for now" kludge until there's something better. Though it's also possible to access parameters a bit more nicely by drilling down using indexing. For example if model >>> m = A(1, name='A') + B() * C()then we can access that parameter in two ways: >>> m.amplitude_0
Parameter('amplitude_0', value=1.0)
>>> m['A'].amplitude
Parameter('amplitude', value=1.0)These are just two separate "views", if you like, of the same parameter (updating one will update the other and vice versa). Ideas for how to make this even better are welcome. |
…of all things, by Sphinx autodoc (which noticed when trying to generate function signatures that the sharedmethod's im_func did not actually point to the original function, but to an unbound method
…eling documentation, including a stub for the full Compound Model documentation.
… in this is mostly repeated in the index page. The rest discusses plans for composite models that are now implemented by this PR.
…ursion, and included a regression test.
… sense that they're listed in the table of context in the opposite order than they were previously.
…onsistency with how the base class was documented.
…minology (may add to this more while going along)
… another tweak to the FLOAT_CMP output checker as to where in an output string it can expect to find a floating point number (I'm really surprised '=' wasn't already one of the delmiters for where a float could begin)
…stion here: astropy#3231 (comment) This also made a slight tweak to how the special method __call__ is handled in _ModelMeta, so that it is unnecessary for these models, that have undefined inputs/outputs until they are instantiated, to define dummy __call__ methods in order to work.
…sion is *not* use. This simply requires the __div__ special method to be defined, but only in Python 2. In this case __div__ does not come with the ambiguities it has for Python numeric types, as there is only one definition of division for models. (__floordiv__ is deliberately left unsupported).
…l outline for the remaining portions to be written.
… it passes in an additional unused argument.
implementation of compound model evaluation: 1) Changed the call signature of the functions that are chained together when creating the sequence of operations in a compound model eval. Previously this just took an *args consisting of the model inputs, but now it takes just two arguments: ``inputs`` which is a tuple of the model inputs, and ``params`` which is actually an iterator of all the parameters for all the submodels--each submodel's evaluate picks off some parameters using islice. (This algorithm could use better explanation in the code, in general.) 2) This change means that the parameter values do not need to be "baked" into the function chain, and can be passed in later. This means that the expression tree only needs to be walked once, and the resulting call chain can be saved off later for future evaluates(). Some quick testing suggests this will speed up evaluate roughly 4 times (possibly more depending on the complexity of the expression), which will be very useful for making fitting more efficient (once I can get back to finishing my improvements to the fitter implementation). 3) The change in the operator lambda signatures, as well as a few other tweaks here and there reduced the number of tuple packaing/unpacking operations. These don't have enormous overhead on their own, but it can't hurt.
… supported model operators. Adding this actually invalided most of the test_slicing_on_instances_2 test, since many of the slices it tested actually returned invalid, unevaluatable models (the error message when this occurs during slicing might be worth improving). I kept test_slicing_on_instances_2, but added a corollary that is essentially the same but doesn't run into this specific problem, so that a few more cases can still be tested.
…e wasted from the other example plot; fixed more doctests with float output
…here: astropy#3231 (comment) I agree it was a bit overload. The main docs still start out by explaining compound model class creation first, since I think it's helpful for understanding how the sausage is made. But it's made more clear that this is 'advanced' usage.
…robably more to be said here (and more examples) but this is a fair amount to start with. The Identity and Mapping classes still need docstrings though (but are documented in the text).
0ae8a95 to
8bdb9bc
Compare
…tains models that have no parameters (such as Mapping or SIP or a few of the projection models)
|
If there are no further comments on this I'd like to merge soon-like so we can have it for AAS? If there are any followup comments once more people get a chance to look at it we can address those later I think. Not all of this is set in stone. |
|
@embray - yes, please feel free to merge this, as I think it is definitely ready for user testing. Thanks for all your work on this! 😄 |
|
🎉 |
… to remove this as part of astropy#3231 but clearly slipped...) There is however code in the wild that depends on this functionality--but maybe if needed it is possible to refer back to older versions of the docs.
I'm creating this PR so that we have something to look at regarding compound models. There's still work to be done on this feature, but this is a good start which brings in the majority of the new functionality. There is still some more work needed before this can be merged, specifically:
__truediv__, but do not support__div__. Perhaps it should just support both and treat them equivalently. Since models don't really support integer-only operations there's not much sense in supporting integer division separately. So maybe to avoid ambiguity it would be better to just support/which on Python 2 (withoutfrom __future__ import division) means__div__and otherwise means__truediv__, and never support//. I haven't made any decisions about this yet...help()bughelp()text for models actually displays something like:for all the supported operators. It would be nice if this returned a nicer docstring.
MappingandIdentitymodels.The bug fixes refer to some issues @nden brought to light. They could be considered corner cases, but it's still important to try to fix them sooner rather than later, though they could in principle be fixed separately from this PR.
There are several other issues with this PR that are mostly issues in the code that I would like to address sooner rather than later, but they aren't issues that affect the functionality a great deal from users' perspectives (with one exception, which is incomplete handling of parameter constraints--constraints can be added to new compound model instances just fine, but I haven't worked out a system for inheriting constraints from the models that compose a compound model). Most of these code issues can be dealt with later. While I would definitely be interested in specific comments on the code and implementation, just keep in mind that there are many known areas for improvement (most of which I think are mentioned in
# TODOcomments, but perhaps not all).TL;DR I'm not happy with the overall quality of this, though I am pretty happy overall with the user-facing functionality, and also more interested in comments on that.
This PR also addresses #1054--although it does not add support for fitting to the old
Serial/SummedCompositeModelclasses, those are superseded by the new compound model framework, and models created with that framework do support fitting.