use crate::daily::lstm::step_1_tensor_preparation::{
dataframe_to_tensors, impute_missing_values, load_daily_csv, normalize_daily_features,
split_daily_data,
};
use crate::daily::lstm::step_3_lstm_model_arch::{DailyLSTMModel, DailyLSTMModelConfig};
use anyhow::Result;
use burn::data::{
dataloader::{batcher::Batcher, DataLoaderBuilder},
dataset::Dataset,
};
use burn::module::Module;
use burn::optim::{AdamConfig, GradientsParams, Optimizer};
use burn::record::Recorder;
use burn::tensor::backend::AutodiffBackend;
use burn::tensor::cast::ToElement;
use burn::tensor::{backend::Backend, Tensor};
use log::info;
use std::collections::HashMap;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct DailyBatch<B: Backend> {
pub features: Tensor<B, 3>,
pub targets: Tensor<B, 2>,
}
pub struct DailyBatcher<B: Backend> {
_phantom: std::marker::PhantomData<B>,
}
impl<B: Backend> DailyBatcher<B> {
pub fn new() -> Self {
Self {
_phantom: std::marker::PhantomData,
}
}
}
impl<B: Backend> Batcher<B, (Tensor<B, 3>, Tensor<B, 2>), DailyBatch<B>> for DailyBatcher<B> {
fn batch(
&self,
items: Vec<(Tensor<B, 3>, Tensor<B, 2>)>,
_device: &B::Device,
) -> DailyBatch<B> {
let (features, targets): (Vec<_>, Vec<_>) = items.into_iter().unzip();
let features = Tensor::cat(features, 0);
let targets = Tensor::cat(targets, 0);
DailyBatch { features, targets }
}
}
pub struct DailyDataset {
features: Vec<Vec<f32>>,
targets: Vec<Vec<f32>>,
}
impl DailyDataset {
pub fn new(features: Vec<Vec<f32>>, targets: Vec<Vec<f32>>) -> Self {
Self { features, targets }
}
}
impl<B: Backend> Dataset<(Tensor<B, 3>, Tensor<B, 2>)> for DailyDataset {
fn get(&self, index: usize) -> Option<(Tensor<B, 3>, Tensor<B, 2>)> {
if index >= self.features.len() {
return None;
}
let device = Default::default();
let x_shape = [1, self.features[index].len() / 6, 6];
let y_shape = [1, self.targets[index].len()];
let x_data: Vec<f32> = self.features[index].iter().map(|&x| x as f32).collect();
let y_data: Vec<f32> = self.targets[index].iter().map(|&x| x as f32).collect();
let x = Tensor::<B, 1>::from_data(x_data.as_slice(), &device).reshape(x_shape);
let y = Tensor::<B, 1>::from_data(y_data.as_slice(), &device).reshape(y_shape);
Some((x, y))
}
fn len(&self) -> usize {
self.features.len()
}
}
pub fn train_daily_lstm<B: AutodiffBackend<Gradients = GradientsParams>>(
csv_path: &str,
config: DailyLSTMModelConfig,
device: &B::Device,
learning_rate: f64,
batch_size: usize,
epochs: usize,
sequence_length: usize,
forecast_horizon: usize,
model_save_path: Option<&Path>,
) -> Result<DailyLSTMModel<B>> {
info!("Loading data from {}", csv_path);
let mut df = load_daily_csv(csv_path)?;
info!("Handling missing values");
impute_missing_values(&mut df, "forward")?;
info!("Normalizing features");
normalize_daily_features(&mut df)?;
info!("Splitting data into training and validation sets");
let (train_df, val_df) = split_daily_data(&df, 0.8)?;
info!("Converting data to tensors");
let (train_x, train_y) =
dataframe_to_tensors::<B>(&train_df, sequence_length, forecast_horizon, device)?;
let (val_x, val_y) =
dataframe_to_tensors::<B>(&val_df, sequence_length, forecast_horizon, device)?;
info!("Creating model");
let mut model = config.init(device);
let optim_config = AdamConfig::new();
let mut optim = optim_config.init();
info!("Starting training");
let mut best_val_loss = f32::INFINITY;
let mut metrics = HashMap::new();
for epoch in 0..epochs {
let train_dataset = DailyDataset::new(
vec![vec![0.0; sequence_length * 6]; train_x.dims()[0]],
vec![vec![0.0; 1]; train_y.dims()[0]],
);
let train_loader = DataLoaderBuilder::new(DailyBatcher::new())
.batch_size(batch_size)
.shuffle(1) .build(train_dataset);
let mut epoch_loss = 0.0;
let num_batches = (train_x.dims()[0] + batch_size - 1) / batch_size;
for batch in train_loader.iter() {
let pred = model.forward(batch.features.clone(), true);
let two = Tensor::<B, 2>::ones_like(&pred);
let loss = ((pred - batch.targets).powf(two)).mean();
let grads = loss.backward();
model = optim.step(learning_rate, model, grads);
epoch_loss += loss.into_scalar().to_f32();
}
epoch_loss /= num_batches as f32;
let val_dataset = DailyDataset::new(
vec![vec![0.0; sequence_length * 6]; val_x.dims()[0]],
vec![vec![0.0; 1]; val_y.dims()[0]],
);
let val_loader = DataLoaderBuilder::new(DailyBatcher::new())
.batch_size(batch_size)
.shuffle(0) .build(val_dataset);
let mut val_loss = 0.0;
let num_val_batches = (val_x.dims()[0] + batch_size - 1) / batch_size;
for batch in val_loader.iter() {
let pred = model.predict(batch.features.clone());
let two = Tensor::<B, 2>::ones_like(&pred);
let loss = ((pred - batch.targets).powf(two)).mean();
val_loss += loss.into_scalar().to_f32();
}
val_loss /= num_val_batches as f32;
println!(
"Epoch {}/{}: Train Loss = {:.6}, Validation Loss = {:.6}",
epoch + 1,
epochs,
epoch_loss,
val_loss
);
if val_loss < best_val_loss {
best_val_loss = val_loss;
if let Some(path) = model_save_path {
println!("New best model found! Saving to {:?}...", path);
}
}
metrics.insert(format!("epoch_{}_train_loss", epoch + 1), epoch_loss);
metrics.insert(format!("epoch_{}_val_loss", epoch + 1), val_loss);
}
Ok(model)
}