use crate::algebra::{add_rows_cholesky_cov_matrix, make_cholesky_cov_matrix, make_covariance_matrix, EMatrix,
EVector};
use crate::conversion::Input;
use crate::parameters::{kernel, kernel::Kernel, prior, prior::Prior};
use chrono::Duration;
use nalgebra::{Cholesky, DMatrix, DVector, Dyn};
mod multivariate_normal;
pub use multivariate_normal::MultivariateNormal;
mod builder;
pub use builder::GaussianProcessBuilder;
mod optimizer;
#[cfg_attr(feature = "friedrich_serde", derive(serde::Deserialize, serde::Serialize))]
pub struct GaussianProcess<KernelType: Kernel, PriorType: Prior>
{
pub prior: PriorType,
pub kernel: KernelType,
pub noise: f64,
pub cholesky_epsilon: Option<f64>,
training_inputs: EMatrix,
training_outputs: EVector,
covmat_cholesky: Cholesky<f64, Dyn>
}
impl GaussianProcess<kernel::Gaussian, prior::ConstantPrior>
{
pub fn default<T: Input>(training_inputs: T, training_outputs: T::InVector) -> Self
{
GaussianProcessBuilder::<kernel::Gaussian, prior::ConstantPrior>::new(training_inputs,
training_outputs).fit_kernel()
.fit_prior()
.train()
}
pub fn builder<T: Input>(training_inputs: T,
training_outputs: T::InVector)
-> GaussianProcessBuilder<kernel::Gaussian, prior::ConstantPrior>
{
GaussianProcessBuilder::<kernel::Gaussian, prior::ConstantPrior>::new(training_inputs,
training_outputs)
}
}
impl<KernelType: Kernel, PriorType: Prior> GaussianProcess<KernelType, PriorType>
{
pub fn new<T: Input>(prior: PriorType,
kernel: KernelType,
noise: f64,
cholesky_epsilon: Option<f64>,
training_inputs: T,
training_outputs: T::InVector)
-> Self
{
assert!(noise >= 0., "The noise parameter should non-negative but we tried to set it to {}", noise);
let training_inputs = T::into_dmatrix(training_inputs);
let training_outputs = T::into_dvector(training_outputs);
assert_eq!(training_inputs.nrows(), training_outputs.nrows());
let training_inputs = EMatrix::new(training_inputs);
let training_outputs = EVector::new(training_outputs - prior.prior(&training_inputs.as_matrix()));
let covmat_cholesky =
make_cholesky_cov_matrix(&training_inputs.as_matrix(), &kernel, noise, cholesky_epsilon);
GaussianProcess { prior,
kernel,
noise,
cholesky_epsilon,
training_inputs,
training_outputs,
covmat_cholesky }
}
pub fn add_samples<T: Input>(&mut self, inputs: &T, outputs: &T::InVector)
{
let inputs = T::to_dmatrix(inputs);
let outputs = T::to_dvector(outputs);
assert_eq!(inputs.nrows(), outputs.nrows());
assert_eq!(inputs.ncols(), self.training_inputs.as_matrix().ncols());
let outputs = outputs - self.prior.prior(&inputs);
self.training_inputs.add_rows(&inputs);
self.training_outputs.add_rows(&outputs);
let nb_new_inputs = inputs.nrows();
add_rows_cholesky_cov_matrix(&mut self.covmat_cholesky,
&self.training_inputs.as_matrix(),
nb_new_inputs,
&self.kernel,
self.noise);
}
pub fn likelihood(&self) -> f64
{
let output = self.training_outputs.as_vector();
let ol = self.covmat_cholesky.l().solve_lower_triangular(&output).expect("likelihood : solve failed");
let data_fit: f64 = ol.norm_squared();
let complexity_penalty: f64 = self.training_inputs
.as_matrix()
.row_iter()
.map(|r| self.kernel.kernel(&r, &r) + self.noise * self.noise)
.map(|c| c.abs().ln())
.sum();
let n = self.training_inputs.as_matrix().nrows();
let normalization_constant = (n as f64) * (2. * std::f64::consts::PI).ln();
-(data_fit + complexity_penalty + normalization_constant) / 2.
}
pub fn predict<T: Input>(&self, inputs: &T) -> T::OutVector
{
let inputs = T::to_dmatrix(inputs);
assert_eq!(inputs.ncols(), self.training_inputs.as_matrix().ncols());
let mut weights = make_covariance_matrix(&self.training_inputs.as_matrix(), &inputs, &self.kernel);
self.covmat_cholesky.solve_mut(&mut weights);
let mut prior = self.prior.prior(&inputs);
prior.gemm_tr(1f64, &weights, &self.training_outputs.as_vector(), 1f64);
T::from_dvector(&prior)
}
pub fn predict_variance<T: Input>(&self, inputs: &T) -> T::OutVector
{
let inputs = T::to_dmatrix(inputs);
assert_eq!(inputs.ncols(), self.training_inputs.as_matrix().ncols());
let cov_train_inputs =
make_covariance_matrix(&self.training_inputs.as_matrix(), &inputs, &self.kernel);
let kl = self.covmat_cholesky
.l()
.solve_lower_triangular(&cov_train_inputs)
.expect("predict_covariance : solve failed");
let variances = inputs.row_iter()
.map(|row| self.kernel.kernel(&row, &row)) .zip(kl.column_iter().map(|col| col.norm_squared())) .map(|(base_cov, predicted_cov)| base_cov - predicted_cov);
let variances = DVector::<f64>::from_iterator(inputs.nrows(), variances);
T::from_dvector(&variances)
}
pub fn predict_mean_variance<T: Input>(&self, inputs: &T) -> (T::OutVector, T::OutVector)
{
let inputs = T::to_dmatrix(inputs);
assert_eq!(inputs.ncols(), self.training_inputs.as_matrix().ncols());
let cov_train_inputs =
make_covariance_matrix(&self.training_inputs.as_matrix(), &inputs, &self.kernel);
let weights = self.covmat_cholesky.solve(&cov_train_inputs);
let mut prior = self.prior.prior(&inputs);
prior.gemm_tr(1f64, &weights, &self.training_outputs.as_vector(), 1f64);
let mean = T::from_dvector(&prior);
let mut variances = DVector::<f64>::zeros(inputs.nrows());
for (i, input) in inputs.row_iter().enumerate()
{
let base_cov = self.kernel.kernel(&input, &input);
let predicted_cov = cov_train_inputs.column(i).dot(&weights.column(i));
variances[i] = base_cov - predicted_cov;
}
let variance = T::from_dvector(&variances);
(mean, variance)
}
pub fn predict_covariance<T: Input>(&self, inputs: &T) -> DMatrix<f64>
{
let inputs = T::to_dmatrix(inputs);
assert_eq!(inputs.ncols(), self.training_inputs.as_matrix().ncols());
let cov_train_inputs =
make_covariance_matrix(&self.training_inputs.as_matrix(), &inputs, &self.kernel);
let mut cov_inputs_inputs = make_covariance_matrix(&inputs, &inputs, &self.kernel);
let kl = self.covmat_cholesky
.l()
.solve_lower_triangular(&cov_train_inputs)
.expect("predict_covariance : solve failed");
cov_inputs_inputs.gemm_tr(-1f64, &kl, &kl, 1f64);
cov_inputs_inputs
}
pub fn sample_at<T: Input>(&self, inputs: &T) -> MultivariateNormal<T>
{
let inputs = T::to_dmatrix(inputs);
assert_eq!(inputs.ncols(), self.training_inputs.as_matrix().ncols());
let cov_train_inputs =
make_covariance_matrix(&self.training_inputs.as_matrix(), &inputs, &self.kernel);
let weights = self.covmat_cholesky.solve(&cov_train_inputs);
let mut cov_inputs_inputs = make_covariance_matrix(&inputs, &inputs, &self.kernel);
cov_inputs_inputs.gemm_tr(-1f64, &cov_train_inputs, &weights, 1f64);
let cov = cov_inputs_inputs;
let mut prior = self.prior.prior(&inputs);
prior.gemm_tr(1f64, &weights, &self.training_outputs.as_vector(), 1f64);
let mean = prior;
MultivariateNormal::new(mean, cov)
}
pub fn fit_parameters(&mut self,
fit_prior: bool,
fit_kernel: bool,
max_iter: usize,
convergence_fraction: f64,
max_time: Duration)
{
if fit_prior
{
let training_outputs =
self.training_outputs.as_vector() + self.prior.prior(&self.training_inputs.as_matrix());
self.prior.fit(&self.training_inputs.as_matrix(), &training_outputs);
let training_outputs = training_outputs - self.prior.prior(&self.training_inputs.as_matrix());
self.training_outputs.assign(&training_outputs);
if !fit_kernel
{
self.covmat_cholesky = make_cholesky_cov_matrix(&self.training_inputs.as_matrix(),
&self.kernel,
self.noise,
self.cholesky_epsilon);
}
}
if fit_kernel
{
if self.kernel.is_scalable()
{
self.scaled_optimize_parameters(max_iter, convergence_fraction, max_time);
}
else
{
self.optimize_parameters(max_iter, convergence_fraction, max_time);
}
}
}
}