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),
422 const Eigen::VectorXd& observation_weights = Eigen::VectorXd())
434 friend struct detail::SlopeAccess;
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)
445 using Eigen::MatrixXd;
446 using Eigen::VectorXd;
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");
461 const int INTERRUPT_FREQ = 100;
462 bool interrupt =
false;
464 if (n != y_in.rows()) {
465 throw std::invalid_argument(
466 "x and y_in must have the same number of rows");
470 throw std::invalid_argument(
"x must not contain NA, NaN, or Inf values");
473 if (!y_in.array().isFinite().all()) {
474 throw std::invalid_argument(
"y must not contain NA, NaN, or Inf values");
477 auto jit_normalization =
normalize(x.derived(),
480 this->centering_type,
485 std::unique_ptr<Loss> loss =
setupLoss(this->loss_type);
487 MatrixXd y = response_prepared ? y_in : loss->preprocessResponse(y_in);
489 const int m = y.cols();
491 VectorXd beta0 = VectorXd::Zero(m);
492 VectorXd beta = VectorXd::Zero(p * m);
494 MatrixXd eta = MatrixXd::Zero(n, m);
496 if (this->intercept) {
497 beta0 = loss->link(detail::weightedMean(y, weights)).transpose();
498 eta.rowwise() = beta0.transpose();
501 MatrixXd residual = loss->residual(eta, y);
502 detail::maskZeroWeightRows(residual, weights);
503 VectorXd gradient(beta.size());
506 bool user_alpha = alpha.size() > 0;
507 bool user_lambda = lambda.size() > 0;
517 if (lambda.size() != beta.size()) {
518 throw std::invalid_argument(
519 "lambda must be the same length as the number of coefficients");
521 if (lambda.minCoeff() < 0) {
522 throw std::invalid_argument(
"lambda must be non-negative");
524 if (!lambda.isFinite().all()) {
525 throw std::invalid_argument(
"lambda must be finite");
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");
536 SortedL1Norm sl1_norm;
543 this->update_clusters,
556 int alpha_max_ind =
whichMax(gradient.cwiseAbs());
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());
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)
569 if (user_alpha && alpha_scale ==
"relative") {
571 if (!alpha.isFinite().all()) {
572 throw std::invalid_argument(
573 "Relative alpha values must produce finite absolute penalties");
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");
585 std::unique_ptr<ScreeningRule> screening_rule =
587 std::vector<int> working_set =
588 screening_rule->initialize(
static_cast<int>(beta.size()), alpha_max_ind);
591 double null_deviance = loss->deviance(eta, y, weights);
592 double dev_prev = null_deviance;
596 double alpha_prev = std::max(alpha_max, alpha(0));
598 std::vector<SlopeFit> fits;
599 std::optional<MatrixXd> quadratic_ols_dual_point;
602 for (
int path_step = 0; path_step < this->path_length; ++path_step) {
604 bool local_interrupt =
false;
606#pragma omp critical(check_interrupt)
609 local_interrupt = check_interrupt();
611 if (local_interrupt) {
616 double alpha_curr = alpha(path_step);
618 assert(alpha_curr <= alpha_prev &&
"Alpha must be decreasing");
620 Eigen::ArrayXd lambda_curr = alpha_curr * lambda;
621 Eigen::ArrayXd lambda_prev = alpha_prev * lambda;
623 const bool near_unregularized =
624 lambda_curr.maxCoeff() == 0.0 ||
627 std::sqrt(std::numeric_limits<double>::epsilon()) * alpha_max);
631 if (loss_type ==
"quadratic" && near_unregularized &&
632 !quadratic_ols_dual_point.has_value()) {
633 quadratic_ols_dual_point =
634 detail::quadraticOlsDualPoint(x.derived(),
643 std::optional<double> quadratic_ols_dual;
644 if (quadratic_ols_dual_point.has_value()) {
645 quadratic_ols_dual = lambda_curr.maxCoeff() == 0.0
648 *quadratic_ols_dual_point,
660 std::vector<double> duals, primals, time;
674 screening_rule->screen(
675 working_set, gradient, lambda_curr, lambda_prev, beta);
677 int working_set_it = 0;
679 for (; total_it < this->max_it; ++working_set_it, ++total_it) {
681 residual = loss->residual(eta, y);
682 detail::maskZeroWeightRows(residual, weights);
692 double primal = loss->loss(eta, y, weights) +
693 sl1_norm.eval(beta(working_set),
694 lambda_curr.head(working_set.size()));
698 const bool numerically_stationary =
699 gradient(working_set).cwiseAbs().maxCoeff() <=
700 numerical_stationarity_tol &&
702 detail::weightedMean(residual, weights).cwiseAbs().maxCoeff() <=
703 numerical_stationarity_tol);
705 MatrixXd dual_point = loss->dualPoint(eta, y, this->intercept, weights);
706 MatrixXd theta = dual_point;
707 VectorXd dual_gradient = VectorXd::Zero(beta.size());
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);
726 double full_dual = std::numeric_limits<double>::quiet_NaN();
728 if (collect_diagnostics) {
741 if (quadratic_ols_dual.has_value() && numerically_stationary) {
742 full_dual = std::max(full_dual, *quadratic_ols_dual);
746 time.emplace_back(timer.elapsed());
747 primals.emplace_back(primal);
748 duals.emplace_back(full_dual);
751 double dual_gap = primal - dual;
753 assert(dual_gap > -1e-6 &&
"Dual gap should be positive");
756 const bool unregularized_stationary = loss_type ==
"quadratic" &&
757 lambda_curr.maxCoeff() == 0.0 &&
758 numerically_stationary;
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();
767 screening_residual.row(i) *= weights(i);
771 screening_rule->checkKktViolations(gradient,
781 if (!std::isfinite(full_dual)) {
793 if (quadratic_ols_dual.has_value() && numerically_stationary) {
794 full_dual = std::max(full_dual, *quadratic_ols_dual);
797 if (primal - full_dual <= tol_scaled || unregularized_stationary) {
805 if (working_set_it % INTERRUPT_FREQ == 0) {
806 bool local_interrupt =
false;
808#pragma omp critical(check_interrupt)
811 local_interrupt = check_interrupt();
813 if (local_interrupt) {
834 if (total_it == this->max_it) {
837 "Maximum number of iterations reached at step = " +
838 std::to_string(path_step) +
".");
841 alpha_prev = alpha_curr;
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;
851 if (return_clusters) {
852 clusters.update(beta);
856 beta.reshaped(p, m).sparseView(),
866 this->centering_type,
872 fits.emplace_back(std::move(
fit));
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)) {
912 const Eigen::MatrixXd& y_in,
913 const double alpha = 1.0,
914 Eigen::ArrayXd lambda = Eigen::ArrayXd::Zero(0),
916 const Eigen::VectorXd& observation_weights = Eigen::VectorXd())
918 Eigen::ArrayXd alpha_arr(1);
919 alpha_arr(0) = alpha;
921 path(x, y_in, alpha_arr, lambda, check_interrupt, observation_weights);
954 Eigen::EigenBase<T>& x,
957 const Eigen::VectorXd& observation_weights = Eigen::VectorXd())
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");
969 Slope model_copy = *
this;
973 std::vector<int> selected;
974 Eigen::ArrayXd alpha(1);
980 this->alpha_estimate = alpha(0);
981 result = model_copy.
path(
982 x, y, alpha, Eigen::ArrayXd::Zero(0), check_interrupt, weights);
984 for (
int it = 0; it < this->alpha_est_maxit; ++it) {
985 T x_selected =
subsetCols(x.derived(), selected);
987 std::vector<int> selected_prev = selected;
990 alpha(0) =
estimateNoise(x_selected, y, this->intercept) / n;
991 this->alpha_estimate = alpha(0);
993 result = model_copy.
path(
994 x, y, alpha, Eigen::ArrayXd::Zero(0), check_interrupt, weights);
995 auto coefs = result.
getCoefs().back();
997 for (
typename Eigen::SparseMatrix<double>::InnerIterator it(coefs, 0);
1000 selected.emplace_back(it.row());
1003 if (selected == selected_prev) {
1007 if (
static_cast<int>(selected.size()) >= n + this->intercept) {
1008 throw std::runtime_error(
1009 "selected >= n - 1 variables, cannot estimate variance");
1015 "Maximum iterations reached in alpha estimation");
1038 template<
typename T>
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())
1047 return relaxImpl(
fit,
1053 observation_weights,
1058 struct RelaxationState
1060 Eigen::SparseMatrix<int> pattern;
1061 Eigen::VectorXd beta0;
1062 Eigen::VectorXd beta;
1063 bool initialized =
false;
1066 template<
typename T>
1067 SlopeFit relaxImpl(
const SlopeFit&
fit,
1069 const Eigen::MatrixXd& y_in,
1071 Eigen::VectorXd beta0,
1072 Eigen::VectorXd beta,
1073 const Eigen::VectorXd& observation_weights,
1074 bool response_prepared,
1075 RelaxationState* state =
nullptr)
1077 using Eigen::MatrixXd;
1078 using Eigen::VectorXd;
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");
1088 if (!std::isfinite(gamma) || gamma < 0.0 || gamma > 1.0) {
1089 throw std::invalid_argument(
"gamma must be between zero and one");
1095 if (beta0.size() == 0) {
1096 beta0 =
fit.getIntercepts(
false);
1099 if (beta.size() == 0) {
1100 beta = MatrixXd(
fit.getCoefs(
false)).reshaped();
1104 bool reuse_solution =
false;
1105 if (state !=
nullptr) {
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;
1115 state->pattern = std::move(pattern);
1119 double alpha =
fit.getAlpha();
1123 std::vector<double> primals, duals, time;
1127 x, x_centers, x_scales, centering_type, scaling_type,
false, weights);
1129 bool update_clusters =
false;
1131 std::unique_ptr<Loss> loss =
setupLoss(this->loss_type);
1133 MatrixXd y = response_prepared ? y_in : loss->preprocessResponse(y_in);
1140 if (!reuse_solution) {
1141 Eigen::ArrayXd lambda_cumsum_relax = Eigen::ArrayXd::Zero(p * m + 1);
1143 eta = linearPredictor(x,
1151 VectorXd gradient = VectorXd::Zero(p * m);
1152 MatrixXd residual(n, m);
1153 MatrixXd working_residual(n, m);
1155 MatrixXd w = MatrixXd::Ones(n, m);
1156 MatrixXd w_ones = weights.replicate(1, m);
1159 slope::Clusters clusters(beta);
1163 if (random_seed.has_value()) {
1164 rng.seed(*random_seed);
1166 rng.seed(std::random_device{}());
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);
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());
1188 double norm_grad = cluster_gradient.lpNorm<Eigen::Infinity>();
1190 norm_grad = std::max(
1192 detail::weightedMean(residual, weights).cwiseAbs().maxCoeff());
1195 if (norm_grad < tol_relax) {
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();
1204 for (
int inner_it = 0; inner_it < max_it_inner_relax; ++inner_it) {
1211 lambda_cumsum_relax,
1223 if (max_abs_gradient < tol_relax) {
1228 eta = working_residual + z;
1230 if (irls_it == max_it_outer_relax) {
1233 "Maximum number of IRLS iterations reached.");
1238 if (state !=
nullptr && !reuse_solution) {
1240 state->beta0 = beta0;
1242 state->initialized =
true;
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;
1252 eta = linearPredictor(x,
1260 const double dev = loss->deviance(eta, y, weights);
1261 MatrixXd null_eta = MatrixXd::Zero(n, m);
1263 null_eta.rowwise() = loss->link(detail::weightedMean(y, weights)).row(0);
1265 const double null_dev = loss->deviance(null_eta, y, weights);
1266 Clusters clusters(beta);
1268 SlopeFit fit_out{ beta0, beta.reshaped(p, m).sparseView(),
1270 fit.getLambda(), dev,
1273 passes, centering_type,
1274 scaling_type, intercept,
1275 x_centers, x_scales };
1280 template<
typename T>
1281 SlopePath relaxPathImpl(
const SlopePath&
path,
1283 const Eigen::MatrixXd& y,
1285 const Eigen::VectorXd& observation_weights,
1286 bool response_prepared)
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),
1298 observation_weights,
1323 template<
typename T>
1327 const Eigen::VectorXd& y,
1328 const double gamma = 0.0,
1329 const Eigen::VectorXd& observation_weights = Eigen::VectorXd())
1331 return relaxPathImpl(
path, x, y, gamma, observation_weights,
false);
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;
1346 double theta1 = 1.0;
1347 double theta2 = 0.5;
1349 double tol_relax = 1e-4;
1350 double alpha_estimate = -1;
1351 int alpha_est_maxit = 1000;
1352 int cd_iterations = 10;
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";
1370 Eigen::VectorXd x_centers;
1371 Eigen::VectorXd x_scales;