Skip to main content

sklears_semi_supervised/deep_learning/
deep_gaussian_processes.rs

1//! Deep Gaussian Processes for semi-supervised learning
2//!
3//! This module implements Deep Gaussian Processes (DGPs), which are hierarchical
4//! compositions of Gaussian processes that can model complex non-linear relationships
5//! and provide uncertainty quantification for semi-supervised learning tasks.
6
7use scirs2_core::ndarray_ext::{Array1, Array2, ArrayView1, ArrayView2, Axis};
8use scirs2_core::random::Random;
9// use scirs2_core::random::rand::seq::SliceRandom;
10use 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/// Kernel function types for Gaussian Processes
48#[derive(Debug, Clone)]
49pub enum KernelType {
50    /// RBF
51    RBF { length_scale: f64, variance: f64 },
52    /// Matern32
53    Matern32 { length_scale: f64, variance: f64 },
54    /// Matern52
55    Matern52 { length_scale: f64, variance: f64 },
56    /// Linear
57    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)] // standard ML notation
112    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/// Single Gaussian Process layer in the deep architecture
128#[derive(Debug, Clone)]
129pub struct GaussianProcessLayer {
130    /// input_dim
131    pub input_dim: usize,
132    /// output_dim
133    pub output_dim: usize,
134    /// num_inducing
135    pub num_inducing: usize,
136    /// kernel
137    pub kernel: KernelType,
138    /// noise_variance
139    pub noise_variance: f64,
140    /// learning_rate
141    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)] // standard ML notation
187    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        // Select random subset of training points as inducing points
195        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)] // standard ML notation
216    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        // Simple Cholesky decomposition (simplified implementation)
222        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)] // standard ML notation
248    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)] // standard ML notation
261    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        // Compute kernel matrices
302        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        // Add noise to diagonal for numerical stability
308        let mut K_uu_noisy = K_uu.clone();
309        self.add_jitter(&mut K_uu_noisy, self.noise_variance);
310
311        // Simplified variational inference (ELBO optimization)
312        let L_uu = self.safe_cholesky(&K_uu_noisy)?;
313
314        // For each output dimension, fit a separate GP
315        for d in 0..self.output_dim {
316            let y_d = y.column(d);
317
318            // Solve for alpha: K_uu * alpha = K_uf * y
319            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            // Store mean function for this output dimension
325            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        // Compute predictive mean using kernel computations
348        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        // Simple prediction using kernel mean
353        for i in 0..n_test {
354            for d in 0..self.output_dim {
355                // Simplified prediction: weighted average based on kernel similarities
356                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)] // standard ML notation
370    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        // Simplified uncertainty estimation
378        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/// Deep Gaussian Process for semi-supervised learning
390///
391/// This implements a multi-layer composition of Gaussian processes that can
392/// learn complex non-linear mappings while providing principled uncertainty
393/// quantification for semi-supervised learning scenarios.
394#[derive(Debug, Clone)]
395pub struct DeepGaussianProcess {
396    /// layer_dims
397    pub layer_dims: Vec<usize>,
398    /// num_inducing
399    pub num_inducing: usize,
400    /// epochs
401    pub epochs: usize,
402    /// learning_rate
403    pub learning_rate: f64,
404    /// noise_variance
405    pub noise_variance: f64,
406    /// random_state
407    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)] // standard ML notation
498    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(&current_data.view())?;
503        }
504
505        Ok(current_data)
506    }
507
508    #[allow(non_snake_case)] // standard ML notation
509    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)] // standard ML notation
545    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        // Count labeled samples
557        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        // Get unique classes from labeled data
565        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        // Set input size from data
571        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        // Create target matrix for labeled samples
586        let y_encoded = self.encode_labels(y);
587
588        // Layer-wise training with all data
589        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                // Last layer: train only on labeled data for classification
597                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                    // Create arrays from the collected rows
615                    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                // Hidden layers: unsupervised pre-training with all data
632                // For simplicity, use identity mapping as target for unsupervised layers
633                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(&current_data.view(), &identity_target.view(), seed)?;
644            }
645
646            // Forward pass for next layer
647            if layer_idx < num_layers - 1 {
648                current_data = self.layers[layer_idx].predict(&current_data.view())?;
649            }
650        }
651
652        self.is_trained = true;
653        Ok(())
654    }
655
656    #[allow(non_snake_case)] // standard ML notation
657    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)] // standard ML notation
667    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)] // standard ML notation
680    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        // Get predictions with uncertainty from last layer
689        let mut current_data = X.to_owned();
690
691        // Forward pass through all but last layer
692        for layer in self.layers.iter().take(self.layers.len() - 1) {
693            current_data = layer.predict(&current_data.view())?;
694        }
695
696        // Get predictions and uncertainties from last layer
697        if let Some(last_layer) = self.layers.last() {
698            let (logits, uncertainties) =
699                last_layer.predict_with_uncertainty(&current_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)] // standard ML notation
727    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)] // standard ML notation
739    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)] // standard ML notation
746    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]; // -1 indicates unlabeled
861
862        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        // Check that probabilities sum to 1
875        for i in 0..6 {
876            let sum: f64 = probabilities.row(i).sum();
877            assert!((sum - 1.0).abs() < 1e-5); // More lenient epsilon for GP
878        }
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        // Check that uncertainties are positive
908        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]; // All unlabeled
935
936        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}