use crate::error::Error;
use crate::math::matmul::gemm_internal;
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::{LSTMLayerWeight, LayerWeight};
use crate::neural_network::layers::recurrent::apply_sigmoid;
use crate::neural_network::layers::recurrent::gate::{FusedGates, project_input, take_cache};
use crate::neural_network::layers::recurrent::validation::{
validate_input_3d, validate_recurrent_dimensions,
};
use crate::neural_network::layers::validation::validate_weight_shape;
use crate::neural_network::traits::{Layer, ParamGrad};
use ndarray::{Array2, Array3, ArrayView3, Axis, Ix2, Ix3, concatenate, s};
use std::borrow::Cow;
#[derive(Debug)]
pub struct LSTM {
input_dim: usize,
units: usize,
gates: FusedGates,
input_cache: Option<Array3<f32>>,
caches: Option<LstmCaches>,
activation: Activation,
}
#[derive(Debug)]
struct LstmCaches {
hs: Vec<Array2<f32>>,
cs: Vec<Array2<f32>>,
cs_activated: Vec<Array2<f32>>,
i: Vec<Array2<f32>>,
f: Vec<Array2<f32>>,
g: Vec<Array2<f32>>,
o: Vec<Array2<f32>>,
}
impl LSTM {
pub fn new(
input_dim: usize,
units: usize,
activation: impl Into<Activation>,
) -> Result<Self, Error> {
validate_recurrent_dimensions(input_dim, units)?;
Ok(Self {
input_dim,
units,
gates: Self::init_gates(input_dim, units, None)?,
input_cache: None,
caches: None,
activation: activation.into(),
})
}
pub fn with_random_state(mut self, random_state: u64) -> Self {
self.gates = Self::init_gates(self.input_dim, self.units, Some(random_state))
.expect("LSTM dimensions were validated in new()");
self
}
fn init_gates(
input_dim: usize,
units: usize,
random_state: Option<u64>,
) -> Result<FusedGates, Error> {
let mut rng = crate::random::make_rng(random_state);
FusedGates::new(input_dim, units, &[0.0, 1.0, 0.0, 0.0], &mut rng)
}
pub fn set_weights(
&mut self,
kernel: Array2<f32>,
recurrent_kernel: Array2<f32>,
bias: Array2<f32>,
) -> Result<(), Error> {
validate_weight_shape("kernel", self.gates.kernel.shape(), kernel.shape())?;
validate_weight_shape(
"recurrent_kernel",
self.gates.recurrent_kernel.shape(),
recurrent_kernel.shape(),
)?;
validate_weight_shape("bias", self.gates.bias.shape(), bias.shape())?;
self.gates.kernel = kernel.as_standard_layout().into_owned();
self.gates.recurrent_kernel = recurrent_kernel.as_standard_layout().into_owned();
self.gates.bias = bias.as_standard_layout().into_owned();
Ok(())
}
#[allow(clippy::too_many_arguments)] pub fn set_gate_weights(
&mut self,
input_kernel: Array2<f32>,
input_recurrent_kernel: Array2<f32>,
input_bias: Array2<f32>,
forget_kernel: Array2<f32>,
forget_recurrent_kernel: Array2<f32>,
forget_bias: Array2<f32>,
cell_kernel: Array2<f32>,
cell_recurrent_kernel: Array2<f32>,
cell_bias: Array2<f32>,
output_kernel: Array2<f32>,
output_recurrent_kernel: Array2<f32>,
output_bias: Array2<f32>,
) -> Result<(), Error> {
let per_gate_kernel = [self.input_dim, self.units];
let per_gate_recurrent = [self.units, self.units];
let per_gate_bias = [1, self.units];
for (name, expected, got) in [
("input_kernel", &per_gate_kernel, input_kernel.shape()),
(
"input_recurrent_kernel",
&per_gate_recurrent,
input_recurrent_kernel.shape(),
),
("input_bias", &per_gate_bias, input_bias.shape()),
("forget_kernel", &per_gate_kernel, forget_kernel.shape()),
(
"forget_recurrent_kernel",
&per_gate_recurrent,
forget_recurrent_kernel.shape(),
),
("forget_bias", &per_gate_bias, forget_bias.shape()),
("cell_kernel", &per_gate_kernel, cell_kernel.shape()),
(
"cell_recurrent_kernel",
&per_gate_recurrent,
cell_recurrent_kernel.shape(),
),
("cell_bias", &per_gate_bias, cell_bias.shape()),
("output_kernel", &per_gate_kernel, output_kernel.shape()),
(
"output_recurrent_kernel",
&per_gate_recurrent,
output_recurrent_kernel.shape(),
),
("output_bias", &per_gate_bias, output_bias.shape()),
] {
validate_weight_shape(name, expected, got)?;
}
let kernel = concatenate(
Axis(1),
&[
input_kernel.view(),
forget_kernel.view(),
cell_kernel.view(),
output_kernel.view(),
],
)
.expect("per-gate kernels share [input_dim, units]");
let recurrent_kernel = concatenate(
Axis(1),
&[
input_recurrent_kernel.view(),
forget_recurrent_kernel.view(),
cell_recurrent_kernel.view(),
output_recurrent_kernel.view(),
],
)
.expect("per-gate recurrent kernels share [units, units]");
let bias = concatenate(
Axis(1),
&[
input_bias.view(),
forget_bias.view(),
cell_bias.view(),
output_bias.view(),
],
)
.expect("per-gate biases share [1, units]");
self.set_weights(kernel, recurrent_kernel, bias)
}
fn run(
&self,
x3: &ArrayView3<f32>,
mut caches: Option<&mut LstmCaches>,
) -> Result<Array2<f32>, Error> {
let (batch, timesteps, _) = (x3.shape()[0], x3.shape()[1], x3.shape()[2]);
let u = self.units;
let act = self.activation;
let mut h_prev = Array2::<f32>::zeros((batch, u));
let mut c_prev = Array2::<f32>::zeros((batch, u));
if let Some(c) = caches.as_deref_mut() {
c.hs.push(h_prev.clone());
c.cs.push(c_prev.clone());
}
let xw = project_input(&self.gates.kernel, x3);
for t in 0..timesteps {
let xw_t = xw.index_axis(Axis(1), t);
let z_all =
gemm_internal(&h_prev, &self.gates.recurrent_kernel) + xw_t + &self.gates.bias;
let i_t = apply_sigmoid(z_all.slice(s![.., 0..u]).to_owned());
let f_t = apply_sigmoid(z_all.slice(s![.., u..2 * u]).to_owned());
let g_t = act
.forward(&z_all.slice(s![.., 2 * u..3 * u]).to_owned().into_dyn())?
.into_dimensionality::<Ix2>()
.unwrap();
let o_t = apply_sigmoid(z_all.slice(s![.., 3 * u..4 * u]).to_owned());
let c_t = &f_t * &c_prev + &i_t * &g_t;
let c_t_activated = act
.forward(&c_t.clone().into_dyn())?
.into_dimensionality::<Ix2>()
.unwrap();
let h_t = &o_t * &c_t_activated;
if let Some(c) = caches.as_deref_mut() {
c.i.push(i_t);
c.f.push(f_t);
c.g.push(g_t);
c.o.push(o_t);
c.cs.push(c_t.clone());
c.cs_activated.push(c_t_activated);
c.hs.push(h_t.clone());
}
h_prev = h_t;
c_prev = c_t;
}
Ok(h_prev)
}
}
impl Layer for LSTM {
fn forward(&mut self, input: &Tensor) -> Result<Tensor, Error> {
validate_input_3d(input)?;
let x3 = input.view().into_dimensionality::<Ix3>().unwrap();
let timesteps = x3.shape()[1];
self.input_cache = Some(x3.to_owned());
let mut caches = LstmCaches {
hs: Vec::with_capacity(timesteps + 1),
cs: Vec::with_capacity(timesteps + 1),
cs_activated: Vec::with_capacity(timesteps),
i: Vec::with_capacity(timesteps),
f: Vec::with_capacity(timesteps),
g: Vec::with_capacity(timesteps),
o: Vec::with_capacity(timesteps),
};
let h_last = self.run(&x3, Some(&mut caches))?;
self.caches = Some(caches);
Ok(h_last.into_dyn())
}
fn predict(&self, input: &Tensor) -> Result<Tensor, Error> {
validate_input_3d(input)?;
let x3 = input.view().into_dimensionality::<Ix3>().unwrap();
Ok(self.run(&x3, None)?.into_dyn())
}
fn backward(&mut self, grad_output: &Tensor) -> Result<Tensor, Error> {
let grad_h_t = grad_output
.clone()
.into_dimensionality::<Ix2>()
.map_err(|_| {
Error::invalid_input(format!(
"LSTM backward expects a 2D gradient [batch, units], got shape {:?}",
grad_output.shape()
))
})?;
let act = self.activation;
let x3 = take_cache(&mut self.input_cache, "LSTM")?;
let LstmCaches {
hs,
cs,
cs_activated,
i: i_vals,
f: f_vals,
g: g_vals,
o: o_vals,
} = take_cache(&mut self.caches, "LSTM")?;
let batch = x3.shape()[0];
let timesteps = x3.shape()[1];
let feat = x3.shape()[2];
let u = self.units;
let mut dz3 = Array3::<f32>::zeros((batch, timesteps, 4 * u));
let mut grad_h = grad_h_t;
let mut grad_c = Array2::<f32>::zeros((batch, u));
for t in (0..timesteps).rev() {
let c_prev = &cs[t];
let c_t_activated = &cs_activated[t];
let i_t = &i_vals[t];
let f_t = &f_vals[t];
let g_t = &g_vals[t];
let o_t = &o_vals[t];
let grad_o_t = &grad_h * c_t_activated;
let grad_cell_act = act
.backward(
&c_t_activated.clone().into_dyn(),
&(&grad_h * o_t).into_dyn(),
)?
.into_dimensionality::<Ix2>()
.unwrap();
grad_c += &grad_cell_act;
let grad_f_t = &grad_c * c_prev;
let grad_i_t = &grad_c * g_t;
let grad_g_t = &grad_c * i_t;
let grad_c_prev = &grad_c * f_t;
let grad_o_raw = &grad_o_t * o_t * &(1.0 - o_t);
let grad_f_raw = &grad_f_t * f_t * &(1.0 - f_t);
let grad_i_raw = &grad_i_t * i_t * &(1.0 - i_t);
let grad_g_raw = act
.backward(&g_t.clone().into_dyn(), &grad_g_t.into_dyn())?
.into_dimensionality::<Ix2>()
.unwrap();
let mut dz_t = Array2::<f32>::zeros((batch, 4 * u));
dz_t.slice_mut(s![.., 0..u]).assign(&grad_i_raw);
dz_t.slice_mut(s![.., u..2 * u]).assign(&grad_f_raw);
dz_t.slice_mut(s![.., 2 * u..3 * u]).assign(&grad_g_raw);
dz_t.slice_mut(s![.., 3 * u..4 * u]).assign(&grad_o_raw);
grad_h = gemm_internal(&dz_t, &self.gates.recurrent_kernel.t());
dz3.index_axis_mut(Axis(1), t).assign(&dz_t);
grad_c = grad_c_prev;
}
let x_flat = x3
.to_shape((batch * timesteps, feat))
.expect("contiguous input reshape");
let mut h_prev3 = Array3::<f32>::zeros((batch, timesteps, u));
for (mut dst, h) in h_prev3.axis_iter_mut(Axis(1)).zip(hs.iter()) {
dst.assign(h);
}
let h_prev_flat = h_prev3
.to_shape((batch * timesteps, u))
.expect("contiguous H_prev reshape");
let dz_flat = dz3
.to_shape((batch * timesteps, 4 * u))
.expect("contiguous DZ reshape");
let grad_kernel = gemm_internal(&x_flat.t(), &dz_flat);
let grad_recurrent = gemm_internal(&h_prev_flat.t(), &dz_flat);
let grad_bias = dz_flat.sum_axis(Axis(0)).insert_axis(Axis(0));
let grad_x3 = crate::neural_network::layers::recurrent::gate::reshape_2d_to_3d(
gemm_internal(&dz_flat, &self.gates.kernel.t()),
(batch, timesteps, feat),
);
self.gates
.store_gradients(grad_kernel, grad_recurrent, grad_bias);
Ok(grad_x3.into_dyn())
}
fn layer_type(&self) -> &str {
"LSTM"
}
fn output_shape(&self) -> String {
format!("(None, {})", self.units)
}
fn param_count(&self) -> TrainingParameters {
TrainingParameters::Trainable(
4 * (self.input_dim * self.units + self.units * self.units + self.units),
)
}
fn parameters(&mut self) -> Vec<ParamGrad<'_>> {
self.gates.parameters()
}
fn get_weights(&self) -> LayerWeight<'_> {
LayerWeight::LSTM(LSTMLayerWeight {
kernel: Cow::Borrowed(&self.gates.kernel),
recurrent_kernel: Cow::Borrowed(&self.gates.recurrent_kernel),
bias: Cow::Borrowed(&self.gates.bias),
})
}
}