use crate::error::{Context, Error};
use crate::neural_network::layers::validation::start_build_many;
use crate::neural_network::traits::Arity;
use crate::neural_network::{Shape, Tensor};
use ndarray::Axis;
pub mod add;
pub mod average;
pub mod concatenate;
pub mod maximum;
pub mod minimum;
pub mod multiply;
pub mod subtract;
pub use add::Add;
pub use average::Average;
pub use concatenate::Concatenate;
pub use maximum::Maximum;
pub use minimum::Minimum;
pub use multiply::Multiply;
pub use subtract::Subtract;
fn elementwise_output_shape(layer: &str, inputs: &[Shape]) -> Result<Shape, Error> {
let Some((first, rest)) = inputs.split_first() else {
return Err(Error::invalid_input(format!(
"{layer} takes 1 input at least, and it received none"
)));
};
first.check_min_rank(layer, 1)?;
let mut merged = first.clone();
for input in rest {
input.check_min_rank(layer, 1)?;
let batch = match (merged.axes()[0], input.axes()[0]) {
(Some(held), Some(found)) if held != found => {
return Err(Error::invalid_input(format!(
"{layer} cannot merge the input shape {merged} with the input shape \
{input}, because the batch axis does not broadcast. Axis 0 holds {held} \
on 1 side and {found} on the other"
)));
}
(Some(held), _) => Some(held),
(None, found) => found,
};
let held_tail = &merged.axes()[1..];
let found_tail = &input.axes()[1..];
let rank = held_tail.len().max(found_tail.len());
let mut axes = Vec::with_capacity(rank + 1);
axes.push(batch);
for position in 0..rank {
let extent = match (
aligned_axis(held_tail, rank, position),
aligned_axis(found_tail, rank, position),
) {
(None, _) | (_, None) => None,
(Some(1), Some(extent)) | (Some(extent), Some(1)) => Some(extent),
(Some(held), Some(found)) if held == found => Some(held),
(Some(held), Some(found)) => {
return Err(Error::invalid_input(format!(
"{layer} cannot merge the input shape {merged} with the input shape \
{input}. Axis {} holds {held} on 1 side and {found} on the other, and \
neither extent is 1",
position + 1
)));
}
};
axes.push(extent);
}
merged = Shape::new(axes);
}
Ok(merged)
}
fn aligned_axis(tail: &[Option<usize>], rank: usize, position: usize) -> Option<usize> {
let inserted = rank - tail.len();
if position < inserted {
Some(1)
} else {
tail[position - inserted]
}
}
fn merged_dims(layer: &str, inputs: &[&Tensor]) -> Result<Vec<usize>, Error> {
let shapes: Vec<Shape> = inputs
.iter()
.map(|tensor| Shape::known(tensor.shape()))
.collect();
let merged = elementwise_output_shape(layer, &shapes)?;
merged.dims().ok_or_else(|| {
Error::computation(format!(
"{layer} merged tensors of fixed extents into the shape {merged}, which leaves an \
axis free"
))
})
}
fn broadcast_input(layer: &str, input: &Tensor, output: &[usize]) -> Result<Tensor, Error> {
let Some((&batch, tail)) = input.shape().split_first() else {
return Err(Error::invalid_input(format!(
"{layer} expects an input with a batch axis, got a tensor of rank 0"
)));
};
if output.len() < input.ndim() {
return Err(Error::invalid_input(format!(
"{layer} cannot broadcast an input of shape {:?} to the output shape {output:?}, \
because the output holds fewer axes",
input.shape()
)));
}
let mut aligned = Vec::with_capacity(output.len());
aligned.push(batch);
aligned.resize(output.len() - tail.len(), 1);
aligned.extend_from_slice(tail);
let view = input
.to_shape(aligned)
.context("Failed to align the rank of a merge input")?;
let broadcast = view.broadcast(output).ok_or_else(|| {
Error::invalid_input(format!(
"{layer} cannot broadcast an input of shape {:?} to the output shape {output:?}",
input.shape()
))
})?;
let mut owned = Tensor::zeros(output);
owned.assign(&broadcast);
Ok(owned)
}
fn reduce_to(grad: &Tensor, target: &[usize]) -> Tensor {
debug_assert!(
grad.ndim() >= target.len(),
"a merge gradient holds the rank of the output"
);
let inserted = grad.ndim() - target.len();
let mut reduced = if inserted == 0 {
let mut owned = Tensor::zeros(grad.raw_dim());
owned.assign(grad);
owned
} else {
let mut summed = grad.sum_axis(Axis(1));
for _ in 1..inserted {
summed = summed.sum_axis(Axis(1));
}
summed
};
for (axis, &extent) in target.iter().enumerate() {
if extent == 1 && reduced.shape()[axis] != 1 {
reduced = reduced.sum_axis(Axis(axis)).insert_axis(Axis(axis));
}
}
reduced
}
fn build_elementwise(
built: &mut Option<Vec<Shape>>,
layer: &str,
arity: Arity,
inputs: &[Shape],
) -> Result<(), Error> {
arity.check(layer, inputs.len())?;
let Some(shapes) = start_build_many(built, layer, inputs)? else {
return Ok(());
};
elementwise_output_shape(layer, &shapes)?;
*built = Some(shapes);
Ok(())
}
macro_rules! merge_layer_base_functions {
() => {
fn known_input_shapes(&self) -> Option<Vec<$crate::neural_network::Shape>> {
self.built.as_ref().map(|shapes| {
shapes
.iter()
.map($crate::neural_network::Shape::free_batch)
.collect()
})
}
fn is_built(&self) -> bool {
self.built.is_some()
}
fn build_config(&self) -> Option<$crate::neural_network::layers::checkpoint::BuildConfig> {
self.built
.as_ref()
.map(|shapes| $crate::neural_network::layers::checkpoint::BuildConfig::new(shapes))
}
$crate::neural_network::layers::no_trainable_parameters_layer_functions!();
};
}
pub(in crate::neural_network::layers::merge) use merge_layer_base_functions;
macro_rules! elementwise_merge_layer_functions {
($layer:literal, $arity:expr) => {
fn arity(&self) -> $crate::neural_network::traits::Arity {
$arity
}
fn build_many(
&mut self,
inputs: &[$crate::neural_network::Shape],
) -> Result<(), $crate::error::Error> {
$crate::neural_network::layers::merge::build_elementwise(
&mut self.built,
$layer,
$arity,
inputs,
)
}
fn compute_output_shape_many(
&self,
inputs: &[$crate::neural_network::Shape],
) -> Result<$crate::neural_network::Shape, $crate::error::Error> {
$crate::neural_network::traits::Arity::check($arity, $layer, inputs.len())?;
$crate::neural_network::layers::merge::elementwise_output_shape($layer, inputs)
}
};
}
pub(in crate::neural_network::layers::merge) use elementwise_merge_layer_functions;
#[cfg(test)]
mod tests {
use super::*;
use ndarray::IxDyn;
#[test]
fn equal_shapes_merge_unchanged() {
let shapes = vec![Shape::known(&[2, 3, 4]), Shape::known(&[2, 3, 4])];
assert_eq!(
elementwise_output_shape("Add", &shapes).unwrap(),
Shape::known(&[2, 3, 4])
);
}
#[test]
fn a_single_input_gives_its_own_shape() {
let shapes = vec![Shape::with_free_batch(&[2, 3])];
assert_eq!(
elementwise_output_shape("Add", &shapes).unwrap(),
Shape::with_free_batch(&[2, 3])
);
}
#[test]
fn an_axis_after_the_batch_axis_broadcasts() {
let shapes = vec![Shape::known(&[2, 3]), Shape::known(&[2, 1])];
assert_eq!(
elementwise_output_shape("Multiply", &shapes).unwrap(),
Shape::known(&[2, 3])
);
let shapes = vec![Shape::known(&[2, 1, 4]), Shape::known(&[2, 3, 1])];
assert_eq!(
elementwise_output_shape("Multiply", &shapes).unwrap(),
Shape::known(&[2, 3, 4])
);
}
#[test]
fn the_batch_axis_does_not_broadcast() {
let shapes = vec![Shape::known(&[2, 3]), Shape::known(&[1, 3])];
let message = elementwise_output_shape("Add", &shapes)
.unwrap_err()
.to_string();
assert!(message.contains("Add"), "{message}");
assert!(message.contains("batch axis"), "{message}");
assert!(message.contains("(2, 3)"), "{message}");
assert!(message.contains("(1, 3)"), "{message}");
}
#[test]
fn a_free_batch_axis_takes_the_other_extent() {
let shapes = vec![Shape::with_free_batch(&[9, 3]), Shape::known(&[2, 3])];
assert_eq!(
elementwise_output_shape("Add", &shapes).unwrap(),
Shape::known(&[2, 3])
);
let shapes = vec![
Shape::with_free_batch(&[9, 3]),
Shape::with_free_batch(&[1, 3]),
];
assert_eq!(
elementwise_output_shape("Add", &shapes).unwrap(),
Shape::with_free_batch(&[9, 3])
);
}
#[test]
fn rank_alignment_inserts_after_the_batch_axis() {
let shapes = vec![Shape::known(&[2, 3, 4]), Shape::known(&[2, 4])];
assert_eq!(
elementwise_output_shape("Add", &shapes).unwrap(),
Shape::known(&[2, 3, 4])
);
let shapes = vec![Shape::known(&[2, 3, 4]), Shape::known(&[2, 3])];
assert!(elementwise_output_shape("Add", &shapes).is_err());
}
#[test]
fn a_free_axis_frees_the_output_axis() {
let free = Shape::new(vec![Some(2), None, Some(4)]);
let shapes = vec![free.clone(), Shape::known(&[2, 3, 4])];
assert_eq!(elementwise_output_shape("Add", &shapes).unwrap(), free);
}
#[test]
fn the_rule_folds_over_every_input() {
let shapes = vec![
Shape::known(&[2, 1, 4]),
Shape::known(&[2, 3, 1]),
Shape::known(&[2, 1, 1]),
];
assert_eq!(
elementwise_output_shape("Average", &shapes).unwrap(),
Shape::known(&[2, 3, 4])
);
let shapes = vec![
Shape::known(&[2, 3]),
Shape::known(&[2, 3]),
Shape::known(&[2, 5]),
];
let message = elementwise_output_shape("Average", &shapes)
.unwrap_err()
.to_string();
assert!(message.contains("Average"), "{message}");
assert!(message.contains("(2, 3)"), "{message}");
assert!(message.contains("(2, 5)"), "{message}");
assert!(message.contains("Axis 1"), "{message}");
}
#[test]
fn a_shape_without_a_batch_axis_is_refused() {
let shapes = vec![Shape::new(Vec::new()), Shape::known(&[2])];
let message = elementwise_output_shape("Add", &shapes)
.unwrap_err()
.to_string();
assert!(message.contains("Add"), "{message}");
assert!(message.contains("rank 1 or more"), "{message}");
}
#[test]
fn broadcast_input_inserts_after_the_batch_axis() {
let input =
Tensor::from_shape_vec(IxDyn(&[2, 4]), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0])
.unwrap();
let wide = broadcast_input("Add", &input, &[2, 3, 4]).unwrap();
assert_eq!(wide.shape(), &[2, 3, 4]);
for position in 0..3 {
assert_eq!(wide[[0, position, 0]], 1.0);
assert_eq!(wide[[1, position, 3]], 8.0);
}
assert!(wide.is_standard_layout(), "a layer emits the C order");
}
#[test]
fn reduce_to_keeps_a_gradient_that_never_broadcast() {
let grad =
Tensor::from_shape_vec(IxDyn(&[2, 3]), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap();
let reduced = reduce_to(&grad, &[2, 3]);
assert_eq!(reduced, grad);
assert!(reduced.is_standard_layout(), "a layer emits the C order");
}
#[test]
fn reduce_to_sums_an_axis_of_one_position() {
let grad =
Tensor::from_shape_vec(IxDyn(&[2, 3]), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap();
let reduced = reduce_to(&grad, &[2, 1]);
assert_eq!(
reduced,
Tensor::from_shape_vec(IxDyn(&[2, 1]), vec![6.0, 15.0]).unwrap()
);
}
#[test]
fn reduce_to_drops_the_inserted_axes() {
let grad = Tensor::from_shape_vec(
IxDyn(&[2, 2, 3]),
vec![
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
],
)
.unwrap();
let reduced = reduce_to(&grad, &[2, 3]);
assert_eq!(
reduced,
Tensor::from_shape_vec(IxDyn(&[2, 3]), vec![5.0, 7.0, 9.0, 17.0, 19.0, 21.0]).unwrap()
);
assert!(reduced.is_standard_layout(), "a layer emits the C order");
}
#[test]
fn reduce_to_handles_both_kinds_of_axis() {
let grad = Tensor::from_shape_vec(
IxDyn(&[2, 2, 3]),
vec![
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
],
)
.unwrap();
let reduced = reduce_to(&grad, &[2, 1]);
assert_eq!(
reduced,
Tensor::from_shape_vec(IxDyn(&[2, 1]), vec![21.0, 57.0]).unwrap()
);
}
}