New parameter handling for models, and other changes#1086
Conversation
|
A comment unrelated to the structural changes in this PR: can we add a E.g. in #1048 it's not clear to me what |
|
I have to admit that I don't fully understand the advantages / disadvantages of the change proposed here. Just one observation: intuitively I would have thought a Here's a high-level description of what RooFit, the most advanced (and most popular / used I think) modelling / fitting framework used by physicists does: PDF Maybe before merging this we can increase the number of unit tests to make sure this works well for all common use cases, e.g. for linked parameters and constraints and composed models? |
|
@piti118 has a lot of experience from writing iminuit and probfit. |
|
Regarding the addition of a In this design the model still stores references to parameters. But the value referenced by the model is just a floating point value or an array or whatever the parameter's value is. The This could be changed of course--the model could store a Either way, another point in this PR is that there needs to be a Parameter descriptor that is distinct from the Parameter instance int that it works like a property--it defines what parameters are available on a model, and enables things like assigning a parameter value as a float, for example, without having to manually create a |
|
I still need to look more at this since I don't fully understand the |
|
I think if you want a pretty good demonstration of the advantages of this approach take a look at the roations module: vs. https://github.com/iguananaut/astropy/blob/master/astropy/modeling/rotations.py#L29 It requires a lot less work to implement a model, and with fewer opportunities for mistakes. This also addresses #980 on some level, albeit indirectly. It would be considerably easier now to write a sort of |
|
We avoid this kind of declaring parameter in iminuit and probfit. Though this makes things very clear think it's just too repetitive. iminuit and probfit take advantage of introspection for funccode.co_varname (see iminuit.describe) so that user do not need to declare parameter like that. |
|
@nden The approach used here of using Compare, for example, this (with what the to this: |
|
@piti118 It's not terribly repetitive if subclassing existing models. It also provides a capability for a great deal more expressivity such as attaching descriptions, custom getter/setters, and default constraints to each parameter. That said, I've already written here that using introspection is also perfectly valid as a means of quickly generating new models. In fact, fitters could do just like iminuit appears to do and, when passed in a function, create a Model instance out of it via introspection. But for common functions like those that |
|
Now I have time to write a long reply. Here is what I was considering whether to go RooFit style or PyMinuit style.
Subclassing would create another performance problem too. I think it does a bit more expensive name look up.
IMO PyMinuit style has much less learning code. User can get started right away. There is nothing user need to know about the internal of PyMinuit to get started. Repetitiveness should be avoid. I absolutely abhor RooRealVar etc. I hate having to tell it again what the parameter names are (and which order).
This could be done with both styles though. RooFit is obvious on how to implements this. Probfit relies on FakeFunc object to replicate func_code. However, this should be obvious from user point of view which one they share too. I think this could be made obvious in both. probfit use same-name policy. But if one rely on shared parameter to be the same object, it should be obvious to user that sometime event hough parameters have the same name they aren't really shared in the fit.
x = lambda p: (p-2)**2
x.constraints = {'p':(0,1)}
One suggestion though Parameter constructor could take another parameter as an object so that it automatiaclly accumulate the parameters class Gauss:
def __init__(self):
mu = Parameter('mu',self)
...
#in Parameter
class Parameter:
def __init__(self, name, acc=None):
acc.param_name.append(name)
...etc... |
|
Under this proposal users can get/set parameter values using normal attribute access. For example: etc. This is slower because it's going through the descriptor lookup process, but the advantage is that it can also transform the value, such as how the Rotation models covert between degrees and radians. But then in the @nden's implementation already does something like this by shoving each parameter into an array and then using it in Numpy expressions, so it doesn't really matter much either way how the parameters are stored in the object-oriented interface. |
|
That's not what I meant though. The problem is that chi^2 minimization will need to call this |
|
I see what you're saying. This can still be gotten around though by having a means on the model to return a list of parameters (the fitter only needs to look this up once): and then the fitter can call this repeatedly, passing in the parameter set each time and avoiding the repeated attribute lookups. This has little to do with my implementation v.s. the existing one in the modeling package, though the point you bring up here is a a good one to keep in mind whichever way we go. The main point of my changes here is to just make it easier to define the parameters on a function when using a class is desired. Taking that heavier-weight class and turning into a function that's fast to evaluate is trivial--the function itself could even be written in Cython or something. |
|
Since a lot of the discussion now is about speed ... it's probably easiest to just give it a try (see #1095) to make sure the design does eventually support fast modeling / fitting. |
|
In the current implementation |
|
Just FYI. Dictionary argument unpack is quite expensive though; you probably want to do tuple unpack instead. def __call__(self, x, *arg)iminuit bottleneck right now is actually the tuple unpacking. |
|
Doesn't necessarily even need to do that if the number of parameters is fixed. For models with a variable number of parameters hypothetically one can auto-generate a function with a fixed number of arguments once all the parameters are fixed (though it would have to be updated every time the parameters are changed). But that sort of strikes me as overoptimization, at least for this package, at least for now. |
|
Just giving my 2 cents though. I'd recommend you thought about the whole process how the fitter will call the function; how will it manipulate the parameters. I do not know what kind of fit astro physicist typically do. If it's just binned chi^2, it probably doesn't really matter but when you have to do things like unbinned LH, these name lookup will make the fitting package unusable. The proposal is a subset of what RooFit does in RooRealVar etc. RooFit has limit and unit built in to parameter too IIRC. |
|
Here is a good concern: how do you combine models. How would this model do it: sharing Parameter object or sharing the name? |
|
@piti118 I appreciate your feedback on this, but most of the points you're bringing up are outside the scope of this PR, the purpose of which is to offer some, what I think are improvements, to the existing API we have implemented in Astropy. This is a high-level object oriented API meant to be user-facing. The details of, for example, how to efficiently fit parameters to a function is something that still needs to be explored but those are lower-level details than this PR is meant to address. Combining models is mostly outside this PR's scope as well. Whether this PR is accepted either in part or in whole, the Astropy modeling package has to address the issues you're bringing up. I would propose moving this discussion to astropy-dev mailinglist where I'm sure more of your input would be greatly appreciated. But for now I'd like to limit discussion on this PR to comparing it with the current implementation in master and the relative weaknesses or strengths of both versions of the API. |
|
I have looked now at Erik's proposal in some more detail and ran some tests. First, this is not an API only change. The fundamental idea on which this package was based has changed. And this is that The part which changes the API is the I played a little bit with a "mixed" implementation - one which uses the |
|
I didn't do any performance testing on the Parameters list, but that makes sense that it's too slow. I still find it problematic, however, that parameter values are copied between the Model object and the Parameters object. The problem being solved there is that it's too fragile to have to keep these two objects in sync with each other, when really the purpose of the Parameters object is just to provide a view of the parameter values that's easier for the fitting routines to use. One other possible implementation I mentioned, I think, in commit in which I changed the I'll see if I can come up with an implementation to demonstrate this idea. Either way, as @nden wrote the changes to |
|
@iguananaut: So I put up a new API PR up at astropy/astropy-api#9 and feel that I'm proposing exactly the same thing as you. Do you mind having a quick look? |
|
I rebased my original PR on top of the (more or less) latest master. I didn't change or updated anything so several tests should be broken. Now I can begin fixing the things that need fixing to work with the several new tests (clean up handling of constraints, mainly) and then I need to address the performance concerns, especially, though not limited to the |
|
Ack, ignore the last push of this branch. I accidentally overwrote the version of the branch that I had rebased on master due to git not being configured as I intended on this system. Hoping I can fix this but I don't know... |
|
@iguananaut May the git-god be with you and help you in these times of trouble. |
|
I've made a PR against this one that fixes the sphinx warnings ... it was just one docstring. |
|
One of the examples on this docs page doesn't work any more: Do we need to modify the example or is this a real issue? |
…ound an array called self._param_indices, which are the indices into the self.model.parameters array of the parameter values being fitted (in particular excluding fixed and tied parameters). The values of tied parameters are updated manually. This fixed a couple bugs, particularly that came up in the LinearLSQFitter and should fix astropy#1624.
|
@cdeil The failure of that example is documented in #1624. I already have a fix for it locally. I'm also going through the docs and updating them to better reflect what's in the PR. I'll merge in your doc fixes as well since it looks like you got some things I missed. Then once Travis is done testing I'll merge this. |
Minor fixes
…te unless the model's linearity somehow depends on its parameters (are there even any examples like that?)
Update modeling docs a bit
…hrough the docs so there are a number of changes glommed together. Much of the changes are just for formatting consistency. A few changes are to the examples so that the work with the API updates. I didn't significantly change the substance of most of the examples. I also updated some of the prose to more accurately reflect the API changes, as well as the examples for implementing models and fitters.
|
sounds good, @embray ! Maybe you can open an issue about the |
…arameter-property Conflicts: docs/modeling/fitting.rst docs/modeling/index.rst docs/modeling/models.rst docs/modeling/parameters.rst
|
I made a few more fixes to the documentation. Gonna wait for the Travis build to finish and then I'll merge. The auto-generated API documentation still needs work (true for much of Astropy, unfortunately). A particular problem I noticed, however, that in the documentation for each model, the docstring used for each Another thing that needs to be fixed is the boilerplate text describing constraints that's appended to each model's docstring. It should be added to the list of class arguments and not just appended to the end of each class's docstring. I'll see if I can figure something out for that, but if not I might just disable it for now, as it can be confusing in some cases. |
|
Sounds good! @embray - Travis is 8 builds behind, so it may take a few hours to get done. Should whoever notices first that the build passes merge, or do you need to do any final checks? |
|
It's fine as long as the build passes. I'm still concerned about whether or not the docs build will pass. |
|
Looks like it passed, so I'm pulling the trigger. Thanks for all the contributions! |
New parameter handling for modeling, and other API changes
|
🎆 |
and multiple param_dims. Now, for ParametricModels, if param_dim is not specified in the model's constructor (which will be the common case) the param_dim is automatically determined from the size of the input parameter values (which must all match). The one exception is that if param_dim > 1 and one of the parameters is passed in as a scalar, that scalar value is automatically repeated accross all param sets. The shape of each parameter value is largely irrelevant so long as it has the same number of elements as the model's param_dim. However one difference retained from astropy#1086 is that the shape of the input parameters is preserved when the user asks for a parameter's values. For example: >>> m = Const1DModel([[1, 2,], [3, 4]]) >>> m.param_dim 1 >>> m.amplitude.value array([[ 1., 2.], [ 3., 4.]]) Otherwise the shape of the parameter has no effect on evaluating or fitting the model. This fix isn't quite complete yet, as there is one test, test_object_pars, which currently fails. The behavior it tests for seems to contradict the desired behavior that @nden has suggested, and yet clearly this test worked in older versions of the code...
…al parameter values. I think this case is a bit odd since it's not possible with the Parametric1D and Parametric2D subclasses. But at least it works--this has effectively the same behavior as pre-astropy#1086 with respect to param sets
Here's an initial look at my proposed changes to the Model interface that I've made references to in #980. This is by no means final or complete, but it does have all the (existing) tests in master passing. I haven't tried it with any of the additional tests being added in #1060 yet, and I suspect many of them would fail currently.
I had two main goals behind this work, though it turned out to be even more pervasive than I'd anticipated, so several other changes came along for the ride:
__init__for models was mutating model classes (in effect, global state) every time a model instance was created, by attaching new_ParameterPropertydescriptor objects to the class. This was mostly harmless, as each model class had a fixed set of parameters. But it's still a strange thing to do in general, and could lead to odd behavior in particular withPolynomialModelobjects (where the class has parameters defined on it for coefficients that aren't defined for a particular model instance). So I wanted to fix that.Constraintsobjects as well). These include subclasses ofModelitself,Parameterobjects, and theParameterscontainer. I found that trying to keep these three classes of objects synchronized was making the code more complicated (especially in the Fitters) and contributed to the complexity of defining new models.This PR contains several changes, so allow me to summarize:
ParameterandParametersclasses so that rather holding their own copies of parameter values, all parameter values are held by the Model alone, and theParameterandParametersclasses merely provide specialized views of the parameter values._ParameterPropertyandParameterclasses have been combined into a single class. If this is decided to be too confusing they can still be broken into separate classes again, but I wanted to make them into a single class because I like that they're both called just "Parameter". More on the dual purpose and usage of theParameterclass below.Parametersclass is also somewhat simpler (though I would like to make it simpler still, or possibly get rid of it entirely and have the model store all its parameters in an array by default).PolynomialModelandOrthogPolyBaseclasses was broken out into a separatePolynomialBaseclass from whichPolynomialModelinherits. For better consistencyOrthogPolyBasewas renamedOrthogonalPolynomialModel(though if people don't like that it can be changed back no problem) and it also inherits fromPolynomialBase.**parsargument renamed to**parameters. Again, trying to be more consistent with abbreviating vs. not abbreviating.RotateNative2CelestialandRotateCelestial2Nativemodels was broken out into a separate, simplerEulerAngleRotationclass. The newParameterinterface also made this class much simpler (through the use of parameter getters/setters).Sorry again for the big cluster of changes. Some of them had to come together, but others are kind of superfluous and I can easily back them out if they're not found to be preferred.
The new
Parameterclass serves a dual purpose and requires some explanation:_ParameterPropertydid previously. But now (with the exception of most polynomial models which I'll discuss below) implementations of newModelsubclasses should list each of its properties in the class definition like so:This is instead of using the
param_nameslist. Now, this may seem redundant because you're assigning aParameterobject to some class attribute and giving the parameter a name that's the same as the class attribute. But this use ofParameterdescriptors is not yet taken to its full advantage. TheParameter()call could also, for example, provide a detailed description of that parameter, and other metadata about the parameter (required units, default constraints, etc.). It also already supportsgetterandsetterarguments that add custom wrappers to the values set to properties. For example some of the rotation models take advantage of this to convert between degrees/radians.Parameteralso works, somewhat as it did before, as a wrapper for the actual parameter value. For example when accessing theg1.amplitudeparameter on a Gaussian1D model aParameterobject is returned. This uses the same class as theParameterdescriptor, but the difference is that it is "bound" to a model instance. What that currently means implementation-wise is that theParameterinstance stores are reference to theModelinstance that's bound to as an internal attribute, and can use it internally. This is very different from the previous implementation, where the model's._amplitudeattribute was actually aParameterinstance that contained the parameter value(s). Now the_.amplitudeattribute just contains the unadorned value(s) for the parameter, but when a users accesses.amplitude(without the underscore) this returns theParameterobject that has a name and constraints, and is bound to the model, and that's how it knows its "value".So in summary, unlike the existing interface that requires manually instantiating lots of
Parameterobjects when implementing a new model, most of that work is hidden away, and instead one just assigns values directly toself.<parameter_name>. If this is unclear, just take a look at how all the built-in Model classes are implemented now compared to before.One thing that this does not yet support but could easily support is assigning a
Parameterof one model to a parameter of a different model in such a way that it copies all the constraints as well as the value. But this functionality could be easily added back in.I also haven't added fully working support for constraints yet, but only because there are very few unit tests for them. I think once #1060 is merged I can work on getting that working--it should be straightforward.
Finally one more word about polynomial models: They are different from the other model classes in that they don't have a fixed set of parameters defined at the class-level. Instead
PolynomialBaseimplements__getattr__and__setattr__to support variable sets of parameters rather than going through descriptors, but the end result is the same.