1pub fn relu(x: f64) -> f64 {
15 x.max(0.0)
16}
17
18pub fn sigmoid(x: f64) -> f64 {
20 1.0 / (1.0 + (-x).exp())
21}
22
23pub fn softmax(x: &[f64]) -> Vec<f64> {
27 if x.is_empty() {
28 return Vec::new();
29 }
30 let max_val = x.iter().copied().fold(f64::NEG_INFINITY, f64::max);
31 let exps: Vec<f64> = x.iter().map(|&v| (v - max_val).exp()).collect();
32 let sum: f64 = exps.iter().sum();
33 exps.iter().map(|&e| e / sum).collect()
34}
35
36pub fn mse_loss(pred: &[f64], target: &[f64]) -> f64 {
40 if pred.is_empty() {
41 return 0.0;
42 }
43 let n = pred.len().min(target.len());
44 let sum: f64 = pred[..n]
45 .iter()
46 .zip(target[..n].iter())
47 .map(|(p, t)| (p - t).powi(2))
48 .sum();
49 sum / n as f64
50}
51
52#[derive(Debug, Clone, PartialEq)]
58pub enum LayerType {
59 Dense,
61 Conv1D,
63 ReLU,
65 Sigmoid,
67 Tanh,
69 Softmax,
71 BatchNorm,
73 Dropout,
75}
76
77#[derive(Debug, Clone)]
83pub struct NeuralLayer {
84 pub weights: Vec<f64>,
86 pub biases: Vec<f64>,
88 pub layer_type: LayerType,
90 pub input_size: usize,
92 pub output_size: usize,
94}
95
96impl NeuralLayer {
97 pub fn new(input_size: usize, output_size: usize, layer_type: LayerType) -> Self {
102 Self {
103 weights: vec![0.0; input_size * output_size],
104 biases: vec![0.0; output_size],
105 layer_type,
106 input_size,
107 output_size,
108 }
109 }
110
111 pub fn forward(&self, input: &[f64]) -> Vec<f64> {
116 match self.layer_type {
117 LayerType::ReLU => input.iter().map(|&x| relu(x)).collect(),
118 LayerType::Sigmoid => input.iter().map(|&x| sigmoid(x)).collect(),
119 LayerType::Tanh => input.iter().map(|&x| x.tanh()).collect(),
120 LayerType::Softmax => softmax(input),
121 LayerType::BatchNorm => {
122 let n = input.len();
125 if n == 0 {
126 return Vec::new();
127 }
128 let mean = input.iter().sum::<f64>() / n as f64;
129 let var = input.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / n as f64;
130 let std = (var + 1e-5).sqrt();
131 input
132 .iter()
133 .enumerate()
134 .map(|(i, &x)| {
135 let gamma = self.weights.get(i).copied().unwrap_or(1.0);
136 let beta = self.biases.get(i).copied().unwrap_or(0.0);
137 gamma * (x - mean) / std + beta
138 })
139 .collect()
140 }
141 LayerType::Dropout => {
142 input.to_vec()
144 }
145 LayerType::Dense | LayerType::Conv1D => {
146 let in_sz = input.len();
148 let out_sz = self.output_size;
149 let mut out = vec![0.0; out_sz];
150 for (j, out_val) in out.iter_mut().enumerate() {
151 let mut acc = self.biases.get(j).copied().unwrap_or(0.0);
152 for (i, &inp) in input.iter().enumerate() {
153 let w = self.weights.get(j * in_sz + i).copied().unwrap_or(0.0);
154 acc += w * inp;
155 }
156 *out_val = acc;
157 }
158 out
159 }
160 }
161 }
162}
163
164#[derive(Debug, Clone)]
170pub struct GpuNeuralNet {
171 pub layers: Vec<NeuralLayer>,
173}
174
175impl GpuNeuralNet {
176 pub fn new() -> Self {
178 Self { layers: Vec::new() }
179 }
180
181 pub fn add_layer(&mut self, layer: NeuralLayer) {
183 self.layers.push(layer);
184 }
185
186 pub fn forward_pass(&self, input: &[f64]) -> Vec<f64> {
188 let mut current = input.to_vec();
189 for layer in &self.layers {
190 current = layer.forward(¤t);
191 }
192 current
193 }
194
195 pub fn batch_forward(&self, inputs: &[Vec<f64>]) -> Vec<Vec<f64>> {
197 inputs.iter().map(|inp| self.forward_pass(inp)).collect()
198 }
199}
200
201impl Default for GpuNeuralNet {
202 fn default() -> Self {
203 Self::new()
204 }
205}
206
207#[derive(Debug, Clone)]
213pub struct BackpropGpu {
214 pub gradients: Vec<Vec<f64>>,
216}
217
218impl BackpropGpu {
219 pub fn new(net: &GpuNeuralNet) -> Self {
221 let gradients = net
222 .layers
223 .iter()
224 .map(|l| vec![0.0; l.weights.len()])
225 .collect();
226 Self { gradients }
227 }
228
229 pub fn backward_pass(&mut self, loss_grad: &[f64]) {
235 let scale = loss_grad.first().copied().unwrap_or(0.0);
236 for grad_buf in &mut self.gradients {
237 for g in grad_buf.iter_mut() {
238 *g = scale;
239 }
240 }
241 }
242}
243
244#[derive(Debug, Clone, PartialEq)]
250pub enum OptimizerType {
251 Sgd,
253 Adam,
255}
256
257#[derive(Debug, Clone)]
265pub struct AdamOptimizer {
266 pub beta1: f64,
268 pub beta2: f64,
270 pub eps: f64,
272 pub lr: f64,
274 pub m: Vec<f64>,
276 pub v: Vec<f64>,
278 pub t: u64,
280}
281
282impl AdamOptimizer {
283 pub fn new(n: usize, lr: f64, beta1: f64, beta2: f64, eps: f64) -> Self {
285 Self {
286 beta1,
287 beta2,
288 eps,
289 lr,
290 m: vec![0.0; n],
291 v: vec![0.0; n],
292 t: 0,
293 }
294 }
295
296 pub fn update(&mut self, params: &mut [f64], grads: &[f64]) {
301 self.t += 1;
302 let t = self.t as f64;
303 let lr_t = self.lr * (1.0 - self.beta2.powf(t)).sqrt() / (1.0 - self.beta1.powf(t));
304 let n = params
305 .len()
306 .min(grads.len())
307 .min(self.m.len())
308 .min(self.v.len());
309 for i in 0..n {
310 self.m[i] = self.beta1 * self.m[i] + (1.0 - self.beta1) * grads[i];
311 self.v[i] = self.beta2 * self.v[i] + (1.0 - self.beta2) * grads[i].powi(2);
312 params[i] -= lr_t * self.m[i] / (self.v[i].sqrt() + self.eps);
313 }
314 }
315}
316
317#[derive(Debug)]
323pub struct GpuTrainer {
324 pub net: GpuNeuralNet,
326 pub backprop: BackpropGpu,
328 pub learning_rate: f64,
330 pub optimizer: OptimizerType,
332 pub adam: Option<AdamOptimizer>,
334}
335
336impl GpuTrainer {
337 pub fn new(net: GpuNeuralNet, learning_rate: f64, optimizer: OptimizerType) -> Self {
339 let backprop = BackpropGpu::new(&net);
340 let total_params: usize = net.layers.iter().map(|l| l.weights.len()).sum();
341 let adam = if optimizer == OptimizerType::Adam {
342 Some(AdamOptimizer::new(
343 total_params,
344 learning_rate,
345 0.9,
346 0.999,
347 1e-8,
348 ))
349 } else {
350 None
351 };
352 Self {
353 net,
354 backprop,
355 learning_rate,
356 optimizer,
357 adam,
358 }
359 }
360
361 pub fn train_step(&mut self, input: &[f64], target: &[f64]) -> f64 {
368 let pred = self.net.forward_pass(input);
370 let loss = mse_loss(&pred, target);
371
372 let n = pred.len().min(target.len());
374 let loss_grad: Vec<f64> = pred[..n]
375 .iter()
376 .zip(target[..n].iter())
377 .map(|(p, t)| 2.0 * (p - t) / n as f64)
378 .collect();
379
380 self.backprop.backward_pass(&loss_grad);
382
383 match self.optimizer {
385 OptimizerType::Sgd => {
386 for (layer, grads) in self
387 .net
388 .layers
389 .iter_mut()
390 .zip(self.backprop.gradients.iter())
391 {
392 for (w, &g) in layer.weights.iter_mut().zip(grads.iter()) {
393 *w -= self.learning_rate * g;
394 }
395 }
396 }
397 OptimizerType::Adam => {
398 if let Some(adam) = &mut self.adam {
399 let mut all_weights: Vec<f64> = self
401 .net
402 .layers
403 .iter()
404 .flat_map(|l| l.weights.iter().copied())
405 .collect();
406 let all_grads: Vec<f64> = self
407 .backprop
408 .gradients
409 .iter()
410 .flat_map(|g| g.iter().copied())
411 .collect();
412 adam.update(&mut all_weights, &all_grads);
413 let mut offset = 0;
415 for layer in &mut self.net.layers {
416 let len = layer.weights.len();
417 layer
418 .weights
419 .copy_from_slice(&all_weights[offset..offset + len]);
420 offset += len;
421 }
422 }
423 }
424 }
425
426 loss
427 }
428}
429
430#[cfg(test)]
435mod tests {
436 use super::*;
437
438 #[test]
441 fn test_relu_positive() {
442 assert!((relu(3.0) - 3.0).abs() < 1e-12);
443 }
444
445 #[test]
446 fn test_relu_negative() {
447 assert!((relu(-5.0)).abs() < 1e-12);
448 }
449
450 #[test]
451 fn test_relu_zero() {
452 assert!((relu(0.0)).abs() < 1e-12);
453 }
454
455 #[test]
456 fn test_sigmoid_zero() {
457 assert!((sigmoid(0.0) - 0.5).abs() < 1e-12);
458 }
459
460 #[test]
461 fn test_sigmoid_large_positive() {
462 assert!((sigmoid(100.0) - 1.0).abs() < 1e-6);
463 }
464
465 #[test]
466 fn test_sigmoid_large_negative() {
467 assert!(sigmoid(-100.0) < 1e-6);
468 }
469
470 #[test]
471 fn test_sigmoid_symmetry() {
472 let x = 2.5;
473 assert!((sigmoid(x) + sigmoid(-x) - 1.0).abs() < 1e-12);
474 }
475
476 #[test]
477 fn test_softmax_sums_to_one() {
478 let x = vec![1.0, 2.0, 3.0, 4.0];
479 let s = softmax(&x);
480 let sum: f64 = s.iter().sum();
481 assert!((sum - 1.0).abs() < 1e-12);
482 }
483
484 #[test]
485 fn test_softmax_monotone() {
486 let x = vec![1.0, 2.0, 3.0];
487 let s = softmax(&x);
488 assert!(s[0] < s[1] && s[1] < s[2]);
489 }
490
491 #[test]
492 fn test_softmax_uniform() {
493 let x = vec![0.0, 0.0, 0.0];
494 let s = softmax(&x);
495 for &v in &s {
496 assert!((v - 1.0 / 3.0).abs() < 1e-12);
497 }
498 }
499
500 #[test]
501 fn test_softmax_empty() {
502 let s = softmax(&[]);
503 assert!(s.is_empty());
504 }
505
506 #[test]
507 fn test_softmax_single() {
508 let s = softmax(&[42.0]);
509 assert!((s[0] - 1.0).abs() < 1e-12);
510 }
511
512 #[test]
513 fn test_softmax_numerical_stability() {
514 let x = vec![1000.0, 1001.0, 1002.0];
516 let s = softmax(&x);
517 let sum: f64 = s.iter().sum();
518 assert!((sum - 1.0).abs() < 1e-10);
519 }
520
521 #[test]
524 fn test_mse_loss_perfect() {
525 let pred = vec![1.0, 2.0, 3.0];
526 assert!((mse_loss(&pred, &pred)).abs() < 1e-12);
527 }
528
529 #[test]
530 fn test_mse_loss_known() {
531 let pred = vec![0.0, 0.0];
533 let target = vec![1.0, 1.0];
534 assert!((mse_loss(&pred, &target) - 1.0).abs() < 1e-12);
535 }
536
537 #[test]
538 fn test_mse_loss_empty() {
539 assert!((mse_loss(&[], &[])).abs() < 1e-12);
540 }
541
542 #[test]
543 fn test_mse_loss_positive() {
544 let pred = vec![1.0, 2.0, 3.0];
545 let target = vec![0.0, 0.0, 0.0];
546 assert!(mse_loss(&pred, &target) > 0.0);
547 }
548
549 #[test]
552 fn test_relu_layer_forward() {
553 let layer = NeuralLayer::new(3, 3, LayerType::ReLU);
554 let out = layer.forward(&[-1.0, 0.0, 2.0]);
555 assert_eq!(out, vec![0.0, 0.0, 2.0]);
556 }
557
558 #[test]
559 fn test_sigmoid_layer_forward() {
560 let layer = NeuralLayer::new(1, 1, LayerType::Sigmoid);
561 let out = layer.forward(&[0.0]);
562 assert!((out[0] - 0.5).abs() < 1e-12);
563 }
564
565 #[test]
566 fn test_tanh_layer_forward() {
567 let layer = NeuralLayer::new(1, 1, LayerType::Tanh);
568 let out = layer.forward(&[0.0]);
569 assert!((out[0]).abs() < 1e-12);
570 }
571
572 #[test]
573 fn test_softmax_layer_forward() {
574 let layer = NeuralLayer::new(3, 3, LayerType::Softmax);
575 let out = layer.forward(&[1.0, 2.0, 3.0]);
576 let sum: f64 = out.iter().sum();
577 assert!((sum - 1.0).abs() < 1e-12);
578 }
579
580 #[test]
581 fn test_dropout_layer_passthrough() {
582 let layer = NeuralLayer::new(4, 4, LayerType::Dropout);
583 let input = vec![1.0, 2.0, 3.0, 4.0];
584 let out = layer.forward(&input);
585 assert_eq!(out, input);
586 }
587
588 #[test]
589 fn test_dense_layer_identity() {
590 let mut layer = NeuralLayer::new(1, 1, LayerType::Dense);
592 layer.weights[0] = 1.0;
593 let out = layer.forward(&[5.0]);
594 assert!((out[0] - 5.0).abs() < 1e-12);
595 }
596
597 #[test]
598 fn test_dense_layer_known_output() {
599 let mut layer = NeuralLayer::new(2, 1, LayerType::Dense);
601 layer.weights = vec![1.0, 2.0];
602 layer.biases = vec![0.5];
603 let out = layer.forward(&[3.0, 4.0]);
605 assert!((out[0] - 11.5).abs() < 1e-12);
606 }
607
608 #[test]
609 fn test_dense_layer_multi_out() {
610 let mut layer = NeuralLayer::new(2, 2, LayerType::Dense);
611 layer.weights = vec![1.0, 0.0, 0.0, 1.0];
614 layer.biases = vec![0.0, 0.0];
615 let out = layer.forward(&[7.0, 3.0]);
616 assert!((out[0] - 7.0).abs() < 1e-12);
617 assert!((out[1] - 3.0).abs() < 1e-12);
618 }
619
620 #[test]
621 fn test_batchnorm_layer_zero_mean() {
622 let mut layer = NeuralLayer::new(4, 4, LayerType::BatchNorm);
623 layer.weights = vec![1.0; 4]; layer.biases = vec![0.0; 4]; let input = vec![1.0, 2.0, 3.0, 4.0];
626 let out = layer.forward(&input);
627 let mean_out: f64 = out.iter().sum::<f64>() / out.len() as f64;
628 assert!(mean_out.abs() < 1e-10);
629 }
630
631 #[test]
634 fn test_empty_net_passthrough() {
635 let net = GpuNeuralNet::new();
636 let input = vec![1.0, 2.0, 3.0];
637 let out = net.forward_pass(&input);
638 assert_eq!(out, input);
639 }
640
641 #[test]
642 fn test_single_relu_net() {
643 let mut net = GpuNeuralNet::new();
644 net.add_layer(NeuralLayer::new(3, 3, LayerType::ReLU));
645 let out = net.forward_pass(&[-1.0, 0.0, 2.0]);
646 assert_eq!(out, vec![0.0, 0.0, 2.0]);
647 }
648
649 #[test]
650 fn test_net_dense_then_relu() {
651 let mut net = GpuNeuralNet::new();
652 let mut dense = NeuralLayer::new(2, 2, LayerType::Dense);
653 dense.weights = vec![1.0, 0.0, 0.0, -1.0];
654 dense.biases = vec![0.0, 0.0];
655 net.add_layer(dense);
656 net.add_layer(NeuralLayer::new(2, 2, LayerType::ReLU));
657 let out = net.forward_pass(&[3.0, 4.0]);
658 assert!((out[0] - 3.0).abs() < 1e-12);
660 assert!((out[1]).abs() < 1e-12);
661 }
662
663 #[test]
664 fn test_batch_forward() {
665 let mut net = GpuNeuralNet::new();
666 net.add_layer(NeuralLayer::new(2, 2, LayerType::ReLU));
667 let inputs = vec![vec![-1.0, 2.0], vec![3.0, -4.0]];
668 let outs = net.batch_forward(&inputs);
669 assert_eq!(outs.len(), 2);
670 assert_eq!(outs[0], vec![0.0, 2.0]);
671 assert_eq!(outs[1], vec![3.0, 0.0]);
672 }
673
674 #[test]
675 fn test_net_default() {
676 let net = GpuNeuralNet::default();
677 assert!(net.layers.is_empty());
678 }
679
680 #[test]
683 fn test_backprop_gradient_shape() {
684 let mut net = GpuNeuralNet::new();
685 net.add_layer(NeuralLayer::new(3, 2, LayerType::Dense));
686 let bp = BackpropGpu::new(&net);
687 assert_eq!(bp.gradients.len(), 1);
688 assert_eq!(bp.gradients[0].len(), 6); }
690
691 #[test]
692 fn test_backprop_backward_sets_gradients() {
693 let mut net = GpuNeuralNet::new();
694 net.add_layer(NeuralLayer::new(2, 2, LayerType::Dense));
695 let mut bp = BackpropGpu::new(&net);
696 bp.backward_pass(&[1.0]);
697 for &g in &bp.gradients[0] {
698 assert!((g - 1.0).abs() < 1e-12);
699 }
700 }
701
702 #[test]
703 fn test_backprop_zero_loss_grad() {
704 let mut net = GpuNeuralNet::new();
705 net.add_layer(NeuralLayer::new(2, 2, LayerType::Dense));
706 let mut bp = BackpropGpu::new(&net);
707 bp.backward_pass(&[0.0]);
708 for &g in &bp.gradients[0] {
709 assert!((g).abs() < 1e-12);
710 }
711 }
712
713 #[test]
716 fn test_adam_decreases_loss() {
717 let mut params = vec![1.0, -1.0, 2.0];
718 let mut adam = AdamOptimizer::new(3, 0.1, 0.9, 0.999, 1e-8);
719 for _ in 0..500 {
721 let grads: Vec<f64> = params.iter().map(|&p| 2.0 * p).collect();
722 adam.update(&mut params, &grads);
723 }
724 for &p in ¶ms {
725 assert!(p.abs() < 0.1, "param={p}");
726 }
727 }
728
729 #[test]
730 fn test_adam_timestep_increments() {
731 let mut adam = AdamOptimizer::new(2, 0.01, 0.9, 0.999, 1e-8);
732 let mut params = vec![1.0, 1.0];
733 let grads = vec![0.1, 0.1];
734 adam.update(&mut params, &grads);
735 assert_eq!(adam.t, 1);
736 adam.update(&mut params, &grads);
737 assert_eq!(adam.t, 2);
738 }
739
740 #[test]
741 fn test_adam_moment_buffers_update() {
742 let mut adam = AdamOptimizer::new(1, 0.01, 0.9, 0.999, 1e-8);
743 let mut params = vec![1.0];
744 adam.update(&mut params, &[0.5]);
745 assert!((adam.m[0] - 0.1 * 0.5).abs() < 1e-12); assert!(adam.v[0] > 0.0);
747 }
748
749 #[test]
752 fn test_trainer_sgd_reduces_loss() {
753 let mut net = GpuNeuralNet::new();
754 let mut layer = NeuralLayer::new(1, 1, LayerType::Dense);
755 layer.weights = vec![2.0];
756 layer.biases = vec![0.0];
757 net.add_layer(layer);
758 let mut trainer = GpuTrainer::new(net, 0.1, OptimizerType::Sgd);
759 let loss_before = mse_loss(&trainer.net.forward_pass(&[1.0]), &[1.0]);
760 let loss_after = trainer.train_step(&[1.0], &[1.0]);
761 let _ = loss_before;
764 assert!(loss_after >= 0.0);
765 }
766
767 #[test]
768 fn test_trainer_adam_train_step() {
769 let mut net = GpuNeuralNet::new();
770 let mut layer = NeuralLayer::new(1, 1, LayerType::Dense);
771 layer.weights = vec![0.0];
772 layer.biases = vec![0.0];
773 net.add_layer(layer);
774 let mut trainer = GpuTrainer::new(net, 0.01, OptimizerType::Adam);
775 let loss = trainer.train_step(&[1.0], &[1.0]);
776 assert!(loss >= 0.0);
777 }
778
779 #[test]
780 fn test_conv1d_layer_forward() {
781 let mut layer = NeuralLayer::new(3, 1, LayerType::Conv1D);
782 layer.weights = vec![1.0, 1.0, 1.0];
783 layer.biases = vec![0.0];
784 let out = layer.forward(&[1.0, 2.0, 3.0]);
785 assert!((out[0] - 6.0).abs() < 1e-12);
786 }
787
788 #[test]
789 fn test_softmax_net_output_probabilities() {
790 let mut net = GpuNeuralNet::new();
791 net.add_layer(NeuralLayer::new(3, 3, LayerType::Softmax));
792 let out = net.forward_pass(&[0.0, 1.0, 2.0]);
793 let sum: f64 = out.iter().sum();
794 assert!((sum - 1.0).abs() < 1e-12);
795 for &p in &out {
796 assert!((0.0..=1.0).contains(&p));
797 }
798 }
799
800 #[test]
801 fn test_mse_symmetric() {
802 let a = vec![1.0, 2.0];
803 let b = vec![3.0, 4.0];
804 assert!((mse_loss(&a, &b) - mse_loss(&b, &a)).abs() < 1e-12);
805 }
806
807 #[test]
808 fn test_layer_type_debug() {
809 let lt = LayerType::Dense;
810 let s = format!("{lt:?}");
811 assert!(s.contains("Dense"));
812 }
813
814 #[test]
815 fn test_optimizer_type_eq() {
816 assert_eq!(OptimizerType::Sgd, OptimizerType::Sgd);
817 assert_ne!(OptimizerType::Sgd, OptimizerType::Adam);
818 }
819
820 #[test]
821 fn test_sigmoid_vs_exp() {
822 let x = 1.0_f64;
824 assert!((sigmoid(x) - 1.0 / (1.0 + (-x).exp())).abs() < 1e-12);
825 }
826}