use scirs2_core::ndarray::{Array1, Array2, Axis};
use sklears_core::{
error::{Result as SklResult, SklearsError},
traits::{Estimator, Fit, Predict, Untrained},
types::Float,
};
use crate::kernels::Kernel;
use crate::utils::{robust_cholesky, triangular_solve};
#[derive(Debug, Clone)]
pub struct SparseGaussianProcessRegressorConfig {
pub n_inducing: usize,
pub inducing_init: InducingPointInit,
pub alpha: f64,
pub optimizer: Option<String>,
pub n_restarts_optimizer: usize,
pub copy_x_train: bool,
pub random_state: Option<u64>,
}
impl Default for SparseGaussianProcessRegressorConfig {
fn default() -> Self {
Self {
n_inducing: 10,
inducing_init: InducingPointInit::Kmeans,
alpha: 1e-10,
optimizer: Some("fmin_l_bfgs_b".to_string()),
n_restarts_optimizer: 0,
copy_x_train: true,
random_state: None,
}
}
}
#[derive(Debug, Clone)]
pub struct SparseGaussianProcessRegressor<S = Untrained> {
state: S,
kernel: Option<Box<dyn Kernel>>,
config: SparseGaussianProcessRegressorConfig,
}
#[derive(Debug, Clone)]
pub enum InducingPointInit {
Random,
Uniform,
Kmeans,
}
#[derive(Debug, Clone)]
#[allow(non_snake_case)]
pub struct SgprTrained {
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, }
impl SparseGaussianProcessRegressor<Untrained> {
pub fn new() -> Self {
Self {
state: Untrained,
kernel: None,
config: SparseGaussianProcessRegressorConfig::default(),
}
}
pub fn kernel(mut self, kernel: Box<dyn Kernel>) -> Self {
self.kernel = Some(kernel);
self
}
pub fn n_inducing(mut self, n_inducing: usize) -> Self {
self.config.n_inducing = n_inducing;
self
}
pub fn inducing_init(mut self, inducing_init: InducingPointInit) -> Self {
self.config.inducing_init = inducing_init;
self
}
pub fn alpha(mut self, alpha: f64) -> Self {
self.config.alpha = alpha;
self
}
pub fn optimizer(mut self, optimizer: Option<String>) -> Self {
self.config.optimizer = optimizer;
self
}
pub fn n_restarts_optimizer(mut self, n_restarts: usize) -> Self {
self.config.n_restarts_optimizer = n_restarts;
self
}
pub fn copy_x_train(mut self, copy_x_train: bool) -> Self {
self.config.copy_x_train = copy_x_train;
self
}
pub fn random_state(mut self, random_state: Option<u64>) -> Self {
self.config.random_state = random_state;
self
}
}
impl Estimator for SparseGaussianProcessRegressor<Untrained> {
type Config = SparseGaussianProcessRegressorConfig;
type Error = SklearsError;
type Float = Float;
fn config(&self) -> &Self::Config {
&self.config
}
}
impl Estimator for SparseGaussianProcessRegressor<SgprTrained> {
type Config = SparseGaussianProcessRegressorConfig;
type Error = SklearsError;
type Float = Float;
fn config(&self) -> &Self::Config {
&self.config
}
}
#[allow(non_snake_case)]
impl Fit<Array2<f64>, Array1<f64>> for SparseGaussianProcessRegressor<Untrained> {
type Fitted = SparseGaussianProcessRegressor<SgprTrained>;
fn fit(self, X: &Array2<f64>, y: &Array1<f64>) -> SklResult<Self::Fitted> {
if X.nrows() != y.len() {
return Err(SklearsError::InvalidInput(
"X and y must have the same number of samples".to_string(),
));
}
let kernel = self
.kernel
.ok_or_else(|| SklearsError::InvalidInput("Kernel must be specified".to_string()))?;
let Z = match self.config.inducing_init {
InducingPointInit::Random => crate::utils::random_inducing_points(
&X.view(),
self.config.n_inducing,
self.config.random_state,
)?,
InducingPointInit::Uniform => crate::utils::uniform_inducing_points(
&X.view(),
self.config.n_inducing,
self.config.random_state,
)?,
InducingPointInit::Kmeans => crate::utils::kmeans_inducing_points(
&X.view(),
self.config.n_inducing,
self.config.random_state,
)?,
};
let Kmm = kernel.compute_kernel_matrix(&Z, None)?;
let Knm = kernel.compute_kernel_matrix(X, Some(&Z))?;
let mut Kmm_reg = Kmm.clone();
for i in 0..Kmm_reg.nrows() {
Kmm_reg[[i, i]] += 1e-8; }
let L_mm = robust_cholesky(&Kmm_reg)?;
let mut Q = Array2::<f64>::zeros(Knm.raw_dim());
for i in 0..Knm.nrows() {
let knm_i = Knm.row(i).to_owned();
let q_i = triangular_solve(&L_mm, &knm_i)?;
Q.row_mut(i).assign(&q_i);
}
let knn_diag = X
.axis_iter(Axis(0))
.enumerate()
.map(|(i, x)| {
let knn_ii = kernel.kernel(&x, &x);
let q_i = Q.row(i);
knn_ii - q_i.dot(&q_i)
})
.collect::<Array1<f64>>();
let sigma_n_sq = self.config.alpha;
let Lambda_diag = knn_diag.mapv(|x| x + sigma_n_sq);
let mut A = Array2::<f64>::eye(self.config.n_inducing);
for i in 0..X.nrows() {
let q_i = Q.row(i);
let lambda_inv = 1.0 / Lambda_diag[i];
for j in 0..self.config.n_inducing {
for k in 0..self.config.n_inducing {
A[[j, k]] += q_i[j] * q_i[k] * lambda_inv;
}
}
}
let L_A = robust_cholesky(&A)?;
let b = Q.t().dot(&(y / &Lambda_diag));
let alpha = triangular_solve(&L_A, &b)?;
let log_marginal_likelihood_value = {
let log_det_A = 2.0 * L_A.diag().mapv(|x| x.ln()).sum();
let log_det_Lambda = Lambda_diag.mapv(|x| x.ln()).sum();
let quadratic = y.dot(&(y / &Lambda_diag)) - alpha.dot(&b);
-0.5 * (quadratic
+ log_det_A
+ log_det_Lambda
+ y.len() as f64 * (2.0 * std::f64::consts::PI).ln())
};
let X_train = if self.config.copy_x_train {
Some(X.to_owned())
} else {
None
};
Ok(SparseGaussianProcessRegressor {
state: SgprTrained {
X_train,
y_train: y.to_owned(),
Z,
alpha,
Kmm: Kmm_reg,
Knm,
L: L_A,
kernel,
sigma_n: self.config.alpha.sqrt(),
log_marginal_likelihood_value,
},
kernel: None,
config: self.config,
})
}
}
#[allow(non_snake_case)]
impl Predict<Array2<f64>, Array1<f64>> for SparseGaussianProcessRegressor<SgprTrained> {
fn predict(&self, X: &Array2<f64>) -> SklResult<Array1<f64>> {
let (mean, _) = self.predict_with_std(X)?;
Ok(mean)
}
}
#[allow(non_snake_case)]
impl SparseGaussianProcessRegressor<SgprTrained> {
#[allow(non_snake_case)]
pub fn predict_with_std(&self, X: &Array2<f64>) -> SklResult<(Array1<f64>, Array1<f64>)> {
let K_star_m = self
.state
.kernel
.compute_kernel_matrix(X, Some(&self.state.Z.to_owned()))?;
let L_mm = robust_cholesky(&self.state.Kmm)?;
let mut Q_star = Array2::<f64>::zeros(K_star_m.raw_dim());
for i in 0..K_star_m.nrows() {
let k_star_i = K_star_m.row(i).to_owned();
let q_star_i = triangular_solve(&L_mm, &k_star_i)?;
Q_star.row_mut(i).assign(&q_star_i);
}
let mean = Q_star.dot(&self.state.alpha);
let k_star_star_diag = X
.axis_iter(Axis(0))
.map(|x| self.state.kernel.kernel(&x, &x))
.collect::<Array1<f64>>();
let var = k_star_star_diag.clone(); let std = var.mapv(|x| (x + self.state.sigma_n.powi(2)).sqrt().max(0.0));
Ok((mean, std))
}
pub fn log_marginal_likelihood(&self) -> f64 {
self.state.log_marginal_likelihood_value
}
pub fn inducing_points(&self) -> &Array2<f64> {
&self.state.Z
}
}
impl Default for SparseGaussianProcessRegressor<Untrained> {
fn default() -> Self {
Self::new()
}
}