26#include <Eigen/SparseCore>
394 Eigen::EigenBase<T>& x,
395 const Eigen::MatrixXd& y_in,
396 Eigen::ArrayXd alpha = Eigen::ArrayXd::Zero(0),
397 Eigen::ArrayXd lambda = Eigen::ArrayXd::Zero(0),
400 using Eigen::MatrixXd;
401 using Eigen::VectorXd;
403 const int n = x.rows();
404 const int p = x.cols();
406 const int INTERRUPT_FREQ = 100;
407 bool interrupt =
false;
409 if (n != y_in.rows()) {
410 throw std::invalid_argument(
411 "x and y_in must have the same number of rows");
415 throw std::invalid_argument(
"x must not contain NA, NaN, or Inf values");
418 if (!y_in.array().isFinite().all()) {
419 throw std::invalid_argument(
"y must not contain NA, NaN, or Inf values");
422 auto jit_normalization =
normalize(x.derived(),
425 this->centering_type,
429 std::unique_ptr<Loss> loss =
setupLoss(this->loss_type);
431 MatrixXd y = loss->preprocessResponse(y_in);
433 const int m = y.cols();
435 VectorXd beta0 = VectorXd::Zero(m);
436 VectorXd beta = VectorXd::Zero(p * m);
438 MatrixXd eta = MatrixXd::Zero(n, m);
440 if (this->intercept) {
441 beta0 = loss->link(y.colwise().mean()).transpose();
442 eta.rowwise() = beta0.transpose();
445 MatrixXd residual = loss->residual(eta, y);
446 VectorXd gradient(beta.size());
449 bool user_alpha = alpha.size() > 0;
450 bool user_lambda = lambda.size() > 0;
454 p * m, this->q, this->lambda_type, n, this->theta1, this->theta2);
456 if (lambda.size() != beta.size()) {
457 throw std::invalid_argument(
458 "lambda must be the same length as the number of coefficients");
460 if (lambda.minCoeff() < 0) {
461 throw std::invalid_argument(
"lambda must be non-negative");
463 if (!lambda.isFinite().all()) {
464 throw std::invalid_argument(
"lambda must be finite");
467 for (
int i = 1; i < lambda.size(); ++i) {
468 if (lambda(i) > lambda(i - 1)) {
469 throw std::invalid_argument(
"lambda must be in decreasing order");
482 this->update_clusters,
492 Eigen::VectorXd::Ones(n),
495 int alpha_max_ind =
whichMax(gradient.cwiseAbs());
497 lambda.maxCoeff() == 0.0 ? 0.0 : sl1_norm.
dualNorm(gradient, lambda);
498 const double numerical_stationarity_tol =
499 std::sqrt(std::numeric_limits<double>::epsilon()) *
500 std::max(1.0, gradient.cwiseAbs().maxCoeff());
502 if (alpha_type ==
"path" ||
503 (alpha_type ==
"estimate" && alpha_estimate != 1)) {
504 if (alpha_min_ratio < 0) {
505 alpha_min_ratio = n > gradient.size() ? 1e-4 : 1e-2;
510 path_length = alpha.size();
511 }
else if (alpha_type ==
"estimate" && alpha_estimate == -1) {
512 if (loss_type !=
"quadratic") {
513 throw std::invalid_argument(
"Automatic alpha estimation is only "
514 "available for the quadratic loss");
519 std::unique_ptr<ScreeningRule> screening_rule =
521 std::vector<int> working_set =
522 screening_rule->initialize(
static_cast<int>(beta.size()), alpha_max_ind);
525 double null_deviance = loss->deviance(eta, y);
526 double dev_prev = null_deviance;
530 double alpha_prev = std::max(alpha_max, alpha(0));
532 std::vector<SlopeFit> fits;
533 std::optional<MatrixXd> quadratic_ols_dual_point;
536 for (
int path_step = 0; path_step < this->path_length; ++path_step) {
538 bool local_interrupt =
false;
540#pragma omp critical(check_interrupt)
543 local_interrupt = check_interrupt();
545 if (local_interrupt) {
550 double alpha_curr = alpha(path_step);
552 assert(alpha_curr <= alpha_prev &&
"Alpha must be decreasing");
554 Eigen::ArrayXd lambda_curr = alpha_curr * lambda;
555 Eigen::ArrayXd lambda_prev = alpha_prev * lambda;
557 const bool near_unregularized =
558 lambda_curr.maxCoeff() == 0.0 ||
561 std::sqrt(std::numeric_limits<double>::epsilon()) * alpha_max);
565 if (loss_type ==
"quadratic" && near_unregularized &&
566 !quadratic_ols_dual_point.has_value()) {
567 quadratic_ols_dual_point =
568 detail::quadraticOlsDualPoint(x.derived(),
576 std::optional<double> quadratic_ols_dual;
577 if (quadratic_ols_dual_point.has_value()) {
578 quadratic_ols_dual = lambda_curr.maxCoeff() == 0.0
581 *quadratic_ols_dual_point,
592 std::vector<double> duals, primals, time;
603 Eigen::VectorXd::Ones(x.rows()),
606 screening_rule->screen(
607 working_set, gradient, lambda_curr, lambda_prev, beta);
611 for (; it < this->max_it; ++it, ++total_it) {
613 residual = loss->residual(eta, y);
620 Eigen::VectorXd::Ones(n),
623 double primal = loss->loss(eta, y) +
624 sl1_norm.
eval(beta(working_set),
625 lambda_curr.head(working_set.size()));
629 const bool numerically_stationary =
630 gradient(working_set).cwiseAbs().maxCoeff() <=
631 numerical_stationarity_tol &&
633 residual.colwise().mean().cwiseAbs().maxCoeff() <=
634 numerical_stationarity_tol);
636 MatrixXd dual_point = loss->dualPoint(eta, y, this->intercept);
637 MatrixXd theta = dual_point;
638 VectorXd dual_gradient = VectorXd::Zero(beta.size());
645 Eigen::VectorXd::Ones(n),
648 sl1_norm.
dualNorm(dual_gradient(working_set),
649 lambda_curr.head(working_set.size()),
651 theta.array() /= std::max(1.0, dual_norm);
652 double dual = loss->dual(theta, y, Eigen::VectorXd::Ones(n));
653 if (quadratic_ols_dual.has_value() && numerically_stationary) {
654 dual = std::max(dual, *quadratic_ols_dual);
657 double full_dual = std::numeric_limits<double>::quiet_NaN();
659 if (collect_diagnostics) {
671 if (quadratic_ols_dual.has_value() && numerically_stationary) {
672 full_dual = std::max(full_dual, *quadratic_ols_dual);
676 time.emplace_back(timer.
elapsed());
677 primals.emplace_back(primal);
678 duals.emplace_back(full_dual);
681 double dual_gap = primal - dual;
683 assert(dual_gap > -1e-6 &&
"Dual gap should be positive");
686 const bool unregularized_stationary = loss_type ==
"quadratic" &&
687 lambda_curr.maxCoeff() == 0.0 &&
688 numerically_stationary;
690 if (dual_gap <= tol_scaled || unregularized_stationary ||
691 it == this->max_it) {
693 screening_rule->checkKktViolations(gradient,
703 if (!std::isfinite(full_dual)) {
714 if (quadratic_ols_dual.has_value() && numerically_stationary) {
715 full_dual = std::max(full_dual, *quadratic_ols_dual);
718 if (primal - full_dual <= tol_scaled || unregularized_stationary) {
726 if (it % INTERRUPT_FREQ == 0) {
727 bool local_interrupt =
false;
729#pragma omp critical(check_interrupt)
732 local_interrupt = check_interrupt();
734 if (local_interrupt) {
754 if (it == this->max_it) {
757 "Maximum number of iterations reached at step = " +
758 std::to_string(path_step) +
".");
761 alpha_prev = alpha_curr;
764 double dev = loss->deviance(eta, y);
765 double dev_ratio = 1 - dev / null_deviance;
766 double dev_change = path_step == 0 ? 1.0 : 1 - dev / dev_prev;
771 if (return_clusters) {
776 beta.reshaped(p, m).sparseView(),
786 this->centering_type,
792 fits.emplace_back(std::move(
fit));
799 int n_unique =
unique(beta.cwiseAbs()).size();
800 if (dev_ratio > dev_ratio_tol || dev_change < dev_change_tol ||
801 n_unique >= this->max_clusters.value_or(n + 1)) {
829 const Eigen::MatrixXd& y_in,
830 const double alpha = 1.0,
831 Eigen::ArrayXd lambda = Eigen::ArrayXd::Zero(0),
834 Eigen::ArrayXd alpha_arr(1);
835 alpha_arr(0) = alpha;
836 SlopePath res =
path(x, y_in, alpha_arr, lambda, check_interrupt);
867 Eigen::EigenBase<T>& x,
875 Slope model_copy = *
this;
878 std::vector<int> selected;
879 Eigen::ArrayXd alpha(1);
885 this->alpha_estimate = alpha(0);
887 model_copy.
path(x, y, alpha, Eigen::ArrayXd::Zero(0), check_interrupt);
889 for (
int it = 0; it < this->alpha_est_maxit; ++it) {
890 T x_selected =
subsetCols(x.derived(), selected);
892 std::vector<int> selected_prev = selected;
895 alpha(0) =
estimateNoise(x_selected, y, this->intercept) / n;
896 this->alpha_estimate = alpha(0);
898 result = model_copy.
path(
899 x, y, alpha, Eigen::ArrayXd::Zero(0), check_interrupt);
900 auto coefs = result.
getCoefs().back();
902 for (
typename Eigen::SparseMatrix<double>::InnerIterator it(coefs, 0);
905 selected.emplace_back(it.row());
908 if (selected == selected_prev) {
912 if (
static_cast<int>(selected.size()) >= n + this->intercept) {
913 throw std::runtime_error(
914 "selected >= n - 1 variables, cannot estimate variance");
920 "Maximum iterations reached in alpha estimation");
943 const Eigen::VectorXd& y_in,
944 const double gamma = 0.0,
945 Eigen::VectorXd beta0 = Eigen::VectorXd(0),
946 Eigen::VectorXd beta = Eigen::VectorXd(0))
948 using Eigen::MatrixXd;
949 using Eigen::VectorXd;
954 if (beta0.size() == 0) {
958 if (beta.size() == 0) {
966 std::vector<double> primals, duals, time;
969 auto jit_normalization =
970 normalize(x, x_centers, x_scales, centering_type, scaling_type, modify_x);
972 bool update_clusters =
false;
974 std::unique_ptr<Loss> loss =
setupLoss(this->loss_type);
976 MatrixXd y = loss->preprocessResponse(y_in);
980 Eigen::ArrayXd lambda_cumsum_relax = Eigen::ArrayXd::Zero(p * m + 1);
984 Eigen::MatrixXd eta = linearPredictor(x,
992 VectorXd gradient = VectorXd::Zero(p * m);
993 MatrixXd residual(n, m);
994 MatrixXd working_residual(n, m);
996 MatrixXd w = MatrixXd::Ones(n, m);
997 MatrixXd w_ones = MatrixXd::Ones(n, m);
1004 if (random_seed.has_value()) {
1005 rng.seed(*random_seed);
1007 rng.seed(std::random_device{}());
1012 for (
int irls_it = 0; irls_it < max_it_outer_relax; irls_it++) {
1013 residual = loss->residual(eta, y);
1015 if (collect_diagnostics) {
1016 primals.push_back(loss->loss(eta, y));
1017 duals.push_back(0.0);
1018 time.push_back(timer.
elapsed());
1030 double norm_grad = cluster_gradient.lpNorm<Eigen::Infinity>();
1032 if (norm_grad < tol_relax) {
1036 loss->updateWeightsAndWorkingResponse(w, z, eta, y);
1037 working_residual = eta - z;
1038 const VectorXd weight_sums = w.colwise().sum().transpose();
1040 for (
int inner_it = 0; inner_it < max_it_inner_relax; ++inner_it) {
1047 lambda_cumsum_relax,
1059 if (max_abs_gradient < tol_relax) {
1064 eta = working_residual + z;
1066 if (irls_it == max_it_outer_relax) {
1068 "Maximum number of IRLS iterations reached.");
1072 double dev = loss->deviance(eta, y);
1077 beta = (1 - gamma) * beta + gamma * old_coefs;
1081 beta.reshaped(p, m).sparseView(),
1112 template<
typename T>
1115 const Eigen::VectorXd& y,
1116 const double gamma = 0.0)
1118 std::vector<SlopeFit> fits;
1120 for (
size_t i = 0; i <
path.
size(); i++) {
1126 auto relaxed_fit =
relax(
path(i), x, y, gamma);
1128 fits.emplace_back(relaxed_fit);
1136 bool collect_diagnostics =
false;
1137 bool intercept =
true;
1138 bool modify_x =
false;
1139 bool return_clusters =
true;
1140 bool update_clusters =
true;
1141 double alpha_min_ratio = -1;
1142 double dev_change_tol = 1e-5;
1143 double dev_ratio_tol = 0.999;
1144 double learning_rate_decr = 0.5;
1146 double theta1 = 1.0;
1147 double theta2 = 0.5;
1149 double tol_relax = 1e-4;
1150 double alpha_estimate = -1;
1151 int alpha_est_maxit = 1000;
1152 int cd_iterations = 10;
1154 int max_it_inner_relax = 1e5;
1155 int max_it_outer_relax = 50;
1156 int path_length = 100;
1157 std::optional<int> max_clusters = std::nullopt;
1158 std::optional<int> random_seed = 0;
1159 std::string alpha_type =
"path";
1160 std::string cd_type =
"permuted";
1161 std::string centering_type =
"mean";
1162 std::string lambda_type =
"bh";
1163 std::string loss_type =
"quadratic";
1164 std::string scaling_type =
"sd";
1165 std::string screening_type =
"strong";
1166 std::string solver_type =
"auto";
1169 Eigen::VectorXd x_centers;
1170 Eigen::VectorXd x_scales;
Representation of the nonzero clusters in SLOPE.
void update(const int old_index, const int new_index, const double c_new)
Updates the cluster structure when an index is changed.
A class representing the results of SLOPE (Sorted L1 Penalized Estimation) fitting.
Eigen::VectorXd getIntercepts(const bool original_scale=true) const
Gets the intercept terms for this SLOPE fit.
const Eigen::ArrayXd & getLambda() const
Gets the lambda (regularization) parameter used.
double getAlpha() const
Gets the alpha (mixing) parameter used.
Eigen::SparseMatrix< double > getCoefs(const bool original_scale=true) const
Gets the sparse coefficient matrix for this fit.
double getNullDeviance() const
Gets the null model deviance.
Container class for SLOPE regression solution paths.
std::size_t size() const
Gets the number of solutions in the path.
std::vector< Eigen::SparseMatrix< double > > getCoefs(const bool original_scale=true) const
Returns the vector of coefficient matrices for each solution in the path.
void setSolver(const std::string &solver)
Sets the numerical solver used to fit the model.
void setAlphaMinRatio(double alpha_min_ratio)
Sets the alpha min ratio.
void setMaxIterations(int max_it)
Sets the maximum number of iterations.
void setAlphaEstimationMaxIterations(const int alpha_est_maxit)
Sets the maximum number of iterations for the alpha estimation procedure.
void setRelaxMaxInnerIterations(int max_it)
Sets the maximum number of inner iterations for the relaxed solver.
void setRandomSeed(std::optional< int > seed)
Sets the random seed.
int getRandomSeed() const
Gets the random seed.
SlopePath path(Eigen::EigenBase< T > &x, const Eigen::MatrixXd &y_in, Eigen::ArrayXd alpha=Eigen::ArrayXd::Zero(0), Eigen::ArrayXd lambda=Eigen::ArrayXd::Zero(0), std::function< bool()> check_interrupt=defaultInterruptChecker)
Computes SLOPE regression solution path for multiple alpha and lambda values.
SlopePath estimateAlpha(Eigen::EigenBase< T > &x, Eigen::MatrixXd &y, std::function< bool()> check_interrupt=defaultInterruptChecker)
Estimates the regularization parameter alpha for SLOPE regression.
void setDevRatioTol(const double dev_ratio_tol)
Sets tolerance in deviance change for early stopping.
void setScaling(const std::string &type)
Sets the scaling type.
void setRandomSeed(const int seed)
Sets the random seed.
void setDevChangeTol(const double dev_change_tol)
Sets tolerance in deviance change for early stopping.
const std::string & getLossType()
Get currently defined loss type.
void setReturnClusters(const bool return_clusters)
Sets the return clusters flag.
void setRelaxMaxOuterIterations(int max_it)
Sets the maximum number of outer (IRLS) iterations for the relaxed solver.
void setMaxClusters(const int max_clusters)
Sets the maximum number of clusters.
void setIntercept(bool intercept)
Sets the intercept flag.
bool getFitIntercept() const
Returns the intercept flag.
void setDiagnostics(const bool collect_diagnostics)
Toggles collection of diagnostics.
void setNormalization(const std::string &type)
Sets normalization type for the design matrix.
SlopeFit relax(const SlopeFit &fit, T &x, const Eigen::VectorXd &y_in, const double gamma=0.0, Eigen::VectorXd beta0=Eigen::VectorXd(0), Eigen::VectorXd beta=Eigen::VectorXd(0))
Relaxes a fitted SLOPE model.
int getAlphaEstimationMaxIterations() const
Gets the maximum number of iterations allowed for the alpha estimation procedure.
void setScreening(const std::string &screening_type)
Sets the type of feature screening used, which discards predictors that are unlikely to be active.
void setOscarParameters(const double theta1, const double theta2)
Sets OSCAR parameters.
void setLambdaType(const std::string &lambda_type)
Sets the lambda type for regularization weights.
void setAlphaType(const std::string &alpha_type)
Sets the alpha type.
SlopeFit fit(Eigen::EigenBase< T > &x, const Eigen::MatrixXd &y_in, const double alpha=1.0, Eigen::ArrayXd lambda=Eigen::ArrayXd::Zero(0), std::function< bool()> check_interrupt=defaultInterruptChecker)
Fits a single SLOPE regression model for given alpha and lambda values.
void setModifyX(const bool modify_x)
Controls if x should be modified-in-place.
void setCentering(const std::string &type)
Sets the center points for feature normalization.
void setHybridCdType(const std::string &cd_type)
Sets the frequence of proximal gradient descent steps.
bool hasRandomSeed() const
Checks if a random seed is set.
void setLearningRateDecr(double learning_rate_decr)
Sets the learning rate decrement.
void setLoss(const std::string &loss_type)
Sets the loss function type.
SlopePath relax(const SlopePath &path, T &x, const Eigen::VectorXd &y, const double gamma=0.0)
Relaxes a fitted SLOPE path.
void setScaling(const Eigen::VectorXd &x_scales)
Sets the scaling factors for feature normalization.
void setRelaxTol(double tol)
Sets the tolerance value for the relaxed SLOPE solver.
void setPathLength(int path_length)
Sets the path length.
void setCentering(const Eigen::VectorXd &x_centers)
Sets the center points for feature normalization.
void setHybridCdIterations(int cd_iterations)
Sets the frequence of proximal gradient descent steps.
void setTol(double tol)
Sets the tolerance value.
void setQ(double q)
Sets the q value.
void setUpdateClusters(bool update_clusters)
Sets the update clusters flag.
Class representing the Sorted L1 Norm.
double eval(const Eigen::VectorXd &beta, const Eigen::ArrayXd &lambda) const
Evaluates the Sorted L1 Norm.
double dualNorm(const Eigen::VectorXd &a, const Eigen::ArrayXd &lambda) const
Computes the dual norm of a vector.
Timer class for measuring elapsed time with high resolution.
void start()
Starts the timer by recording the current time point.
void resume()
Resumes the timer after a pause.
double elapsed() const
Returns the elapsed time in seconds since start() was called.
void pause()
Pauses the timer.
static void addWarning(WarningCode code, const std::string &message)
Log a new warning.
The declaration of the Clusters class.
Definitions of constants used in libslope.
Diagnostics for SLOPE optimization.
Functions for estimating noise level and regularization parameter alpha.
An implementation of the coordinate descent step in the hybrid algorithm for solving SLOPE.
Thread-safe warning logging facility for the slope library.
The declartion of the Objctive class and its subclasses, which represent the data-fitting part of the...
Mathematical support functions for the slope package.
constexpr double EPSILON
Small value used for floating-point comparisons to handle precision issues.
constexpr double MAX_DIV
Maximum allowed divisor.
Namespace containing SLOPE regression implementation.
double coordinateDescent(Eigen::VectorXd &beta0, Eigen::VectorXd &beta, Eigen::MatrixXd &residual, Clusters &clusters, const Eigen::ArrayXd &lambda_cumsum, const T &x, const Eigen::MatrixXd &w, const Eigen::VectorXd &weight_sums, const Eigen::VectorXd &x_centers, const Eigen::VectorXd &x_scales, const bool intercept, const JitNormalization jit_normalization, const bool update_clusters, std::mt19937 &rng, const std::string &cd_type="cyclical")
Eigen::ArrayXd regularizationPath(const Eigen::ArrayXd &alpha_in, const int path_length, double alpha_min_ratio, const double alpha_max)
std::unique_ptr< SolverBase > setupSolver(const std::string &solver_type, const std::string &loss, JitNormalization jit_normalization, bool intercept, bool update_clusters, int cd_iterations, const std::string &cd_type, std::optional< int > random_seed=std::nullopt)
Factory function to create and configure a SLOPE solver.
double computeDualFromPoint(const Eigen::VectorXd &beta, Eigen::MatrixXd theta, const std::unique_ptr< Loss > &loss, const SortedL1Norm &sl1_norm, const Eigen::ArrayXd &lambda, const MatrixType &x, const Eigen::MatrixXd &y, const Eigen::VectorXd &x_centers, const Eigen::VectorXd &x_scales, const JitNormalization &jit_normalization)
Scales a candidate into the SLOPE dual constraint and evaluates it.
std::unique_ptr< Loss > setupLoss(const std::string &loss)
Factory function to create the appropriate loss function based on the distribution family.
bool defaultInterruptChecker()
Default no-op interrupt checker.
std::unordered_set< double > unique(const Eigen::MatrixXd &x)
Create a set of unique values from an Eigen matrix.
double estimateNoise(Eigen::EigenBase< T > &x, Eigen::MatrixXd &y, const bool fit_intercept)
Estimates noise (standard error) in a linear model using OLS residuals.
T subsetCols(const Eigen::MatrixBase< T > &x, const std::vector< int > &indices)
Extract specified columns from a dense matrix.
std::unique_ptr< ScreeningRule > createScreeningRule(const std::string &screening_type)
Creates a screening rule based on the provided type.
@ MAXIT_REACHED
Maximum iterations reached without convergence.
Eigen::ArrayXd lambdaSequence(const int p, const double q, const std::string &type, const int n=-1, const double theta1=1.0, const double theta2=1.0)
void updateGradient(Eigen::VectorXd &gradient, const T &x, const Eigen::MatrixXd &residual, const std::vector< int > &active_set, const Eigen::VectorXd &x_centers, const Eigen::VectorXd &x_scales, const Eigen::VectorXd &w, const JitNormalization jit_normalization)
Computes the gradient for selected coefficients.
Eigen::VectorXd clusterGradient(Eigen::VectorXd &beta, Eigen::MatrixXd &residual, Clusters &clusters, const T &x, const Eigen::MatrixXd &w, const Eigen::VectorXd &x_centers, const Eigen::VectorXd &x_scales, const JitNormalization jit_normalization)
std::vector< int > activeSet(const Eigen::VectorXd &beta)
Identifies previously active variables.
JitNormalization normalize(Eigen::MatrixBase< T > &x, Eigen::VectorXd &x_centers, Eigen::VectorXd &x_scales, const std::string ¢ering_type, const std::string &scaling_type, const bool modify_x)
int whichMax(const T &x)
Returns the index of the maximum element in a container.
bool isFinite(const Eigen::DenseBase< Derived > &x)
Check if all elements in a dense matrix are finite.
Functions to normalize the design matrix and rescale coefficients in case the design was normalized.
Functions for generating regularization sequences for SLOPE.
Screening rules for SLOPE regression optimization.
Factory function to create the appropriate loss function based on.
Factory function to create and configure a SLOPE solver.
SLOPE (Sorted L-One Penalized Estimation) fitting results.
Defines the SlopePath class for storing and accessing SLOPE regression solution paths.
The declaration of the SortedL1Norm class.
Simple high-resolution timer class for performance measurements.