|
| 1 | +import numpy as np |
1 | 2 | from numba import float64 |
2 | 3 | from skglm.datafits.single_task import Huber |
3 | 4 | from sklearn.base import BaseEstimator, RegressorMixin |
@@ -101,32 +102,58 @@ def _grad_scalar(self, residual): |
101 | 102 | return tau - 1 |
102 | 103 |
|
103 | 104 |
|
104 | | -class SimpleQuantileRegressor(BaseEstimator, RegressorMixin): |
105 | | - """Simple quantile regression without progressive smoothing.""" |
| 105 | +class SmoothQuantileRegressor(BaseEstimator, RegressorMixin): |
| 106 | + """Quantile regression with progressive smoothing.""" |
106 | 107 |
|
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): |
108 | 110 | self.quantile = quantile |
109 | 111 | 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 |
111 | 115 | self.max_iter = max_iter |
112 | 116 | self.tol = tol |
| 117 | + self.verbose = verbose |
113 | 118 |
|
114 | 119 | def fit(self, X, y): |
115 | | - """Fit using FISTA with fixed delta.""" |
| 120 | + """Fit using progressive smoothing: delta_init --> delta_final.""" |
116 | 121 | 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) |
117 | 124 |
|
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") |
121 | 129 |
|
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) |
127 | 134 |
|
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 |
130 | 157 |
|
131 | 158 | return self |
132 | 159 |
|
|
0 commit comments