use ndarray::ArrayD;
use rustyml::error::{Error, NnError};
use rustyml::neural_network::layers::regularization::normalization::batch_normalization::BatchNormalization;
use rustyml::neural_network::layers::regularization::normalization::layer_normalization::{
LayerNormalization, LayerNormalizationAxis,
};
use rustyml::neural_network::traits::Layer;
use crate::common::assert_allclose;
fn tensor1(data: Vec<f32>) -> rustyml::neural_network::Tensor {
let n = data.len();
ArrayD::from_shape_vec(vec![n], data).expect("tensor1: shape/data mismatch")
}
fn tensor2(data: Vec<f32>, rows: usize, cols: usize) -> rustyml::neural_network::Tensor {
ArrayD::from_shape_vec(vec![rows, cols], data).expect("tensor2: shape/data mismatch")
}
#[test]
fn bn_constructor_rejects_empty_input_shape() {
let result = BatchNormalization::new(vec![], 0.9, 1e-5);
assert!(
matches!(result, Err(Error::EmptyInput(_))),
"expected EmptyInput, got {:?}",
result
);
}
#[test]
fn bn_constructor_rejects_momentum_above_one() {
let result = BatchNormalization::new(vec![4, 3], 1.5, 1e-5);
assert!(
matches!(result, Err(Error::InvalidParameter { .. })),
"expected InvalidParameter, got {:?}",
result
);
}
#[test]
fn bn_constructor_rejects_negative_momentum() {
let result = BatchNormalization::new(vec![4, 3], -0.1, 1e-5);
assert!(
matches!(result, Err(Error::InvalidParameter { .. })),
"expected InvalidParameter, got {:?}",
result
);
}
#[test]
fn bn_constructor_rejects_zero_epsilon() {
let result = BatchNormalization::new(vec![4, 3], 0.9, 0.0);
assert!(
matches!(result, Err(Error::InvalidParameter { .. })),
"expected InvalidParameter, got {:?}",
result
);
}
#[test]
fn bn_constructor_rejects_negative_epsilon() {
let result = BatchNormalization::new(vec![4, 3], 0.9, -1e-5);
assert!(
matches!(result, Err(Error::InvalidParameter { .. })),
"expected InvalidParameter, got {:?}",
result
);
}
#[test]
fn bn_constructor_accepts_boundary_momentum_values() {
assert!(BatchNormalization::new(vec![4, 3], 0.0, 1e-5).is_ok());
assert!(BatchNormalization::new(vec![4, 3], 1.0, 1e-5).is_ok());
}
#[test]
fn bn_forward_rejects_wrong_input_shape() {
let mut bn = BatchNormalization::new(vec![4, 3], 0.9, 1e-5).unwrap();
let wrong = tensor2(vec![1.0f32; 6], 2, 3);
let result = bn.forward(&wrong);
assert!(
matches!(result, Err(Error::ShapeMismatch { .. })),
"expected ShapeMismatch, got {:?}",
result
);
}
#[test]
fn bn_train_output_has_batch_mean_zero_and_var_one() {
let data = vec![
2.0f32, 4.0, 6.0, 4.0, 2.0, 8.0, 6.0, 4.0, 4.0, 8.0, 2.0, 6.0, ];
let input = tensor2(data, 4, 3);
let mut bn = BatchNormalization::new(vec![4, 3], 0.9, 1e-5).unwrap();
let output = bn.forward(&input).unwrap();
assert_eq!(output.shape(), &[4, 3]);
for feat in 0..3 {
let col: Vec<f32> = (0..4).map(|r| output[[r, feat]]).collect();
let mean: f32 = col.iter().sum::<f32>() / 4.0;
let var: f32 = col.iter().map(|&v| (v - mean).powi(2)).sum::<f32>() / 4.0;
assert!(
mean.abs() < 1e-5,
"feature {feat}: batch mean too far from 0, got {mean}"
);
assert!(
(var - 1.0).abs() < 5e-4,
"feature {feat}: batch var too far from 1, got {var}"
);
}
}
#[test]
fn bn_train_forward_concrete_values_4x1() {
let input = tensor2(vec![2.0f32, 4.0, 6.0, 8.0], 4, 1);
let mut bn = BatchNormalization::new(vec![4, 1], 0.9, 1e-5).unwrap();
let output = bn.forward(&input).unwrap();
let std = (5.0f32 + 1e-5f32).sqrt();
let expected = tensor2(vec![-3.0 / std, -1.0 / std, 1.0 / std, 3.0 / std], 4, 1);
assert_allclose(&output, &expected, 1e-5);
}
#[test]
fn bn_running_stats_update_after_one_forward() {
let input_train = tensor2(vec![2.0f32, 4.0, 6.0, 8.0], 4, 1);
let mut bn = BatchNormalization::new(vec![4, 1], 0.9, 1e-5).unwrap();
bn.forward(&input_train).unwrap();
bn.set_training_if_mode_dependent(false);
let input_eval = tensor2(vec![5.0f32, 5.0, 5.0, 5.0], 4, 1);
let output = bn.forward(&input_eval).unwrap();
let expected_val = 4.5_f32 / (1.4f32 + 1e-5f32).sqrt();
let expected = tensor2(vec![expected_val; 4], 4, 1);
assert_allclose(&output, &expected, 1e-4);
}
#[test]
fn bn_eval_uses_running_stats_from_set_weights() {
let mut bn = BatchNormalization::new(vec![2, 3], 0.9, 1e-5).unwrap();
let gamma = tensor1(vec![1.0f32, 1.0, 1.0]);
let beta = tensor1(vec![0.0f32, 0.0, 0.0]);
let running_mean = tensor1(vec![1.0f32, 2.0, 3.0]);
let running_var = tensor1(vec![4.0f32, 9.0, 1.0]);
bn.set_weights(gamma, beta, running_mean, running_var)
.unwrap();
bn.set_training_if_mode_dependent(false);
let input = tensor2(vec![1.0f32, 2.0, 3.0, 5.0, 8.0, 4.0], 2, 3);
let output = bn.forward(&input).unwrap();
let eps = 1e-5f32;
let e00 = 0.0f32;
let e01 = 0.0f32;
let e02 = 0.0f32;
let e10 = 4.0 / (4.0f32 + eps).sqrt();
let e11 = 6.0 / (9.0f32 + eps).sqrt();
let e12 = 1.0 / (1.0f32 + eps).sqrt();
let expected = tensor2(vec![e00, e01, e02, e10, e11, e12], 2, 3);
assert_allclose(&output, &expected, 1e-4);
}
#[test]
fn bn_predict_equals_forward_in_eval_mode() {
let mut bn = BatchNormalization::new(vec![2, 3], 0.9, 1e-5).unwrap();
let gamma = tensor1(vec![1.0f32, 1.0, 1.0]);
let beta = tensor1(vec![0.0f32, 0.0, 0.0]);
let running_mean = tensor1(vec![1.0f32, 2.0, 3.0]);
let running_var = tensor1(vec![4.0f32, 9.0, 1.0]);
bn.set_weights(gamma, beta, running_mean, running_var)
.unwrap();
bn.set_training_if_mode_dependent(false);
let input = tensor2(vec![1.0f32, 2.0, 3.0, 5.0, 8.0, 4.0], 2, 3);
let out_forward = bn.forward(&input).unwrap();
let out_predict = bn.predict(&input).unwrap();
assert_allclose(&out_forward, &out_predict, 0.0f32);
}
#[test]
fn bn_set_weights_rejects_wrong_gamma_shape() {
let mut bn = BatchNormalization::new(vec![4, 3], 0.9, 1e-5).unwrap();
let gamma_bad = tensor1(vec![1.0f32, 1.0]); let beta = tensor1(vec![0.0f32, 0.0, 0.0]);
let rm = tensor1(vec![0.0f32, 0.0, 0.0]);
let rv = tensor1(vec![1.0f32, 1.0, 1.0]);
let result = bn.set_weights(gamma_bad, beta, rm, rv);
assert!(
matches!(
result,
Err(Error::NeuralNetwork(NnError::WeightShape { .. }))
),
"expected NnError::WeightShape, got {:?}",
result
);
}
#[test]
fn bn_eval_applies_custom_gamma_and_beta() {
let mut bn = BatchNormalization::new(vec![1, 1], 0.9, 1e-5).unwrap();
let gamma = tensor1(vec![2.0f32]);
let beta = tensor1(vec![1.0f32]);
let running_mean = tensor1(vec![0.0f32]);
let running_var = tensor1(vec![1.0f32]);
bn.set_weights(gamma, beta, running_mean, running_var)
.unwrap();
bn.set_training_if_mode_dependent(false);
let input = tensor2(vec![3.0f32], 1, 1);
let output = bn.forward(&input).unwrap();
let eps = 1e-5f32;
let x_norm = 3.0_f32 / (1.0f32 + eps).sqrt();
let expected_val = x_norm * 2.0 + 1.0;
let expected = tensor2(vec![expected_val], 1, 1);
assert_allclose(&output, &expected, 1e-4);
}
#[test]
fn bn_running_stats_accumulate_over_multiple_forwards() {
let mut bn = BatchNormalization::new(vec![2, 1], 0.5, 1e-5).unwrap();
let x = tensor2(vec![0.0f32, 2.0], 2, 1);
bn.forward(&x).unwrap();
bn.forward(&x).unwrap();
bn.set_training_if_mode_dependent(false);
let x_eval = tensor2(vec![1.0f32, 5.0], 2, 1);
let output = bn.forward(&x_eval).unwrap();
let denom = (1.0f32 + 1e-5f32).sqrt();
let e0 = (1.0 - 0.75) / denom;
let e1 = (5.0 - 0.75) / denom;
let expected = tensor2(vec![e0, e1], 2, 1);
assert_allclose(&output, &expected, 1e-4);
}
#[test]
fn bn_uniform_batch_output_is_zero() {
let data = vec![
3.0f32, 7.0, -2.0, 3.0, 7.0, -2.0, 3.0, 7.0, -2.0, 3.0, 7.0, -2.0, ];
let input = tensor2(data, 4, 3);
let mut bn = BatchNormalization::new(vec![4, 3], 0.9, 1e-5).unwrap();
let output = bn.forward(&input).unwrap();
let zeros = tensor2(vec![0.0f32; 12], 4, 3);
assert_allclose(&output, &zeros, 1e-6);
}
#[test]
fn bn_training_and_eval_modes_produce_different_outputs() {
let input = tensor2(vec![1.0f32, 1.0, 3.0, 3.0], 2, 2);
let mut bn_train = BatchNormalization::new(vec![2, 2], 0.9, 1e-5).unwrap();
let out_train = bn_train.forward(&input).unwrap();
let mut bn_eval = BatchNormalization::new(vec![2, 2], 0.9, 1e-5).unwrap();
bn_eval.set_training_if_mode_dependent(false);
let out_eval = bn_eval.forward(&input).unwrap();
let differs = out_train
.iter()
.zip(out_eval.iter())
.any(|(a, b)| (a - b).abs() > 1e-5);
assert!(
differs,
"training and eval outputs are unexpectedly identical"
);
}
#[test]
fn ln_constructor_rejects_zero_epsilon() {
let result = LayerNormalization::new(vec![4, 3], 0.0);
assert!(
matches!(result, Err(Error::InvalidParameter { .. })),
"expected InvalidParameter, got {:?}",
result
);
}
#[test]
fn ln_constructor_rejects_negative_epsilon() {
let result = LayerNormalization::new(vec![4, 3], -1e-5);
assert!(
matches!(result, Err(Error::InvalidParameter { .. })),
"expected InvalidParameter, got {:?}",
result
);
}
#[test]
fn ln_constructor_rejects_multiple_empty_axes() {
let result = LayerNormalization::new(vec![4, 3], 1e-5)
.unwrap()
.with_normalized_axis(LayerNormalizationAxis::Multiple(vec![]));
assert!(
matches!(result, Err(Error::InvalidParameter { .. })),
"expected InvalidParameter, got {:?}",
result
);
}
#[test]
fn ln_constructor_rejects_multiple_out_of_bounds_axis() {
let result = LayerNormalization::new(vec![4, 3], 1e-5)
.unwrap()
.with_normalized_axis(LayerNormalizationAxis::Multiple(vec![0, 2]));
assert!(
matches!(result, Err(Error::InvalidParameter { .. })),
"expected InvalidParameter, got {:?}",
result
);
}
#[test]
fn ln_constructor_rejects_multiple_duplicate_axes() {
let result = LayerNormalization::new(vec![4, 3], 1e-5)
.unwrap()
.with_normalized_axis(LayerNormalizationAxis::Multiple(vec![0, 0]));
assert!(
matches!(result, Err(Error::InvalidParameter { .. })),
"expected InvalidParameter, got {:?}",
result
);
}
#[test]
fn ln_default_each_sample_has_mean_zero_and_var_one() {
let data = vec![
1.0f32, 3.0, 5.0, 7.0, 2.0, -2.0, 0.0, 4.0, ];
let input = tensor2(data, 2, 4);
let mut ln = LayerNormalization::new(vec![2, 4], 1e-5).unwrap();
let output = ln.forward(&input).unwrap();
assert_eq!(output.shape(), &[2, 4]);
for row in 0..2 {
let vals: Vec<f32> = (0..4).map(|c| output[[row, c]]).collect();
let mean: f32 = vals.iter().sum::<f32>() / 4.0;
let var: f32 = vals.iter().map(|&v| (v - mean).powi(2)).sum::<f32>() / 4.0;
assert!(mean.abs() < 1e-5, "row {row}: mean {mean} too far from 0");
assert!(
(var - 1.0).abs() < 5e-4,
"row {row}: var {var} too far from 1"
);
}
}
#[test]
fn ln_default_forward_concrete_values() {
let data = vec![
1.0f32, 3.0, 5.0, 7.0, 2.0, -2.0, 0.0, 4.0, ];
let input = tensor2(data, 2, 4);
let mut ln = LayerNormalization::new(vec![2, 4], 1e-5).unwrap();
let output = ln.forward(&input).unwrap();
let std_val = (5.0f32 + 1e-5f32).sqrt();
let expected = tensor2(
vec![
-3.0 / std_val,
-1.0 / std_val,
1.0 / std_val,
3.0 / std_val,
1.0 / std_val,
-3.0 / std_val,
-1.0 / std_val,
3.0 / std_val,
],
2,
4,
);
assert_allclose(&output, &expected, 1e-5);
}
#[test]
fn ln_custom_axis0_concrete_values() {
let data = vec![1.0f32, 4.0, 3.0, 2.0, 5.0, 6.0];
let input = tensor2(data, 3, 2);
let mut ln = LayerNormalization::new(vec![3, 2], 1e-5)
.unwrap()
.with_normalized_axis(LayerNormalizationAxis::Custom(0))
.unwrap();
let output = ln.forward(&input).unwrap();
let std_val = (8.0f32 / 3.0 + 1e-5f32).sqrt();
let expected = tensor2(
vec![
-2.0 / std_val,
0.0 / std_val,
0.0 / std_val,
-2.0 / std_val,
2.0 / std_val,
2.0 / std_val,
],
3,
2,
);
assert_allclose(&output, &expected, 1e-5);
}
#[test]
fn ln_custom_axis0_each_column_has_mean_zero_and_var_one() {
let data: Vec<f32> = (0..15).map(|v| v as f32 * 1.3 - 4.0).collect();
let input = tensor2(data, 5, 3);
let mut ln = LayerNormalization::new(vec![5, 3], 1e-5)
.unwrap()
.with_normalized_axis(LayerNormalizationAxis::Custom(0))
.unwrap();
let output = ln.forward(&input).unwrap();
assert_eq!(output.shape(), &[5, 3]);
for col in 0..3 {
let vals: Vec<f32> = (0..5).map(|r| output[[r, col]]).collect();
let mean: f32 = vals.iter().sum::<f32>() / 5.0;
let var: f32 = vals.iter().map(|&v| (v - mean).powi(2)).sum::<f32>() / 5.0;
assert!(mean.abs() < 1e-5, "col {col}: mean {mean} too far from 0");
assert!(
(var - 1.0).abs() < 5e-4,
"col {col}: var {var} too far from 1"
);
}
}
#[test]
fn ln_multiple_axes_output_has_mean_zero_and_var_one() {
let data: Vec<f32> = (0..12).map(|i| 0.5 * i as f32 - 2.75).collect();
let input = tensor2(data, 3, 4);
let mut ln = LayerNormalization::new(vec![3, 4], 1e-5)
.unwrap()
.with_normalized_axis(LayerNormalizationAxis::Multiple(vec![0, 1]))
.unwrap();
let output = ln.forward(&input).unwrap();
assert_eq!(output.shape(), &[3, 4]);
let flat: Vec<f32> = output.iter().cloned().collect();
let n = flat.len() as f32;
let mean: f32 = flat.iter().sum::<f32>() / n;
let var: f32 = flat.iter().map(|&v| (v - mean).powi(2)).sum::<f32>() / n;
assert!(mean.abs() < 1e-5, "global mean {mean} too far from 0");
assert!((var - 1.0).abs() < 5e-4, "global var {var} too far from 1");
}
#[test]
fn ln_multiple_single_axis_on_3d_input() {
let data: Vec<f32> = (0..24).map(|i| i as f32 * 0.5 - 5.0).collect();
let shape = vec![2, 3, 4];
let input = ArrayD::from_shape_vec(shape.clone(), data).unwrap();
let mut ln = LayerNormalization::new(vec![2, 3, 4], 1e-5)
.unwrap()
.with_normalized_axis(LayerNormalizationAxis::Multiple(vec![1]))
.unwrap();
let output = ln.forward(&input).unwrap();
assert_eq!(output.shape(), &[2, 3, 4]);
for b in 0..2 {
for s in 0..4 {
let vals: Vec<f32> = (0..3).map(|c| output[[b, c, s]]).collect();
let mean: f32 = vals.iter().sum::<f32>() / 3.0;
let var: f32 = vals.iter().map(|&v| (v - mean).powi(2)).sum::<f32>() / 3.0;
assert!(mean.abs() < 1e-5, "b={b} s={s}: mean {mean} too far from 0");
assert!(
(var - 1.0).abs() < 5e-4,
"b={b} s={s}: var {var} too far from 1"
);
}
}
}
#[test]
fn ln_default_constant_row_is_finite_and_zero() {
let data = vec![
5.0f32, 5.0, 5.0, 5.0, 1.0, 2.0, 3.0, 4.0, ];
let input = tensor2(data, 2, 4);
let mut ln = LayerNormalization::new(vec![2, 4], 1e-5).unwrap();
let output = ln.forward(&input).unwrap();
for c in 0..4 {
assert!(
output[[0, c]].is_finite(),
"output[0,{c}] = {} is not finite",
output[[0, c]]
);
assert_eq!(
output[[0, c]],
0.0,
"output[0,{c}] should be 0 for constant row"
);
}
for c in 0..4 {
assert!(
output[[1, c]].is_finite(),
"output[1,{c}] = {} is not finite",
output[[1, c]]
);
}
}
#[test]
fn ln_predict_equals_forward() {
let data = vec![1.0f32, 3.0, 5.0, 7.0, 2.0, -2.0, 0.0, 4.0];
let input = tensor2(data, 2, 4);
let mut ln = LayerNormalization::new(vec![2, 4], 1e-5).unwrap();
let out_forward = ln.forward(&input).unwrap();
let out_predict = ln.predict(&input).unwrap();
assert_allclose(&out_forward, &out_predict, 1e-6);
}
#[test]
fn ln_set_weights_custom_gamma_beta() {
let input = tensor2(vec![0.0f32, 4.0], 1, 2);
let mut ln = LayerNormalization::new(vec![1, 2], 1e-5).unwrap();
let gamma = tensor1(vec![3.0f32, 3.0]);
let beta = tensor1(vec![10.0f32, 10.0]);
ln.set_weights(gamma, beta).unwrap();
let output = ln.forward(&input).unwrap();
let eps = 1e-5f32;
let std = (4.0f32 + eps).sqrt();
let x_norm_0 = -2.0_f32 / std;
let x_norm_1 = 2.0_f32 / std;
let e0 = x_norm_0 * 3.0 + 10.0;
let e1 = x_norm_1 * 3.0 + 10.0;
let expected = tensor2(vec![e0, e1], 1, 2);
assert_allclose(&output, &expected, 1e-4);
}
#[test]
fn ln_set_weights_rejects_wrong_gamma_shape() {
let mut ln = LayerNormalization::new(vec![2, 4], 1e-5).unwrap();
let bad_gamma = tensor1(vec![1.0f32, 1.0, 1.0]); let beta = tensor1(vec![0.0f32; 4]);
let result = ln.set_weights(bad_gamma, beta);
assert!(
matches!(
result,
Err(Error::NeuralNetwork(NnError::WeightShape { .. }))
),
"expected NnError::WeightShape, got {:?}",
result
);
}
#[test]
fn ln_forward_rejects_wrong_input_shape() {
let mut ln = LayerNormalization::new(vec![2, 4], 1e-5).unwrap();
let wrong = tensor2(vec![1.0f32; 12], 3, 4);
let result = ln.forward(&wrong);
assert!(
matches!(result, Err(Error::ShapeMismatch { .. })),
"expected ShapeMismatch, got {:?}",
result
);
}
#[test]
fn ln_mode_switch_does_not_change_forward_output() {
let data = vec![1.0f32, 3.0, 5.0, 7.0, 2.0, -2.0, 0.0, 4.0];
let input = tensor2(data, 2, 4);
let mut ln = LayerNormalization::new(vec![2, 4], 1e-5).unwrap();
let out_train = ln.forward(&input).unwrap();
ln.set_training_if_mode_dependent(false);
let out_eval = ln.forward(&input).unwrap();
assert_allclose(&out_train, &out_eval, 1e-6);
}
#[test]
fn ln_predict_equals_forward_in_eval_mode() {
let data = vec![1.0f32, 3.0, 5.0, 7.0, 2.0, -2.0, 0.0, 4.0];
let input = tensor2(data, 2, 4);
let mut ln = LayerNormalization::new(vec![2, 4], 1e-5).unwrap();
ln.set_training_if_mode_dependent(false);
let out_forward = ln.forward(&input).unwrap();
let out_predict = ln.predict(&input).unwrap();
assert_allclose(&out_forward, &out_predict, 0.0f32);
}
#[test]
fn ln_multiple_valid_axes_forward_succeeds() {
let data: Vec<f32> = (0..12).map(|i| i as f32).collect();
let input = tensor2(data, 3, 4);
let mut ln = LayerNormalization::new(vec![3, 4], 1e-5)
.unwrap()
.with_normalized_axis(LayerNormalizationAxis::Multiple(vec![0, 1]))
.unwrap();
let result = ln.forward(&input);
assert!(
result.is_ok(),
"forward with valid Multiple axes failed: {:?}",
result
);
}
#[test]
fn bn_backward_before_forward_errors() {
let mut bn = BatchNormalization::new(vec![2, 3], 0.9, 1e-5).unwrap();
let grad = tensor2(vec![1.0f32; 6], 2, 3);
let err = bn.backward(&grad).unwrap_err();
assert!(
matches!(
err,
Error::NeuralNetwork(NnError::ForwardPassNotRun("BatchNormalization"))
),
"expected ForwardPassNotRun(\"BatchNormalization\"), got {:?}",
err
);
}
#[test]
fn ln_backward_before_forward_errors() {
let mut ln = LayerNormalization::new(vec![2, 4], 1e-5).unwrap();
let grad = tensor2(vec![1.0f32; 8], 2, 4);
let err = ln.backward(&grad).unwrap_err();
assert!(
matches!(
err,
Error::NeuralNetwork(NnError::ForwardPassNotRun("LayerNormalization"))
),
"expected ForwardPassNotRun(\"LayerNormalization\"), got {:?}",
err
);
}
#[test]
fn ln_custom_axis_out_of_bounds_forward_errors() {
let mut ln = LayerNormalization::new(vec![4], 1e-5)
.unwrap()
.with_normalized_axis(LayerNormalizationAxis::Custom(5))
.unwrap();
let input = tensor1(vec![1.0f32, 2.0, 3.0, 4.0]); let result = ln.forward(&input);
assert!(
matches!(result, Err(Error::InvalidParameter { .. })),
"expected InvalidParameter, got {:?}",
result
);
}
#[test]
fn ln_custom_axis_out_of_bounds_predict_errors() {
let ln = LayerNormalization::new(vec![4], 1e-5)
.unwrap()
.with_normalized_axis(LayerNormalizationAxis::Custom(5))
.unwrap();
let input = tensor1(vec![1.0f32, 2.0, 3.0, 4.0]); let result = ln.predict(&input);
assert!(
matches!(result, Err(Error::InvalidParameter { .. })),
"expected InvalidParameter, got {:?}",
result
);
}
#[test]
fn ln_default_scalar_input_forward_errors() {
let mut ln = LayerNormalization::new(vec![], 1e-5).unwrap();
let scalar = ArrayD::from_shape_vec(vec![], vec![3.0f32]).unwrap(); let result = ln.forward(&scalar);
assert!(
matches!(result, Err(Error::InvalidInput(_))),
"expected InvalidInput, got {:?}",
result
);
}
#[test]
fn ln_default_scalar_input_predict_errors() {
let ln = LayerNormalization::new(vec![], 1e-5).unwrap();
let scalar = ArrayD::from_shape_vec(vec![], vec![3.0f32]).unwrap(); let result = ln.predict(&scalar);
assert!(
matches!(result, Err(Error::InvalidInput(_))),
"expected InvalidInput, got {:?}",
result
);
}
#[test]
fn bn_backward_eval_mode_passes_gradient_through() {
let mut bn = BatchNormalization::new(vec![2, 3], 0.9, 1e-5).unwrap();
bn.set_training_if_mode_dependent(false);
let input = tensor2(vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0], 2, 3);
bn.forward(&input).unwrap();
let grad = tensor2(vec![0.5f32, -1.5, 2.0, -3.0, 4.5, -6.0], 2, 3);
let grad_input = bn.backward(&grad).unwrap();
assert_allclose(&grad_input, &grad, 0.0f32);
}
#[test]
fn ln_backward_eval_mode_passes_gradient_through() {
let mut ln = LayerNormalization::new(vec![2, 4], 1e-5).unwrap();
ln.set_training_if_mode_dependent(false);
let input = tensor2(vec![1.0f32, 3.0, 5.0, 7.0, 2.0, -2.0, 0.0, 4.0], 2, 4);
ln.forward(&input).unwrap();
let grad = tensor2(vec![0.5f32, -1.5, 2.0, -3.0, 4.5, -6.0, 7.0, -8.5], 2, 4);
let grad_input = ln.backward(&grad).unwrap();
assert_allclose(&grad_input, &grad, 0.0f32);
}
#[test]
fn bn_new_scalar_param_branch_forward_1d() {
let mut bn = BatchNormalization::new(vec![4], 0.9, 1e-5).unwrap();
let input = tensor1(vec![1.0f32, 2.0, 3.0, 4.0]);
let output = bn.forward(&input).unwrap();
assert_eq!(output.shape(), &[4]);
let std = (1.25f32 + 1e-5f32).sqrt();
let expected = tensor1(vec![-1.5 / std, -0.5 / std, 0.5 / std, 1.5 / std]);
assert_allclose(&output, &expected, 1e-5);
}
#[test]
fn bn_spatial_4d_normalizes_per_channel() {
use rustyml::neural_network::Tensor;
use rustyml::neural_network::layers::TrainingParameters;
let mut bn = BatchNormalization::new(vec![1, 2, 2, 2], 0.9, 1e-5).unwrap();
assert!(matches!(bn.param_count(), TrainingParameters::Trainable(4)));
let x: Tensor =
ArrayD::from_shape_vec(vec![1, 2, 2, 2], vec![1., 2., 3., 4., 5., 6., 7., 8.]).unwrap();
let out = bn.forward(&x).unwrap();
assert_eq!(out.shape(), &[1, 2, 2, 2]);
let inv = 1.0 / (1.25f32 + 1e-5).sqrt();
let expected: Tensor = ArrayD::from_shape_vec(
vec![1, 2, 2, 2],
vec![
-1.5 * inv,
-0.5 * inv,
0.5 * inv,
1.5 * inv,
-1.5 * inv,
-0.5 * inv,
0.5 * inv,
1.5 * inv,
],
)
.unwrap();
assert_allclose(&out, &expected, 1e-4);
}
#[test]
fn bn_spatial_4d_backward_shape() {
use rustyml::neural_network::Tensor;
let mut bn = BatchNormalization::new(vec![2, 3, 2, 2], 0.9, 1e-5).unwrap();
let x: Tensor =
ArrayD::from_shape_fn(vec![2, 3, 2, 2], |idx| (idx[1] + idx[2] + idx[3]) as f32);
bn.forward(&x).unwrap();
let grad: Tensor = ArrayD::ones(vec![2, 3, 2, 2]);
let grad_in = bn.backward(&grad).unwrap();
assert_eq!(grad_in.shape(), &[2, 3, 2, 2]);
assert!(grad_in.iter().all(|v| v.is_finite()));
}