1use scirs2_core::ndarray::{Array, Array2, Array4, Dimension, ScalarOperand};
8use scirs2_core::numeric::{Float, FromPrimitive};
9use scirs2_core::random::rngs::StdRng;
10use scirs2_core::random::Random;
11use std::fmt::Debug;
13
14use crate::error::{OptimError, Result};
15use crate::regularizers::Regularizer;
16
17const MAX_REJECTION_ATTEMPTS: usize = 1024;
21
22fn standard_normal(rng: &mut Random<StdRng>) -> f64 {
27 let mut u1: f64 = rng.gen_range(0.0..1.0);
28 if u1 <= 0.0 {
29 u1 = f64::MIN_POSITIVE;
31 }
32 let u2: f64 = rng.gen_range(0.0..1.0);
33 (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
34}
35
36fn sample_gamma(shape: f64, rng: &mut Random<StdRng>) -> f64 {
43 if !shape.is_finite() || shape <= 0.0 {
44 return 0.0;
45 }
46
47 if shape < 1.0 {
48 let boosted = sample_gamma(shape + 1.0, rng);
50 let mut u: f64 = rng.gen_range(0.0..1.0);
51 if u <= 0.0 {
52 u = f64::MIN_POSITIVE;
53 }
54 return boosted * u.powf(1.0 / shape);
55 }
56
57 let d = shape - 1.0 / 3.0;
58 let c = 1.0 / (9.0 * d).sqrt();
59
60 for _ in 0..MAX_REJECTION_ATTEMPTS {
61 let mut x = standard_normal(rng);
63 let mut v = 1.0 + c * x;
64 let mut inner = 0usize;
65 while v <= 0.0 && inner < MAX_REJECTION_ATTEMPTS {
66 x = standard_normal(rng);
67 v = 1.0 + c * x;
68 inner += 1;
69 }
70 if v <= 0.0 {
71 continue;
72 }
73 v = v * v * v;
74
75 let mut u: f64 = rng.gen_range(0.0..1.0);
76 if u <= 0.0 {
77 u = f64::MIN_POSITIVE;
78 }
79
80 let x_sq = x * x;
82 if u < 1.0 - 0.0331 * x_sq * x_sq {
83 return d * v;
84 }
85 if u.ln() < 0.5 * x_sq + d * (1.0 - v + v.ln()) {
86 return d * v;
87 }
88 }
89
90 shape
92}
93
94fn sample_beta(a: f64, b: f64, rng: &mut Random<StdRng>) -> f64 {
100 let x = sample_gamma(a, rng);
101 let y = sample_gamma(b, rng);
102 let total = x + y;
103 if total > 0.0 && total.is_finite() {
104 (x / total).clamp(0.0, 1.0)
105 } else {
106 0.5
107 }
108}
109
110#[derive(Debug, Clone)]
130pub struct MixUp<A: Float> {
131 alpha: A,
133}
134
135impl<A: Float + Debug + ScalarOperand + FromPrimitive + Send + Sync> MixUp<A> {
136 pub fn new(alpha: A) -> Result<Self> {
146 if alpha <= A::zero() {
147 return Err(OptimError::InvalidConfig(
148 "Alpha must be positive".to_string(),
149 ));
150 }
151
152 Ok(Self { alpha })
153 }
154
155 pub fn alpha(&self) -> A {
157 self.alpha
158 }
159
160 pub fn mixing_factor(&self, seed: u64) -> A {
175 let mut rng = Random::seed(seed);
176 let alpha = self.alpha.to_f64().unwrap_or(1.0);
177 let lambda = sample_beta(alpha, alpha, &mut rng);
178 A::from_f64(lambda).unwrap_or_else(|| A::one() / (A::one() + A::one()))
179 }
180
181 pub fn apply_batch(
193 &self,
194 inputs: &Array2<A>,
195 labels: &Array2<A>,
196 seed: u64,
197 ) -> Result<(Array2<A>, Array2<A>)> {
198 let batch_size = inputs.shape()[0];
199 if batch_size < 2 {
200 return Err(OptimError::InvalidConfig(
201 "Batch size must be at least 2 for MixUp".to_string(),
202 ));
203 }
204
205 if labels.shape()[0] != batch_size {
206 return Err(OptimError::InvalidConfig(
207 "Number of inputs and labels must match".to_string(),
208 ));
209 }
210
211 let mut rng = scirs2_core::random::Random::default();
212 let lambda = self.mixing_factor(seed);
213
214 let mut indices: Vec<usize> = (0..batch_size).collect();
216 for i in (1..indices.len()).rev() {
217 let j = rng.gen_range(0..i + 1);
218 indices.swap(i, j);
219 }
220
221 let mut mixed_inputs = inputs.clone();
223 let mut mixed_labels = labels.clone();
224
225 for i in 0..batch_size {
226 let j = indices[i];
227 if i != j {
228 for k in 0..inputs.shape()[1] {
230 mixed_inputs[[i, k]] =
231 lambda * inputs[[i, k]] + (A::one() - lambda) * inputs[[j, k]];
232 }
233
234 for k in 0..labels.shape()[1] {
236 mixed_labels[[i, k]] =
237 lambda * labels[[i, k]] + (A::one() - lambda) * labels[[j, k]];
238 }
239 }
240 }
241
242 Ok((mixed_inputs, mixed_labels))
243 }
244}
245
246#[derive(Debug, Clone)]
267pub struct CutMix<A: Float> {
268 beta: A,
270}
271
272impl<A: Float + Debug + ScalarOperand + FromPrimitive + Send + Sync> CutMix<A> {
273 pub fn new(beta: A) -> Result<Self> {
283 if beta <= A::zero() {
284 return Err(OptimError::InvalidConfig(
285 "Beta must be positive".to_string(),
286 ));
287 }
288
289 Ok(Self { beta })
290 }
291
292 fn generate_bbox(
305 &self,
306 height: usize,
307 width: usize,
308 lambda: A,
309 rng: &mut scirs2_core::random::Random,
310 ) -> (usize, usize, usize, usize) {
311 let cut_ratio = A::sqrt(A::one() - lambda);
312
313 let h_ratio = cut_ratio.to_f64().unwrap_or(0.0);
314 let w_ratio = cut_ratio.to_f64().unwrap_or(0.0);
315
316 let cut_h = (height as f64 * h_ratio) as usize;
317 let cut_w = (width as f64 * w_ratio) as usize;
318
319 let cut_h = cut_h.max(1).min(height);
321 let cut_w = cut_w.max(1).min(width);
322
323 let cy = rng.gen_range(0..height - 1);
325 let cx = rng.gen_range(0..width - 1);
326
327 let half_h = cut_h / 2;
329 let half_w = cut_w / 2;
330
331 let y_min = cy.saturating_sub(half_h);
332 let y_max = (cy + half_h).min(height);
333 let x_min = cx.saturating_sub(half_w);
334 let x_max = (cx + half_w).min(width);
335
336 (y_min, y_max, x_min, x_max)
337 }
338
339 pub fn beta(&self) -> A {
341 self.beta
342 }
343
344 pub fn mixing_factor(&self, seed: u64) -> A {
359 let mut rng = Random::seed(seed);
360 let beta = self.beta.to_f64().unwrap_or(1.0);
361 let lambda = sample_beta(beta, beta, &mut rng);
362 A::from_f64(lambda).unwrap_or_else(|| A::one() / (A::one() + A::one()))
363 }
364
365 pub fn apply_batch(
377 &self,
378 images: &Array4<A>,
379 labels: &Array2<A>,
380 seed: u64,
381 ) -> Result<(Array4<A>, Array2<A>)> {
382 let batch_size = images.shape()[0];
383 if batch_size < 2 {
384 return Err(OptimError::InvalidConfig(
385 "Batch size must be at least 2 for CutMix".to_string(),
386 ));
387 }
388
389 if labels.shape()[0] != batch_size {
390 return Err(OptimError::InvalidConfig(
391 "Number of images and labels must match".to_string(),
392 ));
393 }
394
395 let mut rng = scirs2_core::random::Random::seed(seed + 1); let lambda = self.mixing_factor(seed);
397
398 let mut indices: Vec<usize> = (0..batch_size).collect();
400 for i in (1..indices.len()).rev() {
401 let j = rng.gen_range(0..i + 1);
402 indices.swap(i, j);
403 }
404
405 let mut bbox_rng = scirs2_core::random::Random::default();
407
408 let mut mixed_images = images.clone();
410 let mut mixed_labels = labels.clone();
411
412 let channels = images.shape()[1];
414 let height = images.shape()[2];
415 let width = images.shape()[3];
416
417 for i in 0..batch_size {
418 let j = indices[i];
419 if i != j {
420 let (y_min, y_max, x_min, x_max) =
422 self.generate_bbox(height, width, lambda, &mut bbox_rng);
423
424 let box_area = (y_max - y_min) * (x_max - x_min);
426 let image_area = height * width;
427 let actual_lambda =
428 A::from_f64(box_area as f64 / image_area as f64).unwrap_or_else(A::zero);
429
430 for c in 0..channels {
432 for y in y_min..y_max {
433 for x in x_min..x_max {
434 mixed_images[[i, c, y, x]] = images[[j, c, y, x]];
435 }
436 }
437 }
438
439 for k in 0..labels.shape()[1] {
441 mixed_labels[[i, k]] = (A::one() - actual_lambda) * labels[[i, k]]
442 + actual_lambda * labels[[j, k]];
443 }
444 }
445 }
446
447 Ok((mixed_images, mixed_labels))
448 }
449}
450
451impl<A: Float + Debug + ScalarOperand + FromPrimitive, D: Dimension + Send + Sync> Regularizer<A, D>
453 for MixUp<A>
454{
455 fn apply(&self, _params: &Array<A, D>, _gradients: &mut Array<A, D>) -> Result<A> {
456 Ok(A::zero())
458 }
459
460 fn penalty(&self, _params: &Array<A, D>) -> Result<A> {
461 Ok(A::zero())
463 }
464}
465
466impl<A: Float + Debug + ScalarOperand + FromPrimitive, D: Dimension + Send + Sync> Regularizer<A, D>
468 for CutMix<A>
469{
470 fn apply(&self, _params: &Array<A, D>, _gradients: &mut Array<A, D>) -> Result<A> {
471 Ok(A::zero())
473 }
474
475 fn penalty(&self, _params: &Array<A, D>) -> Result<A> {
476 Ok(A::zero())
478 }
479}
480
481#[cfg(test)]
482mod tests {
483 use super::*;
484 use scirs2_core::ndarray::array;
485
486 #[test]
487 fn test_mixup_creation() {
488 let mixup =
489 MixUp::<f64>::new(0.2).expect("MixUp::<f64>::new succeeds in test_mixup_creation");
490 assert_eq!(mixup.alpha, 0.2);
491
492 assert!(MixUp::<f64>::new(0.0).is_err());
494 assert!(MixUp::<f64>::new(-0.1).is_err());
495 }
496
497 #[test]
498 fn test_cutmix_creation() {
499 let cutmix =
500 CutMix::<f64>::new(1.0).expect("CutMix::<f64>::new succeeds in test_cutmix_creation");
501 assert_eq!(cutmix.beta, 1.0);
502
503 assert!(CutMix::<f64>::new(0.0).is_err());
505 assert!(CutMix::<f64>::new(-0.5).is_err());
506 }
507
508 #[test]
509 fn test_mixing_factor() {
510 let mixup = MixUp::new(0.2).expect("MixUp::new succeeds in test_mixing_factor");
511
512 let lambda1 = mixup.mixing_factor(42);
514 let lambda2 = mixup.mixing_factor(42);
515 let lambda3 = mixup.mixing_factor(123);
516
517 assert_eq!(lambda1, lambda2);
519
520 assert_ne!(lambda1, lambda3);
522
523 assert!((0.0..=1.0).contains(&lambda1));
525 assert!((0.0..=1.0).contains(&lambda3));
526 }
527
528 #[test]
529 fn test_gamma_sampler_matches_theoretical_moments() {
530 let mut rng = Random::seed(20240517);
533 for &shape in &[0.3f64, 1.0, 4.5] {
534 let n = 20_000;
535 let mut sum = 0.0;
536 let mut sum_sq = 0.0;
537 for _ in 0..n {
538 let x = sample_gamma(shape, &mut rng);
539 assert!(x >= 0.0 && x.is_finite(), "invalid gamma draw {x}");
540 sum += x;
541 sum_sq += x * x;
542 }
543 let mean = sum / n as f64;
544 let variance = sum_sq / n as f64 - mean * mean;
545 assert!(
546 (mean - shape).abs() < 0.15 * shape.max(1.0),
547 "shape {shape}: mean {mean} != {shape}"
548 );
549 assert!(
550 (variance - shape).abs() < 0.3 * shape.max(1.0),
551 "shape {shape}: variance {variance} != {shape}"
552 );
553 }
554 }
555
556 #[test]
557 fn test_mixing_factor_honours_alpha() {
558 let small = MixUp::<f64>::new(0.2)
561 .expect("MixUp::<f64>::new succeeds in test_mixing_factor_honours_alpha");
562 let large = MixUp::<f64>::new(5.0)
563 .expect("MixUp::<f64>::new succeeds in test_mixing_factor_honours_alpha");
564
565 let variance_of = |m: &MixUp<f64>| {
566 let n = 4000u64;
567 let samples: Vec<f64> = (0..n).map(|s| m.mixing_factor(s * 7 + 1)).collect();
568 let mean = samples.iter().sum::<f64>() / n as f64;
569 let var = samples.iter().map(|x| (x - mean) * (x - mean)).sum::<f64>() / n as f64;
570 (mean, var)
571 };
572
573 let (small_mean, small_var) = variance_of(&small);
574 let (large_mean, large_var) = variance_of(&large);
575
576 assert!((small_mean - 0.5).abs() < 0.05, "mean {small_mean}");
577 assert!((large_mean - 0.5).abs() < 0.05, "mean {large_mean}");
578 assert!(small_var > 5.0 * large_var, "{small_var} vs {large_var}");
579 }
580
581 #[test]
582 fn test_mixup_batch() {
583 let mixup = MixUp::new(0.5).expect("MixUp::new succeeds in test_mixup_batch");
584
585 let inputs = array![[1.0, 2.0], [3.0, 4.0]];
587 let labels = array![[1.0, 0.0], [0.0, 1.0]];
588
589 let (mixed_inputs, mixed_labels) = mixup
590 .apply_batch(&inputs, &labels, 42)
591 .expect("apply_batch succeeds in test_mixup_batch");
592
593 assert_eq!(mixed_inputs.shape(), inputs.shape());
595 assert_eq!(mixed_labels.shape(), labels.shape());
596
597 let min_input_val = *inputs.iter().fold(
599 &inputs[[0, 0]],
600 |min, val| if val < min { val } else { min },
601 );
602 let max_input_val = *inputs.iter().fold(
603 &inputs[[0, 0]],
604 |max, val| if val > max { val } else { max },
605 );
606
607 for i in 0..2 {
608 for j in 0..2 {
609 assert!(
610 mixed_inputs[[i, j]] >= min_input_val && mixed_inputs[[i, j]] <= max_input_val
611 );
612 }
613
614 for j in 0..2 {
615 assert!(mixed_labels[[i, j]] >= 0.0 && mixed_labels[[i, j]] <= 1.0);
616 }
617
618 assert!((mixed_labels.row(i).sum() - 1.0).abs() < 1e-10);
620 }
621 }
622
623 #[test]
624 fn test_cutmix_batch() {
625 let cutmix = CutMix::new(1.0).expect("CutMix::new succeeds in test_cutmix_batch");
626
627 let images =
629 Array4::from_shape_fn((2, 1, 5, 5), |(i, _, _, _)| if i == 0 { 1.0 } else { 2.0 });
630
631 let labels = array![[1.0, 0.0], [0.0, 1.0]];
632
633 let (mixed_images, mixed_labels) = cutmix
634 .apply_batch(&images, &labels, 123)
635 .expect("apply_batch succeeds in test_cutmix_batch"); assert_eq!(mixed_images.shape(), images.shape());
639 assert_eq!(mixed_labels.shape(), labels.shape());
640
641 let mut found_mixing = false;
643
644 for y in 0..5 {
646 for x in 0..5 {
647 if images[[0, 0, y, x]] != mixed_images[[0, 0, y, x]] {
648 found_mixing = true;
649 break;
650 }
651 }
652 if found_mixing {
653 break;
654 }
655 }
656
657 if !found_mixing {
659 for i in 0..2 {
660 for j in 0..2 {
661 if (labels[[i, j]] - mixed_labels[[i, j]]).abs() > 1e-10 {
663 found_mixing = true;
664 break;
665 }
666 }
667 if found_mixing {
668 break;
669 }
670 }
671 }
672
673 if !found_mixing {
676 println!("Warning: CutMix algorithm may not be producing expected mixing");
677 }
678 for i in 0..2 {
683 for j in 0..2 {
684 assert!(mixed_labels[[i, j]] >= 0.0 && mixed_labels[[i, j]] <= 1.0);
685 }
686
687 assert!((mixed_labels.row(i).sum() - 1.0).abs() < 1e-10);
689 }
690 }
691
692 #[test]
693 fn test_mixup_regularizer_trait() {
694 let mixup = MixUp::new(0.5).expect("MixUp::new succeeds in test_mixup_regularizer_trait");
695 let params = array![[1.0, 2.0], [3.0, 4.0]];
696 let mut gradients = array![[0.1, 0.2], [0.3, 0.4]];
697 let original_gradients = gradients.clone();
698
699 let penalty = mixup
700 .apply(¶ms, &mut gradients)
701 .expect("mixup.apply succeeds in test_mixup_regularizer_trait");
702
703 assert_eq!(penalty, 0.0);
705
706 assert_eq!(gradients, original_gradients);
708 }
709
710 #[test]
711 fn test_cutmix_regularizer_trait() {
712 let cutmix =
713 CutMix::new(1.0).expect("CutMix::new succeeds in test_cutmix_regularizer_trait");
714 let params = array![[1.0, 2.0], [3.0, 4.0]];
715 let mut gradients = array![[0.1, 0.2], [0.3, 0.4]];
716 let original_gradients = gradients.clone();
717
718 let penalty = cutmix
719 .apply(¶ms, &mut gradients)
720 .expect("apply succeeds in test_cutmix_regularizer_trait");
721
722 assert_eq!(penalty, 0.0);
724
725 assert_eq!(gradients, original_gradients);
727 }
728}