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
New: Linear fitting 2.0 #1462
New: Linear fitting 2.0 #1462
Conversation
Right, I think this is ready to be reviewed. If people would like to test it, then try: %matplotlib qt4
import hyperspy.api as hs
import numpy as np
eds = hs.datasets.example_signals.EDS_TEM_Spectrum()
eds.add_elements(["Cu"])
eds2 = hs.stack([eds, eds])
eds2.data = np.stack((eds.data for _ in range(200)))
eds2.get_dimensions_from_data()
m = eds2.create_model()
m.remove(0) # Remove non-linear background
m.multifit(fitter="lsq_linear", bounded=True) |
I'm tempted to rename "lsq_linear" to "linear" instead of the numpy.linalg.lstsq solver. The lsq_linear supports bounds and seems to be faster. (Thanks @tjof2!) |
I would certainly recommend scipy's |
Next big question: Documentation? Building on http://hyperspy.org/hyperspy-doc/current/user_guide/model.html#fitting-the-model-to-the-data - it would be good to have a brief summary of when (I'm well aware I was intending to update it myself as to why one should choose |
I would appreciate help on estimating the chisq and red_chisq parameters, as I don't really know anything about estimating errors with fitting. |
Proposed documentation here |
@thomasaarholt I added some paragraph breaks and a bit about MLE (which I think is worth mentioning) |
Absolutely. I was hoping someone would notice my lack of mentioning MLE and feel like they should add it. Thanks :) Currently the acronym used for MLE is "ml". Should we stick with MLE in the text, or change it? Could put |
@francisco-dlp thoughts? MLE is more common as the acronym I think, but leaving it as |
463e701
to
ea6e477
Compare
Rebased from the linear_parameters branch/PR. |
1aed949
to
5cec013
Compare
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.
Looks good except for the comments above and:
- When the model includes convolution (e.g. eelsmodel with low-loss), a linear model remains linear but the component's data must be convolved with the low-loss.
- Instead of using the linearity check only to raise an error, wouldn't it be useful to use it to automatically detect when the model is linear and use the
linear
fitter instead ofnlls
? Some may not be aware of the possibility of fitting faster a linear model, so if HyperSpy does it for them it'll be welcome as it's just a couple of lines ahead.
hyperspy/model.py
Outdated
-This could use the previous function to reduce typing- | ||
""" | ||
self._set_p0() | ||
deactivated_components = [] |
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.
deactivating components should be placed in a try/except/finally statement so that, if something goes wrong, the model doesn't end up in a funny state e.g.
try:
# Deactive components
# do something
except:
raise
finally:
# reactivate components
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.
It looks that this method needs to be updated with #1453
hyperspy/model.py
Outdated
@@ -838,6 +840,67 @@ def _model_function(self, param): | |||
to_return = self.__call__(non_convolved=False, onlyactive=True) | |||
return to_return | |||
|
|||
def get_raw_signal_for_linear_fitting(self): |
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.
All the new model methods should be private as they aren't for use consumption.
hyperspy/model.py
Outdated
signal_to_fit_to = signal_to_fit_to / self.signal.axes_manager[-1].scale | ||
return signal_to_fit_to | ||
|
||
def check_all_active_components_are_linear(self): |
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 needs updating with the new Component.is_linear
hyperspy/model.py
Outdated
return False | ||
return True | ||
|
||
def get_nonlinear_components(self): |
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 could be a property e.g. nonlinear_components
. Also why not a linear_components
property? I would be useful for users to know what's making their model nonlinear.
hyperspy/model.py
Outdated
break | ||
return nonlinear | ||
|
||
def check_and_set_linear_parameters_not_zero(self): |
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.
Make it private
hyperspy/model.py
Outdated
fitter = preferences.Model.default_fitter | ||
if self.check_all_active_components_are_linear(): | ||
fitter = "linear" | ||
else: fitter = preferences.Model.default_fitter |
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.
Add new line to make it look better
hyperspy/model.py
Outdated
signal_axis = self.axis.axis[np.where(self.channel_switches)] | ||
component_data = np.array([component.function(signal_axis) | ||
for component in self if len(component.free_parameters) > 0 and component.active]) | ||
print(len(component_data)) |
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.
Remove debugging print
hyperspy/model.py
Outdated
output = linear(component_data.T, y) | ||
fit_coefficients = output["x"] | ||
self.p0 = tuple([oldp0*fit_coefficient for oldp0, fit_coefficient in zip(self.p0, fit_coefficients)]) | ||
self.fit_output = output |
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.
What about std?
hyperspy/model.py
Outdated
params, residuals, rank, singular = linear_lstsq(component_data.T, y) | ||
fit_coefficients = params | ||
self.fit_output = [params, residuals, rank, singular] | ||
self.p0 = tuple([oldp0*fit_coefficient for oldp0, fit_coefficient in zip(self.p0, fit_coefficients)]) |
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.
What about std?
hyperspy/model.py
Outdated
output = nnls(component_data.T, y) | ||
fit_coefficients = output[0] | ||
self.p0 = tuple([oldp0*fit_coefficient for oldp0, fit_coefficient in zip(self.p0, fit_coefficients)]) | ||
self.fit_output = output |
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.
What about std?
Actually, hold off on checking this out. I'm making some more changes. |
Just out of curiosity, I was wondering how far you got with this. In EELSMODEL the linear fit has changed a lot also for core loss fitting, but it is essential that you make the fine structure and the powerlaw linear. In that case most core loss tasks can be fully linearised which leads to high speed and more importantly to a single convergence point and no longer any local minima (very desirable). For hints on the power law, check out this component and the fine structure for linear fitting can be found here If these things are already in, all the better. |
Hi @joverbee! I hit a bit of a dead-end with this.
...I realised that the least squares fitting was now faster than my linear approach. That's on a pixel by pixel fitting approach where the function is rerun on the spectra in each pixel. Now, feeding the fitting function a multidimensional array, and fitting everything simultaneously would be a lot faster (and is where I was working towards), but since I had a bug somewhere, I didn't want to go ahead with the multi-pixel fitting. This would also mean adding a new method to the api, and I wasn't sure I wanted to go that route before I had something that actually worked properly on a given spectrum. I may very well pick this up again at some point and look at it with fresh eyes, but for now it's in my backlog. I did not consider the eels fine structure in my approach. I'm not sure how it is modelled (I tried looking at your link, but it wasn't quite clear), since I've never needed it myself. I did bear in mind the powerlaw, and was thinking of ways to elegantly it on a component-by-component basis when a particular component demands further calculation to make it linear (in this case, take the log). On the other hand: eelsmodel looks interesting! What is it, exactly? :) A readme file would clarify the repo a lot! |
I've also been meaning to clear up the (rather embarassing) commit history on this, but haven't gotten round to it yet. |
@thomasaarholt I don't see how least squares can ever be faster than linear fitting as in linear fitting you do one matrix inversion while in least square you do an itterative update of many of them. If this is the case, then you are still doing something wrong. For EELSMODEL the gain is enormous and the added benefit of a single convergence point is very important as well. Sorry for the code in the fine structure paert being a bit opaque, but the idea is this: Give the model some extra shape freedom in the edge onset region of a Hartree Slater model by -adding- a piecewise linear (or spline) function to a limited region near the onset. Together with the linear convolution with the low loss this makes a very good description of the actual shape of EELS edges. In earlier times I had this fine structure -multiplied- with the atomic cross section but even though it makes physical sense in terms of density of states, it makes the model nonlinear and forces us into nonlinear optimisation... hope this help and I sympathise with your struggle with Github and commiting to big projects, I am also far from proficient and got into big difficulties before which had nothing to do with the actual coding. |
To clarify: The actual inversion calculation is of course super fast. It is the matter of constructing all the components to fit which is slow. For each component in the model I need to check if it is linear, if it should be convoluted, if it has a constant term, etc. This essentially constitutes a for-loop which needs to run at every new pixel, and is a bit slower than I'd like. The solution to this is calculating the components using the parameter.map['values'], and similar, which returns an array for a given components parameters, across all pixels in the navigation space. Then instead of performing the (slow) component calculation and the (fast) linear fitting for each pixel, I'll be calculating the component values only once, and linear fitting only once. Thanks for the clarification on the EELS model! |
4cc6bce
to
2dcc03f
Compare
@joverbee Would you mind clarifying how you linearise your (double) power law background? |
Documentation 2.0. Thanks @tjof2 clearer docs corrections to docs Deleted some unnecessary lines from linear fitting docs
5546fac
to
20af8d4
Compare
MODEL: Underscore bug MODEL: default fitter no longer from prefs MODEL: Working with twins but not in pseudocomponents fix twin situation fix twin situation moved test files removed old case for appending components Change test case to use artificial data add standard error Remove old code TEST: Add and remove test cases, and fix hydrogenic Cleaned up linear fitting procedure, simplified linear model estimation procedure and formatted code Removed unused code, cleaned up with pep8 moved error message Rewrote component function calls to support multidimensional models fixed small bugs with previous commits more fixes to the previous, now working made expression fix_free separation 2D-able corrected shape Add 2D model support for linear fitting Added test cases for model2D linear fitting model.py typo hotfix for Zanetta large multifit update - mostly working need to fix expression multi getting closer minor new twin tactic much changes beginning of EELS CL support further changes to eels modified convolve. Nearly there further GOS changes hello fixed Model2D
updated bleasdale to take parameters in function call remove print readied most comps for linear multifit minor fixing components and adding tests
20af8d4
to
2e020c6
Compare
remove unused code docstring and PEP improvements update docs remove old lsq_linear reference remove old import fixed fit_component and signal_range
ebcc142
to
187da73
Compare
@thomasaarholt are you planning on finishing this? It would be quite handy! |
@dnjohnstone I was hoping to do so, but increasingly it looks like it's just a lot of work to get properly right and I haven't had the time to sit down and do it. @ericpre's work adding Some of the problem is that I kept expanding the scope as I went along, and so it turned into an ever-expanding workload. I still do want to finish it, because I had a lot of great discussions and help from various people that made this all very neat, but there are so many aspects to it that I'm worried that I would introduce errors that would go unnoticed for some time. @dnjohnstone do you have a specific usecase in mind? |
@thomasaarholt, do you think that this could be ready for the 1.6 release? |
I spent yesterday editing this PR to support linear fitting in its most basic form. I'm submitting a new PR on this on Saturday. That should be ready for 1.6. I will then improve it by one or two PRs to support a faster implementation for certain cases (where the starting values of the components to be fit are the same across all navigation pixels), and finally as a "fit the whole navigation space at once if memory allows". |
That's great.
I didn't get this. Am I wrong in thinking that the linear model shouldn't use starting parameters?
It'll be desirable to get this for this release too. I'll be more available from next week and, if you like, I can try to help with this one. |
Regarding your question - you're sorta correct, but the linear parameters need to be nonzero at the beginning in order to generate a component that isn't just zeroes. At the moment I check each pixel to make sure the parameters aren't zero, and if so set it to one before fitting. I could just set all values to be one before beginning the fitting. |
There may be no need to do that, you could use the |
Yes, I believe you are right. I'll incorporate that. |
@francisco-dlp something to chew on for after the "basic linear fit" PR I'm working on is finished: If the dataset is too big, one would fit on a pixel-by-pixel method. However, I think that estimating whether one has enough memory for linear fitting the entire dataset in one go can either be fragile or overestimate the amount of memory needed. Instead one could have a separate What do you think? |
Hm, how about if we always try to fit simultaneously with linear, unless there's argument like "fit_pixel_by_pixel" (not that name :p). If we run out of memory, we try to catch that and print a message informing about the above argument. |
Closed in favour of #2422. |
Description of the change
This PR adds support for multivariate linear regression in hyperspy. Linear fitting is approximately 20 times faster than leastsq for a random dataset.
A linear component is one which will only be stretched in the "up"-direction. For instance, the expression
a*x**2 + b*x + c
is linear for all parameters. It also works in 2D, for instance fora*x**2 + b*y**2
. For components that can be stretched or changed in other directions, like the Exponential componentA*exp(-x/k)
,A
may be free butk
must be fixed.It can be used with
m.multifit
in order to fit across the entire navigation space simultaneously. In this case, if all parameters have equal initial values (ifm.assign_current_values_to_all()
has been called), the components'function
need only be calculated for one pixel, and vectorised numpy can perform the regression extremely quickly (Thanks Paul Quinn!). If the components vary across the sample, for instance if an EELS CLonset_energy
has been manually changed across several regions of the sample, this is supported as well.This PR requires numpy>1.10.
Progress of the PR
.function
to support full navigation spaceMinimum Working Example