Skip to main content

trustformers_optim/
convergence.rs

1//! # Convergence Improvement Methods
2//!
3//! This module implements advanced optimization techniques that improve convergence
4//! speed, stability, and final performance through sophisticated momentum variants
5//! and variance reduction methods.
6//!
7//! ## Available Methods
8//!
9//! - **QHM (Quasi-Hyperbolic Momentum)**: Generalizes momentum and Nesterov acceleration
10//! - **AggMo (Aggregated Momentum)**: Maintains multiple momentum buffers for better convergence
11//! - **SVRG (Stochastic Variance Reduced Gradient)**: Reduces gradient variance for better convergence
12//! - **SAG (Stochastic Average Gradient)**: Maintains running average of gradients
13//! - **Nesterov Accelerated Gradient (NAG)**: Classical acceleration method with lookahead
14//! - **Heavy Ball Method**: Momentum-based acceleration with inertia
15//! - **FISTA**: Fast Iterative Shrinkage-Thresholding Algorithm for proximal methods
16//! - **Adaptive Batch Sizing**: Dynamically adjusts batch size based on training progress
17//! - **Loss Surface Smoothing**: Reduces noise in the loss surface for better convergence
18
19use crate::optimizer::OptimizerState;
20use anyhow::{anyhow, Result};
21use serde::{Deserialize, Serialize};
22use std::collections::HashMap;
23use trustformers_core::tensor::Tensor;
24
25/// Configuration for Quasi-Hyperbolic Momentum (QHM).
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct QHMConfig {
28    /// Learning rate
29    pub learning_rate: f32,
30    /// Momentum parameter (β)
31    pub momentum: f32,
32    /// Averaging parameter (ν) - controls interpolation between current gradient and momentum
33    pub nu: f32,
34    /// Weight decay
35    pub weight_decay: f32,
36}
37
38impl Default for QHMConfig {
39    fn default() -> Self {
40        Self {
41            learning_rate: 1e-3,
42            momentum: 0.9,
43            nu: 0.7,
44            weight_decay: 0.0,
45        }
46    }
47}
48
49/// Quasi-Hyperbolic Momentum optimizer.
50///
51/// QHM interpolates between the current gradient and the momentum buffer,
52/// providing a generalization of both momentum and Nesterov acceleration.
53/// Update rule: p = p - lr * (nu * g + (1 - nu) * momentum)
54#[derive(Debug)]
55pub struct QHM {
56    config: QHMConfig,
57    momentum_buffers: HashMap<usize, Tensor>,
58    current_step: usize,
59}
60
61impl QHM {
62    /// Create a new QHM optimizer.
63    pub fn new(config: QHMConfig) -> Self {
64        Self {
65            config,
66            momentum_buffers: HashMap::new(),
67            current_step: 0,
68        }
69    }
70
71    /// Create QHM with default configuration.
72    pub fn with_defaults(learning_rate: f32, momentum: f32, nu: f32) -> Self {
73        Self::new(QHMConfig {
74            learning_rate,
75            momentum,
76            nu,
77            weight_decay: 0.0,
78        })
79    }
80
81    /// Get the configuration.
82    pub fn get_config(&self) -> &QHMConfig {
83        &self.config
84    }
85
86    /// Update configuration.
87    pub fn set_config(&mut self, config: QHMConfig) {
88        self.config = config;
89    }
90}
91
92impl OptimizerState for QHM {
93    fn zero_grad(&mut self) -> Result<()> {
94        // QHM doesn't need explicit gradient zeroing
95        Ok(())
96    }
97
98    fn step(&mut self, parameters: &mut [Tensor]) -> Result<()> {
99        self.current_step += 1;
100
101        for (param_id, parameter) in parameters.iter_mut().enumerate() {
102            // Access gradient from parameter (should be computed during forward/backward pass)
103            let gradient = match parameter.grad() {
104                Ok(grad) => grad,
105                Err(_) => {
106                    // If gradient is not available, skip this parameter
107                    continue;
108                },
109            };
110
111            // Apply weight decay to gradient
112            let effective_grad = if self.config.weight_decay > 0.0 {
113                gradient.add(&parameter.mul_scalar(self.config.weight_decay)?)?
114            } else {
115                gradient
116            };
117
118            // Get or initialize momentum buffer
119            let momentum_buffer = if let Some(buffer) = self.momentum_buffers.get(&param_id) {
120                // Update momentum: momentum = β * momentum + (1 - β) * grad
121                let updated = buffer
122                    .mul_scalar(self.config.momentum)?
123                    .add(&effective_grad.mul_scalar(1.0 - self.config.momentum)?)?;
124                self.momentum_buffers.insert(param_id, updated.clone());
125                updated
126            } else {
127                // Initialize momentum buffer with current gradient
128                let initial_momentum = effective_grad.clone();
129                self.momentum_buffers.insert(param_id, initial_momentum.clone());
130                initial_momentum
131            };
132
133            // QHM update: interpolate between current gradient and momentum
134            let update_direction = effective_grad
135                .mul_scalar(self.config.nu)?
136                .add(&momentum_buffer.mul_scalar(1.0 - self.config.nu)?)?;
137
138            // Apply update
139            *parameter = parameter.sub(&update_direction.mul_scalar(self.config.learning_rate)?)?;
140        }
141
142        Ok(())
143    }
144
145    fn get_lr(&self) -> f32 {
146        self.config.learning_rate
147    }
148
149    fn set_lr(&mut self, lr: f32) {
150        self.config.learning_rate = lr;
151    }
152
153    fn state_dict(&self) -> Result<HashMap<String, Tensor>> {
154        let mut state = HashMap::new();
155
156        // Save configuration
157        state.insert(
158            "learning_rate".to_string(),
159            Tensor::scalar(self.config.learning_rate)?,
160        );
161        state.insert(
162            "momentum".to_string(),
163            Tensor::scalar(self.config.momentum)?,
164        );
165        state.insert("nu".to_string(), Tensor::scalar(self.config.nu)?);
166        state.insert(
167            "weight_decay".to_string(),
168            Tensor::scalar(self.config.weight_decay)?,
169        );
170        state.insert(
171            "current_step".to_string(),
172            Tensor::scalar(self.current_step as f32)?,
173        );
174
175        // Save momentum buffers
176        for (&param_id, buffer) in &self.momentum_buffers {
177            state.insert(format!("momentum_buffer_{}", param_id), buffer.clone());
178        }
179
180        Ok(state)
181    }
182
183    fn load_state_dict(&mut self, state: HashMap<String, Tensor>) -> Result<()> {
184        // Load configuration
185        if let Some(lr) = state.get("learning_rate") {
186            self.config.learning_rate = lr.to_scalar()?;
187        }
188        if let Some(momentum) = state.get("momentum") {
189            self.config.momentum = momentum.to_scalar()?;
190        }
191        if let Some(nu) = state.get("nu") {
192            self.config.nu = nu.to_scalar()?;
193        }
194        if let Some(wd) = state.get("weight_decay") {
195            self.config.weight_decay = wd.to_scalar()?;
196        }
197        if let Some(step) = state.get("current_step") {
198            self.current_step = step.to_scalar()? as usize;
199        }
200
201        // Load momentum buffers
202        self.momentum_buffers.clear();
203        for (key, tensor) in state {
204            if let Some(param_id_str) = key.strip_prefix("momentum_buffer_") {
205                if let Ok(param_id) = param_id_str.parse::<usize>() {
206                    self.momentum_buffers.insert(param_id, tensor);
207                }
208            }
209        }
210
211        Ok(())
212    }
213}
214
215/// Configuration for Aggregated Momentum (AggMo).
216#[derive(Debug, Clone, Serialize, Deserialize)]
217pub struct AggMoConfig {
218    /// Learning rate
219    pub learning_rate: f32,
220    /// List of momentum coefficients
221    pub momentum_coefficients: Vec<f32>,
222    /// Weight decay
223    pub weight_decay: f32,
224}
225
226impl Default for AggMoConfig {
227    fn default() -> Self {
228        Self {
229            learning_rate: 1e-3,
230            momentum_coefficients: vec![0.0, 0.9, 0.99],
231            weight_decay: 0.0,
232        }
233    }
234}
235
236/// Aggregated Momentum optimizer.
237///
238/// AggMo maintains multiple momentum buffers with different decay rates
239/// and averages their contributions to improve convergence.
240#[derive(Debug)]
241pub struct AggMo {
242    config: AggMoConfig,
243    momentum_buffers: HashMap<usize, Vec<Tensor>>, // param_id -> list of momentum buffers
244    current_step: usize,
245}
246
247impl AggMo {
248    /// Create a new AggMo optimizer.
249    pub fn new(config: AggMoConfig) -> Self {
250        assert!(
251            !config.momentum_coefficients.is_empty(),
252            "Must provide at least one momentum coefficient"
253        );
254        Self {
255            config,
256            momentum_buffers: HashMap::new(),
257            current_step: 0,
258        }
259    }
260
261    /// Create AggMo with default configuration.
262    pub fn with_defaults(learning_rate: f32, momentum_coefficients: Vec<f32>) -> Self {
263        Self::new(AggMoConfig {
264            learning_rate,
265            momentum_coefficients,
266            weight_decay: 0.0,
267        })
268    }
269
270    /// Get the configuration.
271    pub fn get_config(&self) -> &AggMoConfig {
272        &self.config
273    }
274
275    /// Get the number of momentum buffers per parameter.
276    pub fn num_momentum_buffers(&self) -> usize {
277        self.config.momentum_coefficients.len()
278    }
279}
280
281impl OptimizerState for AggMo {
282    fn zero_grad(&mut self) -> Result<()> {
283        Ok(())
284    }
285
286    fn step(&mut self, parameters: &mut [Tensor]) -> Result<()> {
287        self.current_step += 1;
288
289        for (param_id, parameter) in parameters.iter_mut().enumerate() {
290            // Access gradient from parameter (should be computed during forward/backward pass)
291            let gradient = match parameter.grad() {
292                Ok(grad) => grad,
293                Err(_) => {
294                    // If gradient is not available, skip this parameter
295                    continue;
296                },
297            };
298
299            // Apply weight decay
300            let effective_grad = if self.config.weight_decay > 0.0 {
301                gradient.add(&parameter.mul_scalar(self.config.weight_decay)?)?
302            } else {
303                gradient
304            };
305
306            // Get or initialize momentum buffers for this parameter
307            let buffers = match self.momentum_buffers.entry(param_id) {
308                std::collections::hash_map::Entry::Occupied(entry) => entry.into_mut(),
309                std::collections::hash_map::Entry::Vacant(entry) => {
310                    // Initialize all momentum buffers with zeros
311                    let init = (0..self.config.momentum_coefficients.len())
312                        .map(|_| Tensor::zeros(&effective_grad.shape()))
313                        .collect::<std::result::Result<Vec<_>, _>>()?;
314                    entry.insert(init)
315                },
316            };
317
318            // Update each momentum buffer
319            let mut aggregated_momentum = Tensor::zeros(&effective_grad.shape())?;
320            for (i, &beta) in self.config.momentum_coefficients.iter().enumerate() {
321                // Update momentum: m_i = β_i * m_i + (1 - β_i) * grad
322                buffers[i] =
323                    buffers[i].mul_scalar(beta)?.add(&effective_grad.mul_scalar(1.0 - beta)?)?;
324
325                // Add to aggregated momentum
326                aggregated_momentum = aggregated_momentum.add(&buffers[i])?;
327            }
328
329            // Average the momentum buffers
330            let num_buffers = self.config.momentum_coefficients.len() as f32;
331            let averaged_momentum = aggregated_momentum.div_scalar(num_buffers)?;
332
333            // Apply update
334            *parameter =
335                parameter.sub(&averaged_momentum.mul_scalar(self.config.learning_rate)?)?;
336        }
337
338        Ok(())
339    }
340
341    fn get_lr(&self) -> f32 {
342        self.config.learning_rate
343    }
344
345    fn set_lr(&mut self, lr: f32) {
346        self.config.learning_rate = lr;
347    }
348
349    fn state_dict(&self) -> Result<HashMap<String, Tensor>> {
350        let mut state = HashMap::new();
351
352        // Save configuration
353        state.insert(
354            "learning_rate".to_string(),
355            Tensor::scalar(self.config.learning_rate)?,
356        );
357        state.insert(
358            "weight_decay".to_string(),
359            Tensor::scalar(self.config.weight_decay)?,
360        );
361        state.insert(
362            "current_step".to_string(),
363            Tensor::scalar(self.current_step as f32)?,
364        );
365        state.insert(
366            "num_momentum_coeffs".to_string(),
367            Tensor::scalar(self.config.momentum_coefficients.len() as f32)?,
368        );
369
370        // Save momentum coefficients
371        for (i, &coeff) in self.config.momentum_coefficients.iter().enumerate() {
372            state.insert(format!("momentum_coeff_{}", i), Tensor::scalar(coeff)?);
373        }
374
375        // Save momentum buffers
376        for (&param_id, buffers) in &self.momentum_buffers {
377            for (buffer_idx, buffer) in buffers.iter().enumerate() {
378                state.insert(
379                    format!("momentum_buffer_{}_{}", param_id, buffer_idx),
380                    buffer.clone(),
381                );
382            }
383        }
384
385        Ok(state)
386    }
387
388    fn load_state_dict(&mut self, state: HashMap<String, Tensor>) -> Result<()> {
389        // Load configuration
390        if let Some(lr) = state.get("learning_rate") {
391            self.config.learning_rate = lr.to_scalar()?;
392        }
393        if let Some(wd) = state.get("weight_decay") {
394            self.config.weight_decay = wd.to_scalar()?;
395        }
396        if let Some(step) = state.get("current_step") {
397            self.current_step = step.to_scalar()? as usize;
398        }
399
400        // Load momentum coefficients
401        if let Some(num_coeffs_tensor) = state.get("num_momentum_coeffs") {
402            let num_coeffs = num_coeffs_tensor.to_scalar()? as usize;
403            let mut coefficients = Vec::with_capacity(num_coeffs);
404            for i in 0..num_coeffs {
405                if let Some(coeff_tensor) = state.get(&format!("momentum_coeff_{}", i)) {
406                    coefficients.push(coeff_tensor.to_scalar()?);
407                }
408            }
409            self.config.momentum_coefficients = coefficients;
410        }
411
412        // Load momentum buffers
413        self.momentum_buffers.clear();
414        let mut param_buffers: HashMap<usize, HashMap<usize, Tensor>> = HashMap::new();
415
416        for (key, tensor) in state {
417            if key.starts_with("momentum_buffer_") {
418                let parts: Vec<&str> = key.split('_').collect();
419                if parts.len() >= 4 {
420                    if let (Ok(param_id), Ok(buffer_idx)) =
421                        (parts[2].parse::<usize>(), parts[3].parse::<usize>())
422                    {
423                        param_buffers.entry(param_id).or_default().insert(buffer_idx, tensor);
424                    }
425                }
426            }
427        }
428
429        // Reconstruct momentum buffers in correct order
430        for (param_id, buffer_map) in param_buffers {
431            let mut buffers = Vec::new();
432            for i in 0..self.config.momentum_coefficients.len() {
433                if let Some(buffer) = buffer_map.get(&i) {
434                    buffers.push(buffer.clone());
435                }
436            }
437            if buffers.len() == self.config.momentum_coefficients.len() {
438                self.momentum_buffers.insert(param_id, buffers);
439            }
440        }
441
442        Ok(())
443    }
444}
445
446/// Configuration for Variance Reduction methods.
447#[derive(Debug, Clone, Serialize, Deserialize)]
448pub struct VarianceReductionConfig {
449    /// Learning rate
450    pub learning_rate: f32,
451    /// Method type
452    pub method: VarianceReductionMethod,
453    /// Gradient history size for SVRG
454    pub history_size: usize,
455    /// Update frequency for full gradient computation
456    pub full_grad_frequency: usize,
457    /// Weight decay
458    pub weight_decay: f32,
459}
460
461impl Default for VarianceReductionConfig {
462    fn default() -> Self {
463        Self {
464            learning_rate: 1e-3,
465            method: VarianceReductionMethod::SVRG,
466            history_size: 100,
467            full_grad_frequency: 10,
468            weight_decay: 0.0,
469        }
470    }
471}
472
473/// Types of variance reduction methods.
474#[derive(Debug, Clone, Serialize, Deserialize)]
475pub enum VarianceReductionMethod {
476    /// Stochastic Variance Reduced Gradient
477    SVRG,
478    /// Stochastic Average Gradient
479    SAG,
480}
481
482/// Variance Reduction optimizer implementing SVRG and SAG methods.
483#[derive(Debug)]
484pub struct VarianceReduction {
485    config: VarianceReductionConfig,
486    gradient_history: HashMap<usize, Vec<Tensor>>,
487    average_gradients: HashMap<usize, Tensor>,
488    full_gradients: HashMap<usize, Tensor>,
489    current_step: usize,
490    last_full_grad_step: usize,
491}
492
493impl VarianceReduction {
494    /// Create a new variance reduction optimizer.
495    pub fn new(config: VarianceReductionConfig) -> Self {
496        Self {
497            config,
498            gradient_history: HashMap::new(),
499            average_gradients: HashMap::new(),
500            full_gradients: HashMap::new(),
501            current_step: 0,
502            last_full_grad_step: 0,
503        }
504    }
505
506    /// Create SVRG optimizer with default settings.
507    pub fn svrg(learning_rate: f32, history_size: usize, full_grad_frequency: usize) -> Self {
508        Self::new(VarianceReductionConfig {
509            learning_rate,
510            method: VarianceReductionMethod::SVRG,
511            history_size,
512            full_grad_frequency,
513            weight_decay: 0.0,
514        })
515    }
516
517    /// Create SAG optimizer with default settings.
518    pub fn sag(learning_rate: f32, history_size: usize) -> Self {
519        Self::new(VarianceReductionConfig {
520            learning_rate,
521            method: VarianceReductionMethod::SAG,
522            history_size,
523            full_grad_frequency: 1, // Not used for SAG
524            weight_decay: 0.0,
525        })
526    }
527
528    fn update_gradient_history(&mut self, param_id: usize, gradient: &Tensor) -> Result<()> {
529        let history = self.gradient_history.entry(param_id).or_default();
530
531        history.push(gradient.clone());
532        if history.len() > self.config.history_size {
533            history.remove(0);
534        }
535
536        Ok(())
537    }
538
539    fn compute_average_gradient(&mut self, param_id: usize) -> Result<Tensor> {
540        if let Some(history) = self.gradient_history.get(&param_id) {
541            if history.is_empty() {
542                return Err(anyhow!("No gradient history available"));
543            }
544
545            let mut sum = history[0].clone();
546            for grad in history.iter().skip(1) {
547                sum = sum.add(grad)?;
548            }
549
550            let average = sum.div_scalar(history.len() as f32)?;
551            self.average_gradients.insert(param_id, average.clone());
552            Ok(average)
553        } else {
554            Err(anyhow!("No gradient history for parameter {}", param_id))
555        }
556    }
557
558    fn should_compute_full_gradient(&self) -> bool {
559        self.current_step - self.last_full_grad_step >= self.config.full_grad_frequency
560    }
561}
562
563impl OptimizerState for VarianceReduction {
564    fn zero_grad(&mut self) -> Result<()> {
565        Ok(())
566    }
567
568    fn step(&mut self, parameters: &mut [Tensor]) -> Result<()> {
569        self.current_step += 1;
570
571        // Check if we need to compute full gradient (for SVRG)
572        let compute_full_grad = match self.config.method {
573            VarianceReductionMethod::SVRG => self.should_compute_full_gradient(),
574            VarianceReductionMethod::SAG => false,
575        };
576
577        if compute_full_grad {
578            self.last_full_grad_step = self.current_step;
579            // In practice, full gradient computation would require access to the full dataset
580            // Here we'll use the current gradient as an approximation
581            for (param_id, parameter) in parameters.iter().enumerate() {
582                // Access gradient from parameter (should be computed during forward/backward pass)
583                let gradient = match parameter.grad() {
584                    Ok(grad) => grad,
585                    Err(_) => {
586                        // If gradient is not available, skip this parameter
587                        continue;
588                    },
589                };
590                self.full_gradients.insert(param_id, gradient);
591            }
592        }
593
594        for (param_id, parameter) in parameters.iter_mut().enumerate() {
595            // Access gradient from parameter (should be computed during forward/backward pass)
596            let current_gradient = match parameter.grad() {
597                Ok(grad) => grad,
598                Err(_) => {
599                    // If gradient is not available, skip this parameter
600                    continue;
601                },
602            };
603
604            // Apply weight decay
605            let effective_grad = if self.config.weight_decay > 0.0 {
606                current_gradient.add(&parameter.mul_scalar(self.config.weight_decay)?)?
607            } else {
608                current_gradient
609            };
610
611            // Update gradient history
612            self.update_gradient_history(param_id, &effective_grad)?;
613
614            // Apply variance reduction
615            let variance_reduced_grad = match self.config.method {
616                VarianceReductionMethod::SVRG => {
617                    // Clone full_grad before mutable borrow for compute_average_gradient
618                    let full_grad_opt = self.full_gradients.get(&param_id).cloned();
619                    if let Some(full_grad) = full_grad_opt {
620                        let avg_grad = self.compute_average_gradient(param_id)?;
621                        // SVRG update: grad - avg_grad + full_grad
622                        effective_grad.sub(&avg_grad)?.add(&full_grad)?
623                    } else {
624                        effective_grad
625                    }
626                },
627                VarianceReductionMethod::SAG => {
628                    // SAG uses running average of gradients
629                    self.compute_average_gradient(param_id)?
630                },
631            };
632
633            // Apply update
634            *parameter =
635                parameter.sub(&variance_reduced_grad.mul_scalar(self.config.learning_rate)?)?;
636        }
637
638        Ok(())
639    }
640
641    fn get_lr(&self) -> f32 {
642        self.config.learning_rate
643    }
644
645    fn set_lr(&mut self, lr: f32) {
646        self.config.learning_rate = lr;
647    }
648
649    fn state_dict(&self) -> Result<HashMap<String, Tensor>> {
650        let mut state = HashMap::new();
651
652        state.insert(
653            "learning_rate".to_string(),
654            Tensor::scalar(self.config.learning_rate)?,
655        );
656        state.insert(
657            "current_step".to_string(),
658            Tensor::scalar(self.current_step as f32)?,
659        );
660        state.insert(
661            "last_full_grad_step".to_string(),
662            Tensor::scalar(self.last_full_grad_step as f32)?,
663        );
664
665        // Note: Saving full gradient history would be expensive
666        // In practice, you might want to save only recent gradients or statistics
667
668        Ok(state)
669    }
670
671    fn load_state_dict(&mut self, state: HashMap<String, Tensor>) -> Result<()> {
672        if let Some(lr) = state.get("learning_rate") {
673            self.config.learning_rate = lr.to_scalar()?;
674        }
675        if let Some(step) = state.get("current_step") {
676            self.current_step = step.to_scalar()? as usize;
677        }
678        if let Some(last_step) = state.get("last_full_grad_step") {
679            self.last_full_grad_step = last_step.to_scalar()? as usize;
680        }
681
682        Ok(())
683    }
684}
685
686/// Configuration for Nesterov Accelerated Gradient (NAG).
687#[derive(Debug, Clone, Serialize, Deserialize)]
688pub struct NesterovAcceleratedGradientConfig {
689    /// Learning rate
690    pub learning_rate: f32,
691    /// Momentum parameter
692    pub momentum: f32,
693    /// Weight decay
694    pub weight_decay: f32,
695    /// Whether to use strong convexity assumption for restart
696    pub restart_on_increase: bool,
697}
698
699impl Default for NesterovAcceleratedGradientConfig {
700    fn default() -> Self {
701        Self {
702            learning_rate: 1e-3,
703            momentum: 0.9,
704            weight_decay: 0.0,
705            restart_on_increase: false,
706        }
707    }
708}
709
710/// Nesterov Accelerated Gradient optimizer.
711///
712/// NAG uses lookahead to evaluate the gradient at the predicted next position,
713/// which can lead to faster convergence than standard momentum methods.
714/// Update rule:
715/// v_t = momentum * v_{t-1} + lr * grad(x_t + momentum * v_{t-1})
716/// x_{t+1} = x_t - v_t
717#[derive(Debug)]
718pub struct NesterovAcceleratedGradient {
719    config: NesterovAcceleratedGradientConfig,
720    velocity_buffers: HashMap<usize, Tensor>,
721    current_step: usize,
722    previous_loss: Option<f32>,
723}
724
725impl NesterovAcceleratedGradient {
726    /// Create a new NAG optimizer.
727    pub fn new(config: NesterovAcceleratedGradientConfig) -> Self {
728        Self {
729            config,
730            velocity_buffers: HashMap::new(),
731            current_step: 0,
732            previous_loss: None,
733        }
734    }
735
736    /// Create NAG with default configuration.
737    pub fn with_defaults(learning_rate: f32, momentum: f32) -> Self {
738        Self::new(NesterovAcceleratedGradientConfig {
739            learning_rate,
740            momentum,
741            weight_decay: 0.0,
742            restart_on_increase: false,
743        })
744    }
745
746    /// Get the configuration.
747    pub fn get_config(&self) -> &NesterovAcceleratedGradientConfig {
748        &self.config
749    }
750
751    /// Set a loss value for restart detection.
752    pub fn set_current_loss(&mut self, loss: f32) {
753        if self.config.restart_on_increase {
754            if let Some(prev_loss) = self.previous_loss {
755                if loss > prev_loss {
756                    // Restart by clearing velocity buffers
757                    self.velocity_buffers.clear();
758                }
759            }
760        }
761        self.previous_loss = Some(loss);
762    }
763}
764
765impl OptimizerState for NesterovAcceleratedGradient {
766    fn zero_grad(&mut self) -> Result<()> {
767        Ok(())
768    }
769
770    fn step(&mut self, parameters: &mut [Tensor]) -> Result<()> {
771        self.current_step += 1;
772
773        for (param_id, parameter) in parameters.iter_mut().enumerate() {
774            // Access gradient from parameter (should be computed during forward/backward pass)
775            let gradient = match parameter.grad() {
776                Ok(grad) => grad,
777                Err(_) => {
778                    // If gradient is not available, skip this parameter
779                    continue;
780                },
781            };
782
783            // Apply weight decay to gradient
784            let effective_grad = if self.config.weight_decay > 0.0 {
785                gradient.add(&parameter.mul_scalar(self.config.weight_decay)?)?
786            } else {
787                gradient
788            };
789
790            // Get or initialize velocity buffer
791            let velocity = if let Some(v) = self.velocity_buffers.get(&param_id) {
792                v.clone()
793            } else {
794                Tensor::zeros_like(parameter)?
795            };
796
797            // Nesterov acceleration: compute gradient at lookahead position
798            let _lookahead_position = parameter.sub(&velocity.mul_scalar(self.config.momentum)?)?;
799
800            // In practice, we'd need to recompute the gradient at the lookahead position
801            // For now, we'll use the current gradient as approximation
802            // Lookahead gradient computation using current gradient state
803
804            // Update velocity: v_t = momentum * v_{t-1} + lr * grad
805            let new_velocity = velocity
806                .mul_scalar(self.config.momentum)?
807                .add(&effective_grad.mul_scalar(self.config.learning_rate)?)?;
808
809            self.velocity_buffers.insert(param_id, new_velocity.clone());
810
811            // Update parameters: x_{t+1} = x_t - v_t
812            *parameter = parameter.sub(&new_velocity)?;
813        }
814
815        Ok(())
816    }
817
818    fn get_lr(&self) -> f32 {
819        self.config.learning_rate
820    }
821
822    fn set_lr(&mut self, lr: f32) {
823        self.config.learning_rate = lr;
824    }
825
826    fn state_dict(&self) -> Result<HashMap<String, Tensor>> {
827        let mut state = HashMap::new();
828
829        state.insert(
830            "learning_rate".to_string(),
831            Tensor::scalar(self.config.learning_rate)?,
832        );
833        state.insert(
834            "momentum".to_string(),
835            Tensor::scalar(self.config.momentum)?,
836        );
837        state.insert(
838            "weight_decay".to_string(),
839            Tensor::scalar(self.config.weight_decay)?,
840        );
841        state.insert(
842            "current_step".to_string(),
843            Tensor::scalar(self.current_step as f32)?,
844        );
845
846        if let Some(loss) = self.previous_loss {
847            state.insert("previous_loss".to_string(), Tensor::scalar(loss)?);
848        }
849
850        for (&param_id, velocity) in &self.velocity_buffers {
851            state.insert(format!("velocity_{}", param_id), velocity.clone());
852        }
853
854        Ok(state)
855    }
856
857    fn load_state_dict(&mut self, state: HashMap<String, Tensor>) -> Result<()> {
858        if let Some(lr) = state.get("learning_rate") {
859            self.config.learning_rate = lr.to_scalar()?;
860        }
861        if let Some(momentum) = state.get("momentum") {
862            self.config.momentum = momentum.to_scalar()?;
863        }
864        if let Some(wd) = state.get("weight_decay") {
865            self.config.weight_decay = wd.to_scalar()?;
866        }
867        if let Some(step) = state.get("current_step") {
868            self.current_step = step.to_scalar()? as usize;
869        }
870        if let Some(loss) = state.get("previous_loss") {
871            self.previous_loss = Some(loss.to_scalar()?);
872        }
873
874        self.velocity_buffers.clear();
875        for (key, tensor) in state {
876            if let Some(param_id_str) = key.strip_prefix("velocity_") {
877                if let Ok(param_id) = param_id_str.parse::<usize>() {
878                    self.velocity_buffers.insert(param_id, tensor);
879                }
880            }
881        }
882
883        Ok(())
884    }
885}
886
887/// Configuration for Heavy Ball Method.
888#[derive(Debug, Clone, Serialize, Deserialize)]
889pub struct HeavyBallConfig {
890    /// Learning rate
891    pub learning_rate: f32,
892    /// Momentum coefficient (β)
893    pub beta: f32,
894    /// Weight decay
895    pub weight_decay: f32,
896    /// Adaptive momentum based on gradient alignment
897    pub adaptive_momentum: bool,
898}
899
900impl Default for HeavyBallConfig {
901    fn default() -> Self {
902        Self {
903            learning_rate: 1e-3,
904            beta: 0.9,
905            weight_decay: 0.0,
906            adaptive_momentum: false,
907        }
908    }
909}
910
911/// Heavy Ball Method optimizer.
912///
913/// Classical momentum-based acceleration method that adds inertia to gradient descent.
914/// Update rule:
915/// v_t = β * v_{t-1} - lr * grad(x_t)
916/// x_{t+1} = x_t + v_t
917#[derive(Debug)]
918pub struct HeavyBall {
919    config: HeavyBallConfig,
920    velocity_buffers: HashMap<usize, Tensor>,
921    previous_gradients: HashMap<usize, Tensor>,
922    current_step: usize,
923}
924
925impl HeavyBall {
926    /// Create a new Heavy Ball optimizer.
927    pub fn new(config: HeavyBallConfig) -> Self {
928        Self {
929            config,
930            velocity_buffers: HashMap::new(),
931            previous_gradients: HashMap::new(),
932            current_step: 0,
933        }
934    }
935
936    /// Create Heavy Ball with default configuration.
937    pub fn with_defaults(learning_rate: f32, beta: f32) -> Self {
938        Self::new(HeavyBallConfig {
939            learning_rate,
940            beta,
941            weight_decay: 0.0,
942            adaptive_momentum: false,
943        })
944    }
945
946    /// Get the configuration.
947    pub fn get_config(&self) -> &HeavyBallConfig {
948        &self.config
949    }
950
951    /// Compute adaptive momentum based on gradient alignment.
952    fn compute_adaptive_momentum(&self, param_id: usize, current_grad: &Tensor) -> Result<f32> {
953        if let Some(prev_grad) = self.previous_gradients.get(&param_id) {
954            // Compute cosine similarity between current and previous gradients
955            let dot_product = current_grad.mul(prev_grad)?.sum(None, false)?;
956            let norm_current = current_grad.norm_squared()?.sqrt()?;
957            let norm_prev = prev_grad.norm_squared()?.sqrt()?;
958
959            let dot_scalar = dot_product.to_scalar()?;
960            let norm_current_scalar = norm_current.to_scalar()?;
961            let norm_prev_scalar = norm_prev.to_scalar()?;
962
963            let denominator = norm_current_scalar * norm_prev_scalar;
964            if denominator > 1e-8 {
965                let cosine_similarity = dot_scalar / denominator;
966                // Increase momentum when gradients are aligned, decrease when opposed
967                let adaptive_beta = self.config.beta * cosine_similarity.max(0.0);
968                Ok(adaptive_beta)
969            } else {
970                Ok(self.config.beta)
971            }
972        } else {
973            Ok(self.config.beta)
974        }
975    }
976}
977
978impl OptimizerState for HeavyBall {
979    fn zero_grad(&mut self) -> Result<()> {
980        Ok(())
981    }
982
983    fn step(&mut self, parameters: &mut [Tensor]) -> Result<()> {
984        self.current_step += 1;
985
986        for (param_id, parameter) in parameters.iter_mut().enumerate() {
987            // Access gradient from parameter (should be computed during forward/backward pass)
988            let gradient = match parameter.grad() {
989                Ok(grad) => grad,
990                Err(_) => {
991                    // If gradient is not available, skip this parameter
992                    continue;
993                },
994            };
995
996            // Apply weight decay to gradient
997            let effective_grad = if self.config.weight_decay > 0.0 {
998                gradient.add(&parameter.mul_scalar(self.config.weight_decay)?)?
999            } else {
1000                gradient
1001            };
1002
1003            // Compute momentum coefficient
1004            let beta = if self.config.adaptive_momentum {
1005                self.compute_adaptive_momentum(param_id, &effective_grad)?
1006            } else {
1007                self.config.beta
1008            };
1009
1010            // Get or initialize velocity buffer
1011            let velocity = if let Some(v) = self.velocity_buffers.get(&param_id) {
1012                v.clone()
1013            } else {
1014                Tensor::zeros_like(parameter)?
1015            };
1016
1017            // Heavy Ball update: v_t = β * v_{t-1} - lr * grad
1018            let new_velocity = velocity
1019                .mul_scalar(beta)?
1020                .sub(&effective_grad.mul_scalar(self.config.learning_rate)?)?;
1021
1022            self.velocity_buffers.insert(param_id, new_velocity.clone());
1023
1024            // Update parameters: x_{t+1} = x_t + v_t
1025            *parameter = parameter.add(&new_velocity)?;
1026
1027            // Store gradient for adaptive momentum
1028            if self.config.adaptive_momentum {
1029                self.previous_gradients.insert(param_id, effective_grad);
1030            }
1031        }
1032
1033        Ok(())
1034    }
1035
1036    fn get_lr(&self) -> f32 {
1037        self.config.learning_rate
1038    }
1039
1040    fn set_lr(&mut self, lr: f32) {
1041        self.config.learning_rate = lr;
1042    }
1043
1044    fn state_dict(&self) -> Result<HashMap<String, Tensor>> {
1045        let mut state = HashMap::new();
1046
1047        state.insert(
1048            "learning_rate".to_string(),
1049            Tensor::scalar(self.config.learning_rate)?,
1050        );
1051        state.insert("beta".to_string(), Tensor::scalar(self.config.beta)?);
1052        state.insert(
1053            "weight_decay".to_string(),
1054            Tensor::scalar(self.config.weight_decay)?,
1055        );
1056        state.insert(
1057            "current_step".to_string(),
1058            Tensor::scalar(self.current_step as f32)?,
1059        );
1060
1061        for (&param_id, velocity) in &self.velocity_buffers {
1062            state.insert(format!("velocity_{}", param_id), velocity.clone());
1063        }
1064
1065        for (&param_id, grad) in &self.previous_gradients {
1066            state.insert(format!("prev_grad_{}", param_id), grad.clone());
1067        }
1068
1069        Ok(state)
1070    }
1071
1072    fn load_state_dict(&mut self, state: HashMap<String, Tensor>) -> Result<()> {
1073        if let Some(lr) = state.get("learning_rate") {
1074            self.config.learning_rate = lr.to_scalar()?;
1075        }
1076        if let Some(beta) = state.get("beta") {
1077            self.config.beta = beta.to_scalar()?;
1078        }
1079        if let Some(wd) = state.get("weight_decay") {
1080            self.config.weight_decay = wd.to_scalar()?;
1081        }
1082        if let Some(step) = state.get("current_step") {
1083            self.current_step = step.to_scalar()? as usize;
1084        }
1085
1086        self.velocity_buffers.clear();
1087        self.previous_gradients.clear();
1088
1089        for (key, tensor) in state {
1090            if let Some(param_id_str) = key.strip_prefix("velocity_") {
1091                if let Ok(param_id) = param_id_str.parse::<usize>() {
1092                    self.velocity_buffers.insert(param_id, tensor);
1093                }
1094            } else if let Some(param_id_str) = key.strip_prefix("prev_grad_") {
1095                if let Ok(param_id) = param_id_str.parse::<usize>() {
1096                    self.previous_gradients.insert(param_id, tensor);
1097                }
1098            }
1099        }
1100
1101        Ok(())
1102    }
1103}
1104
1105/// Configuration for FISTA (Fast Iterative Shrinkage-Thresholding Algorithm).
1106#[derive(Debug, Clone, Serialize, Deserialize)]
1107pub struct FISTAConfig {
1108    /// Learning rate
1109    pub learning_rate: f32,
1110    /// Proximal threshold parameter
1111    pub threshold: f32,
1112    /// Whether to use adaptive restart
1113    pub adaptive_restart: bool,
1114    /// Weight decay
1115    pub weight_decay: f32,
1116}
1117
1118impl Default for FISTAConfig {
1119    fn default() -> Self {
1120        Self {
1121            learning_rate: 1e-3,
1122            threshold: 1e-4,
1123            adaptive_restart: true,
1124            weight_decay: 0.0,
1125        }
1126    }
1127}
1128
1129/// FISTA optimizer for problems with L1 regularization or other proximal operators.
1130///
1131/// FISTA is designed for problems of the form: min f(x) + λ||x||_1
1132/// where f(x) is smooth and convex, and λ||x||_1 is the L1 regularization term.
1133#[derive(Debug)]
1134pub struct FISTA {
1135    config: FISTAConfig,
1136    previous_params: HashMap<usize, Tensor>,
1137    current_step: usize,
1138    momentum_coefficient: f32,
1139    previous_momentum: f32,
1140}
1141
1142impl FISTA {
1143    /// Create a new FISTA optimizer.
1144    pub fn new(config: FISTAConfig) -> Self {
1145        Self {
1146            config,
1147            previous_params: HashMap::new(),
1148            current_step: 0,
1149            momentum_coefficient: 1.0,
1150            previous_momentum: 1.0,
1151        }
1152    }
1153
1154    /// Create FISTA with default configuration.
1155    pub fn with_defaults(learning_rate: f32, threshold: f32) -> Self {
1156        Self::new(FISTAConfig {
1157            learning_rate,
1158            threshold,
1159            adaptive_restart: true,
1160            weight_decay: 0.0,
1161        })
1162    }
1163
1164    /// Get the configuration.
1165    pub fn get_config(&self) -> &FISTAConfig {
1166        &self.config
1167    }
1168
1169    /// Apply soft thresholding (proximal operator for L1 regularization).
1170    fn soft_threshold(&self, tensor: &Tensor, threshold: f32) -> Result<Tensor> {
1171        let threshold_tensor = Tensor::scalar(threshold)?;
1172        let zero_tensor = Tensor::zeros_like(tensor)?;
1173
1174        // Soft thresholding: sign(x) * max(0, |x| - threshold)
1175        let abs_tensor = tensor.abs()?;
1176        let thresholded = abs_tensor.sub(&threshold_tensor)?.max(&zero_tensor)?;
1177        let sign_tensor = tensor.sign()?;
1178
1179        Ok(sign_tensor.mul(&thresholded)?)
1180    }
1181
1182    /// Update momentum coefficient using FISTA formula.
1183    fn update_momentum_coefficient(&mut self) {
1184        let t = self.current_step as f32;
1185        self.previous_momentum = self.momentum_coefficient;
1186        self.momentum_coefficient = (1.0 + (1.0 + 4.0 * t * t).sqrt()) / 2.0;
1187    }
1188}
1189
1190impl OptimizerState for FISTA {
1191    fn zero_grad(&mut self) -> Result<()> {
1192        Ok(())
1193    }
1194
1195    fn step(&mut self, parameters: &mut [Tensor]) -> Result<()> {
1196        self.current_step += 1;
1197        self.update_momentum_coefficient();
1198
1199        for (param_id, parameter) in parameters.iter_mut().enumerate() {
1200            // Access gradient from parameter (should be computed during forward/backward pass)
1201            let gradient = match parameter.grad() {
1202                Ok(grad) => grad,
1203                Err(_) => {
1204                    // If gradient is not available, skip this parameter
1205                    continue;
1206                },
1207            };
1208
1209            // Apply weight decay to gradient
1210            let effective_grad = if self.config.weight_decay > 0.0 {
1211                gradient.add(&parameter.mul_scalar(self.config.weight_decay)?)?
1212            } else {
1213                gradient
1214            };
1215
1216            // Get previous parameter value
1217            let previous_param = if let Some(prev) = self.previous_params.get(&param_id) {
1218                prev.clone()
1219            } else {
1220                parameter.clone()
1221            };
1222
1223            // Momentum coefficient ratio
1224            let beta = (self.previous_momentum - 1.0) / self.momentum_coefficient;
1225
1226            // Compute extrapolated point
1227            let extrapolated = parameter.add(&previous_param.sub(parameter)?.mul_scalar(beta)?)?;
1228
1229            // Gradient step
1230            let grad_step =
1231                extrapolated.sub(&effective_grad.mul_scalar(self.config.learning_rate)?)?;
1232
1233            // Apply proximal operator (soft thresholding)
1234            let new_parameter = self.soft_threshold(&grad_step, self.config.threshold)?;
1235
1236            // Store current parameter for next iteration
1237            self.previous_params.insert(param_id, parameter.clone());
1238
1239            // Update parameter
1240            *parameter = new_parameter;
1241        }
1242
1243        Ok(())
1244    }
1245
1246    fn get_lr(&self) -> f32 {
1247        self.config.learning_rate
1248    }
1249
1250    fn set_lr(&mut self, lr: f32) {
1251        self.config.learning_rate = lr;
1252    }
1253
1254    fn state_dict(&self) -> Result<HashMap<String, Tensor>> {
1255        let mut state = HashMap::new();
1256
1257        state.insert(
1258            "learning_rate".to_string(),
1259            Tensor::scalar(self.config.learning_rate)?,
1260        );
1261        state.insert(
1262            "threshold".to_string(),
1263            Tensor::scalar(self.config.threshold)?,
1264        );
1265        state.insert(
1266            "weight_decay".to_string(),
1267            Tensor::scalar(self.config.weight_decay)?,
1268        );
1269        state.insert(
1270            "current_step".to_string(),
1271            Tensor::scalar(self.current_step as f32)?,
1272        );
1273        state.insert(
1274            "momentum_coefficient".to_string(),
1275            Tensor::scalar(self.momentum_coefficient)?,
1276        );
1277        state.insert(
1278            "previous_momentum".to_string(),
1279            Tensor::scalar(self.previous_momentum)?,
1280        );
1281
1282        for (&param_id, param) in &self.previous_params {
1283            state.insert(format!("prev_param_{}", param_id), param.clone());
1284        }
1285
1286        Ok(state)
1287    }
1288
1289    fn load_state_dict(&mut self, state: HashMap<String, Tensor>) -> Result<()> {
1290        if let Some(lr) = state.get("learning_rate") {
1291            self.config.learning_rate = lr.to_scalar()?;
1292        }
1293        if let Some(threshold) = state.get("threshold") {
1294            self.config.threshold = threshold.to_scalar()?;
1295        }
1296        if let Some(wd) = state.get("weight_decay") {
1297            self.config.weight_decay = wd.to_scalar()?;
1298        }
1299        if let Some(step) = state.get("current_step") {
1300            self.current_step = step.to_scalar()? as usize;
1301        }
1302        if let Some(momentum) = state.get("momentum_coefficient") {
1303            self.momentum_coefficient = momentum.to_scalar()?;
1304        }
1305        if let Some(prev_momentum) = state.get("previous_momentum") {
1306            self.previous_momentum = prev_momentum.to_scalar()?;
1307        }
1308
1309        self.previous_params.clear();
1310        for (key, tensor) in state {
1311            if let Some(param_id_str) = key.strip_prefix("prev_param_") {
1312                if let Ok(param_id) = param_id_str.parse::<usize>() {
1313                    self.previous_params.insert(param_id, tensor);
1314                }
1315            }
1316        }
1317
1318        Ok(())
1319    }
1320}
1321
1322/// Configuration for Adaptive Batch Sizing.
1323#[derive(Debug, Clone, Serialize, Deserialize)]
1324pub struct AdaptiveBatchSizingConfig {
1325    /// Initial batch size
1326    pub initial_batch_size: usize,
1327    /// Minimum batch size
1328    pub min_batch_size: usize,
1329    /// Maximum batch size
1330    pub max_batch_size: usize,
1331    /// Tolerance for gradient variance
1332    pub gradient_variance_tolerance: f32,
1333    /// Learning rate adaptation factor
1334    pub lr_adaptation_factor: f32,
1335    /// Window size for gradient variance calculation
1336    pub variance_window_size: usize,
1337    /// Threshold for increasing batch size
1338    pub increase_threshold: f32,
1339    /// Threshold for decreasing batch size
1340    pub decrease_threshold: f32,
1341}
1342
1343impl Default for AdaptiveBatchSizingConfig {
1344    fn default() -> Self {
1345        Self {
1346            initial_batch_size: 32,
1347            min_batch_size: 8,
1348            max_batch_size: 512,
1349            gradient_variance_tolerance: 0.1,
1350            lr_adaptation_factor: 0.8,
1351            variance_window_size: 10,
1352            increase_threshold: 0.05,
1353            decrease_threshold: 0.2,
1354        }
1355    }
1356}
1357
1358/// Adaptive Batch Sizing utility for dynamically adjusting batch size based on training progress.
1359///
1360/// This strategy monitors gradient variance and training stability to determine optimal batch sizes.
1361/// When gradient variance is high, it increases batch size to reduce noise.
1362/// When variance is low, it may decrease batch size to improve convergence speed.
1363#[derive(Debug)]
1364pub struct AdaptiveBatchSizing {
1365    config: AdaptiveBatchSizingConfig,
1366    current_batch_size: usize,
1367    gradient_variance_history: Vec<f32>,
1368    loss_history: Vec<f32>,
1369    current_step: usize,
1370    last_adjustment_step: usize,
1371}
1372
1373impl AdaptiveBatchSizing {
1374    /// Create a new adaptive batch sizing utility.
1375    pub fn new(config: AdaptiveBatchSizingConfig) -> Self {
1376        let initial_batch_size = config.initial_batch_size;
1377        Self {
1378            config,
1379            current_batch_size: initial_batch_size,
1380            gradient_variance_history: Vec::new(),
1381            loss_history: Vec::new(),
1382            current_step: 0,
1383            last_adjustment_step: 0,
1384        }
1385    }
1386
1387    /// Create with default configuration.
1388    pub fn with_defaults(
1389        initial_batch_size: usize,
1390        min_batch_size: usize,
1391        max_batch_size: usize,
1392    ) -> Self {
1393        Self::new(AdaptiveBatchSizingConfig {
1394            initial_batch_size,
1395            min_batch_size,
1396            max_batch_size,
1397            ..Default::default()
1398        })
1399    }
1400
1401    /// Get current batch size.
1402    pub fn current_batch_size(&self) -> usize {
1403        self.current_batch_size
1404    }
1405
1406    /// Get the configuration.
1407    pub fn get_config(&self) -> &AdaptiveBatchSizingConfig {
1408        &self.config
1409    }
1410
1411    /// Update with current gradient variance and loss.
1412    pub fn update(&mut self, gradient_variance: f32, current_loss: f32) -> Result<usize> {
1413        self.current_step += 1;
1414
1415        // Add to history
1416        self.gradient_variance_history.push(gradient_variance);
1417        self.loss_history.push(current_loss);
1418
1419        // Keep only recent history
1420        if self.gradient_variance_history.len() > self.config.variance_window_size {
1421            self.gradient_variance_history.remove(0);
1422        }
1423        if self.loss_history.len() > self.config.variance_window_size {
1424            self.loss_history.remove(0);
1425        }
1426
1427        // Check if we should adjust batch size
1428        if self.should_adjust_batch_size() {
1429            self.adjust_batch_size()?;
1430            self.last_adjustment_step = self.current_step;
1431        }
1432
1433        Ok(self.current_batch_size)
1434    }
1435
1436    /// Compute gradient variance from gradients.
1437    pub fn compute_gradient_variance(&self, gradients: &[Tensor]) -> Result<f32> {
1438        if gradients.is_empty() {
1439            return Ok(0.0);
1440        }
1441
1442        // Compute mean gradient
1443        let mut mean_grad = gradients[0].clone();
1444        for grad in gradients.iter().skip(1) {
1445            mean_grad = mean_grad.add(grad)?;
1446        }
1447        mean_grad = mean_grad.div_scalar(gradients.len() as f32)?;
1448
1449        // Compute variance
1450        let mut variance_sum = 0.0;
1451        for grad in gradients {
1452            let diff = grad.sub(&mean_grad)?;
1453            let squared_norm = diff.mul(&diff)?.sum(None, false)?;
1454            variance_sum += squared_norm.to_scalar()?;
1455        }
1456
1457        Ok(variance_sum / gradients.len() as f32)
1458    }
1459
1460    fn should_adjust_batch_size(&self) -> bool {
1461        // Don't adjust too frequently
1462        if self.current_step - self.last_adjustment_step < 5 {
1463            return false;
1464        }
1465
1466        // Need enough history
1467        self.gradient_variance_history.len() >= 3
1468    }
1469
1470    fn adjust_batch_size(&mut self) -> Result<()> {
1471        let recent_variance = self.recent_average_variance();
1472        let variance_trend = self.variance_trend();
1473        let loss_trend = self.loss_trend();
1474
1475        // Decide whether to increase or decrease batch size
1476        if recent_variance > self.config.decrease_threshold && variance_trend > 0.0 {
1477            // High variance and increasing - increase batch size
1478            self.increase_batch_size();
1479        } else if recent_variance < self.config.increase_threshold && loss_trend < -0.01 {
1480            // Low variance and decreasing loss - try smaller batch size
1481            self.decrease_batch_size();
1482        }
1483
1484        Ok(())
1485    }
1486
1487    fn recent_average_variance(&self) -> f32 {
1488        if self.gradient_variance_history.is_empty() {
1489            return 0.0;
1490        }
1491
1492        let recent_window = std::cmp::min(5, self.gradient_variance_history.len());
1493        let start_idx = self.gradient_variance_history.len() - recent_window;
1494
1495        self.gradient_variance_history[start_idx..].iter().sum::<f32>() / recent_window as f32
1496    }
1497
1498    fn variance_trend(&self) -> f32 {
1499        if self.gradient_variance_history.len() < 3 {
1500            return 0.0;
1501        }
1502
1503        let len = self.gradient_variance_history.len();
1504        let recent = self.gradient_variance_history[len - 2..].iter().sum::<f32>() / 2.0;
1505        let older = self.gradient_variance_history[len - 4..len - 2].iter().sum::<f32>() / 2.0;
1506
1507        recent - older
1508    }
1509
1510    fn loss_trend(&self) -> f32 {
1511        if self.loss_history.len() < 3 {
1512            return 0.0;
1513        }
1514
1515        let len = self.loss_history.len();
1516        let recent = self.loss_history[len - 2..].iter().sum::<f32>() / 2.0;
1517        let older = self.loss_history[len - 4..len - 2].iter().sum::<f32>() / 2.0;
1518
1519        (recent - older) / older.max(1e-8)
1520    }
1521
1522    fn increase_batch_size(&mut self) {
1523        let new_size = (self.current_batch_size as f32 * 1.5) as usize;
1524        self.current_batch_size = new_size.min(self.config.max_batch_size);
1525    }
1526
1527    fn decrease_batch_size(&mut self) {
1528        let new_size = (self.current_batch_size as f32 * 0.8) as usize;
1529        self.current_batch_size = new_size.max(self.config.min_batch_size);
1530    }
1531
1532    /// Get suggested learning rate adjustment based on batch size changes.
1533    pub fn get_lr_adjustment(&self, original_batch_size: usize) -> f32 {
1534        let ratio = self.current_batch_size as f32 / original_batch_size as f32;
1535        ratio.sqrt() * self.config.lr_adaptation_factor
1536    }
1537
1538    /// Reset state for new training run.
1539    pub fn reset(&mut self) {
1540        self.current_batch_size = self.config.initial_batch_size;
1541        self.gradient_variance_history.clear();
1542        self.loss_history.clear();
1543        self.current_step = 0;
1544        self.last_adjustment_step = 0;
1545    }
1546}
1547
1548/// Configuration for Loss Surface Smoothing.
1549#[derive(Debug, Clone, Serialize, Deserialize)]
1550pub struct LossSurfaceSmoothingConfig {
1551    /// Smoothing strength parameter
1552    pub smoothing_strength: f32,
1553    /// Noise injection variance
1554    pub noise_variance: f32,
1555    /// Exponential moving average decay
1556    pub ema_decay: f32,
1557    /// Number of gradient steps to average
1558    pub averaging_window: usize,
1559    /// Whether to use gradient averaging
1560    pub use_gradient_averaging: bool,
1561    /// Whether to use noise injection
1562    pub use_noise_injection: bool,
1563}
1564
1565impl Default for LossSurfaceSmoothingConfig {
1566    fn default() -> Self {
1567        Self {
1568            smoothing_strength: 0.1,
1569            noise_variance: 1e-4,
1570            ema_decay: 0.9,
1571            averaging_window: 5,
1572            use_gradient_averaging: true,
1573            use_noise_injection: false,
1574        }
1575    }
1576}
1577
1578/// Loss Surface Smoothing utility for reducing noise in the loss landscape.
1579///
1580/// This implements several techniques to smooth the loss surface:
1581/// - Gradient averaging over multiple steps
1582/// - Exponential moving average of gradients
1583/// - Controlled noise injection for exploration
1584/// - Parameter smoothing to reduce sharp changes
1585#[derive(Debug)]
1586pub struct LossSurfaceSmoothing {
1587    config: LossSurfaceSmoothingConfig,
1588    gradient_history: HashMap<usize, Vec<Tensor>>,
1589    ema_gradients: HashMap<usize, Tensor>,
1590    smoothed_parameters: HashMap<usize, Tensor>,
1591    current_step: usize,
1592}
1593
1594impl LossSurfaceSmoothing {
1595    /// Create a new loss surface smoothing utility.
1596    pub fn new(config: LossSurfaceSmoothingConfig) -> Self {
1597        Self {
1598            config,
1599            gradient_history: HashMap::new(),
1600            ema_gradients: HashMap::new(),
1601            smoothed_parameters: HashMap::new(),
1602            current_step: 0,
1603        }
1604    }
1605
1606    /// Create with default configuration.
1607    pub fn with_defaults(smoothing_strength: f32, use_noise: bool) -> Self {
1608        Self::new(LossSurfaceSmoothingConfig {
1609            smoothing_strength,
1610            use_noise_injection: use_noise,
1611            ..Default::default()
1612        })
1613    }
1614
1615    /// Get the configuration.
1616    pub fn get_config(&self) -> &LossSurfaceSmoothingConfig {
1617        &self.config
1618    }
1619
1620    /// Apply smoothing to gradients.
1621    pub fn smooth_gradients(&mut self, parameters: &mut [Tensor]) -> Result<()> {
1622        self.current_step += 1;
1623
1624        for (param_id, parameter) in parameters.iter_mut().enumerate() {
1625            let original_grad = parameter.grad()?;
1626            let mut smoothed_grad = original_grad.clone();
1627
1628            // Apply gradient averaging
1629            if self.config.use_gradient_averaging {
1630                smoothed_grad = self.apply_gradient_averaging(param_id, &original_grad)?;
1631            }
1632
1633            // Apply exponential moving average
1634            smoothed_grad = self.apply_ema_smoothing(param_id, &smoothed_grad)?;
1635
1636            // Apply noise injection for exploration
1637            if self.config.use_noise_injection {
1638                smoothed_grad = self.apply_noise_injection(&smoothed_grad)?;
1639            }
1640
1641            // Update parameter gradient
1642            parameter.set_grad(smoothed_grad)?;
1643        }
1644
1645        Ok(())
1646    }
1647
1648    /// Apply parameter smoothing.
1649    pub fn smooth_parameters(&mut self, parameters: &mut [Tensor]) -> Result<()> {
1650        for (param_id, parameter) in parameters.iter_mut().enumerate() {
1651            if let Some(smoothed_param) = self.smoothed_parameters.get(&param_id) {
1652                // Apply exponential moving average to parameters
1653                let new_smoothed = smoothed_param
1654                    .mul_scalar(self.config.ema_decay)?
1655                    .add(&parameter.mul_scalar(1.0 - self.config.ema_decay)?)?;
1656
1657                // Interpolate between original and smoothed parameters
1658                *parameter = parameter
1659                    .mul_scalar(1.0 - self.config.smoothing_strength)?
1660                    .add(&new_smoothed.mul_scalar(self.config.smoothing_strength)?)?;
1661
1662                self.smoothed_parameters.insert(param_id, new_smoothed);
1663            } else {
1664                // Initialize smoothed parameter
1665                self.smoothed_parameters.insert(param_id, parameter.clone());
1666            }
1667        }
1668
1669        Ok(())
1670    }
1671
1672    fn apply_gradient_averaging(&mut self, param_id: usize, gradient: &Tensor) -> Result<Tensor> {
1673        let history = self.gradient_history.entry(param_id).or_default();
1674
1675        history.push(gradient.clone());
1676        if history.len() > self.config.averaging_window {
1677            history.remove(0);
1678        }
1679
1680        // Compute average of recent gradients
1681        if history.len() == 1 {
1682            Ok(gradient.clone())
1683        } else {
1684            let mut sum = history[0].clone();
1685            for grad in history.iter().skip(1) {
1686                sum = sum.add(grad)?;
1687            }
1688            Ok(sum.div_scalar(history.len() as f32)?)
1689        }
1690    }
1691
1692    fn apply_ema_smoothing(&mut self, param_id: usize, gradient: &Tensor) -> Result<Tensor> {
1693        if let Some(ema_grad) = self.ema_gradients.get(&param_id) {
1694            let new_ema = ema_grad
1695                .mul_scalar(self.config.ema_decay)?
1696                .add(&gradient.mul_scalar(1.0 - self.config.ema_decay)?)?;
1697            self.ema_gradients.insert(param_id, new_ema.clone());
1698            Ok(new_ema)
1699        } else {
1700            self.ema_gradients.insert(param_id, gradient.clone());
1701            Ok(gradient.clone())
1702        }
1703    }
1704
1705    fn apply_noise_injection(&self, gradient: &Tensor) -> Result<Tensor> {
1706        let noise = Tensor::randn_like(gradient)
1707            .map_err(|e| anyhow!("Failed to create noise tensor: {}", e))?
1708            .mul_scalar(self.config.noise_variance.sqrt())
1709            .map_err(|e| anyhow!("Failed to scale noise tensor: {}", e))?;
1710        gradient
1711            .add(&noise)
1712            .map_err(|e| anyhow!("Failed to add noise to gradient: {}", e))
1713    }
1714
1715    /// Reset state for new training run.
1716    pub fn reset(&mut self) {
1717        self.gradient_history.clear();
1718        self.ema_gradients.clear();
1719        self.smoothed_parameters.clear();
1720        self.current_step = 0;
1721    }
1722
1723    /// Get smoothing statistics.
1724    pub fn get_statistics(&self) -> HashMap<String, f32> {
1725        let mut stats = HashMap::new();
1726        stats.insert("current_step".to_string(), self.current_step as f32);
1727        stats.insert(
1728            "num_tracked_params".to_string(),
1729            self.gradient_history.len() as f32,
1730        );
1731        stats.insert(
1732            "smoothing_strength".to_string(),
1733            self.config.smoothing_strength,
1734        );
1735        stats.insert("ema_decay".to_string(), self.config.ema_decay);
1736        stats
1737    }
1738}
1739
1740#[cfg(test)]
1741mod tests {
1742    use super::*;
1743
1744    #[test]
1745    fn test_qhm_config_default() {
1746        let config = QHMConfig::default();
1747        assert_eq!(config.learning_rate, 1e-3);
1748        assert_eq!(config.momentum, 0.9);
1749        assert_eq!(config.nu, 0.7);
1750        assert_eq!(config.weight_decay, 0.0);
1751    }
1752
1753    #[test]
1754    fn test_aggmo_config_default() {
1755        let config = AggMoConfig::default();
1756        assert_eq!(config.learning_rate, 1e-3);
1757        assert_eq!(config.momentum_coefficients, vec![0.0, 0.9, 0.99]);
1758        assert_eq!(config.weight_decay, 0.0);
1759    }
1760
1761    #[test]
1762    fn test_qhm_creation() {
1763        let optimizer = QHM::with_defaults(1e-3, 0.9, 0.7);
1764        assert_eq!(optimizer.get_lr(), 1e-3);
1765        assert_eq!(optimizer.current_step, 0);
1766    }
1767
1768    #[test]
1769    fn test_aggmo_creation() {
1770        let optimizer = AggMo::with_defaults(1e-3, vec![0.0, 0.9, 0.99]);
1771        assert_eq!(optimizer.get_lr(), 1e-3);
1772        assert_eq!(optimizer.num_momentum_buffers(), 3);
1773    }
1774
1775    #[test]
1776    fn test_variance_reduction_svrg() {
1777        let optimizer = VarianceReduction::svrg(1e-3, 50, 10);
1778        assert_eq!(optimizer.get_lr(), 1e-3);
1779        assert_eq!(optimizer.current_step, 0);
1780    }
1781
1782    #[test]
1783    fn test_variance_reduction_sag() {
1784        let optimizer = VarianceReduction::sag(1e-3, 100);
1785        assert_eq!(optimizer.get_lr(), 1e-3);
1786        assert!(matches!(
1787            optimizer.config.method,
1788            VarianceReductionMethod::SAG
1789        ));
1790    }
1791
1792    #[test]
1793    fn test_nesterov_accelerated_gradient_config() {
1794        let config = NesterovAcceleratedGradientConfig::default();
1795        assert_eq!(config.learning_rate, 1e-3);
1796        assert_eq!(config.momentum, 0.9);
1797        assert_eq!(config.weight_decay, 0.0);
1798        assert!(!config.restart_on_increase);
1799    }
1800
1801    #[test]
1802    fn test_nesterov_accelerated_gradient_creation() {
1803        let optimizer = NesterovAcceleratedGradient::with_defaults(1e-3, 0.9);
1804        assert_eq!(optimizer.get_lr(), 1e-3);
1805        assert_eq!(optimizer.current_step, 0);
1806        assert!(optimizer.previous_loss.is_none());
1807    }
1808
1809    #[test]
1810    fn test_nesterov_restart_on_increase() {
1811        let mut optimizer = NesterovAcceleratedGradient::new(NesterovAcceleratedGradientConfig {
1812            learning_rate: 1e-3,
1813            momentum: 0.9,
1814            weight_decay: 0.0,
1815            restart_on_increase: true,
1816        });
1817
1818        // Set initial loss
1819        optimizer.set_current_loss(1.0);
1820        assert_eq!(optimizer.previous_loss, Some(1.0));
1821
1822        // Increasing loss should trigger restart
1823        optimizer.set_current_loss(1.5);
1824        assert_eq!(optimizer.previous_loss, Some(1.5));
1825    }
1826
1827    #[test]
1828    fn test_heavy_ball_config() {
1829        let config = HeavyBallConfig::default();
1830        assert_eq!(config.learning_rate, 1e-3);
1831        assert_eq!(config.beta, 0.9);
1832        assert_eq!(config.weight_decay, 0.0);
1833        assert!(!config.adaptive_momentum);
1834    }
1835
1836    #[test]
1837    fn test_heavy_ball_creation() {
1838        let optimizer = HeavyBall::with_defaults(1e-3, 0.9);
1839        assert_eq!(optimizer.get_lr(), 1e-3);
1840        assert_eq!(optimizer.current_step, 0);
1841        assert_eq!(optimizer.get_config().beta, 0.9);
1842    }
1843
1844    #[test]
1845    fn test_heavy_ball_adaptive_momentum() {
1846        let optimizer = HeavyBall::new(HeavyBallConfig {
1847            learning_rate: 1e-3,
1848            beta: 0.9,
1849            weight_decay: 0.0,
1850            adaptive_momentum: true,
1851        });
1852
1853        assert!(optimizer.config.adaptive_momentum);
1854    }
1855
1856    #[test]
1857    fn test_fista_config() {
1858        let config = FISTAConfig::default();
1859        assert_eq!(config.learning_rate, 1e-3);
1860        assert_eq!(config.threshold, 1e-4);
1861        assert!(config.adaptive_restart);
1862        assert_eq!(config.weight_decay, 0.0);
1863    }
1864
1865    #[test]
1866    fn test_fista_creation() {
1867        let optimizer = FISTA::with_defaults(1e-3, 1e-4);
1868        assert_eq!(optimizer.get_lr(), 1e-3);
1869        assert_eq!(optimizer.current_step, 0);
1870        assert_eq!(optimizer.momentum_coefficient, 1.0);
1871        assert_eq!(optimizer.previous_momentum, 1.0);
1872    }
1873
1874    #[test]
1875    fn test_fista_momentum_update() {
1876        let mut optimizer = FISTA::with_defaults(1e-3, 1e-4);
1877
1878        // Momentum coefficient should update with step (increment step first)
1879        optimizer.current_step = 1;
1880        optimizer.update_momentum_coefficient();
1881        assert!(optimizer.momentum_coefficient > 1.0);
1882        assert_eq!(optimizer.previous_momentum, 1.0);
1883
1884        let prev_momentum = optimizer.momentum_coefficient;
1885        optimizer.current_step = 2;
1886        optimizer.update_momentum_coefficient();
1887        assert!(optimizer.momentum_coefficient > prev_momentum);
1888    }
1889
1890    #[test]
1891    fn test_adaptive_batch_sizing_config() {
1892        let config = AdaptiveBatchSizingConfig::default();
1893        assert_eq!(config.initial_batch_size, 32);
1894        assert_eq!(config.min_batch_size, 8);
1895        assert_eq!(config.max_batch_size, 512);
1896        assert_eq!(config.gradient_variance_tolerance, 0.1);
1897        assert_eq!(config.lr_adaptation_factor, 0.8);
1898        assert_eq!(config.variance_window_size, 10);
1899        assert_eq!(config.increase_threshold, 0.05);
1900        assert_eq!(config.decrease_threshold, 0.2);
1901    }
1902
1903    #[test]
1904    fn test_adaptive_batch_sizing_creation() {
1905        let abs = AdaptiveBatchSizing::with_defaults(64, 16, 256);
1906        assert_eq!(abs.current_batch_size(), 64);
1907        assert_eq!(abs.get_config().min_batch_size, 16);
1908        assert_eq!(abs.get_config().max_batch_size, 256);
1909    }
1910
1911    #[test]
1912    fn test_adaptive_batch_sizing_lr_adjustment() {
1913        let abs = AdaptiveBatchSizing::with_defaults(64, 16, 256);
1914        let lr_adj = abs.get_lr_adjustment(32);
1915        assert!(lr_adj > 0.0);
1916        assert!(lr_adj < 2.0);
1917    }
1918
1919    #[test]
1920    fn test_adaptive_batch_sizing_reset() {
1921        let mut abs = AdaptiveBatchSizing::with_defaults(64, 16, 256);
1922        abs.current_step = 10;
1923        abs.reset();
1924        assert_eq!(abs.current_step, 0);
1925        assert_eq!(abs.current_batch_size(), 64);
1926    }
1927
1928    #[test]
1929    fn test_loss_surface_smoothing_config() {
1930        let config = LossSurfaceSmoothingConfig::default();
1931        assert_eq!(config.smoothing_strength, 0.1);
1932        assert_eq!(config.noise_variance, 1e-4);
1933        assert_eq!(config.ema_decay, 0.9);
1934        assert_eq!(config.averaging_window, 5);
1935        assert!(config.use_gradient_averaging);
1936        assert!(!config.use_noise_injection);
1937    }
1938
1939    #[test]
1940    fn test_loss_surface_smoothing_creation() {
1941        let lss = LossSurfaceSmoothing::with_defaults(0.2, true);
1942        assert_eq!(lss.get_config().smoothing_strength, 0.2);
1943        assert!(lss.get_config().use_noise_injection);
1944        assert_eq!(lss.current_step, 0);
1945    }
1946
1947    #[test]
1948    fn test_loss_surface_smoothing_statistics() {
1949        let lss = LossSurfaceSmoothing::with_defaults(0.1, false);
1950        let stats = lss.get_statistics();
1951        assert_eq!(stats.get("current_step"), Some(&0.0));
1952        assert_eq!(stats.get("num_tracked_params"), Some(&0.0));
1953        assert_eq!(stats.get("smoothing_strength"), Some(&0.1));
1954        assert_eq!(stats.get("ema_decay"), Some(&0.9));
1955    }
1956
1957    #[test]
1958    fn test_loss_surface_smoothing_reset() {
1959        let mut lss = LossSurfaceSmoothing::with_defaults(0.1, false);
1960        lss.current_step = 5;
1961        lss.reset();
1962        assert_eq!(lss.current_step, 0);
1963    }
1964}