#![cfg(feature = "neural_network")]
use ndarray::Array;
use ndarray_rand::rand::random;
use rustyml::neural_network::Tensor;
use rustyml::neural_network::layer::activation_layer::linear::Linear;
use rustyml::neural_network::layer::activation_layer::relu::ReLU;
use rustyml::neural_network::layer::activation_layer::softmax::Softmax;
use rustyml::neural_network::layer::dense::Dense;
use rustyml::neural_network::loss_function::categorical_cross_entropy::CategoricalCrossEntropy;
use rustyml::neural_network::loss_function::mean_squared_error::MeanSquaredError;
use rustyml::neural_network::optimizer::adam::Adam;
use rustyml::neural_network::optimizer::sgd::SGD;
use rustyml::neural_network::sequential::Sequential;
#[test]
fn fit_with_batches_test() {
let x = Array::ones((1000, 784)).into_dyn(); let y = Array::ones((1000, 10)).into_dyn();
let mut model = Sequential::new();
model
.add(Dense::new(784, 128, ReLU::new()).unwrap())
.add(Dense::new(128, 64, ReLU::new()).unwrap())
.add(Dense::new(64, 10, Softmax::new()).unwrap())
.compile(
Adam::new(0.001, 0.9, 0.999, 1e-8).unwrap(),
CategoricalCrossEntropy::new(),
);
model.fit_with_batches(&x, &y, 1, 32).unwrap();
}
#[test]
fn test_fit_linear_regression_convergence() {
let mut x_data = Vec::new();
let mut y_data = Vec::new();
for i in 0..100 {
let x_val = i as f32 / 50.0; let y_val = 2.0 * x_val + 1.0; x_data.push(x_val);
y_data.push(y_val);
}
let x = Array::from_shape_vec((100, 1), x_data).unwrap().into_dyn();
let y = Array::from_shape_vec((100, 1), y_data).unwrap().into_dyn();
let mut model = Sequential::new();
model
.add(Dense::new(1, 1, Linear::new()).unwrap()) .compile(SGD::new(0.01).unwrap(), MeanSquaredError::new());
let initial_predictions = model.predict(&x).unwrap();
let initial_loss = calculate_mse(&y, &initial_predictions);
model.fit(&x, &y, 100).unwrap();
let final_predictions = model.predict(&x).unwrap();
let final_loss = calculate_mse(&y, &final_predictions);
assert!(
final_loss < initial_loss,
"Final loss ({:.6}) should be less than initial loss ({:.6})",
final_loss,
initial_loss
);
assert!(
final_loss < 0.4,
"For simple linear relationship, final loss ({:.6}) should be less than 0.4",
final_loss
);
let test_x = Array::from_shape_vec((1, 1), vec![1.0]).unwrap().into_dyn();
let prediction = model.predict(&test_x).unwrap();
let expected = 3.0;
assert!(
(prediction[[0, 0]] - expected).abs() <= 0.5,
"Prediction ({:.3}) for input 1.0 should be close to expected value ({:.3})",
prediction[[0, 0]],
expected
);
}
#[test]
fn test_fit_classification_convergence() {
let mut x_data = Vec::new();
let mut y_data = Vec::new();
for i in 0..50 {
let x1 = -2.0 + (i as f32 / 25.0) + (random::<f32>() - 0.5) * 0.5; let x2 = -2.0 + (i as f32 / 25.0) + (random::<f32>() - 0.5) * 0.5; x_data.extend_from_slice(&[x1, x2]);
y_data.extend_from_slice(&[1.0, 0.0]); }
for i in 0..50 {
let x1 = 0.5 + (i as f32 / 25.0) + (random::<f32>() - 0.5) * 0.5; let x2 = 0.5 + (i as f32 / 25.0) + (random::<f32>() - 0.5) * 0.5; x_data.extend_from_slice(&[x1, x2]);
y_data.extend_from_slice(&[0.0, 1.0]); }
let x = Array::from_shape_vec((100, 2), x_data).unwrap().into_dyn();
let y = Array::from_shape_vec((100, 2), y_data).unwrap().into_dyn();
let mut model = Sequential::new();
model
.add(Dense::new(2, 4, ReLU::new()).unwrap())
.add(Dense::new(4, 2, Softmax::new()).unwrap())
.compile(
Adam::new(0.01, 0.9, 0.999, 1e-8).unwrap(),
CategoricalCrossEntropy::new(),
);
let initial_predictions = model.predict(&x).unwrap();
let initial_accuracy = calculate_accuracy(&y, &initial_predictions);
println!("Initial accuracy: {:.3}", initial_accuracy);
model.fit(&x, &y, 150).unwrap();
let final_predictions = model.predict(&x).unwrap();
let final_accuracy = calculate_accuracy(&y, &final_predictions);
println!("Final accuracy: {:.3}", final_accuracy);
if initial_accuracy < 0.9 {
assert!(
final_accuracy > initial_accuracy,
"Final accuracy ({:.3}) should be higher than initial accuracy ({:.3})",
final_accuracy,
initial_accuracy
);
} else {
assert!(
final_accuracy >= initial_accuracy - 0.05,
"Final accuracy ({:.3}) should not be significantly worse than initial accuracy ({:.3})",
final_accuracy,
initial_accuracy
);
}
assert!(
final_accuracy > 0.7,
"For classification task, final accuracy ({:.3}) should be greater than 0.7",
final_accuracy
);
let test_x = Array::from_shape_vec((2, 2), vec![-1.5, -1.5, 1.5, 1.5])
.unwrap()
.into_dyn();
let predictions = model.predict(&test_x).unwrap();
assert!(
predictions[[0, 0]] > predictions[[0, 1]],
"Sample [-1.5, -1.5] should be classified as class 0"
);
assert!(
predictions[[1, 1]] > predictions[[1, 0]],
"Sample [1.5, 1.5] should be classified as class 1"
);
}
#[test]
fn test_fit_error_handling() {
let mut model = Sequential::new();
model.add(Dense::new(2, 1, Linear::new()).unwrap());
let x = Array::ones((5, 2)).into_dyn();
let y = Array::ones((5, 1)).into_dyn();
let result = model.fit(&x, &y, 10);
assert!(result.is_err(), "Uncompiled model should return error");
model.compile(SGD::new(0.01).unwrap(), MeanSquaredError::new());
let empty_x = Array::zeros((0, 2)).into_dyn();
let empty_y = Array::zeros((0, 1)).into_dyn();
let result = model.fit(&empty_x, &empty_y, 10);
assert!(result.is_err(), "Empty data should return error");
let x_mismatch = Array::ones((5, 2)).into_dyn();
let y_mismatch = Array::ones((3, 1)).into_dyn();
let result = model.fit(&x_mismatch, &y_mismatch, 10);
assert!(result.is_err(), "Sample count mismatch should return error");
}
fn calculate_mse(y_true: &Tensor, y_pred: &Tensor) -> f32 {
let diff = y_pred - y_true;
let squared_diff = &diff * &diff;
squared_diff.sum() / (y_true.len() as f32)
}
fn calculate_accuracy(y_true: &Tensor, y_pred: &Tensor) -> f32 {
let mut correct = 0;
let n_samples = y_true.shape()[0];
for i in 0..n_samples {
let true_class = if y_true[[i, 0]] > y_true[[i, 1]] {
0
} else {
1
};
let pred_class = if y_pred[[i, 0]] > y_pred[[i, 1]] {
0
} else {
1
};
if true_class == pred_class {
correct += 1;
}
}
correct as f32 / n_samples as f32
}