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, DepthwiseGradients, depthwise_forward_row, depthwise_item_gradients,
};
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_depth_multiplier, validate_filters, validate_input_shape_2d, validate_kernel_size_2d,
validate_strides_2d,
};
use crate::neural_network::layers::layer_weight::{LayerWeight, SeparableConv2DLayerWeight};
use crate::neural_network::layers::shape_helpers::calculate_output_height_and_weight;
use crate::neural_network::layers::validation::validate_weight_shape;
use crate::neural_network::traits::{Layer, ParamGrad};
use crate::parallel_gates::naive_conv_parallel_min_flops;
use ndarray::{Array1, Array4};
use ndarray_rand::{RandomExt, rand_distr::Uniform};
use rayon::prelude::*;
use std::borrow::Cow;
#[derive(Debug)]
pub struct SeparableConv2D {
filters: usize,
kernel_size: (usize, usize),
strides: (usize, usize),
padding: PaddingType,
depth_multiplier: usize,
depthwise_weights: Array4<f32>,
pointwise_weights: Array4<f32>,
bias: Array1<f32>,
activation: Activation,
output_cache: Option<Tensor>,
input_cache: Option<Tensor>,
depthwise_output_cache: Option<Tensor>,
input_shape: Vec<usize>,
depthwise_weight_gradients: Option<Array4<f32>>,
pointwise_weight_gradients: Option<Array4<f32>>,
bias_gradients: Option<Array1<f32>>,
}
impl SeparableConv2D {
pub fn new(
filters: usize,
kernel_size: (usize, usize),
input_shape: Vec<usize>,
strides: (usize, usize),
depth_multiplier: usize,
activation: impl Into<Activation>,
) -> Result<Self, Error> {
validate_filters(filters)?;
validate_kernel_size_2d(kernel_size)?;
validate_strides_2d(strides)?;
validate_depth_multiplier(depth_multiplier)?;
validate_input_shape_2d(&input_shape, kernel_size)?;
let channels = input_shape[3];
let (depthwise_weights, pointwise_weights) =
Self::init_weights_arrays(filters, channels, kernel_size, depth_multiplier, None);
let bias = Array1::zeros(filters);
Ok(SeparableConv2D {
filters,
kernel_size,
strides,
padding: PaddingType::Valid,
depth_multiplier,
depthwise_weights,
pointwise_weights,
bias,
activation: activation.into(),
output_cache: None,
input_cache: None,
depthwise_output_cache: None,
input_shape,
depthwise_weight_gradients: None,
pointwise_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[3];
let (depthwise_weights, pointwise_weights) = Self::init_weights_arrays(
self.filters,
channels,
self.kernel_size,
self.depth_multiplier,
Some(random_state),
);
self.depthwise_weights = depthwise_weights;
self.pointwise_weights = pointwise_weights;
self
}
fn init_weights_arrays(
filters: usize,
channels: usize,
kernel_size: (usize, usize),
depth_multiplier: usize,
random_state: Option<u64>,
) -> (Array4<f32>, Array4<f32>) {
let depthwise_fan_in = channels * kernel_size.0 * kernel_size.1;
let depthwise_fan_out = depth_multiplier * kernel_size.0 * kernel_size.1;
let depthwise_bound = (6.0 / (depthwise_fan_in + depthwise_fan_out) as f32).sqrt();
let mut rng = crate::random::make_rng(random_state);
let depthwise_weights = Array4::random_using(
(kernel_size.0, kernel_size.1, channels, depth_multiplier),
Uniform::new(-depthwise_bound, depthwise_bound).unwrap(),
&mut rng,
);
let pointwise_fan_in = channels * depth_multiplier;
let pointwise_fan_out = filters;
let pointwise_bound = (6.0 / (pointwise_fan_in + pointwise_fan_out) as f32).sqrt();
let pointwise_weights = Array4::random_using(
(1, 1, channels * depth_multiplier, filters),
Uniform::new(-pointwise_bound, pointwise_bound).unwrap(),
&mut rng,
);
(depthwise_weights, pointwise_weights)
}
fn calculate_output_shape(&self, input_shape: &[usize]) -> Vec<usize> {
let batch_size = input_shape[0];
let input_height = input_shape[1];
let input_width = input_shape[2];
let (output_height, output_width) = calculate_output_height_and_weight(
self.padding,
input_height,
input_width,
self.kernel_size,
self.strides,
);
vec![batch_size, output_height, output_width, self.filters]
}
fn depthwise_geometry(&self, input_shape: &[usize]) -> DepthwiseGeometry {
let (height, width) = (input_shape[1], input_shape[2]);
let depthwise_shape = self.calculate_depthwise_output_shape(input_shape);
let (out_height, out_width) = (depthwise_shape[1], depthwise_shape[2]);
let (pad_h, pad_w) = self.calculate_padding(height, width, out_height, out_width);
DepthwiseGeometry {
input: (height, width),
output: (out_height, out_width),
channels: input_shape[3],
depth_multiplier: self.depth_multiplier,
kernel: self.kernel_size,
strides: self.strides,
pad_before: (pad_h / 2, pad_w / 2),
}
}
fn depthwise_convolve(&self, input: &Tensor) -> Tensor {
let g = self.depthwise_geometry(input.shape());
let batch_size = input.shape()[0];
let out_channels = g.out_channels();
let input_std = input.as_standard_layout();
let src = input_std
.as_slice()
.expect("standard-layout array is contiguous");
let ker = self
.depthwise_weights
.as_slice()
.expect("depthwise weights must be contiguous");
let mut output = Array4::<f32>::zeros((batch_size, g.output.0, g.output.1, out_channels));
let flops =
2 * batch_size * out_channels * g.output.0 * g.output.1 * g.kernel.0 * g.kernel.1;
let row_len = g.output.1 * out_channels;
let out_flat = output.as_slice_mut().expect("output is contiguous");
if flops >= naive_conv_parallel_min_flops() {
out_flat
.par_chunks_mut(row_len)
.enumerate()
.for_each(|(i, row)| {
depthwise_forward_row(&g, src, ker, None, i / g.output.0, i % g.output.0, row)
});
} else {
for (i, row) in out_flat.chunks_mut(row_len).enumerate() {
depthwise_forward_row(&g, src, ker, None, i / g.output.0, i % g.output.0, row);
}
}
output.into_dyn()
}
fn pointwise_convolve(&self, input: &Tensor) -> Tensor {
conv_forward(
input,
self.pointwise_weights
.as_slice()
.expect("pointwise weights must be contiguous"),
self.pointwise_weights.shape(),
self.bias.as_slice().expect("bias must be contiguous"),
&[1, 1],
PaddingType::Valid,
)
.expect("1x1 pointwise convolution geometry is always valid")
}
fn calculate_depthwise_output_shape(&self, input_shape: &[usize]) -> Vec<usize> {
let batch_size = input_shape[0];
let input_height = input_shape[1];
let input_width = input_shape[2];
let channels = input_shape[3];
let (output_height, output_width) = calculate_output_height_and_weight(
self.padding,
input_height,
input_width,
self.kernel_size,
self.strides,
);
vec![
batch_size,
output_height,
output_width,
channels * self.depth_multiplier,
]
}
fn calculate_padding(
&self,
input_height: usize,
input_width: usize,
output_height: usize,
output_width: usize,
) -> (usize, usize) {
match self.padding {
PaddingType::Valid => (0, 0),
PaddingType::Same => {
let pad_h = ((output_height - 1) * self.strides.0 + self.kernel_size.0)
.saturating_sub(input_height);
let pad_w = ((output_width - 1) * self.strides.1 + self.kernel_size.1)
.saturating_sub(input_width);
(pad_h, pad_w)
}
}
}
pub fn set_weights(
&mut self,
depthwise_weights: Array4<f32>,
pointwise_weights: Array4<f32>,
bias: Array1<f32>,
) -> Result<(), Error> {
validate_weight_shape(
"depthwise_weight",
self.depthwise_weights.shape(),
depthwise_weights.shape(),
)?;
validate_weight_shape(
"pointwise_weight",
self.pointwise_weights.shape(),
pointwise_weights.shape(),
)?;
validate_weight_shape("bias", self.bias.shape(), bias.shape())?;
self.depthwise_weights = depthwise_weights;
self.pointwise_weights = pointwise_weights;
self.bias = bias;
Ok(())
}
}
impl Layer for SeparableConv2D {
fn forward(&mut self, input: &Tensor) -> Result<Tensor, Error> {
if input.ndim() != 4 {
return Err(Error::invalid_input("input tensor is not 4D"));
}
self.input_cache = Some(input.clone());
let depthwise_output = self.depthwise_convolve(input);
let output = self.pointwise_convolve(&depthwise_output);
self.depthwise_output_cache = Some(depthwise_output);
let activated = self.activation.forward(&output.into_dyn())?;
self.output_cache = Some(activated.clone());
Ok(activated)
}
fn predict(&self, input: &Tensor) -> Result<Tensor, Error> {
if input.ndim() != 4 {
return Err(Error::invalid_input("input tensor is not 4D"));
}
let depthwise_output = self.depthwise_convolve(input);
let output = self.pointwise_convolve(&depthwise_output);
let activated = self.activation.forward(&output.into_dyn())?;
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("SeparableConv2D"))?;
let grad_upstream = self.activation.backward(&activated, grad_output)?;
let (Some(input), Some(depthwise_output)) =
(&self.input_cache, &self.depthwise_output_cache)
else {
return Err(Error::forward_pass_not_run("SeparableConv2D"));
};
let batch_size = input.shape()[0];
let g = self.depthwise_geometry(input.shape());
let pw_grads = conv_backward(
&grad_upstream,
depthwise_output,
self.pointwise_weights
.as_slice()
.expect("pointwise weights must be contiguous"),
self.pointwise_weights.shape(),
&[1, 1],
PaddingType::Valid,
)
.expect("1x1 pointwise convolution geometry is always valid");
self.pointwise_weight_gradients = Some(
Array4::from_shape_vec(self.pointwise_weights.raw_dim(), pw_grads.weight_grad)
.expect("pointwise weight gradient shape matches weights"),
);
self.bias_gradients = Some(Array1::from_vec(pw_grads.bias_grad));
let depthwise_grad = pw_grads.input_grad;
let input_std = input.as_standard_layout();
let src = input_std
.as_slice()
.expect("standard-layout array is contiguous");
let grad_std = depthwise_grad.as_standard_layout();
let grad = grad_std
.as_slice()
.expect("standard-layout array is contiguous");
let ker = self
.depthwise_weights
.as_slice()
.expect("depthwise weights must be contiguous");
let flops =
2 * batch_size * g.out_channels() * g.output.0 * g.output.1 * g.kernel.0 * g.kernel.1;
let run = |b: usize| depthwise_item_gradients(&g, src, grad, ker, b);
let per_b: Vec<DepthwiseGradients> = if flops >= naive_conv_parallel_min_flops() {
(0..batch_size).into_par_iter().map(run).collect()
} else {
(0..batch_size).map(run).collect()
};
let mut depthwise_weight_grads = vec![0.0f32; self.depthwise_weights.len()];
let mut input_gradients =
Vec::with_capacity(batch_size * g.input.0 * g.input.1 * g.channels);
for part in per_b {
for (acc, v) in depthwise_weight_grads.iter_mut().zip(part.weight) {
*acc += v;
}
input_gradients.extend(part.input);
}
self.depthwise_weight_gradients = Some(
Array4::from_shape_vec(self.depthwise_weights.raw_dim(), depthwise_weight_grads)
.expect("depthwise weight gradient shape matches weights"),
);
Ok(Array4::from_shape_vec(
(batch_size, g.input.0, g.input.1, g.channels),
input_gradients,
)
.expect("input gradient shape matches input")
.into_dyn())
}
fn layer_type(&self) -> &str {
"SeparableConv2D"
}
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]
)
}
fn param_count(&self) -> TrainingParameters {
TrainingParameters::Trainable(
self.depthwise_weights.len() + self.pointwise_weights.len() + self.bias.len(),
)
}
fn parameters(&mut self) -> Vec<ParamGrad<'_>> {
let Self {
depthwise_weights,
pointwise_weights,
bias,
depthwise_weight_gradients,
pointwise_weight_gradients,
bias_gradients,
..
} = self;
let mut params = Vec::new();
if let (Some(gd), Some(gp), Some(gb)) = (
depthwise_weight_gradients.as_ref(),
pointwise_weight_gradients.as_ref(),
bias_gradients.as_ref(),
) {
params.push(ParamGrad::weight(
depthwise_weights
.as_slice_mut()
.expect("depthwise weights must be contiguous"),
gd.as_slice()
.expect("depthwise weight gradient must be contiguous"),
));
params.push(ParamGrad::weight(
pointwise_weights
.as_slice_mut()
.expect("pointwise weights must be contiguous"),
gp.as_slice()
.expect("pointwise weight gradient must be contiguous"),
));
params.push(ParamGrad::no_decay(
bias.as_slice_mut().expect("bias must be contiguous"),
gb.as_slice().expect("bias gradient must be contiguous"),
));
}
params
}
fn get_weights(&self) -> LayerWeight<'_> {
LayerWeight::SeparableConv2D(SeparableConv2DLayerWeight {
depthwise_weight: Cow::Borrowed(&self.depthwise_weights),
pointwise_weight: Cow::Borrowed(&self.pointwise_weights),
bias: Cow::Borrowed(&self.bias),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::neural_network::layers::activation::linear::Linear;
use crate::neural_network::traits::Layer;
use ndarray::ArrayD;
#[test]
fn separable_stage_channel_order_hand_derived() {
let mut layer =
SeparableConv2D::new(1, (1, 1), vec![1, 1, 1, 2], (1, 1), 2, Linear::new()).unwrap();
assert_eq!(layer.depthwise_weights.shape(), &[1, 1, 2, 2]);
assert_eq!(layer.pointwise_weights.shape(), &[1, 1, 4, 1]);
let depthwise =
Array4::from_shape_vec((1, 1, 2, 2), vec![1.0, 10.0, 100.0, 1000.0]).unwrap();
let pointwise = Array4::from_shape_vec((1, 1, 4, 1), vec![1.0, 2.0, 4.0, 8.0]).unwrap();
layer
.set_weights(depthwise, pointwise, Array1::zeros(1))
.unwrap();
let input = ArrayD::from_shape_vec(ndarray::IxDyn(&[1, 1, 1, 2]), vec![2.0, 3.0]).unwrap();
let out = layer.predict(&input).unwrap();
assert_eq!(out.shape(), &[1, 1, 1, 1]);
assert_eq!(out.iter().copied().collect::<Vec<f32>>(), vec![25242.0]);
}
#[test]
fn separable_spatial_pass_hand_derived() {
let mut layer =
SeparableConv2D::new(1, (2, 2), vec![1, 3, 3, 1], (1, 1), 1, Linear::new()).unwrap();
let depthwise = Array4::from_shape_vec((2, 2, 1, 1), vec![1.0, 1.0, 1.0, 1.0]).unwrap();
let pointwise = Array4::from_shape_vec((1, 1, 1, 1), vec![3.0]).unwrap();
layer
.set_weights(depthwise, pointwise, Array1::zeros(1))
.unwrap();
let input = ArrayD::from_shape_vec(
ndarray::IxDyn(&[1, 3, 3, 1]),
vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0],
)
.unwrap();
let out = layer.predict(&input).unwrap();
assert_eq!(out.shape(), &[1, 2, 2, 1]);
assert_eq!(
out.iter().copied().collect::<Vec<f32>>(),
vec![36.0, 48.0, 72.0, 84.0]
);
}
}