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