15#include "observation_weights.h"
43 std::map<std::string, double>
params;
114 {
"gamma", { 0.0 } },
123std::vector<std::map<std::string, double>>
124createGrid(
const std::map<std::string, std::vector<double>>& param_values);
127findBestParameters(
CvResult& cv_result,
const std::unique_ptr<Score>& scorer);
132 Eigen::EigenBase<T>& x_test,
133 const Eigen::MatrixXd& y_test,
134 const std::unique_ptr<Loss>& loss,
135 const std::unique_ptr<Score>& scorer,
136 const Eigen::VectorXd& observation_weights,
137 const bool score_class_labels)
139 Eigen::MatrixXd targets = y_test;
140 if (score_class_labels) {
141 targets = loss->predict(loss->link(y_test));
143 Eigen::ArrayXd scores(path.
size());
144 for (
int j = 0; j < path.
size(); ++j) {
145 const auto eta = path(j).predict(x_test,
"linear");
146 scores(j) = scorer->eval(eta, targets, loss, observation_weights);
153fitToFold(Eigen::MatrixBase<T>& x,
154 const Eigen::MatrixXd& y,
156 const std::unique_ptr<Loss>& loss,
157 const std::unique_ptr<Score>& scorer,
158 const Eigen::ArrayXd& alphas,
164 const Eigen::VectorXd& observation_weights,
165 const bool score_class_labels)
167 const auto train_idx = folds.getTrainingIndices(fold, rep);
168 const auto test_idx = folds.getTestIndices(fold, rep);
169 const Eigen::VectorXd train_weights = observation_weights(train_idx);
170 const Eigen::VectorXd test_weights = observation_weights(test_idx);
174 thread_model.setModifyX(gamma == 0);
175 auto [x_train, y_train, x_test, y_test] = folds.split(x, y, fold, rep);
177 SlopeAccess::path(thread_model, x_train, y_train, alphas, train_weights);
179 path = SlopeAccess::relax(
180 thread_model, path, x_train, y_train, gamma, train_weights);
182 return scoreFoldPath(
183 path, x_test, y_test, loss, scorer, test_weights, score_class_labels);
186 thread_model.setModifyX(
false);
187 auto x_train = x(train_idx, all);
188 auto x_test = x(test_idx, all);
189 Eigen::MatrixXd y_train = y(train_idx, all);
190 Eigen::MatrixXd y_test = y(test_idx, all);
192 SlopeAccess::path(thread_model, x_train, y_train, alphas, train_weights);
194 path = SlopeAccess::relax(
195 thread_model, path, x_train, y_train, gamma, train_weights);
197 return scoreFoldPath(
198 path, x_test, y_test, loss, scorer, test_weights, score_class_labels);
203fitToFold(Eigen::SparseMatrixBase<T>& x,
204 const Eigen::MatrixXd& y,
206 const std::unique_ptr<Loss>& loss,
207 const std::unique_ptr<Score>& scorer,
208 const Eigen::ArrayXd& alphas,
214 const Eigen::VectorXd& observation_weights,
215 const bool score_class_labels)
217 thread_model.setModifyX(gamma == 0);
218 const auto train_idx = folds.getTrainingIndices(fold, rep);
219 const auto test_idx = folds.getTestIndices(fold, rep);
220 const Eigen::VectorXd train_weights = observation_weights(train_idx);
221 const Eigen::VectorXd test_weights = observation_weights(test_idx);
222 auto [x_train, y_train, x_test, y_test] = folds.split(x, y, fold, rep);
224 SlopeAccess::path(thread_model, x_train, y_train, alphas, train_weights);
226 path = SlopeAccess::relax(
227 thread_model, path, x_train, y_train, gamma, train_weights);
229 return scoreFoldPath(
230 path, x_test, y_test, loss, scorer, test_weights, score_class_labels);
265 Eigen::EigenBase<T>& x,
266 const Eigen::MatrixXd& y_in,
268 const Eigen::VectorXd& observation_weights = Eigen::VectorXd(0))
274 throw std::invalid_argument(
"x and y must have the same number of rows");
277 throw std::invalid_argument(
"x must not contain NA, NaN, or Inf values");
279 if (!y_in.allFinite()) {
280 throw std::invalid_argument(
"y must not contain NA, NaN, or Inf values");
282 const Eigen::VectorXd weights =
283 detail::normalizeObservationWeights(observation_weights, n);
287 auto y = loss->preprocessResponse(y_in);
290 auto hyperparams = config.default_hyperparams;
293 for (
const auto& [key, values] : config.hyperparams) {
294 hyperparams[key] = values;
297 auto grid = detail::createGrid(hyperparams);
301 config.predefined_folds.has_value()
302 ?
Folds(*config.predefined_folds)
303 :
Folds(n, config.n_folds, config.n_repeats, config.random_seed);
307 for (
int fold = 0; fold < folds.
numFolds(); ++fold) {
310 const Eigen::VectorXd train_weights = weights(train_indices);
311 const Eigen::VectorXd test_weights = weights(test_indices);
312 const std::string fold_context =
"fold " + std::to_string(fold + 1) +
313 " in repeat " + std::to_string(rep + 1);
314 if (train_weights.sum() <= 0) {
315 throw std::invalid_argument(
"Training " + fold_context +
316 " has zero total observation weight");
318 if (test_weights.sum() <= 0) {
319 throw std::invalid_argument(
"Validation " + fold_context +
320 " has zero total observation weight");
324 const bool score_class_labels =
325 config.metric ==
"accuracy" || config.metric ==
"misclass";
329 for (
const auto& params : grid) {
333 double q = params.at(
"q");
334 double gamma = params.at(
"gamma");
339 detail::SlopeAccess::path(model, x, y, Eigen::ArrayXd(0), weights);
341 result.
alphas = initial_path.getAlpha();
342 int n_alpha = result.
alphas.size();
344 assert((result.
alphas > 0).all());
346 Eigen::MatrixXd scores = Eigen::MatrixXd::Zero(n_evals, n_alpha);
349 Eigen::setNbThreads(1);
353 std::vector<std::string> thread_errors(n_evals);
354 std::atomic<bool> had_exception{
false };
360 omp_set_max_active_levels(1);
364#pragma omp parallel for num_threads(Threads::get()) \
365 shared(scores, thread_errors, had_exception)
367 for (
int i = 0; i < n_evals; ++i) {
369 auto [rep, fold] = std::div(i, folds.
numFolds());
371 Slope thread_model = model;
376 scores.row(i) = detail::fitToFold(x.derived(),
390 }
catch (
const std::exception& e) {
391 thread_errors[i] = e.what();
392 had_exception =
true;
394 thread_errors[i] =
"Unknown exception";
395 had_exception =
true;
400 std::string error_message =
"Exception(s) during cross-validation:\n";
401 for (
int i = 0; i < n_evals; ++i) {
402 if (!thread_errors[i].empty()) {
404 "Fold " + std::to_string(i) +
": " + thread_errors[i] +
"\n";
407 throw std::runtime_error(error_message);
412 result.
score = std::move(scores);
413 cv_result.
results.push_back(result);
417 Eigen::setNbThreads(0);
420 detail::findBestParameters(cv_result, scorer);
Manages data partitioning for cross-validation.
size_t numEvals() const
Get the total number of folds (repetitions * folds).
size_t numFolds() const
Get the number of folds.
std::vector< int > getTrainingIndices(size_t fold_idx, size_t rep_idx=0) const
Get training indices for a specific fold and repetition.
const std::vector< int > & getTestIndices(size_t fold_idx, size_t rep_idx=0) const
Get test indices for a specific fold and repetition.
size_t numRepetitions() const
Get the number of repetitions.
static std::unique_ptr< Score > create(const std::string &metric)
Container class for SLOPE regression solution paths.
std::size_t size() const
Gets the number of solutions in the path.
const std::string & getLossType()
Get currently defined loss type.
void setModifyX(const bool modify_x)
Controls if x should be modified-in-place.
void setAlphaScale(const std::string &alpha_scale)
Sets the scale of user-supplied alpha values.
void setQ(double q)
Sets the q value.
Eigen compatibility layer for version differences.
Cross-validation fold management for SLOPE models.
Namespace containing SLOPE regression implementation.
std::unique_ptr< Loss > setupLoss(const std::string &loss)
Factory function to create the appropriate loss function based on the distribution family.
CvResult crossValidate(Slope model, Eigen::EigenBase< T > &x, const Eigen::MatrixXd &y_in, const CvConfig &config=CvConfig(), const Eigen::VectorXd &observation_weights=Eigen::VectorXd(0))
Performs cross-validation on a SLOPE model to select optimal hyperparameters.
Eigen::VectorXd stdDevs(const Eigen::SparseMatrixBase< T > &x)
Computes the standard deviation for each column of a matrix.
bool isFinite(const Eigen::DenseBase< Derived > &x)
Check if all elements in a dense matrix are finite.
Scoring metrics for model evaluation.
SLOPE (Sorted L-One Penalized Estimation) optimization.
Configuration settings for cross-validation.
int n_repeats
Number of times to repeat the cross-validation (default: 1).
bool copy_x
Whether to copy the design matrix for each fold (default: true).
std::map< std::string, std::vector< double > > hyperparams
Map of hyperparameter names to vectors of values to evaluate.
std::optional< std::vector< std::vector< std::vector< int > > > > predefined_folds
Optional user-defined fold assignments for custom cross-validation splits.
int n_folds
Number of folds for cross-validation (default: 10).
std::string metric
Evaluation metric used for model assessment (default: "mse").
std::map< std::string, std::vector< double > > default_hyperparams
Map of hyperparameter names to vectors of values to evaluate.
uint64_t random_seed
Seed for reproducible fold assignments across platforms (default: 42).
Contains overall results from a cross-validation process.
double best_score
The score achieved by the optimal hyperparameter configuration.
std::map< std::string, double > best_params
std::vector< GridResult > results
int best_ind
Index of the best performing configuration in the results vector.
Stores cross-validation results for a specific set of hyperparameters.
Eigen::ArrayXd mean_scores
Array of scores averaged across all folds for each alpha value.
std::map< std::string, double > params
Map of hyperparameter names to their values for the configuration.
Eigen::ArrayXd std_errors
Eigen::ArrayXd alphas
Array of regularization parameters used in the regularization path.