use super::traits::{Layer, Loss, Optimizer};
use crate::error::{Error, IoError, NnError};
use crate::math::reduction::det_reduce;
use crate::neural_network::Tensor;
use crate::neural_network::layers::TrainingParameters;
use crate::neural_network::layers::layer_weight::LayerWeight;
use crate::neural_network::layers::serialize_model::{
LayerInfo, SerializableLayer, SerializableSequential, apply_weights_to_layer,
};
use crate::parallel_gates::sq_sum_f32_parallel_min_elems;
use ndarray::Axis;
use ndarray_rand::rand::seq::SliceRandom;
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufWriter, Write};
pub struct Sequential {
layers: Vec<Box<dyn Layer>>,
optimizer: Option<Box<dyn Optimizer>>,
loss: Option<Box<dyn Loss>>,
seed: Option<u64>,
}
impl Default for Sequential {
fn default() -> Self {
Self::new()
}
}
fn global_grad_norm(layers: &mut [Box<dyn Layer>]) -> f32 {
let mut sum_sq = 0.0_f64;
for layer in layers.iter_mut() {
for pg in layer.parameters() {
sum_sq += det_reduce(
pg.grad,
pg.grad.len() >= sq_sum_f32_parallel_min_elems(),
|block| block.iter().map(|&g| (g as f64) * (g as f64)).sum::<f64>(),
|a, b| a + b,
0.0,
);
}
}
sum_sq.sqrt() as f32
}
impl Sequential {
pub fn new() -> Self {
Self {
layers: Vec::new(),
optimizer: None,
loss: None,
seed: None,
}
}
pub fn set_seed(&mut self, seed: u64) -> &mut Self {
self.seed = Some(seed);
self
}
pub fn set_learning_rate(&mut self, learning_rate: f32) -> &mut Self {
if let Some(ref mut optimizer) = self.optimizer {
optimizer.set_learning_rate(learning_rate);
}
self
}
pub fn new_with_seed(seed: u64) -> Self {
let mut model = Self::new();
model.seed = Some(seed);
model
}
pub fn add<L: 'static + Layer>(&mut self, layer: L) -> &mut Self {
self.layers.push(Box::new(layer));
self
}
pub fn compile<O, LFunc>(&mut self, optimizer: O, loss: LFunc) -> &mut Self
where
O: 'static + Optimizer,
LFunc: 'static + Loss,
{
self.optimizer = Some(Box::new(optimizer));
self.loss = Some(Box::new(loss));
self
}
fn validate_training_inputs(&self, x: &Tensor, y: &Tensor) -> Result<(), Error> {
if self.optimizer.is_none() {
return Err(Error::NeuralNetwork(NnError::NotCompiled("optimizer")));
}
if self.loss.is_none() {
return Err(Error::NeuralNetwork(NnError::NotCompiled("loss function")));
}
if self.layers.is_empty() {
return Err(Error::NeuralNetwork(NnError::EmptyModel));
}
if x.is_empty() || y.is_empty() {
return Err(Error::empty_input("input tensors"));
}
if x.shape()[0] != y.shape()[0] {
return Err(Error::dimension_mismatch(x.shape()[0], y.shape()[0]));
}
Ok(())
}
fn train_batch(&mut self, x: &Tensor, y: &Tensor) -> Result<f32, Error> {
let mut layers_iter = self.layers.iter_mut();
let first_layer = layers_iter
.next()
.ok_or_else(|| Error::computation("no layers in model"))?;
first_layer.set_training_if_mode_dependent(true);
let mut output = first_layer.forward(x)?;
for layer in layers_iter {
layer.set_training_if_mode_dependent(true);
output = layer.forward(&output)?;
}
let loss_value = self.loss.as_ref().unwrap().compute_loss(y, &output)?;
let mut grad = self.loss.as_ref().unwrap().compute_grad(y, &output)?;
if let Some(ref mut optimizer) = self.optimizer {
optimizer.step();
}
for layer in self.layers.iter_mut().rev() {
grad = layer.backward(&grad)?;
}
let clip_norm = self.optimizer.as_ref().and_then(|opt| opt.clip_norm());
let grad_scale = match clip_norm {
Some(max_norm) => {
let norm = global_grad_norm(&mut self.layers);
if norm.is_finite() && norm > max_norm {
max_norm / norm
} else {
1.0
}
}
None => 1.0,
};
if let Some(ref mut optimizer) = self.optimizer {
for layer in self.layers.iter_mut().rev() {
optimizer.update(&mut **layer, grad_scale);
}
}
Ok(loss_value)
}
pub fn fit(&mut self, x: &Tensor, y: &Tensor, epochs: u32) -> Result<&mut Self, Error> {
self.validate_training_inputs(x, y)?;
#[cfg(feature = "show_progress")]
let progress_bar = crate::create_progress_bar(
epochs as u64,
"[{elapsed_precise}] {bar:40} {pos}/{len} | Loss: {msg}",
);
for _ in 0..epochs {
#[cfg(feature = "show_progress")]
let loss_value = self.train_batch(x, y)?;
#[cfg(not(feature = "show_progress"))]
let _ = self.train_batch(x, y)?;
#[cfg(feature = "show_progress")]
{
progress_bar.set_message(format!("{:.6}", loss_value));
progress_bar.inc(1);
}
}
#[cfg(feature = "show_progress")]
progress_bar.finish_with_message("Training completed");
Ok(self)
}
pub fn fit_with_batches(
&mut self,
x: &Tensor,
y: &Tensor,
epochs: u32,
batch_size: usize,
) -> Result<&mut Self, Error> {
self.validate_training_inputs(x, y)?;
let n_samples = x.shape()[0];
if batch_size == 0 {
return Err(Error::invalid_parameter(
"batch_size",
"must be greater than 0",
));
}
if batch_size > n_samples {
return Err(Error::invalid_parameter(
"batch_size",
format!(
"({}) cannot be larger than dataset size ({})",
batch_size, n_samples
),
));
}
let create_batch_tensors =
|x: &Tensor, y: &Tensor, indices: &[usize]| -> Result<(Tensor, Tensor), Error> {
Ok((x.select(Axis(0), indices), y.select(Axis(0), indices)))
};
let mut indices: Vec<usize> = (0..n_samples).collect();
let mut shuffle_rng = crate::random::make_rng(self.seed);
#[cfg(feature = "show_progress")]
let total_batches = n_samples.div_ceil(batch_size);
#[cfg(feature = "show_progress")]
let total_iterations = epochs as u64 * total_batches as u64;
#[cfg(feature = "show_progress")]
let progress_bar = crate::create_progress_bar(
total_iterations,
"[{elapsed_precise}] {bar:40} {pos}/{len} | Epoch {msg}",
);
for epoch in 0..epochs {
indices.shuffle(&mut shuffle_rng);
#[cfg(feature = "show_progress")]
let (mut epoch_loss, mut batch_count) = (0.0_f32, 0_usize);
for batch_indices in indices.chunks(batch_size) {
let (batch_x, batch_y) = create_batch_tensors(x, y, batch_indices)?;
let batch_loss = self.train_batch(&batch_x, &batch_y)?;
#[cfg(feature = "show_progress")]
{
batch_count += 1;
epoch_loss += batch_loss;
progress_bar.set_message(format!(
"{}/{} | Avg Loss: {:.6}",
epoch + 1,
epochs,
epoch_loss / batch_count as f32
));
progress_bar.inc(1);
}
#[cfg(not(feature = "show_progress"))]
let _ = batch_loss;
}
#[cfg(not(feature = "show_progress"))]
let _ = epoch;
}
#[cfg(feature = "show_progress")]
progress_bar.finish_with_message("Training completed");
Ok(self)
}
pub fn predict(&self, x: &Tensor) -> Result<Tensor, Error> {
if x.is_empty() {
return Err(Error::empty_input("input tensor"));
}
let mut layers_iter = self.layers.iter();
let first_layer = layers_iter
.next()
.ok_or_else(|| Error::NeuralNetwork(NnError::EmptyModel))?;
let mut output = first_layer.predict(x)?;
for layer in layers_iter {
output = layer.predict(&output)?;
}
Ok(output)
}
pub fn summary(&self) {
let col1_width = 33;
let col2_width = 24;
let col3_width = 15;
let mut output = String::new();
output.push_str("Model: \"sequential\"\n");
output.push_str(&format!(
"┏{}┳{}┳{}┓\n",
"━".repeat(col1_width),
"━".repeat(col2_width),
"━".repeat(col3_width)
));
output.push_str(&format!(
"┃ {:<31} ┃ {:<22} ┃ {:>13} ┃\n",
"Layer (type)", "Output Shape", "Param #"
));
output.push_str(&format!(
"┡{}╇{}╇{}┩\n",
"━".repeat(col1_width),
"━".repeat(col2_width),
"━".repeat(col3_width)
));
let mut total_params: usize = 0;
let mut trainable_param_count: usize = 0;
let mut non_trainable_param_count: usize = 0;
let mut type_counts: HashMap<&str, usize> = HashMap::new();
for layer in self.layers.iter() {
let layer_type = layer.layer_type();
let count = type_counts.entry(layer_type).or_insert(0);
let layer_name = if *count == 0 {
layer_type.to_lowercase()
} else {
format!("{}_{}", layer_type.to_lowercase(), count)
};
*count += 1;
let out_shape = layer.output_shape();
let param_count_num = match layer.param_count() {
TrainingParameters::Trainable(count) => {
trainable_param_count += count;
total_params += count;
count
}
TrainingParameters::NonTrainable(count) => {
non_trainable_param_count += count;
total_params += count;
count
}
TrainingParameters::NoTrainable => 0,
};
output.push_str(&format!(
"│ {:<31} │ {:<22} │ {:>13} │\n",
format!("{} ({})", layer_name, layer_type),
out_shape,
param_count_num
));
}
output.push_str(&format!(
"└{}┴{}┴{}┘\n",
"─".repeat(col1_width),
"─".repeat(col2_width),
"─".repeat(col3_width)
));
output.push_str(&format!(
" Total params: {} ({} B)\n",
total_params,
total_params * 4
)); output.push_str(&format!(
" Trainable params: {} ({} B)\n",
trainable_param_count,
trainable_param_count * 4
));
output.push_str(&format!(
" Non-trainable params: {} ({} B)",
non_trainable_param_count,
non_trainable_param_count * 4
));
println!("{}", output);
}
pub fn get_weights(&self) -> Vec<LayerWeight<'_>> {
let mut weights = Vec::with_capacity(self.layers.len());
for layer in &self.layers {
weights.push(layer.get_weights());
}
weights
}
pub fn save_to_path(
&self,
path: impl AsRef<std::path::Path>,
) -> crate::error::RustymlResult<()> {
let serializable_layers = self
.layers
.iter()
.map(|layer| {
let layer_info = LayerInfo {
layer_type: layer.layer_type().to_string(),
output_shape: layer.output_shape(),
};
SerializableLayer {
info: layer_info,
weights: layer.get_weights(),
}
})
.collect();
let serializable_model = SerializableSequential {
layers: serializable_layers,
};
let bytes = postcard::to_allocvec(&serializable_model)?;
let file = File::create(path)?;
let mut writer = BufWriter::new(file);
writer.write_all(&bytes)?;
writer.flush()?;
Ok(())
}
pub fn load_from_path(
&mut self,
path: impl AsRef<std::path::Path>,
) -> crate::error::RustymlResult<()> {
let bytes = std::fs::read(path)?;
let serializable_model: SerializableSequential<'static> = postcard::from_bytes(&bytes)?;
if serializable_model.layers.len() != self.layers.len() {
return Err(Error::Io(IoError::ModelStructureMismatch(format!(
"layer count mismatch: model has {} layers, file has {} layers",
self.layers.len(),
serializable_model.layers.len()
))));
}
for (i, serializable_layer) in serializable_model.layers.iter().enumerate() {
let expected_type = self.layers[i].layer_type();
let saved_type = serializable_layer.info.layer_type.as_str();
if expected_type != saved_type {
return Err(Error::Io(IoError::ModelStructureMismatch(format!(
"layer {} type mismatch: model has `{}`, file has `{}`",
i, expected_type, saved_type
))));
}
apply_weights_to_layer(
&mut *self.layers[i],
&serializable_layer.weights,
saved_type,
)?;
}
Ok(())
}
}