sklears_gaussian_process/
sparse_gpr.rs1use 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#[derive(Debug, Clone)]
16pub struct SparseGaussianProcessRegressorConfig {
17 pub n_inducing: usize,
19 pub inducing_init: InducingPointInit,
21 pub alpha: f64,
23 pub optimizer: Option<String>,
25 pub n_restarts_optimizer: usize,
27 pub copy_x_train: bool,
29 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#[derive(Debug, Clone)]
61pub struct SparseGaussianProcessRegressor<S = Untrained> {
62 state: S,
63 kernel: Option<Box<dyn Kernel>>,
64 config: SparseGaussianProcessRegressorConfig,
65}
66
67#[derive(Debug, Clone)]
69pub enum InducingPointInit {
70 Random,
72 Uniform,
74 Kmeans,
76}
77
78#[derive(Debug, Clone)]
80#[allow(non_snake_case)]
81pub struct SgprTrained {
82 pub X_train: Option<Array2<f64>>, pub y_train: Array1<f64>, pub Z: Array2<f64>, pub alpha: Array1<f64>, pub Kmm: Array2<f64>, pub Knm: Array2<f64>, pub L: Array2<f64>, pub kernel: Box<dyn Kernel>, pub sigma_n: f64, pub log_marginal_likelihood_value: f64, }
103
104impl SparseGaussianProcessRegressor<Untrained> {
105 pub fn new() -> Self {
107 Self {
108 state: Untrained,
109 kernel: None,
110 config: SparseGaussianProcessRegressorConfig::default(),
111 }
112 }
113
114 pub fn kernel(mut self, kernel: Box<dyn Kernel>) -> Self {
116 self.kernel = Some(kernel);
117 self
118 }
119
120 pub fn n_inducing(mut self, n_inducing: usize) -> Self {
122 self.config.n_inducing = n_inducing;
123 self
124 }
125
126 pub fn inducing_init(mut self, inducing_init: InducingPointInit) -> Self {
128 self.config.inducing_init = inducing_init;
129 self
130 }
131
132 pub fn alpha(mut self, alpha: f64) -> Self {
134 self.config.alpha = alpha;
135 self
136 }
137
138 pub fn optimizer(mut self, optimizer: Option<String>) -> Self {
140 self.config.optimizer = optimizer;
141 self
142 }
143
144 pub fn n_restarts_optimizer(mut self, n_restarts: usize) -> Self {
146 self.config.n_restarts_optimizer = n_restarts;
147 self
148 }
149
150 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 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 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 let Kmm = kernel.compute_kernel_matrix(&Z, None)?;
219 let Knm = kernel.compute_kernel_matrix(X, Some(&Z))?;
220
221 let mut Kmm_reg = Kmm.clone();
223 for i in 0..Kmm_reg.nrows() {
224 Kmm_reg[[i, i]] += 1e-8; }
226
227 let L_mm = robust_cholesky(&Kmm_reg)?;
229
230 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 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 let sigma_n_sq = self.config.alpha;
251 let Lambda_diag = knn_diag.mapv(|x| x + sigma_n_sq);
252
253 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 let L_A = robust_cholesky(&A)?;
267
268 let b = Q.t().dot(&(y / &Lambda_diag));
270 let alpha = triangular_solve(&L_A, &b)?;
271
272 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,
293 y_train: y.to_owned(),
294 Z,
295 alpha,
296 Kmm: Kmm_reg,
297 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 #[allow(non_snake_case)]
322 pub fn predict_with_std(&self, X: &Array2<f64>) -> SklResult<(Array1<f64>, Array1<f64>)> {
323 let K_star_m = self
325 .state
326 .kernel
327 .compute_kernel_matrix(X, Some(&self.state.Z.to_owned()))?;
328
329 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 let mean = Q_star.dot(&self.state.alpha);
340
341 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(); let std = var.mapv(|x| (x + self.state.sigma_n.powi(2)).sqrt().max(0.0));
349
350 Ok((mean, std))
351 }
352
353 pub fn log_marginal_likelihood(&self) -> f64 {
355 self.state.log_marginal_likelihood_value
356 }
357
358 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}