use approx::assert_abs_diff_eq;
use ndarray::{Array, Array2, IxDyn};
use rustyml::neural_network::Tensor;
use rustyml::neural_network::layers::Activation;
use rustyml::neural_network::layers::dense::Dense;
use rustyml::neural_network::layers::regularization::dropout::dropout::Dropout;
use rustyml::neural_network::losses::{CategoricalCrossEntropy, MeanSquaredError};
use rustyml::neural_network::optimizers::{Adam, SGD};
use rustyml::neural_network::sequential::Sequential;
use rustyml::neural_network::traits::Loss;
use rustyml::{error::Error, neural_network::NnError};
fn t2(rows: usize, cols: usize, data: Vec<f32>) -> Tensor {
Array2::from_shape_vec((rows, cols), data)
.unwrap()
.into_dyn()
}
#[test]
fn test_predict_identity_weights_linear_dense() {
let mut dense = Dense::new(2, 2, Activation::Linear).unwrap();
let w = Array2::from_shape_vec((2, 2), vec![1.0_f32, 0.0, 0.0, 1.0]).unwrap();
let b = Array2::from_shape_vec((1, 2), vec![0.0_f32, 0.0]).unwrap();
dense.set_weights(w, b).unwrap();
let mut model = Sequential::new();
model.add(dense);
let x = t2(1, 2, vec![3.0, 4.0]);
let out = model.predict(&x).unwrap();
assert_abs_diff_eq!(out[[0, 0]], 3.0_f32, epsilon = 1e-6);
assert_abs_diff_eq!(out[[0, 1]], 4.0_f32, epsilon = 1e-6);
}
#[test]
fn test_predict_scalar_affine() {
let mut dense = Dense::new(1, 1, Activation::Linear).unwrap();
let w = Array2::from_shape_vec((1, 1), vec![2.0_f32]).unwrap();
let b = Array2::from_shape_vec((1, 1), vec![1.0_f32]).unwrap();
dense.set_weights(w, b).unwrap();
let mut model = Sequential::new();
model.add(dense);
let x = t2(1, 1, vec![5.0]);
let out = model.predict(&x).unwrap();
assert_abs_diff_eq!(out[[0, 0]], 11.0_f32, epsilon = 1e-6);
}
#[test]
fn test_predict_2d_linear_transform() {
let mut dense = Dense::new(3, 2, Activation::Linear).unwrap();
let w = Array2::from_shape_vec(
(3, 2),
vec![
1.0_f32, 0.0, 0.0, 1.0, 1.0, 1.0, ],
)
.unwrap();
let b = Array2::from_shape_vec((1, 2), vec![0.0_f32, 0.0]).unwrap();
dense.set_weights(w, b).unwrap();
let mut model = Sequential::new();
model.add(dense);
let x = t2(1, 3, vec![2.0, 3.0, 4.0]);
let out = model.predict(&x).unwrap();
assert_abs_diff_eq!(out[[0, 0]], 6.0_f32, epsilon = 1e-5);
assert_abs_diff_eq!(out[[0, 1]], 7.0_f32, epsilon = 1e-5);
}
#[test]
fn test_predict_two_layer_stack() {
let mut d1 = Dense::new(3, 2, Activation::Linear).unwrap();
let w1 = Array2::from_shape_vec((3, 2), vec![1.0_f32, 0.0, 0.0, 1.0, 0.0, 0.0]).unwrap();
let b1 = Array2::from_shape_vec((1, 2), vec![0.0_f32, 0.0]).unwrap();
d1.set_weights(w1, b1).unwrap();
let mut d2 = Dense::new(2, 1, Activation::Linear).unwrap();
let w2 = Array2::from_shape_vec((2, 1), vec![1.0_f32, 1.0]).unwrap();
let b2 = Array2::from_shape_vec((1, 1), vec![0.0_f32]).unwrap();
d2.set_weights(w2, b2).unwrap();
let mut model = Sequential::new();
model.add(d1).add(d2);
let x = t2(1, 3, vec![5.0, 7.0, 99.0]);
let out = model.predict(&x).unwrap();
assert_abs_diff_eq!(out[[0, 0]], 12.0_f32, epsilon = 1e-5);
}
#[test]
fn test_predict_dense_softmax_equal_input() {
let mut dense = Dense::new(1, 3, Activation::Softmax).unwrap();
let w = Array2::from_shape_vec((1, 3), vec![1.0_f32, 2.0, 3.0]).unwrap();
let b = Array2::from_shape_vec((1, 3), vec![0.0_f32, 0.0, 0.0]).unwrap();
dense.set_weights(w, b).unwrap();
let mut model = Sequential::new();
model.add(dense);
let x = t2(1, 1, vec![0.0]);
let out = model.predict(&x).unwrap();
let third = 1.0_f32 / 3.0;
assert_abs_diff_eq!(out[[0, 0]], third, epsilon = 1e-5);
assert_abs_diff_eq!(out[[0, 1]], third, epsilon = 1e-5);
assert_abs_diff_eq!(out[[0, 2]], third, epsilon = 1e-5);
let sum: f32 = out.iter().sum();
assert_abs_diff_eq!(sum, 1.0_f32, epsilon = 1e-6);
}
#[test]
fn test_predict_dense_softmax_known_probs() {
let mut dense = Dense::new(1, 3, Activation::Softmax).unwrap();
let w: Array2<f32> = Array2::zeros((1, 3));
let b: Array2<f32> = Array2::zeros((1, 3));
dense.set_weights(w, b).unwrap();
let mut model = Sequential::new();
model.add(dense);
let x = t2(1, 1, vec![99.0]);
let out = model.predict(&x).unwrap();
let third = 1.0_f32 / 3.0;
assert_abs_diff_eq!(out[[0, 0]], third, epsilon = 1e-5);
assert_abs_diff_eq!(out[[0, 1]], third, epsilon = 1e-5);
assert_abs_diff_eq!(out[[0, 2]], third, epsilon = 1e-5);
}
#[test]
fn test_predict_is_deterministic() {
let mut dense = Dense::new(3, 2, Activation::Linear).unwrap();
let w = Array2::from_shape_vec((3, 2), vec![0.1_f32, 0.2, 0.3, 0.4, 0.5, 0.6]).unwrap();
let b = Array2::from_shape_vec((1, 2), vec![0.01_f32, -0.02]).unwrap();
dense.set_weights(w, b).unwrap();
let mut model = Sequential::new();
model.add(dense);
let x = t2(1, 3, vec![1.0, -1.0, 2.0]);
let out1 = model.predict(&x).unwrap();
let out2 = model.predict(&x).unwrap();
crate::common::assert_allclose(&out1, &out2, 0.0_f32);
}
#[test]
fn test_summary_does_not_panic() {
let mut model = Sequential::new();
model
.add(Dense::new(4, 8, Activation::ReLU).unwrap())
.add(Dense::new(8, 2, Activation::Softmax).unwrap());
model.summary();
}
#[test]
fn test_fit_before_compile_returns_not_compiled() {
let mut model = Sequential::new();
model.add(Dense::new(2, 1, Activation::Linear).unwrap());
let x = t2(2, 2, vec![1.0, 0.0, 0.0, 1.0]);
let y = t2(2, 1, vec![1.0, 0.0]);
assert!(
matches!(
model.fit(&x, &y, 1),
Err(Error::NeuralNetwork(NnError::NotCompiled(_)))
),
"expected NotCompiled"
);
}
#[test]
fn test_fit_empty_model_returns_empty_model_error() {
let mut model = Sequential::new();
model.compile(
SGD::new(0.01, 0.0, false, 0.0).unwrap(),
MeanSquaredError::new(),
);
let x = t2(2, 2, vec![1.0, 0.0, 0.0, 1.0]);
let y = t2(2, 1, vec![1.0, 0.0]);
assert!(
matches!(
model.fit(&x, &y, 1),
Err(Error::NeuralNetwork(NnError::EmptyModel))
),
"expected EmptyModel"
);
}
#[test]
fn test_predict_empty_model_returns_empty_model_error() {
let model = Sequential::new();
let x = t2(1, 2, vec![1.0, 2.0]);
let err = model.predict(&x).unwrap_err();
assert!(
matches!(err, Error::NeuralNetwork(NnError::EmptyModel)),
"expected EmptyModel, got: {err:?}"
);
}
#[test]
fn test_fit_empty_x_returns_empty_input_error() {
let mut model = Sequential::new();
model
.add(Dense::new(2, 1, Activation::Linear).unwrap())
.compile(
SGD::new(0.01, 0.0, false, 0.0).unwrap(),
MeanSquaredError::new(),
);
let x: Tensor = Array::zeros((0, 2)).into_dyn();
let y: Tensor = Array::zeros((0, 1)).into_dyn();
assert!(
matches!(model.fit(&x, &y, 1), Err(Error::EmptyInput(_))),
"expected EmptyInput"
);
}
#[test]
fn test_fit_batch_size_mismatch_returns_dimension_mismatch() {
let mut model = Sequential::new();
model
.add(Dense::new(2, 1, Activation::Linear).unwrap())
.compile(
SGD::new(0.01, 0.0, false, 0.0).unwrap(),
MeanSquaredError::new(),
);
let x = t2(3, 2, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
let y = t2(2, 1, vec![1.0, 2.0]); assert!(
matches!(model.fit(&x, &y, 1), Err(Error::DimensionMismatch { .. })),
"expected DimensionMismatch"
);
}
#[test]
fn test_predict_empty_x_returns_empty_input_error() {
let mut model = Sequential::new();
model.add(Dense::new(2, 1, Activation::Linear).unwrap());
let x: Tensor = Array::zeros((0, 2)).into_dyn();
let err = model.predict(&x).unwrap_err();
assert!(
matches!(err, Error::EmptyInput(_)),
"expected EmptyInput, got: {err:?}"
);
}
#[test]
fn test_fit_with_batches_zero_batch_size_returns_invalid_parameter() {
let mut model = Sequential::new();
model
.add(Dense::new(2, 1, Activation::Linear).unwrap())
.compile(
SGD::new(0.01, 0.0, false, 0.0).unwrap(),
MeanSquaredError::new(),
);
let x = t2(4, 2, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]);
let y = t2(4, 1, vec![1.0, 2.0, 3.0, 4.0]);
assert!(
matches!(
model.fit_with_batches(&x, &y, 1, 0),
Err(Error::InvalidParameter { .. })
),
"expected InvalidParameter"
);
}
#[test]
fn test_fit_with_batches_batch_size_exceeds_samples_returns_invalid_parameter() {
let mut model = Sequential::new();
model
.add(Dense::new(2, 1, Activation::Linear).unwrap())
.compile(
SGD::new(0.01, 0.0, false, 0.0).unwrap(),
MeanSquaredError::new(),
);
let x = t2(3, 2, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
let y = t2(3, 1, vec![1.0, 2.0, 3.0]);
assert!(
matches!(
model.fit_with_batches(&x, &y, 1, 100),
Err(Error::InvalidParameter { .. })
),
"expected InvalidParameter"
);
}
#[test]
fn test_fit_zero_epochs_unchanged_weights() {
let mut dense = Dense::new(1, 1, Activation::Linear).unwrap();
let w = Array2::from_shape_vec((1, 1), vec![3.0_f32]).unwrap();
let b = Array2::from_shape_vec((1, 1), vec![0.0_f32]).unwrap();
dense.set_weights(w, b).unwrap();
let mut model = Sequential::new();
model.add(dense).compile(
SGD::new(0.01, 0.0, false, 0.0).unwrap(),
MeanSquaredError::new(),
);
let x = t2(1, 1, vec![2.0]);
let y = t2(1, 1, vec![10.0]);
model.fit(&x, &y, 0).unwrap();
let out = model.predict(&x).unwrap();
assert_abs_diff_eq!(out[[0, 0]], 6.0_f32, epsilon = 1e-5);
}
#[test]
fn test_convergence_linear_regression_y_eq_2x_plus_1() {
let x = t2(4, 1, vec![1.0, 2.0, 3.0, 4.0]);
let y = t2(4, 1, vec![3.0, 5.0, 7.0, 9.0]);
let mut model = Sequential::new();
model
.add(
Dense::new(1, 1, Activation::Linear)
.unwrap()
.with_random_state(0),
)
.compile(
SGD::new(0.01, 0.0, false, 0.0).unwrap(),
MeanSquaredError::new(),
);
model.fit(&x, &y, 300).unwrap();
let x_test = t2(1, 1, vec![3.0]);
let pred = model.predict(&x_test).unwrap();
assert_abs_diff_eq!(pred[[0, 0]], 7.0_f32, epsilon = 0.5);
}
#[test]
fn test_convergence_linear_regression_with_batches() {
let x = t2(4, 1, vec![1.0, 2.0, 3.0, 4.0]);
let y = t2(4, 1, vec![3.0, 5.0, 7.0, 9.0]);
let mut model = Sequential::new_with_seed(0);
model
.add(
Dense::new(1, 1, Activation::Linear)
.unwrap()
.with_random_state(0),
)
.compile(
SGD::new(0.01, 0.0, false, 0.0).unwrap(),
MeanSquaredError::new(),
);
model.fit_with_batches(&x, &y, 500, 2).unwrap();
let x_test = t2(1, 1, vec![2.0]);
let pred = model.predict(&x_test).unwrap();
assert_abs_diff_eq!(pred[[0, 0]], 5.0_f32, epsilon = 1.0);
}
#[test]
fn test_convergence_2class_softmax_adam() {
#[rustfmt::skip]
let x = t2(8, 2, vec![
1.0, 0.0, 0.9, 0.1,
0.8, 0.2,
0.7, 0.3,
0.0, 1.0, 0.1, 0.9,
0.2, 0.8,
0.3, 0.7,
]);
#[rustfmt::skip]
let y = t2(8, 2, vec![
1.0, 0.0,
1.0, 0.0,
1.0, 0.0,
1.0, 0.0,
0.0, 1.0,
0.0, 1.0,
0.0, 1.0,
0.0, 1.0,
]);
let mut model = Sequential::new();
model
.add(
Dense::new(2, 8, Activation::Tanh)
.unwrap()
.with_random_state(0),
)
.add(
Dense::new(8, 2, Activation::Softmax)
.unwrap()
.with_random_state(0),
)
.compile(
Adam::new(0.01, 0.9, 0.999, 1e-8, 0.0).unwrap(),
CategoricalCrossEntropy::new(false),
);
model.fit(&x, &y, 600).unwrap();
let x0 = t2(1, 2, vec![0.9, 0.1]);
let p0 = model.predict(&x0).unwrap();
assert!(
p0[[0, 0]] > 0.7,
"class-0 point: expected p(class_0) > 0.7, got {}",
p0[[0, 0]]
);
let x1 = t2(1, 2, vec![0.1, 0.9]);
let p1 = model.predict(&x1).unwrap();
assert!(
p1[[0, 1]] > 0.7,
"class-1 point: expected p(class_1) > 0.7, got {}",
p1[[0, 1]]
);
}
#[test]
fn test_predict_deterministic_after_training() {
let x = t2(2, 2, vec![1.0, 0.0, 0.0, 1.0]);
let y = t2(2, 2, vec![1.0, 0.0, 0.0, 1.0]);
let mut model = Sequential::new();
model
.add(Dense::new(2, 2, Activation::Linear).unwrap())
.compile(
SGD::new(0.01, 0.0, false, 0.0).unwrap(),
MeanSquaredError::new(),
);
model.fit(&x, &y, 5).unwrap();
let x_test = t2(1, 2, vec![3.0, -1.5]);
let p1 = model.predict(&x_test).unwrap();
let p2 = model.predict(&x_test).unwrap();
crate::common::assert_allclose(&p1, &p2, 0.0_f32);
}
#[test]
fn test_fit_history_has_one_loss_per_epoch() {
let mut model = Sequential::new();
model
.add(Dense::new(1, 1, Activation::Linear).unwrap())
.compile(
SGD::new(0.01, 0.0, false, 0.0).unwrap(),
MeanSquaredError::new(),
);
let x = t2(4, 1, vec![1.0, 2.0, 3.0, 4.0]);
let y = t2(4, 1, vec![3.0, 5.0, 7.0, 9.0]);
let history = model.fit(&x, &y, 25).unwrap();
assert_eq!(history.loss().len(), 25, "one entry per epoch");
assert!(
history.loss().iter().all(|l| l.is_finite()),
"every recorded loss is finite: {:?}",
history.loss()
);
assert!(
history.loss()[24] < history.loss()[0],
"25 epochs of gradient descent must reduce the loss: {} -> {}",
history.loss()[0],
history.loss()[24]
);
}
#[test]
fn test_fit_zero_epochs_yields_empty_history() {
let mut model = Sequential::new();
model
.add(Dense::new(1, 1, Activation::Linear).unwrap())
.compile(
SGD::new(0.01, 0.0, false, 0.0).unwrap(),
MeanSquaredError::new(),
);
let x = t2(2, 1, vec![1.0, 2.0]);
let y = t2(2, 1, vec![1.0, 2.0]);
assert!(model.fit(&x, &y, 0).unwrap().loss().is_empty());
}
#[test]
fn test_fit_history_records_the_pre_update_loss() {
let mut model = Sequential::new();
model
.add(Dense::new(1, 1, Activation::Linear).unwrap())
.compile(
SGD::new(0.01, 0.0, false, 0.0).unwrap(),
MeanSquaredError::new(),
);
let x = t2(4, 1, vec![1.0, 2.0, 3.0, 4.0]);
let y = t2(4, 1, vec![3.0, 5.0, 7.0, 9.0]);
let before = model.evaluate(&x, &y).unwrap();
let history = model.fit(&x, &y, 10).unwrap();
let after = model.evaluate(&x, &y).unwrap();
assert_abs_diff_eq!(history.loss()[0], before, epsilon = 1e-5);
assert!(
after < history.loss()[9],
"the trained model's loss ({}) is below the last recorded epoch loss ({}), which was \
measured one update earlier",
after,
history.loss()[9]
);
}
#[test]
fn test_fit_with_batches_weights_batches_by_sample_count() {
let x = t2(5, 1, vec![1.0, 1.0, 1.0, 1.0, 1.0]);
let y = t2(5, 1, vec![0.0, 1.0, 4.0, 9.0, 100.0]);
for seed in 0..5_u64 {
let mut model = Sequential::new_with_seed(seed);
model
.add(
Dense::new(1, 1, Activation::Linear)
.unwrap()
.with_random_state(7),
)
.compile(
SGD::new(1e-30, 0.0, false, 0.0).unwrap(),
MeanSquaredError::new(),
);
let predictions = model.predict(&x).unwrap();
let per_sample: Vec<f32> = predictions
.iter()
.zip(y.iter())
.map(|(p, t)| (p - t) * (p - t))
.collect();
let total: f32 = per_sample.iter().sum();
let weighted = total / 5.0;
for (i, &l) in per_sample.iter().enumerate() {
assert!(
(l - weighted).abs() > 1.0,
"sample {i}'s loss ({l}) must differ from the mean ({weighted}), or the two \
aggregation rules would coincide and the test would prove nothing"
);
}
let history = model.fit_with_batches(&x, &y, 1, 2).unwrap();
assert_abs_diff_eq!(model.evaluate(&x, &y).unwrap(), weighted, epsilon = 1e-3);
assert_abs_diff_eq!(history.loss()[0], weighted, epsilon = 1e-3);
for &l in &per_sample {
let unweighted = (total + l) / 6.0;
assert!(
(history.loss()[0] - unweighted).abs() > 1e-2,
"seed {seed}: the epoch loss ({}) must not be a plain mean over batches ({})",
history.loss()[0],
unweighted
);
}
}
}
#[test]
fn test_fit_with_batches_full_batch_equivalent() {
let x = t2(4, 1, vec![1.0, 2.0, 3.0, 4.0]);
let y = t2(4, 1, vec![3.0, 5.0, 7.0, 9.0]);
let mut model = Sequential::new();
model
.add(Dense::new(1, 1, Activation::Linear).unwrap())
.compile(
SGD::new(0.01, 0.0, false, 0.0).unwrap(),
MeanSquaredError::new(),
);
model.fit_with_batches(&x, &y, 400, 4).unwrap();
let x_test = t2(1, 1, vec![4.0]);
let pred = model.predict(&x_test).unwrap();
assert_abs_diff_eq!(pred[[0, 0]], 9.0_f32, epsilon = 1.0);
}
#[test]
fn test_train_batch_reproduces_fit() {
let x = t2(4, 1, vec![1.0, 2.0, 3.0, 4.0]);
let y = t2(4, 1, vec![3.0, 5.0, 7.0, 9.0]);
let build = || {
let mut model = Sequential::new();
model
.add(
Dense::new(1, 1, Activation::Linear)
.unwrap()
.with_random_state(11),
)
.compile(
SGD::new(0.01, 0.0, false, 0.0).unwrap(),
MeanSquaredError::new(),
);
model
};
let mut looped = build();
let history = looped.fit(&x, &y, 3).unwrap();
let mut stepped = build();
let manual: Vec<f32> = (0..3)
.map(|_| stepped.train_batch(&x, &y).unwrap())
.collect();
assert_eq!(history.loss(), manual.as_slice());
crate::common::assert_allclose(
&looped.predict(&x).unwrap(),
&stepped.predict(&x).unwrap(),
0.0_f32,
);
}
#[test]
fn test_train_batch_returns_the_pre_update_loss() {
let mut model = Sequential::new();
model
.add(Dense::new(1, 1, Activation::Linear).unwrap())
.compile(
SGD::new(0.01, 0.0, false, 0.0).unwrap(),
MeanSquaredError::new(),
);
let x = t2(4, 1, vec![1.0, 2.0, 3.0, 4.0]);
let y = t2(4, 1, vec![3.0, 5.0, 7.0, 9.0]);
let before = model.evaluate(&x, &y).unwrap();
let reported = model.train_batch(&x, &y).unwrap();
let after = model.evaluate(&x, &y).unwrap();
assert_abs_diff_eq!(reported, before, epsilon = 1e-5);
assert!(
after < reported,
"the update the call performed must lower the loss it reported: {reported} -> {after}"
);
}
#[test]
fn test_train_batch_on_uncompiled_model_is_an_error() {
let mut model = Sequential::new();
model.add(Dense::new(1, 1, Activation::Linear).unwrap());
let x = t2(2, 1, vec![1.0, 2.0]);
let y = t2(2, 1, vec![1.0, 2.0]);
assert!(
matches!(
model.train_batch(&x, &y),
Err(Error::NeuralNetwork(NnError::NotCompiled(_)))
),
"expected NotCompiled"
);
}
#[test]
fn test_evaluate_matches_predict_then_compute_loss() {
let mut model = Sequential::new();
model
.add(Dense::new(2, 3, Activation::ReLU).unwrap())
.add(Dense::new(3, 1, Activation::Linear).unwrap())
.compile(
SGD::new(0.01, 0.0, false, 0.0).unwrap(),
MeanSquaredError::new(),
);
let x = t2(3, 2, vec![1.0, -0.5, 0.25, 2.0, -1.0, 0.5]);
let y = t2(3, 1, vec![1.0, -2.0, 0.5]);
let manual = MeanSquaredError::new()
.compute_loss(&y, &model.predict(&x).unwrap())
.unwrap();
assert_abs_diff_eq!(model.evaluate(&x, &y).unwrap(), manual, epsilon = 1e-6);
assert_abs_diff_eq!(model.evaluate(&x, &y).unwrap(), manual, epsilon = 0.0_f32);
}
#[test]
fn test_evaluate_runs_in_inference_mode() {
let mut model = Sequential::new();
model
.add(Dense::new(4, 32, Activation::Linear).unwrap())
.add(Dropout::new(0.5, vec![16, 32]).unwrap())
.add(Dense::new(32, 1, Activation::Linear).unwrap())
.compile(
SGD::new(1e-30, 0.0, false, 0.0).unwrap(),
MeanSquaredError::new(),
);
let x = Array::linspace(-1.0, 1.0, 64)
.into_shape_with_order((16, 4))
.unwrap()
.into_dyn();
let y = Array::linspace(0.0, 1.0, 16)
.into_shape_with_order((16, 1))
.unwrap()
.into_dyn();
let eval_first = model.evaluate(&x, &y).unwrap();
let eval_second = model.evaluate(&x, &y).unwrap();
assert_abs_diff_eq!(eval_first, eval_second, epsilon = 0.0_f32);
let train_first = model.train_batch(&x, &y).unwrap();
let train_second = model.train_batch(&x, &y).unwrap();
assert!(
(train_first - train_second).abs() > 1e-6,
"training mode must resample the dropout mask, but two passes agreed: \
{train_first} and {train_second}"
);
assert!(
(train_first - eval_first).abs() > 1e-6,
"a dropout model's training loss ({train_first}) must not match its inference loss \
({eval_first})"
);
}
#[test]
fn test_evaluate_on_uncompiled_model_reports_the_missing_loss() {
let mut model = Sequential::new();
model.add(Dense::new(1, 1, Activation::Linear).unwrap());
let x = t2(2, 1, vec![1.0, 2.0]);
let y = t2(2, 1, vec![1.0, 2.0]);
assert!(
matches!(
model.evaluate(&x, &y),
Err(Error::NeuralNetwork(NnError::NotCompiled("loss function")))
),
"expected NotCompiled(\"loss function\")"
);
}
#[test]
fn test_rank_zero_inputs_are_rejected() {
let mut model = Sequential::new();
model
.add(Dense::new(1, 1, Activation::Linear).unwrap())
.compile(
SGD::new(0.01, 0.0, false, 0.0).unwrap(),
MeanSquaredError::new(),
);
let scalar: Tensor = Array::from_elem(IxDyn(&[]), 1.0_f32);
let y = t2(1, 1, vec![1.0]);
assert!(matches!(
model.fit(&scalar, &y, 1),
Err(Error::InvalidInput(_))
));
assert!(matches!(
model.evaluate(&y, &scalar),
Err(Error::InvalidInput(_))
));
}
#[test]
fn test_learning_rate_reads_back_through_the_model() {
let mut model = Sequential::new();
assert_eq!(model.learning_rate(), None);
model
.add(Dense::new(1, 1, Activation::Linear).unwrap())
.compile(
Adam::new(0.003, 0.9, 0.999, 1e-8, 0.0).unwrap(),
MeanSquaredError::new(),
);
assert_eq!(model.learning_rate(), Some(0.003));
let halved = model.learning_rate().unwrap() * 0.5;
model.set_learning_rate(halved);
assert_eq!(model.learning_rate(), Some(0.0015));
}
#[test]
fn test_batch_losses_and_epoch_mean_match_keras() {
let mut dense = Dense::new(1, 1, Activation::Linear).unwrap();
dense
.set_weights(
Array2::from_shape_vec((1, 1), vec![0.5_f32]).unwrap(),
Array2::from_shape_vec((1, 1), vec![0.0_f32]).unwrap(),
)
.unwrap();
let mut model = Sequential::new();
model.add(dense).compile(
SGD::new(0.1, 0.0, false, 0.0).unwrap(),
MeanSquaredError::new(),
);
let x = t2(5, 1, vec![0.0, 1.0, 2.0, 3.0, 4.0]);
let y = t2(5, 1, vec![0.0, 2.0, 4.0, 6.0, 8.0]);
assert_abs_diff_eq!(model.evaluate(&x, &y).unwrap(), 13.5_f32, epsilon = 1e-5);
let batches = [
(
t2(2, 1, vec![0.0, 1.0]),
t2(2, 1, vec![0.0, 2.0]),
1.125_f32,
),
(
t2(2, 1, vec![2.0, 3.0]),
t2(2, 1, vec![4.0, 6.0]),
10.856_251_f32,
),
(t2(1, 1, vec![4.0]), t2(1, 1, vec![8.0]), 4.473_228_f32),
];
let mut weighted = 0.0_f64;
let mut samples = 0_usize;
for (batch_x, batch_y, expected) in &batches {
let loss = model.train_batch(batch_x, batch_y).unwrap();
assert_abs_diff_eq!(loss, *expected, epsilon = 1e-4);
weighted += loss as f64 * batch_x.shape()[0] as f64;
samples += batch_x.shape()[0];
}
assert_abs_diff_eq!(
(weighted / samples as f64) as f32,
5.687_146_f32,
epsilon = 1e-4
);
assert_abs_diff_eq!(model.evaluate(&x, &y).unwrap(), 9.242_0_f32, epsilon = 1e-4);
}