Skip to content
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

ENH - add support for intercept in SqrtLasso #214

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
24 changes: 17 additions & 7 deletions skglm/experimental/sqrt_lasso.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,13 @@ class SqrtLasso(LinearModel, RegressorMixin):

verbose : bool, default False
Amount of verbosity. 0/False is silent.

fit_intercept: bool, optional (default=True)
Whether or not to fit an intercept.
"""

def __init__(self, alpha=1., max_iter=100, max_pn_iter=100, p0=10,
tol=1e-4, verbose=0):
tol=1e-4, verbose=0, fit_intercept=True):
super().__init__()
self.alpha = alpha
self.max_iter = max_iter
Expand All @@ -113,6 +116,7 @@ def __init__(self, alpha=1., max_iter=100, max_pn_iter=100, p0=10,
self.p0 = p0
self.tol = tol
self.verbose = verbose
self.fit_intercept = fit_intercept

def fit(self, X, y):
"""Fit the model according to the given training data.
Expand All @@ -132,7 +136,10 @@ def fit(self, X, y):
Fitted estimator.
"""
self.coef_ = self.path(X, y, alphas=[self.alpha])[1][0]
self.intercept_ = 0. # TODO handle fit_intercept
if self.fit_intercept:
self.intercept_ = self.coef_[-1]
else:
self.intercept_ = 0.
return self

def path(self, X, y, alphas=None, eps=1e-3, n_alphas=10):
Expand Down Expand Up @@ -169,7 +176,7 @@ def path(self, X, y, alphas=None, eps=1e-3, n_alphas=10):
if not hasattr(self, "solver_"):
self.solver_ = ProxNewton(
tol=self.tol, max_iter=self.max_iter, verbose=self.verbose,
fit_intercept=False)
fit_intercept=self.fit_intercept)
# build path
if alphas is None:
alpha_max = norm(X.T @ y, ord=np.inf) / (np.sqrt(len(y)) * norm(y))
Expand All @@ -182,7 +189,7 @@ def path(self, X, y, alphas=None, eps=1e-3, n_alphas=10):
sqrt_quadratic = compiled_clone(SqrtQuadratic())
l1_penalty = compiled_clone(L1(1.)) # alpha is set along the path

coefs = np.zeros((n_alphas, n_features))
coefs = np.zeros((n_alphas, n_features + self.fit_intercept))

for i in range(n_alphas):
if self.verbose:
Expand All @@ -193,12 +200,14 @@ def path(self, X, y, alphas=None, eps=1e-3, n_alphas=10):

l1_penalty.alpha = alphas[i]
# no warm start for the first alpha
coef_init = coefs[i].copy() if i else np.zeros(n_features)
coef_init = coefs[i].copy() if i else np.zeros(n_features
+ self.fit_intercept)

try:
coef, _, _ = self.solver_.solve(
X, y, sqrt_quadratic, l1_penalty,
w_init=coef_init, Xw_init=X @ coef_init)
w_init=coef_init, Xw_init=X @ coef_init[:-1] + coef_init[-1]
if self.fit_intercept else X @ coef_init)
coefs[i] = coef
except ValueError as val_exception:
# make sure to catch residual error
Expand All @@ -209,7 +218,8 @@ def path(self, X, y, alphas=None, eps=1e-3, n_alphas=10):
# save coef despite not converging
# coef_init holds a ref to coef
coef = coef_init
res_norm = norm(y - X @ coef)
X_coef = X @ coef[:-1] + coef[-1] if self.fit_intercept else X @ coef
res_norm = norm(y - X_coef)
warnings.warn(
f"Small residuals prevented the solver from converging "
f"at alpha={alphas[i]:.2e} (residuals' norm: {res_norm:.4e}). "
Expand Down
11 changes: 7 additions & 4 deletions skglm/experimental/tests/test_sqrt_lasso.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ def test_alpha_max():

sqrt_lasso = SqrtLasso(alpha=alpha_max).fit(X, y)

np.testing.assert_equal(sqrt_lasso.coef_, 0)
if sqrt_lasso.fit_intercept:
np.testing.assert_equal(sqrt_lasso.coef_[:-1], 0)
else:
np.testing.assert_equal(sqrt_lasso.coef_, 0)
Comment on lines +19 to +22
Copy link
Collaborator

Choose a reason for hiding this comment

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

WDYT about this refactoring?

Suggested change
if sqrt_lasso.fit_intercept:
np.testing.assert_equal(sqrt_lasso.coef_[:-1], 0)
else:
np.testing.assert_equal(sqrt_lasso.coef_, 0)
np.testing.assert_equal(sqrt_lasso.coef_[:n_features], 0)



def test_vs_statsmodels():
Expand All @@ -31,7 +34,7 @@ def test_vs_statsmodels():
n_alphas = 3
alphas = alpha_max * np.geomspace(1, 1e-2, n_alphas+1)[1:]

sqrt_lasso = SqrtLasso(tol=1e-9)
sqrt_lasso = SqrtLasso(tol=1e-9, fit_intercept=False)
coefs_skglm = sqrt_lasso.path(X, y, alphas)[1]

coefs_statsmodels = np.zeros((len(alphas), n_features))
Expand All @@ -54,7 +57,7 @@ def test_prox_newton_cp():

alpha_max = norm(X.T @ y, ord=np.inf) / norm(y)
alpha = alpha_max / 10
clf = SqrtLasso(alpha=alpha, tol=1e-12).fit(X, y)
clf = SqrtLasso(alpha=alpha, fit_intercept=False, tol=1e-12).fit(X, y)
w, _, _ = _chambolle_pock_sqrt(X, y, alpha, max_iter=1000)
np.testing.assert_allclose(clf.coef_, w)

Expand All @@ -70,7 +73,7 @@ def test_PDCD_WS(with_dual_init):
dual_init = y / norm(y) if with_dual_init else None

w = PDCD_WS(dual_init=dual_init).solve(X, y, SqrtQuadratic(), L1(alpha))[0]
clf = SqrtLasso(alpha=alpha, tol=1e-12).fit(X, y)
clf = SqrtLasso(alpha=alpha, fit_intercept=False, tol=1e-12).fit(X, y)
np.testing.assert_allclose(clf.coef_, w, atol=1e-6)


Expand Down
Loading