1- import numpy as np
21from numba import float64
32from skglm .datafits .single_task import Huber
4- from skglm .utils .sparse_ops import spectral_norm
3+ from sklearn .base import BaseEstimator , RegressorMixin
4+ from sklearn .utils .validation import check_X_y , check_array
5+ from skglm .solvers import FISTA
6+ from skglm .penalties import L1
7+ from skglm .estimators import GeneralizedLinearEstimator
58
69
710class QuantileHuber (Huber ):
811 r"""Quantile Huber loss for quantile regression.
912
10- Implements the smoothed pinball loss with quadratic region :
13+ Implements the smoothed pinball loss:
1114
1215 .. math::
1316
@@ -25,17 +28,13 @@ class QuantileHuber(Huber):
2528 Desired quantile level between 0 and 1.
2629 delta : float, default=1.0
2730 Width of quadratic region.
28-
29- References
30- ----------
31- Chen, C. (2007). A Finite Smoothing Algorithm for Quantile Regression.
32- Journal of Computational and Graphical Statistics, 16(1), 136–164.
33- http://www.jstor.org/stable/27594233
3431 """
3532
3633 def __init__ (self , quantile = 0.5 , delta = 1.0 ):
3734 if not 0 < quantile < 1 :
3835 raise ValueError ("quantile must be between 0 and 1" )
36+ if delta <= 0 :
37+ raise ValueError ("delta must be positive" )
3938 self .delta = float (delta )
4039 self .quantile = float (quantile )
4140
@@ -45,80 +44,93 @@ def get_spec(self):
4544 def params_to_dict (self ):
4645 return dict (delta = self .delta , quantile = self .quantile )
4746
48- def _loss_and_grad_scalar (self , residual ):
49- """Calculate loss and gradient for a single residual."""
47+ def value (self , y , w , Xw ):
48+ """Compute the quantile Huber loss value."""
49+ n_samples = len (y )
50+ res = 0.0
51+ for i in range (n_samples ):
52+ residual = y [i ] - Xw [i ]
53+ res += self ._loss_scalar (residual )
54+ return res / n_samples
55+
56+ def _loss_scalar (self , residual ):
57+ """Calculate loss for a single residual."""
5058 tau = self .quantile
5159 delta = self .delta
52- abs_r = abs (residual )
53-
54- # Quadratic core: |r| ≤ delta
55- if abs_r <= delta :
56- if residual >= 0 :
57- # 0 ≤ r ≤ delta
58- loss = tau * residual ** 2 / (2 * delta )
59- grad = tau * residual / delta
60- else :
61- # -delta ≤ r < 0
62- loss = (1 - tau ) * residual ** 2 / (2 * delta )
63- grad = (1 - tau ) * residual / delta
64- return loss , grad
65-
66- # Linear tails: |r| > delta
67- if residual > delta :
68- loss = tau * (residual - delta / 2 )
69- grad = tau
70- return loss , grad
60+ r = residual
61+
62+ if r >= delta :
63+ # Upper linear tail: r >= delta
64+ return tau * (r - delta / 2 )
65+ elif r >= 0 :
66+ # Upper quadratic: 0 <= r < delta
67+ return tau * r ** 2 / (2 * delta )
68+ elif r > - delta :
69+ # Lower quadratic: -delta < r < 0
70+ return (1 - tau ) * r ** 2 / (2 * delta )
7171 else :
72- loss = (1 - tau ) * (- residual - delta / 2 )
73- grad = tau - 1
74- return loss , grad
72+ # Lower linear tail: r <= -delta
73+ return (1 - tau ) * (- r - delta / 2 )
7574
76- def value (self , y , w , Xw ):
77- """Compute the quantile Huber loss value."""
78- residuals = y - Xw
79- loss = np .zeros_like (residuals )
80- for i , r in enumerate (residuals ):
81- loss [i ], _ = self ._loss_and_grad_scalar (r )
82- return np .mean (loss )
83-
84- def raw_grad (self , y , Xw ):
85- """Compute gradient of datafit w.r.t Xw."""
86- residuals = y - Xw
87- grad = np .zeros_like (residuals )
88- for i , r in enumerate (residuals ):
89- _ , grad [i ] = self ._loss_and_grad_scalar (r )
90- return - grad
91-
92- def get_lipschitz (self , X , y ):
93- """Compute coordinate-wise Lipschitz constants."""
94- weight = max (self .quantile , 1 - self .quantile )
95- return weight * (X ** 2 ).sum (axis = 0 ) / (len (y ) * self .delta )
96-
97- def get_global_lipschitz (self , X , y ):
98- """Compute global Lipschitz constant."""
99- weight = max (self .quantile , 1 - self .quantile )
100- return weight * np .linalg .norm (X , 2 ) ** 2 / (len (y ) * self .delta )
101-
102- def get_lipschitz_sparse (self , X_data , X_indptr , X_indices , y ):
103- """Compute coordinate-wise Lipschitz constants for sparse X."""
104- n_samples = len (y )
105- weight = max (self .quantile , 1 - self .quantile )
106- n_features = len (X_indptr ) - 1
107- lipschitz = np .zeros (n_features , dtype = X_data .dtype )
108- for j in range (n_features ):
109- nrm2 = 0.0
110- for idx in range (X_indptr [j ], X_indptr [j + 1 ]):
111- nrm2 += X_data [idx ] ** 2
112- lipschitz [j ] = weight * nrm2 / (n_samples * self .delta )
113- return lipschitz
114-
115- def get_global_lipschitz_sparse (self , X_data , X_indptr , X_indices , y ):
116- """Compute global Lipschitz constant for sparse X."""
75+ def gradient_scalar (self , X , y , w , Xw , j ):
76+ """Compute gradient w.r.t. w_j - following parent class pattern."""
11777 n_samples = len (y )
118- weight = max (self .quantile , 1 - self .quantile )
119- return weight * spectral_norm (
120- X_data , X_indptr , X_indices , n_samples
121- ) ** 2 / (n_samples * self .delta )
78+ grad_j = 0.0
79+ for i in range (n_samples ):
80+ residual = y [i ] - Xw [i ]
81+ grad_j += - X [i , j ] * self ._grad_scalar (residual )
82+ return grad_j / n_samples
83+
84+ def _grad_scalar (self , residual ):
85+ """Calculate gradient for a single residual."""
86+ tau = self .quantile
87+ delta = self .delta
88+ r = residual
89+
90+ if r >= delta :
91+ # Upper linear tail: r >= delta
92+ return tau
93+ elif r >= 0 :
94+ # Upper quadratic: 0 <= r < delta
95+ return tau * r / delta
96+ elif r > - delta :
97+ # Lower quadratic: -delta < r < 0
98+ return (1 - tau ) * r / delta
99+ else :
100+ # Lower linear tail: r <= -delta
101+ return tau - 1
102+
103+
104+ class SimpleQuantileRegressor (BaseEstimator , RegressorMixin ):
105+ """Simple quantile regression without progressive smoothing."""
106+
107+ def __init__ (self , quantile = 0.5 , alpha = 0.1 , delta = 0.1 , max_iter = 1000 , tol = 1e-4 ):
108+ self .quantile = quantile
109+ self .alpha = alpha
110+ self .delta = delta
111+ self .max_iter = max_iter
112+ self .tol = tol
113+
114+ def fit (self , X , y ):
115+ """Fit using FISTA with fixed delta."""
116+ X , y = check_X_y (X , y )
117+
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 )
121+
122+ est = GeneralizedLinearEstimator (
123+ datafit = datafit ,
124+ penalty = penalty ,
125+ solver = solver
126+ )
127+
128+ est .fit (X , y )
129+ self .coef_ = est .coef_
130+
131+ return self
122132
123- def intercept_update_step (self , y , Xw ):
124- return - np .mean (self .raw_grad (y , Xw ))
133+ def predict (self , X ):
134+ """Predict using the fitted model."""
135+ X = check_array (X )
136+ return X @ self .coef_
0 commit comments