1#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum NormMode {
11 Training,
13 Inference,
15}
16
17#[derive(Clone, Debug)]
19pub struct BatchNormConfig {
20 pub num_features: usize,
22 pub epsilon: f64,
24 pub momentum: f64,
26 pub affine: bool,
28}
29
30impl BatchNormConfig {
31 pub fn default_for(num_features: usize) -> Self {
33 Self {
34 num_features,
35 epsilon: 1e-5,
36 momentum: 0.1,
37 affine: true,
38 }
39 }
40}
41
42#[derive(Clone, Debug, Default)]
44pub struct BatchNormStats {
45 pub total_forward_passes: u64,
47 pub training_passes: u64,
49 pub inference_passes: u64,
51}
52
53pub struct TensorBatchNorm {
76 pub config: BatchNormConfig,
78 pub gamma: Vec<f64>,
80 pub beta: Vec<f64>,
82 pub running_mean: Vec<f64>,
84 pub running_var: Vec<f64>,
86 pub mode: NormMode,
88 pub stats: BatchNormStats,
90}
91
92impl TensorBatchNorm {
93 pub fn new(config: BatchNormConfig) -> Self {
95 let n = config.num_features;
96 Self {
97 gamma: vec![1.0; n],
98 beta: vec![0.0; n],
99 running_mean: vec![0.0; n],
100 running_var: vec![1.0; n],
101 mode: NormMode::Training,
102 stats: BatchNormStats::default(),
103 config,
104 }
105 }
106
107 pub fn set_mode(&mut self, mode: NormMode) {
109 self.mode = mode;
110 }
111
112 pub fn forward(&mut self, input: &[Vec<f64>]) -> Option<Vec<Vec<f64>>> {
120 let n = self.config.num_features;
121 let batch = input.len();
122
123 if batch == 0 {
124 return None;
125 }
126 for row in input {
127 if row.len() != n {
128 return None;
129 }
130 }
131
132 let output = match self.mode {
133 NormMode::Training => self.forward_training(input, batch, n),
134 NormMode::Inference => self.forward_inference(input, batch, n),
135 };
136
137 self.stats.total_forward_passes += 1;
139 match self.mode {
140 NormMode::Training => self.stats.training_passes += 1,
141 NormMode::Inference => self.stats.inference_passes += 1,
142 }
143
144 Some(output)
145 }
146
147 fn forward_training(&mut self, input: &[Vec<f64>], batch: usize, n: usize) -> Vec<Vec<f64>> {
152 let batch_f = batch as f64;
153 let momentum = self.config.momentum;
154 let epsilon = self.config.epsilon;
155
156 let mut batch_mean = vec![0.0_f64; n];
158 let mut batch_var = vec![0.0_f64; n];
159
160 for row in input {
161 for f in 0..n {
162 batch_mean[f] += row[f];
163 }
164 }
165 for m in batch_mean.iter_mut() {
166 *m /= batch_f;
167 }
168
169 for row in input {
170 for f in 0..n {
171 let diff = row[f] - batch_mean[f];
172 batch_var[f] += diff * diff;
173 }
174 }
175 for v in batch_var.iter_mut() {
176 *v /= batch_f; }
178
179 for f in 0..n {
181 self.running_mean[f] =
182 (1.0 - momentum) * self.running_mean[f] + momentum * batch_mean[f];
183 self.running_var[f] = (1.0 - momentum) * self.running_var[f] + momentum * batch_var[f];
184 }
185
186 self.normalise(input, batch, n, &batch_mean, &batch_var, epsilon)
188 }
189
190 fn forward_inference(&self, input: &[Vec<f64>], batch: usize, n: usize) -> Vec<Vec<f64>> {
191 let epsilon = self.config.epsilon;
192 self.normalise(
193 input,
194 batch,
195 n,
196 &self.running_mean,
197 &self.running_var,
198 epsilon,
199 )
200 }
201
202 fn normalise(
204 &self,
205 input: &[Vec<f64>],
206 batch: usize,
207 n: usize,
208 mean: &[f64],
209 var: &[f64],
210 epsilon: f64,
211 ) -> Vec<Vec<f64>> {
212 let mut output = vec![vec![0.0_f64; n]; batch];
213 for b in 0..batch {
214 for f in 0..n {
215 let x_hat = (input[b][f] - mean[f]) / (var[f] + epsilon).sqrt();
216 output[b][f] = if self.config.affine {
217 self.gamma[f] * x_hat + self.beta[f]
218 } else {
219 x_hat
220 };
221 }
222 }
223 output
224 }
225
226 pub fn set_gamma(&mut self, gamma: Vec<f64>) -> bool {
231 if gamma.len() != self.config.num_features {
232 return false;
233 }
234 self.gamma = gamma;
235 true
236 }
237
238 pub fn set_beta(&mut self, beta: Vec<f64>) -> bool {
243 if beta.len() != self.config.num_features {
244 return false;
245 }
246 self.beta = beta;
247 true
248 }
249
250 pub fn stats(&self) -> &BatchNormStats {
252 &self.stats
253 }
254}
255
256#[cfg(test)]
261mod tests {
262 use super::*;
263
264 fn make_bn(num_features: usize) -> TensorBatchNorm {
269 TensorBatchNorm::new(BatchNormConfig::default_for(num_features))
270 }
271
272 fn mean(v: &[f64]) -> f64 {
274 v.iter().sum::<f64>() / v.len() as f64
275 }
276
277 fn variance(v: &[f64]) -> f64 {
279 let m = mean(v);
280 v.iter().map(|x| (x - m).powi(2)).sum::<f64>() / v.len() as f64
281 }
282
283 #[test]
288 fn test_new_initialisation() {
289 let bn = make_bn(3);
290 assert_eq!(bn.gamma, vec![1.0, 1.0, 1.0]);
291 assert_eq!(bn.beta, vec![0.0, 0.0, 0.0]);
292 assert_eq!(bn.running_mean, vec![0.0, 0.0, 0.0]);
293 assert_eq!(bn.running_var, vec![1.0, 1.0, 1.0]);
294 assert_eq!(bn.mode, NormMode::Training);
295 }
296
297 #[test]
302 fn test_training_output_shape() {
303 let mut bn = make_bn(4);
304 let batch = vec![
305 vec![1.0, 2.0, 3.0, 4.0],
306 vec![5.0, 6.0, 7.0, 8.0],
307 vec![9.0, 10.0, 11.0, 12.0],
308 ];
309 let out = bn.forward(&batch).expect("forward returned None");
310 assert_eq!(out.len(), 3);
311 for row in &out {
312 assert_eq!(row.len(), 4);
313 }
314 }
315
316 #[test]
317 fn test_training_output_mean_near_zero() {
318 let mut bn = make_bn(2);
319 bn.config.affine = false;
321 let batch = vec![
322 vec![1.0, 10.0],
323 vec![2.0, 20.0],
324 vec![3.0, 30.0],
325 vec![4.0, 40.0],
326 ];
327 let out = bn.forward(&batch).expect("forward None");
328
329 let f0: Vec<f64> = out.iter().map(|r| r[0]).collect();
331 let f1: Vec<f64> = out.iter().map(|r| r[1]).collect();
332
333 assert!(mean(&f0).abs() < 1e-10, "mean of feature 0 ≈ 0");
334 assert!(mean(&f1).abs() < 1e-10, "mean of feature 1 ≈ 0");
335 }
336
337 #[test]
338 fn test_training_output_var_near_one() {
339 let mut bn = make_bn(2);
340 bn.config.affine = false;
341 let batch = vec![
342 vec![1.0, 10.0],
343 vec![2.0, 20.0],
344 vec![3.0, 30.0],
345 vec![4.0, 40.0],
346 ];
347 let out = bn.forward(&batch).expect("forward None");
348 let f0: Vec<f64> = out.iter().map(|r| r[0]).collect();
349 let f1: Vec<f64> = out.iter().map(|r| r[1]).collect();
350
351 let var0 = variance(&f0);
353 let var1 = variance(&f1);
354 assert!((var0 - 1.0).abs() < 1e-4, "var feature 0 ≈ 1, got {var0}");
355 assert!((var1 - 1.0).abs() < 1e-4, "var feature 1 ≈ 1, got {var1}");
356 }
357
358 #[test]
363 fn test_running_mean_updated() {
364 let mut bn = make_bn(1);
365 let batch = vec![vec![3.0], vec![7.0]];
368 bn.forward(&batch);
369 let expected = 0.1 * 5.0; assert!(
371 (bn.running_mean[0] - expected).abs() < 1e-12,
372 "running_mean = {}, expected {expected}",
373 bn.running_mean[0]
374 );
375 }
376
377 #[test]
378 fn test_running_var_updated() {
379 let mut bn = make_bn(1);
380 let batch = vec![vec![3.0], vec![7.0]];
384 bn.forward(&batch);
385 let expected = 0.9 * 1.0 + 0.1 * 4.0;
386 assert!(
387 (bn.running_var[0] - expected).abs() < 1e-12,
388 "running_var = {}, expected {expected}",
389 bn.running_var[0]
390 );
391 }
392
393 #[test]
394 fn test_running_stats_accumulate_over_multiple_passes() {
395 let mut bn = make_bn(1);
396 let batch1 = vec![vec![0.0], vec![2.0]]; let batch2 = vec![vec![4.0], vec![8.0]]; bn.forward(&batch1);
400 let rm1 = bn.running_mean[0]; let rv1 = bn.running_var[0]; bn.forward(&batch2);
404 let rm2_expected = 0.9 * rm1 + 0.1 * 6.0;
405 let rv2_expected = 0.9 * rv1 + 0.1 * 4.0;
406
407 assert!((bn.running_mean[0] - rm2_expected).abs() < 1e-12);
408 assert!((bn.running_var[0] - rv2_expected).abs() < 1e-12);
409 }
410
411 #[test]
416 fn test_inference_uses_running_stats() {
417 let mut bn = make_bn(1);
418
419 bn.running_mean[0] = 5.0;
421 bn.running_var[0] = 4.0; bn.config.affine = false;
423 bn.set_mode(NormMode::Inference);
424
425 let batch = vec![vec![7.0]];
426 let out = bn.forward(&batch).expect("None");
427 let expected = (7.0 - 5.0) / (4.0_f64 + 1e-5).sqrt();
429 assert!((out[0][0] - expected).abs() < 1e-9);
430 }
431
432 #[test]
433 fn test_inference_running_stats_not_updated() {
434 let mut bn = make_bn(1);
435 bn.running_mean[0] = 3.0;
436 bn.running_var[0] = 2.0;
437 bn.set_mode(NormMode::Inference);
438
439 bn.forward(&[vec![10.0], vec![20.0]]);
440 assert!((bn.running_mean[0] - 3.0).abs() < 1e-15);
442 assert!((bn.running_var[0] - 2.0).abs() < 1e-15);
443 }
444
445 #[test]
450 fn test_affine_true_applies_gamma_beta() {
451 let mut bn = make_bn(1);
452 bn.gamma[0] = 2.0;
453 bn.beta[0] = 1.0;
454 bn.config.affine = true;
455
456 let batch = vec![vec![1.0], vec![3.0]]; let out = bn.forward(&batch).expect("None");
460
461 let eps: f64 = 1e-5;
464 let expected0 = 2.0 * ((1.0 - 2.0) / (1.0_f64 + eps).sqrt()) + 1.0;
465 let expected1 = 2.0 * ((3.0 - 2.0) / (1.0_f64 + eps).sqrt()) + 1.0;
466 assert!((out[0][0] - expected0).abs() < 1e-9);
467 assert!((out[1][0] - expected1).abs() < 1e-9);
468 }
469
470 #[test]
471 fn test_affine_false_no_gamma_beta() {
472 let mut bn = make_bn(1);
473 bn.gamma[0] = 99.0; bn.beta[0] = 99.0;
475 bn.config.affine = false;
476
477 let batch = vec![vec![1.0], vec![3.0]]; let out = bn.forward(&batch).expect("None");
479 let eps: f64 = 1e-5;
480 let expected0 = (1.0 - 2.0) / (1.0_f64 + eps).sqrt();
481 let expected1 = (3.0 - 2.0) / (1.0_f64 + eps).sqrt();
482 assert!((out[0][0] - expected0).abs() < 1e-9);
483 assert!((out[1][0] - expected1).abs() < 1e-9);
484 }
485
486 #[test]
491 fn test_set_gamma_correct_size() {
492 let mut bn = make_bn(3);
493 assert!(bn.set_gamma(vec![2.0, 3.0, 4.0]));
494 assert_eq!(bn.gamma, vec![2.0, 3.0, 4.0]);
495 }
496
497 #[test]
498 fn test_set_gamma_wrong_size_returns_false() {
499 let mut bn = make_bn(3);
500 let old = bn.gamma.clone();
501 assert!(!bn.set_gamma(vec![1.0, 2.0]));
502 assert_eq!(bn.gamma, old, "gamma must be unchanged after rejection");
503 }
504
505 #[test]
506 fn test_set_beta_correct_size() {
507 let mut bn = make_bn(2);
508 assert!(bn.set_beta(vec![0.5, -0.5]));
509 assert_eq!(bn.beta, vec![0.5, -0.5]);
510 }
511
512 #[test]
513 fn test_set_beta_wrong_size_returns_false() {
514 let mut bn = make_bn(2);
515 let old = bn.beta.clone();
516 assert!(!bn.set_beta(vec![1.0, 2.0, 3.0]));
517 assert_eq!(bn.beta, old);
518 }
519
520 #[test]
521 fn test_set_gamma_empty_wrong_size() {
522 let mut bn = make_bn(2);
523 assert!(!bn.set_gamma(vec![]));
524 }
525
526 #[test]
527 fn test_set_beta_empty_wrong_size() {
528 let mut bn = make_bn(2);
529 assert!(!bn.set_beta(vec![]));
530 }
531
532 #[test]
537 fn test_forward_none_empty_batch() {
538 let mut bn = make_bn(3);
539 assert!(bn.forward(&[]).is_none());
540 }
541
542 #[test]
543 fn test_forward_none_wrong_feature_count() {
544 let mut bn = make_bn(3);
545 let bad = vec![vec![1.0, 2.0]]; assert!(bn.forward(&bad).is_none());
547 }
548
549 #[test]
550 fn test_forward_none_one_row_wrong_features() {
551 let mut bn = make_bn(4);
552 let batch = vec![
553 vec![1.0, 2.0, 3.0, 4.0],
554 vec![5.0, 6.0, 7.0], ];
556 assert!(bn.forward(&batch).is_none());
557 }
558
559 #[test]
564 fn test_stats_training_pass_count() {
565 let mut bn = make_bn(2);
566 let batch = vec![vec![1.0, 2.0], vec![3.0, 4.0]];
567 bn.forward(&batch);
568 bn.forward(&batch);
569 assert_eq!(bn.stats().training_passes, 2);
570 assert_eq!(bn.stats().inference_passes, 0);
571 assert_eq!(bn.stats().total_forward_passes, 2);
572 }
573
574 #[test]
575 fn test_stats_inference_pass_count() {
576 let mut bn = make_bn(2);
577 bn.set_mode(NormMode::Inference);
578 let batch = vec![vec![1.0, 2.0], vec![3.0, 4.0]];
579 bn.forward(&batch);
580 assert_eq!(bn.stats().inference_passes, 1);
581 assert_eq!(bn.stats().training_passes, 0);
582 assert_eq!(bn.stats().total_forward_passes, 1);
583 }
584
585 #[test]
586 fn test_stats_mixed_modes() {
587 let mut bn = make_bn(1);
588 let b = vec![vec![1.0], vec![2.0]];
589 bn.set_mode(NormMode::Training);
590 bn.forward(&b);
591 bn.forward(&b);
592 bn.set_mode(NormMode::Inference);
593 bn.forward(&b);
594 assert_eq!(bn.stats().training_passes, 2);
595 assert_eq!(bn.stats().inference_passes, 1);
596 assert_eq!(bn.stats().total_forward_passes, 3);
597 }
598
599 #[test]
600 fn test_stats_not_incremented_on_none() {
601 let mut bn = make_bn(2);
602 bn.forward(&[]); assert_eq!(bn.stats().total_forward_passes, 0);
604 }
605
606 #[test]
611 fn test_custom_epsilon() {
612 let config = BatchNormConfig {
613 num_features: 1,
614 epsilon: 1.0,
615 momentum: 0.1,
616 affine: false,
617 };
618 let mut bn = TensorBatchNorm::new(config);
619 let batch = vec![vec![1.0], vec![3.0]];
622 let out = bn.forward(&batch).expect("None");
623 let expected = (1.0 - 2.0) / (1.0_f64 + 1.0).sqrt();
624 assert!((out[0][0] - expected).abs() < 1e-9);
625 }
626
627 #[test]
628 fn test_custom_momentum() {
629 let config = BatchNormConfig {
630 num_features: 1,
631 epsilon: 1e-5,
632 momentum: 0.5, affine: false,
634 };
635 let mut bn = TensorBatchNorm::new(config);
636 let batch = vec![vec![1.0], vec![3.0]];
639 bn.forward(&batch);
640 assert!((bn.running_mean[0] - 1.0).abs() < 1e-12);
641 }
642
643 #[test]
648 fn test_set_mode_switches_correctly() {
649 let mut bn = make_bn(1);
650 assert_eq!(bn.mode, NormMode::Training);
651 bn.set_mode(NormMode::Inference);
652 assert_eq!(bn.mode, NormMode::Inference);
653 bn.set_mode(NormMode::Training);
654 assert_eq!(bn.mode, NormMode::Training);
655 }
656
657 #[test]
662 fn test_default_for_values() {
663 let cfg = BatchNormConfig::default_for(8);
664 assert_eq!(cfg.num_features, 8);
665 assert!((cfg.epsilon - 1e-5).abs() < 1e-15);
666 assert!((cfg.momentum - 0.1).abs() < 1e-15);
667 assert!(cfg.affine);
668 }
669
670 #[test]
675 fn test_multi_feature_independent_normalisation() {
676 let mut bn = make_bn(2);
677 bn.config.affine = false;
678 let batch = vec![vec![0.0, 10.0], vec![4.0, 10.0]];
681 let out = bn.forward(&batch).expect("None");
682
683 let eps = 1e-5;
685 let exp0_0 = (0.0 - 2.0) / (4.0_f64 + eps).sqrt();
686 let exp0_1 = (4.0 - 2.0) / (4.0_f64 + eps).sqrt();
687 assert!((out[0][0] - exp0_0).abs() < 1e-9);
688 assert!((out[1][0] - exp0_1).abs() < 1e-9);
689
690 let exp1 = (10.0 - 10.0) / (0.0_f64 + eps).sqrt();
692 assert!((out[0][1] - exp1).abs() < 1e-9);
693 assert!((out[1][1] - exp1).abs() < 1e-9);
694 }
695
696 #[test]
701 fn test_norm_mode_copy_clone() {
702 let m = NormMode::Training;
703 let m2 = m; let m3 = m; assert_eq!(m, m2);
706 assert_eq!(m, m3);
707 assert_ne!(m, NormMode::Inference);
708 }
709
710 #[test]
715 fn test_large_batch_normalisation_accuracy() {
716 let n = 1;
717 let mut bn = TensorBatchNorm::new(BatchNormConfig {
718 num_features: n,
719 epsilon: 1e-8,
720 momentum: 0.1,
721 affine: false,
722 });
723
724 let batch: Vec<Vec<f64>> = (1..=100).map(|i| vec![i as f64]).collect();
726 let out = bn.forward(&batch).expect("None");
727
728 let vals: Vec<f64> = out.iter().map(|r| r[0]).collect();
729 let m = mean(&vals);
730 let v = variance(&vals);
731
732 assert!(m.abs() < 1e-10, "mean ≈ 0, got {m}");
733 assert!((v - 1.0).abs() < 1e-6, "var ≈ 1, got {v}");
734 }
735}