use crate::error::Error;
use crate::neural_network::layers::recurrent::validation::validate_dimension_greater_than_zero;
use crate::neural_network::traits::ParamRef;
use crate::neural_network::{Fans, Initializer};
use ndarray::{Array2, Array3, ArrayView3, s};
use ndarray_rand::rand::rngs::StdRng;
#[derive(Debug)]
pub struct FusedGates {
pub kernel: Array2<f32>,
pub recurrent_kernel: Array2<f32>,
pub bias: Array2<f32>,
}
impl FusedGates {
pub fn empty() -> Self {
Self {
kernel: Array2::zeros((0, 0)),
recurrent_kernel: Array2::zeros((0, 0)),
bias: Array2::zeros((0, 0)),
}
}
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 kernel =
Initializer::GlorotUniform.draw((input_dim, width), Fans::new(input_dim, units), 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(&Initializer::Orthogonal.draw_orthogonal(units, Fans::NONE, 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,
})
}
pub fn units(&self) -> usize {
self.recurrent_kernel.shape()[0]
}
pub fn parameters_mut(&mut self) -> Vec<ParamRef<'_>> {
let Self {
kernel,
recurrent_kernel,
bias,
} = self;
vec![
ParamRef::weight(
"kernel",
kernel
.as_slice_mut()
.expect("fused kernel must be contiguous"),
),
ParamRef::weight(
"recurrent_kernel",
recurrent_kernel
.as_slice_mut()
.expect("fused recurrent kernel must be contiguous"),
),
ParamRef::no_decay(
"bias",
bias.as_slice_mut().expect("fused bias must be contiguous"),
),
]
}
}
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(gemmkit_ndarray::dot(&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]")
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
use ndarray::{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_mut_three_named_entries() {
let mut rng = crate::random::make_rng(Some(3));
let mut gates = FusedGates::new(2, 2, &[0.0, 0.0], &mut rng).unwrap();
let params = gates.parameters_mut();
assert_eq!(params.len(), 3);
let names: Vec<&str> = params.iter().map(|param| param.name).collect();
assert_eq!(names, vec!["kernel", "recurrent_kernel", "bias"]);
}
#[test]
fn parameters_mut_decay_flags() {
let mut rng = crate::random::make_rng(Some(3));
let mut gates = FusedGates::new(2, 2, &[0.0, 0.0], &mut rng).unwrap();
let decays: Vec<bool> = gates
.parameters_mut()
.iter()
.map(|param| param.decays)
.collect();
assert_eq!(decays, vec![true, true, false]);
}
#[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);
}
}