use crate::error::Error;
use crate::neural_network::layers::ParamCounts;
use crate::neural_network::layers::activation::{Activation, ActivationCache};
use crate::neural_network::layers::convolution::PaddingType;
use crate::neural_network::layers::convolution::conv_transpose_engine::{
conv_transpose_backward, conv_transpose_forward, transpose_output_length,
};
use crate::neural_network::layers::convolution::validation::{
validate_dilation, validate_filters, validate_kernel_size_2d,
validate_stride_dilation_exclusive, validate_strides_2d, validate_transpose_input_shape,
};
use crate::neural_network::layers::validation::{
start_build, validate_built_input, validate_optional_weight, validate_weight_shape,
};
use crate::neural_network::layers::{built_layer_shape_functions, named_weight_layer_functions};
use crate::neural_network::traits::{LayerBase, ParamRef, UnaryLayer};
use crate::neural_network::{Ctx, Fans, Initializer, Shape, Tensor};
use ndarray::{Array1, Array4};
#[derive(Debug)]
pub struct Conv2DTranspose {
filters: usize,
kernel_size: (usize, usize),
strides: (usize, usize),
dilation_rate: (usize, usize),
padding: PaddingType,
weights: Array4<f32>,
bias: Array1<f32>,
activation: Activation,
built: Option<Shape>,
channels: usize,
random_state: Option<u64>,
use_bias: bool,
}
impl Conv2DTranspose {
pub fn new(
filters: usize,
kernel_size: (usize, usize),
strides: (usize, usize),
activation: impl Into<Activation>,
) -> Result<Self, Error> {
validate_filters(filters)?;
validate_kernel_size_2d(kernel_size)?;
validate_strides_2d(strides)?;
let activation = activation.into();
activation.validate()?;
Ok(Conv2DTranspose {
filters,
kernel_size,
strides,
dilation_rate: (1, 1),
padding: PaddingType::Valid,
weights: Array4::zeros((0, 0, 0, 0)),
bias: Array1::zeros(0),
activation,
built: None,
channels: 0,
random_state: None,
use_bias: true,
})
}
pub fn with_padding(mut self, padding: PaddingType) -> Self {
self.padding = padding;
self
}
pub fn with_dilation_rate(mut self, dilation_rate: (usize, usize)) -> Result<Self, Error> {
let dilation = [dilation_rate.0, dilation_rate.1];
validate_dilation(&dilation)?;
validate_stride_dilation_exclusive(&[self.strides.0, self.strides.1], &dilation)?;
self.dilation_rate = dilation_rate;
Ok(self)
}
pub fn with_random_state(mut self, random_state: u64) -> Self {
self.random_state = Some(random_state);
if self.built.is_some() {
self.draw_parameters();
}
self
}
fn draw_parameters(&mut self) {
let mut rng = crate::random::make_rng(self.random_state);
self.weights = Initializer::GlorotUniform.draw(
(
self.kernel_size.0,
self.kernel_size.1,
self.filters,
self.channels,
),
Fans::conv(
self.channels,
self.filters,
self.kernel_size.0 * self.kernel_size.1,
),
&mut rng,
);
self.bias = Array1::zeros(self.filters);
}
fn calculate_output_shape(&self, input_shape: &[usize]) -> Vec<usize> {
vec![
input_shape[0],
transpose_output_length(
input_shape[1],
self.kernel_size.0,
self.strides.0,
self.dilation_rate.0,
self.padding,
),
transpose_output_length(
input_shape[2],
self.kernel_size.1,
self.strides.1,
self.dilation_rate.1,
self.padding,
),
self.filters,
]
}
pub fn with_use_bias(mut self, use_bias: bool) -> Self {
self.use_bias = use_bias;
self
}
pub fn set_weights(
&mut self,
weights: Array4<f32>,
bias: impl Into<Option<Array1<f32>>>,
) -> Result<(), Error> {
if self.built.is_none() {
return Err(Error::not_built("Conv2DTranspose"));
}
validate_weight_shape("kernel", self.weights.shape(), weights.shape())?;
let bias = validate_optional_weight("bias", "use_bias", self.use_bias, bias.into())?;
if let Some(bias) = bias.as_ref() {
validate_weight_shape("bias", self.bias.shape(), bias.shape())?;
}
self.weights = weights;
if let Some(bias) = bias {
self.bias = bias;
}
Ok(())
}
}
struct Conv2DTransposeCache {
input: Tensor,
activation: ActivationCache,
}
impl LayerBase for Conv2DTranspose {
fn layer_type(&self) -> &str {
"Conv2DTranspose"
}
fn param_count(&self) -> ParamCounts {
let bias = if self.use_bias { self.bias.len() } else { 0 };
ParamCounts::trainable(self.weights.len() + bias)
}
fn parameters_mut(&mut self) -> Vec<ParamRef<'_>> {
let Self {
weights,
bias,
use_bias,
..
} = self;
let mut params = vec![ParamRef::weight(
"kernel",
weights.as_slice_mut().expect("weights must be contiguous"),
)];
if *use_bias {
params.push(ParamRef::no_decay(
"bias",
bias.as_slice_mut().expect("bias must be contiguous"),
));
}
params
}
built_layer_shape_functions!();
named_weight_layer_functions!(
trainable "kernel" => weights,
trainable "bias" => bias if use_bias,
);
}
impl UnaryLayer for Conv2DTranspose {
fn build(&mut self, input: &Shape) -> Result<(), Error> {
let Some(built) = start_build(&self.built, "Conv2DTranspose", input)? else {
return Ok(());
};
built.check_rank("Conv2DTranspose", 4)?;
let (batch, tail) = built.split_batch("Conv2DTranspose")?;
let mut dims = vec![batch.unwrap_or(1)];
dims.extend(tail);
validate_transpose_input_shape(&dims, 2, "[batch_size, height, width, channels]")?;
self.channels = dims[3];
self.built = Some(built);
self.draw_parameters();
Ok(())
}
fn forward(&self, input: &Tensor, ctx: &mut Ctx) -> Result<Tensor, Error> {
validate_built_input(&self.built, "Conv2DTranspose", input.shape())?;
let output = conv_transpose_forward(
input,
self.weights.as_slice().expect("weights must be contiguous"),
self.weights.shape(),
self.use_bias
.then(|| self.bias.as_slice().expect("bias must be contiguous")),
&[self.strides.0, self.strides.1],
&[self.dilation_rate.0, self.dilation_rate.1],
self.padding,
)?;
if !ctx.is_training() {
return self.activation.forward(&output);
}
let (activated, activation) = self.activation.forward_train(output)?;
ctx.push_cache(
"Conv2DTranspose",
Conv2DTransposeCache {
input: input.clone(),
activation,
},
);
Ok(activated)
}
fn backward(&self, grad_output: &Tensor, ctx: &mut Ctx) -> Result<Tensor, Error> {
let cache: Conv2DTransposeCache = ctx.pop_cache("Conv2DTranspose")?;
let grad_upstream = self.activation.backward(&cache.activation, grad_output)?;
let grads = conv_transpose_backward(
&grad_upstream,
&cache.input,
self.weights.as_slice().expect("weights must be contiguous"),
self.weights.shape(),
&[self.strides.0, self.strides.1],
&[self.dilation_rate.0, self.dilation_rate.1],
self.padding,
)?;
ctx.add_grad(
"kernel",
Array4::from_shape_vec(self.weights.raw_dim(), grads.weight_grad)
.expect("weight gradient shape matches weights")
.into_dyn(),
)?;
if self.use_bias {
ctx.add_grad(
"bias",
Array1::from_shape_vec(self.bias.raw_dim(), grads.bias_grad)
.expect("bias gradient shape matches bias")
.into_dyn(),
)?;
}
Ok(grads.input_grad)
}
fn compute_output_shape(&self, input: &Shape) -> Result<Shape, Error> {
input.check_rank("Conv2DTranspose", 4)?;
let (batch, tail) = input.split_batch("Conv2DTranspose")?;
let mut dims = vec![0];
dims.extend(tail);
Ok(Shape::from_batch(
batch,
&self.calculate_output_shape(&dims)[1..],
))
}
}