use crate::error::Error;
use crate::neural_network::layers::ParamCounts;
use crate::neural_network::layers::activation::{Activation, ActivationCache};
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::convolution_engine::{
ConvPadding, conv_backward, conv_forward, effective_kernel,
};
use crate::neural_network::layers::convolution::validation::{
valid_output_size, validate_depth_multiplier, validate_dilation, validate_filters,
validate_input_shape_2d, validate_kernel_size_2d, validate_strides_2d,
validate_valid_kernel_fits,
};
use crate::neural_network::layers::shape_helpers::calculate_output_height_and_weight;
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 SeparableConv2D {
filters: usize,
kernel_size: (usize, usize),
strides: (usize, usize),
dilation_rate: (usize, usize),
padding: PaddingType,
depth_multiplier: usize,
depthwise_weights: Array4<f32>,
pointwise_weights: Array4<f32>,
bias: Array1<f32>,
activation: Activation,
channels: usize,
built: Option<Shape>,
random_state: Option<u64>,
use_bias: bool,
}
impl SeparableConv2D {
pub fn new(
filters: usize,
kernel_size: (usize, 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)?;
let activation = activation.into();
activation.validate()?;
Ok(SeparableConv2D {
filters,
kernel_size,
strides,
dilation_rate: (1, 1),
padding: PaddingType::Valid,
depth_multiplier,
depthwise_weights: Array4::zeros((0, 0, 0, 0)),
pointwise_weights: Array4::zeros((0, 0, 0, 0)),
bias: Array1::zeros(0),
activation,
channels: 0,
built: None,
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)?;
self.dilation_rate = dilation_rate;
Ok(self)
}
fn effective_kernel_size(&self) -> (usize, usize) {
(
effective_kernel(self.kernel_size.0, self.dilation_rate.0),
effective_kernel(self.kernel_size.1, self.dilation_rate.1),
)
}
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 (kernel_height, kernel_width) = self.kernel_size;
let mut rng = crate::random::make_rng(self.random_state);
self.depthwise_weights = Initializer::GlorotUniform.draw(
(
kernel_height,
kernel_width,
self.channels,
self.depth_multiplier,
),
Fans::conv(
self.channels,
self.depth_multiplier,
kernel_height * kernel_width,
),
&mut rng,
);
let width = self.channels * self.depth_multiplier;
self.pointwise_weights = Initializer::GlorotUniform.draw(
(1, 1, width, self.filters),
Fans::new(width, self.filters),
&mut rng,
);
self.bias = Array1::zeros(self.filters);
}
fn calculate_output_shape(&self, input_shape: &[usize]) -> Result<Vec<usize>, Error> {
let batch_size = input_shape[0];
let input_height = input_shape[1];
let input_width = input_shape[2];
let (keff_h, keff_w) = self.effective_kernel_size();
let (output_height, output_width) = match self.padding {
PaddingType::Valid => (
valid_output_size(
"SeparableConv2D",
"height",
input_height,
keff_h,
self.strides.0,
)?,
valid_output_size(
"SeparableConv2D",
"width",
input_width,
keff_w,
self.strides.1,
)?,
),
PaddingType::Same => (
input_height.div_ceil(self.strides.0),
input_width.div_ceil(self.strides.1),
),
};
Ok(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,
dilation: self.dilation_rate,
pad_before: (pad_h / 2, pad_w / 2),
}
}
fn validate_input(&self, input: &Tensor) -> Result<(), Error> {
validate_built_input(&self.built, "SeparableConv2D", input.shape())?;
validate_valid_kernel_fits(
self.padding.into(),
&[self.kernel_size.0, self.kernel_size.1],
&[self.dilation_rate.0, self.dilation_rate.1],
&input.shape()[1..3],
)?;
Ok(())
}
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));
depthwise_forward(
&g,
src,
ker,
None,
output.as_slice_mut().expect("output is contiguous"),
);
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.use_bias
.then(|| self.bias.as_slice().expect("bias must be contiguous")),
&[1, 1],
&[1, 1],
ConvPadding::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.effective_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 (keff_h, keff_w) = self.effective_kernel_size();
let pad_h =
((output_height - 1) * self.strides.0 + keff_h).saturating_sub(input_height);
let pad_w =
((output_width - 1) * self.strides.1 + keff_w).saturating_sub(input_width);
(pad_h, pad_w)
}
}
}
pub fn with_use_bias(mut self, use_bias: bool) -> Self {
self.use_bias = use_bias;
self
}
pub fn set_weights(
&mut self,
depthwise_weights: Array4<f32>,
pointwise_weights: Array4<f32>,
bias: impl Into<Option<Array1<f32>>>,
) -> Result<(), Error> {
if self.built.is_none() {
return Err(Error::not_built("SeparableConv2D"));
}
validate_weight_shape(
"depthwise_kernel",
self.depthwise_weights.shape(),
depthwise_weights.shape(),
)?;
validate_weight_shape(
"pointwise_kernel",
self.pointwise_weights.shape(),
pointwise_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.depthwise_weights = depthwise_weights;
self.pointwise_weights = pointwise_weights;
if let Some(bias) = bias {
self.bias = bias;
}
Ok(())
}
}
struct SeparableConv2DCache {
input: Tensor,
depthwise_output: Tensor,
activation: ActivationCache,
}
impl LayerBase for SeparableConv2D {
fn layer_type(&self) -> &str {
"SeparableConv2D"
}
fn param_count(&self) -> ParamCounts {
let bias = if self.use_bias { self.bias.len() } else { 0 };
ParamCounts::trainable(self.depthwise_weights.len() + self.pointwise_weights.len() + bias)
}
fn parameters_mut(&mut self) -> Vec<ParamRef<'_>> {
let Self {
depthwise_weights,
pointwise_weights,
bias,
use_bias,
..
} = self;
let mut params = vec![
ParamRef::weight(
"depthwise_kernel",
depthwise_weights
.as_slice_mut()
.expect("depthwise weights must be contiguous"),
),
ParamRef::weight(
"pointwise_kernel",
pointwise_weights
.as_slice_mut()
.expect("pointwise 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 "depthwise_kernel" => depthwise_weights,
trainable "pointwise_kernel" => pointwise_weights,
trainable "bias" => bias if use_bias,
);
}
impl UnaryLayer for SeparableConv2D {
fn build(&mut self, input: &Shape) -> Result<(), Error> {
let Some(built) = start_build(&self.built, "SeparableConv2D", input)? else {
return Ok(());
};
built.check_rank("SeparableConv2D", 4)?;
let (batch, tail) = built.split_batch("SeparableConv2D")?;
let mut dims = vec![batch.unwrap_or(1)];
dims.extend(tail);
validate_input_shape_2d(&dims)?;
self.compute_output_shape(&built)?;
self.channels = dims[3];
self.built = Some(built);
self.draw_parameters();
Ok(())
}
fn forward(&self, input: &Tensor, ctx: &mut Ctx) -> Result<Tensor, Error> {
if !self.is_built() {
return Err(Error::not_built("SeparableConv2D"));
}
self.validate_input(input)?;
let depthwise_output = self.depthwise_convolve(input);
let output = self.pointwise_convolve(&depthwise_output);
if !ctx.is_training() {
return self.activation.forward(&output.into_dyn());
}
let (activated, activation) = self.activation.forward_train(output.into_dyn())?;
ctx.push_cache(
"SeparableConv2D",
SeparableConv2DCache {
input: input.clone(),
depthwise_output,
activation,
},
);
Ok(activated)
}
fn backward(&self, grad_output: &Tensor, ctx: &mut Ctx) -> Result<Tensor, Error> {
let cache: SeparableConv2DCache = ctx.pop_cache("SeparableConv2D")?;
let grad_upstream = self.activation.backward(&cache.activation, grad_output)?;
let input = &cache.input;
let depthwise_output = &cache.depthwise_output;
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],
&[1, 1],
ConvPadding::Valid,
)
.expect("1x1 pointwise convolution geometry is always valid");
ctx.add_grad(
"pointwise_kernel",
Array4::from_shape_vec(self.pointwise_weights.raw_dim(), pw_grads.weight_grad)
.expect("pointwise weight gradient shape matches weights")
.into_dyn(),
)?;
if self.use_bias {
ctx.add_grad("bias", Array1::from_vec(pw_grads.bias_grad).into_dyn())?;
}
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 dw_grads = depthwise_backward(&g, src, grad, ker, batch_size);
ctx.add_grad(
"depthwise_kernel",
Array4::from_shape_vec(self.depthwise_weights.raw_dim(), dw_grads.weight)
.expect("depthwise weight gradient shape matches weights")
.into_dyn(),
)?;
Ok(Array4::from_shape_vec(
(batch_size, g.input.0, g.input.1, g.channels),
dw_grads.input,
)
.expect("input gradient shape matches input")
.into_dyn())
}
fn compute_output_shape(&self, input: &Shape) -> Result<Shape, Error> {
input.check_rank("SeparableConv2D", 4)?;
let (batch, tail) = input.split_batch("SeparableConv2D")?;
let mut dims = vec![0];
dims.extend(tail);
Ok(Shape::from_batch(
batch,
&self.calculate_output_shape(&dims)?[1..],
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::neural_network::layers::activation::linear::Linear;
use ndarray::ArrayD;
#[test]
fn separable_stage_channel_order_hand_derived() {
let mut layer = SeparableConv2D::new(1, (1, 1), (1, 1), 2, Linear::new()).unwrap();
layer.build(&Shape::known(&[1, 1, 1, 2])).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.forward(&input, &mut Ctx::inference()).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), (1, 1), 1, Linear::new()).unwrap();
layer.build(&Shape::known(&[1, 3, 3, 1])).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.forward(&input, &mut Ctx::inference()).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]
);
}
}