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 Subtract {
built: Option<Vec<Shape>>,
}
impl Subtract {
pub fn new() -> Self {
Self::default()
}
}
struct SubtractCache {
first: Vec<usize>,
second: Vec<usize>,
output: Vec<usize>,
}
impl LayerBase for Subtract {
fn layer_type(&self) -> &str {
"Subtract"
}
merge_layer_base_functions!();
}
impl Layer for Subtract {
elementwise_merge_layer_functions!("Subtract", Arity::Exactly(2));
fn forward_many(&self, inputs: &[&Tensor], ctx: &mut Ctx) -> Result<Tensor, Error> {
Arity::Exactly(2).check("Subtract", inputs.len())?;
let dims = merged_dims("Subtract", inputs)?;
let mut output = broadcast_input("Subtract", inputs[0], &dims)?;
let second = broadcast_input("Subtract", inputs[1], &dims)?;
output -= &second;
if ctx.is_training() {
ctx.push_cache(
"Subtract",
SubtractCache {
first: inputs[0].shape().to_vec(),
second: inputs[1].shape().to_vec(),
output: dims,
},
);
}
Ok(output)
}
fn backward_many(&self, grad_output: &Tensor, ctx: &mut Ctx) -> Result<Vec<Tensor>, Error> {
let cache: SubtractCache = ctx.pop_cache("Subtract")?;
if grad_output.shape() != cache.output.as_slice() {
return Err(Error::shape_mismatch(cache.output, grad_output.shape()));
}
let negated = -grad_output;
Ok(vec![
reduce_to(grad_output, &cache.first),
reduce_to(&negated, &cache.second),
])
}
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::IxDyn;
fn tensor(dims: &[usize], values: Vec<f32>) -> Tensor {
Tensor::from_shape_vec(IxDyn(dims), values).expect("the value count fits the extents")
}
#[test]
fn forward_subtracts_the_second_input_from_the_first() {
let first = tensor(&[2, 3], vec![5.0, 6.0, 7.0, 8.0, 9.0, 10.0]);
let second = tensor(&[2, 3], vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
let mut layer = Subtract::new();
let mut ctx = Ctx::training();
let output = layer
.forward_many_mut(&[&first, &second], &mut ctx)
.unwrap();
assert_eq!(output, tensor(&[2, 3], vec![4.0, 4.0, 4.0, 4.0, 4.0, 4.0]));
assert!(output.is_standard_layout(), "a layer emits the C order");
assert_eq!(layer.layer_type(), "Subtract");
assert_eq!(layer.param_count(), ParamCounts::none());
}
#[test]
fn the_order_of_the_two_inputs_decides_the_sign() {
let first = tensor(&[1, 2], vec![1.0, 2.0]);
let second = tensor(&[1, 2], vec![4.0, 8.0]);
let mut ctx = Ctx::inference();
let forward = Subtract::new()
.forward_many_mut(&[&first, &second], &mut ctx)
.unwrap();
let reversed = Subtract::new()
.forward_many_mut(&[&second, &first], &mut ctx)
.unwrap();
assert_eq!(forward, tensor(&[1, 2], vec![-3.0, -6.0]));
assert_eq!(reversed, tensor(&[1, 2], vec![3.0, 6.0]));
assert_eq!(ctx.pending_caches(), 0);
}
#[test]
fn backward_negates_the_gradient_of_the_second_input() {
let first = tensor(&[2, 2], vec![1.0, 2.0, 3.0, 4.0]);
let second = tensor(&[2, 2], vec![4.0, 3.0, 2.0, 1.0]);
let mut layer = Subtract::new();
let mut ctx = Ctx::training();
layer
.forward_many_mut(&[&first, &second], &mut ctx)
.unwrap();
let grad = tensor(&[2, 2], vec![1.0, 2.0, 3.0, 4.0]);
let grads = layer.backward_many(&grad, &mut ctx).unwrap();
assert_eq!(grads.len(), 2, "1 gradient per input");
assert_eq!(grads[0], grad);
assert_eq!(grads[1], tensor(&[2, 2], vec![-1.0, -2.0, -3.0, -4.0]));
assert_eq!(ctx.pending_caches(), 0, "the backward pass took the cache");
}
#[test]
fn an_axis_of_one_position_broadcasts_and_sums_back() {
let first = tensor(&[2, 3], vec![5.0, 6.0, 7.0, 8.0, 9.0, 10.0]);
let second = tensor(&[2, 1], vec![1.0, 2.0]);
let mut layer = Subtract::new();
let mut ctx = Ctx::training();
let output = layer
.forward_many_mut(&[&first, &second], &mut ctx)
.unwrap();
assert_eq!(output, tensor(&[2, 3], vec![4.0, 5.0, 6.0, 6.0, 7.0, 8.0]));
let grad = tensor(&[2, 3], vec![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], vec![-6.0, -15.0]));
}
#[test]
fn rank_alignment_inserts_after_the_batch_axis() {
let first = tensor(
&[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,
],
);
let second = tensor(&[2, 3], vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
let mut layer = Subtract::new();
let mut ctx = Ctx::training();
let output = layer
.forward_many_mut(&[&first, &second], &mut ctx)
.unwrap();
assert_eq!(
output,
tensor(
&[2, 2, 3],
vec![0.0, 0.0, 0.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 6.0, 6.0, 6.0],
)
);
let grad = Tensor::ones([2, 2, 3].as_slice());
let grads = layer.backward_many(&grad, &mut ctx).unwrap();
assert_eq!(grads[0], grad);
assert_eq!(
grads[1],
tensor(&[2, 3], vec![-2.0, -2.0, -2.0, -2.0, -2.0, -2.0]),
"the inserted axis of 2 positions sums away"
);
}
#[test]
fn the_layer_reports_the_shapes_it_built_for() {
let mut layer = Subtract::new();
assert!(!layer.is_built());
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]),
])
);
assert_eq!(layer.output_shape(), "(None, 3)");
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("Subtract"), "{message}");
assert!(message.contains("already built"), "{message}");
}
#[test]
fn the_batch_axis_does_not_broadcast() {
let mut layer = Subtract::new();
let message = layer
.build_many(&[Shape::known(&[2, 3]), Shape::known(&[1, 3])])
.unwrap_err()
.to_string();
assert!(message.contains("Subtract"), "{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 records nothing");
let first = tensor(&[2, 3], vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
let second = tensor(&[1, 3], vec![1.0, 2.0, 3.0]);
let mut ctx = Ctx::training();
let message = Subtract::new()
.forward_many_mut(&[&first, &second], &mut ctx)
.unwrap_err()
.to_string();
assert!(message.contains("batch axis"), "{message}");
}
#[test]
fn an_axis_that_cannot_broadcast_is_refused() {
let shapes = [Shape::known(&[2, 3]), Shape::known(&[2, 5])];
let message = Subtract::new()
.compute_output_shape_many(&shapes)
.unwrap_err()
.to_string();
assert!(message.contains("Subtract"), "{message}");
assert!(message.contains("Axis 1"), "{message}");
assert!(message.contains("neither extent is 1"), "{message}");
let first = tensor(&[2, 3], vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
let second = tensor(&[2, 5], vec![0.0; 10]);
let mut ctx = Ctx::training();
assert!(
Subtract::new()
.forward_many_mut(&[&first, &second], &mut ctx)
.is_err()
);
}
#[test]
fn the_layer_takes_exactly_two_inputs() {
assert_eq!(Subtract::new().arity(), Arity::Exactly(2));
let message = Subtract::new()
.compute_output_shape_many(&[Shape::known(&[2, 3])])
.unwrap_err()
.to_string();
assert!(message.contains("Subtract"), "{message}");
assert!(message.contains("exactly 2"), "{message}");
assert!(message.contains("received 1"), "{message}");
let mut layer = Subtract::new();
let message = layer
.build_many(&[
Shape::known(&[2, 3]),
Shape::known(&[2, 3]),
Shape::known(&[2, 3]),
])
.unwrap_err()
.to_string();
assert!(message.contains("exactly 2"), "{message}");
assert!(message.contains("received 3"), "{message}");
let single = tensor(&[2, 3], vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
let mut ctx = Ctx::training();
let message = Subtract::new()
.forward_many(&[&single], &mut ctx)
.unwrap_err()
.to_string();
assert!(message.contains("received 1"), "{message}");
}
#[test]
fn a_gradient_of_another_shape_is_refused() {
let first = tensor(&[2, 2], vec![1.0, 2.0, 3.0, 4.0]);
let second = tensor(&[2, 2], vec![1.0, 1.0, 1.0, 1.0]);
let mut layer = Subtract::new();
let mut ctx = Ctx::training();
layer
.forward_many_mut(&[&first, &second], &mut ctx)
.unwrap();
let grad = tensor(&[2, 3], vec![1.0, 1.0, 1.0, 1.0, 1.0, 1.0]);
assert!(layer.backward_many(&grad, &mut ctx).is_err());
}
#[test]
fn a_backward_pass_without_a_forward_pass_is_refused() {
let mut ctx = Ctx::training();
let grad = tensor(&[2, 2], vec![1.0, 1.0, 1.0, 1.0]);
let message = Subtract::new()
.backward_many(&grad, &mut ctx)
.unwrap_err()
.to_string();
assert!(
message.contains("forward pass has not been run"),
"{message}"
);
}
}