use candle_core::{D, Result as CandleResult, Tensor};
use candle_nn::VarBuilder;
use candle_nn::rnn::{Direction, LSTM, LSTMConfig, RNN, lstm};
use super::ops::reverse_time;
pub(super) struct BiLstm {
forward: LSTM,
backward: LSTM,
}
impl BiLstm {
pub(super) fn new(in_dim: usize, hidden: usize, vb: VarBuilder) -> CandleResult<Self> {
let forward = lstm(
in_dim,
hidden,
LSTMConfig {
direction: Direction::Forward,
..LSTMConfig::default()
},
vb.clone(),
)?;
let backward = lstm(
in_dim,
hidden,
LSTMConfig {
direction: Direction::Backward,
..LSTMConfig::default()
},
vb,
)?;
Ok(Self { forward, backward })
}
pub(super) fn forward(&self, input: &Tensor) -> CandleResult<Tensor> {
let forward = self.forward.states_to_tensor(&self.forward.seq(input)?)?;
let reversed = reverse_time(input)?;
let backward = self.backward.states_to_tensor(&self.backward.seq(&reversed)?)?;
let backward = reverse_time(&backward)?;
Tensor::cat(&[forward, backward], D::Minus1)?.contiguous()
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use candle_core::{DType, Device};
use super::*;
fn twin_bilstm() -> BiLstm {
let device = Device::Cpu;
let mut tensors = HashMap::new();
for (suffix, scale) in [("", 0.5_f32), ("_reverse", 0.5_f32)] {
tensors.insert(
format!("weight_ih_l0{suffix}"),
Tensor::from_vec(vec![scale; 4], (4, 1), &device).unwrap(),
);
tensors.insert(
format!("weight_hh_l0{suffix}"),
Tensor::from_vec(vec![scale; 4], (4, 1), &device).unwrap(),
);
tensors.insert(
format!("bias_ih_l0{suffix}"),
Tensor::zeros(4, DType::F32, &device).unwrap(),
);
tensors.insert(
format!("bias_hh_l0{suffix}"),
Tensor::zeros(4, DType::F32, &device).unwrap(),
);
}
let vb = VarBuilder::from_tensors(tensors, DType::F32, &device);
BiLstm::new(1, 1, vb).expect("build the bilstm")
}
fn half(output: &Tensor, index: usize) -> Vec<f32> {
output
.narrow(D::Minus1, index, 1)
.unwrap()
.flatten_all()
.unwrap()
.to_vec1::<f32>()
.unwrap()
}
#[test]
fn should_concatenate_both_directions_along_the_feature_axis() {
let input = Tensor::from_vec(vec![1.0_f32, 2.0, 3.0], (1, 3, 1), &Device::Cpu).unwrap();
let output = twin_bilstm().forward(&input).expect("forward");
assert_eq!(output.dims(), &[1, 3, 2]);
}
#[test]
fn should_run_the_backward_direction_over_the_reversed_sequence() {
let ascending = Tensor::from_vec(vec![1.0_f32, 2.0, 9.0], (1, 3, 1), &Device::Cpu).unwrap();
let output = twin_bilstm().forward(&ascending).expect("forward");
assert_ne!(
half(&output, 0),
half(&output, 1),
"identical weights leave the reading order as the only difference between halves"
);
}
#[test]
fn should_mirror_the_forward_pass_on_a_palindromic_sequence() {
let palindrome = Tensor::from_vec(vec![1.0_f32, 7.0, 1.0], (1, 3, 1), &Device::Cpu).unwrap();
let output = twin_bilstm().forward(&palindrome).expect("forward");
let forward: Vec<f32> = half(&output, 0);
let mut mirrored = forward.clone();
mirrored.reverse();
assert_eq!(half(&output, 1), mirrored);
}
}