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 Minimum {
built: Option<Vec<Shape>>,
}
impl Minimum {
pub fn new() -> Self {
Minimum::default()
}
}
struct MinimumCache {
output: Vec<usize>,
inputs: Vec<Vec<usize>>,
winners: Vec<usize>,
}
impl LayerBase for Minimum {
fn layer_type(&self) -> &str {
"Minimum"
}
merge_layer_base_functions!();
}
impl Layer for Minimum {
elementwise_merge_layer_functions!("Minimum", Arity::AtLeast(1));
fn forward_many(&self, inputs: &[&Tensor], ctx: &mut Ctx) -> Result<Tensor, Error> {
Arity::AtLeast(1).check("Minimum", inputs.len())?;
let dims = merged_dims("Minimum", inputs)?;
let mut folded = broadcast_input("Minimum", inputs[0], &dims)?;
let mut winners = vec![0_usize; folded.len()];
for (position, &input) in inputs.iter().enumerate().skip(1) {
let lifted = broadcast_input("Minimum", 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(
"Minimum",
MinimumCache {
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: MinimumCache = ctx.pop_cache("Minimum")?;
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 Minimum 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()).expect("the values fill the shape")
}
fn forward(inputs: &[&Tensor], ctx: &mut Ctx) -> (Minimum, Tensor) {
let mut layer = Minimum::new();
let output = layer
.forward_many_mut(inputs, ctx)
.expect("the shapes merge");
(layer, output)
}
#[test]
fn forward_takes_the_smallest_value() {
let a = tensor(&[2, 2], &[1.0, 5.0, 3.0, 2.0]);
let b = tensor(&[2, 2], &[4.0, 2.0, 3.0, 6.0]);
let c = tensor(&[2, 2], &[0.0, 9.0, 7.0, 1.0]);
let mut ctx = Ctx::inference();
let (_, output) = forward(&[&a, &b, &c], &mut ctx);
assert_eq!(output, tensor(&[2, 2], &[0.0, 2.0, 3.0, 1.0]));
assert!(output.is_standard_layout(), "a layer emits the C order");
}
#[test]
fn forward_accepts_a_single_input() {
let a = tensor(&[2, 3], &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
let mut ctx = Ctx::training();
let (layer, output) = forward(&[&a], &mut ctx);
assert_eq!(output, a);
assert_eq!(layer.arity(), Arity::AtLeast(1));
let grads = layer
.backward_many(&Tensor::ones(output.raw_dim()), &mut ctx)
.expect("the cache is there");
assert_eq!(grads.len(), 1);
assert_eq!(grads[0], Tensor::ones([2, 3].as_slice()));
}
#[test]
fn backward_routes_the_whole_gradient_to_the_winner() {
let a = tensor(&[2, 2], &[1.0, 5.0, 3.0, 2.0]);
let b = tensor(&[2, 2], &[4.0, 2.0, 3.0, 6.0]);
let c = tensor(&[2, 2], &[0.0, 9.0, 7.0, 1.0]);
let mut ctx = Ctx::training();
let (layer, _) = forward(&[&a, &b, &c], &mut ctx);
let grads = layer
.backward_many(&tensor(&[2, 2], &[1.0, 2.0, 3.0, 4.0]), &mut ctx)
.expect("the cache is there");
assert_eq!(grads.len(), 3);
assert_eq!(grads[0], tensor(&[2, 2], &[0.0, 0.0, 3.0, 0.0]));
assert_eq!(grads[1], tensor(&[2, 2], &[0.0, 2.0, 0.0, 0.0]));
assert_eq!(grads[2], tensor(&[2, 2], &[1.0, 0.0, 0.0, 4.0]));
assert_eq!(ctx.pending_caches(), 0, "the backward pass took the cache");
}
#[test]
fn backward_gives_a_tie_to_the_first_input() {
let a = tensor(&[1, 3], &[2.0, 2.0, 5.0]);
let b = tensor(&[1, 3], &[2.0, 7.0, 5.0]);
let c = tensor(&[1, 3], &[2.0, 7.0, 5.0]);
let mut ctx = Ctx::training();
let (layer, output) = forward(&[&a, &b, &c], &mut ctx);
assert_eq!(output, tensor(&[1, 3], &[2.0, 2.0, 5.0]));
let grads = layer
.backward_many(&Tensor::ones(output.raw_dim()), &mut ctx)
.expect("the cache is there");
assert_eq!(grads[0], tensor(&[1, 3], &[1.0, 1.0, 1.0]));
assert_eq!(grads[1], tensor(&[1, 3], &[0.0, 0.0, 0.0]));
assert_eq!(grads[2], tensor(&[1, 3], &[0.0, 0.0, 0.0]));
}
#[test]
fn a_nan_wins_its_position() {
let a = tensor(&[1, 2], &[1.0, f32::NAN]);
let b = tensor(&[1, 2], &[f32::NAN, 2.0]);
let layer = Minimum::new();
let mut ctx = Ctx::training();
let output = layer
.forward_many(&[&a, &b], &mut ctx)
.expect("the shapes merge");
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)
.expect("the cache is there");
assert_eq!(grads[0], tensor(&[1, 2], &[0.0, 1.0]));
assert_eq!(grads[1], tensor(&[1, 2], &[1.0, 0.0]));
}
#[test]
fn an_axis_of_one_position_broadcasts() {
let a = tensor(&[2, 3], &[1.0, 5.0, 3.0, 4.0, 2.0, 6.0]);
let b = tensor(&[2, 1], &[2.0, 5.0]);
let mut ctx = Ctx::training();
let (layer, output) = forward(&[&a, &b], &mut ctx);
assert_eq!(output, tensor(&[2, 3], &[1.0, 2.0, 2.0, 4.0, 2.0, 5.0]));
let grads = layer
.backward_many(&Tensor::ones(output.raw_dim()), &mut ctx)
.expect("the cache is there");
assert_eq!(grads[0], tensor(&[2, 3], &[1.0, 0.0, 0.0, 1.0, 1.0, 0.0]));
assert_eq!(
grads[1],
tensor(&[2, 1], &[2.0, 1.0]),
"the gradient of a broadcast input sums back to its own shape"
);
}
#[test]
fn a_shorter_input_aligns_after_the_batch_axis() {
let a = tensor(&[2, 2, 2], &[1.0, 8.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]);
let b = tensor(&[2, 2], &[2.0, 7.0, 6.0, 9.0]);
let mut ctx = Ctx::training();
let (layer, output) = forward(&[&a, &b], &mut ctx);
assert_eq!(
output,
tensor(&[2, 2, 2], &[1.0, 7.0, 2.0, 4.0, 5.0, 6.0, 6.0, 8.0])
);
let grads = layer
.backward_many(&Tensor::ones(output.raw_dim()), &mut ctx)
.expect("the cache is there");
assert_eq!(
grads[0],
tensor(&[2, 2, 2], &[1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0])
);
assert_eq!(
grads[1],
tensor(&[2, 2], &[1.0, 1.0, 1.0, 0.0]),
"the inserted axis goes away, and the sum runs over it"
);
}
#[test]
fn the_batch_axis_is_refused() {
let mut layer = Minimum::new();
let message = layer
.build_many(&[Shape::known(&[2, 3]), Shape::known(&[1, 3])])
.expect_err("the batch axis does not broadcast")
.to_string();
assert!(message.contains("Minimum"), "{message}");
assert!(message.contains("batch axis"), "{message}");
assert!(message.contains("(2, 3)"), "{message}");
assert!(message.contains("(1, 3)"), "{message}");
let a = tensor(&[2, 3], &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
let b = tensor(&[1, 3], &[1.0, 2.0, 3.0]);
let mut ctx = Ctx::training();
let refused = Minimum::new().forward_many_mut(&[&a, &b], &mut ctx);
assert!(refused.is_err());
}
#[test]
fn an_axis_that_cannot_broadcast_is_refused() {
let mut layer = Minimum::new();
let message = layer
.build_many(&[Shape::known(&[2, 3]), Shape::known(&[2, 5])])
.expect_err("3 and 5 do not broadcast")
.to_string();
assert!(message.contains("Minimum"), "{message}");
assert!(message.contains("Axis 1"), "{message}");
assert!(!layer.is_built(), "a refused build records nothing");
let refused =
layer.compute_output_shape_many(&[Shape::known(&[2, 3]), Shape::known(&[2, 5])]);
assert!(refused.is_err());
}
#[test]
fn a_second_build_for_other_shapes_is_refused() {
let mut layer = Minimum::new();
let shapes = vec![Shape::known(&[2, 3]), Shape::known(&[2, 3])];
layer.build_many(&shapes).expect("the shapes merge");
assert_eq!(
layer.known_input_shapes(),
Some(vec![
Shape::with_free_batch(&[2, 3]),
Shape::with_free_batch(&[2, 3])
]),
"a build report frees the batch axis"
);
assert_eq!(layer.output_shape(), "(None, 3)");
layer.build_many(&shapes).expect("the build is idempotent");
let message = layer
.build_many(&[Shape::known(&[2, 4]), Shape::known(&[2, 4])])
.expect_err("the layer is built for another shape")
.to_string();
assert!(message.contains("Minimum"), "{message}");
assert!(message.contains("(2, 3)"), "{message}");
assert!(message.contains("(2, 4)"), "{message}");
}
#[test]
fn no_input_at_all_is_refused() {
let mut layer = Minimum::new();
let message = layer
.build_many(&[])
.expect_err("the layer takes 1 input at least")
.to_string();
assert!(message.contains("Minimum"), "{message}");
assert!(message.contains("1 or more"), "{message}");
let mut ctx = Ctx::training();
assert!(layer.forward_many(&[], &mut ctx).is_err());
assert!(layer.compute_output_shape_many(&[]).is_err());
assert_eq!(layer.param_count(), ParamCounts::none());
}
}