mod matmul;
pub use self::matmul::matmul;
mod relu;
pub use self::relu::relu;
mod sgd;
pub use self::sgd::sgd;
mod mse_loss;
pub use self::mse_loss::mse_loss;
#[cfg(test)]
mod tests {
use super::*;
use crate::nn::tensors::{Tensor, WithGrad};
use crate::nn::TensorFloat;
use tensor_optim::TensorOps;
#[test]
fn matmul_forward_and_backward_produces_correct_shapes_and_values() {
let a_data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]; let b_data = [7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
let a = WithGrad::new(Tensor::new(&[2, 3], &a_data));
let b = WithGrad::new(Tensor::new(&[3, 2], &b_data));
let (out, back) = matmul(&a, &b);
assert_eq!(out.shape(), &[2, 2]);
let expected = [
1.0 * 7.0 + 2.0 * 9.0 + 3.0 * 11.0,
1.0 * 8.0 + 2.0 * 10.0 + 3.0 * 12.0,
4.0 * 7.0 + 5.0 * 9.0 + 6.0 * 11.0,
4.0 * 8.0 + 5.0 * 10.0 + 6.0 * 12.0,
];
assert_eq!(out.data(), &expected);
let grad_output = Tensor::new(&[2, 2], &[1.0, 0.0, 0.0, 1.0]);
#[cfg(not(feature = "alloc"))]
let (grad_a, grad_b) = back.call(grad_output);
#[cfg(feature = "alloc")]
let (grad_a, grad_b) = back(grad_output);
assert_eq!(grad_a.shape(), &[2, 3]);
assert_eq!(grad_b.shape(), &[3, 2]);
assert!(grad_a.data().iter().all(|x: &TensorFloat| x.is_finite()));
assert!(grad_b.data().iter().all(|x: &TensorFloat| x.is_finite()));
}
#[test]
#[should_panic(expected = "inner dimensions must match for matmul")] fn matmul_panics_on_invalid_shape() {
let a = WithGrad::new(Tensor::new(&[2, 3], &[1.0; 6]));
let b = WithGrad::new(Tensor::new(&[4, 2], &[1.0; 8]));
#[cfg(feature = "dyntensor")]
let _ = matmul(&a, &b);
#[cfg(not(feature = "dyntensor"))]
let _ = matmul::<6, 8, 4, 2>(&a, &b);
}
#[test]
#[cfg(feature = "alloc")]
fn mse_loss_forward_and_backward_matches_expected() {
let pred_data = [1.0, 2.0, 3.0];
let target_data = [1.0, 3.0, 2.0];
let prediction = WithGrad::new(Tensor::new(&[3], &pred_data));
let target = Tensor::new(&[3], &target_data);
let (loss, back) = mse_loss(&prediction, &target);
let expected_loss = ((1.0 as TensorFloat - 1.0).powi(2)
+ (2.0 as TensorFloat - 3.0).powi(2)
+ (3.0 as TensorFloat - 2.0).powi(2))
/ 3.0;
assert!((loss - expected_loss).abs() < 1e-12);
#[cfg(feature = "alloc")]
let grad = back(1.0);
#[cfg(not(feature = "alloc"))]
let grad = back.call(1.0);
let expected_grad: alloc::vec::Vec<_> = pred_data
.iter()
.zip(target_data.iter())
.map(|(&y, &t)| 2.0 * (y - t) / 3.0)
.collect();
assert_eq!(grad.shape(), &[3]);
grad.data()
.iter()
.zip(expected_grad.iter())
.for_each(|(&g, &e)| assert!((g - e).abs() < 1e-12));
}
#[test]
fn relu_forward_and_backward() {
let input_data = [-1.0, 0.0, 1.0, 2.0];
let input = WithGrad::new(Tensor::new(&[4], &input_data));
let (out, back) = relu(&input);
let expected_out = [0.0, 0.0, 1.0, 2.0];
assert_eq!(out.data(), &expected_out);
let grad_output = Tensor::new(&[4], &[1.0; 4]);
#[cfg(feature = "alloc")]
let grad_input = back(grad_output);
#[cfg(not(feature = "alloc"))]
let grad_input = back.call(grad_output);
let expected_grad = [0.0, 0.0, 1.0, 1.0];
assert_eq!(grad_input.data(), &expected_grad);
}
#[test]
fn sgd_updates_parameters_and_zeros_gradients() {
let init_params = [10.0, 20.0, 30.0];
let mut wg = WithGrad::new(Tensor::new(&[3], &init_params));
wg.set_grad(Tensor::new(&[3], &[1.0, 2.0, 3.0]));
sgd(&mut wg, 0.1);
let expected_params = [10.0 - 0.1 * 1.0, 20.0 - 0.1 * 2.0, 30.0 - 0.1 * 3.0];
assert_eq!(wg.get_value().data(), &expected_params);
assert!(wg.get_grad().data().iter().all(|&g| g == 0.0));
}
}