use std::marker::PhantomData;
use static_assertions::assert_impl_all;
use crate::{Element, Symbol, Tape, Tensor, Value};
use super::{Module, Visitor};
assert_impl_all!(Conv2d<f64>: Send, Sync);
pub fn conv2d<'tape, E: Element>(
input: Value<'tape, E>,
weights: Value<'tape, E>,
bias: Value<'tape, E>,
stride: usize,
padding: usize,
) -> Value<'tape, E> {
let input_shape = input.shape();
let weights_shape = weights.shape();
let bias_shape = bias.shape();
assert_eq!(
input_shape.rank(),
4,
"conv2d input must be rank 4 [batch, channels, height, width], got {input_shape}"
);
assert_eq!(
weights_shape.rank(),
4,
"conv2d weights must be rank 4 [filters, channels, kernel_height, kernel_width], \
got {weights_shape}"
);
assert_eq!(
input_shape.axes()[1],
weights_shape.axes()[1],
"conv2d input {input_shape} and weights {weights_shape} disagree on channels"
);
assert_eq!(
bias_shape.rank(),
1,
"conv2d bias must be rank 1 [filters], got {bias_shape}"
);
assert_eq!(
bias_shape.axes()[0],
weights_shape.axes()[0],
"conv2d bias {bias_shape} and weights {weights_shape} disagree on filters"
);
assert!(stride > 0, "conv2d stride must be positive");
let batch = input_shape.axes()[0];
let channels = input_shape.axes()[1];
let height = input_shape.axes()[2];
let width = input_shape.axes()[3];
let filters = weights_shape.axes()[0];
let kernel_height = weights_shape.axes()[2];
let kernel_width = weights_shape.axes()[3];
let mut padded = input;
if padding > 0 {
padded = padded.pad(2, padding, height + 2 * padding);
padded = padded.pad(3, padding, width + 2 * padding);
}
let windows = padded
.unfold(2, kernel_height, stride, 1)
.unfold(4, kernel_width, stride, 1);
let windows_shape = windows.shape();
let out_height = windows_shape.axes()[2];
let out_width = windows_shape.axes()[4];
let patches = windows.permute([0, 2, 4, 1, 3, 5]).reshape([
batch * out_height * out_width,
channels * kernel_height * kernel_width,
]);
let kernel = weights
.permute([1, 2, 3, 0])
.reshape([channels * kernel_height * kernel_width, filters]);
let product = patches.matmul(kernel);
let shifted = product + bias.broadcast_along_like(0, product);
shifted
.reshape([batch, out_height, out_width, filters])
.permute([0, 3, 1, 2])
}
#[derive(Debug, Clone)]
pub struct Conv2d<E> {
weights: Symbol,
bias: Symbol,
stride: usize,
padding: usize,
_marker: PhantomData<E>,
}
impl<E: Element> Conv2d<E> {
pub fn new(
tape: &Tape<E>,
weights: Tensor<E>,
bias: Tensor<E>,
stride: usize,
padding: usize,
) -> Self {
let weights_shape = weights.shape();
let bias_shape = bias.shape();
assert_eq!(
weights_shape.rank(),
4,
"conv2d weights must be rank 4 [filters, channels, kernel_height, kernel_width], \
got {weights_shape}"
);
assert_eq!(
bias_shape.rank(),
1,
"conv2d bias must be rank 1 [filters], got {bias_shape}"
);
assert_eq!(
bias_shape.axes()[0],
weights_shape.axes()[0],
"conv2d bias {bias_shape} and weights {weights_shape} disagree on filters"
);
assert!(stride > 0, "conv2d stride must be positive");
Self {
weights: tape.parameter(weights).symbol(),
bias: tape.parameter(bias).symbol(),
stride,
padding,
_marker: PhantomData,
}
}
pub fn parameters(&self) -> impl Iterator<Item = Symbol> + '_ {
super::parameters(self).into_iter()
}
}
#[cfg(test)]
#[path = "tests/convolution_tests.rs"]
mod tests;
impl<E: Element> Conv2d<E> {
pub fn weights(&self) -> Symbol {
self.weights
}
pub fn bias(&self) -> Symbol {
self.bias
}
}
impl<E: Element> Module<E> for Conv2d<E> {
fn express<'tape>(&self, input: Value<'tape, E>) -> Value<'tape, E> {
let tape = input.tape();
let weights = tape.resolve(self.weights);
let bias = tape.resolve(self.bias);
conv2d(input, weights, bias, self.stride, self.padding)
}
fn visit(&self, visitor: &mut dyn Visitor) {
visitor.parameter("weights", self.weights);
visitor.parameter("bias", self.bias);
}
}