Skip to main content

sklears_gaussian_process/
sparse_gpr.rs

1//! Sparse Gaussian Process Regression implementations
2
3// SciRS2 Policy - Use scirs2-autograd for ndarray types and operations
4use scirs2_core::ndarray::{Array1, Array2, Axis};
5use sklears_core::{
6    error::{Result as SklResult, SklearsError},
7    traits::{Estimator, Fit, Predict, Untrained},
8    types::Float,
9};
10
11use crate::kernels::Kernel;
12use crate::utils::{robust_cholesky, triangular_solve};
13
14/// Configuration for Sparse Gaussian Process Regressor
15#[derive(Debug, Clone)]
16pub struct SparseGaussianProcessRegressorConfig {
17    /// n_inducing
18    pub n_inducing: usize,
19    /// inducing_init
20    pub inducing_init: InducingPointInit,
21    /// alpha
22    pub alpha: f64,
23    /// optimizer
24    pub optimizer: Option<String>,
25    /// n_restarts_optimizer
26    pub n_restarts_optimizer: usize,
27    /// copy_x_train
28    pub copy_x_train: bool,
29    /// random_state
30    pub random_state: Option<u64>,
31}
32
33impl Default for SparseGaussianProcessRegressorConfig {
34    fn default() -> Self {
35        Self {
36            n_inducing: 10,
37            inducing_init: InducingPointInit::Kmeans,
38            alpha: 1e-10,
39            optimizer: Some("fmin_l_bfgs_b".to_string()),
40            n_restarts_optimizer: 0,
41            copy_x_train: true,
42            random_state: None,
43        }
44    }
45}
46
47/// # Examples
48///
49/// ```ignore
50/// let X = array![[1.0], [2.0], [3.0], [4.0], [5.0], [6.0]];
51/// let y = array![1.0, 4.0, 9.0, 16.0, 25.0, 36.0];
52///
53/// let kernel = RBF::new(1.0);
54/// let sgpr = SparseGaussianProcessRegressor::new()
55///     .kernel(Box::new(kernel))
56///     .n_inducing(4);
57/// let fitted = sgpr.fit(&X, &y).expect("fit should succeed with valid training data");
58/// let predictions = fitted.predict(&X).expect("predict should succeed on trained model");
59/// ```
60#[derive(Debug, Clone)]
61pub struct SparseGaussianProcessRegressor<S = Untrained> {
62    state: S,
63    kernel: Option<Box<dyn Kernel>>,
64    config: SparseGaussianProcessRegressorConfig,
65}
66
67/// Methods for initializing inducing points
68#[derive(Debug, Clone)]
69pub enum InducingPointInit {
70    /// Random
71    Random,
72    /// Uniform
73    Uniform,
74    /// Kmeans
75    Kmeans,
76}
77
78/// Trained state for Sparse Gaussian Process Regressor
79#[derive(Debug, Clone)]
80#[allow(non_snake_case)]
81pub struct SgprTrained {
82    /// X_train
83    pub X_train: Option<Array2<f64>>, // Training inputs
84    /// y_train
85    pub y_train: Array1<f64>, // Training outputs
86    /// Z
87    pub Z: Array2<f64>, // Inducing points
88    /// alpha
89    pub alpha: Array1<f64>, // Solved coefficients
90    /// Kmm
91    pub Kmm: Array2<f64>, // Kernel matrix between inducing points
92    /// Knm
93    pub Knm: Array2<f64>, // Kernel matrix between training and inducing points
94    /// L
95    pub L: Array2<f64>, // Cholesky decomposition
96    /// kernel
97    pub kernel: Box<dyn Kernel>, // Kernel function
98    /// sigma_n
99    pub sigma_n: f64, // Noise level
100    /// log_marginal_likelihood_value
101    pub log_marginal_likelihood_value: f64, // Log marginal likelihood
102}
103
104impl SparseGaussianProcessRegressor<Untrained> {
105    /// Create a new SparseGaussianProcessRegressor instance
106    pub fn new() -> Self {
107        Self {
108            state: Untrained,
109            kernel: None,
110            config: SparseGaussianProcessRegressorConfig::default(),
111        }
112    }
113
114    /// Set the kernel function
115    pub fn kernel(mut self, kernel: Box<dyn Kernel>) -> Self {
116        self.kernel = Some(kernel);
117        self
118    }
119
120    /// Set the number of inducing points
121    pub fn n_inducing(mut self, n_inducing: usize) -> Self {
122        self.config.n_inducing = n_inducing;
123        self
124    }
125
126    /// Set the inducing point initialization method
127    pub fn inducing_init(mut self, inducing_init: InducingPointInit) -> Self {
128        self.config.inducing_init = inducing_init;
129        self
130    }
131
132    /// Set the noise level
133    pub fn alpha(mut self, alpha: f64) -> Self {
134        self.config.alpha = alpha;
135        self
136    }
137
138    /// Set the optimizer
139    pub fn optimizer(mut self, optimizer: Option<String>) -> Self {
140        self.config.optimizer = optimizer;
141        self
142    }
143
144    /// Set the number of optimizer restarts
145    pub fn n_restarts_optimizer(mut self, n_restarts: usize) -> Self {
146        self.config.n_restarts_optimizer = n_restarts;
147        self
148    }
149
150    /// Set whether to copy X during training
151    pub fn copy_x_train(mut self, copy_x_train: bool) -> Self {
152        self.config.copy_x_train = copy_x_train;
153        self
154    }
155
156    /// Set the random state
157    pub fn random_state(mut self, random_state: Option<u64>) -> Self {
158        self.config.random_state = random_state;
159        self
160    }
161}
162
163impl Estimator for SparseGaussianProcessRegressor<Untrained> {
164    type Config = SparseGaussianProcessRegressorConfig;
165    type Error = SklearsError;
166    type Float = Float;
167
168    fn config(&self) -> &Self::Config {
169        &self.config
170    }
171}
172
173impl Estimator for SparseGaussianProcessRegressor<SgprTrained> {
174    type Config = SparseGaussianProcessRegressorConfig;
175    type Error = SklearsError;
176    type Float = Float;
177
178    fn config(&self) -> &Self::Config {
179        &self.config
180    }
181}
182
183#[allow(non_snake_case)]
184impl Fit<Array2<f64>, Array1<f64>> for SparseGaussianProcessRegressor<Untrained> {
185    type Fitted = SparseGaussianProcessRegressor<SgprTrained>;
186
187    fn fit(self, X: &Array2<f64>, y: &Array1<f64>) -> SklResult<Self::Fitted> {
188        if X.nrows() != y.len() {
189            return Err(SklearsError::InvalidInput(
190                "X and y must have the same number of samples".to_string(),
191            ));
192        }
193
194        let kernel = self
195            .kernel
196            .ok_or_else(|| SklearsError::InvalidInput("Kernel must be specified".to_string()))?;
197
198        // Initialize inducing points
199        let Z = match self.config.inducing_init {
200            InducingPointInit::Random => crate::utils::random_inducing_points(
201                &X.view(),
202                self.config.n_inducing,
203                self.config.random_state,
204            )?,
205            InducingPointInit::Uniform => crate::utils::uniform_inducing_points(
206                &X.view(),
207                self.config.n_inducing,
208                self.config.random_state,
209            )?,
210            InducingPointInit::Kmeans => crate::utils::kmeans_inducing_points(
211                &X.view(),
212                self.config.n_inducing,
213                self.config.random_state,
214            )?,
215        };
216
217        // Compute kernel matrices
218        let Kmm = kernel.compute_kernel_matrix(&Z, None)?;
219        let Knm = kernel.compute_kernel_matrix(X, Some(&Z))?;
220
221        // Add regularization to Kmm
222        let mut Kmm_reg = Kmm.clone();
223        for i in 0..Kmm_reg.nrows() {
224            Kmm_reg[[i, i]] += 1e-8; // Small jitter for numerical stability
225        }
226
227        // Cholesky decomposition of Kmm
228        let L_mm = robust_cholesky(&Kmm_reg)?;
229
230        // Solve for Q = Knm * Kmm^{-1}
231        let mut Q = Array2::<f64>::zeros(Knm.raw_dim());
232        for i in 0..Knm.nrows() {
233            let knm_i = Knm.row(i).to_owned();
234            let q_i = triangular_solve(&L_mm, &knm_i)?;
235            Q.row_mut(i).assign(&q_i);
236        }
237
238        // Compute diagonal of Knn - Q * Q^T
239        let knn_diag = X
240            .axis_iter(Axis(0))
241            .enumerate()
242            .map(|(i, x)| {
243                let knn_ii = kernel.kernel(&x, &x);
244                let q_i = Q.row(i);
245                knn_ii - q_i.dot(&q_i)
246            })
247            .collect::<Array1<f64>>();
248
249        // Add noise
250        let sigma_n_sq = self.config.alpha;
251        let Lambda_diag = knn_diag.mapv(|x| x + sigma_n_sq);
252
253        // Construct A = I + Q^T * Lambda^{-1} * Q
254        let mut A = Array2::<f64>::eye(self.config.n_inducing);
255        for i in 0..X.nrows() {
256            let q_i = Q.row(i);
257            let lambda_inv = 1.0 / Lambda_diag[i];
258            for j in 0..self.config.n_inducing {
259                for k in 0..self.config.n_inducing {
260                    A[[j, k]] += q_i[j] * q_i[k] * lambda_inv;
261                }
262            }
263        }
264
265        // Cholesky decomposition of A
266        let L_A = robust_cholesky(&A)?;
267
268        // Solve for alpha
269        let b = Q.t().dot(&(y / &Lambda_diag));
270        let alpha = triangular_solve(&L_A, &b)?;
271
272        // Compute log marginal likelihood (simplified)
273        let log_marginal_likelihood_value = {
274            let log_det_A = 2.0 * L_A.diag().mapv(|x| x.ln()).sum();
275            let log_det_Lambda = Lambda_diag.mapv(|x| x.ln()).sum();
276            let quadratic = y.dot(&(y / &Lambda_diag)) - alpha.dot(&b);
277            -0.5 * (quadratic
278                + log_det_A
279                + log_det_Lambda
280                + y.len() as f64 * (2.0 * std::f64::consts::PI).ln())
281        };
282
283        let X_train = if self.config.copy_x_train {
284            Some(X.to_owned())
285        } else {
286            None
287        };
288
289        Ok(SparseGaussianProcessRegressor {
290            state: SgprTrained {
291                // X_train
292                X_train,
293                y_train: y.to_owned(),
294                Z,
295                alpha,
296                Kmm: Kmm_reg,
297                // Knm
298                Knm,
299                L: L_A,
300                kernel,
301                sigma_n: self.config.alpha.sqrt(),
302                log_marginal_likelihood_value,
303            },
304            kernel: None,
305            config: self.config,
306        })
307    }
308}
309
310#[allow(non_snake_case)]
311impl Predict<Array2<f64>, Array1<f64>> for SparseGaussianProcessRegressor<SgprTrained> {
312    fn predict(&self, X: &Array2<f64>) -> SklResult<Array1<f64>> {
313        let (mean, _) = self.predict_with_std(X)?;
314        Ok(mean)
315    }
316}
317
318#[allow(non_snake_case)]
319impl SparseGaussianProcessRegressor<SgprTrained> {
320    /// Predict with uncertainty estimates
321    #[allow(non_snake_case)]
322    pub fn predict_with_std(&self, X: &Array2<f64>) -> SklResult<(Array1<f64>, Array1<f64>)> {
323        // Compute kernel between test points and inducing points
324        let K_star_m = self
325            .state
326            .kernel
327            .compute_kernel_matrix(X, Some(&self.state.Z.to_owned()))?;
328
329        // Solve for q_star = K_star_m * Kmm^{-1}
330        let L_mm = robust_cholesky(&self.state.Kmm)?;
331        let mut Q_star = Array2::<f64>::zeros(K_star_m.raw_dim());
332        for i in 0..K_star_m.nrows() {
333            let k_star_i = K_star_m.row(i).to_owned();
334            let q_star_i = triangular_solve(&L_mm, &k_star_i)?;
335            Q_star.row_mut(i).assign(&q_star_i);
336        }
337
338        // Predict mean
339        let mean = Q_star.dot(&self.state.alpha);
340
341        // Predict variance (simplified)
342        let k_star_star_diag = X
343            .axis_iter(Axis(0))
344            .map(|x| self.state.kernel.kernel(&x, &x))
345            .collect::<Array1<f64>>();
346
347        let var = k_star_star_diag.clone(); // Simplified variance computation
348        let std = var.mapv(|x| (x + self.state.sigma_n.powi(2)).sqrt().max(0.0));
349
350        Ok((mean, std))
351    }
352
353    /// Get the log marginal likelihood
354    pub fn log_marginal_likelihood(&self) -> f64 {
355        self.state.log_marginal_likelihood_value
356    }
357
358    /// Get the inducing points
359    pub fn inducing_points(&self) -> &Array2<f64> {
360        &self.state.Z
361    }
362}
363
364impl Default for SparseGaussianProcessRegressor<Untrained> {
365    fn default() -> Self {
366        Self::new()
367    }
368}