use crate::error::Error;
use crate::math::matmul::gemm_par_auto;
use crate::neural_network::Tensor;
use crate::neural_network::layers::TrainingParameters;
use crate::neural_network::layers::activation::Activation;
use crate::neural_network::layers::layer_weight::{DenseLayerWeight, LayerWeight};
use crate::neural_network::layers::validation::validate_weight_shape;
use crate::neural_network::traits::{Layer, ParamGrad};
use ndarray::{Array, Array2, Axis};
use ndarray_rand::{RandomExt, rand_distr::Uniform};
use std::borrow::Cow;
#[derive(Debug)]
pub struct Dense {
input_dim: usize,
output_dim: usize,
weights: Array2<f32>,
bias: Array2<f32>,
input_cache: Option<Array2<f32>>,
output_cache: Option<Tensor>,
grad_weights: Option<Array2<f32>>,
grad_bias: Option<Array2<f32>>,
activation: Activation,
}
impl Dense {
pub fn new(
input_dim: usize,
units: usize,
activation: impl Into<Activation>,
) -> Result<Self, Error> {
if input_dim == 0 {
return Err(Error::invalid_parameter(
"input_dim",
"must be greater than 0",
));
}
if units == 0 {
return Err(Error::invalid_parameter("units", "must be greater than 0"));
}
Ok(Self {
input_dim,
output_dim: units,
weights: Self::init_weights_array(input_dim, units, None),
bias: Array::zeros((1, units)),
input_cache: None,
output_cache: None,
grad_weights: None,
grad_bias: None,
activation: activation.into(),
})
}
pub fn with_random_state(mut self, random_state: u64) -> Self {
self.weights =
Self::init_weights_array(self.input_dim, self.output_dim, Some(random_state));
self
}
fn init_weights_array(
input_dim: usize,
units: usize,
random_state: Option<u64>,
) -> Array2<f32> {
let limit = (6.0 / (input_dim + units) as f32).sqrt();
let mut rng = crate::random::make_rng(random_state);
Array::random_using(
(input_dim, units),
Uniform::new(-limit, limit).unwrap(),
&mut rng,
)
}
pub fn set_weights(&mut self, weights: Array2<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.as_standard_layout().into_owned();
self.bias = bias.as_standard_layout().into_owned();
Ok(())
}
}
impl Layer for Dense {
fn forward(&mut self, input: &Tensor) -> Result<Tensor, Error> {
if input.ndim() != 2 {
return Err(Error::invalid_input("input tensor is not 2D"));
}
let input_2d = input.view().into_dimensionality::<ndarray::Ix2>().unwrap();
self.input_cache = Some(input_2d.to_owned());
let z = gemm_par_auto(&input_2d, &self.weights) + &self.bias;
let output = self.activation.forward(&z.into_dyn())?;
self.output_cache = Some(output.clone());
Ok(output)
}
fn predict(&self, input: &Tensor) -> Result<Tensor, Error> {
if input.ndim() != 2 {
return Err(Error::invalid_input("input tensor is not 2D"));
}
let input_2d = input.view().into_dimensionality::<ndarray::Ix2>().unwrap();
let z = gemm_par_auto(&input_2d, &self.weights) + &self.bias;
self.activation.forward(&z.into_dyn())
}
fn backward(&mut self, grad_output: &Tensor) -> Result<Tensor, Error> {
let activated = self
.output_cache
.take()
.ok_or_else(|| Error::forward_pass_not_run("Dense"))?;
if grad_output.shape() != activated.shape() {
return Err(Error::shape_mismatch(
activated.shape(),
grad_output.shape(),
));
}
let grad_upstream = self.activation.backward(&activated, grad_output)?;
let grad_upstream_2d = grad_upstream
.into_dimensionality::<ndarray::Ix2>()
.map_err(|_| {
Error::invalid_input(format!(
"Dense backward expects a 2D gradient [batch, output_dim], got shape {:?}",
grad_output.shape()
))
})?;
let input = self
.input_cache
.take()
.ok_or_else(|| Error::forward_pass_not_run("Dense"))?;
let grad_w = gemm_par_auto(&input.t(), &grad_upstream_2d);
let grad_b = grad_upstream_2d.sum_axis(Axis(0)).insert_axis(Axis(0));
self.grad_weights = Some(grad_w.as_standard_layout().to_owned());
self.grad_bias = Some(grad_b.as_standard_layout().to_owned());
let grad_input = gemm_par_auto(&grad_upstream_2d, &self.weights.t());
Ok(grad_input.into_dyn())
}
fn layer_type(&self) -> &str {
"Dense"
}
fn output_shape(&self) -> String {
format!("(None, {})", self.output_dim)
}
fn param_count(&self) -> TrainingParameters {
TrainingParameters::Trainable(self.input_dim * self.output_dim + self.output_dim)
}
fn parameters(&mut self) -> Vec<ParamGrad<'_>> {
let Self {
weights,
bias,
grad_weights,
grad_bias,
..
} = self;
let mut params = Vec::new();
if let (Some(grad_a), Some(grad_b)) = (grad_weights.as_ref(), grad_bias.as_ref()) {
params.push(ParamGrad::weight(
weights.as_slice_mut().expect("weights must be contiguous"),
grad_a.as_slice().expect("grad_weights must be contiguous"),
));
params.push(ParamGrad::no_decay(
bias.as_slice_mut().expect("bias must be contiguous"),
grad_b.as_slice().expect("grad_bias must be contiguous"),
));
}
params
}
fn get_weights(&self) -> LayerWeight<'_> {
LayerWeight::Dense(DenseLayerWeight {
weight: Cow::Borrowed(&self.weights),
bias: Cow::Borrowed(&self.bias),
})
}
}