use crate::{Shape, Tape, Tensor};
use crate::Module;
use super::{Conv2d, conv2d};
#[test]
fn conv2d_matches_the_hand_computed_fixture() {
let tape: Tape<f64> = Tape::new();
let input = tape.leaf(Tensor::new(
[1, 1, 3, 3],
(1..=9).map(|v| v as f64).collect::<Vec<_>>(),
));
let weights = tape.leaf(Tensor::new([1, 1, 2, 2], [1.0, 0.0, 0.0, 1.0]));
let bias = tape.leaf(Tensor::new([1], [10.0]));
let output = conv2d(input, weights, bias, 1, 0);
assert_eq!(output.shape(), Shape::new([1, 1, 2, 2]));
let output = output.symbol();
let network = tape.into_network();
let run = network.forward(&network.parameters(), []);
assert_eq!(run.of(output).to_vec(), &[16.0, 18.0, 22.0, 24.0]);
}
#[test]
fn conv2d_mixes_channels_into_filters() {
let tape: Tape<f64> = Tape::new();
let input = tape.leaf(Tensor::new(
[1, 2, 2, 2],
[1.0, 2.0, 3.0, 4.0, 10.0, 20.0, 30.0, 40.0],
));
let weights = tape.leaf(Tensor::new([2, 2, 1, 1], [1.0, 1.0, 2.0, 3.0]));
let bias = tape.leaf(Tensor::filled([2], 0.0));
let output = conv2d(input, weights, bias, 1, 0);
assert_eq!(output.shape(), Shape::new([1, 2, 2, 2]));
let output = output.symbol();
let network = tape.into_network();
let run = network.forward(&network.parameters(), []);
assert_eq!(
run.of(output).to_vec(),
&[11.0, 22.0, 33.0, 44.0, 32.0, 64.0, 96.0, 128.0]
);
}
#[test]
fn conv2d_strides_the_windows() {
let tape: Tape<f64> = Tape::new();
let input = tape.leaf(Tensor::new([1, 1, 1, 5], [1.0, 2.0, 3.0, 4.0, 5.0]));
let weights = tape.leaf(Tensor::new([1, 1, 1, 2], [1.0, 1.0]));
let bias = tape.leaf(Tensor::filled([1], 0.0));
let output = conv2d(input, weights, bias, 2, 0);
assert_eq!(output.shape(), Shape::new([1, 1, 1, 2]));
let output = output.symbol();
let network = tape.into_network();
let run = network.forward(&network.parameters(), []);
assert_eq!(run.of(output).to_vec(), &[3.0, 7.0]);
}
#[test]
fn padding_surrounds_the_input_with_zeros() {
let tape: Tape<f64> = Tape::new();
let input = tape.leaf(Tensor::new([1, 1, 2, 2], [1.0, 2.0, 3.0, 4.0]));
let weights = tape.leaf(Tensor::filled([1, 1, 3, 3], 1.0));
let bias = tape.leaf(Tensor::filled([1], 0.0));
let output = conv2d(input, weights, bias, 1, 1);
assert_eq!(output.shape(), Shape::new([1, 1, 2, 2]));
let output = output.symbol();
let network = tape.into_network();
let run = network.forward(&network.parameters(), []);
assert_eq!(run.of(output).to_vec(), &[10.0; 4]);
}
#[test]
fn conv2d_gradients_flow_to_every_operand() {
let tape: Tape<f64> = Tape::new();
let input = tape.leaf(Tensor::new(
[1, 1, 3, 3],
(1..=9).map(|v| v as f64).collect::<Vec<_>>(),
));
let weights = tape.leaf(Tensor::filled([1, 1, 2, 2], 1.0));
let bias = tape.leaf(Tensor::filled([1], 0.0));
let output = conv2d(input, weights, bias, 1, 0);
let loss = output.sum();
let (input, weights, bias, loss) = (
input.symbol(),
weights.symbol(),
bias.symbol(),
loss.symbol(),
);
let network = tape.into_network();
let run = network.forward(&network.parameters(), []);
let gradients = run.backward(loss);
assert_eq!(
gradients.of(input).to_vec(),
&[1.0, 2.0, 1.0, 2.0, 4.0, 2.0, 1.0, 2.0, 1.0]
);
assert_eq!(gradients.of(weights).to_vec(), &[12.0, 16.0, 24.0, 28.0]);
assert_eq!(gradients.of(bias).to_vec(), &[4.0]);
}
#[test]
#[should_panic(expected = "disagree on channels")]
fn conv2d_rejects_mismatched_channels() {
let tape: Tape<f64> = Tape::new();
let input = tape.leaf(Tensor::filled([1, 2, 3, 3], 0.0));
let weights = tape.leaf(Tensor::filled([1, 3, 2, 2], 0.0));
let bias = tape.leaf(Tensor::filled([1], 0.0));
conv2d(input, weights, bias, 1, 0);
}
#[test]
fn new_allocates_weights_and_bias() {
let tape = Tape::new();
let layer = Conv2d::new(
&tape,
Tensor::filled([4, 2, 3, 3], 0.0_f64),
Tensor::filled([4], 0.0),
1,
1,
);
assert_eq!(tape.len(), 2);
assert_eq!(layer.parameters().count(), 2);
}
#[test]
#[should_panic(expected = "must be rank 4")]
fn new_rejects_non_stack_weights() {
let tape = Tape::new();
Conv2d::new(
&tape,
Tensor::filled([4, 2, 3], 0.0_f64),
Tensor::filled([4], 0.0),
1,
0,
);
}
#[test]
#[should_panic(expected = "disagree on filters")]
fn new_rejects_mismatched_bias() {
let tape = Tape::new();
Conv2d::new(
&tape,
Tensor::filled([4, 2, 3, 3], 0.0_f64),
Tensor::filled([3], 0.0),
1,
0,
);
}
#[test]
#[should_panic(expected = "stride must be positive")]
fn new_rejects_a_zero_stride() {
let tape = Tape::new();
Conv2d::new(
&tape,
Tensor::filled([4, 2, 3, 3], 0.0_f64),
Tensor::filled([4], 0.0),
0,
0,
);
}
#[test]
fn express_records_tensor_granularity() {
let tape = Tape::new();
let layer = Conv2d::new(
&tape,
Tensor::new([1, 1, 2, 2], [1.0, 0.0, 0.0, 1.0]),
Tensor::new([1], [10.0]),
1,
1,
);
let input = tape.leaf(Tensor::new(
[1, 1, 3, 3],
(1..=9).map(|v| v as f64).collect::<Vec<_>>(),
));
let nodes_before = tape.len();
let output = layer.express(input);
assert_eq!(tape.len(), nodes_before + 13);
assert_eq!(output.shape(), Shape::new([1, 1, 4, 4]));
}