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::conv_op_helpers::{
DepthwiseGeometry, depthwise_backward, depthwise_forward,
};
use crate::neural_network::layers::convolution::PaddingType;
use crate::neural_network::layers::convolution::validation::{
validate_depth_multiplier, validate_input_shape_1d, validate_kernel_size_1d,
validate_strides_1d,
};
use crate::neural_network::layers::layer_weight::{DepthwiseConv1DLayerWeight, LayerWeight};
use crate::neural_network::layers::validation::validate_weight_shape;
use crate::neural_network::traits::{Layer, ParamGrad};
use ndarray::{Array1, Array3};
use ndarray_rand::{RandomExt, rand_distr::Uniform};
use std::borrow::Cow;
#[derive(Debug)]
pub struct DepthwiseConv1D {
channels: usize,
depth_multiplier: usize,
kernel_size: usize,
stride: usize,
padding: PaddingType,
weights: Array3<f32>,
bias: Array1<f32>,
activation: Activation,
output_cache: Option<Tensor>,
input_cache: Option<Tensor>,
input_shape: Vec<usize>,
weight_gradients: Option<Array3<f32>>,
bias_gradients: Option<Array1<f32>>,
}
impl DepthwiseConv1D {
pub fn new(
kernel_size: usize,
input_shape: Vec<usize>,
stride: usize,
activation: impl Into<Activation>,
) -> Result<Self, Error> {
validate_kernel_size_1d(kernel_size)?;
validate_strides_1d(stride)?;
validate_input_shape_1d(&input_shape, kernel_size)?;
let activation = activation.into();
activation.validate()?;
let channels = input_shape[2];
let weights = Self::init_weights_array(channels, 1, kernel_size, None);
let bias = Array1::zeros(channels);
Ok(Self {
channels,
depth_multiplier: 1,
kernel_size,
stride,
padding: PaddingType::Valid,
weights,
bias,
activation,
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_depth_multiplier(mut self, depth_multiplier: usize) -> Result<Self, Error> {
validate_depth_multiplier(depth_multiplier)?;
self.depth_multiplier = depth_multiplier;
self.weights =
Self::init_weights_array(self.channels, depth_multiplier, self.kernel_size, None);
self.bias = Array1::zeros(self.channels * depth_multiplier);
Ok(self)
}
pub fn with_random_state(mut self, random_state: u64) -> Self {
self.weights = Self::init_weights_array(
self.channels,
self.depth_multiplier,
self.kernel_size,
Some(random_state),
);
self
}
fn init_weights_array(
channels: usize,
depth_multiplier: usize,
kernel_size: usize,
random_state: Option<u64>,
) -> Array3<f32> {
let fan_in = channels * kernel_size;
let fan_out = depth_multiplier * kernel_size;
let weight_bound = (6.0 / (fan_in + fan_out) as f32).sqrt();
let mut rng = crate::random::make_rng(random_state);
Array3::random_using(
(kernel_size, channels, depth_multiplier),
Uniform::new(-weight_bound, weight_bound).unwrap(),
&mut rng,
)
}
fn calculate_output_length(&self, input_length: usize) -> usize {
match self.padding {
PaddingType::Valid => (input_length - self.kernel_size) / self.stride + 1,
PaddingType::Same => input_length.div_ceil(self.stride),
}
}
pub fn set_weights(&mut self, weights: Array3<f32>, bias: Array1<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(())
}
fn geometry(&self, input_shape: &[usize]) -> DepthwiseGeometry {
let length = input_shape[1];
let out_length = self.calculate_output_length(length);
let pad = match self.padding {
PaddingType::Valid => 0,
PaddingType::Same => {
((out_length - 1) * self.stride + self.kernel_size).saturating_sub(length)
}
};
DepthwiseGeometry {
input: (1, length),
output: (1, out_length),
channels: self.channels,
depth_multiplier: self.depth_multiplier,
kernel: (1, self.kernel_size),
strides: (1, self.stride),
pad_before: (0, pad / 2),
}
}
fn convolve(&self, input: &Tensor) -> Result<Tensor, Error> {
if input.ndim() != 3 {
return Err(Error::invalid_input("input tensor is not 3D"));
}
let channels = input.shape()[2];
if channels != self.channels {
return Err(Error::dimension_mismatch(self.channels, channels));
}
let g = self.geometry(input.shape());
let batch_size = input.shape()[0];
let input_std = input.as_standard_layout();
let src = input_std
.as_slice()
.expect("standard-layout array is contiguous");
let ker = self.weights.as_slice().expect("weights must be contiguous");
let bias = Some(self.bias.as_slice().expect("bias must be contiguous"));
let mut output = Array3::<f32>::zeros((batch_size, g.output.1, g.out_channels()));
depthwise_forward(
&g,
src,
ker,
bias,
output.as_slice_mut().expect("output is contiguous"),
);
self.activation.forward(&output.into_dyn())
}
}
impl Layer for DepthwiseConv1D {
fn forward(&mut self, input: &Tensor) -> Result<Tensor, Error> {
let activated = self.convolve(input)?;
self.input_cache = Some(input.clone());
self.input_shape = input.shape().to_vec();
self.output_cache = Some(activated.clone());
Ok(activated)
}
fn predict(&self, input: &Tensor) -> Result<Tensor, Error> {
self.convolve(input)
}
fn backward(&mut self, grad_output: &Tensor) -> Result<Tensor, Error> {
let activated = self
.output_cache
.take()
.ok_or_else(|| Error::forward_pass_not_run("DepthwiseConv1D"))?;
let grad_upstream = self.activation.backward(&activated, grad_output)?;
let input = self
.input_cache
.as_ref()
.ok_or_else(|| Error::forward_pass_not_run("DepthwiseConv1D"))?;
let batch_size = input.shape()[0];
let g = self.geometry(input.shape());
let input_std = input.as_standard_layout();
let src = input_std
.as_slice()
.expect("standard-layout array is contiguous");
let grad_std = grad_upstream.as_standard_layout();
let grad = grad_std
.as_slice()
.expect("standard-layout array is contiguous");
let ker = self.weights.as_slice().expect("weights must be contiguous");
let grads = depthwise_backward(&g, src, grad, ker, batch_size);
self.weight_gradients = Some(
Array3::from_shape_vec(self.weights.raw_dim(), grads.weight)
.expect("weight gradient shape matches weights"),
);
self.bias_gradients = Some(Array1::from_vec(grads.bias));
Ok(
Array3::from_shape_vec((batch_size, g.input.1, g.channels), grads.input)
.expect("input gradient shape matches input")
.into_dyn(),
)
}
fn layer_type(&self) -> &str {
"DepthwiseConv1D"
}
fn output_shape(&self) -> String {
let output_length = self.calculate_output_length(self.input_shape[1]);
format!(
"({}, {}, {})",
self.input_shape[0],
output_length,
self.channels * self.depth_multiplier
)
}
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::DepthwiseConv1D(DepthwiseConv1DLayerWeight {
weight: Cow::Borrowed(&self.weights),
bias: Cow::Borrowed(&self.bias),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::neural_network::layers::activation::linear::Linear;
use ndarray::ArrayD;
#[test]
fn depthwise_1d_forward_keeps_channels_separate_hand_derived() {
let mut layer = DepthwiseConv1D::new(2, vec![1, 5, 2], 1, Linear::new()).unwrap();
let weights = Array3::from_shape_vec((2, 2, 1), vec![1.0, 2.0, 1.0, 2.0]).unwrap();
layer.set_weights(weights, Array1::zeros(2)).unwrap();
let input = ArrayD::from_shape_vec(
ndarray::IxDyn(&[1, 5, 2]),
vec![1.0, 1.0, 2.0, 1.0, 3.0, 1.0, 4.0, 1.0, 5.0, 1.0],
)
.unwrap();
let out = layer.predict(&input).unwrap();
assert_eq!(out.shape(), &[1, 4, 2]);
assert_eq!(
out.iter().copied().collect::<Vec<f32>>(),
vec![3.0, 4.0, 5.0, 4.0, 7.0, 4.0, 9.0, 4.0]
);
}
#[test]
fn depthwise_1d_depth_multiplier_output_channel_order() {
let mut layer = DepthwiseConv1D::new(1, vec![1, 1, 2], 1, Linear::new())
.unwrap()
.with_depth_multiplier(2)
.unwrap();
assert_eq!(layer.weights.shape(), &[1, 2, 2]);
let weights = Array3::from_shape_vec((1, 2, 2), vec![1.0, 10.0, 100.0, 1000.0]).unwrap();
layer.set_weights(weights, Array1::zeros(4)).unwrap();
let input = ArrayD::from_shape_vec(ndarray::IxDyn(&[1, 1, 2]), vec![2.0, 3.0]).unwrap();
let out = layer.predict(&input).unwrap();
assert_eq!(out.shape(), &[1, 1, 4]);
assert_eq!(
out.iter().copied().collect::<Vec<f32>>(),
vec![2.0, 20.0, 300.0, 3000.0]
);
}
}