sklears_kernel_approximation/sparse_gp/
kernels.rs1use scirs2_core::ndarray::{Array1, Array2};
7use sklears_core::error::{Result, SklearsError};
8use std::f64::consts::PI;
9
10pub trait SparseKernel: Clone + Send + Sync {
12 fn kernel_matrix(&self, x1: &Array2<f64>, x2: &Array2<f64>) -> Array2<f64>;
14
15 fn kernel_diagonal(&self, x: &Array2<f64>) -> Array1<f64>;
17
18 fn parameters(&self) -> Vec<f64>;
20
21 fn set_parameters(&mut self, params: &[f64]);
23
24 fn parameter_gradients(&self, x1: &Array2<f64>, x2: &Array2<f64>) -> Vec<Array2<f64>>;
26}
27
28#[derive(Debug, Clone)]
30pub struct RBFKernel {
31 pub length_scale: f64,
33
34 pub signal_variance: f64,
36}
37
38impl RBFKernel {
39 pub fn new(length_scale: f64, signal_variance: f64) -> Self {
41 Self {
42 length_scale,
43 signal_variance,
44 }
45 }
46
47 fn squared_distances(&self, x1: &Array2<f64>, x2: &Array2<f64>) -> Array2<f64> {
49 let n1 = x1.nrows();
50 let n2 = x2.nrows();
51 let mut distances = Array2::zeros((n1, n2));
52
53 for i in 0..n1 {
54 for j in 0..n2 {
55 let mut dist_sq = 0.0;
56 for k in 0..x1.ncols() {
57 let diff = x1[(i, k)] - x2[(j, k)];
58 dist_sq += diff * diff;
59 }
60 distances[(i, j)] = dist_sq;
61 }
62 }
63
64 distances
65 }
66
67 fn compute_rbf_matrix(&self, distances_sq: &Array2<f64>) -> Array2<f64> {
69 let scale_factor = -0.5 / (self.length_scale * self.length_scale);
70
71 distances_sq.mapv(|d| self.signal_variance * (scale_factor * d).exp())
73 }
74}
75
76impl SparseKernel for RBFKernel {
77 fn kernel_matrix(&self, x1: &Array2<f64>, x2: &Array2<f64>) -> Array2<f64> {
78 let distances_sq = self.squared_distances(x1, x2);
79 self.compute_rbf_matrix(&distances_sq)
80 }
81
82 fn kernel_diagonal(&self, x: &Array2<f64>) -> Array1<f64> {
83 Array1::from_elem(x.nrows(), self.signal_variance)
85 }
86
87 fn parameters(&self) -> Vec<f64> {
88 vec![self.length_scale, self.signal_variance]
89 }
90
91 fn set_parameters(&mut self, params: &[f64]) {
92 if params.len() >= 2 {
93 self.length_scale = params[0];
94 self.signal_variance = params[1];
95 }
96 }
97
98 fn parameter_gradients(&self, x1: &Array2<f64>, x2: &Array2<f64>) -> Vec<Array2<f64>> {
99 let distances_sq = self.squared_distances(x1, x2);
100 let k_matrix = self.compute_rbf_matrix(&distances_sq);
101
102 let scale_factor = -0.5 / (self.length_scale * self.length_scale);
103
104 let grad_length_scale = distances_sq.mapv(|d| {
106 let exp_term = (scale_factor * d).exp();
107 self.signal_variance * exp_term * d
108 / (self.length_scale * self.length_scale * self.length_scale)
109 });
110
111 let grad_signal_variance = k_matrix.mapv(|k| k / self.signal_variance);
113
114 vec![grad_length_scale, grad_signal_variance]
115 }
116}
117
118#[derive(Debug, Clone)]
120pub struct MaternKernel {
121 pub length_scale: f64,
123
124 pub signal_variance: f64,
126
127 pub nu: f64,
129}
130
131impl MaternKernel {
132 pub fn new(length_scale: f64, signal_variance: f64, nu: f64) -> Self {
134 Self {
135 length_scale,
136 signal_variance,
137 nu,
138 }
139 }
140
141 fn matern_kernel_value(&self, distance: f64) -> f64 {
143 if distance < 1e-8 {
144 return self.signal_variance;
145 }
146
147 let sqrt_2nu = (2.0 * self.nu).sqrt();
148 let scaled_distance = sqrt_2nu * distance / self.length_scale;
149
150 match self.nu {
151 nu if (nu - 0.5).abs() < 1e-8 => {
153 self.signal_variance * (-scaled_distance).exp()
155 }
156 nu if (nu - 1.5).abs() < 1e-8 => {
157 let term = 1.0 + scaled_distance;
159 self.signal_variance * term * (-scaled_distance).exp()
160 }
161 nu if (nu - 2.5).abs() < 1e-8 => {
162 let term = 1.0 + scaled_distance + scaled_distance * scaled_distance / 3.0;
164 self.signal_variance * term * (-scaled_distance).exp()
165 }
166 _ => {
167 let gamma_term = gamma_function(self.nu);
169 let bessel_term = modified_bessel_k(self.nu, scaled_distance);
170
171 self.signal_variance
172 * (1.0 / (gamma_term * 2_f64.powf(self.nu - 1.0)))
173 * scaled_distance.powf(self.nu)
174 * bessel_term
175 }
176 }
177 }
178}
179
180impl SparseKernel for MaternKernel {
181 fn kernel_matrix(&self, x1: &Array2<f64>, x2: &Array2<f64>) -> Array2<f64> {
182 let n1 = x1.nrows();
183 let n2 = x2.nrows();
184 let mut kernel_matrix = Array2::zeros((n1, n2));
185
186 for i in 0..n1 {
187 for j in 0..n2 {
188 let mut distance = 0.0;
189 for k in 0..x1.ncols() {
190 let diff = x1[(i, k)] - x2[(j, k)];
191 distance += diff * diff;
192 }
193 distance = distance.sqrt();
194 kernel_matrix[(i, j)] = self.matern_kernel_value(distance);
195 }
196 }
197
198 kernel_matrix
199 }
200
201 fn kernel_diagonal(&self, x: &Array2<f64>) -> Array1<f64> {
202 Array1::from_elem(x.nrows(), self.signal_variance)
203 }
204
205 fn parameters(&self) -> Vec<f64> {
206 vec![self.length_scale, self.signal_variance, self.nu]
207 }
208
209 fn set_parameters(&mut self, params: &[f64]) {
210 if params.len() >= 3 {
211 self.length_scale = params[0];
212 self.signal_variance = params[1];
213 self.nu = params[2];
214 }
215 }
216
217 fn parameter_gradients(&self, x1: &Array2<f64>, x2: &Array2<f64>) -> Vec<Array2<f64>> {
218 let n1 = x1.nrows();
220 let n2 = x2.nrows();
221
222 let grad_length_scale = Array2::zeros((n1, n2));
223 let grad_signal_variance = Array2::zeros((n1, n2));
224 let grad_nu = Array2::zeros((n1, n2));
225
226 vec![grad_length_scale, grad_signal_variance, grad_nu]
227 }
228}
229
230pub struct KernelOps;
232
233impl KernelOps {
234 pub fn cholesky_with_jitter(matrix: &Array2<f64>, jitter: f64) -> Result<Array2<f64>> {
236 let mut regularized = matrix.clone();
237
238 for i in 0..regularized.nrows() {
240 regularized[(i, i)] += jitter;
241 }
242
243 Self::cholesky_decomposition(®ularized)
245 }
246
247 fn cholesky_decomposition(matrix: &Array2<f64>) -> Result<Array2<f64>> {
249 let n = matrix.nrows();
250 let mut l = Array2::zeros((n, n));
251
252 for i in 0..n {
253 for j in 0..=i {
254 if i == j {
255 let mut sum = 0.0;
257 for k in 0..j {
258 sum += l[(j, k)] * l[(j, k)];
259 }
260 let val = matrix[(j, j)] - sum;
261 if val <= 0.0 {
262 return Err(SklearsError::NumericalError(
263 "Matrix not positive definite".to_string(),
264 ));
265 }
266 l[(j, j)] = val.sqrt();
267 } else {
268 let mut sum = 0.0;
270 for k in 0..j {
271 sum += l[(i, k)] * l[(j, k)];
272 }
273 l[(i, j)] = (matrix[(i, j)] - sum) / l[(j, j)];
274 }
275 }
276 }
277
278 Ok(l)
279 }
280
281 pub fn solve_triangular_lower(l: &Array2<f64>, b: &Array1<f64>) -> Array1<f64> {
283 let n = l.nrows();
284 let mut x = Array1::zeros(n);
285
286 for i in 0..n {
287 let mut sum = 0.0;
288 for j in 0..i {
289 sum += l[(i, j)] * x[j];
290 }
291 x[i] = (b[i] - sum) / l[(i, i)];
292 }
293
294 x
295 }
296
297 pub fn solve_triangular_upper(u: &Array2<f64>, b: &Array1<f64>) -> Array1<f64> {
299 let n = u.nrows();
300 let mut x = Array1::zeros(n);
301
302 for i in (0..n).rev() {
303 let mut sum = 0.0;
304 for j in (i + 1)..n {
305 sum += u[(i, j)] * x[j];
306 }
307 x[i] = (b[i] - sum) / u[(i, i)];
308 }
309
310 x
311 }
312
313 pub fn log_det_from_cholesky(l: &Array2<f64>) -> f64 {
315 2.0 * l.diag().mapv(|x| x.ln()).sum()
316 }
317
318 pub fn invert_using_cholesky(matrix: &Array2<f64>) -> Result<Array2<f64>> {
320 let l = Self::cholesky_with_jitter(matrix, 1e-6)?;
321 let n = matrix.nrows();
322 let mut inv = Array2::zeros((n, n));
323
324 for j in 0..n {
326 let mut e = Array1::zeros(n);
327 e[j] = 1.0;
328
329 let y = Self::solve_triangular_lower(&l, &e);
331
332 let lt = l.t().to_owned();
334 let x = Self::solve_triangular_upper(<, &y);
335
336 for i in 0..n {
337 inv[(i, j)] = x[i];
338 }
339 }
340
341 Ok(inv)
342 }
343}
344
345fn gamma_function(x: f64) -> f64 {
348 if (x - 0.5).abs() < 1e-8 {
349 PI.sqrt()
350 } else if (x - 1.0).abs() < 1e-8 {
351 1.0
352 } else if (x - 1.5).abs() < 1e-8 {
353 PI.sqrt() / 2.0
354 } else if (x - 2.0).abs() < 1e-8 {
355 1.0
356 } else if (x - 2.5).abs() < 1e-8 {
357 3.0 * PI.sqrt() / 4.0
358 } else {
359 (2.0 * PI / x).sqrt() * (x / std::f64::consts::E).powf(x)
361 }
362}
363
364fn modified_bessel_k(nu: f64, z: f64) -> f64 {
366 if z < 1e-8 {
367 return f64::INFINITY;
368 }
369
370 (PI / (2.0 * z)).sqrt() * (-z).exp() * (1.0 + (4.0 * nu * nu - 1.0) / (8.0 * z))
372}
373
374pub mod simd_kernel {
376 use super::*;
377
378 pub fn simd_rbf_kernel_matrix(
382 x1: &Array2<f64>,
383 x2: &Array2<f64>,
384 length_scale: f64,
385 signal_variance: f64,
386 ) -> Array2<f64> {
387 let kernel = RBFKernel::new(length_scale, signal_variance);
388 kernel.kernel_matrix(x1, x2)
389 }
390
391 pub fn simd_kernel_diagonal<K: SparseKernel>(kernel: &K, x: &Array2<f64>) -> Array1<f64> {
393 kernel.kernel_diagonal(x)
394 }
395
396 pub fn simd_squared_distances(x1: &Array2<f64>, x2: &Array2<f64>) -> Array2<f64> {
398 let n1 = x1.nrows();
399 let n2 = x2.nrows();
400 let mut distances = Array2::zeros((n1, n2));
401
402 for i in 0..n1 {
404 for j in 0..n2 {
405 let mut dist_sq = 0.0;
406 for k in 0..x1.ncols() {
407 let diff = x1[(i, k)] - x2[(j, k)];
408 dist_sq += diff * diff;
409 }
410 distances[(i, j)] = dist_sq;
411 }
412 }
413
414 distances
415 }
416}
417
418#[allow(non_snake_case)]
419#[cfg(test)]
420mod tests {
421 use super::*;
422 use scirs2_core::ndarray::array;
423
424 #[test]
425 fn test_rbf_kernel() {
426 let kernel = RBFKernel::new(1.0, 1.0);
427 let x1 = array![[0.0, 0.0], [1.0, 1.0]];
428 let x2 = array![[0.0, 0.0], [2.0, 2.0]];
429
430 let k = kernel.kernel_matrix(&x1, &x2);
431 assert_eq!(k.shape(), &[2, 2]);
432 assert!((k[(0, 0)] - 1.0).abs() < 1e-10);
433 assert!(k[(0, 1)] < 1.0);
434 }
435
436 #[test]
437 fn test_kernel_parameters() {
438 let mut kernel = RBFKernel::new(1.0, 1.0);
439 let params = kernel.parameters();
440 assert_eq!(params, vec![1.0, 1.0]);
441
442 kernel.set_parameters(&[2.0, 0.5]);
443 assert_eq!(kernel.length_scale, 2.0);
444 assert_eq!(kernel.signal_variance, 0.5);
445 }
446
447 #[test]
448 fn test_cholesky_decomposition() {
449 let matrix = array![[4.0, 2.0], [2.0, 3.0]];
450 let l = KernelOps::cholesky_with_jitter(&matrix, 0.0).unwrap();
451
452 let reconstructed = l.dot(&l.t());
454 for i in 0..2 {
455 for j in 0..2 {
456 assert!((reconstructed[(i, j)] - matrix[(i, j)]).abs() < 1e-10);
457 }
458 }
459 }
460
461 #[test]
462 fn test_triangular_solve() {
463 let l = array![[2.0, 0.0], [1.0, 1.5]];
464 let b = array![4.0, 3.0];
465
466 let x = KernelOps::solve_triangular_lower(&l, &b);
467 let reconstructed = l.dot(&x);
468
469 for i in 0..2 {
470 assert!((reconstructed[i] - b[i]).abs() < 1e-10);
471 }
472 }
473}