use crate::error::{Context, Error};
use crate::neural_network::layers::ParamCounts;
use crate::neural_network::layers::activation::{Activation, ActivationCache, ActivationInput};
use crate::neural_network::layers::validation::{
start_build, validate_optional_weight, validate_weight_shape,
};
use crate::neural_network::layers::{build_config_function, named_weight_layer_functions};
use crate::neural_network::traits::{LayerBase, ParamRef, UnaryLayer};
use crate::neural_network::{Ctx, Fans, Initializer, Shape, Tensor};
use gemmkit_ndarray::dot;
use gemmkit_ndarray::{Activation as FusedActivation, Bias, Parallelism};
use ndarray::{Array, Array2, ArrayView2, Axis, CowArray, Ix2};
#[derive(Debug)]
pub struct Dense {
input_dim: usize,
built: Option<Shape>,
random_state: Option<u64>,
output_dim: usize,
weights: Array2<f32>,
bias: Array2<f32>,
activation: Activation,
use_bias: bool,
}
impl Dense {
pub fn new(units: usize, activation: impl Into<Activation>) -> Result<Self, Error> {
if units == 0 {
return Err(Error::invalid_parameter("units", "must be greater than 0"));
}
let activation = activation.into();
activation.validate()?;
Ok(Self {
input_dim: 0,
built: None,
random_state: None,
output_dim: units,
weights: Array::zeros((0, 0)),
bias: Array::zeros((0, 0)),
activation,
use_bias: true,
})
}
fn draw_parameters(&mut self) {
let mut rng = crate::random::make_rng(self.random_state);
self.weights = Initializer::GlorotUniform.draw(
(self.input_dim, self.output_dim),
Fans::new(self.input_dim, self.output_dim),
&mut rng,
);
self.bias = Array::zeros((1, self.output_dim));
}
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
}
pub fn with_use_bias(mut self, use_bias: bool) -> Self {
self.use_bias = use_bias;
self
}
pub fn set_weights(
&mut self,
weights: Array2<f32>,
bias: impl Into<Option<Array2<f32>>>,
) -> Result<(), Error> {
if self.built.is_none() {
return Err(Error::not_built("Dense"));
}
validate_weight_shape("kernel", self.weights.shape(), 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.weights = weights.as_standard_layout().into_owned();
if let Some(bias) = bias {
self.bias = bias.as_standard_layout().into_owned();
}
Ok(())
}
fn fold<'a>(
tensor: &'a Tensor,
columns: usize,
role: &str,
) -> Result<CowArray<'a, f32, Ix2>, Error> {
let shape = tensor.shape();
if shape.len() < 2 {
return Err(Error::invalid_input(format!(
"Dense {role} must have rank 2 or more, got shape {shape:?}"
)));
}
if shape[shape.len() - 1] != columns {
return Err(Error::invalid_input(format!(
"Dense {role} must have {columns} elements on the last axis, got shape {shape:?}"
)));
}
let rows = shape[..shape.len() - 1].iter().product::<usize>();
tensor
.to_shape((rows, columns))
.context("Failed to fold the leading axes of a Dense tensor")
}
fn project(&self, input: &ArrayView2<'_, f32>, input_shape: &[usize]) -> Result<Tensor, Error> {
let bias = self
.use_bias
.then(|| self.bias.as_slice().expect("bias must be contiguous"));
let mut output = Array2::from_elem((input.nrows(), self.output_dim), 0.0);
let fused_act = match self.activation {
Activation::ReLU => Some(FusedActivation::Relu),
_ => None,
};
gemmkit_ndarray::gemm_fused(
1.0,
input,
&self.weights,
0.0,
&mut output,
bias.map(Bias::PerCol),
fused_act,
Parallelism::Rayon(0),
);
let mut output_shape = input_shape.to_vec();
let last_axis = output_shape.len() - 1;
output_shape[last_axis] = self.output_dim;
output
.into_dyn()
.into_shape_with_order(output_shape)
.context("Failed to restore the rank of the Dense output")
}
fn fuses(&self) -> bool {
matches!(self.activation, Activation::Linear | Activation::ReLU)
}
}
struct DenseCache {
input: Array2<f32>,
input_shape: Vec<usize>,
activation: ActivationCache,
}
impl LayerBase for Dense {
fn layer_type(&self) -> &str {
"Dense"
}
fn param_count(&self) -> ParamCounts {
let bias = if self.use_bias { self.bias.len() } else { 0 };
ParamCounts::trainable(self.weights.len() + bias)
}
fn parameters_mut(&mut self) -> Vec<ParamRef<'_>> {
let Self {
weights,
bias,
use_bias,
..
} = self;
let mut params = vec![ParamRef::weight(
"kernel",
weights.as_slice_mut().expect("weights must be contiguous"),
)];
if *use_bias {
params.push(ParamRef::no_decay(
"bias",
bias.as_slice_mut().expect("bias must be contiguous"),
));
}
params
}
fn known_input_shapes(&self) -> Option<Vec<Shape>> {
self.built.as_ref().map(|shape| vec![shape.free_batch()])
}
build_config_function!();
named_weight_layer_functions!(
trainable "kernel" => weights,
trainable "bias" => bias if use_bias,
);
}
impl UnaryLayer for Dense {
fn build(&mut self, input: &Shape) -> Result<(), Error> {
input.check_min_rank("Dense", 2)?;
let Some(input_dim) = input.axes()[input.rank() - 1] else {
return Err(Error::invalid_input(format!(
"Dense needs a fixed extent on the last axis, and the shape {input} leaves that \
axis free"
)));
};
if input_dim == 0 {
return Err(Error::invalid_input(
"Dense needs a positive extent on the last axis, got 0",
));
}
let canonical = Shape::new(vec![None, Some(input_dim)]);
let Some(built) = start_build(&self.built, "Dense", &canonical)? else {
return Ok(());
};
self.input_dim = input_dim;
self.built = Some(built);
self.draw_parameters();
Ok(())
}
fn forward(&self, input: &Tensor, ctx: &mut Ctx) -> Result<Tensor, Error> {
if self.built.is_none() {
return Err(Error::not_built("Dense"));
}
let input_2d = Self::fold(input, self.input_dim, "input")?;
let projected = self.project(&input_2d.view(), input.shape())?;
if !ctx.is_training() {
if self.fuses() {
return Ok(projected);
}
return self.activation.forward(&projected);
}
let (output, activation) = if self.fuses() {
let cache = ActivationCache::new(ActivationInput::Output, projected.clone());
(projected, cache)
} else {
self.activation.forward_train(projected)?
};
ctx.push_cache(
"Dense",
DenseCache {
input: input_2d.into_owned(),
input_shape: input.shape().to_vec(),
activation,
},
);
Ok(output)
}
fn backward(&self, grad_output: &Tensor, ctx: &mut Ctx) -> Result<Tensor, Error> {
let cache: DenseCache = ctx.pop_cache("Dense")?;
if grad_output.shape() != cache.activation.shape() {
return Err(Error::shape_mismatch(
cache.activation.shape(),
grad_output.shape(),
));
}
let grad_upstream = self.activation.backward(&cache.activation, grad_output)?;
let grad_upstream_2d = Self::fold(&grad_upstream, self.output_dim, "gradient")?;
let grad_w = dot(&cache.input.t(), &grad_upstream_2d);
ctx.add_grad("kernel", grad_w.as_standard_layout().to_owned().into_dyn())?;
if self.use_bias {
let grad_b = grad_upstream_2d.sum_axis(Axis(0)).insert_axis(Axis(0));
ctx.add_grad("bias", grad_b.as_standard_layout().to_owned().into_dyn())?;
}
let grad_input = dot(&grad_upstream_2d, &self.weights.t());
grad_input
.into_dyn()
.into_shape_with_order(cache.input_shape)
.context("Failed to restore the rank of the Dense input gradient")
}
fn compute_output_shape(&self, input: &Shape) -> Result<Shape, Error> {
input.check_min_rank("Dense", 2)?;
let mut axes = input.axes().to_vec();
let last = axes.len() - 1;
if self.built.is_some() {
match axes[last] {
Some(extent) if extent == self.input_dim => {}
Some(extent) => {
return Err(Error::invalid_input(format!(
"Dense input must have {} elements on the last axis, got {extent}",
self.input_dim
)));
}
None => {
return Err(Error::invalid_input(format!(
"Dense input must have {} elements on the last axis, and that axis is \
free",
self.input_dim
)));
}
}
}
axes[last] = Some(self.output_dim);
Ok(Shape::new(axes))
}
}