slope 6.6.0
Loading...
Searching...
No Matches
cv.h
Go to the documentation of this file.
1
10
11#pragma once
12
13#include "eigen_compat.h"
14#include "folds.h"
15#include "observation_weights.h"
16#include "score.h"
17#include "slope.h"
18#include <atomic>
19#include <vector>
20
21#ifdef _OPENMP
22#include <omp.h>
23#endif
24
25namespace slope {
26
27using slope::all;
28
36{
40 Eigen::MatrixXd score;
41
43 std::map<std::string, double> params;
44
46 Eigen::ArrayXd alphas;
47
49 Eigen::ArrayXd mean_scores;
50
53 Eigen::ArrayXd std_errors;
54};
55
64{
67 std::vector<GridResult> results;
68
71 std::map<std::string, double> best_params;
72
74 double best_score;
75
78
82};
83
92{
94 int n_folds = 10;
95
97 int n_repeats = 1;
98
100 std::string metric = "mse";
101
103 uint64_t random_seed = 42;
104
106 bool copy_x = true;
107
109 std::map<std::string, std::vector<double>> hyperparams;
110
112 std::map<std::string, std::vector<double>> default_hyperparams = {
113 { "q", { 0.1 } },
114 { "gamma", { 0.0 } },
115 };
116
118 std::optional<std::vector<std::vector<std::vector<int>>>> predefined_folds;
119};
120
121namespace detail {
122
123std::vector<std::map<std::string, double>>
124createGrid(const std::map<std::string, std::vector<double>>& param_values);
125
126void
127findBestParameters(CvResult& cv_result, const std::unique_ptr<Score>& scorer);
128
129template<typename T>
130Eigen::ArrayXd
131scoreFoldPath(const SlopePath& path,
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)
138{
139 Eigen::MatrixXd targets = y_test;
140 if (score_class_labels) {
141 targets = loss->predict(loss->link(y_test));
142 }
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);
147 }
148 return scores;
149}
150
151template<typename T>
152Eigen::ArrayXd
153fitToFold(Eigen::MatrixBase<T>& x,
154 const Eigen::MatrixXd& y,
155 const Folds& folds,
156 const std::unique_ptr<Loss>& loss,
157 const std::unique_ptr<Score>& scorer,
158 const Eigen::ArrayXd& alphas,
159 Slope& thread_model,
160 const int fold,
161 const int rep,
162 const double gamma,
163 const bool copy_x,
164 const Eigen::VectorXd& observation_weights,
165 const bool score_class_labels)
166{
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);
171
172 if (copy_x) {
173 // Relaxation must reuse the original predictors and their normalization.
174 thread_model.setModifyX(gamma == 0);
175 auto [x_train, y_train, x_test, y_test] = folds.split(x, y, fold, rep);
176 auto path =
177 SlopeAccess::path(thread_model, x_train, y_train, alphas, train_weights);
178 if (gamma > 0) {
179 path = SlopeAccess::relax(
180 thread_model, path, x_train, y_train, gamma, train_weights);
181 }
182 return scoreFoldPath(
183 path, x_test, y_test, loss, scorer, test_weights, score_class_labels);
184 }
185
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);
191 auto path =
192 SlopeAccess::path(thread_model, x_train, y_train, alphas, train_weights);
193 if (gamma > 0) {
194 path = SlopeAccess::relax(
195 thread_model, path, x_train, y_train, gamma, train_weights);
196 }
197 return scoreFoldPath(
198 path, x_test, y_test, loss, scorer, test_weights, score_class_labels);
199}
200
201template<typename T>
202Eigen::ArrayXd
203fitToFold(Eigen::SparseMatrixBase<T>& x,
204 const Eigen::MatrixXd& y,
205 const Folds& folds,
206 const std::unique_ptr<Loss>& loss,
207 const std::unique_ptr<Score>& scorer,
208 const Eigen::ArrayXd& alphas,
209 Slope& thread_model,
210 const int fold,
211 const int rep,
212 const double gamma,
213 const bool copy_x,
214 const Eigen::VectorXd& observation_weights,
215 const bool score_class_labels)
216{
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);
223 auto path =
224 SlopeAccess::path(thread_model, x_train, y_train, alphas, train_weights);
225 if (gamma > 0) {
226 path = SlopeAccess::relax(
227 thread_model, path, x_train, y_train, gamma, train_weights);
228 }
229 return scoreFoldPath(
230 path, x_test, y_test, loss, scorer, test_weights, score_class_labels);
231}
232
233} // namespace detail
234
262template<typename T>
265 Eigen::EigenBase<T>& x,
266 const Eigen::MatrixXd& y_in,
267 const CvConfig& config = CvConfig(),
268 const Eigen::VectorXd& observation_weights = Eigen::VectorXd(0))
269{
270 CvResult cv_result;
271
272 int n = y_in.rows();
273 if (x.rows() != n) {
274 throw std::invalid_argument("x and y must have the same number of rows");
275 }
276 if (!isFinite(x.derived())) {
277 throw std::invalid_argument("x must not contain NA, NaN, or Inf values");
278 }
279 if (!y_in.allFinite()) {
280 throw std::invalid_argument("y must not contain NA, NaN, or Inf values");
281 }
282 const Eigen::VectorXd weights =
283 detail::normalizeObservationWeights(observation_weights, n);
284
285 auto loss = setupLoss(model.getLossType());
286
287 auto y = loss->preprocessResponse(y_in);
288 auto scorer = Score::create(config.metric);
289
290 auto hyperparams = config.default_hyperparams;
291
292 // Override with user-specified parameters
293 for (const auto& [key, values] : config.hyperparams) {
294 hyperparams[key] = values;
295 }
296
297 auto grid = detail::createGrid(hyperparams);
298
299 // Total number of evaluations (n_repeats * n_folds)
300 Folds folds =
301 config.predefined_folds.has_value()
302 ? Folds(*config.predefined_folds)
303 : Folds(n, config.n_folds, config.n_repeats, config.random_seed);
304
305 int n_evals = folds.numEvals();
306 for (int rep = 0; rep < folds.numRepetitions(); ++rep) {
307 for (int fold = 0; fold < folds.numFolds(); ++fold) {
308 const auto train_indices = folds.getTrainingIndices(fold, rep);
309 const auto test_indices = folds.getTestIndices(fold, rep);
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");
317 }
318 if (test_weights.sum() <= 0) {
319 throw std::invalid_argument("Validation " + fold_context +
320 " has zero total observation weight");
321 }
322 }
323 }
324 const bool score_class_labels =
325 config.metric == "accuracy" || config.metric == "misclass";
326
327 // Full-data alpha generation must leave predictors available for each fold.
328 model.setModifyX(false);
329 for (const auto& params : grid) {
330 GridResult result;
331 result.params = params;
332
333 double q = params.at("q");
334 double gamma = params.at("gamma");
335
336 model.setQ(q);
337
338 auto initial_path =
339 detail::SlopeAccess::path(model, x, y, Eigen::ArrayXd(0), weights);
340
341 result.alphas = initial_path.getAlpha();
342 int n_alpha = result.alphas.size();
343
344 assert((result.alphas > 0).all());
345
346 Eigen::MatrixXd scores = Eigen::MatrixXd::Zero(n_evals, n_alpha);
347
348#ifdef _OPENMP
349 Eigen::setNbThreads(1);
350#endif
351
352 // Thread-safety for exceptions
353 std::vector<std::string> thread_errors(n_evals);
354 std::atomic<bool> had_exception{ false };
355
356#ifdef _OPENMP
357 // omp_set_max_active_levels() is OpenMP 3.0; MSVC's vcomp runtime only
358 // implements 2.0, where omp_set_nested() is the way to stop nesting.
359#if _OPENMP >= 200805
360 omp_set_max_active_levels(1);
361#else
362 omp_set_nested(0);
363#endif
364#pragma omp parallel for num_threads(Threads::get()) \
365 shared(scores, thread_errors, had_exception)
366#endif
367 for (int i = 0; i < n_evals; ++i) {
368 try {
369 auto [rep, fold] = std::div(i, folds.numFolds());
370
371 Slope thread_model = model;
372 // The full-data path has already converted its alphas to absolute
373 // values.
374 thread_model.setAlphaScale("absolute");
375
376 scores.row(i) = detail::fitToFold(x.derived(),
377 y,
378 folds,
379 loss,
380 scorer,
381 result.alphas,
382 thread_model,
383 fold,
384 rep,
385 gamma,
386 config.copy_x,
387 weights,
388 score_class_labels);
389
390 } catch (const std::exception& e) {
391 thread_errors[i] = e.what();
392 had_exception = true;
393 } catch (...) {
394 thread_errors[i] = "Unknown exception";
395 had_exception = true;
396 }
397 }
398
399 if (had_exception) {
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()) {
403 error_message +=
404 "Fold " + std::to_string(i) + ": " + thread_errors[i] + "\n";
405 }
406 }
407 throw std::runtime_error(error_message);
408 }
409
410 result.mean_scores = scores.colwise().mean();
411 result.std_errors = stdDevs(scores).array() / std::sqrt(n_evals);
412 result.score = std::move(scores);
413 cv_result.results.push_back(result);
414 }
415
416#ifdef _OPENMP
417 Eigen::setNbThreads(0);
418#endif
419
420 detail::findBestParameters(cv_result, scorer);
421
422 return cv_result;
423}
424
425} // namespace slope
Manages data partitioning for cross-validation.
Definition folds.h:29
size_t numEvals() const
Get the total number of folds (repetitions * folds).
Definition folds.h:167
size_t numFolds() const
Get the number of folds.
Definition folds.h:153
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.
Definition folds.h:160
static std::unique_ptr< Score > create(const std::string &metric)
Container class for SLOPE regression solution paths.
Definition slope_path.h:32
std::size_t size() const
Gets the number of solutions in the path.
Definition slope_path.h:257
The SLOPE model.
Definition slope.h:62
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.
Definition clusters.h:11
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.
Definition cv.h:264
Eigen::VectorXd stdDevs(const Eigen::SparseMatrixBase< T > &x)
Computes the standard deviation for each column of a matrix.
Definition math.h:754
bool isFinite(const Eigen::DenseBase< Derived > &x)
Check if all elements in a dense matrix are finite.
Definition utils.h:405
Scoring metrics for model evaluation.
SLOPE (Sorted L-One Penalized Estimation) optimization.
Configuration settings for cross-validation.
Definition cv.h:92
int n_repeats
Number of times to repeat the cross-validation (default: 1).
Definition cv.h:97
bool copy_x
Whether to copy the design matrix for each fold (default: true).
Definition cv.h:106
std::map< std::string, std::vector< double > > hyperparams
Map of hyperparameter names to vectors of values to evaluate.
Definition cv.h:109
std::optional< std::vector< std::vector< std::vector< int > > > > predefined_folds
Optional user-defined fold assignments for custom cross-validation splits.
Definition cv.h:118
int n_folds
Number of folds for cross-validation (default: 10).
Definition cv.h:94
std::string metric
Evaluation metric used for model assessment (default: "mse").
Definition cv.h:100
std::map< std::string, std::vector< double > > default_hyperparams
Map of hyperparameter names to vectors of values to evaluate.
Definition cv.h:112
uint64_t random_seed
Seed for reproducible fold assignments across platforms (default: 42).
Definition cv.h:103
Contains overall results from a cross-validation process.
Definition cv.h:64
double best_score
The score achieved by the optimal hyperparameter configuration.
Definition cv.h:74
std::map< std::string, double > best_params
Definition cv.h:71
std::vector< GridResult > results
Definition cv.h:67
int best_ind
Index of the best performing configuration in the results vector.
Definition cv.h:77
int best_alpha_ind
Definition cv.h:81
Stores cross-validation results for a specific set of hyperparameters.
Definition cv.h:36
Eigen::MatrixXd score
Definition cv.h:40
Eigen::ArrayXd mean_scores
Array of scores averaged across all folds for each alpha value.
Definition cv.h:49
std::map< std::string, double > params
Map of hyperparameter names to their values for the configuration.
Definition cv.h:43
Eigen::ArrayXd std_errors
Definition cv.h:53
Eigen::ArrayXd alphas
Array of regularization parameters used in the regularization path.
Definition cv.h:46