use super::config::{EarlyStopping, LearningRateSchedule, OptimizerType};
#[derive(Debug, Clone)]
pub struct TrainingState {
pub epoch: usize,
pub batch: usize,
pub total_batches: usize,
pub current_lr: f32,
pub best_val_loss: f32,
pub epochs_without_improvement: usize,
pub momentum_buffer: Vec<f32>,
pub adam_m: Vec<f32>,
pub adam_v: Vec<f32>,
pub adam_t: usize,
pub train_loss_history: Vec<f32>,
pub val_loss_history: Vec<f32>,
pub train_acc_history: Vec<f32>,
pub val_acc_history: Vec<f32>,
}
impl TrainingState {
#[must_use]
pub fn new(num_params: usize, initial_lr: f32) -> Self {
Self {
epoch: 0,
batch: 0,
total_batches: 0,
current_lr: initial_lr,
best_val_loss: f32::MAX,
epochs_without_improvement: 0,
momentum_buffer: vec![0.0; num_params],
adam_m: vec![0.0; num_params],
adam_v: vec![0.0; num_params],
adam_t: 0,
train_loss_history: Vec::new(),
val_loss_history: Vec::new(),
train_acc_history: Vec::new(),
val_acc_history: Vec::new(),
}
}
pub fn update_learning_rate(
&mut self,
base_lr: f32,
schedule: &LearningRateSchedule,
total_epochs: usize,
) {
self.current_lr = match schedule {
LearningRateSchedule::Constant => base_lr,
LearningRateSchedule::StepDecay {
decay_factor,
step_size,
} => {
let num_decays = self.epoch / step_size;
base_lr * decay_factor.powi(num_decays as i32)
}
LearningRateSchedule::CosineAnnealing { min_lr } => {
let progress = self.epoch as f32 / total_epochs as f32;
let cos_value = (std::f32::consts::PI * progress).cos();
min_lr + (base_lr - min_lr) * (1.0 + cos_value) / 2.0
}
LearningRateSchedule::WarmupDecay {
warmup_epochs,
decay_factor,
} => {
if self.epoch < *warmup_epochs {
base_lr * (self.epoch + 1) as f32 / *warmup_epochs as f32
} else {
let epochs_after_warmup = self.epoch - warmup_epochs;
base_lr * decay_factor.powi(epochs_after_warmup as i32)
}
}
};
}
pub fn should_stop(&self, config: &Option<EarlyStopping>) -> bool {
if let Some(es) = config {
self.epochs_without_improvement >= es.patience
} else {
false
}
}
pub fn update_early_stopping(&mut self, val_loss: f32, config: &Option<EarlyStopping>) {
if let Some(es) = config {
if val_loss < self.best_val_loss - es.min_delta {
self.best_val_loss = val_loss;
self.epochs_without_improvement = 0;
} else {
self.epochs_without_improvement += 1;
}
}
}
}
pub fn sgd_update(weights: &mut [f32], gradients: &[f32], lr: f32) {
for (w, g) in weights.iter_mut().zip(gradients.iter()) {
*w -= lr * g;
}
}
pub fn sgd_momentum_update(
weights: &mut [f32],
gradients: &[f32],
momentum_buffer: &mut [f32],
lr: f32,
momentum: f32,
) {
for ((w, g), m) in weights
.iter_mut()
.zip(gradients.iter())
.zip(momentum_buffer.iter_mut())
{
*m = momentum * *m + g;
*w -= lr * *m;
}
}
#[derive(Debug, Clone, Copy)]
pub struct AdamParams {
pub lr: f32,
pub beta1: f32,
pub beta2: f32,
pub epsilon: f32,
}
impl Default for AdamParams {
fn default() -> Self {
Self {
lr: 0.001,
beta1: 0.9,
beta2: 0.999,
epsilon: 1e-8,
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn adam_update(
weights: &mut [f32],
gradients: &[f32],
m: &mut [f32],
v: &mut [f32],
t: usize,
lr: f32,
beta1: f32,
beta2: f32,
epsilon: f32,
) {
let bias_correction1 = 1.0 - beta1.powi(t as i32);
let bias_correction2 = 1.0 - beta2.powi(t as i32);
for i in 0..weights.len() {
m[i] = beta1 * m[i] + (1.0 - beta1) * gradients[i];
v[i] = beta2 * v[i] + (1.0 - beta2) * gradients[i].powi(2);
let m_hat = m[i] / bias_correction1;
let v_hat = v[i] / bias_correction2;
weights[i] -= lr * m_hat / (v_hat.sqrt() + epsilon);
}
}
#[allow(clippy::too_many_arguments)]
pub fn adamw_update(
weights: &mut [f32],
gradients: &[f32],
m: &mut [f32],
v: &mut [f32],
t: usize,
lr: f32,
beta1: f32,
beta2: f32,
epsilon: f32,
weight_decay: f32,
) {
let bias_correction1 = 1.0 - beta1.powi(t as i32);
let bias_correction2 = 1.0 - beta2.powi(t as i32);
for i in 0..weights.len() {
weights[i] -= lr * weight_decay * weights[i];
m[i] = beta1 * m[i] + (1.0 - beta1) * gradients[i];
v[i] = beta2 * v[i] + (1.0 - beta2) * gradients[i].powi(2);
let m_hat = m[i] / bias_correction1;
let v_hat = v[i] / bias_correction2;
weights[i] -= lr * m_hat / (v_hat.sqrt() + epsilon);
}
}
pub fn clip_gradients(gradients: &mut [f32], max_norm: f32) {
let total_norm: f32 = gradients.iter().map(|g| g.powi(2)).sum::<f32>().sqrt();
if total_norm > max_norm {
let scale = max_norm / (total_norm + 1e-6);
for g in gradients.iter_mut() {
*g *= scale;
}
}
}
pub fn apply_optimizer_update(
params: &mut [f32],
gradients: &[f32],
state: &mut TrainingState,
optimizer: &OptimizerType,
) {
match optimizer {
OptimizerType::SGD => {
sgd_update(params, gradients, state.current_lr);
}
OptimizerType::SGDMomentum { momentum } => {
let momentum_f = *momentum as f32 / 100.0;
sgd_momentum_update(
params,
gradients,
&mut state.momentum_buffer,
state.current_lr,
momentum_f,
);
}
OptimizerType::Adam => {
state.adam_t += 1;
adam_update(
params,
gradients,
&mut state.adam_m,
&mut state.adam_v,
state.adam_t,
state.current_lr,
0.9,
0.999,
1e-8,
);
}
OptimizerType::AdamW { weight_decay } => {
state.adam_t += 1;
let wd = *weight_decay as f32 / 100.0;
adamw_update(
params,
gradients,
&mut state.adam_m,
&mut state.adam_v,
state.adam_t,
state.current_lr,
0.9,
0.999,
1e-8,
wd,
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gradient_clipping() {
let mut grads = vec![10.0, 20.0, 30.0];
clip_gradients(&mut grads, 1.0);
let norm: f32 = grads.iter().map(|g| g.powi(2)).sum::<f32>().sqrt();
assert!(norm <= 1.0 + 1e-6);
}
#[test]
fn test_optimizer_sgd() {
let mut weights = vec![1.0, 2.0, 3.0];
let gradients = vec![0.1, 0.2, 0.3];
sgd_update(&mut weights, &gradients, 0.1);
assert!((weights[0] - 0.99).abs() < 1e-6);
assert!((weights[1] - 1.98).abs() < 1e-6);
assert!((weights[2] - 2.97).abs() < 1e-6);
}
#[test]
fn test_optimizer_adam() {
let mut weights = vec![1.0, 2.0, 3.0];
let gradients = vec![0.1, 0.2, 0.3];
let mut m = vec![0.0; 3];
let mut v = vec![0.0; 3];
adam_update(
&mut weights,
&gradients,
&mut m,
&mut v,
1,
0.001,
0.9,
0.999,
1e-8,
);
assert!(weights[0] < 1.0);
assert!(weights[1] < 2.0);
assert!(weights[2] < 3.0);
}
#[test]
fn test_training_state_lr_schedule() {
let mut state = TrainingState::new(100, 0.1);
state.epoch = 50;
state.update_learning_rate(0.1, &LearningRateSchedule::Constant, 100);
assert!((state.current_lr - 0.1).abs() < 1e-6);
state.update_learning_rate(
0.1,
&LearningRateSchedule::StepDecay {
decay_factor: 0.5,
step_size: 10,
},
100,
);
assert!((state.current_lr - 0.003125).abs() < 1e-6);
state.epoch = 50;
state.update_learning_rate(
0.1,
&LearningRateSchedule::CosineAnnealing { min_lr: 0.0 },
100,
);
assert!(state.current_lr > 0.0 && state.current_lr < 0.1);
}
#[test]
fn test_early_stopping() {
let mut state = TrainingState::new(100, 0.1);
let early_stopping = Some(EarlyStopping {
patience: 3,
min_delta: 0.01,
});
assert!(!state.should_stop(&early_stopping));
state.update_early_stopping(1.0, &early_stopping);
assert_eq!(state.epochs_without_improvement, 0);
state.update_early_stopping(1.0, &early_stopping);
assert_eq!(state.epochs_without_improvement, 1);
state.update_early_stopping(0.995, &early_stopping);
assert_eq!(state.epochs_without_improvement, 2);
state.update_early_stopping(1.0, &early_stopping);
assert_eq!(state.epochs_without_improvement, 3);
assert!(state.should_stop(&early_stopping));
}
}