use super::{
broadcast_input, elementwise_merge_layer_functions, merge_layer_base_functions, merged_dims,
reduce_to,
};
use crate::error::{Context, Error};
use crate::neural_network::layers::ParamCounts;
use crate::neural_network::traits::{Arity, Layer, LayerBase};
use crate::neural_network::{Ctx, Shape, Tensor};
use ndarray::IxDyn;
#[derive(Debug, Default)]
pub struct Maximum {
built: Option<Vec<Shape>>,
}
impl Maximum {
pub fn new() -> Self {
Self::default()
}
}
struct MaximumCache {
output: Vec<usize>,
inputs: Vec<Vec<usize>>,
winners: Vec<usize>,
}
impl LayerBase for Maximum {
fn layer_type(&self) -> &str {
"Maximum"
}
merge_layer_base_functions!();
}
impl Layer for Maximum {
elementwise_merge_layer_functions!("Maximum", Arity::AtLeast(1));
fn forward_many(&self, inputs: &[&Tensor], ctx: &mut Ctx) -> Result<Tensor, Error> {
Arity::AtLeast(1).check("Maximum", inputs.len())?;
let dims = merged_dims("Maximum", inputs)?;
let mut folded = broadcast_input("Maximum", inputs[0], &dims)?;
let mut winners = vec![0_usize; folded.len()];
for (position, &input) in inputs.iter().enumerate().skip(1) {
let lifted = broadcast_input("Maximum", input, &dims)?;
for ((value, &candidate), winner) in
folded.iter_mut().zip(lifted.iter()).zip(winners.iter_mut())
{
let takes = !value.is_nan() && (candidate.is_nan() || candidate > *value);
if takes {
*value = candidate;
*winner = position;
}
}
}
if ctx.is_training() {
ctx.push_cache(
"Maximum",
MaximumCache {
output: dims,
inputs: inputs
.iter()
.map(|tensor| tensor.shape().to_vec())
.collect(),
winners,
},
);
}
Ok(folded)
}
fn backward_many(&self, grad_output: &Tensor, ctx: &mut Ctx) -> Result<Vec<Tensor>, Error> {
let cache: MaximumCache = ctx.pop_cache("Maximum")?;
if grad_output.shape() != cache.output.as_slice() {
return Err(Error::shape_mismatch(cache.output, grad_output.shape()));
}
let mut routed = vec![vec![0.0_f32; cache.winners.len()]; cache.inputs.len()];
for ((offset, &winner), &grad) in cache.winners.iter().enumerate().zip(grad_output.iter()) {
routed[winner][offset] = grad;
}
let mut grads = Vec::with_capacity(cache.inputs.len());
for (values, shape) in routed.into_iter().zip(cache.inputs.iter()) {
let whole = Tensor::from_shape_vec(IxDyn(&cache.output), values)
.context("Failed to give a Maximum gradient the shape of the output")?;
grads.push(reduce_to(&whole, shape));
}
Ok(grads)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tensor(dims: &[usize], values: &[f32]) -> Tensor {
Tensor::from_shape_vec(IxDyn(dims), values.to_vec()).unwrap()
}
#[test]
fn the_fold_takes_the_larger_value() {
let left = tensor(&[2, 2], &[1.0, 8.0, 3.0, 4.0]);
let right = tensor(&[2, 2], &[5.0, 2.0, 7.0, 0.0]);
let layer = Maximum::new();
let mut ctx = Ctx::inference();
let output = layer.forward_many(&[&left, &right], &mut ctx).unwrap();
assert_eq!(output, tensor(&[2, 2], &[5.0, 8.0, 7.0, 4.0]));
assert!(output.is_standard_layout(), "a layer emits the C order");
assert_eq!(ctx.pending_caches(), 0, "an inference pass parks nothing");
}
#[test]
fn a_single_input_comes_back_unchanged() {
let only = tensor(&[2, 2], &[1.0, -2.0, 3.0, -4.0]);
let layer = Maximum::new();
assert_eq!(layer.arity(), Arity::AtLeast(1));
let mut ctx = Ctx::training();
let output = layer.forward_many(&[&only], &mut ctx).unwrap();
assert_eq!(output, only);
let grad = tensor(&[2, 2], &[1.0, 2.0, 3.0, 4.0]);
let grads = layer.backward_many(&grad, &mut ctx).unwrap();
assert_eq!(grads.len(), 1);
assert_eq!(grads[0], grad, "the 1 input takes every position");
}
#[test]
fn the_winner_takes_the_whole_gradient() {
let left = tensor(&[2, 2], &[1.0, 8.0, 3.0, 4.0]);
let right = tensor(&[2, 2], &[5.0, 2.0, 7.0, 0.0]);
let layer = Maximum::new();
let mut ctx = Ctx::training();
layer.forward_many(&[&left, &right], &mut ctx).unwrap();
let grads = layer
.backward_many(&tensor(&[2, 2], &[10.0, 20.0, 30.0, 40.0]), &mut ctx)
.unwrap();
assert_eq!(grads.len(), 2);
assert_eq!(grads[0], tensor(&[2, 2], &[0.0, 20.0, 0.0, 40.0]));
assert_eq!(grads[1], tensor(&[2, 2], &[10.0, 0.0, 30.0, 0.0]));
assert_eq!(ctx.pending_caches(), 0, "the backward pass takes the cache");
}
#[test]
fn tie_routes_the_gradient_to_the_first_input() {
let first = tensor(&[1, 3], &[1.0, 2.0, 3.0]);
let second = tensor(&[1, 3], &[3.0, 2.0, 1.0]);
let third = tensor(&[1, 3], &[2.0, 2.0, 2.0]);
let layer = Maximum::new();
let mut ctx = Ctx::training();
let output = layer
.forward_many(&[&first, &second, &third], &mut ctx)
.unwrap();
assert_eq!(output, tensor(&[1, 3], &[3.0, 2.0, 3.0]));
let grads = layer
.backward_many(&tensor(&[1, 3], &[1.0, 1.0, 1.0]), &mut ctx)
.unwrap();
assert_eq!(grads[0], tensor(&[1, 3], &[0.0, 1.0, 1.0]));
assert_eq!(grads[1], tensor(&[1, 3], &[1.0, 0.0, 0.0]));
assert_eq!(
grads[2],
tensor(&[1, 3], &[0.0, 0.0, 0.0]),
"a later input that ties takes nothing"
);
}
#[test]
fn a_nan_wins_its_position() {
let left = tensor(&[1, 2], &[1.0, f32::NAN]);
let right = tensor(&[1, 2], &[f32::NAN, 2.0]);
let layer = Maximum::new();
let mut ctx = Ctx::training();
let output = layer.forward_many(&[&left, &right], &mut ctx).unwrap();
assert!(output[[0, 0]].is_nan(), "a later NaN takes the position");
assert!(output[[0, 1]].is_nan(), "an earlier NaN keeps the position");
let grads = layer
.backward_many(&tensor(&[1, 2], &[1.0, 1.0]), &mut ctx)
.unwrap();
assert_eq!(grads[0], tensor(&[1, 2], &[0.0, 1.0]));
assert_eq!(grads[1], tensor(&[1, 2], &[1.0, 0.0]));
}
#[test]
fn each_side_broadcasts_into_the_other() {
let left = tensor(&[2, 1, 3], &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
let right = tensor(&[2, 2, 1], &[0.0, 9.0, 7.0, 0.0]);
let layer = Maximum::new();
let mut ctx = Ctx::training();
let output = layer.forward_many(&[&left, &right], &mut ctx).unwrap();
assert_eq!(
output,
tensor(
&[2, 2, 3],
&[1.0, 2.0, 3.0, 9.0, 9.0, 9.0, 7.0, 7.0, 7.0, 4.0, 5.0, 6.0]
)
);
let grads = layer
.backward_many(&Tensor::ones([2, 2, 3].as_slice()), &mut ctx)
.unwrap();
assert_eq!(
grads[0],
tensor(&[2, 1, 3], &[1.0, 1.0, 1.0, 1.0, 1.0, 1.0]),
"the gradient keeps the shape of its own input"
);
assert_eq!(grads[1], tensor(&[2, 2, 1], &[0.0, 3.0, 3.0, 0.0]));
assert!(grads[0].is_standard_layout(), "a layer emits the C order");
}
#[test]
fn rank_alignment_inserts_after_the_batch_axis() {
let wide = tensor(
&[2, 2, 3],
&[
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
],
);
let narrow = tensor(&[2, 3], &[5.0, 5.0, 5.0, 8.0, 8.0, 8.0]);
let layer = Maximum::new();
let mut ctx = Ctx::training();
let output = layer.forward_many(&[&wide, &narrow], &mut ctx).unwrap();
assert_eq!(
output,
tensor(
&[2, 2, 3],
&[
5.0, 5.0, 5.0, 5.0, 5.0, 6.0, 8.0, 8.0, 9.0, 10.0, 11.0, 12.0
]
)
);
let grads = layer
.backward_many(&Tensor::ones([2, 2, 3].as_slice()), &mut ctx)
.unwrap();
assert_eq!(
grads[0],
tensor(
&[2, 2, 3],
&[0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0]
)
);
assert_eq!(
grads[1],
tensor(&[2, 3], &[2.0, 1.0, 1.0, 1.0, 0.0, 0.0]),
"the inserted axis sums away"
);
}
#[test]
fn the_build_records_every_input_shape() {
let mut layer = Maximum::new();
assert!(!layer.is_built());
assert_eq!(layer.output_shape(), "Unknown");
layer
.build_many(&[Shape::known(&[2, 1, 4]), Shape::known(&[2, 3, 1])])
.unwrap();
assert!(layer.is_built());
assert_eq!(
layer.known_input_shapes(),
Some(vec![
Shape::with_free_batch(&[2, 1, 4]),
Shape::with_free_batch(&[2, 3, 1]),
])
);
assert_eq!(layer.output_shape(), "(None, 3, 4)");
assert_eq!(layer.param_count(), ParamCounts::none());
assert!(layer.weights().is_empty(), "the layer holds no array");
}
#[test]
fn the_batch_axis_does_not_broadcast() {
let mut layer = Maximum::new();
let message = layer
.build_many(&[Shape::known(&[2, 3]), Shape::known(&[1, 3])])
.unwrap_err()
.to_string();
assert!(message.contains("Maximum"), "{message}");
assert!(message.contains("batch axis"), "{message}");
let layer = Maximum::new();
let mut ctx = Ctx::inference();
let tall = tensor(&[2, 3], &[0.0; 6]);
let flat = tensor(&[1, 3], &[0.0; 3]);
let message = layer
.forward_many(&[&tall, &flat], &mut ctx)
.unwrap_err()
.to_string();
assert!(message.contains("batch axis"), "{message}");
}
#[test]
fn an_axis_that_cannot_broadcast_is_refused() {
let mut layer = Maximum::new();
let message = layer
.build_many(&[Shape::known(&[2, 3]), Shape::known(&[2, 5])])
.unwrap_err()
.to_string();
assert!(message.contains("Maximum"), "{message}");
assert!(message.contains("Axis 1"), "{message}");
assert!(
Maximum::new()
.compute_output_shape_many(&[Shape::known(&[2, 3, 4]), Shape::known(&[2, 3])])
.is_err(),
"the rank alignment refuses a tail that does not right-align"
);
}
#[test]
fn a_second_build_for_other_shapes_is_refused() {
let mut layer = Maximum::new();
layer
.build_many(&[Shape::known(&[2, 3]), Shape::known(&[2, 3])])
.unwrap();
layer
.build_many(&[Shape::known(&[5, 3]), Shape::known(&[5, 3])])
.unwrap();
assert!(
layer
.build_many(&[Shape::known(&[2, 4]), Shape::known(&[2, 4])])
.is_err()
);
assert!(layer.build_many(&[Shape::known(&[2, 3])]).is_err());
}
#[test]
fn no_input_at_all_is_refused() {
let mut layer = Maximum::new();
let message = layer.build_many(&[]).unwrap_err().to_string();
assert!(message.contains("Maximum"), "{message}");
assert!(message.contains("1 or more"), "{message}");
let layer = Maximum::new();
let mut ctx = Ctx::inference();
let message = layer.forward_many(&[], &mut ctx).unwrap_err().to_string();
assert!(message.contains("1 or more"), "{message}");
assert!(layer.compute_output_shape_many(&[]).is_err());
}
#[test]
fn a_gradient_of_another_shape_is_refused() {
let left = tensor(&[1, 3], &[1.0, 2.0, 3.0]);
let right = tensor(&[1, 3], &[3.0, 2.0, 1.0]);
let layer = Maximum::new();
let mut ctx = Ctx::training();
layer.forward_many(&[&left, &right], &mut ctx).unwrap();
assert!(
layer
.backward_many(&tensor(&[1, 2], &[1.0, 1.0]), &mut ctx)
.is_err()
);
}
}