Skip to main content

sklears_model_selection/
noise_injection.rs

1//! Noise injection for robustness testing
2//!
3//! This module provides various noise injection strategies for testing model
4//! robustness and evaluating performance under different perturbation scenarios.
5
6use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2};
7use scirs2_core::random::essentials::{Normal as RandNormal, Uniform};
8use scirs2_core::random::prelude::*;
9use scirs2_core::random::rngs::StdRng;
10// use scirs2_core::random::Distribution;
11#[cfg(feature = "serde")]
12use serde::{Deserialize, Serialize};
13use sklears_core::prelude::*;
14
15fn noise_error(msg: &str) -> SklearsError {
16    SklearsError::InvalidInput(msg.to_string())
17}
18
19#[derive(Debug, Clone, Copy, PartialEq)]
20pub enum NoiseType {
21    /// Gaussian
22    Gaussian,
23    /// Uniform
24    Uniform,
25    /// SaltAndPepper
26    SaltAndPepper,
27    /// Dropout
28    Dropout,
29    /// Multiplicative
30    Multiplicative,
31    /// Adversarial
32    Adversarial,
33    /// OutlierInjection
34    OutlierInjection,
35    /// LabelNoise
36    LabelNoise,
37    /// FeatureSwap
38    FeatureSwap,
39    /// MixedNoise
40    MixedNoise,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq)]
44pub enum AdversarialMethod {
45    /// FGSM
46    FGSM,
47    /// PGD
48    PGD,
49    /// RandomNoise
50    RandomNoise,
51    /// BoundaryAttack
52    BoundaryAttack,
53}
54
55#[derive(Debug, Clone)]
56pub struct NoiseConfig {
57    pub noise_type: NoiseType,
58    pub intensity: f64,
59    pub probability: f64,
60    pub random_state: Option<u64>,
61    pub adaptive: bool,
62    pub preserve_statistics: bool,
63    pub adversarial_method: Option<AdversarialMethod>,
64    pub outlier_factor: f64,
65    pub label_flip_rate: f64,
66    pub feature_swap_rate: f64,
67}
68
69impl Default for NoiseConfig {
70    fn default() -> Self {
71        Self {
72            noise_type: NoiseType::Gaussian,
73            intensity: 0.1,
74            probability: 1.0,
75            random_state: None,
76            adaptive: false,
77            preserve_statistics: false,
78            adversarial_method: None,
79            outlier_factor: 3.0,
80            label_flip_rate: 0.1,
81            feature_swap_rate: 0.1,
82        }
83    }
84}
85
86#[derive(Debug, Clone)]
87pub struct RobustnessTestResult {
88    pub original_performance: f64,
89    pub noisy_performance: f64,
90    pub performance_degradation: f64,
91    pub noise_sensitivity: f64,
92    pub robustness_score: f64,
93    pub noise_statistics: NoiseStatistics,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct NoiseStatistics {
98    pub noise_type: String,
99    pub intensity: f64,
100    pub affected_samples: usize,
101    pub affected_features: usize,
102    pub signal_to_noise_ratio: f64,
103    pub perturbation_magnitude: f64,
104}
105
106pub struct NoiseInjector {
107    config: NoiseConfig,
108    rng: StdRng,
109}
110
111impl NoiseInjector {
112    pub fn new(config: NoiseConfig) -> Self {
113        let rng = if let Some(seed) = config.random_state {
114            StdRng::seed_from_u64(seed)
115        } else {
116            StdRng::from_rng(&mut scirs2_core::random::thread_rng())
117        };
118
119        Self { config, rng }
120    }
121
122    pub fn inject_feature_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
123        match self.config.noise_type {
124            NoiseType::Gaussian => self.inject_gaussian_noise(x),
125            NoiseType::Uniform => self.inject_uniform_noise(x),
126            NoiseType::SaltAndPepper => self.inject_salt_pepper_noise(x),
127            NoiseType::Dropout => self.inject_dropout_noise(x),
128            NoiseType::Multiplicative => self.inject_multiplicative_noise(x),
129            NoiseType::Adversarial => self.inject_adversarial_noise(x),
130            NoiseType::OutlierInjection => self.inject_outlier_noise(x),
131            NoiseType::FeatureSwap => self.inject_feature_swap_noise(x),
132            NoiseType::MixedNoise => self.inject_mixed_noise(x),
133            _ => Err(noise_error("Unsupported noise type for data type")),
134        }
135    }
136
137    pub fn inject_label_noise(&mut self, y: &ArrayView1<i32>) -> Result<Array1<i32>> {
138        if self.config.noise_type != NoiseType::LabelNoise {
139            return Err(noise_error("Unsupported noise type for data type"));
140        }
141
142        let mut noisy_y = y.to_owned();
143        let unique_labels: Vec<i32> = {
144            let mut labels: Vec<i32> = y.iter().cloned().collect();
145            labels.sort_unstable();
146            labels.dedup();
147            labels
148        };
149
150        if unique_labels.len() < 2 {
151            return Ok(noisy_y);
152        }
153
154        let flip_dist = Bernoulli::new(self.config.label_flip_rate)
155            .map_err(|_| noise_error("Invalid label flip rate"))?;
156
157        for i in 0..noisy_y.len() {
158            if self.rng.sample(flip_dist) {
159                let current_label = noisy_y[i];
160                let available_labels: Vec<i32> = unique_labels
161                    .iter()
162                    .filter(|&&label| label != current_label)
163                    .cloned()
164                    .collect();
165
166                if !available_labels.is_empty() {
167                    let new_label_idx = self.rng.random_range(0..available_labels.len());
168                    noisy_y[i] = available_labels[new_label_idx];
169                }
170            }
171        }
172
173        Ok(noisy_y)
174    }
175
176    fn inject_gaussian_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
177        let mut noisy_x = x.to_owned();
178        let (n_samples, n_features) = x.dim();
179
180        for i in 0..n_samples {
181            for j in 0..n_features {
182                if self.rng.random::<f64>() < self.config.probability {
183                    let noise_std = if self.config.adaptive {
184                        self.config.intensity * x[[i, j]].abs()
185                    } else {
186                        self.config.intensity
187                    };
188
189                    let normal = RandNormal::new(0.0, noise_std)
190                        .map_err(|_| noise_error("Random number generation failed"))?;
191
192                    let noise = self.rng.sample(normal);
193                    noisy_x[[i, j]] += noise;
194                }
195            }
196        }
197
198        Ok(noisy_x)
199    }
200
201    fn inject_uniform_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
202        let mut noisy_x = x.to_owned();
203        let (n_samples, n_features) = x.dim();
204
205        for i in 0..n_samples {
206            for j in 0..n_features {
207                if self.rng.random::<f64>() < self.config.probability {
208                    let noise_range = if self.config.adaptive {
209                        self.config.intensity * x[[i, j]].abs()
210                    } else {
211                        self.config.intensity
212                    };
213
214                    let uniform =
215                        Uniform::new(-noise_range, noise_range).expect("operation should succeed");
216                    let noise = self.rng.sample(uniform);
217                    noisy_x[[i, j]] += noise;
218                }
219            }
220        }
221
222        Ok(noisy_x)
223    }
224
225    fn inject_salt_pepper_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
226        let mut noisy_x = x.to_owned();
227        let (n_samples, n_features) = x.dim();
228
229        let min_val = x.iter().cloned().fold(f64::INFINITY, f64::min);
230        let max_val = x.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
231
232        for i in 0..n_samples {
233            for j in 0..n_features {
234                if self.rng.random::<f64>() < self.config.probability {
235                    if self.rng.random::<f64>() < 0.5 {
236                        noisy_x[[i, j]] = min_val;
237                    } else {
238                        noisy_x[[i, j]] = max_val;
239                    }
240                }
241            }
242        }
243
244        Ok(noisy_x)
245    }
246
247    fn inject_dropout_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
248        let mut noisy_x = x.to_owned();
249        let (n_samples, n_features) = x.dim();
250
251        for i in 0..n_samples {
252            for j in 0..n_features {
253                if self.rng.random::<f64>() < self.config.intensity {
254                    noisy_x[[i, j]] = 0.0;
255                }
256            }
257        }
258
259        Ok(noisy_x)
260    }
261
262    fn inject_multiplicative_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
263        let mut noisy_x = x.to_owned();
264        let (n_samples, n_features) = x.dim();
265
266        for i in 0..n_samples {
267            for j in 0..n_features {
268                if self.rng.random::<f64>() < self.config.probability {
269                    let noise_factor = if self.config.intensity > 0.0 {
270                        let gamma = Gamma::new(1.0 / self.config.intensity, self.config.intensity)
271                            .map_err(|_| noise_error("Random number generation failed"))?;
272                        self.rng.sample(gamma)
273                    } else {
274                        1.0
275                    };
276
277                    noisy_x[[i, j]] *= noise_factor;
278                }
279            }
280        }
281
282        Ok(noisy_x)
283    }
284
285    fn inject_adversarial_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
286        match self
287            .config
288            .adversarial_method
289            .unwrap_or(AdversarialMethod::RandomNoise)
290        {
291            AdversarialMethod::FGSM => self.inject_fgsm_noise(x),
292            AdversarialMethod::PGD => self.inject_pgd_noise(x),
293            AdversarialMethod::RandomNoise => self.inject_random_adversarial_noise(x),
294            AdversarialMethod::BoundaryAttack => self.inject_boundary_attack_noise(x),
295        }
296    }
297
298    fn inject_fgsm_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
299        let mut noisy_x = x.to_owned();
300        let (n_samples, n_features) = x.dim();
301
302        for i in 0..n_samples {
303            for j in 0..n_features {
304                let gradient_sign = if self.rng.random::<f64>() < 0.5 {
305                    -1.0
306                } else {
307                    1.0
308                };
309                let perturbation = self.config.intensity * gradient_sign;
310                noisy_x[[i, j]] += perturbation;
311            }
312        }
313
314        Ok(noisy_x)
315    }
316
317    fn inject_pgd_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
318        let mut noisy_x = x.to_owned();
319        let (n_samples, n_features) = x.dim();
320        let step_size = self.config.intensity * 0.1;
321        let num_steps = 10;
322
323        for _ in 0..num_steps {
324            for i in 0..n_samples {
325                for j in 0..n_features {
326                    let gradient_sign = if self.rng.random::<f64>() < 0.5 {
327                        -1.0
328                    } else {
329                        1.0
330                    };
331                    let perturbation = step_size * gradient_sign;
332                    noisy_x[[i, j]] += perturbation;
333
334                    let max_perturbation = self.config.intensity;
335                    noisy_x[[i, j]] = (noisy_x[[i, j]] - x[[i, j]])
336                        .max(-max_perturbation)
337                        .min(max_perturbation)
338                        + x[[i, j]];
339                }
340            }
341        }
342
343        Ok(noisy_x)
344    }
345
346    fn inject_random_adversarial_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
347        let mut noisy_x = x.to_owned();
348        let (n_samples, n_features) = x.dim();
349
350        for i in 0..n_samples {
351            let mut perturbation_norm: f64 = 0.0;
352            let mut perturbations: Vec<f64> = vec![0.0; n_features];
353
354            for j in 0..n_features {
355                perturbations[j] = self.rng.random_range(-1.0..1.0);
356                perturbation_norm += perturbations[j].powi(2);
357            }
358
359            perturbation_norm = perturbation_norm.sqrt();
360            if perturbation_norm > 0.0 {
361                for j in 0..n_features {
362                    perturbations[j] =
363                        (perturbations[j] / perturbation_norm) * self.config.intensity;
364                    noisy_x[[i, j]] += perturbations[j];
365                }
366            }
367        }
368
369        Ok(noisy_x)
370    }
371
372    fn inject_boundary_attack_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
373        let mut noisy_x = x.to_owned();
374        let (n_samples, n_features) = x.dim();
375
376        for i in 0..n_samples {
377            for j in 0..n_features {
378                let direction = if self.rng.random::<f64>() < 0.5 {
379                    -1.0
380                } else {
381                    1.0
382                };
383                let magnitude = self.rng.random::<f64>() * self.config.intensity;
384                noisy_x[[i, j]] += direction * magnitude;
385            }
386        }
387
388        Ok(noisy_x)
389    }
390
391    fn inject_outlier_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
392        let mut noisy_x = x.to_owned();
393        let (n_samples, n_features) = x.dim();
394
395        for j in 0..n_features {
396            let feature_values: Vec<f64> = (0..n_samples).map(|i| x[[i, j]]).collect();
397            let mean = feature_values.iter().sum::<f64>() / n_samples as f64;
398            let variance = feature_values
399                .iter()
400                .map(|&val| (val - mean).powi(2))
401                .sum::<f64>()
402                / n_samples as f64;
403            let std_dev = variance.sqrt();
404
405            for i in 0..n_samples {
406                if self.rng.random::<f64>() < self.config.probability {
407                    let outlier_direction = if self.rng.random::<f64>() < 0.5 {
408                        -1.0
409                    } else {
410                        1.0
411                    };
412                    let outlier_magnitude = self.config.outlier_factor * std_dev;
413                    noisy_x[[i, j]] = mean + outlier_direction * outlier_magnitude;
414                }
415            }
416        }
417
418        Ok(noisy_x)
419    }
420
421    fn inject_feature_swap_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
422        let mut noisy_x = x.to_owned();
423        let (n_samples, n_features) = x.dim();
424
425        if n_features < 2 {
426            return Ok(noisy_x);
427        }
428
429        for i in 0..n_samples {
430            if self.rng.random::<f64>() < self.config.feature_swap_rate {
431                let feature1 = self.rng.random_range(0..n_features);
432                let feature2 = self.rng.random_range(0..n_features);
433
434                if feature1 != feature2 {
435                    let temp = noisy_x[[i, feature1]];
436                    noisy_x[[i, feature1]] = noisy_x[[i, feature2]];
437                    noisy_x[[i, feature2]] = temp;
438                }
439            }
440        }
441
442        Ok(noisy_x)
443    }
444
445    fn inject_mixed_noise(&mut self, x: &ArrayView2<f64>) -> Result<Array2<f64>> {
446        let mut noisy_x = x.to_owned();
447        let noise_types = [
448            NoiseType::Gaussian,
449            NoiseType::Uniform,
450            NoiseType::SaltAndPepper,
451            NoiseType::Multiplicative,
452        ];
453
454        let original_noise_type = self.config.noise_type;
455        let original_intensity = self.config.intensity;
456
457        for &noise_type in &noise_types {
458            self.config.noise_type = noise_type;
459            self.config.intensity = original_intensity / noise_types.len() as f64;
460
461            noisy_x = match noise_type {
462                NoiseType::Gaussian => self.inject_gaussian_noise(&noisy_x.view())?,
463                NoiseType::Uniform => self.inject_uniform_noise(&noisy_x.view())?,
464                NoiseType::SaltAndPepper => self.inject_salt_pepper_noise(&noisy_x.view())?,
465                NoiseType::Multiplicative => self.inject_multiplicative_noise(&noisy_x.view())?,
466                _ => noisy_x,
467            };
468        }
469
470        self.config.noise_type = original_noise_type;
471        self.config.intensity = original_intensity;
472
473        Ok(noisy_x)
474    }
475
476    pub fn compute_noise_statistics(
477        &self,
478        original: &ArrayView2<f64>,
479        noisy: &ArrayView2<f64>,
480    ) -> NoiseStatistics {
481        let (n_samples, n_features) = original.dim();
482        let mut affected_samples = 0;
483        let mut affected_features = 0;
484        let mut total_perturbation = 0.0;
485
486        for i in 0..n_samples {
487            let mut sample_affected = false;
488            for j in 0..n_features {
489                let perturbation = (noisy[[i, j]] - original[[i, j]]).abs();
490                if perturbation > 1e-10 {
491                    if !sample_affected {
492                        affected_samples += 1;
493                        sample_affected = true;
494                    }
495                    total_perturbation += perturbation;
496                }
497            }
498        }
499
500        for j in 0..n_features {
501            let mut feature_affected = false;
502            for i in 0..n_samples {
503                if (noisy[[i, j]] - original[[i, j]]).abs() > 1e-10 {
504                    feature_affected = true;
505                    break;
506                }
507            }
508            if feature_affected {
509                affected_features += 1;
510            }
511        }
512
513        let signal_power: f64 = original.iter().map(|&x| x.powi(2)).sum();
514        let noise_power: f64 = original
515            .iter()
516            .zip(noisy.iter())
517            .map(|(&orig, &noise)| (noise - orig).powi(2))
518            .sum();
519
520        let snr = if noise_power > 0.0 {
521            10.0 * (signal_power / noise_power).log10()
522        } else {
523            f64::INFINITY
524        };
525
526        let avg_perturbation = total_perturbation / (n_samples * n_features) as f64;
527
528        NoiseStatistics {
529            noise_type: format!("{:?}", self.config.noise_type),
530            intensity: self.config.intensity,
531            affected_samples,
532            affected_features,
533            signal_to_noise_ratio: snr,
534            perturbation_magnitude: avg_perturbation,
535        }
536    }
537}
538
539pub fn robustness_test<M, F>(
540    model: &M,
541    x: &ArrayView2<f64>,
542    y: &ArrayView1<f64>,
543    noise_configs: Vec<NoiseConfig>,
544    eval_fn: F,
545) -> Result<Vec<RobustnessTestResult>>
546where
547    M: Clone,
548    F: Fn(&M, &ArrayView2<f64>, &ArrayView1<f64>) -> f64 + Copy,
549{
550    let original_performance = eval_fn(model, x, y);
551    let mut results = Vec::new();
552
553    for config in noise_configs {
554        let mut injector = NoiseInjector::new(config.clone());
555        let noisy_x = injector.inject_feature_noise(x)?;
556        let noisy_performance = eval_fn(model, &noisy_x.view(), y);
557
558        let performance_degradation = original_performance - noisy_performance;
559        let noise_sensitivity = performance_degradation / config.intensity.max(1e-10);
560        let robustness_score =
561            1.0 - (performance_degradation / original_performance.max(1e-10)).abs();
562
563        let noise_statistics = injector.compute_noise_statistics(x, &noisy_x.view());
564
565        results.push(RobustnessTestResult {
566            original_performance,
567            noisy_performance,
568            performance_degradation,
569            noise_sensitivity,
570            robustness_score: robustness_score.max(0.0),
571            noise_statistics,
572        });
573    }
574
575    Ok(results)
576}
577
578#[allow(non_snake_case)]
579#[cfg(test)]
580mod tests {
581    use super::*;
582    use scirs2_core::ndarray::{arr1, arr2, Array2};
583
584    fn create_test_data() -> Array2<f64> {
585        arr2(&[
586            [1.0, 2.0, 3.0],
587            [4.0, 5.0, 6.0],
588            [7.0, 8.0, 9.0],
589            [10.0, 11.0, 12.0],
590        ])
591    }
592
593    #[test]
594    fn test_gaussian_noise() {
595        let x = create_test_data();
596        let config = NoiseConfig {
597            noise_type: NoiseType::Gaussian,
598            intensity: 0.1,
599            random_state: Some(42),
600            ..Default::default()
601        };
602
603        let mut injector = NoiseInjector::new(config);
604        let noisy_x = injector
605            .inject_feature_noise(&x.view())
606            .expect("operation should succeed");
607
608        assert_eq!(noisy_x.dim(), x.dim());
609        assert!(noisy_x != x);
610    }
611
612    #[test]
613    fn test_uniform_noise() {
614        let x = create_test_data();
615        let config = NoiseConfig {
616            noise_type: NoiseType::Uniform,
617            intensity: 0.2,
618            random_state: Some(42),
619            ..Default::default()
620        };
621
622        let mut injector = NoiseInjector::new(config);
623        let noisy_x = injector
624            .inject_feature_noise(&x.view())
625            .expect("operation should succeed");
626
627        assert_eq!(noisy_x.dim(), x.dim());
628    }
629
630    #[test]
631    fn test_dropout_noise() {
632        let x = create_test_data();
633        let config = NoiseConfig {
634            noise_type: NoiseType::Dropout,
635            intensity: 0.3,
636            random_state: Some(42),
637            ..Default::default()
638        };
639
640        let mut injector = NoiseInjector::new(config);
641        let noisy_x = injector
642            .inject_feature_noise(&x.view())
643            .expect("operation should succeed");
644
645        let zero_count = noisy_x.iter().filter(|&&val| val == 0.0).count();
646        assert!(zero_count > 0);
647    }
648
649    #[test]
650    fn test_label_noise() {
651        let y = arr1(&[0, 1, 2, 0, 1, 2]);
652        let config = NoiseConfig {
653            noise_type: NoiseType::LabelNoise,
654            label_flip_rate: 0.5,
655            random_state: Some(42),
656            ..Default::default()
657        };
658
659        let mut injector = NoiseInjector::new(config);
660        let noisy_y = injector
661            .inject_label_noise(&y.view())
662            .expect("operation should succeed");
663
664        assert_eq!(noisy_y.len(), y.len());
665        assert!(noisy_y != y);
666    }
667
668    #[test]
669    fn test_adversarial_noise() {
670        let x = create_test_data();
671        let config = NoiseConfig {
672            noise_type: NoiseType::Adversarial,
673            intensity: 0.1,
674            adversarial_method: Some(AdversarialMethod::FGSM),
675            random_state: Some(42),
676            ..Default::default()
677        };
678
679        let mut injector = NoiseInjector::new(config);
680        let noisy_x = injector
681            .inject_feature_noise(&x.view())
682            .expect("operation should succeed");
683
684        assert_eq!(noisy_x.dim(), x.dim());
685    }
686
687    #[test]
688    fn test_outlier_injection() {
689        let x = create_test_data();
690        let config = NoiseConfig {
691            noise_type: NoiseType::OutlierInjection,
692            probability: 0.1,
693            outlier_factor: 3.0,
694            random_state: Some(42),
695            ..Default::default()
696        };
697
698        let mut injector = NoiseInjector::new(config);
699        let noisy_x = injector
700            .inject_feature_noise(&x.view())
701            .expect("operation should succeed");
702
703        assert_eq!(noisy_x.dim(), x.dim());
704    }
705
706    #[test]
707    fn test_mixed_noise() {
708        let x = create_test_data();
709        let config = NoiseConfig {
710            noise_type: NoiseType::MixedNoise,
711            intensity: 0.1,
712            random_state: Some(42),
713            ..Default::default()
714        };
715
716        let mut injector = NoiseInjector::new(config);
717        let noisy_x = injector
718            .inject_feature_noise(&x.view())
719            .expect("operation should succeed");
720
721        assert_eq!(noisy_x.dim(), x.dim());
722    }
723
724    #[test]
725    fn test_noise_statistics() {
726        let x = create_test_data();
727        let config = NoiseConfig {
728            noise_type: NoiseType::Gaussian,
729            intensity: 0.1,
730            random_state: Some(42),
731            ..Default::default()
732        };
733
734        let mut injector = NoiseInjector::new(config);
735        let noisy_x = injector
736            .inject_feature_noise(&x.view())
737            .expect("operation should succeed");
738        let stats = injector.compute_noise_statistics(&x.view(), &noisy_x.view());
739
740        assert!(stats.affected_samples > 0);
741        assert!(stats.affected_features > 0);
742        assert!(stats.signal_to_noise_ratio.is_finite());
743        assert!(stats.perturbation_magnitude >= 0.0);
744    }
745
746    #[test]
747    fn test_adaptive_noise() {
748        let x = create_test_data();
749        let config = NoiseConfig {
750            noise_type: NoiseType::Gaussian,
751            intensity: 0.1,
752            adaptive: true,
753            random_state: Some(42),
754            ..Default::default()
755        };
756
757        let mut injector = NoiseInjector::new(config);
758        let noisy_x = injector
759            .inject_feature_noise(&x.view())
760            .expect("operation should succeed");
761
762        assert_eq!(noisy_x.dim(), x.dim());
763    }
764}