use ndarray::{Array, ArrayD, IxDyn};
use rustyml::neural_network::layers::*;
use rustyml::neural_network::sequential::SequentialBuilder;
use rustyml::neural_network::traits::{Layer, UnaryLayer};
use rustyml::neural_network::{Ctx, Shape};
use std::collections::BTreeSet;
#[test]
fn the_pooling_family_answers_before_any_forward_pass() {
let pool_1d = MaxPooling1D::new(2).with_stride(2).unwrap();
let computed = pool_1d
.compute_output_shape(&Shape::with_free_batch(&[1, 8, 2]))
.unwrap();
assert_eq!(computed.to_string(), "(None, 4, 2)");
let pool_2d = MaxPooling2D::new((2, 2));
let computed = pool_2d
.compute_output_shape(&Shape::with_free_batch(&[1, 6, 6, 3]))
.unwrap();
assert_eq!(computed.to_string(), "(None, 3, 3, 3)");
let average_1d = AveragePooling1D::new(3);
let computed = average_1d
.compute_output_shape(&Shape::known(&[4, 6, 1]))
.unwrap();
assert_eq!(computed.to_string(), "(4, 2, 1)");
let global = GlobalMaxPooling1D::new();
assert_eq!(global.output_shape(), "Unknown");
let computed = global
.compute_output_shape(&Shape::with_free_batch(&[5, 9, 7]))
.unwrap();
assert_eq!(computed.to_string(), "(None, 7)");
}
#[test]
fn a_layer_answers_for_a_shape_it_was_not_built_for() {
let mut layer = MaxPooling1D::new(2).with_stride(2).unwrap();
layer.build(&Shape::known(&[1, 8, 2])).unwrap();
assert_eq!(layer.output_shape(), "(None, 4, 2)");
let computed = layer
.compute_output_shape(&Shape::with_free_batch(&[64, 20, 2]))
.unwrap();
assert_eq!(computed.to_string(), "(None, 10, 2)");
}
#[test]
fn a_forward_pass_changes_no_answer() {
let mut layer = Conv2D::new(4, (3, 3), (1, 1), ReLU::new()).unwrap();
let asked = Shape::with_free_batch(&[2, 10, 10, 3]);
let before = layer.compute_output_shape(&asked).unwrap();
layer
.forward_mut(&ArrayD::zeros(IxDyn(&[2, 8, 8, 3])), &mut Ctx::inference())
.unwrap();
let after = layer.compute_output_shape(&asked).unwrap();
assert_eq!(before, after);
assert_eq!(before.to_string(), "(None, 8, 8, 4)");
}
#[test]
fn the_batch_axis_passes_through_as_it_arrives() {
let layer = Conv2D::new(2, (2, 2), (1, 1), Linear::new()).unwrap();
let free = layer
.compute_output_shape(&Shape::with_free_batch(&[1, 4, 4, 1]))
.unwrap();
assert_eq!(free.axes()[0], None);
assert_eq!(free.to_string(), "(None, 3, 3, 2)");
let fixed = layer
.compute_output_shape(&Shape::known(&[6, 4, 4, 1]))
.unwrap();
assert_eq!(fixed.axes()[0], Some(6));
assert_eq!(fixed.to_string(), "(6, 3, 3, 2)");
}
#[test]
fn a_recurrent_layer_keeps_a_free_time_axis() {
let sequence = LSTM::new(2, Tanh::new())
.unwrap()
.with_return_sequences(true);
let computed = sequence
.compute_output_shape(&Shape::new(vec![None, None, Some(3)]))
.unwrap();
assert_eq!(computed.to_string(), "(None, None, 2)");
let last = LSTM::new(2, Tanh::new()).unwrap();
let computed = last
.compute_output_shape(&Shape::new(vec![None, Some(7), Some(3)]))
.unwrap();
assert_eq!(computed.to_string(), "(None, 2)");
}
#[test]
fn a_data_driven_layer_answers_with_no_data() {
let layer = UpSampling2D::new((2, 3), Interpolation::Nearest).unwrap();
assert_eq!(layer.output_shape(), "Unknown");
let computed = layer
.compute_output_shape(&Shape::with_free_batch(&[1, 5, 6, 2]))
.unwrap();
assert_eq!(computed.to_string(), "(None, 10, 18, 2)");
}
#[test]
fn a_convolution_refuses_the_wrong_rank() {
let layer = Conv2D::new(4, (3, 3), (1, 1), ReLU::new()).unwrap();
let message = layer
.compute_output_shape(&Shape::with_free_batch(&[2, 8, 3]))
.unwrap_err()
.to_string();
assert!(message.contains("Conv2D"), "{message}");
assert!(message.contains("rank 4"), "{message}");
assert!(message.contains("(None, 8, 3)"), "{message}");
}
#[test]
fn a_dense_layer_refuses_the_wrong_feature_count() {
let mut layer = Dense::new(4, Linear::new()).unwrap();
layer.build(&Shape::with_free_batch(&[2, 7])).unwrap();
let message = layer
.compute_output_shape(&Shape::with_free_batch(&[2, 5]))
.unwrap_err()
.to_string();
assert!(message.contains("Dense"), "{message}");
assert!(message.contains("7 elements on the last axis"), "{message}");
assert!(message.contains("got 5"), "{message}");
}
#[test]
fn a_pooling_layer_refuses_a_window_that_does_not_fit() {
let layer = MaxPooling1D::new(4);
let message = layer
.compute_output_shape(&Shape::with_free_batch(&[1, 3, 2]))
.unwrap_err()
.to_string();
assert!(message.contains("pool_size"), "{message}");
assert!(
message.contains("cannot exceed the corresponding input dimension"),
"{message}"
);
}
#[test]
fn a_global_pooling_layer_refuses_a_free_spatial_axis() {
let layer = GlobalMaxPooling1D::new();
let message = layer
.compute_output_shape(&Shape::new(vec![None, Some(4), None]))
.unwrap_err()
.to_string();
assert!(message.contains("GlobalMaxPooling1D"), "{message}");
assert!(message.contains("axis 2"), "{message}");
}
#[test]
fn p_relu_refuses_an_axis_it_holds_slopes_for() {
let mut layer = PReLU::new(0.25).unwrap();
layer.build(&Shape::with_free_batch(&[2, 3, 4])).unwrap();
let message = layer
.compute_output_shape(&Shape::with_free_batch(&[2, 5, 4]))
.unwrap_err()
.to_string();
assert!(message.contains("PReLU"), "{message}");
assert!(message.contains("axis 1"), "{message}");
assert!(message.contains("shared_axes"), "{message}");
}
#[test]
fn repeat_vector_refuses_a_rank_it_cannot_repeat() {
let layer = RepeatVector::new(3).unwrap();
let message = layer
.compute_output_shape(&Shape::with_free_batch(&[2, 4, 5]))
.unwrap_err()
.to_string();
assert!(message.contains("RepeatVector"), "{message}");
assert!(message.contains("rank 2"), "{message}");
}
#[test]
fn reshape_refuses_an_element_count_it_cannot_match() {
let layer = Reshape::new(vec![2, 2]).unwrap();
assert!(
layer
.compute_output_shape(&Shape::with_free_batch(&[2, 5]))
.is_err()
);
let computed = layer
.compute_output_shape(&Shape::with_free_batch(&[2, 4]))
.unwrap();
assert_eq!(computed.to_string(), "(None, 2, 2)");
}
#[test]
fn a_cropping_layer_refuses_a_border_that_removes_everything() {
let layer = Cropping2D::new(2);
let message = layer
.compute_output_shape(&Shape::with_free_batch(&[1, 4, 8, 3]))
.unwrap_err()
.to_string();
assert!(message.contains("Cropping2D"), "{message}");
assert!(message.contains("at least 1 must remain"), "{message}");
}
fn valid_convolutions_that_cannot_fit() -> Vec<(Box<dyn Layer>, Shape, &'static str)> {
vec![
(
Box::new(Conv1D::new(1, 3, 1, Linear::new()).unwrap()),
Shape::with_free_batch(&[1, 2, 1]),
"Conv1D",
),
(
Box::new(Conv2D::new(1, (3, 3), (1, 1), Linear::new()).unwrap()),
Shape::with_free_batch(&[1, 2, 2, 1]),
"Conv2D",
),
(
Box::new(Conv3D::new(1, (3, 3, 3), (1, 1, 1), Linear::new()).unwrap()),
Shape::with_free_batch(&[1, 2, 2, 2, 1]),
"Conv3D",
),
(
Box::new(DepthwiseConv1D::new(3, 1, Linear::new()).unwrap()),
Shape::with_free_batch(&[1, 2, 1]),
"DepthwiseConv1D",
),
(
Box::new(DepthwiseConv2D::new((3, 3), (1, 1), Linear::new()).unwrap()),
Shape::with_free_batch(&[1, 2, 2, 1]),
"DepthwiseConv2D",
),
(
Box::new(SeparableConv1D::new(1, 3, 1, 1, Linear::new()).unwrap()),
Shape::with_free_batch(&[1, 2, 1]),
"SeparableConv1D",
),
(
Box::new(SeparableConv2D::new(1, (3, 3), (1, 1), 1, Linear::new()).unwrap()),
Shape::with_free_batch(&[1, 2, 2, 1]),
"SeparableConv2D",
),
]
}
#[test]
fn every_valid_convolution_refuses_a_kernel_larger_than_the_input() {
for (layer, input, name) in valid_convolutions_that_cannot_fit() {
let message = match layer.compute_output_shape_many(std::slice::from_ref(&input)) {
Ok(answer) => panic!("{name} answered {answer}, and it must refuse the shape {input}"),
Err(error) => error.to_string(),
};
assert!(message.contains(name), "{name}: {message}");
assert!(
message.contains("effective kernel extent, which is 3"),
"{name}: {message}"
);
assert!(message.contains("is 2."), "{name}: {message}");
}
}
#[test]
fn every_valid_convolution_refuses_the_build_as_well() {
for (mut layer, input, name) in valid_convolutions_that_cannot_fit() {
let message = layer
.build_many(std::slice::from_ref(&input))
.unwrap_err()
.to_string();
assert!(message.contains(name), "{name}: {message}");
assert!(
message.contains("effective kernel extent, which is 3"),
"{name}: {message}"
);
assert!(message.contains("is 2."), "{name}: {message}");
assert_eq!(layer.param_count().total(), 0, "{name} drew a weight");
}
}
#[test]
fn same_and_causal_padding_accept_an_oversized_kernel() {
let mut same = Conv2D::new(1, (3, 3), (1, 1), Linear::new())
.unwrap()
.with_padding(PaddingType::Same);
same.build(&Shape::with_free_batch(&[1, 2, 2, 1])).unwrap();
let computed = same
.compute_output_shape(&Shape::with_free_batch(&[1, 2, 2, 1]))
.unwrap();
assert_eq!(computed.to_string(), "(None, 2, 2, 1)");
let mut causal = Conv1D::new(1, 3, 1, Linear::new())
.unwrap()
.with_padding(ConvPadding::Causal);
causal.build(&Shape::with_free_batch(&[1, 2, 1])).unwrap();
let computed = causal
.compute_output_shape(&Shape::with_free_batch(&[1, 2, 1]))
.unwrap();
assert_eq!(computed.to_string(), "(None, 2, 1)");
}
#[test]
fn the_valid_rule_reads_the_dilated_extent() {
let layer = Conv2D::new(1, (2, 2), (1, 1), Linear::new())
.unwrap()
.with_dilation_rate((3, 3))
.unwrap();
let message = layer
.compute_output_shape(&Shape::with_free_batch(&[1, 3, 3, 1]))
.unwrap_err()
.to_string();
assert!(
message.contains("effective kernel extent, which is 4"),
"{message}"
);
let computed = layer
.compute_output_shape(&Shape::with_free_batch(&[1, 4, 4, 1]))
.unwrap();
assert_eq!(computed.to_string(), "(None, 1, 1, 1)");
}
#[test]
fn the_transposed_convolutions_accept_a_single_position() {
let mut conv_1d = Conv1DTranspose::new(1, 3, 1, Linear::new()).unwrap();
conv_1d.build(&Shape::with_free_batch(&[1, 1, 1])).unwrap();
assert_eq!(
conv_1d
.compute_output_shape(&Shape::with_free_batch(&[1, 1, 1]))
.unwrap()
.to_string(),
"(None, 3, 1)"
);
let mut conv_2d = Conv2DTranspose::new(1, (3, 3), (1, 1), Linear::new()).unwrap();
conv_2d
.build(&Shape::with_free_batch(&[1, 1, 1, 1]))
.unwrap();
assert_eq!(
conv_2d
.compute_output_shape(&Shape::with_free_batch(&[1, 1, 1, 1]))
.unwrap()
.to_string(),
"(None, 3, 3, 1)"
);
let mut conv_3d = Conv3DTranspose::new(1, (3, 3, 3), (1, 1, 1), Linear::new()).unwrap();
conv_3d
.build(&Shape::with_free_batch(&[1, 1, 1, 1, 1]))
.unwrap();
assert_eq!(
conv_3d
.compute_output_shape(&Shape::with_free_batch(&[1, 1, 1, 1, 1]))
.unwrap()
.to_string(),
"(None, 3, 3, 3, 1)"
);
}
#[test]
fn a_model_build_refuses_an_oversized_valid_kernel() {
let refused = SequentialBuilder::new()
.add(Conv2D::new(2, (5, 5), (1, 1), ReLU::new()).unwrap())
.add(Conv2D::new(2, (3, 3), (1, 1), ReLU::new()).unwrap())
.add(Conv2D::new(2, (3, 3), (1, 1), ReLU::new()).unwrap())
.add(Flatten::new())
.build(&Shape::known(&[2, 8, 8, 1]));
let message = match refused {
Ok(_) => panic!("a 3 by 3 Valid kernel does not fit a 2 by 2 input"),
Err(error) => error.to_string(),
};
assert!(message.contains("layer 2"), "{message}");
assert!(message.contains("Conv2D"), "{message}");
assert!(message.contains("(2, 2, 2, 2)"), "{message}");
assert!(
message.contains("effective kernel extent, which is 3"),
"{message}"
);
}
#[test]
fn a_whole_stack_answers_before_any_tensor_exists() {
let layers: Vec<Box<dyn Layer>> = vec![
Box::new(Conv2D::new(4, (3, 3), (1, 1), ReLU::new()).unwrap()),
Box::new(MaxPooling2D::new((2, 2))),
Box::new(Flatten::new()),
Box::new(Dense::new(5, Linear::new()).unwrap()),
];
let mut shape = Shape::with_free_batch(&[1, 8, 8, 1]);
for (position, layer) in layers.iter().enumerate() {
shape = layer
.compute_output_shape_many(std::slice::from_ref(&shape))
.unwrap_or_else(|error| panic!("layer {position} ({}): {error}", layer.layer_type()));
}
assert_eq!(shape.to_string(), "(None, 5)");
}
#[test]
fn a_bad_stack_names_the_layer_that_refuses() {
let layers: Vec<Box<dyn Layer>> = vec![
Box::new(Conv2D::new(4, (3, 3), (1, 1), ReLU::new()).unwrap()),
Box::new(MaxPooling2D::new((4, 4))),
];
let mut shape = Shape::with_free_batch(&[1, 4, 4, 1]);
let mut refused = None;
for (position, layer) in layers.iter().enumerate() {
match layer.compute_output_shape_many(std::slice::from_ref(&shape)) {
Ok(next) => shape = next,
Err(error) => {
refused = Some(format!(
"layer {position} ({}): {error}",
layer.layer_type()
));
break;
}
}
}
let report = refused.expect("the stack must be refused");
assert!(report.starts_with("layer 1 (MaxPooling2D)"), "{report}");
}
#[test]
fn output_shape_reads_the_shape_the_layer_holds() {
let mut declared = AveragePooling1D::new(2);
declared.build(&Shape::known(&[1, 6, 1])).unwrap();
assert_eq!(declared.output_shape(), "(None, 3, 1)");
let mut learned = UpSampling2D::new(2, Interpolation::Nearest).unwrap();
assert_eq!(learned.output_shape(), "Unknown");
learned
.forward_mut(
&Array::zeros(IxDyn(&[1, 3, 3, 2])).into_dyn(),
&mut Ctx::inference(),
)
.unwrap();
assert_eq!(learned.output_shape(), "(None, 6, 6, 2)");
let global = GlobalMaxPooling1D::new();
assert_eq!(global.output_shape(), "Unknown");
}
#[test]
fn a_built_layer_refuses_the_extents_its_summary_does_not_name() {
use ndarray::Array4;
let mut layer = rustyml::neural_network::layers::Conv2D::new(
3,
(2, 2),
(1, 1),
rustyml::neural_network::layers::Activation::Linear,
)
.unwrap();
layer
.build(&rustyml::neural_network::Shape::known(&[2, 4, 4, 2]))
.unwrap();
assert_eq!(layer.output_shape(), "(None, 3, 3, 3)");
let wider = Array4::<f32>::ones((2, 6, 6, 2)).into_dyn();
assert!(layer.forward(&wider, &mut Ctx::inference()).is_err());
let bigger_batch = Array4::<f32>::ones((5, 4, 4, 2)).into_dyn();
assert!(layer.forward(&bigger_batch, &mut Ctx::inference()).is_ok());
}
fn unbuilt_layers() -> Vec<(Shape, Box<dyn Layer>, &'static str)> {
let flat = Shape::with_free_batch(&[1, 4]);
let sequence = Shape::with_free_batch(&[1, 5, 2]);
let signal = Shape::with_free_batch(&[1, 8, 2]);
let image = Shape::with_free_batch(&[1, 8, 8, 2]);
let volume = Shape::with_free_batch(&[1, 4, 4, 4, 2]);
vec![
(
flat.clone(),
Box::new(Dense::new(4, Linear::new()).unwrap()),
"(None, 4)",
),
(
Shape::with_free_batch(&[1, 2, 3]),
Box::new(Flatten::new()),
"(None, 6)",
),
(flat.clone(), Box::new(Identity::new()), "(None, 4)"),
(
flat.clone(),
Box::new(Reshape::new(vec![2, 2]).unwrap()),
"(None, 2, 2)",
),
(
Shape::with_free_batch(&[1, 2, 3]),
Box::new(Permute::new(vec![2, 1]).unwrap()),
"(None, 3, 2)",
),
(
flat.clone(),
Box::new(RepeatVector::new(3).unwrap()),
"(None, 3, 4)",
),
(flat.clone(), Box::new(Rescaling::new(2.0)), "(None, 4)"),
(
Shape::with_free_batch(&[1, 5]),
Box::new(Embedding::new(10, 3).unwrap()),
"(None, 5, 3)",
),
(flat.clone(), Box::new(ReLU::new()), "(None, 4)"),
(
flat.clone(),
Box::new(LeakyReLU::new(0.1).unwrap()),
"(None, 4)",
),
(flat.clone(), Box::new(ELU::new(1.0).unwrap()), "(None, 4)"),
(flat.clone(), Box::new(SELU::new()), "(None, 4)"),
(flat.clone(), Box::new(Softplus::new()), "(None, 4)"),
(flat.clone(), Box::new(Softsign::new()), "(None, 4)"),
(flat.clone(), Box::new(HardSigmoid::new()), "(None, 4)"),
(flat.clone(), Box::new(Exponential::new()), "(None, 4)"),
(flat.clone(), Box::new(Linear::new()), "(None, 4)"),
(flat.clone(), Box::new(Sigmoid::new()), "(None, 4)"),
(flat.clone(), Box::new(Tanh::new()), "(None, 4)"),
(flat.clone(), Box::new(Softmax::new()), "(None, 4)"),
(
flat.clone(),
Box::new(PReLU::new(0.25).unwrap()),
"(None, 4)",
),
(
signal.clone(),
Box::new(Conv1D::new(4, 3, 1, ReLU::new()).unwrap()),
"(None, 6, 4)",
),
(
image.clone(),
Box::new(Conv2D::new(4, (3, 3), (1, 1), ReLU::new()).unwrap()),
"(None, 6, 6, 4)",
),
(
volume.clone(),
Box::new(Conv3D::new(3, (2, 2, 2), (1, 1, 1), ReLU::new()).unwrap()),
"(None, 3, 3, 3, 3)",
),
(
Shape::with_free_batch(&[1, 4, 2]),
Box::new(Conv1DTranspose::new(3, 2, 2, Linear::new()).unwrap()),
"(None, 8, 3)",
),
(
Shape::with_free_batch(&[1, 4, 4, 2]),
Box::new(Conv2DTranspose::new(3, (2, 2), (2, 2), Linear::new()).unwrap()),
"(None, 8, 8, 3)",
),
(
Shape::with_free_batch(&[1, 2, 2, 2, 1]),
Box::new(Conv3DTranspose::new(2, (2, 2, 2), (2, 2, 2), Linear::new()).unwrap()),
"(None, 4, 4, 4, 2)",
),
(
signal.clone(),
Box::new(SeparableConv1D::new(4, 3, 1, 1, ReLU::new()).unwrap()),
"(None, 6, 4)",
),
(
image.clone(),
Box::new(SeparableConv2D::new(4, (3, 3), (1, 1), 1, ReLU::new()).unwrap()),
"(None, 6, 6, 4)",
),
(
signal.clone(),
Box::new(DepthwiseConv1D::new(3, 1, ReLU::new()).unwrap()),
"(None, 6, 2)",
),
(
image.clone(),
Box::new(DepthwiseConv2D::new((3, 3), (1, 1), ReLU::new()).unwrap()),
"(None, 6, 6, 2)",
),
(
signal.clone(),
Box::new(MaxPooling1D::new(2)),
"(None, 4, 2)",
),
(
image.clone(),
Box::new(MaxPooling2D::new((2, 2))),
"(None, 4, 4, 2)",
),
(
volume.clone(),
Box::new(MaxPooling3D::new((2, 2, 2))),
"(None, 2, 2, 2, 2)",
),
(
signal.clone(),
Box::new(AveragePooling1D::new(2)),
"(None, 4, 2)",
),
(
image.clone(),
Box::new(AveragePooling2D::new((2, 2))),
"(None, 4, 4, 2)",
),
(
volume.clone(),
Box::new(AveragePooling3D::new((2, 2, 2))),
"(None, 2, 2, 2, 2)",
),
(
signal.clone(),
Box::new(GlobalMaxPooling1D::new()),
"(None, 2)",
),
(
image.clone(),
Box::new(GlobalMaxPooling2D::new()),
"(None, 2)",
),
(
volume.clone(),
Box::new(GlobalMaxPooling3D::new()),
"(None, 2)",
),
(
signal.clone(),
Box::new(GlobalAveragePooling1D::new()),
"(None, 2)",
),
(
image.clone(),
Box::new(GlobalAveragePooling2D::new()),
"(None, 2)",
),
(
volume.clone(),
Box::new(GlobalAveragePooling3D::new()),
"(None, 2)",
),
(
Shape::with_free_batch(&[1, 4, 2]),
Box::new(UpSampling1D::new(2).unwrap()),
"(None, 8, 2)",
),
(
Shape::with_free_batch(&[1, 4, 4, 2]),
Box::new(UpSampling2D::new(2, Interpolation::Nearest).unwrap()),
"(None, 8, 8, 2)",
),
(
Shape::with_free_batch(&[1, 2, 2, 2, 1]),
Box::new(UpSampling3D::new(2).unwrap()),
"(None, 4, 4, 4, 1)",
),
(
Shape::with_free_batch(&[1, 4, 2]),
Box::new(ZeroPadding1D::new(1)),
"(None, 6, 2)",
),
(
Shape::with_free_batch(&[1, 4, 4, 2]),
Box::new(ZeroPadding2D::new(1)),
"(None, 6, 6, 2)",
),
(
Shape::with_free_batch(&[1, 2, 2, 2, 1]),
Box::new(ZeroPadding3D::new(1)),
"(None, 4, 4, 4, 1)",
),
(
Shape::with_free_batch(&[1, 6, 2]),
Box::new(Cropping1D::new(1)),
"(None, 4, 2)",
),
(
Shape::with_free_batch(&[1, 6, 6, 2]),
Box::new(Cropping2D::new(1)),
"(None, 4, 4, 2)",
),
(
volume.clone(),
Box::new(Cropping3D::new(1)),
"(None, 2, 2, 2, 2)",
),
(
sequence.clone(),
Box::new(SimpleRNN::new(3, Tanh::new()).unwrap()),
"(None, 3)",
),
(
sequence.clone(),
Box::new(LSTM::new(3, Tanh::new()).unwrap()),
"(None, 3)",
),
(
sequence.clone(),
Box::new(GRU::new(3, Tanh::new()).unwrap()),
"(None, 3)",
),
(
flat.clone(),
Box::new(Dropout::new(0.5).unwrap()),
"(None, 4)",
),
(
signal.clone(),
Box::new(SpatialDropout1D::new(0.5).unwrap()),
"(None, 8, 2)",
),
(
image.clone(),
Box::new(SpatialDropout2D::new(0.5).unwrap()),
"(None, 8, 8, 2)",
),
(
volume.clone(),
Box::new(SpatialDropout3D::new(0.5).unwrap()),
"(None, 4, 4, 4, 2)",
),
(
flat.clone(),
Box::new(GaussianDropout::new(0.3).unwrap()),
"(None, 4)",
),
(
flat.clone(),
Box::new(GaussianNoise::new(0.1).unwrap()),
"(None, 4)",
),
(
flat.clone(),
Box::new(BatchNormalization::new(0.9, 1e-5).unwrap()),
"(None, 4)",
),
(
flat.clone(),
Box::new(LayerNormalization::new(1e-5).unwrap()),
"(None, 4)",
),
(
flat.clone(),
Box::new(GroupNormalization::new(2, 1e-5).unwrap()),
"(None, 4)",
),
(
signal.clone(),
Box::new(InstanceNormalization::new(1e-5).unwrap()),
"(None, 8, 2)",
),
(
flat.clone(),
Box::new(UnitNormalization::new(UnitNormalizationAxis::Default).unwrap()),
"(None, 4)",
),
]
}
#[test]
fn every_layer_type_answers_before_its_build() {
let mut covered = BTreeSet::new();
for (input, layer, expected) in unbuilt_layers() {
let name = layer.layer_type().to_string();
assert!(
layer.build_config().is_none(),
"{name} must hold no build in this table"
);
let computed = layer
.compute_output_shape_many(std::slice::from_ref(&input))
.unwrap_or_else(|error| {
panic!("{name} refused the shape {input} before its build: {error}")
});
assert_eq!(computed.to_string(), expected, "{name}");
assert!(
covered.insert(name.clone()),
"{name} reaches the table more than once"
);
}
assert_eq!(
covered.len(),
66,
"the table above is the only roster of the covered layer types. A new layer type needs \
a case here, and a removed one needs its case deleted. Move this count in the same \
change"
);
}
#[test]
fn a_build_changes_no_answer() {
for (input, mut layer, expected) in unbuilt_layers() {
let name = layer.layer_type().to_string();
let before = layer
.compute_output_shape_many(std::slice::from_ref(&input))
.unwrap();
layer
.build_many(std::slice::from_ref(&input))
.unwrap_or_else(|error| panic!("{name} refused to build for {input}: {error}"));
let after = layer
.compute_output_shape_many(std::slice::from_ref(&input))
.unwrap_or_else(|error| panic!("{name} refused {input} after its build: {error}"));
assert_eq!(before, after, "{name}");
assert_eq!(after.to_string(), expected, "{name}");
}
}
#[test]
fn a_model_refuses_an_input_shape_with_an_empty_feature_axis() {
let error = SequentialBuilder::new()
.add(Flatten::new())
.build(&Shape::known(&[1, 0, 4]))
.err()
.expect("a 0 extent must not build");
let text = error.to_string();
assert!(
text.contains("axis 1") && text.contains("0 elements"),
"the refusal must name the empty axis, got {text}"
);
SequentialBuilder::new()
.add(Flatten::new())
.build(&Shape::with_free_batch(&[1, 3, 4]))
.expect("a free batch axis is not an empty axis");
}