use crate::error::Error;
use crate::neural_network::Tensor;
use crate::neural_network::layers::TrainingParameters;
use crate::neural_network::layers::activation::Activation;
use crate::neural_network::layers::convolution::PaddingType;
use crate::neural_network::layers::convolution::convolution_engine::{conv_backward, conv_forward};
use crate::neural_network::layers::convolution::validation::{
validate_filters, validate_input_shape_3d, validate_kernel_size_3d, validate_strides_3d,
};
use crate::neural_network::layers::layer_weight::{Conv3DLayerWeight, LayerWeight};
use crate::neural_network::layers::validation::validate_weight_shape;
use crate::neural_network::traits::{Layer, ParamGrad};
use ndarray::{Array2, Array5};
use ndarray_rand::{RandomExt, rand_distr::Uniform};
use std::borrow::Cow;
#[derive(Debug)]
pub struct Conv3D {
filters: usize,
kernel_size: (usize, usize, usize),
strides: (usize, usize, usize),
padding: PaddingType,
weights: Array5<f32>,
bias: Array2<f32>,
activation: Activation,
output_cache: Option<Tensor>,
input_cache: Option<Tensor>,
input_shape: Vec<usize>,
weight_gradients: Option<Array5<f32>>,
bias_gradients: Option<Array2<f32>>,
}
impl Conv3D {
pub fn new(
filters: usize,
kernel_size: (usize, usize, usize),
input_shape: Vec<usize>,
strides: (usize, usize, usize),
activation: impl Into<Activation>,
) -> Result<Self, Error> {
validate_filters(filters)?;
validate_kernel_size_3d(kernel_size)?;
validate_strides_3d(strides)?;
validate_input_shape_3d(&input_shape, kernel_size)?;
let channels = input_shape[1];
let weights = Self::init_weights_array(filters, channels, kernel_size, None);
let bias = Array2::zeros((1, filters));
Ok(Conv3D {
filters,
kernel_size,
strides,
padding: PaddingType::Valid,
weights,
bias,
activation: activation.into(),
output_cache: None,
input_cache: None,
input_shape,
weight_gradients: None,
bias_gradients: None,
})
}
pub fn with_padding(mut self, padding: PaddingType) -> Self {
self.padding = padding;
self
}
pub fn with_random_state(mut self, random_state: u64) -> Self {
let channels = self.input_shape[1];
self.weights =
Self::init_weights_array(self.filters, channels, self.kernel_size, Some(random_state));
self
}
fn init_weights_array(
filters: usize,
channels: usize,
kernel_size: (usize, usize, usize),
random_state: Option<u64>,
) -> Array5<f32> {
let (kd, kh, kw) = kernel_size;
let fan_in = channels * kd * kh * kw;
let fan_out = filters * kd * kh * kw;
let limit = (6.0 / (fan_in + fan_out) as f32).sqrt();
let mut rng = crate::random::make_rng(random_state);
Array5::random_using(
(filters, channels, kd, kh, kw),
Uniform::new(-limit, limit).unwrap(),
&mut rng,
)
}
fn calculate_output_shape(&self, input_shape: &[usize]) -> Vec<usize> {
let (batch_size, _, depth, height, width) = (
input_shape[0],
input_shape[1],
input_shape[2],
input_shape[3],
input_shape[4],
);
let (kd, kh, kw) = self.kernel_size;
let (sd, sh, sw) = self.strides;
let (output_depth, output_height, output_width) = match self.padding {
PaddingType::Valid => (
(depth - kd) / sd + 1,
(height - kh) / sh + 1,
(width - kw) / sw + 1,
),
PaddingType::Same => (depth.div_ceil(sd), height.div_ceil(sh), width.div_ceil(sw)),
};
vec![
batch_size,
self.filters,
output_depth,
output_height,
output_width,
]
}
pub fn set_weights(&mut self, weights: Array5<f32>, bias: Array2<f32>) -> Result<(), Error> {
validate_weight_shape("weight", self.weights.shape(), weights.shape())?;
validate_weight_shape("bias", self.bias.shape(), bias.shape())?;
self.weights = weights;
self.bias = bias;
Ok(())
}
}
impl Layer for Conv3D {
fn forward(&mut self, input: &Tensor) -> Result<Tensor, Error> {
if input.ndim() != 5 {
return Err(Error::invalid_input("input tensor is not 5D"));
}
self.input_cache = Some(input.clone());
let output = conv_forward(
input,
self.weights.as_slice().expect("weights must be contiguous"),
self.weights.shape(),
self.bias.as_slice().expect("bias must be contiguous"),
&[self.strides.0, self.strides.1, self.strides.2],
self.padding,
)?;
let activated = self.activation.forward(&output)?;
self.output_cache = Some(activated.clone());
Ok(activated)
}
fn predict(&self, input: &Tensor) -> Result<Tensor, Error> {
if input.ndim() != 5 {
return Err(Error::invalid_input("input tensor is not 5D"));
}
let output = conv_forward(
input,
self.weights.as_slice().expect("weights must be contiguous"),
self.weights.shape(),
self.bias.as_slice().expect("bias must be contiguous"),
&[self.strides.0, self.strides.1, self.strides.2],
self.padding,
)?;
let activated = self.activation.forward(&output)?;
Ok(activated)
}
fn backward(&mut self, grad_output: &Tensor) -> Result<Tensor, Error> {
let activated = self
.output_cache
.take()
.ok_or_else(|| Error::forward_pass_not_run("Conv3D"))?;
let grad_upstream = self.activation.backward(&activated, grad_output)?;
let input = self
.input_cache
.as_ref()
.ok_or_else(|| Error::forward_pass_not_run("Conv3D"))?;
let grads = conv_backward(
&grad_upstream,
input,
self.weights.as_slice().expect("weights must be contiguous"),
self.weights.shape(),
&[self.strides.0, self.strides.1, self.strides.2],
self.padding,
)?;
self.weight_gradients = Some(
Array5::from_shape_vec(self.weights.raw_dim(), grads.weight_grad)
.expect("weight gradient shape matches weights"),
);
self.bias_gradients = Some(
Array2::from_shape_vec(self.bias.raw_dim(), grads.bias_grad)
.expect("bias gradient shape matches bias"),
);
Ok(grads.input_grad)
}
fn layer_type(&self) -> &str {
"Conv3D"
}
fn output_shape(&self) -> String {
let output_shape = self.calculate_output_shape(&self.input_shape);
format!(
"({}, {}, {}, {}, {})",
output_shape[0], output_shape[1], output_shape[2], output_shape[3], output_shape[4]
)
}
fn param_count(&self) -> TrainingParameters {
TrainingParameters::Trainable(self.weights.len() + self.bias.len())
}
fn parameters(&mut self) -> Vec<ParamGrad<'_>> {
let Self {
weights,
bias,
weight_gradients,
bias_gradients,
..
} = self;
let mut params = Vec::new();
if let (Some(grad_a), Some(grad_b)) = (weight_gradients.as_ref(), bias_gradients.as_ref()) {
params.push(ParamGrad::weight(
weights.as_slice_mut().expect("weights must be contiguous"),
grad_a
.as_slice()
.expect("weight_gradients must be contiguous"),
));
params.push(ParamGrad::no_decay(
bias.as_slice_mut().expect("bias must be contiguous"),
grad_b
.as_slice()
.expect("bias_gradients must be contiguous"),
));
}
params
}
fn get_weights(&self) -> LayerWeight<'_> {
LayerWeight::Conv3D(Conv3DLayerWeight {
weight: Cow::Borrowed(&self.weights),
bias: Cow::Borrowed(&self.bias),
})
}
}