slope 6.5.4
Loading...
Searching...
No Matches
hybrid_cd.h
Go to the documentation of this file.
1
7#pragma once
8
9#include "../clusters.h"
10#include "../eigen_compat.h"
11#include "../math.h"
12#include "slope_threshold.h"
13#include <Eigen/Core>
14#include <cassert>
15#include <random>
16#include <vector>
17
18namespace slope {
19
20namespace detail {
21
22class SparseClusterWorkspace
23{
24public:
25 SparseClusterWorkspace() = default;
26
27 SparseClusterWorkspace(const int n, const int m) { resize(n, m); }
28
29 void resize(const int n, const int m)
30 {
31 if (values.rows() != n || values.cols() != m) {
32 values = Eigen::MatrixXd::Zero(n, m);
33 active.assign(values.size(), false);
34 touched.clear();
35 }
36 }
37
38 void add(const int row, const int col, const double value)
39 {
40 const Eigen::Index index = row + values.rows() * col;
41 if (!active[index]) {
42 active[index] = true;
43 touched.emplace_back(index);
44 }
45 values.data()[index] += value;
46 }
47
48 void clear()
49 {
50 for (const Eigen::Index index : touched) {
51 values.data()[index] = 0.0;
52 active[index] = false;
53 }
54 touched.clear();
55 }
56
57 Eigen::MatrixXd values;
58 std::vector<unsigned char> active;
59 std::vector<Eigen::Index> touched;
60};
61
62} // namespace detail
63
87template<typename T>
88std::pair<double, double>
89computeGradientAndHessian(const Eigen::MatrixBase<T>& x,
90 const int ind,
91 const Eigen::MatrixXd& w,
92 const Eigen::MatrixXd& residual,
93 const Eigen::VectorXd& x_centers,
94 const Eigen::VectorXd& x_scales,
95 const double s,
96 const JitNormalization jit_normalization,
97 const int n,
98 const Eigen::VectorXd& weight_sums)
99{
100 double gradient = 0.0;
101 double hessian = 0.0;
102
103 int p = x.cols();
104
105 auto [k, j] = std::div(ind, p);
106
107 // TODO: Benchmark avoiding these copies in the dense path.
108 Eigen::VectorXd residual_v = residual.col(k);
109 Eigen::VectorXd w_v = w.col(k);
110
111 switch (jit_normalization) {
113 gradient = s *
114 (x.col(j).cwiseProduct(w_v).dot(residual_v) -
115 w_v.dot(residual_v) * x_centers(j)) /
116 (n * x_scales(j));
117 hessian =
118 (x.col(j).cwiseAbs2().dot(w_v) - 2 * x_centers(j) * x.col(j).dot(w_v) +
119 std::pow(x_centers(j), 2) * weight_sums(k)) /
120 (std::pow(x_scales(j), 2) * n);
121 break;
122
124 gradient = s *
125 (x.col(j).cwiseProduct(w_v).dot(residual_v) -
126 w_v.dot(residual_v) * x_centers(j)) /
127 n;
128 hessian =
129 (x.col(j).cwiseAbs2().dot(w_v) - 2 * x_centers(j) * x.col(j).dot(w_v) +
130 std::pow(x_centers(j), 2) * weight_sums(k)) /
131 n;
132 break;
133
135 gradient =
136 s * (x.col(j).cwiseProduct(w_v).dot(residual_v)) / (n * x_scales(j));
137 hessian = x.col(j).cwiseAbs2().dot(w_v) / (std::pow(x_scales(j), 2) * n);
138 break;
139
141 gradient = s * (x.col(j).cwiseProduct(w_v).dot(residual_v)) / n;
142 hessian = x.col(j).cwiseAbs2().dot(w_v) / n;
143 break;
144 }
145
146 return { gradient, hessian };
147}
148
149template<typename T>
150std::pair<double, double>
151computeGradientAndHessian(const Eigen::MatrixBase<T>& x,
152 const int ind,
153 const Eigen::MatrixXd& w,
154 const Eigen::MatrixXd& residual,
155 const Eigen::VectorXd& x_centers,
156 const Eigen::VectorXd& x_scales,
157 const double s,
158 const JitNormalization jit_normalization,
159 const int n)
160{
161 const Eigen::VectorXd weight_sums = w.colwise().sum().transpose();
163 ind,
164 w,
165 residual,
166 x_centers,
167 x_scales,
168 s,
169 jit_normalization,
170 n,
171 weight_sums);
172}
173
174template<typename T>
175std::pair<double, double>
176computeGradientAndHessian(const Eigen::SparseMatrixBase<T>& x,
177 const int ind,
178 const Eigen::MatrixXd& w,
179 const Eigen::MatrixXd& residual,
180 const Eigen::VectorXd& x_centers,
181 const Eigen::VectorXd& x_scales,
182 const double s,
183 const JitNormalization jit_normalization,
184 const int n,
185 const Eigen::VectorXd& weight_sums)
186{
187 const int p = x.cols();
188 auto [k, j] = std::div(ind, p);
189
190 double weighted_x_residual_sum = 0.0;
191 double weighted_x_sum = 0.0;
192 double weighted_x_squared_sum = 0.0;
193
194 for (typename T::InnerIterator it(x.derived(), j); it; ++it) {
195 const int i = it.row();
196 const double value = it.value();
197 const double weight = w(i, k);
198
199 weighted_x_residual_sum += value * weight * residual(i, k);
200 weighted_x_sum += value * weight;
201 weighted_x_squared_sum += value * value * weight;
202 }
203
204 const bool center = jit_normalization == JitNormalization::Center ||
205 jit_normalization == JitNormalization::Both;
206 const bool scale = jit_normalization == JitNormalization::Scale ||
207 jit_normalization == JitNormalization::Both;
208 const double offset = center ? x_centers(j) : 0.0;
209 const double feature_scale = scale ? x_scales(j) : 1.0;
210
211 if (center) {
212 weighted_x_residual_sum -=
213 offset * w.col(k).cwiseProduct(residual.col(k)).sum();
214 weighted_x_squared_sum +=
215 offset * offset * weight_sums(k) - 2.0 * offset * weighted_x_sum;
216 }
217
218 const double gradient = s * weighted_x_residual_sum / (n * feature_scale);
219 const double hessian =
220 weighted_x_squared_sum / (n * feature_scale * feature_scale);
221
222 return { gradient, hessian };
223}
224
225template<typename T>
226std::pair<double, double>
227computeGradientAndHessian(const Eigen::SparseMatrixBase<T>& x,
228 const int ind,
229 const Eigen::MatrixXd& w,
230 const Eigen::MatrixXd& residual,
231 const Eigen::VectorXd& x_centers,
232 const Eigen::VectorXd& x_scales,
233 const double s,
234 const JitNormalization jit_normalization,
235 const int n)
236{
237 const Eigen::VectorXd weight_sums = w.colwise().sum().transpose();
239 ind,
240 w,
241 residual,
242 x_centers,
243 x_scales,
244 s,
245 jit_normalization,
246 n,
247 weight_sums);
248}
249
273template<typename T>
274std::pair<double, double>
275computeClusterGradientAndHessian(const Eigen::MatrixBase<T>& x,
276 const int c_ind,
277 const std::vector<int>& s,
278 const Clusters& clusters,
279 const Eigen::MatrixXd& w,
280 const Eigen::MatrixXd& residual,
281 const Eigen::VectorXd& x_centers,
282 const Eigen::VectorXd& x_scales,
283 const JitNormalization jit_normalization)
284{
285 int n = x.rows();
286 int p = x.cols();
287 int m = residual.cols();
288
289 Eigen::MatrixXd x_s = Eigen::MatrixXd::Zero(n, m);
290
291 auto s_it = s.cbegin();
292 auto c_it = clusters.cbegin(c_ind);
293
294 for (; c_it != clusters.cend(c_ind); ++c_it, ++s_it) {
295 int ind = *c_it;
296 auto [k, j] = std::div(ind, p);
297 double s = *s_it;
298
299 switch (jit_normalization) {
301 x_s.col(k) += x.col(j) * (s / x_scales(j));
302 x_s.col(k).array() -= x_centers(j) * s / x_scales(j);
303 break;
304
306 x_s.col(k) += x.col(j) * s;
307 x_s.col(k).array() -= x_centers(j) * s;
308 break;
309
311 x_s.col(k) += x.col(j) * (s / x_scales(j));
312 break;
313
315 x_s.col(k) += x.col(j) * s;
316 break;
317 }
318 }
319
320 double hess = 0;
321 double grad = 0;
322
323 for (int k = 0; k < m; ++k) {
324 hess += x_s.col(k).cwiseAbs2().dot(w.col(k)) / n;
325 grad += x_s.col(k).cwiseProduct(w.col(k)).dot(residual.col(k)) / n;
326 }
327
328 return { hess, grad };
329}
330
353namespace detail {
354
355template<typename T>
356std::pair<double, double>
357computeSparseClusterGradientAndHessian(const Eigen::SparseMatrixBase<T>& x,
358 const int c_ind,
359 const std::vector<int>& s,
360 const Clusters& clusters,
361 const Eigen::MatrixXd& w,
362 const Eigen::MatrixXd& residual,
363 const Eigen::VectorXd& weight_sums,
364 const Eigen::VectorXd& x_centers,
365 const Eigen::VectorXd& x_scales,
366 const JitNormalization jit_normalization,
367 SparseClusterWorkspace& workspace)
368{
369 int n = x.rows();
370 int p = x.cols();
371 int m = residual.cols();
372
373 workspace.resize(n, m);
374
375 Eigen::ArrayXd offset = Eigen::ArrayXd::Zero(m);
376
377 auto s_it = s.cbegin();
378 auto c_it = clusters.cbegin(c_ind);
379
380 for (; c_it != clusters.cend(c_ind); ++c_it, ++s_it) {
381 int ind = *c_it;
382 auto [k, j] = std::div(ind, p);
383 double s_ind = *s_it;
384
385 switch (jit_normalization) {
387 offset(k) += x_centers(j) * s_ind;
388 break;
390 offset(k) += x_centers(j) * s_ind / x_scales(j);
391 break;
393 break;
395 break;
396 }
397
398 double scale = s_ind;
399 if (jit_normalization == JitNormalization::Scale ||
400 jit_normalization == JitNormalization::Both) {
401 scale /= x_scales(j);
402 }
403
404 for (typename T::InnerIterator it(x.derived(), j); it; ++it) {
405 workspace.add(it.row(), k, it.value() * scale);
406 }
407 }
408
409 double hess = 0;
410 double grad = 0;
411 Eigen::ArrayXd weighted_sum = Eigen::ArrayXd::Zero(m);
412
413 for (const Eigen::Index index : workspace.touched) {
414 const int k = index / n;
415 const int i = index - k * n;
416 const double value = workspace.values.data()[index];
417 const double weight = w(i, k);
418
419 hess += value * value * weight;
420 grad += value * weight * residual(i, k);
421 weighted_sum(k) += value * weight;
422 }
423
424 if (jit_normalization == JitNormalization::Center ||
425 jit_normalization == JitNormalization::Both) {
426 for (int k = 0; k < m; ++k) {
427 hess += -2 * offset(k) * weighted_sum(k) +
428 std::pow(offset(k), 2) * weight_sums(k);
429 grad -= offset(k) * w.col(k).cwiseProduct(residual.col(k)).sum();
430 }
431 }
432
433 workspace.clear();
434
435 return { hess / n, grad / n };
436}
437
438template<typename T>
439std::pair<double, double>
440computeClusterGradientAndHessianWithWorkspace(
441 const Eigen::MatrixBase<T>& x,
442 const int c_ind,
443 const std::vector<int>& s,
444 const Clusters& clusters,
445 const Eigen::MatrixXd& w,
446 const Eigen::MatrixXd& residual,
447 const Eigen::VectorXd&,
448 const Eigen::VectorXd& x_centers,
449 const Eigen::VectorXd& x_scales,
450 const JitNormalization jit_normalization,
451 SparseClusterWorkspace&)
452{
454 x, c_ind, s, clusters, w, residual, x_centers, x_scales, jit_normalization);
455}
456
457template<typename T>
458std::pair<double, double>
459computeClusterGradientAndHessianWithWorkspace(
460 const Eigen::SparseMatrixBase<T>& x,
461 const int c_ind,
462 const std::vector<int>& s,
463 const Clusters& clusters,
464 const Eigen::MatrixXd& w,
465 const Eigen::MatrixXd& residual,
466 const Eigen::VectorXd& weight_sums,
467 const Eigen::VectorXd& x_centers,
468 const Eigen::VectorXd& x_scales,
469 const JitNormalization jit_normalization,
470 SparseClusterWorkspace& workspace)
471{
472 return computeSparseClusterGradientAndHessian(x,
473 c_ind,
474 s,
475 clusters,
476 w,
477 residual,
478 weight_sums,
479 x_centers,
480 x_scales,
481 jit_normalization,
482 workspace);
483}
484
485} // namespace detail
486
487template<typename T>
488std::pair<double, double>
489computeClusterGradientAndHessian(const Eigen::SparseMatrixBase<T>& x,
490 const int c_ind,
491 const std::vector<int>& s,
492 const Clusters& clusters,
493 const Eigen::MatrixXd& w,
494 const Eigen::MatrixXd& residual,
495 const Eigen::VectorXd& x_centers,
496 const Eigen::VectorXd& x_scales,
497 const JitNormalization jit_normalization)
498{
499 detail::SparseClusterWorkspace workspace(x.rows(), residual.cols());
500 const Eigen::VectorXd weight_sums = w.colwise().sum().transpose();
501 return detail::computeSparseClusterGradientAndHessian(x,
502 c_ind,
503 s,
504 clusters,
505 w,
506 residual,
507 weight_sums,
508 x_centers,
509 x_scales,
510 jit_normalization,
511 workspace);
512}
513
544template<typename T>
545double
546coordinateDescent(Eigen::VectorXd& beta0,
547 Eigen::VectorXd& beta,
548 Eigen::MatrixXd& residual,
549 Clusters& clusters,
550 const Eigen::ArrayXd& lambda_cumsum,
551 const T& x,
552 const Eigen::MatrixXd& w,
553 const Eigen::VectorXd& weight_sums,
554 const Eigen::VectorXd& x_centers,
555 const Eigen::VectorXd& x_scales,
556 const bool intercept,
557 const JitNormalization jit_normalization,
558 const bool update_clusters,
559 std::mt19937& rng,
560 const std::string& cd_type = "cyclical")
561{
562 using namespace Eigen;
563
564 const int n = x.rows();
565 const int p = x.cols();
566 const int m = residual.cols();
567
568 double max_abs_gradient = 0;
569 detail::SparseClusterWorkspace sparse_workspace;
570
571 // Create a vector of indices to process
572 std::vector<int> indices;
573 indices.reserve(clusters.size());
574 for (int i = 0; i < clusters.size(); ++i) {
575 if (clusters.coeff(i) != 0) { // Skip zero cluster
576 indices.push_back(i);
577 }
578 }
579
580 if (cd_type == "permuted") {
581 std::shuffle(indices.begin(), indices.end(), rng);
582 }
583
584 for (int c_ind : indices) {
585 // Skip if index is no longer valid due to cluster updates
586 if (c_ind >= clusters.size()) {
587 continue;
588 }
589
590 double c_old = clusters.coeff(c_ind);
591
592 if (c_old == 0) {
593 // We do not update the zero cluster because it can be very large, but
594 // often does not change.
595 continue;
596 }
597
598 int cluster_size = clusters.cluster_size(c_ind);
599 std::vector<int> s;
600 s.reserve(cluster_size);
601
602 for (auto c_it = clusters.cbegin(c_ind); c_it != clusters.cend(c_ind);
603 ++c_it) {
604 int ind = *c_it;
605 assert(ind >= 0 && ind < beta.size() && "Invalid index in cluster");
606 double s_ind = sign(beta(ind));
607 s.emplace_back(s_ind);
608 }
609
610 double hess = 1;
611 double grad = 0;
612 VectorXd x_s(n);
613
614 if (cluster_size == 1) {
615 int ind = *clusters.cbegin(c_ind);
616 std::tie(grad, hess) = computeGradientAndHessian(x,
617 ind,
618 w,
619 residual,
620 x_centers,
621 x_scales,
622 s[0],
623 jit_normalization,
624 n,
625 weight_sums);
626 } else {
627 std::tie(hess, grad) =
628 detail::computeClusterGradientAndHessianWithWorkspace(x,
629 c_ind,
630 s,
631 clusters,
632 w,
633 residual,
634 weight_sums,
635 x_centers,
636 x_scales,
637 jit_normalization,
638 sparse_workspace);
639 }
640
641 max_abs_gradient = std::max(max_abs_gradient, std::abs(grad));
642
643 double c_tilde;
644 int new_index;
645
646 const double gamma = hess * c_old - grad;
647 std::tie(c_tilde, new_index) =
648 slopeThreshold(gamma, hess, c_ind, lambda_cumsum, clusters);
649
650 assert(c_tilde == 0 || new_index < clusters.size());
651 assert(new_index >= 0 && new_index <= clusters.size());
652
653 double c_diff = c_old - c_tilde;
654
655 if (c_diff != 0) {
656 auto s_it = s.cbegin();
657 auto c_it = clusters.cbegin(c_ind);
658 for (; c_it != clusters.cend(c_ind); ++c_it, ++s_it) {
659 int ind = *c_it;
660 auto [k, j] = std::div(ind, p);
661 double s_ind = *s_it;
662
663 // Update coefficient
664 beta(ind) = c_tilde * s_ind;
665
666 // Update residual
667 switch (jit_normalization) {
669 residual.col(k) -= x.col(j) * (s_ind * c_diff / x_scales(j));
670 residual.col(k).array() +=
671 x_centers(j) * s_ind * c_diff / x_scales(j);
672 break;
673
675 residual.col(k) -= x.col(j) * (s_ind * c_diff);
676 residual.col(k).array() += x_centers(j) * s_ind * c_diff;
677 break;
678
680 residual.col(k) -= x.col(j) * (s_ind * c_diff / x_scales(j));
681 break;
682
684 residual.col(k) -= x.col(j) * (s_ind * c_diff);
685 break;
686 }
687 }
688 }
689
690 if (update_clusters) {
691 clusters.update(c_ind, new_index, std::abs(c_tilde));
692 } else {
693 clusters.setCoeff(c_ind, std::abs(c_tilde));
694 }
695 }
696
697 if (intercept) {
698 for (int k = 0; k < residual.cols(); ++k) {
699 double beta0_update = residual.col(k).dot(w.col(k)) / n;
700 residual.col(k).array() -= beta0_update;
701 beta0(k) -= beta0_update;
702 }
703 }
704
705 return max_abs_gradient;
706}
707
708} // namespace slope
Representation of the nonzero clusters in SLOPE.
Definition clusters.h:23
void update(const int old_index, const int new_index, const double c_new)
Updates the cluster structure when an index is changed.
double coeff(const int i) const
Returns the coefficient of the cluster with the given index.
std::size_t size()
Returns the number of stored nonzero clusters.
Definition clusters.h:40
int cluster_size(const int i) const
Returns the size of the cluster with the given index.
void setCoeff(const int i, const double x)
Sets the coefficient of the cluster with the given index.
std::vector< int >::const_iterator cend(const int i) const
Returns a constant iterator pointing to the end of the cluster with the given index.
std::vector< int >::const_iterator cbegin(const int i) const
Returns a constant iterator pointing to the beginning of the cluster with the given index.
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:546
std::tuple< double, int > slopeThreshold(const double x, const int j, const Eigen::ArrayXd &lambda_cumsum, const Clusters &clusters)
std::pair< double, double > computeGradientAndHessian(const Eigen::MatrixBase< T > &x, const int ind, const Eigen::MatrixXd &w, const Eigen::MatrixXd &residual, const Eigen::VectorXd &x_centers, const Eigen::VectorXd &x_scales, const double s, const JitNormalization jit_normalization, const int n, const Eigen::VectorXd &weight_sums)
Definition hybrid_cd.h:89
int sign(T val)
Returns the sign of a given value.
Definition math.h:38
JitNormalization
Enums to control predictor standardization behavior.
@ None
No JIT normalization.
std::pair< double, double > computeClusterGradientAndHessian(const Eigen::MatrixBase< T > &x, const int c_ind, const std::vector< int > &s, const Clusters &clusters, const Eigen::MatrixXd &w, const Eigen::MatrixXd &residual, const Eigen::VectorXd &x_centers, const Eigen::VectorXd &x_scales, const JitNormalization jit_normalization)
Definition hybrid_cd.h:275
The declaration of the slopeThreshold function.