Skip to content

Compound models initial work#3231

Merged
embray merged 101 commits into
astropy:masterfrom
embray:modeling/compound
Jan 9, 2015
Merged

Compound models initial work#3231
embray merged 101 commits into
astropy:masterfrom
embray:modeling/compound

Conversation

@embray

@embray embray commented Dec 18, 2014

Copy link
Copy Markdown
Member

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:

  • Resolve question of division--right now models only support __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 (without from __future__ import division) means __div__ and otherwise means __truediv__, and never support //. I haven't made any decisions about this yet...
  • Add documentation (working on that right now)
  • Assorted bug fixes
  • Fix help() bug
  • Improve docstrings, particularly on arithmetic methods-- I noticed that in Python 3, anyways, the help() text for models actually displays something like:
 |  __add__ lambda left, right
 |      # Perform an arithmetic operation on two models.

for all the supported operators. It would be nice if this returned a nicer docstring.

  • Improve docstrings for the Mapping and Identity models.
  • Timing comparison--this would be separate from this PR but I would still like to add benchmark tests for models to the Astropy benchmark suite sooner rather than later. I started work on writing some benchmarks but had to postpone it for now. Some manually testing though suggests a slight uptick in overhead, though within an order of magnitude. Some of that will also be improved by other code improvements I have in the pipeline (improvements overall to modeling, especially with how parameters are handled, not pertaining specifically to compound models).

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 # TODO comments, 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/SummedCompositeModel classes, those are superseded by the new compound model framework, and models created with that framework do support fitting.

@mdboom

mdboom commented Dec 18, 2014

Copy link
Copy Markdown
Contributor

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.

@embray

embray commented Dec 18, 2014

Copy link
Copy Markdown
Member Author

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.

@embray

embray commented Dec 18, 2014

Copy link
Copy Markdown
Member Author

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

embray commented Dec 18, 2014

Copy link
Copy Markdown
Member Author

@nden's fix in 0d66a05 addresses some of the known bugs, though there is another bug involving compound models using AffineTransformation2D that I need to address.

@nden

nden commented Dec 18, 2014

Copy link
Copy Markdown
Contributor

@embray I've noticed that now a sliced model does not have the correct parameters (perhaps a regression?)

offx = Shift(1, name='x')
offy = Shift(1, name='y')
sclx = Scale(1, name='xscale')
scly = Scale(2, name='yscale')
rot = Rotation2D(angle=60, name='rotation')
m = (offx & offy) | rot | (sclx & scly)
sm=m['xscale': 'yscale']
sm.parameters
Out[61]: array([ 1.        ,  1.        ,  1.04719755,  1.        ,  2.        ])

Evaluation of sm fails.

@embray

embray commented Dec 18, 2014

Copy link
Copy Markdown
Member Author

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.

@embray

embray commented Dec 18, 2014

Copy link
Copy Markdown
Member Author

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.

@embray

embray commented Dec 18, 2014

Copy link
Copy Markdown
Member Author

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 yscale. That example was never meant to work--the end result is something like:

(Shift & Shift) | Rotation2D | Scale

where the result of Rotation2D, which has two outputs, is being passed to Scale which has only one input, so it doesn't make sense to begin with and is expected not to work. There are two possibilities for how to handle this:

  1. When taking such a nonsensical slice, simply raise an exception immediately.
  2. It might be possible (and I think nicer) to implicitly convert this to the equivalent of:
(Shift & Shift) | Rotation2D | (Scale & Identity(1))

So the x output from Rotation2D is scaled, while the y output is passed through. I'm not sure how easy that would be but it's worth looking into. In the short term making it erroneous (option 1) will probably be easier.

@nden

nden commented Dec 18, 2014

Copy link
Copy Markdown
Contributor

Yeah, I was also thinking how to handle these cases. Another one is sm = ['y':'rotation'] . There are other ways for this to fail depending on the type of model combination, so may be requiring users to be explicit and raising an Exception is better than trying to guess the correct thing and supply an Identity model. It's a matter of matching n_inputs with n_outputs, right?

@embray

embray commented Dec 18, 2014

Copy link
Copy Markdown
Member Author

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.

@cdeil

cdeil commented Dec 18, 2014

Copy link
Copy Markdown
Member

Is this for 1.0 or later?

@embray

embray commented Dec 18, 2014

Copy link
Copy Markdown
Member Author

It should be for 1.0.

@astrofrog

Copy link
Copy Markdown
Member

@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.

@astrofrog

Copy link
Copy Markdown
Member

@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.

@embray

embray commented Dec 19, 2014

Copy link
Copy Markdown
Member Author

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.

@astrofrog

Copy link
Copy Markdown
Member

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.

@embray

embray commented Dec 19, 2014

Copy link
Copy Markdown
Member Author

I've tracked down the main bug in slicing. It's extremely subtle. Updating coming soon.

@embray

embray commented Dec 19, 2014

Copy link
Copy Markdown
Member Author

Meanwhile it seems like there's a bug with my isinstancemethod function on Python 3, which could be the root of some other issues. It's strange though, I tested that very extensively on all Python versions when I first wrote it so I don't know what's causing it to fail now.

@cdeil

cdeil commented Dec 19, 2014

Copy link
Copy Markdown
Member

There's still the [doctest] astropy.utils.introspection.isinstancemethod failure on Python 3:
https://travis-ci.org/astropy/astropy/jobs/44530894#L2584

And the sphinx build fails because of these warnings (see https://travis-ci.org/astropy/astropy/jobs/44530890):

/home/travis/build/astropy/astropy/docs/api/astropy.modeling.Model.rst:131: WARNING: error while formatting arguments for astropy.modeling.Model.rename: <unbound method _ModelMeta.rename> is not a Python function
/home/travis/build/astropy/astropy/docs/api/astropy.modeling.Model.rst:175: WARNING: error while formatting arguments for astropy.modeling.Model.rename: <unbound method _ModelMeta.rename> is not a Python function
looking for now-outdated files... none found
/home/travis/build/astropy/astropy/docs/api/astropy.modeling.Parameter.rst:12: WARNING: py:obj reference target not found: Parameter.__init__

This is huge ... thanks @embray! I will try this out today.

@cdeil

cdeil commented Dec 19, 2014

Copy link
Copy Markdown
Member

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 _tree member and maybe by not having to navigate the tree myself, e.g. by model part name?
Also printing a string representation of the tree (in this case signal + background) would be very nice for complex composite models.

Comment thread astropy/modeling/core.py Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)).

@cdeil

cdeil commented Dec 19, 2014

Copy link
Copy Markdown
Member

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!

@embray

embray commented Dec 19, 2014

Copy link
Copy Markdown
Member Author

@cdeil

There's still the [doctest] astropy.utils.introspection.isinstancemethod failure on Python 3

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 __metaclass__ which won't work on Python 3, so I need to use six.add_metaclass for that example, I guess. Not ideal but oh well.

I'll fix the other doc issues as I work on updating the docs for this.

Looks like composite models always make copies of their parts.

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.

Is there a way to do this without accessing the private _tree member and maybe by not having to navigate the tree myself, e.g. by model part name?

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 __repr__ shows some useful information. First let me say something about classes, then I'll get back to your specific example. Using your example, it would look something like:

>>> 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 + background

The only problem with that right now is that the __repr__ doesn't display as much useful information. There's still a compound model class underlying compound, so you can get that info by entering:

>>> 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 __repr__ and then using indexing is pretty good, but that still doesn't give a good API for introspecting the expression itself).

@embray

embray commented Dec 19, 2014

Copy link
Copy Markdown
Member Author

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?

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 A has a parameter amplitude and we write:

>>> 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.
… 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).
…tains models that have no parameters (such as Mapping or SIP or a few of the projection models)
@embray

embray commented Dec 31, 2014

Copy link
Copy Markdown
Member Author

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.

@astrofrog

Copy link
Copy Markdown
Member

@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! 😄

embray added a commit that referenced this pull request Jan 9, 2015
@embray embray merged commit 33d661e into astropy:master Jan 9, 2015
@embray embray added this to the v1.0.0 milestone Jan 9, 2015
@mdboom

mdboom commented Jan 12, 2015

Copy link
Copy Markdown
Contributor

🎉

embray added a commit to embray/astropy that referenced this pull request Feb 4, 2015
… 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants