use num_traits::Float;
use crate::neural::{activations::ActivationFn, NeuralError};
#[derive(Clone, Copy)]
struct Lcg64 {
state: u64,
}
impl Lcg64 {
const fn new(seed: u64) -> Self {
Self {
state: seed ^ 0x853c49e6748fea9b,
}
}
fn next_u64(&mut self) -> u64 {
self.state = self
.state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
self.state
}
fn next_f64(&mut self) -> f64 {
let bits = self.next_u64() >> 11;
let unit = (bits as f64) * (1.0 / (1u64 << 53) as f64);
unit * 2.0 - 1.0
}
}
#[derive(Clone, Copy)]
pub struct GradDense<S: Float + Copy, const IN: usize, const OUT: usize> {
pub dw: [[S; IN]; OUT],
pub db: [S; OUT],
}
impl<S: Float + Copy, const IN: usize, const OUT: usize> GradDense<S, IN, OUT> {
pub fn zeros() -> Self {
Self {
dw: [[S::zero(); IN]; OUT],
db: [S::zero(); OUT],
}
}
pub fn accumulate(&mut self, other: &Self) {
for i in 0..OUT {
self.db[i] = self.db[i] + other.db[i];
for j in 0..IN {
self.dw[i][j] = self.dw[i][j] + other.dw[i][j];
}
}
}
pub fn scale(&mut self, factor: S) {
for i in 0..OUT {
self.db[i] = self.db[i] * factor;
for j in 0..IN {
self.dw[i][j] = self.dw[i][j] * factor;
}
}
}
}
#[derive(Clone, Copy)]
pub struct DenseLayer<S, A, const IN: usize, const OUT: usize>
where
S: Float + Copy,
A: ActivationFn<S>,
{
pub weights: [[S; IN]; OUT],
pub biases: [S; OUT],
pub activation: A,
}
impl<S, A, const IN: usize, const OUT: usize> DenseLayer<S, A, IN, OUT>
where
S: Float + Copy,
A: ActivationFn<S>,
{
pub fn new(activation: A) -> Self {
let fan_sum = (IN + OUT) as f64;
let limit = libm::sqrt(6.0 / fan_sum);
let seed = (IN as u64).wrapping_mul(6_364_136_223_846_793_005)
^ (OUT as u64).wrapping_mul(2_654_435_761);
let mut rng = Lcg64::new(seed);
let weights: [[S; IN]; OUT] = core::array::from_fn(|_| {
core::array::from_fn(|_| {
let v = rng.next_f64() * limit;
S::from(v).unwrap_or(S::zero())
})
});
Self {
weights,
biases: [S::zero(); OUT],
activation,
}
}
pub fn with_params(weights: [[S; IN]; OUT], biases: [S; OUT], activation: A) -> Self {
Self {
weights,
biases,
activation,
}
}
pub fn forward(&self, input: &[S; IN]) -> [S; OUT] {
core::array::from_fn(|i| {
let z = self.weights[i]
.iter()
.zip(input.iter())
.fold(self.biases[i], |acc, (&w, &x)| acc + w * x);
self.activation.apply(z)
})
}
pub fn forward_with_preact(&self, input: &[S; IN]) -> ([S; OUT], [S; OUT]) {
let mut pre = [S::zero(); OUT];
let mut out = [S::zero(); OUT];
for (i, (pre_i, out_i)) in pre.iter_mut().zip(out.iter_mut()).enumerate() {
let z = self.weights[i]
.iter()
.zip(input.iter())
.fold(self.biases[i], |acc, (&w, &x)| acc + w * x);
*pre_i = z;
*out_i = self.activation.apply(z);
}
(out, pre)
}
pub fn backward(
&self,
input: &[S; IN],
pre_act: &[S; OUT],
grad_output: &[S; OUT],
) -> ([S; IN], GradDense<S, IN, OUT>) {
let mut delta = [S::zero(); OUT];
for i in 0..OUT {
delta[i] = grad_output[i] * self.activation.derivative(pre_act[i]);
}
let grad_input: [S; IN] = core::array::from_fn(|j| {
self.weights
.iter()
.zip(delta.iter())
.fold(S::zero(), |acc, (row, &d)| acc + row[j] * d)
});
let dw: [[S; IN]; OUT] =
core::array::from_fn(|i| core::array::from_fn(|j| delta[i] * input[j]));
let db: [S; OUT] = core::array::from_fn(|i| delta[i]);
(grad_input, GradDense { dw, db })
}
pub fn update_weights(
&mut self,
grad: &GradDense<S, IN, OUT>,
lr: S,
) -> Result<(), NeuralError> {
for i in 0..OUT {
if !grad.db[i].is_finite() {
return Err(NeuralError::NumericalOverflow);
}
self.biases[i] = self.biases[i] - lr * grad.db[i];
for j in 0..IN {
if !grad.dw[i][j].is_finite() {
return Err(NeuralError::NumericalOverflow);
}
self.weights[i][j] = self.weights[i][j] - lr * grad.dw[i][j];
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::neural::activations::{Linear, Tanh};
fn numerical_grad_input<S, A, const IN: usize, const OUT: usize>(
layer: &DenseLayer<S, A, IN, OUT>,
input: &[S; IN],
loss_grad_out: &[S; OUT],
h: S,
) -> [S; IN]
where
S: Float + Copy,
A: ActivationFn<S>,
{
core::array::from_fn(|j| {
let mut xp = *input;
let mut xm = *input;
xp[j] = xp[j] + h;
xm[j] = xm[j] - h;
let yp = layer.forward(&xp);
let ym = layer.forward(&xm);
let mut acc = S::zero();
for i in 0..OUT {
acc = acc + loss_grad_out[i] * (yp[i] - ym[i]) / (h + h);
}
acc
})
}
#[test]
fn forward_shape_linear() {
let layer = DenseLayer::<f64, Linear<f64>, 3, 2>::new(Linear::new());
let y = layer.forward(&[1.0, 2.0, 3.0]);
assert_eq!(y.len(), 2);
}
#[test]
fn forward_zero_weights_bias_gives_zero_for_linear() {
let layer = DenseLayer::<f64, Linear<f64>, 2, 2>::with_params(
[[0.0; 2]; 2],
[0.0; 2],
Linear::new(),
);
let y = layer.forward(&[5.0, -3.0]);
assert_eq!(y, [0.0, 0.0]);
}
#[test]
fn forward_known_weights() {
let layer = DenseLayer::<f64, Linear<f64>, 2, 2>::with_params(
[[1.0, 0.0], [0.0, 1.0]],
[0.5, -0.5],
Linear::new(),
);
let y = layer.forward(&[3.0, 7.0]);
assert!((y[0] - 3.5).abs() < 1e-12);
assert!((y[1] - 6.5).abs() < 1e-12);
}
#[test]
fn xavier_init_range() {
let layer = DenseLayer::<f64, Tanh<f64>, 4, 4>::new(Tanh::new());
let limit = libm::sqrt(6.0 / 8.0);
for row in &layer.weights {
for &w in row {
assert!(
w.abs() <= limit + 1e-10,
"weight {w} outside Xavier range [{}, {}]",
-limit,
limit
);
}
}
}
#[test]
fn backward_gradient_numerical_check() {
let layer = DenseLayer::<f64, Tanh<f64>, 3, 2>::new(Tanh::new());
let input = [0.5_f64, -0.3, 1.1];
let (_, pre_act) = layer.forward_with_preact(&input);
let grad_out = [1.0_f64, -1.0];
let (analytic_grad_in, _grad) = layer.backward(&input, &pre_act, &grad_out);
let numerical_grad_in = numerical_grad_input(&layer, &input, &grad_out, 1e-5);
for j in 0..3 {
let err = (analytic_grad_in[j] - numerical_grad_in[j]).abs();
assert!(
err < 1e-5,
"grad_input[{j}]: analytic={}, numerical={}, err={}",
analytic_grad_in[j],
numerical_grad_in[j],
err
);
}
}
#[test]
fn backward_gradient_weights_numerical_check() {
let mut layer = DenseLayer::<f64, Tanh<f64>, 2, 2>::new(Tanh::new());
let input = [1.0_f64, -0.5];
let grad_out = [1.0_f64, 0.0];
let h = 1e-5;
let original = layer.weights[0][0];
layer.weights[0][0] = original + h;
let yp = layer.forward(&input);
layer.weights[0][0] = original - h;
let ym = layer.forward(&input);
layer.weights[0][0] = original;
let num_dw00 =
(yp[0] - ym[0]) / (2.0 * h) * grad_out[0] + (yp[1] - ym[1]) / (2.0 * h) * grad_out[1];
let (_, pre_act) = layer.forward_with_preact(&input);
let (_, grad) = layer.backward(&input, &pre_act, &grad_out);
let err = (grad.dw[0][0] - num_dw00).abs();
assert!(
err < 1e-5,
"dw[0][0]: analytic={}, numerical={}, err={}",
grad.dw[0][0],
num_dw00,
err
);
}
#[test]
fn update_weights_reduces_loss() {
let mut layer =
DenseLayer::<f64, Linear<f64>, 1, 1>::with_params([[0.0]], [0.0], Linear::new());
let input = [1.0_f64];
let target = [2.0_f64];
let initial_y = layer.forward(&input)[0];
let initial_loss = (initial_y - target[0]).powi(2);
let lr = 0.1_f64;
for _ in 0..100 {
let (y, pre) = layer.forward_with_preact(&input);
let g_out = [2.0 * (y[0] - target[0])];
let (_, grad) = layer.backward(&input, &pre, &g_out);
layer.update_weights(&grad, lr).expect("update failed");
}
let final_y = layer.forward(&input)[0];
let final_loss = (final_y - target[0]).powi(2);
assert!(
final_loss < initial_loss + 1e-6 || final_loss < 1e-6,
"loss did not decrease: initial={initial_loss}, final={final_loss}"
);
}
}