#![allow(clippy::needless_range_loop)]
pub fn relu(x: f64) -> f64 {
x.max(0.0)
}
pub fn sigmoid(x: f64) -> f64 {
1.0 / (1.0 + (-x).exp())
}
pub fn softmax(x: &[f64]) -> Vec<f64> {
if x.is_empty() {
return Vec::new();
}
let max_val = x.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let exps: Vec<f64> = x.iter().map(|&v| (v - max_val).exp()).collect();
let sum: f64 = exps.iter().sum();
exps.iter().map(|&e| e / sum).collect()
}
pub fn mse_loss(pred: &[f64], target: &[f64]) -> f64 {
if pred.is_empty() {
return 0.0;
}
let n = pred.len().min(target.len());
let sum: f64 = pred[..n]
.iter()
.zip(target[..n].iter())
.map(|(p, t)| (p - t).powi(2))
.sum();
sum / n as f64
}
#[derive(Debug, Clone, PartialEq)]
pub enum LayerType {
Dense,
Conv1D,
ReLU,
Sigmoid,
Tanh,
Softmax,
BatchNorm,
Dropout,
}
#[derive(Debug, Clone)]
pub struct NeuralLayer {
pub weights: Vec<f64>,
pub biases: Vec<f64>,
pub layer_type: LayerType,
pub input_size: usize,
pub output_size: usize,
}
impl NeuralLayer {
pub fn new(input_size: usize, output_size: usize, layer_type: LayerType) -> Self {
Self {
weights: vec![0.0; input_size * output_size],
biases: vec![0.0; output_size],
layer_type,
input_size,
output_size,
}
}
pub fn forward(&self, input: &[f64]) -> Vec<f64> {
match self.layer_type {
LayerType::ReLU => input.iter().map(|&x| relu(x)).collect(),
LayerType::Sigmoid => input.iter().map(|&x| sigmoid(x)).collect(),
LayerType::Tanh => input.iter().map(|&x| x.tanh()).collect(),
LayerType::Softmax => softmax(input),
LayerType::BatchNorm => {
let n = input.len();
if n == 0 {
return Vec::new();
}
let mean = input.iter().sum::<f64>() / n as f64;
let var = input.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / n as f64;
let std = (var + 1e-5).sqrt();
input
.iter()
.enumerate()
.map(|(i, &x)| {
let gamma = self.weights.get(i).copied().unwrap_or(1.0);
let beta = self.biases.get(i).copied().unwrap_or(0.0);
gamma * (x - mean) / std + beta
})
.collect()
}
LayerType::Dropout => {
input.to_vec()
}
LayerType::Dense | LayerType::Conv1D => {
let in_sz = input.len();
let out_sz = self.output_size;
let mut out = vec![0.0; out_sz];
for j in 0..out_sz {
let mut acc = self.biases.get(j).copied().unwrap_or(0.0);
for i in 0..in_sz {
let w = self.weights.get(j * in_sz + i).copied().unwrap_or(0.0);
acc += w * input[i];
}
out[j] = acc;
}
out
}
}
}
}
#[derive(Debug, Clone)]
pub struct GpuNeuralNet {
pub layers: Vec<NeuralLayer>,
}
impl GpuNeuralNet {
pub fn new() -> Self {
Self { layers: Vec::new() }
}
pub fn add_layer(&mut self, layer: NeuralLayer) {
self.layers.push(layer);
}
pub fn forward_pass(&self, input: &[f64]) -> Vec<f64> {
let mut current = input.to_vec();
for layer in &self.layers {
current = layer.forward(¤t);
}
current
}
pub fn batch_forward(&self, inputs: &[Vec<f64>]) -> Vec<Vec<f64>> {
inputs.iter().map(|inp| self.forward_pass(inp)).collect()
}
}
impl Default for GpuNeuralNet {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct BackpropGpu {
pub gradients: Vec<Vec<f64>>,
}
impl BackpropGpu {
pub fn new(net: &GpuNeuralNet) -> Self {
let gradients = net
.layers
.iter()
.map(|l| vec![0.0; l.weights.len()])
.collect();
Self { gradients }
}
pub fn backward_pass(&mut self, loss_grad: &[f64]) {
let scale = loss_grad.first().copied().unwrap_or(0.0);
for grad_buf in &mut self.gradients {
for g in grad_buf.iter_mut() {
*g = scale;
}
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum OptimizerType {
Sgd,
Adam,
}
#[derive(Debug, Clone)]
pub struct AdamOptimizer {
pub beta1: f64,
pub beta2: f64,
pub eps: f64,
pub lr: f64,
pub m: Vec<f64>,
pub v: Vec<f64>,
pub t: u64,
}
impl AdamOptimizer {
pub fn new(n: usize, lr: f64, beta1: f64, beta2: f64, eps: f64) -> Self {
Self {
beta1,
beta2,
eps,
lr,
m: vec![0.0; n],
v: vec![0.0; n],
t: 0,
}
}
pub fn update(&mut self, params: &mut [f64], grads: &[f64]) {
self.t += 1;
let t = self.t as f64;
let lr_t = self.lr * (1.0 - self.beta2.powf(t)).sqrt() / (1.0 - self.beta1.powf(t));
let n = params
.len()
.min(grads.len())
.min(self.m.len())
.min(self.v.len());
for i in 0..n {
self.m[i] = self.beta1 * self.m[i] + (1.0 - self.beta1) * grads[i];
self.v[i] = self.beta2 * self.v[i] + (1.0 - self.beta2) * grads[i].powi(2);
params[i] -= lr_t * self.m[i] / (self.v[i].sqrt() + self.eps);
}
}
}
#[derive(Debug)]
pub struct GpuTrainer {
pub net: GpuNeuralNet,
pub backprop: BackpropGpu,
pub learning_rate: f64,
pub optimizer: OptimizerType,
pub adam: Option<AdamOptimizer>,
}
impl GpuTrainer {
pub fn new(net: GpuNeuralNet, learning_rate: f64, optimizer: OptimizerType) -> Self {
let backprop = BackpropGpu::new(&net);
let total_params: usize = net.layers.iter().map(|l| l.weights.len()).sum();
let adam = if optimizer == OptimizerType::Adam {
Some(AdamOptimizer::new(
total_params,
learning_rate,
0.9,
0.999,
1e-8,
))
} else {
None
};
Self {
net,
backprop,
learning_rate,
optimizer,
adam,
}
}
pub fn train_step(&mut self, input: &[f64], target: &[f64]) -> f64 {
let pred = self.net.forward_pass(input);
let loss = mse_loss(&pred, target);
let n = pred.len().min(target.len());
let loss_grad: Vec<f64> = pred[..n]
.iter()
.zip(target[..n].iter())
.map(|(p, t)| 2.0 * (p - t) / n as f64)
.collect();
self.backprop.backward_pass(&loss_grad);
match self.optimizer {
OptimizerType::Sgd => {
for (layer, grads) in self
.net
.layers
.iter_mut()
.zip(self.backprop.gradients.iter())
{
for (w, &g) in layer.weights.iter_mut().zip(grads.iter()) {
*w -= self.learning_rate * g;
}
}
}
OptimizerType::Adam => {
if let Some(adam) = &mut self.adam {
let mut all_weights: Vec<f64> = self
.net
.layers
.iter()
.flat_map(|l| l.weights.iter().copied())
.collect();
let all_grads: Vec<f64> = self
.backprop
.gradients
.iter()
.flat_map(|g| g.iter().copied())
.collect();
adam.update(&mut all_weights, &all_grads);
let mut offset = 0;
for layer in &mut self.net.layers {
let len = layer.weights.len();
layer
.weights
.copy_from_slice(&all_weights[offset..offset + len]);
offset += len;
}
}
}
}
loss
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_relu_positive() {
assert!((relu(3.0) - 3.0).abs() < 1e-12);
}
#[test]
fn test_relu_negative() {
assert!((relu(-5.0)).abs() < 1e-12);
}
#[test]
fn test_relu_zero() {
assert!((relu(0.0)).abs() < 1e-12);
}
#[test]
fn test_sigmoid_zero() {
assert!((sigmoid(0.0) - 0.5).abs() < 1e-12);
}
#[test]
fn test_sigmoid_large_positive() {
assert!((sigmoid(100.0) - 1.0).abs() < 1e-6);
}
#[test]
fn test_sigmoid_large_negative() {
assert!(sigmoid(-100.0) < 1e-6);
}
#[test]
fn test_sigmoid_symmetry() {
let x = 2.5;
assert!((sigmoid(x) + sigmoid(-x) - 1.0).abs() < 1e-12);
}
#[test]
fn test_softmax_sums_to_one() {
let x = vec![1.0, 2.0, 3.0, 4.0];
let s = softmax(&x);
let sum: f64 = s.iter().sum();
assert!((sum - 1.0).abs() < 1e-12);
}
#[test]
fn test_softmax_monotone() {
let x = vec![1.0, 2.0, 3.0];
let s = softmax(&x);
assert!(s[0] < s[1] && s[1] < s[2]);
}
#[test]
fn test_softmax_uniform() {
let x = vec![0.0, 0.0, 0.0];
let s = softmax(&x);
for &v in &s {
assert!((v - 1.0 / 3.0).abs() < 1e-12);
}
}
#[test]
fn test_softmax_empty() {
let s = softmax(&[]);
assert!(s.is_empty());
}
#[test]
fn test_softmax_single() {
let s = softmax(&[42.0]);
assert!((s[0] - 1.0).abs() < 1e-12);
}
#[test]
fn test_softmax_numerical_stability() {
let x = vec![1000.0, 1001.0, 1002.0];
let s = softmax(&x);
let sum: f64 = s.iter().sum();
assert!((sum - 1.0).abs() < 1e-10);
}
#[test]
fn test_mse_loss_perfect() {
let pred = vec![1.0, 2.0, 3.0];
assert!((mse_loss(&pred, &pred)).abs() < 1e-12);
}
#[test]
fn test_mse_loss_known() {
let pred = vec![0.0, 0.0];
let target = vec![1.0, 1.0];
assert!((mse_loss(&pred, &target) - 1.0).abs() < 1e-12);
}
#[test]
fn test_mse_loss_empty() {
assert!((mse_loss(&[], &[])).abs() < 1e-12);
}
#[test]
fn test_mse_loss_positive() {
let pred = vec![1.0, 2.0, 3.0];
let target = vec![0.0, 0.0, 0.0];
assert!(mse_loss(&pred, &target) > 0.0);
}
#[test]
fn test_relu_layer_forward() {
let layer = NeuralLayer::new(3, 3, LayerType::ReLU);
let out = layer.forward(&[-1.0, 0.0, 2.0]);
assert_eq!(out, vec![0.0, 0.0, 2.0]);
}
#[test]
fn test_sigmoid_layer_forward() {
let layer = NeuralLayer::new(1, 1, LayerType::Sigmoid);
let out = layer.forward(&[0.0]);
assert!((out[0] - 0.5).abs() < 1e-12);
}
#[test]
fn test_tanh_layer_forward() {
let layer = NeuralLayer::new(1, 1, LayerType::Tanh);
let out = layer.forward(&[0.0]);
assert!((out[0]).abs() < 1e-12);
}
#[test]
fn test_softmax_layer_forward() {
let layer = NeuralLayer::new(3, 3, LayerType::Softmax);
let out = layer.forward(&[1.0, 2.0, 3.0]);
let sum: f64 = out.iter().sum();
assert!((sum - 1.0).abs() < 1e-12);
}
#[test]
fn test_dropout_layer_passthrough() {
let layer = NeuralLayer::new(4, 4, LayerType::Dropout);
let input = vec![1.0, 2.0, 3.0, 4.0];
let out = layer.forward(&input);
assert_eq!(out, input);
}
#[test]
fn test_dense_layer_identity() {
let mut layer = NeuralLayer::new(1, 1, LayerType::Dense);
layer.weights[0] = 1.0;
let out = layer.forward(&[5.0]);
assert!((out[0] - 5.0).abs() < 1e-12);
}
#[test]
fn test_dense_layer_known_output() {
let mut layer = NeuralLayer::new(2, 1, LayerType::Dense);
layer.weights = vec![1.0, 2.0];
layer.biases = vec![0.5];
let out = layer.forward(&[3.0, 4.0]);
assert!((out[0] - 11.5).abs() < 1e-12);
}
#[test]
fn test_dense_layer_multi_out() {
let mut layer = NeuralLayer::new(2, 2, LayerType::Dense);
layer.weights = vec![1.0, 0.0, 0.0, 1.0];
layer.biases = vec![0.0, 0.0];
let out = layer.forward(&[7.0, 3.0]);
assert!((out[0] - 7.0).abs() < 1e-12);
assert!((out[1] - 3.0).abs() < 1e-12);
}
#[test]
fn test_batchnorm_layer_zero_mean() {
let mut layer = NeuralLayer::new(4, 4, LayerType::BatchNorm);
layer.weights = vec![1.0; 4]; layer.biases = vec![0.0; 4]; let input = vec![1.0, 2.0, 3.0, 4.0];
let out = layer.forward(&input);
let mean_out: f64 = out.iter().sum::<f64>() / out.len() as f64;
assert!(mean_out.abs() < 1e-10);
}
#[test]
fn test_empty_net_passthrough() {
let net = GpuNeuralNet::new();
let input = vec![1.0, 2.0, 3.0];
let out = net.forward_pass(&input);
assert_eq!(out, input);
}
#[test]
fn test_single_relu_net() {
let mut net = GpuNeuralNet::new();
net.add_layer(NeuralLayer::new(3, 3, LayerType::ReLU));
let out = net.forward_pass(&[-1.0, 0.0, 2.0]);
assert_eq!(out, vec![0.0, 0.0, 2.0]);
}
#[test]
fn test_net_dense_then_relu() {
let mut net = GpuNeuralNet::new();
let mut dense = NeuralLayer::new(2, 2, LayerType::Dense);
dense.weights = vec![1.0, 0.0, 0.0, -1.0];
dense.biases = vec![0.0, 0.0];
net.add_layer(dense);
net.add_layer(NeuralLayer::new(2, 2, LayerType::ReLU));
let out = net.forward_pass(&[3.0, 4.0]);
assert!((out[0] - 3.0).abs() < 1e-12);
assert!((out[1]).abs() < 1e-12);
}
#[test]
fn test_batch_forward() {
let mut net = GpuNeuralNet::new();
net.add_layer(NeuralLayer::new(2, 2, LayerType::ReLU));
let inputs = vec![vec![-1.0, 2.0], vec![3.0, -4.0]];
let outs = net.batch_forward(&inputs);
assert_eq!(outs.len(), 2);
assert_eq!(outs[0], vec![0.0, 2.0]);
assert_eq!(outs[1], vec![3.0, 0.0]);
}
#[test]
fn test_net_default() {
let net = GpuNeuralNet::default();
assert!(net.layers.is_empty());
}
#[test]
fn test_backprop_gradient_shape() {
let mut net = GpuNeuralNet::new();
net.add_layer(NeuralLayer::new(3, 2, LayerType::Dense));
let bp = BackpropGpu::new(&net);
assert_eq!(bp.gradients.len(), 1);
assert_eq!(bp.gradients[0].len(), 6); }
#[test]
fn test_backprop_backward_sets_gradients() {
let mut net = GpuNeuralNet::new();
net.add_layer(NeuralLayer::new(2, 2, LayerType::Dense));
let mut bp = BackpropGpu::new(&net);
bp.backward_pass(&[1.0]);
for &g in &bp.gradients[0] {
assert!((g - 1.0).abs() < 1e-12);
}
}
#[test]
fn test_backprop_zero_loss_grad() {
let mut net = GpuNeuralNet::new();
net.add_layer(NeuralLayer::new(2, 2, LayerType::Dense));
let mut bp = BackpropGpu::new(&net);
bp.backward_pass(&[0.0]);
for &g in &bp.gradients[0] {
assert!((g).abs() < 1e-12);
}
}
#[test]
fn test_adam_decreases_loss() {
let mut params = vec![1.0, -1.0, 2.0];
let mut adam = AdamOptimizer::new(3, 0.1, 0.9, 0.999, 1e-8);
for _ in 0..500 {
let grads: Vec<f64> = params.iter().map(|&p| 2.0 * p).collect();
adam.update(&mut params, &grads);
}
for &p in ¶ms {
assert!(p.abs() < 0.1, "param={p}");
}
}
#[test]
fn test_adam_timestep_increments() {
let mut adam = AdamOptimizer::new(2, 0.01, 0.9, 0.999, 1e-8);
let mut params = vec![1.0, 1.0];
let grads = vec![0.1, 0.1];
adam.update(&mut params, &grads);
assert_eq!(adam.t, 1);
adam.update(&mut params, &grads);
assert_eq!(adam.t, 2);
}
#[test]
fn test_adam_moment_buffers_update() {
let mut adam = AdamOptimizer::new(1, 0.01, 0.9, 0.999, 1e-8);
let mut params = vec![1.0];
adam.update(&mut params, &[0.5]);
assert!((adam.m[0] - 0.1 * 0.5).abs() < 1e-12); assert!(adam.v[0] > 0.0);
}
#[test]
fn test_trainer_sgd_reduces_loss() {
let mut net = GpuNeuralNet::new();
let mut layer = NeuralLayer::new(1, 1, LayerType::Dense);
layer.weights = vec![2.0];
layer.biases = vec![0.0];
net.add_layer(layer);
let mut trainer = GpuTrainer::new(net, 0.1, OptimizerType::Sgd);
let loss_before = mse_loss(&trainer.net.forward_pass(&[1.0]), &[1.0]);
let loss_after = trainer.train_step(&[1.0], &[1.0]);
let _ = loss_before;
assert!(loss_after >= 0.0);
}
#[test]
fn test_trainer_adam_train_step() {
let mut net = GpuNeuralNet::new();
let mut layer = NeuralLayer::new(1, 1, LayerType::Dense);
layer.weights = vec![0.0];
layer.biases = vec![0.0];
net.add_layer(layer);
let mut trainer = GpuTrainer::new(net, 0.01, OptimizerType::Adam);
let loss = trainer.train_step(&[1.0], &[1.0]);
assert!(loss >= 0.0);
}
#[test]
fn test_conv1d_layer_forward() {
let mut layer = NeuralLayer::new(3, 1, LayerType::Conv1D);
layer.weights = vec![1.0, 1.0, 1.0];
layer.biases = vec![0.0];
let out = layer.forward(&[1.0, 2.0, 3.0]);
assert!((out[0] - 6.0).abs() < 1e-12);
}
#[test]
fn test_softmax_net_output_probabilities() {
let mut net = GpuNeuralNet::new();
net.add_layer(NeuralLayer::new(3, 3, LayerType::Softmax));
let out = net.forward_pass(&[0.0, 1.0, 2.0]);
let sum: f64 = out.iter().sum();
assert!((sum - 1.0).abs() < 1e-12);
for &p in &out {
assert!((0.0..=1.0).contains(&p));
}
}
#[test]
fn test_mse_symmetric() {
let a = vec![1.0, 2.0];
let b = vec![3.0, 4.0];
assert!((mse_loss(&a, &b) - mse_loss(&b, &a)).abs() < 1e-12);
}
#[test]
fn test_layer_type_debug() {
let lt = LayerType::Dense;
let s = format!("{lt:?}");
assert!(s.contains("Dense"));
}
#[test]
fn test_optimizer_type_eq() {
assert_eq!(OptimizerType::Sgd, OptimizerType::Sgd);
assert_ne!(OptimizerType::Sgd, OptimizerType::Adam);
}
#[test]
fn test_sigmoid_vs_exp() {
let x = 1.0_f64;
assert!((sigmoid(x) - 1.0 / (1.0 + (-x).exp())).abs() < 1e-12);
}
}