use crate::{Optimizer, OptimizerError, OptimizerResult};
use parking_lot::RwLock;
use std::collections::HashMap;
use std::sync::Arc;
use torsh_tensor::{
creation::{randn, zeros},
Tensor,
};
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct ValidationConfig {
pub tolerance: f32,
pub num_steps: usize,
pub learning_rate: f32,
pub verbose: bool,
}
impl Default for ValidationConfig {
fn default() -> Self {
Self {
tolerance: 1e-4,
num_steps: 10,
learning_rate: 0.01,
verbose: false,
}
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct ValidationResult {
pub passed: bool,
pub max_difference: f32,
pub avg_difference: f32,
pub step_differences: Vec<f32>,
pub metrics: HashMap<String, f32>,
}
#[allow(dead_code)]
pub struct CrossFrameworkValidator {
config: ValidationConfig,
}
#[allow(dead_code)]
impl CrossFrameworkValidator {
pub fn new(config: ValidationConfig) -> Self {
Self { config }
}
pub fn default() -> Self {
Self::new(ValidationConfig::default())
}
pub fn validate_against_pytorch<O>(
&self,
mut torsh_optimizer: O,
pytorch_reference: &[f32],
) -> OptimizerResult<ValidationResult>
where
O: crate::Optimizer,
{
let mut differences = Vec::new();
let mut max_diff = 0.0f32;
let mut sum_diff = 0.0f32;
let param = Arc::new(RwLock::new(zeros(&[2, 2])?));
for step in 0..self.config.num_steps {
let grad_data = vec![0.1, 0.2, 0.3, 0.4];
let grad_tensor = Tensor::from_vec(grad_data, &[2, 2])?;
param.write().set_grad(Some(grad_tensor));
torsh_optimizer.step()?;
let torsh_values = param.read().to_vec()?;
let pytorch_values = &pytorch_reference[step * 4..(step + 1) * 4];
let step_diff = torsh_values
.iter()
.zip(pytorch_values.iter())
.map(|(a, b)| (a - b).abs())
.fold(0.0f32, |acc, x| acc.max(x));
differences.push(step_diff);
max_diff = max_diff.max(step_diff);
sum_diff += step_diff;
if self.config.verbose {
println!("Step {}: max_diff = {:.6}", step, step_diff);
}
}
let avg_diff = sum_diff / self.config.num_steps as f32;
let passed = max_diff < self.config.tolerance;
let mut metrics = HashMap::new();
metrics.insert("convergence_rate".to_string(), avg_diff);
metrics.insert("stability_score".to_string(), 1.0 / (1.0 + max_diff));
Ok(ValidationResult {
passed,
max_difference: max_diff,
avg_difference: avg_diff,
step_differences: differences,
metrics,
})
}
pub fn validate_convergence<O>(&self, mut optimizer: O) -> OptimizerResult<ValidationResult>
where
O: crate::Optimizer,
{
let mut losses = Vec::new();
let param = Arc::new(RwLock::new(randn::<f32>(&[2, 1])?));
for step in 0..self.config.num_steps {
let current = param.read().clone();
let loss = current.pow(2.0)?.sum()?.to_vec()?[0] * 0.5;
losses.push(loss);
param.write().set_grad(Some(current.clone()));
optimizer.step()?;
if self.config.verbose {
println!("Step {}: loss = {:.6}", step, loss);
}
}
let initial_loss = losses[0];
let final_loss = losses[losses.len() - 1];
let loss_reduction = (initial_loss - final_loss) / initial_loss;
let passed = loss_reduction > 0.1;
let mut metrics = HashMap::new();
metrics.insert("initial_loss".to_string(), initial_loss);
metrics.insert("final_loss".to_string(), final_loss);
metrics.insert("loss_reduction".to_string(), loss_reduction);
Ok(ValidationResult {
passed,
max_difference: final_loss,
avg_difference: losses.iter().sum::<f32>() / losses.len() as f32,
step_differences: losses,
metrics,
})
}
pub fn run_validation_suite<O>(
&self,
optimizer: O,
) -> OptimizerResult<HashMap<String, ValidationResult>>
where
O: crate::Optimizer,
{
let mut results = HashMap::new();
let convergence_result = self.validate_convergence(optimizer)?;
results.insert("convergence".to_string(), convergence_result);
Ok(results)
}
fn validate_gradient_descent_properties<O>(
&self,
_optimizer: O,
) -> OptimizerResult<ValidationResult>
where
O: crate::Optimizer,
{
let param = Arc::new(RwLock::new(zeros(&[2, 2])?));
let initial_params = param.read().to_vec()?;
let mut optimizer = crate::SGD::new(vec![param.clone()], 0.1, None, None, None, false);
let grad_tensor = Tensor::from_vec(vec![1.0, 1.0, 1.0, 1.0], &[2, 2])?;
param.write().set_grad(Some(grad_tensor));
optimizer.step()?;
let final_params = param.read().to_vec()?;
let moved_correctly = initial_params
.iter()
.zip(final_params.iter())
.all(|(initial, final_val)| final_val < initial);
let max_movement = initial_params
.iter()
.zip(final_params.iter())
.map(|(initial, final_val)| ((*initial - *final_val) as f32).abs())
.fold(0.0f32, |acc, x| acc.max(x));
let mut metrics = HashMap::new();
metrics.insert("max_movement".to_string(), max_movement);
metrics.insert(
"correct_direction".to_string(),
if moved_correctly { 1.0 } else { 0.0 },
);
Ok(ValidationResult {
passed: moved_correctly,
max_difference: max_movement,
avg_difference: max_movement / 4.0, step_differences: vec![max_movement],
metrics,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{adam::Adam, sgd::SGD};
#[test]
fn test_cross_framework_validator_creation() -> OptimizerResult<()> {
let config = ValidationConfig::default();
let _validator = CrossFrameworkValidator::new(config);
Ok(())
}
#[test]
fn test_convergence_validation() -> OptimizerResult<()> {
let validator = CrossFrameworkValidator::default();
let param = Arc::new(RwLock::new(randn::<f32>(&[2, 1])?));
let optimizer = SGD::new(vec![param], 0.01, None, None, None, false);
let result = validator.validate_convergence(optimizer)?;
assert!(result.metrics.contains_key("loss_reduction"));
Ok(())
}
#[test]
fn test_gradient_descent_properties() -> OptimizerResult<()> {
let validator = CrossFrameworkValidator::default();
let param = Arc::new(RwLock::new(zeros(&[2, 2])?));
let optimizer = SGD::new(vec![param], 0.1, None, None, None, false);
let result = validator.validate_gradient_descent_properties(optimizer)?;
assert_eq!(result.metrics.get("correct_direction"), Some(&1.0));
Ok(())
}
#[test]
fn test_validation_suite() -> OptimizerResult<()> {
let validator = CrossFrameworkValidator::default();
let param = Arc::new(RwLock::new(randn::<f32>(&[2, 1])?));
let optimizer = Adam::new(vec![param], None, None, None, None, false);
let results = validator.run_validation_suite(optimizer)?;
assert!(results.contains_key("convergence"));
Ok(())
}
}