Thanks to visit codestin.com
Credit goes to github.com

Skip to content

ENH add sparse output to SplineTransformer - #24145

Merged
ogrisel merged 37 commits into
scikit-learn:mainfrom
lorentzenchr:sparse_splines
Jun 5, 2023
Merged

ENH add sparse output to SplineTransformer#24145
ogrisel merged 37 commits into
scikit-learn:mainfrom
lorentzenchr:sparse_splines

Conversation

@lorentzenchr

@lorentzenchr lorentzenchr commented Aug 8, 2022

Copy link
Copy Markdown
Member

Reference Issues/PRs

Fixes #20998.

What does this implement/fix? Explain your changes.

This PR adds argument sparse sparse_output to SplineTransformer. Set to True, it returns a sparse csr matrix.

Any other comments?

This is available only for scipy >= 1.8. Further improvements will be possible with scipy 1.10 (extrapolate argument for BSpline.design_matrix).

@lorentzenchr lorentzenchr added this to the 1.2 milestone Aug 8, 2022

@thomasjpfan thomasjpfan left a comment

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.

Thank you for the PR

Comment thread sklearn/preprocessing/_polynomial.py Outdated
Comment thread sklearn/preprocessing/_polynomial.py Outdated
Comment thread sklearn/preprocessing/_polynomial.py Outdated
dtype = np.float64
XBS = np.zeros((n_samples, n_out), dtype=dtype, order=self.order)
if use_sparse:
output_list = []

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.

There is a bunch of sparse matrices being constructed in this implementation. For each feature:

  1. A CSR design matrix is constructed
  2. This matrix can be converted into a lil matrix
  3. hstack converts it all back to CSR.

What is the runtime for transform with sparse output compared to dense output?

@lorentzenchr lorentzenchr Aug 8, 2022

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.

import numpy as np
from sklearn.preprocessing import SplineTransformer

X = np.linspace([-1, -10, 100], [1, 10, 100], 10000)
st_sparse = SplineTransformer(sparse_output=True, extrapolation="error").fit(X)
st_dense = SplineTransformer(extrapolation="error").fit(X)

%timeit st_dense.transform(X)
2.13 ms ± 20.6 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

%timeit st_sparse.transform(X)
43.7 ms ± 336 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

That's unfortunate. Having n_features=1 doesn't change the result.
But memory consumption should be better.

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.

The bottleneck seems to be in scipy's _bspl.pyx function _make_design_matrix. In contrast, evaluate_spline does all loops explicitly. We might report this upstream.

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.

@ev-br @egorz734 friendly ping

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not sure what's going on here TBH, but csr->lil->csr does sound expensive. Where is the bottleneck, what does the profiler say?

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.

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.

Are there scipy nightly builds to install? Otherwise, this is above my current time budget to benchmark.
Scipy 1.10 will not only speed up BSpline.design_matrix but also make it easier to implement spline extrapolation in scikit-learn, thereby further reducing runtime and memory consumption.

@thomasjpfan thomasjpfan Aug 26, 2022

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.

SciPy has nightly builds:

pip install -i https://pypi.anaconda.org/scipy-wheels-nightly/simple scipy

but their CI is failing and not updating the nightly builds. I opened MacPython/scipy-wheels#175 to track the SciPy issue.

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.

With scipy==1.10.0.dev0, one gets improved runtime:

import numpy as np
from sklearn.preprocessing import SplineTransformer

X = np.linspace([-1, -10, 100], [1, 10, 100], 10000)

st_sparse = SplineTransformer(sparse_output=True, extrapolation="error").fit(X)
st_dense = SplineTransformer(extrapolation="error").fit(X)

%timeit st_dense.transform(X)
1.89 ms ± 10.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

%timeit st_sparse.transform(X)
4.89 ms ± 53.7 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

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.

On my laptop, I get (scipy 1.10.1)

%timeit st_dense.transform(X)
2.12 ms ± 46.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

%timeit st_sparse.transform(X)
4.61 ms ± 162 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

@jjerphan jjerphan left a comment

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.

Thank you for adding this support, @lorentzenchr.

Here are a few comments and questions.

Comment thread doc/whats_new/v1.2.rst Outdated
Comment thread sklearn/preprocessing/_polynomial.py Outdated
Comment thread sklearn/preprocessing/_polynomial.py Outdated
Comment thread sklearn/preprocessing/_polynomial.py Outdated
Comment thread sklearn/preprocessing/_polynomial.py Outdated
Comment thread sklearn/preprocessing/_polynomial.py Outdated
Comment thread sklearn/preprocessing/_polynomial.py Outdated
Comment thread sklearn/preprocessing/_polynomial.py Outdated
Comment thread sklearn/preprocessing/_polynomial.py Outdated
Comment thread sklearn/preprocessing/_polynomial.py
Comment thread sklearn/preprocessing/_polynomial.py
@jeremiedbb

Copy link
Copy Markdown
Member

We won't have time to finish the review on this one before the 1.2 release. Moving it to 1.3

@jeremiedbb jeremiedbb modified the milestones: 1.2, 1.3 Nov 24, 2022
@lorentzenchr

lorentzenchr commented Apr 30, 2023

Copy link
Copy Markdown
Member Author

Finally, CI is 🟢.

Edit: Codecov does not count as it is clear that not everything is tested for a single scipy version.

@jjerphan jjerphan left a comment

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.

LGTM!

I just have a few last comments. Errors on the CI with scipy-dev are unrelated (see #26154).

Comment thread doc/whats_new/v1.2.rst
Comment thread sklearn/preprocessing/_polynomial.py Outdated
Comment thread sklearn/preprocessing/_polynomial.py
Comment thread sklearn/preprocessing/_polynomial.py
Comment thread sklearn/preprocessing/tests/test_polynomial.py

@ogrisel ogrisel left a comment

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.

Here is another pass of review. The performance is still poor with extrapolation (at least "periodic") even with scipy 1.10.1. From the inline comments, it's not clear if this is expected or not.

If it's expected and there is no easy way around it, I think the inline comments should be updated to make this more explicit (see below for details).

Other than that, LGTM!

Comment thread sklearn/preprocessing/_polynomial.py Outdated
Comment thread sklearn/preprocessing/tests/test_polynomial.py
Comment thread sklearn/preprocessing/tests/test_polynomial.py Outdated
XBS = sparse.hstack(output_list)
elif self.sparse_output:
# TODO: Remove ones scipy 1.10 is the minimum version. See comments above.
XBS = sparse.csr_matrix(XBS)

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.

This comment seems to imply that with scipy 1.10, the .tolil conversion would no longer be necessary.

However, as far as I understand, we still have to go through this condition when extrapolate="periodic", even with scipy 1.10 or later:

Would there be a way to avoid the .tolil conversion completely with recent scipy versions?

At the moment, sparse periodic extrapolation is more than 20x slower than its dense counterpart:

In [1]: import numpy as np
   ...: from sklearn.preprocessing import SplineTransformer
   ...: 
   ...: X = np.linspace([-1, -10, 100], [1, 10, 101], 10000)
   ...: extrapolation="periodic"
   ...: st_sparse = SplineTransformer(sparse_output=True, extrapolation=extrapolation).fit(X)
   ...: st_dense = SplineTransformer(extrapolation=extrapolation).fit(X)

In [2]: %timeit st_dense.transform(X)
1.29 ms ± 1.25 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)

In [3]: %timeit st_sparse.transform(X)
64.1 ms ± 851 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

scipy version: 1.10.1

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.

It is slower, yes, but this is not a performance regression as sparse output is a new feature and the default is dense output.

And yes, with scipy >= 1.10, we can get rid of (at least most of) the lil-conversions as we can use design_matrix(..., extrapolate=True).

@ogrisel

ogrisel commented May 17, 2023

Copy link
Copy Markdown
Member

Oops, I broke the linter when resolving the conflicts via the github UI. Let me push a fix.

@ogrisel ogrisel added Waiting for Reviewer Waiting for Second Reviewer First reviewer is done, need a second one! labels Jun 5, 2023
@ogrisel
ogrisel merged commit e5c6590 into scikit-learn:main Jun 5, 2023
@ogrisel ogrisel removed Waiting for Reviewer Waiting for Second Reviewer First reviewer is done, need a second one! labels Jun 5, 2023
@ogrisel
ogrisel deleted the sparse_splines branch June 5, 2023 13:04
@ogrisel

ogrisel commented Jun 5, 2023

Copy link
Copy Markdown
Member

Merged. Thanks @lorentzenchr!

manudarmi pushed a commit to primait/scikit-learn that referenced this pull request Jun 12, 2023
Co-authored-by: Julien Jerphanion <[email protected]>
Co-authored-by: Olivier Grisel <[email protected]>
REDVM pushed a commit to REDVM/scikit-learn that referenced this pull request Nov 16, 2023
Co-authored-by: Julien Jerphanion <[email protected]>
Co-authored-by: Olivier Grisel <[email protected]>
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.

Add sparse matrix output to SplineTransformer

6 participants