11import numpy as np
2+ from numpy .linalg import norm
23from numba import float64
3- from skglm .datafits .single_task import Huber
4+ from skglm .datafits .base import BaseDatafit
45from sklearn .base import BaseEstimator , RegressorMixin
5- from sklearn .utils .validation import check_X_y , check_array
6- from skglm .solvers import FISTA
6+ from skglm .solvers import FISTA , AndersonCD
77from skglm .penalties import L1
88from skglm .estimators import GeneralizedLinearEstimator
99
1010
11- class QuantileHuber (Huber ):
11+ class QuantileHuber (BaseDatafit ):
1212 r"""Quantile Huber loss for quantile regression.
1313
1414 Implements the smoothed pinball loss:
@@ -51,11 +51,11 @@ def value(self, y, w, Xw):
5151 res = 0.0
5252 for i in range (n_samples ):
5353 residual = y [i ] - Xw [i ]
54- res += self ._loss_scalar (residual )
54+ res += self ._loss_sample (residual )
5555 return res / n_samples
5656
57- def _loss_scalar (self , residual ):
58- """Calculate loss for a single residual ."""
57+ def _loss_sample (self , residual ):
58+ """Calculate loss for a single sample ."""
5959 tau = self .quantile
6060 delta = self .delta
6161 r = residual
@@ -79,11 +79,11 @@ def gradient_scalar(self, X, y, w, Xw, j):
7979 grad_j = 0.0
8080 for i in range (n_samples ):
8181 residual = y [i ] - Xw [i ]
82- grad_j += - X [i , j ] * self ._grad_scalar (residual )
82+ grad_j += - X [i , j ] * self ._grad_per_sample (residual )
8383 return grad_j / n_samples
8484
85- def _grad_scalar (self , residual ):
86- """Calculate gradient for a single residual ."""
85+ def _grad_per_sample (self , residual ):
86+ """Calculate gradient for a single sample ."""
8787 tau = self .quantile
8888 delta = self .delta
8989 r = residual
@@ -101,12 +101,26 @@ def _grad_scalar(self, residual):
101101 # Lower linear tail: r <= -delta
102102 return tau - 1
103103
104+ def get_lipschitz (self , X , y ):
105+ n_features = X .shape [1 ]
106+
107+ lipschitz = np .zeros (n_features , dtype = X .dtype )
108+ c = max (self .quantile , 1 - self .quantile ) / self .delta
109+ for j in range (n_features ):
110+ lipschitz [j ] = c * (X [:, j ] ** 2 ).sum () / len (y )
111+
112+ return lipschitz
113+
114+ def get_global_lipschitz (self , X , y ):
115+ c = max (self .quantile , 1 - self .quantile ) / self .delta
116+ return c * norm (X , ord = 2 ) ** 2 / len (y )
117+
104118
105119class SmoothQuantileRegressor (BaseEstimator , RegressorMixin ):
106120 """Quantile regression with progressive smoothing."""
107121
108122 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 ):
123+ n_deltas = 10 , max_iter = 1000 , tol = 1e-4 , verbose = False , solver = "FISTA" ):
110124 self .quantile = quantile
111125 self .alpha = alpha
112126 self .delta_init = delta_init
@@ -115,10 +129,10 @@ def __init__(self, quantile=0.5, alpha=0.1, delta_init=1.0, delta_final=1e-3,
115129 self .max_iter = max_iter
116130 self .tol = tol
117131 self .verbose = verbose
132+ self .solver = solver
118133
119134 def fit (self , X , y ):
120135 """Fit using progressive smoothing: delta_init --> delta_final."""
121- X , y = check_X_y (X , y )
122136 w = np .zeros (X .shape [1 ])
123137 deltas = np .geomspace (self .delta_init , self .delta_final , self .n_deltas )
124138
@@ -127,19 +141,26 @@ def fit(self, X, y):
127141 f"Progressive smoothing: delta { self .delta_init :.3f} --> "
128142 f"{ self .delta_final :.3f} in { self .n_deltas } steps" )
129143
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 )
144+ datafit = QuantileHuber (quantile = self .quantile , delta = self .delta_init )
145+ penalty = L1 (alpha = self .alpha )
146+ # Solver selection
147+ if isinstance (self .solver , str ):
148+ if self .solver == "FISTA" :
149+ solver = FISTA (max_iter = self .max_iter , tol = self .tol )
150+ solver .warm_start = True
151+ elif self .solver == "AndersonCD" :
152+ solver = AndersonCD (max_iter = self .max_iter , tol = self .tol ,
153+ warm_start = True , fit_intercept = False )
154+ else :
155+ raise ValueError (f"Unknown solver: { self .solver } " )
156+ else :
157+ solver = self .solver
134158
135- est = GeneralizedLinearEstimator (
136- datafit = datafit ,
137- penalty = penalty ,
138- solver = solver
139- )
159+ est = GeneralizedLinearEstimator (
160+ datafit = datafit , penalty = penalty , solver = solver )
140161
141- if i > 0 :
142- est . coef_ = w . copy ( )
162+ for i , delta in enumerate ( deltas ) :
163+ datafit . delta = float ( delta )
143164
144165 est .fit (X , y )
145166 w = est .coef_ .copy ()
@@ -151,13 +172,16 @@ def fit(self, X, y):
151172
152173 print (
153174 f" Stage { i + 1 :2d} : delta={ delta :.4f} , "
154- f"coverage={ coverage :.3f} , pinball_loss={ pinball_loss :.6f} " )
175+ f"coverage={ coverage :.3f} , pinball_loss={ pinball_loss :.6f} , "
176+ f"n_iter={ est .n_iter_ } "
177+ )
155178
156- self .coef_ = w
179+ self .est = est
157180
158181 return self
159182
160183 def predict (self , X ):
161184 """Predict using the fitted model."""
162- X = check_array (X )
163- return X @ self .coef_
185+ if not hasattr (self , "est" ):
186+ raise ValueError ("Call 'fit' before 'predict'." )
187+ return self .est .predict (X )
0 commit comments