8000 [MRG] Add jitter to LassoLars (#15179) · scikit-learn/scikit-learn@abfb6fd · GitHub
[go: up one dir, main page]

Skip to content

Commit abfb6fd

Browse files
[MRG] Add jitter to LassoLars (#15179)
* Adding jitter to LassoLars fit * CircleCI fail * MR comments * Jitter becomes default, added test based on issue description * flake8 fixes * Removing unexpected cython files * Better coverage * PR comments * PR comments * PR comments * PR comments * PR comments * Linting * Apply suggestions from code review * addressed comments * added whatnew entry * test both estimators * update whatsnew * removed random_state for lassolarsIC Co-authored-by: Nicolas Hug <contact@nicolas-hug.com>
1 parent 9358a6e commit abfb6fd

File tree

3 files changed

+63
-2
lines changed

3 files changed

+63
-2
lines changed

doc/whats_new/v0.23.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,11 @@ Changelog
298298
of strictly inferior for maximum of `absgrad` and `tol` in `utils.optimize._newton_cg`.
299299
:pr:`16266` by :user:`Rushabh Vasani <rushabh-v>`.
300300

301+
- |Enhancement| :class:`linear_model.LassoLars` and
302+
:class:`linear_model.Lars` now support a `jitter` parameter that adds
303+
random noise to the target. This might help with stability in some edge
304+
cases. :pr:`15179` by :user:`angelaambroz`.
305+
301306
:mod:`sklearn.metrics`
302307
......................
303308

sklearn/linear_model/_least_angle.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from ..base import RegressorMixin, MultiOutputMixin
2222
# mypy error: Module 'sklearn.utils' has no attribute 'arrayfuncs'
2323
from ..utils import arrayfuncs, as_float_array # type: ignore
24+
from ..utils import check_random_state
2425
from ..model_selection import check_cv
2526
from ..exceptions import ConvergenceWarning
2627

@@ -800,6 +801,16 @@ class Lars(MultiOutputMixin, RegressorMixin, LinearModel):
800801
setting ``fit_path`` to ``False`` will lead to a speedup, especially
801802
with a small alpha.
802803
804+
jitter : float, default=None
805+
Upper bound on a uniform noise parameter to be added to the
806+
`y` values, to satisfy the model's assumption of
807+
one-at-a-time computations. Might help with stability.
808+
809+
random_state : int, RandomState instance or None (default)
810+
Determines random number generation for jittering. Pass an int
811+
for reproducible output across multiple function calls.
812+
See :term:`Glossary <random_state>`. Ignored if `jitter` is None.
813+
803814
Attributes
804815
----------
805816
alphas_ : array-like of shape (n_alphas + 1,) | list of n_targets such \
@@ -846,7 +857,8 @@ class Lars(MultiOutputMixin, RegressorMixin, LinearModel):
846857

847858
def __init__(self, fit_intercept=True, verbose=False, normalize=True,
848859
precompute='auto', n_nonzero_coefs=500,
849-
eps=np.finfo(np.float).eps, copy_X=True, fit_path=True):
860+
eps=np.finfo(np.float).eps, copy_X=True, fit_path=True,
861+
jitter=None, random_state=None):
850862
self.fit_intercept = fit_intercept
851863
self.verbose = verbose
852864
self.normalize = normalize
@@ -855,6 +867,8 @@ def __init__(self, fit_intercept=True, verbose=False, normalize=True,
855867
self.eps = eps
856868
self.copy_X = copy_X
857869
self.fit_path = fit_path
870+
self.jitter = jitter
871+
self.random_state = random_state
858872

859873
@staticmethod
860874
def _get_gram(precompute, X, y):
@@ -954,6 +968,12 @@ def fit(self, X, y, Xy=None):
954968
else:
955969
max_iter = self.max_iter
956970

971+
if self.jitter is not None:
972+
rng = check_random_state(self.random_state)
973+
974+
noise = rng.uniform(high=self.jitter, size=len(y))
975+
y = y + noise
976+
957977
self._fit(X, y, max_iter=max_iter, alpha=alpha, fit_path=self.fit_path,
958978
Xy=Xy)
959979

@@ -1031,6 +1051,16 @@ class LassoLars(Lars):
10311051
algorithm are typically in congruence with the solution of the
10321052
coordinate descent Lasso estimator.
10331053
1054+
jitter : float, default=None
1055+
Upper bound on a uniform noise parameter to be added to the
1056+
`y` values, to satisfy the model's assumption of
1057+
one-at-a-time computations. Might help with stability.
1058+
1059+
random_state : int, RandomState instance or None (default)
1060+
Determines random number generation for jittering. Pass an int
1061+
for reproducible output across multiple function calls.
1062+
See :term:`Glossary <random_state>`. Ignored if `jitter` is None.
1063+
10341064
Attributes
10351065
----------
10361066
alphas_ : array-like of shape (n_alphas + 1,) | list of n_targets such \
@@ -1083,7 +1113,7 @@ class LassoLars(Lars):
10831113
def __init__(self, alpha=1.0, fit_intercept=True, verbose=False,
10841114
normalize=True, precompute='auto', max_iter=500,
10851115
eps=np.finfo(np.float).eps, copy_X=True, fit_path=True,
1086-
positive=False):
1116+
positive=False, jitter=None, random_state=None):
10871117
self.alpha = alpha
10881118
self.fit_intercept = fit_intercept
10891119
self.max_iter = max_iter
@@ -1094,6 +1124,8 @@ def __init__(self, alpha=1.0, fit_intercept=True, verbose=False,
10941124
self.copy_X = copy_X
10951125
self.eps = eps
10961126
self.fit_path = fit_path
1127+
self.jitter = jitter
1128+
self.random_state = random_state
10971129

10981130

10991131
###############################################################################

sklearn/linear_model/tests/test_least_angle.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import pytest
77
from scipy import linalg
88

9+
from sklearn.base import clone
910
from sklearn.model_selection import train_test_split
1011
from sklearn.utils._testing import assert_allclose
1112
from sklearn.utils._testing import assert_array_almost_equal
@@ -17,6 +18,7 @@
1718
from sklearn import linear_model, datasets
1819
from sklearn.linear_model._least_angle import _lars_path_residues
1920
from sklearn.linear_model import LassoLarsIC, lars_path
21+
from sklearn.linear_model import Lars, LassoLars
2022

2123
# TODO: use another dataset that has multiple drops
2224
diabetes = datasets.load_diabetes()
@@ -733,6 +735,28 @@ def test_lasso_lars_fit_copyX_behaviour(copy_X):
733735
assert copy_X == np.array_equal(X, X_copy)
734736

735737

738+
@pytest.mark.parametrize('est', (LassoLars(alpha=1e-3), Lars()))
739+
def test_lars_with_jitter(est):
740+
# Test that a small amount of jitter helps stability,
741+
# using example provided in issue #2746
742+
743+
X = np.array([[0.0, 0.0, 0.0, -1.0, 0.0],
744+
[0.0, -1.0, 0.0, 0.0, 0.0]])
745+
y = [-2.5, -2.5]
746+
expected_coef = [0, 2.5, 0, 2.5, 0]
747+
748+
# set to fit_intercept to False since target is constant and we want check
749+
# the value of coef. coef would be all zeros otherwise.
750+
est.set_params(fit_intercept=False)
751+
est_jitter = clone(est).set_params(jitter=10e-8, random_state=0)
752+
753+
est.fit(X, y)
754+
est_jitter.fit(X, y)
755+
756+
assert np.mean((est.coef_ - est_jitter.coef_)**2) > .1
757+
np.testing.assert_allclose(est_jitter.coef_, expected_coef, rtol=1e-3)
758+
759+
736760
def test_X_none_gram_not_none():
737761
with pytest.raises(ValueError,
738762
match="X cannot be None if Gram is not None"):

0 commit comments

Comments
 (0)
0