1use scirs2_core::ndarray_ext::{Array1, Array2, ArrayView1, ArrayView2, Axis};
8use scirs2_core::random::Random;
9use sklears_core::error::{Result, SklearsError};
11use sklears_core::traits::{Fit, Predict, PredictProba};
12use thiserror::Error;
13
14#[derive(Error, Debug)]
15pub enum DeepGaussianProcessError {
16 #[error("Invalid number of layers: {0}")]
17 InvalidNumLayers(usize),
18 #[error("Invalid inducing points: {0}")]
19 InvalidInducingPoints(usize),
20 #[error("Invalid kernel parameter: {0}")]
21 InvalidKernelParameter(f64),
22 #[error("Invalid learning rate: {0}")]
23 InvalidLearningRate(f64),
24 #[error("Invalid epochs: {0}")]
25 InvalidEpochs(usize),
26 #[error("Insufficient labeled samples: need at least 1")]
27 InsufficientLabeledSamples,
28 #[error("Shape mismatch: expected {expected:?}, got {actual:?}")]
29 ShapeMismatch {
30 expected: Vec<usize>,
31 actual: Vec<usize>,
32 },
33 #[error("Training failed: {0}")]
34 TrainingFailed(String),
35 #[error("Model not trained")]
36 ModelNotTrained,
37 #[error("Matrix operation failed: {0}")]
38 MatrixOperationFailed(String),
39}
40
41impl From<DeepGaussianProcessError> for SklearsError {
42 fn from(err: DeepGaussianProcessError) -> Self {
43 SklearsError::FitError(err.to_string())
44 }
45}
46
47#[derive(Debug, Clone)]
49pub enum KernelType {
50 RBF { length_scale: f64, variance: f64 },
52 Matern32 { length_scale: f64, variance: f64 },
54 Matern52 { length_scale: f64, variance: f64 },
56 Linear { variance: f64, offset: f64 },
58}
59
60impl KernelType {
61 fn compute(&self, x1: &ArrayView1<f64>, x2: &ArrayView1<f64>) -> f64 {
62 match self {
63 KernelType::RBF {
64 length_scale,
65 variance,
66 } => {
67 let squared_distance = x1
68 .iter()
69 .zip(x2.iter())
70 .map(|(a, b)| (a - b).powi(2))
71 .sum::<f64>();
72 variance * (-0.5 * squared_distance / length_scale.powi(2)).exp()
73 }
74 KernelType::Matern32 {
75 length_scale,
76 variance,
77 } => {
78 let distance = x1
79 .iter()
80 .zip(x2.iter())
81 .map(|(a, b)| (a - b).powi(2))
82 .sum::<f64>()
83 .sqrt();
84 let scaled_distance = (3.0_f64.sqrt() * distance) / length_scale;
85 variance * (1.0 + scaled_distance) * (-scaled_distance).exp()
86 }
87 KernelType::Matern52 {
88 length_scale,
89 variance,
90 } => {
91 let distance = x1
92 .iter()
93 .zip(x2.iter())
94 .map(|(a, b)| (a - b).powi(2))
95 .sum::<f64>()
96 .sqrt();
97 let scaled_distance = (5.0_f64.sqrt() * distance) / length_scale;
98 variance
99 * (1.0
100 + scaled_distance
101 + (5.0 * distance.powi(2)) / (3.0 * length_scale.powi(2)))
102 * (-scaled_distance).exp()
103 }
104 KernelType::Linear { variance, offset } => {
105 let dot_product = x1.iter().zip(x2.iter()).map(|(a, b)| a * b).sum::<f64>();
106 variance * (dot_product + offset)
107 }
108 }
109 }
110
111 #[allow(non_snake_case)] fn compute_matrix(&self, X1: &ArrayView2<f64>, X2: &ArrayView2<f64>) -> Array2<f64> {
113 let (n1, _) = X1.dim();
114 let (n2, _) = X2.dim();
115 let mut K = Array2::zeros((n1, n2));
116
117 for i in 0..n1 {
118 for j in 0..n2 {
119 K[[i, j]] = self.compute(&X1.row(i), &X2.row(j));
120 }
121 }
122
123 K
124 }
125}
126
127#[derive(Debug, Clone)]
129pub struct GaussianProcessLayer {
130 pub input_dim: usize,
132 pub output_dim: usize,
134 pub num_inducing: usize,
136 pub kernel: KernelType,
138 pub noise_variance: f64,
140 pub learning_rate: f64,
142 inducing_points: Array2<f64>,
143 mean_function: Array1<f64>,
144 is_trained: bool,
145}
146
147impl GaussianProcessLayer {
148 pub fn new(input_dim: usize, output_dim: usize, num_inducing: usize) -> Self {
149 Self {
150 input_dim,
151 output_dim,
152 num_inducing,
153 kernel: KernelType::RBF {
154 length_scale: 1.0,
155 variance: 1.0,
156 },
157 noise_variance: 0.1,
158 learning_rate: 0.01,
159 inducing_points: Array2::zeros((num_inducing, input_dim)),
160 mean_function: Array1::zeros(output_dim),
161 is_trained: false,
162 }
163 }
164
165 pub fn kernel(mut self, kernel: KernelType) -> Self {
166 self.kernel = kernel;
167 self
168 }
169
170 pub fn noise_variance(mut self, noise_variance: f64) -> Result<Self> {
171 if noise_variance <= 0.0 {
172 return Err(DeepGaussianProcessError::InvalidKernelParameter(noise_variance).into());
173 }
174 self.noise_variance = noise_variance;
175 Ok(self)
176 }
177
178 pub fn learning_rate(mut self, learning_rate: f64) -> Result<Self> {
179 if learning_rate <= 0.0 {
180 return Err(DeepGaussianProcessError::InvalidLearningRate(learning_rate).into());
181 }
182 self.learning_rate = learning_rate;
183 Ok(self)
184 }
185
186 #[allow(non_snake_case)] fn initialize_inducing_points(&mut self, X: &ArrayView2<f64>, random_state: Option<u64>) {
188 let (n_samples, _) = X.dim();
189 let mut rng = match random_state {
190 Some(seed) => Random::seed(seed),
191 None => Random::seed(42),
192 };
193
194 let num_selected = self.num_inducing.min(n_samples);
196 let mut selected_indices = Vec::new();
197 for _ in 0..num_selected {
198 let idx = rng.gen_range(0..n_samples);
199 if !selected_indices.contains(&idx) {
200 selected_indices.push(idx);
201 }
202 }
203
204 for (i, &idx) in selected_indices.iter().enumerate() {
205 self.inducing_points.row_mut(i).assign(&X.row(idx));
206 }
207 }
208
209 fn add_jitter(&self, matrix: &mut Array2<f64>, jitter: f64) {
210 for i in 0..matrix.nrows() {
211 matrix[[i, i]] += jitter;
212 }
213 }
214
215 #[allow(non_snake_case)] fn safe_cholesky(&self, matrix: &Array2<f64>) -> Result<Array2<f64>> {
217 let mut A = matrix.clone();
218 let jitter = 1e-6;
219 self.add_jitter(&mut A, jitter);
220
221 let n = A.nrows();
223 let mut L: Array2<f64> = Array2::zeros((n, n));
224
225 for i in 0..n {
226 for j in 0..=i {
227 if i == j {
228 let sum: f64 = (0..j).map(|k| L[[i, k]].powi(2)).sum();
229 let val = A[[i, i]] - sum;
230 if val <= 0.0 {
231 return Err(DeepGaussianProcessError::MatrixOperationFailed(
232 "Matrix is not positive definite".to_string(),
233 )
234 .into());
235 }
236 L[[i, j]] = val.sqrt();
237 } else {
238 let sum: f64 = (0..j).map(|k| L[[i, k]] * L[[j, k]]).sum();
239 L[[i, j]] = (A[[i, j]] - sum) / L[[j, j]];
240 }
241 }
242 }
243
244 Ok(L)
245 }
246
247 #[allow(non_snake_case)] fn solve_triangular_lower(&self, L: &Array2<f64>, b: &Array1<f64>) -> Array1<f64> {
249 let n = L.nrows();
250 let mut x = Array1::zeros(n);
251
252 for i in 0..n {
253 let sum: f64 = (0..i).map(|j| L[[i, j]] * x[j]).sum();
254 x[i] = (b[i] - sum) / L[[i, i]];
255 }
256
257 x
258 }
259
260 #[allow(non_snake_case)] fn solve_triangular_upper(&self, U: &Array2<f64>, b: &Array1<f64>) -> Array1<f64> {
262 let n = U.nrows();
263 let mut x = Array1::zeros(n);
264
265 for i in (0..n).rev() {
266 let sum: f64 = ((i + 1)..n).map(|j| U[[i, j]] * x[j]).sum();
267 x[i] = (b[i] - sum) / U[[i, i]];
268 }
269
270 x
271 }
272
273 #[allow(non_snake_case)]
274 pub fn fit(
275 &mut self,
276 X: &ArrayView2<f64>,
277 y: &ArrayView2<f64>,
278 random_state: Option<u64>,
279 ) -> Result<()> {
280 let (n_samples, n_features) = X.dim();
281 let (n_targets, output_dim) = y.dim();
282
283 if n_features != self.input_dim {
284 return Err(DeepGaussianProcessError::ShapeMismatch {
285 expected: vec![n_samples, self.input_dim],
286 actual: vec![n_samples, n_features],
287 }
288 .into());
289 }
290
291 if n_samples != n_targets || output_dim != self.output_dim {
292 return Err(DeepGaussianProcessError::ShapeMismatch {
293 expected: vec![n_samples, self.output_dim],
294 actual: vec![n_targets, output_dim],
295 }
296 .into());
297 }
298
299 self.initialize_inducing_points(X, random_state);
300
301 let K_uu = self
303 .kernel
304 .compute_matrix(&self.inducing_points.view(), &self.inducing_points.view());
305 let K_uf = self.kernel.compute_matrix(&self.inducing_points.view(), X);
306
307 let mut K_uu_noisy = K_uu.clone();
309 self.add_jitter(&mut K_uu_noisy, self.noise_variance);
310
311 let L_uu = self.safe_cholesky(&K_uu_noisy)?;
313
314 for d in 0..self.output_dim {
316 let y_d = y.column(d);
317
318 let K_uf_y = K_uf.dot(&y_d);
320 let alpha = self.solve_triangular_lower(&L_uu, &K_uf_y);
321 let L_uu_t = L_uu.t().to_owned();
322 let solution = self.solve_triangular_upper(&L_uu_t, &alpha);
323
324 self.mean_function[d] = solution.mean().unwrap_or(0.0);
326 }
327
328 self.is_trained = true;
329 Ok(())
330 }
331
332 #[allow(non_snake_case)]
333 pub fn predict(&self, X: &ArrayView2<f64>) -> Result<Array2<f64>> {
334 if !self.is_trained {
335 return Err(DeepGaussianProcessError::ModelNotTrained.into());
336 }
337
338 let (n_test, n_features) = X.dim();
339 if n_features != self.input_dim {
340 return Err(DeepGaussianProcessError::ShapeMismatch {
341 expected: vec![n_test, self.input_dim],
342 actual: vec![n_test, n_features],
343 }
344 .into());
345 }
346
347 let K_su = self.kernel.compute_matrix(X, &self.inducing_points.view());
349
350 let mut predictions = Array2::zeros((n_test, self.output_dim));
351
352 for i in 0..n_test {
354 for d in 0..self.output_dim {
355 let weights: f64 = K_su.row(i).sum();
357 if weights > 0.0 {
358 predictions[[i, d]] =
359 self.mean_function[d] * (weights / self.num_inducing as f64);
360 } else {
361 predictions[[i, d]] = self.mean_function[d];
362 }
363 }
364 }
365
366 Ok(predictions)
367 }
368
369 #[allow(non_snake_case)] pub fn predict_with_uncertainty(
371 &self,
372 X: &ArrayView2<f64>,
373 ) -> Result<(Array2<f64>, Array2<f64>)> {
374 let predictions = self.predict(X)?;
375 let (n_test, _) = X.dim();
376
377 let mut uncertainties = Array2::zeros((n_test, self.output_dim));
379 for i in 0..n_test {
380 for d in 0..self.output_dim {
381 uncertainties[[i, d]] = self.noise_variance.sqrt();
382 }
383 }
384
385 Ok((predictions, uncertainties))
386 }
387}
388
389#[derive(Debug, Clone)]
395pub struct DeepGaussianProcess {
396 pub layer_dims: Vec<usize>,
398 pub num_inducing: usize,
400 pub epochs: usize,
402 pub learning_rate: f64,
404 pub noise_variance: f64,
406 pub random_state: Option<u64>,
408 layers: Vec<GaussianProcessLayer>,
409 n_classes: usize,
410 is_trained: bool,
411}
412
413impl Default for DeepGaussianProcess {
414 fn default() -> Self {
415 Self {
416 layer_dims: vec![10, 5, 2],
417 num_inducing: 20,
418 epochs: 50,
419 learning_rate: 0.01,
420 noise_variance: 0.1,
421 random_state: None,
422 layers: Vec::new(),
423 n_classes: 0,
424 is_trained: false,
425 }
426 }
427}
428
429impl DeepGaussianProcess {
430 pub fn new() -> Self {
431 Self::default()
432 }
433
434 pub fn layer_dims(mut self, layer_dims: Vec<usize>) -> Result<Self> {
435 if layer_dims.len() < 2 {
436 return Err(DeepGaussianProcessError::InvalidNumLayers(layer_dims.len()).into());
437 }
438 self.layer_dims = layer_dims;
439 Ok(self)
440 }
441
442 pub fn num_inducing(mut self, num_inducing: usize) -> Result<Self> {
443 if num_inducing == 0 {
444 return Err(DeepGaussianProcessError::InvalidInducingPoints(num_inducing).into());
445 }
446 self.num_inducing = num_inducing;
447 Ok(self)
448 }
449
450 pub fn epochs(mut self, epochs: usize) -> Result<Self> {
451 if epochs == 0 {
452 return Err(DeepGaussianProcessError::InvalidEpochs(epochs).into());
453 }
454 self.epochs = epochs;
455 Ok(self)
456 }
457
458 pub fn learning_rate(mut self, learning_rate: f64) -> Result<Self> {
459 if learning_rate <= 0.0 {
460 return Err(DeepGaussianProcessError::InvalidLearningRate(learning_rate).into());
461 }
462 self.learning_rate = learning_rate;
463 Ok(self)
464 }
465
466 pub fn noise_variance(mut self, noise_variance: f64) -> Result<Self> {
467 if noise_variance <= 0.0 {
468 return Err(DeepGaussianProcessError::InvalidKernelParameter(noise_variance).into());
469 }
470 self.noise_variance = noise_variance;
471 Ok(self)
472 }
473
474 pub fn random_state(mut self, random_state: u64) -> Self {
475 self.random_state = Some(random_state);
476 self
477 }
478
479 fn initialize_layers(&mut self) {
480 self.layers.clear();
481
482 for i in 0..self.layer_dims.len() - 1 {
483 let layer = GaussianProcessLayer::new(
484 self.layer_dims[i],
485 self.layer_dims[i + 1],
486 self.num_inducing,
487 )
488 .learning_rate(self.learning_rate)
489 .expect("operation should succeed")
490 .noise_variance(self.noise_variance)
491 .expect("operation should succeed");
492
493 self.layers.push(layer);
494 }
495 }
496
497 #[allow(non_snake_case)] fn forward_pass(&self, X: &ArrayView2<f64>) -> Result<Array2<f64>> {
499 let mut current_data = X.to_owned();
500
501 for layer in self.layers.iter() {
502 current_data = layer.predict(¤t_data.view())?;
503 }
504
505 Ok(current_data)
506 }
507
508 #[allow(non_snake_case)] fn softmax(&self, X: &ArrayView2<f64>) -> Array2<f64> {
510 let mut result = X.to_owned();
511
512 for mut row in result.rows_mut() {
513 let max_val = row.fold(f64::NEG_INFINITY, |a, &b| a.max(b));
514 for val in row.iter_mut() {
515 *val = (*val - max_val).exp();
516 }
517 let sum: f64 = row.sum();
518 if sum > 0.0 {
519 for val in row.iter_mut() {
520 *val /= sum;
521 }
522 }
523 }
524
525 result
526 }
527
528 fn encode_labels(&self, y: &ArrayView1<i32>) -> Array2<f64> {
529 let n_samples = y.len();
530 let mut encoded = Array2::zeros((n_samples, self.n_classes));
531
532 for (i, &label) in y.iter().enumerate() {
533 if label >= 0 {
534 let class_idx = label as usize;
535 if class_idx < self.n_classes {
536 encoded[[i, class_idx]] = 1.0;
537 }
538 }
539 }
540
541 encoded
542 }
543
544 #[allow(non_snake_case)] pub fn fit(&mut self, X: &ArrayView2<f64>, y: &ArrayView1<i32>) -> Result<()> {
546 let (n_samples, n_features) = X.dim();
547
548 if y.len() != n_samples {
549 return Err(DeepGaussianProcessError::ShapeMismatch {
550 expected: vec![n_samples],
551 actual: vec![y.len()],
552 }
553 .into());
554 }
555
556 let labeled_mask: Vec<bool> = y.iter().map(|&label| label >= 0).collect();
558 let n_labeled = labeled_mask.iter().filter(|&&x| x).count();
559
560 if n_labeled == 0 {
561 return Err(DeepGaussianProcessError::InsufficientLabeledSamples.into());
562 }
563
564 let mut classes: Vec<i32> = y.iter().filter(|&&label| label >= 0).cloned().collect();
566 classes.sort_unstable();
567 classes.dedup();
568 self.n_classes = classes.len();
569
570 if self.layer_dims.is_empty() {
572 self.layer_dims = vec![n_features, n_features / 2, self.n_classes];
573 } else {
574 self.layer_dims[0] = n_features;
575 if self.layer_dims.len() > 1 {
576 *self
577 .layer_dims
578 .last_mut()
579 .expect("operation should succeed") = self.n_classes;
580 }
581 }
582
583 self.initialize_layers();
584
585 let y_encoded = self.encode_labels(y);
587
588 let mut current_data = X.to_owned();
590 let num_layers = self.layers.len();
591
592 for layer_idx in 0..num_layers {
593 let seed = self.random_state.map(|s| s + layer_idx as u64);
594
595 if layer_idx == num_layers - 1 {
596 let labeled_indices: Vec<usize> = labeled_mask
598 .iter()
599 .enumerate()
600 .filter(|(_, &is_labeled)| is_labeled)
601 .map(|(i, _)| i)
602 .collect();
603
604 if !labeled_indices.is_empty() {
605 let X_labeled = labeled_indices
606 .iter()
607 .map(|&i| current_data.row(i))
608 .collect::<Vec<_>>();
609 let y_labeled = labeled_indices
610 .iter()
611 .map(|&i| y_encoded.row(i))
612 .collect::<Vec<_>>();
613
614 let X_labeled_array =
616 Array2::from_shape_fn((X_labeled.len(), current_data.ncols()), |(i, j)| {
617 X_labeled[i][j]
618 });
619 let y_labeled_array =
620 Array2::from_shape_fn((y_labeled.len(), y_encoded.ncols()), |(i, j)| {
621 y_labeled[i][j]
622 });
623
624 self.layers[layer_idx].fit(
625 &X_labeled_array.view(),
626 &y_labeled_array.view(),
627 seed,
628 )?;
629 }
630 } else {
631 let target_dim = self.layers[layer_idx].output_dim;
634 let identity_target =
635 Array2::from_shape_fn((current_data.nrows(), target_dim), |(i, j)| {
636 if j < current_data.ncols() {
637 current_data[[i, j]]
638 } else {
639 0.0
640 }
641 });
642
643 self.layers[layer_idx].fit(¤t_data.view(), &identity_target.view(), seed)?;
644 }
645
646 if layer_idx < num_layers - 1 {
648 current_data = self.layers[layer_idx].predict(¤t_data.view())?;
649 }
650 }
651
652 self.is_trained = true;
653 Ok(())
654 }
655
656 #[allow(non_snake_case)] pub fn predict_proba(&self, X: &ArrayView2<f64>) -> Result<Array2<f64>> {
658 if !self.is_trained {
659 return Err(DeepGaussianProcessError::ModelNotTrained.into());
660 }
661
662 let logits = self.forward_pass(X)?;
663 Ok(self.softmax(&logits.view()))
664 }
665
666 #[allow(non_snake_case)] pub fn predict(&self, X: &ArrayView2<f64>) -> Result<Array1<i32>> {
668 let probabilities = self.predict_proba(X)?;
669 let predictions = probabilities.map_axis(Axis(1), |row| {
670 row.iter()
671 .enumerate()
672 .max_by(|(_, a), (_, b)| a.partial_cmp(b).expect("operation should succeed"))
673 .map(|(idx, _)| idx as i32)
674 .expect("operation should succeed")
675 });
676 Ok(predictions)
677 }
678
679 #[allow(non_snake_case)] pub fn predict_with_uncertainty(
681 &self,
682 X: &ArrayView2<f64>,
683 ) -> Result<(Array1<i32>, Array2<f64>)> {
684 if !self.is_trained {
685 return Err(DeepGaussianProcessError::ModelNotTrained.into());
686 }
687
688 let mut current_data = X.to_owned();
690
691 for layer in self.layers.iter().take(self.layers.len() - 1) {
693 current_data = layer.predict(¤t_data.view())?;
694 }
695
696 if let Some(last_layer) = self.layers.last() {
698 let (logits, uncertainties) =
699 last_layer.predict_with_uncertainty(¤t_data.view())?;
700 let probabilities = self.softmax(&logits.view());
701 let predictions = probabilities.map_axis(Axis(1), |row| {
702 row.iter()
703 .enumerate()
704 .max_by(|(_, a), (_, b)| a.partial_cmp(b).expect("operation should succeed"))
705 .map(|(idx, _)| idx as i32)
706 .expect("operation should succeed")
707 });
708
709 Ok((predictions, uncertainties))
710 } else {
711 Err(DeepGaussianProcessError::ModelNotTrained.into())
712 }
713 }
714}
715
716#[derive(Debug, Clone)]
717pub struct FittedDeepGaussianProcess {
718 model: DeepGaussianProcess,
719}
720
721impl Fit<ArrayView2<'_, f64>, ArrayView1<'_, i32>, FittedDeepGaussianProcess>
722 for DeepGaussianProcess
723{
724 type Fitted = FittedDeepGaussianProcess;
725
726 #[allow(non_snake_case)] fn fit(
728 mut self,
729 X: &ArrayView2<'_, f64>,
730 y: &ArrayView1<'_, i32>,
731 ) -> Result<FittedDeepGaussianProcess> {
732 DeepGaussianProcess::fit(&mut self, X, y)?;
733 Ok(FittedDeepGaussianProcess { model: self })
734 }
735}
736
737impl Predict<ArrayView2<'_, f64>, Array1<i32>> for FittedDeepGaussianProcess {
738 #[allow(non_snake_case)] fn predict(&self, X: &ArrayView2<'_, f64>) -> Result<Array1<i32>> {
740 self.model.predict(X)
741 }
742}
743
744impl PredictProba<ArrayView2<'_, f64>, Array2<f64>> for FittedDeepGaussianProcess {
745 #[allow(non_snake_case)] fn predict_proba(&self, X: &ArrayView2<'_, f64>) -> Result<Array2<f64>> {
747 self.model.predict_proba(X)
748 }
749}
750
751#[allow(non_snake_case)]
752#[cfg(test)]
753mod tests {
754 use super::*;
755 use approx::assert_abs_diff_eq;
756 use scirs2_core::array;
757
758 #[test]
759 fn test_kernel_computation() {
760 let kernel = KernelType::RBF {
761 length_scale: 1.0,
762 variance: 1.0,
763 };
764 let x1 = array![1.0, 2.0];
765 let x2 = array![1.0, 2.0];
766
767 let result = kernel.compute(&x1.view(), &x2.view());
768 assert_abs_diff_eq!(result, 1.0, epsilon = 1e-10);
769
770 let x3 = array![3.0, 4.0];
771 let result2 = kernel.compute(&x1.view(), &x3.view());
772 assert!(result2 < 1.0 && result2 > 0.0);
773 }
774
775 #[test]
776 fn test_gaussian_process_layer_creation() {
777 let layer = GaussianProcessLayer::new(4, 2, 10)
778 .learning_rate(0.01)
779 .expect("operation should succeed")
780 .noise_variance(0.1)
781 .expect("operation should succeed");
782
783 assert_eq!(layer.input_dim, 4);
784 assert_eq!(layer.output_dim, 2);
785 assert_eq!(layer.num_inducing, 10);
786 assert_eq!(layer.learning_rate, 0.01);
787 assert_eq!(layer.noise_variance, 0.1);
788 }
789
790 #[test]
791 #[allow(non_snake_case)]
792 fn test_gaussian_process_layer_fit_predict() {
793 let mut layer = GaussianProcessLayer::new(2, 1, 5)
794 .learning_rate(0.01)
795 .expect("operation should succeed")
796 .noise_variance(0.1)
797 .expect("operation should succeed");
798
799 let X = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0], [4.0, 5.0]];
800 let y = array![[1.0], [2.0], [3.0], [4.0]];
801
802 layer
803 .fit(&X.view(), &y.view(), Some(42))
804 .expect("operation should succeed");
805 assert!(layer.is_trained);
806
807 let predictions = layer.predict(&X.view()).expect("operation should succeed");
808 assert_eq!(predictions.dim(), (4, 1));
809
810 let (pred_mean, pred_var) = layer
811 .predict_with_uncertainty(&X.view())
812 .expect("operation should succeed");
813 assert_eq!(pred_mean.dim(), (4, 1));
814 assert_eq!(pred_var.dim(), (4, 1));
815 }
816
817 #[test]
818 fn test_deep_gaussian_process_creation() {
819 let dgp = DeepGaussianProcess::new()
820 .layer_dims(vec![4, 3, 2])
821 .expect("operation should succeed")
822 .num_inducing(10)
823 .expect("operation should succeed")
824 .epochs(20)
825 .expect("operation should succeed")
826 .learning_rate(0.01)
827 .expect("operation should succeed")
828 .noise_variance(0.1)
829 .expect("operation should succeed")
830 .random_state(42);
831
832 assert_eq!(dgp.layer_dims, vec![4, 3, 2]);
833 assert_eq!(dgp.num_inducing, 10);
834 assert_eq!(dgp.epochs, 20);
835 assert_eq!(dgp.learning_rate, 0.01);
836 assert_eq!(dgp.noise_variance, 0.1);
837 assert_eq!(dgp.random_state, Some(42));
838 }
839
840 #[test]
841 #[allow(non_snake_case)]
842 fn test_deep_gaussian_process_fit_predict() {
843 let dgp = DeepGaussianProcess::new()
844 .layer_dims(vec![2, 3, 2])
845 .expect("operation should succeed")
846 .num_inducing(5)
847 .expect("operation should succeed")
848 .epochs(10)
849 .expect("operation should succeed")
850 .random_state(42);
851
852 let X = array![
853 [1.0, 2.0],
854 [2.0, 3.0],
855 [3.0, 4.0],
856 [4.0, 5.0],
857 [5.0, 6.0],
858 [6.0, 7.0]
859 ];
860 let y = array![0, 1, 0, 1, -1, -1]; let fitted = dgp
863 .fit(&X.view(), &y.view())
864 .expect("operation should succeed");
865
866 let predictions = fitted.predict(&X.view()).expect("operation should succeed");
867 assert_eq!(predictions.len(), 6);
868
869 let probabilities = fitted
870 .predict_proba(&X.view())
871 .expect("operation should succeed");
872 assert_eq!(probabilities.dim(), (6, 2));
873
874 for i in 0..6 {
876 let sum: f64 = probabilities.row(i).sum();
877 assert!((sum - 1.0).abs() < 1e-5); }
879 }
880
881 #[test]
882 #[allow(non_snake_case)]
883 fn test_deep_gaussian_process_with_uncertainty() {
884 let dgp = DeepGaussianProcess::new()
885 .layer_dims(vec![2, 2])
886 .expect("operation should succeed")
887 .num_inducing(3)
888 .expect("operation should succeed")
889 .epochs(5)
890 .expect("operation should succeed")
891 .random_state(42);
892
893 let X = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0]];
894 let y = array![0, 1, 0];
895
896 let fitted = dgp
897 .fit(&X.view(), &y.view())
898 .expect("operation should succeed");
899
900 let (predictions, uncertainties) = fitted
901 .model
902 .predict_with_uncertainty(&X.view())
903 .expect("operation should succeed");
904 assert_eq!(predictions.len(), 3);
905 assert_eq!(uncertainties.dim(), (3, 2));
906
907 for value in uncertainties.iter() {
909 assert!(*value >= 0.0);
910 }
911 }
912
913 #[test]
914 fn test_deep_gaussian_process_invalid_parameters() {
915 assert!(DeepGaussianProcess::new().layer_dims(vec![]).is_err());
916 assert!(DeepGaussianProcess::new().layer_dims(vec![10]).is_err());
917 assert!(DeepGaussianProcess::new().num_inducing(0).is_err());
918 assert!(DeepGaussianProcess::new().learning_rate(0.0).is_err());
919 assert!(DeepGaussianProcess::new().learning_rate(-0.1).is_err());
920 assert!(DeepGaussianProcess::new().noise_variance(0.0).is_err());
921 assert!(DeepGaussianProcess::new().epochs(0).is_err());
922 }
923
924 #[test]
925 #[allow(non_snake_case)]
926 fn test_deep_gaussian_process_insufficient_labeled_samples() {
927 let dgp = DeepGaussianProcess::new()
928 .layer_dims(vec![2, 2])
929 .expect("operation should succeed")
930 .epochs(5)
931 .expect("operation should succeed");
932
933 let X = array![[1.0, 2.0], [2.0, 3.0]];
934 let y = array![-1, -1]; let result = dgp.fit(&X.view(), &y.view());
937 assert!(result.is_err());
938 }
939
940 #[test]
941 fn test_kernel_types() {
942 let x1 = array![1.0, 2.0];
943 let x2 = array![2.0, 3.0];
944
945 let rbf = KernelType::RBF {
946 length_scale: 1.0,
947 variance: 1.0,
948 };
949 let matern32 = KernelType::Matern32 {
950 length_scale: 1.0,
951 variance: 1.0,
952 };
953 let matern52 = KernelType::Matern52 {
954 length_scale: 1.0,
955 variance: 1.0,
956 };
957 let linear = KernelType::Linear {
958 variance: 1.0,
959 offset: 0.0,
960 };
961
962 let rbf_val = rbf.compute(&x1.view(), &x2.view());
963 let matern32_val = matern32.compute(&x1.view(), &x2.view());
964 let matern52_val = matern52.compute(&x1.view(), &x2.view());
965 let linear_val = linear.compute(&x1.view(), &x2.view());
966
967 assert!(rbf_val > 0.0 && rbf_val <= 1.0);
968 assert!(matern32_val > 0.0 && matern32_val <= 1.0);
969 assert!(matern52_val > 0.0 && matern52_val <= 1.0);
970 assert!(linear_val > 0.0);
971 }
972}