use ndarray::Array;
use rustyml::neural_network::Tensor;
use rustyml::neural_network::layers::regularization::normalization::group_normalization::GroupNormalization;
use rustyml::neural_network::layers::regularization::normalization::instance_normalization::InstanceNormalization;
use rustyml::neural_network::traits::Layer;
use rustyml::{error::Error, neural_network::NnError};
use crate::common::assert_allclose;
fn param1d(vals: &[f32]) -> Tensor {
Array::from_shape_vec(vec![vals.len()], vals.to_vec())
.unwrap()
.into_dyn()
}
#[test]
fn group_norm_single_group_forward_values() {
let mut gn = GroupNormalization::new(vec![1, 3, 2], 1, 1e-5).unwrap();
let input = Array::from_shape_vec((1, 3, 2), vec![1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0])
.unwrap()
.into_dyn();
let output = gn.forward(&input).unwrap();
let std_val = (17.5_f32 / 6.0 + 1e-5).sqrt();
let expected_flat = vec![
-2.5 / std_val,
-1.5 / std_val,
-0.5 / std_val,
0.5 / std_val,
1.5 / std_val,
2.5 / std_val,
];
let expected = Array::from_shape_vec((1, 3, 2), expected_flat)
.unwrap()
.into_dyn();
assert_allclose(&output, &expected, 1e-5_f32);
}
#[test]
fn group_norm_two_groups_forward_values() {
let mut gn = GroupNormalization::new(vec![1, 2, 4], 2, 1e-5).unwrap();
let input = Array::from_shape_vec((1, 2, 4), vec![1.0_f32, 2.0, 5.0, 6.0, 3.0, 4.0, 7.0, 8.0])
.unwrap()
.into_dyn();
let output = gn.forward(&input).unwrap();
let std_val = (1.25_f32 + 1e-5).sqrt();
let expected_flat = vec![
-1.5 / std_val, -0.5 / std_val, -1.5 / std_val, -0.5 / std_val, 0.5 / std_val, 1.5 / std_val, 0.5 / std_val, 1.5 / std_val, ];
let expected = Array::from_shape_vec((1, 2, 4), expected_flat)
.unwrap()
.into_dyn();
assert_allclose(&output, &expected, 1e-5_f32);
}
#[test]
fn group_norm_two_batches_forward_values() {
let mut gn = GroupNormalization::new(vec![2, 3, 4], 2, 1e-5).unwrap();
let input = Array::from_shape_vec(
(2, 3, 4),
vec![
1.0, 4.0, 7.0, 10.0, 2.0, 5.0, 8.0, 11.0, 3.0, 6.0, 9.0, 12.0, 2.0, 5.0, 8.0, 11.0, 3.0, 6.0, 9.0, 12.0, 4.0, 7.0, 10.0, 13.0,
],
)
.unwrap()
.into_dyn();
let output = gn.forward(&input).unwrap();
let std_val = (17.5_f32 / 6.0 + 1e-5).sqrt();
let n = [-2.5_f32, -1.5, -0.5, 0.5, 1.5, 2.5].map(|c| c / std_val);
let expected_flat = vec![
n[0], n[3], n[0], n[3], n[1], n[4], n[1], n[4], n[2], n[5], n[2], n[5], n[0], n[3], n[0], n[3], n[1], n[4], n[1], n[4], n[2], n[5], n[2], n[5],
];
let expected = Array::from_shape_vec((2, 3, 4), expected_flat)
.unwrap()
.into_dyn();
assert_allclose(&output, &expected, 1e-5_f32);
}
#[test]
fn group_norm_custom_gamma_beta_forward_values() {
let mut gn = GroupNormalization::new(vec![1, 2, 4], 2, 1e-5).unwrap();
gn.set_weights(
param1d(&[2.0, 3.0, 4.0, 5.0]),
param1d(&[1.0, 2.0, 3.0, 4.0]),
)
.unwrap();
let input = Array::from_shape_vec((1, 2, 4), vec![1.0_f32, 2.0, 5.0, 6.0, 3.0, 4.0, 7.0, 8.0])
.unwrap()
.into_dyn();
let output = gn.forward(&input).unwrap();
let std_val = (1.25_f32 + 1e-5).sqrt();
let c = [-1.5_f32, -0.5, 0.5, 1.5].map(|v| v / std_val);
let expected_flat = vec![
c[0] * 2.0 + 1.0, c[1] * 3.0 + 2.0, c[0] * 4.0 + 3.0, c[1] * 5.0 + 4.0, c[2] * 2.0 + 1.0, c[3] * 3.0 + 2.0, c[2] * 4.0 + 3.0, c[3] * 5.0 + 4.0, ];
let expected = Array::from_shape_vec((1, 2, 4), expected_flat)
.unwrap()
.into_dyn();
assert_allclose(&output, &expected, 1e-5_f32);
}
#[test]
fn group_norm_constant_input_yields_zero_output() {
let mut gn = GroupNormalization::new(vec![1, 3, 4], 1, 1e-5).unwrap();
let input = Array::from_elem((1, 3, 4), 5.0_f32).into_dyn();
let output = gn.forward(&input).unwrap();
let expected = Array::zeros((1, 3, 4)).into_dyn();
assert_allclose(&output, &expected, 1e-6_f32);
}
#[test]
fn group_norm_channel_axis_is_last() {
let mut gn = GroupNormalization::new(vec![1, 2, 4], 2, 1e-5).unwrap();
let input = Array::from_shape_vec(
(1, 2, 4),
vec![2.0_f32, 4.0, 100.0, 200.0, 6.0, 8.0, 300.0, 400.0],
)
.unwrap()
.into_dyn();
let output = gn.forward(&input).unwrap();
let inv0 = 1.0 / (5.0_f32 + 1e-5).sqrt();
let inv1 = 1.0 / (12500.0_f32 + 1e-5).sqrt();
let expected_flat = vec![
-3.0 * inv0, -inv0, -150.0 * inv1, -50.0 * inv1, 1.0 * inv0, 3.0 * inv0, 50.0 * inv1, 150.0 * inv1, ];
let expected = Array::from_shape_vec((1, 2, 4), expected_flat)
.unwrap()
.into_dyn();
assert_allclose(&output, &expected, 1e-5_f32);
}
#[test]
fn group_norm_predict_equals_forward() {
let mut gn = GroupNormalization::new(vec![1, 4, 4], 2, 1e-5).unwrap();
gn.set_training_if_mode_dependent(false);
let input = Array::from_shape_vec(
(1, 4, 4),
(0..16).map(|v| 0.5 * v as f32 - 3.75).collect::<Vec<_>>(),
)
.unwrap()
.into_dyn();
let out_fwd = gn.forward(&input).unwrap();
let out_pred = gn.predict(&input).unwrap();
assert_allclose(&out_pred, &out_fwd, 1e-6_f32);
}
#[test]
fn group_norm_constructor_invalid_parameter_errors() {
let cases = [
(0, 1e-5_f32, "num_groups=0"),
(2, 0.0_f32, "epsilon=0.0"),
(2, -1e-5_f32, "epsilon=-1e-5"),
];
for (num_groups, epsilon, desc) in cases {
let err = GroupNormalization::new(vec![1, 4, 4], num_groups, epsilon).unwrap_err();
assert!(
matches!(err, Error::InvalidParameter { .. }),
"expected InvalidParameter for {}, got {:?}",
desc,
err
);
}
}
#[test]
fn group_norm_error_empty_input_shape() {
let err = GroupNormalization::new(vec![], 2, 1e-5).unwrap_err();
assert!(
matches!(err, Error::EmptyInput(_)),
"expected EmptyInput, got {:?}",
err
);
}
#[test]
fn group_norm_error_channels_not_divisible_by_groups_at_forward() {
let mut gn = GroupNormalization::new(vec![1, 4, 3], 2, 1e-5).unwrap();
let input = Array::ones((1, 4, 3)).into_dyn();
let err = gn.forward(&input).unwrap_err();
assert!(
matches!(err, Error::InvalidParameter { .. }),
"expected InvalidParameter for non-divisible channels/groups, got {:?}",
err
);
}
#[test]
fn group_norm_error_backward_before_forward() {
let mut gn = GroupNormalization::new(vec![1, 4, 4], 2, 1e-5).unwrap();
let grad = Array::ones((1, 4, 4)).into_dyn();
let err = gn.backward(&grad).unwrap_err();
assert!(
matches!(
err,
Error::NeuralNetwork(NnError::ForwardPassNotRun("GroupNormalization"))
),
"expected ForwardPassNotRun, got {:?}",
err
);
}
#[test]
fn group_norm_set_weights_shape_mismatch() {
let mut gn = GroupNormalization::new(vec![1, 4, 4], 2, 1e-5).unwrap();
let bad_gamma = param1d(&[1.0, 1.0, 1.0]);
let beta = param1d(&[0.0, 0.0, 0.0, 0.0]);
let err = gn.set_weights(bad_gamma, beta).unwrap_err();
assert!(
matches!(err, Error::NeuralNetwork(NnError::WeightShape { .. })),
"expected WeightShape error, got {:?}",
err
);
}
#[test]
fn instance_norm_forward_values() {
let mut inn = InstanceNormalization::new(vec![1, 4, 2], 1e-5).unwrap();
let input = Array::from_shape_vec((1, 4, 2), vec![1.0_f32, 5.0, 2.0, 6.0, 3.0, 7.0, 4.0, 8.0])
.unwrap()
.into_dyn();
let output = inn.forward(&input).unwrap();
let std_val = (1.25_f32 + 1e-5).sqrt();
let c = [-1.5_f32, -0.5, 0.5, 1.5].map(|v| v / std_val);
let expected_flat = vec![c[0], c[0], c[1], c[1], c[2], c[2], c[3], c[3]];
let expected = Array::from_shape_vec((1, 4, 2), expected_flat)
.unwrap()
.into_dyn();
assert_allclose(&output, &expected, 1e-5_f32);
}
#[test]
fn instance_norm_custom_gamma_beta_forward_values() {
let mut inn = InstanceNormalization::new(vec![1, 4, 2], 1e-5).unwrap();
inn.set_weights(param1d(&[2.0, 3.0]), param1d(&[0.5, -0.5]))
.unwrap();
let input = Array::from_shape_vec((1, 4, 2), vec![1.0_f32, 5.0, 2.0, 6.0, 3.0, 7.0, 4.0, 8.0])
.unwrap()
.into_dyn();
let output = inn.forward(&input).unwrap();
let std_val = (1.25_f32 + 1e-5).sqrt();
let c = [-1.5_f32, -0.5, 0.5, 1.5].map(|v| v / std_val);
let expected_flat: Vec<f32> = c
.iter()
.flat_map(|&v| [v * 2.0 + 0.5, v * 3.0 - 0.5])
.collect();
let expected = Array::from_shape_vec((1, 4, 2), expected_flat)
.unwrap()
.into_dyn();
assert_allclose(&output, &expected, 1e-5_f32);
}
#[test]
fn instance_norm_multiple_batches_forward_values() {
let input = Array::from_shape_vec(
(2, 3, 3),
vec![
0.0_f32, 3.0, -1.0, 1.0, 4.0, 0.0, 2.0, 5.0, 1.0, 10.0, -5.0, 100.0, 11.0, -4.0, 101.0, 12.0, -3.0, 102.0, ],
)
.unwrap()
.into_dyn();
let mut inn = InstanceNormalization::new(vec![2, 3, 3], 1e-5).unwrap();
let output = inn.forward(&input).unwrap();
let var = 2.0_f32 / 3.0;
let std_val = (var + 1e-5).sqrt();
let a = 1.0 / std_val;
let expected_flat: Vec<f32> = vec![
-a, -a, -a, 0.0, 0.0, 0.0, a, a, a, -a, -a, -a, 0.0, 0.0, 0.0, a, a, a, ];
let expected = Array::from_shape_vec((2, 3, 3), expected_flat)
.unwrap()
.into_dyn();
assert_allclose(&output, &expected, 1e-5_f32);
}
#[test]
fn instance_norm_constant_input_yields_zero_output() {
let mut inn = InstanceNormalization::new(vec![2, 3, 4], 1e-5).unwrap();
let input = Array::from_elem((2, 3, 4), 7.0_f32).into_dyn();
let output = inn.forward(&input).unwrap();
let expected = Array::zeros((2, 3, 4)).into_dyn();
assert_allclose(&output, &expected, 1e-6_f32);
}
#[test]
fn instance_norm_channel_axis_is_last() {
let mut inn = InstanceNormalization::new(vec![1, 3, 2], 1e-5).unwrap();
let input = Array::from_shape_vec((1, 3, 2), vec![1.0_f32, 10.0, 2.0, 20.0, 3.0, 30.0])
.unwrap()
.into_dyn();
let output = inn.forward(&input).unwrap();
let inv0 = 1.0 / (2.0_f32 / 3.0 + 1e-5).sqrt();
let inv1 = 1.0 / (200.0_f32 / 3.0 + 1e-5).sqrt();
let expected_flat = vec![
-inv0, -10.0 * inv1, 0.0, 0.0, 1.0 * inv0, 10.0 * inv1, ];
let expected = Array::from_shape_vec((1, 3, 2), expected_flat)
.unwrap()
.into_dyn();
assert_allclose(&output, &expected, 1e-5_f32);
}
#[test]
fn group_norm_full_groups_equals_instance_norm() {
let data: Vec<f32> = vec![
1.0, 5.0, 10.0, 2.0, 6.0, 11.0, 3.0, 7.0, 12.0, 4.0, 8.0, 13.0, ];
let input = Array::from_shape_vec((1, 4, 3), data).unwrap().into_dyn();
let mut gn = GroupNormalization::new(vec![1, 4, 3], 3, 1e-5).unwrap();
let mut inn = InstanceNormalization::new(vec![1, 4, 3], 1e-5).unwrap();
let out_gn = gn.forward(&input).unwrap();
let out_in = inn.forward(&input).unwrap();
assert_allclose(&out_gn, &out_in, 1e-6_f32);
let std_val = (1.25_f32 + 1e-5).sqrt();
let c = [-1.5_f32, -0.5, 0.5, 1.5].map(|v| v / std_val);
let expected_flat = vec![
c[0], c[0], c[0], c[1], c[1], c[1], c[2], c[2], c[2], c[3], c[3], c[3], ];
let expected = Array::from_shape_vec((1, 4, 3), expected_flat)
.unwrap()
.into_dyn();
assert_allclose(&out_gn, &expected, 1e-5_f32);
assert_allclose(&out_in, &expected, 1e-5_f32);
}
#[test]
fn group_norm_full_groups_equals_instance_norm_with_affine() {
let data: Vec<f32> = vec![1.0, 5.0, 2.0, 6.0, 3.0, 7.0, 4.0, 8.0];
let input = Array::from_shape_vec((1, 4, 2), data).unwrap().into_dyn();
let gamma = param1d(&[2.0, 0.5]);
let beta = param1d(&[1.0, -1.0]);
let mut gn = GroupNormalization::new(vec![1, 4, 2], 2, 1e-5).unwrap();
gn.set_weights(gamma.clone(), beta.clone()).unwrap();
let mut inn = InstanceNormalization::new(vec![1, 4, 2], 1e-5).unwrap();
inn.set_weights(gamma, beta).unwrap();
let out_gn = gn.forward(&input).unwrap();
let out_in = inn.forward(&input).unwrap();
assert_allclose(&out_gn, &out_in, 1e-6_f32);
}
#[test]
fn instance_norm_predict_equals_forward() {
let mut inn = InstanceNormalization::new(vec![2, 3, 4], 1e-5).unwrap();
inn.set_training_if_mode_dependent(false);
let input = Array::from_shape_vec(
(2, 3, 4),
(0..24).map(|v| 0.5 * v as f32 - 5.75).collect::<Vec<_>>(),
)
.unwrap()
.into_dyn();
let out_fwd = inn.forward(&input).unwrap();
let out_pred = inn.predict(&input).unwrap();
assert_allclose(&out_pred, &out_fwd, 1e-6_f32);
}
#[test]
fn instance_norm_predict_equals_forward_training_mode() {
let mut inn = InstanceNormalization::new(vec![1, 4, 2], 1e-5).unwrap();
inn.set_training_if_mode_dependent(true);
let input = Array::from_shape_vec((1, 4, 2), vec![1.0_f32, 5.0, 2.0, 6.0, 3.0, 7.0, 4.0, 8.0])
.unwrap()
.into_dyn();
let out_fwd = inn.forward(&input).unwrap();
let out_pred = inn.predict(&input).unwrap();
assert_allclose(&out_pred, &out_fwd, 1e-6_f32);
}
#[test]
fn instance_norm_constructor_invalid_parameter_errors() {
let cases = [(0.0_f32, "epsilon=0.0"), (-1e-3_f32, "epsilon=-1e-3")];
for (epsilon, desc) in cases {
let err = InstanceNormalization::new(vec![1, 3, 4], epsilon).unwrap_err();
assert!(
matches!(err, Error::InvalidParameter { .. }),
"expected InvalidParameter for {}, got {:?}",
desc,
err
);
}
}
#[test]
fn instance_norm_error_empty_input_shape() {
let err = InstanceNormalization::new(vec![], 1e-5).unwrap_err();
assert!(
matches!(err, Error::EmptyInput(_)),
"expected EmptyInput, got {:?}",
err
);
}
#[test]
fn instance_norm_error_backward_before_forward() {
let mut inn = InstanceNormalization::new(vec![1, 3, 4], 1e-5).unwrap();
let grad = Array::ones((1, 3, 4)).into_dyn();
let err = inn.backward(&grad).unwrap_err();
assert!(
matches!(
err,
Error::NeuralNetwork(NnError::ForwardPassNotRun("InstanceNormalization"))
),
"expected ForwardPassNotRun, got {:?}",
err
);
}
#[test]
fn instance_norm_set_weights_shape_mismatch() {
let mut inn = InstanceNormalization::new(vec![1, 4, 4], 1e-5).unwrap();
let bad_gamma = param1d(&[1.0, 1.0, 1.0]);
let beta = param1d(&[0.0, 0.0, 0.0, 0.0]);
let err = inn.set_weights(bad_gamma, beta).unwrap_err();
assert!(
matches!(err, Error::NeuralNetwork(NnError::WeightShape { .. })),
"expected WeightShape error, got {:?}",
err
);
}
#[test]
fn group_norm_output_shape_matches_input() {
let mut gn = GroupNormalization::new(vec![2, 5, 6], 3, 1e-5).unwrap();
let input = Array::ones((2, 5, 6)).into_dyn();
let output = gn.forward(&input).unwrap();
assert_eq!(output.shape(), &[2, 5, 6]);
}
#[test]
fn instance_norm_output_shape_matches_input() {
let mut inn = InstanceNormalization::new(vec![2, 4, 6], 1e-5).unwrap();
let input = Array::ones((2, 4, 6)).into_dyn();
let output = inn.forward(&input).unwrap();
assert_eq!(output.shape(), &[2, 4, 6]);
}
#[test]
fn group_norm_backward_eval_mode_passes_gradient_through() {
let mut gn = GroupNormalization::new(vec![1, 4, 4], 2, 1e-5).unwrap();
gn.set_training_if_mode_dependent(false);
let input = Array::from_shape_vec(
(1, 4, 4),
(0..16).map(|v| 0.5 * v as f32 - 3.75).collect::<Vec<_>>(),
)
.unwrap()
.into_dyn();
gn.forward(&input).unwrap();
let grad = Array::from_shape_vec(
(1, 4, 4),
(0..16).map(|v| v as f32 - 7.5).collect::<Vec<_>>(),
)
.unwrap()
.into_dyn();
let grad_input = gn.backward(&grad).unwrap();
assert_allclose(&grad_input, &grad, 0.0_f32);
}
#[test]
fn instance_norm_backward_eval_mode_passes_gradient_through() {
let mut inn = InstanceNormalization::new(vec![1, 3, 4], 1e-5).unwrap();
inn.set_training_if_mode_dependent(false);
let input = Array::from_shape_vec(
(1, 3, 4),
(0..12).map(|v| 0.5 * v as f32 - 2.75).collect::<Vec<_>>(),
)
.unwrap()
.into_dyn();
inn.forward(&input).unwrap();
let grad = Array::from_shape_vec(
(1, 3, 4),
(0..12).map(|v| v as f32 - 5.5).collect::<Vec<_>>(),
)
.unwrap()
.into_dyn();
let grad_input = inn.backward(&grad).unwrap();
assert_allclose(&grad_input, &grad, 0.0_f32);
}
#[test]
fn group_norm_forward_below_3d_input_errors() {
let mut gn = GroupNormalization::new(vec![4, 8], 2, 1e-5).unwrap();
let input = Array::ones((4, 8)).into_dyn();
let err = gn.forward(&input).unwrap_err();
assert!(
matches!(err, Error::InvalidInput(_)),
"expected InvalidInput for <3D input, got {:?}",
err
);
}
#[test]
fn instance_norm_forward_below_3d_input_errors() {
let mut inn = InstanceNormalization::new(vec![4, 8], 1e-5).unwrap();
let input = Array::ones((4, 8)).into_dyn();
let err = inn.forward(&input).unwrap_err();
assert!(
matches!(err, Error::InvalidInput(_)),
"expected InvalidInput for <3D input, got {:?}",
err
);
}