slope 6.6.0
Loading...
Searching...
No Matches
slope.h
Go to the documentation of this file.
1
5
6#pragma once
7
8#include "clusters.h"
9#include "constants.h"
10#include "diagnostics.h"
11#include "estimate_alpha.h"
12#include "logger.h"
13#include "losses/loss.h"
14#include "losses/setup_loss.h"
15#include "math.h"
16#include "normalize.h"
17#include "observation_weights.h"
19#include "screening.h"
20#include "slope_fit.h"
21#include "slope_path.h"
22#include "solvers/hybrid_cd.h"
24#include "sorted_l1_norm.h"
25#include "timer.h"
26#include <Eigen/Core>
27#include <Eigen/SparseCore>
28#include <cassert>
29#include <cmath>
30#include <limits>
31#include <memory>
32#include <optional>
33
37namespace slope {
38
39namespace detail {
40struct SlopeAccess;
41}
42
47inline bool
49{
50 return false;
51}
52
61class Slope
62{
63public:
69 Slope() = default;
70
79 void setSolver(const std::string& solver);
80
86 void setIntercept(bool intercept);
87
95 void setNormalization(const std::string& type);
96
107 void setUpdateClusters(bool update_clusters);
108
115 void setReturnClusters(const bool return_clusters);
116
124 void setAlphaMinRatio(double alpha_min_ratio);
125
140 void setAlphaType(const std::string& alpha_type);
141
153 void setAlphaScale(const std::string& alpha_scale);
154
161 void setLearningRateDecr(double learning_rate_decr);
162
169 void setQ(double q);
170
177 void setOscarParameters(const double theta1, const double theta2);
178
184 void setTol(double tol);
185
191 void setRelaxTol(double tol);
192
201
209
217 void setMaxIterations(int max_it);
218
224 void setPathLength(int path_length);
225
233 void setHybridCdIterations(int cd_iterations);
234
240 void setHybridCdType(const std::string& cd_type);
241
249 void setLambdaType(const std::string& lambda_type);
250
261 void setLoss(const std::string& loss_type);
262
272 void setScreening(const std::string& screening_type);
273
281 void setModifyX(const bool modify_x);
282
287 void setDevChangeTol(const double dev_change_tol);
288
294 void setDevRatioTol(const double dev_ratio_tol);
295
303 void setMaxClusters(const int max_clusters);
304
309 void setCentering(const std::string& type);
310
316 void setCentering(const Eigen::VectorXd& x_centers);
317
322 void setScaling(const std::string& type);
323
330 void setDiagnostics(const bool collect_diagnostics);
331
337 void setScaling(const Eigen::VectorXd& x_scales);
338
349 void setAlphaEstimationMaxIterations(const int alpha_est_maxit);
350
356 void setRandomSeed(const int seed);
357
363 void setRandomSeed(std::optional<int> seed);
364
368 bool hasRandomSeed() const;
369
373 int getRandomSeed() const;
374
380
384 bool getFitIntercept() const;
385
390 const std::string& getLossType();
391
415 template<typename T>
417 Eigen::EigenBase<T>& x,
418 const Eigen::MatrixXd& y_in,
419 Eigen::ArrayXd alpha = Eigen::ArrayXd::Zero(0),
420 Eigen::ArrayXd lambda = Eigen::ArrayXd::Zero(0),
421 std::function<bool()> check_interrupt = defaultInterruptChecker,
422 const Eigen::VectorXd& observation_weights = Eigen::VectorXd())
423 {
424 return pathImpl(x,
425 y_in,
426 std::move(alpha),
427 std::move(lambda),
428 check_interrupt,
429 observation_weights,
430 false);
431 }
432
433private:
434 friend struct detail::SlopeAccess;
435
436 template<typename T>
437 SlopePath pathImpl(Eigen::EigenBase<T>& x,
438 const Eigen::MatrixXd& y_in,
439 Eigen::ArrayXd alpha,
440 Eigen::ArrayXd lambda,
441 std::function<bool()> check_interrupt,
442 const Eigen::VectorXd& observation_weights,
443 bool response_prepared)
444 {
445 using Eigen::MatrixXd;
446 using Eigen::VectorXd;
447
448 const int n = x.rows();
449 const int p = x.cols();
450 const VectorXd weights =
451 detail::normalizeObservationWeights(observation_weights, n);
452 const int n_positive = detail::positiveWeightCount(weights);
453 if (!detail::uniformObservationWeights(weights) &&
454 (this->alpha_type == "estimate" ||
455 (lambda.size() == 0 && this->lambda_type == "gaussian"))) {
456 throw std::invalid_argument(
457 "Nonuniform observation weights are not supported for noise-based "
458 "alpha estimation or automatic Gaussian lambda correction");
459 }
460
461 const int INTERRUPT_FREQ = 100;
462 bool interrupt = false;
463
464 if (n != y_in.rows()) {
465 throw std::invalid_argument(
466 "x and y_in must have the same number of rows");
467 }
468
469 if (!isFinite(x.derived())) {
470 throw std::invalid_argument("x must not contain NA, NaN, or Inf values");
471 }
472
473 if (!y_in.array().isFinite().all()) {
474 throw std::invalid_argument("y must not contain NA, NaN, or Inf values");
475 }
476
477 auto jit_normalization = normalize(x.derived(),
478 this->x_centers,
479 this->x_scales,
480 this->centering_type,
481 this->scaling_type,
482 this->modify_x,
483 weights);
484
485 std::unique_ptr<Loss> loss = setupLoss(this->loss_type);
486
487 MatrixXd y = response_prepared ? y_in : loss->preprocessResponse(y_in);
488
489 const int m = y.cols();
490
491 VectorXd beta0 = VectorXd::Zero(m);
492 VectorXd beta = VectorXd::Zero(p * m);
493
494 MatrixXd eta = MatrixXd::Zero(n, m); // linear predictor
495
496 if (this->intercept) {
497 beta0 = loss->link(detail::weightedMean(y, weights)).transpose();
498 eta.rowwise() = beta0.transpose();
499 }
500
501 MatrixXd residual = loss->residual(eta, y);
502 detail::maskZeroWeightRows(residual, weights);
503 VectorXd gradient(beta.size());
504
505 // Path data
506 bool user_alpha = alpha.size() > 0;
507 bool user_lambda = lambda.size() > 0;
508
509 if (!user_lambda) {
510 lambda = lambdaSequence(p * m,
511 this->q,
512 this->lambda_type,
513 n_positive,
514 this->theta1,
515 this->theta2);
516 } else {
517 if (lambda.size() != beta.size()) {
518 throw std::invalid_argument(
519 "lambda must be the same length as the number of coefficients");
520 }
521 if (lambda.minCoeff() < 0) {
522 throw std::invalid_argument("lambda must be non-negative");
523 }
524 if (!lambda.isFinite().all()) {
525 throw std::invalid_argument("lambda must be finite");
526 }
527 // Check that lambda is in decreasing order
528 for (int i = 1; i < lambda.size(); ++i) {
529 if (lambda(i) > lambda(i - 1)) {
530 throw std::invalid_argument("lambda must be in decreasing order");
531 }
532 }
533 }
534
535 // Setup the regularization sequence and path
536 SortedL1Norm sl1_norm;
537
538 // TODO: Make this part of the slope class
539 auto solver = setupSolver(this->solver_type,
540 this->loss_type,
541 jit_normalization,
542 this->intercept,
543 this->update_clusters,
544 this->cd_iterations,
545 this->cd_type,
546 this->random_seed);
547
548 updateGradient(gradient,
549 x.derived(),
550 residual,
551 this->x_centers,
552 this->x_scales,
553 weights,
554 jit_normalization);
555
556 int alpha_max_ind = whichMax(gradient.cwiseAbs());
557 double alpha_max =
558 lambda.maxCoeff() == 0.0 ? 0.0 : sl1_norm.dualNorm(gradient, lambda);
559 const double numerical_stationarity_tol =
560 std::sqrt(std::numeric_limits<double>::epsilon()) *
561 std::max(1.0, gradient.cwiseAbs().maxCoeff());
562
563 if (alpha_type == "path" ||
564 (alpha_type == "estimate" && alpha_estimate != 1)) {
565 const double min_ratio = alpha_min_ratio < 0
566 ? (n_positive > gradient.size() ? 1e-4 : 1e-2)
567 : alpha_min_ratio;
568 alpha = regularizationPath(alpha, path_length, min_ratio, alpha_max);
569 if (user_alpha && alpha_scale == "relative") {
570 alpha *= alpha_max;
571 if (!alpha.isFinite().all()) {
572 throw std::invalid_argument(
573 "Relative alpha values must produce finite absolute penalties");
574 }
575 }
576 path_length = alpha.size();
577 } else if (alpha_type == "estimate" && alpha_estimate == -1) {
578 if (loss_type != "quadratic") {
579 throw std::invalid_argument("Automatic alpha estimation is only "
580 "available for the quadratic loss");
581 }
582 }
583
584 // Screening setup
585 std::unique_ptr<ScreeningRule> screening_rule =
586 createScreeningRule(this->screening_type);
587 std::vector<int> working_set =
588 screening_rule->initialize(static_cast<int>(beta.size()), alpha_max_ind);
589
590 // Path variables
591 double null_deviance = loss->deviance(eta, y, weights);
592 double dev_prev = null_deviance;
593
594 Timer timer;
595
596 double alpha_prev = std::max(alpha_max, alpha(0));
597
598 std::vector<SlopeFit> fits;
599 std::optional<MatrixXd> quadratic_ols_dual_point;
600
601 // Regularization path loop
602 for (int path_step = 0; path_step < this->path_length; ++path_step) {
603 // Check for interrupt at the start of each path step
604 bool local_interrupt = false;
605#ifdef _OPENMP
606#pragma omp critical(check_interrupt)
607#endif
608 {
609 local_interrupt = check_interrupt();
610 }
611 if (local_interrupt) {
612 interrupt = true;
613 break;
614 }
615
616 double alpha_curr = alpha(path_step);
617
618 assert(alpha_curr <= alpha_prev && "Alpha must be decreasing");
619
620 Eigen::ArrayXd lambda_curr = alpha_curr * lambda;
621 Eigen::ArrayXd lambda_prev = alpha_prev * lambda;
622
623 const bool near_unregularized =
624 lambda_curr.maxCoeff() == 0.0 ||
625 (alpha_max > 0.0 &&
626 alpha_curr <=
627 std::sqrt(std::numeric_limits<double>::epsilon()) * alpha_max);
628 // The QR residual avoids amplifying roundoff in the loss gradient when
629 // the penalty is below numerical resolution. It remains only a candidate
630 // until computeDualFromPoint checks and, if necessary, scales it below.
631 if (loss_type == "quadratic" && near_unregularized &&
632 !quadratic_ols_dual_point.has_value()) {
633 quadratic_ols_dual_point =
634 detail::quadraticOlsDualPoint(x.derived(),
635 y,
636 this->x_centers,
637 this->x_scales,
638 jit_normalization,
639 this->intercept,
640 weights);
641 }
642
643 std::optional<double> quadratic_ols_dual;
644 if (quadratic_ols_dual_point.has_value()) {
645 quadratic_ols_dual = lambda_curr.maxCoeff() == 0.0
646 ? 0.0
648 *quadratic_ols_dual_point,
649 loss,
650 sl1_norm,
651 lambda_curr,
652 x.derived(),
653 y,
654 this->x_centers,
655 this->x_scales,
656 jit_normalization,
657 weights);
658 }
659
660 std::vector<double> duals, primals, time;
661 timer.start();
662
663 // Update gradient for the full set
664 // TODO: Only update for non-working set since gradient is updated before
665 // the convergence check in the inner loop for the working set
666 updateGradient(gradient,
667 x.derived(),
668 residual,
669 x_centers,
670 x_scales,
671 weights,
672 jit_normalization);
673
674 screening_rule->screen(
675 working_set, gradient, lambda_curr, lambda_prev, beta);
676
677 int working_set_it = 0;
678 int total_it = 0;
679 for (; total_it < this->max_it; ++working_set_it, ++total_it) {
680 // Compute primal, dual, and gap
681 residual = loss->residual(eta, y);
682 detail::maskZeroWeightRows(residual, weights);
683 updateGradient(gradient,
684 x.derived(),
685 residual,
686 working_set,
687 this->x_centers,
688 this->x_scales,
689 weights,
690 jit_normalization);
691
692 double primal = loss->loss(eta, y, weights) +
693 sl1_norm.eval(beta(working_set),
694 lambda_curr.head(working_set.size()));
695
696 // The OLS bound can be loose on correlated designs, so it may certify
697 // convergence only after the loss itself is numerically stationary.
698 const bool numerically_stationary =
699 gradient(working_set).cwiseAbs().maxCoeff() <=
700 numerical_stationarity_tol &&
701 (!this->intercept ||
702 detail::weightedMean(residual, weights).cwiseAbs().maxCoeff() <=
703 numerical_stationarity_tol);
704
705 MatrixXd dual_point = loss->dualPoint(eta, y, this->intercept, weights);
706 MatrixXd theta = dual_point;
707 VectorXd dual_gradient = VectorXd::Zero(beta.size());
708 updateGradient(dual_gradient,
709 x.derived(),
710 theta,
711 working_set,
712 this->x_centers,
713 this->x_scales,
714 weights,
715 jit_normalization);
716 double dual_norm =
717 sl1_norm.dualNorm(dual_gradient(working_set),
718 lambda_curr.head(working_set.size()),
720 theta.array() /= std::max(1.0, dual_norm);
721 double dual = loss->dual(theta, y, weights);
722 if (quadratic_ols_dual.has_value() && numerically_stationary) {
723 dual = std::max(dual, *quadratic_ols_dual);
724 }
725
726 double full_dual = std::numeric_limits<double>::quiet_NaN();
727
728 if (collect_diagnostics) {
729 timer.pause();
730 full_dual = computeDualFromPoint(beta,
731 dual_point,
732 loss,
733 sl1_norm,
734 lambda_curr,
735 x.derived(),
736 y,
737 this->x_centers,
738 this->x_scales,
739 jit_normalization,
740 weights);
741 if (quadratic_ols_dual.has_value() && numerically_stationary) {
742 full_dual = std::max(full_dual, *quadratic_ols_dual);
743 }
744 timer.resume();
745
746 time.emplace_back(timer.elapsed());
747 primals.emplace_back(primal);
748 duals.emplace_back(full_dual);
749 }
750
751 double dual_gap = primal - dual;
752
753 assert(dual_gap > -1e-6 && "Dual gap should be positive");
754
755 double tol_scaled = (std::abs(primal) + constants::EPSILON) * this->tol;
756 const bool unregularized_stationary = loss_type == "quadratic" &&
757 lambda_curr.maxCoeff() == 0.0 &&
758 numerically_stationary;
759
760 if (dual_gap <= tol_scaled || unregularized_stationary ||
761 working_set_it + 1 == this->max_it) {
762 MatrixXd screening_residual = residual;
763 for (int i = 0; i < n; ++i) {
764 if (weights(i) == 0.0) {
765 screening_residual.row(i).setZero();
766 } else {
767 screening_residual.row(i) *= weights(i);
768 }
769 }
770 bool no_violations =
771 screening_rule->checkKktViolations(gradient,
772 beta,
773 lambda_curr,
774 working_set,
775 x.derived(),
776 screening_residual,
777 this->x_centers,
778 this->x_scales,
779 jit_normalization);
780 if (no_violations) {
781 if (!std::isfinite(full_dual)) {
782 full_dual = computeDualFromPoint(beta,
783 dual_point,
784 loss,
785 sl1_norm,
786 lambda_curr,
787 x.derived(),
788 y,
789 this->x_centers,
790 this->x_scales,
791 jit_normalization,
792 weights);
793 if (quadratic_ols_dual.has_value() && numerically_stationary) {
794 full_dual = std::max(full_dual, *quadratic_ols_dual);
795 }
796 }
797 if (primal - full_dual <= tol_scaled || unregularized_stationary) {
798 break;
799 }
800 } else {
801 working_set_it = 0; // Restart if there are KKT violations
802 }
803 }
804
805 if (working_set_it % INTERRUPT_FREQ == 0) {
806 bool local_interrupt = false;
807#ifdef _OPENMP
808#pragma omp critical(check_interrupt)
809#endif
810 {
811 local_interrupt = check_interrupt();
812 }
813 if (local_interrupt) {
814 interrupt = true;
815 break;
816 }
817 }
818
819 solver->run(beta0,
820 beta,
821 eta,
822 lambda_curr,
823 loss,
824 sl1_norm,
825 gradient,
826 working_set,
827 x.derived(),
828 this->x_centers,
829 this->x_scales,
830 y,
831 weights);
832 }
833
834 if (total_it == this->max_it) {
837 "Maximum number of iterations reached at step = " +
838 std::to_string(path_step) + ".");
839 }
840
841 alpha_prev = alpha_curr;
842
843 // Compute early stopping criteria
844 double dev = loss->deviance(eta, y, weights);
845 double dev_ratio = 1 - dev / null_deviance;
846 double dev_change = path_step == 0 ? 1.0 : 1 - dev / dev_prev;
847 dev_prev = dev;
848
849 Clusters clusters;
850
851 if (return_clusters) {
852 clusters.update(beta);
853 }
854
855 SlopeFit fit{ beta0,
856 beta.reshaped(p, m).sparseView(),
857 clusters,
858 alpha_curr,
859 lambda,
860 dev,
861 null_deviance,
862 primals,
863 duals,
864 time,
865 total_it,
866 this->centering_type,
867 this->scaling_type,
868 this->intercept,
869 this->x_centers,
870 this->x_scales };
871
872 fits.emplace_back(std::move(fit));
873
874 if (interrupt) {
875 break;
876 }
877
878 if (!user_alpha) {
879 int n_unique = unique(beta.cwiseAbs()).size();
880 if (dev_ratio > dev_ratio_tol || dev_change < dev_change_tol ||
881 n_unique >= this->max_clusters.value_or(n_positive + 1)) {
882 break;
883 }
884 }
885 }
886
887 return fits;
888 }
889
890public:
910 template<typename T>
911 SlopeFit fit(Eigen::EigenBase<T>& x,
912 const Eigen::MatrixXd& y_in,
913 const double alpha = 1.0,
914 Eigen::ArrayXd lambda = Eigen::ArrayXd::Zero(0),
915 std::function<bool()> check_interrupt = defaultInterruptChecker,
916 const Eigen::VectorXd& observation_weights = Eigen::VectorXd())
917 {
918 Eigen::ArrayXd alpha_arr(1);
919 alpha_arr(0) = alpha;
920 SlopePath res =
921 path(x, y_in, alpha_arr, lambda, check_interrupt, observation_weights);
922
923 return { res(0) };
924 };
925
952 template<typename T>
954 Eigen::EigenBase<T>& x,
955 Eigen::MatrixXd& y,
956 std::function<bool()> check_interrupt = defaultInterruptChecker,
957 const Eigen::VectorXd& observation_weights = Eigen::VectorXd())
958 {
959 const auto weights =
960 detail::normalizeObservationWeights(observation_weights, x.rows());
961 if (!detail::uniformObservationWeights(weights)) {
962 throw std::invalid_argument("Nonuniform observation weights are not "
963 "supported for noise-based alpha estimation");
964 }
965 int n = x.rows();
966 int p = x.cols();
967
968 // Create a copy with alpha type set to path to avoid recursion
969 Slope model_copy = *this;
970 model_copy.setAlphaType("path");
971 model_copy.setAlphaScale("absolute");
972
973 std::vector<int> selected;
974 Eigen::ArrayXd alpha(1);
975 SlopePath result;
976
977 // Estimate the noise level, if possible
978 if (n >= p + 30) {
979 alpha(0) = estimateNoise(x, y, this->intercept) / n;
980 this->alpha_estimate = alpha(0);
981 result = model_copy.path(
982 x, y, alpha, Eigen::ArrayXd::Zero(0), check_interrupt, weights);
983 } else {
984 for (int it = 0; it < this->alpha_est_maxit; ++it) {
985 T x_selected = subsetCols(x.derived(), selected);
986
987 std::vector<int> selected_prev = selected;
988 selected.clear();
989
990 alpha(0) = estimateNoise(x_selected, y, this->intercept) / n;
991 this->alpha_estimate = alpha(0);
992
993 result = model_copy.path(
994 x, y, alpha, Eigen::ArrayXd::Zero(0), check_interrupt, weights);
995 auto coefs = result.getCoefs().back();
996
997 for (typename Eigen::SparseMatrix<double>::InnerIterator it(coefs, 0);
998 it;
999 ++it) {
1000 selected.emplace_back(it.row());
1001 }
1002
1003 if (selected == selected_prev) {
1004 return result;
1005 }
1006
1007 if (static_cast<int>(selected.size()) >= n + this->intercept) {
1008 throw std::runtime_error(
1009 "selected >= n - 1 variables, cannot estimate variance");
1010 }
1011 }
1012
1015 "Maximum iterations reached in alpha estimation");
1016 }
1017
1018 return result;
1019 }
1020
1038 template<typename T>
1040 T& x,
1041 const Eigen::VectorXd& y_in,
1042 const double gamma = 0.0,
1043 Eigen::VectorXd beta0 = Eigen::VectorXd(0),
1044 Eigen::VectorXd beta = Eigen::VectorXd(0),
1045 const Eigen::VectorXd& observation_weights = Eigen::VectorXd())
1046 {
1047 return relaxImpl(fit,
1048 x,
1049 y_in,
1050 gamma,
1051 std::move(beta0),
1052 std::move(beta),
1053 observation_weights,
1054 false);
1055 }
1056
1057private:
1058 struct RelaxationState
1059 {
1060 Eigen::SparseMatrix<int> pattern;
1061 Eigen::VectorXd beta0;
1062 Eigen::VectorXd beta;
1063 bool initialized = false;
1064 };
1065
1066 template<typename T>
1067 SlopeFit relaxImpl(const SlopeFit& fit,
1068 T& x,
1069 const Eigen::MatrixXd& y_in,
1070 double gamma,
1071 Eigen::VectorXd beta0,
1072 Eigen::VectorXd beta,
1073 const Eigen::VectorXd& observation_weights,
1074 bool response_prepared,
1075 RelaxationState* state = nullptr)
1076 {
1077 using Eigen::MatrixXd;
1078 using Eigen::VectorXd;
1079
1080 int n = x.rows();
1081 int p = x.cols();
1082 const VectorXd weights =
1083 detail::normalizeObservationWeights(observation_weights, n);
1084 if (y_in.rows() != n || !isFinite(x) || !y_in.allFinite()) {
1085 throw std::invalid_argument(
1086 "Relaxation requires finite x and y with matching rows");
1087 }
1088 if (!std::isfinite(gamma) || gamma < 0.0 || gamma > 1.0) {
1089 throw std::invalid_argument("gamma must be between zero and one");
1090 }
1091 if (gamma == 1.0) {
1092 return fit;
1093 }
1094
1095 if (beta0.size() == 0) {
1096 beta0 = fit.getIntercepts(false);
1097 }
1098
1099 if (beta.size() == 0) {
1100 beta = MatrixXd(fit.getCoefs(false)).reshaped();
1101 }
1102
1103 auto working_set = activeSet(beta);
1104 bool reuse_solution = false;
1105 if (state != nullptr) {
1106 auto pattern = patternMatrix(beta);
1107 reuse_solution =
1108 state->initialized && pattern.rows() == state->pattern.rows() &&
1109 pattern.cols() == state->pattern.cols() &&
1110 (pattern.cols() == 0 || pattern.isApprox(state->pattern));
1111 if (reuse_solution) {
1112 beta0 = state->beta0;
1113 beta = state->beta;
1114 } else {
1115 state->pattern = std::move(pattern);
1116 }
1117 }
1118
1119 double alpha = fit.getAlpha();
1120
1121 Timer timer;
1122
1123 std::vector<double> primals, duals, time;
1124 timer.start();
1125
1126 auto jit_normalization = normalize(
1127 x, x_centers, x_scales, centering_type, scaling_type, false, weights);
1128
1129 bool update_clusters = false;
1130
1131 std::unique_ptr<Loss> loss = setupLoss(this->loss_type);
1132
1133 MatrixXd y = response_prepared ? y_in : loss->preprocessResponse(y_in);
1134
1135 int m = y.cols();
1136
1137 MatrixXd eta;
1138 int passes = 0;
1139
1140 if (!reuse_solution) {
1141 Eigen::ArrayXd lambda_cumsum_relax = Eigen::ArrayXd::Zero(p * m + 1);
1142
1143 eta = linearPredictor(x,
1144 working_set,
1145 beta0,
1146 beta,
1147 x_centers,
1148 x_scales,
1149 jit_normalization,
1150 intercept);
1151 VectorXd gradient = VectorXd::Zero(p * m);
1152 MatrixXd residual(n, m);
1153 MatrixXd working_residual(n, m);
1154
1155 MatrixXd w = MatrixXd::Ones(n, m);
1156 MatrixXd w_ones = weights.replicate(1, m);
1157 MatrixXd z = y;
1158
1159 slope::Clusters clusters(beta);
1160
1161 std::mt19937 rng;
1162
1163 if (random_seed.has_value()) {
1164 rng.seed(*random_seed);
1165 } else {
1166 rng.seed(std::random_device{}());
1167 }
1168
1169 for (int irls_it = 0; irls_it < max_it_outer_relax; irls_it++) {
1170 residual = loss->residual(eta, y);
1171 detail::maskZeroWeightRows(residual, weights);
1172
1173 if (collect_diagnostics) {
1174 primals.push_back(loss->loss(eta, y, weights));
1175 duals.push_back(0.0);
1176 time.push_back(timer.elapsed());
1177 }
1178
1179 Eigen::VectorXd cluster_gradient = clusterGradient(beta,
1180 residual,
1181 clusters,
1182 x,
1183 w_ones,
1184 x_centers,
1185 x_scales,
1186 jit_normalization);
1187
1188 double norm_grad = cluster_gradient.lpNorm<Eigen::Infinity>();
1189 if (intercept) {
1190 norm_grad = std::max(
1191 norm_grad,
1192 detail::weightedMean(residual, weights).cwiseAbs().maxCoeff());
1193 }
1194
1195 if (norm_grad < tol_relax) {
1196 break;
1197 }
1198
1199 loss->updateWeightsAndWorkingResponse(w, z, eta, y, weights);
1200 working_residual = eta - z;
1201 detail::maskZeroWeightRows(working_residual, weights);
1202 const VectorXd weight_sums = w.colwise().sum().transpose();
1203
1204 for (int inner_it = 0; inner_it < max_it_inner_relax; ++inner_it) {
1205 passes++;
1206
1207 double max_abs_gradient = coordinateDescent(beta0,
1208 beta,
1209 working_residual,
1210 clusters,
1211 lambda_cumsum_relax,
1212 x,
1213 w,
1214 weight_sums,
1215 x_centers,
1216 x_scales,
1217 intercept,
1218 jit_normalization,
1219 update_clusters,
1220 rng,
1221 cd_type);
1222
1223 if (max_abs_gradient < tol_relax) {
1224 break;
1225 }
1226 }
1227
1228 eta = working_residual + z;
1229
1230 if (irls_it == max_it_outer_relax) {
1233 "Maximum number of IRLS iterations reached.");
1234 }
1235 }
1236 }
1237
1238 if (state != nullptr && !reuse_solution) {
1239 // Cache before blending so later steps use their own penalized fit.
1240 state->beta0 = beta0;
1241 state->beta = beta;
1242 state->initialized = true;
1243 }
1244
1245 if (gamma > 0) {
1246 Eigen::VectorXd old_coefs = MatrixXd(fit.getCoefs(false)).reshaped();
1247 Eigen::VectorXd old_intercept = fit.getIntercepts(false);
1248 beta = (1 - gamma) * beta + gamma * old_coefs;
1249 beta0 = (1 - gamma) * beta0 + gamma * old_intercept;
1250 }
1251
1252 eta = linearPredictor(x,
1253 working_set,
1254 beta0,
1255 beta,
1256 x_centers,
1257 x_scales,
1258 jit_normalization,
1259 intercept);
1260 const double dev = loss->deviance(eta, y, weights);
1261 MatrixXd null_eta = MatrixXd::Zero(n, m);
1262 if (intercept) {
1263 null_eta.rowwise() = loss->link(detail::weightedMean(y, weights)).row(0);
1264 }
1265 const double null_dev = loss->deviance(null_eta, y, weights);
1266 Clusters clusters(beta);
1267
1268 SlopeFit fit_out{ beta0, beta.reshaped(p, m).sparseView(),
1269 clusters, alpha,
1270 fit.getLambda(), dev,
1271 null_dev, primals,
1272 duals, time,
1273 passes, centering_type,
1274 scaling_type, intercept,
1275 x_centers, x_scales };
1276
1277 return fit_out;
1278 }
1279
1280 template<typename T>
1281 SlopePath relaxPathImpl(const SlopePath& path,
1282 T& x,
1283 const Eigen::MatrixXd& y,
1284 double gamma,
1285 const Eigen::VectorXd& observation_weights,
1286 bool response_prepared)
1287 {
1288 std::vector<SlopeFit> fits;
1289 fits.reserve(path.size());
1290 RelaxationState state;
1291 for (size_t i = 0; i < path.size(); ++i) {
1292 fits.emplace_back(relaxImpl(path(i),
1293 x,
1294 y,
1295 gamma,
1296 {},
1297 {},
1298 observation_weights,
1299 response_prepared,
1300 &state));
1301 }
1302 return fits;
1303 }
1304
1305public:
1323 template<typename T>
1325 const SlopePath& path,
1326 T& x,
1327 const Eigen::VectorXd& y,
1328 const double gamma = 0.0,
1329 const Eigen::VectorXd& observation_weights = Eigen::VectorXd())
1330 {
1331 return relaxPathImpl(path, x, y, gamma, observation_weights, false);
1332 }
1333
1334private:
1335 // Parameters
1336 bool collect_diagnostics = false;
1337 bool intercept = true;
1338 bool modify_x = false;
1339 bool return_clusters = true;
1340 bool update_clusters = true;
1341 double alpha_min_ratio = -1;
1342 double dev_change_tol = 1e-5;
1343 double dev_ratio_tol = 0.999;
1344 double learning_rate_decr = 0.5;
1345 double q = 0.1;
1346 double theta1 = 1.0;
1347 double theta2 = 0.5;
1348 double tol = 1e-4;
1349 double tol_relax = 1e-4;
1350 double alpha_estimate = -1;
1351 int alpha_est_maxit = 1000;
1352 int cd_iterations = 10;
1353 int max_it = 1e5;
1354 int max_it_inner_relax = 1e5;
1355 int max_it_outer_relax = 50;
1356 int path_length = 100;
1357 std::optional<int> max_clusters = std::nullopt;
1358 std::optional<int> random_seed = 0;
1359 std::string alpha_scale = "absolute";
1360 std::string alpha_type = "path";
1361 std::string cd_type = "permuted";
1362 std::string centering_type = "mean";
1363 std::string lambda_type = "bh";
1364 std::string loss_type = "quadratic";
1365 std::string scaling_type = "sd";
1366 std::string screening_type = "strong";
1367 std::string solver_type = "auto";
1368
1369 // Data
1370 Eigen::VectorXd x_centers;
1371 Eigen::VectorXd x_scales;
1372};
1373
1374namespace detail {
1375
1377struct SlopeAccess
1378{
1379 template<typename T>
1380 static SlopePath path(Slope& model,
1381 Eigen::EigenBase<T>& x,
1382 const Eigen::MatrixXd& y,
1383 const Eigen::ArrayXd& alphas,
1384 const Eigen::VectorXd& weights)
1385 {
1386 return model.pathImpl(
1387 x, y, alphas, {}, defaultInterruptChecker, weights, true);
1388 }
1389
1390 template<typename T>
1391 static SlopePath relax(Slope& model,
1392 const SlopePath& path,
1393 T& x,
1394 const Eigen::MatrixXd& y,
1395 double gamma,
1396 const Eigen::VectorXd& weights)
1397 {
1398 return model.relaxPathImpl(path, x, y, gamma, weights, true);
1399 }
1400};
1401
1402} // namespace detail
1403
1404} // namespace slope
A class representing the results of SLOPE (Sorted L1 Penalized Estimation) fitting.
Definition slope_fit.h:27
Container class for SLOPE regression solution paths.
Definition slope_path.h:32
std::vector< Eigen::SparseMatrix< double > > getCoefs(const bool original_scale=true) const
Returns the vector of coefficient matrices for each solution in the path.
Definition slope_path.h:97
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.
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), const Eigen::VectorXd &observation_weights=Eigen::VectorXd())
Relaxes a fitted SLOPE model.
Definition slope.h:1039
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.
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, const Eigen::VectorXd &observation_weights=Eigen::VectorXd())
Computes SLOPE regression solution path for multiple alpha and lambda values.
Definition slope.h:416
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, const Eigen::VectorXd &observation_weights=Eigen::VectorXd())
Fits a single SLOPE regression model for given alpha and lambda values.
Definition slope.h:911
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.
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.
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.
SlopePath estimateAlpha(Eigen::EigenBase< T > &x, Eigen::MatrixXd &y, std::function< bool()> check_interrupt=defaultInterruptChecker, const Eigen::VectorXd &observation_weights=Eigen::VectorXd())
Estimates the regularization parameter alpha for SLOPE regression.
Definition slope.h:953
Slope()=default
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.
void setAlphaScale(const std::string &alpha_scale)
Sets the scale of user-supplied alpha values.
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.
SlopePath relax(const SlopePath &path, T &x, const Eigen::VectorXd &y, const double gamma=0.0, const Eigen::VectorXd &observation_weights=Eigen::VectorXd())
Relaxes a fitted SLOPE path.
Definition slope.h:1324
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.
Definition constants.h:27
constexpr double MAX_DIV
Maximum allowed divisor.
Definition constants.h:45
Namespace containing SLOPE regression implementation.
Definition clusters.h:11
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")
Definition hybrid_cd.h:601
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.
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.
Definition slope.h:48
std::unordered_set< double > unique(const Eigen::MatrixXd &x)
Create a set of unique values from an Eigen matrix.
Definition utils.h:381
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.
Definition utils.h:332
std::unique_ptr< ScreeningRule > createScreeningRule(const std::string &screening_type)
Creates a screening rule based on the provided type.
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, const Eigen::VectorXd &observation_weights=Eigen::VectorXd())
Scales a candidate into the SLOPE dual constraint and evaluates it.
@ MAXIT_REACHED
Maximum iterations reached without convergence.
Definition logger.h:29
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)
JitNormalization normalize(Eigen::MatrixBase< T > &x, Eigen::VectorXd &x_centers, Eigen::VectorXd &x_scales, const std::string &centering_type, const std::string &scaling_type, const bool modify_x, const Eigen::VectorXd &observation_weights=Eigen::VectorXd())
Definition normalize.h:307
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.
Definition math.h:318
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)
Definition math.h:958
std::vector< int > activeSet(const Eigen::VectorXd &beta)
Identifies previously active variables.
int whichMax(const T &x)
Returns the index of the maximum element in a container.
Definition math.h:512
Eigen::SparseMatrix< int > patternMatrix(const Eigen::VectorXd &beta)
Returns the cluster pattern as a sparse matrix.
bool isFinite(const Eigen::DenseBase< Derived > &x)
Check if all elements in a dense matrix are finite.
Definition utils.h:405
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.