slope 6.6.0
Loading...
Searching...
No Matches
hybrid_cd.h
Go to the documentation of this file.
1
6
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
22template<typename W, typename R>
23double
24weightedResidualSum(const Eigen::MatrixBase<W>& weights,
25 const Eigen::MatrixBase<R>& residual)
26{
27 if (!(weights.array() == 0.0).any()) {
28 return weights.dot(residual);
29 }
30 double value = 0.0;
31 for (Eigen::Index i = 0; i < weights.size(); ++i) {
32 if (weights(i) != 0.0) {
33 value += weights(i) * residual(i);
34 }
35 }
36 return value;
37}
38
39class SparseClusterWorkspace
40{
41public:
42 SparseClusterWorkspace() = default;
43
44 SparseClusterWorkspace(const int n, const int m) { resize(n, m); }
45
46 void resize(const int n, const int m)
47 {
48 if (values.rows() != n || values.cols() != m) {
49 values = Eigen::MatrixXd::Zero(n, m);
50 active.assign(values.size(), false);
51 touched.clear();
52 }
53 }
54
55 void add(const int row, const int col, const double value)
56 {
57 const Eigen::Index index = row + values.rows() * col;
58 if (!active[index]) {
59 active[index] = true;
60 touched.emplace_back(index);
61 }
62 values.data()[index] += value;
63 }
64
65 void clear()
66 {
67 for (const Eigen::Index index : touched) {
68 values.data()[index] = 0.0;
69 active[index] = false;
70 }
71 touched.clear();
72 }
73
74 Eigen::MatrixXd values;
75 std::vector<unsigned char> active;
76 std::vector<Eigen::Index> touched;
77};
78
79} // namespace detail
80
104template<typename T>
105std::pair<double, double>
106computeGradientAndHessian(const Eigen::MatrixBase<T>& x,
107 const int ind,
108 const Eigen::MatrixXd& w,
109 const Eigen::MatrixXd& residual,
110 const Eigen::VectorXd& x_centers,
111 const Eigen::VectorXd& x_scales,
112 const double s,
113 const JitNormalization jit_normalization,
114 const int n,
115 const Eigen::VectorXd& weight_sums)
116{
117 double gradient = 0.0;
118 double hessian = 0.0;
119
120 int p = x.cols();
121
122 auto [k, j] = std::div(ind, p);
123
124 if ((w.col(k).array() == 0.0).any()) {
125 const bool center = jit_normalization == JitNormalization::Center ||
126 jit_normalization == JitNormalization::Both;
127 const bool scale = jit_normalization == JitNormalization::Scale ||
128 jit_normalization == JitNormalization::Both;
129 for (int i = 0; i < n; ++i) {
130 if (w(i, k) == 0.0) {
131 continue;
132 }
133 double value = x(i, j) - (center ? x_centers(j) : 0.0);
134 if (scale) {
135 value /= x_scales(j);
136 }
137 gradient += s * value * w(i, k) * residual(i, k);
138 hessian += value * value * w(i, k);
139 }
140 return { gradient / n, hessian / n };
141 }
142
143 // TODO: Benchmark avoiding these copies in the dense path.
144 Eigen::VectorXd residual_v = residual.col(k);
145 Eigen::VectorXd w_v = w.col(k);
146
147 switch (jit_normalization) {
149 gradient = s *
150 (x.col(j).cwiseProduct(w_v).dot(residual_v) -
151 w_v.dot(residual_v) * x_centers(j)) /
152 (n * x_scales(j));
153 hessian =
154 (x.col(j).cwiseAbs2().dot(w_v) - 2 * x_centers(j) * x.col(j).dot(w_v) +
155 std::pow(x_centers(j), 2) * weight_sums(k)) /
156 (std::pow(x_scales(j), 2) * n);
157 break;
158
160 gradient = s *
161 (x.col(j).cwiseProduct(w_v).dot(residual_v) -
162 w_v.dot(residual_v) * x_centers(j)) /
163 n;
164 hessian =
165 (x.col(j).cwiseAbs2().dot(w_v) - 2 * x_centers(j) * x.col(j).dot(w_v) +
166 std::pow(x_centers(j), 2) * weight_sums(k)) /
167 n;
168 break;
169
171 gradient =
172 s * (x.col(j).cwiseProduct(w_v).dot(residual_v)) / (n * x_scales(j));
173 hessian = x.col(j).cwiseAbs2().dot(w_v) / (std::pow(x_scales(j), 2) * n);
174 break;
175
177 gradient = s * (x.col(j).cwiseProduct(w_v).dot(residual_v)) / n;
178 hessian = x.col(j).cwiseAbs2().dot(w_v) / n;
179 break;
180 }
181
182 return { gradient, hessian };
183}
184
185template<typename T>
186std::pair<double, double>
187computeGradientAndHessian(const Eigen::MatrixBase<T>& x,
188 const int ind,
189 const Eigen::MatrixXd& w,
190 const Eigen::MatrixXd& residual,
191 const Eigen::VectorXd& x_centers,
192 const Eigen::VectorXd& x_scales,
193 const double s,
194 const JitNormalization jit_normalization,
195 const int n)
196{
197 const Eigen::VectorXd weight_sums = w.colwise().sum().transpose();
199 ind,
200 w,
201 residual,
202 x_centers,
203 x_scales,
204 s,
205 jit_normalization,
206 n,
207 weight_sums);
208}
209
210template<typename T>
211std::pair<double, double>
212computeGradientAndHessian(const Eigen::SparseMatrixBase<T>& x,
213 const int ind,
214 const Eigen::MatrixXd& w,
215 const Eigen::MatrixXd& residual,
216 const Eigen::VectorXd& x_centers,
217 const Eigen::VectorXd& x_scales,
218 const double s,
219 const JitNormalization jit_normalization,
220 const int n,
221 const Eigen::VectorXd& weight_sums)
222{
223 const int p = x.cols();
224 auto [k, j] = std::div(ind, p);
225
226 double weighted_x_residual_sum = 0.0;
227 double weighted_x_sum = 0.0;
228 double weighted_x_squared_sum = 0.0;
229
230 for (typename T::InnerIterator it(x.derived(), j); it; ++it) {
231 const int i = it.row();
232 const double value = it.value();
233 const double weight = w(i, k);
234
235 if (weight == 0.0) {
236 continue;
237 }
238
239 weighted_x_residual_sum += value * weight * residual(i, k);
240 weighted_x_sum += value * weight;
241 weighted_x_squared_sum += value * value * weight;
242 }
243
244 const bool center = jit_normalization == JitNormalization::Center ||
245 jit_normalization == JitNormalization::Both;
246 const bool scale = jit_normalization == JitNormalization::Scale ||
247 jit_normalization == JitNormalization::Both;
248 const double offset = center ? x_centers(j) : 0.0;
249 const double feature_scale = scale ? x_scales(j) : 1.0;
250
251 if (center) {
252 weighted_x_residual_sum -=
253 offset * detail::weightedResidualSum(w.col(k), residual.col(k));
254 weighted_x_squared_sum +=
255 offset * offset * weight_sums(k) - 2.0 * offset * weighted_x_sum;
256 }
257
258 const double gradient = s * weighted_x_residual_sum / (n * feature_scale);
259 const double hessian =
260 weighted_x_squared_sum / (n * feature_scale * feature_scale);
261
262 return { gradient, hessian };
263}
264
265template<typename T>
266std::pair<double, double>
267computeGradientAndHessian(const Eigen::SparseMatrixBase<T>& x,
268 const int ind,
269 const Eigen::MatrixXd& w,
270 const Eigen::MatrixXd& residual,
271 const Eigen::VectorXd& x_centers,
272 const Eigen::VectorXd& x_scales,
273 const double s,
274 const JitNormalization jit_normalization,
275 const int n)
276{
277 const Eigen::VectorXd weight_sums = w.colwise().sum().transpose();
279 ind,
280 w,
281 residual,
282 x_centers,
283 x_scales,
284 s,
285 jit_normalization,
286 n,
287 weight_sums);
288}
289
313template<typename T>
314std::pair<double, double>
315computeClusterGradientAndHessian(const Eigen::MatrixBase<T>& x,
316 const int c_ind,
317 const std::vector<int>& s,
318 const Clusters& clusters,
319 const Eigen::MatrixXd& w,
320 const Eigen::MatrixXd& residual,
321 const Eigen::VectorXd& x_centers,
322 const Eigen::VectorXd& x_scales,
323 const JitNormalization jit_normalization)
324{
325 int n = x.rows();
326 int p = x.cols();
327 int m = residual.cols();
328
329 Eigen::MatrixXd x_s = Eigen::MatrixXd::Zero(n, m);
330
331 auto s_it = s.cbegin();
332 auto c_it = clusters.cbegin(c_ind);
333
334 for (; c_it != clusters.cend(c_ind); ++c_it, ++s_it) {
335 int ind = *c_it;
336 auto [k, j] = std::div(ind, p);
337 double s = *s_it;
338
339 switch (jit_normalization) {
341 x_s.col(k) += x.col(j) * (s / x_scales(j));
342 x_s.col(k).array() -= x_centers(j) * s / x_scales(j);
343 break;
344
346 x_s.col(k) += x.col(j) * s;
347 x_s.col(k).array() -= x_centers(j) * s;
348 break;
349
351 x_s.col(k) += x.col(j) * (s / x_scales(j));
352 break;
353
355 x_s.col(k) += x.col(j) * s;
356 break;
357 }
358 }
359
360 double hess = 0;
361 double grad = 0;
362
363 for (int k = 0; k < m; ++k) {
364 if ((w.col(k).array() == 0.0).any()) {
365 for (int i = 0; i < n; ++i) {
366 if (w(i, k) != 0.0) {
367 hess += x_s(i, k) * x_s(i, k) * w(i, k) / n;
368 grad += x_s(i, k) * w(i, k) * residual(i, k) / n;
369 }
370 }
371 } else {
372 hess += x_s.col(k).cwiseAbs2().dot(w.col(k)) / n;
373 grad += x_s.col(k).cwiseProduct(w.col(k)).dot(residual.col(k)) / n;
374 }
375 }
376
377 return { hess, grad };
378}
379
402namespace detail {
403
404template<typename T>
405std::pair<double, double>
406computeSparseClusterGradientAndHessian(const Eigen::SparseMatrixBase<T>& x,
407 const int c_ind,
408 const std::vector<int>& s,
409 const Clusters& clusters,
410 const Eigen::MatrixXd& w,
411 const Eigen::MatrixXd& residual,
412 const Eigen::VectorXd& weight_sums,
413 const Eigen::VectorXd& x_centers,
414 const Eigen::VectorXd& x_scales,
415 const JitNormalization jit_normalization,
416 SparseClusterWorkspace& workspace)
417{
418 int n = x.rows();
419 int p = x.cols();
420 int m = residual.cols();
421
422 workspace.resize(n, m);
423
424 Eigen::ArrayXd offset = Eigen::ArrayXd::Zero(m);
425
426 auto s_it = s.cbegin();
427 auto c_it = clusters.cbegin(c_ind);
428
429 for (; c_it != clusters.cend(c_ind); ++c_it, ++s_it) {
430 int ind = *c_it;
431 auto [k, j] = std::div(ind, p);
432 double s_ind = *s_it;
433
434 switch (jit_normalization) {
436 offset(k) += x_centers(j) * s_ind;
437 break;
439 offset(k) += x_centers(j) * s_ind / x_scales(j);
440 break;
442 break;
444 break;
445 }
446
447 double scale = s_ind;
448 if (jit_normalization == JitNormalization::Scale ||
449 jit_normalization == JitNormalization::Both) {
450 scale /= x_scales(j);
451 }
452
453 for (typename T::InnerIterator it(x.derived(), j); it; ++it) {
454 if (w(it.row(), k) != 0.0) {
455 workspace.add(it.row(), k, it.value() * scale);
456 }
457 }
458 }
459
460 double hess = 0;
461 double grad = 0;
462 Eigen::ArrayXd weighted_sum = Eigen::ArrayXd::Zero(m);
463
464 for (const Eigen::Index index : workspace.touched) {
465 const int k = index / n;
466 const int i = index - k * n;
467 const double value = workspace.values.data()[index];
468 const double weight = w(i, k);
469
470 if (weight == 0.0) {
471 continue;
472 }
473
474 hess += value * value * weight;
475 grad += value * weight * residual(i, k);
476 weighted_sum(k) += value * weight;
477 }
478
479 if (jit_normalization == JitNormalization::Center ||
480 jit_normalization == JitNormalization::Both) {
481 for (int k = 0; k < m; ++k) {
482 hess += -2 * offset(k) * weighted_sum(k) +
483 std::pow(offset(k), 2) * weight_sums(k);
484 grad -= offset(k) * weightedResidualSum(w.col(k), residual.col(k));
485 }
486 }
487
488 workspace.clear();
489
490 return { hess / n, grad / n };
491}
492
493template<typename T>
494std::pair<double, double>
495computeClusterGradientAndHessianWithWorkspace(
496 const Eigen::MatrixBase<T>& x,
497 const int c_ind,
498 const std::vector<int>& s,
499 const Clusters& clusters,
500 const Eigen::MatrixXd& w,
501 const Eigen::MatrixXd& residual,
502 const Eigen::VectorXd&,
503 const Eigen::VectorXd& x_centers,
504 const Eigen::VectorXd& x_scales,
505 const JitNormalization jit_normalization,
506 SparseClusterWorkspace&)
507{
509 x, c_ind, s, clusters, w, residual, x_centers, x_scales, jit_normalization);
510}
511
512template<typename T>
513std::pair<double, double>
514computeClusterGradientAndHessianWithWorkspace(
515 const Eigen::SparseMatrixBase<T>& x,
516 const int c_ind,
517 const std::vector<int>& s,
518 const Clusters& clusters,
519 const Eigen::MatrixXd& w,
520 const Eigen::MatrixXd& residual,
521 const Eigen::VectorXd& weight_sums,
522 const Eigen::VectorXd& x_centers,
523 const Eigen::VectorXd& x_scales,
524 const JitNormalization jit_normalization,
525 SparseClusterWorkspace& workspace)
526{
527 return computeSparseClusterGradientAndHessian(x,
528 c_ind,
529 s,
530 clusters,
531 w,
532 residual,
533 weight_sums,
534 x_centers,
535 x_scales,
536 jit_normalization,
537 workspace);
538}
539
540} // namespace detail
541
542template<typename T>
543std::pair<double, double>
544computeClusterGradientAndHessian(const Eigen::SparseMatrixBase<T>& x,
545 const int c_ind,
546 const std::vector<int>& s,
547 const Clusters& clusters,
548 const Eigen::MatrixXd& w,
549 const Eigen::MatrixXd& residual,
550 const Eigen::VectorXd& x_centers,
551 const Eigen::VectorXd& x_scales,
552 const JitNormalization jit_normalization)
553{
554 detail::SparseClusterWorkspace workspace(x.rows(), residual.cols());
555 const Eigen::VectorXd weight_sums = w.colwise().sum().transpose();
556 return detail::computeSparseClusterGradientAndHessian(x,
557 c_ind,
558 s,
559 clusters,
560 w,
561 residual,
562 weight_sums,
563 x_centers,
564 x_scales,
565 jit_normalization,
566 workspace);
567}
568
599template<typename T>
600double
601coordinateDescent(Eigen::VectorXd& beta0,
602 Eigen::VectorXd& beta,
603 Eigen::MatrixXd& residual,
604 Clusters& clusters,
605 const Eigen::ArrayXd& lambda_cumsum,
606 const T& x,
607 const Eigen::MatrixXd& w,
608 const Eigen::VectorXd& weight_sums,
609 const Eigen::VectorXd& x_centers,
610 const Eigen::VectorXd& x_scales,
611 const bool intercept,
612 const JitNormalization jit_normalization,
613 const bool update_clusters,
614 std::mt19937& rng,
615 const std::string& cd_type = "cyclical")
616{
617 using namespace Eigen;
618
619 const int n = x.rows();
620 const int p = x.cols();
621 const int m = residual.cols();
622
623 double max_abs_gradient = 0;
624 detail::SparseClusterWorkspace sparse_workspace;
625
626 // Create a vector of indices to process
627 std::vector<int> indices;
628 indices.reserve(clusters.size());
629 for (int i = 0; i < clusters.size(); ++i) {
630 if (clusters.coeff(i) != 0) { // Skip zero cluster
631 indices.push_back(i);
632 }
633 }
634
635 if (cd_type == "permuted") {
636 std::shuffle(indices.begin(), indices.end(), rng);
637 }
638
639 for (int c_ind : indices) {
640 // Skip if index is no longer valid due to cluster updates
641 if (c_ind >= clusters.size()) {
642 continue;
643 }
644
645 double c_old = clusters.coeff(c_ind);
646
647 if (c_old == 0) {
648 // We do not update the zero cluster because it can be very large, but
649 // often does not change.
650 continue;
651 }
652
653 int cluster_size = clusters.cluster_size(c_ind);
654 std::vector<int> s;
655 s.reserve(cluster_size);
656
657 for (auto c_it = clusters.cbegin(c_ind); c_it != clusters.cend(c_ind);
658 ++c_it) {
659 int ind = *c_it;
660 assert(ind >= 0 && ind < beta.size() && "Invalid index in cluster");
661 double s_ind = sign(beta(ind));
662 s.emplace_back(s_ind);
663 }
664
665 double hess = 1;
666 double grad = 0;
667 VectorXd x_s(n);
668
669 if (cluster_size == 1) {
670 int ind = *clusters.cbegin(c_ind);
671 std::tie(grad, hess) = computeGradientAndHessian(x,
672 ind,
673 w,
674 residual,
675 x_centers,
676 x_scales,
677 s[0],
678 jit_normalization,
679 n,
680 weight_sums);
681 } else {
682 std::tie(hess, grad) =
683 detail::computeClusterGradientAndHessianWithWorkspace(x,
684 c_ind,
685 s,
686 clusters,
687 w,
688 residual,
689 weight_sums,
690 x_centers,
691 x_scales,
692 jit_normalization,
693 sparse_workspace);
694 }
695
696 max_abs_gradient = std::max(max_abs_gradient, std::abs(grad));
697
698 double c_tilde;
699 int new_index;
700
701 const double gamma = hess * c_old - grad;
702 std::tie(c_tilde, new_index) =
703 slopeThreshold(gamma, hess, c_ind, lambda_cumsum, clusters);
704
705 assert(c_tilde == 0 || new_index < clusters.size());
706 assert(new_index >= 0 && new_index <= clusters.size());
707
708 double c_diff = c_old - c_tilde;
709
710 if (c_diff != 0) {
711 auto s_it = s.cbegin();
712 auto c_it = clusters.cbegin(c_ind);
713 for (; c_it != clusters.cend(c_ind); ++c_it, ++s_it) {
714 int ind = *c_it;
715 auto [k, j] = std::div(ind, p);
716 double s_ind = *s_it;
717
718 // Update coefficient
719 beta(ind) = c_tilde * s_ind;
720
721 // Update residual
722 switch (jit_normalization) {
724 residual.col(k) -= x.col(j) * (s_ind * c_diff / x_scales(j));
725 residual.col(k).array() +=
726 x_centers(j) * s_ind * c_diff / x_scales(j);
727 break;
728
730 residual.col(k) -= x.col(j) * (s_ind * c_diff);
731 residual.col(k).array() += x_centers(j) * s_ind * c_diff;
732 break;
733
735 residual.col(k) -= x.col(j) * (s_ind * c_diff / x_scales(j));
736 break;
737
739 residual.col(k) -= x.col(j) * (s_ind * c_diff);
740 break;
741 }
742 }
743 }
744
745 if (update_clusters) {
746 clusters.update(c_ind, new_index, std::abs(c_tilde));
747 } else {
748 clusters.setCoeff(c_ind, std::abs(c_tilde));
749 }
750 }
751
752 if (intercept) {
753 for (int k = 0; k < residual.cols(); ++k) {
754 double beta0_update =
755 detail::weightedResidualSum(w.col(k), residual.col(k)) / n;
756 residual.col(k).array() -= beta0_update;
757 beta0(k) -= beta0_update;
758 }
759 }
760
761 return max_abs_gradient;
762}
763
764} // 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.
The declaration of the Clusters class.
Eigen compatibility layer for version differences.
Mathematical support functions for the slope package.
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
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:106
int sign(T val)
Returns the sign of a given value.
Definition math.h:40
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:315
The declaration of the slopeThreshold function.