Initial commit

This commit is contained in:
redclawsystems
2026-03-04 00:08:42 +00:00
commit 4d88dc0584
4449 changed files with 1556714 additions and 0 deletions
@@ -0,0 +1,397 @@
//! Gaussian Process regression implementation
//!
//! Gaussian Processes provide a probabilistic approach to regression
//! that can quantify uncertainty in predictions.
use crate::error::Result;
use rtx_tensor::Tensor;
use std::f32;
/// Kernel functions for Gaussian Process
#[derive(Debug, Clone)]
pub enum Kernel {
/// Radial Basis Function (RBF) kernel: k(x, x') = exp(-γ||x - x'||²)
RBF { gamma: f32 },
/// Linear kernel: k(x, x') = x^T x'
Linear,
/// Polynomial kernel: k(x, x') = (γ x^T x' + r)^d
Polynomial { gamma: f32, degree: f32, coef0: f32 },
/// Matern kernel with ν = 3/2
Matern32 { length_scale: f32 },
}
/// Gaussian Process regressor
#[derive(Debug, Clone)]
pub struct GaussianProcess {
/// Kernel function
kernel: Kernel,
/// Noise level (alpha)
alpha: f32,
/// Normalization of target values
normalize_y: bool,
/// Training data
x_train: Option<Tensor>,
/// Training targets
y_train: Option<Tensor>,
/// Cholesky decomposition of K + αI
l_matrix: Option<Tensor>,
/// α = L^(-1) y
alpha_vec: Option<Tensor>,
/// Mean of training targets (if normalized)
y_mean: f32,
/// Standard deviation of training targets (if normalized)
y_std: f32,
/// Number of training samples
n_train: usize,
/// Number of features
n_features: usize,
}
impl GaussianProcess {
/// Create new Gaussian Process regressor
pub fn new() -> Self {
Self {
kernel: Kernel::RBF { gamma: 1.0 },
alpha: 1e-10,
normalize_y: false,
x_train: None,
y_train: None,
l_matrix: None,
alpha_vec: None,
y_mean: 0.0,
y_std: 1.0,
n_train: 0,
n_features: 0,
}
}
/// Set kernel function
pub fn kernel(mut self, kernel: Kernel) -> Self {
self.kernel = kernel;
self
}
/// Set noise level (regularization parameter)
pub fn alpha(mut self, alpha: f32) -> Self {
self.alpha = alpha;
self
}
/// Set whether to normalize target values
pub fn normalize_y(mut self, normalize: bool) -> Self {
self.normalize_y = normalize;
self
}
/// Fit the Gaussian Process model
pub fn fit(&mut self, x: &Tensor, y: &Tensor) -> Result<&mut Self> {
// Validate input
if x.shape().ndim() != 2 {
return Err(crate::error::MLError::invalid_input(
"X must be 2-dimensional".to_string(),
));
}
if y.shape().ndim() != 1 {
return Err(crate::error::MLError::invalid_input(
"y must be 1-dimensional".to_string(),
));
}
self.n_train = x.shape().dims()[0];
self.n_features = x.shape().dims()[1];
if y.shape().dims()[0] != self.n_train {
return Err(crate::error::MLError::invalid_input(
"X and y must have same number of samples".to_string(),
));
}
// Store training data
self.x_train = Some(x.clone());
// Normalize targets if requested
let y_normalized = if self.normalize_y {
let y_data = y.to_cpu()?;
self.y_mean = y_data.iter().sum::<f32>() / self.n_train as f32;
self.y_std = {
let variance = y_data
.iter()
.map(|&val| (val - self.y_mean).powi(2))
.sum::<f32>()
/ self.n_train as f32;
variance.sqrt().max(1e-8) // Avoid division by zero
};
let normalized_data: Vec<f32> = y_data
.iter()
.map(|&val| (val - self.y_mean) / self.y_std)
.collect();
Tensor::from_data(normalized_data, vec![self.n_train], y.device())?
} else {
self.y_mean = 0.0;
self.y_std = 1.0;
y.clone()
};
self.y_train = Some(y_normalized.clone());
// Compute kernel matrix K
let k_matrix = self.compute_kernel_matrix(x, x)?;
// Add noise term: K + αI
let mut k_data = k_matrix.to_cpu()?;
for i in 0..self.n_train {
k_data[i * self.n_train + i] += self.alpha;
}
let k_noisy = Tensor::from_data(k_data, vec![self.n_train, self.n_train], x.device())?;
// Cholesky decomposition: K + αI = L L^T
self.l_matrix = Some(self.cholesky_decomposition(&k_noisy)?);
// Solve L α = y for α
let l_matrix = self.l_matrix.as_ref().unwrap();
self.alpha_vec = Some(self.solve_triangular(l_matrix, &y_normalized, true)?);
Ok(self)
}
/// Predict mean and standard deviation for test data
pub fn predict(&self, x: &Tensor) -> Result<Tensor> {
let x_train = self.x_train.as_ref().ok_or_else(|| {
crate::error::MLError::not_fitted("Model has not been fitted yet".to_string())
})?;
let _l_matrix = self.l_matrix.as_ref().unwrap();
let alpha_vec = self.alpha_vec.as_ref().unwrap();
// Validate input
if x.shape().ndim() != 2 {
return Err(crate::error::MLError::invalid_input(
"X must be 2-dimensional".to_string(),
));
}
let n_test = x.shape().dims()[0];
let n_features = x.shape().dims()[1];
if n_features != self.n_features {
return Err(crate::error::MLError::invalid_input(format!(
"X has {} features but model was fitted with {} features",
n_features, self.n_features
)));
}
// Compute kernel matrix between test and training data
let k_star = self.compute_kernel_matrix(x, x_train)?;
// Compute mean predictions: μ* = K* α
let mu_star = k_star.matmul(alpha_vec)?;
// Denormalize predictions if needed
let predictions = if self.normalize_y {
let mu_data = mu_star.to_cpu()?;
let denormalized: Vec<f32> = mu_data
.iter()
.map(|&val| val * self.y_std + self.y_mean)
.collect();
Tensor::from_data(denormalized, vec![n_test], x.device())?
} else {
mu_star
};
Ok(predictions)
}
/// Predict with uncertainty (returns mean and standard deviation)
pub fn predict_with_uncertainty(&self, x: &Tensor) -> Result<(Tensor, Tensor)> {
let x_train = self.x_train.as_ref().ok_or_else(|| {
crate::error::MLError::not_fitted("Model has not been fitted yet".to_string())
})?;
let l_matrix = self.l_matrix.as_ref().unwrap();
let alpha_vec = self.alpha_vec.as_ref().unwrap();
// Validate input
if x.shape().ndim() != 2 {
return Err(crate::error::MLError::invalid_input(
"X must be 2-dimensional".to_string(),
));
}
let n_test = x.shape().dims()[0];
// Compute kernel matrices
let k_star = self.compute_kernel_matrix(x, x_train)?;
let k_star_star = self.compute_kernel_matrix(x, x)?;
// Compute mean predictions
let mu_star = k_star.matmul(alpha_vec)?;
// Compute variance: σ²* = K** - K* L^(-T) L^(-1) K*^T
let v = self.solve_triangular(l_matrix, &k_star.transpose(0, 1)?, true)?;
let var_star = k_star_star.subtract(&v.transpose(0, 1)?.matmul(&v)?)?;
// Extract diagonal for pointwise variances
let var_data = var_star.to_cpu()?;
let std_data: Vec<f32> = (0..n_test)
.map(|i| var_data[i * n_test + i].max(0.0).sqrt())
.collect();
let std_tensor = Tensor::from_data(std_data, vec![n_test], x.device())?;
// Denormalize if needed
let (mean_final, std_final) = if self.normalize_y {
let mu_data = mu_star.to_cpu()?;
let denormalized_mean: Vec<f32> = mu_data
.iter()
.map(|&val| val * self.y_std + self.y_mean)
.collect();
let std_data = std_tensor.to_cpu()?;
let denormalized_std: Vec<f32> = std_data.iter().map(|&val| val * self.y_std).collect();
(
Tensor::from_data(denormalized_mean, vec![n_test], x.device())?,
Tensor::from_data(denormalized_std, vec![n_test], x.device())?,
)
} else {
(mu_star, std_tensor)
};
Ok((mean_final, std_final))
}
/// Compute kernel matrix between two sets of points
fn compute_kernel_matrix(&self, x1: &Tensor, x2: &Tensor) -> Result<Tensor> {
let x1_data = x1.to_cpu()?;
let x2_data = x2.to_cpu()?;
let n1 = x1.shape().dims()[0];
let n2 = x2.shape().dims()[0];
let n_features = x1.shape().dims()[1];
let mut k_data = vec![0.0f32; n1 * n2];
for i in 0..n1 {
for j in 0..n2 {
let x1_point = &x1_data[i * n_features..(i + 1) * n_features];
let x2_point = &x2_data[j * n_features..(j + 1) * n_features];
k_data[i * n2 + j] = self.kernel_function(x1_point, x2_point);
}
}
Tensor::from_data(k_data, vec![n1, n2], x1.device()).map_err(std::convert::Into::into)
}
/// Compute kernel function between two points
fn kernel_function(&self, x1: &[f32], x2: &[f32]) -> f32 {
match &self.kernel {
Kernel::RBF { gamma } => {
let sq_dist: f32 = x1
.iter()
.zip(x2.iter())
.map(|(&a, &b)| (a - b).powi(2))
.sum();
(-gamma * sq_dist).exp()
}
Kernel::Linear => x1.iter().zip(x2.iter()).map(|(&a, &b)| a * b).sum(),
Kernel::Polynomial {
gamma,
degree,
coef0,
} => {
let dot_product: f32 = x1.iter().zip(x2.iter()).map(|(&a, &b)| a * b).sum();
(gamma * dot_product + coef0).powf(*degree)
}
Kernel::Matern32 { length_scale } => {
let dist: f32 = x1
.iter()
.zip(x2.iter())
.map(|(&a, &b)| (a - b).powi(2))
.sum::<f32>()
.sqrt();
let sqrt3_dist_l = (3.0_f32).sqrt() * dist / length_scale;
(1.0 + sqrt3_dist_l) * (-sqrt3_dist_l).exp()
}
}
}
/// Cholesky decomposition
fn cholesky_decomposition(&self, matrix: &Tensor) -> Result<Tensor> {
// Use the tensor's built-in Cholesky solve method as a reference
// For now, implement a simple decomposition
let n = matrix.shape().dims()[0];
let data = matrix.to_cpu()?;
let mut l_data = vec![0.0f32; n * n];
for i in 0..n {
for j in 0..=i {
if i == j {
let mut sum = 0.0;
for k in 0..j {
sum += l_data[j * n + k] * l_data[j * n + k];
}
l_data[j * n + j] = (data[j * n + j] - sum).sqrt().max(1e-12);
} else {
let mut sum = 0.0;
for k in 0..j {
sum += l_data[i * n + k] * l_data[j * n + k];
}
l_data[i * n + j] = (data[i * n + j] - sum) / l_data[j * n + j];
}
}
}
Tensor::from_data(l_data, vec![n, n], matrix.device()).map_err(std::convert::Into::into)
}
/// Solve triangular system
fn solve_triangular(&self, l: &Tensor, b: &Tensor, lower: bool) -> Result<Tensor> {
let n = l.shape().dims()[0];
let l_data = l.to_cpu()?;
let b_data = b.to_cpu()?;
let mut x_data = vec![0.0f32; n];
if lower {
// Forward substitution: L x = b
for i in 0..n {
let mut sum = 0.0;
for j in 0..i {
sum += l_data[i * n + j] * x_data[j];
}
x_data[i] = (b_data[i] - sum) / l_data[i * n + i];
}
} else {
// Backward substitution: U x = b
for i in (0..n).rev() {
let mut sum = 0.0;
for j in (i + 1)..n {
sum += l_data[i * n + j] * x_data[j];
}
x_data[i] = (b_data[i] - sum) / l_data[i * n + i];
}
}
Tensor::from_data(x_data, vec![n], l.device()).map_err(std::convert::Into::into)
}
/// Get fitted parameters
pub fn params(&self) -> (Kernel, f32) {
(self.kernel.clone(), self.alpha)
}
/// Check if model is fitted
pub fn is_fitted(&self) -> bool {
self.x_train.is_some() && self.y_train.is_some()
}
}
impl Default for GaussianProcess {
fn default() -> Self {
Self::new()
}
}