irlba
A C++ library for IRLBA
Loading...
Searching...
No Matches
compute.hpp
Go to the documentation of this file.
1#ifndef IRLBA_COMPUTE_HPP
2#define IRLBA_COMPUTE_HPP
3
4#include <cmath>
5#include <cstdint>
6#include <random>
7#include <stdexcept>
8#include <type_traits>
9
10#include "utils.hpp"
11#include "lanczos.hpp"
12#include "Options.hpp"
13#include "Matrix/simple.hpp"
14
15#include "sanisizer/sanisizer.hpp"
16#include "Eigen/Dense"
17
23namespace irlba {
24
28template<class Matrix_, class EigenMatrix_, class EigenVector_>
29void exact(const Matrix_& matrix, const Eigen::Index requested_number, EigenMatrix_& outU, EigenMatrix_& outV, EigenVector_& outD) {
30 Eigen::BDCSVD<EigenMatrix_, Eigen::ComputeThinU | Eigen::ComputeThinV> svd(matrix.rows(), matrix.cols());
31
32 auto realizer = matrix.new_known_realize_workspace();
33 EigenMatrix_ buffer;
34 svd.compute(realizer->realize(buffer));
35
36 outD.resize(requested_number);
37 outD = svd.singularValues().head(requested_number);
38
39 outU.resize(matrix.rows(), requested_number);
40 outU = svd.matrixU().leftCols(requested_number);
41
42 outV.resize(matrix.cols(), requested_number);
43 outV = svd.matrixV().leftCols(requested_number);
44}
45
46// The R package's default is 7 but larger values can improve performance for large 'requested_number'.
47inline Eigen::Index choose_extra_work(const Eigen::Index requested, const std::optional<Eigen::Index>& extra_work) {
48 if (extra_work.has_value()) {
49 return *extra_work;
50 } else {
51 return std::max(requested, static_cast<Eigen::Index>(7));
52 }
53}
54
55// Basically (requested >= smaller / 2), but avoiding a cast to double.
56inline bool requested_greater_than_or_equal_to_half_smaller(const Eigen::Index requested, const Eigen::Index smaller) {
57 const Eigen::Index half_smaller = smaller / 2;
58 if (requested == half_smaller) {
59 return smaller % 2 == 0;
60 } else {
61 return requested > half_smaller;
62 }
63}
64
65// Basically min(requested_number + extra_work, smaller), but avoiding overflow from the sum.
66inline Eigen::Index choose_requested_plus_extra_work_or_smaller(const Eigen::Index requested_number, const Eigen::Index extra_work, const Eigen::Index smaller) {
67 if (requested_number >= smaller) {
68 return smaller;
69 } else {
70 // This is guaranteed to fit into an Eigen::Index;
71 // either it's equal to 'smaller' or it's less than 'smaller'.
72 return requested_number + sanisizer::min(smaller - requested_number, extra_work);
73 }
74}
75
76// Setting 'k'. This looks kinda weird, but this is deliberate, see the text below Algorithm 3.1 of Baglama and Reichel.
77//
78// - Our n_converged is their k'.
79// - Our requested_number is their k.
80// - Our work is their m.
81// - Our returned k is their k + k''.
82//
83// They don't mention anything about the value corresponding to our input k, but I guess we always want k to increase.
84// So, if our proposed new value of k is lower than our current value of k, we just pick the latter.
85inline Eigen::Index update_k(Eigen::Index k, const Eigen::Index requested_number, const Eigen::Index n_converged, const Eigen::Index work) {
86 // Here, the goal is to reproduce this code from irlba's convtests() function, but without any risk of wraparound or overflow.
87 //
88 // if (k < requested_number + n_converged) {
89 // k = requested_number + n_converged;
90 // }
91 // if (k > work - 3) {
92 // k = work - 3;
93 // }
94 // if (k < 1) {
95 // k = 1;
96 // }
97
98 if (work <= 3) {
99 return 1;
100 }
101 const Eigen::Index limit = work - 3;
102
103 const auto less_than_requested_plus_converged = [&](const Eigen::Index val) -> bool {
104 return val < requested_number || static_cast<Eigen::Index>(val - requested_number) < n_converged;
105 };
106
107 if (less_than_requested_plus_converged(k)) {
108 if (less_than_requested_plus_converged(limit)) {
109 return limit;
110 } else {
111 const Eigen::Index output = requested_number + n_converged;
112 return std::max(output, static_cast<Eigen::Index>(1));
113 }
114 } else {
115 const Eigen::Index output = std::min(k, limit);
116 return std::max(output, static_cast<Eigen::Index>(1));
117 }
118}
126struct Metrics {
130 int iterations = 0;
131
136
140 bool converged = false;
141};
142
168template<class Matrix_, class EigenMatrix_, class EigenVector_>
170 const Matrix_& matrix,
171 const Eigen::Index number,
172 EigenMatrix_& outU,
173 EigenMatrix_& outV,
174 EigenVector_& outD,
175 const Options<EigenVector_>& options
176) {
177 const Eigen::Index smaller = std::min(matrix.rows(), matrix.cols());
178 Eigen::Index requested_number = sanisizer::cast<Eigen::Index>(number);
179 if (requested_number > smaller) {
180 if (options.cap_number) {
181 requested_number = smaller;
182 } else {
183 throw std::runtime_error("requested number of singular values cannot be greater than the smaller matrix dimension");
184 }
185 } else if (requested_number == smaller && !options.exact_for_large_number) {
186 throw std::runtime_error("requested number of singular values must be less than the smaller matrix dimension for IRLBA iterations");
187 }
188
189 // Optimization if no SVs are requested. Also protect the exact SVD code
190 // from a zero-extent matrix, as this causes Eigen to shit itself.
191 if (requested_number == 0) {
192 outD.resize(requested_number);
193 outU.resize(matrix.rows(), requested_number);
194 outV.resize(matrix.cols(), requested_number);
195 Metrics met;
196 met.converged = true;
197 return met;
198 }
199
200 // Falling back to an exact SVD for small matrices or if the requested number is too large
201 // (not enough of a workspace). Hey, I don't make the rules.
202 if (
203 (options.exact_for_small_matrix && smaller < 6) ||
204 (options.exact_for_large_number && requested_greater_than_or_equal_to_half_smaller(requested_number, smaller))
205 ) {
206 exact(matrix, requested_number, outU, outV, outD);
207 Metrics met;
208 met.converged = true;
209 return met;
210 }
211
212 const Eigen::Index extra_work = choose_extra_work(requested_number, options.extra_work);
213
214 // We know work must be positive at this point, as we would have returned early if requested_number = 0.
215 // Similar, if smaller = 0, we would have either thrown earlier or set requested_number = 0.
216 const Eigen::Index work = choose_requested_plus_extra_work_or_smaller(requested_number, extra_work, smaller);
217
218 // Don't worry about sanitizing dimensions for Eigen constructors,
219 // as the former are stored as Eigen::Index and the latter accepts Eigen::Index inputs.
220 EigenMatrix_ V(matrix.cols(), work);
221 std::mt19937_64 eng(options.seed);
222 if (options.initial.has_value()) {
223 const auto& init = *(options.initial);
224 if (init.size() != V.rows()) {
225 throw std::runtime_error("initialization vector does not have expected number of rows");
226 }
227 V.col(0) = init;
228 } else {
229 fill_with_random_normals(V, 0, eng);
230 }
231 V.col(0) /= V.col(0).norm();
232
233 bool converged = false;
234 int iter = 0, mult = 0;
235 Eigen::Index k = 0;
236
237 // No need for QR preconditioning when we're dealing with a square matrix.
238 Eigen::BDCSVD<EigenMatrix_, Eigen::NoQRPreconditioner | Eigen::ComputeFullU | Eigen::ComputeFullV> svd(work, work);
239
240 LanczosWorkspace<EigenVector_, Matrix_> lpwork(matrix);
241
242 EigenMatrix_ W(matrix.rows(), work);
243 EigenMatrix_ Wtmp(matrix.rows(), work);
244 EigenMatrix_ Vtmp(matrix.cols(), work);
245
246 EigenMatrix_ B(work, work);
247 B.setZero(work, work);
248 EigenVector_ res(work);
249 EigenVector_ F(matrix.cols());
250
251 EigenVector_ prevS(work);
252 const typename EigenMatrix_::Scalar tol = options.convergence_tolerance;
253 const typename EigenMatrix_::Scalar svtol_actual = choose_singular_value_tolerance(options);
254 const auto inv_eps = choose_invariant_tolerance<typename EigenMatrix_::Scalar>(options);
255
256 for (; iter < options.max_iterations; ++iter) {
257 // Technically, this is only a 'true' Lanczos bidiagonalization
258 // when k = 0. All other times, we're just recycling the machinery,
259 // see the text below Equation 3.11 in Baglama and Reichel.
260 mult += run_lanczos_bidiagonalization(lpwork, W, V, B, eng, k, inv_eps);
261
262// if (iter < 2) {
263// std::cout << "B is currently:\n" << B << std::endl;
264// std::cout << "W is currently:\n" << W << std::endl;
265// std::cout << "V is currently:\n" << V << std::endl;
266// }
267
268 svd.compute(B);
269 const auto& BS = svd.singularValues();
270 const auto& BU = svd.matrixU();
271 const auto& BV = svd.matrixV();
272
273 // Checking for convergence.
274 if (B.coeff(work - 1, work - 1) == 0) { // a.k.a. the final value of 'S' from the Lanczos iterations.
275 converged = true;
276 break;
277 }
278
279 const auto& F = lpwork.F; // i.e., the residuals, see Algorithm 2.1 of Baglama and Reichel.
280 auto R_F = F.norm();
281
282 // Computes the convergence criterion defined in on the LHS of
283 // Equation 2.13 of Baglama and Riechel. (e_m is literally the unit
284 // vector along the m-th dimension, so it ends up just being the
285 // m-th row of the U matrix.) We expose it here as we will be
286 // re-using the same values to update B, see below.
287 res = R_F * BU.row(work - 1);
288
289 Eigen::Index n_converged = 0;
290 if (iter > 0) {
291 const auto Smax = *std::max_element(BS.begin(), BS.end());
292 const auto threshold = Smax * tol;
293
294 for (Eigen::Index j = 0; j < work; ++j) {
295 if (std::abs(res[j]) <= threshold) {
296 const auto ratio = std::abs(BS[j] - prevS[j]) / BS[j];
297 if (ratio <= svtol_actual) {
298 ++n_converged;
299 }
300 }
301 }
302
303 if (n_converged >= requested_number) {
304 converged = true;
305 break;
306 }
307 }
308 prevS = BS;
309
310 k = update_k(k, requested_number, n_converged, work);
311 Vtmp.leftCols(k).noalias() = V * BV.leftCols(k); // don't write directly into V, to avoid aliasing problems.
312 V.swap(Vtmp);
313
314 // See Equation 3.2 of Baglama and Reichel, where our 'V' is their
315 // 'P', and our 'F / R_F' is their 'p_{m+1}' (Equation 2.2). 'F'
316 // was orthogonal to the old 'V' and so it should still be
317 // orthogonal to the new left-most columns of 'V'; the input
318 // expectations of the Lanczos bidiagonalization are still met, so
319 // V is ok to use in the next run_lanczos_bidiagonalization().
320 V.col(k) = F / R_F;
321
322 Wtmp.leftCols(k).noalias() = W * BU.leftCols(k); // don't write directly into W, to avoid aliasing problems.
323 W.swap(Wtmp);
324
325 B.setZero(work, work);
326 for (I<decltype(k)> l = 0; l < k; ++l) {
327 B.coeffRef(l, l) = BS[l];
328
329 // This assignment looks weird but is deliberate, see Equation
330 // 3.6 of Baglama and Reichel. Happily, this is the same value
331 // used to determine convergence in 2.13, so we just re-use it.
332 // (See the equation just above Equation 3.5; I think they
333 // misplaced a tilde on the final 'u', given no other 'u' has
334 // a B_m superscript as well as a tilde.)
335 B.coeffRef(l, k) = res[l];
336 }
337 }
338
339 // See Equation 2.11 of Baglama and Reichel for how to get from B's
340 // singular triplets to mat's singular triplets.
341 outD.resize(requested_number);
342 outD = svd.singularValues().head(requested_number);
343
344 outU.resize(matrix.rows(), requested_number);
345 outU.noalias() = W * svd.matrixU().leftCols(requested_number);
346
347 outV.resize(matrix.cols(), requested_number);
348 outV.noalias() = V * svd.matrixV().leftCols(requested_number);
349
350 Metrics met;
351 met.converged = converged;
352 met.multiplications = mult;
353 met.iterations = (converged ? iter + 1 : iter);
354 return met;
355}
356
380template<class InputEigenMatrix_, class OutputEigenMatrix_, class EigenVector_>
382 const InputEigenMatrix_& matrix,
383 Eigen::Index number,
384 OutputEigenMatrix_& outU,
385 OutputEigenMatrix_& outV,
386 EigenVector_& outD,
387 const Options<EigenVector_>& options
388) {
390
391 // Force the compiler to use virtual dispatch to avoid realizing multiple
392 // instances of this function for each InputEigenMatrix_ type. If you
393 // don't like this, call compute() directly.
395
396 return compute<Interface>(wrapped, number, outU, outV, outD, options);
397}
398
404template<class EigenMatrix_, class EigenVector_>
405struct Results {
411 EigenMatrix_ U;
412
418 EigenMatrix_ V;
419
424 EigenVector_ D;
425
430};
431
445template<class EigenMatrix_ = Eigen::MatrixXd, class EigenVector_ = Eigen::VectorXd, class Matrix_>
446Results<EigenMatrix_, EigenVector_> compute(const Matrix_& matrix, Eigen::Index number, const Options<EigenVector_>& options) {
448 output.metrics = compute(matrix, number, output.U, output.V, output.D, options);
449 return output;
450}
451
466template<class OutputEigenMatrix_ = Eigen::MatrixXd, class EigenVector_ = Eigen::VectorXd, class InputEigenMatrix_>
467Results<OutputEigenMatrix_, EigenVector_> compute_simple(const InputEigenMatrix_& matrix, Eigen::Index number, const Options<EigenVector_>& options) {
469 output.metrics = compute_simple(matrix, number, output.U, output.V, output.D, options);
470 return output;
471}
472
473}
474
475#endif
Options for IRLBA.
Interface for a matrix to use in compute().
Definition interface.hpp:142
A Matrix-compatible wrapper around a "simple" matrix.
Definition simple.hpp:89
Metrics compute(const Matrix_ &matrix, const Eigen::Index number, EigenMatrix_ &outU, EigenMatrix_ &outV, EigenVector_ &outD, const Options< EigenVector_ > &options)
Definition compute.hpp:169
Metrics compute_simple(const InputEigenMatrix_ &matrix, Eigen::Index number, OutputEigenMatrix_ &outU, OutputEigenMatrix_ &outV, EigenVector_ &outD, const Options< EigenVector_ > &options)
Definition compute.hpp:381
Simple wrapper around an Eigen matrix.
Metrics for IRLBA progress from compute().
Definition compute.hpp:126
int iterations
Definition compute.hpp:130
int multiplications
Definition compute.hpp:135
bool converged
Definition compute.hpp:140
Options for running IRLBA in compute() and pca().
Definition Options.hpp:21
double convergence_tolerance
Definition Options.hpp:39
bool exact_for_large_number
Definition Options.hpp:74
int max_iterations
Definition Options.hpp:62
std::optional< EigenVector_ > initial
Definition Options.hpp:91
bool exact_for_small_matrix
Definition Options.hpp:68
bool cap_number
Definition Options.hpp:80
std::optional< Eigen::Index > extra_work
Definition Options.hpp:56
std::mt19937_64::result_type seed
Definition Options.hpp:85
Results of the IRLBA-based SVD by compute().
Definition compute.hpp:405
EigenVector_ D
Definition compute.hpp:424
Metrics metrics
Definition compute.hpp:429
EigenMatrix_ U
Definition compute.hpp:411
EigenMatrix_ V
Definition compute.hpp:418