use super::{
broadcast_input, elementwise_merge_layer_functions, merge_layer_base_functions, merged_dims,
reduce_to,
};
use crate::error::Error;
use crate::neural_network::layers::ParamCounts;
use crate::neural_network::traits::{Arity, Layer, LayerBase};
use crate::neural_network::{Ctx, Shape, Tensor};
#[derive(Debug, Default)]
pub struct Add {
built: Option<Vec<Shape>>,
}
impl Add {
pub fn new() -> Self {
Self::default()
}
}
struct AddCache {
inputs: Vec<Vec<usize>>,
output: Vec<usize>,
}
impl LayerBase for Add {
fn layer_type(&self) -> &str {
"Add"
}
merge_layer_base_functions!();
}
impl Layer for Add {
elementwise_merge_layer_functions!("Add", Arity::AtLeast(1));
fn forward_many(&self, inputs: &[&Tensor], ctx: &mut Ctx) -> Result<Tensor, Error> {
Arity::AtLeast(1).check("Add", inputs.len())?;
let dims = merged_dims("Add", inputs)?;
let mut output = Tensor::zeros(dims.as_slice());
for input in inputs {
if input.shape() == dims.as_slice() {
output += *input;
} else {
output += &broadcast_input("Add", input, &dims)?;
}
}
if ctx.is_training() {
ctx.push_cache(
"Add",
AddCache {
inputs: inputs.iter().map(|input| input.shape().to_vec()).collect(),
output: dims,
},
);
}
Ok(output)
}
fn backward_many(&self, grad_output: &Tensor, ctx: &mut Ctx) -> Result<Vec<Tensor>, Error> {
let cache: AddCache = ctx.pop_cache("Add")?;
if grad_output.shape() != cache.output.as_slice() {
return Err(Error::shape_mismatch(cache.output, grad_output.shape()));
}
Ok(cache
.inputs
.iter()
.map(|shape| reduce_to(grad_output, shape))
.collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::IxDyn;
fn tensor(dims: &[usize], values: &[f32]) -> Tensor {
Tensor::from_shape_vec(IxDyn(dims), values.to_vec()).unwrap()
}
#[test]
fn the_forward_pass_sums_every_input() {
let first = tensor(&[2, 3], &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
let second = tensor(&[2, 3], &[10.0, 20.0, 30.0, 40.0, 50.0, 60.0]);
let third = tensor(&[2, 3], &[100.0, 200.0, 300.0, 400.0, 500.0, 600.0]);
let mut layer = Add::new();
let mut ctx = Ctx::training();
let output = layer
.forward_many_mut(&[&first, &second, &third], &mut ctx)
.unwrap();
assert_eq!(
output,
tensor(&[2, 3], &[111.0, 222.0, 333.0, 444.0, 555.0, 666.0])
);
assert!(output.is_standard_layout(), "a layer emits the C order");
}
#[test]
fn one_input_alone_comes_back_unchanged() {
let only = tensor(&[2, 3], &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
let mut layer = Add::new();
let mut ctx = Ctx::training();
let output = layer.forward_many_mut(&[&only], &mut ctx).unwrap();
assert_eq!(output, only);
let grads = layer.backward_many(&only, &mut ctx).unwrap();
assert_eq!(grads.len(), 1);
assert_eq!(grads[0], only);
}
#[test]
fn the_backward_pass_gives_the_gradient_to_every_input() {
let first = tensor(&[2, 3], &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
let second = tensor(&[2, 3], &[10.0, 20.0, 30.0, 40.0, 50.0, 60.0]);
let third = tensor(&[2, 3], &[100.0, 200.0, 300.0, 400.0, 500.0, 600.0]);
let mut layer = Add::new();
let mut ctx = Ctx::training();
layer
.forward_many_mut(&[&first, &second, &third], &mut ctx)
.unwrap();
let grad = tensor(&[2, 3], &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
let grads = layer.backward_many(&grad, &mut ctx).unwrap();
assert_eq!(grads.len(), 3);
for one in &grads {
assert_eq!(one, &grad);
assert!(one.is_standard_layout(), "a layer emits the C order");
}
assert_eq!(ctx.pending_caches(), 0, "the backward pass takes the cache");
}
#[test]
fn an_extent_of_one_broadcasts_and_its_gradient_reduces() {
let wide = tensor(&[2, 3], &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
let column = tensor(&[2, 1], &[10.0, 20.0]);
let mut layer = Add::new();
let mut ctx = Ctx::training();
let output = layer.forward_many_mut(&[&wide, &column], &mut ctx).unwrap();
assert_eq!(
output,
tensor(&[2, 3], &[11.0, 12.0, 13.0, 24.0, 25.0, 26.0])
);
let grad = tensor(&[2, 3], &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
let grads = layer.backward_many(&grad, &mut ctx).unwrap();
assert_eq!(grads[0], grad);
assert_eq!(grads[1], tensor(&[2, 1], &[6.0, 15.0]));
}
#[test]
fn rank_alignment_inserts_after_the_batch_axis() {
let deep = 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 flat = tensor(&[2, 3], &[10.0, 20.0, 30.0, 40.0, 50.0, 60.0]);
let mut layer = Add::new();
let mut ctx = Ctx::training();
let output = layer.forward_many_mut(&[&deep, &flat], &mut ctx).unwrap();
assert_eq!(
output,
tensor(
&[2, 2, 3],
&[
11.0, 22.0, 33.0, 14.0, 25.0, 36.0, 47.0, 58.0, 69.0, 50.0, 61.0, 72.0,
],
)
);
let grad = 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 grads = layer.backward_many(&grad, &mut ctx).unwrap();
assert_eq!(grads[0], grad);
assert_eq!(
grads[1],
tensor(&[2, 3], &[5.0, 7.0, 9.0, 17.0, 19.0, 21.0]),
"the inserted axis goes away, and the gradient sums over it"
);
}
#[test]
fn the_batch_axis_does_not_broadcast() {
let mut layer = Add::new();
let message = layer
.build_many(&[Shape::known(&[2, 3]), Shape::known(&[1, 3])])
.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}");
assert!(!layer.is_built(), "a refused build allocates nothing");
}
#[test]
fn two_extents_that_neither_hold_one_are_refused() {
let mut layer = Add::new();
let message = layer
.build_many(&[Shape::known(&[2, 3]), Shape::known(&[2, 5])])
.unwrap_err()
.to_string();
assert!(message.contains("Axis 1"), "{message}");
let left = tensor(&[2, 3], &[1.0; 6]);
let right = tensor(&[2, 5], &[1.0; 10]);
let mut ctx = Ctx::training();
assert!(
Add::new()
.forward_many_mut(&[&left, &right], &mut ctx)
.is_err()
);
assert_eq!(ctx.pending_caches(), 0, "a refused pass parks no cache");
}
#[test]
fn a_second_build_for_other_shapes_is_refused() {
let mut layer = Add::new();
layer
.build_many(&[Shape::known(&[2, 3]), Shape::known(&[2, 1])])
.unwrap();
layer
.build_many(&[Shape::known(&[8, 3]), Shape::known(&[8, 1])])
.unwrap();
let message = layer
.build_many(&[Shape::known(&[2, 5]), Shape::known(&[2, 1])])
.unwrap_err()
.to_string();
assert!(message.contains("already built"), "{message}");
}
#[test]
fn the_layer_takes_one_input_at_least() {
let mut layer = Add::new();
assert_eq!(layer.arity(), Arity::AtLeast(1));
let message = layer.build_many(&[]).unwrap_err().to_string();
assert!(message.contains("Add"), "{message}");
assert!(message.contains("1 or more"), "{message}");
assert!(layer.compute_output_shape_many(&[]).is_err());
let mut ctx = Ctx::training();
assert!(layer.forward_many(&[], &mut ctx).is_err());
assert_eq!(ctx.pending_caches(), 0, "a refused pass parks no cache");
}
#[test]
fn a_gradient_of_another_shape_is_refused() {
let input = tensor(&[2, 3], &[1.0; 6]);
let mut layer = Add::new();
let mut ctx = Ctx::training();
layer.forward_many_mut(&[&input, &input], &mut ctx).unwrap();
let message = layer
.backward_many(&tensor(&[2, 4], &[1.0; 8]), &mut ctx)
.unwrap_err()
.to_string();
assert!(message.contains("[2, 3]"), "{message}");
assert!(message.contains("[2, 4]"), "{message}");
}
#[test]
fn an_inference_pass_parks_no_cache() {
let input = tensor(&[2, 3], &[1.0; 6]);
let mut layer = Add::new();
let mut ctx = Ctx::inference();
let output = layer.forward_many_mut(&[&input, &input], &mut ctx).unwrap();
assert_eq!(output, tensor(&[2, 3], &[2.0; 6]));
assert_eq!(ctx.pending_caches(), 0);
}
#[test]
fn the_layer_holds_no_array_and_reports_every_build_shape() {
let mut layer = Add::new();
assert_eq!(layer.param_count(), ParamCounts::none());
assert!(layer.weights().is_empty());
assert!(layer.weights_mut().is_empty());
assert!(!layer.is_built());
assert!(layer.build_config().is_none());
assert_eq!(layer.output_shape(), "Unknown");
layer
.build_many(&[Shape::known(&[2, 3]), Shape::known(&[2, 1])])
.unwrap();
assert!(layer.is_built());
assert_eq!(
layer.known_input_shapes(),
Some(vec![
Shape::with_free_batch(&[2, 3]),
Shape::with_free_batch(&[2, 1]),
]),
"every reported shape carries a free batch axis"
);
assert_eq!(layer.output_shape(), "(None, 3)");
assert_eq!(
layer.build_config().unwrap().input_shapes,
vec![
Shape::with_free_batch(&[2, 3]),
Shape::with_free_batch(&[2, 1]),
]
);
}
}