Train an OPQ codec using the Non-Para OPQ algorithm. Alternates between a codebook step (Lloyd's k-means on the rotated training set) and a Procrustes step (SVD-based rotation update to minimize reconstruction error) for `opq_iters` iterations. - `opq_iters`: number of alternating Procrustes+codebook iterations. - `kmeans_iters`: Lloyd's k-means iterations per subspace per OPQ iter.
(
vectors: &[&[f32]],
dim: usize,
m: usize,
k: usize,
opq_iters: usize,
kmeans_iters: usize,
)
| 72 | /// - `opq_iters`: number of alternating Procrustes+codebook iterations. |
| 73 | /// - `kmeans_iters`: Lloyd's k-means iterations per subspace per OPQ iter. |
| 74 | pub fn train( |
| 75 | vectors: &[&[f32]], |
| 76 | dim: usize, |
| 77 | m: usize, |
| 78 | k: usize, |
| 79 | opq_iters: usize, |
| 80 | kmeans_iters: usize, |
| 81 | ) -> Self { |
| 82 | assert!(!vectors.is_empty(), "training set must be non-empty"); |
| 83 | assert!(dim > 0 && m > 0 && k > 0, "dim/m/k must be positive"); |
| 84 | assert!( |
| 85 | dim.is_multiple_of(m), |
| 86 | "dim ({dim}) must be divisible by m ({m})" |
| 87 | ); |
| 88 | let sub_dim = dim / m; |
| 89 | let seed = dim as u64 ^ ((m as u64) << 16) ^ ((k as u64) << 32); |
| 90 | |
| 91 | let mut rotation = identity(dim); |
| 92 | let mut codebooks: Vec<Vec<Vec<f32>>> = Vec::new(); |
| 93 | |
| 94 | let iters = opq_iters.max(1); |
| 95 | |
| 96 | for iter in 0..iters { |
| 97 | // Codebook step: train PQ on the current rotated training set. |
| 98 | let rotated: Vec<Vec<f32>> = |
| 99 | vectors.iter().map(|v| matvec(&rotation, v, dim)).collect(); |
| 100 | codebooks = train_codebooks(&rotated, m, k, sub_dim, kmeans_iters, seed ^ iter as u64); |
| 101 | |
| 102 | // Procrustes step: find R minimising ‖R·X - Y‖_F where Y is |
| 103 | // the dequantized reconstruction of R·X. |
| 104 | // |
| 105 | // Closed-form solution (Ge et al. CVPR 2013, §3.2): |
| 106 | // M = X · Yᵀ (dim × dim) |
| 107 | // SVD(M) = U Σ Vᵀ |
| 108 | // R_new = V · Uᵀ |
| 109 | // |
| 110 | // Skip rotation update on the last iteration — codebooks were |
| 111 | // already retrained with the current R. |
| 112 | if iter + 1 < iters { |
| 113 | let n = vectors.len(); |
| 114 | // Build dim×N matrices X (original) and Y (reconstructed). |
| 115 | // DMatrix is column-major; we store column j = vector j. |
| 116 | let x_mat = DMatrix::from_fn(dim, n, |row, col| vectors[col][row]); |
| 117 | let y_mat = { |
| 118 | let recon: Vec<Vec<f32>> = rotated |
| 119 | .iter() |
| 120 | .map(|rv| { |
| 121 | let codes = pq_encode(rv, &codebooks, m, sub_dim); |
| 122 | dequantize_codes(&codes, &codebooks) |
| 123 | }) |
| 124 | .collect(); |
| 125 | DMatrix::from_fn(dim, n, |row, col| recon[col][row]) |
| 126 | }; |
| 127 | |
| 128 | // M = X · Yᵀ (dim × dim) |
| 129 | let m_mat = &x_mat * y_mat.transpose(); |
| 130 | |
| 131 | // Guard: skip rotation update if M contains NaN (degenerate |
nothing calls this directly
no test coverage detected