1use crate::{OptimizerError, OptimizerResult};
9use serde::{Deserialize, Serialize};
10use std::collections::{HashMap, VecDeque};
11use std::ops::Add;
12use torsh_tensor::Tensor;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct RobustnessConfig {
17 pub gradient_clipping: bool,
19 pub max_gradient_norm: f32,
21 pub outlier_detection: bool,
23 pub outlier_threshold: f32,
25 pub smooth_aggregation: bool,
27 pub smoothing_factor: f32,
29 pub adversarial_training: bool,
31 pub perturbation_budget: f32,
33 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#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct RobustnessStats {
56 pub total_gradients: usize,
58 pub clipped_gradients: usize,
60 pub outliers_detected: usize,
62 pub avg_gradient_norm: f32,
64 pub max_gradient_norm: f32,
66 pub gradient_variance: f32,
68 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#[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
147pub 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 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 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 if self.config.gradient_clipping {
185 processed_grad = self.clip_gradient(&processed_grad)?;
186 }
187
188 if self.config.smooth_aggregation {
190 processed_grad = self.smooth_gradient(param_name, &processed_grad)?;
191 }
192
193 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 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 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 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 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 let scale_factor = mean_norm / current_norm;
244 return Ok(gradient.mul_scalar(scale_factor)?);
245 }
246 }
247
248 Ok(gradient.zeros_like()?)
250 }
251
252 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 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 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 self.smoothed_gradients
283 .insert(param_name.to_string(), gradient.clone());
284 Ok(gradient.clone())
285 }
286 }
287 }
288
289 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 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 if let Some(history) = self.gradient_history.get(param_name) {
303 self.stats.gradient_variance = history.variance();
304 }
305
306 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 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 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 pub fn get_stats(&self) -> &RobustnessStats {
339 &self.stats
340 }
341
342 pub fn reset_stats(&mut self) {
344 self.stats = RobustnessStats::new();
345 self.gradient_history.clear();
346 self.smoothed_gradients.clear();
347 }
348
349 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 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
391pub trait RobustOptimizer {
393 fn enable_robustness(&mut self, config: RobustnessConfig);
395
396 fn robustness_stats(&self) -> Option<&RobustnessStats>;
398
399 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 assert_eq!(smoothed1.data()?, grad1.data()?);
457
458 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}