// Copyright (C) 2026 Kiyotsugu Arai // SPDX-License-Identifier: LGPL-3.0-or-later // RandomizedSVD.hpp — Randomized SVD // // Halko-Martinsson-Tropp (2011) algorithm. // Approximates the top k singular values/vectors of a large matrix in O(mnk). // // Reference: "Finding Structure with Randomness: Probabilistic Algorithms // for Constructing Approximate Matrix Decompositions" #ifndef SANGI_MATH_LINALG_RANDOMIZED_SVD_HPP #define SANGI_MATH_LINALG_RANDOMIZED_SVD_HPP #include #include #include #include #include #include namespace sangi { template struct RandomizedSVDResult { Matrix U; // (m × k) left singular vectors std::vector S; // (k) singular values Matrix Vt; // (k × n) right singular vectors (transposed) }; /// Randomized SVD /// @param A input matrix (m × n) /// @param k number of singular values to compute /// @param oversampling oversampling count (improves accuracy, usually 5–10) /// @param powerIter number of power iterations (useful when spectral decay is slow) /// @param seed random seed template RandomizedSVDResult randomizedSVD(const BaseMatrix& A, size_t k, size_t oversampling = 5, size_t powerIter = 1, unsigned seed = 42) { size_t m = A.rows(), n = A.cols(); size_t l = std::min(k + oversampling, std::min(m, n)); std::mt19937 rng(seed); std::normal_distribution normal(T{0}, T{1}); // Step 1: random projection Ω (n × l) Matrix Omega(n, l); for (size_t i = 0; i < n; ++i) for (size_t j = 0; j < l; ++j) Omega(i, j) = normal(rng); // Step 2: Y = A * Ω (m × l) Matrix Y(m, l, T{0}); for (size_t i = 0; i < m; ++i) for (size_t j = 0; j < l; ++j) for (size_t p = 0; p < n; ++p) Y(i, j) += A(i, p) * Omega(p, j); // Step 3: power iteration (improves spectral decay) for (size_t q = 0; q < powerIter; ++q) { // Z = A^T * Y (n × l) Matrix Z(n, l, T{0}); for (size_t i = 0; i < n; ++i) for (size_t j = 0; j < l; ++j) for (size_t p = 0; p < m; ++p) Z(i, j) += A(p, i) * Y(p, j); // Y = A * Z (m × l) Y = Matrix(m, l, T{0}); for (size_t i = 0; i < m; ++i) for (size_t j = 0; j < l; ++j) for (size_t p = 0; p < n; ++p) Y(i, j) += A(i, p) * Z(p, j); } // Step 4: QR decomposition (Y = Q * R) — modified Gram-Schmidt Matrix Q(m, l, T{0}); for (size_t j = 0; j < l; ++j) { for (size_t i = 0; i < m; ++i) Q(i, j) = Y(i, j); for (size_t p = 0; p < j; ++p) { T dot = T{0}; for (size_t i = 0; i < m; ++i) dot += Q(i, p) * Q(i, j); for (size_t i = 0; i < m; ++i) Q(i, j) -= dot * Q(i, p); } T norm = T{0}; for (size_t i = 0; i < m; ++i) norm += Q(i, j) * Q(i, j); norm = std::sqrt(norm); if (norm > T{1e-15}) for (size_t i = 0; i < m; ++i) Q(i, j) /= norm; } // Step 5: B = Q^T * A (l × n) Matrix B(l, n, T{0}); for (size_t i = 0; i < l; ++i) for (size_t j = 0; j < n; ++j) for (size_t p = 0; p < m; ++p) B(i, j) += Q(p, i) * A(p, j); // Step 6: full SVD of B (small: l × n, l << m) // Extract the top k via power method Matrix Ub(l, k, T{0}); Matrix Vtb(k, n, T{0}); std::vector Sb(k, T{0}); Matrix Bwork = B; for (size_t r = 0; r < k; ++r) { std::vector v(n); for (size_t j = 0; j < n; ++j) v[j] = normal(rng); for (int iter = 0; iter < 100; ++iter) { // u = B * v std::vector u(l, T{0}); for (size_t i = 0; i < l; ++i) for (size_t j = 0; j < n; ++j) u[i] += Bwork(i, j) * v[j]; T nu = T{0}; for (auto x : u) nu += x * x; nu = std::sqrt(nu); if (nu < T{1e-15}) break; for (auto& x : u) x /= nu; // v = B^T * u std::fill(v.begin(), v.end(), T{0}); for (size_t j = 0; j < n; ++j) for (size_t i = 0; i < l; ++i) v[j] += Bwork(i, j) * u[i]; T nv = T{0}; for (auto x : v) nv += x * x; Sb[r] = std::sqrt(nv); if (Sb[r] < T{1e-15}) break; for (auto& x : v) x /= Sb[r]; } // u = B * v / sigma std::vector u(l, T{0}); for (size_t i = 0; i < l; ++i) for (size_t j = 0; j < n; ++j) u[i] += Bwork(i, j) * v[j]; if (Sb[r] > T{1e-15}) for (auto& x : u) x /= Sb[r]; for (size_t i = 0; i < l; ++i) Ub(i, r) = u[i]; for (size_t j = 0; j < n; ++j) Vtb(r, j) = v[j]; // Deflation for (size_t i = 0; i < l; ++i) for (size_t j = 0; j < n; ++j) Bwork(i, j) -= Sb[r] * u[i] * v[j]; } // Step 7: U = Q * Ub (m × k) Matrix U(m, k, T{0}); for (size_t i = 0; i < m; ++i) for (size_t r = 0; r < k; ++r) for (size_t p = 0; p < l; ++p) U(i, r) += Q(i, p) * Ub(p, r); return { std::move(U), std::move(Sb), std::move(Vtb) }; } } // namespace sangi #endif // SANGI_MATH_LINALG_RANDOMIZED_SVD_HPP