📄 test_nn.rs
/home/palash/git/iron_learn/tests/nn/test_nn.rs
Language: rs • Lines: 213
#[cfg(test)]
mod tests {
    use iron_learn::nn::loss_functions::LossFunctionType;
    use iron_learn::nn::types::TrainingConfig;
    use iron_learn::nn::types::TrainingHook;
    use iron_learn::nn::DistributionType;
    use iron_learn::nn::LayerType;
    use iron_learn::Tensor;
    use iron_learn::{CpuTensor, NeuralNet, NeuralNetBuilder};
    use std::fs;
    use tempfile::tempdir;

    // A helper function to create a network for testing
    fn setup_test_net() -> NeuralNet<CpuTensor<f32>, f32> {
        let hl = 4;
        let input = 2;

        let mut nn = NeuralNetBuilder::<CpuTensor<f32>, f32>::new();

        let layers = [
            (
                input,
                hl,
                LayerType::ReLU,
                "Input",
                "AL 1",
                &DistributionType::Normal,
            ),
            (
                hl,
                hl,
                LayerType::Tanh,
                "HL1",
                "AL2",
                &DistributionType::Xavier,
            ),
            (hl, hl, LayerType::Tanh, "HL1", "AL2", &DistributionType::He),
            (
                hl,
                1,
                LayerType::Sigmoid,
                "HL2",
                "Output",
                &DistributionType::Uniform,
            ),
        ];

        for layer in layers {
            nn.add_linear(layer.0, layer.1, layer.3, layer.5);
            nn.add_activation(layer.2, layer.4);
        }
        nn.build(LossFunctionType::MeanSquaredError, "TestNet")
    }

    #[test]
    fn test_fit() {
        let mut net = setup_test_net();

        let input =
            CpuTensor::new(vec![4, 2], vec![0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0]).unwrap();
        let target = CpuTensor::new(vec![4, 1], vec![0.0, 1.0, 1.0, 0.0]).unwrap();

        let mut monitor_captured = vec![];

        let monitor = |epoch: usize,
                       err: f32,
                       err_val: f32,
                       lr: f32,
                       _nn: &mut NeuralNet<CpuTensor<f32>, f32>| {
            monitor_captured.push((epoch, err, err_val, lr));
        };

        let config = TrainingConfig {
            epochs: 5,
            epoch_offset: 0,
            base_lr: 1.0,
            lr_adjustment: false,
            weight_normalization: false,
        };

        let hook_config = TrainingHook::new(1, monitor);

        let result = net.fit(&input, &target, &input, &target, config, hook_config);

        assert!(result.is_ok());
        assert_eq!(monitor_captured.len(), 5);
        println!();
        println!("{:?}", monitor_captured);
        assert!(monitor_captured.iter().all(|a| a.3 == 1.0));
    }

    #[test]
    fn test_fit_cos_annealing() {
        let mut net = setup_test_net();

        let input =
            CpuTensor::new(vec![4, 2], vec![0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0]).unwrap();
        let target = CpuTensor::new(vec![4, 1], vec![0.0, 1.0, 1.0, 0.0]).unwrap();

        let mut monitor_captured = vec![];

        let monitor = |epoch: usize,
                       err: f32,
                       err_val: f32,
                       lr: f32,
                       _nn: &mut NeuralNet<CpuTensor<f32>, f32>| {
            monitor_captured.push((epoch, err, err_val, lr));
        };

        let config = TrainingConfig {
            epochs: 5,
            epoch_offset: 0,
            base_lr: 1.0,
            lr_adjustment: true,
            weight_normalization: false,
        };

        let hook_config = TrainingHook::new(1, monitor);

        let result = net.fit(&input, &target, &input, &target, config, hook_config);

        assert!(result.is_ok());
        assert_eq!(monitor_captured.len(), 5);
        assert!(!monitor_captured.iter().all(|a| a.2 == 1.0));
        assert!(monitor_captured[0].2 > monitor_captured[4].2);
    }

    #[test]
    fn test_predict_flow() {
        let mut net = setup_test_net();

        let input =
            CpuTensor::new(vec![4, 2], vec![0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0]).unwrap();

        let result = net.predict(&input);

        assert!(result.is_ok());
        // Verify output shape matches expectations
    }

    #[test]
    fn test_save_model_io() {
        let net = setup_test_net();
        let dir = tempdir().unwrap();
        let file_path = dir.path().join("subdir/model.json");
        let path_str = file_path.to_str().unwrap();

        net.save_model(path_str);

        // Check if file exists and is not empty
        let metadata = fs::metadata(path_str).unwrap();
        assert!(metadata.len() > 0);

        // Verify content contains the model name
        let content = fs::read_to_string(path_str).unwrap();
        assert!(content.contains(&net.name));
    }

    #[test]
    fn test_early_stopping_overfit() {
        let mut net = setup_test_net();
        let dir = tempdir().unwrap();
        net.name = dir.path().to_str().unwrap().to_string();

        // 1. Training Set: Simple linear mapping
        // The model will easily minimize this error.
        let x_train = CpuTensor::new(vec![2, 2], vec![0.5, 0.5, 1.0, 1.0]).unwrap();
        let y_train = CpuTensor::new(vec![2, 1], vec![0.0, 1.0]).unwrap();

        // 2. Validation Set: The "Trap"
        // Use the same inputs, but provide targets that are the exact opposite.
        // As training loss goes down (approaching 0 and 1),
        // the validation error on these points will mathematically increase.
        let x_val = x_train.clone();
        let y_val = CpuTensor::new(vec![2, 1], vec![1.0, 0.0]).unwrap();

        let mut epochs_run = 0;
        let monitor = |epoch: usize, _, _, _, _: &mut NeuralNet<CpuTensor<f32>, f32>| {
            epochs_run = epoch;
        };

        let config = TrainingConfig {
            epochs: 500,
            epoch_offset: 0,
            base_lr: 0.1, // Sufficiently high to move the weights quickly
            lr_adjustment: false,
            weight_normalization: true,
        };

        let hook_config = TrainingHook::new(1000, monitor);

        let _ = net.fit(&x_train, &y_train, &x_val, &y_val, config, hook_config);

        let _ = net.get_epoch_error();

        // Assert that we stopped before the 500 limit
        assert!(
            epochs_run < 400,
            "Model failed to overfit; ran for {} epochs",
            epochs_run
        );

        let path = format!("model_outputs/{}/model.json", net.name);

        // Check that the last good model was saved
        let _save_path = dir
            .path()
            .join(format!("model_outputs/{}/model.json", net.name));

        assert!(dir.path().exists());
        let _ = fs::remove_file(path);
    }
}