Skip to content

Commit 8a4d932

Browse files
authored
Merge pull request #31 from DataboyUsen/main
feat: add ElasticNet penalty support to ReHLine solver
2 parents 964b747 + 9d16676 commit 8a4d932

7 files changed

Lines changed: 481 additions & 36 deletions

File tree

rehline/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Import from internal C++ module
22
from ._base import (ReHLine_solver, _BaseReHLine,
33
_make_constraint_rehline_param, _make_loss_rehline_param)
4-
from ._class import CQR_Ridge, ReHLine, plqERM_Ridge
4+
from ._class import CQR_Ridge, ReHLine, plqERM_Ridge, plqERM_ElasticNet
55
from ._internal import rehline_internal, rehline_result
66
from ._path_sol import plqERM_Ridge_path_sol
77
from ._sklearn_mixin import plq_Ridge_Classifier, plq_Ridge_Regressor
@@ -14,6 +14,7 @@
1414
"ReHLine",
1515
"plqERM_Ridge",
1616
"CQR_Ridge",
17+
"plqERM_ElasticNet",
1718
"plqMF_Ridge",
1819
"plqERM_Ridge_path_sol",
1920
"plq_Ridge_Classifier",

rehline/_base.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,9 +293,11 @@ def ReHLine_solver(
293293
T=np.empty(shape=(0, 0)),
294294
A=np.empty(shape=(0, 0)),
295295
b=np.empty(shape=(0)),
296+
rho=0.0,
296297
Lambda=np.empty(shape=(0, 0)),
297298
Gamma=np.empty(shape=(0, 0)),
298299
xi=np.empty(shape=(0, 0)),
300+
mu=np.empty(shape=(0, 0)),
299301
max_iter=1000,
300302
tol=1e-4,
301303
shrink=1,
@@ -309,8 +311,10 @@ def ReHLine_solver(
309311
result.Gamma = np.maximum(0, np.minimum(Gamma, Tau))
310312
if len(xi) > 0:
311313
result.xi = np.maximum(xi, 0.0)
314+
if len(mu) > 0:
315+
result.mu = np.maximum(0, np.minimum(mu, rho))
312316
rehline_internal(
313-
result, X, A, b, U, V, S, T, Tau, max_iter, tol, shrink, verbose, trace_freq
317+
result, X, A, b, U, V, S, T, Tau, max_iter, tol, rho, shrink, verbose, trace_freq
314318
)
315319
return result
316320

rehline/_class.py

Lines changed: 226 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
check_is_fitted, check_X_y)
1515

1616
from ._base import (ReHLine_solver, _BaseReHLine,
17-
_make_constraint_rehline_param, _make_loss_rehline_param)
17+
_make_constraint_rehline_param, _make_loss_rehline_param, _cast_sample_weight)
1818

1919

2020
class ReHLine(_BaseReHLine, BaseEstimator):
@@ -441,6 +441,231 @@ def decision_function(self, X):
441441

442442
X = check_array(X)
443443
return np.dot(X, self.coef_)
444+
445+
446+
class plqERM_ElasticNet(_BaseReHLine, BaseEstimator):
447+
r"""Empirical Risk Minimization (ERM) with a piecewise linear-quadratic (PLQ) objective with a elastic net penalty.
448+
449+
.. math::
450+
451+
\min_{\mathbf{\beta} \in \mathbb{R}^d} C \sum_{i=1}^n \text{PLQ}(y_i, \mathbf{x}_i^T \mathbf{\beta}) + \text{l1_ratio} \| \mathbf{\beta} \|_1 + \frac{1}{2} (1 - \text{l1_ratio}) \| \mathbf{\beta} \|_2^2, \ \text{ s.t. } \
452+
\mathbf{A} \mathbf{\beta} + \mathbf{b} \geq \mathbf{0},
453+
454+
The function supports various loss functions, including:
455+
- 'hinge', 'svm' or 'SVM'
456+
- 'check' or 'quantile' or 'quantile regression' or 'QR'
457+
- 'sSVM' or 'smooth SVM' or 'smooth hinge'
458+
- 'TV'
459+
- 'huber' or 'Huber'
460+
- 'SVR' or 'svr'
461+
462+
The following constraint types are supported:
463+
* 'nonnegative' or '>=0': A non-negativity constraint.
464+
* 'fair' or 'fairness': A fairness constraint.
465+
* 'custom': A custom constraint, where the user must provide the constraint matrix 'A' and vector 'b'.
466+
467+
Parameters
468+
----------
469+
loss : dict
470+
A dictionary specifying the loss function parameters.
471+
472+
constraint : list of dict
473+
A list of dictionaries, where each dictionary represents a constraint.
474+
Each dictionary must contain a 'name' key, which specifies the type of constraint.
475+
476+
C : float, default=1.0
477+
Regularization parameter. The strength of the regularization is
478+
inversely proportional to C. Must be strictly positive.
479+
`C` will be absorbed by the ReHLine parameters when `self.make_ReLHLoss` is conducted.
480+
481+
l1_ratio : float, default=0.5
482+
The ElasticNet mixing parameter, with 0 <= l1_ratio < 1. For l1_ratio = 0 the penalty
483+
is an L2 penalty. For 0 < l1_ratio < 1, the penalty is a combination of L1 and L2.
484+
485+
verbose : int, default=0
486+
Enable verbose output. Note that this setting takes advantage of a
487+
per-process runtime setting in liblinear that, if enabled, may not work
488+
properly in a multithreaded context.
489+
490+
max_iter : int, default=1000
491+
The maximum number of iterations to be run.
492+
493+
_U, _V: array of shape (L, n_samples), default=np.empty(shape=(0, 0))
494+
The parameters pertaining to the ReLU part in the loss function.
495+
496+
_Tau, _S, _T: array of shape (H, n_samples), default=np.empty(shape=(0, 0))
497+
The parameters pertaining to the ReHU part in the loss function.
498+
499+
_A: array of shape (K, n_features), default=np.empty(shape=(0, 0))
500+
The coefficient matrix in the linear constraint.
501+
502+
_b: array of shape (K, ), default=np.empty(shape=0)
503+
The intercept vector in the linear constraint.
504+
505+
Attributes
506+
----------
507+
coef\_ : array-like
508+
The optimized model coefficients.
509+
510+
n_iter\_ : int
511+
The number of iterations performed by the ReHLine solver.
512+
513+
opt_result\_ : object
514+
The optimization result object.
515+
516+
dual_obj\_ : array-like
517+
The dual objective function values.
518+
519+
primal_obj\_ : array-like
520+
The primal objective function values.
521+
522+
Methods
523+
-------
524+
fit(X, y, sample_weight=None)
525+
Fit the model based on the given training data.
526+
527+
decision_function(X)
528+
The decision function evaluated on the given dataset.
529+
530+
Notes
531+
-----
532+
The `plqERM_ElasticNet` class is a subclass of `_BaseReHLine` and `BaseEstimator`, which suggests that it is part of a larger framework for implementing ReHLine algorithms.
533+
534+
"""
535+
536+
def __init__(self, loss,
537+
constraint=[],
538+
C=1.,
539+
l1_ratio=0.5,
540+
U=np.empty(shape=(0,0)), V=np.empty(shape=(0,0)),
541+
Tau=np.empty(shape=(0,0)),
542+
S=np.empty(shape=(0,0)), T=np.empty(shape=(0,0)),
543+
A=np.empty(shape=(0,0)), b=np.empty(shape=(0)),
544+
max_iter=1000, tol=1e-4, shrink=1, warm_start=0,
545+
verbose=0, trace_freq=100):
546+
self.loss = loss
547+
self.constraint = constraint
548+
self.C = C
549+
self.l1_ratio = l1_ratio
550+
self.rho = l1_ratio / (1 - l1_ratio)
551+
self.C_eff = C / (1 - l1_ratio)
552+
self._U = U
553+
self._V = V
554+
self._S = S
555+
self._T = T
556+
self._Tau = Tau
557+
self._A = A
558+
self._b = b
559+
self.L = U.shape[0]
560+
self.H = S.shape[0]
561+
self.K = A.shape[0]
562+
self.max_iter = max_iter
563+
self.tol = tol
564+
self.shrink = shrink
565+
self.warm_start = warm_start
566+
self.verbose = verbose
567+
self.trace_freq = trace_freq
568+
self._Lambda = np.empty(shape=(0, 0))
569+
self._Gamma = np.empty(shape=(0, 0))
570+
self._xi = np.empty(shape=(0, 0))
571+
self._mu = np.empty(shape=(0, 0))
572+
self.coef_ = None
573+
574+
def fit(self, X, y, sample_weight=None):
575+
"""Fit the model based on the given training data.
576+
577+
Parameters
578+
----------
579+
580+
X: {array-like} of shape (n_samples, n_features)
581+
Training vector, where `n_samples` is the number of samples and
582+
`n_features` is the number of features.
583+
584+
y : array-like of shape (n_samples,)
585+
The target variable.
586+
587+
sample_weight : array-like of shape (n_samples,), default=None
588+
Array of weights that are assigned to individual
589+
samples. If not provided, then each sample is given unit weight.
590+
591+
Returns
592+
-------
593+
self : object
594+
An instance of the estimator.
595+
596+
597+
"""
598+
n, d = X.shape
599+
600+
## loss -> rehline params
601+
self._U, self._V, self._Tau, self._S, self._T = _make_loss_rehline_param(loss=self.loss, X=X, y=y)
602+
603+
## constrain -> rehline params
604+
self._A, self._b = _make_constraint_rehline_param(constraint=self.constraint, X=X, y=y)
605+
self.auto_shape()
606+
607+
sample_weight = _check_sample_weight(sample_weight, X, dtype=X.dtype)
608+
609+
U_weight, V_weight, Tau_weight, S_weight, T_weight = _cast_sample_weight(self._U, self._V, self._Tau, self._S, self._T,
610+
C=self.C_eff, sample_weight=sample_weight)
611+
612+
if not self.warm_start:
613+
## remove warm_start params
614+
self._Lambda = np.empty(shape=(0, 0))
615+
self._Gamma = np.empty(shape=(0, 0))
616+
self._xi = np.empty(shape=(0, 0))
617+
self._mu = np.empty(shape=(0, 0))
618+
619+
result = ReHLine_solver(X=X,
620+
U=U_weight, V=V_weight,
621+
Tau=Tau_weight,
622+
S=S_weight, T=T_weight,
623+
A=self._A, b=self._b,
624+
rho=self.rho,
625+
Lambda=self._Lambda, Gamma=self._Gamma, xi=self._xi, mu=self._mu,
626+
max_iter=self.max_iter, tol=self.tol,
627+
shrink=self.shrink, verbose=self.verbose,
628+
trace_freq=self.trace_freq)
629+
630+
self.opt_result_ = result
631+
# primal solution
632+
self.coef_ = result.beta
633+
# dual solution
634+
self._Lambda = result.Lambda
635+
self._Gamma = result.Gamma
636+
self._xi = result.xi
637+
self._mu = result.mu
638+
# algo convergence
639+
self.n_iter_ = result.niter
640+
self.dual_obj_ = result.dual_objfns
641+
self.primal_obj_ = result.primal_objfns
642+
643+
if self.n_iter_ >= self.max_iter:
644+
warnings.warn(
645+
"ReHLine failed to converge, increase the number of iterations: `max_iter`.",
646+
ConvergenceWarning,
647+
)
648+
649+
return self
650+
651+
def decision_function(self, X):
652+
"""The decision function evaluated on the given dataset
653+
654+
Parameters
655+
----------
656+
X : array-like of shape (n_samples, n_features)
657+
The data matrix.
658+
659+
Returns
660+
-------
661+
ndarray of shape (n_samples, )
662+
Returns the decision function of the samples.
663+
"""
664+
# Check if fit has been called
665+
check_is_fitted(self)
666+
667+
X = check_array(X)
668+
return np.dot(X, self.coef_)
444669

445670

446671
class CQR_Ridge(_BaseReHLine, BaseEstimator):

rehline/_sklearn_mixin.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,10 @@ class plq_Ridge_Classifier(plqERM_Ridge, ClassifierMixin):
8383
8484
Attributes
8585
----------
86-
coef_ : ndarray of shape (n_features,)
86+
``coef_`` : ndarray of shape (n_features,)
8787
Coefficients excluding the intercept.
8888
89-
intercept_ : float
89+
``intercept_`` : float
9090
Intercept term. 0.0 if ``fit_intercept=False``.
9191
9292
classes_ : ndarray of shape (2,)
@@ -336,11 +336,11 @@ class plq_Ridge_Regressor(plqERM_Ridge, RegressorMixin):
336336
337337
Attributes
338338
----------
339-
coef_ : ndarray of shape (n_features,)
339+
``coef_`` : ndarray of shape (n_features,)
340340
Learned linear coefficients (excluding the intercept term).
341-
intercept_ : float
341+
``intercept_`` : float
342342
Intercept term extracted from the last coefficient when ``fit_intercept=True``, otherwise 0.0.
343-
n_features_in_ : int
343+
``n_features_in_`` : int
344344
Number of input features seen during :meth:`fit` (before intercept augmentation).
345345
346346
Notes
@@ -444,7 +444,7 @@ def fit(self, X, y, sample_weight=None):
444444
return self
445445

446446
def decision_function(self, X):
447-
"""Compute f(X) = X @ coef_ + intercept_.
447+
"""Compute f(X) = X @ ``coef_`` + ``intercept_``.
448448
449449
Parameters
450450
----------

src/rehline.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@ void rehline_internal(
2222
const MapMat& X, const MapMat& A, const MapVec& b,
2323
const MapMat& U, const MapMat& V,
2424
const MapMat& S, const MapMat& T, const MapMat& Tau,
25-
int max_iter, double tol, int shrink = 1,
25+
int max_iter, double tol, double rho = 0.0, int shrink = 1,
2626
int verbose = 0, int trace_freq = 100
2727
)
2828
{
2929
rehline::rehline_solver(result, X, A, b, U, V, S, T, Tau,
30-
max_iter, tol, shrink, verbose, trace_freq);
30+
max_iter, tol, rho, shrink, verbose, trace_freq);
3131
}
3232

3333
PYBIND11_MODULE(_internal, m) {
@@ -37,6 +37,7 @@ PYBIND11_MODULE(_internal, m) {
3737
.def_readwrite("xi", &ReHLineResult::xi)
3838
.def_readwrite("Lambda", &ReHLineResult::Lambda)
3939
.def_readwrite("Gamma", &ReHLineResult::Gamma)
40+
.def_readwrite("mu", &ReHLineResult::mu)
4041
.def_readwrite("niter", &ReHLineResult::niter)
4142
.def_readwrite("dual_objfns", &ReHLineResult::dual_objfns)
4243
.def_readwrite("primal_objfns", &ReHLineResult::primal_objfns);

0 commit comments

Comments
 (0)