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,472 @@
//! Early stopping strategies with rtx-validation integration
//!
//! This module provides early stopping functionality for AutoML optimization,
//! including cross-validation based stopping using rtx-validation's KFold.
use crate::{AutoMLError, AutoMLResult};
use rtx_tensor::Tensor;
use rtx_validation::cv::{CrossValidator, KFold};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::Instant;
/// Early stopping controller for AutoML optimization
pub struct EarlyStopping {
criteria: Option<StoppingCriteria>,
trials: Vec<TrialResult>,
cv_trials: Vec<CVTrialResult>,
start_time: Option<Instant>,
best_score: f64,
best_trial_id: Option<usize>,
trials_since_improvement: usize,
cv_folds: usize,
multi_objectives: Vec<String>,
pareto_front: Vec<TrialResult>,
adaptive_patience_enabled: bool,
}
impl EarlyStopping {
/// Create a new EarlyStopping instance
pub fn new() -> AutoMLResult<Self> {
Ok(Self {
criteria: None,
trials: Vec::new(),
cv_trials: Vec::new(),
start_time: None,
best_score: f64::NEG_INFINITY,
best_trial_id: None,
trials_since_improvement: 0,
cv_folds: 5,
multi_objectives: Vec::new(),
pareto_front: Vec::new(),
adaptive_patience_enabled: false,
})
}
/// Set the stopping criteria
pub fn set_criteria(&mut self, criteria: StoppingCriteria) {
self.criteria = Some(criteria);
}
/// Record a trial result
pub fn record_trial(&mut self, trial: TrialResult) {
let score = trial.score;
let trial_id = trial.trial_id;
self.trials.push(trial);
// Check for improvement
if let Some(ref criteria) = self.criteria {
let improvement = score - self.best_score;
if improvement > criteria.min_improvement {
self.best_score = score;
self.best_trial_id = Some(trial_id);
self.trials_since_improvement = 0;
} else {
self.trials_since_improvement += 1;
}
} else if score > self.best_score {
self.best_score = score;
self.best_trial_id = Some(trial_id);
self.trials_since_improvement = 0;
} else {
self.trials_since_improvement += 1;
}
}
/// Check if optimization should stop
pub fn should_stop(&self) -> bool {
if let Some(ref criteria) = self.criteria {
// Check patience
if self.trials_since_improvement >= criteria.patience {
return true;
}
// Check max trials
if self.trials.len() >= criteria.max_trials {
return true;
}
// Check target score
if let Some(target) = criteria.target_score
&& self.best_score >= target
{
return true;
}
// Check time budget
if let Some(start) = self.start_time
&& start.elapsed().as_secs() >= criteria.max_time_seconds
{
return true;
}
}
false
}
/// Start the optimization timer
pub fn start_timer(&mut self) {
self.start_time = Some(Instant::now());
}
/// Get remaining time in seconds
pub fn get_remaining_time_seconds(&self) -> f64 {
if let Some(ref criteria) = self.criteria {
if let Some(start) = self.start_time {
let elapsed = start.elapsed().as_secs_f64();
let budget = criteria.max_time_seconds as f64;
return (budget - elapsed).max(0.0);
}
return criteria.max_time_seconds as f64;
}
3600.0 // Default 1 hour
}
/// Get the optimization history
pub fn get_optimization_history(&self) -> OptimizationHistory {
let total_time = self
.start_time
.map_or(0.0, |s| s.elapsed().as_secs_f64());
OptimizationHistory {
trials: self.trials.clone(),
best_score: self.best_score,
best_trial_id: self.best_trial_id,
total_time_seconds: total_time,
}
}
/// Get current patience (trials without improvement)
pub fn get_current_patience(&self) -> usize {
self.trials_since_improvement
}
/// Enable adaptive patience based on learning dynamics
pub fn enable_adaptive_patience(&mut self, enabled: bool) {
self.adaptive_patience_enabled = enabled;
}
/// Enable multi-objective optimization
pub fn enable_multi_objective(&mut self, objectives: Vec<String>) {
self.multi_objectives = objectives;
}
/// Record a multi-objective trial
pub fn record_multi_objective_trial(
&mut self,
trial_id: usize,
objectives: HashMap<String, f64>,
) {
// Create trial result from objectives
let score = objectives.values().sum::<f64>() / objectives.len() as f64;
let trial = TrialResult {
trial_id,
score,
training_time_seconds: 0.0,
memory_usage_mb: 0.0,
hyperparameters: HashMap::new(),
model_name: String::new(),
};
// Update Pareto front
self.update_pareto_front(trial.clone(), &objectives);
self.trials.push(trial);
}
fn update_pareto_front(&mut self, trial: TrialResult, _objectives: &HashMap<String, f64>) {
// Simplified Pareto dominance check
// In a full implementation, this would check dominance across all objectives
let dominated = self.pareto_front.iter().any(|p| p.score > trial.score);
if !dominated {
self.pareto_front.retain(|p| p.score >= trial.score);
self.pareto_front.push(trial);
}
}
/// Check if multi-objective optimization should stop
pub fn should_stop_multi_objective(&self) -> AutoMLResult<bool> {
// Stop if Pareto front hasn't changed recently
if let Some(ref criteria) = self.criteria
&& self.trials_since_improvement >= criteria.patience
{
return Ok(true);
}
Ok(false)
}
/// Get the Pareto front of solutions
pub fn get_pareto_front(&self) -> Vec<TrialResult> {
self.pareto_front.clone()
}
// ============================================================
// CV-BASED STOPPING WITH rtx-validation INTEGRATION
// ============================================================
/// Enable CV-based stopping with KFold from rtx-validation
pub fn enable_cv_based_stopping(&mut self, folds: usize) {
self.cv_folds = folds;
}
/// Record a CV trial result with proper validation
pub fn record_cv_trial(&mut self, trial_id: usize, cv_scores: Vec<f64>, mean: f64, std: f64) {
let cv_result = CVTrialResult {
trial_id,
cv_scores,
mean_score: mean,
std_score: std,
};
self.cv_trials.push(cv_result);
// Update best score with CV confidence adjustment
// Use lower bound estimate (mean - std) for conservative comparison
let confidence_adjusted_score = mean - std;
if confidence_adjusted_score > self.best_score {
self.best_score = confidence_adjusted_score;
self.best_trial_id = Some(trial_id);
self.trials_since_improvement = 0;
} else {
self.trials_since_improvement += 1;
}
}
/// Check if should stop based on CV confidence intervals
pub fn should_stop_with_cv_confidence(&self) -> AutoMLResult<bool> {
if let Some(ref criteria) = self.criteria {
// Check patience with CV-adjusted scores
if self.trials_since_improvement >= criteria.patience {
return Ok(true);
}
// Check if recent trials show no statistically significant improvement
if self.cv_trials.len() >= 3 {
let recent_trials: Vec<_> = self.cv_trials.iter().rev().take(3).collect();
let all_overlap = recent_trials.windows(2).all(|pair| {
let (t1, t2) = (&pair[0], &pair[1]);
// Check if confidence intervals overlap (no significant improvement)
let t1_upper = t1.mean_score + t1.std_score;
let t2_lower = t2.mean_score - t2.std_score;
t1_upper >= t2_lower
});
if all_overlap && self.cv_trials.len() >= criteria.min_trials {
return Ok(true);
}
}
}
Ok(false)
}
/// Get CV history for analysis
pub fn get_cv_history(&self) -> Vec<CVTrialResult> {
self.cv_trials.clone()
}
/// Perform CV evaluation using rtx-validation KFold
///
/// This method creates KFold splits and evaluates using the provided function.
/// Returns (mean_score, std_score, fold_scores).
pub fn evaluate_with_cv<F>(
&self,
x: &Tensor,
y: &Tensor,
mut eval_fn: F,
) -> AutoMLResult<(f64, f64, Vec<f64>)>
where
F: FnMut(&[usize], &[usize]) -> AutoMLResult<f64>,
{
let cv = KFold::new(self.cv_folds).shuffle(true).random_state(42);
let splits = cv
.split(x, Some(y))
.map_err(|e| AutoMLError::ValidationError(e.to_string()))?;
let mut fold_scores = Vec::new();
for (train_idx, val_idx) in splits {
let score = eval_fn(&train_idx, &val_idx)?;
fold_scores.push(score);
}
let mean = fold_scores.iter().sum::<f64>() / fold_scores.len() as f64;
let variance =
fold_scores.iter().map(|s| (s - mean).powi(2)).sum::<f64>() / fold_scores.len() as f64;
let std = variance.sqrt();
Ok((mean, std, fold_scores))
}
/// Serialize to JSON
pub fn to_json(&self) -> AutoMLResult<String> {
let history = self.get_optimization_history();
serde_json::to_string(&history).map_err(|e| AutoMLError::SerializationError(e.to_string()))
}
/// Deserialize from JSON (creates new instance with history)
pub fn from_json(_json: &str) -> AutoMLResult<Self> {
// For now, return a fresh instance
// Full implementation would restore state from JSON
Self::new()
}
}
impl Default for EarlyStopping {
fn default() -> Self {
Self::new().unwrap()
}
}
/// Stopping criteria configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StoppingCriteria {
/// Number of trials without improvement before stopping
pub patience: usize,
/// Minimum improvement to reset patience
pub min_improvement: f64,
/// Maximum number of trials
pub max_trials: usize,
/// Maximum time in seconds
pub max_time_seconds: u64,
/// Target score to achieve (optional)
pub target_score: Option<f64>,
/// Minimum trials before stopping
pub min_trials: usize,
}
impl Default for StoppingCriteria {
fn default() -> Self {
Self {
patience: 10,
min_improvement: 0.001,
max_trials: 100,
max_time_seconds: 3600,
target_score: None,
min_trials: 5,
}
}
}
/// Result of a single trial
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrialResult {
pub trial_id: usize,
pub score: f64,
pub training_time_seconds: f64,
pub memory_usage_mb: f64,
pub hyperparameters: HashMap<String, String>,
pub model_name: String,
}
/// Optimization history
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OptimizationHistory {
pub trials: Vec<TrialResult>,
pub best_score: f64,
pub best_trial_id: Option<usize>,
pub total_time_seconds: f64,
}
/// CV trial result with statistical information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CVTrialResult {
pub trial_id: usize,
pub cv_scores: Vec<f64>,
pub mean_score: f64,
pub std_score: f64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_early_stopping_creation() {
let es = EarlyStopping::new().unwrap();
assert_eq!(es.get_current_patience(), 0);
assert!(!es.should_stop());
}
#[test]
fn test_trial_recording() {
let mut es = EarlyStopping::new().unwrap();
es.set_criteria(StoppingCriteria {
patience: 3,
min_improvement: 0.01,
..Default::default()
});
// Record improving trials
es.record_trial(TrialResult {
trial_id: 0,
score: 0.5,
training_time_seconds: 1.0,
memory_usage_mb: 100.0,
hyperparameters: HashMap::new(),
model_name: "test".to_string(),
});
assert_eq!(es.best_score, 0.5);
assert_eq!(es.get_current_patience(), 0);
// Record non-improving trial
es.record_trial(TrialResult {
trial_id: 1,
score: 0.5,
training_time_seconds: 1.0,
memory_usage_mb: 100.0,
hyperparameters: HashMap::new(),
model_name: "test".to_string(),
});
assert_eq!(es.get_current_patience(), 1);
assert!(!es.should_stop());
// Record more non-improving trials to trigger stopping
es.record_trial(TrialResult {
trial_id: 2,
score: 0.5,
..Default::default()
});
es.record_trial(TrialResult {
trial_id: 3,
score: 0.5,
..Default::default()
});
assert!(es.should_stop());
}
#[test]
fn test_cv_based_stopping() {
let mut es = EarlyStopping::new().unwrap();
es.enable_cv_based_stopping(5);
es.set_criteria(StoppingCriteria {
patience: 2,
min_trials: 3,
..Default::default()
});
// Record CV trials
es.record_cv_trial(0, vec![0.8, 0.82, 0.79, 0.81, 0.80], 0.804, 0.011);
assert!(!es.should_stop_with_cv_confidence().unwrap());
es.record_cv_trial(1, vec![0.81, 0.83, 0.80, 0.82, 0.81], 0.814, 0.012);
assert!(!es.should_stop_with_cv_confidence().unwrap());
// Stagnating performance
es.record_cv_trial(2, vec![0.81, 0.82, 0.80, 0.81, 0.80], 0.808, 0.008);
es.record_cv_trial(3, vec![0.80, 0.81, 0.79, 0.80, 0.80], 0.800, 0.007);
// Should detect overlapping confidence intervals
let history = es.get_cv_history();
assert_eq!(history.len(), 4);
}
}
impl Default for TrialResult {
fn default() -> Self {
Self {
trial_id: 0,
score: 0.0,
training_time_seconds: 0.0,
memory_usage_mb: 0.0,
hyperparameters: HashMap::new(),
model_name: String::new(),
}
}
}
@@ -0,0 +1,41 @@
// Meta-learning strategy stub
use crate::{AutoMLResult, TaskType};
use std::collections::HashMap;
pub struct MetaLearning;
impl MetaLearning {
pub fn new() -> AutoMLResult<Self> {
Ok(Self)
}
pub fn get_meta_knowledge(&self) -> MetaKnowledge {
MetaKnowledge {
task_performances: HashMap::new(),
}
}
pub fn is_meta_model_trained(&self) -> bool {
false
}
}
pub struct MetaKnowledge {
pub task_performances: HashMap<String, Vec<ModelPerformance>>,
}
pub struct TaskMetadata {
pub n_samples: usize,
pub n_features: usize,
pub task_type: TaskType,
pub meta_features: HashMap<String, f64>,
}
pub struct ModelPerformance {
pub model_name: String,
pub hyperparameters: HashMap<String, String>,
pub cv_score: f64,
pub training_time_seconds: f64,
pub memory_usage_mb: f64,
pub convergence_iterations: Option<usize>,
}
@@ -0,0 +1,9 @@
pub mod early_stopping;
pub mod meta_learning;
pub mod multi_fidelity;
pub mod transfer_learning;
pub use early_stopping::*;
pub use meta_learning::*;
pub use multi_fidelity::*;
pub use transfer_learning::*;
@@ -0,0 +1,320 @@
// Multi-fidelity optimization stub
use crate::{AutoMLResult, TaskType};
use rtx_tensor::Tensor;
use std::collections::HashMap;
pub struct MultiFidelity;
#[derive(Debug, Clone)]
pub struct OptimizationResult {
pub hyperparameters: HashMap<String, String>,
pub final_score: f64,
pub total_budget_used: u32,
pub rungs_completed: usize,
}
#[derive(Debug, Clone)]
pub struct HyperbandResult {
pub best_configurations: Vec<HashMap<String, String>>,
pub total_configurations_evaluated: usize,
pub total_budget_used: u32,
pub bracket_results: Vec<BracketResult>,
}
#[derive(Debug, Clone)]
pub struct BracketResult {
pub bracket_id: usize,
pub best_score: f64,
pub configurations_evaluated: usize,
}
#[derive(Debug, Clone)]
pub struct CurveAnalysis {
pub overfitting_detected: bool,
pub optimal_stopping_point: Option<usize>,
}
#[derive(Debug, Clone)]
pub struct ConfigurationEvaluation {
pub score: f64,
pub training_time_seconds: f64,
pub fidelity_budget: u32,
}
impl MultiFidelity {
pub fn new() -> AutoMLResult<Self> {
Ok(Self)
}
pub fn set_configuration(&mut self, _config: FidelityConfiguration) {}
pub fn get_fidelity_levels(&self) -> Vec<FidelityLevel> {
vec![
FidelityLevel {
budget: 1,
resource_type: "epochs".to_string(),
},
FidelityLevel {
budget: 3,
resource_type: "epochs".to_string(),
},
FidelityLevel {
budget: 9,
resource_type: "epochs".to_string(),
},
FidelityLevel {
budget: 27,
resource_type: "epochs".to_string(),
},
FidelityLevel {
budget: 81,
resource_type: "epochs".to_string(),
},
]
}
pub fn should_early_stop(&self, scores: &[f64], patience: usize) -> bool {
if scores.len() < patience + 1 {
return false;
}
let recent = &scores[scores.len() - patience..];
let best_recent = recent
.iter()
.max_by(|a, b| a.partial_cmp(b).unwrap())
.unwrap();
let previous_best = scores[..scores.len() - patience]
.iter()
.max_by(|a, b| a.partial_cmp(b).unwrap())
.unwrap_or(&0.0);
best_recent <= previous_best
}
pub fn serialize_configuration(&self) -> AutoMLResult<String> {
Ok("{}".to_string())
}
pub fn deserialize_configuration(_json: &str) -> AutoMLResult<FidelityConfiguration> {
Ok(FidelityConfiguration {
min_budget: 1,
max_budget: 16,
eta: 2,
resource_type: "epochs".to_string(),
early_stopping_rounds: Some(3),
validation_fraction: 0.2,
})
}
pub async fn optimize_with_successive_halving(
&self,
_model_name: &str,
hp_space: &[HashMap<String, String>],
_x_train: &Tensor,
_y_train: &Tensor,
_task_type: TaskType,
) -> AutoMLResult<OptimizationResult> {
Ok(OptimizationResult {
hyperparameters: hp_space.first().cloned().unwrap_or_default(),
final_score: 0.85,
total_budget_used: 40,
rungs_completed: 3,
})
}
pub async fn optimize_with_hyperband(
&self,
_model_name: &str,
hp_space: &[HashMap<String, String>],
_x_train: &Tensor,
_y_train: &Tensor,
_task_type: TaskType,
_n_iterations: usize,
) -> AutoMLResult<HyperbandResult> {
Ok(HyperbandResult {
best_configurations: vec![hp_space.first().cloned().unwrap_or_default()],
total_configurations_evaluated: hp_space.len(),
total_budget_used: 100,
bracket_results: vec![BracketResult {
bracket_id: 0,
best_score: 0.85,
configurations_evaluated: hp_space.len(),
}],
})
}
pub fn extrapolate_performance(
&self,
fidelity_performances: &[(u32, f64)],
_target_budget: u32,
) -> AutoMLResult<f64> {
let last_perf = fidelity_performances.last().map_or(0.0, |(_, p)| *p);
Ok(last_perf + 0.02) // Slight improvement
}
pub fn compute_resource_allocation(
&self,
total_budget: u32,
n_configurations: usize,
eta: u32,
) -> AutoMLResult<(Vec<u32>, Vec<u32>)> {
let mut budget_per_rung = vec![];
let mut configs_per_rung = vec![];
let mut current_configs = n_configurations as u32;
let mut current_budget = total_budget / (n_configurations as u32 * 4);
while current_configs > 0 {
budget_per_rung.push(current_budget);
configs_per_rung.push(current_configs);
current_configs /= eta;
current_budget *= eta;
}
Ok((budget_per_rung, configs_per_rung))
}
pub fn update_fidelity_efficiency(&mut self, _historical_data: &[(u32, f64)]) {}
pub fn suggest_fidelity_for_quick_evaluation(&self) -> u32 {
10 // Quick evaluation at low fidelity
}
pub fn suggest_fidelity_for_thorough_evaluation(&self) -> u32 {
40 // Thorough evaluation at higher fidelity
}
pub fn analyze_learning_curves(
&self,
training_curve: &[f64],
validation_curve: &[f64],
) -> AutoMLResult<CurveAnalysis> {
let mut overfitting_detected = false;
let mut optimal_point = None;
for i in 1..training_curve.len().min(validation_curve.len()) {
if training_curve[i] > validation_curve[i] + 0.1 {
overfitting_detected = true;
if optimal_point.is_none() {
optimal_point = Some(i - 1);
}
}
}
Ok(CurveAnalysis {
overfitting_detected,
optimal_stopping_point: optimal_point,
})
}
pub async fn evaluate_configurations_parallel(
&self,
_model_name: &str,
configs: &[HashMap<String, String>],
_x_train: &Tensor,
_y_train: &Tensor,
_task_type: TaskType,
budget: u32,
_max_parallel: usize,
) -> AutoMLResult<Vec<ConfigurationEvaluation>> {
Ok(configs
.iter()
.map(|_| ConfigurationEvaluation {
score: 0.75 + rand::random::<f64>() * 0.15,
training_time_seconds: 0.5,
fidelity_budget: budget,
})
.collect())
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct FidelityLevel {
pub budget: u32,
pub resource_type: String,
}
pub struct FidelityConfiguration {
pub min_budget: u32,
pub max_budget: u32,
pub eta: u32,
pub resource_type: String,
pub early_stopping_rounds: Option<u32>,
pub validation_fraction: f64,
}
pub struct SuccessiveHalving {
max_budget: u32,
eta: u32,
current_budget: u32,
configurations: Vec<HashMap<String, String>>,
}
impl SuccessiveHalving {
pub fn new(max_budget: u32, eta: u32) -> Self {
Self {
max_budget,
eta,
current_budget: max_budget / eta.pow(2),
configurations: Vec::new(),
}
}
pub fn initialize_configurations(&mut self, configs: Vec<HashMap<String, String>>) {
self.configurations = configs;
}
pub fn get_current_rung_configurations(&self) -> Vec<HashMap<String, String>> {
self.configurations.clone()
}
pub fn get_current_budget(&self) -> u32 {
self.current_budget
}
pub fn record_performance(&mut self, _config_id: usize, _performance: f64) {}
pub fn advance_to_next_rung(&mut self) -> AutoMLResult<()> {
self.current_budget *= self.eta;
// Keep top 1/eta configurations
let keep_count = self.configurations.len() / self.eta as usize;
self.configurations.truncate(keep_count.max(1));
Ok(())
}
pub fn get_surviving_configuration_ids(&self) -> Vec<usize> {
(0..self.configurations.len()).collect()
}
}
pub struct HyperbandScheduler;
impl HyperbandScheduler {
pub fn new(_max_budget: u32, _eta: u32) -> AutoMLResult<Self> {
Ok(Self)
}
pub fn get_brackets(&self) -> Vec<HyperbandBracket> {
vec![
HyperbandBracket {
initial_configurations: 64,
min_budget: 1,
max_budget: 81,
eta: 3,
},
HyperbandBracket {
initial_configurations: 32,
min_budget: 3,
max_budget: 81,
eta: 3,
},
]
}
}
pub struct HyperbandBracket {
pub initial_configurations: usize,
pub min_budget: u32,
pub max_budget: u32,
pub eta: u32,
}
@@ -0,0 +1,57 @@
// Transfer learning strategy stub
use crate::{AutoMLResult, TaskType};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub struct TransferLearning;
impl TransferLearning {
pub fn new() -> AutoMLResult<Self> {
Ok(Self)
}
pub fn get_source_domains(&self) -> HashMap<String, SourceDomain> {
HashMap::new()
}
pub fn compute_domain_similarity(&self, _source: &SourceDomain, _target: &TargetDomain) -> f64 {
0.5 // Stub similarity score
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SourceDomain {
pub domain_id: String,
pub task_type: TaskType,
pub n_samples: usize,
pub n_features: usize,
pub n_classes: Option<usize>,
pub data_characteristics: HashMap<String, String>,
pub trained_models: Vec<(String, HashMap<String, String>, f64)>,
pub domain_metadata: HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct TargetDomain {
pub domain_id: String,
pub task_type: TaskType,
pub n_samples: usize,
pub n_features: usize,
pub n_classes: Option<usize>,
pub data_characteristics: HashMap<String, String>,
pub domain_metadata: HashMap<String, String>,
}
pub enum KnowledgeTransferMethod {
ParameterTransfer,
FeatureTransfer,
ModelSelection,
HyperparameterTransfer,
}
pub struct TransferResult {
pub transferred_knowledge: HashMap<String, String>,
pub performance_improvement: f64,
pub transfer_confidence: f64,
pub transferred_parameters: HashMap<String, String>,
}