Skip to content

Commit e4888d9

Browse files
add progressive smoothing
1 parent 010c399 commit e4888d9

File tree

3 files changed

+56
-22
lines changed

3 files changed

+56
-22
lines changed

examples/plot_smooth_quantile.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
import numpy as np
55
import time
66
from sklearn.linear_model import QuantileRegressor
7-
from skglm.experimental.quantile_huber import QuantileHuber, SimpleQuantileRegressor
7+
from skglm.experimental.quantile_huber import QuantileHuber, SmoothQuantileRegressor
88
import matplotlib.pyplot as plt
99
from sklearn.datasets import make_regression
1010

11-
# TODO: no smoothing and no intercept handling yet
11+
# TODO: no intercept handling yet
1212

1313

1414
def pinball_loss(residuals, quantile):
@@ -25,7 +25,7 @@ def plot_quantile_huber():
2525
quantiles = [0.1, 0.5, 0.9]
2626
delta = 0.5
2727
residuals = np.linspace(-3, 3, 500)
28-
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4))
28+
_, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4))
2929
for tau in quantiles:
3030
qh = QuantileHuber(quantile=tau, delta=delta)
3131
loss = [qh._loss_scalar(r) for r in residuals]
@@ -49,15 +49,22 @@ def plot_quantile_huber():
4949
tau = 0.8
5050

5151
start = time.time()
52-
sk = QuantileRegressor(quantile=tau, alpha=0.001, fit_intercept=False)
52+
sk = QuantileRegressor(quantile=tau, alpha=0.1, fit_intercept=False)
5353
sk.fit(X, y)
5454
sk_time = time.time() - start
5555
sk_pred = sk.predict(X)
5656
sk_cov = np.mean(y <= sk_pred)
5757
sk_pinball = pinball_loss(y - sk_pred, tau)
5858

5959
start = time.time()
60-
qh = SimpleQuantileRegressor(quantile=tau, alpha=0.001, delta=0.05)
60+
qh = SmoothQuantileRegressor(
61+
quantile=tau,
62+
alpha=0.1,
63+
delta_init=0.5,
64+
delta_final=0.05,
65+
n_deltas=5,
66+
verbose=True
67+
)
6168
qh.fit(X, y)
6269
qh_time = time.time() - start
6370
qh_pred = qh.predict(X)

skglm/experimental/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from .sqrt_lasso import SqrtLasso, SqrtQuadratic
33
from .pdcd_ws import PDCD_WS
44
from .quantile_regression import Pinball
5-
from .quantile_huber import QuantileHuber, SimpleQuantileRegressor
5+
from .quantile_huber import QuantileHuber, SmoothQuantileRegressor
66

77
__all__ = [
88
IterativeReweightedL1,
@@ -11,5 +11,5 @@
1111
SqrtQuadratic,
1212
SqrtLasso,
1313
QuantileHuber,
14-
SimpleQuantileRegressor,
14+
SmoothQuantileRegressor,
1515
]

skglm/experimental/quantile_huber.py

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import numpy as np
12
from numba import float64
23
from skglm.datafits.single_task import Huber
34
from sklearn.base import BaseEstimator, RegressorMixin
@@ -101,32 +102,58 @@ def _grad_scalar(self, residual):
101102
return tau - 1
102103

103104

104-
class SimpleQuantileRegressor(BaseEstimator, RegressorMixin):
105-
"""Simple quantile regression without progressive smoothing."""
105+
class SmoothQuantileRegressor(BaseEstimator, RegressorMixin):
106+
"""Quantile regression with progressive smoothing."""
106107

107-
def __init__(self, quantile=0.5, alpha=0.1, delta=0.1, max_iter=1000, tol=1e-4):
108+
def __init__(self, quantile=0.5, alpha=0.1, delta_init=1.0, delta_final=1e-3,
109+
n_deltas=10, max_iter=1000, tol=1e-4, verbose=False):
108110
self.quantile = quantile
109111
self.alpha = alpha
110-
self.delta = delta
112+
self.delta_init = delta_init
113+
self.delta_final = delta_final
114+
self.n_deltas = n_deltas
111115
self.max_iter = max_iter
112116
self.tol = tol
117+
self.verbose = verbose
113118

114119
def fit(self, X, y):
115-
"""Fit using FISTA with fixed delta."""
120+
"""Fit using progressive smoothing: delta_init --> delta_final."""
116121
X, y = check_X_y(X, y)
122+
w = np.zeros(X.shape[1])
123+
deltas = np.geomspace(self.delta_init, self.delta_final, self.n_deltas)
117124

118-
datafit = QuantileHuber(quantile=self.quantile, delta=self.delta)
119-
penalty = L1(alpha=self.alpha)
120-
solver = FISTA(max_iter=self.max_iter, tol=self.tol)
125+
if self.verbose:
126+
print(
127+
f"Progressive smoothing: delta {self.delta_init:.3f} --> "
128+
f"{self.delta_final:.3f} in {self.n_deltas} steps")
121129

122-
est = GeneralizedLinearEstimator(
123-
datafit=datafit,
124-
penalty=penalty,
125-
solver=solver
126-
)
130+
for i, delta in enumerate(deltas):
131+
datafit = QuantileHuber(quantile=self.quantile, delta=delta)
132+
penalty = L1(alpha=self.alpha)
133+
solver = FISTA(max_iter=self.max_iter, tol=self.tol)
127134

128-
est.fit(X, y)
129-
self.coef_ = est.coef_
135+
est = GeneralizedLinearEstimator(
136+
datafit=datafit,
137+
penalty=penalty,
138+
solver=solver
139+
)
140+
141+
if i > 0:
142+
est.coef_ = w.copy()
143+
144+
est.fit(X, y)
145+
w = est.coef_.copy()
146+
147+
if self.verbose:
148+
residuals = y - X @ w
149+
coverage = np.mean(residuals <= 0)
150+
pinball_loss = np.mean(residuals * (self.quantile - (residuals < 0)))
151+
152+
print(
153+
f" Stage {i+1:2d}: delta={delta:.4f}, "
154+
f"coverage={coverage:.3f}, pinball_loss={pinball_loss:.6f}")
155+
156+
self.coef_ = w
130157

131158
return self
132159

0 commit comments

Comments
 (0)