use crate::kernels::Kernel;
use crate::marginal_likelihood::log_marginal_likelihood;
use scirs2_core::ndarray::{ArrayView1, ArrayView2};
use sklears_core::error::{Result as SklResult, SklearsError};
#[derive(Debug, Clone)]
pub struct KernelOptimizer {
pub max_iter: usize,
pub learning_rate: f64,
pub tol: f64,
pub use_line_search: bool,
pub n_restarts: usize,
pub bounds: Option<Vec<(f64, f64)>>,
pub random_state: Option<u64>,
}
impl Default for KernelOptimizer {
fn default() -> Self {
Self {
max_iter: 100,
learning_rate: 0.01,
tol: 1e-6,
use_line_search: true,
n_restarts: 5,
bounds: None,
random_state: Some(42),
}
}
}
#[derive(Debug, Clone)]
pub struct OptimizationResult {
pub optimized_params: Vec<f64>,
pub final_objective: f64,
pub n_iterations: usize,
pub converged: bool,
pub history: Vec<f64>,
pub final_gradient_norm: f64,
}
#[allow(non_snake_case)]
impl KernelOptimizer {
pub fn new() -> Self {
Self::default()
}
pub fn max_iter(mut self, max_iter: usize) -> Self {
self.max_iter = max_iter;
self
}
pub fn learning_rate(mut self, learning_rate: f64) -> Self {
self.learning_rate = learning_rate;
self
}
pub fn tol(mut self, tol: f64) -> Self {
self.tol = tol;
self
}
pub fn use_line_search(mut self, use_line_search: bool) -> Self {
self.use_line_search = use_line_search;
self
}
pub fn n_restarts(mut self, n_restarts: usize) -> Self {
self.n_restarts = n_restarts;
self
}
pub fn bounds(mut self, bounds: Vec<(f64, f64)>) -> Self {
self.bounds = Some(bounds);
self
}
pub fn random_state(mut self, random_state: Option<u64>) -> Self {
self.random_state = random_state;
self
}
pub fn optimize_kernel(
&self,
kernel: &mut dyn Kernel,
X: ArrayView2<f64>,
y: ArrayView1<f64>,
) -> SklResult<OptimizationResult> {
let mut best_result = None;
let mut best_objective = f64::INFINITY;
for restart in 0..self.n_restarts {
let seed = self.random_state.map(|s| s + restart as u64);
let initial_params = if restart == 0 {
kernel.get_params()
} else {
self.random_initialize_params(kernel, seed)?
};
let result = self.optimize_from_initial(kernel, &X, &y, initial_params)?;
if result.final_objective < best_objective {
best_objective = result.final_objective;
best_result = Some(result);
}
}
let final_result = best_result.ok_or_else(|| {
SklearsError::InvalidOperation("No optimization result found".to_string())
})?;
kernel.set_params(&final_result.optimized_params)?;
Ok(final_result)
}
fn optimize_from_initial(
&self,
kernel: &mut dyn Kernel,
X: &ArrayView2<f64>,
y: &ArrayView1<f64>,
initial_params: Vec<f64>,
) -> SklResult<OptimizationResult> {
let mut params = initial_params;
let mut history = Vec::new();
let mut converged = false;
let mut m = vec![0.0; params.len()]; let mut v = vec![0.0; params.len()]; let beta1 = 0.9;
let beta2 = 0.999;
let epsilon = 1e-8;
for iteration in 0..self.max_iter {
kernel.set_params(¶ms)?;
let (objective, gradient) = self.compute_objective_and_gradient(kernel, X, y)?;
history.push(objective);
let gradient_norm = gradient.iter().map(|x| x * x).sum::<f64>().sqrt();
if gradient_norm < self.tol {
converged = true;
break;
}
let clipped_gradient = self.apply_bounds_to_gradient(¶ms, &gradient);
for i in 0..params.len() {
m[i] = beta1 * m[i] + (1.0 - beta1) * clipped_gradient[i];
v[i] = beta2 * v[i] + (1.0 - beta2) * clipped_gradient[i] * clipped_gradient[i];
let m_hat = m[i] / (1.0 - beta1.powi(iteration as i32 + 1));
let v_hat = v[i] / (1.0 - beta2.powi(iteration as i32 + 1));
let update = self.learning_rate * m_hat / (v_hat.sqrt() + epsilon);
params[i] -= update;
}
self.apply_bounds_to_params(&mut params);
if self.use_line_search && iteration % 10 == 0 {
params = self.line_search(kernel, X, y, ¶ms, &clipped_gradient)?;
}
}
let final_gradient = self.compute_gradient(kernel, X, y)?;
let final_gradient_norm = final_gradient.iter().map(|x| x * x).sum::<f64>().sqrt();
Ok(OptimizationResult {
optimized_params: params,
final_objective: history.last().copied().unwrap_or(f64::INFINITY),
n_iterations: history.len(),
converged,
history,
final_gradient_norm,
})
}
#[allow(non_snake_case)]
fn compute_objective_and_gradient(
&self,
kernel: &dyn Kernel,
X: &ArrayView2<f64>,
y: &ArrayView1<f64>,
) -> SklResult<(f64, Vec<f64>)> {
let X_owned = X.to_owned();
let y_owned = y.to_owned();
let neg_log_ml = -log_marginal_likelihood(&X_owned.view(), &y_owned.view(), kernel, 1e-6)?;
let gradient = self.compute_gradient(kernel, X, y)?;
Ok((neg_log_ml, gradient))
}
#[allow(non_snake_case)]
fn compute_gradient(
&self,
kernel: &dyn Kernel,
X: &ArrayView2<f64>,
y: &ArrayView1<f64>,
) -> SklResult<Vec<f64>> {
let params = kernel.get_params();
let mut gradient = vec![0.0; params.len()];
let h = 1e-6;
let X_owned = X.to_owned();
let y_owned = y.to_owned();
for i in 0..params.len() {
let mut params_plus = params.clone();
let mut params_minus = params.clone();
params_plus[i] += h;
params_minus[i] -= h;
let mut kernel_plus = kernel.clone_box();
let mut kernel_minus = kernel.clone_box();
kernel_plus.set_params(¶ms_plus)?;
kernel_minus.set_params(¶ms_minus)?;
let f_plus = -log_marginal_likelihood(
&X_owned.view(),
&y_owned.view(),
kernel_plus.as_ref(),
1e-6,
)?;
let f_minus = -log_marginal_likelihood(
&X_owned.view(),
&y_owned.view(),
kernel_minus.as_ref(),
1e-6,
)?;
gradient[i] = (f_plus - f_minus) / (2.0 * h);
}
Ok(gradient)
}
fn apply_bounds_to_gradient(&self, params: &[f64], gradient: &[f64]) -> Vec<f64> {
if let Some(bounds) = &self.bounds {
let mut clipped = gradient.to_vec();
for i in 0..params.len() {
if i < bounds.len() {
let (lower, upper) = bounds[i];
if params[i] <= lower && gradient[i] < 0.0 {
clipped[i] = 0.0;
}
if params[i] >= upper && gradient[i] > 0.0 {
clipped[i] = 0.0;
}
}
}
clipped
} else {
gradient.to_vec()
}
}
fn apply_bounds_to_params(&self, params: &mut [f64]) {
if let Some(bounds) = &self.bounds {
for i in 0..params.len() {
if i < bounds.len() {
let (lower, upper) = bounds[i];
params[i] = params[i].max(lower).min(upper);
}
}
}
}
#[allow(non_snake_case)]
fn line_search(
&self,
kernel: &mut dyn Kernel,
X: &ArrayView2<f64>,
y: &ArrayView1<f64>,
params: &[f64],
direction: &[f64],
) -> SklResult<Vec<f64>> {
let mut alpha = 1.0;
let rho = 0.5;
let c1 = 1e-4;
kernel.set_params(params)?;
let X_owned = X.to_owned();
let y_owned = y.to_owned();
let f0 = -log_marginal_likelihood(&X_owned.view(), &y_owned.view(), kernel, 1e-6)?;
let grad_f = self.compute_gradient(kernel, X, y)?;
let dir_deriv: f64 = grad_f
.iter()
.zip(direction.iter())
.map(|(g, d)| g * d)
.sum();
for _ in 0..20 {
let new_params: Vec<f64> = params
.iter()
.zip(direction.iter())
.map(|(p, d)| p - alpha * d)
.collect();
let mut bounded_params = new_params;
self.apply_bounds_to_params(&mut bounded_params);
kernel.set_params(&bounded_params)?;
let f_new = -log_marginal_likelihood(&X_owned.view(), &y_owned.view(), kernel, 1e-6)?;
if f_new <= f0 + c1 * alpha * dir_deriv {
return Ok(bounded_params);
}
alpha *= rho;
}
Ok(params.to_vec())
}
fn random_initialize_params(
&self,
kernel: &dyn Kernel,
seed: Option<u64>,
) -> SklResult<Vec<f64>> {
let current_params = kernel.get_params();
let mut params = current_params.clone();
let scale = 0.5; #[allow(clippy::needless_range_loop)] for i in 0..params.len() {
let noise = if let Some(s) = seed {
let a = 1664525u64;
let c = 1013904223u64;
let m = 2u64.pow(32);
let x = (a.wrapping_mul(s + i as u64).wrapping_add(c)) % m;
(x as f64 / m as f64 - 0.5) * 2.0 } else {
0.0 };
params[i] *= 1.0 + scale * noise;
params[i] = params[i].abs().max(1e-6);
}
self.apply_bounds_to_params(&mut params);
Ok(params)
}
}
#[allow(non_snake_case)]
pub fn optimize_kernel_parameters(
kernel: &mut dyn Kernel,
X: ArrayView2<f64>,
y: ArrayView1<f64>,
optimizer_config: Option<KernelOptimizer>,
) -> SklResult<OptimizationResult> {
let optimizer = optimizer_config.unwrap_or_default();
optimizer.optimize_kernel(kernel, X, y)
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
use crate::kernels::{Kernel, RBF};
use scirs2_core::ndarray::{Array1, Array2};
#[test]
fn test_kernel_optimizer_creation() {
let optimizer = KernelOptimizer::new();
assert_eq!(optimizer.max_iter, 100);
assert_eq!(optimizer.n_restarts, 5);
assert!(optimizer.use_line_search);
}
#[test]
#[allow(non_snake_case)]
fn test_kernel_optimization() {
let optimizer = KernelOptimizer::new()
.max_iter(10)
.n_restarts(1)
.learning_rate(0.1);
let X = Array2::from_shape_vec((5, 1), vec![1.0, 2.0, 3.0, 4.0, 5.0])
.expect("shape and data length should match");
let y = Array1::from_vec(vec![1.0, 4.0, 9.0, 16.0, 25.0]);
let mut kernel: Box<dyn Kernel> = Box::new(RBF::new(1.0));
let result = optimizer
.optimize_kernel(kernel.as_mut(), X.view(), y.view())
.expect("operation should succeed");
assert!(result.final_objective.is_finite());
assert_eq!(result.n_iterations, 10); assert!(!result.history.is_empty());
}
#[test]
fn test_bounds_application() {
let bounds = vec![(0.1, 10.0)];
let optimizer = KernelOptimizer::new().bounds(bounds);
let mut params = vec![-1.0]; optimizer.apply_bounds_to_params(&mut params);
assert_eq!(params[0], 0.1);
params[0] = 15.0; optimizer.apply_bounds_to_params(&mut params);
assert_eq!(params[0], 10.0);
}
#[test]
#[allow(non_snake_case)]
fn test_gradient_computation() {
let optimizer = KernelOptimizer::new();
let kernel: Box<dyn Kernel> = Box::new(RBF::new(1.0));
let X = Array2::from_shape_vec((3, 1), vec![1.0, 2.0, 3.0])
.expect("shape and data length should match");
let y = Array1::from_vec(vec![1.0, 2.0, 3.0]);
let gradient = optimizer
.compute_gradient(kernel.as_ref(), &X.view(), &y.view())
.expect("operation should succeed");
assert_eq!(gradient.len(), 1); assert!(gradient[0].is_finite());
}
#[test]
fn test_random_initialization() {
let optimizer = KernelOptimizer::new();
let kernel: Box<dyn Kernel> = Box::new(RBF::new(1.0));
let params1 = optimizer
.random_initialize_params(kernel.as_ref(), Some(42))
.expect("operation should succeed");
let params2 = optimizer
.random_initialize_params(kernel.as_ref(), Some(42))
.expect("operation should succeed");
let params3 = optimizer
.random_initialize_params(kernel.as_ref(), Some(43))
.expect("operation should succeed");
assert_eq!(params1[0], params2[0]);
assert_ne!(params1[0], params3[0]);
assert!(params1[0] > 0.0);
assert!(params3[0] > 0.0);
}
}