Skip to main content

legume_numeric/matrix/
dmatrix_rsvd.rs

1use crate::matrix::traits::*;
2use nalgebra::{DMatrix, DVector};
3use nalgebra_sparse::{csc::CscMatrix, csr::CsrMatrix};
4
5/// Fixed start-vector seed for the randomized-SVD subspace iteration. The
6/// iteration converges onto the dominant subspace, so pinning the start makes
7/// `rsvd` reproducible without altering the subspace it recovers.
8const RSVD_SUBSPACE_SEED: u64 = 0x5253_5644_5342_5350; // "RSVDSBSP"
9
10/// Compute the Nystrom basis: `U * diag(1 / (s + eps))`.
11///
12/// Given the left singular vectors `u` and singular values `s` from an SVD,
13/// returns the pseudo-inverted projection matrix used for out-of-sample
14/// Nystrom extension.
15pub fn nystrom_basis(u: &DMatrix<f32>, s: &DVector<f32>) -> DMatrix<f32> {
16    let eps = 1e-8;
17    let sinv = DVector::from_iterator(s.len(), s.iter().map(|&si| 1.0 / (si + eps)));
18    u * DMatrix::from_diagonal(&sinv)
19}
20
21trait IntoDense<OutMat> {
22    fn matmul(&self, other: &OutMat) -> OutMat;
23    fn transpose_matmul(&self, other: &OutMat) -> OutMat;
24    fn num_rows(&self) -> usize;
25    fn num_columns(&self) -> usize;
26}
27
28impl<T> IntoDense<DMatrix<T>> for DMatrix<T>
29where
30    T: nalgebra::RealField + num_traits::Float + Copy,
31{
32    fn matmul(&self, other: &DMatrix<T>) -> DMatrix<T> {
33        self * other
34    }
35
36    fn transpose_matmul(&self, other: &DMatrix<T>) -> DMatrix<T> {
37        self.transpose() * other
38    }
39
40    fn num_rows(&self) -> usize {
41        self.nrows()
42    }
43    fn num_columns(&self) -> usize {
44        self.ncols()
45    }
46}
47
48impl<T> IntoDense<DMatrix<T>> for CscMatrix<T>
49where
50    T: nalgebra::RealField + num_traits::Float + Copy,
51{
52    fn matmul(&self, other: &DMatrix<T>) -> DMatrix<T> {
53        self * other
54    }
55    fn transpose_matmul(&self, other: &DMatrix<T>) -> DMatrix<T> {
56        self.transpose() * other
57    }
58    fn num_rows(&self) -> usize {
59        self.nrows()
60    }
61    fn num_columns(&self) -> usize {
62        self.ncols()
63    }
64}
65
66impl<T> IntoDense<DMatrix<T>> for CsrMatrix<T>
67where
68    T: nalgebra::RealField + num_traits::Float + Copy,
69{
70    fn matmul(&self, other: &DMatrix<T>) -> DMatrix<T> {
71        self * other
72    }
73    fn transpose_matmul(&self, other: &DMatrix<T>) -> DMatrix<T> {
74        self.transpose() * other
75    }
76    fn num_rows(&self) -> usize {
77        self.nrows()
78    }
79    fn num_columns(&self) -> usize {
80        self.ncols()
81    }
82}
83
84fn _subspace_iteration<T, D>(xx: &D, rank_and_oversample: usize) -> anyhow::Result<DMatrix<T>>
85where
86    T: nalgebra::RealField + num_traits::Float + Copy,
87    D: IntoDense<DMatrix<T>>,
88{
89    let max_iter = 5; // five should be enough
90
91    let nc = xx.num_columns();
92    // Fixed seed: the subspace iterations below converge onto the dominant
93    // subspace regardless of the start, so a pinned (rather than entropy) draw
94    // makes the whole randomized SVD reproducible run-to-run — which in turn
95    // pins every downstream consumer (binary-sketch collapse, layout, SVD fits)
96    // — without changing what subspace it recovers.
97    let mut qq = DMatrix::<T>::runif_seeded(nc, rank_and_oversample, RSVD_SUBSPACE_SEED);
98    let half = T::from(0.5).expect("no half found");
99    qq.iter_mut().for_each(|x| *x -= half);
100
101    // Each half-step re-orthonormalises the iterate with a thin QR. The
102    // basis must span exactly the range of the product it came from: a
103    // pivoted LU factor does not (its permutation is lost), and iterating on
104    // a row-permuted range does not converge onto the dominant subspace.
105    for _i in 0..max_iter {
106        let ll = xx.matmul(&qq).qr().q();
107        qq = xx.transpose_matmul(&ll).qr().q();
108    }
109
110    // let qq = DMatrix::<T>::runif(nc, rank_and_oversample);
111
112    let qr_q = xx.matmul(&qq).qr().q();
113    let kk = rank_and_oversample.min(qr_q.ncols());
114    let ret = qr_q.columns(0, kk).into_owned();
115
116    Ok(ret)
117}
118
119fn _randomized_svd<T, D>(
120    xx: &D,
121    max_rank: usize,
122) -> anyhow::Result<(DMatrix<T>, DVector<T>, DMatrix<T>)>
123where
124    T: nalgebra::RealField + num_traits::Float + Copy,
125    D: IntoDense<DMatrix<T>>,
126{
127    let nr = xx.num_rows();
128    let nc = xx.num_columns();
129
130    let mut rank = nr.min(nc);
131    let mut oversample = 0;
132
133    if max_rank > 0 && rank > max_rank {
134        rank = max_rank;
135        oversample = 5;
136    }
137
138    debug_assert!(rank > 0, "Must be at least rank = 1");
139
140    // Keep the oversampled basis through the projection: its columns are
141    // not ordered by singular value, so truncating here would discard part
142    // of the dominant subspace. The rank is applied to the small SVD below.
143    let qq = _subspace_iteration(xx, rank + oversample)?;
144    let rank = rank.min(qq.ncols());
145
146    // let bb = qq.transpose() * xx
147    let bb = xx.transpose_matmul(&qq).transpose();
148
149    let svd = bb.svd(true, true);
150
151    if let (Some(svd_u), Some(svd_vt)) = (svd.u, svd.v_t) {
152        return Ok((
153            qq.clone() * svd_u.columns(0, rank).into_owned(),
154            svd.singular_values.rows(0, rank).into_owned(),
155            svd_vt.transpose().columns(0, rank).into_owned(),
156        ));
157    }
158    Err(anyhow::anyhow!("randomized SVD failed"))
159}
160
161impl<T> RandomizedAlgs for DMatrix<T>
162where
163    T: nalgebra::RealField + num_traits::Float + Copy,
164{
165    type InMat = DMatrix<T>;
166    type OutMat = DMatrix<T>;
167    type DVec = DVector<T>;
168    type Scalar = T;
169
170    fn rsvd(&self, max_rank: usize) -> anyhow::Result<(Self::OutMat, Self::DVec, Self::OutMat)> {
171        _randomized_svd(self, max_rank)
172    }
173}
174
175impl<T> RandomizedAlgs for CscMatrix<T>
176where
177    T: nalgebra::RealField + num_traits::Float + Copy,
178{
179    type InMat = CscMatrix<T>;
180    type OutMat = DMatrix<T>;
181    type DVec = DVector<T>;
182    type Scalar = T;
183
184    fn rsvd(&self, max_rank: usize) -> anyhow::Result<(Self::OutMat, Self::DVec, Self::OutMat)> {
185        _randomized_svd(self, max_rank)
186    }
187}
188
189impl<T> RandomizedAlgs for CsrMatrix<T>
190where
191    T: nalgebra::RealField + num_traits::Float + Copy,
192{
193    type InMat = CsrMatrix<T>;
194    type OutMat = DMatrix<T>;
195    type DVec = DVector<T>;
196    type Scalar = T;
197
198    fn rsvd(&self, max_rank: usize) -> anyhow::Result<(Self::OutMat, Self::DVec, Self::OutMat)> {
199        _randomized_svd(self, max_rank)
200    }
201}
202
203#[cfg(test)]
204#[path = "dmatrix_rsvd_tests.rs"]
205mod tests;