Skip to main content

torsh_optim/
robustness.rs

1//! Robustness features for optimizers
2//!
3//! This module provides tools for making optimizers more robust to various
4//! forms of adversarial perturbations, noisy gradients, and training instabilities.
5//! It includes implementations of robust optimization techniques and defensive
6//! training strategies.
7
8use crate::{OptimizerError, OptimizerResult};
9use serde::{Deserialize, Serialize};
10use std::collections::{HashMap, VecDeque};
11use std::ops::Add;
12use torsh_tensor::Tensor;
13
14/// Configuration for robust optimization
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct RobustnessConfig {
17    /// Enable gradient clipping for stability
18    pub gradient_clipping: bool,
19    /// Maximum gradient norm for clipping
20    pub max_gradient_norm: f32,
21    /// Enable outlier detection and filtering
22    pub outlier_detection: bool,
23    /// Z-score threshold for outlier detection
24    pub outlier_threshold: f32,
25    /// Enable smooth gradient aggregation
26    pub smooth_aggregation: bool,
27    /// Smoothing factor for gradient aggregation
28    pub smoothing_factor: f32,
29    /// Enable adversarial training support
30    pub adversarial_training: bool,
31    /// Perturbation budget for adversarial examples
32    pub perturbation_budget: f32,
33    /// Number of adversarial steps
34    pub adversarial_steps: usize,
35}
36
37impl Default for RobustnessConfig {
38    fn default() -> Self {
39        Self {
40            gradient_clipping: true,
41            max_gradient_norm: 1.0,
42            outlier_detection: true,
43            outlier_threshold: 3.0,
44            smooth_aggregation: true,
45            smoothing_factor: 0.1,
46            adversarial_training: false,
47            perturbation_budget: 0.01,
48            adversarial_steps: 1,
49        }
50    }
51}
52
53/// Statistics for robustness monitoring
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct RobustnessStats {
56    /// Number of gradients processed
57    pub total_gradients: usize,
58    /// Number of gradients clipped
59    pub clipped_gradients: usize,
60    /// Number of outliers detected
61    pub outliers_detected: usize,
62    /// Average gradient norm
63    pub avg_gradient_norm: f32,
64    /// Maximum gradient norm observed
65    pub max_gradient_norm: f32,
66    /// Gradient variance over time
67    pub gradient_variance: f32,
68    /// Stability score (0-1, higher is more stable)
69    pub stability_score: f32,
70}
71
72impl RobustnessStats {
73    pub fn new() -> Self {
74        Self {
75            total_gradients: 0,
76            clipped_gradients: 0,
77            outliers_detected: 0,
78            avg_gradient_norm: 0.0,
79            max_gradient_norm: 0.0,
80            gradient_variance: 0.0,
81            stability_score: 1.0,
82        }
83    }
84
85    pub fn clipping_rate(&self) -> f32 {
86        if self.total_gradients == 0 {
87            0.0
88        } else {
89            self.clipped_gradients as f32 / self.total_gradients as f32
90        }
91    }
92
93    pub fn outlier_rate(&self) -> f32 {
94        if self.total_gradients == 0 {
95            0.0
96        } else {
97            self.outliers_detected as f32 / self.total_gradients as f32
98        }
99    }
100}
101
102/// Gradient history for analysis
103#[derive(Debug, Clone)]
104struct GradientHistory {
105    norms: VecDeque<f32>,
106    window_size: usize,
107}
108
109impl GradientHistory {
110    fn new(window_size: usize) -> Self {
111        Self {
112            norms: VecDeque::new(),
113            window_size,
114        }
115    }
116
117    fn add(&mut self, norm: f32) {
118        self.norms.push_back(norm);
119        if self.norms.len() > self.window_size {
120            self.norms.pop_front();
121        }
122    }
123
124    fn mean(&self) -> f32 {
125        if self.norms.is_empty() {
126            0.0
127        } else {
128            self.norms.iter().sum::<f32>() / self.norms.len() as f32
129        }
130    }
131
132    fn variance(&self) -> f32 {
133        if self.norms.len() < 2 {
134            0.0
135        } else {
136            let mean = self.mean();
137            let sum_sq_diff: f32 = self.norms.iter().map(|x| (x - mean).powi(2)).sum();
138            sum_sq_diff / (self.norms.len() - 1) as f32
139        }
140    }
141
142    fn std_dev(&self) -> f32 {
143        self.variance().sqrt()
144    }
145}
146
147/// Robustness manager for optimizers
148pub struct RobustnessManager {
149    config: RobustnessConfig,
150    stats: RobustnessStats,
151    gradient_history: HashMap<String, GradientHistory>,
152    smoothed_gradients: HashMap<String, Tensor>,
153}
154
155impl RobustnessManager {
156    pub fn new(config: RobustnessConfig) -> Self {
157        Self {
158            config,
159            stats: RobustnessStats::new(),
160            gradient_history: HashMap::new(),
161            smoothed_gradients: HashMap::new(),
162        }
163    }
164
165    /// Apply robustness transformations to gradients
166    pub fn process_gradients(
167        &mut self,
168        gradients: &HashMap<String, Tensor>,
169    ) -> OptimizerResult<HashMap<String, Tensor>> {
170        let mut processed_gradients = HashMap::new();
171
172        for (param_name, gradient) in gradients {
173            let mut processed_grad = gradient.clone();
174
175            // Step 1: Outlier detection and filtering
176            if self.config.outlier_detection {
177                if self.is_outlier(param_name, &processed_grad)? {
178                    self.stats.outliers_detected += 1;
179                    processed_grad = self.filter_outlier(param_name, &processed_grad)?;
180                }
181            }
182
183            // Step 2: Gradient clipping
184            if self.config.gradient_clipping {
185                processed_grad = self.clip_gradient(&processed_grad)?;
186            }
187
188            // Step 3: Smooth aggregation
189            if self.config.smooth_aggregation {
190                processed_grad = self.smooth_gradient(param_name, &processed_grad)?;
191            }
192
193            // Update statistics
194            self.update_statistics(param_name, &processed_grad)?;
195            processed_gradients.insert(param_name.clone(), processed_grad);
196        }
197
198        Ok(processed_gradients)
199    }
200
201    /// Check if gradient is an outlier
202    fn is_outlier(&mut self, param_name: &str, gradient: &Tensor) -> OptimizerResult<bool> {
203        let grad_norm = gradient.norm()?.item()?;
204
205        let history = self
206            .gradient_history
207            .entry(param_name.to_string())
208            .or_insert_with(|| GradientHistory::new(100));
209
210        if history.norms.len() < 10 {
211            // Not enough history, assume not an outlier
212            history.add(grad_norm);
213            return Ok(false);
214        }
215
216        let mean = history.mean();
217        let std_dev = history.std_dev();
218
219        if std_dev < 1e-8 {
220            // Gradients are essentially constant, not an outlier
221            history.add(grad_norm);
222            return Ok(false);
223        }
224
225        let z_score = (grad_norm - mean).abs() / std_dev;
226        let is_outlier = z_score > self.config.outlier_threshold;
227
228        if !is_outlier {
229            history.add(grad_norm);
230        }
231
232        Ok(is_outlier)
233    }
234
235    /// Filter outlier gradients
236    fn filter_outlier(&self, param_name: &str, gradient: &Tensor) -> OptimizerResult<Tensor> {
237        if let Some(history) = self.gradient_history.get(param_name) {
238            let mean_norm = history.mean();
239            let current_norm = gradient.norm()?.item()?;
240
241            if current_norm > 1e-8 {
242                // Scale down to mean norm
243                let scale_factor = mean_norm / current_norm;
244                return Ok(gradient.mul_scalar(scale_factor)?);
245            }
246        }
247
248        // Fallback: return zero gradient
249        Ok(gradient.zeros_like()?)
250    }
251
252    /// Clip gradients to maximum norm
253    fn clip_gradient(&mut self, gradient: &Tensor) -> OptimizerResult<Tensor> {
254        let grad_norm = gradient.norm()?.item()?;
255
256        if grad_norm > self.config.max_gradient_norm {
257            self.stats.clipped_gradients += 1;
258            let scale_factor = self.config.max_gradient_norm / grad_norm;
259            Ok(gradient.mul_scalar(scale_factor)?)
260        } else {
261            Ok(gradient.clone())
262        }
263    }
264
265    /// Apply smooth aggregation to gradients
266    fn smooth_gradient(&mut self, param_name: &str, gradient: &Tensor) -> OptimizerResult<Tensor> {
267        let alpha = self.config.smoothing_factor;
268
269        match self.smoothed_gradients.get(param_name) {
270            Some(prev_smoothed) => {
271                // Exponential moving average: smoothed = alpha * new + (1 - alpha) * prev
272                let new_contribution = gradient.mul_scalar(alpha)?;
273                let prev_contribution = prev_smoothed.mul_scalar(1.0 - alpha)?;
274                let smoothed = new_contribution.add(&prev_contribution)?;
275
276                self.smoothed_gradients
277                    .insert(param_name.to_string(), smoothed.clone());
278                Ok(smoothed)
279            }
280            None => {
281                // First gradient, use as-is
282                self.smoothed_gradients
283                    .insert(param_name.to_string(), gradient.clone());
284                Ok(gradient.clone())
285            }
286        }
287    }
288
289    /// Update robustness statistics
290    fn update_statistics(&mut self, param_name: &str, gradient: &Tensor) -> OptimizerResult<()> {
291        let grad_norm = gradient.norm()?.item()?;
292
293        self.stats.total_gradients += 1;
294        self.stats.max_gradient_norm = self.stats.max_gradient_norm.max(grad_norm);
295
296        // Update average gradient norm
297        let prev_avg = self.stats.avg_gradient_norm;
298        let n = self.stats.total_gradients as f32;
299        self.stats.avg_gradient_norm = (prev_avg * (n - 1.0) + grad_norm) / n;
300
301        // Update gradient variance
302        if let Some(history) = self.gradient_history.get(param_name) {
303            self.stats.gradient_variance = history.variance();
304        }
305
306        // Update stability score (inverse of coefficient of variation)
307        if self.stats.avg_gradient_norm > 1e-8 {
308            let cv = self.stats.gradient_variance.sqrt() / self.stats.avg_gradient_norm;
309            self.stats.stability_score = 1.0 / (1.0 + cv);
310        }
311
312        Ok(())
313    }
314
315    /// Generate adversarial perturbations for robust training
316    pub fn generate_adversarial_perturbation(
317        &self,
318        input: &Tensor,
319        gradient: &Tensor,
320    ) -> OptimizerResult<Tensor> {
321        if !self.config.adversarial_training {
322            return Ok(input.zeros_like()?);
323        }
324
325        let grad_norm = gradient.norm()?.item()?;
326        if grad_norm < 1e-8 {
327            return Ok(input.zeros_like()?);
328        }
329
330        // Generate perturbation in direction of gradient (FGSM-style)
331        let perturbation_direction = gradient.div_scalar(grad_norm)?;
332        let perturbation = perturbation_direction.mul_scalar(self.config.perturbation_budget)?;
333
334        Ok(perturbation)
335    }
336
337    /// Get current robustness statistics
338    pub fn get_stats(&self) -> &RobustnessStats {
339        &self.stats
340    }
341
342    /// Reset statistics
343    pub fn reset_stats(&mut self) {
344        self.stats = RobustnessStats::new();
345        self.gradient_history.clear();
346        self.smoothed_gradients.clear();
347    }
348
349    /// Check if training appears stable
350    pub fn is_training_stable(&self) -> bool {
351        self.stats.stability_score > 0.5
352            && self.stats.clipping_rate() < 0.3
353            && self.stats.outlier_rate() < 0.1
354    }
355
356    /// Get recommendations for improving robustness
357    pub fn get_recommendations(&self) -> Vec<String> {
358        let mut recommendations = Vec::new();
359
360        if self.stats.clipping_rate() > 0.5 {
361            recommendations
362                .push("Consider reducing learning rate - high gradient clipping rate".to_string());
363        }
364
365        if self.stats.outlier_rate() > 0.2 {
366            recommendations.push(
367                "High outlier rate detected - consider data cleaning or regularization".to_string(),
368            );
369        }
370
371        if self.stats.stability_score < 0.3 {
372            recommendations
373                .push("Low stability score - consider increasing smoothing factor".to_string());
374        }
375
376        if self.stats.gradient_variance > self.stats.avg_gradient_norm.powi(2) {
377            recommendations.push(
378                "High gradient variance - consider batch normalization or different architecture"
379                    .to_string(),
380            );
381        }
382
383        if recommendations.is_empty() {
384            recommendations.push("Training appears stable and robust".to_string());
385        }
386
387        recommendations
388    }
389}
390
391/// Trait for robust optimizers
392pub trait RobustOptimizer {
393    /// Enable robustness features
394    fn enable_robustness(&mut self, config: RobustnessConfig);
395
396    /// Get robustness statistics
397    fn robustness_stats(&self) -> Option<&RobustnessStats>;
398
399    /// Check if training is stable
400    fn is_stable(&self) -> bool;
401}
402
403#[cfg(test)]
404mod tests {
405    use super::*;
406    use torsh_tensor::creation::randn;
407
408    #[test]
409    fn test_robustness_config_default() {
410        let config = RobustnessConfig::default();
411        assert!(config.gradient_clipping);
412        assert_eq!(config.max_gradient_norm, 1.0);
413    }
414
415    #[test]
416    fn test_robustness_manager_creation() {
417        let config = RobustnessConfig::default();
418        let manager = RobustnessManager::new(config);
419        assert_eq!(manager.stats.total_gradients, 0);
420    }
421
422    #[test]
423    fn test_gradient_clipping() -> OptimizerResult<()> {
424        let config = RobustnessConfig {
425            max_gradient_norm: 1.0,
426            gradient_clipping: true,
427            ..Default::default()
428        };
429        let mut manager = RobustnessManager::new(config);
430
431        let large_gradient = randn::<f32>(&[2, 2]).unwrap().mul_scalar(10.0).unwrap();
432        let clipped = manager.clip_gradient(&large_gradient).unwrap();
433
434        let clipped_norm = clipped.norm().unwrap().to_vec()?[0];
435        assert!(clipped_norm <= 1.0 + 1e-6);
436
437        Ok(())
438    }
439
440    #[test]
441    fn test_gradient_smoothing() -> OptimizerResult<()> {
442        let config = RobustnessConfig {
443            smooth_aggregation: true,
444            smoothing_factor: 0.5,
445            ..Default::default()
446        };
447        let mut manager = RobustnessManager::new(config);
448
449        let grad1 = randn::<f32>(&[2, 2]).unwrap();
450        let grad2 = randn::<f32>(&[2, 2]).unwrap();
451
452        let smoothed1 = manager.smooth_gradient("param1", &grad1).unwrap();
453        let smoothed2 = manager.smooth_gradient("param1", &grad2).unwrap();
454
455        // First gradient should be unchanged
456        assert_eq!(smoothed1.data()?, grad1.data()?);
457
458        // Second should be blend
459        let expected = grad2
460            .mul_scalar(0.5)
461            .unwrap()
462            .add(&grad1.mul_scalar(0.5).unwrap())
463            .unwrap();
464        let diff = smoothed2.sub(&expected).unwrap().norm().unwrap().item()?;
465        assert!(diff < 1e-6);
466        Ok(())
467    }
468
469    #[test]
470    fn test_statistics_tracking() {
471        let config = RobustnessConfig::default();
472        let mut manager = RobustnessManager::new(config);
473
474        let gradient = randn::<f32>(&[2, 2]).unwrap();
475        manager.update_statistics("param1", &gradient).unwrap();
476
477        assert_eq!(manager.stats.total_gradients, 1);
478        assert!(manager.stats.avg_gradient_norm > 0.0);
479    }
480
481    #[test]
482    fn test_adversarial_perturbation() -> OptimizerResult<()> {
483        let config = RobustnessConfig {
484            adversarial_training: true,
485            perturbation_budget: 0.1,
486            ..Default::default()
487        };
488        let manager = RobustnessManager::new(config);
489
490        let input = randn::<f32>(&[2, 2]).unwrap();
491        let gradient = randn::<f32>(&[2, 2]).unwrap();
492
493        let perturbation = manager
494            .generate_adversarial_perturbation(&input, &gradient)
495            .unwrap();
496        let pert_norm = perturbation.norm().unwrap().to_vec()?[0];
497
498        assert!(pert_norm <= 0.1 + 1e-6);
499
500        Ok(())
501    }
502
503    #[test]
504    fn test_stability_assessment() {
505        let mut stats = RobustnessStats::new();
506        stats.stability_score = 0.8;
507        stats.total_gradients = 100;
508        stats.clipped_gradients = 10;
509        stats.outliers_detected = 5;
510
511        let manager = RobustnessManager {
512            config: RobustnessConfig::default(),
513            stats,
514            gradient_history: HashMap::new(),
515            smoothed_gradients: HashMap::new(),
516        };
517
518        assert!(manager.is_training_stable());
519    }
520}