use crate::error::Error;
use crate::neural_network::layers::recurrent::orthogonal_init;
use crate::neural_network::layers::recurrent::validation::validate_dimension_greater_than_zero;
use crate::neural_network::traits::ParamGrad;
use ndarray::{Array, Array2, Array3, ArrayView3, s};
use ndarray_rand::rand::rngs::StdRng;
use ndarray_rand::{RandomExt, rand_distr::Uniform};
#[derive(Debug)]
pub struct FusedGates {
pub kernel: Array2<f32>,
pub recurrent_kernel: Array2<f32>,
pub bias: Array2<f32>,
pub grad_kernel: Option<Array2<f32>>,
pub grad_recurrent_kernel: Option<Array2<f32>>,
pub grad_bias: Option<Array2<f32>>,
}
impl FusedGates {
pub fn new(
input_dim: usize,
units: usize,
bias_init: &[f32],
rng: &mut StdRng,
) -> Result<Self, Error> {
validate_dimension_greater_than_zero(input_dim, "input_dim")?;
validate_dimension_greater_than_zero(units, "units")?;
let n_gates = bias_init.len();
let width = n_gates * units;
let limit = (6.0 / (input_dim + units) as f32).sqrt();
let kernel = Array::random_using(
(input_dim, width),
Uniform::new(-limit, limit).unwrap(),
rng,
);
let mut recurrent_kernel = Array2::<f32>::zeros((units, width));
for g in 0..n_gates {
recurrent_kernel
.slice_mut(s![.., g * units..(g + 1) * units])
.assign(&orthogonal_init(units, rng));
}
let mut bias = Array2::<f32>::zeros((1, width));
for (g, &b) in bias_init.iter().enumerate() {
bias.slice_mut(s![.., g * units..(g + 1) * units]).fill(b);
}
Ok(Self {
kernel,
recurrent_kernel,
bias,
grad_kernel: None,
grad_recurrent_kernel: None,
grad_bias: None,
})
}
pub fn parameters(&mut self) -> Vec<ParamGrad<'_>> {
let Self {
kernel,
recurrent_kernel,
bias,
grad_kernel,
grad_recurrent_kernel,
grad_bias,
..
} = self;
let mut params = Vec::new();
if let (Some(gk), Some(grk), Some(gb)) = (
grad_kernel.as_ref(),
grad_recurrent_kernel.as_ref(),
grad_bias.as_ref(),
) {
params.push(ParamGrad::weight(
kernel
.as_slice_mut()
.expect("fused kernel must be contiguous"),
gk.as_slice()
.expect("fused kernel gradient must be contiguous"),
));
params.push(ParamGrad::weight(
recurrent_kernel
.as_slice_mut()
.expect("fused recurrent kernel must be contiguous"),
grk.as_slice()
.expect("fused recurrent kernel gradient must be contiguous"),
));
params.push(ParamGrad::no_decay(
bias.as_slice_mut().expect("fused bias must be contiguous"),
gb.as_slice()
.expect("fused bias gradient must be contiguous"),
));
}
params
}
#[inline]
pub fn store_gradients(
&mut self,
grad_kernel: Array2<f32>,
grad_recurrent: Array2<f32>,
grad_bias: Array2<f32>,
) {
self.grad_kernel = Some(grad_kernel);
self.grad_recurrent_kernel = Some(grad_recurrent);
self.grad_bias = Some(grad_bias);
}
}
pub fn project_input(kernel: &Array2<f32>, x3: &ArrayView3<f32>) -> Array3<f32> {
let (batch, timesteps, input_dim) = (x3.shape()[0], x3.shape()[1], x3.shape()[2]);
let width = kernel.shape()[1];
let x2 = x3
.to_shape((batch * timesteps, input_dim))
.expect("contiguous [batch*timesteps, input_dim] reshape");
reshape_2d_to_3d(
crate::math::matmul::gemm_par_auto(&x2, kernel),
(batch, timesteps, width),
)
}
pub fn reshape_2d_to_3d(m: Array2<f32>, dims: (usize, usize, usize)) -> Array3<f32> {
let m = if m.is_standard_layout() {
m
} else {
m.as_standard_layout().into_owned()
};
m.into_shape_with_order(dims)
.expect("row-major [d0*d1, d2] reshapes to [d0, d1, d2]")
}
#[inline]
pub fn take_cache<T>(cache: &mut Option<T>, layer: &'static str) -> crate::error::RustymlResult<T> {
cache
.take()
.ok_or_else(|| Error::forward_pass_not_run(layer))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::NnError;
use approx::assert_abs_diff_eq;
use ndarray::{Array2, Array3, array};
#[test]
fn fused_gates_new_shapes() {
let mut rng = crate::random::make_rng(Some(42));
let gates = FusedGates::new(4, 3, &[0.0, 1.0, 0.0, 0.0], &mut rng).unwrap();
assert_eq!(gates.kernel.shape(), &[4, 12]);
assert_eq!(gates.recurrent_kernel.shape(), &[3, 12]);
assert_eq!(gates.bias.shape(), &[1, 12]);
}
#[test]
fn fused_gates_new_bias_blocks() {
let mut rng = crate::random::make_rng(Some(42));
let gates = FusedGates::new(2, 2, &[0.0, 1.0, 0.5], &mut rng).unwrap();
let b = &gates.bias;
for c in 0..2 {
assert_abs_diff_eq!(b[[0, c]], 0.0_f32, epsilon = 1e-6);
}
for c in 2..4 {
assert_abs_diff_eq!(b[[0, c]], 1.0_f32, epsilon = 1e-6);
}
for c in 4..6 {
assert_abs_diff_eq!(b[[0, c]], 0.5_f32, epsilon = 1e-6);
}
}
#[test]
fn fused_gates_new_recurrent_blocks_orthogonal() {
let mut rng = crate::random::make_rng(Some(7));
let units = 3;
let gates = FusedGates::new(2, units, &[0.0, 0.0], &mut rng).unwrap();
for g in 0..2 {
let block = gates
.recurrent_kernel
.slice(s![.., g * units..(g + 1) * units]);
let gram = block.t().dot(&block);
for i in 0..units {
for j in 0..units {
let expected = if i == j { 1.0 } else { 0.0 };
assert_abs_diff_eq!(gram[[i, j]], expected, epsilon = 1e-4);
}
}
}
}
#[test]
fn fused_gates_new_rejects_zero_dims() {
let mut rng = crate::random::make_rng(Some(1));
assert!(FusedGates::new(0, 3, &[0.0], &mut rng).is_err());
assert!(FusedGates::new(3, 0, &[0.0], &mut rng).is_err());
}
#[test]
fn parameters_empty_when_gradients_none() {
let mut rng = crate::random::make_rng(Some(3));
let mut gates = FusedGates::new(2, 2, &[0.0, 0.0], &mut rng).unwrap();
assert!(
gates.parameters().is_empty(),
"parameters() must be empty before gradients are computed"
);
}
#[test]
fn parameters_three_entries_after_gradients() {
let mut rng = crate::random::make_rng(Some(3));
let mut gates = FusedGates::new(2, 2, &[0.0, 0.0], &mut rng).unwrap();
gates.store_gradients(
Array2::zeros((2, 4)),
Array2::zeros((2, 4)),
Array2::zeros((1, 4)),
);
assert_eq!(gates.parameters().len(), 3);
}
#[test]
fn store_gradients_stores_unchanged() {
let mut rng = crate::random::make_rng(Some(5));
let mut gates = FusedGates::new(2, 1, &[0.0, 0.0], &mut rng).unwrap();
let grad_kernel = array![[10.0_f32, -100.0], [3.0, 42.0]];
let grad_recurrent = array![[5.0_f32, -5.0]];
let grad_bias = array![[8.0_f32, -8.0]];
gates.store_gradients(
grad_kernel.clone(),
grad_recurrent.clone(),
grad_bias.clone(),
);
for (got, exp) in gates
.grad_kernel
.as_ref()
.unwrap()
.iter()
.zip(grad_kernel.iter())
{
assert_abs_diff_eq!(got, exp, epsilon = 1e-6);
}
for (got, exp) in gates
.grad_recurrent_kernel
.as_ref()
.unwrap()
.iter()
.zip(grad_recurrent.iter())
{
assert_abs_diff_eq!(got, exp, epsilon = 1e-6);
}
for (got, exp) in gates
.grad_bias
.as_ref()
.unwrap()
.iter()
.zip(grad_bias.iter())
{
assert_abs_diff_eq!(got, exp, epsilon = 1e-6);
}
}
#[test]
fn project_input_matches_per_timestep_gemm() {
let x3 = Array3::from_shape_vec((1, 2, 2), vec![1.0_f32, 2.0, 3.0, 4.0]).unwrap();
let kernel = array![[1.0_f32, 0.0, 2.0, 0.0], [0.0, 1.0, 0.0, 2.0]];
let out = project_input(&kernel, &x3.view());
assert_eq!(out.shape(), &[1, 2, 4]);
assert_abs_diff_eq!(out[[0, 0, 0]], 1.0_f32, epsilon = 1e-6);
assert_abs_diff_eq!(out[[0, 0, 1]], 2.0_f32, epsilon = 1e-6);
assert_abs_diff_eq!(out[[0, 0, 2]], 2.0_f32, epsilon = 1e-6);
assert_abs_diff_eq!(out[[0, 0, 3]], 4.0_f32, epsilon = 1e-6);
assert_abs_diff_eq!(out[[0, 1, 0]], 3.0_f32, epsilon = 1e-6);
assert_abs_diff_eq!(out[[0, 1, 1]], 4.0_f32, epsilon = 1e-6);
assert_abs_diff_eq!(out[[0, 1, 2]], 6.0_f32, epsilon = 1e-6);
assert_abs_diff_eq!(out[[0, 1, 3]], 8.0_f32, epsilon = 1e-6);
}
#[test]
fn take_cache_none_returns_forward_pass_not_run() {
let mut cache: Option<Array2<f32>> = None;
let err = take_cache(&mut cache, "Gate").unwrap_err();
assert!(
matches!(
err,
Error::NeuralNetwork(NnError::ForwardPassNotRun("Gate"))
),
"expected ForwardPassNotRun(\"Gate\"), got: {err:?}"
);
}
#[test]
fn take_cache_some_returns_value_and_empties() {
let mut cache: Option<Array2<f32>> = Some(array![[7.0_f32, 8.0]]);
let value = take_cache(&mut cache, "Gate").expect("value present");
assert_eq!(value.shape(), &[1, 2]);
assert_abs_diff_eq!(value[[0, 0]], 7.0_f32, epsilon = 1e-6);
assert_abs_diff_eq!(value[[0, 1]], 8.0_f32, epsilon = 1e-6);
assert!(cache.is_none(), "take_cache must leave None behind");
}
}